@bongos/core 1.25.30 → 1.25.32
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 +42 -47
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/api/openapi.json +3 -6
- package/docs/copy-inventory.md +13 -13
- package/docs/copy-registry.json +14 -14
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +229 -203
- package/modules/government/docket.js +37 -6
- package/modules/hall-ui/public/city.js +1 -1
- package/modules/hall-ui/public/gov-feeds.js +2 -2
- package/modules/hall-ui/public/index.html +4 -4
- package/modules/hall-ui/public/shell-game.css +2 -3
- package/modules/hall-ui/public/shell-game.js +6 -5
- package/modules/hall-ui/public/welcome-boot.css +18 -31
- package/modules/hall-ui/public/welcome-boot.js +81 -97
- package/modules/hall-ui/records/start-screen.md +6 -6
- package/modules/hall-ui/records/welcome-tower.md +29 -20
- package/modules/lifecycle/routes/tasks.js +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/module-api.js +1 -1
- package/tests/docket_load_speed.mjs +99 -0
- package/tests/hall_city_living.mjs +1 -1
- package/tests/hall_gov_counts.mjs +1 -1
- package/tests/hall_gov_real_site.mjs +1 -1
- package/tests/hall_sky_time.mjs +3 -2
- package/tests/hall_start_screen.mjs +3 -2
- package/tests/hall_street_deck.mjs +1 -1
- package/tests/hall_street_depth.mjs +2 -2
- package/tests/welcome_boot.mjs +35 -27
- package/tests/welcome_boot_browser.mjs +25 -19
- package/modules/hall-ui/public/welcome-day.webp +0 -0
- package/modules/hall-ui/public/welcome-night.webp +0 -0
|
@@ -631,6 +631,12 @@ async function attachAsks(matters, readAsks, timeoutMs) {
|
|
|
631
631
|
// task 1004705) and the docket says `routed: true`. null routes nothing.
|
|
632
632
|
async function buildDocket({ viewer, permissions, contributions = registeredContributions(), now = Date.now(), timeoutMs = SOURCE_TIMEOUT_MS, decisions = null, government = null, readAsks = null, readFlags = null, seats = null } = {}) {
|
|
633
633
|
const held = new Set(permissions || []);
|
|
634
|
+
// `decisions`, `government` and `seats` may arrive as promises (docketFor starts
|
|
635
|
+
// those reads alongside the sources, task 1005310); they are awaited once the
|
|
636
|
+
// sources have answered. The handler is attached here, before anything waits, so
|
|
637
|
+
// a failure is never an unhandled rejection while the sources are reading.
|
|
638
|
+
const governmentReads = Promise.all([decisions, government, seats]);
|
|
639
|
+
governmentReads.catch(() => {});
|
|
634
640
|
const ctx = Object.freeze({
|
|
635
641
|
builderId: String(viewer.id),
|
|
636
642
|
rank: viewer.rank || null,
|
|
@@ -681,8 +687,11 @@ async function buildDocket({ viewer, permissions, contributions = registeredCont
|
|
|
681
687
|
const order = declared.map((d) => d.source);
|
|
682
688
|
sourceStatus.sort((a, b) => order.indexOf(a.source) - order.indexOf(b.source));
|
|
683
689
|
matters.sort(compareMatters);
|
|
684
|
-
|
|
685
|
-
|
|
690
|
+
// Asks and reword flags are two independent reads on the same matters, each
|
|
691
|
+
// writing its own field: read together (task 1005310), alongside the
|
|
692
|
+
// government reads started above.
|
|
693
|
+
await Promise.all([attachAsks(matters, readAsks, timeoutMs), attachFlags(matters, readFlags, timeoutMs)]);
|
|
694
|
+
[decisions, government, seats] = await governmentReads;
|
|
686
695
|
// THE DIRECTION DESK (task 1004705): who each matter waits on, from real seats.
|
|
687
696
|
const yours = seats ? routing.routeAll(matters, ctx.builderId, seats, now) : null;
|
|
688
697
|
|
|
@@ -872,16 +881,38 @@ async function findMatterFor(builder, matterId) {
|
|
|
872
881
|
|
|
873
882
|
// The route's entry point: resolve the viewer's permissions server-side (fresh,
|
|
874
883
|
// uncached — ADR 0016), then build.
|
|
875
|
-
|
|
884
|
+
//
|
|
885
|
+
// ONE BUILD PER VIEWER AT A TIME (task 1005310). The street asks for the docket
|
|
886
|
+
// and for GET /government/counts in the same breath, and counts is built from
|
|
887
|
+
// this same docket, so every street load built it twice, side by side, and each
|
|
888
|
+
// slowed the other. A second ask for the SAME viewer that arrives while a build
|
|
889
|
+
// is running shares that build; the moment it finishes nothing is kept, so the
|
|
890
|
+
// next ask builds afresh. Nothing is cached: no answer is older than the request
|
|
891
|
+
// that receives it, and permissions are still resolved inside every build.
|
|
892
|
+
const building = new Map();
|
|
893
|
+
function docketFor(builder) {
|
|
894
|
+
const key = String(builder && builder.id);
|
|
895
|
+
const running = building.get(key);
|
|
896
|
+
if (running) return running;
|
|
897
|
+
const p = buildDocketFor(builder).finally(() => { building.delete(key); });
|
|
898
|
+
building.set(key, p);
|
|
899
|
+
return p;
|
|
900
|
+
}
|
|
901
|
+
async function buildDocketFor(builder) {
|
|
876
902
|
const permissions = await resolver.resolveBuilderPermissions(builder.id);
|
|
877
903
|
// The table is read per request, uncached (ADR 0016). A failed read leaves
|
|
878
904
|
// every building's decision null; it never fails the city.
|
|
879
|
-
|
|
905
|
+
// The government reads (decision rules → the government → its seats) run
|
|
906
|
+
// ALONGSIDE the sources (task 1005310): buildDocket waits for them only once
|
|
907
|
+
// its sources have answered. Every step below catches its own failure.
|
|
908
|
+
const decisions = board.decisionRulesView().catch((err) => {
|
|
880
909
|
log.warn({ err: err && err.message }, 'governor docket: decision rules unreadable');
|
|
881
910
|
return null;
|
|
882
911
|
});
|
|
883
|
-
const government = permissions.includes(GOVERNMENT_GATE.permission)
|
|
884
|
-
|
|
912
|
+
const government = permissions.includes(GOVERNMENT_GATE.permission)
|
|
913
|
+
? decisions.then((d) => loadGovernment({ decisions: d }))
|
|
914
|
+
: Promise.resolve(null);
|
|
915
|
+
const seats = government.then((g) => (g ? loadSeats(g) : null));
|
|
885
916
|
return buildDocket({ viewer: builder, permissions, decisions, government, readAsks: asksDb.asksFor, readFlags: asksDb.unclearFor, seats });
|
|
886
917
|
}
|
|
887
918
|
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
leads: '/api/bongos/government/leads',
|
|
57
57
|
// the pots each goal draws from, the reserve, the spend cap (task 1004712)
|
|
58
58
|
treasury: '/api/bongos/government/treasury',
|
|
59
|
-
claims: '/api/bongos/tasks/claimable?
|
|
59
|
+
claims: '/api/bongos/tasks/claimable?view=claims',
|
|
60
60
|
tasks: '/api/bongos/tasks?status=active&limit=200',
|
|
61
61
|
cost: '/api/bongos/public/cost-summary',
|
|
62
62
|
roster: '/api/bongos/builders/roster',
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
presets: '/government/presets',
|
|
43
43
|
goals: '/goals',
|
|
44
44
|
rollups: '/goals/rollups',
|
|
45
|
-
// the
|
|
46
|
-
claims: '/tasks/claimable?
|
|
45
|
+
// the open claims alone (?view=claims, task 1005310): the claimable feed is not built
|
|
46
|
+
claims: '/tasks/claimable?view=claims',
|
|
47
47
|
tasks: '/tasks?status=active&limit=200',
|
|
48
48
|
cost: '/public/cost-summary',
|
|
49
49
|
roster: '/builders/roster',
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
21
21
|
<link rel="stylesheet" href="/builders/style.css?v=2026-11-11-helm4d">
|
|
22
22
|
<link rel="stylesheet" href="/builders/visor.css?v=2026-11-14-world">
|
|
23
|
-
<link rel="stylesheet" href="/builders/welcome-boot.css?v=2026-11-
|
|
24
|
-
<link rel="stylesheet" href="/builders/shell-game.css?v=2026-11-
|
|
23
|
+
<link rel="stylesheet" href="/builders/welcome-boot.css?v=2026-11-14-cloud">
|
|
24
|
+
<link rel="stylesheet" href="/builders/shell-game.css?v=2026-11-14-cloud">
|
|
25
25
|
<link rel="stylesheet" href="/builders/hall-ask.css">
|
|
26
26
|
<link rel="stylesheet" href="/builders/home-city.css?v=2026-11-14-world">
|
|
27
27
|
<link rel="stylesheet" href="/builders/home-gallery.css">
|
|
@@ -102,8 +102,8 @@
|
|
|
102
102
|
BEFORE builders.js so the chrome ids exist when events are wired. -->
|
|
103
103
|
<!-- task 1004785: the first-visit-of-the-day welcome (the tower). Before shell.js, which asks it
|
|
104
104
|
before the craft picker so the two never stack. Home only. -->
|
|
105
|
-
<script src="/builders/welcome-boot.js?v=2026-11-
|
|
106
|
-
<script src="/builders/shell-game.js?v=2026-11-
|
|
105
|
+
<script src="/builders/welcome-boot.js?v=2026-11-14-cloud" defer></script>
|
|
106
|
+
<script src="/builders/shell-game.js?v=2026-11-14-cloud" defer></script>
|
|
107
107
|
<script src="/builders/shell-trail.js?v=2026-11-05-numbers" defer></script>
|
|
108
108
|
<script src="/builders/craft-mode.js?v=2026-11-10-visor" defer></script>
|
|
109
109
|
<script src="/builders/shell.js?v=2026-11-13-founding" defer></script>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* shell-game.css — the game's start screen and load screen (task 1005143; shell-game.js start()),
|
|
2
|
-
in the Helm look (task 1005192, ADR 0402). Both stand over the
|
|
2
|
+
in the Helm look (task 1005192, ADR 0402). Both stand over the city in the clouds (task 1005308). The start
|
|
3
3
|
screen is one full-height night panel on the left, closed left and right by an ice hairline:
|
|
4
4
|
the mark and the project's name in the name voice, the five choices as full-width rows, one
|
|
5
5
|
line at the foot. The load screen is the same panel language, centred: the week in the name
|
|
@@ -18,11 +18,10 @@ html.gs-open { overflow: hidden; }
|
|
|
18
18
|
.gs--leaving { opacity: 0; pointer-events: none; }
|
|
19
19
|
.gs--load { align-items: center; justify-content: center; padding: var(--ds-space-lg) var(--ds-space-gutter-narrow); }
|
|
20
20
|
|
|
21
|
-
/* the still: the
|
|
21
|
+
/* the still: the city in the clouds (.cloud-scene, welcome-boot.css), covering the screen. The start screen leaves it bare (the panel is
|
|
22
22
|
its own ground); the load screen lays the scrim over it so the centred panel reads anywhere. */
|
|
23
23
|
.gs__art { position: absolute; inset: 0; }
|
|
24
24
|
.gs--load .gs__art::after { content: ""; position: absolute; inset: 0; background: var(--scrim); }
|
|
25
|
-
.gs__img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
26
25
|
|
|
27
26
|
/* the two panels: night over the art, an ice hairline left and right, no corner, no shadow */
|
|
28
27
|
.gs__menu, .gs__load {
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
|
|
88
88
|
// ---------- the start screen and the load screen (task 1005143, BONGOS-V3 R03) ----------
|
|
89
89
|
// A fresh sign-in opens on the start screen: Start, Options, Achievements, Extras, Quit over a
|
|
90
|
-
//
|
|
90
|
+
// the city in the clouds (task 1005308; the painting before it is retired). Start turns game mode on and passes through the load screen, where the project city
|
|
91
91
|
// builds itself up week by week under a tip, and then the world is there. welcome-boot.js calls
|
|
92
|
-
// start() on Home (where a sign-in lands) and hands in the
|
|
92
|
+
// start() on Home (where a sign-in lands) and hands in the scene's painter; this file draws both screens.
|
|
93
93
|
// Rules are in modules/hall-ui/records/start-screen.md.
|
|
94
94
|
var MENU = [
|
|
95
95
|
{ id: 'start', label: 'Start' },
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
}
|
|
156
156
|
// The Helm panel (task 1005192, ADR 0402): the mark and the name, the choices, one line at the foot.
|
|
157
157
|
function screenHtml(o) {
|
|
158
|
-
return '<div class="gs__art" aria-hidden="true"
|
|
158
|
+
return '<div class="gs__art" aria-hidden="true"></div>' +
|
|
159
159
|
'<div class="gs__menu">' +
|
|
160
160
|
'<div class="gs__top">' +
|
|
161
161
|
'<div class="gs__head">' + markHtml() +
|
|
@@ -369,8 +369,8 @@
|
|
|
369
369
|
return v === 'game' || v === 'classic' ? v : null;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
-
// Put the start screen up. `o`: { name,
|
|
373
|
-
//
|
|
372
|
+
// Put the start screen up. `o`: { name, scene, done, signIn } — the project's name, the painter of
|
|
373
|
+
// the city that stands behind both screens (scene(host), welcome-boot.js paintScene), what to do once the world is showing, and (signed out) how to sign
|
|
374
374
|
// in once a view is picked. True when it is up; false for a classic pick made before sign-in,
|
|
375
375
|
// which goes straight to the hall.
|
|
376
376
|
function start(o) {
|
|
@@ -387,6 +387,7 @@
|
|
|
387
387
|
root.setAttribute('aria-modal', 'true');
|
|
388
388
|
root.setAttribute('aria-labelledby', 'gs-title');
|
|
389
389
|
root.innerHTML = screenHtml(opts);
|
|
390
|
+
if (typeof opts.scene === 'function') { try { opts.scene(root.querySelector('.gs__art')); } catch (_) { /* the ground stands */ } }
|
|
390
391
|
root.addEventListener('click', onClick);
|
|
391
392
|
document.addEventListener('keydown', onKey, true);
|
|
392
393
|
document.body.appendChild(root);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* welcome-boot.css — the first-visit-of-the-day welcome (tasks 1004785, 1004818; welcome-boot.js).
|
|
2
|
-
The scene is the
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
The scene is the city in the clouds, drawn in the hour's sky (task 1005308; the circuit-city
|
|
3
|
+
painting it replaced is retired). Its ink follows day or night rather than the hall theme, so it
|
|
4
|
+
is fixed here rather than --ink. .cloud-scene is also the ground of the game's start and load
|
|
5
|
+
screens (shell-game.js), which this sheet shares the page with. */
|
|
5
6
|
html.wb-open { overflow: hidden; }
|
|
6
7
|
.wb {
|
|
7
8
|
position: fixed; inset: 0; z-index: 1000; overflow: hidden;
|
|
@@ -10,35 +11,21 @@ html.wb-open { overflow: hidden; }
|
|
|
10
11
|
}
|
|
11
12
|
.wb--day { color: #14161c; background: linear-gradient(180deg, #a9d6dc 0%, #f1d9d0 55%, #e9cfc6 100%); }
|
|
12
13
|
|
|
13
|
-
/* the
|
|
14
|
+
/* the scene: the city in the clouds (welcome-boot.js paintScene), on the hour's sky. It covers the
|
|
15
|
+
screen; the city stands on the bottom edge, so a tall screen shows more sky and a wide one crops it. */
|
|
14
16
|
.wb__art {
|
|
15
|
-
position: absolute;
|
|
16
|
-
width: max(100vw, calc(100vh * 1024 / 572)); aspect-ratio: 1024 / 572;
|
|
17
|
-
transform: translate(-50%, -50%);
|
|
17
|
+
position: absolute; inset: 0; z-index: 1;
|
|
18
18
|
transition: transform 2.2s cubic-bezier(.45, 0, .25, 1);
|
|
19
19
|
}
|
|
20
|
-
.wb__pan { position: absolute; inset: 0; transform-origin:
|
|
21
|
-
@keyframes wb-pan { from { transform: scale(1); } to { transform: scale(1.
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
/* the motion: pulses climb the circuit towers, cars cross, and at night the lights blink */
|
|
27
|
-
.wb-pulses { opacity: 0; transition: opacity 1.6s ease-out; }
|
|
28
|
-
.wb--live .wb-pulses { opacity: 1; }
|
|
29
|
-
.wb-pulse { fill: none; stroke-width: 2.2; stroke-linecap: round; stroke-dasharray: 7 100; stroke-dashoffset: 7; animation: wb-climb linear infinite; }
|
|
30
|
-
@keyframes wb-climb { to { stroke-dashoffset: -100; } }
|
|
31
|
-
.wb--night .wb-pulse { filter: drop-shadow(0 0 3px currentColor); }
|
|
32
|
-
.wb--day .wb-pulse { opacity: .8; stroke-width: 1.8; }
|
|
33
|
-
.wb-car { animation: wb-car linear infinite; }
|
|
34
|
-
@keyframes wb-car {
|
|
35
|
-
0% { transform: translate(0, 0); opacity: 0; }
|
|
36
|
-
10%, 88% { opacity: 1; }
|
|
37
|
-
100% { transform: translate(var(--wb-dx), var(--wb-dy)); opacity: 0; }
|
|
20
|
+
.wb__pan { position: absolute; inset: 0; transform-origin: 50% 70%; animation: wb-pan 40s ease-in-out infinite alternate; }
|
|
21
|
+
@keyframes wb-pan { from { transform: scale(1); } to { transform: scale(1.05) translateX(-1%); } }
|
|
22
|
+
.cloud-scene { background: linear-gradient(to bottom, var(--sky-now-top), var(--sky-now-mid) 46%, var(--sky-now-haze)); }
|
|
23
|
+
.cloud-scene__svg {
|
|
24
|
+
position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none;
|
|
25
|
+
background: radial-gradient(ellipse 70% 60% at 50% 72%, var(--sky-now-cloud), var(--sky-now-base) 30%, var(--sky-now-haze) 62%, transparent);
|
|
38
26
|
}
|
|
39
|
-
.
|
|
40
|
-
|
|
41
|
-
@keyframes wb-moon { from { opacity: .35; } to { opacity: .8; } }
|
|
27
|
+
.cloud-scene__svg .hc-stars { opacity: var(--sky-now-stars); }
|
|
28
|
+
|
|
42
29
|
.wb__cloud { position: absolute; z-index: 2; border-radius: 50%; background: rgba(255, 255, 255, .06); filter: blur(30px); animation: wb-drift linear infinite; pointer-events: none; }
|
|
43
30
|
.wb--day .wb__cloud { background: rgba(255, 255, 255, .35); filter: blur(28px); }
|
|
44
31
|
.wb__cloud--a { top: 8vh; width: 40vw; height: 7vh; animation-duration: 80s; animation-delay: -20s; }
|
|
@@ -71,7 +58,7 @@ html.wb-open { overflow: hidden; }
|
|
|
71
58
|
font-family: var(--font-display); font-size: clamp(18px, 2.6vw, 26px); font-weight: 300;
|
|
72
59
|
letter-spacing: -.01em; text-transform: lowercase; white-space: nowrap;
|
|
73
60
|
}
|
|
74
|
-
/* the mark's ink follows the
|
|
61
|
+
/* the mark's ink follows the hour: pale by night, dark by day; the moon stays salmon */
|
|
75
62
|
.wb__mark { --mark-ink: rgba(255, 255, 255, .88); }
|
|
76
63
|
.wb__planet { transform-box: fill-box; transform-origin: center; }
|
|
77
64
|
.wb__moonlet { transform-box: view-box; transform-origin: 100px 100px; }
|
|
@@ -137,7 +124,7 @@ html.wb-open { overflow: hidden; }
|
|
|
137
124
|
.wb__role small { display: block; margin-top: 3px; }
|
|
138
125
|
.wb__role i { display: inline-block; width: 9px; height: 9px; border-radius: 50%; margin-right: 7px; }
|
|
139
126
|
|
|
140
|
-
/* the veil: white by day, black by night, lifting slowly once the
|
|
127
|
+
/* the veil: white by day, black by night, lifting slowly once the scene is painted */
|
|
141
128
|
.wb__veil { position: absolute; inset: 0; z-index: 6; pointer-events: none; background: #000; opacity: 1; transition: opacity 1.6s ease-out; }
|
|
142
129
|
.wb--day .wb__veil { background: #fff; }
|
|
143
130
|
.wb--lit .wb__veil { opacity: 0; }
|
|
@@ -145,7 +132,7 @@ html.wb-open { overflow: hidden; }
|
|
|
145
132
|
/* the way out: the view sinks past the rooftops and the welcome fades into the hall underneath;
|
|
146
133
|
bound for another room, the veil comes back instead */
|
|
147
134
|
.wb--leaving { pointer-events: none; }
|
|
148
|
-
.wb--leaving .wb__art { transform:
|
|
135
|
+
.wb--leaving .wb__art { transform: translateY(-30vh) scale(1.12); }
|
|
149
136
|
.wb--leaving .wb__banner { transform: translate(-50%, -40vh); }
|
|
150
137
|
.wb--done.wb--leaving .wb__after { opacity: 0; transform: translateY(24px); transition: opacity .5s, transform .5s; }
|
|
151
138
|
.wb--leaving:not(.wb--to-room) { opacity: 0; transition: opacity 1.8s ease-in .5s; }
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// welcome-boot.js — the welcome on a builder's first visit of the day (tasks 1004785, 1004818; idea 1001262).
|
|
2
2
|
//
|
|
3
|
-
// Home only. The scene is the
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
3
|
+
// Home only. The scene is the city in the clouds (task 1005196), drawn by home-city-draw.js and
|
|
4
|
+
// city-draw.js in the hour's sky (sky-time.js): the Cloud Bongos core and the museum standing in
|
|
5
|
+
// cloud, with a slow drift. It replaced the owner's circuit-city painting (a day and a
|
|
6
|
+
// night WebP still), retired everywhere by task 1005308 (owner, 2026-11-14), and with it the
|
|
7
|
+
// lights that were pinned to the painting's pixels. paintScene() is also what the game's start and
|
|
8
|
+
// load screens stand on (shell-game.js), so every screen before the hall shows the same city.
|
|
9
|
+
// The banner at the top is the logo, animated, and a "Built on cloudbongos.com" credit sits in the
|
|
10
|
+
// bottom-right corner (task 1004953). The ink follows the local time of day: day from 06:00 to
|
|
11
|
+
// 17:59, night otherwise. It opens from a white (day) or black (night) veil, then
|
|
12
|
+
// "Good morning, <name>" comes up with two choices. The way out is slow: the view sinks past the
|
|
13
|
+
// rooftops and fades into the hall underneath.
|
|
11
14
|
//
|
|
12
15
|
// Rules:
|
|
13
16
|
// - A signed-in builder sees it once per LOCAL day. A fresh sign-in (the sign-in click leaves a
|
|
@@ -22,10 +25,11 @@
|
|
|
22
25
|
// picker asked (OTBCraftMode.markAsked), so the two never stack.
|
|
23
26
|
// - Picking a role also sets the hall's craft mode. Governing is a seat (craft-mode.js), so the
|
|
24
27
|
// Owner role shows only when OTBCraftMode offers it.
|
|
25
|
-
// -
|
|
26
|
-
//
|
|
28
|
+
// - Without the city's painters (a page that does not load them) the hour's sky gradient stands alone,
|
|
29
|
+
// and the welcome works the same.
|
|
30
|
+
// - prefers-reduced-motion: no motion; the still city and the greeting at once, leaving is immediate.
|
|
27
31
|
//
|
|
28
|
-
// The pure parts (the day rule, the logo, the roles, the
|
|
32
|
+
// The pure parts (the day rule, the logo, the roles, the scene's node rewrite) are exported for
|
|
29
33
|
// tests/welcome_boot.mjs; tests/welcome_boot_browser.mjs runs the overlay itself in a real browser.
|
|
30
34
|
(function () {
|
|
31
35
|
'use strict';
|
|
@@ -33,8 +37,7 @@
|
|
|
33
37
|
var DAY_KEY = 'hall-welcome-day:';
|
|
34
38
|
var SIGNIN_KEY = 'hall-welcome-signin';
|
|
35
39
|
var SIGNIN_HREF = '/api/bongos/auth/web/start';
|
|
36
|
-
var
|
|
37
|
-
var ART_WAIT = 1500; // the veil lifts once the painting is in, or after this long
|
|
40
|
+
var ART_WAIT = 1500; // the veil lifts once the scene is painted, or after this long
|
|
38
41
|
var RUN_MS = 3400; // the city powering up before the greeting
|
|
39
42
|
var LEAVE_MS = 2400; // the way down into the hall
|
|
40
43
|
|
|
@@ -46,9 +49,9 @@
|
|
|
46
49
|
var h = now.getHours();
|
|
47
50
|
return h < 12 ? 'Good morning' : h < 18 ? 'Good afternoon' : 'Good evening';
|
|
48
51
|
}
|
|
49
|
-
// Day is 06:00 to 17:59 local: a white veil and
|
|
52
|
+
// Day is 06:00 to 17:59 local: a white veil and dark ink. Night: a black veil and pale ink.
|
|
53
|
+
// (The scene's own colours follow the hour more finely: sky-time.js.)
|
|
50
54
|
function timeOfDay(now) { var h = now.getHours(); return h >= 6 && h < 18 ? 'day' : 'night'; }
|
|
51
|
-
function artFor(when) { return when === 'day' ? ART.day : ART.night; }
|
|
52
55
|
function nameOf(builder) {
|
|
53
56
|
return String((builder && (builder.display_name || builder.github_login)) || 'builder');
|
|
54
57
|
}
|
|
@@ -88,73 +91,61 @@
|
|
|
88
91
|
}
|
|
89
92
|
function roleById(id) { return ROLES.filter(function (r) { return r.id === id; })[0] || null; }
|
|
90
93
|
|
|
91
|
-
// ---------- the
|
|
92
|
-
//
|
|
93
|
-
// the
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
var TOWERS = [
|
|
97
|
-
{ x: 140, y: 168, w: 62, h: 118, glow: '#60a5fa' },
|
|
98
|
-
{ x: 280, y: 150, w: 58, h: 200, glow: '#22d3ee' },
|
|
99
|
-
{ x: 184, y: 296, w: 100, h: 196, glow: 'var(--accent)' },
|
|
100
|
-
{ x: 636, y: 222, w: 42, h: 140, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
|
|
101
|
-
{ x: 702, y: 212, w: 84, h: 190, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
|
|
102
|
-
{ x: 2, y: 196, w: 52, h: 150, glow: 'color-mix(in srgb, var(--sea) 55%, #ffffff)' },
|
|
103
|
-
{ x: 910, y: 410, w: 110, h: 160, glow: '#22d3ee' },
|
|
104
|
-
];
|
|
105
|
-
// The hover-car lanes on the right (the masts' blinking lights went in task 1005087).
|
|
106
|
-
var LANES = [
|
|
107
|
-
{ x1: 770, y1: 350, x2: 1040, y2: 362, dur: 5.5 },
|
|
108
|
-
{ x1: 1040, y1: 340, x2: 760, y2: 352, dur: 7 },
|
|
109
|
-
{ x1: 820, y1: 258, x2: 1040, y2: 254, dur: 6.5 },
|
|
110
|
-
];
|
|
111
|
-
var MOON = [665, 82];
|
|
112
|
-
function seeded(s) { return function () { s = (s * 16807) % 2147483647; return (s - 1) / 2147483646; }; }
|
|
113
|
-
// Where each data pulse climbs: a vertical run inside a tower, bottom to top. Pure, so it is testable.
|
|
114
|
-
function pulseRuns(towers, perTower, seed) {
|
|
115
|
-
var rnd = seeded(seed || 7), out = [];
|
|
116
|
-
towers.forEach(function (t, ti) {
|
|
117
|
-
for (var i = 0; i < perTower; i++) {
|
|
118
|
-
var x = Math.round((t.x + 6 + rnd() * (t.w - 12)) * 10) / 10;
|
|
119
|
-
var y1 = Math.round((t.y + t.h * (0.75 + rnd() * 0.25)) * 10) / 10;
|
|
120
|
-
var y0 = Math.round((t.y + t.h * rnd() * 0.3) * 10) / 10;
|
|
121
|
-
out.push({ tower: ti, x: x, y0: y0, y1: y1, glow: t.glow, dur: Math.round((2.2 + rnd() * 2.4) * 100) / 100, delay: Math.round(rnd() * 400) / 100 });
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
|
|
94
|
+
// ---------- the scene: the city in the clouds (task 1005308) ----------
|
|
95
|
+
// Home's own painters draw it (window.OTBHomeCityDraw + window.GovCity), from an empty week: the
|
|
96
|
+
// Cloud Bongos core and the museum, standing in cloud, in the hour's --sky-now-* tokens. It is a
|
|
97
|
+
// backdrop, so every door the city carries on Home (a link, a tooltip, a goal) becomes a plain
|
|
98
|
+
// group, nothing in it takes focus, and its ids are prefixed so they never meet Home's own city's.
|
|
127
99
|
var NS = 'http://www.w3.org/2000/svg';
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
var
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
// data pulses climbing the circuit towers (the whole set brightens as the city powers up)
|
|
145
|
-
var pulses = el('g', { 'class': 'wb-pulses' }, svg);
|
|
146
|
-
pulseRuns(TOWERS, 3, 7).forEach(function (r) {
|
|
147
|
-
el('line', { 'class': 'wb-pulse', x1: r.x, y1: r.y1, x2: r.x, y2: r.y0, pathLength: 100, stroke: night ? r.glow : '#ffffff',
|
|
148
|
-
style: 'color:' + r.glow + ';animation-duration:' + r.dur + 's;animation-delay:' + (-r.delay) + 's' }, pulses);
|
|
149
|
-
});
|
|
150
|
-
// hover cars, each a light with a short trail, crossing on its lane
|
|
151
|
-
LANES.forEach(function (l, i) {
|
|
152
|
-
var g = el('g', { 'class': 'wb-car', style: '--wb-dx:' + (l.x2 - l.x1) + 'px;--wb-dy:' + (l.y2 - l.y1) + 'px;animation-duration:' + l.dur + 's;animation-delay:' + (-i * 1.7) + 's' }, svg);
|
|
153
|
-
var dir = l.x2 > l.x1 ? -1 : 1;
|
|
154
|
-
el('line', { x1: l.x1, y1: l.y1, x2: l.x1 + dir * 26, y2: l.y1, stroke: night ? 'rgba(125,211,252,.55)' : 'rgba(255,255,255,.7)', 'stroke-width': 2, 'stroke-linecap': 'round' }, g);
|
|
155
|
-
el('circle', { cx: l.x1, cy: l.y1, r: 2.4, fill: night ? '#e0f2fe' : '#ffffff' }, g);
|
|
100
|
+
var SCENE_ID = 'cs-';
|
|
101
|
+
var DOOR_ATTRS = /^(href|tabindex|aria-label|data-tip|data-tip-body|data-goal|data-group|data-building|data-craft-door)$/;
|
|
102
|
+
var QUIET_RE = /\bhc-quiet\b/g, SPACES_RE = /\s+/g, URL_REF_RE = /url\(#/g;
|
|
103
|
+
// One shape node rewritten for the backdrop: { tag, attrs, kids, text } -> the same, door-free. Pure.
|
|
104
|
+
function sceneShape(n) {
|
|
105
|
+
var attrs = {};
|
|
106
|
+
Object.keys(n.attrs || {}).forEach(function (k) {
|
|
107
|
+
if (DOOR_ATTRS.test(k)) return;
|
|
108
|
+
var v = String(n.attrs[k]);
|
|
109
|
+
if (k === 'id') v = SCENE_ID + v;
|
|
110
|
+
else if (k === 'class') v = v.replace(QUIET_RE, '').replace(SPACES_RE, ' ').trim();
|
|
111
|
+
else v = v.replace(URL_REF_RE, 'url(#' + SCENE_ID);
|
|
112
|
+
if (k === 'class' && !v) return;
|
|
113
|
+
attrs[k] = v;
|
|
156
114
|
});
|
|
157
|
-
return
|
|
115
|
+
return { tag: n.tag === 'a' ? 'g' : n.tag, attrs: attrs, kids: (n.kids || []).map(sceneShape), text: n.text == null ? null : n.text };
|
|
116
|
+
}
|
|
117
|
+
// The scene as shapes, or null when the painters are not on the page. Pure given the painters.
|
|
118
|
+
function sceneShapes(Draw, Gov) {
|
|
119
|
+
if (!Draw || typeof Draw.citySvg !== 'function' || typeof Draw.cityModel !== 'function') return null;
|
|
120
|
+
var out = Draw.citySvg(Draw.cityModel({ weeks: [{}] }), 0, { gov: Gov || null });
|
|
121
|
+
return { viewBox: out.viewBox, nodes: out.nodes.map(sceneShape) };
|
|
122
|
+
}
|
|
123
|
+
function build(n) {
|
|
124
|
+
var e = document.createElementNS(NS, n.tag);
|
|
125
|
+
Object.keys(n.attrs).forEach(function (k) { e.setAttribute(k, n.attrs[k]); });
|
|
126
|
+
if (n.text != null) e.textContent = n.text;
|
|
127
|
+
n.kids.forEach(function (c) { e.appendChild(build(c)); });
|
|
128
|
+
return e;
|
|
129
|
+
}
|
|
130
|
+
// Paint the city into host (it covers it; the host's class gives it the hour's sky). True when
|
|
131
|
+
// the city is drawn; false leaves the sky alone, which is still the right picture.
|
|
132
|
+
function paintScene(host) {
|
|
133
|
+
if (!host || typeof document === 'undefined') return false;
|
|
134
|
+
host.classList.add('cloud-scene');
|
|
135
|
+
var sky = window.OTBSkyTime;
|
|
136
|
+
if (sky && typeof sky.apply === 'function') { try { sky.apply(document); } catch (_) { /* the tokens' night default stands */ } }
|
|
137
|
+
var shapes;
|
|
138
|
+
try { shapes = sceneShapes(window.OTBHomeCityDraw, window.GovCity); } catch (_) { shapes = null; }
|
|
139
|
+
if (!shapes) return false;
|
|
140
|
+
var svg = document.createElementNS(NS, 'svg');
|
|
141
|
+
svg.setAttribute('class', 'cloud-scene__svg');
|
|
142
|
+
svg.setAttribute('viewBox', shapes.viewBox);
|
|
143
|
+
svg.setAttribute('preserveAspectRatio', 'xMidYMax slice');
|
|
144
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
145
|
+
svg.setAttribute('focusable', 'false');
|
|
146
|
+
shapes.nodes.forEach(function (n) { svg.appendChild(build(n)); });
|
|
147
|
+
host.appendChild(svg);
|
|
148
|
+
return true;
|
|
158
149
|
}
|
|
159
150
|
|
|
160
151
|
// ---------- the overlay ----------
|
|
@@ -170,10 +161,7 @@
|
|
|
170
161
|
|
|
171
162
|
function bannerTag(builder) { return builder ? 'div' : 'header'; }
|
|
172
163
|
function overlayHtml(builder, now, when) {
|
|
173
|
-
return '<div class="wb__art" aria-hidden="true"><div class="wb__pan">' +
|
|
174
|
-
'<img class="wb__img" alt="" src="' + artFor(when) + '" decoding="async">' +
|
|
175
|
-
'<div class="wb__fx" id="wb-fx"></div>' +
|
|
176
|
-
'</div></div>' +
|
|
164
|
+
return '<div class="wb__art" aria-hidden="true"><div class="wb__pan" id="wb-clouds"></div></div>' +
|
|
177
165
|
'<div class="wb__cloud wb__cloud--a" aria-hidden="true"></div><div class="wb__cloud wb__cloud--b" aria-hidden="true"></div>' +
|
|
178
166
|
// For a visitor the welcome IS the page (the shell draws no header for them), so its logo
|
|
179
167
|
// banner is the page's one <header>; over a signed-in hall the shell's header stands, and the
|
|
@@ -224,7 +212,7 @@
|
|
|
224
212
|
if (first && first.focus) first.focus();
|
|
225
213
|
}
|
|
226
214
|
|
|
227
|
-
// The veil lifts once the
|
|
215
|
+
// The veil lifts once the scene is painted (or after ART_WAIT), then the city powers up.
|
|
228
216
|
function begin() {
|
|
229
217
|
if (!root || root.classList.contains('wb--lit')) return;
|
|
230
218
|
root.classList.add('wb--lit');
|
|
@@ -324,13 +312,9 @@
|
|
|
324
312
|
document.addEventListener('keydown', onKey, true);
|
|
325
313
|
document.body.appendChild(root);
|
|
326
314
|
document.documentElement.classList.add('wb-open');
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
var go1 = function () { if (window.requestAnimationFrame) window.requestAnimationFrame(function () { window.requestAnimationFrame(begin); }); else begin(); };
|
|
331
|
-
img.addEventListener('load', go1);
|
|
332
|
-
img.addEventListener('error', function () { img.hidden = true; if (root) root.classList.add('wb--bare'); go1(); });
|
|
333
|
-
if (img.complete && img.naturalWidth) go1();
|
|
315
|
+
// No city painters on the page: the hour's sky stands alone; the welcome goes on regardless.
|
|
316
|
+
if (!paintScene(root.querySelector('#wb-clouds'))) root.classList.add('wb--bare');
|
|
317
|
+
if (window.requestAnimationFrame) window.requestAnimationFrame(function () { window.requestAnimationFrame(begin); });
|
|
334
318
|
at(ART_WAIT, begin);
|
|
335
319
|
var skip = root.querySelector('[data-wb-skip]');
|
|
336
320
|
if (skip && skip.focus) skip.focus();
|
|
@@ -378,7 +362,7 @@
|
|
|
378
362
|
// The hall's front opens on Game or Classic (task 1005219): the pick is kept, the visitor
|
|
379
363
|
// signs in, and comes back to that menu (shell-game.js). Without that file, the welcome.
|
|
380
364
|
if (canChoose && game.start({
|
|
381
|
-
name: projectName(window.__BRANDING__),
|
|
365
|
+
name: projectName(window.__BRANDING__), scene: paintScene,
|
|
382
366
|
signIn: function () { markSigningIn(); go(SIGNIN_HREF); },
|
|
383
367
|
})) return true;
|
|
384
368
|
show(null);
|
|
@@ -398,11 +382,11 @@
|
|
|
398
382
|
shown = true;
|
|
399
383
|
var c = craft();
|
|
400
384
|
// A fresh sign-in, or a new visit to Home, opens on the game's start screen instead (tasks
|
|
401
|
-
// 1005143, 1005219; shell-game.js): the same
|
|
385
|
+
// 1005143, 1005219; shell-game.js): the same city behind it, and the craft picker asked once
|
|
402
386
|
// the world is showing. Classic picked before signing in: no screen, straight to the hall.
|
|
403
387
|
if ((fresh || ask) && canChoose) {
|
|
404
388
|
if (game.start({
|
|
405
|
-
name: projectName(window.__BRANDING__),
|
|
389
|
+
name: projectName(window.__BRANDING__), scene: paintScene,
|
|
406
390
|
done: function () { if (c && typeof c.onAccess === 'function') c.onAccess(access, builder); },
|
|
407
391
|
})) return true;
|
|
408
392
|
if (fresh) return false;
|
|
@@ -413,9 +397,9 @@
|
|
|
413
397
|
}
|
|
414
398
|
|
|
415
399
|
var api = {
|
|
416
|
-
DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES,
|
|
400
|
+
DAY_KEY: DAY_KEY, SIGNIN_KEY: SIGNIN_KEY, ROLES: ROLES, SCENE_ID: SCENE_ID,
|
|
417
401
|
localDay: localDay, firstVisitToday: firstVisitToday, greetingFor: greetingFor, nameOf: nameOf, timeOfDay: timeOfDay,
|
|
418
|
-
|
|
402
|
+
projectName: projectName, logoHtml: logoHtml, BUILT_ON: BUILT_ON, rolesFor: rolesFor, sceneShape: sceneShape, sceneShapes: sceneShapes, paintScene: paintScene,
|
|
419
403
|
onAccess: onAccess, finish: finish, close: close,
|
|
420
404
|
};
|
|
421
405
|
if (typeof window !== 'undefined') { window.OTBWelcome = api; watchSignInLinks(); }
|
|
@@ -4,23 +4,23 @@ Covers: Home (`shell-game.js` `start()`, `shell-game.css`, the hand-off in `welc
|
|
|
4
4
|
|
|
5
5
|
## What it settled
|
|
6
6
|
|
|
7
|
-
- **A fresh sign-in opens on the start screen, on Home.** Home is where a sign-in lands (`postLoginLanding`), and `welcome-boot.js` already knows a fresh sign-in from a return visit (the flag the sign-in click leaves). On one, `OTBWelcome.onAccess` calls `OTBShellGame.start({ name,
|
|
7
|
+
- **A fresh sign-in opens on the start screen, on Home.** Home is where a sign-in lands (`postLoginLanding`), and `welcome-boot.js` already knows a fresh sign-in from a return visit (the flag the sign-in click leaves). On one, `OTBWelcome.onAccess` calls `OTBShellGame.start({ name, scene, done })` in place of its own greeting. The day's first visit without a sign-in keeps the welcome; a signed-out visitor keeps the welcome. A sign-in that returns to a deep link or another page goes straight there, as the welcome always did. `shell.js` is untouched.
|
|
8
8
|
- **Five choices, one column: Start, Options, Achievements, Extras, Quit.** Start is the one filled pill. Options is a link to `/settings`, which IS the one options menu since task 1005144 (`records/options-menu.md`); Achievements is `/profile`. Extras opens three links under the menu (the thinking sky, the atlas, the docs). Quit calls `window.close()`; a browser only closes a tab a script opened, so when the tab is still there 300ms later the screen says "Close it to quit" instead of doing nothing.
|
|
9
9
|
- **Start turns game mode on** (`set(true)`), then shows the load screen. Nothing else in the hall's interface sets it.
|
|
10
|
-
- **The load screen is the still, a tip, and the city building up.** The still is the
|
|
10
|
+
- **The load screen is the still, a tip, and the city building up.** The still is the city in the clouds in the hour's sky, painted by `welcome-boot.js` `paintScene`, which is handed in as `scene` (this file draws no city of its own for it; task 1005308). In a card of the hall's own tokens, the project city is drawn by `home-city-draw.js` `citySvg` from the one bounded read Home already uses (`GET /home/city?weeks=10`), at 31 stops from the oldest week to now, about three seconds, with "The city, N weeks ago" under it. Each stop is a whole drawing set as state; nothing animates in JavaScript. With reduced motion it is the finished city at once. With no city to draw (the read failed, the scripts are missing) the still and the tip stand for 1.4s. Either way the world opens.
|
|
11
11
|
- **Tips rotate** (`localStorage 'hall-game-tip'` counts them), so each load shows the next one. A tip must be true of the hall today; the first one is the Escape rule from `game-mode.md` (rewritten by task 1005144, and again by task 1005250: Escape opens the game menu).
|
|
12
12
|
- **The craft picker waits for the world.** While the start screen is up the shell asks no picker (`onAccess` returned true). `done` runs once the screen is gone and asks `OTBCraftMode.onAccess` then.
|
|
13
13
|
- **Keys.** While either screen is up it keeps every key: the arrows and Tab walk the menu, Escape folds Extras away and does nothing else, so the shell's Escape cannot flip the bars underneath. Once the world shows, the last Escape opens the game menu (task 1005250; the options menu until then); the screen's capture listener calls `preventDefault`, so the menu never opens from under it.
|
|
14
|
-
- **The look is Helm (task 1005192, [ADR 0402](../../../docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md)).** The start screen is one full-height night panel over the
|
|
15
|
-
- **The load screen is the same panel, centred** over the
|
|
14
|
+
- **The look is Helm (task 1005192, [ADR 0402](../../../docs/adr/0402-helm-is-the-halls-look-and-the-hall-is-dark-only.md)).** The start screen is one full-height night panel over the city in the clouds (`--night-glass`, an ice hairline left and right at `--line-strong`), `--helm-panel-w` wide. On a desk it stands off the left edge by `--helm-inset`; that token is a `clamp`, so on a phone the panel is the whole width and the sheet still has no `@media`. In it: the mark (the project's logo when it has one) over the name in the name voice; Start as a full-width accent bar under `--accent-on` that names its key ("Enter", drawn by CSS so the choice's own text stays "Start"); the other four as quiet rows on `--rule-soft` hairlines; and at the foot, over the strong hairline, the tip the load screen will show next (`#gs-line`), with the Quit note under it when the browser keeps the tab. The rows are list rows in a panel, so they are bars, not cut buttons. A row's focus is the inset two-tone ring (`--press-focus`), which is what makes focus visible on the accent bar.
|
|
15
|
+
- **The load screen is the same panel, centred** over the city in the clouds under the hall's `--scrim`: the week in the name voice (`#gs-when`), one segment per week of the history (`#gs-segs`, the shared `.segs` / `.seg`; filled up to the week being shown, counted the way the sentence words it, and hidden from a screen reader because the sentence says the same), the city, and the tip under a hairline. With no city there are no segments.
|
|
16
16
|
- **The literal floor is zero.** Every colour, size, corner, shadow, face and measure in `shell-game.css` is a hall-tier token (`style.css`); `tests/hall_start_screen.mjs` fails on a typed one. The fade into the world is one CSS opacity transition, stopped by the hall's reduced-motion rule.
|
|
17
17
|
|
|
18
18
|
## For the tasks after this one
|
|
19
19
|
|
|
20
20
|
- R04 (task 1005144): point Options at the options menu and move Achievements/Extras targets if those pages lose their interfaces. Escape on the start screen is free for the menu to use.
|
|
21
|
-
- `shell-game.js` is loaded on every hall page but only Home calls `start()`; `shell-game.css` is linked on Home only. A second page that wants the start screen links the sheet and calls `start()` with its own
|
|
21
|
+
- `shell-game.js` is loaded on every hall page but only Home calls `start()`; `shell-game.css` is linked on Home only. A second page that wants the start screen links the sheet and calls `start()` with its own `scene` painter.
|
|
22
22
|
|
|
23
|
-
-
|
|
23
|
+
- Since task 1005308 (owner, 2026-11-14) both screens stand on the city in the clouds from task 1005196; the circuit-city painting is retired everywhere.
|
|
24
24
|
|
|
25
25
|
Tests: `tests/hall_start_screen.mjs` (the hand-off and pure parts in a vm; both screens, the panel's place at desk and phone widths, the segments, Quit, the no-city and reduced-motion paths in a real browser).
|
|
26
26
|
|