@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.10

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 (266) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/kiro-identity.d.ts +64 -0
  15. package/dist/backend/kiro-identity.js +101 -0
  16. package/dist/backend/kiro-identity.js.map +1 -1
  17. package/dist/backend/kiro-retired-model.d.ts +62 -0
  18. package/dist/backend/kiro-retired-model.js +160 -0
  19. package/dist/backend/kiro-retired-model.js.map +1 -0
  20. package/dist/backend/kiro.d.ts +71 -2
  21. package/dist/backend/kiro.js +352 -11
  22. package/dist/backend/kiro.js.map +1 -1
  23. package/dist/backend/types.d.ts +59 -0
  24. package/dist/backend/types.js.map +1 -1
  25. package/dist/backend-signin-guide.d.ts +1 -0
  26. package/dist/backend-signin-guide.js +9 -0
  27. package/dist/backend-signin-guide.js.map +1 -0
  28. package/dist/backend-signin-pending.d.ts +10 -0
  29. package/dist/backend-signin-pending.js +44 -0
  30. package/dist/backend-signin-pending.js.map +1 -0
  31. package/dist/browser-backend-signins.d.ts +56 -0
  32. package/dist/browser-backend-signins.js +189 -0
  33. package/dist/browser-backend-signins.js.map +1 -0
  34. package/dist/cache-ledger.d.ts +38 -2
  35. package/dist/cache-ledger.js +133 -8
  36. package/dist/cache-ledger.js.map +1 -1
  37. package/dist/cache-prices.d.ts +2 -1
  38. package/dist/cache-prices.js +5 -2
  39. package/dist/cache-prices.js.map +1 -1
  40. package/dist/cache-service.d.ts +68 -0
  41. package/dist/cache-service.js +355 -13
  42. package/dist/cache-service.js.map +1 -1
  43. package/dist/channel/adapters/discord.d.ts +11 -0
  44. package/dist/channel/adapters/discord.js +71 -14
  45. package/dist/channel/adapters/discord.js.map +1 -1
  46. package/dist/channel/mcp-tools.js +29 -2
  47. package/dist/channel/mcp-tools.js.map +1 -1
  48. package/dist/channel/tool-router.js +2 -1
  49. package/dist/channel/tool-router.js.map +1 -1
  50. package/dist/classic-channel-manager.d.ts +11 -0
  51. package/dist/classic-channel-manager.js +39 -1
  52. package/dist/classic-channel-manager.js.map +1 -1
  53. package/dist/cli.js +21 -2
  54. package/dist/cli.js.map +1 -1
  55. package/dist/config-validator.d.ts +7 -1
  56. package/dist/config-validator.js +53 -10
  57. package/dist/config-validator.js.map +1 -1
  58. package/dist/config.d.ts +19 -0
  59. package/dist/config.js +31 -1
  60. package/dist/config.js.map +1 -1
  61. package/dist/cpu-quota.d.ts +23 -0
  62. package/dist/cpu-quota.js +86 -0
  63. package/dist/cpu-quota.js.map +1 -0
  64. package/dist/daemon.d.ts +23 -0
  65. package/dist/daemon.js +106 -7
  66. package/dist/daemon.js.map +1 -1
  67. package/dist/dashboard-permissions.d.ts +20 -6
  68. package/dist/dashboard-permissions.js +58 -9
  69. package/dist/dashboard-permissions.js.map +1 -1
  70. package/dist/doctor.js +2 -1
  71. package/dist/doctor.js.map +1 -1
  72. package/dist/fleet-manager.d.ts +72 -0
  73. package/dist/fleet-manager.js +507 -89
  74. package/dist/fleet-manager.js.map +1 -1
  75. package/dist/forged-envelope-scan.d.ts +9 -0
  76. package/dist/forged-envelope-scan.js +30 -5
  77. package/dist/forged-envelope-scan.js.map +1 -1
  78. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  79. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  80. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +24 -1
  81. package/dist/general-knowledge/skills/web-appearance/SKILL.md +24 -1
  82. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +37 -9
  83. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +44 -11
  84. package/dist/instance-config-impact.js +10 -5
  85. package/dist/instance-config-impact.js.map +1 -1
  86. package/dist/instance-lifecycle.d.ts +19 -0
  87. package/dist/instance-lifecycle.js +55 -12
  88. package/dist/instance-lifecycle.js.map +1 -1
  89. package/dist/kiro-db-reader.d.ts +5 -0
  90. package/dist/kiro-db-reader.js +18 -0
  91. package/dist/kiro-db-reader.js.map +1 -1
  92. package/dist/kiro-engine-status.d.ts +2 -0
  93. package/dist/kiro-engine-status.js +9 -0
  94. package/dist/kiro-engine-status.js.map +1 -1
  95. package/dist/kiro-migrate.d.ts +49 -0
  96. package/dist/kiro-migrate.js +105 -0
  97. package/dist/kiro-migrate.js.map +1 -0
  98. package/dist/kiro-transcript-lane.d.ts +2 -0
  99. package/dist/kiro-transcript-lane.js.map +1 -1
  100. package/dist/kiro-transcript-worker.js +1 -0
  101. package/dist/kiro-transcript-worker.js.map +1 -1
  102. package/dist/kiro-tui-forensic.d.ts +7 -0
  103. package/dist/kiro-tui-forensic.js +110 -0
  104. package/dist/kiro-tui-forensic.js.map +1 -0
  105. package/dist/kiro-usage-run.d.ts +7 -0
  106. package/dist/kiro-usage-run.js +39 -0
  107. package/dist/kiro-usage-run.js.map +1 -0
  108. package/dist/kiro-usage-scan.d.ts +49 -0
  109. package/dist/kiro-usage-scan.js +223 -0
  110. package/dist/kiro-usage-scan.js.map +1 -0
  111. package/dist/kiro-usage-worker.d.ts +1 -0
  112. package/dist/kiro-usage-worker.js +10 -0
  113. package/dist/kiro-usage-worker.js.map +1 -0
  114. package/dist/kiro-usage.d.ts +58 -0
  115. package/dist/kiro-usage.js +310 -0
  116. package/dist/kiro-usage.js.map +1 -0
  117. package/dist/locale.js +88 -8
  118. package/dist/locale.js.map +1 -1
  119. package/dist/log-service.js +1 -1
  120. package/dist/log-service.js.map +1 -1
  121. package/dist/login-url.d.ts +4 -0
  122. package/dist/login-url.js +22 -0
  123. package/dist/login-url.js.map +1 -0
  124. package/dist/needs-you-hub.d.ts +2 -1
  125. package/dist/needs-you-hub.js +1 -1
  126. package/dist/needs-you-hub.js.map +1 -1
  127. package/dist/needs-you.d.ts +8 -2
  128. package/dist/needs-you.js +6 -0
  129. package/dist/needs-you.js.map +1 -1
  130. package/dist/notice-delivery.d.ts +16 -0
  131. package/dist/notice-delivery.js +35 -0
  132. package/dist/notice-delivery.js.map +1 -0
  133. package/dist/office-share-page.d.ts +16 -0
  134. package/dist/office-share-page.js +85 -0
  135. package/dist/office-share-page.js.map +1 -0
  136. package/dist/office-share-service.d.ts +91 -0
  137. package/dist/office-share-service.js +176 -0
  138. package/dist/office-share-service.js.map +1 -0
  139. package/dist/office-share.d.ts +258 -0
  140. package/dist/office-share.js +549 -0
  141. package/dist/office-share.js.map +1 -0
  142. package/dist/opencode-usage-scan.d.ts +28 -0
  143. package/dist/opencode-usage-scan.js +92 -0
  144. package/dist/opencode-usage-scan.js.map +1 -0
  145. package/dist/opencode-usage-worker.d.ts +1 -0
  146. package/dist/opencode-usage-worker.js +10 -0
  147. package/dist/opencode-usage-worker.js.map +1 -0
  148. package/dist/outbound-handlers.js +22 -2
  149. package/dist/outbound-handlers.js.map +1 -1
  150. package/dist/outbound-schemas.d.ts +26 -2
  151. package/dist/outbound-schemas.js +27 -2
  152. package/dist/outbound-schemas.js.map +1 -1
  153. package/dist/public-web-gateway.js +7 -1
  154. package/dist/public-web-gateway.js.map +1 -1
  155. package/dist/quickstart-api.js +13 -2
  156. package/dist/quickstart-api.js.map +1 -1
  157. package/dist/quickstart.js +2 -1
  158. package/dist/quickstart.js.map +1 -1
  159. package/dist/remote-card-answer.d.ts +83 -0
  160. package/dist/remote-card-answer.js +208 -0
  161. package/dist/remote-card-answer.js.map +1 -0
  162. package/dist/reply-triggers.d.ts +2 -0
  163. package/dist/reply-triggers.js +3 -0
  164. package/dist/reply-triggers.js.map +1 -0
  165. package/dist/settings-api.d.ts +2 -0
  166. package/dist/settings-api.js +41 -16
  167. package/dist/settings-api.js.map +1 -1
  168. package/dist/settings-change.js +17 -14
  169. package/dist/settings-change.js.map +1 -1
  170. package/dist/settings-effect.js +7 -1
  171. package/dist/settings-effect.js.map +1 -1
  172. package/dist/settings-instance-patch.js +4 -1
  173. package/dist/settings-instance-patch.js.map +1 -1
  174. package/dist/setup-form.d.ts +1 -1
  175. package/dist/setup-form.js +55 -6
  176. package/dist/setup-form.js.map +1 -1
  177. package/dist/setup-host.d.ts +9 -0
  178. package/dist/setup-host.js +127 -6
  179. package/dist/setup-host.js.map +1 -1
  180. package/dist/setup-signin-assets.d.ts +2 -0
  181. package/dist/setup-signin-assets.js +6 -0
  182. package/dist/setup-signin-assets.js.map +1 -0
  183. package/dist/setup-wizard.js +2 -0
  184. package/dist/setup-wizard.js.map +1 -1
  185. package/dist/structured-signin-transport.d.ts +8 -0
  186. package/dist/structured-signin-transport.js +79 -0
  187. package/dist/structured-signin-transport.js.map +1 -0
  188. package/dist/structured-signin.d.ts +131 -0
  189. package/dist/structured-signin.js +388 -0
  190. package/dist/structured-signin.js.map +1 -0
  191. package/dist/tool-permissions.js +12 -0
  192. package/dist/tool-permissions.js.map +1 -1
  193. package/dist/topic-commands.js +6 -2
  194. package/dist/topic-commands.js.map +1 -1
  195. package/dist/transcript-sources.d.ts +26 -2
  196. package/dist/transcript-sources.js +103 -40
  197. package/dist/transcript-sources.js.map +1 -1
  198. package/dist/turn-reply-guard.d.ts +1 -1
  199. package/dist/turn-reply-guard.js +1 -1
  200. package/dist/turn-reply-guard.js.map +1 -1
  201. package/dist/types.d.ts +28 -8
  202. package/dist/ui/backend-signin.js +63 -10
  203. package/dist/ui/chat-strings.js +2 -2
  204. package/dist/ui/fleet-office-editor.js +14 -9
  205. package/dist/ui/fleet-office-layout.js +70 -32
  206. package/dist/ui/fleet-office-model.js +6 -3
  207. package/dist/ui/fleet-office-overview.js +167 -0
  208. package/dist/ui/fleet-office-pack.d.ts +7 -2
  209. package/dist/ui/fleet-office-pack.js +119 -15
  210. package/dist/ui/fleet-office-skin.js +98 -23
  211. package/dist/ui/fleet-office-store.js +39 -5
  212. package/dist/ui/fleet-office-view.js +5 -4
  213. package/dist/ui/fleet-office-zones.js +172 -12
  214. package/dist/ui/fleet-office.js +547 -186
  215. package/dist/ui/fleet-usage.js +19 -15
  216. package/dist/ui/office-share-dialog.js +145 -0
  217. package/dist/ui/office-share-page.js +164 -0
  218. package/dist/ui/panel-chat.js +8 -1
  219. package/dist/ui/panel-log.js +2 -2
  220. package/dist/ui/panel-needs.js +4 -4
  221. package/dist/ui/panel-settings.js +37 -11
  222. package/dist/ui/settings-dialogs.js +8 -1
  223. package/dist/ui/settings-guests.js +17 -7
  224. package/dist/ui/settings-model.js +14 -6
  225. package/dist/ui/settings-registry.js +4 -0
  226. package/dist/ui/settings-strings.js +16 -2
  227. package/dist/ui/shared/app-i18n.js +4 -2
  228. package/dist/ui/shared/app-shell.js +14 -6
  229. package/dist/ui/shared/app-stream.js +2 -1
  230. package/dist/ui/shared/app.css +67 -2
  231. package/dist/ui/shared/office/cozy.png +0 -0
  232. package/dist/ui/shared/side-panel.js +95 -0
  233. package/dist/ui/structured-signin.js +115 -0
  234. package/dist/usage-report.d.ts +20 -0
  235. package/dist/usage-report.js +54 -17
  236. package/dist/usage-report.js.map +1 -1
  237. package/dist/usage-rollup.d.ts +9 -1
  238. package/dist/usage-rollup.js +20 -4
  239. package/dist/usage-rollup.js.map +1 -1
  240. package/dist/web-api.d.ts +18 -0
  241. package/dist/web-api.js +201 -0
  242. package/dist/web-api.js.map +1 -1
  243. package/dist/web-auth.js +1 -1
  244. package/dist/web-auth.js.map +1 -1
  245. package/dist/web-backend-login.d.ts +1 -7
  246. package/dist/web-backend-login.js +2 -24
  247. package/dist/web-backend-login.js.map +1 -1
  248. package/dist/web-office-acts.d.ts +173 -0
  249. package/dist/web-office-acts.js +285 -0
  250. package/dist/web-office-acts.js.map +1 -0
  251. package/dist/web-office-skins.d.ts +3 -2
  252. package/dist/web-office-skins.js +3 -2
  253. package/dist/web-office-skins.js.map +1 -1
  254. package/dist/web-office-store.js +40 -7
  255. package/dist/web-office-store.js.map +1 -1
  256. package/dist/web-settings-patch.d.ts +2 -1
  257. package/dist/web-settings-patch.js +4 -1
  258. package/dist/web-settings-patch.js.map +1 -1
  259. package/dist/working-recovery.d.ts +2 -2
  260. package/dist/working-recovery.js +43 -8
  261. package/dist/working-recovery.js.map +1 -1
  262. package/dist/working-state-journal.d.ts +2 -0
  263. package/dist/working-state-journal.js +16 -1
  264. package/dist/working-state-journal.js.map +1 -1
  265. package/package.json +3 -2
  266. package/templates/windows-shortcuts.ps1 +155 -140
