@songsid/agend 2.3.2-beta.1 → 2.3.2-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/agent-cli.js +69 -2
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +11 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.js +3 -0
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/antigravity-usage-scan.d.ts +58 -0
- package/dist/antigravity-usage-scan.js +257 -0
- package/dist/antigravity-usage-scan.js.map +1 -0
- package/dist/antigravity-usage-worker.d.ts +1 -0
- package/dist/antigravity-usage-worker.js +10 -0
- package/dist/antigravity-usage-worker.js.map +1 -0
- package/dist/auth-api.js +4 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +52 -0
- package/dist/backend/kiro-classic-archive.js +444 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +83 -0
- package/dist/backend/kiro-identity.js +144 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-migration-verify.d.ts +10 -0
- package/dist/backend/kiro-migration-verify.js +52 -0
- package/dist/backend/kiro-migration-verify.js.map +1 -0
- package/dist/backend/kiro-retired-model.d.ts +62 -0
- package/dist/backend/kiro-retired-model.js +160 -0
- package/dist/backend/kiro-retired-model.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +21 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +8 -0
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +169 -2
- package/dist/backend/kiro.js +703 -28
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +108 -0
- package/dist/backend/types.js +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-signin-guide.d.ts +1 -0
- package/dist/backend-signin-guide.js +9 -0
- package/dist/backend-signin-guide.js.map +1 -0
- package/dist/backend-signin-pending.d.ts +10 -0
- package/dist/backend-signin-pending.js +44 -0
- package/dist/backend-signin-pending.js.map +1 -0
- package/dist/browser-backend-signins.d.ts +56 -0
- package/dist/browser-backend-signins.js +189 -0
- package/dist/browser-backend-signins.js.map +1 -0
- package/dist/cache-ledger.d.ts +38 -2
- package/dist/cache-ledger.js +133 -8
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-prices.d.ts +2 -1
- package/dist/cache-prices.js +5 -2
- package/dist/cache-prices.js.map +1 -1
- package/dist/cache-service.d.ts +68 -0
- package/dist/cache-service.js +355 -13
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +71 -14
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/ipc-timeouts.js +3 -0
- package/dist/channel/ipc-timeouts.js.map +1 -1
- package/dist/channel/mcp-tools.js +41 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +2 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +11 -0
- package/dist/classic-channel-manager.js +39 -1
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +54 -2
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +55 -10
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +33 -1
- package/dist/config.js.map +1 -1
- package/dist/cpu-quota.d.ts +23 -0
- package/dist/cpu-quota.js +86 -0
- package/dist/cpu-quota.js.map +1 -0
- package/dist/daemon.d.ts +68 -0
- package/dist/daemon.js +448 -26
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +31 -6
- package/dist/dashboard-permissions.js +84 -9
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +1 -1
- package/dist/fleet-control-audit.js +3 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-manager.d.ts +133 -2
- package/dist/fleet-manager.js +905 -94
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +9 -0
- package/dist/forged-envelope-scan.js +30 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +24 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +49 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +49 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +47 -9
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +54 -11
- package/dist/instance-config-impact.js +10 -5
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +23 -0
- package/dist/instance-lifecycle.js +62 -12
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-db-reader.d.ts +5 -0
- package/dist/kiro-db-reader.js +18 -0
- package/dist/kiro-db-reader.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +2 -0
- package/dist/kiro-engine-status.js +9 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-migrate.d.ts +60 -0
- package/dist/kiro-migrate.js +160 -0
- package/dist/kiro-migrate.js.map +1 -0
- package/dist/kiro-session-proof.d.ts +41 -0
- package/dist/kiro-session-proof.js +89 -0
- package/dist/kiro-session-proof.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +2 -0
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/kiro-transcript-worker.js +1 -0
- package/dist/kiro-transcript-worker.js.map +1 -1
- package/dist/kiro-tui-forensic.d.ts +12 -0
- package/dist/kiro-tui-forensic.js +110 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/kiro-usage-run.d.ts +7 -0
- package/dist/kiro-usage-run.js +39 -0
- package/dist/kiro-usage-run.js.map +1 -0
- package/dist/kiro-usage-scan.d.ts +49 -0
- package/dist/kiro-usage-scan.js +223 -0
- package/dist/kiro-usage-scan.js.map +1 -0
- package/dist/kiro-usage-worker.d.ts +1 -0
- package/dist/kiro-usage-worker.js +10 -0
- package/dist/kiro-usage-worker.js.map +1 -0
- package/dist/kiro-usage.d.ts +58 -0
- package/dist/kiro-usage.js +310 -0
- package/dist/kiro-usage.js.map +1 -0
- package/dist/locale.js +230 -8
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/login-url.d.ts +4 -0
- package/dist/login-url.js +22 -0
- package/dist/login-url.js.map +1 -0
- package/dist/needs-you-hub.d.ts +2 -1
- package/dist/needs-you-hub.js +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +10 -2
- package/dist/needs-you.js +10 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/notice-delivery.d.ts +16 -0
- package/dist/notice-delivery.js +35 -0
- package/dist/notice-delivery.js.map +1 -0
- package/dist/office-share-page.d.ts +16 -0
- package/dist/office-share-page.js +85 -0
- package/dist/office-share-page.js.map +1 -0
- package/dist/office-share-service.d.ts +91 -0
- package/dist/office-share-service.js +176 -0
- package/dist/office-share-service.js.map +1 -0
- package/dist/office-share.d.ts +268 -0
- package/dist/office-share.js +574 -0
- package/dist/office-share.js.map +1 -0
- package/dist/opencode-usage-scan.d.ts +28 -0
- package/dist/opencode-usage-scan.js +92 -0
- package/dist/opencode-usage-scan.js.map +1 -0
- package/dist/opencode-usage-worker.d.ts +1 -0
- package/dist/opencode-usage-worker.js +10 -0
- package/dist/opencode-usage-worker.js.map +1 -0
- package/dist/outbound-handlers.js +22 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +65 -2
- package/dist/outbound-schemas.js +56 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +7 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.js +13 -2
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.js +2 -1
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-card-answer.d.ts +83 -0
- package/dist/remote-card-answer.js +208 -0
- package/dist/remote-card-answer.js.map +1 -0
- package/dist/remote-card-prompt.d.ts +38 -0
- package/dist/remote-card-prompt.js +86 -0
- package/dist/remote-card-prompt.js.map +1 -0
- package/dist/reply-triggers.d.ts +2 -0
- package/dist/reply-triggers.js +3 -0
- package/dist/reply-triggers.js.map +1 -0
- package/dist/settings-api.d.ts +2 -0
- package/dist/settings-api.js +41 -16
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +20 -14
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +4 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +55 -6
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.d.ts +9 -0
- package/dist/setup-host.js +127 -6
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-signin-assets.d.ts +2 -0
- package/dist/setup-signin-assets.js +6 -0
- package/dist/setup-signin-assets.js.map +1 -0
- package/dist/setup-wizard.js +2 -0
- package/dist/setup-wizard.js.map +1 -1
- package/dist/structured-signin-transport.d.ts +8 -0
- package/dist/structured-signin-transport.js +79 -0
- package/dist/structured-signin-transport.js.map +1 -0
- package/dist/structured-signin.d.ts +131 -0
- package/dist/structured-signin.js +388 -0
- package/dist/structured-signin.js.map +1 -0
- package/dist/tool-permissions.js +29 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.js +6 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +27 -2
- package/dist/transcript-sources.js +104 -40
- package/dist/transcript-sources.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +1 -1
- package/dist/turn-reply-guard.js +1 -1
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/types.d.ts +33 -8
- package/dist/ui/backend-signin.js +63 -10
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +4 -4
- package/dist/ui/fleet-office-editor.js +68 -11
- package/dist/ui/fleet-office-layout.js +110 -35
- package/dist/ui/fleet-office-model.js +6 -3
- package/dist/ui/fleet-office-overview.js +167 -0
- package/dist/ui/fleet-office-pack.d.ts +13 -3
- package/dist/ui/fleet-office-pack.js +214 -24
- package/dist/ui/fleet-office-skin.js +98 -23
- package/dist/ui/fleet-office-store.js +82 -5
- package/dist/ui/fleet-office-view.js +5 -4
- package/dist/ui/fleet-office-zones.js +232 -21
- package/dist/ui/fleet-office.js +549 -186
- package/dist/ui/fleet-usage.js +19 -15
- package/dist/ui/office-share-dialog.js +145 -0
- package/dist/ui/office-share-page.js +164 -0
- package/dist/ui/panel-chat.js +22 -3
- package/dist/ui/panel-log.js +4 -4
- package/dist/ui/panel-needs.js +5 -5
- package/dist/ui/panel-settings.js +38 -12
- package/dist/ui/settings-dialogs.js +8 -1
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +22 -6
- package/dist/ui/settings-registry.js +4 -0
- package/dist/ui/settings-strings.js +18 -2
- package/dist/ui/shared/app-i18n.js +4 -2
- package/dist/ui/shared/app-shell.js +14 -6
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +85 -4
- package/dist/ui/shared/office/cozy.png +0 -0
- package/dist/ui/shared/side-panel.js +95 -0
- package/dist/ui/structured-signin.js +115 -0
- package/dist/usage-report.d.ts +20 -0
- package/dist/usage-report.js +54 -17
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +9 -1
- package/dist/usage-rollup.js +20 -4
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +23 -1
- package/dist/web-api.js +292 -3
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-backend-login.d.ts +1 -7
- package/dist/web-backend-login.js +2 -24
- package/dist/web-backend-login.js.map +1 -1
- package/dist/web-office-acts.d.ts +173 -0
- package/dist/web-office-acts.js +285 -0
- package/dist/web-office-acts.js.map +1 -0
- package/dist/web-office-pack-read.d.ts +35 -0
- package/dist/web-office-pack-read.js +252 -0
- package/dist/web-office-pack-read.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +232 -0
- package/dist/web-office-pack-tools.js +754 -0
- package/dist/web-office-pack-tools.js.map +1 -0
- package/dist/web-office-skins.d.ts +71 -6
- package/dist/web-office-skins.js +321 -35
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +5 -2
- package/dist/web-office-store.js +69 -10
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +2 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/working-recovery.d.ts +2 -2
- package/dist/working-recovery.js +43 -8
- package/dist/working-recovery.js.map +1 -1
- package/dist/working-state-journal.d.ts +2 -0
- package/dist/working-state-journal.js +16 -1
- package/dist/working-state-journal.js.map +1 -1
- package/package.json +3 -2
- package/templates/windows-shortcuts.ps1 +155 -140
package/dist/ui/fleet-office.js
CHANGED
|
@@ -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
|
|
6
|
-
// name tag; General and the instances in no team share the lobby; a grid that
|
|
7
|
-
// (fleet-office-model.js autoLayout). Zones (a skin whose pack maps states to areas — the
|
|
8
|
-
//
|
|
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 {
|
|
72
|
+
import { ShareButton } from "./office-share-dialog.js";
|
|
73
|
+
import { applyOverrides, fitNight, fitSeatFor } 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, fitSeatFor(baseSkin.pack, fitNight(baseSkin.pack, ov))); // #2665/#2691: a night place or seat 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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
}, [
|
|
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 (!
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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 (!
|
|
348
|
-
|
|
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
|
-
}, [
|
|
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
|
|
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:
|
|
798
|
+
if (kept && kept.fit) return { v: fitL(fitMode), fit: true };
|
|
427
799
|
if (kept) return { v: viewFromSaved(kept, size), fit: false };
|
|
428
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
|
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
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const
|
|
607
|
-
let
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
const
|
|
651
|
-
if (
|
|
652
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
|
747
|
-
const d =
|
|
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,20 +1092,29 @@ 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 });
|
|
768
1113
|
setTip(null);
|
|
769
1114
|
};
|
|
770
1115
|
const closeLayout = () => setLayout(null);
|
|
1116
|
+
// #2691: whom a seat can be reserved for — the office's members (its teams' too).
|
|
1117
|
+
const layoutMembers = useMemo(() => (layout && custom ? membersOf(custom, view).map((n) => n.name) : []), [layout && layout.id, custom, view]);
|
|
771
1118
|
const onDraft = (draft) => setLayout((cur) => (cur ? { ...cur, draft } : cur));
|
|
772
1119
|
// Save: the session's list, this office's overrides replaced (none: back to the style's own), with the session's rev.
|
|
773
1120
|
// A change made meanwhile (409) reads the list again: the session takes it (the view too), the draft is kept, and
|
|
@@ -787,7 +1134,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
787
1134
|
}
|
|
788
1135
|
return { ok: false, message: r.status === 429 ? t("orgl.rateLimited") : t("orgl.failed", r.error || r.status) };
|
|
789
1136
|
};
|
|
790
|
-
const styleName = (id) => (
|
|
1137
|
+
const styleName = (id) => (styles.includes(id) ? to(`style_${id}`) : (packs.find((pk) => pk.id === id) || { name: id }).name);
|
|
791
1138
|
const f = filterOk(filter);
|
|
792
1139
|
const opts = teamOptions(view);
|
|
793
1140
|
const teamValue = (opts.find((o) => o.team === f.team) || opts[0]).value;
|
|
@@ -795,14 +1142,17 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
795
1142
|
// While Edit layout is open the office stays (its picker is disabled; this is the guard behind it).
|
|
796
1143
|
const pick = (id) => { if (layoutRef.current) return; store.saveChoice(id); setChoice(id); setTip(null); };
|
|
797
1144
|
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
1145
|
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),
|
|
801
|
-
|
|
1146
|
+
// The overview: the scene's rooms (or areas; All offices: the tiles) and a dot for each person (a call in its colour),
|
|
1147
|
+
// the view's frame on it.
|
|
1148
|
+
const miniRects = overview ? arranged.tiles.map((tt) => ({ key: tt.key, x: tt.x, y: tt.y, w: tt.w, h: tt.h }))
|
|
1149
|
+
: 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
1150
|
const dotR = Math.max(L.width, L.height) / 90;
|
|
803
|
-
|
|
1151
|
+
// The switcher: the whole fleet, All offices (two or more stored), each stored office.
|
|
1152
|
+
const officeSelect = (cls) => html`<select class=${`tb-select ${cls}`} value=${layout ? layout.officeId : overview ? ALL_ID : custom ? custom.id : AUTO_ID} disabled=${!!layout}
|
|
804
1153
|
aria-label=${to("office")} onChange=${(e) => pick(e.target.value)}>
|
|
805
1154
|
<option value=${AUTO_ID}>${to("allOffice")}</option>
|
|
1155
|
+
${overviewOk(stored) ? html`<option value=${ALL_ID}>${to("allOffices")}</option>` : null}
|
|
806
1156
|
${stored.map((o) => html`<option key=${o.id} value=${o.id}>${o.name}</option>`)}
|
|
807
1157
|
</select>`;
|
|
808
1158
|
return html`<div class=${`oo${fs ? ` fs ${fs}` : ""}`} ref=${ooRef}>
|
|
@@ -810,7 +1160,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
810
1160
|
<label class="oo-pick"><span class="oo-pick-label">${to("office")}</span>${officeSelect("oo-office")}</label>
|
|
811
1161
|
${auto ? html`<label class="oo-pick"><span class="oo-pick-label">${to("styleLabel")}</span>
|
|
812
1162
|
<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
|
-
${
|
|
1163
|
+
${styles.map((id) => html`<option key=${id} value=${id}>${to(`style_${id}`)}</option>`)}
|
|
814
1164
|
${packs.map((pk) => html`<option key=${`pack:${pk.id}`} value=${pk.id}>${pk.name}</option>`)}
|
|
815
1165
|
</select></label>` : null}
|
|
816
1166
|
${auto ? html`<label class="oo-pick"><span class="oo-pick-label">${to("show")}</span>
|
|
@@ -819,32 +1169,46 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
819
1169
|
</select></label>
|
|
820
1170
|
<${People} nodes=${everyone} pinned=${f.pinned} onChange=${(pinned) => setF({ ...f, pinned })} />` : null}
|
|
821
1171
|
<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>
|
|
1172
|
+
${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}
|
|
1173
|
+
${overview ? html`<label class="check oo-lobby-tile" title=${lobbyAll.length ? undefined : to("lobbyEveryone")}><input type="checkbox" checked=${lobbyOn && lobbyAll.length > 0}
|
|
1174
|
+
disabled=${!lobbyAll.length} onChange=${() => { const v = !lobbyOn; store.saveLobbyTile(v); setLobbyOn(v); setTip(null); }} /><span>${to("lobbyTile")}</span></label>
|
|
1175
|
+
${lobbyAll.length ? null : html`<span class="muted oo-note oo-lobby-none">${to("lobbyEveryone")}</span>`}` : null}
|
|
822
1176
|
${custom && baseSkin.id !== custom.skin && !loading ? html`<span class="muted oo-note">${to("skinUnknown", styleName(custom.skin))}</span>` : null}
|
|
823
1177
|
${placed.ignored ? html`<span class="muted oo-note">${to("layoutIgnored")}</span>` : null}
|
|
824
1178
|
${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
|
|
1179
|
+
${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => { if (!overview) setEditing(true); }} disabled=${!!layout || overview}
|
|
1180
|
+
title=${overview ? to("overviewNoEdit") : undefined}>${t("orge.edit")}</button>` : null}
|
|
826
1181
|
${canLayout ? html`<button type="button" class="btn btn-sm oo-layout" onClick=${openLayout}>${t("orgl.edit")}</button>` : null}
|
|
1182
|
+
${canShare ? html`<${ShareButton} offices=${stored} current=${overview ? "all" : custom ? custom.id : null} disabled=${!!layout}
|
|
1183
|
+
get=${lease ? (path, o) => lease.fetch(path, o) : undefined} />` : null}
|
|
827
1184
|
</div>
|
|
828
|
-
${editing ? html`<${OfficeEditor} offices=${stored} rev=${storedRev} view=${view} store=${store} packs=${packs}
|
|
1185
|
+
${editing && !overview ? html`<${OfficeEditor} offices=${stored} rev=${storedRev} view=${view} store=${store} packs=${packs}
|
|
829
1186
|
ask=${ask} current=${() => shown.current} onSaved=${adopt} onClose=${() => setEditing(false)} />` : null}
|
|
830
|
-
${
|
|
1187
|
+
${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
1188
|
<div class=${`oo-stage${layout ? " editing" : ""}`} ref=${stageRef} onPointerDown=${onDown} onPointerMove=${g.move} onPointerUp=${g.up} onPointerCancel=${g.up}
|
|
832
1189
|
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
|
-
${
|
|
1190
|
+
<canvas ref=${canvasRef} class=${`oo-canvas${tipDesk && tipDesk.node.i ? " hot" : ""}`} role="application" tabindex="0" aria-label=${to(overview ? "labelAll" : "label")}
|
|
1191
|
+
onPointerMove=${onMove} onPointerLeave=${() => setTip(null)} onClick=${onClick} ondblclick=${onDblClick}></canvas>
|
|
1192
|
+
${overview ? tiles.map((tl) => {
|
|
1193
|
+
const text = to("tileLabel", tl.name, tl.count), at = (el) => { if (el) labelEls.current.set(tl.key, el); else labelEls.current.delete(tl.key); };
|
|
1194
|
+
return tl.lobby ? html`<span key=${tl.key} ref=${at} class="oo-tile-label lobby">${text}</span>`
|
|
1195
|
+
: 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)}
|
|
1196
|
+
onClick=${() => pick(tl.key)}>${text}</button>`;
|
|
1197
|
+
}) : null}
|
|
1198
|
+
${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs} members=${layoutMembers}
|
|
836
1199
|
stale=${layoutStale ? t("orgl.stale") : ""} onChange=${onDraft} onSave=${saveLayout} onClose=${closeLayout} />` : null}
|
|
837
1200
|
${tipText ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
|
|
838
1201
|
<strong>${tipText.primary}</strong>${tipText.secondary ? html` <span class="muted mono">${tipText.secondary}</span>` : null}
|
|
839
1202
|
<div>${tipText.sub}</div><div class="muted">${caption(officePose(tipDesk.node.state), tipDesk.node.state)}</div>
|
|
840
|
-
${tipDesk.
|
|
1203
|
+
${tipDesk.office ? html`<div class="muted oo-tip-office">${tipDesk.office}</div>` : null}
|
|
1204
|
+
${tipDesk.node.i && onOpen && lastType.current === "touch" ? html`<div class="muted">${to("tapAgain")}</div>` : null}
|
|
841
1205
|
</div>` : null}
|
|
842
|
-
<ul class="oo-desks" aria-label=${to("desks")}>${
|
|
843
|
-
const nt = nodeText(d.node), cap = caption(officePose(d.node.state), d.node.state);
|
|
1206
|
+
<ul class="oo-desks" aria-label=${to("desks")}>${people.map((d) => {
|
|
1207
|
+
const nt = nodeText(d.node), cap = caption(officePose(d.node.state), d.node.state), where = d.office ? ` · ${d.office}` : "";
|
|
844
1208
|
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}
|
|
1209
|
+
? 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
1210
|
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>`;
|
|
1211
|
+
: html`<span class="oo-desk" data-name=${d.node.name} data-key=${d.key}>${nt.full}</span>`}</li>`;
|
|
848
1212
|
})}</ul>
|
|
849
1213
|
<div class="oc-zoom oo-zoom" role="group" aria-label=${t("orgc.view")}>
|
|
850
1214
|
<output ref=${pctRef} class="oo-zoom-pct"></output>
|
|
@@ -861,8 +1225,7 @@ export function Office({ view, traffic, q = "", lease, onOpen }) {
|
|
|
861
1225
|
<rect class="oo-mini-matte" x=${-L.width} y=${-L.height} width=${L.width * 3} height=${L.height * 3} fill=${matte} />
|
|
862
1226
|
<rect class="oo-mini-scene" x="0" y="0" width=${L.width} height=${L.height} />
|
|
863
1227
|
${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
|
-
${
|
|
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} />`; })}
|
|
1228
|
+
${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
1229
|
<rect ref=${miniViewRef} class="oc-mini-view oo-mini-view" />
|
|
867
1230
|
</svg>
|
|
868
1231
|
</div>
|