@bongos/core 1.21.113 → 1.21.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.bongos-core.json +603 -183
  2. package/.claude/skills/ask-for-help/SKILL.md +1 -1
  3. package/.claude/skills/bongos-feedback/SKILL.md +1 -1
  4. package/.claude/skills/builder-sequence/SKILL.md +1 -1
  5. package/.claude/skills/builder-ship/SKILL.md +1 -0
  6. package/GOVERNMENT.md +128 -11
  7. package/clients/bongos-client/README.md +1 -1
  8. package/clients/bongos-client/bongos-client.global.js +28 -0
  9. package/clients/bongos-client/index.cjs +28 -0
  10. package/clients/bongos-client/index.d.ts +45 -2
  11. package/clients/bongos-client/index.mjs +28 -0
  12. package/docs/adr/0372-an-ask-is-stored-per-docket-matter-and-the-server-refuses-slop.md +33 -0
  13. package/docs/adr/0373-an-ai-session-files-a-blocker-with-an-ask.md +31 -0
  14. package/docs/adr/0374-a-building-lead-is-a-held-position-appointed-by-a-people-matter.md +30 -0
  15. package/docs/adr/0375-who-may-build-without-a-vote-comes-from-the-form-of-government.md +38 -0
  16. package/docs/adr/0376-every-ask-is-routed-to-a-real-seat-on-the-server.md +30 -0
  17. package/docs/adr/0377-governance-is-a-held-position-read-on-me.md +24 -0
  18. package/docs/adr/0378-a-leads-build-outs-are-rows-built-at-once-or-by-a-passed-vote.md +33 -0
  19. package/docs/adr/0379-the-treasury-is-pots-per-goal-a-reserve-and-borrowing-over-real-spend.md +32 -0
  20. package/docs/adr/0380-a-strictness-preset-is-a-bundle-of-laws-switched-by-one-laws-vote.md +26 -0
  21. package/docs/adr/README.md +10 -1
  22. package/docs/api/openapi.json +989 -87
  23. package/docs/api-reference.md +18 -4
  24. package/docs/copy-inventory.md +343 -262
  25. package/docs/copy-registry.json +1223 -426
  26. package/docs/file-map.md +2 -1
  27. package/docs/module-api-changelog.md +2 -0
  28. package/docs/packs/engineer.md +2 -1
  29. package/docs/page-inventory.json +91 -6
  30. package/docs/page-readings.json +5017 -4458
  31. package/modules/builder-settings/law-locks.js +202 -0
  32. package/modules/builder-settings/routes/settings.js +66 -6
  33. package/modules/builder-settings/settings.js +7 -0
  34. package/modules/discord/ship-broadcast.js +22 -13
  35. package/modules/government/asks-db.js +54 -0
  36. package/modules/government/asks.js +269 -0
  37. package/modules/government/board-idea-reads.js +68 -0
  38. package/modules/government/board.js +21 -57
  39. package/modules/government/build-rights.js +148 -0
  40. package/modules/government/buildouts.js +160 -0
  41. package/modules/government/counts.js +81 -0
  42. package/modules/government/db.js +242 -0
  43. package/modules/government/decision-board.js +234 -8
  44. package/modules/government/decision-rules.js +2 -0
  45. package/modules/government/docket.js +114 -6
  46. package/modules/government/law-board.js +131 -0
  47. package/modules/government/laws.js +241 -0
  48. package/modules/government/leads.js +170 -0
  49. package/modules/government/migrations/government_027_laws.sql +143 -0
  50. package/modules/government/migrations/government_028_asks.sql +57 -0
  51. package/modules/government/migrations/government_029_leads.sql +135 -0
  52. package/modules/government/migrations/government_030_buildouts.sql +111 -0
  53. package/modules/government/migrations/government_031_treasury.sql +107 -0
  54. package/modules/government/module.json +2 -1
  55. package/modules/government/pollers/treasury-borrow.js +62 -0
  56. package/modules/government/positions.js +107 -0
  57. package/modules/government/presets.js +149 -0
  58. package/modules/government/routes/government.js +425 -2
  59. package/modules/government/routing.js +154 -0
  60. package/modules/government/treasury-db.js +166 -0
  61. package/modules/government/treasury.js +241 -0
  62. package/modules/hall-ui/public/atlas.html +2 -2
  63. package/modules/hall-ui/public/blockers.html +2 -2
  64. package/modules/hall-ui/public/board-room.html +2 -2
  65. package/modules/hall-ui/public/board-room.js +2 -0
  66. package/modules/hall-ui/public/city-draw.js +604 -11
  67. package/modules/hall-ui/public/city.css +157 -1
  68. package/modules/hall-ui/public/city.html +37 -7
  69. package/modules/hall-ui/public/city.js +441 -21
  70. package/modules/hall-ui/public/city.states.json +110 -5
  71. package/modules/hall-ui/public/collab.html +2 -2
  72. package/modules/hall-ui/public/commands.html +2 -2
  73. package/modules/hall-ui/public/copy-desk.html +2 -2
  74. package/modules/hall-ui/public/craft-mode.js +112 -29
  75. package/modules/hall-ui/public/deploy.html +2 -2
  76. package/modules/hall-ui/public/diagram-viewer.css +12 -0
  77. package/modules/hall-ui/public/diagram-viewer.js +4 -0
  78. package/modules/hall-ui/public/diagrams.html +2 -2
  79. package/modules/hall-ui/public/drachmae.html +2 -2
  80. package/modules/hall-ui/public/fleet.html +2 -2
  81. package/modules/hall-ui/public/gate.html +2 -2
  82. package/modules/hall-ui/public/goal-map.html +2 -2
  83. package/modules/hall-ui/public/goals.html +2 -2
  84. package/modules/hall-ui/public/gov-feeds.js +141 -0
  85. package/modules/hall-ui/public/gov-living.js +1002 -0
  86. package/modules/hall-ui/public/gov-positions.js +97 -0
  87. package/modules/hall-ui/public/government.html +2 -2
  88. package/modules/hall-ui/public/hall-render.js +3 -0
  89. package/modules/hall-ui/public/idea.html +2 -2
  90. package/modules/hall-ui/public/ideas.html +2 -2
  91. package/modules/hall-ui/public/index.html +3 -3
  92. package/modules/hall-ui/public/modules.html +2 -2
  93. package/modules/hall-ui/public/my-agents.css +334 -0
  94. package/modules/hall-ui/public/my-agents.html +72 -0
  95. package/modules/hall-ui/public/my-agents.js +603 -0
  96. package/modules/hall-ui/public/my-agents.states.json +37 -0
  97. package/modules/hall-ui/public/primer.html +2 -2
  98. package/modules/hall-ui/public/profile-connections.js +40 -28
  99. package/modules/hall-ui/public/profile.html +2 -2
  100. package/modules/hall-ui/public/project-settings.html +2 -2
  101. package/modules/hall-ui/public/ranks.html +2 -2
  102. package/modules/hall-ui/public/roadmap.html +2 -2
  103. package/modules/hall-ui/public/room.css +8 -0
  104. package/modules/hall-ui/public/roster.html +2 -2
  105. package/modules/hall-ui/public/sessions.html +2 -2
  106. package/modules/hall-ui/public/settings.html +2 -2
  107. package/modules/hall-ui/public/shell.js +116 -16
  108. package/modules/hall-ui/public/street-card.js +18 -1
  109. package/modules/hall-ui/public/street-deck.css +299 -0
  110. package/modules/hall-ui/public/street-deck.js +1145 -0
  111. package/modules/hall-ui/public/street-depth.js +712 -0
  112. package/modules/hall-ui/public/street-desk.js +5 -2
  113. package/modules/hall-ui/public/street-draw.js +109 -16
  114. package/modules/hall-ui/public/street-inside.js +137 -22
  115. package/modules/hall-ui/public/street-office.js +705 -0
  116. package/modules/hall-ui/public/street.css +161 -11
  117. package/modules/hall-ui/public/street.html +41 -11
  118. package/modules/hall-ui/public/street.js +78 -66
  119. package/modules/hall-ui/public/street.states.json +129 -21
  120. package/modules/hall-ui/public/studio.html +2 -2
  121. package/modules/hall-ui/public/style.css +5 -1
  122. package/modules/hall-ui/public/task.html +2 -2
  123. package/modules/hall-ui/public/thinking.html +2 -2
  124. package/modules/hall-ui/public/tweak-editor.html +2 -2
  125. package/modules/hall-ui/public/watch.html +2 -2
  126. package/modules/hall-ui/public/watch.js +14 -1
  127. package/modules/hall-ui/public/work.html +2 -2
  128. package/modules/hall-ui/records/living-city.md +57 -0
  129. package/modules/hall-ui/records/my-agents.md +21 -0
  130. package/modules/hall-ui/records/oversight-family.md +1 -0
  131. package/modules/hall-ui/records/profile-connections.md +10 -0
  132. package/modules/hall-ui/records/reading-rooms.md +2 -0
  133. package/modules/ideas/routes/blockers.js +92 -3
  134. package/modules/lifecycle/module.json +2 -1
  135. package/modules/lifecycle/moving.js +17 -1
  136. package/modules/lifecycle/routes/moving.js +6 -0
  137. package/modules/lifecycle/routes/project-settings.js +23 -0
  138. package/modules/onboarding/approval-broadcast.js +8 -0
  139. package/modules/onboarding/routes/access-requests.js +16 -6
  140. package/modules/platform-identity/connections.js +13 -2
  141. package/modules/platform-identity/routes/connections.js +7 -1
  142. package/modules/public-landing/public/account-private.states.json +2 -2
  143. package/modules/public-landing/public/account.html +65 -5
  144. package/modules/public-landing/public/account.states.json +3 -3
  145. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.jpg.json +1 -1
  146. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp +0 -0
  147. package/modules/public-landing/public/assets/world/r2-bongos-chrome-b.webp.json +13 -0
  148. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.jpg.json +1 -1
  149. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp +0 -0
  150. package/modules/public-landing/public/assets/world/r2-chrome-blob-one.webp.json +13 -0
  151. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.jpg.json +1 -1
  152. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp +0 -0
  153. package/modules/public-landing/public/assets/world/r2-chrome-blobs-set.webp.json +13 -0
  154. package/modules/public-landing/public/assets/world.css +4 -4
  155. package/modules/public-landing/public/connections-empty.states.json +14 -0
  156. package/modules/public-landing/public/connections-inactive.states.json +14 -0
  157. package/modules/public-landing/public/connections.html +450 -0
  158. package/modules/public-landing/public/connections.probes.json +414 -0
  159. package/modules/public-landing/public/connections.states.json +17 -0
  160. package/modules/public-landing/public/contact.html +2 -2
  161. package/modules/public-landing/public/guild.html +2 -2
  162. package/modules/public-landing/public/index.html +2 -2
  163. package/modules/public-landing/public/invitations.html +2 -2
  164. package/modules/public-landing/public/privacy.html +2 -2
  165. package/modules/public-landing/public/projects.html +2 -2
  166. package/modules/public-landing/public/projects.probes.json +6 -2
  167. package/modules/public-landing/public/terms.html +2 -2
  168. package/modules/ui-design/kit/fixtures/connections-empty.json +12 -0
  169. package/modules/ui-design/kit/fixtures/connections-inactive.json +7 -0
  170. package/modules/ui-design/kit/fixtures/connections.json +22 -0
  171. package/modules/ui-design/kit/serve.js +95 -0
  172. package/package-lock.json +2 -2
  173. package/package.json +1 -1
  174. package/release-notes.json +122 -0
  175. package/scripts/discord/README.md +8 -5
  176. package/scripts/discord/post.js +12 -1
  177. package/scripts/gds/artifact-audit.js +20 -3
  178. package/scripts/gds/ask.js +164 -0
  179. package/scripts/gds/autobongos-verify.js +21 -2
  180. package/scripts/gds/government-walk.js +638 -4
  181. package/scripts/gds/pulse.js +9 -6
  182. package/scripts/gds/ship-escalation.js +22 -3
  183. package/scripts/gds/ship-finish.js +5 -4
  184. package/scripts/gds/ship-merge-gate.js +18 -6
  185. package/scripts/hall-preview/README.md +41 -1
  186. package/scripts/hall-preview/pref-writes.js +140 -0
  187. package/scripts/hall-preview/refresh-fixtures.js +101 -7
  188. package/scripts/hall-preview/scrub-identity.js +366 -7
  189. package/scripts/hall-preview/server.js +27 -1
  190. package/scripts/render-diagrams.sh +4 -2
  191. package/src/bongos/auth-idp.js +6 -1
  192. package/src/bongos/project-settings.js +41 -0
  193. package/src/bongos/route-rank-check.js +27 -0
  194. package/src/bongos/routes/me.js +68 -7
  195. package/src/bongos/serve-internal.js +5 -1
  196. package/src/module-api.js +7 -1
  197. package/src/platform-server.js +10 -0
  198. package/tests/application_lifecycle.mjs +47 -5
  199. package/tests/conductor_main_e2e.mjs +21 -0
  200. package/tests/connections_api.mjs +18 -0
  201. package/tests/craft_mode_nav.mjs +5 -3
  202. package/tests/craft_mode_tallies.mjs +52 -16
  203. package/tests/discord_approvals.mjs +11 -0
  204. package/tests/discord_channels.mjs +20 -0
  205. package/tests/fixtures/gov-seated.mjs +64 -0
  206. package/tests/fixtures/government-ask-cards.json +1284 -0
  207. package/tests/government_act_alone_buildings.mjs +1 -0
  208. package/tests/government_agent_asks.mjs +322 -0
  209. package/tests/government_asks.mjs +408 -0
  210. package/tests/government_build_rights.mjs +253 -0
  211. package/tests/government_buildouts.mjs +382 -0
  212. package/tests/government_counts.mjs +143 -0
  213. package/tests/government_decision_rules.mjs +5 -1
  214. package/tests/government_laws.mjs +488 -0
  215. package/tests/government_laws_session.mjs +321 -0
  216. package/tests/government_leads.mjs +375 -0
  217. package/tests/government_positions.mjs +127 -0
  218. package/tests/government_presets.mjs +215 -0
  219. package/tests/government_routes.mjs +8 -1
  220. package/tests/government_routing.mjs +118 -0
  221. package/tests/government_seed.mjs +5 -0
  222. package/tests/government_treasury.mjs +394 -0
  223. package/tests/governor_docket.mjs +26 -2
  224. package/tests/hall_buildouts.mjs +208 -0
  225. package/tests/hall_city.mjs +14 -6
  226. package/tests/hall_city_living.mjs +617 -0
  227. package/tests/hall_gov_counts.mjs +336 -0
  228. package/tests/hall_gov_living.mjs +636 -0
  229. package/tests/hall_gov_real_site.mjs +655 -0
  230. package/tests/hall_matter_card.mjs +6 -1
  231. package/tests/hall_my_agents.mjs +481 -0
  232. package/tests/hall_nav.mjs +107 -4
  233. package/tests/hall_page_gate_map.mjs +4 -0
  234. package/tests/hall_preview_governor_fixtures.mjs +30 -3
  235. package/tests/hall_preview_scrub.mjs +59 -1
  236. package/tests/hall_preview_security.mjs +169 -0
  237. package/tests/hall_reading_world.mjs +44 -0
  238. package/tests/hall_roster_world.mjs +6 -2
  239. package/tests/hall_street.mjs +18 -2
  240. package/tests/hall_street_amend.mjs +1 -1
  241. package/tests/hall_street_deck.mjs +609 -0
  242. package/tests/hall_street_depth.mjs +559 -0
  243. package/tests/hall_street_desk.mjs +1 -1
  244. package/tests/hall_street_inside.mjs +29 -5
  245. package/tests/hall_tweak_editor.mjs +4 -1
  246. package/tests/hub_account_page.mjs +94 -3
  247. package/tests/hub_connections_page.mjs +507 -0
  248. package/tests/hub_invite_badge.mjs +1 -1
  249. package/tests/idp_device_exchange.mjs +18 -0
  250. package/tests/landing_legal_pages.mjs +8 -5
  251. package/tests/landing_page.mjs +23 -4
  252. package/tests/nav_permission_atoms.mjs +4 -1
  253. package/tests/profile_connections_ui.mjs +88 -25
  254. package/tests/projects_hub.mjs +15 -0
  255. package/tests/render_check.mjs +2 -2
  256. package/tests/ship_broadcast_routing.mjs +57 -16
  257. package/tests/ship_broadcast_server.mjs +40 -7
  258. package/tests/ship_merge_gate.mjs +19 -0
  259. package/tests/ui_design_kit.mjs +2 -2
  260. package/tests/watch_applications_queue.mjs +23 -0