@@ -1,12 +1,12 @@
1
1
  // Fleet → Org chart → Office: the fleet as a small pixel office, the third view beside Canvas and List
2
2
  // (fleet-canvas.js). The same data as theirs: the structure from /ui/org (buildOrgView), the live state from the app
3
- // store, the traffic from fleet-traffic.js's store — nothing new is read but the offices people made (/ui/offices).
3
+ // store, the traffic from fleet-traffic.js's store — nothing new is read but the offices people made (/ui/offices) and what agents do in them (/ui/office-acts, #2578).
4
4
  //
5
- // - Two layouts, by skin. Desks (the built-in "pixel" skin, the default): each team is a room, each member a desk with a
6
- // name tag; General and the instances in no team share the lobby; a grid that wraps at the stage's width
7
- // (fleet-office-model.js autoLayout). Zones (a skin whose pack maps states to areas — the built-in "studio"): a
8
- // 1280×720 scene; each instance takes a seat in the area for its state and walks there along the pack's waypoints
9
- // when its state changes (fleet-office-zones.js).
5
+ // - Two layouts, by skin. Desks (the built-in "pixel" skin; an unknown style, and a saved office with none): each team
6
+ // is a room, each member a desk with a name tag; General and the instances in no team share the lobby; a grid that
7
+ // wraps at the stage's width (fleet-office-model.js autoLayout). Zones (a skin whose pack maps states to areas — the
8
+ // built-in "studio", and "cozy" drawn from one picture, the default, #2631): a 1280×720 scene; each instance takes a
9
+ // seat in the area for its state and walks there along the pack's waypoints when its state changes (fleet-office-zones.js).
10
10
  // - #2540: the canvas fills the Fleet panel (no frame: the whole width of the Fleet area, down to the window's foot);
11
11
  // the scene is panned and zoomed as the org chart's canvas is (fleet-canvas-view.js: drag, wheel, pinch, + / − / Fit
12
12
  // and the `+ - 0` keys, an overview), Fit filling the panel (contain; Fill: cover), + and − in pixel levels, the zoom
@@ -34,6 +34,8 @@
34
34
  // ("+N", in its corner, never over its name); there idle and asleep people say nothing, at most a few speak (calls
35
35
  // first, then work), no bubble covers a name tag, and the tags show on hover or focus only. "Hide paused and stopped"
36
36
  // (per browser) leaves the sleepers out.
37
+ // - Share… (#2636; an admin, on the dashboard reached directly or over the public link): a link anyone can open to watch an office live, read-only,
38
+ // until it ends (office-share-dialog.js; the page it opens is office-share-page.js).
37
39
  // - Edit layout (an admin, a stored office on a zones skin): fleet-office-layout.js, saved as the office's overrides.
38
40
  // An editing session (#2513 review) is bound to the office it opened on, that office's style (the pack's id and sha)
39
41
  // and the list and rev read then: a re-read of the offices meanwhile (the stream) changes the view, never the session,
@@ -44,6 +46,14 @@
44
46
  // (webkit's prefix too); where a page cannot take an element fullscreen (an iPhone) or the browser refuses, "immersive":
45
47
  // the container covers the window (CSS). Either way only the stage shows, with its floating controls (zoom, Fit, the
46
48
  // overview, a compact office switcher, Exit); the stage's resize handling re-fits it. Nothing about it is kept.
49
+ // - All offices (#2554; offered with two or more stored offices): every stored office as a tile on one canvas, each
50
+ // drawn as its own view would be — its style (a pack loaded once and shared), its members, seats, poses, bubbles,
51
+ // walking and in-office traffic — scaled into its tile (fleet-office-overview.js: the tiles, the lobby tile of whoever
52
+ // is in no office, hit-testing, culling, the flights between tiles). The same canvas, controls and loop; tiles off
53
+ // screen are not drawn. A tile's label (a button, a readable size whatever the zoom) or a double-click on it opens
54
+ // that office; a person opens their chat. A tile too small to read (under TAG_MIN_K) shows name tags on hover only;
55
+ // bubbles are drawn after every tile (never cut at a tile's edge). Fit keeps the tiles clear of the controls
56
+ // (FIT_PAD). Edit offices and Edit layout wait for a single office.
47
57
  import { html, useEffect, useLayoutEffect, useMemo, useRef, useState } from "/assets/app-html.js";
48
58
  import { appStore } from "/assets/app-store.js";
49
59
  import { t, lang, register } from "/assets/app-i18n.js";
@@ -53,12 +63,15 @@ import { nodeText } from "./fleet-org.js";
53
63
  import { hits, needle } from "./fleet-canvas-layout.js";
54
64
  import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
55
65
  officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
