@songsid/agend 2.3.2-beta.8 → 2.3.2
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/README.md +6 -0
- package/README.zh-TW.md +6 -0
- package/dist/agent-cli.js +48 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +6 -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/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.js +23 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +15 -0
- package/dist/backend/codex-session-lookup.js +58 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +15 -4
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +57 -0
- package/dist/backend/kiro-classic-archive.js +622 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-classic-links.d.ts +24 -0
- package/dist/backend/kiro-classic-links.js +135 -0
- package/dist/backend/kiro-classic-links.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +100 -1
- package/dist/backend/kiro-identity.js +262 -31
- 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-reconcile-store.d.ts +27 -0
- package/dist/backend/kiro-reconcile-store.js +344 -0
- package/dist/backend/kiro-reconcile-store.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +35 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +31 -1
- package/dist/backend/kiro-v2-store.js +22 -12
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +124 -1
- package/dist/backend/kiro.js +553 -29
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +102 -0
- package/dist/backend/types.js +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-service.d.ts +13 -1
- package/dist/cache-service.js +45 -2
- package/dist/cache-service.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 +40 -5
- 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 +6 -0
- package/dist/classic-channel-manager.js +25 -0
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +50 -4
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +17 -8
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +66 -0
- package/dist/daemon.js +421 -19
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +15 -4
- package/dist/dashboard-permissions.js +49 -23
- 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 +90 -3
- package/dist/fleet-manager.js +569 -41
- 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-instance.d.ts +24 -0
- package/dist/general-instance.js +53 -0
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +60 -18
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +60 -18
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +7 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/kiro-migrate.d.ts +19 -2
- package/dist/kiro-migrate.js +133 -35
- package/dist/kiro-migrate.js.map +1 -1
- package/dist/kiro-reconcile.d.ts +21 -0
- package/dist/kiro-reconcile.js +88 -0
- package/dist/kiro-reconcile.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-tui-forensic.d.ts +19 -0
- package/dist/kiro-tui-forensic.js +124 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/locale.js +198 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +2 -2
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you.d.ts +3 -1
- package/dist/needs-you.js +4 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/office-levels.d.ts +64 -0
- package/dist/office-levels.js +66 -0
- package/dist/office-levels.js.map +1 -0
- package/dist/office-share-page.d.ts +3 -1
- package/dist/office-share-page.js +8 -3
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +6 -3
- package/dist/office-share-service.js +12 -8
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +80 -13
- package/dist/office-share.js +145 -25
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +20 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +47 -2
- package/dist/outbound-schemas.js +41 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +39 -1
- package/dist/quickstart-api.js +115 -17
- package/dist/quickstart-api.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/scheduler/task-board.d.ts +28 -6
- package/dist/scheduler/task-board.js +66 -11
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/service-installer.d.ts +13 -0
- package/dist/service-installer.js +15 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +25 -0
- package/dist/service-owner.js +43 -2
- package/dist/service-owner.js.map +1 -1
- package/dist/settings-api.d.ts +2 -0
- package/dist/settings-api.js +18 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +10 -5
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +1 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-form.d.ts +2 -2
- package/dist/setup-form.js +139 -93
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.d.ts +25 -0
- package/dist/setup-i18n.js +226 -0
- package/dist/setup-i18n.js.map +1 -0
- package/dist/task-board-waker.js +7 -2
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tool-permissions.js +18 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/transcript-sources.d.ts +1 -0
- package/dist/transcript-sources.js +20 -3
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/ui/backend-signin.js +4 -3
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-office-card.js +114 -0
- package/dist/ui/fleet-office-editor.js +81 -9
- package/dist/ui/fleet-office-layout.js +87 -10
- package/dist/ui/fleet-office-levels.js +105 -0
- package/dist/ui/fleet-office-model.js +9 -1
- package/dist/ui/fleet-office-pack.d.ts +18 -3
- package/dist/ui/fleet-office-pack.js +217 -23
- package/dist/ui/fleet-office-skin.js +39 -3
- package/dist/ui/fleet-office-store.js +74 -3
- package/dist/ui/fleet-office-view.js +8 -0
- package/dist/ui/fleet-office-zones.js +102 -19
- package/dist/ui/fleet-office.js +323 -50
- package/dist/ui/fleet-org.js +2 -0
- package/dist/ui/fleet-task-card.js +2 -2
- package/dist/ui/fleet-task-common.js +30 -7
- package/dist/ui/fleet-task-work.js +2 -2
- package/dist/ui/fleet-tasks.js +10 -11
- package/dist/ui/office-share-dialog.js +31 -7
- package/dist/ui/office-share-page.js +50 -1
- package/dist/ui/panel-chat.js +14 -2
- package/dist/ui/panel-log.js +4 -4
- package/dist/ui/panel-needs.js +1 -1
- package/dist/ui/panel-settings.js +16 -8
- package/dist/ui/settings-discord.js +3 -2
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +17 -9
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +16 -4
- package/dist/ui/settings-token.js +3 -2
- package/dist/ui/settings-wizard.js +25 -9
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-stream.js +1 -1
- package/dist/ui/shared/app.css +74 -2
- package/dist/ui/shared/setup-host-text.js +107 -0
- package/dist/ui/structured-signin.js +89 -38
- package/dist/usage-rollup.d.ts +34 -3
- package/dist/usage-rollup.js +56 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +10 -1
- package/dist/web-api.js +207 -17
- 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-office-acts.d.ts +5 -2
- package/dist/web-office-acts.js +9 -4
- package/dist/web-office-acts.js.map +1 -1
- 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 +241 -0
- package/dist/web-office-pack-tools.js +785 -0
- package/dist/web-office-pack-tools.js.map +1 -0
- package/dist/web-office-skins.d.ts +68 -4
- package/dist/web-office-skins.js +324 -33
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +47 -4
- package/dist/web-office-store.js +149 -15
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +2 -2
- package/templates/windows-shortcuts.ps1 +18 -17
package/dist/ui/fleet-office.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
// goes through the same view transform.
|
|
16
16
|
// - Speech bubbles: now and then a short line for its state (typed out; at once with reduced motion) — the page's own
|
|
17
17
|
// lines or the skin's, never anything an agent said. Furniture can react: an item with `activeWhen` animates while
|
|
18
|
-
// anyone shown is in that state
|
|
18
|
+
// anyone shown is in that state — or (#2698, "activeOn": "seated") once its person sits down; one that "hides" its
|
|
19
|
+
// people carries their name tags and bubbles while it is active, and hovering it hits them (furnitureAt).
|
|
19
20
|
// - Each occupant is a little pixel person in a pose that loops a few frames: working (typing, the lamp on), idle (tea),
|
|
20
21
|
// paused or stopped (asleep, a closed sign), needs you / crashed / looks stuck (a hand up, a blinking light).
|
|
21
22
|
// - Traffic: each new message, delegation or report is an item carried from the sender's desk to the receiver's along
|
|
@@ -59,22 +60,24 @@ import { appStore } from "/assets/app-store.js";
|
|
|
59
60
|
import { t, lang, register } from "/assets/app-i18n.js";
|
|
60
61
|
import { identityOf } from "/assets/identity-color.js";
|
|
61
62
|
import { Icon } from "/assets/ui-icons.js";
|
|
62
|
-
import { nodeText } from "./fleet-org.js";
|
|
63
|
+
import { nodeText, stateWords } from "./fleet-org.js";
|
|
63
64
|
import { hits, needle } from "./fleet-canvas-layout.js";
|
|
64
65
|
import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
|
|
65
66
|
officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
|
|
66
67
|
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 { CALLING, activeFurniture, bubbleAt, hiddenBy, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
|
|
68
69
|
import { actsOk, createOfficeStore } from "./fleet-office-store.js";
|
|
70
|
+
import { badgeOf, fixedOf } from "./fleet-office-levels.js";
|
|
71
|
+
import { OfficeCard, cardModel } from "./fleet-office-card.js";
|
|
69
72
|
import { POLL_MS } from "./fleet-traffic.js";
|
|
70
73
|
import { OfficeEditor, officeForSave } from "./fleet-office-editor.js";
|
|
71
74
|
import { LayoutEditor, handlesOf } from "./fleet-office-layout.js";
|
|
72
75
|
import { ShareButton } from "./office-share-dialog.js";
|
|
73
|
-
import { applyOverrides } from "./fleet-office-pack.js";
|
|
76
|
+
import { applyOverrides, fitNight, fitSeatFor, stripBox } from "./fleet-office-pack.js";
|
|
74
77
|
import { ALL_ID, LOBBY_TILE, OV, createCrossFlights, cropToRooms, fromTile, inTile, labelAt, lobbyNodes, overviewOk, tileAt, tileBox, tileLayout, visibleTiles } from "./fleet-office-overview.js";
|
|
75
78
|
import { centreOn, gestureState, gestures, miniPoint, toScreen, toWorld, wheelFactor, zoomKey, zoomTo } from "./fleet-canvas-view.js";
|
|
76
79
|
import { OK_MAX, OK_MIN, clampPan, desksK, edgeMatte, edgeStrips, fitScene, layerScale, miniBox, miniFrame, openingView, pinchOffice, savedOf, stepK, viewFromSaved,
|
|
77
|
-
zoomLabel, zoomOffice } from "./fleet-office-view.js";
|
|
80
|
+
wholeInView, zoomLabel, zoomOffice } from "./fleet-office-view.js";
|
|
78
81
|
|
|
79
82
|
register("orgo", {
|
|
80
83
|
en: {
|
|
@@ -88,6 +91,8 @@ register("orgo", {
|
|
|
88
91
|
b_paused: "Zzz…|Back later", b_stopped: "Off duty|Zzz…", b_needs: "Need you!|A question for you|Over here!",
|
|
89
92
|
b_crashed: "Ouch, crashed|Help!", b_stuck: "Hmm, stuck?|Still thinking…",
|
|
90
93
|
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.",
|
|
94
|
+
styleForAll: "The whole fleet's style, for everyone: every device and share link", styleHere: "The whole fleet's style in this browser",
|
|
95
|
+
styleFailed: "The style was not saved for everyone ({0}).", styleBusy: "Too many changes. Try again in a minute.",
|
|
91
96
|
fit: "Fit the office", overview: "Overview: click to move there", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen",
|
|
92
97
|
fill: "Fill", fillTitle: "Fill the panel (the scene's overflow is cropped; drag to see it)", zoomLevel: "Zoom {0}",
|
|
93
98
|
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.",
|
|
@@ -104,6 +109,8 @@ register("orgo", {
|
|
|
104
109
|
b_paused: "Zzz…|晚點回來", b_stopped: "下班了|Zzz…", b_needs: "需要你!|有個問題|在這裡!",
|
|
105
110
|
b_crashed: "糟糕,當掉了|救命!", b_stuck: "嗯,卡住了?|還在想…",
|
|
106
111
|
hideAsleep: "隱藏暫停與停止的成員", nightOn: "0–6 點就寢:閒著的人去睡", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
|
|
112
|
+
styleForAll: "整個 fleet 的風格,所有人共用:每台裝置與分享連結都一樣", styleHere: "這個瀏覽器裡整個 fleet 的風格",
|
|
113
|
+
styleFailed: "風格沒有替所有人儲存({0})。", styleBusy: "變更太頻繁,請一分鐘後再試。",
|
|
107
114
|
fit: "顯示整個辦公室", overview: "總覽:點一下移到那裡", fullscreen: "全螢幕", exitFullscreen: "結束全螢幕",
|
|
108
115
|
fill: "填滿", fillTitle: "填滿整個面板(超出的部分會被裁切,拖曳即可看到)", zoomLevel: "縮放 {0}",
|
|
109
116
|
allOffices: "所有辦公室", labelAll: "所有辦公室:每間辦公室是一格,以它自己的風格顯示。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示全部,F 切換全螢幕。雙擊一格即可開啟那間辦公室。",
|
|
@@ -147,6 +154,14 @@ function sampledMatte(img) {
|
|
|
147
154
|
return m;
|
|
148
155
|
}
|
|
149
156
|
const now = () => (typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now());
|
|
157
|
+
/** #2661: how often the levels are read again while the Office is shown (the host reuses them for a minute). */
|
|
158
|
+
const LEVELS_MS = 60_000;
|
|
159
|
+
/** #2661: how long a level-up's ring shows. */
|
|
160
|
+
const UP_MS = 1800;
|
|
161
|
+
/** #2661: Find in office zooms in to at least this (CSS px a logical unit). */
|
|
162
|
+
const FIND_K = 2;
|
|
163
|
+
/** #2661: below this stage width (CSS px) the card is a bottom sheet. */
|
|
164
|
+
const SHEET_W = 560;
|
|
150
165
|
/** How often the picture can change when nothing is in the air (the poses loop at 4 fps; a bubble types faster). */
|
|
151
166
|
const FPS = 12;
|
|
152
167
|
const STYLES = BUILT_IN_SKINS;
|
|
@@ -166,6 +181,7 @@ function readPal(el) {
|
|
|
166
181
|
bg: v("--bg", D.bg), surface: v("--surface", D.surface), surface2: v("--surface-2", D.surface2), border: v("--border", D.border),
|
|
167
182
|
text: v("--text", D.text), text2: v("--text-2", D.text2), text3: v("--text-3", D.text3), ok: v("--ok", D.ok), warn: v("--warn", D.warn),
|
|
168
183
|
danger: v("--danger", D.danger), focus: v("--focus", D.focus),
|
|
184
|
+
accent: v("--accent", D.accent), accentDim: v("--accent-dim", D.accentDim), okBg: v("--ok-bg", D.okBg), warnBg: v("--warn-bg", D.warnBg), dangerBg: v("--danger-bg", D.dangerBg),
|
|
169
185
|
fx: { task: v("--fx-task", D.fx.task), send: v("--fx-send", D.fx.send), report: v("--fx-report", D.fx.report), broadcast: v("--fx-broadcast", D.fx.broadcast) },
|
|
170
186
|
};
|
|
171
187
|
}
|
|
@@ -178,22 +194,26 @@ function teamOptions(view) {
|
|
|
178
194
|
|
|
179
195
|
// ── A scene: one office's layout, looks and skin, drawn by itself (the single view) or as a tile (All offices, #2554) ──
|
|
180
196
|
|
|
181
|
-
/**
|
|
182
|
-
|
|
197
|
+
/**
|
|
198
|
+
* What each desk shows: the pose, the light, the coat; dimmed when the Find box matches someone else; #2661: its level
|
|
199
|
+
* badge (`badge(name)`: fleet-office-levels.js badgeOf, or nothing).
|
|
200
|
+
*/
|
|
201
|
+
function looksOf(L, work, q, mode, zones, badge = () => null) {
|
|
183
202
|
return new Map(L.rooms.flatMap((r) => r.desks).map((d) => {
|
|
184
203
|
const n = d.node;
|
|
185
204
|
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
205
|
const match = !!q && hits(n, q);
|
|
187
206
|
// At night (#2568) the idle are drawn asleep (their state stays idle: the tooltip and the list say so).
|
|
188
207
|
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
|
|
208
|
+
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,
|
|
209
|
+
badge: badge(n.name) }];
|
|
190
210
|
}));
|
|
191
211
|
}
|
|
192
212
|
/** A stored office's own layout on a zones style (its overrides); `ignored`: they do not fit this style. */
|
|
193
213
|
function withOverrides(baseSkin, zones, office) {
|
|
194
214
|
const ov = zones && office ? office.overrides : null;
|
|
195
215
|
if (!ov || ov.pack !== baseSkin.id) return { skin: baseSkin, ignored: false };
|
|
196
|
-
const a = applyOverrides(baseSkin.pack, ov);
|
|
216
|
+
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
217
|
return a.ok ? { skin: { ...baseSkin, pack: a.pack }, ignored: false } : { skin: baseSkin, ignored: true };
|
|
198
218
|
}
|
|
199
219
|
/** 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`. */
|
|
@@ -203,14 +223,56 @@ function officeScene(office, nodes, skin, zones, deskW, prevSeats, night = false
|
|
|
203
223
|
return lay ? customLayout({ ...office, layout: lay }, nodes) : autoLayout([{ key: office.id, name: office.name, lobby: !!office.lobby, nodes }], { width: deskW });
|
|
204
224
|
}
|
|
205
225
|
/** 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) {
|
|
226
|
+
export function partOf(L, looks, skin, zones, walkers, flights) {
|
|
207
227
|
const all = L.rooms.flatMap((r) => r.desks);
|
|
208
228
|
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
|
-
|
|
229
|
+
// #2661: each desk's room (Desks: who is in the same area as a level-up, drawPart).
|
|
230
|
+
const roomOf = new Map(L.rooms.flatMap((r) => r.desks.map((d) => [d.key, r.key])));
|
|
231
|
+
return { L, looks, skin, zones, walkers, flights, all, desks: all.filter((d) => d.node), layers: null, roomOf,
|
|
210
232
|
active: zones ? activeFurniture(skin.pack, shown, byName) : new Set(), lines: bubbleDefaults(), lang: lang() };
|
|
211
233
|
}
|
|
212
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* At bedtime (#2568): each item with a `night` drawn as it — its strip playing, at its night place: the office's
|
|
236
|
+
* (Edit layout, #2665: applyOverrides puts it on `night`), else the pack's night x/y, else its (moved) day place.
|
|
237
|
+
*/
|
|
213
238
|
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));
|
|
239
|
+
/** #2698: where someone is at `t` against their seat: "walking"; "seated" (still, at the seat zones gave them — or no
|
|
240
|
+
* walker yet: drawn there); "standing" (still elsewhere: a stroll's spot, an act's point, the seat left before a walk). */
|
|
241
|
+
const postureAt = (p, t) => {
|
|
242
|
+
const seats = new Map(p.all.filter((d) => d.node).map((d) => [d.node.name, d.seat]));
|
|
243
|
+
return (name) => {
|
|
244
|
+
const at = p.walkers && p.walkers.at(name, t), s = seats.get(name);
|
|
245
|
+
if (!at) return "seated";
|
|
246
|
+
if (at.walking) return "walking";
|
|
247
|
+
return s && Math.abs(at.x - s.x) < 1 && Math.abs(at.y - s.y) < 1 ? "seated" : "standing";
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* The furniture active at `t` and who is drawn on an item instead of themselves (`hidden`: name → item id). A pack
|
|
252
|
+
* with no "activeOn": "seated" nor "hides" (#2698): the scene's `active`, as before, and nobody hidden; else worked out
|
|
253
|
+
* from where everyone is at `t` (their walkers). An item asleep at bedtime (a `night`) hides nobody.
|
|
254
|
+
*/
|
|
255
|
+
export function furnitureAt(p, t) {
|
|
256
|
+
const pk = p.zones && p.skin && p.skin.pack, list = (pk && pk.furniture) || [];
|
|
257
|
+
if (!list.some((f) => f.activeOn === "seated" || f.hides)) return { active: p.active || new Set(), hidden: new Map() };
|
|
258
|
+
const looks = [...p.looks.values()], byName = new Map(looks.filter((l) => l.name).map((l) => [l.name, l.state]));
|
|
259
|
+
const posture = postureAt(p, t), active = activeFurniture(pk, new Set(looks.map((l) => l.state)), byName, posture);
|
|
260
|
+
const hidden = hiddenBy(pk, active, byName, posture);
|
|
261
|
+
if (p.night) for (const [n, id] of [...hidden]) if (list.some((f) => f.id === id && f.night)) hidden.delete(n);
|
|
262
|
+
return { active, hidden };
|
|
263
|
+
}
|
|
264
|
+
/** #2698: the box an item hiding its people is hit in — its active picture, down to its name tags' row. */
|
|
265
|
+
const hidingBox = (f) => {
|
|
266
|
+
const b = stripBox(f.activeSprite || f.sprite, f.x, f.y);
|
|
267
|
+
return { ...b, h: Math.max(b.h, f.y + 17 - b.y) };
|
|
268
|
+
};
|
|
269
|
+
/** #2698: name tags in one row centred on (x, y) — each { look, x } (its centre), TAG_GAP apart. */
|
|
270
|
+
const TAG_GAP = 4;
|
|
271
|
+
export function tagRow(ctx, x, y, looks) {
|
|
272
|
+
const ws = looks.map((l) => tagBox(ctx, x, y, l.label).w), total = ws.reduce((a, w) => a + w, 0) + TAG_GAP * Math.max(0, ws.length - 1);
|
|
273
|
+
let left = x - total / 2;
|
|
274
|
+
return looks.map((look, i) => { const cx = Math.round(left + ws[i] / 2); left += ws[i] + TAG_GAP; return { look, x: cx }; });
|
|
275
|
+
}
|
|
214
276
|
/** #2596: each NPC with a `walk` where its walker is now (`at(key)`): moved there — its tag and depth with it — and, while
|
|
215
277
|
* it walks, `walking` and its `heading` (its walk strip: walkStrip); asleep, or with no walker (not strolling), where it is. */
|
|
216
278
|
export const strollFurniture = (list, at) => list.map((f) => {
|
|
@@ -238,6 +300,8 @@ export function ghostsOf(pack, people = []) {
|
|
|
238
300
|
}
|
|
239
301
|
return out;
|
|
240
302
|
}
|
|
303
|
+
/** #2665: Edit layout's faint night places by day — each item with a `night` as it is drawn at bedtime (nightFurniture). */
|
|
304
|
+
export const nightGhostsOf = (pack) => nightFurniture(pack.furniture.filter((f) => f.night));
|
|
241
305
|
/** All offices: the tile `name` is drawn in ({ tile, skin }), or null. */
|
|
242
306
|
const senderTile = (parts, name) => (name ? parts.find((p) => desksOf(p.L).some((d) => d.node.name === name)) || null : null);
|
|
243
307
|
/**
|
|
@@ -304,7 +368,7 @@ function paintLayers(p, env, ls, tagsOff = false) {
|
|
|
304
368
|
* hovered (its name tag shows even in a crowd); `tagsOff` (#2554: a tile too small to read them): the name tags show
|
|
305
369
|
* only for the one hovered and the Find box's matches. Whether anything in it is moving (in the air, or someone walking).
|
|
306
370
|
*/
|
|
307
|
-
function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer = null) {
|
|
371
|
+
export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer = null) {
|
|
308
372
|
const sk = p.skin;
|
|
309
373
|
const ls = layerScale(a, p.L.width, p.L.height);
|
|
310
374
|
if (!p.layers || p.layers.ls !== ls || p.layers.tagsOff !== tagsOff) p.layers = paintLayers(p, env, ls, tagsOff);
|
|
@@ -321,7 +385,7 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
321
385
|
// Furniture under the people; the people (nearest last) with the furniture whose depth is "y" among them, by base
|
|
322
386
|
// y (a tie: the item first, the person in front); furniture over them; things in the air; the pack's foreground
|
|
323
387
|
// (door frames, pillars, table fronts); then what must stay readable: crowd counts and name tags (bubbles below).
|
|
324
|
-
const t = ms > 0 ? ms : now();
|
|
388
|
+
const t = ms > 0 ? ms : now(), live = furnitureAt(p, t);
|
|
325
389
|
const people = p.desks.map((d) => {
|
|
326
390
|
const at = p.walkers.at(d.node.name, t) || { ...d.seat, walking: false };
|
|
327
391
|
if (at.walking) walking = true;
|
|
@@ -330,13 +394,17 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
330
394
|
const furniture = strollFurniture(p.night ? nightFurniture(sk.pack.furniture) : sk.pack.furniture, (k) => p.walkers.at(k, t));
|
|
331
395
|
if (furniture.some((f) => f.walking)) walking = true;
|
|
332
396
|
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 })),
|
|
333
|
-
...(p.ghosts ? ghostsOf(sk.pack, people).map((g) => ({ y: g.y, k: 1, g })) : [])
|
|
397
|
+
...(p.ghosts ? ghostsOf(sk.pack, people).map((g) => ({ y: g.y, k: 1, g })) : []),
|
|
398
|
+
// #2665: by day, each item's night place too (at night the items are there already).
|
|
399
|
+
...(p.ghosts && !p.night ? nightGhostsOf(sk.pack).map((n) => ({ y: n.y, k: 1, n })) : [])]
|
|
334
400
|
.sort((x, y) => x.y - y.y || x.k - y.k);
|
|
335
|
-
for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep ||
|
|
401
|
+
for (const f of furniture) if (f.depth !== "y" && f.depth < CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env);
|
|
336
402
|
for (const it of post) if (it.t === 0) sk.drawProp(ctx, it, ms, env); // a letter on the floor: under the people
|
|
337
|
-
for (const { f, p: q, g } of band) {
|
|
338
|
-
if (f) { sk.drawFurniture(ctx, f, f.asleep ||
|
|
403
|
+
for (const { f, p: q, g, n } of band) {
|
|
404
|
+
if (f) { sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env); continue; }
|
|
405
|
+
if (n) { ctx.save(); ctx.globalAlpha = GHOST_ALPHA; sk.drawFurniture(ctx, n, true, 0, env); ctx.restore(); continue; }
|
|
339
406
|
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; }
|
|
407
|
+
if (live.hidden.has(q.look.name)) continue; // #2698: drawn as the item (never while walking)
|
|
340
408
|
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 } };
|
|
341
409
|
// An agent's own pose (office_set, #2578) while its act lasts and nothing calls it; walking is walking.
|
|
342
410
|
const posed = actPosed(p, q.look);
|
|
@@ -344,7 +412,7 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
344
412
|
sk.drawCharacter(ctx, box, look, ms, env);
|
|
345
413
|
if (!q.at.walking) speakers.push({ look: q.look, ...speakerAt(sk, look, q.at), area: q.d.area });
|
|
346
414
|
}
|
|
347
|
-
for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep ||
|
|
415
|
+
for (const f of furniture) if (f.depth !== "y" && f.depth >= CHAR_DEPTH) sk.drawFurniture(ctx, f, f.asleep || live.active.has(f.id), ms, env);
|
|
348
416
|
for (const it of up) sk.drawProp(ctx, it, ms, env);
|
|
349
417
|
for (const it of post) if (it.t > 0) sk.drawProp(ctx, it, ms, env);
|
|
350
418
|
if (sk.drawForeground) sk.drawForeground(ctx, p.L, env);
|
|
@@ -352,21 +420,62 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
352
420
|
for (const [ar, n] of p.L.crowd || []) if (p.L.areas[ar] && sk.drawCrowd) sk.drawCrowd(ctx, p.L.areas[ar], n, env);
|
|
353
421
|
// In an area over capacity the name tags show on hover or focus only (#2513: a crowd's tags covered each other).
|
|
354
422
|
const over = p.L.over || new Set();
|
|
423
|
+
const tagged = (q) => !((tagsOff || over.has(q.d.area)) && q.d.key !== hot && !q.look.match);
|
|
355
424
|
for (const q of people) {
|
|
356
|
-
if (
|
|
425
|
+
if (live.hidden.has(q.look.name) || !tagged(q)) continue;
|
|
357
426
|
sk.drawTag(ctx, q.at.x, q.at.y, q.look, env);
|
|
358
427
|
tags.push(tagBox(ctx, q.at.x, q.at.y, q.look.label));
|
|
359
428
|
}
|
|
360
429
|
// A furniture item's own name (#2574: an NPC drawn as furniture), tagged like a person; none on a tile too small to read.
|
|
361
|
-
|
|
430
|
+
// #2698: an item hiding its people carries their tags too, in one row at its tag point (its own first; each keeps
|
|
431
|
+
// its place in the row while another's tag is left out), and their bubbles, over its picture. The tags drawn are
|
|
432
|
+
// kept on the scene (`heldTags`): hitPart hits each person on theirs, past the picture's edge too (#2699 review).
|
|
433
|
+
const held = [];
|
|
434
|
+
for (const f of furniture) {
|
|
435
|
+
const on = people.filter((q) => live.hidden.get(q.look.name) === f.id);
|
|
436
|
+
if (!on.length) {
|
|
437
|
+
if (!tagsOff && f.label) { const nl = npcLook(p, f); sk.drawTag(ctx, f.x, f.y, nl, env); tags.push(tagBox(ctx, f.x, f.y, nl.label)); }
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const order = new Map((f.activeFor || []).map((n, i) => [n, i]));
|
|
441
|
+
on.sort((x, y) => (order.get(x.look.name) ?? 0) - (order.get(y.look.name) ?? 0));
|
|
442
|
+
const row = tagRow(ctx, f.x, f.y, [...(f.label ? [npcLook(p, f)] : []), ...on.map((q) => q.look)]);
|
|
443
|
+
const top = f.sprite ? Math.min(f.y - 50, stripBox(f.activeSprite || f.sprite, f.x, f.y).y + 2) : f.y - 50;
|
|
444
|
+
for (const [i, r] of row.entries()) {
|
|
445
|
+
const q = f.label ? on[i - 1] : on[i];
|
|
446
|
+
if (q) speakers.push({ look: q.look, x: r.x + 10, y: top, area: q.d.area });
|
|
447
|
+
if (q ? !tagged(q) : tagsOff) continue;
|
|
448
|
+
sk.drawTag(ctx, r.x, f.y, r.look, env);
|
|
449
|
+
const box = tagBox(ctx, r.x, f.y, r.look.label);
|
|
450
|
+
tags.push(box);
|
|
451
|
+
if (q) held.push({ name: q.look.name, id: f.id, box });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
p.heldTags = held;
|
|
362
455
|
} else {
|
|
363
|
-
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 }); }
|
|
456
|
+
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, area: p.roomOf.get(d.key) ?? null }); }
|
|
364
457
|
blit(p.layers.front);
|
|
365
458
|
// Tags left out of the layer: the hovered desk's front again, with its tag.
|
|
366
459
|
const hd = tagsOff && hot ? p.desks.find((d) => d.key === hot) : null;
|
|
367
460
|
if (hd) sk.drawDeskFront(ctx, hd, p.looks.get(hd.key), env);
|
|
368
461
|
for (const it of up) sk.drawProp(ctx, it, ms, env);
|
|
369
462
|
}
|
|
463
|
+
// #2661: a level up — a short ring over the head (UP_MS), and the people in the same area (a room's on Desks) glance
|
|
464
|
+
// over: a small "!" over theirs. Never with reduced motion (no loop: ms 0).
|
|
465
|
+
const rising = ms && p.ups && p.ups.size ? speakers.filter((sp) => { const t0 = p.ups.get(sp.look.name); return t0 !== undefined && now() - t0 < UP_MS; }) : [];
|
|
466
|
+
for (const sp of rising) {
|
|
467
|
+
const f = (now() - p.ups.get(sp.look.name)) / UP_MS;
|
|
468
|
+
ctx.save();
|
|
469
|
+
ctx.globalAlpha = 1 - f;
|
|
470
|
+
ctx.strokeStyle = env.pal.warn; ctx.lineWidth = 2;
|
|
471
|
+
ctx.beginPath(); ctx.arc(sp.x, sp.y - 8, 6 + f * 14, 0, Math.PI * 2); ctx.stroke();
|
|
472
|
+
ctx.fillStyle = env.pal.text; ctx.font = "700 9px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "bottom";
|
|
473
|
+
for (const o of speakers) {
|
|
474
|
+
if (o === sp || o.look.dim || o.look.pose === "away" || (o.area ?? null) !== (sp.area ?? null)) continue;
|
|
475
|
+
ctx.fillText("!", o.x, o.y - 4);
|
|
476
|
+
}
|
|
477
|
+
ctx.restore();
|
|
478
|
+
}
|
|
370
479
|
// Speech bubbles: a short line for the state, now and then (typed out; whole at once when still), placed so no two
|
|
371
480
|
// overlap (placeBubbles; #2431).
|
|
372
481
|
const bubbles = [];
|
|
@@ -384,16 +493,44 @@ function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false, defer
|
|
|
384
493
|
const placed = placeBubbles(thinBubbles(bubbles, p.zones ? p.L.over : null), 22, tags);
|
|
385
494
|
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); };
|
|
386
495
|
if (defer) defer.push(say); else say();
|
|
387
|
-
return walking || up.length > 0 || p.flights.size() > 0 || post.some((it) => it.t > 0);
|
|
496
|
+
return walking || up.length > 0 || p.flights.size() > 0 || post.some((it) => it.t > 0) || rising.length > 0;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* #2661: an NPC's name tag (a furniture item's label) — with the title a pack pins on it (`levels.fixed` by its id),
|
|
500
|
+
* while levels are on: "label · title", and a mark for a badge (an NPC has no level).
|
|
501
|
+
*/
|
|
502
|
+
function npcLook(p, f) {
|
|
503
|
+
const fx = p.levelsOn ? fixedOf(f.id, p.skin.pack && p.skin.pack.levels) : null;
|
|
504
|
+
return fx ? { label: `${f.label} · ${fx.title}`, badge: { mark: true, tier: 0 } } : { label: f.label };
|
|
388
505
|
}
|
|
389
506
|
/**
|
|
390
507
|
* 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
|
|
391
508
|
* in (an uploaded character's strip, anchor and size: #2513 review), nearest first; on desks, the desk.
|
|
392
509
|
*/
|
|
393
|
-
function hitPart(p, x, y, t) {
|
|
510
|
+
export function hitPart(p, x, y, t) {
|
|
394
511
|
if (!p.zones) return deskAt(p.L, x, y);
|
|
395
|
-
|
|
396
|
-
|
|
512
|
+
// #2698: someone an item hides is hit on it, after everyone drawn as themselves: on their tag as drawPart last drew
|
|
513
|
+
// it (the row can run past the picture: a long label, long names), else on its picture and the tag row under it
|
|
514
|
+
// (several share its width, left to right in the row's order).
|
|
515
|
+
const { hidden } = furnitureAt(p, t);
|
|
516
|
+
const d = personAt(hidden.size ? p.all.filter((dk) => !dk.node || !hidden.has(dk.node.name)) : p.all, x, y, (dk) => p.walkers.at(dk.node.name, t),
|
|
517
|
+
(dk, at) => (p.skin.charBox && p.looks.get(dk.key) ? p.skin.charBox(p.looks.get(dk.key), at) : null));
|
|
518
|
+
if (d || !hidden.size) return d;
|
|
519
|
+
for (const h of [...(p.heldTags || [])].reverse()) {
|
|
520
|
+
const b = h.box;
|
|
521
|
+
if (hidden.get(h.name) !== h.id || x < b.x || x >= b.x + b.w || y < b.y || y >= b.y + b.h) continue;
|
|
522
|
+
return p.all.find((dk) => dk.node && dk.node.name === h.name) || null;
|
|
523
|
+
}
|
|
524
|
+
const sk = p.skin.pack, drawn = strollFurniture(p.night ? nightFurniture(sk.furniture) : sk.furniture, (k) => p.walkers.at(k, t));
|
|
525
|
+
for (const f of [...drawn].reverse()) {
|
|
526
|
+
if (!f.sprite || ![...hidden.values()].includes(f.id)) continue;
|
|
527
|
+
const b = hidingBox(f);
|
|
528
|
+
if (x < b.x || x >= b.x + b.w || y < b.y || y >= b.y + b.h) continue;
|
|
529
|
+
const order = new Map((f.activeFor || []).map((n, i) => [n, i]));
|
|
530
|
+
const on = p.all.filter((dk) => dk.node && hidden.get(dk.node.name) === f.id).sort((m, n) => (order.get(m.node.name) ?? 0) - (order.get(n.node.name) ?? 0));
|
|
531
|
+
return on[Math.min(on.length - 1, Math.floor(((x - b.x) / b.w) * on.length))] || null;
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
397
534
|
}
|
|
398
535
|
|
|
399
536
|
function People({ nodes, pinned, onChange }) {
|
|
@@ -426,10 +563,11 @@ function People({ nodes, pinned, onChange }) {
|
|
|
426
563
|
/**
|
|
427
564
|
* The view: `view` buildOrgView's answer; `traffic` the live items (null: Live is off); `q` the Find box's needle;
|
|
428
565
|
* `lease` the tab's (its reads); `onOpen(name)` what a desk opens (the canvas's: the chat; none: a desk opens nothing,
|
|
429
|
-
* as on a share link's page, #2636); `assetUrl(path)` where a skin pack's picture is read (the share's address)
|
|
566
|
+
* as on a share link's page, #2636); `assetUrl(path)` where a skin pack's picture is read (the share's address), and
|
|
567
|
+
* `assetImage(url)` how (the share's, which waits out a 429, #2662; none: the loader's own);
|
|
430
568
|
* `opening` the office it opens on (the share's), instead of the one this browser chose last.
|
|
431
569
|
*/
|
|
432
|
-
export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening }) {
|
|
570
|
+
export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetImage, opening }) {
|
|
433
571
|
const store = useMemo(() => createOfficeStore(lease ? { fetch: (path, o) => lease.fetch(path, o) } : {}), [lease]);
|
|
434
572
|
const [stored, setStored] = useState([]);
|
|
435
573
|
const [storedRev, setStoredRev] = useState(null); // the rev the list was read at (a save sends it)
|
|
@@ -438,7 +576,12 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
438
576
|
const [editing, setEditing] = useState(false);
|
|
439
577
|
const [choice, setChoice] = useState(() => opening ?? store.choice());
|
|
440
578
|
const [filter, setFilter] = useState(() => store.filter());
|
|
441
|
-
|
|
579
|
+
// #2716: the whole-fleet office's style — the host's (for everyone) and this browser's own pick. An admin sees the
|
|
580
|
+
// host's (their own pick only while the host keeps none) and picks for everyone; anyone else's own pick, else the host's.
|
|
581
|
+
const [hostStyle, setHostStyle] = useState(undefined); // undefined: not read yet; null: none kept
|
|
582
|
+
const [ownStyle, setOwnStyle] = useState(() => store.skinChosen());
|
|
583
|
+
const [styleErr, setStyleErr] = useState("");
|
|
584
|
+
const style = canEdit ? hostStyle ?? ownStyle ?? "cozy" : ownStyle ?? hostStyle ?? "cozy";
|
|
442
585
|
const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
|
|
443
586
|
// 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).
|
|
444
587
|
const [nightOn, setNightOn] = useState(() => store.nightOn());
|
|
@@ -460,6 +603,10 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
460
603
|
const [themeRev, setThemeRev] = useState(0);
|
|
461
604
|
const [pal, setPal] = useState(DEFAULT_PAL); // the tokens in force (read once the stage is there)
|
|
462
605
|
const [tip, setTip] = useState(null); // the desk key the tooltip is for
|
|
606
|
+
// #2661: the person whose card is open (their key, as `tip`), and the desk-list entry it was opened from (focus goes
|
|
607
|
+
// back there when it closes); the card's element and where it points (placed every frame: it follows them).
|
|
608
|
+
const [card, setCard] = useState(null);
|
|
609
|
+
const cardFrom = useRef(null), cardEl = useRef(null), cardDesk = useRef(null);
|
|
463
610
|
const stageRef = useRef(null), canvasRef = useRef(null), tipRef = useRef(null), lastType = useRef("mouse");
|
|
464
611
|
// Fullscreen: null, "native" (the Fullscreen API) or "immersive" (the CSS cover); per visit, never kept.
|
|
465
612
|
const ooRef = useRef(null), [fs, setFs] = useState(null), fsRef = useRef(null), fsAsked = useRef(false), fsTarget = useRef(null);
|
|
@@ -479,15 +626,34 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
479
626
|
// The editor's save and its 409 re-read are stamped when sent (`ask`), never when answered; `adopt` returns the
|
|
480
627
|
// snapshot in force, so a re-read the stream overtook leaves the editor's draft on the newer one.
|
|
481
628
|
const asked = useRef(0), shown = useRef(null); // a counter; shown: { rev, offices, arrived }
|
|
629
|
+
const migrated = useRef(false); // #2716: this page's own style sent up
|
|
482
630
|
const ask = () => ++asked.current;
|
|
483
631
|
const adopt = (d, askedAt) => {
|
|
484
632
|
const cur = shown.current;
|
|
485
633
|
if (!d) return cur;
|
|
486
634
|
if (cur && d.rev < cur.rev && askedAt <= cur.arrived) return cur;
|
|
487
635
|
shown.current = { rev: d.rev, offices: d.offices, arrived: ++asked.current };
|
|
488
|
-
setStored(d.offices); setStoredRev(d.rev);
|
|
636
|
+
setStored(d.offices); setStoredRev(d.rev); setHostStyle(d.autoSkin || null);
|
|
489
637
|
return shown.current;
|
|
490
638
|
};
|
|
639
|
+
// #2716: an admin's own pick from before the host kept one (or since an older host's save dropped it) goes up once.
|
|
640
|
+
useEffect(() => {
|
|
641
|
+
if (!canEdit || hostStyle !== null || !ownStyle || migrated.current || !lease) return;
|
|
642
|
+
migrated.current = true;
|
|
643
|
+
void store.saveAutoSkin(ownStyle).then((r) => { if (r.ok && lease.current()) setHostStyle(r.autoSkin); });
|
|
644
|
+
}, [canEdit, hostStyle]);
|
|
645
|
+
/** #2716: a style picked here — this browser's own; an admin's, for everyone (on failure: said, and read again). */
|
|
646
|
+
const pickStyle = (id) => {
|
|
647
|
+
store.saveSkin(id); setOwnStyle(id); setTip(null); setStyleErr("");
|
|
648
|
+
if (!canEdit || !lease) return;
|
|
649
|
+
const was = hostStyle;
|
|
650
|
+
setHostStyle(id);
|
|
651
|
+
void store.saveAutoSkin(id).then((r) => {
|
|
652
|
+
if (!lease.current() || r.ok) return;
|
|
653
|
+
setHostStyle(was);
|
|
654
|
+
setStyleErr(to(r.status === 429 ? "styleBusy" : "styleFailed", r.error));
|
|
655
|
+
});
|
|
656
|
+
};
|
|
491
657
|
useEffect(() => {
|
|
492
658
|
if (!lease) return undefined;
|
|
493
659
|
const read = async () => { const at = ask(); const d = await store.offices(); if (lease.current()) adopt(d, at); };
|
|
@@ -500,6 +666,27 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
500
666
|
// A read a newer one or the stream overtook is dropped (it is older: review — an opening read answered after the one
|
|
501
667
|
// the stream's return began brought a cleared act back); read again when the stream comes back, and — with no live
|
|
502
668
|
// stream (the public link, or a stream polling) — every POLL_MS while the page is shown.
|
|
669
|
+
// #2661: everyone's level (fleet-office-levels.js levelsOk): read on opening and every LEVELS_MS while shown — the
|
|
670
|
+
// host works them out at most once a minute, so nothing here asks more often.
|
|
671
|
+
const [lv, setLv] = useState(null);
|
|
672
|
+
// Who went up a level since the last read (name → when it was seen): a ring over them for UP_MS (drawPart).
|
|
673
|
+
const ups = useRef(new Map()), lastLevels = useRef(null);
|
|
674
|
+
useEffect(() => {
|
|
675
|
+
if (!lease) return undefined;
|
|
676
|
+
const read = async () => {
|
|
677
|
+
const d = await store.levels();
|
|
678
|
+
if (!lease.current() || !d) return;
|
|
679
|
+
const before = lastLevels.current, t = now();
|
|
680
|
+
if (before) for (const [name, e] of d.levels) { const b = before.get(name); if (b && e.level > b.level) ups.current.set(name, t); }
|
|
681
|
+
for (const [name, t0] of ups.current) if (t - t0 >= UP_MS) ups.current.delete(name);
|
|
682
|
+
lastLevels.current = d.levels;
|
|
683
|
+
setLv(d);
|
|
684
|
+
loop.current?.wake();
|
|
685
|
+
};
|
|
686
|
+
void read();
|
|
687
|
+
const h = lease.interval(() => { if (!globalThis.document?.hidden) void read(); }, LEVELS_MS);
|
|
688
|
+
return () => lease.clear(h);
|
|
689
|
+
}, [lease, store]);
|
|
503
690
|
const [acts, setActs] = useState([]);
|
|
504
691
|
const actSeen = useRef(new Set());
|
|
505
692
|
useEffect(() => {
|
|
@@ -585,7 +772,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
585
772
|
if (!want.length) return undefined;
|
|
586
773
|
let live = true;
|
|
587
774
|
const get = lease ? (path, o) => lease.fetch(path, o) : undefined;
|
|
588
|
-
for (const [k, pk] of want) loadPackSkin(pk.id, pk.sha, { ...(get ? { fetch: get } : {}), ...(assetUrl ? { url: assetUrl } : {}) }).then((sk) => { if (live) setPackSkins((m) => new Map(m).set(k, sk)); });
|
|
775
|
+
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)); });
|
|
589
776
|
return () => { live = false; };
|
|
590
777
|
}, [neededKey]);
|
|
591
778
|
const loading = !!packInfo && !packSkins.has(packKey);
|
|
@@ -646,10 +833,10 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
646
833
|
});
|
|
647
834
|
}, [overview, stored, view, packs, packSkins, cozyRev, hideAsleep, lobbyOn, lobbyAll, style, night]);
|
|
648
835
|
const arranged = useMemo(() => tileLayout(tiles.map((tl) => ({ key: tl.key, w: tl.L.width, h: tl.L.height })), size), [tiles, size.w, size.h]);
|
|
649
|
-
const tileLooks = useMemo(() => { const work = workOn(traffic); return tiles.map((tl) => looksOf(tl.L, work, q, mode, tl.zones)); }, [tiles, traffic, q, mode, themeRev]);
|
|
836
|
+
const tileLooks = useMemo(() => { const work = workOn(traffic); return tiles.map((tl) => looksOf(tl.L, work, q, mode, tl.zones, (n) => badgeOf(n, lv, tl.skin.pack && tl.skin.pack.levels))); }, [tiles, traffic, q, mode, themeRev, lv]);
|
|
650
837
|
// The scene the view pans and zooms: the office's, or the overview of every tile.
|
|
651
838
|
const L = useMemo(() => (overview ? { mode: "overview", rooms: [], width: arranged.width, height: arranged.height } : single), [overview, single, arranged]);
|
|
652
|
-
const looks = useMemo(() => looksOf(L, workOn(traffic), q, mode, zones), [L, traffic, q, mode, themeRev]);
|
|
839
|
+
const looks = useMemo(() => looksOf(L, workOn(traffic), q, mode, zones, (n) => badgeOf(n, lv, skin.pack && skin.pack.levels)), [L, traffic, q, mode, themeRev, lv, skin]);
|
|
653
840
|
// #2564: what comes in from other offices — one office's and style's own: a letter's door and drop are that layout's,
|
|
654
841
|
// so another office (or style) starts with none (one shown meanwhile is not carried over to a different room).
|
|
655
842
|
// #2578: the acts of the people in this stored office (by name); none elsewhere. On a desks style (review: accepted
|
|
@@ -725,6 +912,23 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
725
912
|
const v = view0(), p = toScreen(v, d.x, d.y), x = Math.max(4, Math.min(p.x, size.w - 244)), above = p.y > 90;
|
|
726
913
|
el.style.setProperty("transform", above ? `translate(${x}px, ${p.y + 6}px) translateY(-100%)` : `translate(${x}px, ${p.y + d.h * v.k}px)`);
|
|
727
914
|
};
|
|
915
|
+
/**
|
|
916
|
+
* #2661: the card beside its person — to their right (left where it would not fit), level with their head, inside the
|
|
917
|
+
* stage — following them as they walk (a walker's position now, else their seat or desk). A bottom sheet is not placed.
|
|
918
|
+
*/
|
|
919
|
+
const placeCard = () => {
|
|
920
|
+
const el = cardEl.current, c = cardDesk.current;
|
|
921
|
+
if (!el || !c || !el.style || el.classList?.contains("sheet")) return;
|
|
922
|
+
const v = view0(), sz = sizeRef.current;
|
|
923
|
+
const w = c.walks ? walkers.at(c.name, now()) : null;
|
|
924
|
+
const pt = w ? { x: w.x, y: w.y - 52, r: 16 } : { x: c.box.x + c.box.w / 2, y: c.box.y, r: (c.box.w / 2) * v.k };
|
|
925
|
+
const p = toScreen(v, pt.x, pt.y), cw = 320, ch = el.offsetHeight || 260, gap = 12 + (w ? pt.r : pt.r);
|
|
926
|
+
let x = p.x + gap;
|
|
927
|
+
if (x + cw > sz.w - 8) x = p.x - gap - cw;
|
|
928
|
+
x = Math.max(8, Math.min(x, sz.w - cw - 8));
|
|
929
|
+
const y = Math.max(8, Math.min(p.y - 24, sz.h - ch - 8));
|
|
930
|
+
el.style.setProperty("transform", `translate(${Math.round(x)}px, ${Math.round(y)}px)`);
|
|
931
|
+
};
|
|
728
932
|
/**
|
|
729
933
|
* Put the view on everything that shows it beside the canvas: the overview's frame (the part of the scene in view;
|
|
730
934
|
* the whole view on its data-k/x/y), the zoom's percentage, the overlay, the tooltip.
|
|
@@ -739,6 +943,9 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
739
943
|
if (mb && mb.style) { // the overview: inside the stage, scaled to it
|
|
740
944
|
const b = miniBox(size, L.width, L.height);
|
|
741
945
|
for (const [a, val] of [["left", b.x], ["top", b.y], ["width", b.w], ["height", b.h]]) mb.style.setProperty(a, `${val}px`);
|
|
946
|
+
// #2737: hidden while a single office is all in view (it would show what the panel shows); All offices keeps it.
|
|
947
|
+
if (!overview && wholeInView(v, size, L.width, L.height)) mb.setAttribute("data-whole", "");
|
|
948
|
+
else mb.removeAttribute("data-whole");
|
|
742
949
|
}
|
|
743
950
|
const pc = pctRef.current;
|
|
744
951
|
if (pc) { const lb = zoomLabel(v.k); if (pc.textContent !== lb) pc.textContent = lb; pc.setAttribute("aria-label", to("zoomLevel", lb)); }
|
|
@@ -920,18 +1127,19 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
920
1127
|
const base = { ctx, ratio, BW, BH, matte, env, dirty: true };
|
|
921
1128
|
if (overview) {
|
|
922
1129
|
// Each tile a scene of its own, where the overview puts it (arranged.tiles, in the same order).
|
|
923
|
-
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] }));
|
|
1130
|
+
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], ups: ups.current, levelsOn: !!(lv && lv.enabled) }));
|
|
924
1131
|
scene.current = { ...base, overview: true, parts, lay: arranged };
|
|
925
1132
|
} else {
|
|
926
|
-
scene.current = { ...base, overview: false, part: { ...partOf(L, looks, skin, zones, walkers, flights), ghosts, night, mail, acts: actsHere, actSeen: actSeen.current } };
|
|
1133
|
+
scene.current = { ...base, overview: false, part: { ...partOf(L, looks, skin, zones, walkers, flights), ghosts, night, mail, acts: actsHere, actSeen: actSeen.current, ups: ups.current, levelsOn: !!(lv && lv.enabled) } };
|
|
927
1134
|
}
|
|
928
1135
|
if (still) draw(0); else loop.current?.wake();
|
|
929
|
-
}, [L, looks, skin, pal, size.w, size.h, ratio, matte, overview, tiles, tileLooks, arranged, ghosts, night, mail, actsHere]);
|
|
1136
|
+
}, [L, looks, skin, pal, size.w, size.h, ratio, matte, overview, tiles, tileLooks, arranged, ghosts, night, mail, actsHere, lv]);
|
|
930
1137
|
|
|
931
1138
|
/** One frame at `ms` (0: still); whether anything is moving (in the air, or someone walking). */
|
|
932
1139
|
function draw(ms) {
|
|
933
1140
|
const s = scene.current;
|
|
934
1141
|
if (!s) return false;
|
|
1142
|
+
placeCard();
|
|
935
1143
|
const { ctx, BW, BH, env } = s;
|
|
936
1144
|
// The view in bitmap px: a logical unit is `a` px (a whole number at a pixel level), the origin on a whole px.
|
|
937
1145
|
const v = view0(), a = v.k * s.ratio, ox = Math.round(v.x * s.ratio), oy = Math.round(v.y * s.ratio);
|
|
@@ -1010,11 +1218,16 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
1010
1218
|
// Everyone drawn: { key, node, box (the desk, in the view's units), dot (where the overview marks them), office (All
|
|
1011
1219
|
// offices: the tile's name) }. In All offices a key is the tile's prefix and the desk's key: one person in two
|
|
1012
1220
|
// offices is two people here.
|
|
1221
|
+
// #2661: and for the card, where they are (`area`: the zone's or the room's name), the style that draws them, their look.
|
|
1222
|
+
const roomOf = (L0, d) => { const r = L0.rooms.find((rm) => rm.desks.includes(d)); return r ? (r.lobby ? to("lobbyRoom") : r.name) : null; };
|
|
1223
|
+
const areaName = (a) => (a ? ({ desks: to("area_desks"), lounge: to("area_lounge"), alert: to("area_alert"), nap: to("area_nap") })[a] || a : null);
|
|
1013
1224
|
const people = overview
|
|
1014
|
-
? tiles.flatMap((tl, i) => { const tt = arranged.tiles[i];
|
|
1225
|
+
? tiles.flatMap((tl, i) => { const tt = arranged.tiles[i], lk = tileLooks[i];
|
|
1015
1226
|
return tt ? desksOf(tl.L).map((d) => ({ key: tl.prefix + d.key, node: d.node, box: tileBox(tt, d), office: tl.name,
|
|
1016
|
-
dot: fromTile(tt, tl.zones ? d.seat : { x: d.x + d.w / 2, y: d.y + d.h / 2 })
|
|
1017
|
-
|
|
1227
|
+
dot: fromTile(tt, tl.zones ? d.seat : { x: d.x + d.w / 2, y: d.y + d.h / 2 }),
|
|
1228
|
+
area: tl.zones ? areaName(d.area) : roomOf(tl.L, d), skin: tl.skin, look: lk && lk.get(d.key) })) : []; })
|
|
1229
|
+
: 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 },
|
|
1230
|
+
area: zones ? areaName(d.area) : roomOf(L, d), skin, look: looks.get(d.key) }));
|
|
1018
1231
|
// Preact owns the markup, not the view: put it back on the overview, the overlay and the tooltip after every render.
|
|
1019
1232
|
const tipDesk = tip ? people.find((d) => d.key === tip) || null : null;
|
|
1020
1233
|
tipDeskRef.current = tipDesk ? tipDesk.box : null;
|
|
@@ -1077,16 +1290,20 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
1077
1290
|
if (d) show(d.box);
|
|
1078
1291
|
};
|
|
1079
1292
|
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); };
|
|
1293
|
+
// #2661: a click (or a tap) on a person opens their card; on the same person again, or off everyone, closes it; on
|
|
1294
|
+
// someone else, switches to theirs. A double-click skips the card and opens the chat.
|
|
1080
1295
|
const onClick = (e) => {
|
|
1081
1296
|
const d = deskOf(e);
|
|
1082
|
-
if (!d || !d.node.i) { setTip(null); return; }
|
|
1083
|
-
if (lastType.current === "touch"
|
|
1084
|
-
|
|
1297
|
+
if (!d || !d.node.i) { setTip(null); setCard(null); return; }
|
|
1298
|
+
if (lastType.current === "touch") setTip(null);
|
|
1299
|
+
cardFrom.current = null;
|
|
1300
|
+
setCard((cur) => (cur === d.key ? null : d.key));
|
|
1085
1301
|
};
|
|
1086
1302
|
// All offices: a double-click on a tile (off its people) opens that office; the lobby is no office.
|
|
1087
1303
|
const onDblClick = (e) => {
|
|
1088
|
-
const s = scene.current, w = pointOf(e);
|
|
1089
|
-
if (
|
|
1304
|
+
const s = scene.current, w = pointOf(e), hit = deskOf(e);
|
|
1305
|
+
if (hit && hit.node.i) { setCard(null); open(hit.node.name); return; } // #2661: a person's chat, past the card
|
|
1306
|
+
if (!overview || !s || !s.overview || !w || hit) return;
|
|
1090
1307
|
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;
|
|
1091
1308
|
if (tl && !tl.lobby) pick(tl.key);
|
|
1092
1309
|
};
|
|
@@ -1104,6 +1321,8 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
1104
1321
|
setTip(null);
|
|
1105
1322
|
};
|
|
1106
1323
|
const closeLayout = () => setLayout(null);
|
|
1324
|
+
// #2691: whom a seat can be reserved for — the office's members (its teams' too).
|
|
1325
|
+
const layoutMembers = useMemo(() => (layout && custom ? membersOf(custom, view).map((n) => n.name) : []), [layout && layout.id, custom, view]);
|
|
1107
1326
|
const onDraft = (draft) => setLayout((cur) => (cur ? { ...cur, draft } : cur));
|
|
1108
1327
|
// Save: the session's list, this office's overrides replaced (none: back to the style's own), with the session's rev.
|
|
1109
1328
|
// A change made meanwhile (409) reads the list again: the session takes it (the view too), the draft is kept, and
|
|
@@ -1132,6 +1351,54 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
1132
1351
|
const pick = (id) => { if (layoutRef.current) return; store.saveChoice(id); setChoice(id); setTip(null); };
|
|
1133
1352
|
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]);
|
|
1134
1353
|
const tipText = tipDesk ? nodeText(tipDesk.node) : null;
|
|
1354
|
+
// #2661: the card's person (gone from the office, filtered out or the office changed: it closes).
|
|
1355
|
+
const cardOf = card ? people.find((d) => d.key === card && d.node.i) || null : null;
|
|
1356
|
+
cardDesk.current = cardOf ? { name: cardOf.node.name, box: cardOf.box, walks: zones && !overview } : null;
|
|
1357
|
+
useEffect(() => { if (card && !cardOf) setCard(null); }, [card, !!cardOf]);
|
|
1358
|
+
const closeCard = () => {
|
|
1359
|
+
setCard(null);
|
|
1360
|
+
const from = cardFrom.current; cardFrom.current = null;
|
|
1361
|
+
if (from) [...(ooRef.current?.querySelectorAll?.(".oo-desk") || [])].find((el) => el.getAttribute("data-key") === from)?.focus?.();
|
|
1362
|
+
};
|
|
1363
|
+
// Esc, or a press anywhere off the card and the scene (the scene's own click decides there), closes it.
|
|
1364
|
+
useEffect(() => {
|
|
1365
|
+
if (!card) return undefined;
|
|
1366
|
+
const doc = globalThis.document;
|
|
1367
|
+
const key = (e) => { if (e.key === "Escape" && !e.defaultPrevented) { e.preventDefault(); closeCard(); } };
|
|
1368
|
+
// A desk-list entry is the scene's too: its own click decides (the same person again closes, another switches) —
|
|
1369
|
+
// closing here first would let that click open it again (#2761 review).
|
|
1370
|
+
const press = (e) => {
|
|
1371
|
+
const el = e.target;
|
|
1372
|
+
if (cardEl.current?.contains?.(el) || el === canvasRef.current) return;
|
|
1373
|
+
if (typeof el?.closest === "function" && el.closest(".oo-desk") && ooRef.current?.contains?.(el)) return;
|
|
1374
|
+
closeCard();
|
|
1375
|
+
};
|
|
1376
|
+
doc?.addEventListener?.("keydown", key, true); doc?.addEventListener?.("pointerdown", press, true);
|
|
1377
|
+
return () => { doc?.removeEventListener?.("keydown", key, true); doc?.removeEventListener?.("pointerdown", press, true); };
|
|
1378
|
+
}, [card]);
|
|
1379
|
+
/** Find in office: zoomed in (to at least TWO px a unit) and centred on them. */
|
|
1380
|
+
const findCard = () => {
|
|
1381
|
+
const c = cardDesk.current;
|
|
1382
|
+
if (!c) return;
|
|
1383
|
+
const w = c.walks ? walkers.at(c.name, now()) : null, at = w ? { x: w.x, y: w.y - 26 } : { x: c.box.x + c.box.w / 2, y: c.box.y + c.box.h / 2 };
|
|
1384
|
+
const v = view0(), k = Math.min(OK_MAX, Math.max(v.k, FIND_K));
|
|
1385
|
+
touched.current = true; fitted.current = false;
|
|
1386
|
+
setView(centreOn({ ...v, k }, size, at.x, at.y));
|
|
1387
|
+
};
|
|
1388
|
+
/** The card's picture: the person's idle frame in the office's style (pack strip or built-in figure). */
|
|
1389
|
+
const drawAvatar = (cv) => {
|
|
1390
|
+
const s = scene.current, c = cardOf;
|
|
1391
|
+
const g = cv && cv.getContext ? cv.getContext("2d") : null;
|
|
1392
|
+
if (!g || !c || !s || !c.skin || !c.look) return;
|
|
1393
|
+
const r = deviceRatio(), W = 56;
|
|
1394
|
+
if (cv.width !== W * r) { cv.width = W * r; cv.height = W * r; }
|
|
1395
|
+
g.setTransform(1, 0, 0, 1, 0, 0); g.clearRect(0, 0, cv.width, cv.height); g.imageSmoothingEnabled = false;
|
|
1396
|
+
const look = { ...c.look, pose: "idle", dim: false, match: false, badge: null, notag: true, free: true, work: 0, sign: null };
|
|
1397
|
+
const at = { x: 0, y: 0 }, b = c.skin.charBox ? c.skin.charBox(look, at) : null, box = b || { x: -16, y: -48, w: 32, h: 48 };
|
|
1398
|
+
const k = Math.min((W - 8) / box.w, (W - 8) / box.h) * r;
|
|
1399
|
+
g.setTransform(k, 0, 0, k, (W * r) / 2 - (box.x + box.w / 2) * k, (W * r) / 2 - (box.y + box.h / 2) * k);
|
|
1400
|
+
try { c.skin.drawCharacter(g, { x: at.x - 44, y: at.y - 53, w: 88, h: 53, seat: at }, look, 0, s.env); } catch { /* a picture not loaded yet: blank */ }
|
|
1401
|
+
};
|
|
1135
1402
|
// The overview: the scene's rooms (or areas; All offices: the tiles) and a dot for each person (a call in its colour),
|
|
1136
1403
|
// the view's frame on it.
|
|
1137
1404
|
const miniRects = overview ? arranged.tiles.map((tt) => ({ key: tt.key, x: tt.x, y: tt.y, w: tt.w, h: tt.h }))
|
|
@@ -1148,7 +1415,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
1148
1415
|
<div class="oo-bar">
|
|
1149
1416
|
<label class="oo-pick"><span class="oo-pick-label">${to("office")}</span>${officeSelect("oo-office")}</label>
|
|
1150
1417
|
${auto ? html`<label class="oo-pick"><span class="oo-pick-label">${to("styleLabel")}</span>
|
|
1151
|
-
<select class="tb-select" value=${STYLES.includes(style) || packs.some((pk) => pk.id === style) ? style : skin.id}
|
|
1418
|
+
<select class="tb-select" value=${STYLES.includes(style) || packs.some((pk) => pk.id === style) ? style : skin.id} title=${to(canEdit ? "styleForAll" : "styleHere")} onChange=${(e) => pickStyle(e.target.value)}>
|
|
1152
1419
|
${styles.map((id) => html`<option key=${id} value=${id}>${to(`style_${id}`)}</option>`)}
|
|
1153
1420
|
${packs.map((pk) => html`<option key=${`pack:${pk.id}`} value=${pk.id}>${pk.name}</option>`)}
|
|
1154
1421
|
</select></label>` : null}
|
|
@@ -1164,6 +1431,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
1164
1431
|
${lobbyAll.length ? null : html`<span class="muted oo-note oo-lobby-none">${to("lobbyEveryone")}</span>`}` : null}
|
|
1165
1432
|
${custom && baseSkin.id !== custom.skin && !loading ? html`<span class="muted oo-note">${to("skinUnknown", styleName(custom.skin))}</span>` : null}
|
|
1166
1433
|
${placed.ignored ? html`<span class="muted oo-note">${to("layoutIgnored")}</span>` : null}
|
|
1434
|
+
${styleErr && auto ? html`<span class="note oo-note oo-style-err" role="status">${styleErr}</span>` : null}
|
|
1167
1435
|
${L.unseated ? html`<span class="muted oo-note">${to("unseated", L.unseated)}</span>` : null}
|
|
1168
1436
|
${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => { if (!overview) setEditing(true); }} disabled=${!!layout || overview}
|
|
1169
1437
|
title=${overview ? to("overviewNoEdit") : undefined}>${t("orge.edit")}</button>` : null}
|
|
@@ -1184,19 +1452,24 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, opening
|
|
|
1184
1452
|
: 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)}
|
|
1185
1453
|
onClick=${() => pick(tl.key)}>${text}</button>`;
|
|
1186
1454
|
}) : null}
|
|
1187
|
-
${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs}
|
|
1455
|
+
${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs} members=${layoutMembers}
|
|
1188
1456
|
stale=${layoutStale ? t("orgl.stale") : ""} onChange=${onDraft} onSave=${saveLayout} onClose=${closeLayout} />` : null}
|
|
1189
|
-
${tipText ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
|
|
1457
|
+
${tipText && tip !== card ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
|
|
1190
1458
|
<strong>${tipText.primary}</strong>${tipText.secondary ? html` <span class="muted mono">${tipText.secondary}</span>` : null}
|
|
1191
1459
|
<div>${tipText.sub}</div><div class="muted">${caption(officePose(tipDesk.node.state), tipDesk.node.state)}</div>
|
|
1192
1460
|
${tipDesk.office ? html`<div class="muted oo-tip-office">${tipDesk.office}</div>` : null}
|
|
1193
|
-
${tipDesk.node.i && onOpen && lastType.current === "touch" ? html`<div class="muted">${to("tapAgain")}</div>` : null}
|
|
1194
1461
|
</div>` : null}
|
|
1462
|
+
${cardOf ? html`<${OfficeCard} key=${cardOf.key}
|
|
1463
|
+
model=${cardModel({ node: cardOf.node, lv, levels: cardOf.skin && cardOf.skin.pack ? cardOf.skin.pack.levels : null, share: !onOpen,
|
|
1464
|
+
state: stateWords(cardOf.node), now: caption(officePose(cardOf.node.state), cardOf.node.state),
|
|
1465
|
+
where: [cardOf.office || (custom ? custom.name : to("allOffice")), cardOf.area].filter(Boolean).join(" · ") })}
|
|
1466
|
+
avatar=${drawAvatar} sheet=${size.w < SHEET_W} place=${(el) => { cardEl.current = el; placeCard(); }}
|
|
1467
|
+
onChat=${onOpen ? () => { const n = cardOf.node.name; setCard(null); open(n); } : null} onFind=${findCard} onClose=${closeCard} />` : null}
|
|
1195
1468
|
<ul class="oo-desks" aria-label=${to("desks")}>${people.map((d) => {
|
|
1196
1469
|
const nt = nodeText(d.node), cap = caption(officePose(d.node.state), d.node.state), where = d.office ? ` · ${d.office}` : "";
|
|
1197
1470
|
return html`<li key=${d.key}>${d.node.i
|
|
1198
|
-
? html`<button type="button" class="oo-desk" data-name=${d.node.name} data-key=${d.key} aria-label=${`${nt.full}${where} · ${cap}
|
|
1199
|
-
onFocus=${() => setTip(d.key)} onBlur=${() => setTip(null)} onClick=${() =>
|
|
1471
|
+
? html`<button type="button" class="oo-desk" data-name=${d.node.name} data-key=${d.key} aria-label=${`${nt.full}${where} · ${cap}.`} aria-haspopup="dialog"
|
|
1472
|
+
onFocus=${() => setTip(d.key)} onBlur=${() => setTip(null)} onClick=${() => { cardFrom.current = d.key; setCard((cur) => (cur === d.key ? null : d.key)); }}>${nt.primary}</button>`
|
|
1200
1473
|
: html`<span class="oo-desk" data-name=${d.node.name} data-key=${d.key}>${nt.full}</span>`}</li>`;
|
|
1201
1474
|
})}</ul>
|
|
1202
1475
|
<div class="oc-zoom oo-zoom" role="group" aria-label=${t("orgc.view")}>
|