@bongos/core 1.21.5 → 1.21.7
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 +152 -72
- package/clients/bongos-client/index.d.ts +1 -1
- package/docs/adr/0361-merges-publish-as-candidates-release-decides-what-others-are-offered.md +1 -0
- package/docs/api/openapi.json +2 -1
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +156 -144
- package/docs/copy-registry.json +286 -158
- package/docs/module-api-changelog.md +4 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +1 -1
- package/docs/onboarding/diagrams/assertions.json +1 -1
- package/docs/page-inventory.json +35 -6
- package/docs/page-readings.json +1056 -1013
- package/modules/autonomy/config-idle.js +192 -0
- package/modules/autonomy/routes/autonomy.js +22 -0
- package/modules/autonomy/runner-health.js +22 -1
- package/modules/hall-ui/public/city-draw.js +438 -0
- package/modules/hall-ui/public/city.css +110 -0
- package/modules/hall-ui/public/city.html +71 -0
- package/modules/hall-ui/public/city.js +177 -0
- package/modules/hall-ui/public/city.states.json +30 -0
- package/modules/hall-ui/public/gate.js +11 -4
- package/modules/hall-ui/public/profile.css +4 -0
- package/modules/hall-ui/public/profile.js +25 -1
- package/modules/hall-ui/public/settings-autobongos.js +19 -1
- package/modules/hall-ui/public/settings.css +26 -0
- package/modules/hall-ui/public/settings.html +8 -0
- package/modules/hall-ui/public/settings.js +43 -3
- package/modules/platform-identity/craft-rollup.js +72 -0
- package/modules/platform-identity/migrations/platform_identity_029_activity_crafts.sql +23 -0
- package/modules/platform-identity/platform-identity.js +12 -6
- package/modules/platform-identity/routes/sso.js +4 -0
- package/modules/platform-identity/tests/platform-identity.mjs +1 -1
- package/modules/provisioning/core-upgrade.js +34 -7
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/seams.js +2 -0
- package/modules/public-landing/public/projects-hall-live.states.json +216 -0
- package/modules/public-landing/public/projects.html +43 -13
- package/modules/public-landing/public/projects.states.json +2 -1
- package/modules/ui-design/kit/fixtures/provisioning-instances-hall-live.json +106 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +64 -0
- package/scripts/gds/autobongos-grade-cap.js +187 -0
- package/scripts/gds/autobongos-loop.js +4 -0
- package/scripts/gds/autobongos-run.js +154 -13
- package/scripts/gds/autobongos-verify.js +211 -14
- package/scripts/gds/provision-core-upgrade.js +14 -2
- package/scripts/gds/provision-repo.js +77 -13
- package/scripts/gds/ship-flow.js +5 -0
- package/scripts/gds/ship-preflight-steps.js +2 -1
- package/scripts/gds/ship.js +10 -0
- package/scripts/gds/update-sweep.js +24 -8
- package/scripts/gds/upgrade-outcome.js +1 -0
- package/src/bongos/auth-admission.js +95 -13
- package/src/bongos/db-kernel.js +1 -1
- package/src/bongos/routes/core-update.js +27 -2
- package/src/bongos/serve-internal.js +14 -0
- package/src/bongos/software-update.js +3 -2
- package/src/module-api.js +1 -1
- package/tests/activity_rollup_order_db.mjs +3 -1
- package/tests/activity_snapshots_db.mjs +2 -0
- package/tests/autobongos_grade_cap.mjs +125 -0
- package/tests/autobongos_loop.mjs +230 -1
- package/tests/autobongos_verify.mjs +239 -3
- package/tests/autonomy_config_idle.mjs +272 -0
- package/tests/core_update_banner.mjs +29 -0
- package/tests/core_upgrade_door.mjs +31 -1
- package/tests/core_upgrade_runner.mjs +20 -0
- package/tests/hall_audit.mjs +10 -1
- package/tests/hall_city.mjs +369 -0
- package/tests/hall_page_gate_map.mjs +5 -0
- package/tests/hub_craft_rollup.mjs +350 -0
- package/tests/profile_rollup_consent.mjs +2 -0
- package/tests/profile_route.mjs +16 -2
- package/tests/project_invite_ui.mjs +65 -7
- package/tests/projects_hub_dns_ready.mjs +7 -1
- package/tests/provision.mjs +78 -0
- package/tests/settings_main_role.mjs +190 -0
- package/tests/software_update.mjs +56 -1
- package/tests/ui_design_kit.mjs +1 -1
- package/tests/update_subscription_engine.mjs +55 -3
- package/tests/wizard_physics_more.mjs +19 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// modules/hall-ui/public/city-draw.js — THE GOVERNOR CITY's drawing, as data
|
|
2
|
+
// (task 1004524, goal 1000125; R02 of the Governor City).
|
|
3
|
+
//
|
|
4
|
+
// WHAT THIS IS. The home view of the reference design, docs/design/mocks/
|
|
5
|
+
// governor-city/Street.dc.html: the isometric kit (isoKit / P(), isoBox,
|
|
6
|
+
// isoGable, isoHip, isoSaw, isoTree, isoLight), the nine civic recipes
|
|
7
|
+
// (isoInstitution), the district work (isoWork), the decision glyphs (glyph)
|
|
8
|
+
// and homeView(), which lays out the 16 x 16 city. Every function below is the
|
|
9
|
+
// prototype's, ported verbatim (same tile maths, same heights, same tones),
|
|
10
|
+
// with ONE change of input: homeView() read the prototype's sample docket and
|
|
11
|
+
// its four hand-placed districts; cityModel() reads the real governor docket
|
|
12
|
+
// (GET /api/bongos/government/docket, task 1004523) for the pins and the open
|
|
13
|
+
// goals with their task rollups (GET /goals, GET /goals/rollups) for the
|
|
14
|
+
// districts. Anything else that differs from the prototype is a defect.
|
|
15
|
+
//
|
|
16
|
+
// PURE. No DOM, no fetch, no clock: the same inputs give the same path list,
|
|
17
|
+
// so tests/hall_city.mjs can pin the model without a browser, and city.js only
|
|
18
|
+
// turns the lists into SVG nodes and spans.
|
|
19
|
+
//
|
|
20
|
+
// THE PALETTE. isoTheme() is the prototype's theme().iso sub-palette for both
|
|
21
|
+
// modes, verbatim. It is drawing data (the faces of a white architectural model
|
|
22
|
+
// on paper in light, dark blocks in the black in dark), the way the sky's
|
|
23
|
+
// canvas carries its own ink channels; the page chrome around the drawing takes
|
|
24
|
+
// the hall's tokens (city.css).
|
|
25
|
+
//
|
|
26
|
+
// REAL DATA, AND WHERE IT HAD TO BEND (each said once, here and in the ship
|
|
27
|
+
// notes):
|
|
28
|
+
// * Districts. The prototype has four hand-placed goal districts (north, east,
|
|
29
|
+
// west, south of the ring road). The city draws one per OPEN goal: the four
|
|
30
|
+
// prototype plots first, then the four empty corners of the 16 x 16 ground,
|
|
31
|
+
// and past eight goals plots are halved along their long side, in the same
|
|
32
|
+
// order, up to sixteen. Goals past sixteen are not drawn (cityModel reports
|
|
33
|
+
// them in `unplaced`).
|
|
34
|
+
// * A building per task. Each task of a goal is one building on its plot
|
|
35
|
+
// (shipped work built as a tower, setback or shed in the prototype's shares,
|
|
36
|
+
// work being built as a scaffold, work not started as a staked slab) until
|
|
37
|
+
// the plot is full. Past that, one building stands for several tasks and
|
|
38
|
+
// grows taller, which is the reference design's own answer to scale
|
|
39
|
+
// (Growth.dc.html: the city fills, then rises).
|
|
40
|
+
// * Review. The prototype's story opens with Review a vacant lot; in this
|
|
41
|
+
// project Review is held (the grader and the testers sign work off), so the
|
|
42
|
+
// held recipe is drawn and the parcels leave through Release.
|
|
43
|
+
// * Pins. A building can hold more matters than the prototype's one; up to
|
|
44
|
+
// three pins stand side by side over it (needs first, the docket's own
|
|
45
|
+
// order), and the line under the button carries the full counts.
|
|
46
|
+
|
|
47
|
+
(function (root) {
|
|
48
|
+
'use strict';
|
|
49
|
+
|
|
50
|
+
// ---------- the iso sub-palette (Street.dc.html theme(mode).iso) ----------
|
|
51
|
+
function isoTheme(mode) {
|
|
52
|
+
if (mode === 'light') {
|
|
53
|
+
return {
|
|
54
|
+
civic: { t: '#fbfafc', l: '#ecebee', r: '#d9d8dd', s: 'rgba(23,24,26,0.34)' },
|
|
55
|
+
work: { t: '#f4f3f6', l: '#e6e5e9', r: '#d4d3d8', s: 'rgba(23,24,26,0.24)' },
|
|
56
|
+
plinth: { t: '#efeef1', l: '#e2e1e5', r: '#cfced3', s: 'rgba(23,24,26,0.28)' },
|
|
57
|
+
roof: { t: '#e9e8ec', l: '#dddce1', r: '#cbcad0', s: 'rgba(23,24,26,0.34)' },
|
|
58
|
+
glass: { t: 'rgba(91,111,208,0.10)', l: 'rgba(91,111,208,0.14)', r: 'rgba(91,111,208,0.22)', s: 'rgba(60,80,180,0.50)' },
|
|
59
|
+
scaffold: { t: 'rgba(23,24,26,0.03)', l: 'rgba(23,24,26,0.02)', r: 'rgba(23,24,26,0.02)', s: 'rgba(23,24,26,0.45)', dash: '3 3' },
|
|
60
|
+
crate: { t: 'rgba(23,24,26,0.06)', l: 'rgba(23,24,26,0.04)', r: 'rgba(23,24,26,0.07)', s: 'rgba(23,24,26,0.55)', dash: '3 2' },
|
|
61
|
+
sign: { t: '#e2e1e5', l: '#e2e1e5', r: '#cfced3', s: 'rgba(23,24,26,0.45)' },
|
|
62
|
+
ground: 'rgba(23,24,26,0.025)', groundLine: 'rgba(23,24,26,0.10)', road: 'rgba(23,24,26,0.05)', lane: 'rgba(23,24,26,0.22)',
|
|
63
|
+
plaza: 'rgba(23,24,26,0.05)', plazaLine: 'rgba(23,24,26,0.16)', plot: 'rgba(23,24,26,0.035)', plotLine: 'rgba(23,24,26,0.14)', grid: 'rgba(23,24,26,0.06)',
|
|
64
|
+
detail: 'rgba(23,24,26,0.18)', wc: 'rgba(23,24,26,0.16)', wcLit: 'rgba(214,140,80,0.60)', door: '#cfced3', beacon: '#fb9569',
|
|
65
|
+
vacantFill: 'rgba(23,24,26,0.02)', vacantLine: 'rgba(23,24,26,0.45)', tree: 'rgba(91,111,208,0.22)', treeLine: 'rgba(60,80,180,0.45)',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
civic: { t: '#3b3b41', l: '#25252a', r: '#18181c', s: 'rgba(255,255,255,0.14)' },
|
|
70
|
+
work: { t: '#2d2d32', l: '#19191c', r: '#0f0f12', s: 'rgba(255,255,255,0.12)' },
|
|
71
|
+
plinth: { t: '#27272c', l: '#141417', r: '#0b0b0d', s: 'rgba(255,255,255,0.10)' },
|
|
72
|
+
roof: { t: '#38383e', l: '#2b2b31', r: '#1d1d22', s: 'rgba(255,255,255,0.14)' },
|
|
73
|
+
glass: { t: 'rgba(150,170,255,0.22)', l: 'rgba(120,140,230,0.17)', r: 'rgba(100,120,210,0.12)', s: 'rgba(170,190,255,0.42)' },
|
|
74
|
+
scaffold: { t: 'rgba(255,255,255,0.03)', l: 'rgba(255,255,255,0.02)', r: 'rgba(255,255,255,0.015)', s: 'rgba(255,255,255,0.45)', dash: '3 3' },
|
|
75
|
+
crate: { t: 'rgba(255,255,255,0.07)', l: 'rgba(255,255,255,0.04)', r: 'rgba(255,255,255,0.03)', s: 'rgba(255,255,255,0.55)', dash: '3 2' },
|
|
76
|
+
sign: { t: '#202024', l: '#202024', r: '#141417', s: 'rgba(255,255,255,0.4)' },
|
|
77
|
+
ground: 'rgba(255,255,255,0.012)', groundLine: 'rgba(255,255,255,0.05)', road: 'rgba(255,255,255,0.025)', lane: 'rgba(255,255,255,0.12)',
|
|
78
|
+
plaza: 'rgba(255,255,255,0.045)', plazaLine: 'rgba(255,255,255,0.16)', plot: 'rgba(255,255,255,0.03)', plotLine: 'rgba(255,255,255,0.13)', grid: 'rgba(255,255,255,0.045)',
|
|
79
|
+
detail: 'rgba(255,255,255,0.10)', wc: 'rgba(255,255,255,0.10)', wcLit: 'rgba(255,214,170,0.80)', door: '#050506', beacon: '#fff4e6',
|
|
80
|
+
vacantFill: 'rgba(255,255,255,0.02)', vacantLine: 'rgba(255,255,255,0.4)', tree: 'rgba(91,111,208,0.32)', treeLine: 'rgba(140,155,230,0.45)',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------- the prototype's primitives ----------
|
|
85
|
+
function push(out, d, f, s, dash, w) { out.push({ d: d, f: f, s: s, dash: dash || 'none', w: w || 1 }); }
|
|
86
|
+
function ellipse(cx, cy, rx, ry) { return 'M' + (cx - rx) + ' ' + cy + 'a' + rx + ' ' + ry + ' 0 1 0 ' + (2 * rx) + ' 0a' + rx + ' ' + ry + ' 0 1 0 ' + (-2 * rx) + ' 0Z'; }
|
|
87
|
+
function rnd(n) {
|
|
88
|
+
const s = Math.sin(n * 12.9898 + 78.233) * 43758.5453;
|
|
89
|
+
return s - Math.floor(s);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isoKit(TW, TH, OX, OY, S, I, lit) {
|
|
93
|
+
const r1 = (n) => Math.round(n * 10) / 10;
|
|
94
|
+
const P = (x, y, z) => [OX + (x - y) * TW / 2, OY + (x + y) * TH / 2 - (z || 0) * S];
|
|
95
|
+
const poly = (pts) => 'M' + pts.map((p) => r1(p[0]) + ' ' + r1(p[1])).join('L') + 'Z';
|
|
96
|
+
const seg = (a, b) => 'M' + r1(a[0]) + ' ' + r1(a[1]) + 'L' + r1(b[0]) + ' ' + r1(b[1]);
|
|
97
|
+
return { P: P, poly: poly, seg: seg, S: S, I: I, wc: lit ? I.wcLit : I.wc };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isoBox(out, k, x0, y0, x1, y1, z0, h, tone, o) {
|
|
101
|
+
o = o || {};
|
|
102
|
+
const P = k.P, z1 = z0 + h, dash = tone.dash || 'none';
|
|
103
|
+
push(out, k.poly([P(x0, y1, z1), P(x1, y1, z1), P(x1, y1, z0), P(x0, y1, z0)]), tone.l, tone.s, dash);
|
|
104
|
+
push(out, k.poly([P(x1, y1, z1), P(x1, y0, z1), P(x1, y0, z0), P(x1, y1, z0)]), tone.r, tone.s, dash);
|
|
105
|
+
push(out, k.poly([P(x0, y0, z1), P(x1, y0, z1), P(x1, y1, z1), P(x0, y1, z1)]), tone.t, tone.s, dash);
|
|
106
|
+
let det = '';
|
|
107
|
+
const ix = Math.min(0.08, (x1 - x0) / 6), iy = Math.min(0.08, (y1 - y0) / 6);
|
|
108
|
+
if (o.rows || o.grid) for (let z = z0 + 8; z < z1 - 5; z += 8) det += k.seg(P(x0 + ix, y1, z), P(x1 - ix, y1, z)) + k.seg(P(x1, y1 - iy, z), P(x1, y0 + iy, z));
|
|
109
|
+
if (o.fins || o.grid) {
|
|
110
|
+
const n = o.n || 6;
|
|
111
|
+
for (let i = 1; i < n; i++) { const fx = x0 + (x1 - x0) * i / n, fy = y0 + (y1 - y0) * i / n; det += k.seg(P(fx, y1, z0 + 2), P(fx, y1, z1 - 2)) + k.seg(P(x1, fy, z0 + 2), P(x1, fy, z1 - 2)); }
|
|
112
|
+
}
|
|
113
|
+
if (det) push(out, det, 'none', o.wc || (o.rows ? k.wc : k.I.detail), o.rows && !o.grid ? '4 3' : 'none');
|
|
114
|
+
if (o.door) {
|
|
115
|
+
const xm = (x0 + x1) / 2, dw = Math.min(0.12, (x1 - x0) / 5), dh = Math.min(14, h * 0.45);
|
|
116
|
+
push(out, k.poly([P(xm - dw, y1, z0), P(xm + dw, y1, z0), P(xm + dw, y1, z0 + dh), P(xm - dw, y1, z0 + dh)]), k.I.door, tone.s);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function isoGable(out, k, x0, y0, x1, y1, z, hr, tone) {
|
|
121
|
+
const P = k.P, ym = (y0 + y1) / 2;
|
|
122
|
+
push(out, k.poly([P(x0, y0, z), P(x1, y0, z), P(x1, ym, z + hr), P(x0, ym, z + hr)]), tone.r, tone.s);
|
|
123
|
+
push(out, k.poly([P(x0, ym, z + hr), P(x1, ym, z + hr), P(x1, y1, z), P(x0, y1, z)]), tone.t, tone.s);
|
|
124
|
+
push(out, k.poly([P(x1, y0, z), P(x1, ym, z + hr), P(x1, y1, z)]), tone.l, tone.s);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isoHip(out, k, x0, y0, x1, y1, z, hr, tone) {
|
|
128
|
+
const P = k.P, a = P((x0 + x1) / 2, (y0 + y1) / 2, z + hr);
|
|
129
|
+
push(out, k.poly([P(x0, y0, z), P(x1, y0, z), a]), tone.r, tone.s);
|
|
130
|
+
push(out, k.poly([P(x0, y0, z), P(x0, y1, z), a]), tone.l, tone.s);
|
|
131
|
+
push(out, k.poly([P(x0, y1, z), P(x1, y1, z), a]), tone.t, tone.s);
|
|
132
|
+
push(out, k.poly([P(x1, y1, z), P(x1, y0, z), a]), tone.l, tone.s);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isoSaw(out, k, x0, y0, x1, y1, z, hr, n, tone, glass) {
|
|
136
|
+
const P = k.P, step = (x1 - x0) / n;
|
|
137
|
+
for (let i = 0; i < n; i++) {
|
|
138
|
+
const xa = x0 + step * i, xb = xa + step;
|
|
139
|
+
push(out, k.poly([P(xa, y0, z), P(xb, y0, z + hr), P(xb, y1, z + hr), P(xa, y1, z)]), tone.t, tone.s);
|
|
140
|
+
push(out, k.poly([P(xa, y1, z), P(xb, y1, z + hr), P(xb, y1, z)]), tone.l, tone.s);
|
|
141
|
+
push(out, k.poly([P(xb, y0, z), P(xb, y0, z + hr), P(xb, y1, z + hr), P(xb, y1, z)]), glass.r, glass.s);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isoTree(out, k, x, y, size) {
|
|
146
|
+
const P = k.P, c = P(x, y, size * 1.15);
|
|
147
|
+
push(out, k.seg(P(x, y, 0), P(x, y, size * 0.9)), 'none', k.I.treeLine);
|
|
148
|
+
push(out, ellipse(Math.round(c[0]), Math.round(c[1]), Math.round(size * 0.42 * k.S * 10) / 10, Math.round(size * 0.42 * k.S * 10) / 10), k.I.tree, k.I.treeLine);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isoLight(out, k, x, y, z, r) {
|
|
152
|
+
const c = k.P(x, y, z);
|
|
153
|
+
push(out, ellipse(Math.round(c[0]), Math.round(c[1]), r * 2.6, r * 2.6), 'rgba(251,149,105,0.10)', 'none');
|
|
154
|
+
push(out, ellipse(Math.round(c[0]), Math.round(c[1]), r, r), k.I.beacon, 'none');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function isoInstitution(out, k, kind, ox, oy, st) {
|
|
158
|
+
const I = k.I;
|
|
159
|
+
const b = (x0, y0, x1, y1, z, h, t, o) => isoBox(out, k, ox + x0, oy + y0, ox + x1, oy + y1, z, h, t, o);
|
|
160
|
+
if (kind === 'charter') {
|
|
161
|
+
b(0.15, 0.15, 1.85, 1.85, 0, 6, I.plinth); b(0.3, 0.3, 1.7, 1.7, 6, 46, I.civic, { rows: true }); b(0.45, 0.45, 1.55, 1.55, 52, 40, I.civic, { rows: true });
|
|
162
|
+
b(0.6, 0.6, 1.4, 1.4, 92, 34, I.civic, { fins: true, n: 5 }); b(0.75, 0.75, 1.25, 1.25, 126, 18, I.glass, { grid: true, n: 3 }); isoLight(out, k, ox + 1, oy + 1, 152, 3.2);
|
|
163
|
+
} else if (kind === 'rules') {
|
|
164
|
+
b(0.1, 0.5, 1.9, 1.5, 0, 4, I.plinth); b(0.22, 0.62, 1.78, 1.38, 4, 28, I.civic, { fins: true, n: 12, wc: st.fixes ? I.wcLit : null }); b(0.12, 0.52, 1.88, 1.48, 32, 4, I.roof);
|
|
165
|
+
} else if (kind === 'treasury') {
|
|
166
|
+
b(0.22, 0.22, 1.78, 1.78, 0, 8, I.plinth); b(0.4, 0.4, 1.6, 1.6, 8, 44, I.civic, { door: true }); b(0.33, 0.33, 1.67, 1.67, 52, 5, I.roof); b(0.62, 0.62, 1.38, 1.38, 57, 9, I.civic);
|
|
167
|
+
} else if (kind === 'planning') {
|
|
168
|
+
b(0.3, 0.3, 1.7, 1.7, 0, 38, I.civic, { rows: true, door: true }); isoSaw(out, k, ox + 0.3, oy + 0.3, ox + 1.7, oy + 1.7, 38, 13, 3, I.roof, I.glass);
|
|
169
|
+
} else if (kind === 'review') {
|
|
170
|
+
b(0.3, 0.3, 1.7, 1.7, 0, 14, I.civic, { door: true }); b(0.38, 0.38, 1.62, 1.62, 14, 36, I.glass, { grid: true, n: 5 }); b(0.34, 0.34, 1.66, 1.66, 50, 3, I.roof);
|
|
171
|
+
} else if (kind === 'release') {
|
|
172
|
+
b(0.3, 0.75, 0.75, 1.25, 0, 66, I.civic, { rows: true }); isoLight(out, k, ox + 0.52, oy + 1, 72, 2.6);
|
|
173
|
+
b(0.75, 0.85, 1.25, 1.15, 44, 12, I.civic); b(1.25, 0.75, 1.7, 1.25, 0, 66, I.civic, { rows: true }); isoLight(out, k, ox + 1.47, oy + 1, 72, 2.6);
|
|
174
|
+
} else if (kind === 'people') {
|
|
175
|
+
b(0.2, 0.2, 0.85, 0.8, 0, 16, I.civic, { door: true }); isoGable(out, k, ox + 0.2, oy + 0.2, ox + 0.85, oy + 0.8, 16, 11, I.roof);
|
|
176
|
+
b(1.1, 0.3, 1.75, 0.9, 0, 20, I.civic, { door: true }); isoGable(out, k, ox + 1.1, oy + 0.3, ox + 1.75, oy + 0.9, 20, 11, I.roof);
|
|
177
|
+
isoTree(out, k, ox + 1.45, oy + 1.45, 12);
|
|
178
|
+
b(0.35, 1.1, 1.0, 1.7, 0, 14, I.civic, { door: true }); isoGable(out, k, ox + 0.35, oy + 1.1, ox + 1.0, oy + 1.7, 14, 10, I.roof);
|
|
179
|
+
} else if (kind === 'disputes') {
|
|
180
|
+
b(0.25, 0.7, 0.75, 1.3, 0, 22, I.civic, { rows: true }); b(0.75, 0.55, 1.25, 1.45, 0, 48, I.civic, { fins: true, n: 4, door: true });
|
|
181
|
+
isoHip(out, k, ox + 0.75, oy + 0.55, ox + 1.25, oy + 1.45, 48, 12, I.roof); b(1.25, 0.7, 1.75, 1.3, 0, 22, I.civic, { rows: true });
|
|
182
|
+
} else if (kind === 'commons') {
|
|
183
|
+
b(0.85, 0.85, 1.15, 1.15, 0, 3, I.plinth);
|
|
184
|
+
isoTree(out, k, ox + 0.45, oy + 0.5, 14); isoTree(out, k, ox + 1.5, oy + 0.45, 12); isoTree(out, k, ox + 0.5, oy + 1.5, 12); isoTree(out, k, ox + 1.5, oy + 1.55, 15);
|
|
185
|
+
} else if (kind === 'automation') {
|
|
186
|
+
b(0.25, 0.3, 1.75, 1.6, 0, 6, I.plinth); b(0.35, 0.4, 1.65, 1.5, 6, 30, I.civic, { rows: true, door: true });
|
|
187
|
+
b(1.15, 0.55, 1.45, 0.85, 36, 42, I.civic); isoLight(out, k, ox + 1.3, oy + 0.7, 84, 2.8);
|
|
188
|
+
b(0.5, 0.9, 1.0, 1.4, 36, 8, I.roof, { fins: true, n: 4 });
|
|
189
|
+
} else if (kind === 'vacant') {
|
|
190
|
+
const P = k.P;
|
|
191
|
+
push(out, k.poly([P(ox + 0.2, oy + 0.2, 0), P(ox + 1.8, oy + 0.2, 0), P(ox + 1.8, oy + 1.8, 0), P(ox + 0.2, oy + 1.8, 0)]), I.vacantFill, I.vacantLine, '4 4');
|
|
192
|
+
push(out, k.seg(P(ox + 0.45, oy + 1.55, 0), P(ox + 0.45, oy + 1.55, 30)), 'none', I.vacantLine);
|
|
193
|
+
b(0.3, 1.52, 0.6, 1.58, 22, 12, I.sign);
|
|
194
|
+
b(1.0, 0.7, 1.35, 1.05, 0, 9, I.crate); b(1.05, 0.75, 1.3, 1.0, 9, 8, I.crate); b(1.25, 1.05, 1.55, 1.35, 0, 9, I.crate);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// The prototype's five work kinds, plus `slab`: a task not started yet, drawn
|
|
199
|
+
// as the staked plot the README names for backlog (a low dashed footing).
|
|
200
|
+
function isoWork(out, k, x, y, kind, h) {
|
|
201
|
+
const I = k.I;
|
|
202
|
+
if (kind === 'tower') isoBox(out, k, x + 0.16, y + 0.16, x + 0.84, y + 0.84, 0, h, I.work, { rows: true });
|
|
203
|
+
else if (kind === 'setback') {
|
|
204
|
+
isoBox(out, k, x + 0.12, y + 0.12, x + 0.88, y + 0.88, 0, Math.round(h * 0.55), I.work, { rows: true });
|
|
205
|
+
isoBox(out, k, x + 0.3, y + 0.3, x + 0.7, y + 0.7, Math.round(h * 0.55), Math.round(h * 0.45), I.work, { rows: true });
|
|
206
|
+
} else if (kind === 'shed') {
|
|
207
|
+
isoBox(out, k, x + 0.1, y + 0.2, x + 0.9, y + 0.8, 0, 12, I.work, { door: true });
|
|
208
|
+
isoGable(out, k, x + 0.1, y + 0.2, x + 0.9, y + 0.8, 12, 8, I.roof);
|
|
209
|
+
} else if (kind === 'trees') { isoTree(out, k, x + 0.35, y + 0.4, 10); isoTree(out, k, x + 0.68, y + 0.7, 8); }
|
|
210
|
+
else if (kind === 'scaffold') isoBox(out, k, x + 0.16, y + 0.16, x + 0.84, y + 0.84, 0, h, I.scaffold);
|
|
211
|
+
else if (kind === 'slab') isoBox(out, k, x + 0.2, y + 0.2, x + 0.8, y + 0.8, 0, 3, I.scaffold);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// The decision glyphs (Street.dc.html glyph()), with Guide.dc.html's two extras
|
|
215
|
+
// (first, signoff), verbatim, so every rule the docket can carry has its mark.
|
|
216
|
+
function glyph(kind, cx, cy, s) {
|
|
217
|
+
const c = (x, y, r) => 'M' + (x - r) + ' ' + y + 'a' + r + ' ' + r + ' 0 1 0 ' + (2 * r) + ' 0a' + r + ' ' + r + ' 0 1 0 ' + (-2 * r) + ' 0Z';
|
|
218
|
+
const arcDots = (n, rad, r, yo) => { let d = ''; for (let i = 0; i < n; i++) { const a = Math.PI - (Math.PI * i) / (n - 1); d += c(cx + Math.cos(a) * rad, cy + yo - Math.sin(a) * rad, r); } return d; };
|
|
219
|
+
let fill = '', line = '';
|
|
220
|
+
if (kind === 'decide') fill = c(cx, cy, s * 0.3);
|
|
221
|
+
else if (kind === 'majority') fill = arcDots(5, s * 0.4, s * 0.085, s * 0.2);
|
|
222
|
+
else if (kind === 'unanimous') { const rad = s * 0.4, yo = s * 0.2; fill = arcDots(5, rad, s * 0.085, yo); line = 'M' + (cx - rad) + ' ' + (cy + yo) + 'A' + rad + ' ' + rad + ' 0 0 1 ' + (cx + rad) + ' ' + (cy + yo); }
|
|
223
|
+
else if (kind === 'consent') { fill = c(cx, cy, s * 0.1); const r = s * 0.36; line = 'M' + cx + ' ' + (cy - r) + 'A' + r + ' ' + r + ' 0 1 1 ' + (cx - r) + ' ' + cy; }
|
|
224
|
+
else if (kind === 'rule') { const h = s * 0.22; fill = 'M' + (cx - h) + ' ' + (cy - h) + 'h' + (2 * h) + 'v' + (2 * h) + 'h' + (-2 * h) + 'Z'; }
|
|
225
|
+
else if (kind === 'first') { fill = c(cx - s * 0.3, cy, s * 0.12); line = c(cx, cy, s * 0.1) + c(cx + s * 0.3, cy, s * 0.1); }
|
|
226
|
+
else if (kind === 'signoff') { fill = c(cx - s * 0.26, cy, s * 0.11) + c(cx + s * 0.26, cy, s * 0.11); line = 'M' + (cx - s * 0.15) + ' ' + cy + 'L' + (cx + s * 0.15) + ' ' + cy; }
|
|
227
|
+
return { fill: fill, line: line };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---------- the city's fixed layout (Street.dc.html homeView) ----------
|
|
231
|
+
const CIVIC_SLOTS = Object.freeze([['planning', 5, 5], ['rules', 7, 5], ['treasury', 9, 5], ['people', 5, 7], ['charter', 7, 7], ['release', 9, 7], ['disputes', 5, 9], ['automation', 7, 9], ['review', 9, 9]]);
|
|
232
|
+
const ROOF = Object.freeze({ charter: 158, rules: 36, treasury: 66, planning: 51, people: 31, release: 72, disputes: 60, review: 53, automation: 86 });
|
|
233
|
+
|
|
234
|
+
// The four prototype districts (route = the parcel road it feeds), then the
|
|
235
|
+
// four empty corners of the ground. Tiles x0..x1, y0..y1 inclusive.
|
|
236
|
+
const BASE_LOTS = Object.freeze([
|
|
237
|
+
Object.freeze({ x0: 5, x1: 10, y0: 0, y1: 3, tall: 60, route: 0 }),
|
|
238
|
+
Object.freeze({ x0: 12, x1: 15, y0: 5, y1: 10, tall: 100, route: 1 }),
|
|
239
|
+
Object.freeze({ x0: 0, x1: 3, y0: 5, y1: 10, tall: 120, route: 2 }),
|
|
240
|
+
Object.freeze({ x0: 5, x1: 10, y0: 12, y1: 15, tall: 50, route: 3 }),
|
|
241
|
+
Object.freeze({ x0: 0, x1: 3, y0: 0, y1: 3, tall: 80, route: null }),
|
|
242
|
+
Object.freeze({ x0: 12, x1: 15, y0: 0, y1: 3, tall: 90, route: null }),
|
|
243
|
+
Object.freeze({ x0: 0, x1: 3, y0: 12, y1: 15, tall: 70, route: null }),
|
|
244
|
+
Object.freeze({ x0: 12, x1: 15, y0: 12, y1: 15, tall: 60, route: null }),
|
|
245
|
+
]);
|
|
246
|
+
const MAX_DISTRICTS = BASE_LOTS.length * 2;
|
|
247
|
+
const MAX_PINS_PER_BUILDING = 3;
|
|
248
|
+
|
|
249
|
+
function halve(L) {
|
|
250
|
+
const w = L.x1 - L.x0 + 1, h = L.y1 - L.y0 + 1;
|
|
251
|
+
if (w >= h) {
|
|
252
|
+
const m = L.x0 + Math.floor(w / 2) - 1;
|
|
253
|
+
return [Object.assign({}, L, { x1: m }), Object.assign({}, L, { x0: m + 1 })];
|
|
254
|
+
}
|
|
255
|
+
const m = L.y0 + Math.floor(h / 2) - 1;
|
|
256
|
+
return [Object.assign({}, L, { y1: m }), Object.assign({}, L, { y0: m + 1 })];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// n open goals → n plots (at most sixteen), in the order the goals come.
|
|
260
|
+
function lotsFor(n) {
|
|
261
|
+
const count = Math.max(0, Math.min(Math.floor(Number(n) || 0), MAX_DISTRICTS));
|
|
262
|
+
if (count <= BASE_LOTS.length) return BASE_LOTS.slice(0, count).map((L) => Object.assign({}, L));
|
|
263
|
+
let splits = count - BASE_LOTS.length;
|
|
264
|
+
const out = [];
|
|
265
|
+
BASE_LOTS.forEach((L) => {
|
|
266
|
+
if (splits > 0) { splits -= 1; halve(L).forEach((h) => out.push(h)); } else out.push(Object.assign({}, L));
|
|
267
|
+
});
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A goal's tasks in the order they fill its plot: shipped first, then the
|
|
272
|
+
// work being built, then what has not started.
|
|
273
|
+
const TASK_ORDER = Object.freeze(['done', 'active', 'ready', 'blocked', 'backlog']);
|
|
274
|
+
function taskList(rollup) {
|
|
275
|
+
const list = [];
|
|
276
|
+
if (!rollup) return list;
|
|
277
|
+
TASK_ORDER.forEach((s) => {
|
|
278
|
+
const n = Math.max(0, Math.floor(Number(rollup[s]) || 0));
|
|
279
|
+
for (let i = 0; i < n; i++) list.push(s);
|
|
280
|
+
});
|
|
281
|
+
return list;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// One goal on one plot → the buildings on it. A building per task until the
|
|
285
|
+
// plot is full; past that each building carries `per` tasks and rises.
|
|
286
|
+
function districtWork(lot, rollup) {
|
|
287
|
+
const tasks = taskList(rollup);
|
|
288
|
+
const tiles = [];
|
|
289
|
+
for (let gx = lot.x0; gx <= lot.x1; gx++) for (let gy = lot.y0; gy <= lot.y1; gy++) tiles.push({ gx: gx, gy: gy, r: rnd(gx * 31 + gy * 7 + 1) });
|
|
290
|
+
tiles.sort((a, b) => a.r - b.r || a.gx - b.gx || a.gy - b.gy);
|
|
291
|
+
const per = Math.max(1, Math.ceil(tasks.length / Math.max(1, tiles.length)));
|
|
292
|
+
// The rise is capped low: the civic quarter stays the tallest thing in the city.
|
|
293
|
+
const grow = per > 1 ? Math.min(1.25, 1 + 0.05 * (per - 1)) : 1;
|
|
294
|
+
const out = [];
|
|
295
|
+
let t = 0;
|
|
296
|
+
tiles.forEach((tile) => {
|
|
297
|
+
const gx = tile.gx, gy = tile.gy;
|
|
298
|
+
const r2 = rnd(gx * 13 + gy * 29 + 5);
|
|
299
|
+
if (t >= tasks.length) {
|
|
300
|
+
// An empty tile keeps the prototype's trees where its draw would have planted them.
|
|
301
|
+
if (tile.r >= 0.52 && tile.r < 0.6) out.push({ gx: gx, gy: gy, kind: 'trees', h: 0, tasks: 0 });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const chunk = tasks.slice(t, t + per);
|
|
305
|
+
t += per;
|
|
306
|
+
let kind;
|
|
307
|
+
if (chunk.indexOf('active') >= 0) kind = 'scaffold';
|
|
308
|
+
else if (chunk.every((s) => s === 'done')) {
|
|
309
|
+
const r3 = rnd(gx * 17 + gy * 11 + 3);
|
|
310
|
+
kind = r3 < 0.58 ? 'tower' : r3 < 0.8 ? 'setback' : 'shed';
|
|
311
|
+
} else kind = 'slab';
|
|
312
|
+
out.push({ gx: gx, gy: gy, kind: kind, h: Math.round((20 + r2 * lot.tall) * grow), tasks: chunk.length });
|
|
313
|
+
});
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function pinState(state) {
|
|
318
|
+
return state === 'needs' || state === 'moving' || state === 'settled' ? state : 'moving';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// The home line, when the docket could not be read.
|
|
322
|
+
const FALLBACK_LINE = 'the docket could not be read just now';
|
|
323
|
+
|
|
324
|
+
// The whole home view as data.
|
|
325
|
+
// docket the GET /government/docket body, or null when it failed
|
|
326
|
+
// goals GET /goals goals[] (any status; only 'open' ones are drawn)
|
|
327
|
+
// rollups GET /goals/rollups rollups[] (goal_id → task counts)
|
|
328
|
+
// mode 'dark' | 'light'
|
|
329
|
+
function cityModel(input) {
|
|
330
|
+
const opts = input || {};
|
|
331
|
+
const docket = opts.docket && typeof opts.docket === 'object' ? opts.docket : null;
|
|
332
|
+
const mode = opts.mode === 'light' ? 'light' : 'dark';
|
|
333
|
+
const I = isoTheme(mode);
|
|
334
|
+
const counts = docket && docket.counts ? docket.counts : { needs: 0, moving: 0, settled: 0 };
|
|
335
|
+
const evening = !!docket && !(Number(counts.needs) > 0);
|
|
336
|
+
const k = isoKit(60, 30, 720, 176, 30 / 28, I, evening);
|
|
337
|
+
const P = k.P, r1 = (n) => Math.round(n * 10) / 10;
|
|
338
|
+
|
|
339
|
+
// ground, the ring road and its lanes, the plaza
|
|
340
|
+
const flat = [];
|
|
341
|
+
push(flat, k.poly([P(0, 0), P(16, 0), P(16, 16), P(0, 16)]), I.ground, I.groundLine);
|
|
342
|
+
[[4, 0, 5, 16], [11, 0, 12, 16], [0, 4, 16, 5], [0, 11, 16, 12]].forEach((q) => push(flat, k.poly([P(q[0], q[1]), P(q[2], q[1]), P(q[2], q[3]), P(q[0], q[3])]), I.road, 'none'));
|
|
343
|
+
push(flat, k.seg(P(4.5, 0), P(4.5, 16)) + k.seg(P(11.5, 0), P(11.5, 16)) + k.seg(P(0, 4.5), P(16, 4.5)) + k.seg(P(0, 11.5), P(16, 11.5)), 'none', I.lane, '2 7');
|
|
344
|
+
push(flat, k.poly([P(5, 5), P(11, 5), P(11, 11), P(5, 11)]), I.plaza, I.plazaLine);
|
|
345
|
+
|
|
346
|
+
// one district per open goal
|
|
347
|
+
const goals = (Array.isArray(opts.goals) ? opts.goals : []).filter((g) => g && g.status === 'open' && g.id != null);
|
|
348
|
+
const byGoal = {};
|
|
349
|
+
(Array.isArray(opts.rollups) ? opts.rollups : []).forEach((r) => { if (r && r.goal_id != null) byGoal[String(r.goal_id)] = r; });
|
|
350
|
+
const lots = lotsFor(goals.length);
|
|
351
|
+
const districts = lots.map((lot, i) => ({ goal_id: String(goals[i].id), title: String(goals[i].title || ''), lot: lot, work: districtWork(lot, byGoal[String(goals[i].id)]) }));
|
|
352
|
+
const unplaced = goals.slice(lots.length).map((g) => String(g.id));
|
|
353
|
+
|
|
354
|
+
let grid = '';
|
|
355
|
+
districts.forEach((D) => {
|
|
356
|
+
const L = D.lot;
|
|
357
|
+
push(flat, k.poly([P(L.x0, L.y0), P(L.x1 + 1, L.y0), P(L.x1 + 1, L.y1 + 1), P(L.x0, L.y1 + 1)]), I.plot, I.plotLine);
|
|
358
|
+
for (let gx = L.x0 + 1; gx <= L.x1; gx++) grid += k.seg(P(gx, L.y0), P(gx, L.y1 + 1));
|
|
359
|
+
for (let gy = L.y0 + 1; gy <= L.y1; gy++) grid += k.seg(P(L.x0, gy), P(L.x1 + 1, gy));
|
|
360
|
+
});
|
|
361
|
+
if (grid) push(flat, grid, 'none', I.grid);
|
|
362
|
+
|
|
363
|
+
// the civic quarter, then the districts' work, painted back to front
|
|
364
|
+
const blds = [];
|
|
365
|
+
CIVIC_SLOTS.forEach((s) => {
|
|
366
|
+
const polys = [];
|
|
367
|
+
isoInstitution(polys, k, s[0], s[1], s[2], { fixes: false });
|
|
368
|
+
blds.push({ key: s[0], depth: s[1] + s[2] + 2, polys: polys });
|
|
369
|
+
});
|
|
370
|
+
districts.forEach((D) => {
|
|
371
|
+
D.work.forEach((w) => {
|
|
372
|
+
const polys = [];
|
|
373
|
+
isoWork(polys, k, w.gx, w.gy, w.kind, w.h);
|
|
374
|
+
blds.push({ key: w.gx + ',' + w.gy, depth: w.gx + w.gy + 1, polys: polys });
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
blds.sort((a, b) => a.depth - b.depth);
|
|
378
|
+
|
|
379
|
+
// a pin over each open matter's building (up to three abreast)
|
|
380
|
+
const slotOf = {};
|
|
381
|
+
CIVIC_SLOTS.forEach((s) => { slotOf[s[0]] = s; });
|
|
382
|
+
const matters = docket && Array.isArray(docket.matters) ? docket.matters : [];
|
|
383
|
+
const atBuilding = {};
|
|
384
|
+
matters.forEach((m) => {
|
|
385
|
+
if (!m || !slotOf[m.building]) return;
|
|
386
|
+
(atBuilding[m.building] = atBuilding[m.building] || []).push(m);
|
|
387
|
+
});
|
|
388
|
+
const pins = [];
|
|
389
|
+
let n = 0;
|
|
390
|
+
CIVIC_SLOTS.forEach((s) => {
|
|
391
|
+
const here = (atBuilding[s[0]] || []).slice(0, MAX_PINS_PER_BUILDING);
|
|
392
|
+
const top = P(s[1] + 1, s[2] + 1, ROOF[s[0]] + 8);
|
|
393
|
+
here.forEach((m, i) => {
|
|
394
|
+
const g = glyph(m.rule, 7, 7, 13);
|
|
395
|
+
const dx = (i - (here.length - 1) / 2) * 26;
|
|
396
|
+
pins.push({ key: String(m.id), building: s[0], state: pinState(m.state), title: String(m.short || m.title || ''), x: r1(top[0] - 15 + dx), y: r1(top[1] - 52), h: 52, gf: g.fill, gl: g.line, delay: -(n * 0.9) });
|
|
397
|
+
n += 1;
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// work on the move: parcels ride the ring road to Review, and leave through Release
|
|
402
|
+
const route = (pts) => pts.map((p, i) => { const q = P(p[0], p[1]); return (i ? 'L' : 'M') + r1(q[0]) + ' ' + r1(q[1]); }).join('');
|
|
403
|
+
const toReview = [
|
|
404
|
+
route([[8, 3.6], [8, 4.5], [11.5, 4.5], [11.5, 10], [10.7, 10]]),
|
|
405
|
+
route([[12.4, 8], [11.5, 8], [11.5, 10.2], [10.7, 10.2]]),
|
|
406
|
+
route([[3.6, 7.5], [4.5, 7.5], [4.5, 11.5], [9.8, 11.5], [9.8, 10.8]]),
|
|
407
|
+
route([[7.5, 12.4], [7.5, 11.5], [10.2, 11.5], [10.2, 10.8]]),
|
|
408
|
+
];
|
|
409
|
+
const fed = {};
|
|
410
|
+
districts.forEach((D) => { if (D.lot.route != null) fed[D.lot.route] = true; });
|
|
411
|
+
const parcels = [];
|
|
412
|
+
toReview.forEach((d, i) => { if (fed[i]) [0, 1].forEach((j) => parcels.push({ key: 'p' + i + j, d: d, dur: 12 + i, delay: -(i * 2.7 + j * 6.2), out: false })); });
|
|
413
|
+
if (districts.length) [0, 1].forEach((j) => parcels.push({ key: 'out' + j, d: route([[10.6, 9.6], [11.5, 9.6], [11.5, 8], [11.5, 0]]), dur: 10, delay: -(j * 5), out: true }));
|
|
414
|
+
|
|
415
|
+
const line = docket && docket.line ? String(docket.line) : FALLBACK_LINE;
|
|
416
|
+
const pinCount = { needs: 0, moving: 0, settled: 0 };
|
|
417
|
+
pins.forEach((p) => { pinCount[p.state] += 1; });
|
|
418
|
+
const label = 'The governor city: the nine civic buildings round the plaza and '
|
|
419
|
+
+ (districts.length === 1 ? 'one district' : districts.length + ' districts') + ', one for each open goal'
|
|
420
|
+
+ (pins.length ? '. Pins: ' + pinCount.needs + ' need you, ' + pinCount.moving + ' moving, ' + pinCount.settled + ' settled.' : '.');
|
|
421
|
+
|
|
422
|
+
return { mode: mode, evening: evening, crate: { fill: I.crate.t, edge: I.crate.s }, flat: flat, blds: blds, pins: pins, parcels: parcels, districts: districts, unplaced: unplaced, line: line, label: label };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Where the start governing button goes: the street (task 1004525 builds it),
|
|
426
|
+
// a hall page of its own beside this one.
|
|
427
|
+
const STREET_HREF = '/builders/street';
|
|
428
|
+
|
|
429
|
+
const api = {
|
|
430
|
+
isoTheme: isoTheme, isoKit: isoKit, isoBox: isoBox, isoGable: isoGable, isoHip: isoHip, isoSaw: isoSaw,
|
|
431
|
+
isoTree: isoTree, isoLight: isoLight, isoInstitution: isoInstitution, isoWork: isoWork, glyph: glyph, rnd: rnd,
|
|
432
|
+
lotsFor: lotsFor, districtWork: districtWork, cityModel: cityModel,
|
|
433
|
+
CIVIC_SLOTS: CIVIC_SLOTS, ROOF: ROOF, BASE_LOTS: BASE_LOTS, MAX_DISTRICTS: MAX_DISTRICTS, MAX_PINS_PER_BUILDING: MAX_PINS_PER_BUILDING,
|
|
434
|
+
FALLBACK_LINE: FALLBACK_LINE, STREET_HREF: STREET_HREF,
|
|
435
|
+
};
|
|
436
|
+
root.GovCity = api;
|
|
437
|
+
if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
|
438
|
+
}(typeof window !== 'undefined' ? window : globalThis));
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* modules/hall-ui/public/city.css — THE GOVERNOR CITY's home (task 1004524,
|
|
2
|
+
goal 1000125, R02). The home branch of the <style> in
|
|
3
|
+
docs/design/mocks/governor-city/Street.dc.html: the same keyframes (ring,
|
|
4
|
+
zoom, travel, bob, drift), the same wordmark, mode switch, start button and
|
|
5
|
+
line, the same sizes. What changed is where the colours come from: the
|
|
6
|
+
prototype wrote its theme() values inline; here every colour is one of the
|
|
7
|
+
hall's tokens (style.css + the branding pack), whose values ARE the
|
|
8
|
+
prototype's (bg #000 / #edecee, ink, ink-soft, the rule, the accent
|
|
9
|
+
#fb9569 and its dark ink #2a1305). The drawing's own faces are drawing data
|
|
10
|
+
in city-draw.js (the prototype's iso sub-palette).
|
|
11
|
+
No :root here (tests/hall_reading_world.mjs): page tokens live on the body.
|
|
12
|
+
The two dark blocks below are byte-identical twins (the hall's switch is
|
|
13
|
+
html[data-theme], set by city.js from the same 'hall-theme' preference
|
|
14
|
+
every hall page reads; the media block is first paint). */
|
|
15
|
+
|
|
16
|
+
body[data-page="city"] {
|
|
17
|
+
--city-contact: drop-shadow(0 6px 8px color-mix(in srgb, var(--ink) 10%, transparent));
|
|
18
|
+
--city-glow: none;
|
|
19
|
+
--city-crate: color-mix(in srgb, var(--ink) 7%, transparent);
|
|
20
|
+
--city-crate-edge: color-mix(in srgb, var(--ink) 55%, transparent);
|
|
21
|
+
/* the hall's ground plate and its dark veil (style.css body::before/::after) are
|
|
22
|
+
the hall chrome's; the city is its own ground, so both are off here */
|
|
23
|
+
--plate: none;
|
|
24
|
+
--plate-veil: none;
|
|
25
|
+
margin: 0;
|
|
26
|
+
height: 100%;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
background: var(--bg);
|
|
29
|
+
color: var(--ink);
|
|
30
|
+
font-family: var(--font-body);
|
|
31
|
+
}
|
|
32
|
+
html[data-theme="dark"] body[data-page="city"] {
|
|
33
|
+
--city-contact: none;
|
|
34
|
+
--city-glow: 0 0 18px color-mix(in srgb, var(--accent) 50%, transparent);
|
|
35
|
+
}
|
|
36
|
+
@media (prefers-color-scheme: dark) {
|
|
37
|
+
html:not([data-theme]) body[data-page="city"] {
|
|
38
|
+
--city-contact: none;
|
|
39
|
+
--city-glow: 0 0 18px color-mix(in srgb, var(--accent) 50%, transparent);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
html:has(body[data-page="city"]) { height: 100%; }
|
|
43
|
+
|
|
44
|
+
/* the header: wordmark left, the sun/moon switch right */
|
|
45
|
+
.city-top { position: absolute; left: 32px; right: 32px; top: 0; height: 64px; display: flex; align-items: center; justify-content: space-between; z-index: 3; }
|
|
46
|
+
.city-mark { font-weight: 500; font-size: 17px; letter-spacing: -0.015em; text-transform: lowercase; }
|
|
47
|
+
.city-mode { background: none; border: 1px solid var(--rule); border-radius: 999px; height: 36px; padding: 0 11px; display: flex; align-items: center; gap: 8px; cursor: pointer; color: var(--ink); font-family: var(--font-body); }
|
|
48
|
+
.city-mode svg { display: block; stroke-width: 1.4; }
|
|
49
|
+
|
|
50
|
+
/* the stage: the prototype's 1440 x 900 artboard, scaled to the window by
|
|
51
|
+
city.js (--city-scale) and centred, so 1440 x 900 is the prototype pixel
|
|
52
|
+
for pixel */
|
|
53
|
+
.city-main { position: fixed; inset: 0; z-index: 1; }
|
|
54
|
+
.city-frame { position: absolute; inset: 0; overflow: hidden; }
|
|
55
|
+
.city-stage { position: absolute; left: 50%; top: 50%; width: 1440px; height: 900px; transform: translate(-50%, -50%) scale(var(--city-scale, 1)); transform-origin: 50% 50%; }
|
|
56
|
+
.city-zoom, .city-drift { position: absolute; left: 0; top: 0; width: 1440px; height: 900px; }
|
|
57
|
+
.city-svg { position: absolute; left: 0; top: 0; pointer-events: none; overflow: visible; }
|
|
58
|
+
.city-blds { filter: var(--city-contact); }
|
|
59
|
+
.city-parcels, .city-pins { position: absolute; left: 0; top: 0; width: 1440px; height: 900px; pointer-events: none; }
|
|
60
|
+
|
|
61
|
+
/* parcels: crates riding the ring road on offset-path */
|
|
62
|
+
.city-parcel { position: absolute; left: 0; top: 0; width: 9px; height: 7px; margin: -4px 0 0 -5px; box-sizing: border-box; border-radius: 2px; background: var(--city-crate); border: 1px solid var(--city-crate-edge); offset-rotate: 0deg; }
|
|
63
|
+
.city-parcel--out { background: var(--accent); border-color: var(--accent); }
|
|
64
|
+
|
|
65
|
+
/* pins: orange and pulsing waits on you; ink and bobbing is moving; grey is settled */
|
|
66
|
+
.city-pin { position: absolute; width: 30px; height: 52px; display: flex; flex-direction: column; align-items: center; }
|
|
67
|
+
.city-pin__head { position: relative; width: 22px; height: 22px; flex-shrink: 0; border-radius: 999px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; }
|
|
68
|
+
.city-pin__head svg { display: block; }
|
|
69
|
+
.city-pin__head path { fill: currentColor; }
|
|
70
|
+
.city-pin__head path + path { fill: none; stroke: currentColor; stroke-width: 1.2; }
|
|
71
|
+
.city-pin__stem { width: 0; flex-grow: 1; border-left: 1px solid transparent; }
|
|
72
|
+
.city-pin__ring { position: absolute; inset: -1px; border-radius: 999px; border: 1px solid var(--accent); }
|
|
73
|
+
.city-pin--needs .city-pin__head { background: var(--accent); color: var(--accent-on); box-shadow: var(--city-glow); }
|
|
74
|
+
.city-pin--needs .city-pin__stem { border-left-color: color-mix(in srgb, var(--accent) 60%, transparent); }
|
|
75
|
+
.city-pin--moving .city-pin__head { background: var(--ink); color: var(--bg-card); }
|
|
76
|
+
.city-pin--moving .city-pin__stem { border-left-color: color-mix(in srgb, var(--ink) 45%, transparent); }
|
|
77
|
+
.city-pin--settled .city-pin__head { background: color-mix(in srgb, var(--ink) 10%, var(--bg)); border: 1px solid color-mix(in srgb, var(--ink) 22%, transparent); color: var(--ink-faint); }
|
|
78
|
+
.city-pin--settled .city-pin__stem { border-left-color: color-mix(in srgb, var(--ink) 22%, transparent); }
|
|
79
|
+
|
|
80
|
+
/* the one door and the one line */
|
|
81
|
+
.city-door { position: absolute; left: 0; right: 0; bottom: 70px; display: flex; flex-direction: column; align-items: center; gap: 14px; z-index: 2; }
|
|
82
|
+
.city-start { display: inline-flex; align-items: center; box-sizing: border-box; font-family: var(--font-body); font-size: 17px; font-weight: 500; height: 56px; padding: 0 38px; border-radius: 999px; border: 0; cursor: pointer; background: var(--accent); color: var(--accent-on); text-transform: lowercase; text-decoration: none; transition: transform 200ms cubic-bezier(.2,.7,.3,1); }
|
|
83
|
+
.city-start:hover { background: var(--accent-lift); transform: translateY(-2px); }
|
|
84
|
+
.city-line { margin: 0; font-size: 14px; font-weight: 400; line-height: 1.4; color: var(--ink-soft); font-variant-numeric: tabular-nums; text-align: center; }
|
|
85
|
+
.city-mode:focus-visible, .city-start:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
86
|
+
|
|
87
|
+
@media (max-width: 560px) {
|
|
88
|
+
.city-top { left: 16px; right: 16px; }
|
|
89
|
+
.city-door { bottom: 40px; padding: 0 16px; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* the prototype's motion, verbatim */
|
|
93
|
+
@keyframes city-ring { 0% { transform: scale(.8); opacity: .9; } 100% { transform: scale(2.3); opacity: 0; } }
|
|
94
|
+
@keyframes city-zoom { from { opacity: 0; transform: scale(.97); } to { opacity: 1; transform: none; } }
|
|
95
|
+
@keyframes city-travel { 0% { offset-distance: 0%; opacity: 0; } 8% { opacity: 1; } 90% { opacity: 1; } 100% { offset-distance: 100%; opacity: 0; } }
|
|
96
|
+
@keyframes city-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
|
|
97
|
+
@keyframes city-drift { from { transform: translate(0, 0); } to { transform: translate(-8px, 5px); } }
|
|
98
|
+
.city-zoom { animation: city-zoom 420ms cubic-bezier(.16,1,.3,1) both; transform-origin: 50% 60%; }
|
|
99
|
+
.city-drift { animation: city-drift 46s ease-in-out infinite alternate; }
|
|
100
|
+
.city-pin { animation: city-bob 6s ease-in-out infinite; }
|
|
101
|
+
.city-pin__ring { animation: city-ring 2.8s cubic-bezier(.2,.7,.3,1) infinite; }
|
|
102
|
+
.city-parcel { animation-name: city-travel; animation-timing-function: linear; animation-iteration-count: infinite; }
|
|
103
|
+
|
|
104
|
+
/* The Kill Switch: under reduced motion nothing moves. The rest frame is the
|
|
105
|
+
whole city with its pins standing still; the parcels, which only mean
|
|
106
|
+
something while they travel, are left out of it. */
|
|
107
|
+
@media (prefers-reduced-motion: reduce) {
|
|
108
|
+
body[data-page="city"] *, body[data-page="city"] *::before, body[data-page="city"] *::after { animation: none !important; transition: none !important; }
|
|
109
|
+
.city-parcels { display: none; }
|
|
110
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
6
|
+
<title>The city — {{worldName}}</title>
|
|
7
|
+
<meta name="description" content="The governor's home: the project drawn as a small city, a pin over every matter that waits on you, and one door into the street.">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<!-- The faces (Manrope, JetBrains Mono) come from the branding pack's font
|
|
11
|
+
links, injected into this head by the server like every hall page's
|
|
12
|
+
(tests/hall_tokens.mjs: no hall page hardcodes a web-font stylesheet).
|
|
13
|
+
The prototype carried its own Google Fonts link for the same two faces. -->
|
|
14
|
+
<link rel="stylesheet" href="/builders/style.css?v=2026-07-02-shell1">
|
|
15
|
+
<link rel="stylesheet" href="/builders/city.css?v=2026-10-02-city1">
|
|
16
|
+
</head>
|
|
17
|
+
<!-- THE GOVERNOR CITY, HOME (task 1004524, goal 1000125, R02). The home view
|
|
18
|
+
of docs/design/mocks/governor-city/Street.dc.html (homeView and the home
|
|
19
|
+
branch of its template), built over the governor docket (GET
|
|
20
|
+
/api/bongos/government/docket, task 1004523) and the open goals with their
|
|
21
|
+
task rollups. The markup below is the prototype's home: the wordmark and
|
|
22
|
+
the sun/moon switch in the header, the city, one button and one line.
|
|
23
|
+
Nothing else is on the page, on purpose: no hall sidebar, no top bar.
|
|
24
|
+
|
|
25
|
+
The drawing is city-draw.js (the prototype's iso kit, verbatim, as pure
|
|
26
|
+
data); city.js reads the three feeds and turns the lists into SVG paths
|
|
27
|
+
and the pin and parcel spans; city.css is the template's <style> on the
|
|
28
|
+
hall's tokens.
|
|
29
|
+
|
|
30
|
+
The button is a link to /builders/street, the street's own hall page,
|
|
31
|
+
which task 1004525 builds. It is an <a>, not the prototype's <button>,
|
|
32
|
+
because pressing it goes to another page.
|
|
33
|
+
|
|
34
|
+
The page is gated on board.vote.cast (src/bongos/serve-internal.js), the
|
|
35
|
+
same atom the docket read checks, so whoever reaches the city can read
|
|
36
|
+
what it draws. -->
|
|
37
|
+
<body data-page="city" class="city">
|
|
38
|
+
|
|
39
|
+
<header class="city-top">
|
|
40
|
+
<span class="city-mark">{{productName}}</span>
|
|
41
|
+
<button type="button" class="city-mode" id="cityMode" aria-label="Switch to light mode" title="Switch to light mode">
|
|
42
|
+
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" aria-hidden="true"><path id="cityModeIcon" d=""></path></svg>
|
|
43
|
+
</button>
|
|
44
|
+
</header>
|
|
45
|
+
|
|
46
|
+
<main class="city-main" id="cityMain">
|
|
47
|
+
<div class="city-frame" id="cityFrame">
|
|
48
|
+
<div class="city-stage" id="cityStage" role="img" aria-label="The governor city">
|
|
49
|
+
<div class="city-zoom">
|
|
50
|
+
<div class="city-drift">
|
|
51
|
+
<svg class="city-svg" id="citySvg" width="1440" height="900" viewBox="0 0 1440 900" aria-hidden="true" focusable="false">
|
|
52
|
+
<g id="cityFlat"></g>
|
|
53
|
+
<g id="cityBlds" class="city-blds"></g>
|
|
54
|
+
</svg>
|
|
55
|
+
<div class="city-parcels" id="cityParcels" aria-hidden="true"></div>
|
|
56
|
+
<div class="city-pins" id="cityPins" aria-hidden="true"></div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="city-door">
|
|
63
|
+
<a class="city-start" id="cityStart" href="/builders/street" aria-label="Start governing: enter the street view">start governing</a>
|
|
64
|
+
<h1 class="city-line" id="cityLine" aria-live="polite">reading the docket</h1>
|
|
65
|
+
</div>
|
|
66
|
+
</main>
|
|
67
|
+
|
|
68
|
+
<script src="/builders/city-draw.js?v=2026-10-02-city1"></script>
|
|
69
|
+
<script src="/builders/city.js?v=2026-10-02-city1"></script>
|
|
70
|
+
</body>
|
|
71
|
+
</html>
|