56
- import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadPackSkin, modeOf, skinFor, tagBox } from "./fleet-office-skin.js";
57
- import { CALLING, activeFurniture, bubbleAt, bubbleRank, createWalkers, linesFor, personAt, placeBubbles, thinBubbles, zonesLayout } from "./fleet-office-zones.js";
58
- import { createOfficeStore } from "./fleet-office-store.js";
66
+ import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadCozySkin, loadPackSkin, modeOf, skinFor, tagBox } from "./fleet-office-skin.js";
67
+ import { CALLING, activeFurniture, bubbleAt, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
68
+ import { actsOk, createOfficeStore } from "./fleet-office-store.js";
69
+ import { POLL_MS } from "./fleet-traffic.js";
59
70
  import { OfficeEditor, officeForSave } from "./fleet-office-editor.js";
60
71
  import { LayoutEditor, handlesOf } from "./fleet-office-layout.js";
61
- import { applyOverrides } from "./fleet-office-pack.js";
72
+ import { ShareButton } from "./office-share-dialog.js";
73
+ import { applyOverrides, fitNight } from "./fleet-office-pack.js";
74
+ import { ALL_ID, LOBBY_TILE, OV, createCrossFlights, cropToRooms, fromTile, inTile, labelAt, lobbyNodes, overviewOk, tileAt, tileBox, tileLayout, visibleTiles } from "./fleet-office-overview.js";
62
75
  import { centreOn, gestureState, gestures, miniPoint, toScreen, toWorld, wheelFactor, zoomKey, zoomTo } from "./fleet-canvas-view.js";
63
76
  import { OK_MAX, OK_MIN, clampPan, desksK, edgeMatte, edgeStrips, fitScene, layerScale, miniBox, miniFrame, openingView, pinchOffice, savedOf, stepK, viewFromSaved,
64
77
  zoomLabel, zoomOffice } from "./fleet-office-view.js";
@@ -70,13 +83,15 @@ register("orgo", {
70
83
  people: "People", peopleN: "People ({0})", findPerson: "Find a person", everyone: "Show everyone", nobody: "No one here matches this filter.",
71
84
  unseated: "{0} without a seat", skinUnknown: "Its style “{0}” is not available here: drawn as Desks.", closed: "Closed", desks: "Desks", tapAgain: "Tap again to open the chat",
72
85
  capWorking: "Typing away", capIdle: "Tea break", capPaused: "Asleep", capNeeds: "Calling for you", capAway: "Desk empty", capStarting: "Just arriving", capQueued: "Waiting to start",
73
- styleLabel: "Style", style_pixel: "Desks", style_studio: "Studio", area_desks: "Work", area_lounge: "Lounge", area_alert: "Help desk", area_nap: "Nap corner",
86
+ styleLabel: "Style", style_pixel: "Desks", style_studio: "Studio", style_cozy: "Cozy", cozyNotice: "Add your skin", area_desks: "Work", area_lounge: "Lounge", area_alert: "Help desk", area_nap: "Nap corner",
74
87
  b_working: "On it.|Typing…|Almost there|In the zone|One more test", b_idle: "Tea time|Ready when you are|Stretching|Anything for me?",
75
88
  b_paused: "Zzz…|Back later", b_stopped: "Off duty|Zzz…", b_needs: "Need you!|A question for you|Over here!",
76
89
  b_crashed: "Ouch, crashed|Help!", b_stuck: "Hmm, stuck?|Still thinking…",
77
- hideAsleep: "Hide paused and stopped", layoutIgnored: "Its saved layout does not fit this style: drawn as the style's own.",
90
+ hideAsleep: "Hide paused and stopped", nightOn: "Bedtime 0–6: the idle sleep", layoutIgnored: "Its saved layout does not fit this style: drawn as the style's own.",
78
91
  fit: "Fit the office", overview: "Overview: click to move there", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen",
79
92
  fill: "Fill", fillTitle: "Fill the panel (the scene's overflow is cropped; drag to see it)", zoomLevel: "Zoom {0}",
93
+ allOffices: "All offices", labelAll: "All offices: each office is a tile, drawn in its own style. Drag to pan; the wheel, a pinch, + and − zoom, 0 fits, F is fullscreen. Double-click a tile to open that office.",
94
+ tileLabel: "{0} ({1})", openOffice: "Open the office {0}", lobbyTile: "Lobby tile (in no office)", lobbyEveryone: "Everyone is in an office", overviewNoEdit: "Pick one office to edit offices or a layout",
80
95
  },
81
96
  "zh-TW": {
82
97
  label: "辦公室:每個 team 是一間房間,每個 instance 一張桌子;General 和未加入 team 的 instance 在大廳。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示整個場景,F 切換全螢幕。",
@@ -84,13 +99,15 @@ register("orgo", {
84
99
  people: "成員", peopleN: "成員({0})", findPerson: "尋找成員", everyone: "顯示全部", nobody: "沒有符合這個篩選的成員。",
85
100
  unseated: "{0} 位沒有座位", skinUnknown: "這裡沒有它的風格「{0}」:以辦公桌顯示。", closed: "休息中", desks: "桌子", tapAgain: "再點一下開啟聊天",
86
101
  capWorking: "埋頭打字中", capIdle: "喝茶休息", capPaused: "睡著了", capNeeds: "在呼叫你", capAway: "空位", capStarting: "剛到座位", capQueued: "等待啟動",
87
- styleLabel: "風格", style_pixel: "辦公桌", style_studio: "工作室", area_desks: "工作區", area_lounge: "休息區", area_alert: "求救區", area_nap: "午睡角",
102
+ styleLabel: "風格", style_pixel: "辦公桌", style_studio: "工作室", style_cozy: "溫馨", cozyNotice: "換上你的外觀包", area_desks: "工作區", area_lounge: "休息區", area_alert: "求救區", area_nap: "午睡角",
88
103
  b_working: "處理中|打字中…|快好了|專心中|再測一次", b_idle: "喝茶中|隨時可以開工|伸個懶腰|有事找我嗎?",
89
104
  b_paused: "Zzz…|晚點回來", b_stopped: "下班了|Zzz…", b_needs: "需要你!|有個問題|在這裡!",
90
105
  b_crashed: "糟糕,當掉了|救命!", b_stuck: "嗯,卡住了?|還在想…",
91
- hideAsleep: "隱藏暫停與停止的成員", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
106
+ hideAsleep: "隱藏暫停與停止的成員", nightOn: "0–6 點就寢:閒著的人去睡", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
92
107
  fit: "顯示整個辦公室", overview: "總覽:點一下移到那裡", fullscreen: "全螢幕", exitFullscreen: "結束全螢幕",
93
108
  fill: "填滿", fillTitle: "填滿整個面板(超出的部分會被裁切,拖曳即可看到)", zoomLevel: "縮放 {0}",
109
+ allOffices: "所有辦公室", labelAll: "所有辦公室:每間辦公室是一格,以它自己的風格顯示。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示全部,F 切換全螢幕。雙擊一格即可開啟那間辦公室。",
110
+ tileLabel: "{0}({1})", openOffice: "開啟辦公室「{0}」", lobbyTile: "大廳格(不在任何辦公室)", lobbyEveryone: "每個成員都已在某間辦公室", overviewNoEdit: "請先選一間辦公室,再編輯辦公室或配置",
94
111
  },
95
112
  });
96
113
  const to = (k, ...v) => t(`orgo.${k}`, ...v);
@@ -105,6 +122,10 @@ const reducedMotion = () => typeof matchMedia === "function" && matchMedia("(pre
105
122
  const deviceRatio = () => Math.max(0.5, Math.min(4, Number(globalThis.devicePixelRatio) || 1));
106
123
  /** The stage's size before it is measured (a test's page has none). */
107
124
  const SIZE0 = { w: 960, h: 600 };
125
+ /** #2554: All offices' Fit keeps this many CSS px clear round the tiles (the zoom controls' column and a margin). */
126
+ const FIT_PAD = 60;
127
+ /** #2554: in All offices a tile's name tags show (always) from this many CSS px a unit up; below it on hover only. */
128
+ export const TAG_MIN_K = 0.75;
108
129
  const PAN_KEYS = { ArrowLeft: [-1, 0], ArrowRight: [1, 0], ArrowUp: [0, -1], ArrowDown: [0, 1] };
109
130
  /** Typing: no single-key shortcut (the `f` of fullscreen) there. */
110
131
  const typing = (el) => !!el && (el.tagName === "INPUT" || el.tagName === "SELECT" || el.tagName === "TEXTAREA" || el.isContentEditable === true);
@@ -155,6 +176,234 @@ function teamOptions(view) {
155
176
  ...view.teams.map((tm, i) => ({ value: `t${i}`, team: tm.name, label: tm.name }))];
156
177
  }
157
178
 
179
+ // ── A scene: one office's layout, looks and skin, drawn by itself (the single view) or as a tile (All offices, #2554) ──
180
+
181
+ /** What each desk shows: the pose, the light, the coat; dimmed when the Find box matches someone else. */
182
+ function looksOf(L, work, q, mode, zones) {
183
+ return new Map(L.rooms.flatMap((r) => r.desks).map((d) => {
184
+ const n = d.node;
185
+ if (!n) return [d.key, { name: "", label: "", state: "missing", pose: "away", light: null, lamp: false, sign: null, coat: "", work: 0, dim: !!q, match: false, free: zones }];
186
+ const match = !!q && hits(n, q);
187
+ // At night (#2568) the idle are drawn asleep (their state stays idle: the tooltip and the list say so).
188
+ return [d.key, { name: n.name, label: nodeText(n).primary, state: n.state, pose: officePose(d.night ? "paused" : n.state), night: !!d.night, light: lightOf(n.state), lamp: lampOf(n.state),
189
+ sign: zones ? null : signOf(n.state), coat: coatOf(n.i, n.i ? identityOf(n.i) : null, mode), work: work.get(n.name) || 0, dim: !!q && !match, match, free: zones }];
190
+ }));
191
+ }
192
+ /** A stored office's own layout on a zones style (its overrides); `ignored`: they do not fit this style. */
193
+ function withOverrides(baseSkin, zones, office) {
194
+ const ov = zones && office ? office.overrides : null;
195
+ if (!ov || ov.pack !== baseSkin.id) return { skin: baseSkin, ignored: false };
196
+ const a = applyOverrides(baseSkin.pack, fitNight(baseSkin.pack, ov)); // #2665: a night place the pack lost: left out
197
+ return a.ok ? { skin: { ...baseSkin, pack: a.pack }, ignored: false } : { skin: baseSkin, ignored: true };
198
+ }
199
+ /** A stored office's scene (or the lobby tile's): seats in its style's areas, its own seats, or one room wrapping at `deskW`. */
200
+ function officeScene(office, nodes, skin, zones, deskW, prevSeats, night = false) {
201
+ if (zones) return zonesLayout(skin.pack, nodes, prevSeats, { night });
202
+ const lay = office.layout || layoutOk(skin.layout);
203
+ return lay ? customLayout({ ...office, layout: lay }, nodes) : autoLayout([{ key: office.id, name: office.name, lobby: !!office.lobby, nodes }], { width: deskW });
204
+ }
205
+ /** What drawPart needs of a scene; its layers are painted on first draw (and again at another resolution). */
206
+ function partOf(L, looks, skin, zones, walkers, flights) {
207
+ const all = L.rooms.flatMap((r) => r.desks);
208
+ const shown = new Set([...looks.values()].map((l) => l.state)), byName = new Map([...looks.values()].filter((l) => l.name).map((l) => [l.name, l.state]));
209
+ return { L, looks, skin, zones, walkers, flights, all, desks: all.filter((d) => d.node), layers: null,
210
+ active: zones ? activeFurniture(skin.pack, shown, byName) : new Set(), lines: bubbleDefaults(), lang: lang() };
211
+ }
212
+ /**
213
+ * At bedtime (#2568): each item with a `night` drawn as it — its strip playing, at its night place: the office's
214
+ * (Edit layout, #2665: applyOverrides puts it on `night`), else the pack's night x/y, else its (moved) day place.
215
+ */
216
+ export const nightFurniture = (list) => list.map((f) => (f.night ? { ...f, sprite: f.night.sprite, activeSprite: null, x: f.night.x ?? f.x, y: f.night.y ?? f.y, asleep: true } : f));
217
+ /** #2596: each NPC with a `walk` where its walker is now (`at(key)`): moved there — its tag and depth with it — and, while
218
+ * it walks, `walking` and its `heading` (its walk strip: walkStrip); asleep, or with no walker (not strolling), where it is. */
219
+ export const strollFurniture = (list, at) => list.map((f) => {
220
+ const w = f.walk && !f.asleep ? at(npcKey(f)) : null;
221
+ return w ? { ...f, x: w.x, y: w.y, walking: !!w.walking, heading: w.heading || null } : f;
222
+ });
223
+ /** Where a seated person's bubble points: over the head of the picture drawn — a pack's figure can be taller than the
224
+ * built-in one (#2558: a 134 px figure had its face under the bubble); never lower than the built-in spot. */
225
+ export function speakerAt(sk, look, at) {
226
+ const top = sk.charBox ? sk.charBox(look, at).y + 2 : Infinity;
227
+ return { x: at.x + 10, y: Math.min(at.y - 50, top) };
228
+ }
229
+ /** The pose an area's seats are for: the first state (asleep, working, calling, idle) the pack sends there. */
230
+ const GHOST_POSE = [["paused", "paused"], ["stopped", "paused"], ["working", "working"], ["needs", "needs"], ["crashed", "needs"], ["stuck", "needs"], ["idle", "idle"]];
231
+ /** How faint Edit layout's figures are: seen through, yet readable over a busy picture (a dimmed person is 0.35). */
232
+ const GHOST_ALPHA = 0.6;
233
+ /** Edit layout's faint figures: one on each seat no one is on, { x, y, look } (the pack's default set; drawn at GHOST_ALPHA). */
234
+ export function ghostsOf(pack, people = []) {
235
+ const taken = (s) => people.some((q) => Math.abs(q.at.x - s.x) < 2 && Math.abs(q.at.y - s.y) < 2);
236
+ const out = [];
237
+ for (const [name, a] of Object.entries(pack.areas)) {
238
+ const hit = GHOST_POSE.find(([st]) => pack.stateZones && pack.stateZones[st] === name);
239
+ const pose = hit ? hit[1] : "idle";
240
+ for (const s of a.seats) if (!taken(s)) out.push({ x: s.x, y: s.y, look: { key: `ghost:${name}`, name: "", label: "", state: pose, pose, coat: "#8a8f99", free: true } });
241
+ }
242
+ return out;
243
+ }
244
+ /** #2665: Edit layout's faint night places by day — each item with a `night` as it is drawn at bedtime (nightFurniture). */
245
+ export const nightGhostsOf = (pack) => nightFurniture(pack.furniture.filter((f) => f.night));
246
+ /** All offices: the tile `name` is drawn in ({ tile, skin }), or null. */
247
+ const senderTile = (parts, name) => (name ? parts.find((p) => desksOf(p.L).some((d) => d.node.name === name)) || null : null);
248
+ /**
249
+ * Whether an agent's act (office_set, #2578) holds now: not past its end; its person not called (a call wins) nor
250
+ * asleep; and no new work begun since — `seen` (kept by the page) remembers each act whose person was seen not working,
251
+ * so working again after that (or after acting while idle) is new work, and the act gives way. The turn it acted in
252
+ * (working at the time) keeps it.
253
+ */
254
+ export function actLive(a, state, at = Date.now(), seen = null) {
255
+ if (!a || at >= a.until || CALLING.has(state) || ASLEEP.has(state)) return false;
256
+ if (!seen) return true;
257
+ const k = `${a.instance}@${a.since ?? a.until}`;
258
+ if (state !== "working") { seen.add(k); return true; }
259
+ return !seen.has(k) && (!a.state || a.state === "working");
260
+ }
261
+ /** How far beside the person an agent going to them stands. */
262
+ const ACT_BESIDE = 48;
263
+ /** Where an agent's act sends it at `t`: its point, beside the person it goes to (where they are now), or null. */
264
+ export function actTarget(a, L, walkers, t) {
265
+ if (a.x !== undefined) return { x: a.x, y: a.y };
266
+ if (!a.person) return null;
267
+ const d = desksOf(L).find((x) => x.node.name === a.person), there = (walkers && walkers.at(a.person, t)) || (d && d.seat);
268
+ return there ? { x: Math.round(Math.min(L.width, there.x + ACT_BESIDE)), y: Math.round(there.y) } : null;
269
+ }
270
+ /**
271
+ * A look in its agent's own pose (office_set, #2578) while its act holds, else as it is. In bed for the night (#2568,
272
+ * `look.night`) it is asleep, as for mail (#2569): bedtime wins over an act.
273
+ */
274
+ function actPosed(p, look) {
275
+ const act = look && p.acts && p.acts.get(look.name);
276
+ return act && act.pose && actLive(act, look.night ? "paused" : look.state, Date.now(), p.actSeen) ? { ...look, pose: officePose(act.pose) } : look;
277
+ }
278
+ /** The back and front layers: pictures of the scene at `ls` layer px per logical unit (`tagsOff`: no name tags but the Find box's matches). */
279
+ function paintLayers(p, env, ls, tagsOff = false) {
280
+ const { L, skin, zones, looks, all } = p;
281
+ const lw = Math.max(1, Math.ceil(L.width * ls)), lh = Math.max(1, Math.ceil(L.height * ls));
282
+ const layer = (fn) => {
283
+ const c = document.createElement("canvas");
284
+ c.width = lw; c.height = lh;
285
+ const g = c.getContext && c.getContext("2d");
286
+ if (!g) return null;
287
+ g.setTransform(ls, 0, 0, ls, 0, 0);
288
+ g.imageSmoothingEnabled = false;
289
+ fn(g);
290
+ return c;
291
+ };
292
+ const back = layer((g) => {
293
+ g.fillStyle = env.pal.bg; g.fillRect(0, 0, L.width, L.height);
294
+ if (zones) {
295
+ skin.drawBackground(g, L, env);
296
+ return;
297
+ }
298
+ skin.drawFloor(g, L, env);
299
+ for (const r of L.rooms) skin.drawRoom(g, r, env);
300
+ for (const d of all) skin.drawDeskBack(g, d, looks.get(d.key), env);
301
+ });
302
+ const front = zones ? null : layer((g) => {
303
+ for (const d of all) { const lk = looks.get(d.key); skin.drawDeskFront(g, d, tagsOff && lk && !lk.match ? { ...lk, notag: true } : lk, env); }
304
+ });
305
+ return { ls, tagsOff, back, front };
306
+ }
307
+ /**
308
+ * One frame of a scene at `ms` (0: still), a logical unit `a` bitmap px, its origin at (ox, oy); `hot`: the desk key
309
+ * hovered (its name tag shows even in a crowd); `tagsOff` (#2554: a tile too small to read them): the name tags show
310
+ * only for the one hovered and the Find box's matches. Whether anything in it is moving (in the air, or someone walking).
311
+ */
312
+ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer = null) {
313
+ const sk = p.skin;
314
+ const ls = layerScale(a, p.L.width, p.L.height);
315
+ if (!p.layers || p.layers.ls !== ls || p.layers.tagsOff !== tagsOff) p.layers = paintLayers(p, env, ls, tagsOff);
316
+ const T = () => ctx.setTransform(a, 0, 0, a, ox, oy);
317
+ const blit = (c) => { if (!c) return; ctx.setTransform(a / ls, 0, 0, a / ls, ox, oy); ctx.drawImage(c, 0, 0); T(); };
318
+ blit(p.layers.back);
319
+ T();
320
+ const speakers = [], tags = [];
321
+ const up = still ? [] : p.flights.frame(ms, p.L);
322
+ // Mail from another office (#2564): flying in at the door, or on the floor until its person picks it up.
323
+ const post = still || !p.mail || !p.zones ? [] : p.mail.frame(ms > 0 ? ms : now(), p.L, p.walkers);
324
+ let walking = false;
325
+ if (p.zones) {
326
+ // Furniture under the people; the people (nearest last) with the furniture whose depth is "y" among them, by base
327
+ // y (a tie: the item first, the person in front); furniture over them; things in the air; the pack's foreground
328
+ // (door frames, pillars, table fronts); then what must stay readable: crowd counts and name tags (bubbles below).
329
+ const t = ms > 0 ? ms : now();
330
+ const people = p.desks.map((d) => {
331
+ const at = p.walkers.at(d.node.name, t) || { ...d.seat, walking: false };
332
+ if (at.walking) walking = true;
333
+ return { d, at, look: p.looks.get(d.key) };
334
+ }).sort((x, y) => x.at.y - y.at.y);
335
+ const furniture = strollFurniture(p.night ? nightFurniture(sk.pack.furniture) : sk.pack.furniture, (k) => p.walkers.at(k, t));
336
+ if (furniture.some((f) => f.walking)) walking = true;
337
+ const band = [...furniture.filter((f) => f.depth === "y").map((f) => ({ y: f.y, k: 0, f })), ...people.map((q) => ({ y: q.at.y, k: 1, p: q })),
338
+ ...(p.ghosts ? ghostsOf(sk.pack, people).map((g) => ({ y: g.y, k: 1, g })) : []),
339
+ // #2665: by day, each item's night place too (at night the items are there already).
340
+ ...(p.ghosts && !p.night ? nightGhostsOf(sk.pack).map((n) => ({ y: n.y, k: 1, n })) : [])]
341
+ .sort((x, y) => x.y - y.y || x.k - y.k);
342
+ for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env);
343
+ for (const it of post) if (it.t === 0) sk.drawProp(ctx, it, ms, env); // a letter on the floor: under the people
344
+ for (const { f, p: q, g, n } of band) {
345
+ if (f) { sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env); continue; }
346
+ if (n) { ctx.save(); ctx.globalAlpha = GHOST_ALPHA; sk.drawFurniture(ctx, n, true, 0, env); ctx.restore(); continue; }
347
+ if (g) { ctx.save(); ctx.globalAlpha = GHOST_ALPHA; sk.drawCharacter(ctx, { x: g.x - 44, y: g.y - 53, w: 88, h: 53, seat: g }, g.look, 0, env); ctx.restore(); continue; }
348
+ const box = { ...q.d, x: q.at.x - 44, y: q.at.y - 53, w: 88, h: 53, seat: { x: q.at.x, y: q.at.y } };
349
+ // An agent's own pose (office_set, #2578) while its act lasts and nothing calls it; walking is walking.
350
+ const posed = actPosed(p, q.look);
351
+ const look = q.at.walking ? { ...q.look, pose: "walk", heading: q.at.heading } : posed;
352
+ sk.drawCharacter(ctx, box, look, ms, env);
353
+ if (!q.at.walking) speakers.push({ look: q.look, ...speakerAt(sk, look, q.at), area: q.d.area });
354
+ }
355
+ for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || p.active.has(f.id), ms, env);
356
+ for (const it of up) sk.drawProp(ctx, it, ms, env);
357
+ for (const it of post) if (it.t > 0) sk.drawProp(ctx, it, ms, env);
358
+ if (sk.drawForeground) sk.drawForeground(ctx, p.L, env);
359
+ // Who found no spot in an area: a count in its corner (never over its name).
360
+ for (const [ar, n] of p.L.crowd || []) if (p.L.areas[ar] && sk.drawCrowd) sk.drawCrowd(ctx, p.L.areas[ar], n, env);
361
+ // In an area over capacity the name tags show on hover or focus only (#2513: a crowd's tags covered each other).
362
+ const over = p.L.over || new Set();
363
+ for (const q of people) {
364
+ if ((tagsOff || over.has(q.d.area)) && q.d.key !== hot && !q.look.match) continue;
365
+ sk.drawTag(ctx, q.at.x, q.at.y, q.look, env);
366
+ tags.push(tagBox(ctx, q.at.x, q.at.y, q.look.label));
367
+ }
368
+ // A furniture item's own name (#2574: an NPC drawn as furniture), tagged like a person; none on a tile too small to read.
369
+ if (!tagsOff) for (const f of furniture) if (f.label) { sk.drawTag(ctx, f.x, f.y, { label: f.label }, env); tags.push(tagBox(ctx, f.x, f.y, f.label)); }
370
+ } else {
371
+ for (const d of p.desks) { const look = p.looks.get(d.key); sk.drawCharacter(ctx, d, actPosed(p, look), ms, env); speakers.push({ look, x: d.x + 58, y: d.y + 12 }); }
372
+ blit(p.layers.front);
373
+ // Tags left out of the layer: the hovered desk's front again, with its tag.
374
+ const hd = tagsOff && hot ? p.desks.find((d) => d.key === hot) : null;
375
+ if (hd) sk.drawDeskFront(ctx, hd, p.looks.get(hd.key), env);
376
+ for (const it of up) sk.drawProp(ctx, it, ms, env);
377
+ }
378
+ // Speech bubbles: a short line for the state, now and then (typed out; whole at once when still), placed so no two
379
+ // overlap (placeBubbles; #2431).
380
+ const bubbles = [];
381
+ for (const sp of speakers) {
382
+ if (sp.look.dim || sp.look.pose === "away") continue;
383
+ const says = sp.look.night ? "paused" : sp.look.state; // asleep at night: its "Zzz…" lines
384
+ // What an agent says itself (office_set, #2578): whole, kept over the area's thinning like a call.
385
+ const act = p.acts && p.acts.get(sp.look.name), own = actLive(act, says, Date.now(), p.actSeen) && act.say ? act.say : null;
386
+ const b = own ? { text: own, shown: [...own].length } : bubbleAt(sp.look.name, says, ms, linesFor(says, p.lang, sk.pack, p.lines));
387
+ if (b) bubbles.push({ key: sp.look.name, area: sp.area, state: sp.look.state, calling: CALLING.has(sp.look.state), rank: own ? 0 : bubbleRank(sp.look.state), shown: b.shown,
388
+ box: bubbleBox(ctx, sp.x, sp.y, b.text), tail: { x: sp.x, y: sp.y }, sp, b });
389
+ }
390
+ // A crowded area speaks less (thinBubbles: calls, then work, a few at most); no bubble covers a name tag. `defer`
391
+ // (All offices): drawn after every tile, unclipped, so a bubble at a tile's edge is whole.
392
+ const placed = placeBubbles(thinBubbles(bubbles, p.zones ? p.L.over : null), 22, tags);
393
+ const say = () => { T(); for (const { sp, b, dy } of placed) sk.drawBubble(ctx, sp.x, sp.y, [...b.text].slice(0, b.shown).join(""), env, -dy); };
394
+ if (defer) defer.push(say); else say();
395
+ return walking || up.length > 0 || p.flights.size() > 0 || post.some((it) => it.t > 0);
396
+ }
397
+ /**
398
+ * The person at (x, y) in a scene's units, as it draws them: on zones where they walk, in the box the skin draws them
399
+ * in (an uploaded character's strip, anchor and size: #2513 review), nearest first; on desks, the desk.
400
+ */
401
+ function hitPart(p, x, y, t) {
402
+ if (!p.zones) return deskAt(p.L, x, y);
403
+ return personAt(p.all, x, y, (d) => p.walkers.at(d.node.name, t),
404
+ (d, at) => (p.skin.charBox && p.looks.get(d.key) ? p.skin.charBox(p.looks.get(d.key), at) : null));
405
+ }
406
+
158
407
  function People({ nodes, pinned, onChange }) {
159
408
  const [open, setOpen] = useState(false);
160
409
  const [find, setFind] = useState("");
@@ -184,20 +433,31 @@ function People({ nodes, pinned, onChange }) {
184
433
 
185
434
  /**
186
435
  * The view: `view` buildOrgView's answer; `traffic` the live items (null: Live is off); `q` the Find box's needle;
187
- * `lease` the tab's (its reads); `onOpen(name)` what a desk opens (the canvas's: the chat).
436
+ * `lease` the tab's (its reads); `onOpen(name)` what a desk opens (the canvas's: the chat; none: a desk opens nothing,
437
+ * as on a share link's page, #2636); `assetUrl(path)` where a skin pack's picture is read (the share's address), and
438
+ * `assetImage(url)` how (the share's, which waits out a 429, #2662; none: the loader's own);
439
+ * `opening` the office it opens on (the share's), instead of the one this browser chose last.
188
440
  */
189
- export function Office({ view, traffic, q = "", lease, onOpen }) {
441
+ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetImage, opening }) {
190
442
  const store = useMemo(() => createOfficeStore(lease ? { fetch: (path, o) => lease.fetch(path, o) } : {}), [lease]);
191
443
  const [stored, setStored] = useState([]);
192
444
  const [storedRev, setStoredRev] = useState(null); // the rev the list was read at (a save sends it)
193
445
  const [canEdit, setCanEdit] = useState(false); // an admin session (never a guest or the public link)
446
+ const [canShare, setCanShare] = useState(false); // #2636: an admin session, here or over the public link
194
447
  const [editing, setEditing] = useState(false);
195
- const [choice, setChoice] = useState(() => store.choice());
448
+ const [choice, setChoice] = useState(() => opening ?? store.choice());
196
449
  const [filter, setFilter] = useState(() => store.filter());
197
450
  const [style, setStyle] = useState(() => store.skin());
198
451
  const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
452
+ // Bedtime (#2568), when it is on here: the idle sleep from NIGHT.FROM to NIGHT.TO on this device's clock (read every NIGHT.TICK).
453
+ const [nightOn, setNightOn] = useState(() => store.nightOn());
454
+ const [nightNow, setNightNow] = useState(() => isNight());
455
+ useEffect(() => { if (!nightOn) return undefined; setNightNow(isNight()); const h = setInterval(() => setNightNow(isNight()), NIGHT.TICK); return () => clearInterval(h); }, [nightOn]);
456
+ const night = nightOn && nightNow;
457
+ const [lobbyOn, setLobbyOn] = useState(() => store.lobbyTile()); // #2554: All offices' lobby tile
199
458
  const [packs, setPacks] = useState([]); // the skin packs the host keeps: [{ id, name, sha }]
200
459
  const [packSkins, setPackSkins] = useState(() => new Map()); // id@sha → a loaded skin, or null (not a pack)
460
+ const [cozyRev, setCozyRev] = useState(0); // #2631: the cozy's picture came in (skinFor has it)
201
461
  // Edit layout's session while it is open: { id, officeId, skin, pack (the style's identity), base (the pack as kept),
202
462
  // start, draft, rev, list (the offices and rev read when it opened — what Save sends) }; null when closed.
203
463
  const [layout, setLayout] = useState(null);
@@ -216,6 +476,10 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
216
476
  const flights = useMemo(() => createFlights(), []);
217
477
  const walkers = useMemo(() => createWalkers(), []);
218
478
  const seatsRef = useRef(new Map()); // the zones layout's last seats: kept where they can be
479
+ // #2554: All offices' tiles keep their own: seats (by tile and style), walkers and flights (by tile); and the items
480
+ // flying between tiles.
481
+ const tileSeats = useRef(new Map()), tileWalkers = useRef(new Map()), tileFlights = useRef(new Map());
482
+ const cross = useMemo(() => createCrossFlights(), []);
219
483
  const still = useMemo(reducedMotion, []);
220
484
 
221
485
  // The host's offices: on opening, and again whenever the stream says they changed. Answers can arrive out of order
@@ -241,6 +505,24 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
241
505
  const off = sub ? lease.hold(sub("offices", () => { void read(); })) : null;
242
506
  return () => { if (off) off(); };
243
507
  }, [lease, store]);
