@bongos/core 1.24.25 → 1.24.26
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 +81 -66
- package/docs/copy-inventory.md +217 -212
- package/docs/copy-registry.json +298 -233
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +855 -852
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/blockers.js +3 -1
- package/modules/hall-ui/public/board-room.html +2 -0
- package/modules/hall-ui/public/collab.html +3 -3
- package/modules/hall-ui/public/collab.js +4 -0
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/criterion-uat-ui.js +1 -1
- package/modules/hall-ui/public/deploy.html +2 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +2 -0
- package/modules/hall-ui/public/gate.html +2 -0
- package/modules/hall-ui/public/goals-page.js +1 -1
- package/modules/hall-ui/public/goals.css +3 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/goals.js +1 -1
- package/modules/hall-ui/public/government.html +2 -0
- package/modules/hall-ui/public/hall-widgets.js +9 -9
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +2 -0
- package/modules/hall-ui/public/lightbulb.js +1 -1
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch-settings.html +2 -0
- package/modules/hall-ui/public/market-watch.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +5 -5
- package/modules/hall-ui/public/project-settings.html +2 -0
- package/modules/hall-ui/public/ranks.html +3 -3
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +2 -0
- package/modules/hall-ui/public/settings-software-update.js +1 -1
- package/modules/hall-ui/public/settings-specialities.js +1 -1
- package/modules/hall-ui/public/settings.html +3 -1
- package/modules/hall-ui/public/settings.js +3 -0
- package/modules/hall-ui/public/studio.js +3 -3
- package/modules/hall-ui/public/style.css +90 -1
- package/modules/hall-ui/public/task-detail.js +2 -2
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +2 -0
- package/modules/hall-ui/public/watch.js +1 -1
- package/modules/hall-ui/public/work.html +3 -3
- package/modules/hall-ui/records/loader.md +35 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/hall-preview/README.md +1 -0
- package/scripts/hall-preview/server.js +9 -0
- package/src/bongos/apex-hall-redirects.js +53 -0
- package/src/bongos/serve-internal.js +3 -18
- package/src/module-api.js +1 -1
- package/tests/hall_loading.mjs +153 -0
- package/tests/software_update.mjs +33 -0
|
@@ -1186,6 +1186,9 @@
|
|
|
1186
1186
|
function hideLayout(hide) {
|
|
1187
1187
|
const layout = document.getElementById('settings-layout');
|
|
1188
1188
|
if (layout) layout.hidden = !!hide;
|
|
1189
|
+
// Every path past /me decides the layout here, so the page loader stops here too (task 1005043).
|
|
1190
|
+
const loading = document.getElementById('settings-loading');
|
|
1191
|
+
if (loading) loading.hidden = true;
|
|
1189
1192
|
}
|
|
1190
1193
|
|
|
1191
1194
|
// ---- bootstrap -----------------------------------------------------------
|
|
@@ -755,7 +755,7 @@
|
|
|
755
755
|
|
|
756
756
|
function statusHtml() {
|
|
757
757
|
if (!home.pages) {
|
|
758
|
-
return
|
|
758
|
+
return home.pagesFailed ? '<p class="st-panel__line">The pages could not be read just now. Try again in a moment.</p>' : '<p class="hall-loading" role="status">Reading the pages…</p>';
|
|
759
759
|
}
|
|
760
760
|
const rows = lib.surfaceRows(home.pages, lib.landingHost(byId('hall-main') && byId('hall-main').getAttribute('data-apex'), window.location && window.location.host)).map((s) => `
|
|
761
761
|
<li class="st-surface">
|
|
@@ -804,7 +804,7 @@
|
|
|
804
804
|
|
|
805
805
|
function siteHtml() {
|
|
806
806
|
if (!home.site) {
|
|
807
|
-
return `<p class="st-panel__line">${
|
|
807
|
+
return home.siteFailed ? `<p class="st-panel__line">${SITE_FAILED}</p>` : '<p class="hall-loading" role="status">Reading the reviews…</p>';
|
|
808
808
|
}
|
|
809
809
|
if (!home.site.length) return `<p class="st-panel__line">${SITE_NONE}</p>`;
|
|
810
810
|
const rows = home.site.map((r) => `
|
|
@@ -1178,7 +1178,7 @@
|
|
|
1178
1178
|
}
|
|
1179
1179
|
|
|
1180
1180
|
function siteListPage() {
|
|
1181
|
-
if (!home.site) return `<p class="st-panel__line">${
|
|
1181
|
+
if (!home.site) return home.siteFailed ? `<p class="st-panel__line">${SITE_FAILED}</p>` : '<p class="hall-loading" role="status">Reading the reviews…</p>';
|
|
1182
1182
|
if (!home.site.length) return `<p class="st-panel__line">${SITE_NONE}</p>`;
|
|
1183
1183
|
return `<ul class="st-doc-list">${home.site.map((r) => `<li><a class="st-doc-list__row" href="${escapeHtml(siteHref(r.link))}">
|
|
1184
1184
|
<span class="st-doc-list__page">${escapeHtml(r.page_title || r.page_path || r.page_id)}</span>
|
|
@@ -4317,7 +4317,8 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
4317
4317
|
.empty-state__action { display: inline-block; margin-top: 6px; font-size: 13px; }
|
|
4318
4318
|
|
|
4319
4319
|
/* skeleton shimmer — standard-height placeholder rows while data loads, so
|
|
4320
|
-
cards don't jump; derives from the ink/bg tokens like everything else
|
|
4320
|
+
cards don't jump; derives from the ink/bg tokens like everything else. It
|
|
4321
|
+
breathes in the loader's beat (task 1005043; the keyframes sit with it). */
|
|
4321
4322
|
.kit-skel {
|
|
4322
4323
|
display: inline-block;
|
|
4323
4324
|
width: 60%;
|
|
@@ -4325,9 +4326,97 @@ main [id] { scroll-margin-top: calc(var(--topbar-h) + 16px); }
|
|
|
4325
4326
|
height: 12px;
|
|
4326
4327
|
border-radius: 2px;
|
|
4327
4328
|
background: var(--skeleton);
|
|
4329
|
+
animation: hall-loading-breathe .8s ease-in-out infinite alternate-reverse;
|
|
4328
4330
|
}
|
|
4329
4331
|
.kit-skel-row td { border-bottom: 1px solid var(--rule-soft); }
|
|
4330
4332
|
|
|
4333
|
+
/* ---- the loader (task 1005043) ----------------------------------------------
|
|
4334
|
+
One loader for everything a hall page waits on, drawn from the Bongos mark
|
|
4335
|
+
(shell.js's brand-mark SVG: the ring, the core and the accent dot). A page
|
|
4336
|
+
opts in by giving its existing placeholder the class; its own words stay as
|
|
4337
|
+
the caption, so no page gains markup. The ring and the dot turn together round
|
|
4338
|
+
the still core and the ink breathes, after the Vibe skeleton the owner pointed
|
|
4339
|
+
at (vibe.monday.com: 0.4 to 1 and back, 0.8s). Three sizes: the default for a
|
|
4340
|
+
panel, --page for a page's main area, --inline for a line in a list or card.
|
|
4341
|
+
|
|
4342
|
+
DESIGN.md holds it: CSS only, transform and filter only (the breath and the
|
|
4343
|
+
arrival are filter: opacity(), not opacity), every colour a var() or a mix of
|
|
4344
|
+
one; the black in the two masks is alpha, not paint. The 20s floor is for the
|
|
4345
|
+
ambient plates: a loader is the live thing on the page, so it keeps a loader's
|
|
4346
|
+
tempo and the dot spends the accent. Each turn eases out of and back into the
|
|
4347
|
+
logo's own pose, so the mark lands on the logo once per beat. It arrives after
|
|
4348
|
+
240ms, so a read that answers faster never shows it. Under the Kill Switch
|
|
4349
|
+
nothing moves and the rest frame is the logo itself. ---- */
|
|
4350
|
+
.hall-loading {
|
|
4351
|
+
--hl-size: 40px;
|
|
4352
|
+
--hl-pad: 28px;
|
|
4353
|
+
--hl-beat: 1.6s;
|
|
4354
|
+
position: relative;
|
|
4355
|
+
display: flex;
|
|
4356
|
+
flex-direction: column;
|
|
4357
|
+
align-items: center;
|
|
4358
|
+
gap: 12px;
|
|
4359
|
+
margin: 0;
|
|
4360
|
+
padding: var(--hl-pad) 0;
|
|
4361
|
+
font-size: 13.5px;
|
|
4362
|
+
color: var(--ink-faint);
|
|
4363
|
+
text-align: center;
|
|
4364
|
+
animation: hall-loading-arrive 500ms cubic-bezier(.16, 1, .3, 1) 240ms both;
|
|
4365
|
+
}
|
|
4366
|
+
.hall-loading::before,
|
|
4367
|
+
.hall-loading::after {
|
|
4368
|
+
content: "";
|
|
4369
|
+
display: block;
|
|
4370
|
+
flex: none;
|
|
4371
|
+
width: var(--hl-size);
|
|
4372
|
+
height: var(--hl-size);
|
|
4373
|
+
-webkit-mask: var(--hl-shape) center / contain no-repeat;
|
|
4374
|
+
mask: var(--hl-shape) center / contain no-repeat;
|
|
4375
|
+
animation: hall-loading-turn var(--hl-beat) cubic-bezier(.45, .05, .55, .95) infinite;
|
|
4376
|
+
}
|
|
4377
|
+
/* the ring and the core: one ink, breathing */
|
|
4378
|
+
.hall-loading::before {
|
|
4379
|
+
--hl-shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Cpath d='M137 31 A78 78 0 1 0 170 66' fill='none' stroke='black' stroke-width='15' stroke-linecap='round'/%3E%3Ccircle cx='100' cy='100' r='55'/%3E%3C/svg%3E");
|
|
4380
|
+
background: color-mix(in srgb, var(--ink) 30%, var(--bg-card));
|
|
4381
|
+
animation:
|
|
4382
|
+
hall-loading-turn var(--hl-beat) cubic-bezier(.45, .05, .55, .95) infinite,
|
|
4383
|
+
hall-loading-breathe calc(var(--hl-beat) / 2) ease-in-out infinite alternate-reverse;
|
|
4384
|
+
}
|
|
4385
|
+
/* the dot: the accent, laid exactly over the mark and turning with it */
|
|
4386
|
+
.hall-loading::after {
|
|
4387
|
+
--hl-shape: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200'%3E%3Ccircle cx='156' cy='46' r='12'/%3E%3C/svg%3E");
|
|
4388
|
+
position: absolute;
|
|
4389
|
+
top: var(--hl-pad);
|
|
4390
|
+
left: calc(50% - var(--hl-size) / 2);
|
|
4391
|
+
background: var(--accent);
|
|
4392
|
+
}
|
|
4393
|
+
.hall-loading--page { --hl-size: 64px; --hl-pad: 72px; gap: 16px; }
|
|
4394
|
+
.hall-loading--inline {
|
|
4395
|
+
--hl-size: 16px;
|
|
4396
|
+
--hl-pad: 0px;
|
|
4397
|
+
display: inline-flex;
|
|
4398
|
+
flex-direction: row;
|
|
4399
|
+
gap: 8px;
|
|
4400
|
+
text-align: left;
|
|
4401
|
+
}
|
|
4402
|
+
.hall-loading--inline::after { top: calc(50% - var(--hl-size) / 2); left: 0; }
|
|
4403
|
+
/* --wait: a page whose every section stays [hidden] until it knows who is
|
|
4404
|
+
reading (the sealed/body pairs) puts one loader FIRST in its <main>. It shows
|
|
4405
|
+
only while nothing after it is showing, and steps aside the moment the page
|
|
4406
|
+
reveals anything (the body, the sealed notice, an error), so no page script
|
|
4407
|
+
has to remember to hide it. Without :has() it never shows: today's page. */
|
|
4408
|
+
/* --zones: the same, for a view made of always-present, empty zones that cards
|
|
4409
|
+
mount into (Home, Watch's panes): it shows while no showing zone after it
|
|
4410
|
+
holds a showing child. */
|
|
4411
|
+
.hall-loading--wait, .hall-loading--zones { display: none; }
|
|
4412
|
+
@supports selector(:has(a)) {
|
|
4413
|
+
.hall-loading--wait:not(:has(~ :not([hidden], script, template, style))) { display: flex; }
|
|
4414
|
+
.hall-loading--zones:not(:has(~ :not([hidden]) > :not([hidden], script, template, style))) { display: flex; }
|
|
4415
|
+
}
|
|
4416
|
+
@keyframes hall-loading-arrive { from { filter: opacity(0); transform: translateY(6px); } }
|
|
4417
|
+
@keyframes hall-loading-turn { to { transform: rotate(1turn); } }
|
|
4418
|
+
@keyframes hall-loading-breathe { from { filter: opacity(.45); } }
|
|
4419
|
+
|
|
4331
4420
|
/* collapsible card body — the Work-backlog <details> idiom, generalized */
|
|
4332
4421
|
.kit-collapse__summary {
|
|
4333
4422
|
cursor: pointer;
|
|
@@ -413,7 +413,7 @@
|
|
|
413
413
|
}
|
|
414
414
|
|
|
415
415
|
async function renderPromote() {
|
|
416
|
-
box.innerHTML = '<p class="
|
|
416
|
+
box.innerHTML = '<p class="hall-loading hall-loading--inline" role="status">Loading versions…</p>';
|
|
417
417
|
// Both dead ends offer the way back through the same delegated [data-go]
|
|
418
418
|
// handler the forms use — a builder must never be stranded mid-panel.
|
|
419
419
|
function bail(message) {
|
|
@@ -658,7 +658,7 @@
|
|
|
658
658
|
function bodyFor(state) {
|
|
659
659
|
var nodes = [];
|
|
660
660
|
var label = '#' + state.id;
|
|
661
|
-
if (state.loading) { nodes.push(h('p', '
|
|
661
|
+
if (state.loading) { nodes.push(h('p', 'hall-loading', 'Loading ' + label + '…')); return nodes; }
|
|
662
662
|
if (state.needsAuth) {
|
|
663
663
|
nodes.push(h('p', 'deeplink-card__title', label));
|
|
664
664
|
nodes.push(h('p', 'deeplink-card__note', 'Sign in to the Builders’ Hall to view this record.'));
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
<p class="tw-hint" id="tw-hint" hidden></p>
|
|
75
75
|
<div class="tw-notice" id="tw-notice" role="status" hidden></div>
|
|
76
76
|
<div id="tw-doc">
|
|
77
|
-
<p class="
|
|
77
|
+
<p class="hall-loading hall-loading--page" role="status">Reading the page…</p>
|
|
78
78
|
</div>
|
|
79
79
|
</article>
|
|
80
80
|
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
<main id="hall-main">
|
|
37
|
+
<!-- task 1005043: the page loader, shown until the sealed notice or a pane has something showing. -->
|
|
38
|
+
<p class="hall-loading hall-loading--page hall-loading--zones" role="status">Loading…</p>
|
|
37
39
|
<!-- Restricted notice — shown by watch.js when the viewer is below the
|
|
38
40
|
floor, with the floor written in from the pack's rank label. -->
|
|
39
41
|
<section class="scroll" id="sealed-scroll" aria-labelledby="sealed-h" hidden>
|
|
@@ -754,7 +754,7 @@
|
|
|
754
754
|
if (!btn || !target.contains(btn)) return;
|
|
755
755
|
const slug = btn.getAttribute('data-doc-open');
|
|
756
756
|
const view = $('#engagement-doc-view');
|
|
757
|
-
if (view) view.innerHTML =
|
|
757
|
+
if (view) view.innerHTML = '<p class="hall-loading" role="status">Reading…</p>';
|
|
758
758
|
try {
|
|
759
759
|
const res = await fetchJson(`/security/docs/${encodeURIComponent(slug)}`);
|
|
760
760
|
const doc = res.doc || {};
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
<p class="claim-count-line" id="claim-count" aria-live="polite"></p>
|
|
106
106
|
|
|
107
107
|
<div class="task-list" id="task-list">
|
|
108
|
-
<div class="
|
|
108
|
+
<div class="hall-loading" role="status">Loading…</div>
|
|
109
109
|
</div>
|
|
110
110
|
<div id="ready-pager-mount"></div>
|
|
111
111
|
</section>
|
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
|
|
118
118
|
<table class="ledger" id="wip-table">
|
|
119
119
|
<thead><tr><th></th><th>#</th><th>Task</th><th>Builder</th><th>For</th><th></th></tr></thead>
|
|
120
|
-
<tbody><tr><td colspan="6" class="ledger__empty"
|
|
120
|
+
<tbody><tr><td colspan="6" class="ledger__empty"><span class="hall-loading" role="status">Loading…</span></td></tr></tbody>
|
|
121
121
|
</table>
|
|
122
122
|
<div id="wip-pager-mount"></div>
|
|
123
123
|
</section>
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
<div class="stat-row" id="works-summary" style="margin-bottom: 14px" hidden></div>
|
|
140
140
|
<p class="scroll__lede">The most recently shipped tasks.</p>
|
|
141
141
|
<div class="history" id="history-list">
|
|
142
|
-
<div class="
|
|
142
|
+
<div class="hall-loading" role="status">Loading…</div>
|
|
143
143
|
</div>
|
|
144
144
|
<div id="done-pager-mount"></div>
|
|
145
145
|
</section>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# The loader — the Bongos mark wherever a hall page waits for its data (task 1005043)
|
|
2
|
+
|
|
3
|
+
Goal 1000095. Owner ask, 2026-11-03: a smooth load screen with the Bongos logo whenever the hall
|
|
4
|
+
waits for data, in the spirit of monday.com's Vibe data loaders (vibe.monday.com, Skeleton: grey
|
|
5
|
+
shapes breathing 0.4 to 1 and back over 0.8s, in the place the content will land).
|
|
6
|
+
|
|
7
|
+
- **One component, in `style.css`: `.hall-loading`.** CSS only. `::before` is the ring and the core
|
|
8
|
+
(a mask of shell.js's brand-mark geometry, painted `--ink` at 30%), `::after` is the dot (the same
|
|
9
|
+
box, painted `--accent`). Both turn together round the still core, easing out of and back into the
|
|
10
|
+
logo's own pose every 1.6s; the ink breathes on half that beat. The element's own text is the
|
|
11
|
+
caption. It arrives 240ms late (filter: opacity), so a read that answers fast never shows it.
|
|
12
|
+
Sizes: the default (40px, a panel), `--page` (64px, a page's main area), `--inline` (16px, a line).
|
|
13
|
+
- **A page opts in by putting the class on the placeholder it already had**, and gives it
|
|
14
|
+
`role="status"`. The words did not change; a bare `…` became `Loading…`. A placeholder a script
|
|
15
|
+
rewrites IN PLACE (atlas's caption, sessions' status line, the goal dependency ledes, the profile's
|
|
16
|
+
activity placeholder) keeps its plain words, because the class would stay on the finished sentence.
|
|
17
|
+
- **Pages that are blank while they wait get a page loader that puts itself away.** The sealed/body
|
|
18
|
+
pages (board-room, deploy, fleet, gate, government, market-watch-settings, project-settings,
|
|
19
|
+
sessions) open their `<main>` on `.hall-loading--wait`, which shows only while every section after
|
|
20
|
+
it is `[hidden]`. Home and Watch, whose zones and panes are always present, use
|
|
21
|
+
`.hall-loading--zones`: it shows while no showing zone has a showing child. Both are `:has()`
|
|
22
|
+
rules inside `@supports`, so a browser without it simply never shows them. No page script hides
|
|
23
|
+
them. Settings' layout is never `[hidden]`, so `hideLayout()` puts its loader away.
|
|
24
|
+
- **Lists that paint later carry a loader inside them**: Blockers' list (its first `renderList`
|
|
25
|
+
replaces it), Collab's three ask lists (its failure path now stops them and says why, where the page
|
|
26
|
+
used to stay blank), the table placeholder rows (the loader sits inside the `ledger__empty` cell).
|
|
27
|
+
- **The skeletons breathe too.** `.kit-skel` and the goal list's `.gr-skel__ring` take the loader's
|
|
28
|
+
breath, so the Goals list's row skeleton is the Vibe pattern exactly.
|
|
29
|
+
- **DESIGN.md holds.** Transform and filter only; every colour a token (the black in the masks is
|
|
30
|
+
alpha); the Kill Switch stops it all and leaves the logo. The 20s ambient floor is for the plates:
|
|
31
|
+
a loader is the live thing on the page, which is also why its dot may spend the accent.
|
|
32
|
+
- **Not covered:** the apex pages (account, projects, community) and the status page run their own
|
|
33
|
+
sheets; the city, street, goal map and thinking sky draw their own worlds and kept their own lines.
|
|
34
|
+
- **Look at it:** `node scripts/hall-preview/server.js --port 4630 --fixture-me --latency 5000` holds
|
|
35
|
+
every read 5s (task 1005043 added `--latency`). Tests: `tests/hall_loading.mjs`.
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.26",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.26",
|
|
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.24.
|
|
3
|
+
"version": "1.24.26",
|
|
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",
|
package/release-notes.json
CHANGED
|
@@ -11071,5 +11071,15 @@
|
|
|
11071
11071
|
"id": "1004593",
|
|
11072
11072
|
"text": "A hosted project's Settings page no longer offers an unreleased version; it now offers exactly what the Upgrade page offers."
|
|
11073
11073
|
}
|
|
11074
|
+
],
|
|
11075
|
+
"1.24.26": [
|
|
11076
|
+
{
|
|
11077
|
+
"id": "1005043",
|
|
11078
|
+
"text": "While a page in the builders' hall is waiting for its data, the Bongos logo now turns gently where the content will appear, instead of a blank space or a line of grey text."
|
|
11079
|
+
},
|
|
11080
|
+
{
|
|
11081
|
+
"id": "1004736",
|
|
11082
|
+
"text": "A hosted project's Update button no longer leads to a missing page; it opens the Upgrade page for that project."
|
|
11083
|
+
}
|
|
11074
11084
|
]
|
|
11075
11085
|
}
|
|
@@ -22,6 +22,7 @@ node scripts/hall-preview/server.js # neutral pack, port 4600
|
|
|
22
22
|
| `--modules-off discord,provisioning` | report those modules disabled in `window.__MODULES__` — verifies `moduleOn()` gating (the Discord links, Instances/Start-a-project widgets must vanish) |
|
|
23
23
|
| `--empty` | drain every array in every fixture — the empty-state screenshot pass |
|
|
24
24
|
| `--fixture-me` | don't read your own account live — serve the checked-in `me.json` snapshot (see below) |
|
|
25
|
+
| `--latency 3000` | hold every data read that many ms before answering, so a page's loading state (the hall's `.hall-loading` mark) stays on screen long enough to look at |
|
|
25
26
|
|
|
26
27
|
Writes are not wired: any `POST/PATCH/DELETE` answers `501 preview_readonly` — except
|
|
27
28
|
the canned writes in `server.js` and, since task 1004685, the five per-builder pref
|
|
@@ -65,6 +65,10 @@ const MODULES_ON = String(arg('--modules-on') || '').split(',').map((s) => s.tri
|
|
|
65
65
|
// own account live. The screenshot matrix in the README wants a stable subject,
|
|
66
66
|
// and it is the explicit way to preview the signed-out fallback (task 1003315).
|
|
67
67
|
const FIXTURE_ME = process.argv.includes('--fixture-me');
|
|
68
|
+
// --latency: hold every data read this many ms before answering (task 1005043),
|
|
69
|
+
// so a page's loading state stays on screen long enough to be looked at. The
|
|
70
|
+
// fixtures answer in a millisecond, which is why no loader had ever been seen.
|
|
71
|
+
const LATENCY = Math.max(0, Number(arg('--latency')) || 0);
|
|
68
72
|
const BRAND_FILE = arg('--brand') || 'config/branding.neutral.json';
|
|
69
73
|
process.env.BONGOS_BRANDING_FILE = path.resolve(ROOT, BRAND_FILE);
|
|
70
74
|
|
|
@@ -295,6 +299,11 @@ function fixtureFor(apiPath, query = {}) {
|
|
|
295
299
|
// ---- app ----------------------------------------------------------------------
|
|
296
300
|
const app = express();
|
|
297
301
|
|
|
302
|
+
// --latency (above): the live channel is left alone, it is a stream, not a read.
|
|
303
|
+
if (LATENCY) {
|
|
304
|
+
app.use((req, res, next) => (/^\/api\//.test(req.path) && !/\/live\/?$/.test(req.path) ? setTimeout(next, LATENCY) : next()));
|
|
305
|
+
}
|
|
306
|
+
|
|
298
307
|
// The API stub — intercepts every /api/gds/*, /api/bongos/*, /api/bongos/v1/*
|
|
299
308
|
// request BEFORE mountInternalSurfaces can reach the real routes (which would
|
|
300
309
|
// touch Postgres — the harness is DB-less by design). A plain function
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/apex-hall-redirects.js — the apex hands the hall's addresses to the hall's own
|
|
4
|
+
// host: /builders/* (the ADR 0036 cutover) and /deploy, the deploy door every hosted
|
|
5
|
+
// project's Update button names (task 1004736).
|
|
6
|
+
//
|
|
7
|
+
// Moved out of serve-internal.js, which sits at the 1500-line size ratchet, and mounted from
|
|
8
|
+
// it at the same point in the chain. `skip(req)` is true for a module surface host and for
|
|
9
|
+
// the hall's own host (on a single-host instance that is every request), so both answer only
|
|
10
|
+
// on the apex. `hostOf(req)` is the request's host, lower-cased, without a port.
|
|
11
|
+
|
|
12
|
+
// #739 (ADR 0036 cutover): the builder surface's canonical home is the subdomain. On the
|
|
13
|
+
// apex, /builders and /builders/* go to builders.<apex>/<rest> so old links keep working.
|
|
14
|
+
// 301 (permanent) — the cutover is proven live (#741; started as a 302 in #739). The URL
|
|
15
|
+
// fragment (#/task/N deep-links) is preserved by the browser across the redirect; the query
|
|
16
|
+
// string is carried explicitly. The target apex comes from the request host, so it stays
|
|
17
|
+
// brand-agnostic.
|
|
18
|
+
function buildersRedirect({ skip, hostOf }) {
|
|
19
|
+
return (req, res, next) => {
|
|
20
|
+
if (skip(req)) return next();
|
|
21
|
+
if (req.path !== '/builders' && !req.path.startsWith('/builders/')) return next();
|
|
22
|
+
const rest = req.path.replace(/^\/builders\/?/, ''); // '/builders'→'', '/builders/watch'→'watch'
|
|
23
|
+
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
24
|
+
return res.redirect(301, `https://builders.${hostOf(req)}/${rest}${qs}`);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// THE DEPLOY DOOR ON THE APEX (task 1004736). A hosted project's Update button is
|
|
29
|
+
// `<hub origin>/deploy?project=<slug>` (routes/core-update.js hubDeployUrl, and the
|
|
30
|
+
// HUB_UPGRADE_URL provisioning writes), and the hub origin is its apex, where nothing served
|
|
31
|
+
// /deploy: every hosted owner who pressed Update met a 404. The page is the hall's, so the
|
|
32
|
+
// apex hands the request over, query and all, and the hall decides (its page gate, or
|
|
33
|
+
// Settings where no door runs). Answered here rather than by rewriting the links, because
|
|
34
|
+
// those already sit in every project's web.env and core and would wait for each project's
|
|
35
|
+
// core to move. 302: the apex may serve its own /deploy one day.
|
|
36
|
+
const APEX_DEPLOY_RE = /^\/deploy(?:\.html)?\/?$/;
|
|
37
|
+
function deployRedirect({ skip, hostOf }) {
|
|
38
|
+
return (req, res, next) => {
|
|
39
|
+
if (skip(req)) return next();
|
|
40
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return next();
|
|
41
|
+
if (!APEX_DEPLOY_RE.test(req.path)) return next();
|
|
42
|
+
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
43
|
+
return res.redirect(302, `https://builders.${hostOf(req)}/deploy${qs}`);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Mount both on `app`, in order, at the caller's point in the chain. */
|
|
48
|
+
function mountApexHallRedirects(app, { skip, hostOf }) {
|
|
49
|
+
app.use(buildersRedirect({ skip, hostOf }));
|
|
50
|
+
app.use(deployRedirect({ skip, hostOf }));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = { mountApexHallRedirects };
|
|
@@ -34,6 +34,7 @@ const fs = require('fs');
|
|
|
34
34
|
const express = require('express');
|
|
35
35
|
|
|
36
36
|
const { buildGdsRouter } = require('./routes');
|
|
37
|
+
const { mountApexHallRedirects } = require('./apex-hall-redirects');
|
|
37
38
|
const { attachFail } = require('./middleware/error-envelope');
|
|
38
39
|
const metrics = require('./metrics');
|
|
39
40
|
const { logger } = require('./logger');
|
|
@@ -923,24 +924,8 @@ function mountInternalSurfaces(app) {
|
|
|
923
924
|
next();
|
|
924
925
|
});
|
|
925
926
|
|
|
926
|
-
//
|
|
927
|
-
|
|
928
|
-
// builders.<apex>/<rest> so old links keep working while the subdomain becomes
|
|
929
|
-
// the front door. Runs ONLY on the apex (skipped on the status + builders
|
|
930
|
-
// hosts; /api is host-agnostic and mounted earlier, and never starts /builders).
|
|
931
|
-
// 301 (permanent) — the cutover is proven live + verified end-to-end (#741;
|
|
932
|
-
// started as a 302 in #739). The URL fragment (#/task/N deep-links) is preserved
|
|
933
|
-
// by the browser across the redirect; the query string is carried explicitly.
|
|
934
|
-
// The redirect target apex is read from the request host (strip a leading
|
|
935
|
-
// "builders."/"status." if present) so it stays brand-agnostic.
|
|
936
|
-
app.use((req, res, next) => {
|
|
937
|
-
if (isModuleSurfaceHost(req) || isBuildersHost(req)) return next();
|
|
938
|
-
if (req.path !== '/builders' && !req.path.startsWith('/builders/')) return next();
|
|
939
|
-
const apex = siteOf(req).host;
|
|
940
|
-
const rest = req.path.replace(/^\/builders\/?/, ''); // '/builders'→'', '/builders/watch'→'watch'
|
|
941
|
-
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?')) : '';
|
|
942
|
-
return res.redirect(301, `https://builders.${apex}/${rest}${qs}`);
|
|
943
|
-
});
|
|
927
|
+
// The apex hands /builders/* (ADR 0036) and /deploy (task 1004736) to the hall's own host.
|
|
928
|
+
mountApexHallRedirects(app, { skip: (req) => isModuleSurfaceHost(req) || isBuildersHost(req), hostOf: (req) => siteOf(req).host });
|
|
944
929
|
|
|
945
930
|
// The /watch monitoring page and the /gate page (task 1179) both sit behind
|
|
946
931
|
// `page.view.government` — Metic and up since ADR 0157. Same server-side page
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.24.
|
|
84
|
+
const CORE_VERSION = '1.24.26'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// tests/hall_loading.mjs — the hall's one loader, the Bongos mark (task 1005043, goal 1000095).
|
|
2
|
+
//
|
|
3
|
+
// Owner ask, 2026-11-03: a smooth load screen with the Bongos logo anywhere the
|
|
4
|
+
// hall waits for data, in the spirit of monday.com's Vibe data loaders. One CSS
|
|
5
|
+
// component in style.css (.hall-loading) draws the mark from shell.js's
|
|
6
|
+
// brand-mark geometry; a page opts in by putting the class on the placeholder it
|
|
7
|
+
// already had. These cases read the real sheets and pages: the loader keeps
|
|
8
|
+
// DESIGN.md's motion and colour rules, its rest frame is the logo, and no hall
|
|
9
|
+
// page is left with a bare "Loading…" line or a blank wait.
|
|
10
|
+
import assert from 'node:assert/strict';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
15
|
+
|
|
16
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const HALL = path.join(ROOT, 'modules/hall-ui/public');
|
|
18
|
+
const read = (f) => fs.readFileSync(path.join(HALL, f), 'utf8');
|
|
19
|
+
const STYLE = read('style.css');
|
|
20
|
+
const SHELL = read('shell.js');
|
|
21
|
+
|
|
22
|
+
// The loader's section of style.css, from its banner to the next rule family.
|
|
23
|
+
const START = STYLE.indexOf('/* ---- the loader (task 1005043)');
|
|
24
|
+
const LOADER = STYLE.slice(START, STYLE.indexOf('/* collapsible card body', START));
|
|
25
|
+
|
|
26
|
+
function keyframes(name) {
|
|
27
|
+
const m = STYLE.match(new RegExp(`@keyframes ${name} \\{`));
|
|
28
|
+
assert.ok(m, `@keyframes ${name} exists`);
|
|
29
|
+
let depth = 1, i = m.index + m[0].length;
|
|
30
|
+
for (; i < STYLE.length && depth; i++) depth += STYLE[i] === '{' ? 1 : STYLE[i] === '}' ? -1 : 0;
|
|
31
|
+
return STYLE.slice(m.index + m[0].length, i - 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test('the loader is drawn from the Bongos mark, in three sizes', () => {
|
|
35
|
+
assert.ok(START > 0 && LOADER.length > 500, 'the loader section is found in style.css');
|
|
36
|
+
for (const sel of ['.hall-loading {', '.hall-loading::before {', '.hall-loading::after {', '.hall-loading--page {', '.hall-loading--inline {']) {
|
|
37
|
+
assert.ok(LOADER.includes(sel), `${sel} is declared`);
|
|
38
|
+
}
|
|
39
|
+
// The same ring arc, core and dot the shell's brand mark draws, so the loader IS the logo.
|
|
40
|
+
const ring = SHELL.match(/<path class="mr" d="([^"]+)"/)[1];
|
|
41
|
+
assert.ok(LOADER.includes(`d='${ring}'`), 'the ring is the brand mark\'s arc');
|
|
42
|
+
assert.ok(SHELL.includes('<circle class="mp" cx="100" cy="100" r="55"'), 'the brand mark\'s core');
|
|
43
|
+
assert.ok(LOADER.includes("circle cx='100' cy='100' r='55'"), 'is the loader\'s core');
|
|
44
|
+
assert.ok(SHELL.includes('<circle cx="156" cy="46" r="12" fill="#fb9569"/>'), 'the brand mark\'s dot');
|
|
45
|
+
assert.ok(LOADER.includes("circle cx='156' cy='46' r='12'"), 'is the loader\'s dot');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('it moves in CSS only, on transform and filter only, and rests on the logo', () => {
|
|
49
|
+
const frames = { 'hall-loading-arrive': keyframes('hall-loading-arrive'), 'hall-loading-turn': keyframes('hall-loading-turn'), 'hall-loading-breathe': keyframes('hall-loading-breathe') };
|
|
50
|
+
for (const [name, body] of Object.entries(frames)) {
|
|
51
|
+
const props = [...body.matchAll(/([a-z-]+)\s*:/g)].map((m) => m[1]);
|
|
52
|
+
assert.ok(props.length, `${name} animates something`);
|
|
53
|
+
for (const prop of props) assert.ok(['transform', 'filter'].includes(prop), `${name} animates ${prop}, not transform or filter`);
|
|
54
|
+
}
|
|
55
|
+
// The rest frame: the turn has no `from`, so 0deg (the logo's own pose, dot top right)
|
|
56
|
+
// is where it starts and where the Kill Switch leaves it; the arrival and the breath
|
|
57
|
+
// have no `to`, so their resting value is the element as styled, fully shown.
|
|
58
|
+
assert.match(frames['hall-loading-turn'], /^\s*to \{ transform: rotate\(1turn\); \}\s*$/);
|
|
59
|
+
assert.match(frames['hall-loading-arrive'], /^\s*from \{/);
|
|
60
|
+
assert.match(frames['hall-loading-breathe'], /^\s*from \{/);
|
|
61
|
+
// The Kill Switch stops every animation, pseudo-elements included.
|
|
62
|
+
assert.match(STYLE, /@media \(prefers-reduced-motion: reduce\) \{\s*\*, \*::before, \*::after \{\s*animation: none !important;/);
|
|
63
|
+
// No motion authored in JavaScript: nothing in the hall drives the loader from script.
|
|
64
|
+
for (const f of fs.readdirSync(HALL).filter((n) => n.endsWith('.js'))) {
|
|
65
|
+
assert.doesNotMatch(read(f), /hall-loading[^'"\n]*['"]\)?\.(animate|style)\b/, `${f} does not animate the loader`);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('every colour is a token; the black in the masks is alpha, not paint', () => {
|
|
70
|
+
const css = LOADER.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
71
|
+
assert.doesNotMatch(css, /#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(/, 'no colour literal');
|
|
72
|
+
const paints = [...css.matchAll(/(?:background|color):\s*([^;]+);/g)].map((m) => m[1].trim());
|
|
73
|
+
assert.ok(paints.length >= 3, 'the ink, the dot and the caption are painted');
|
|
74
|
+
for (const p of paints) assert.match(p, /^(var\(--[a-z-]+\)|color-mix\(in srgb, var\(--ink\) \d+%, var\(--bg-card\)\))$/, `${p} is a token or a mix of one`);
|
|
75
|
+
// The one accent is the dot: the live thing on a waiting page (the One Warm Thing Rule).
|
|
76
|
+
assert.equal((css.match(/var\(--accent\)/g) || []).length, 1, 'the accent paints the dot alone');
|
|
77
|
+
const masks = [...css.matchAll(/url\("data:image\/svg\+xml,([^"]+)"\)/g)].map((m) => decodeURIComponent(m[1]));
|
|
78
|
+
assert.equal(masks.length, 2, 'two masks: the ring and core, and the dot');
|
|
79
|
+
for (const m of masks) assert.doesNotMatch(m.replace(/stroke='black'|fill='none'/g, ''), /fill='|stroke='|#/, 'a mask carries no colour of its own');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('the page loaders step aside by themselves, and never show without :has()', () => {
|
|
83
|
+
assert.match(STYLE, /\.hall-loading--wait, \.hall-loading--zones \{ display: none; \}/);
|
|
84
|
+
assert.match(STYLE, /@supports selector\(:has\(a\)\) \{\s*\.hall-loading--wait:not\(:has\(~ :not\(\[hidden\], script, template, style\)\)\) \{ display: flex; \}\s*\.hall-loading--zones:not\(:has\(~ :not\(\[hidden\]\) > :not\(\[hidden\], script, template, style\)\)\) \{ display: flex; \}\s*\}/);
|
|
85
|
+
// A page whose sections all start [hidden] carries the --wait loader FIRST in its main.
|
|
86
|
+
for (const page of ['board-room', 'deploy', 'fleet', 'gate', 'government', 'market-watch-settings', 'project-settings', 'sessions']) {
|
|
87
|
+
const html = read(`${page}.html`);
|
|
88
|
+
const main = html.slice(html.indexOf('<main id="hall-main">'));
|
|
89
|
+
const first = main.replace(/<main id="hall-main">\s*(<!--[\s\S]*?-->\s*)*/, '');
|
|
90
|
+
assert.match(first, /^<p class="hall-loading hall-loading--page hall-loading--wait" role="status">Loading…<\/p>/, `${page} opens its main on the page loader`);
|
|
91
|
+
const after = first.slice(first.indexOf('</p>') + 4);
|
|
92
|
+
const kids = [...after.matchAll(/^ {8}<(section|div|p|nav)\b([^>]*)>/gm)];
|
|
93
|
+
assert.ok(kids.length >= 2, `${page} has sections after the loader`);
|
|
94
|
+
for (const [, tag, attrs] of kids) assert.match(attrs, /\bhidden\b/, `${page}: a <${tag}> after the loader starts hidden, or the loader would never show`);
|
|
95
|
+
}
|
|
96
|
+
assert.match(read('index.html'), /<section class="view" id="view-home"[^>]*>\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page hall-loading--zones" role="status">Loading…<\/p>/, 'Home waits on its zones');
|
|
97
|
+
assert.match(read('watch.html'), /<main id="hall-main">\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page hall-loading--zones" role="status">Loading…<\/p>/, 'Watch waits on its panes');
|
|
98
|
+
// Settings' layout is never [hidden] while it loads, so its loader is put away by hideLayout().
|
|
99
|
+
assert.match(read('settings.html'), /id="settings-groups"[^>]*>\s*<!--[^>]*-->\s*<p class="hall-loading hall-loading--page" id="settings-loading" role="status">/);
|
|
100
|
+
assert.match(read('settings.js'), /function hideLayout\(hide\) \{[\s\S]*?getElementById\('settings-loading'\)[\s\S]*?loading\.hidden = true;/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('no hall page waits on a bare line of text any more', () => {
|
|
104
|
+
// A placeholder an element's own script rewrites IN PLACE keeps its plain words:
|
|
105
|
+
// the class would stay behind on the finished sentence. Each is named with why.
|
|
106
|
+
const IN_PLACE = new Set([
|
|
107
|
+
'atlas.html#caption', // the map's caption, rewritten by atlas.js
|
|
108
|
+
'sessions.html#ledger-status', // the ledger's status line, rewritten by sessions.js
|
|
109
|
+
'goals.html#goal-deps-card-lede', // the dependency card's lede, rewritten by goals-page.js
|
|
110
|
+
'goals.html#goal-deps-lede',
|
|
111
|
+
'profile.html#activity-placeholder', // reused for the signed-out notice by profile.js
|
|
112
|
+
]);
|
|
113
|
+
const WAIT = /<(p|div|li|span|td)\b([^>]*)>\s*(?:Loading|Reading|Opening|Checking)\b[^<]*…\s*<\/\1>|<(p|div|li|span|td)\b([^>]*)>…<\/\4>/g;
|
|
114
|
+
const misses = [];
|
|
115
|
+
for (const f of fs.readdirSync(HALL).filter((n) => n.endsWith('.html'))) {
|
|
116
|
+
for (const m of read(f).matchAll(WAIT)) {
|
|
117
|
+
const attrs = m[2] || m[5] || '';
|
|
118
|
+
if (/page-head__sub/.test(attrs)) continue; // the page head is hidden on every page but Home (task 1004836)
|
|
119
|
+
const id = (attrs.match(/\bid="([^"]+)"/) || [])[1];
|
|
120
|
+
if (id && IN_PLACE.has(`${f}#${id}`)) continue;
|
|
121
|
+
if (!/\bhall-loading\b/.test(attrs)) misses.push(`${f}: ${m[0].slice(0, 90)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
assert.deepEqual(misses, [], 'every waiting placeholder wears the loader');
|
|
125
|
+
// The table placeholder rows keep their cell, with the loader inside it.
|
|
126
|
+
assert.match(read('work.html'), /<td colspan="6" class="ledger__empty"><span class="hall-loading" role="status">Loading…<\/span><\/td>/);
|
|
127
|
+
assert.match(read('hall-widgets.js'), /<td colspan="4" class="ledger__empty"><span class="hall-loading" role="status">Loading…<\/span><\/td>/);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('the placeholders the page scripts write wear it too, and a failure stops it', () => {
|
|
131
|
+
const WRITTEN = {
|
|
132
|
+
'blockers.js': '<li class="hall-loading hall-loading--page" role="status">Loading…</li>',
|
|
133
|
+
'goals.js': '<div class="hall-loading" role="status">Loading your goals…</div>',
|
|
134
|
+
'lightbulb.js': '<p class="hall-loading" role="status">Loading…</p>',
|
|
135
|
+
'task-detail.js': '<p class="hall-loading hall-loading--inline" role="status">Loading versions…</p>',
|
|
136
|
+
'criterion-uat-ui.js': '<p class="hall-loading hall-loading--inline" role="status">Loading…</p>',
|
|
137
|
+
'watch.js': '<p class="hall-loading" role="status">Reading…</p>',
|
|
138
|
+
'studio.js': '<p class="hall-loading" role="status">Reading the reviews…</p>',
|
|
139
|
+
'settings-specialities.js': '<p class="hall-loading hall-loading--inline" role="status">Loading this project’s skills…</p>',
|
|
140
|
+
'settings-software-update.js': '<p class="hall-loading hall-loading--inline" role="status">Checking for a newer version…</p>',
|
|
141
|
+
'goals-page.js': '<div class="hall-loading" role="status">Loading…</div>',
|
|
142
|
+
};
|
|
143
|
+
for (const [f, markup] of Object.entries(WRITTEN)) assert.ok(read(f).includes(markup), `${f} writes the loader`);
|
|
144
|
+
// Blockers' first paint replaces the list wholesale, so its loader cannot outlive the rows.
|
|
145
|
+
assert.match(read('blockers.js'), /listEl\.innerHTML = rows\.length/);
|
|
146
|
+
// Collab used to be blank on a failed read; with loaders in its lists it must stop them.
|
|
147
|
+
const collab = read('collab.js');
|
|
148
|
+
const failure = collab.slice(collab.lastIndexOf('load().catch('));
|
|
149
|
+
assert.match(failure, /\['for-me', 'mine', 'all'\]\.forEach\(\(k\) => \{ const host = \$\(`#cb-\$\{k\}`\); if \(host\) host\.innerHTML = ''; \}\);/);
|
|
150
|
+
// The skeleton bars and the goal list's rings breathe in the loader's beat.
|
|
151
|
+
assert.match(STYLE, /\.kit-skel \{[^}]*animation: hall-loading-breathe \.8s ease-in-out infinite alternate-reverse;/);
|
|
152
|
+
assert.match(read('goals.css'), /\.gr-skel__ring \{[^}]*animation: hall-loading-breathe \.8s ease-in-out infinite alternate-reverse;/);
|
|
153
|
+
});
|