@songsid/agend 2.3.2-beta.14 → 2.3.2-beta.16
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/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 +5 -0
- package/dist/backend/kiro-classic-archive.js +132 -7
- package/dist/backend/kiro-classic-archive.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +2 -0
- package/dist/backend/kiro-identity.js +4 -0
- package/dist/backend/kiro-identity.js.map +1 -1
- 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 +12 -3
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +11 -0
- package/dist/backend/kiro-v2-store.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/mcp-tools.js +5 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +17 -4
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +4 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/fleet-manager.d.ts +23 -1
- package/dist/fleet-manager.js +127 -9
- package/dist/fleet-manager.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/web-appearance/SKILL.canvas-worker-off.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +16 -9
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +16 -9
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/kiro-reconcile.d.ts +21 -0
- package/dist/kiro-reconcile.js +80 -0
- package/dist/kiro-reconcile.js.map +1 -0
- package/dist/kiro-tui-forensic.d.ts +7 -0
- package/dist/kiro-tui-forensic.js +17 -3
- package/dist/kiro-tui-forensic.js.map +1 -1
- package/dist/locale.js +20 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.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 +4 -3
- package/dist/office-share-service.js +6 -6
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +35 -8
- package/dist/office-share.js +50 -15
- 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 +6 -0
- package/dist/outbound-schemas.js +6 -0
- 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 +25 -1
- package/dist/quickstart-api.js +53 -6
- package/dist/quickstart-api.js.map +1 -1
- 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.js +4 -0
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +2 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/setup-form.js +17 -3
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.js +2 -0
- package/dist/setup-i18n.js.map +1 -1
- package/dist/task-board-waker.js +7 -2
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tool-permissions.js +1 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/types.d.ts +5 -0
- package/dist/ui/fleet-office-card.js +114 -0
- package/dist/ui/fleet-office-editor.js +27 -7
- package/dist/ui/fleet-office-layout.js +14 -6
- 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 +6 -0
- package/dist/ui/fleet-office-pack.js +62 -6
- package/dist/ui/fleet-office-skin.js +39 -3
- package/dist/ui/fleet-office-store.js +31 -3
- package/dist/ui/fleet-office-view.js +8 -0
- package/dist/ui/fleet-office-zones.js +10 -2
- package/dist/ui/fleet-office.js +208 -30
- 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 +1 -0
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-settings.js +7 -1
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +4 -0
- package/dist/ui/settings-wizard.js +15 -4
- package/dist/ui/shared/app.css +50 -0
- package/dist/ui/shared/setup-host-text.js +2 -0
- 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 +5 -0
- package/dist/web-api.js +116 -14
- 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 +7 -2
- package/dist/web-office-acts.js.map +1 -1
- package/dist/web-office-pack-tools.d.ts +10 -1
- package/dist/web-office-pack-tools.js +36 -5
- package/dist/web-office-pack-tools.js.map +1 -1
- package/dist/web-office-skins.js +6 -0
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +28 -4
- package/dist/web-office-store.js +49 -8
- 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 +1 -1
package/dist/ui/fleet-office.js
CHANGED
|
@@ -60,13 +60,15 @@ import { appStore } from "/assets/app-store.js";
|
|
|
60
60
|
import { t, lang, register } from "/assets/app-i18n.js";
|
|
61
61
|
import { identityOf } from "/assets/identity-color.js";
|
|
62
62
|
import { Icon } from "/assets/ui-icons.js";
|
|
63
|
-
import { nodeText } from "./fleet-org.js";
|
|
63
|
+
import { nodeText, stateWords } from "./fleet-org.js";
|
|
64
64
|
import { hits, needle } from "./fleet-canvas-layout.js";
|
|
65
65
|
import { AUTO_ID, LOBBY, autoLayout, backingScale, coatOf, createFlights, customLayout, deskAt, desksOf, filterOk, lampOf, layoutOk, lightOf, membersOf,
|
|
66
66
|
officePose, officeRooms, signOf, workOn } from "./fleet-office-model.js";
|
|
67
67
|
import { BUILT_IN_SKINS, CHAR_DEPTH, DEFAULT_PAL, bubbleBox, loadCozySkin, loadPackSkin, modeOf, skinFor, tagBox } from "./fleet-office-skin.js";
|
|
68
68
|
import { CALLING, activeFurniture, bubbleAt, hiddenBy, bubbleRank, NIGHT, WANDER, createMail, createWalkers, isNight, linesFor, npcKey, personAt, placeBubbles, thinBubbles, walkTargets, zonesLayout } from "./fleet-office-zones.js";
|
|
69
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";
|
|
70
72
|
import { POLL_MS } from "./fleet-traffic.js";
|
|
71
73
|
import { OfficeEditor, officeForSave } from "./fleet-office-editor.js";
|
|
72
74
|
import { LayoutEditor, handlesOf } from "./fleet-office-layout.js";
|
|
@@ -75,7 +77,7 @@ import { applyOverrides, fitNight, fitSeatFor, stripBox } from "./fleet-office-p
|
|
|
75
77
|
import { ALL_ID, LOBBY_TILE, OV, createCrossFlights, cropToRooms, fromTile, inTile, labelAt, lobbyNodes, overviewOk, tileAt, tileBox, tileLayout, visibleTiles } from "./fleet-office-overview.js";
|
|
76
78
|
import { centreOn, gestureState, gestures, miniPoint, toScreen, toWorld, wheelFactor, zoomKey, zoomTo } from "./fleet-canvas-view.js";
|
|
77
79
|
import { OK_MAX, OK_MIN, clampPan, desksK, edgeMatte, edgeStrips, fitScene, layerScale, miniBox, miniFrame, openingView, pinchOffice, savedOf, stepK, viewFromSaved,
|
|
78
|
-
zoomLabel, zoomOffice } from "./fleet-office-view.js";
|
|
80
|
+
wholeInView, zoomLabel, zoomOffice } from "./fleet-office-view.js";
|
|
79
81
|
|
|
80
82
|
register("orgo", {
|
|
81
83
|
en: {
|
|
@@ -89,6 +91,8 @@ register("orgo", {
|
|
|
89
91
|
b_paused: "Zzz…|Back later", b_stopped: "Off duty|Zzz…", b_needs: "Need you!|A question for you|Over here!",
|
|
90
92
|
b_crashed: "Ouch, crashed|Help!", b_stuck: "Hmm, stuck?|Still thinking…",
|
|
91
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.",
|
|
92
96
|
fit: "Fit the office", overview: "Overview: click to move there", fullscreen: "Fullscreen", exitFullscreen: "Exit fullscreen",
|
|
93
97
|
fill: "Fill", fillTitle: "Fill the panel (the scene's overflow is cropped; drag to see it)", zoomLevel: "Zoom {0}",
|
|
94
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.",
|
|
@@ -105,6 +109,8 @@ register("orgo", {
|
|
|
105
109
|
b_paused: "Zzz…|晚點回來", b_stopped: "下班了|Zzz…", b_needs: "需要你!|有個問題|在這裡!",
|
|
106
110
|
b_crashed: "糟糕,當掉了|救命!", b_stuck: "嗯,卡住了?|還在想…",
|
|
107
111
|
hideAsleep: "隱藏暫停與停止的成員", nightOn: "0–6 點就寢:閒著的人去睡", layoutIgnored: "它儲存的配置不適用這個風格:以風格本身的配置顯示。",
|
|
112
|
+
styleForAll: "整個 fleet 的風格,所有人共用:每台裝置與分享連結都一樣", styleHere: "這個瀏覽器裡整個 fleet 的風格",
|
|
113
|
+
styleFailed: "風格沒有替所有人儲存({0})。", styleBusy: "變更太頻繁,請一分鐘後再試。",
|
|
108
114
|
fit: "顯示整個辦公室", overview: "總覽:點一下移到那裡", fullscreen: "全螢幕", exitFullscreen: "結束全螢幕",
|
|
109
115
|
fill: "填滿", fillTitle: "填滿整個面板(超出的部分會被裁切,拖曳即可看到)", zoomLevel: "縮放 {0}",
|
|
110
116
|
allOffices: "所有辦公室", labelAll: "所有辦公室:每間辦公室是一格,以它自己的風格顯示。拖曳平移;滾輪、雙指、+ 和 − 縮放,0 顯示全部,F 切換全螢幕。雙擊一格即可開啟那間辦公室。",
|
|
@@ -148,6 +154,14 @@ function sampledMatte(img) {
|
|
|
148
154
|
return m;
|
|
149
155
|
}
|
|
150
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;
|
|
151
165
|
/** How often the picture can change when nothing is in the air (the poses loop at 4 fps; a bubble types faster). */
|
|
152
166
|
const FPS = 12;
|
|
153
167
|
const STYLES = BUILT_IN_SKINS;
|
|
@@ -167,6 +181,7 @@ function readPal(el) {
|
|
|
167
181
|
bg: v("--bg", D.bg), surface: v("--surface", D.surface), surface2: v("--surface-2", D.surface2), border: v("--border", D.border),
|
|
168
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),
|
|
169
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),
|
|
170
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) },
|
|
171
186
|
};
|
|
172
187
|
}
|
|
@@ -179,15 +194,19 @@ function teamOptions(view) {
|
|
|
179
194
|
|
|
180
195
|
// ── A scene: one office's layout, looks and skin, drawn by itself (the single view) or as a tile (All offices, #2554) ──
|
|
181
196
|
|
|
182
|
-
/**
|
|
183
|
-
|
|
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) {
|
|
184
202
|
return new Map(L.rooms.flatMap((r) => r.desks).map((d) => {
|
|
185
203
|
const n = d.node;
|
|
186
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 }];
|
|
187
205
|
const match = !!q && hits(n, q);
|
|
188
206
|
// At night (#2568) the idle are drawn asleep (their state stays idle: the tooltip and the list say so).
|
|
189
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),
|
|
190
|
-
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) }];
|
|
191
210
|
}));
|
|
192
211
|
}
|
|
193
212
|
/** A stored office's own layout on a zones style (its overrides); `ignored`: they do not fit this style. */
|
|
@@ -207,7 +226,9 @@ function officeScene(office, nodes, skin, zones, deskW, prevSeats, night = false
|
|
|
207
226
|
export function partOf(L, looks, skin, zones, walkers, flights) {
|
|
208
227
|
const all = L.rooms.flatMap((r) => r.desks);
|
|
209
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]));
|
|
210
|
-
|
|
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,
|
|
211
232
|
active: zones ? activeFurniture(skin.pack, shown, byName) : new Set(), lines: bubbleDefaults(), lang: lang() };
|
|
212
233
|
}
|
|
213
234
|
/**
|
|
@@ -413,12 +434,12 @@ export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false
|
|
|
413
434
|
for (const f of furniture) {
|
|
414
435
|
const on = people.filter((q) => live.hidden.get(q.look.name) === f.id);
|
|
415
436
|
if (!on.length) {
|
|
416
|
-
if (!tagsOff && f.label) { sk.drawTag(ctx, f.x, f.y,
|
|
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)); }
|
|
417
438
|
continue;
|
|
418
439
|
}
|
|
419
440
|
const order = new Map((f.activeFor || []).map((n, i) => [n, i]));
|
|
420
441
|
on.sort((x, y) => (order.get(x.look.name) ?? 0) - (order.get(y.look.name) ?? 0));
|
|
421
|
-
const row = tagRow(ctx, f.x, f.y, [...(f.label ? [
|
|
442
|
+
const row = tagRow(ctx, f.x, f.y, [...(f.label ? [npcLook(p, f)] : []), ...on.map((q) => q.look)]);
|
|
422
443
|
const top = f.sprite ? Math.min(f.y - 50, stripBox(f.activeSprite || f.sprite, f.x, f.y).y + 2) : f.y - 50;
|
|
423
444
|
for (const [i, r] of row.entries()) {
|
|
424
445
|
const q = f.label ? on[i - 1] : on[i];
|
|
@@ -432,13 +453,29 @@ export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false
|
|
|
432
453
|
}
|
|
433
454
|
p.heldTags = held;
|
|
434
455
|
} else {
|
|
435
|
-
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 }); }
|
|
436
457
|
blit(p.layers.front);
|
|
437
458
|
// Tags left out of the layer: the hovered desk's front again, with its tag.
|
|
438
459
|
const hd = tagsOff && hot ? p.desks.find((d) => d.key === hot) : null;
|
|
439
460
|
if (hd) sk.drawDeskFront(ctx, hd, p.looks.get(hd.key), env);
|
|
440
461
|
for (const it of up) sk.drawProp(ctx, it, ms, env);
|
|
441
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
|
+
}
|
|
442
479
|
// Speech bubbles: a short line for the state, now and then (typed out; whole at once when still), placed so no two
|
|
443
480
|
// overlap (placeBubbles; #2431).
|
|
444
481
|
const bubbles = [];
|
|
@@ -456,7 +493,15 @@ export function drawPart(ctx, p, a, ox, oy, ms, still, hot, env, tagsOff = false
|
|
|
456
493
|
const placed = placeBubbles(thinBubbles(bubbles, p.zones ? p.L.over : null), 22, tags);
|
|
457
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); };
|
|
458
495
|
if (defer) defer.push(say); else say();
|
|
459
|
-
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 };
|
|
460
505
|
}
|
|
461
506
|
/**
|
|
462
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
|
|
@@ -531,7 +576,12 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
531
576
|
const [editing, setEditing] = useState(false);
|
|
532
577
|
const [choice, setChoice] = useState(() => opening ?? store.choice());
|
|
533
578
|
const [filter, setFilter] = useState(() => store.filter());
|
|
534
|
-
|
|
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";
|
|
535
585
|
const [hideAsleep, setHideAsleep] = useState(() => store.hideAsleep());
|
|
536
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).
|
|
537
587
|
const [nightOn, setNightOn] = useState(() => store.nightOn());
|
|
@@ -553,6 +603,10 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
553
603
|
const [themeRev, setThemeRev] = useState(0);
|
|
554
604
|
const [pal, setPal] = useState(DEFAULT_PAL); // the tokens in force (read once the stage is there)
|
|
555
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);
|
|
556
610
|
const stageRef = useRef(null), canvasRef = useRef(null), tipRef = useRef(null), lastType = useRef("mouse");
|
|
557
611
|
// Fullscreen: null, "native" (the Fullscreen API) or "immersive" (the CSS cover); per visit, never kept.
|
|
558
612
|
const ooRef = useRef(null), [fs, setFs] = useState(null), fsRef = useRef(null), fsAsked = useRef(false), fsTarget = useRef(null);
|
|
@@ -572,15 +626,34 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
572
626
|
// The editor's save and its 409 re-read are stamped when sent (`ask`), never when answered; `adopt` returns the
|
|
573
627
|
// snapshot in force, so a re-read the stream overtook leaves the editor's draft on the newer one.
|
|
574
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
|
|
575
630
|
const ask = () => ++asked.current;
|
|
576
631
|
const adopt = (d, askedAt) => {
|
|
577
632
|
const cur = shown.current;
|
|
578
633
|
if (!d) return cur;
|
|
579
634
|
if (cur && d.rev < cur.rev && askedAt <= cur.arrived) return cur;
|
|
580
635
|
shown.current = { rev: d.rev, offices: d.offices, arrived: ++asked.current };
|
|
581
|
-
setStored(d.offices); setStoredRev(d.rev);
|
|
636
|
+
setStored(d.offices); setStoredRev(d.rev); setHostStyle(d.autoSkin || null);
|
|
582
637
|
return shown.current;
|
|
583
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
|
+
};
|
|
584
657
|
useEffect(() => {
|
|
585
658
|
if (!lease) return undefined;
|
|
586
659
|
const read = async () => { const at = ask(); const d = await store.offices(); if (lease.current()) adopt(d, at); };
|
|
@@ -593,6 +666,27 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
593
666
|
// A read a newer one or the stream overtook is dropped (it is older: review — an opening read answered after the one
|
|
594
667
|
// the stream's return began brought a cleared act back); read again when the stream comes back, and — with no live
|
|
595
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]);
|
|
596
690
|
const [acts, setActs] = useState([]);
|
|
597
691
|
const actSeen = useRef(new Set());
|
|
598
692
|
useEffect(() => {
|
|
@@ -739,10 +833,10 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
739
833
|
});
|
|
740
834
|
}, [overview, stored, view, packs, packSkins, cozyRev, hideAsleep, lobbyOn, lobbyAll, style, night]);
|
|
741
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]);
|
|
742
|
-
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]);
|
|
743
837
|
// The scene the view pans and zooms: the office's, or the overview of every tile.
|
|
744
838
|
const L = useMemo(() => (overview ? { mode: "overview", rooms: [], width: arranged.width, height: arranged.height } : single), [overview, single, arranged]);
|
|
745
|
-
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]);
|
|
746
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,
|
|
747
841
|
// so another office (or style) starts with none (one shown meanwhile is not carried over to a different room).
|
|
748
842
|
// #2578: the acts of the people in this stored office (by name); none elsewhere. On a desks style (review: accepted
|
|
@@ -818,6 +912,23 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
818
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;
|
|
819
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)`);
|
|
820
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
|
+
};
|
|
821
932
|
/**
|
|
822
933
|
* Put the view on everything that shows it beside the canvas: the overview's frame (the part of the scene in view;
|
|
823
934
|
* the whole view on its data-k/x/y), the zoom's percentage, the overlay, the tooltip.
|
|
@@ -832,6 +943,9 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
832
943
|
if (mb && mb.style) { // the overview: inside the stage, scaled to it
|
|
833
944
|
const b = miniBox(size, L.width, L.height);
|
|
834
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");
|
|
835
949
|
}
|
|
836
950
|
const pc = pctRef.current;
|
|
837
951
|
if (pc) { const lb = zoomLabel(v.k); if (pc.textContent !== lb) pc.textContent = lb; pc.setAttribute("aria-label", to("zoomLevel", lb)); }
|
|
@@ -1013,18 +1127,19 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1013
1127
|
const base = { ctx, ratio, BW, BH, matte, env, dirty: true };
|
|
1014
1128
|
if (overview) {
|
|
1015
1129
|
// Each tile a scene of its own, where the overview puts it (arranged.tiles, in the same order).
|
|
1016
|
-
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) }));
|
|
1017
1131
|
scene.current = { ...base, overview: true, parts, lay: arranged };
|
|
1018
1132
|
} else {
|
|
1019
|
-
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) } };
|
|
1020
1134
|
}
|
|
1021
1135
|
if (still) draw(0); else loop.current?.wake();
|
|
1022
|
-
}, [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]);
|
|
1023
1137
|
|
|
1024
1138
|
/** One frame at `ms` (0: still); whether anything is moving (in the air, or someone walking). */
|
|
1025
1139
|
function draw(ms) {
|
|
1026
1140
|
const s = scene.current;
|
|
1027
1141
|
if (!s) return false;
|
|
1142
|
+
placeCard();
|
|
1028
1143
|
const { ctx, BW, BH, env } = s;
|
|
1029
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.
|
|
1030
1145
|
const v = view0(), a = v.k * s.ratio, ox = Math.round(v.x * s.ratio), oy = Math.round(v.y * s.ratio);
|
|
@@ -1103,11 +1218,16 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1103
1218
|
// Everyone drawn: { key, node, box (the desk, in the view's units), dot (where the overview marks them), office (All
|
|
1104
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
|
|
1105
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);
|
|
1106
1224
|
const people = overview
|
|
1107
|
-
? tiles.flatMap((tl, i) => { const tt = arranged.tiles[i];
|
|
1225
|
+
? tiles.flatMap((tl, i) => { const tt = arranged.tiles[i], lk = tileLooks[i];
|
|
1108
1226
|
return tt ? desksOf(tl.L).map((d) => ({ key: tl.prefix + d.key, node: d.node, box: tileBox(tt, d), office: tl.name,
|
|
1109
|
-
dot: fromTile(tt, tl.zones ? d.seat : { x: d.x + d.w / 2, y: d.y + d.h / 2 })
|
|
1110
|
-
|
|
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) }));
|
|
1111
1231
|
// Preact owns the markup, not the view: put it back on the overview, the overlay and the tooltip after every render.
|
|
1112
1232
|
const tipDesk = tip ? people.find((d) => d.key === tip) || null : null;
|
|
1113
1233
|
tipDeskRef.current = tipDesk ? tipDesk.box : null;
|
|
@@ -1170,16 +1290,20 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1170
1290
|
if (d) show(d.box);
|
|
1171
1291
|
};
|
|
1172
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.
|
|
1173
1295
|
const onClick = (e) => {
|
|
1174
1296
|
const d = deskOf(e);
|
|
1175
|
-
if (!d || !d.node.i) { setTip(null); return; }
|
|
1176
|
-
if (lastType.current === "touch"
|
|
1177
|
-
|
|
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));
|
|
1178
1301
|
};
|
|
1179
1302
|
// All offices: a double-click on a tile (off its people) opens that office; the lobby is no office.
|
|
1180
1303
|
const onDblClick = (e) => {
|
|
1181
|
-
const s = scene.current, w = pointOf(e);
|
|
1182
|
-
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;
|
|
1183
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;
|
|
1184
1308
|
if (tl && !tl.lobby) pick(tl.key);
|
|
1185
1309
|
};
|
|
@@ -1227,6 +1351,54 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1227
1351
|
const pick = (id) => { if (layoutRef.current) return; store.saveChoice(id); setChoice(id); setTip(null); };
|
|
1228
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]);
|
|
1229
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
|
+
};
|
|
1230
1402
|
// The overview: the scene's rooms (or areas; All offices: the tiles) and a dot for each person (a call in its colour),
|
|
1231
1403
|
// the view's frame on it.
|
|
1232
1404
|
const miniRects = overview ? arranged.tiles.map((tt) => ({ key: tt.key, x: tt.x, y: tt.y, w: tt.w, h: tt.h }))
|
|
@@ -1243,7 +1415,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1243
1415
|
<div class="oo-bar">
|
|
1244
1416
|
<label class="oo-pick"><span class="oo-pick-label">${to("office")}</span>${officeSelect("oo-office")}</label>
|
|
1245
1417
|
${auto ? html`<label class="oo-pick"><span class="oo-pick-label">${to("styleLabel")}</span>
|
|
1246
|
-
<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)}>
|
|
1247
1419
|
${styles.map((id) => html`<option key=${id} value=${id}>${to(`style_${id}`)}</option>`)}
|
|
1248
1420
|
${packs.map((pk) => html`<option key=${`pack:${pk.id}`} value=${pk.id}>${pk.name}</option>`)}
|
|
1249
1421
|
</select></label>` : null}
|
|
@@ -1259,6 +1431,7 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1259
1431
|
${lobbyAll.length ? null : html`<span class="muted oo-note oo-lobby-none">${to("lobbyEveryone")}</span>`}` : null}
|
|
1260
1432
|
${custom && baseSkin.id !== custom.skin && !loading ? html`<span class="muted oo-note">${to("skinUnknown", styleName(custom.skin))}</span>` : null}
|
|
1261
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}
|
|
1262
1435
|
${L.unseated ? html`<span class="muted oo-note">${to("unseated", L.unseated)}</span>` : null}
|
|
1263
1436
|
${canEdit && storedRev !== null ? html`<button type="button" class="btn btn-sm oo-edit" onClick=${() => { if (!overview) setEditing(true); }} disabled=${!!layout || overview}
|
|
1264
1437
|
title=${overview ? to("overviewNoEdit") : undefined}>${t("orge.edit")}</button>` : null}
|
|
@@ -1281,17 +1454,22 @@ export function Office({ view, traffic, q = "", lease, onOpen, assetUrl, assetIm
|
|
|
1281
1454
|
}) : null}
|
|
1282
1455
|
${layout ? html`<${LayoutEditor} key=${layout.id} base=${layout.base} start=${layout.start} place=${placeOverlay} dock=${!fs} members=${layoutMembers}
|
|
1283
1456
|
stale=${layoutStale ? t("orgl.stale") : ""} onChange=${onDraft} onSave=${saveLayout} onClose=${closeLayout} />` : null}
|
|
1284
|
-
${tipText ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
|
|
1457
|
+
${tipText && tip !== card ? html`<div ref=${tipRef} class="oo-tip" role="tooltip">
|
|
1285
1458
|
<strong>${tipText.primary}</strong>${tipText.secondary ? html` <span class="muted mono">${tipText.secondary}</span>` : null}
|
|
1286
1459
|
<div>${tipText.sub}</div><div class="muted">${caption(officePose(tipDesk.node.state), tipDesk.node.state)}</div>
|
|
1287
1460
|
${tipDesk.office ? html`<div class="muted oo-tip-office">${tipDesk.office}</div>` : null}
|
|
1288
|
-
${tipDesk.node.i && onOpen && lastType.current === "touch" ? html`<div class="muted">${to("tapAgain")}</div>` : null}
|
|
1289
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}
|
|
1290
1468
|
<ul class="oo-desks" aria-label=${to("desks")}>${people.map((d) => {
|
|
1291
1469
|
const nt = nodeText(d.node), cap = caption(officePose(d.node.state), d.node.state), where = d.office ? ` · ${d.office}` : "";
|
|
1292
1470
|
return html`<li key=${d.key}>${d.node.i
|
|
1293
|
-
? html`<button type="button" class="oo-desk" data-name=${d.node.name} data-key=${d.key} aria-label=${`${nt.full}${where} · ${cap}
|
|
1294
|
-
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>`
|
|
1295
1473
|
: html`<span class="oo-desk" data-name=${d.node.name} data-key=${d.key}>${nt.full}</span>`}</li>`;
|
|
1296
1474
|
})}</ul>
|
|
1297
1475
|
<div class="oc-zoom oo-zoom" role="group" aria-label=${t("orgc.view")}>
|
package/dist/ui/fleet-org.js
CHANGED
|
@@ -60,6 +60,8 @@ export function orgState(i, exec, awaiting, needs) {
|
|
|
60
60
|
* What a node's card says (fleet-canvas.js; the Office view's desk too, fleet-office.js): its names, its state and
|
|
61
61
|
* backend (`sub`), and all of it in one line (`full`, the card's title and label).
|
|
62
62
|
*/
|
|
63
|
+
/** #2661: a node's state in words (as its card and tooltip say it). */
|
|
64
|
+
export const stateWords = (n) => to(n.state);
|
|
63
65
|
export function nodeText(n) {
|
|
64
66
|
const it = n.i;
|
|
65
67
|
const names = it ? namesOf(it) : { primary: n.name, secondary: "" };
|
|
@@ -175,7 +175,7 @@ export function CardDialog({ id, tick, projects, writable = true, place = null,
|
|
|
175
175
|
</div>
|
|
176
176
|
|
|
177
177
|
<div class="tc-actions">
|
|
178
|
-
<${StatusPill} status=${card.status} />
|
|
178
|
+
<${StatusPill} status=${card.status} card=${card} />
|
|
179
179
|
${writable ? html`
|
|
180
180
|
${card.status === "review" ? html`
|
|
181
181
|
<button type="button" class="btn btn-primary btn-sm" disabled=${busy} onClick=${() => moved(() => moveCard(card, "done"))}><${Icon} name="check" size=${14} />${isMain ? t("taskcard.confirmMain") : t("tasks.confirm")}</button>
|
|
@@ -260,7 +260,7 @@ export function CardDialog({ id, tick, projects, writable = true, place = null,
|
|
|
260
260
|
|
|
261
261
|
${isMain ? html`<${Field} title=${t("taskcard.subs", children.length)}>
|
|
262
262
|
${children.length ? html`<ul class="tc-children">${children.map((c) => html`<li key=${c.id}><button type="button" onClick=${() => onOpen(c.id)}>
|
|
263
|
-
<${StatusPill} status=${c.status} /><span class="tb-code">${c.id}</span><span class="tc-child-title">${c.title}</span><span class="tb-who">${c.assignee || ""}</span></button></li>`)}</ul>` : null}
|
|
263
|
+
<${StatusPill} status=${c.status} card=${c} /><span class="tb-code">${c.id}</span><span class="tc-child-title">${c.title}</span><span class="tb-who">${c.assignee || ""}</span></button></li>`)}</ul>` : null}
|
|
264
264
|
${finished || !writable ? null : html`<${QuickAdd} placeholder=${t("taskcard.quickAdd")} onAdd=${(title) => run(() => boardApi.create({ parent: card.id, title }))} />`}
|
|
265
265
|
</${Field}>` : null}
|
|
266
266
|
|
|
@@ -10,7 +10,8 @@ import { toast } from "/assets/ui-toast.js";
|
|
|
10
10
|
|
|
11
11
|
register("tasks", {
|
|
12
12
|
en: {
|
|
13
|
-
open: "To do", claimed: "In progress", blocked: "Blocked", review: "
|
|
13
|
+
open: "To do", claimed: "In progress", blocked: "Blocked", review: "To confirm", done: "Done", cancelled: "Cancelled",
|
|
14
|
+
reviewYou: "Waiting for you", reviewBy: "Waiting for {0}", reviewByTip: "{0} confirms it, not you",
|
|
14
15
|
main: "Main task", sub: "Sub task", urgent: "Urgent", high: "High", normal: "Normal", low: "Low",
|
|
15
16
|
sizeS: "Small", sizeM: "Medium", sizeL: "Large", sizeNone: "Not sized",
|
|
16
17
|
projects: "Projects", noProject: "No project", noProjectDesc: "Tasks the fleet opened outside any project",
|
|
@@ -53,7 +54,6 @@ register("tasks", {
|
|
|
53
54
|
nowDoing: "Working on", nowBlocked: "Blocked", nowNext: "Next", ownerTip: "Main task owner: {0}", moreCards: "+{0}",
|
|
54
55
|
wrapTag: "To fill in", wrapLine: "Not wrapped up yet: {0}", gapUnchecked: "{0} checklist items unchecked", gapUncheckedOne: "1 checklist item unchecked",
|
|
55
56
|
gapNoResult: "no result", confirmGapTitle: "Confirm {0}?", confirmAnyway: "Confirm anyway",
|
|
56
|
-
askedFirst: "{0} handed it out and has been asked; you can answer it here too",
|
|
57
57
|
confirmGapLabel: "{0}. You can send it back to have it filled in — or confirm anyway, with a note if you like",
|
|
58
58
|
"err.too_long": "{0} is too long: {1} characters, at most {2}", "err.state_lines": "State is at most {0} lines (it has {1})",
|
|
59
59
|
"err.acceptance_empty": "An acceptance item cannot be empty", "err.acceptance_max": "At most {0} acceptance items",
|
|
@@ -76,7 +76,7 @@ register("tasks", {
|
|
|
76
76
|
"note.deps_unmet": "Note: {0} not done yet",
|
|
77
77
|
// #2246 step 3: who works on what (fleet-task-work.js)
|
|
78
78
|
instances: "Instances", instancesTip: "The fleet's instances and what each works on", instancesHead: "{0} instance(s) · {1} working",
|
|
79
|
-
workSentBack: "Sent back", workDoing: "In progress", workBlocked: "Blocked", workReview: "
|
|
79
|
+
workSentBack: "Sent back", workDoing: "In progress", workBlocked: "Blocked", workReview: "To confirm", workMain: "Owns",
|
|
80
80
|
workReady: "Can start", workWaiting: "Waiting on", workSubs: "sub tasks {0}/{1}", workClaimable: "nobody's: can be taken",
|
|
81
81
|
workWaitsFor: "waits for {0}", workNone: "Nothing on the board", workNewComments: "Comments it has not read yet",
|
|
82
82
|
workOutside: "Not in the fleet", workFailed: "Could not read who works on what: {0}", workJoin: ", ",
|
|
@@ -84,6 +84,7 @@ register("tasks", {
|
|
|
84
84
|
},
|
|
85
85
|
"zh-TW": {
|
|
86
86
|
open: "待辦", claimed: "進行中", blocked: "卡住", review: "待確認", done: "完成", cancelled: "取消",
|
|
87
|
+
reviewYou: "等你確認", reviewBy: "等 {0} 確認", reviewByTip: "由 {0} 確認,不是你",
|
|
87
88
|
main: "主任務", sub: "子任務", urgent: "緊急", high: "高", normal: "一般", low: "低",
|
|
88
89
|
sizeS: "小", sizeM: "中", sizeL: "大", sizeNone: "未定",
|
|
89
90
|
projects: "專案", noProject: "未分類", noProjectDesc: "fleet 在專案以外開的 task",
|
|
@@ -126,7 +127,6 @@ register("tasks", {
|
|
|
126
127
|
nowDoing: "執行中", nowBlocked: "卡住", nowNext: "下一步", ownerTip: "主任務負責人:{0}", moreCards: "+{0}",
|
|
127
128
|
wrapTag: "待補齊", wrapLine: "負責人還沒補齊:{0}", gapUnchecked: "還有 {0} 項沒勾", gapUncheckedOne: "還有 1 項沒勾",
|
|
128
129
|
gapNoResult: "沒有產出總結", confirmGapTitle: "確認 {0}?", confirmAnyway: "還是確認",
|
|
129
|
-
askedFirst: "已先通知派工者 {0};你也可以直接在這裡回覆",
|
|
130
130
|
confirmGapLabel: "{0}。可以先退回請負責人補齊;還是要確認的話,可以留一句確認意見",
|
|
131
131
|
"err.too_long": "{0}太長:{1} 字,上限 {2} 字", "err.state_lines": "目前狀態最多 {0} 行(現在 {1} 行)",
|
|
132
132
|
"err.acceptance_empty": "驗收條件不能是空的", "err.acceptance_max": "驗收條件最多 {0} 項",
|
|
@@ -190,6 +190,17 @@ export const percentDone = (units) => (units.total ? Math.round((units.done / un
|
|
|
190
190
|
/** The key a card's project is filed under: its id, or NO_PROJECT. */
|
|
191
191
|
export const projectKey = (card) => (card.projectId === null || card.projectId === undefined ? NO_PROJECT : card.projectId);
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* #2080: whether a card waits for the person — Waiting for you, and its count: the host's word (`waitsForPerson`, the
|
|
195
|
+
* same rule as Needs you: a card the person handed out, in review needing their confirmation or blocked on a question,
|
|
196
|
+
* or one whose dispatcher was deleted); a card read without it, by the person's own hand-out alone. An agent's card in
|
|
197
|
+
* review is its confirmer's (`confirmer`), never the person's.
|
|
198
|
+
*/
|
|
199
|
+
export function waitsForYou(card) {
|
|
200
|
+
if (typeof card.waitsForPerson === "boolean") return card.waitsForPerson;
|
|
201
|
+
return !!card.dispatcher && card.dispatcher.kind === "person" && ((card.status === "review" && card.needsReview) || card.status === "blocked");
|
|
202
|
+
}
|
|
203
|
+
|
|
193
204
|
/**
|
|
194
205
|
* One project's numbers for its card on Home and its header, from the cards at hand (`key`: a project id or
|
|
195
206
|
* NO_PROJECT). The board's read carries the server's (over every card, finished ones too); this is the fallback.
|
|
@@ -201,7 +212,7 @@ export function projectStats(key, cards) {
|
|
|
201
212
|
if (projectKey(c) !== key) continue;
|
|
202
213
|
if (!lastActivity || c.updatedAt > lastActivity) lastActivity = c.updatedAt;
|
|
203
214
|
if (c.status === "cancelled") continue;
|
|
204
|
-
if (c.status === "review") waiting++;
|
|
215
|
+
if (c.status === "review" && waitsForYou(c)) waiting++;
|
|
205
216
|
if (c.status === "blocked") blocked++;
|
|
206
217
|
if (c.kind !== "main") continue;
|
|
207
218
|
mains++;
|
|
@@ -454,8 +465,20 @@ export function ProgressBar({ progress, wide = false }) {
|
|
|
454
465
|
${barSegments(progress).filter((s) => s.k === "done").map((s) => html`<rect key=${s.k} class=${`tb-seg st-${s.k}`} x=${s.x} width=${s.w} height="6" />`)}</svg>`;
|
|
455
466
|
}
|
|
456
467
|
|
|
457
|
-
|
|
458
|
-
|
|
468
|
+
/** #2080: whom a card in review waits for, in words: you, the agent that confirms it, or (neither known) "To confirm". */
|
|
469
|
+
export function reviewLabel(card) {
|
|
470
|
+
if (waitsForYou(card)) return t("tasks.reviewYou");
|
|
471
|
+
return card.confirmer ? t("tasks.reviewBy", card.confirmer) : t("tasks.review");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* A card's status. #2080: with the card, one in review says whom it waits for — the person ("Waiting for you") or the
|
|
476
|
+
* agent that confirms it ("Waiting for atlas").
|
|
477
|
+
*/
|
|
478
|
+
export function StatusPill({ status, card = null }) {
|
|
479
|
+
const by = card && status === "review" && !waitsForYou(card) && card.confirmer ? card.confirmer : null;
|
|
480
|
+
const label = !card || status !== "review" ? t(`tasks.${status}`) : reviewLabel(card);
|
|
481
|
+
return html`<span class=${`tb-pill st-${status}${by ? " by-agent" : ""}`} title=${by ? t("tasks.reviewByTip", by) : null}>${label}</span>`;
|
|
459
482
|
}
|
|
460
483
|
|
|
461
484
|
/**
|
|
@@ -8,7 +8,7 @@ import { Dialog } from "/assets/ui-dialog.js";
|
|
|
8
8
|
import { Icon } from "/assets/ui-icons.js";
|
|
9
9
|
import { Skeleton } from "/assets/ui-states.js";
|
|
10
10
|
import { GroupMark, InstanceAvatar, dotLabel, dotState, groupKind, groupLabel, groupOf, namesOf, namesStore } from "/assets/instance-nav.js";
|
|
11
|
-
import { Who, boardApi, errorText } from "./fleet-task-common.js";
|
|
11
|
+
import { Who, boardApi, errorText, reviewLabel } from "./fleet-task-common.js";
|
|
12
12
|
|
|
13
13
|
const LABEL = { back: "tasks.workSentBack", doing: "tasks.workDoing", blocked: "tasks.workBlocked", review: "tasks.workReview", main: "tasks.workMain", ready: "tasks.workReady", waiting: "tasks.workWaiting" };
|
|
14
14
|
|
|
@@ -33,7 +33,7 @@ export function WorkLines({ lines, onOpen }) {
|
|
|
33
33
|
if (!lines.length) return html`<p class="muted tw-none">${t("tasks.workNone")}</p>`;
|
|
34
34
|
return html`<ul class="tw-lines">${lines.map((l) => html`<li key=${`${l.kind}:${l.card.id}`}>
|
|
35
35
|
<button type="button" class="tw-line" title=${`${l.card.id} ${l.card.title}`} onClick=${() => onOpen(l.card.id)}>
|
|
36
|
-
<span class=${`tw-kind k-${l.kind}`}>${t(LABEL[l.kind])}</span><span class="tb-code">${l.card.id}</span><span class="tw-title">${l.card.title}</span>
|
|
36
|
+
<span class=${`tw-kind k-${l.kind}`}>${l.kind === "review" ? reviewLabel(l.card) : t(LABEL[l.kind])}</span><span class="tb-code">${l.card.id}</span><span class="tw-title">${l.card.title}</span>
|
|
37
37
|
${l.note ? html`<span class="muted tw-note">${l.note}</span>` : null}
|
|
38
38
|
${l.unread ? html`<span class="tw-unread" title=${t("tasks.workNewComments")} aria-label=${t("tasks.workNewComments")}><${Icon} name="chat" size=${13} /></span>` : null}
|
|
39
39
|
</button></li>`)}</ul>`;
|