508
+ // #2578: what agents do here by their own hand (office_set) — on opening, then as the stream brings it.
509
+ // A read a newer one or the stream overtook is dropped (it is older: review — an opening read answered after the one
510
+ // the stream's return began brought a cleared act back); read again when the stream comes back, and — with no live
511
+ // stream (the public link, or a stream polling) — every POLL_MS while the page is shown.
512
+ const [acts, setActs] = useState([]);
513
+ const actSeen = useRef(new Set());
514
+ useEffect(() => {
515
+ if (!lease) return undefined;
516
+ let heard = 0; // reads begun and events heard: the newest wins
517
+ const read = async () => { const at = ++heard, d = await store.acts(); if (lease.current() && d && at === heard) setActs(d); };
518
+ void read();
519
+ const sub = appStore.get().streamOn;
520
+ const polled = () => globalThis.document?.body?.dataset?.webTransport === "poll" || appStore.get().connection === "polling";
521
+ const offs = sub ? [lease.hold(sub("office_acts", (d) => { heard++; if (d && Array.isArray(d.acts)) setActs(actsOk(d.acts, d.now)); else void read(); })),
522
+ lease.hold(sub("connection", (c) => { if (c === "live") void read(); }))] : [];
523
+ const h = lease.interval(() => { if (polled() && !globalThis.document?.hidden) void read(); }, POLL_MS);
524
+ return () => { for (const off of offs) off(); lease.clear(h); };
525
+ }, [lease, store]);
244
526
  // The skin packs: on opening, and again whenever the stream says one was uploaded or deleted.
245
527
  useEffect(() => {
246
528
  if (!lease) return undefined;
@@ -251,13 +533,15 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
251
533
  return () => { if (off) off(); };
252
534
  }, [lease, store]);
253
535
  // Whether this page may edit the offices: an admin session (the host asks again on every save), never a guest page or
254
- // the public link (whose sessions the host refuses outright).
536
+ // the public link (whose sessions the host refuses outright). #2636 follow-up (the user's choice): Share… is an admin
537
+ // session's here or over the public link (the host asks again too); never a guest's or a read-only one's.
255
538
  useEffect(() => {
256
539
  const body = globalThis.document?.body;
257
- if (!body || body.dataset.tier === "guest" || body.dataset.publicLink === "1") return undefined;
540
+ if (!body || body.dataset.tier === "guest") return undefined;
541
+ const overLink = body.dataset.publicLink === "1";
258
542
  let live = true;
259
543
  const auth = globalThis.AgendAuth;
260
- if (auth && typeof auth.canWrite === "function") Promise.resolve(auth.canWrite()).then((v) => { if (live) setCanEdit(v === true); }, () => {});
544
+ if (auth && typeof auth.canWrite === "function") Promise.resolve(auth.canWrite()).then((v) => { if (live) { setCanEdit(v === true && !overLink); setCanShare(v === true); } }, () => {});
261
545
  return () => { live = false; };
262
546
  }, []);
263
547
  // The stage's width (the grid wraps at it), and the theme (the tokens the canvas reads).
@@ -291,20 +575,38 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
291
575
  // snaps to whole numbers of these (fleet-office-view.js), so a pixel-level view maps a logical unit onto whole pixels.
292
576
  const ratio = backingScale(size.w, size.h, dpr);
293
577
 
294
- const custom = choice !== AUTO_ID ? stored.find((o) => o.id === choice) || null : null;
578
+ // #2554: All offices — chosen, and at least two offices stored (else the whole fleet, as an office gone is).
579
+ const overview = choice === ALL_ID && overviewOk(stored);
580
+ const custom = choice !== AUTO_ID && !overview ? stored.find((o) => o.id === choice) || null : null;
295
581
  // The style: a built-in one, or an uploaded pack once it is loaded (until then, and when it is not one, Desks).
296
582
  const wanted = custom ? custom.skin : style;
