@songsid/agend 2.3.0-beta.24 → 2.3.0-beta.26
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/adapter-world.js.map +1 -1
- package/dist/agend-home.d.ts +5 -0
- package/dist/agend-home.js +7 -0
- package/dist/agend-home.js.map +1 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +8 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-file-read.js +1 -1
- package/dist/agent-file-read.js.map +1 -1
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/apply-job.js.map +1 -1
- package/dist/atomic-write.js.map +1 -1
- package/dist/auth-api.d.ts +5 -0
- package/dist/auth-api.js +21 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.d.ts +4 -0
- package/dist/authority/authority-state.js +17 -8
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +14 -6
- package/dist/authority/web-authority.js +19 -5
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.d.ts +2 -2
- package/dist/backend/antigravity.js +2 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-package.js.map +1 -1
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +1 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-kas-store.js.map +1 -1
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro-v3-identity.js.map +1 -1
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.js.map +1 -1
- package/dist/backend/mock.js.map +1 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/opencode-permission.js.map +1 -1
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/session-signals.js.map +1 -1
- package/dist/backend-availability.js.map +1 -1
- package/dist/backend-command.js.map +1 -1
- package/dist/backend-outage.js.map +1 -1
- package/dist/binary-probe.js.map +1 -1
- package/dist/bug-report.js.map +1 -1
- package/dist/bundled-skills.js.map +1 -1
- package/dist/cache-service.js +1 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +29 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +6 -0
- package/dist/channel/adapters/telegram.js +54 -14
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +10 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.js +2 -2
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-queue.js.map +1 -1
- package/dist/channel/sticker-download.js.map +1 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.js +3 -2
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli-entry.js.map +1 -1
- package/dist/cli-flags.js.map +1 -1
- package/dist/cli.js +15 -9
- package/dist/cli.js.map +1 -1
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.js.map +1 -1
- package/dist/completion-install.js.map +1 -1
- package/dist/completion.js.map +1 -1
- package/dist/config-validator.js +20 -6
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +11 -3
- package/dist/config.js.map +1 -1
- package/dist/connection-secrets.js.map +1 -1
- package/dist/context-guardian.js.map +1 -1
- package/dist/coredump-filter.js.map +1 -1
- package/dist/cpu-profile.js.map +1 -1
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +73 -3
- package/dist/daemon.js.map +1 -1
- package/dist/daily-summary.js.map +1 -1
- package/dist/darwin-memory.js.map +1 -1
- package/dist/dashboard-live.d.ts +8 -0
- package/dist/dashboard-live.js +35 -0
- package/dist/dashboard-live.js.map +1 -0
- package/dist/dashboard-permissions.d.ts +69 -0
- package/dist/dashboard-permissions.js +138 -0
- package/dist/dashboard-permissions.js.map +1 -0
- package/dist/decision-list.js.map +1 -1
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/delivery-reconciliation.test.js.map +1 -1
- package/dist/diagnostic-redaction.js.map +1 -1
- package/dist/doctor.js.map +1 -1
- package/dist/e2e-isolation.js.map +1 -1
- package/dist/event-log.js.map +1 -1
- package/dist/export-import.js +3 -2
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-label.js.map +1 -1
- package/dist/fleet-lock.js +3 -2
- package/dist/fleet-lock.js.map +1 -1
- package/dist/fleet-manager.d.ts +77 -6
- package/dist/fleet-manager.js +488 -147
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.js +3 -2
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/fleet-startup.d.ts +14 -0
- package/dist/fleet-startup.js +30 -0
- package/dist/fleet-startup.js.map +1 -0
- package/dist/fleet-traffic.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +0 -20
- package/dist/forged-envelope-scan.js +3 -2
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/full-restart.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 +36 -12
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +45 -14
- package/dist/host-memory.js.map +1 -1
- package/dist/instance-config-impact.js +6 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.js +1 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +2 -0
- package/dist/instructions.js +6 -2
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.js.map +1 -1
- package/dist/kiro-db-reader.js.map +1 -1
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-log-gate.js.map +1 -1
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +22 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.js +3 -2
- package/dist/logger.js.map +1 -1
- package/dist/login-controller.js.map +1 -1
- package/dist/login-manager.js.map +1 -1
- package/dist/login-shell.js.map +1 -1
- package/dist/memory-pressure.js.map +1 -1
- package/dist/muse-usage-relay.js.map +1 -1
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.js.map +1 -1
- package/dist/network-family.js.map +1 -1
- package/dist/operator-control-paths.js +3 -4
- package/dist/operator-control-paths.js.map +1 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +68 -68
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.js.map +1 -1
- package/dist/pane-context-cache.js.map +1 -1
- package/dist/paths.js.map +1 -1
- package/dist/private-dir.js.map +1 -1
- package/dist/probe-worker-pool.js.map +1 -1
- package/dist/profile-control.js.map +1 -1
- package/dist/prompt-file-ref.js.map +1 -1
- package/dist/provider-probe.js.map +1 -1
- package/dist/provider-secret-registry.js.map +1 -1
- package/dist/public-link-auto-open.d.ts +32 -0
- package/dist/public-link-auto-open.js +86 -0
- package/dist/public-link-auto-open.js.map +1 -0
- package/dist/public-link-owner.d.ts +10 -0
- package/dist/public-link-owner.js +36 -0
- package/dist/public-link-owner.js.map +1 -0
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.js.map +1 -1
- package/dist/public-web-gateway.js +4 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +35 -1
- package/dist/public-web-link.js +142 -43
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +5 -0
- package/dist/quickstart.js +25 -16
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/resource-report.js.map +1 -1
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.js.map +1 -1
- package/dist/rollout-index.js.map +1 -1
- package/dist/runtime-cpu-profile.js.map +1 -1
- package/dist/scheduler/db.js.map +1 -1
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/task-board-text.js +16 -11
- package/dist/scheduler/task-board-text.js.map +1 -1
- package/dist/scheduler/task-board.d.ts +50 -10
- package/dist/scheduler/task-board.js +138 -44
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/scheduler/types.d.ts +1 -1
- package/dist/sd-notify.js.map +1 -1
- package/dist/search/search-index.js.map +1 -1
- package/dist/secret-file.js.map +1 -1
- package/dist/secret-store.js.map +1 -1
- package/dist/service-installer.d.ts +7 -1
- package/dist/service-installer.js +13 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +33 -4
- package/dist/service-plan.js +177 -8
- package/dist/service-plan.js.map +1 -1
- package/dist/session-oom.d.ts +21 -0
- package/dist/session-oom.js +139 -0
- package/dist/session-oom.js.map +1 -0
- package/dist/settings-api.js +6 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-baseline.js.map +1 -1
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +71 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +4 -0
- package/dist/settings-confirmation.js +2 -0
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-control.js.map +1 -1
- package/dist/settings-effect.js +6 -1
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-instance-patch.js +13 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/settings-transaction.js.map +1 -1
- package/dist/setup-auth.js.map +1 -1
- package/dist/setup-host.d.ts +5 -1
- package/dist/setup-host.js +8 -1
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +5 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/spawn-gate.d.ts +4 -0
- package/dist/spawn-gate.js +15 -5
- package/dist/spawn-gate.js.map +1 -1
- package/dist/startup-memory-deadline.d.ts +23 -0
- package/dist/startup-memory-deadline.js +55 -0
- package/dist/startup-memory-deadline.js.map +1 -0
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/storm-window.js.map +1 -1
- package/dist/systemd-peer-bus.js.map +1 -1
- package/dist/target-queue-worker.js.map +1 -1
- package/dist/task-board-summary.d.ts +4 -4
- package/dist/task-board-summary.js +4 -4
- package/dist/task-board-summary.js.map +1 -1
- package/dist/task-board-tool.js +3 -2
- package/dist/task-board-tool.js.map +1 -1
- package/dist/task-board-waker.js +8 -4
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.d.ts +1 -0
- package/dist/tmux-manager.js +2 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.js +1 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-archiver.js.map +1 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/topic-link.js.map +1 -1
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.js.map +1 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/lease.js +3 -2
- package/dist/tunnel/lease.js.map +1 -1
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/tunnel/public-fetch.js.map +1 -1
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/types.d.ts +26 -2
- package/dist/ui/chat-render.js +1 -15
- package/dist/ui/chat-store.js +171 -61
- package/dist/ui/chat-strings.js +7 -2
- package/dist/ui/chat-thread.js +48 -1
- package/dist/ui/fleet-canvas-layout.js +3 -0
- package/dist/ui/fleet-canvas.js +23 -20
- package/dist/ui/fleet-office-editor.js +149 -0
- package/dist/ui/fleet-office-model.js +358 -0
- package/dist/ui/fleet-office-pack.js +199 -0
- package/dist/ui/fleet-office-skin.js +474 -0
- package/dist/ui/fleet-office-store.js +49 -0
- package/dist/ui/fleet-office-zones.js +177 -0
- package/dist/ui/fleet-office.js +434 -0
- package/dist/ui/fleet-org.js +18 -5
- package/dist/ui/guest-refused.html +41 -0
- package/dist/ui/image-lightbox.js +9 -3
- package/dist/ui/panel-chat.js +14 -13
- package/dist/ui/panel-settings.js +30 -12
- package/dist/ui/settings-dialogs.js +15 -6
- package/dist/ui/settings-model.js +40 -3
- package/dist/ui/settings-strings.js +38 -0
- package/dist/ui/settings-web-authority.js +7 -2
- package/dist/ui/shared/agend-auth.js +2 -0
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-prefs.js +2 -0
- package/dist/ui/shared/app-shell.js +12 -0
- package/dist/ui/shared/app-store.js +1 -0
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +132 -3
- package/dist/ui/shared/guest-refused.js +35 -0
- package/dist/ui/shared/header-tools.js +2 -2
- package/dist/ui/shared/instance-nav.js +25 -8
- package/dist/ui/shared/ui-dialog.js +2 -2
- package/dist/ui/shared/usage-dialog.js +152 -6
- package/dist/ui/shared/view-strings.js +20 -0
- package/dist/ui/shared/web-authorize.js +3 -1
- package/dist/update-check.js.map +1 -1
- package/dist/update-dispatch.js.map +1 -1
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.js.map +1 -1
- package/dist/update-marker.js.map +1 -1
- package/dist/update-preflight.js.map +1 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/providers.d.ts +8 -0
- package/dist/usage/providers.js +6 -2
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/usage/usage-api.js +2 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.js +1 -1
- package/dist/usage-rollup.js.map +1 -1
- package/dist/user-systemd.js.map +1 -1
- package/dist/view-api.js +1 -1
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-ambient.js.map +1 -1
- package/dist/web-api.d.ts +15 -2
- package/dist/web-api.js +174 -25
- package/dist/web-api.js.map +1 -1
- package/dist/web-app-icon.js.map +1 -1
- package/dist/web-appearance-policy.js.map +1 -1
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-auth.js +5 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-backend-login.js.map +1 -1
- package/dist/web-card.js.map +1 -1
- package/dist/web-channel-echo.js.map +1 -1
- package/dist/web-chat-history.d.ts +41 -11
- package/dist/web-chat-history.js +93 -22
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.js.map +1 -1
- package/dist/web-effect-bundle.d.ts +2 -2
- package/dist/web-effect-library.js.map +1 -1
- package/dist/web-effect-playback.js.map +1 -1
- package/dist/web-effect-profiles.d.ts +16 -16
- package/dist/web-effect-profiles.js +3 -2
- package/dist/web-effect-profiles.js.map +1 -1
- package/dist/web-effect-service.js.map +1 -1
- package/dist/web-effect-tools.js.map +1 -1
- package/dist/web-effect-trials.js.map +1 -1
- package/dist/web-effect-upload.js.map +1 -1
- package/dist/web-effect.d.ts +7 -7
- package/dist/web-effect.js.map +1 -1
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-guest-access.js +1 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-guest-rooms.js.map +1 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-icons.js.map +1 -1
- package/dist/web-identity-color.js.map +1 -1
- package/dist/web-images.js.map +1 -1
- package/dist/web-libavoid.js.map +1 -1
- package/dist/web-login.js.map +1 -1
- package/dist/web-mentions.js.map +1 -1
- package/dist/web-mod-agent.js.map +1 -1
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-office-store.d.ts +104 -0
- package/dist/web-office-store.js +236 -0
- package/dist/web-office-store.js.map +1 -0
- package/dist/web-owner-css-store.js.map +1 -1
- package/dist/web-owner-css.js.map +1 -1
- package/dist/web-png.js.map +1 -1
- package/dist/web-prefs.js +2 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-profile.js.map +1 -1
- package/dist/web-reactions.d.ts +2 -1
- package/dist/web-request-context.d.ts +5 -0
- package/dist/web-request-context.js.map +1 -1
- package/dist/web-returning-device.js +1 -1
- package/dist/web-returning-device.js.map +1 -1
- package/dist/web-schedules.js.map +1 -1
- package/dist/web-send-dedupe.d.ts +36 -0
- package/dist/web-send-dedupe.js +69 -0
- package/dist/web-send-dedupe.js.map +1 -0
- package/dist/web-session-authorization.js.map +1 -1
- package/dist/web-session.js +1 -1
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +1 -0
- package/dist/web-settings-patch.js +6 -4
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/web-terminal-http.js.map +1 -1
- package/dist/web-terminal.js.map +1 -1
- package/dist/web-theme-store.js.map +1 -1
- package/dist/web-upload.d.ts +5 -1
- package/dist/web-upload.js +6 -4
- package/dist/web-upload.js.map +1 -1
- package/dist/web-widget.js.map +1 -1
- package/dist/webhook-emitter.js.map +1 -1
- package/dist/windows-shortcuts.js +1 -1
- package/dist/windows-shortcuts.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/dist/ws-server.js.map +1 -1
- package/dist/yaml-shared-nodes.js.map +1 -1
- package/launcher/agend-install.cjs +281 -6
- package/launcher/private-home.cjs +43 -0
- package/package.json +3 -2
- package/templates/windows-shortcuts.ps1 +1 -1
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
// The Office view's art (fleet-office.js draws with it on a <canvas>). A skin draws a layout that fleet-office-model.js
|
|
2
|
+
// (desks) or fleet-office-zones.js (zones) made. Two skins are built in and draw everything procedurally — little
|
|
3
|
+
// pixel people, desks, lamps — so no art ships: "pixel" (the desks layout: a room per team, a desk per instance) and
|
|
4
|
+
// "studio" (the zones layout: work desks, a lounge, an alert corner and a nap corner; people walk to the area for their
|
|
5
|
+
// state). Every drawing call goes through the OfficeSkin interface below, so a skin pack (pictures from an artist,
|
|
6
|
+
// format v1 in fleet-office-pack.js) can replace any part; a part a pack does not supply falls back to the built-in
|
|
7
|
+
// drawing. No pack can be added yet (no upload): spriteSkin() and checkSkinPack() are the seam a later step plugs into.
|
|
8
|
+
//
|
|
9
|
+
// ── The interface ──
|
|
10
|
+
//
|
|
11
|
+
// @typedef {"working"|"idle"|"paused"|"needs"|"walk"} OfficePose (fleet-office-model.js POSES; "away" draws no one)
|
|
12
|
+
// @typedef {{ pal: OfficePalette, mode: "dark"|"light", labels: { lobby, closed, areas: { [area]: string } } }} OfficeEnv
|
|
13
|
+
// @typedef {{ name: string, label: string, state: string, pose: OfficePose|"away", light: "warn"|"danger"|null,
|
|
14
|
+
// lamp: boolean, sign: "closed"|null, coat: string, work: number, dim: boolean, match: boolean, free: boolean }} OfficeLook
|
|
15
|
+
// (one occupant; `free`: standing in a zone, not behind a desk cell)
|
|
16
|
+
// @typedef {object} OfficeSkin
|
|
17
|
+
// @property {string} id
|
|
18
|
+
// @property {object} [pack] a checked pack (fleet-office-pack.js); with `pack.stateZones` the zones layout is used
|
|
19
|
+
// @property {(ctx, L, env) => void} drawFloor desks: under everything — the stage and the corridors (L.edges)
|
|
20
|
+
// @property {(ctx, room, env) => void} drawRoom desks: a room's floor, wall and name (room: autoLayout's)
|
|
21
|
+
// @property {(ctx, desk, look, env) => void} drawDeskBack desks: behind the character — chair, desk top, screen, lamp
|
|
22
|
+
// @property {(ctx, desk, look, env) => void} drawDeskFront desks: in front — the desk's front, its sign, folders, name tag
|
|
23
|
+
// @property {(ctx, L, env) => void} [drawBackground] zones: the whole 1280×720 background, areas and their names
|
|
24
|
+
// @property {(ctx, item, active, ms, env) => void} [drawFurniture] zones: one furniture item (active: activeWhen holds)
|
|
25
|
+
// @property {(ctx, x, y, look, env) => void} [drawTag] zones: a name tag under feet at (x, y)
|
|
26
|
+
// @property {(ctx, desk, look, ms, env) => void} drawCharacter the occupant in its pose at time `ms` (0: still); in
|
|
27
|
+
// zones `desk` is the character's box at its current position (its feet at desk.seat)
|
|
28
|
+
// @property {(ctx, prop, ms, env) => void} drawProp a traffic item in the air: { kind, hue, x, y, t }
|
|
29
|
+
// @property {(ctx, x, y, text, env, lift?) => void} drawBubble a speech bubble whose tail points at (x, y), its box `lift` px higher
|
|
30
|
+
// The desks layout's back and front layers change only when the fleet does: the renderer keeps them as pictures and
|
|
31
|
+
// draws the characters, props and bubbles each frame. In zones the background is kept; furniture, people (walking
|
|
32
|
+
// along the pack's waypoints), tags, props and bubbles are drawn each frame, furniture by depth around the people.
|
|
33
|
+
import { frameAt, nameHash } from "./fleet-office-model.js";
|
|
34
|
+
import { CHAR_DEPTH, LOGICAL } from "./fleet-office-pack.js";
|
|
35
|
+
|
|
36
|
+
export { nameHash };
|
|
37
|
+
|
|
38
|
+
// ── The palette: the dashboard's tokens, read by the renderer; furniture keeps its own wood and metal ──
|
|
39
|
+
|
|
40
|
+
/** @typedef {{ bg, surface, surface2, border, text, text2, text3, ok, warn, danger, focus, fx: { task, send, report, broadcast } }} OfficePalette */
|
|
41
|
+
export const DEFAULT_PAL = {
|
|
42
|
+
bg: "#212121", surface: "#2f2f2f", surface2: "#3a3a3a", border: "#3d3d3d", text: "#ececec", text2: "#b4b4b4", text3: "#a3a3a3",
|
|
43
|
+
ok: "#4cc38a", warn: "#e3b341", danger: "#ff8a80", focus: "#7ab7ff", fx: { task: "#b794f6", send: "#2dd4bf", report: "#f472b6", broadcast: "#fb923c" },
|
|
44
|
+
};
|
|
45
|
+
/** Light or dark, from the page's own background token (whatever chose it: the person, the system, a theme). */
|
|
46
|
+
export function modeOf(pal) {
|
|
47
|
+
const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(String(pal && pal.bg).trim());
|
|
48
|
+
if (!m) return "dark";
|
|
49
|
+
const [r, g, b] = m.slice(1).map((x) => parseInt(x, 16));
|
|
50
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b > 140 ? "light" : "dark";
|
|
51
|
+
}
|
|
52
|
+
const ROOM = {
|
|
53
|
+
dark: { floorA: "#2b313b", floorB: "#272c35", wall: "#363d49", wallLine: "#454e5d", lobbyA: "#2f3330", lobbyB: "#2b2f2c", corridor: "#2a2a2a" },
|
|
54
|
+
light: { floorA: "#efe7d8", floorB: "#e8dfcd", wall: "#ddd0b8", wallLine: "#cdbd9f", lobbyA: "#e6eadf", lobbyB: "#dfe4d7", corridor: "#ececec" },
|
|
55
|
+
};
|
|
56
|
+
const WOOD = { top: "#b07a45", edge: "#8a5a30", front: "#7a4f2a", leg: "#5c3b1f" };
|
|
57
|
+
const METAL = { frame: "#2b2f36", screenOff: "#3d4752", screenOn: "#9fe3ff", code: "#2d6f8f", pole: "#6b6f76", shade: "#d9b13b", chair: "#454b57", chairTop: "#545b69" };
|
|
58
|
+
const SKIN_TONES = ["#f2c9a0", "#e5b48a", "#c98e62", "#9a6440"];
|
|
59
|
+
const HAIR = ["#3b2a20", "#6b4423", "#c48a3a", "#1f1f24", "#7a4f8f", "#b9b9c2", "#a8452f"];
|
|
60
|
+
|
|
61
|
+
const P = 3; // one art pixel of a character, in CSS px
|
|
62
|
+
const CX = 44, TOP = 14; // the character's centre and head top in its desk cell
|
|
63
|
+
const px = (ctx, x, y, w, h, c) => { ctx.fillStyle = c; ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h)); };
|
|
64
|
+
/** An art-pixel block of the character at desk `d`: column c, row r (0, 0 is the head's top-left), w×h art px. */
|
|
65
|
+
const art = (ctx, d, c, r, w, h, color, dy = 0) => px(ctx, d.x + CX - 5 * P + c * P, d.y + TOP + dy + r * P, w * P, h * P, color);
|
|
66
|
+
|
|
67
|
+
/** Text shortened to `max` px with an ellipsis (canvas measures; a grapheme cluster is not split). */
|
|
68
|
+
export function fitText(ctx, s, max) {
|
|
69
|
+
const str = String(s);
|
|
70
|
+
if (!ctx.measureText || ctx.measureText(str).width <= max) return str;
|
|
71
|
+
const chars = Array.from(str);
|
|
72
|
+
while (chars.length && ctx.measureText(`${chars.join("")}…`).width > max) chars.pop();
|
|
73
|
+
return `${chars.join("")}…`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── The built-in pixel skin ──
|
|
77
|
+
|
|
78
|
+
function drawFloor(ctx, L, env) {
|
|
79
|
+
const r = ROOM[env.mode];
|
|
80
|
+
ctx.save();
|
|
81
|
+
ctx.strokeStyle = r.corridor;
|
|
82
|
+
ctx.lineWidth = 10;
|
|
83
|
+
ctx.lineCap = "round";
|
|
84
|
+
const at = new Map(L.waypoints.map((w) => [w.id, w]));
|
|
85
|
+
ctx.beginPath();
|
|
86
|
+
for (const [a, b] of L.edges) { const p = at.get(a), q = at.get(b); if (p && q) { ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); } }
|
|
87
|
+
ctx.stroke();
|
|
88
|
+
ctx.restore();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function drawRoom(ctx, room, env) {
|
|
92
|
+
const r = ROOM[env.mode], { pal } = env, T = 16;
|
|
93
|
+
const [a, b] = room.lobby ? [r.lobbyA, r.lobbyB] : [r.floorA, r.floorB];
|
|
94
|
+
px(ctx, room.x, room.y, room.w, room.h, a);
|
|
95
|
+
for (let y = room.y + 26; y < room.y + room.h; y += T) for (let x = room.x + (((y - room.y) / T) % 2) * T; x < room.x + room.w; x += 2 * T) {
|
|
96
|
+
px(ctx, x, y, Math.min(T, room.x + room.w - x), Math.min(T, room.y + room.h - y), b);
|
|
97
|
+
}
|
|
98
|
+
px(ctx, room.x, room.y, room.w, 26, r.wall);
|
|
99
|
+
px(ctx, room.x, room.y + 24, room.w, 2, r.wallLine);
|
|
100
|
+
if (room.lobby) { // a plant by the lobby's wall
|
|
101
|
+
const x = room.x + room.w - 22, y = room.y + 4;
|
|
102
|
+
px(ctx, x + 3, y + 12, 10, 8, "#b5653a"); px(ctx, x, y + 4, 6, 8, "#3f8f4f"); px(ctx, x + 6, y, 5, 12, "#4fa65d"); px(ctx, x + 10, y + 5, 6, 7, "#3f8f4f");
|
|
103
|
+
}
|
|
104
|
+
ctx.save();
|
|
105
|
+
ctx.strokeStyle = pal.border;
|
|
106
|
+
ctx.lineWidth = 2;
|
|
107
|
+
ctx.strokeRect(room.x + 1, room.y + 1, room.w - 2, room.h - 2);
|
|
108
|
+
ctx.fillStyle = pal.text;
|
|
109
|
+
ctx.font = "600 12px system-ui, sans-serif";
|
|
110
|
+
ctx.textBaseline = "middle";
|
|
111
|
+
const name = room.lobby ? env.labels.lobby : room.name;
|
|
112
|
+
ctx.fillText(fitText(ctx, name, room.w - (room.lobby ? 48 : 52)), room.x + 10, room.y + 13);
|
|
113
|
+
if (!room.lobby) {
|
|
114
|
+
ctx.fillStyle = pal.text3;
|
|
115
|
+
ctx.font = "11px system-ui, sans-serif";
|
|
116
|
+
ctx.textAlign = "right";
|
|
117
|
+
ctx.fillText(String(room.desks.filter((d) => d.node).length), room.x + room.w - 10, room.y + 13);
|
|
118
|
+
}
|
|
119
|
+
ctx.restore();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function drawDeskBack(ctx, d, look) {
|
|
123
|
+
ctx.save();
|
|
124
|
+
if (look.dim) ctx.globalAlpha = 0.35;
|
|
125
|
+
// The chair (behind the character), the desk top, the screen, the lamp — and its light while it works.
|
|
126
|
+
px(ctx, d.x + 30, d.y + 26, 30, 26, METAL.chair); px(ctx, d.x + 30, d.y + 26, 30, 4, METAL.chairTop);
|
|
127
|
+
px(ctx, d.x + 8, d.y + 48, 80, 6, WOOD.top); px(ctx, d.x + 8, d.y + 53, 80, 1, WOOD.edge);
|
|
128
|
+
if (look.lamp) { // its light, from the shade down onto the desk
|
|
129
|
+
ctx.fillStyle = "rgba(255, 214, 102, 0.5)";
|
|
130
|
+
ctx.beginPath(); ctx.moveTo(d.x + 11, d.y + 31); ctx.lineTo(d.x + 19, d.y + 31); ctx.lineTo(d.x + 30, d.y + 49); ctx.lineTo(d.x + 8, d.y + 49); ctx.closePath(); ctx.fill();
|
|
131
|
+
}
|
|
132
|
+
px(ctx, d.x + 62, d.y + 28, 24, 18, METAL.frame); px(ctx, d.x + 64, d.y + 30, 20, 13, look.lamp ? METAL.screenOn : METAL.screenOff);
|
|
133
|
+
px(ctx, d.x + 72, d.y + 46, 4, 2, METAL.frame);
|
|
134
|
+
px(ctx, d.x + 11, d.y + 46, 8, 2, METAL.pole); px(ctx, d.x + 14, d.y + 31, 2, 15, METAL.pole);
|
|
135
|
+
px(ctx, d.x + 9, d.y + 26, 12, 5, look.lamp ? "#ffd86b" : METAL.shade);
|
|
136
|
+
ctx.restore();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function drawCharacter(ctx, d, look, ms, env) {
|
|
140
|
+
if (look.pose === "away") return;
|
|
141
|
+
const h = nameHash(look.name), skin = SKIN_TONES[h % SKIN_TONES.length], hair = HAIR[(h >>> 3) % HAIR.length];
|
|
142
|
+
const coat = look.coat, shirt = "#f4f1ea", eye = "#1d1d22";
|
|
143
|
+
const f = frameAt(ms, FRAMES[look.pose] || 1, FPS);
|
|
144
|
+
ctx.save();
|
|
145
|
+
if (look.dim) ctx.globalAlpha = 0.35;
|
|
146
|
+
const head = (dy, closed) => {
|
|
147
|
+
art(ctx, d, 2, 0, 6, 2, hair, dy); art(ctx, d, 2, 2, 6, 4, skin, dy); art(ctx, d, 2, 2, 1, 2, hair, dy); art(ctx, d, 7, 2, 1, 2, hair, dy);
|
|
148
|
+
if (closed) { art(ctx, d, 3, 4, 1, 0.34, eye, dy); art(ctx, d, 6, 4, 1, 0.34, eye, dy); }
|
|
149
|
+
else { art(ctx, d, 3, 3, 1, 1, eye, dy); art(ctx, d, 6, 3, 1, 1, eye, dy); }
|
|
150
|
+
art(ctx, d, 2.6, 4.6, 0.7, 0.5, "rgba(255, 120, 120, 0.45)", dy); art(ctx, d, 6.7, 4.6, 0.7, 0.5, "rgba(255, 120, 120, 0.45)", dy);
|
|
151
|
+
};
|
|
152
|
+
const body = (dy) => { art(ctx, d, 1, 6, 8, 5, coat, dy); art(ctx, d, 4, 6, 2, 2, shirt, dy); };
|
|
153
|
+
if (look.pose === "working") {
|
|
154
|
+
head(0, false); body(0);
|
|
155
|
+
// Typing: the hands take turns on the keyboard; the screen's lines change.
|
|
156
|
+
art(ctx, d, 0, 8, 1, 3, coat); art(ctx, d, 9, 8, 1, 3, coat);
|
|
157
|
+
art(ctx, d, 1, f % 2 ? 10.4 : 11, 2, 0.8, skin); art(ctx, d, 7, f % 2 ? 11 : 10.4, 2, 0.8, skin);
|
|
158
|
+
if (!look.free) { // the keyboard and the screen's lines (in zones the desk is furniture)
|
|
159
|
+
px(ctx, d.x + 30, d.y + 47, 28, 3, "#d8d8dc");
|
|
160
|
+
for (let i = 0; i < 3; i++) px(ctx, d.x + 66, d.y + 32 + i * 4, 4 + ((f + i * 3) % 4) * 3, 2, METAL.code);
|
|
161
|
+
}
|
|
162
|
+
} else if (look.pose === "idle") {
|
|
163
|
+
head(0, false); body(0);
|
|
164
|
+
// Tea: the cup comes up to sip on the second half of the loop; steam rises while it is down.
|
|
165
|
+
const up = f >= 2;
|
|
166
|
+
art(ctx, d, 0, 8, 1, 3, coat);
|
|
167
|
+
if (up) { art(ctx, d, 7, 6, 2, 2, coat); art(ctx, d, 6, 4.7, 2, 1.6, "#ffffff"); art(ctx, d, 6, 4.7, 2, 0.4, "#8a5a30"); }
|
|
168
|
+
else {
|
|
169
|
+
art(ctx, d, 9, 8, 1, 2, coat); art(ctx, d, 8.6, 9.6, 2, 1.8, "#ffffff"); art(ctx, d, 8.6, 9.6, 2, 0.4, "#8a5a30");
|
|
170
|
+
art(ctx, d, 9 + (f % 2) * 0.7, 7.6 - f * 0.5, 0.5, 0.8, "rgba(220, 220, 230, 0.7)");
|
|
171
|
+
}
|
|
172
|
+
} else if (look.pose === "paused") {
|
|
173
|
+
// Asleep: head down on the desk, arms folded under it; Zzz rising.
|
|
174
|
+
body(3); art(ctx, d, 1, 9.2, 8, 1.5, coat); head(4, true);
|
|
175
|
+
ctx.fillStyle = env.pal.text2;
|
|
176
|
+
ctx.font = "700 11px system-ui, sans-serif";
|
|
177
|
+
const zs = ms > 0 ? f + 1 : 3;
|
|
178
|
+
for (let i = 0; i < zs; i++) ctx.fillText(i ? "z" : "Z", d.x + CX + 12 + i * 7, d.y + TOP + 6 - i * 6);
|
|
179
|
+
} else if (look.pose === "needs") {
|
|
180
|
+
head(0, false); body(0);
|
|
181
|
+
// A hand up, waving; the call light over the head blinks (warn, or danger when it crashed).
|
|
182
|
+
art(ctx, d, 0, 8, 1, 3, coat); art(ctx, d, 9, 2 + f, 1, 6 - f, coat); art(ctx, d, 9 - f * 0.4, 1 + f, 1.4, 1.2, skin);
|
|
183
|
+
const on = ms <= 0 || f === 0, c = look.light === "danger" ? env.pal.danger : env.pal.warn;
|
|
184
|
+
ctx.globalAlpha = (look.dim ? 0.35 : 1) * (on ? 1 : 0.35);
|
|
185
|
+
if (on) { ctx.fillStyle = c; ctx.globalAlpha *= 0.25; ctx.beginPath(); ctx.arc(d.x + CX, d.y + 6, 9, 0, Math.PI * 2); ctx.fill(); ctx.globalAlpha = look.dim ? 0.35 : 1; }
|
|
186
|
+
px(ctx, d.x + CX - 4, d.y + 2, 8, 7, c); px(ctx, d.x + CX - 3, d.y + 9, 6, 2, METAL.pole);
|
|
187
|
+
} else {
|
|
188
|
+
// Walking: a bob, arms and legs swinging in turn.
|
|
189
|
+
const up = f ? -1 : 0;
|
|
190
|
+
head(up, false); body(up); art(ctx, d, 0, 7 + f * 0.5, 1, 3, coat, up); art(ctx, d, 9, 7.5 - f * 0.5, 1, 3, coat, up);
|
|
191
|
+
}
|
|
192
|
+
if (look.free && look.pose !== "paused") { // standing: legs (behind a desk cell they are hidden)
|
|
193
|
+
const step = look.pose === "walk" ? f : 0, legs = "#3a3f4b";
|
|
194
|
+
art(ctx, d, 2.5, 11, 2, step ? 1.6 : 2, legs); art(ctx, d, 5.5, 11, 2, step ? 2 : 1.6, legs);
|
|
195
|
+
}
|
|
196
|
+
ctx.restore();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function drawDeskFront(ctx, d, look, env) {
|
|
200
|
+
const { pal } = env;
|
|
201
|
+
ctx.save();
|
|
202
|
+
if (look.dim) ctx.globalAlpha = 0.35;
|
|
203
|
+
if (!look.frontless) { // (a skin pack's desk front drew it already)
|
|
204
|
+
px(ctx, d.x + 10, d.y + 54, 76, 16, WOOD.front); px(ctx, d.x + 10, d.y + 54, 76, 2, WOOD.edge);
|
|
205
|
+
px(ctx, d.x + 12, d.y + 70, 4, 6, WOOD.leg); px(ctx, d.x + 80, d.y + 70, 4, 6, WOOD.leg);
|
|
206
|
+
}
|
|
207
|
+
// The work it was given (a delegation in progress): a folder each, up to three.
|
|
208
|
+
for (let i = 0; i < Math.min(3, look.work); i++) { px(ctx, d.x + 20 - i, d.y + 45 - i * 2, 12, 3, pal.fx.task); px(ctx, d.x + 20 - i, d.y + 44 - i * 2, 5, 1, pal.fx.task); }
|
|
209
|
+
if (look.sign === "closed") {
|
|
210
|
+
px(ctx, d.x + 30, d.y + 57, 36, 11, pal.surface2);
|
|
211
|
+
ctx.strokeStyle = pal.text3; ctx.lineWidth = 1; ctx.strokeRect(d.x + 30.5, d.y + 57.5, 35, 10);
|
|
212
|
+
ctx.fillStyle = pal.text2; ctx.font = "600 8px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
213
|
+
ctx.fillText(fitText(ctx, env.labels.closed, 32), d.x + 48, d.y + 63);
|
|
214
|
+
}
|
|
215
|
+
// The name tag.
|
|
216
|
+
ctx.fillStyle = pal.surface;
|
|
217
|
+
ctx.fillRect(d.x + 6, d.y + 78, d.w - 12, 16);
|
|
218
|
+
ctx.strokeStyle = look.match ? pal.focus : pal.border;
|
|
219
|
+
ctx.lineWidth = look.match ? 2 : 1;
|
|
220
|
+
ctx.strokeRect(d.x + 6.5, d.y + 78.5, d.w - 13, 15);
|
|
221
|
+
ctx.fillStyle = pal.text; ctx.font = "600 11px system-ui, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
|
222
|
+
ctx.fillText(fitText(ctx, look.label, d.w - 18), d.x + d.w / 2, d.y + 86.5);
|
|
223
|
+
ctx.restore();
|
|
224
|
+
if (look.match) { ctx.save(); ctx.strokeStyle = pal.focus; ctx.lineWidth = 2; ctx.strokeRect(d.x + 2, d.y + 2, d.w - 4, d.h - 6); ctx.restore(); }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const Q = 2; // a prop's art pixel
|
|
228
|
+
function drawProp(ctx, it, ms, env) {
|
|
229
|
+
const c = env.pal.fx[it.hue] || env.pal.fx.send, x0 = it.x - 8, y0 = it.y - 8 - Math.sin(Math.PI * it.t) * 10;
|
|
230
|
+
const b = (c2, r, w, h, col) => px(ctx, x0 + c2 * Q, y0 + r * Q, w * Q, h * Q, col);
|
|
231
|
+
ctx.save();
|
|
232
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.18)";
|
|
233
|
+
ctx.beginPath(); ctx.ellipse(it.x, it.y + 4, 7, 2.5, 0, 0, Math.PI * 2); ctx.fill();
|
|
234
|
+
if (it.hue === "task") { b(0, 0, 4, 2, c); b(0, 1, 8, 6, c); b(0, 2, 8, 1, "rgba(255, 255, 255, 0.35)"); }
|
|
235
|
+
else if (it.hue === "report") { b(1, 0, 6, 8, "#fbfbfb"); b(1, 0, 6, 1, c); b(2, 4, 1, 1, c); b(3, 5, 1, 1, c); b(4, 4, 1, 1, c); b(5, 3, 1, 1, c); }
|
|
236
|
+
else if (it.hue === "broadcast") { b(0, 3, 2, 2, c); b(2, 2, 2, 4, c); b(4, 1, 2, 6, c); if (frameAt(ms, 2, 6)) { b(7, 1, 1, 2, c); b(7, 5, 1, 2, c); } else b(7, 3, 1, 2, c); }
|
|
237
|
+
else { b(0, 1, 8, 6, c); for (const [cc, r] of [[0, 1], [1, 2], [2, 3], [3, 4], [4, 4], [5, 3], [6, 2], [7, 1]]) b(cc, r, 1, 1, "#ffffff"); }
|
|
238
|
+
ctx.restore();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Frames per pose and the loop's speed (the built-in strips). */
|
|
242
|
+
export const FRAMES = { working: 4, idle: 4, paused: 3, needs: 2, walk: 2 };
|
|
243
|
+
export const FPS = 4;
|
|
244
|
+
|
|
245
|
+
const BUBBLE_FONT = "600 11px system-ui, sans-serif";
|
|
246
|
+
/**
|
|
247
|
+
* The box a speech bubble with its tail at (x, y) takes for `text` (its tail excluded): what drawBubble draws, and what
|
|
248
|
+
* the Office places so no two overlap (#2431: two neighbours' bubbles covered each other on a phone).
|
|
249
|
+
*/
|
|
250
|
+
export function bubbleBox(ctx, x, y, text, lift = 0) {
|
|
251
|
+
ctx.save();
|
|
252
|
+
ctx.font = BUBBLE_FONT;
|
|
253
|
+
const t = fitText(ctx, text, 150), w = Math.ceil((ctx.measureText ? ctx.measureText(t).width : t.length * 6) + 14), h = 19;
|
|
254
|
+
ctx.restore();
|
|
255
|
+
const bx = Math.round(x - 10), by = Math.round(y - h - 6 - lift);
|
|
256
|
+
return { x: bx, y: by, w, h, text: t };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** A speech bubble, its tail at (x, y): short words on light paper in either theme; `lift` raises the box (its tail longer). */
|
|
260
|
+
function drawBubble(ctx, x, y, text, env, lift = 0) {
|
|
261
|
+
if (!text) return;
|
|
262
|
+
const { x: bx, y: by, w, h, text: t } = bubbleBox(ctx, x, y, text, lift);
|
|
263
|
+
ctx.save();
|
|
264
|
+
ctx.font = BUBBLE_FONT;
|
|
265
|
+
ctx.fillStyle = "#fffdf6"; ctx.strokeStyle = env.mode === "light" ? "rgba(0, 0, 0, 0.35)" : "rgba(0, 0, 0, 0.6)"; ctx.lineWidth = 1;
|
|
266
|
+
ctx.beginPath(); ctx.moveTo(bx + 4, by); ctx.lineTo(bx + w - 4, by); ctx.lineTo(bx + w, by + 4); ctx.lineTo(bx + w, by + h - 4); ctx.lineTo(bx + w - 4, by + h);
|
|
267
|
+
ctx.lineTo(x + 6, by + h); ctx.lineTo(x, y); ctx.lineTo(x + 1, by + h); ctx.lineTo(bx + 4, by + h); ctx.lineTo(bx, by + h - 4); ctx.lineTo(bx, by + 4); ctx.closePath();
|
|
268
|
+
ctx.fill(); ctx.stroke();
|
|
269
|
+
ctx.fillStyle = "#1d1d22"; ctx.textBaseline = "middle"; ctx.textAlign = "left";
|
|
270
|
+
ctx.fillText(t, bx + 7, by + h / 2 + 0.5);
|
|
271
|
+
ctx.restore();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Zones: a name tag under someone's feet at (x, y). */
|
|
275
|
+
function drawTag(ctx, x, y, look, env) {
|
|
276
|
+
const { pal } = env;
|
|
277
|
+
ctx.save();
|
|
278
|
+
if (look.dim) ctx.globalAlpha = 0.35;
|
|
279
|
+
ctx.font = "600 10px system-ui, sans-serif";
|
|
280
|
+
const t = fitText(ctx, look.label, 84), w = Math.ceil((ctx.measureText ? ctx.measureText(t).width : t.length * 6) + 10);
|
|
281
|
+
ctx.fillStyle = pal.surface; ctx.fillRect(Math.round(x - w / 2), y + 3, w, 14);
|
|
282
|
+
ctx.strokeStyle = look.match ? pal.focus : pal.border; ctx.lineWidth = look.match ? 2 : 1; ctx.strokeRect(Math.round(x - w / 2) + 0.5, y + 3.5, w - 1, 13);
|
|
283
|
+
ctx.fillStyle = pal.text; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(t, x, y + 10.5);
|
|
284
|
+
ctx.restore();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** @type {OfficeSkin} */
|
|
288
|
+
export const pixelSkin = { id: "pixel", drawFloor, drawRoom, drawDeskBack, drawCharacter, drawDeskFront, drawProp, drawBubble, drawTag };
|
|
289
|
+
|
|
290
|
+
// ── The built-in studio: the zones layout, drawn procedurally ──
|
|
291
|
+
|
|
292
|
+
const grid = (xs, ys) => ys.flatMap((y) => xs.map((x) => ({ x, y })));
|
|
293
|
+
const HALL_X = 676;
|
|
294
|
+
const STUDIO_AREAS = {
|
|
295
|
+
desks: { name: "desks", rect: { x: 24, y: 64, w: 632, h: 448 }, seats: grid([104, 248, 392, 536], [190, 320, 450]) },
|
|
296
|
+
lounge: { name: "lounge", rect: { x: 696, y: 64, w: 560, h: 304 }, seats: grid([748, 824, 900, 976, 1052, 1128, 1204], [250, 340]) },
|
|
297
|
+
alert: { name: "alert", rect: { x: 696, y: 392, w: 300, h: 304 }, seats: grid([744, 814, 884], [530, 600, 670]) },
|
|
298
|
+
nap: { name: "nap", rect: { x: 1020, y: 392, w: 236, h: 304 }, seats: grid([1084, 1184], [530, 600, 670]) },
|
|
299
|
+
};
|
|
300
|
+
/** The studio's walkable graph: an aisle under each row of seats, joined to the hall that runs down the middle. */
|
|
301
|
+
function studioGraph(areas) {
|
|
302
|
+
const waypoints = [], edges = [], hall = new Set([380, 704]);
|
|
303
|
+
for (const a of Object.values(areas)) {
|
|
304
|
+
for (const y of [...new Set(a.seats.map((s) => s.y))]) {
|
|
305
|
+
const ay = y + 22, xs = [...new Set([...a.seats.filter((s) => s.y === y).map((s) => s.x), HALL_X])].sort((p, q) => p - q);
|
|
306
|
+
hall.add(ay);
|
|
307
|
+
const ids = xs.map((x) => { const id = `${a.name}-${ay}-${x}`; waypoints.push({ id, x, y: ay }); return id; });
|
|
308
|
+
ids.forEach((id, k) => { if (k) edges.push([ids[k - 1], id]); });
|
|
309
|
+
const join = ids[xs.indexOf(HALL_X)];
|
|
310
|
+
edges.push([join, `hall-${ay}`]);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const ys = [...hall].sort((p, q) => p - q);
|
|
314
|
+
ys.forEach((y, k) => { waypoints.push({ id: `hall-${y}`, x: HALL_X, y }); if (k) edges.push([`hall-${ys[k - 1]}`, `hall-${y}`]); });
|
|
315
|
+
return { waypoints, edges };
|
|
316
|
+
}
|
|
317
|
+
const STUDIO_FURNITURE = [
|
|
318
|
+
{ id: "board", shape: "board", x: 340, y: 100, depth: 5 },
|
|
319
|
+
{ id: "rack-work", shape: "rack", tone: "ok", x: 610, y: 160, depth: 6, activeWhen: ["working"] },
|
|
320
|
+
...STUDIO_AREAS.desks.seats.map((s, i) => ({ id: `desk-${i}`, shape: "desk", x: s.x, y: s.y, depth: 1100 })),
|
|
321
|
+
{ id: "sofa", shape: "sofa", x: 900, y: 150, depth: 10 },
|
|
322
|
+
{ id: "coffee", shape: "coffee", x: 1210, y: 130, depth: 20, activeWhen: ["idle"] },
|
|
323
|
+
{ id: "plant-1", shape: "plant", x: 724, y: 120, depth: 20 }, { id: "plant-2", shape: "plant", x: 40, y: 500, depth: 20 },
|
|
324
|
+
{ id: "rack-alert", shape: "rack", tone: "danger", x: 960, y: 460, depth: 900, activeWhen: ["needs", "stuck", "crashed"] },
|
|
325
|
+
{ id: "bug", shape: "bug", x: 760, y: 450, depth: 950, activeWhen: ["crashed"] },
|
|
326
|
+
...STUDIO_AREAS.nap.seats.map((s, i) => ({ id: `bag-${i}`, shape: "beanbag", x: s.x, y: s.y + 4, depth: 990 })),
|
|
327
|
+
].sort((a, b) => a.depth - b.depth);
|
|
328
|
+
/** The studio's pack: the same shape as a checked one (fleet-office-pack.js), furniture by `shape` instead of sprites. */
|
|
329
|
+
export const STUDIO_PACK = {
|
|
330
|
+
format: 1, id: "studio", scale: 1, areas: STUDIO_AREAS, door: { x: HALL_X, y: 704 }, furniture: STUDIO_FURNITURE, ...studioGraph(STUDIO_AREAS),
|
|
331
|
+
stateZones: { working: "desks", idle: "lounge", needs: "alert", crashed: "alert", stuck: "alert", paused: "nap", stopped: "nap", default: "lounge" },
|
|
332
|
+
characters: {}, byInstance: {}, props: {}, bubbles: {},
|
|
333
|
+
};
|
|
334
|
+
const ZONE_FLOOR = {
|
|
335
|
+
dark: { desks: ["#2b313b", "#272c35"], lounge: ["#2c3430", "#28302c"], alert: ["#3a2c2e", "#35282a"], nap: ["#302c3a", "#2c2835"], hall: "#262626" },
|
|
336
|
+
light: { desks: ["#efe7d8", "#e8dfcd"], lounge: ["#e3ecdf", "#dce6d7"], alert: ["#f3e1df", "#eed9d6"], nap: ["#e8e3f1", "#e1dbec"], hall: "#ececec" },
|
|
337
|
+
};
|
|
338
|
+
function drawBackground(ctx, L, env) {
|
|
339
|
+
const { pal } = env, F = ZONE_FLOOR[env.mode], T = 24;
|
|
340
|
+
px(ctx, 0, 0, LOGICAL.w, LOGICAL.h, F.hall);
|
|
341
|
+
for (const a of Object.values(L.areas)) {
|
|
342
|
+
const [c1, c2] = F[a.name] || F.desks, r = a.rect;
|
|
343
|
+
px(ctx, r.x, r.y, r.w, r.h, c1);
|
|
344
|
+
for (let y = r.y; y < r.y + r.h; y += T) for (let x = r.x + (((y - r.y) / T) % 2) * T; x < r.x + r.w; x += 2 * T) px(ctx, x, y, Math.min(T, r.x + r.w - x), Math.min(T, r.y + r.h - y), c2);
|
|
345
|
+
ctx.save();
|
|
346
|
+
ctx.strokeStyle = pal.border; ctx.lineWidth = 2; ctx.strokeRect(r.x + 1, r.y + 1, r.w - 2, r.h - 2);
|
|
347
|
+
ctx.fillStyle = pal.text; ctx.font = "600 16px system-ui, sans-serif"; ctx.textBaseline = "top"; ctx.textAlign = "left";
|
|
348
|
+
ctx.fillText(fitText(ctx, env.labels.areas[a.name] || a.name, r.w - 60), r.x + 12, r.y + 10);
|
|
349
|
+
const crowd = L.crowd && L.crowd.get(a.name);
|
|
350
|
+
if (crowd) { ctx.fillStyle = pal.text2; ctx.textAlign = "right"; ctx.fillText(`+${crowd}`, r.x + r.w - 12, r.y + 10); }
|
|
351
|
+
ctx.restore();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function drawFurniture(ctx, f, active, ms, env) {
|
|
355
|
+
const x = f.x, y = f.y;
|
|
356
|
+
switch (f.shape) {
|
|
357
|
+
case "desk":
|
|
358
|
+
px(ctx, x - 40, y - 24, 80, 6, WOOD.top); px(ctx, x - 38, y - 18, 76, 18, WOOD.front); px(ctx, x - 38, y - 18, 76, 2, WOOD.edge);
|
|
359
|
+
px(ctx, x + 12, y - 44, 24, 18, METAL.frame); px(ctx, x + 14, y - 42, 20, 13, METAL.screenOn); px(ctx, x + 22, y - 26, 4, 2, METAL.frame);
|
|
360
|
+
break;
|
|
361
|
+
case "board":
|
|
362
|
+
px(ctx, x - 90, y - 26, 180, 52, "#f4f4f0"); px(ctx, x - 92, y - 28, 184, 3, "#8a8f98"); px(ctx, x - 92, y + 25, 184, 3, "#8a8f98");
|
|
363
|
+
for (let i = 0; i < 4; i++) px(ctx, x - 78, y - 16 + i * 10, 40 + ((i * 37) % 90), 3, ["#4b8bf4", "#d97757", "#3fa58c", "#9b6bff"][i]);
|
|
364
|
+
break;
|
|
365
|
+
case "rack": {
|
|
366
|
+
px(ctx, x - 22, y - 70, 44, 90, "#2b2f36"); px(ctx, x - 20, y - 68, 40, 86, "#363b44");
|
|
367
|
+
const on = env.pal[f.tone] || env.pal.ok;
|
|
368
|
+
for (let r = 0; r < 6; r++) for (let c = 0; c < 3; c++) {
|
|
369
|
+
const lit = active && (ms <= 0 || frameAt(ms + (r * 3 + c) * 170, 2, 3) === 0);
|
|
370
|
+
px(ctx, x - 14 + c * 10, y - 60 + r * 13, 6, 4, lit ? on : "#4a505a");
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case "sofa":
|
|
375
|
+
px(ctx, x - 110, y - 30, 220, 26, "#3f7286"); px(ctx, x - 118, y - 14, 236, 34, "#4f8aa1"); px(ctx, x - 118, y - 20, 16, 40, "#3f7286"); px(ctx, x + 102, y - 20, 16, 40, "#3f7286");
|
|
376
|
+
break;
|
|
377
|
+
case "coffee":
|
|
378
|
+
px(ctx, x - 18, y - 34, 36, 50, "#5a5f69"); px(ctx, x - 14, y - 28, 28, 12, "#2b2f36"); px(ctx, x - 6, y + 2, 12, 10, "#ffffff");
|
|
379
|
+
if (active) for (let i = 0; i < 3; i++) { const k = frameAt(ms + i * 300, 4, 4); px(ctx, x - 4 + ((i + k) % 2) * 4, y - 6 - i * 7 - k, 2, 4, "rgba(230, 230, 236, 0.7)"); }
|
|
380
|
+
break;
|
|
381
|
+
case "plant":
|
|
382
|
+
px(ctx, x - 9, y, 18, 16, "#b5653a"); px(ctx, x - 14, y - 18, 10, 18, "#3f8f4f"); px(ctx, x - 4, y - 26, 9, 26, "#4fa65d"); px(ctx, x + 5, y - 16, 10, 16, "#3f8f4f");
|
|
383
|
+
break;
|
|
384
|
+
case "bug": {
|
|
385
|
+
const wob = active && ms > 0 ? Math.round(Math.sin(ms / 160) * 26) : 0, c = active ? env.pal.danger : "#6b6f76";
|
|
386
|
+
const bx = x + wob;
|
|
387
|
+
px(ctx, bx - 9, y - 6, 18, 12, c); px(ctx, bx - 5, y - 11, 10, 6, c); px(ctx, bx - 1, y - 6, 2, 12, "#1d1d22");
|
|
388
|
+
for (const k of [-1, 1]) for (const r of [-4, 0, 4]) px(ctx, bx + k * 11 - (k < 0 ? 3 : 0), y + r, 3, 2, "#1d1d22");
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case "beanbag":
|
|
392
|
+
px(ctx, x - 26, y - 14, 52, 20, "#7a5a8c"); px(ctx, x - 22, y - 20, 44, 8, "#8d6aa0");
|
|
393
|
+
break;
|
|
394
|
+
default:
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/** @type {OfficeSkin} */
|
|
399
|
+
export const studioSkin = { ...pixelSkin, id: "studio", pack: STUDIO_PACK, drawBackground, drawFurniture };
|
|
400
|
+
|
|
401
|
+
// ── Skin packs (the seam; nothing registers one yet) ──
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* An OfficeSkin from a checked pack (fleet-office-pack.js checkSkinPack) and its loaded pictures (`images`: src → a
|
|
405
|
+
* drawable image). What it lacks, or a picture not loaded, is drawn by the built-in skins.
|
|
406
|
+
*/
|
|
407
|
+
export function spriteSkin(pack, images, base = pixelSkin) {
|
|
408
|
+
const img = (src) => (src && images.get(src)) || null;
|
|
409
|
+
const strip = (ctx, st, ms, x, y, frame = null) => {
|
|
410
|
+
const im = img(st && st.src);
|
|
411
|
+
if (!im) return false;
|
|
412
|
+
const f = frame ?? frameAt(ms, st.frames, st.fps), s = pack.scale;
|
|
413
|
+
ctx.save();
|
|
414
|
+
ctx.imageSmoothingEnabled = false;
|
|
415
|
+
ctx.drawImage(im, f * st.w, 0, st.w, st.h, Math.round(x - st.ax * s), Math.round(y - st.ay * s), st.w * s, st.h * s);
|
|
416
|
+
ctx.restore();
|
|
417
|
+
return true;
|
|
418
|
+
};
|
|
419
|
+
return {
|
|
420
|
+
...base,
|
|
421
|
+
id: pack.id,
|
|
422
|
+
pack,
|
|
423
|
+
drawRoom(ctx, room, env) {
|
|
424
|
+
const floor = img(pack.room.floor), wall = img(pack.room.wall);
|
|
425
|
+
if (!floor) { base.drawRoom(ctx, room, env); return; }
|
|
426
|
+
ctx.save();
|
|
427
|
+
ctx.imageSmoothingEnabled = false;
|
|
428
|
+
const pat = ctx.createPattern(floor, "repeat");
|
|
429
|
+
if (pat) { ctx.fillStyle = pat; ctx.fillRect(room.x, room.y, room.w, room.h); }
|
|
430
|
+
if (wall) ctx.drawImage(wall, room.x, room.y, room.w, 26);
|
|
431
|
+
ctx.fillStyle = env.pal.text; ctx.font = "600 12px system-ui, sans-serif"; ctx.textBaseline = "middle";
|
|
432
|
+
ctx.fillText(fitText(ctx, room.lobby ? env.labels.lobby : room.name, room.w - 20), room.x + 10, room.y + 13);
|
|
433
|
+
ctx.restore();
|
|
434
|
+
},
|
|
435
|
+
drawDeskBack(ctx, d, look, env) {
|
|
436
|
+
const im = img(look.lamp && pack.desk.lit ? pack.desk.lit : pack.desk.back);
|
|
437
|
+
if (!im) { base.drawDeskBack(ctx, d, look, env); return; }
|
|
438
|
+
ctx.save(); ctx.imageSmoothingEnabled = false; if (look.dim) ctx.globalAlpha = 0.35; ctx.drawImage(im, d.x, d.y, d.w, d.h); ctx.restore();
|
|
439
|
+
},
|
|
440
|
+
drawDeskFront(ctx, d, look, env) {
|
|
441
|
+
const im = img(pack.desk.front);
|
|
442
|
+
if (im) { ctx.save(); ctx.imageSmoothingEnabled = false; if (look.dim) ctx.globalAlpha = 0.35; ctx.drawImage(im, d.x, d.y, d.w, d.h); ctx.restore(); }
|
|
443
|
+
// The name tag, the sign and the folders are words and counts: always drawn here.
|
|
444
|
+
base.drawDeskFront(ctx, d, { ...look, frontless: !!im }, env);
|
|
445
|
+
},
|
|
446
|
+
drawBackground(ctx, L, env) {
|
|
447
|
+
const im = img(pack.background);
|
|
448
|
+
if (!im) { drawBackground(ctx, L, env); return; }
|
|
449
|
+
ctx.save(); ctx.imageSmoothingEnabled = false; ctx.drawImage(im, 0, 0, LOGICAL.w, LOGICAL.h); ctx.restore();
|
|
450
|
+
},
|
|
451
|
+
drawFurniture(ctx, f, active, ms, env) {
|
|
452
|
+
if (f.shape) { drawFurniture(ctx, f, active, ms, env); return; }
|
|
453
|
+
// Active: its strip (or activeSprite) plays; otherwise its first frame holds.
|
|
454
|
+
if (active) strip(ctx, f.activeSprite || f.sprite, ms, f.x, f.y);
|
|
455
|
+
else strip(ctx, f.sprite, 0, f.x, f.y, 0);
|
|
456
|
+
},
|
|
457
|
+
drawCharacter(ctx, d, look, ms, env) {
|
|
458
|
+
if (look.pose === "away") return;
|
|
459
|
+
const set = pack.characters[pack.byInstance[look.name]] || pack.characters.default;
|
|
460
|
+
ctx.save();
|
|
461
|
+
if (look.dim) ctx.globalAlpha = 0.35;
|
|
462
|
+
const done = !!set && strip(ctx, set[look.pose] || set.idle, ms, d.seat.x, d.seat.y);
|
|
463
|
+
ctx.restore();
|
|
464
|
+
if (!done) base.drawCharacter(ctx, d, look, ms, env);
|
|
465
|
+
},
|
|
466
|
+
drawProp(ctx, it, ms, env) { if (!strip(ctx, pack.props[it.hue], ms, it.x, it.y)) base.drawProp(ctx, it, ms, env); },
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** The skins this page has: the built-in ones (a pack would be added here once loaded). */
|
|
471
|
+
export const SKINS = new Map([["pixel", pixelSkin], ["studio", studioSkin]]);
|
|
472
|
+
export const skinFor = (id) => SKINS.get(id) || pixelSkin;
|
|
473
|
+
/** The people's depth in zones (fleet-office-pack.js). */
|
|
474
|
+
export { CHAR_DEPTH };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// The Office view's data, behind one small seam (fleet-office.js uses only these calls):
|
|
2
|
+
// - the offices people made: the host's (GET / PUT /ui/offices, src/web-office-store.ts — one list for every viewer);
|
|
3
|
+
// what comes back is checked again here (fleet-office-model.js officesOk), never trusted;
|
|
4
|
+
// - this browser's own choices (localStorage; blocked storage: this page only, as the canvas's Canvas/List): which
|
|
5
|
+
// office is shown, the auto office's member filter, and its style (skin).
|
|
6
|
+
// TODO(office editor): adding and renaming offices, assigning people, dragging seats and drawing paths call
|
|
7
|
+
// saveOffices() with the whole list and the rev they read (a change in between answers 409).
|
|
8
|
+
import { AUTO_ID, filterOk, officesOk } from "./fleet-office-model.js";
|
|
9
|
+
|
|
10
|
+
const CHOICE_KEY = "agend_org_office", FILTER_KEY = "agend_org_office_filter", SKIN_KEY = "agend_org_office_skin";
|
|
11
|
+
|
|
12
|
+
/** `fetch`: the caller's (a lease's, so an answer after the person moved on is dropped); `storage`: default localStorage. */
|
|
13
|
+
export function createOfficeStore({ fetch: get = globalThis.fetch, storage } = {}) {
|
|
14
|
+
const st = () => { try { return storage || globalThis.localStorage || null; } catch { return null; } };
|
|
15
|
+
const read = (k) => { try { const s = st(); return s ? s.getItem(k) : null; } catch { return null; } };
|
|
16
|
+
const write = (k, v) => { try { const s = st(); if (s) s.setItem(k, v); } catch { /* this page only */ } };
|
|
17
|
+
const headers = { "Content-Type": "application/json" };
|
|
18
|
+
return {
|
|
19
|
+
/** { rev, offices } from the host; null when it could not be read (no offices shown but the auto one). */
|
|
20
|
+
async offices() {
|
|
21
|
+
try {
|
|
22
|
+
const r = await get("/ui/offices", { method: "GET", headers });
|
|
23
|
+
if (!r || !r.ok) return null;
|
|
24
|
+
const d = await r.json();
|
|
25
|
+
return { rev: Number.isSafeInteger(d && d.rev) ? d.rev : 0, offices: officesOk(d && d.offices) };
|
|
26
|
+
} catch { return null; }
|
|
27
|
+
},
|
|
28
|
+
/**
|
|
29
|
+
* Replace the host's list (an admin session; `rev`: the one read — required: the host refuses a save without it,
|
|
30
|
+
* so two editors never overwrite each other unseen). { ok, rev, offices } or { ok: false, status, error }.
|
|
31
|
+
*/
|
|
32
|
+
async saveOffices(list, rev) {
|
|
33
|
+
if (!Number.isSafeInteger(rev)) return { ok: false, status: 0, error: "offices_rev_required" };
|
|
34
|
+
try {
|
|
35
|
+
const r = await get("/ui/offices", { method: "PUT", headers, body: JSON.stringify({ offices: list, rev }) });
|
|
36
|
+
const d = await r.json().catch(() => ({}));
|
|
37
|
+
return r.ok ? { ok: true, rev: d.rev, offices: officesOk(d.offices) } : { ok: false, status: r.status, error: d.error || "offices_failed" };
|
|
38
|
+
} catch { return { ok: false, status: 0, error: "offices_failed" }; }
|
|
39
|
+
},
|
|
40
|
+
/** The office shown here: an id (one the host no longer has falls back to the auto office where it is used). */
|
|
41
|
+
choice: () => { const v = read(CHOICE_KEY); return typeof v === "string" && v && v.length <= 40 ? v : AUTO_ID; },
|
|
42
|
+
saveChoice: (id) => write(CHOICE_KEY, String(id)),
|
|
43
|
+
filter: () => { const s = read(FILTER_KEY); try { return filterOk(s ? JSON.parse(s) : null); } catch { return filterOk(null); } },
|
|
44
|
+
saveFilter: (f) => write(FILTER_KEY, JSON.stringify(filterOk(f))),
|
|
45
|
+
/** The whole fleet's style here: a skin id ("pixel": desks, "studio": zones); an unknown one is the default where used. */
|
|
46
|
+
skin: () => { const v = read(SKIN_KEY); return typeof v === "string" && /^[A-Za-z0-9_-]{1,40}$/.test(v) ? v : "pixel"; },
|
|
47
|
+
saveSkin: (id) => write(SKIN_KEY, String(id)),
|
|
48
|
+
};
|
|
49
|
+
}
|