@@ -0,0 +1,1145 @@
1
+ // modules/hall-ui/public/street-deck.js — THE DECISION DECK: decide in a row
2
+ // (task 1004685, the living-city prototype; V3 of four builders).
3
+ //
4
+ // WHAT THIS IS. The owner's first worry is volume: thirty-nine things can wait on
5
+ // a person at once, and a street of pins does not get through them. The deck is
6
+ // one card at a time at /street#deck (every building) and /street#deck=<building>.
7
+ // Since task 1004702 (the Tweak Mode glass language, the owner's approved mocks
8
+ // in docs/design/mocks/decision-cards/) the street stays in view: the card is a
9
+ // glass panel beside the building that asks, a hairline to its pin, a halo on
10
+ // the pin and a light hush over the rest of the scene; the cards still to come
11
+ // are two slivers under it, and a slim dock at the foot holds the run (how many
12
+ // in hand of the to-do count, today's pips, the keys, the way back). On a phone
13
+ // it is a sheet from the foot. Keys 1 / 2 / 3 pick, S skips, D passes it on to a
14
+ // seat, Escape leaves. Answering slides the card away and the next one rises (a
15
+ // class the stylesheet animates; the motion dies under reduced motion).
16
+ //
17
+ // THE WORDS speak as "I", the agent that asked (the owner's pick, 2026-10-13):
18
+ // the question, the said line (saidOf), the answers with "my pick", and the
19
+ // last line as a sentence (footOf). drawAsk() is the one drawing, for the deck,
20
+ // a building's card on the street, the card inside a building and the city
21
+ // card's first ask (city.js answers it through sendAnswer, the deck's path).
22
+ //
23
+ // REAL CARDS (task 1004703): a matter whose agent stored its ask (the server
24
+ // refuses slop: modules/government/asks.js) carries it from the docket, and the
25
+ // deck draws that card. A matter with no stored ask shows in its own words, with
26
+ // its floor's desk beside it (task 1004717: the prototype overlay's rewrites,
27
+ // marks and sample asks are gone; every card is a real matter).
28
+ //
29
+ // ONE CARD SHAPE (the ask format, GovLiving.askFormat): a question of at most
30
+ // twelve words, two or three tap answers each with a one-line result, a pick and
31
+ // why, who it waits on and what happens if nobody answers. Every ask the living
32
+ // model holds (gov-living.js: every docket matter and the open blockers) is
33
+ // drawn in it; the interiors draw the SAME card in place (street-office.js: a
34
+ // build-out vote with its picture, the reserve loan).
35
+ //
36
+ // REAL ASKS ARE REAL. A card over a real docket matter sends what its desk row
37
+ // sends: GovDesk.cardFor() gives the row (the same acts, chips, routes and
38
+ // permission checks as the floor desk and the street card), and an answer's
39
+ // `act` (the stored ask names it) is mapped to that row's act by actFor(). Nothing
40
+ // here invents a route. An answer that records nothing (not me, keep it off,
41
+ // hold) leaves the deck for this visit and says it still waits. The recorded
42
+ // sign-off opens its line in place (GovMatterCard.recordStrip / recordRun, the
43
+ // goal page's own two routes). The preview harness refuses writes with
44
+ // preview_readonly: the deck says the request reached its route and that the
45
+ // preview stores nothing, and clears the card, so the walk can be done there.
46
+ //
47
+ // BATCHING. After an answer, asks like it (same source, same building, the same
48
+ // answer offered) are offered three at a time as a card of their own, in the
49
+ // ask format: "same answer for these 3?" — the 21 Review sign-offs go in seven
50
+ // taps, not twenty-one.
51
+ //
52
+ // TWO HALVES. The model half is pure (no DOM, no fetch, no clock: `now` is
53
+ // passed in) and node-require-safe, so tests/hall_street_deck.mjs runs it
54
+ // without a browser. The page half (`page`) mounts into street.js through an env
55
+ // of street.js's own senders, draws #streetDeck, and is skipped where there is
56
+ // no document. It READS NOTHING: it draws the street's own living model
57
+ // (street.js data.living, over gov-feeds.js), and its numbers are gov-living.js
58
+ // countsOf over its session, the same numbers the street's header, the city and
59
+ // the shell's tally print (tests/hall_gov_counts.mjs).
60
+
61
+ (function (root) {
62
+ 'use strict';
63
+ const req = typeof require === 'function' ? require : null;
64
+ const L = root.GovLiving || (req ? req('./gov-living.js') : null);
65
+ const C = root.GovMatterCard || (req ? req('./street-card.js') : null);
66
+ const D = root.GovDesk || (req ? req('./street-desk.js') : null);
67
+
68
+ const NAMES = L ? L.BUILDING_NAMES : {};
69
+ const KEYS = L ? L.BUILDING_KEYS : [];
70
+ const BATCH = 3;
71
+ const SEATS = 5;
72
+ const str = (v) => (v == null ? '' : String(v));
73
+ const fit = (s, n, w) => (D && D.fit ? D.fit(s, n, w) : str(s).slice(0, n));
74
+
75
+ // ---------- an answer's act → the matter card's act ----------
76
+ // A stored ask's answers name the act they take (ask.options[].act).
77
+ // A blocker's three options are its decide chips; "force:<key>" is the force
78
+ // sign-off with that chip; an override is released (approve) or held (reject:
79
+ // the decline route is the Board Room's, so holding records nothing here).
80
+ const ALIAS = Object.freeze({ approve: 'release', reject: 'hold' });
81
+ const DECIDE = Object.freeze({ 'go ahead': 1, 'not now': 1, 'need more': 1 });
82
+ const QUIET = Object.freeze({ hold: 1, abstain: 1 });
83
+ function actFor(act) {
84
+ if (act == null || act === '') return null;
85
+ const a = str(act);
86
+ if (DECIDE[a]) return { key: 'decide', input: { option: a, label: a } };
87
+ const m = /^([a-z_]+):([a-z_]+)$/.exec(a);
88
+ if (m) return { key: m[1], input: { option: m[2] } };
89
+ return { key: ALIAS[a] || a, input: {} };
90
+ }
91
+
92
+ // What tapping an answer does, for this viewer:
93
+ // send the desk row's own request (plan.card is the row's card)
94
+ // record the recorded sign-off opens its line in place
95
+ // note records nothing: the ask leaves the deck this visit, still waiting
96
+ // off the viewer may not take this act from here (plan.why)
97
+ function optionPlan(ask, opt, cv) {
98
+ if (!ask || !opt) return { mode: 'off', why: 'nothing to answer' };
99
+ const a = actFor(opt.act);
100
+ if (!a || QUIET[a.key]) return { mode: 'note' };
101
+ const row = cv && cv.row;
102
+ if (!row || !Array.isArray(row.actions)) return { mode: 'off', why: cv && cv.loading ? 'reading the sitting…' : 'not yours to decide here' };
103
+ if (!row.actions.some((x) => x.key === a.key)) return { mode: 'off', why: 'not yours to do here' };
104
+ const choice = (row.choices || {})[a.key];
105
+ if (a.key === 'record') return choice && choice.file ? { mode: 'record', key: 'record', card: row.card, id: row.card && row.card.subject ? str(row.card.subject.id) : '' } : { mode: 'off', why: 'not yours to sign off' };
106
+ let input = a.input;
107
+ if (choice && !choice.file && !choice.field && choice.options.length) {
108
+ const hit = choice.options.find((o) => o.key === str(a.input.option));
109
+ if (!hit) return { mode: 'off', why: 'not an answer this matter takes' };
110
+ input = hit.input;
111
+ }
112
+ return { mode: 'send', key: a.key, input: input, card: row.card };
113
+ }
114
+
115
+ // ---------- one card ----------
116
+ // A seat the server routed (task 1004705, `waits_on.real`) is named; an ask
117
+ // the docket did not route waits on you, or on others, unnamed.
118
+ function seatWords(ask) {
119
+ const w = ask.waits_on || {};
120
+ if (!w.real) return ask.needs_you ? 'waits on you' : 'waits on others';
121
+ const label = str(w.label) || 'someone';
122
+ if (ask.needs_you) return label === 'you' || label === 'the owner' || /^the .* Lead$/.test(label) ? 'waits on you' : 'waits on you · ' + label;
123
+ return 'waits on ' + label;
124
+ }
125
+ // ---------- the words, in the asker's own voice (task 1004702) ----------
126
+ // The owner's pick (2026-10-13): a card speaks as "I", the agent that asked.
127
+ // The said line is the stored ask's why; a why not already said as "I" is
128
+ // led by the pick in that voice: "I'd keep it off." for a tap answer said as
129
+ // an imperative, "I'd pick the cheap host." for one said as a thing.
130
+ const lowerFirst = (s) => { const t = str(s); return t ? t.charAt(0).toLowerCase() + t.slice(1) : ''; };
131
+ const upperFirst = (s) => { const t = str(s); return t ? t.charAt(0).toUpperCase() + t.slice(1) : ''; };
132
+ const stop = (s) => { const t = str(s).trim(); return t ? t + (/[.!?]$/.test(t) ? '' : '.') : ''; };
133
+ function saidOf(why, rec) {
134
+ const w = stop(why);
135
+ if (/^I(?:'|’|\s)/.test(w)) return w;
136
+ const label = rec && rec.label ? str(rec.label).trim().replace(/[.!?]+$/, '') : '';
137
+ const thing = /^(the|a|an|my|your|our|this|that|these|those|one|all|both|none|neither|no|yes|not|nobody|someone)\b/i.test(label) || /^[0-9$]/.test(label);
138
+ const pick = !label ? '' : thing ? "I'd pick " + lowerFirst(label) + '.' : "I'd " + lowerFirst(label) + '.';
139
+ return [pick, upperFirst(w)].filter(Boolean).join(' ');
140
+ }
141
+ // The card's last line, as a sentence: who it waits on (the "you" half is the
142
+ // only salmon on it), then what happens if nobody answers.
143
+ function footOf(ask, silent) {
144
+ const w = (ask && ask.waits_on) || {};
145
+ const seat = str(w.seat);
146
+ let you = '';
147
+ let rest = '';
148
+ if (ask && ask.needs_you) {
149
+ const lead = /^lead:(\w+)$/.exec(seat);
150
+ if (lead && NAMES[lead[1]]) you = "It's the " + NAMES[lead[1]] + " Lead's call, and that's you.";
151
+ else if (/^(board|committee):/.test(seat) && w.real && str(w.label)) you = upperFirst(w.label) + ' answers ' + (ask.batch ? 'these' : 'this') + ', and you sit in it.';
152
+ else you = 'Only you can answer ' + (ask.batch ? 'these' : 'this') + '.';
153
+ } else if (ask) {
154
+ rest = 'It waits on ' + (str(w.label) || 'others') + '.';
155
+ }
156
+ const left = str(silent).trim() ? 'Left alone, ' + lowerFirst(stop(silent)) : '';
157
+ return { you: you, rest: [rest, left].filter(Boolean).join(' ') };
158
+ }
159
+ // Who holds a building, said to the viewer: their seat, its Lead, or the owner
160
+ // until a Lead is seated (GET /government/leads; unread names nobody).
161
+ function holdWords(living, key) {
162
+ if (!living || !living.seats_real) return '';
163
+ const b = (living.buildings || []).find((x) => x && x.key === key);
164
+ const v = living.viewer || {};
165
+ if (!b) return '';
166
+ if (b.lead) return str(b.lead.id) === str(v.id) ? 'Your seat.' : 'Led by ' + str(b.lead.name || 'its Lead') + '.';
167
+ const owner = (v.seats || []).some((s) => s && s.role === 'owner');
168
+ return owner ? 'You hold it until a Lead is seated.' : 'The owner holds it until a Lead is seated.';
169
+ }
170
+ // what waits there: the salmon half of a building's line while it is yours
171
+ function waitWords(onYou, others) {
172
+ if (onYou) return onYou === 1 ? 'One thing waits on you.' : onYou + ' wait on you.';
173
+ if (others) return others === 1 ? 'One thing waits on others.' : others + ' wait on others.';
174
+ return 'Nothing waits here.';
175
+ }
176
+
177
+ // How many sit on the board a 'board:<building>' seat names (the docket's real holders).
178
+ function boardSeats(docket, seat) {
179
+ const m = /^board:(\w+)$/.exec(str(seat));
180
+ const b = m && docket && Array.isArray(docket.buildings) ? docket.buildings.find((x) => x && x.key === m[1]) : null;
181
+ return b && Array.isArray(b.holders) ? Math.min(12, b.holders.length) : 0;
182
+ }
183
+ // ctx { docket, room, constitution, goals (open), now, viewerId }
184
+ function cardView(ask, ctx) {
185
+ const c = ctx || {};
186
+ const shown = ask && ask.shown;
187
+ if (!ask) return null;
188
+ const cv = D && D.cardFor && c.docket ? D.cardFor(ask.id, c.docket, { room: c.room, constitution: c.constitution, now: c.now || 0, goals: c.goals || [], viewerId: c.viewerId }) : null;
189
+ const age = D && D.ageWord ? D.ageWord(ask.opened_at, c.now || 0) : '';
190
+ const view = {
191
+ id: str(ask.id), building: ask.building, buildingName: NAMES[ask.building] || str(ask.building),
192
+ kicker: (NAMES[ask.building] || str(ask.building)) + (age ? ' · ' + age : ''),
193
+ askReal: !!ask.ask_real, needsYou: !!ask.needs_you, urgent: !!ask.urgent,
194
+ waits: seatWords(ask), seat: ask.waits_on ? str(ask.waits_on.seat) : '',
195
+ silent: '', question: '', options: [], rec: null, refused: !shown, errors: ask.format ? ask.format.errors : [],
196
+ routing: shown && shown.routing_note ? str(shown.routing_note) : '', tally: ask.tally || null, kind: ask.kind || null,
197
+ // a board's vote shows its seats, one pip each, lit as the sitting's tally says
198
+ seats: boardSeats(c.docket, ask.waits_on ? str(ask.waits_on.seat) : ''),
199
+ card: cv,
200
+ };
201
+ if (!shown) {
202
+ // no stored ask: the matter in its own words, and its floor's desk, where its acts are
203
+ view.question = fit(ask.raw ? ask.raw.title : '', 90);
204
+ view.desk = '/builders/street#building=' + str(ask.building);
205
+ view.foot = footOf(ask, '');
206
+ return view;
207
+ }
208
+ view.question = str(shown.question);
209
+ view.silent = str(shown.if_nobody_answers);
210
+ // the agent's own stored card (task 1004703), and its floor's desk beside it
211
+ view.desk = '/builders/street#building=' + str(ask.building);
212
+ view.options = (shown.options || []).map((o, i) => {
213
+ const plan = optionPlan(ask, o, cv);
214
+ return { n: i + 1, key: str(o.key), label: str(o.label), result: str(o.result), act: o.act == null ? null : str(o.act), rec: !!shown.recommend && str(shown.recommend.key) === str(o.key), plan: plan, off: plan.mode === 'off', why: plan.why || '' };
215
+ });
216
+ const r = shown.recommend ? view.options.find((o) => o.key === str(shown.recommend.key)) : null;
217
+ view.rec = r ? { label: r.label, why: str(shown.recommend.why) } : null;
218
+ // the words in the asker's voice, and the last line as a sentence (task 1004702)
219
+ view.said = ask.batch ? str(shown.said) : saidOf(shown.recommend ? shown.recommend.why : '', view.rec);
220
+ view.foot = footOf(ask, view.silent);
221
+ return view;
222
+ }
223
+
224
+ // ---------- the queue ----------
225
+ // Every ask that needs a person, in the model's order (needs-you first, then the oldest); answered and
226
+ // passed-on asks leave; skipped ones go to the back in the order skipped.
227
+ // session { done{}, delegated{}, skipped[], extra[] }
228
+ function queueOf(living, session, scope) {
229
+ const s = session || {};
230
+ const all = (living && living.city && Array.isArray(living.city.asks) ? living.city.asks : []).concat(Array.isArray(s.extra) ? s.extra : []);
231
+ const seen = {};
232
+ const open = all.filter((a) => {
233
+ if (!a || seen[a.id] || a.state !== 'needs') return false;
234
+ seen[a.id] = true;
235
+ if ((s.done || {})[a.id] || (s.delegated || {})[a.id]) return false;
236
+ return !scope || a.building === scope;
237
+ });
238
+ open.sort(L.compareAsks);
239
+ const skipped = (s.skipped || []).map(str);
240
+ const front = open.filter((a) => skipped.indexOf(a.id) < 0);
241
+ const back = skipped.map((id) => open.find((a) => a.id === id)).filter(Boolean);
242
+ return front.concat(back);
243
+ }
244
+ // The deck's session as gov-living.js countsOf reads one: what left it this visit
245
+ // (answered or passed on) and what a building added to it.
246
+ function sessionCounts(session) {
247
+ const s = session || {};
248
+ return { answered: Object.keys(s.done || {}).concat(Object.keys(s.delegated || {})), extra: Array.isArray(s.extra) ? s.extra : [] };
249
+ }
250
+ function scopesOf(living, session) {
251
+ const q = queueOf(living, session, null);
252
+ const by = {};
253
+ q.forEach((a) => { by[a.building] = (by[a.building] || 0) + 1; });
254
+ return KEYS.filter((k) => by[k]).map((k) => ({ key: k, label: NAMES[k], n: by[k], you: q.some((a) => a.building === k && a.needs_you) }));
255
+ }
256
+
257
+ // ---------- batching ----------
258
+ // Asks like this one: the same source at the same building, offering the same
259
+ // answer (key and act), which this viewer may give from here without a file.
260
+ function likesOf(queue, from, ctx) {
261
+ if (!from) return [];
262
+ return queue.filter((a) => {
263
+ if (a.id === from.id || a.source !== from.source || a.building !== from.building || !a.shown) return false;
264
+ const o = (a.shown.options || []).find((x) => str(x.key) === from.key && str(x.act) === str(from.act));
265
+ if (!o) return false;
266
+ const cv = D && D.cardFor && ctx && ctx.docket ? D.cardFor(a.id, ctx.docket, { room: ctx.room, constitution: ctx.constitution, now: ctx.now || 0, goals: ctx.goals || [], viewerId: ctx.viewerId }) : null;
267
+ const mode = optionPlan(a, o, cv).mode;
268
+ return mode === 'send' || mode === 'note';
269
+ });
270
+ }
271
+ // The batch is a card of its own, in the ask format (askFormat accepts it).
272
+ function batchAsk(from, likes, more) {
273
+ const n = likes.length;
274
+ const label = str(from.label);
275
+ return {
276
+ id: 'batch:' + from.id, source: 'batch', building: from.building, state: 'needs', needs_you: true, batch: true,
277
+ // who the likes wait on is who the batch waits on
278
+ waits_on: likes[0] && likes[0].waits_on ? likes[0].waits_on : { seat: 'you', label: 'you' }, opened_at: null,
279
+ likes: likes.map((a) => ({ id: a.id, question: a.shown ? str(a.shown.question) : '' })), more: more, from: from,
280
+ shown: {
281
+ question: 'Same answer for ' + (n === 1 ? 'this one' : 'these ' + n) + '?',
282
+ options: [
283
+ { key: 'all', act: null, label: n === 1 ? label : fit(label + ', all ' + n, 40, 5), result: n === 1 ? 'It gets the same answer and leaves your deck.' : 'Each gets the same answer and leaves your deck.' },
284
+ { key: 'one', act: null, label: 'One at a time', result: 'They come up one by one, as before.' },
285
+ ],
286
+ recommend: { key: 'all', why: 'They ask the same thing of the same seat.' },
287
+ waits_on: 'you', if_nobody_answers: n === 1 ? 'It waits in your deck.' : 'They wait in your deck.',
288
+ said: n === 1 ? "It asks what the last one asked. I'd answer it the same." : "They ask the same thing of the same seat. I'd answer them together.",
289
+ },
290
+ };
291
+ }
292
+
293
+ // ---------- passing it on ----------
294
+ // The seats an ask can be passed to: the owner and each building's Lead, never
295
+ // the seat it already waits on nor the viewer. Up to five, each a one-tap chip.
296
+ // The server passes it on (task 1004705, POST /government/asks/:matter/pass):
297
+ // only a stored ask that waits on you, on a routed docket, and only to real
298
+ // seats. Anything else cannot be passed on from here.
299
+ function seatsFor(living, ask, viewerId) {
300
+ const out = [];
301
+ if (!(ask && living && living.routed && ask.ask_real && ask.needs_you)) return out;
302
+ const add = (seat, label, sub) => { if (out.length < SEATS && seat !== (ask && ask.waits_on && ask.waits_on.seat) && !out.some((x) => x.seat === seat)) out.push({ seat: seat, label: fit(label, 16), sub: sub }); };
303
+ const o = living && living.owner;
304
+ if (o && str(o.id) !== str(viewerId)) add('owner', o.name || 'the owner', 'the owner');
305
+ (living ? living.buildings : []).forEach((b) => {
306
+ if (b.lead && str(b.lead.id) !== str(viewerId)) add('lead:' + b.key, b.lead.name, b.name + ' Lead');
307
+ });
308
+ return out;
309
+ }
310
+
311
+ // ---------- the whole deck ----------
312
+ // input { living, docket, room, constitution, goals, now, viewerId, scope,
313
+ // session { done, delegated, skipped, extra, batch, noBatch, decided } }
314
+ function deckModel(input) {
315
+ const o = input || {};
316
+ const session = o.session || {};
317
+ const living = o.living || null;
318
+ const scope = o.scope && KEYS.indexOf(o.scope) >= 0 ? o.scope : null;
319
+ const ctx = { docket: o.docket, room: o.room, constitution: o.constitution, goals: o.goals, now: o.now, viewerId: o.viewerId };
320
+ const queue = queueOf(living, session, scope);
321
+ // the deck's numbers are the ones every surface prints (gov-living.js countsOf),
322
+ // over the same session: answered and passed-on asks leave, this visit's join
323
+ const counts = L.countsOf(living, sessionCounts(session));
324
+ let current = null;
325
+ let batch = null;
326
+ const b = session.batch;
327
+ if (b && !(session.noBatch || {})[b.source + ':' + b.building]) {
328
+ const likes = likesOf(queue, b, ctx);
329
+ if (likes.length) {
330
+ const take = likes.slice(0, BATCH);
331
+ batch = batchAsk(b, take, likes.length - take.length);
332
+ current = cardView(batch, ctx);
333
+ current.batch = true; current.likes = batch.likes; current.more = batch.more; current.from = b;
334
+ current.kicker = take.length + ' more like this';
335
+ if (batch.more) current.foot.rest = [current.foot.rest, batch.more + ' more like ' + (batch.more === 1 ? 'this' : 'these') + ' after.'].filter(Boolean).join(' ');
336
+ }
337
+ }
338
+ if (!current && queue.length) current = cardView(queue[0], ctx);
339
+ const you = queue.filter((a) => a.needs_you).length;
340
+ const others = queue.length - you;
341
+ // the headline: what is on you while anything is, then what waits on others
342
+ const lead = you ? { n: you, words: L.countWords('on_you', you), you: true } : others ? { n: others, words: L.countWords('on_others', others), you: false } : { n: 0, words: 'to decide', you: false };
343
+ return {
344
+ scope: scope, scopeName: scope ? NAMES[scope] : '', remaining: queue.length, you: you, others: others, lead: lead, counts: counts,
345
+ title: queue.length ? lead.n + ' ' + lead.words : 'nothing to decide',
346
+ sub: !queue.length ? '' : you ? (others ? others + ' ' + L.countWords('on_others', others) + ' after these' : 'nothing waits on others') : 'nothing to do',
347
+ scopes: scopesOf(living, session), current: current, behind: Math.max(0, Math.min(2, queue.length - 1)),
348
+ queue: queue.map((a) => a.id),
349
+ // passing it on is the server's, over a routed docket, to seats GET /government/leads names (task 1004705)
350
+ pass: !!(living && living.routed && living.seats_real),
351
+ // how many cards are in hand: one, or the batch's likes ("3 of 38 to do")
352
+ hand: current ? (current.batch ? current.likes.length : 1) : 0,
353
+ };
354
+ }
355
+
356
+ const model = {
357
+ actFor: actFor, optionPlan: optionPlan, cardView: cardView, queueOf: queueOf, scopesOf: scopesOf, likesOf: likesOf, sessionCounts: sessionCounts,
358
+ batchAsk: batchAsk, seatsFor: seatsFor, deckModel: deckModel, saidOf: saidOf, footOf: footOf, holdWords: holdWords, waitWords: waitWords,
359
+ BATCH: BATCH, ALIAS: ALIAS,
360
+ };
361
+
362
+ // =====================================================================
363
+ // THE PAGE HALF. Mounted by street.js with an env of its own senders:
364
+ // env { data() feeds, getJson(url), send(req), say(text, desk), draw(),
365
+ // load(), mode(), today() decisions today, decided() takes a pip,
366
+ // done(id) the desk drops the row too }
367
+ // =====================================================================
368
+ // ---------- the glass card, drawn (task 1004702) ----------
369
+ // One drawing for every surface: the deck's panel beside its building, a
370
+ // building's card on the street, the card inside a building, and the city
371
+ // card's first ask (city.js). The words come first: the question, one line in
372
+ // the asker's voice, the answers (each with its result, the pick marked "my
373
+ // pick" in ink), then who it waits on and what happens if nobody answers, as
374
+ // a sentence. No uppercase kickers, no ruled sections; salmon only on the dot
375
+ // and the "you" words of what needs you.
376
+ // on { pick(option), skip(), pass(), seats[], passTo(seat), close(),
377
+ // record: { state, pick(file), send(), painted(strip) } } (any may be absent)
378
+ // o { where: the deck's top line, inner: in place inside a building's card,
379
+ // acts: skip / pass it on as text buttons, busy, recOpen, delegateOpen,
380
+ // rise, picture, qid, cls, pos ("1 of 38", a phone's where line) }
381
+ function drawAsk(doc, cv, on, o) {
382
+ o = o || {};
383
+ on = on || {};
384
+ const h = (tag, cls, text) => {
385
+ const el = doc.createElement(tag);
386
+ if (cls) el.className = cls;
387
+ if (text != null) el.textContent = String(text);
388
+ return el;
389
+ };
390
+ const btn = (cls, text, fn) => {
391
+ const b = h('button', cls, text);
392
+ b.setAttribute('type', 'button');
393
+ if (o.busy) b.setAttribute('disabled', '');
394
+ b.addEventListener('click', fn);
395
+ return b;
396
+ };
397
+ const card = h('article', 'ask' + (cv.needsYou ? ' ask--needs' : '') + (cv.batch ? ' ask--batch' : '') + (o.rise ? ' ask--rise' : '') + (o.inner ? ' ask--here' : '') + (o.cls ? ' ' + o.cls : ''));
398
+ card.setAttribute('data-ask', cv.id);
399
+ if (o.where) {
400
+ const where = h('div', 'ask__where');
401
+ const dot = h('span', 'ask__dot' + (cv.needsYou ? ' ask__dot--you' : ''));
402
+ dot.setAttribute('aria-hidden', 'true');
403
+ where.appendChild(dot);
404
+ where.appendChild(h('b', 'ask__bld', cv.buildingName));
405
+ where.appendChild(h('span', 'ask__sep', '·'));
406
+ where.appendChild(h('span', 'ask__waits ask__waits--wide', cv.waits));
407
+ if (o.pos) where.appendChild(h('span', 'ask__waits ask__waits--narrow', o.pos));
408
+ where.appendChild(h('span', 'ask__gap'));
409
+ if (on.close) {
410
+ const x = btn('ask__esc', null, on.close);
411
+ x.setAttribute('aria-label', 'Close');
412
+ x.setAttribute('aria-keyshortcuts', 'Escape');
413
+ x.appendChild(h('kbd', '', 'esc'));
414
+ where.appendChild(x);
415
+ }
416
+ card.appendChild(where);
417
+ }
418
+ const q = h(o.inner ? 'h3' : 'h2', 'ask__q', cv.question);
419
+ q.setAttribute('tabindex', '-1');
420
+ if (o.qid) q.setAttribute('id', o.qid);
421
+ card.appendChild(q);
422
+ if (cv.batch && cv.likes) {
423
+ const list = h('ul', 'ask__likes');
424
+ cv.likes.forEach((l) => list.appendChild(h('li', '', l.question)));
425
+ card.appendChild(list);
426
+ }
427
+ if (o.picture) card.appendChild(o.picture);
428
+ if (cv.said) card.appendChild(h('p', 'ask__said', cv.said));
429
+ if (!cv.options.length && cv.desk) {
430
+ // no stored ask: its acts are on its floor's desk
431
+ const said = h('p', 'ask__said');
432
+ const go = h('a', 'ask__desk', 'Answer it on its desk');
433
+ go.setAttribute('href', cv.desk);
434
+ said.appendChild(go);
435
+ said.appendChild(h('span', '', ', where its acts are.'));
436
+ card.appendChild(said);
437
+ }
438
+ if (cv.options.length) {
439
+ const opts = h('div', 'ask__opts');
440
+ opts.setAttribute('role', 'group');
441
+ opts.setAttribute('aria-label', 'answers');
442
+ cv.options.forEach((op) => {
443
+ const b = h('button', 'ask__opt' + (op.rec ? ' ask__opt--rec' : '') + (op.off ? ' ask__opt--off' : ''));
444
+ b.setAttribute('type', 'button');
445
+ b.setAttribute('data-key', op.key);
446
+ b.setAttribute('data-n', String(op.n));
447
+ b.setAttribute('aria-keyshortcuts', String(op.n));
448
+ if (op.off) { b.setAttribute('aria-disabled', 'true'); b.setAttribute('title', op.why); }
449
+ if (o.busy) b.setAttribute('disabled', '');
450
+ const n = h('kbd', 'ask__n', op.n);
451
+ n.setAttribute('aria-hidden', 'true');
452
+ b.appendChild(n);
453
+ b.appendChild(h('span', 'ask__label', op.label));
454
+ b.appendChild(h('span', 'ask__result', op.off ? op.why : op.result));
455
+ if (op.rec) b.appendChild(h('span', 'ask__pick', 'my pick'));
456
+ b.addEventListener('click', () => { if (on.pick) on.pick(op); });
457
+ opts.appendChild(b);
458
+ });
459
+ card.appendChild(opts);
460
+ }
461
+ if (on.record && o.recOpen && C && C.recordStrip) {
462
+ const strip = C.recordStrip(doc, on.record.state(), { pick: on.record.pick, send: on.record.send }, 'card', o.busy);
463
+ on.record.painted(strip);
464
+ const line = h('div', 'ask__record');
465
+ line.appendChild(h('span', 'ask__record-ask', 'the recording'));
466
+ line.appendChild(strip.node);
467
+ card.appendChild(line);
468
+ }
469
+ const f = cv.foot || { you: '', rest: '' };
470
+ if (f.you || f.rest || cv.seats) {
471
+ const foot = h('p', 'ask__foot');
472
+ if (f.you) foot.appendChild(h('span', 'ask__you', f.you));
473
+ if (f.you && f.rest) foot.appendChild(doc.createTextNode ? doc.createTextNode(' ') : h('span', '', ' '));
474
+ if (f.rest) foot.appendChild(h('span', '', f.rest));
475
+ if (cv.seats) {
476
+ // a board's vote: one pip per seat, lit as the sitting's tally counts them
477
+ const tally = h('span', 'ask__tally');
478
+ const t = cv.tally || {};
479
+ const voted = Math.min(cv.seats, (Number(t.for) || 0) + (Number(t.against) || 0));
480
+ const dots = h('span', 'desk-pips');
481
+ dots.setAttribute('aria-hidden', 'true');
482
+ for (let i = 0; i < cv.seats; i++) dots.appendChild(h('span', 'desk-pip' + (i < voted ? ' desk-pip--on' : '')));
483
+ tally.appendChild(dots);
484
+ tally.appendChild(h('span', '', voted + ' of ' + cv.seats + ' voted'));
485
+ foot.appendChild(tally);
486
+ }
487
+ card.appendChild(foot);
488
+ }
489
+ if (o.acts && (on.skip || on.pass)) {
490
+ const more = h('div', 'ask__more');
491
+ if (on.skip) { const sk = btn('ask__text', 'skip', on.skip); sk.setAttribute('aria-keyshortcuts', 'S'); sk.setAttribute('data-act', 'skip'); more.appendChild(sk); }
492
+ if (on.pass && !cv.batch && !(Array.isArray(on.seats) && !on.seats.length)) { const p = btn('ask__text', o.delegateOpen ? 'keep it' : 'pass it on', on.pass); p.setAttribute('aria-keyshortcuts', 'D'); p.setAttribute('aria-expanded', o.delegateOpen ? 'true' : 'false'); p.setAttribute('data-act', 'pass'); more.appendChild(p); }
493
+ card.appendChild(more);
494
+ }
495
+ if (o.delegateOpen && on.seats && on.seats.length) {
496
+ const chips = h('div', 'ask__seats');
497
+ chips.setAttribute('role', 'group');
498
+ chips.setAttribute('aria-label', 'pass it on to');
499
+ chips.appendChild(h('span', 'ask__seats-ask', 'pass it to'));
500
+ on.seats.forEach((st, i) => {
501
+ const c = btn('desk-chip desk-chip--choice ask__seat', st.label, () => on.passTo(st));
502
+ c.setAttribute('title', st.sub);
503
+ c.setAttribute('aria-keyshortcuts', String(i + 1));
504
+ chips.appendChild(c);
505
+ });
506
+ card.appendChild(chips);
507
+ }
508
+ return card;
509
+ }
510
+
511
+ // One answer through the same path a desk row takes: { ok, toast, sent }.
512
+ // `send(req)` is the page's own sender; the preview harness refuses writes
513
+ // with preview_readonly, and the card says the request reached its route.
514
+ function sendAnswer(opt, send) {
515
+ const plan = opt && opt.plan;
516
+ const sentence = (s) => upperFirst(stop(s));
517
+ if (!plan || plan.mode === 'off') return Promise.resolve({ ok: false, toast: sentence((plan && plan.why) || 'not from here') });
518
+ if (plan.mode === 'note') return Promise.resolve({ ok: true, toast: 'Noted. ' + opt.result });
519
+ const refused = C.check(plan.card, plan.key, plan.input);
520
+ if (refused) return Promise.resolve({ ok: false, toast: refused });
521
+ const r = C.request(plan.card, plan.key, plan.input);
522
+ if (!r) return Promise.resolve({ ok: true, toast: 'Noted. Nothing goes on the record, and it still waits.' });
523
+ return send(r).then((out) => {
524
+ if (out.ok) return { ok: true, toast: C.answer(plan.card, plan.key, out.body).toast || 'Done.', sent: r, stored: true };
525
+ if (out && out.status === 501 && out.body && out.body.error && out.body.error.code === 'preview_readonly') return { ok: true, toast: 'Sent to ' + r.method + ' ' + r.url.replace(/^\/api\/bongos/, '') + '. This preview stores nothing.', sent: r };
526
+ return { ok: false, toast: C.refusal(out.status, out.body) };
527
+ });
528
+ }
529
+
530
+ // ---------- the scene around a panel (task 1004702) ----------
531
+ // A light hush over the scene with a clear circle around the building that
532
+ // asks, a halo on its pin (a 24 s breath, still under reduced motion) and a
533
+ // hairline from the pin to the panel. `layer` is a box over `stage`; the
534
+ // panel is placed beside the pin when `place` (a wide stage), else left where
535
+ // the stylesheet puts it (a phone's sheet). Returns where it all went, or null
536
+ // when there is no layout to measure (a test's DOM).
537
+ const SVG_NS = 'http://www.w3.org/2000/svg';
538
+ const rect = (el) => (el && typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null);
539
+ function spot(doc, layer, stage, anchor, panel, o) {
540
+ o = o || {};
541
+ while (layer && layer.firstChild) layer.removeChild(layer.firstChild);
542
+ const s = rect(stage), a = rect(anchor);
543
+ if (!layer || !s || !a || !(a.width || a.height)) return null;
544
+ const px = Math.round(a.left + a.width / 2 - s.left);
545
+ const py = Math.round(a.top + a.height / 2 - s.top);
546
+ const at = o.place || panel;
547
+ if (o.place && panel) {
548
+ const w = panel.offsetWidth || 476, hgt = panel.offsetHeight || 420;
549
+ const gap = o.gap || 60;
550
+ let left = px + gap;
551
+ if (left + w > s.width - 16) left = px - gap - w;
552
+ left = Math.max(16, Math.min(left, s.width - w - 16));
553
+ const floor = (o.bottom != null ? o.bottom : s.height - 16) - hgt;
554
+ const top = Math.max(o.top != null ? o.top : 16, Math.min(py - 150, floor));
555
+ o.place.style.left = Math.round(left) + 'px';
556
+ o.place.style.top = Math.round(top) + 'px';
557
+ }
558
+ const hush = doc.createElement('div');
559
+ hush.className = 'gov-hush';
560
+ hush.setAttribute('aria-hidden', 'true');
561
+ hush.style.setProperty('--fx', px + 'px');
562
+ hush.style.setProperty('--fy', (py + (o.fy || 40)) + 'px');
563
+ hush.style.setProperty('--fr', (o.fr || 90) + 'px');
564
+ layer.appendChild(hush);
565
+ const halo = doc.createElement('span');
566
+ // salmon only while what it asks needs you
567
+ halo.className = 'gov-halo' + (o.you ? ' gov-halo--you' : '');
568
+ halo.setAttribute('aria-hidden', 'true');
569
+ halo.style.left = px + 'px';
570
+ halo.style.top = py + 'px';
571
+ layer.appendChild(halo);
572
+ const p = rect(at);
573
+ if (!p) return { x: px, y: py };
574
+ const pl = p.left - s.left, pr = p.right - s.left, pt = p.top - s.top, pb = p.bottom - s.top;
575
+ let x1 = px, y1 = py, x2, y2;
576
+ if (pt > py + 24) { y1 = py + 22; x2 = Math.max(pl + 24, Math.min(px, pr - 24)); y2 = pt; }
577
+ else if (pr < px) { x1 = px - 22; x2 = pr; y2 = Math.max(pt + 28, Math.min(py, pb - 28)); }
578
+ else { x1 = px + 22; x2 = pl; y2 = Math.max(pt + 28, Math.min(py, pb - 28)); }
579
+ const svg = doc.createElementNS(SVG_NS, 'svg');
580
+ svg.setAttribute('class', 'gov-leader');
581
+ svg.setAttribute('width', '1');
582
+ svg.setAttribute('height', '1');
583
+ svg.setAttribute('aria-hidden', 'true');
584
+ const line = doc.createElementNS(SVG_NS, 'line');
585
+ [['x1', x1], ['y1', y1], ['x2', x2], ['y2', y2], ['stroke', 'currentColor'], ['stroke-width', 1]].forEach((kv) => line.setAttribute(kv[0], String(kv[1])));
586
+ const dot = doc.createElementNS(SVG_NS, 'circle');
587
+ [['cx', x2], ['cy', y2], ['r', 2.5], ['fill', 'currentColor']].forEach((kv) => dot.setAttribute(kv[0], String(kv[1])));
588
+ svg.appendChild(line);
589
+ svg.appendChild(dot);
590
+ layer.appendChild(svg);
591
+ return { x: px, y: py };
592
+ }
593
+
594
+ const page = (function () {
595
+ let env = null;
596
+ const session = { done: {}, delegated: {}, skipped: [], extra: [], batch: null, noBatch: {}, delegateOpen: false, rec: {}, recOpen: null, busy: false, listeners: [] };
597
+ let lastDrawn = null;
598
+ let doc = null;
599
+
600
+ function data() { return env ? env.data() : {}; }
601
+ function now() { return Date.now(); }
602
+ // NO READS OF ITS OWN: the deck draws the street's living model (street.js
603
+ // data.living, built over gov-feeds.js), so the street, its header and the deck
604
+ // stand on one read of each feed, and an act's re-read reaches all three
605
+ function living() { const d = data(); return (d && d.living) || null; }
606
+ // the living model's feeds as the street read them (the office reads the viewer here)
607
+ function feeds() { const d = data(); return (d && d.livingFeeds) || {}; }
608
+
609
+ function here() {
610
+ const hsh = root.location ? str(root.location.hash) : '';
611
+ const m = /^#deck(?:=([a-z]+))?$/.exec(hsh);
612
+ if (!m) return null;
613
+ return { scope: m[1] && KEYS.indexOf(m[1]) >= 0 ? m[1] : null };
614
+ }
615
+ function viewerId() { const d = data(); return d.docket && d.docket.viewer ? str(d.docket.viewer.id) : null; }
616
+ function ctxOf() {
617
+ const d = data();
618
+ const lv = living();
619
+ return { living: lv, docket: d.docket, room: d.room, constitution: d.constitution, goals: (d.goals || []).filter((g) => g && g.status === 'open'), now: now(), viewerId: viewerId() };
620
+ }
621
+ function deck(scope) { return deckModel(Object.assign(ctxOf(), { scope: scope, session: session })); }
622
+
623
+ // ---------- small DOM ----------
624
+ function h(tag, cls, text) {
625
+ const el = doc.createElement(tag);
626
+ if (cls) el.className = cls;
627
+ if (text != null) el.textContent = String(text);
628
+ return el;
629
+ }
630
+ function clear(el) { while (el && el.firstChild) el.removeChild(el.firstChild); }
631
+ function btn(cls, text, on) {
632
+ const b = h('button', cls, text);
633
+ b.setAttribute('type', 'button');
634
+ if (session.busy) b.setAttribute('disabled', '');
635
+ b.addEventListener('click', on);
636
+ return b;
637
+ }
638
+ function pips(n, cls) {
639
+ const t = D && D.todayPips ? D.todayPips(n) : { pips: [], text: n + ' decided' };
640
+ const box = h('span', cls || 'deck-pips');
641
+ box.setAttribute('aria-hidden', 'true');
642
+ t.pips.forEach((p) => box.appendChild(h('span', 'street-today__pip' + (p.on ? ' street-today__pip--on' : '') + (p.fresh && lastDrawn !== n ? ' desk-pop' : ''))));
643
+ return box;
644
+ }
645
+
646
+ // ---------- the card, drawn (shared with the interiors) ----------
647
+ // handlers { pick(option), skip(), pass(), seats[], passTo(seat), record:
648
+ // { state, pick(file), send() } } — any may be absent
649
+ function cardEl(cv, on, opts) {
650
+ const o = Object.assign({ busy: session.busy, recOpen: session.recOpen === cv.id, delegateOpen: session.delegateOpen }, opts || {});
651
+ return drawAsk(doc, cv, on, o);
652
+ }
653
+
654
+ // ---------- the deck, drawn ----------
655
+ // The street stays in view (task 1004702): the card is a glass panel beside
656
+ // the building that asks, a hairline to its pin, the cards still to come two
657
+ // glass slivers under it, and a slim glass bar at the foot holds the run.
658
+ function open() { return !!here(); }
659
+ function draw() {
660
+ if (!env || !doc) return;
661
+ const box = doc.getElementById('streetDeck');
662
+ const win = doc.getElementById('streetWindow');
663
+ const at = here();
664
+ if (!box) return;
665
+ if (!at) {
666
+ if (!box.hidden) { box.hidden = true; clear(box); }
667
+ if (win && win.getAttribute('data-view') === 'deck') win.removeAttribute('data-view');
668
+ session.delegateOpen = false;
669
+ return;
670
+ }
671
+ const arriving = box.hidden;
672
+ box.hidden = false;
673
+ if (win) win.setAttribute('data-view', 'deck');
674
+ const dm = deck(at.scope);
675
+ paint(box, dm, arriving);
676
+ }
677
+ let rising = false;
678
+ function backTo(dm) {
679
+ const back = h('a', 'deck-dock__back', dm.scope ? 'back to ' + dm.scopeName : 'back to the street');
680
+ back.setAttribute('href', dm.scope ? '/builders/street#building=' + dm.scope : '/builders/street');
681
+ back.setAttribute('id', 'deckBack');
682
+ if (!dm.scope) back.addEventListener('click', (e) => { if (e && e.preventDefault) e.preventDefault(); leaveDeck(); });
683
+ return back;
684
+ }
685
+ function leaveDeck() {
686
+ const dm = deck(here() ? here().scope : null);
687
+ const to = dm.scope ? '/builders/street#building=' + dm.scope : '/builders/street';
688
+ if (root.history && root.history.pushState) { root.history.pushState(null, '', to); env.draw(); draw(); }
689
+ else if (root.location) root.location.hash = dm.scope ? '#building=' + dm.scope : '';
690
+ }
691
+ function paint(box, dm, arriving) {
692
+ clear(box);
693
+ // the scene's hush, the halo and the hairline: spot() fills this layer once the panel stands
694
+ const scene = h('div', 'deck-scene');
695
+ scene.setAttribute('aria-hidden', 'true');
696
+ box.appendChild(scene);
697
+ const wrap = h('div', 'deck-at');
698
+ for (let i = dm.behind; i >= 1; i--) { const g = h('div', 'deck-ghost deck-ghost--' + i); g.setAttribute('aria-hidden', 'true'); wrap.appendChild(g); }
699
+ const pos = dm.hand + ' of ' + dm.lead.n;
700
+ if (dm.current) {
701
+ const cv = dm.current;
702
+ const seats = cv.batch ? [] : seatsFor(living(), find(cv.id), viewerId());
703
+ const card = cardEl(cv, {
704
+ pick: (op) => pick(cv, op),
705
+ skip: () => skip(cv),
706
+ pass: () => togglePass(),
707
+ seats: seats,
708
+ passTo: (s) => passTo(cv, s),
709
+ close: leaveDeck,
710
+ record: recordHandlers(cv),
711
+ }, { rise: rising || arriving, where: true, acts: true, qid: 'deckTitle', cls: 'deck-panel gov-glass', pos: pos });
712
+ rising = false;
713
+ wrap.appendChild(card);
714
+ } else {
715
+ const q = h('div', 'deck-clear deck-panel gov-glass');
716
+ const tick = doc.createElementNS ? doc.createElementNS(SVG_NS, 'svg') : h('span');
717
+ if (tick.setAttribute) { tick.setAttribute('width', '56'); tick.setAttribute('height', '56'); tick.setAttribute('viewBox', '0 0 24 24'); tick.setAttribute('aria-hidden', 'true'); tick.setAttribute('class', 'deck-clear__tick desk-pop'); }
718
+ const ring = doc.createElementNS ? doc.createElementNS(SVG_NS, 'circle') : h('span');
719
+ if (ring.setAttribute) { ring.setAttribute('cx', '12'); ring.setAttribute('cy', '12'); ring.setAttribute('r', '9.5'); }
720
+ const mark = doc.createElementNS ? doc.createElementNS(SVG_NS, 'path') : h('span');
721
+ if (mark.setAttribute) mark.setAttribute('d', 'M7.5 12.2l3 3 6-6.5');
722
+ tick.appendChild(ring); tick.appendChild(mark);
723
+ q.appendChild(tick);
724
+ const line = h('p', 'deck-clear__line', dm.scope ? 'Nothing waits at ' + dm.scopeName : 'Nothing waits on a person');
725
+ line.setAttribute('id', 'deckTitle');
726
+ line.setAttribute('tabindex', '-1');
727
+ q.appendChild(line);
728
+ q.appendChild(h('p', 'deck-clear__sub', env.today() + ' decided today'));
729
+ wrap.appendChild(q);
730
+ }
731
+ box.appendChild(wrap);
732
+
733
+ // the dock: where you are in the run, today, the keys, the way back
734
+ const dock = h('div', 'deck-dock gov-glass');
735
+ dock.setAttribute('role', 'status');
736
+ const run = h('span', 'deck-dock__run');
737
+ run.appendChild(h('span', 'deck-dock__n' + (dm.lead.you ? ' deck-dock__n--you' : ''), dm.hand));
738
+ run.appendChild(h('span', 'deck-dock__of', ' of ' + dm.lead.n + ' ' + (dm.remaining ? dm.lead.words : 'to do')));
739
+ dock.appendChild(run);
740
+ dock.appendChild(h('span', 'deck-dock__sep'));
741
+ const today = h('span', 'deck-dock__today');
742
+ const n = env.today();
743
+ today.appendChild(pips(n));
744
+ today.appendChild(h('span', 'deck-dock__decided', n + ' decided today'));
745
+ dock.appendChild(today);
746
+ lastDrawn = n;
747
+ if (dm.current) {
748
+ dock.appendChild(h('span', 'deck-dock__sep'));
749
+ const keys = h('span', 'deck-dock__keys');
750
+ const g = h('span', 'deck-dock__k');
751
+ dm.current.options.forEach((op) => g.appendChild(h('kbd', '', op.n)));
752
+ if (dm.current.options.length) { g.appendChild(h('span', '', 'answer')); keys.appendChild(g); }
753
+ const key = (k, word, act, fn) => {
754
+ const b = h('button', 'deck-dock__k deck-dock__btn');
755
+ b.setAttribute('type', 'button');
756
+ b.setAttribute('data-act', act);
757
+ b.setAttribute('aria-keyshortcuts', k);
758
+ if (session.busy) b.setAttribute('disabled', '');
759
+ b.appendChild(h('kbd', '', k));
760
+ b.appendChild(h('span', '', word));
761
+ b.addEventListener('click', fn);
762
+ keys.appendChild(b);
763
+ };
764
+ key('S', 'skip', 'dock-skip', () => skip(dm.current));
765
+ // passing it on is the server's: only over a routed docket, a stored ask that waits on you
766
+ const seats = dm.current.batch ? [] : seatsFor(living(), find(dm.current.id), viewerId());
767
+ if (dm.pass && seats.length) key('D', session.delegateOpen ? 'keep it' : 'pass it on', 'dock-pass', togglePass);
768
+ dock.appendChild(keys);
769
+ }
770
+ dock.appendChild(backTo(dm));
771
+ box.appendChild(dock);
772
+ place(box, scene, wrap, dock, dm);
773
+ }
774
+ // Beside the building's pin on a wide stage; on a phone the panel is a sheet
775
+ // from the foot and the street scrolls so the building stays in view above it.
776
+ function anchorFor(building) {
777
+ const pins = doc.getElementById('streetPins');
778
+ const hit = pins && pins.querySelector ? pins.querySelector('.street-pin[data-building="' + building + '"] .street-pin__head') : null;
779
+ if (hit) return hit;
780
+ const doors = doc.getElementById('streetDoors');
781
+ return doors && doors.querySelector ? doors.querySelector('[data-door="' + building + '"]') : null;
782
+ }
783
+ function place(box, scene, wrap, dock, dm) {
784
+ const cv = dm.current;
785
+ const stage = doc.getElementById('hall-main');
786
+ const anchor = cv ? anchorFor(cv.building) : null;
787
+ if (!anchor || !stage || typeof stage.getBoundingClientRect !== 'function') return;
788
+ const narrow = root.matchMedia ? root.matchMedia('(max-width: 899px)').matches : false;
789
+ const win = doc.getElementById('streetWindow');
790
+ if (narrow && win && typeof win.getBoundingClientRect === 'function') {
791
+ const a = anchor.getBoundingClientRect(), wr = win.getBoundingClientRect();
792
+ win.scrollLeft += Math.round(a.left + a.width / 2 - (wr.left + wr.width / 2));
793
+ }
794
+ const d = rect(dock), s = rect(stage);
795
+ spot(doc, scene, stage, anchor, wrap, { place: narrow ? null : wrap, bottom: d && s ? d.top - s.top - 14 : null, top: 60, you: cv.needsYou });
796
+ }
797
+ function togglePass() { session.delegateOpen = !session.delegateOpen; draw(); focusCard(session.delegateOpen ? 'seat' : 'pass'); }
798
+
799
+ // ---------- acting ----------
800
+ function find(id) {
801
+ const lv = living();
802
+ return (lv ? lv.city.asks : []).concat(session.extra).find((a) => a && a.id === id) || null;
803
+ }
804
+ function cardNode() {
805
+ const box = doc.getElementById('streetDeck');
806
+ let hit = null;
807
+ const walk = (el) => { (el && el.children ? Array.prototype.slice.call(el.children) : []).forEach((k) => { if (!hit && /\bask\b/.test(str(k.className)) && k.getAttribute && k.getAttribute('data-ask')) hit = k; else walk(k); }); };
808
+ walk(box);
809
+ return hit;
810
+ }
811
+ function focusCard(what) {
812
+ const c = cardNode();
813
+ if (!c) { const t = doc.getElementById('deckTitle'); if (t && t.focus) t.focus(); return; }
814
+ let target = null;
815
+ const walk = (el) => (el && el.children ? Array.prototype.slice.call(el.children) : []).forEach((k) => {
816
+ if (target) return;
817
+ const cls = str(k.className);
818
+ if (what === 'seat' && /\bask__seat\b/.test(cls)) target = k;
819
+ else if (what === 'pass' && k.getAttribute && k.getAttribute('data-act') === 'pass') target = k;
820
+ else if (!what && /\bask__q\b/.test(cls)) target = k;
821
+ walk(k);
822
+ });
823
+ walk(c);
824
+ if (target && target.focus) target.focus();
825
+ }
826
+ // The card leaves (a class the stylesheet animates: it slides away, or sinks
827
+ // to the back for a skip), then the deck redraws with the next one rising.
828
+ function leave(cls, then) {
829
+ const c = cardNode();
830
+ if (c) c.className += ' ' + cls;
831
+ const go = () => { rising = true; session.delegateOpen = false; then(); draw(); focusCard(); };
832
+ if (typeof setTimeout !== 'function') return go();
833
+ setTimeout(go, 320);
834
+ }
835
+ function settle(ask, opt, toast) {
836
+ session.done[ask.id] = true;
837
+ if (env.done) env.done(ask.id);
838
+ env.decided();
839
+ env.say(toast, true);
840
+ session.batch = opt && (opt.plan.mode === 'send' || opt.plan.mode === 'note') ? { id: ask.id, source: ask.source, building: ask.building, key: opt.key, act: opt.act, label: opt.label } : null;
841
+ notify();
842
+ }
843
+ function preview(out) { return !!(out && out.status === 501 && out.body && out.body.error && out.body.error.code === 'preview_readonly'); }
844
+ function sentence(s) { const t = str(s); return t ? t.charAt(0).toUpperCase() + t.slice(1) + (/[.!?]$/.test(t) ? '' : '.') : ''; }
845
+ // One answer, through the same path a desk row takes: { ok, toast } once the
846
+ // route (or nothing, for a note) has answered. Nothing is
847
+ // counted here; settle() does that, after the card has left.
848
+ function perform(ask, opt) {
849
+ return sendAnswer(opt, env.send).then((res) => { if (res.stored && env.load) env.load(); return res; });
850
+ }
851
+ // A tap on an answer: sent first (the card waits, its buttons held), then
852
+ // the card slides away and the next one rises; a refusal keeps the card.
853
+ function pick(cv, op) {
854
+ if (session.busy || !op) return Promise.resolve(false);
855
+ if (cv.batch) return batch(cv, op);
856
+ const ask = find(cv.id);
857
+ if (!ask) return Promise.resolve(false);
858
+ if (op.plan.mode === 'record') { session.recOpen = ask.id; draw(); focusRecord(); return Promise.resolve(false); }
859
+ if (op.plan.mode === 'off') { env.say(sentence(op.why || 'not from here'), true); return Promise.resolve(false); }
860
+ session.busy = op.plan.mode === 'send';
861
+ if (session.busy) draw();
862
+ return perform(ask, op).then((res) => {
863
+ session.busy = false;
864
+ if (!res.ok) { env.say(res.toast, true); draw(); return false; }
865
+ return new Promise((done) => leave('ask--gone', () => { settle(ask, op, res.toast); done(true); }));
866
+ });
867
+ }
868
+ // Same answer for the likes, one request at a time; each counts a pip, and
869
+ // the first refusal stops the run and says why.
870
+ function batch(cv, op) {
871
+ const from = cv.from;
872
+ if (op.key === 'one') {
873
+ session.noBatch[from.source + ':' + from.building] = true;
874
+ session.batch = null;
875
+ return new Promise((done) => leave('ask--skip', () => done(true)));
876
+ }
877
+ const ctx = ctxOf();
878
+ const ids = cv.likes.map((l) => l.id);
879
+ let i = 0, n = 0;
880
+ session.busy = true;
881
+ draw();
882
+ const next = () => {
883
+ if (i >= ids.length) return Promise.resolve(null);
884
+ const ask = find(ids[i++]);
885
+ if (!ask) return next();
886
+ const vw = cardView(ask, ctx).options.find((x) => x.key === from.key);
887
+ if (!vw) return next();
888
+ return perform(ask, vw).then((res) => {
889
+ if (!res.ok) return res;
890
+ session.done[ask.id] = true;
891
+ if (env.done) env.done(ask.id);
892
+ runEffect(ask, vw);
893
+ env.decided();
894
+ n += 1;
895
+ return next();
896
+ });
897
+ };
898
+ return next().then((stop) => {
899
+ session.busy = false;
900
+ session.batch = from;
901
+ return new Promise((done) => leave('ask--gone', () => {
902
+ env.say(stop ? stop.toast : 'Same answer for ' + n + ' more: ' + str(from.label) + '.', true);
903
+ notify();
904
+ done(!stop);
905
+ }));
906
+ });
907
+ }
908
+ function skip(cv) {
909
+ if (session.busy) return;
910
+ if (cv.batch) { session.noBatch[cv.from.source + ':' + cv.from.building] = true; session.batch = null; leave('ask--skip', () => {}); return; }
911
+ session.skipped = session.skipped.filter((x) => x !== cv.id).concat([cv.id]);
912
+ session.batch = null;
913
+ leave('ask--skip', () => {});
914
+ }
915
+ function passTo(cv, seat, after) {
916
+ if (session.busy) return;
917
+ const gone = (toast) => {
918
+ session.delegated[cv.id] = seat.seat;
919
+ session.batch = null;
920
+ leave('ask--gone', () => { env.say(toast, true); notify(); if (after) after(); });
921
+ };
922
+ // the server moves it to the seat (task 1004705), or says why not
923
+ session.busy = true;
924
+ draw();
925
+ env.send({ method: 'POST', url: '/api/bongos/government/asks/' + encodeURIComponent(cv.id) + '/pass', body: { seat: seat.seat } }).then((out) => {
926
+ session.busy = false;
927
+ session.delegateOpen = false;
928
+ if (out.ok) { if (env.load) env.load(); gone('Passed to ' + seat.label + '. It leaves your deck.'); return; }
929
+ if (preview(out)) { gone('Sent to POST /government/asks/' + cv.id + '/pass. This preview stores nothing.'); return; }
930
+ env.say(C.refusal(out.status, out.body), true);
931
+ draw();
932
+ });
933
+ }
934
+ // the recorded sign-off on a deck card: its line, its two routes
935
+ function recState(id) { return session.rec[id] || (session.rec[id] = { file: null, phase: 'empty', pct: 0 }); }
936
+ let painter = null;
937
+ function recordHandlers(cv) {
938
+ const ask = find(cv.id);
939
+ const op = cv.options.find((o) => o.plan.mode === 'record');
940
+ if (!ask || !op) return null;
941
+ return {
942
+ state: () => recState(cv.id),
943
+ painted: (s) => { painter = s; },
944
+ pick: (f) => { C.recordPick(recState(cv.id), f); draw(); focusRecord(); },
945
+ send: () => {
946
+ session.busy = true;
947
+ draw();
948
+ return C.recordRun(op.plan.id, recState(cv.id), {
949
+ upload: (url, file, onPct) => C.uploadXhr(root.XMLHttpRequest, url, file, onPct),
950
+ send: env.send,
951
+ change: () => { draw(); },
952
+ progress: (st) => { if (painter) painter.paint(st); },
953
+ }).then((out) => {
954
+ session.busy = false;
955
+ if (!out || !out.ok) { draw(); focusRecord(); return false; }
956
+ delete session.rec[cv.id];
957
+ session.recOpen = null;
958
+ leave('ask--gone', () => settle(ask, op, C.answer(op.plan.card, 'record', out.body).toast));
959
+ return true;
960
+ });
961
+ },
962
+ };
963
+ }
964
+ function focusRecord() {
965
+ const c = cardNode();
966
+ let t = null;
967
+ const walk = (el) => (el && el.children ? Array.prototype.slice.call(el.children) : []).forEach((k) => { if (!t && k.getAttribute && /^record-(send|pick)$/.test(str(k.getAttribute('data-act')))) t = k; walk(k); });
968
+ walk(c);
969
+ if (t && str(t.tagName).toLowerCase() === 'label' && t.children && t.children[1]) t = t.children[1];
970
+ if (t && t.focus) t.focus();
971
+ }
972
+
973
+ // ---------- the keys: 1 / 2 / 3 pick, S skip, D pass it on ----------
974
+ function onKey(e) {
975
+ if (!open() || !e || e.ctrlKey || e.metaKey || e.altKey) return false;
976
+ const t = e.target;
977
+ const tag = t && t.tagName ? str(t.tagName).toLowerCase() : '';
978
+ if (tag === 'input' || tag === 'textarea' || tag === 'select' || (t && t.isContentEditable)) return false;
979
+ const dm = deck(here().scope);
980
+ const cv = dm.current;
981
+ const k = str(e.key);
982
+ if (!cv) { if (k !== 'Escape') return false; leaveDeck(); if (e.preventDefault) e.preventDefault(); return true; }
983
+ let done = false;
984
+ if (session.delegateOpen && /^[1-5]$/.test(k)) {
985
+ const seats = seatsFor(living(), find(cv.id), viewerId());
986
+ const s = seats[Number(k) - 1];
987
+ if (s) { passTo(cv, s); done = true; }
988
+ } else if (/^[1-3]$/.test(k)) {
989
+ const op = cv.options[Number(k) - 1];
990
+ if (op) { pick(cv, op); done = true; }
991
+ } else if (k === 's' || k === 'S') { skip(cv); done = true; }
992
+ else if ((k === 'd' || k === 'D') && !cv.batch) { session.delegateOpen = !session.delegateOpen; draw(); focusCard(session.delegateOpen ? 'seat' : 'pass'); done = true; }
993
+ else if (k === 'Escape' && session.delegateOpen) { session.delegateOpen = false; draw(); focusCard('pass'); done = true; }
994
+ else if (k === 'Escape') { leaveDeck(); done = true; }
995
+ if (done && e.preventDefault) e.preventDefault();
996
+ return done;
997
+ }
998
+
999
+ // ---------- a building's card on the street (task 1004702) ----------
1000
+ // A pin opens its building's card over it, in place: who holds it and what
1001
+ // waits, the ask inside (a matter's stored ask, answered through its desk
1002
+ // row's own route), then "go inside", "pass it on" and S to skip to the
1003
+ // pin's next matter. A matter with no stored ask keeps the matter card's own
1004
+ // acts (street.js fillCard); both stand as glass beside the pin.
1005
+ // on { close(), turn(key) the pin's next matter, done() redraw after an answer }
1006
+ let here2 = null;
1007
+ function streetCard(body, key, keys, on) {
1008
+ const ask = doc ? find(key) : null;
1009
+ if (!ask || !ask.shown || !body) { here2 = null; return false; }
1010
+ const lv = living();
1011
+ const cv = cardView(ask, ctxOf());
1012
+ clear(body);
1013
+ const head = h('div', 'bcard__head');
1014
+ head.appendChild(h('span', 'bcard__name', cv.buildingName));
1015
+ const q = queueOf(lv, session, ask.building);
1016
+ const you = q.filter((a) => a.needs_you).length;
1017
+ const hold = holdWords(lv, ask.building);
1018
+ const line = h('p', 'bcard__f', hold);
1019
+ line.appendChild(h('span', 'bcard__n' + (you ? ' bcard__n--you' : ''), (hold ? ' ' : '') + waitWords(you, q.length - you)));
1020
+ head.appendChild(line);
1021
+ body.appendChild(head);
1022
+ const seats = lv && lv.routed && lv.seats_real ? seatsFor(lv, ask, viewerId()) : [];
1023
+ const next = Array.isArray(keys) && keys.length > 1 ? keys[(keys.indexOf(key) + 1) % keys.length] : null;
1024
+ const acts = {
1025
+ pick: (op) => answerHere(ask.id, op).then((ok) => { if (ok) on.done(); return ok; }),
1026
+ seats: seats,
1027
+ passTo: (s) => passTo(cv, s, on.done),
1028
+ };
1029
+ body.appendChild(cardEl(cv, acts, { inner: true }));
1030
+ const row = h('div', 'bcard__acts');
1031
+ const inside = h('a', 'bcard__link', 'go inside');
1032
+ inside.setAttribute('href', '/builders/street#building=' + ask.building);
1033
+ row.appendChild(inside);
1034
+ if (seats.length) {
1035
+ const p = h('button', 'bcard__link', session.delegateOpen ? 'keep it' : 'pass it on');
1036
+ p.setAttribute('type', 'button');
1037
+ p.setAttribute('data-act', 'pass');
1038
+ p.setAttribute('aria-keyshortcuts', 'D');
1039
+ p.setAttribute('aria-expanded', session.delegateOpen ? 'true' : 'false');
1040
+ p.addEventListener('click', () => { session.delegateOpen = !session.delegateOpen; on.redraw(); });
1041
+ row.appendChild(p);
1042
+ }
1043
+ row.appendChild(h('span', 'ask__gap'));
1044
+ if (next) {
1045
+ const s = h('button', 'bcard__skip');
1046
+ s.setAttribute('type', 'button');
1047
+ s.setAttribute('data-act', 'skip');
1048
+ s.setAttribute('aria-keyshortcuts', 'S');
1049
+ s.appendChild(h('kbd', '', 'S'));
1050
+ s.appendChild(h('span', '', 'skip'));
1051
+ s.addEventListener('click', () => on.turn(next));
1052
+ row.appendChild(s);
1053
+ }
1054
+ body.appendChild(row);
1055
+ here2 = { cv: cv, acts: acts, next: next, on: on };
1056
+ const frame = body.parentNode;
1057
+ if (frame && !frame.govKeys && frame.addEventListener) {
1058
+ frame.govKeys = true;
1059
+ frame.addEventListener('keydown', streetKeys);
1060
+ }
1061
+ return true;
1062
+ }
1063
+ // 1 / 2 / 3 answer the card in place, S skips to the pin's next matter
1064
+ function streetKeys(e) {
1065
+ if (!here2 || !e || e.ctrlKey || e.metaKey || e.altKey || session.busy) return;
1066
+ const k = str(e.key);
1067
+ let done = false;
1068
+ if (/^[1-3]$/.test(k)) { const op = here2.cv.options[Number(k) - 1]; if (op) { here2.acts.pick(op); done = true; } }
1069
+ else if ((k === 's' || k === 'S') && here2.next) { here2.on.turn(here2.next); done = true; }
1070
+ if (done && e.preventDefault) e.preventDefault();
1071
+ }
1072
+ // The street card stands beside its pin, over a light hush (a phone keeps its sheet).
1073
+ function placeStreetCard(frame, key) {
1074
+ const stage = doc ? doc.getElementById('hall-main') : null;
1075
+ if (!frame || !stage || frame.hidden || typeof stage.getBoundingClientRect !== 'function') return unspot();
1076
+ const pins = doc.getElementById('streetPins');
1077
+ const list = pins && pins.children ? Array.prototype.slice.call(pins.children) : [];
1078
+ const pin = list.find((p) => str(p.getAttribute('data-matters') || p.getAttribute('data-matter')).split(' ').indexOf(str(key)) >= 0);
1079
+ const anchor = pin && pin.querySelector ? pin.querySelector('.street-pin__head') : null;
1080
+ let layer = doc.getElementById('streetSpot');
1081
+ if (!layer) {
1082
+ layer = doc.createElement('div');
1083
+ layer.setAttribute('id', 'streetSpot');
1084
+ layer.className = 'gov-spot';
1085
+ layer.setAttribute('aria-hidden', 'true');
1086
+ stage.appendChild(layer);
1087
+ }
1088
+ const narrow = root.matchMedia ? root.matchMedia('(max-width: 640px)').matches : false;
1089
+ layer.hidden = false;
1090
+ if (!anchor) { clear(layer); return null; }
1091
+ return spot(doc, layer, stage, anchor, frame, { place: narrow ? null : frame, top: 72, you: /street-pin--(needs|urgent)/.test(str(pin.className)) });
1092
+ }
1093
+ function unspot() {
1094
+ const layer = doc ? doc.getElementById('streetSpot') : null;
1095
+ if (layer) { clear(layer); layer.hidden = true; }
1096
+ here2 = null;
1097
+ session.delegateOpen = false;
1098
+ return null;
1099
+ }
1100
+
1101
+ // ---------- what the interiors share ----------
1102
+ function isDone(id) { return !!session.done[id] || !!session.delegated[id]; }
1103
+ function onChange(fn) { session.listeners.push(fn); }
1104
+ function notify() {
1105
+ session.listeners.forEach((fn) => { try { fn(); } catch (e) { /* a listener never blocks */ } });
1106
+ // every count on the page (the street's header, the shell's tally) follows the deck
1107
+ if (root.GovFeeds && typeof root.GovFeeds.changed === 'function') root.GovFeeds.changed();
1108
+ }
1109
+ // answer an ask from an interior card: the same path, then the interior redraws
1110
+ function answerHere(id, op) {
1111
+ const ask = find(id);
1112
+ if (!ask || session.busy || !op) return Promise.resolve(false);
1113
+ if (op.plan.mode === 'off') { env.say(sentence(op.why || 'not from here'), true); return Promise.resolve(false); }
1114
+ return perform(ask, op).then((res) => {
1115
+ if (!res.ok) { env.say(res.toast, true); return false; }
1116
+ settle(ask, op, res.toast);
1117
+ if (env && env.draw) env.draw();
1118
+ return true;
1119
+ });
1120
+ }
1121
+ function cardFor(ask) { return cardView(ask, ctxOf()); }
1122
+
1123
+ function mount(e) {
1124
+ env = e;
1125
+ doc = root.document || (typeof document !== 'undefined' ? document : null);
1126
+ if (!doc) return Promise.resolve();
1127
+ root.addEventListener('hashchange', () => { session.delegateOpen = false; draw(); const at = here(); if (at) { const t = doc.getElementById('deckTitle'); if (t && t.focus) t.focus(); } });
1128
+ root.addEventListener('popstate', draw);
1129
+ root.addEventListener('keydown', onKey);
1130
+ // the page's counts leave out what the deck answered (gov-feeds.js)
1131
+ if (root.GovFeeds && typeof root.GovFeeds.useSession === 'function') root.GovFeeds.useSession(() => sessionCounts(session));
1132
+ draw();
1133
+ return Promise.resolve();
1134
+ }
1135
+
1136
+ return {
1137
+ mount: mount, draw: draw, onKey: onKey, living: living, feeds: feeds, cardEl: cardEl, cardFor: cardFor, isDone: isDone, onChange: onChange,
1138
+ answerHere: answerHere, session: session, here: here, streetCard: streetCard, placeStreetCard: placeStreetCard, unspot: unspot,
1139
+ };
1140
+ }());
1141
+
1142
+ const api = Object.assign({ page: page, drawAsk: drawAsk, sendAnswer: sendAnswer, spot: spot }, model);
1143
+ root.GovDeck = api;
1144
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
1145
+ }(typeof window !== 'undefined' ? window : globalThis));