297
- const packInfo = STYLES.includes(wanted) ? null : packs.find((pk) => pk.id === wanted) || null;
583
+ // A kept pack wins its id: one kept as "cozy" before it was built in (#2631) is still the style of the offices naming it.
584
+ const packOf = (id) => packs.find((pk) => pk.id === id) || null;
585
+ const styles = STYLES.filter((id) => !packOf(id));
586
+ const packInfo = packOf(wanted);
298
587
  const packKey = packInfo ? `${packInfo.id}@${packInfo.sha}` : null;
588
+ // The packs to load: this office's style; in All offices every tile's (each loaded once, shared by the tiles using it).
589
+ const needed = [...new Map((overview ? [...stored.map((o) => o.skin), ...(lobbyOn ? [style] : [])] : [wanted]).map(packOf).filter(Boolean)
590
+ .map((pk) => [`${pk.id}@${pk.sha}`, pk])).entries()];
591
+ const neededKey = needed.map(([k]) => k).join(",");
299
592
  useEffect(() => {
300
- if (!packInfo || packSkins.has(packKey)) return undefined;
593
+ const want = needed.filter(([k]) => !packSkins.has(k));
594
+ if (!want.length) return undefined;
301
595
  let live = true;
302
596
  const get = lease ? (path, o) => lease.fetch(path, o) : undefined;
303
- loadPackSkin(packInfo.id, packInfo.sha, get ? { fetch: get } : {}).then((sk) => { if (live) setPackSkins((m) => new Map(m).set(packKey, sk)); });
597
+ for (const [k, pk] of want) loadPackSkin(pk.id, pk.sha, { ...(get ? { fetch: get } : {}), ...(assetUrl ? { url: assetUrl } : {}), ...(assetImage ? { image: assetImage } : {}) }).then((sk) => { if (live) setPackSkins((m) => new Map(m).set(k, sk)); });
304
598
  return () => { live = false; };
305
- }, [packKey]);
599
+ }, [neededKey]);
306
600
  const loading = !!packInfo && !packSkins.has(packKey);
307
601
  const baseSkin = packInfo ? packSkins.get(packKey) || skinFor("pixel") : skinFor(STYLES.includes(wanted) ? wanted : custom ? wanted : "pixel");
602
+ // #2631: the cozy's picture, once, when this office (or a tile, or the lobby tile) is drawn in it; until then its bare floors.
603
+ const cozyWanted = (overview ? [...stored.map((o) => o.skin), ...(lobbyOn ? [style] : [])] : [wanted]).includes("cozy");
604
+ useEffect(() => {
605
+ if (!cozyWanted) return undefined;
606
+ let live = true;
607
+ loadCozySkin().then((sk) => { if (live && sk && sk !== baseSkin) setCozyRev((n) => n + 1); });
608
+ return () => { live = false; };
609
+ }, [cozyWanted]);
308
610
  const zones = !!(baseSkin.pack && baseSkin.pack.stateZones);
309
611
  /** The style's identity: a pack's id and sha (a pack replaced is another), or the built-in skin's id. */
310
612
  const styleKey = packKey || baseSkin.id;
@@ -312,42 +614,90 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
312
614
  const layoutStale = !!layout && (!custom || custom.id !== layout.officeId || custom.skin !== layout.skin || styleKey !== layout.pack || !zones || loading);
313
615
  // A stored office's own layout on a zones style (its overrides; while Edit layout is open, the draft).
314
616
  const placed = useMemo(() => {
315
- if (!zones) return { skin: baseSkin, ignored: false };
316
- if (layout && !layoutStale) return { skin: { ...baseSkin, pack: layout.draft }, ignored: false };
317
- const ov = custom && custom.overrides;
318
- if (!ov || ov.pack !== baseSkin.id) return { skin: baseSkin, ignored: false };
319
- const a = applyOverrides(baseSkin.pack, ov);
320
- return a.ok ? { skin: { ...baseSkin, pack: a.pack }, ignored: false } : { skin: baseSkin, ignored: true };
617
+ if (zones && layout && !layoutStale) return { skin: { ...baseSkin, pack: layout.draft }, ignored: false };
618
+ return withOverrides(baseSkin, zones, custom);
321
619
  }, [baseSkin, zones, custom, layout, layoutStale]);
322
620
  const skin = placed.skin;
323
- const L = useMemo(() => {
621
+ // While Edit layout is open: a faint figure on every seat, in the pose its area is for (#2558), so a seat is placed by
622
+ // what will stand, sit or lie there, not by a dot.
623
+ const ghosts = zones && !!layout && !layoutStale;
624
+ const keep = (n) => !(hideAsleep && ASLEEP.has(n.state));
625
+ const single = useMemo(() => {
626
+ if (overview) return { rooms: [], width: 1, height: 1 }; // All offices: the tiles below
324
627
  const all = custom ? membersOf(custom, view) : [...new Map(officeRooms(view, filter).flatMap((r) => r.nodes).map((n) => [n.name, n])).values()];
325
- const keep = (n) => !(hideAsleep && ASLEEP.has(n.state));
326
628
  const nodes = all.filter(keep);
327
- if (zones) { const z = zonesLayout(skin.pack, nodes, seatsRef.current); seatsRef.current = z.seats; return z; }
629
+ if (zones) { const z = zonesLayout(skin.pack, nodes, seatsRef.current, { night }); seatsRef.current = z.seats; return z; }
328
630
  // The desks wrap at the stage's width at the scale they open on (desksK: a pixel level near one CSS px a unit).
329
631
  const deskW = Math.max(240, Math.floor(width / desksK(ratio)));
330
632
  if (!custom) return autoLayout(officeRooms(view, filter).map((r) => (hideAsleep ? { ...r, nodes: r.nodes.filter(keep) } : r)), { width: deskW });
331
- const lay = custom.layout || layoutOk(skin.layout);
332
- return lay ? customLayout({ ...custom, layout: lay }, nodes) : autoLayout([{ key: custom.id, name: custom.name, lobby: false, nodes }], { width: deskW });
333
- }, [view, filter, custom, width, ratio, skin, hideAsleep]);
334
- const looks = useMemo(() => {
335
- const work = workOn(traffic);
336
- return new Map(L.rooms.flatMap((r) => r.desks).map((d) => {
337
- const n = d.node;
338
- if (!n) return [d.key, { name: "", label: "", state: "missing", pose: "away", light: null, lamp: false, sign: null, coat: "", work: 0, dim: !!q, match: false, free: zones }];
339
- const match = !!q && hits(n, q);
340
- return [d.key, { name: n.name, label: nodeText(n).primary, state: n.state, pose: officePose(n.state), light: lightOf(n.state), lamp: lampOf(n.state),
341
- sign: zones ? null : signOf(n.state), coat: coatOf(n.i, n.i ? identityOf(n.i) : null, mode), work: work.get(n.name) || 0, dim: !!q && !match, match, free: zones }];
342
- }));
343
- }, [L, traffic, q, mode, themeRev]);
344
- useEffect(() => { flights.sync(traffic || [], now(), still); }, [traffic]);
345
- // Zones: everyone's seat is where they walk to.
633
+ return officeScene(custom, nodes, skin, false, deskW);
634
+ }, [overview, view, filter, custom, width, ratio, skin, hideAsleep, night]);
635
+ // ── #2554: All offices' tiles — each stored office (then the lobby: whoever is in no office) laid out as its own view
636
+ // lays it out, in its own style (desks wrap at a cell's width), with its own seats, walkers and flights ──
637
+ const lobbyAll = useMemo(() => (overview ? lobbyNodes(stored, view) : []), [overview, stored, view]);
638
+ const lobbyList = lobbyOn ? lobbyAll : [];
639
+ const tiles = useMemo(() => {
640
+ if (!overview) return [];
641
+ const mine = (m, k, make) => { if (!m.current.has(k)) m.current.set(k, make()); return m.current.get(k); };
642
+ const specs = stored.map((o) => ({ key: o.id, office: o, name: o.name, lobby: false, wanted: o.skin, nodes: membersOf(o, view) }));
643
+ if (lobbyList.length) specs.push({ key: LOBBY_TILE, office: { id: LOBBY_TILE, name: null, lobby: true, layout: null }, name: to("lobbyRoom"), lobby: true, wanted: style, nodes: lobbyList });
644
+ return specs.map((sp) => {
645
+ const info = packOf(sp.wanted), pk = info ? `${info.id}@${info.sha}` : null;
646
+ const base = info ? packSkins.get(pk) || skinFor("pixel") : skinFor(sp.wanted);
647
+ const z = !!(base.pack && base.pack.stateZones);
648
+ const sk = withOverrides(base, z, sp.lobby ? null : sp.office).skin;
649
+ const seatKey = `${sp.key}@${pk || base.id}`;
650
+ // A desks office is as large as its rooms (cropped: no margins, no corridors outside them).
651
+ const TL0 = officeScene(sp.office, sp.nodes.filter(keep), sk, z, OV.CELL_W, tileSeats.current.get(seatKey), night), TL = z ? TL0 : cropToRooms(TL0);
652
+ if (z) tileSeats.current.set(seatKey, TL0.seats);
653
+ return { key: sp.key, name: sp.name, lobby: sp.lobby, count: sp.nodes.length, skin: sk, zones: z, L: TL, prefix: `${sp.key}\u0001`,
654
+ walkers: mine(tileWalkers, sp.key, createWalkers), flights: mine(tileFlights, sp.key, createFlights) };
655
+ });
656
+ }, [overview, stored, view, packs, packSkins, cozyRev, hideAsleep, lobbyOn, lobbyAll, style, night]);
657
+ const arranged = useMemo(() => tileLayout(tiles.map((tl) => ({ key: tl.key, w: tl.L.width, h: tl.L.height })), size), [tiles, size.w, size.h]);
658
+ const tileLooks = useMemo(() => { const work = workOn(traffic); return tiles.map((tl) => looksOf(tl.L, work, q, mode, tl.zones)); }, [tiles, traffic, q, mode, themeRev]);
659
+ // The scene the view pans and zooms: the office's, or the overview of every tile.
660
+ const L = useMemo(() => (overview ? { mode: "overview", rooms: [], width: arranged.width, height: arranged.height } : single), [overview, single, arranged]);
661
+ const looks = useMemo(() => looksOf(L, workOn(traffic), q, mode, zones), [L, traffic, q, mode, themeRev]);
662
+ // #2564: what comes in from other offices — one office's and style's own: a letter's door and drop are that layout's,
663
+ // so another office (or style) starts with none (one shown meanwhile is not carried over to a different room).
664
+ // #2578: the acts of the people in this stored office (by name); none elsewhere. On a desks style (review: accepted
665
+ // acts drew nothing there) the line and the pose show; nobody walks (the host refuses go there).
666
+ const actsHere = useMemo(() => new Map(custom && !overview ? acts.filter((a) => a.office === custom.id).map((a) => [a.instance, a]) : []), [acts, custom, overview]);
667
+ const mailKey = `${custom ? custom.id : AUTO_ID}@${baseSkin.id}`;
668
+ const mail = useMemo(() => { const m = createMail(); m.prime(traffic || []); return m; }, [mailKey]);
669
+ useEffect(() => { flights.sync(traffic || [], now(), still); if (zones && !overview && mail.sync(traffic || [], now(), L, still)) loop.current?.wake(); }, [traffic, mail]);
670
+ // Zones: everyone's seat is where they walk to — an idle person now and then strolls near it and back (#2560; not
671
+ // with reduced motion, nor while Edit layout is open: the seats are being placed); someone with mail from another
672
+ // office walks to it first (#2564). An NPC drawn as furniture with a `walk` strolls near its spot alike (#2596).
673
+ const wander = zones && !overview && !still && !layout;
674
+ useEffect(() => {
675
+ if (!zones || overview) return undefined;
676
+ const go = () => {
677
+ const t = now(), to = walkTargets(desksOf(L), L, t, wander, skin.pack.furniture, night);
678
+ // An act's walk; not someone in bed for the night (#2568: bedtime wins, as for mail).
679
+ for (const d of desksOf(L)) { const a = actsHere.get(d.node.name), at = !d.night && actLive(a, d.node.state, Date.now(), actSeen.current) ? actTarget(a, L, walkers, t) : null; if (at) to.set(d.node.name, at); }
680
+ for (const [name, at] of mail.targets(L, t)) to.set(name, at);
681
+ walkers.sync(to, t, L, still);
682
+ // Reduced motion has no loop: drawn again here, or an act's walk (set after the scene was drawn) never shows.
683
+ if (still) { if (scene.current && !globalThis.document?.hidden) draw(0); } else loop.current?.wake();
684
+ };
685
+ go();
686
+ if (still) return undefined;
687
+ const h = setInterval(go, WANDER.TICK);
688
+ return () => clearInterval(h);
689
+ }, [L, wander, mail, skin, night, actsHere]);
690
+ // …and in each tile on a zones style; the traffic within a tile is its own, between tiles the overview's.
691
+ useEffect(() => {
692
+ for (const tl of tiles) if (tl.zones) tl.walkers.sync(new Map(desksOf(tl.L).map((d) => [d.node.name, d.seat])), now(), tl.L, still);
693
+ loop.current?.wake();
694
+ }, [tiles]);
346
695
  useEffect(() => {
347
- if (!zones) return;
348
- walkers.sync(new Map(desksOf(L).map((d) => [d.node.name, d.seat])), now(), L, still);
696
+ if (!overview) return;
697
+ for (const tl of tiles) tl.flights.sync(traffic || [], now(), still);
698
+ cross.sync(traffic || [], now(), still);
349
699
  loop.current?.wake();
350
- }, [L]);
700
+ }, [traffic, tiles]);
351
701
 
352
702
  // ── The view (#2540): { k, x, y } as the org chart canvas's (fleet-canvas-view.js) — logical units → the stage's CSS
353
703
  // px. Set without a render (a ref): the canvas redraws, the overview, the tooltip and Edit layout's overlay follow ──
@@ -355,7 +705,9 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
355
705
  // touched: a person moved the view (it is kept); fitted: the view is a fit (a resize fits it again, to the new size).
356
706
  const touched = useRef(false), fitted = useRef(false), drag = useRef(gestureState());
357
707
  const miniViewRef = useRef(null), miniRef = useRef(null), overlayEl = useRef(null), tipDeskRef = useRef(null), pctRef = useRef(null);
358
- const officeKey = `${custom ? custom.id : AUTO_ID}@${baseSkin.id}`; // what a view is kept for: the office and its style
708
+ const labelEls = useRef(new Map()); // #2554: the tiles' labels (tile key → element)
709
+ // What a view is kept for: the office and its style (All offices: its own key).
710
+ const officeKey = overview ? `${ALL_ID}@tiles` : `${custom ? custom.id : AUTO_ID}@${baseSkin.id}`;
359
711
  const keyRef = useRef(officeKey), saveTimer = useRef(0);
360
712
  // The fit mode (Fit: contain; Fill: cover), kept per office and style like the view.
361
713
  const [fitRev, setFitRev] = useState(0);
@@ -400,6 +752,21 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
400
752
  const pc = pctRef.current;
401
753
  if (pc) { const lb = zoomLabel(v.k); if (pc.textContent !== lb) pc.textContent = lb; pc.setAttribute("aria-label", to("zoomLevel", lb)); }
402
754
  if (overlayEl.current) placeOverlay(overlayEl.current);
755
+ // All offices: each tile's label just above it, as wide as the room up to the next label in its row (or the stage's
756
+ // edge); hidden while its tile is off screen (nothing past the stage's edge for a focus to scroll it to: the
757
+ // controls stay where they are).
758
+ const shownTiles = overview ? new Set(visibleTiles(arranged, v, size).map((tt) => tt.key)) : null;
759
+ for (const [i, tt] of (overview ? arranged.tiles : []).entries()) {
760
+ const el = labelEls.current.get(tt.key);
761
+ if (!el || !el.style) continue;
762
+ const lp = toScreen(v, tt.x, tt.y), next = arranged.tiles[i + 1];
763
+ const room = next && next.label.y === tt.label.y ? (next.x - tt.x) * v.k - 6 : size.w - lp.x - 8;
764
+ el.style.setProperty("left", `${lp.x}px`); el.style.setProperty("top", `${lp.y}px`);
765
+ el.style.setProperty("max-width", `${Math.max(40, room)}px`);
766
+ el.style.setProperty("visibility", shownTiles.has(tt.key) ? "visible" : "hidden");
767
+ }
768
+ const st = stageRef.current;
769
+ if (st && (st.scrollLeft || st.scrollTop)) { st.scrollLeft = 0; st.scrollTop = 0; }
403
770
  placeTip();
404
771
  };
405
772
  /** …and redraw. */
@@ -420,14 +787,20 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
420
787
  if (saveTimer.current) clearTimeout(saveTimer.current);
421
788
  saveTimer.current = setTimeout(flushSave, 400);
422
789
  };
790
+ /**
791
+ * The scene fitted (`mode`: contain or cover). All offices' contain keeps FIT_PAD px clear all round, so no tile sits
792
+ * under the zoom controls at the side.
793
+ */
794
+ const fitL = (mode) => fitScene(L.width, L.height, size, mode, overview && mode !== "cover" ? FIT_PAD : 0);
423
795
  /** The view an office opens on: its kept one (a kept fit: fitted to this size), else its opening; and whether a fit. */
424
796
  const openView = () => {
425
797
  const kept = store.view(officeKey);
426
- if (kept && kept.fit) return { v: fitScene(L.width, L.height, size, fitMode), fit: true };
798
+ if (kept && kept.fit) return { v: fitL(fitMode), fit: true };
427
799
  if (kept) return { v: viewFromSaved(kept, size), fit: false };
428
- return { v: openingView(L.width, L.height, size, ratio, zones, fitMode), fit: zones };
800
+ const fitted0 = zones || overview; // All offices opens whole, as a zones scene
801
+ return { v: overview ? fitL(fitMode) : openingView(L.width, L.height, size, ratio, fitted0, fitMode), fit: fitted0 };
429
802
  };
430
- // An office (or style) opened: its kept view, else its opening (zones fitted; desks from the top left). A resize fits
803
+ // An office (or style) opened: its kept view, else its opening (zones fitted; desks from the top left, or centred where smaller). A resize fits
431
804
  // a fitted view again (and opens an untouched one again); a view a person moved keeps the same point in the middle.
432
805
  // Either way it is then kept on the scene (clampPan): a kept view from a bigger panel cannot leave it off-screen.
433
806
  const opened = useRef(null), sized = useRef(null);
@@ -440,16 +813,16 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
440
813
  opened.current = officeKey; keyRef.current = officeKey; touched.current = false;
441
814
  reopen();
442
815
  } else if (was && (was.w !== size.w || was.h !== size.h || was.ratio !== ratio)) {
443
- if (fitted.current) vp.current = fitScene(L.width, L.height, size, fitMode);
816
+ if (fitted.current) vp.current = fitL(fitMode);
444
817
  else if (!touched.current) reopen();
445
818
  else { const v = vp.current, cx = (was.w / 2 - v.x) / v.k, cy = (was.h / 2 - v.y) / v.k; vp.current = { k: v.k, x: size.w / 2 - cx * v.k, y: size.h / 2 - cy * v.k }; }
446
- } else if (fitted.current) vp.current = fitScene(L.width, L.height, size, fitMode); // the scene's size changed
819
+ } else if (fitted.current) vp.current = fitL(fitMode); // the scene's size changed
447
820
  vp.current = clampPan(view0(), L.width, L.height, size);
448
821
  raw.current = vp.current.k;
449
822
  }, [officeKey, size.w, size.h, ratio, L.width, L.height]);
450
823
  useEffect(() => () => flushSave(), []);
451
824
  /** Fit (the button, `0`): the office's mode — `mode` when it was just switched. */
452
- const fit = (mode = fitMode) => { const v = fitScene(L.width, L.height, size, mode); raw.current = v.k; setView(v, true, true); };
825
+ const fit = (mode = fitMode) => { const v = fitL(mode); raw.current = v.k; setView(v, true, true); };
453
826
  /** Fill on or off: kept for this office, and fitted at once. */
454
827
  const toggleFill = () => {
455
828
  const next = fitMode === "cover" ? "contain" : "cover";
@@ -532,14 +905,15 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
532
905
  return () => root.classList.remove("oo-immersive");
533
906
  }, [fs]);
534
907
  /** A desk opens its chat: out of fullscreen first (the chat is beside the view, or instead of it). */
535
- const open = (name) => { if (fsRef.current) exitFs(); onOpen(name); };
908
+ const open = (name) => { if (!onOpen) return; if (fsRef.current) exitFs(); onOpen(name); };
536
909
 
537
910
  // ── Drawing: the back and front layers are pictures of the scene at the view's resolution (remade when the office
538
911
  // or the zoom changes, never on a pan); each frame draws the matte, the layers, the people and the items in the air ──
539
912
  const scene = useRef(null);
540
913
  const loop = useRef(null);
541
- // The matte around the scene: the pack's own, else its background's edge (sampled once), else the skin's (tokens).
542
- const matte = (skin.pack && skin.pack.matte) || sampledMatte(skin.backdrop) || (skin.matte ? skin.matte({ pal, mode }) : pal.bg);
914
+ // The matte around the scene: the pack's own, else its background's edge (sampled once), else the skin's (tokens);
915
+ // All offices: the dashboard's background (each tile is its own scene).
916
+ const matte = overview ? pal.bg : (skin.pack && skin.pack.matte) || sampledMatte(skin.backdrop) || (skin.matte ? skin.matte({ pal, mode }) : pal.bg);
543
917
  // …and behind the canvas too (the stage), so no page grey shows while it is (re)sized.
544
918
  useLayoutEffect(() => { const el = stageRef.current; if (el && el.style) el.style.setProperty("background-color", matte); }, [matte]);
545
919
  useLayoutEffect(() => {
@@ -552,108 +926,56 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
552
926
  cv.style.setProperty("width", `${BW / ratio}px`);
553
927
  cv.style.setProperty("height", `${BH / ratio}px`);
554
928
  const env = { pal, mode, labels: { lobby: to("lobbyRoom"), closed: to("closed"), areas: { desks: to("area_desks"), lounge: to("area_lounge"), alert: to("area_alert"), nap: to("area_nap") } } };
555
- const all = L.rooms.flatMap((r) => r.desks);
556
- /** The layers at `ls` layer px per logical unit. */
557
- const paint = (ls) => {
558
- const lw = Math.max(1, Math.ceil(L.width * ls)), lh = Math.max(1, Math.ceil(L.height * ls));
559
- const layer = (fn) => {
560
- const c = document.createElement("canvas");
561
- c.width = lw; c.height = lh;
562
- const g = c.getContext && c.getContext("2d");
563
- if (!g) return null;
564
- g.setTransform(ls, 0, 0, ls, 0, 0);
565
- g.imageSmoothingEnabled = false;
566
- fn(g);
567
- return c;
568
- };
569
- const back = layer((g) => {
570
- g.fillStyle = env.pal.bg; g.fillRect(0, 0, L.width, L.height);
571
- if (zones) {
572
- skin.drawBackground(g, L, env);
573
- return;
574
- }
575
- skin.drawFloor(g, L, env);
576
- for (const r of L.rooms) skin.drawRoom(g, r, env);
577
- for (const d of all) skin.drawDeskBack(g, d, looks.get(d.key), env);
578
- });
579
- const front = zones ? null : layer((g) => { for (const d of all) skin.drawDeskFront(g, d, looks.get(d.key), env); });
580
- return { ls, back, front };
581
- };
582
- const shown = new Set([...looks.values()].map((l) => l.state));
583
- scene.current = { ctx, ratio, BW, BH, paint, layers: null, matte, env, desks: all.filter((d) => d.node), looks, L, skin, zones, dirty: true,
584
- active: zones ? activeFurniture(skin.pack, shown) : new Set(), lines: bubbleDefaults(), lang: lang() };
929
+ const base = { ctx, ratio, BW, BH, matte, env, dirty: true };
930
+ if (overview) {
931
+ // Each tile a scene of its own, where the overview puts it (arranged.tiles, in the same order).
932
+ const parts = tiles.map((tl, i) => ({ ...partOf(tl.L, tileLooks[i], tl.skin, tl.zones, tl.walkers, tl.flights), night, key: tl.key, prefix: tl.prefix, tile: arranged.tiles[i] }));
933
+ scene.current = { ...base, overview: true, parts, lay: arranged };
934
+ } else {
935
+ scene.current = { ...base, overview: false, part: { ...partOf(L, looks, skin, zones, walkers, flights), ghosts, night, mail, acts: actsHere, actSeen: actSeen.current } };
936
+ }
585
937
  if (still) draw(0); else loop.current?.wake();
586
- }, [L, looks, skin, pal, size.w, size.h, ratio, matte]);
938
+ }, [L, looks, skin, pal, size.w, size.h, ratio, matte, overview, tiles, tileLooks, arranged, ghosts, night, mail, actsHere]);
587
939
 
588
940
  /** One frame at `ms` (0: still); whether anything is moving (in the air, or someone walking). */
589
941
  function draw(ms) {
590
942
  const s = scene.current;
591
943
  if (!s) return false;
592
- const { ctx, BW, BH, env, skin: sk } = s;
944
+ const { ctx, BW, BH, env } = s;
593
945
  // The view in bitmap px: a logical unit is `a` px (a whole number at a pixel level), the origin on a whole px.
594
946
  const v = view0(), a = v.k * s.ratio, ox = Math.round(v.x * s.ratio), oy = Math.round(v.y * s.ratio);
595
- const ls = layerScale(a, s.L.width, s.L.height);
596
- if (!s.layers || s.layers.ls !== ls) s.layers = s.paint(ls);
597
- const T = () => ctx.setTransform(a, 0, 0, a, ox, oy);
598
- const blit = (c) => { if (!c) return; ctx.setTransform(a / ls, 0, 0, a / ls, ox, oy); ctx.drawImage(c, 0, 0); T(); };
599
947
  ctx.setTransform(1, 0, 0, 1, 0, 0);
600
948
  ctx.clearRect(0, 0, BW, BH);
601
949
  ctx.fillStyle = s.matte; ctx.fillRect(0, 0, BW, BH);
602
950
  ctx.imageSmoothingEnabled = false;
603
- blit(s.layers.back);
604
- T();
605
- const speakers = [], tags = [];
606
- const up = still ? [] : flights.frame(ms, s.L);
607
- let walking = false;
608
- if (s.zones) {
609
- // Furniture under the people; the people (nearest last) with the furniture whose depth is "y" among them, by base
610
- // y (a tie: the item first, the person in front); furniture over them; things in the air; the pack's foreground
611
- // (door frames, pillars, table fronts); then what must stay readable: crowd counts and name tags (bubbles below).
612
- const t = ms > 0 ? ms : now();
613
- const people = s.desks.map((d) => {
614
- const at = walkers.at(d.node.name, t) || { ...d.seat, walking: false };
615
- if (at.walking) walking = true;
616
- return { d, at, look: s.looks.get(d.key) };
617
- }).sort((a, b) => a.at.y - b.at.y);
618
- const furniture = s.skin.pack.furniture;
619
- const band = [...furniture.filter((f) => f.depth === "y").map((f) => ({ y: f.y, k: 0, f })), ...people.map((p) => ({ y: p.at.y, k: 1, p }))]
620
- .sort((a, b) => a.y - b.y || a.k - b.k);
621
- for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, s.active.has(f.id), ms, env);
622
- for (const { f, p } of band) {
623
- if (f) { sk.drawFurniture(ctx, f, s.active.has(f.id), ms, env); continue; }
624
- const box = { ...p.d, x: p.at.x - 44, y: p.at.y - 53, w: 88, h: 53, seat: { x: p.at.x, y: p.at.y } };
625
- sk.drawCharacter(ctx, box, p.at.walking ? { ...p.look, pose: "walk", heading: p.at.heading } : p.look, ms, env);
626
- if (!p.at.walking) speakers.push({ look: p.look, x: p.at.x + 10, y: p.at.y - 50, area: p.d.area });
627
- }
628
- for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, s.active.has(f.id), ms, env);
629
- for (const it of up) sk.drawProp(ctx, it, ms, env);
630
- if (sk.drawForeground) sk.drawForeground(ctx, s.L, env);
631
- // Who found no spot in an area: a count in its corner (never over its name).
632
- for (const [a, n] of s.L.crowd || []) if (s.L.areas[a] && sk.drawCrowd) sk.drawCrowd(ctx, s.L.areas[a], n, env);
633
- // In an area over capacity the name tags show on hover or focus only (#2513: a crowd's tags covered each other).
634
- const over = s.L.over || new Set(), hot = tipNow.current;
635
- for (const p of people) {
636
- if (over.has(p.d.area) && p.d.key !== hot && !p.look.match) continue;
637
- sk.drawTag(ctx, p.at.x, p.at.y, p.look, env);
638
- tags.push(tagBox(ctx, p.at.x, p.at.y, p.look.label));
639
- }
640
- } else {
641
- for (const d of s.desks) { const look = s.looks.get(d.key); sk.drawCharacter(ctx, d, look, ms, env); speakers.push({ look, x: d.x + 58, y: d.y + 12 }); }
642
- blit(s.layers.front);
643
- for (const it of up) sk.drawProp(ctx, it, ms, env);
951
+ if (!s.overview) return drawPart(ctx, s.part, a, ox, oy, ms, still, tipNow.current, env);
952
+ // All offices: each tile on screen drawn as its own view, scaled into it and clipped to it; one off screen is not
953
+ // drawn (its flights still land). Then a frame round each tile, and the items flying between tiles.
954
+ const hot = tipNow.current, on = new Set(visibleTiles(s.lay, v, sizeRef.current).map((tt) => tt.key)), said = [];
955
+ let moving = false;
956
+ for (const p of s.parts) {
957
+ const tt = p.tile;
958
+ if (!on.has(tt.key)) { if (!still) p.flights.frame(ms, p.L); continue; }
959
+ const tx = Math.round((v.x + tt.x * v.k) * s.ratio), ty = Math.round((v.y + tt.y * v.k) * s.ratio), tw = tt.w * v.k * s.ratio, th = tt.h * v.k * s.ratio;
960
+ ctx.save();
961
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
962
+ ctx.beginPath(); ctx.rect(tx, ty, tw, th); ctx.clip();
963
+ if (drawPart(ctx, p, a * tt.s, tx, ty, ms, still, hot && hot.startsWith(p.prefix) ? hot.slice(p.prefix.length) : null, env, v.k * tt.s < TAG_MIN_K, said)) moving = true;
964
+ ctx.restore();
965
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
966
+ ctx.strokeStyle = env.pal.border; ctx.lineWidth = 1;
967
+ ctx.strokeRect(tx - 0.5, ty - 0.5, tw + 1, th + 1);
644
968
  }
645
- // Speech bubbles: a short line for the state, now and then (typed out; whole at once when still), placed so no two
646
- // overlap (placeBubbles; #2431).
647
- const bubbles = [];
648
- for (const sp of speakers) {
649
- if (sp.look.dim || sp.look.pose === "away") continue;
650
- const b = bubbleAt(sp.look.name, sp.look.state, ms, linesFor(sp.look.state, s.lang, sk.pack, s.lines));
651
- if (b) bubbles.push({ key: sp.look.name, area: sp.area, state: sp.look.state, calling: CALLING.has(sp.look.state), rank: bubbleRank(sp.look.state), shown: b.shown,
652
- box: bubbleBox(ctx, sp.x, sp.y, b.text), tail: { x: sp.x, y: sp.y }, sp, b });
969
+ for (const say of said) say();
970
+ const up = still ? [] : cross.frame(ms, s.parts);
971
+ // Each drawn by its sender's tile's skin (#2562: their own prop, else that pack's for its kind), at that tile's scale;
972
+ // a sender in no tile drawn: the built-in item, in overview units.
973
+ for (const it of up) {
974
+ const from = senderTile(s.parts, it.from);
975
+ if (from) { ctx.setTransform(a * from.tile.s, 0, 0, a * from.tile.s, ox, oy); from.skin.drawProp(ctx, { ...it, x: it.x / from.tile.s, y: it.y / from.tile.s }, ms, env); }
976
+ else { ctx.setTransform(a, 0, 0, a, ox, oy); skinFor("pixel").drawProp(ctx, it, ms, env); }
653
977
  }
654
- // A crowded area speaks less (thinBubbles: calls, then work, a few at most); no bubble covers a name tag.
655
- for (const { sp, b, dy } of placeBubbles(thinBubbles(bubbles, s.zones ? s.L.over : null), 22, tags)) sk.drawBubble(ctx, sp.x, sp.y, [...b.text].slice(0, b.shown).join(""), env, -dy);
656
- return walking || up.length > 0 || flights.size() > 0;
978
+ return moving || up.length > 0 || cross.size() > 0;
657
979
  }
658
980
  // The loop: drawing only when the frame (FPS) changed or something is in the air; stopped while the tab is hidden,
659
981
  // gone with the view. Office review: while something walks or flies, one requestAnimationFrame per display frame (the
@@ -694,22 +1016,38 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
694
1016
  tipNow.current = tip;
695
1017
  useEffect(() => { if (!scene.current) return; scene.current.dirty = true; if (still) draw(0); else loop.current?.wake(); }, [tip]);
696
1018
 
1019
+ // Everyone drawn: { key, node, box (the desk, in the view's units), dot (where the overview marks them), office (All
1020
+ // offices: the tile's name) }. In All offices a key is the tile's prefix and the desk's key: one person in two
1021
+ // offices is two people here.
1022
+ const people = overview
1023
+ ? tiles.flatMap((tl, i) => { const tt = arranged.tiles[i];
1024
+ return tt ? desksOf(tl.L).map((d) => ({ key: tl.prefix + d.key, node: d.node, box: tileBox(tt, d), office: tl.name,
1025
+ dot: fromTile(tt, tl.zones ? d.seat : { x: d.x + d.w / 2, y: d.y + d.h / 2 }) })) : []; })
1026
+ : desksOf(L).map((d) => ({ key: d.key, node: d.node, box: d, office: null, dot: zones ? d.seat : { x: d.x + d.w / 2, y: d.y + d.h / 2 } }));
697
1027
  // Preact owns the markup, not the view: put it back on the overview, the overlay and the tooltip after every render.
698
- const tipDesk = tip ? desksOf(L).find((d) => d.key === tip) || null : null;
699
- tipDeskRef.current = tipDesk;
1028
+ const tipDesk = tip ? people.find((d) => d.key === tip) || null : null;
1029
+ tipDeskRef.current = tipDesk ? tipDesk.box : null;
700
1030
  useLayoutEffect(mark);
701
1031
  // ── Pointing (#2540: through the view): hover (or a first tap) shows a desk's card, a click (or a second tap) opens
702
1032
  // its chat; a drag on the scene pans it (never on Edit layout's handles: they move), the wheel and a pinch zoom ──
703
- const deskOf = (e) => {
1033
+ /** The view's point under a pointer event (logical units), or null. */
1034
+ const pointOf = (e) => {
704
1035
  const r = canvasRef.current ? canvasRef.current.getBoundingClientRect() : null;
705
- if (!r) return null;
706
- const { x, y } = toWorld(view0(), e.clientX - r.left, e.clientY - r.top);
707
- // The zones scene draws people where they walk: they are hit there, not at the seat they are heading for, and in
708
- // the box the skin draws them in (an uploaded character's strip, anchor and size: #2513 review), nearest first.
709
- if (!zones) return deskAt(L, x, y);
710
- const t = now(), s = scene.current, sk = (s && s.skin) || skin, lk = s ? s.looks : looks;
711
- return personAt(L.rooms.flatMap((rm) => rm.desks), x, y, (d) => walkers.at(d.node.name, t),
712
- (d, at) => (sk.charBox && lk.get(d.key) ? sk.charBox(lk.get(d.key), at) : null));
1036
+ return r ? toWorld(view0(), e.clientX - r.left, e.clientY - r.top) : null;
1037
+ };
1038
+ /** The person under a pointer event as drawn ({ key, node }), or null; All offices: in the tile there, in its own units. */
1039
+ const deskOf = (e) => {
1040
+ const w = pointOf(e), s = scene.current;
1041
+ if (!w) return null;
1042
+ // The zones scene draws people where they walk: they are hit there, not at the seat they are heading for (hitPart).
1043
+ if (!s || !s.overview) {
1044
+ if (overview) return null;
1045
+ return hitPart(s && s.part ? s.part : { L, zones, walkers, skin, looks, all: L.rooms.flatMap((rm) => rm.desks) }, w.x, w.y, now());
1046
+ }
1047
+ const tt = tileAt(s.lay, w.x, w.y), p = tt ? s.parts.find((x) => x.tile.key === tt.key) : null;
1048
+ if (!p) return null;
1049
+ const at = inTile(tt, w.x, w.y), d = hitPart(p, at.x, at.y, now());
1050
+ return d ? { key: p.prefix + d.key, node: d.node } : null;
713
1051
  };
714
1052
  const g = gestures(drag.current, { el: () => stageRef.current, pan: (dx, dy) => { const v = view0(); setView({ ...v, x: v.x + dx, y: v.y + dy }); },
715
1053
  pinch: (from, to) => { raw.current = Math.min(OK_MAX, Math.max(OK_MIN, raw.current * (to.dist / from.dist))); setView(pinchOffice(view0(), from, to, raw.current)); } });
@@ -722,7 +1060,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
722
1060
  if (drag.current.pts.size === 2) raw.current = view0().k; // a pinch starts from the zoom shown
723
1061
  };
724
1062
  const onWheel = (e) => {
725
- if (!onScene(e.target) && !(typeof e.target?.closest === "function" && e.target.closest(".ol-handle"))) return;
1063
+ if (!onScene(e.target) && !(typeof e.target?.closest === "function" && e.target.closest(".ol-handle, .oo-tile-label"))) return;
726
1064
  e.preventDefault();
727
1065
  const r = stageRef.current.getBoundingClientRect(), v = view0();
728
1066
  raw.current = Math.min(OK_MAX, Math.max(OK_MIN, raw.current * wheelFactor(e)));
@@ -743,9 +1081,9 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
743
1081
  if (!el || typeof el.getAttribute !== "function") return;
744
1082
  const hk = el.getAttribute("data-handle");
745
1083
  if (hk && layout) { const hd = handlesOf(layout.draft).find((x) => x.key === hk); if (hd) show({ x: hd.x - 12, y: hd.y - 12, w: 24, h: 24 }); return; }
746
- const name = el.classList && el.classList.contains("oo-desk") ? el.getAttribute("data-name") : null;
747
- const d = name ? desksOf(L).find((x) => x.node.name === name) : null;
748
- if (d) show(d);
1084
+ const key = el.classList && el.classList.contains("oo-desk") ? el.getAttribute("data-key") : null;
1085
+ const d = key ? people.find((x) => x.key === key) : null;
1086
+ if (d) show(d.box);
749
1087
  };
750
1088
  const onMove = (e) => { if (e.pointerType === "touch" || g.busy()) return; const d = deskOf(e); const key = d ? d.key : null; if (key !== tip) setTip(key); };
751
1089
  const onClick = (e) => {
@@ -754,14 +1092,21 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
754
1092
  if (lastType.current === "touch" && tip !== d.key) { setTip(d.key); return; } // a first tap shows; a second opens
755
1093
  open(d.node.name);
756
1094
  };
1095
+ // All offices: a double-click on a tile (off its people) opens that office; the lobby is no office.
1096
+ const onDblClick = (e) => {
1097
+ const s = scene.current, w = pointOf(e);
1098
+ if (!overview || !s || !s.overview || !w || deskOf(e)) return;
1099
+ const tt = tileAt(s.lay, w.x, w.y) || labelAt(s.lay, w.x, w.y), tl = tt ? tiles.find((x) => x.key === tt.key) : null;
1100
+ if (tl && !tl.lobby) pick(tl.key);
1101
+ };
757
1102
  const onMini = (e) => {
758
1103
  const w = miniPoint(miniRef.current ? miniRef.current.getBoundingClientRect() : null, { x: 0, y: 0, w: L.width, h: L.height }, e.clientX, e.clientY);
759
1104
  if (w) setView(centreOn(view0(), size, w.x, w.y));
760
1105
  };
761
1106
 
762
- const auto = !custom;
1107
+ const auto = !custom && !overview;
763
1108
  // Edit layout: an admin, a stored office, a zones style (built in, or a pack loaded here); not while the offices'
764
- // editor is open, nor a second session.
1109
+ // editor is open, nor a second session (nor in All offices: custom is null there).
765
1110
  const canLayout = canEdit && storedRev !== null && !!custom && zones && !loading && baseSkin.id === custom.skin && !editing && !layout;
766
1111
  const openLayout = () => {
767
1112
  setLayout({ id: ++layoutSeq.current, officeId: custom.id, skin: custom.skin, pack: styleKey, base: baseSkin.pack, start: skin.pack, draft: skin.pack, rev: storedRev, list: stored });
@@ -787,7 +1132,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
787
1132
  }
788
1133
  return { ok: false, message: r.status === 429 ? t("orgl.rateLimited") : t("orgl.failed", r.error || r.status) };
789
1134
  };
790
- const styleName = (id) => (STYLES.includes(id) ? to(`style_${id}`) : (packs.find((pk) => pk.id === id) || { name: id }).name);
1135
+ const styleName = (id) => (styles.includes(id) ? to(`style_${id}`) : (packs.find((pk) => pk.id === id) || { name: id }).name);
791
1136
  const f = filterOk(filter);
792
1137
  const opts = teamOptions(view);
793
1138
  const teamValue = (opts.find((o) => o.team === f.team) || opts[0]).value;
@@ -795,14 +1140,17 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
795
1140
  // While Edit layout is open the office stays (its picker is disabled; this is the guard behind it).
796
1141
  const pick = (id) => { if (layoutRef.current) return; store.saveChoice(id); setChoice(id); setTip(null); };
797
1142
  const everyone = useMemo(() => { const m = new Map(); for (const n of [...view.top, ...view.teams.flatMap((tm) => tm.nodes), ...view.rest.nodes]) if (!m.has(n.name)) m.set(n.name, n); return [...m.values()]; }, [view]);
798
- const desks = desksOf(L);
799
1143
  const tipText = tipDesk ? nodeText(tipDesk.node) : null;
800
- // The overview: the scene's rooms (or areas) and a dot for each person (a call in its colour), the view's frame on it.
801
- const miniRects = zones ? Object.values(L.areas || {}).map((a) => ({ key: a.name, ...a.rect })) : L.rooms.map((r) => ({ key: r.key, x: r.x, y: r.y, w: r.w, h: r.h }));
1144
+ // The overview: the scene's rooms (or areas; All offices: the tiles) and a dot for each person (a call in its colour),
1145
+ // the view's frame on it.
1146
+ const miniRects = overview ? arranged.tiles.map((tt) => ({ key: tt.key, x: tt.x, y: tt.y, w: tt.w, h: tt.h }))
1147
+ : zones ? Object.values(L.areas || {}).map((a) => ({ key: a.name, ...a.rect })) : L.rooms.map((r) => ({ key: r.key, x: r.x, y: r.y, w: r.w, h: r.h }));
802
1148
  const dotR = Math.max(L.width, L.height) / 90;
803
- const officeSelect = (cls) => html`<select class=${`tb-select ${cls}`} value=${layout ? layout.officeId : custom ? custom.id : AUTO_ID} disabled=${!!layout}
1149
+ // The switcher: the whole fleet, All offices (two or more stored), each stored office.
1150
+ const officeSelect = (cls) => html`<select class=${`tb-select ${cls}`} value=${layout ? layout.officeId : overview ? ALL_ID : custom ? custom.id : AUTO_ID} disabled=${!!layout}
804
1151
  aria-label=${to("office")} onChange=${(e) => pick(e.target.value)}>
805
1152
  <option value=${AUTO_ID}>${to("allOffice")}</option>
1153
+ ${overviewOk(stored) ? html`<option value=${ALL_ID}>${to("allOffices")}</option>` : null}
806
1154
  ${stored.map((o) => html`<option key=${o.id} value=${o.id}>${o.name}</option>`)}
807
1155
  </select>`;
808
1156
  return html`<div class=${`oo${fs ? ` fs ${fs}` : ""}`} ref=${ooRef}>
@@ -810,7 +1158,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
810
1158
  <label class="oo-pick"><span class="oo-pick-label">${to("office")}</span>${officeSelect("oo-office")}</label>
811
1159
  ${auto ? html`<label class="oo-pick"><span class="oo-pick-label">${to("styleLabel")}</span>
812
1160
  <select class="tb-select" value=${STYLES.includes(style) || packs.some((pk) => pk.id === style) ? style : skin.id} onChange=${(e) => { store.saveSkin(e.target.value); setStyle(e.target.value); setTip(null); }}>
813
- ${STYLES.map((id) => html`<option key=${id} value=${id}>${to(`style_${id}`)}</option>`)}
1161
+ ${styles.map((id) => html`<option key=${id} value=${id}>${to(`style_${id}`)}</option>`)}
814
1162
  ${packs.map((pk) => html`<option key=${`pack:${pk.id}`} value=${pk.id}>${pk.name}</option>`)}
815
1163
  </select></label>` : null}
816
1164
  ${auto ? html`<label class="oo-pick"><span class="oo-pick-label">${to("show")}</span>
@@ -819,32 +1167,46 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
819
1167
  </select></label>
820
1168
  <${People} nodes=${everyone} pinned=${f.pinned} onChange=${(pinned) => setF({ ...f, pinned })} />` : null}
821
1169
  <label class="check oo-hide-asleep"><input type="checkbox" checked=${hideAsleep} onChange=${() => { const v = !hideAsleep; store.saveHideAsleep(v); setHideAsleep(v); setTip(null); }} /><span>${to("hideAsleep")}</span></label>
1170
+ ${zones || overview ? html`<label class="check oo-night"><input type="checkbox" checked=${nightOn} onChange=${() => { const v = !nightOn; store.saveNightOn(v); setNightOn(v); }} /><span>${to("nightOn")}</span></label>` : null}
1171
+ ${overview ? html`<label class="check oo-lobby-tile" title=${lobbyAll.length ? undefined : to("lobbyEveryone")}><input type="checkbox" checked=${lobbyOn && lobbyAll.length > 0}
1172
+ disabled=${!lobbyAll.length} onChange=${() => { const v = !lobbyOn; store.saveLobbyTile(v); setLobbyOn(v); setTip(null); }} /><span>${to("lobbyTile")}</span></label>
1173
+ ${lobbyAll.length ? null : html`<span class="muted oo-note oo-lobby-none">${to("lobbyEveryone")}</span>`}` : null}
822
1174
  ${custom && baseSkin.id !== custom.skin && !loading ? html`<span class="muted oo-note">${to("skinUnknown", styleName(custom.skin))}</span>` : null}
823
1175
  ${placed.ignored ? html`<span class="muted oo-note">${to("layoutIgnored")}</span>` : null}
824
1176
  ${L.unseated ? html`<span class="muted oo-note">${to("unseated", L.unseated)}</span>` : null}
825
- ${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => setEditing(true)} disabled=${!!layout}>${t("orge.edit")}</button>` : null}
1177
+ ${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => { if (!overview) setEditing(true); }} disabled=${!!layout || overview}
1178
+ title=${overview ? to("overviewNoEdit") : undefined}>${t("orge.edit")}</button>` : null}
826
1179
  ${canLayout ? html`<button type="button" class="btn btn-sm oo-layout" onClick=${openLayout}>${t("orgl.edit")}</button>` : null}
1180
+ ${canShare ? html`<${ShareButton} offices=${stored} current=${overview ? "all" : custom ? custom.id : null} disabled=${!!layout}
1181
+ get=${lease ? (path, o) => lease.fetch(path, o) : undefined} />` : null}
827
1182
  </div>
828
- ${editing ? html`<${OfficeEditor} offices=${stored} rev=${storedRev} view=${view} store=${store} packs=${packs}
1183
+ ${editing && !overview ? html`<${OfficeEditor} offices=${stored} rev=${storedRev} view=${view} store=${store} packs=${packs}
829
1184
  ask=${ask} current=${() => shown.current} onSaved=${adopt} onClose=${() => setEditing(false)} />` : null}
830
- ${desks.length || L.rooms.length ? null : html`<p class="note oo-empty">${to("nobody")} ${auto && (f.team || f.pinned.length) ? html`<button type="button" class="btn btn-sm" onClick=${() => setF({ team: "", pinned: [] })}>${to("everyone")}</button>` : null}</p>`}
1185
+ ${overview || people.length || L.rooms.length ? null : html`<p class="note oo-empty">${to("nobody")} ${auto && (f.team || f.pinned.length) ? html`<button type="button" class="btn btn-sm" onClick=${() => setF({ team: "", pinned: [] })}>${to("everyone")}</button>` : null}</p>`}
831
1186
  <div class=${`oo-stage${layout ? " editing" : ""}`} ref=${stageRef} onPointerDown=${onDown} onPointerMove=${g.move} onPointerUp=${g.up} onPointerCancel=${g.up}
832
1187
  onWheel=${onWheel} onKeyDown=${onKeyDown} onFocusIn=${onFocusIn} onClickCapture=${g.clickCapture}>
833
- <canvas ref=${canvasRef} class=${`oo-canvas${tipDesk && tipDesk.node.i ? " hot" : ""}`} role="application" tabindex="0" aria-label=${to("label")}
834
- onPointerMove=${onMove} onPointerLeave=${() => setTip(null)} onClick=${onClick}></canvas>
835
- ${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay}
1188
+ <canvas ref=${canvasRef} class=${`oo-canvas${tipDesk && tipDesk.node.i ? " hot" : ""}`} role="application" tabindex="0" aria-label=${to(overview ? "labelAll" : "label")}
1189
+ onPointerMove=${onMove} onPointerLeave=${() => setTip(null)} onClick=${onClick} ondblclick=${onDblClick}></canvas>
1190
+ ${overview ? tiles.map((tl) => {
1191
+ const text = to("tileLabel", tl.name, tl.count), at = (el) => { if (el) labelEls.current.set(tl.key, el); else labelEls.current.delete(tl.key); };
1192
+ return tl.lobby ? html`<span key=${tl.key} ref=${at} class="oo-tile-label lobby">${text}</span>`
1193
+ : html`<button key=${tl.key} ref=${at} type="button" class="oo-tile-label" data-office=${tl.key} title=${to("openOffice", tl.name)} aria-label=${to("openOffice", tl.name)}
1194
+ onClick=${() => pick(tl.key)}>${text}</button>`;
1195
+ }) : null}
1196
+ ${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs}
836
1197
  stale=${layoutStale ? t("orgl.stale") : ""} onChange=${onDraft} onSave=${saveLayout} onClose=${closeLayout} />` : null}
837
1198
  ${tipText ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
838
1199
  <strong>${tipText.primary}</strong>${tipText.secondary ? html` <span class="muted mono">${tipText.secondary}</span>` : null}
839
1200
  <div>${tipText.sub}</div><div class="muted">${caption(officePose(tipDesk.node.state), tipDesk.node.state)}</div>
840
- ${tipDesk.node.i && lastType.current === "touch" ? html`<div class="muted">${to("tapAgain")}</div>` : null}
1201
+ ${tipDesk.office ? html`<div class="muted oo-tip-office">${tipDesk.office}</div>` : null}
1202
+ ${tipDesk.node.i && onOpen && lastType.current === "touch" ? html`<div class="muted">${to("tapAgain")}</div>` : null}
841
1203
  </div>` : null}
842
- <ul class="oo-desks" aria-label=${to("desks")}>${desks.map((d) => {
843
- const nt = nodeText(d.node), cap = caption(officePose(d.node.state), d.node.state);
1204
+ <ul class="oo-desks" aria-label=${to("desks")}>${people.map((d) => {
1205
+ const nt = nodeText(d.node), cap = caption(officePose(d.node.state), d.node.state), where = d.office ? ` · ${d.office}` : "";
844
1206
  return html`<li key=${d.key}>${d.node.i
845
- ? html`<button type="button" class="oo-desk" data-name=${d.node.name} aria-label=${`${nt.full} · ${cap}. ${t("orgc.openChat")}`}
1207
+ ? html`<button type="button" class="oo-desk" data-name=${d.node.name} data-key=${d.key} aria-label=${`${nt.full}${where} · ${cap}.${onOpen ? ` ${t("orgc.openChat")}` : ""}`}
846
1208
  onFocus=${() => setTip(d.key)} onBlur=${() => setTip(null)} onClick=${() => open(d.node.name)}>${nt.primary}</button>`
847
- : html`<span class="oo-desk" data-name=${d.node.name}>${nt.full}</span>`}</li>`;
1209
+ : html`<span class="oo-desk" data-name=${d.node.name} data-key=${d.key}>${nt.full}</span>`}</li>`;
848
1210
  })}</ul>
849
1211
  <div class="oc-zoom oo-zoom" role="group" aria-label=${t("orgc.view")}>
850
1212
  <output ref=${pctRef} class="oo-zoom-pct"></output>
@@ -861,8 +1223,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
861
1223
  <rect class="oo-mini-matte" x=${-L.width} y=${-L.height} width=${L.width * 3} height=${L.height * 3} fill=${matte} />
862
1224
  <rect class="oo-mini-scene" x="0" y="0" width=${L.width} height=${L.height} />
863
1225
  ${miniRects.map((r) => html`<rect key=${r.key} class="oo-mini-room" x=${r.x} y=${r.y} width=${r.w} height=${r.h} />`)}
864
- ${desks.map((d) => { const at = zones ? d.seat : { x: d.x + d.w / 2, y: d.y + d.h / 2 };
865
- return html`<circle key=${d.key} class=${`oo-mini-dot st-${d.node.state}${q && hits(d.node, q) ? " match" : ""}`} cx=${at.x} cy=${at.y} r=${dotR} />`; })}
1226
+ ${people.map((d) => html`<circle key=${d.key} class=${`oo-mini-dot st-${d.node.state}${q && hits(d.node, q) ? " match" : ""}`} cx=${d.dot.x} cy=${d.dot.y} r=${dotR} />`)}
866
1227
  <rect ref=${miniViewRef} class="oc-mini-view oo-mini-view" />
867
1228
  </svg>
868
1229
  </div>