codex-work-receipt 0.10.1 → 0.11.0

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.
@@ -19,9 +19,17 @@ const DOM_TO_IMAGE_SOURCE = fs.readFileSync(
19
19
  process.env.CODEX_WORK_RECEIPT_DOM_TO_IMAGE || require.resolve("dom-to-image-more"),
20
20
  "utf8",
21
21
  );
22
+ const OFFICE_STYLE_SOURCE = fs.readFileSync(
23
+ new URL("./office-v6.css", import.meta.url),
24
+ "utf8",
25
+ );
22
26
  const MODELFLARE_LOGO_DATA_URL = `data:image/png;base64,${fs.readFileSync(
23
27
  new URL("../../docs/images/sponsors/modelflare-logo.png", import.meta.url),
24
28
  ).toString("base64")}`;
29
+ const TICKET_BUDDY_WINDOW_DATA_URL = `data:image/png;base64,${fs.readFileSync(
30
+ new URL("../../assets/codex-pet/ai-work-receipt/window-pet.png.base64", import.meta.url),
31
+ "utf8",
32
+ ).trim()}`;
25
33
  const MODELFLARE_URL = "https://modelflare.dev/sign-up?partner=OB9YXNSEEGOL";
26
34
 
27
35
  function inlineScript(value) {
@@ -80,6 +88,15 @@ function formatSeconds(milliseconds, locale) {
80
88
  return `${formatDecimal(Number(milliseconds || 0) / 1000, locale)}s`;
81
89
  }
82
90
 
91
+ function getHourInTimezone(date, timezone) {
92
+ const hourPart = new Intl.DateTimeFormat("en-US", {
93
+ timeZone: timezone,
94
+ hour: "2-digit",
95
+ hourCycle: "h23",
96
+ }).formatToParts(date).find((item) => item.type === "hour");
97
+ return Number(hourPart?.value || 0);
98
+ }
99
+
83
100
  function renderBreakdown(items, { key, labels = {}, unit, emptyLabel, locale }) {
84
101
  if (!items.length) return `<p class="structure-empty">${escapeHtml(emptyLabel)}</p>`;
85
102
  const maximum = Math.max(1, ...items.map((item) => Number(item.count || 0)));
@@ -225,6 +242,15 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
225
242
  const startAt = new Date(record.period.start_at);
226
243
  const endAt = new Date(record.period.end_at);
227
244
  const timezone = record.period.timezone;
245
+ const generatedAt = new Date(record.generated_at);
246
+ const generatedDate = formatDate(generatedAt, timezone, locale);
247
+ const generatedTime = formatTime(generatedAt, timezone, locale);
248
+ const generatedHour = getHourInTimezone(generatedAt, timezone);
249
+ const generatedScene = generatedHour >= 6 && generatedHour < 18 ? "day" : "night";
250
+ const generatedShift = copy.office.shifts[generatedScene];
251
+ const shiftHeader = copy.office.shiftHeader.replace("{shift}", generatedShift);
252
+ const shiftSign = copy.office.shiftSign.replace("{shift}", generatedShift);
253
+ const shiftOnDuty = copy.office.shiftOnDuty.replace("{shift}", generatedShift);
228
254
  const displayDate = formatDate(endAt, timezone, locale);
229
255
  const rangeStartDate = record.period.range_start_date || formatDateKey(record.period.start_at.slice(0, 10), "zh-CN").replaceAll("/", "-");
230
256
  const rangeEndDate = record.period.range_end_date || formatDateKey(record.period.end_at.slice(0, 10), "zh-CN").replaceAll("/", "-");
@@ -287,6 +313,22 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
287
313
  .reduce((total, group) => total + group.commands.length, 0);
288
314
  const featureCountLabel = copy.sidebar.features.countLabel
289
315
  .replace("{count}", formatNumber(featureCommandCount, locale));
316
+ const commandDescription = copy.office.commandDescription
317
+ .replace("{count}", formatNumber(featureCommandCount, locale));
318
+ const officeConfig = JSON.stringify({
319
+ generatedScene,
320
+ shifts: copy.office.shifts,
321
+ shiftHeader: copy.office.shiftHeader,
322
+ shiftSign: copy.office.shiftSign,
323
+ shiftOnDuty: copy.office.shiftOnDuty,
324
+ switchedAutoDay: copy.office.switchedAutoDay,
325
+ switchedAutoNight: copy.office.switchedAutoNight,
326
+ switchedDay: copy.office.switchedDay,
327
+ switchedNight: copy.office.switchedNight,
328
+ windPause: copy.office.windPause,
329
+ windResume: copy.office.windResume,
330
+ workReceiptDescription: copy.office.workReceiptDescription,
331
+ }).replaceAll("<", "\\u003c");
290
332
  const insights = renderInsights(record, copy, locale);
291
333
  const rows = [
292
334
  receiptRow(copy.rows.scope, scopeLabel),
@@ -346,10 +388,11 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
346
388
  </div>`;
347
389
 
348
390
  return `<!doctype html>
349
- <html lang="${escapeHtml(copy.htmlLang)}" data-theme="${escapeHtml(record.presentation.default_theme)}">
391
+ <html lang="${escapeHtml(copy.htmlLang)}" data-theme="${escapeHtml(record.presentation.default_theme)}" data-scene="${generatedScene}">
350
392
  <head>
351
393
  <meta charset="utf-8">
352
394
  <meta name="viewport" content="width=device-width, initial-scale=1">
395
+ <meta name="color-scheme" content="light dark">
353
396
  <title>${escapeHtml(copy.pageTitle)} · ${escapeHtml(displayDate)}</title>
354
397
  <style>
355
398
  :root {
@@ -1230,12 +1273,142 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1230
1273
  .sidebar { flex-direction: column; }
1231
1274
  .sidebar-card { flex-basis: auto; }
1232
1275
  }
1276
+ ${OFFICE_STYLE_SOURCE}
1233
1277
  </style>
1234
1278
  </head>
1235
1279
  <body>
1236
- <div class="layout">
1237
- <main class="page">
1238
- <div class="toolbar">
1280
+ <div class="ambient" aria-hidden="true"><div class="stars"></div><div class="haze"></div></div>
1281
+
1282
+ <header class="world-header">
1283
+ <div class="office-id">
1284
+ <div class="office-id__mark">C</div>
1285
+ <div><strong id="officeShiftName">${escapeHtml(shiftHeader)}</strong><span>WORLD'S EDGE / RECEIPT OFFICE NO. 07</span></div>
1286
+ </div>
1287
+
1288
+ <nav class="primary-nav" aria-label="${escapeHtml(copy.office.primaryNavAria)}">
1289
+ <button class="primary-nav__button is-active" type="button" aria-current="page">${escapeHtml(copy.office.todayReceipt)}</button>
1290
+ <button class="primary-nav__button" type="button" data-planned-message="${escapeHtml(copy.office.ticketHousePlanned)}">${escapeHtml(copy.office.ticketHouse)}<small>${escapeHtml(copy.office.planned)}</small></button>
1291
+ <button class="primary-nav__button" type="button" data-planned-message="${escapeHtml(copy.office.badgeWallPlanned)}">${escapeHtml(copy.office.badgeWall)}<small>${escapeHtml(copy.office.planned)}</small></button>
1292
+ </nav>
1293
+
1294
+ <div class="header-controls">
1295
+ <div class="scene-mode-switch" role="group" aria-label="${escapeHtml(copy.office.shiftAria)}">
1296
+ <button type="button" data-scene-mode="auto" aria-pressed="true" title="${escapeHtml(copy.office.autoModeTitle)}">${escapeHtml(copy.office.autoMode)}</button>
1297
+ <button type="button" data-scene-mode="day" aria-pressed="false">${escapeHtml(copy.office.dayMode)}</button>
1298
+ <button type="button" data-scene-mode="night" aria-pressed="false">${escapeHtml(copy.office.nightMode)}</button>
1299
+ </div>
1300
+
1301
+ <button class="world-button command-drawer-toggle" id="commandDrawerToggle" type="button" aria-expanded="false" aria-controls="commandDrawer" title="${escapeHtml(copy.office.commandToggleTitle)}">
1302
+ <span class="command-symbol" aria-hidden="true">&gt;_</span>
1303
+ <span>${escapeHtml(copy.sidebar.features.title)}</span>
1304
+ </button>
1305
+
1306
+ <button class="world-button" id="windToggle" type="button" aria-pressed="false" title="${escapeHtml(copy.office.windPause)}">
1307
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" aria-hidden="true"><path d="M3 8h10.5a3.5 3.5 0 1 0-3.1-5.1"/><path d="M3 12h16a3 3 0 1 1-2.6 4.5"/><path d="M3 16h7"/></svg>
1308
+ <span>${escapeHtml(copy.office.windPause)}</span>
1309
+ </button>
1310
+ </div>
1311
+ </header>
1312
+
1313
+ <main class="world">
1314
+ <section class="scene" aria-label="${escapeHtml(copy.office.sceneAria)}">
1315
+ <div class="scene-intro">
1316
+ <div class="arrival-shift"><i class="status-light"></i><span id="arrivalShiftLabel">${escapeHtml(shiftOnDuty)}</span></div>
1317
+ <p class="arrival-time">${escapeHtml(generatedDate)} · ${escapeHtml(generatedTime)} · ${escapeHtml(copy.office.arrivalSigned)}</p>
1318
+ <h1>${escapeHtml(copy.office.arrivalTitle)}<br><em>${escapeHtml(copy.office.arrivalEmphasis)}</em></h1>
1319
+ <p class="arrival-copy">${escapeHtml(copy.office.arrivalDescription)}</p>
1320
+ <div class="arrival-stats" aria-label="${escapeHtml(copy.office.arrivalSummaryAria)}">
1321
+ <span><strong>${escapeHtml(formatNumber(record.stats.session_count, locale))}</strong> ${escapeHtml(copy.office.sessionsSummary)}</span>
1322
+ <span><strong>${escapeHtml(formatNumber(record.stats.completed_turns, locale))}</strong> ${escapeHtml(copy.office.turnsSummary)}</span>
1323
+ <span><strong>${escapeHtml(formatDuration(record.stats.work_duration_ms))}</strong> ${escapeHtml(copy.office.durationSummary)}</span>
1324
+ </div>
1325
+ </div>
1326
+ <div class="moon" aria-hidden="true"></div>
1327
+ <div class="mountains" aria-hidden="true"></div>
1328
+ <div class="wind-lines" aria-hidden="true"></div>
1329
+ <div class="ambient-receipt" aria-hidden="true"><div class="ambient-paper"></div></div>
1330
+ <div class="grass" aria-hidden="true"></div>
1331
+ <div class="cliff" aria-hidden="true"></div>
1332
+
1333
+ <div class="office">
1334
+ <div class="antenna" aria-hidden="true"><i></i></div>
1335
+ <div class="roof" aria-hidden="true"></div>
1336
+ <div class="wall" aria-hidden="true"></div>
1337
+ <div class="office-sign" id="officeSceneSign">${escapeHtml(shiftSign)}</div>
1338
+ <div class="window">
1339
+ <img class="pet" src="${TICKET_BUDDY_WINDOW_DATA_URL}" alt="${escapeHtml(copy.office.petAlt)}">
1340
+ </div>
1341
+ <div class="door" aria-hidden="true"></div>
1342
+ <button class="printer" id="printerCommandToggle" type="button" aria-expanded="false" aria-controls="commandDrawer" title="${escapeHtml(copy.office.printerCommandTitle)}"><span>CWR / ${escapeHtml(copy.office.commandDrawerCompact)}</span></button>
1343
+
1344
+ <div class="office-info-cluster">
1345
+ <div class="notice-lamp" aria-hidden="true"><i></i></div>
1346
+ <div class="notice-title">${escapeHtml(copy.office.officeNotice)}</div>
1347
+ <a class="log-board" href="${escapeHtml(changelogUrl)}" target="_blank" rel="noopener noreferrer">
1348
+ <small>${escapeHtml(copy.office.changelogKicker)}</small>
1349
+ ${escapeHtml(copy.office.changelogText)}
1350
+ </a>
1351
+ <a class="github-board github-star-link" href="${escapeHtml(githubStarPrompt.url)}" target="_blank" rel="noopener noreferrer">
1352
+ <small>${escapeHtml(copy.office.githubKicker)}</small>
1353
+ ${escapeHtml(copy.office.githubText)}
1354
+ </a>
1355
+ <a class="sponsor-crate" href="${MODELFLARE_URL}" target="_blank" rel="noopener noreferrer">
1356
+ <img src="${MODELFLARE_LOGO_DATA_URL}" alt="${escapeHtml(copy.sidebar.sponsorAlt)}" width="38" height="38">
1357
+ <span><small>${escapeHtml(copy.office.sponsorKicker)}</small>ModelFlare<strong class="sponsor-offer">${escapeHtml(copy.office.sponsorOffer)}</strong></span>
1358
+ </a>
1359
+ </div>
1360
+ </div>
1361
+ </section>
1362
+
1363
+ <aside class="today-package" aria-label="${escapeHtml(copy.office.todayPackTitle)}">
1364
+ <div class="today-package__heading">
1365
+ <div><small>${escapeHtml(copy.office.todayPackKicker)}</small><strong>${escapeHtml(copy.office.todayPackTitle)}</strong></div>
1366
+ <span>${escapeHtml(generatedDate)}</span>
1367
+ </div>
1368
+ <button class="package-item is-active" type="button" id="workReceiptShortcut" aria-current="true">
1369
+ <span class="package-item__icon package-item__icon--receipt" aria-hidden="true">${locale === "en" ? "WR" : "票"}</span>
1370
+ <span><strong>${escapeHtml(copy.office.workReceipt)}</strong><small>${escapeHtml(copy.office.workReceiptDescription)}</small></span>
1371
+ <b>${escapeHtml(copy.office.delivered)}</b>
1372
+ </button>
1373
+ <button class="package-item" type="button" data-planned-message="${escapeHtml(copy.office.moodReceiptPlanned)}">
1374
+ <span class="package-item__icon package-item__icon--mood" aria-hidden="true">${locale === "en" ? "MO" : "心"}</span>
1375
+ <span><strong>${escapeHtml(copy.office.moodReceipt)}</strong><small>${escapeHtml(copy.office.moodReceiptDescription)}</small></span>
1376
+ <b>${escapeHtml(copy.office.planned)}</b>
1377
+ </button>
1378
+ <button class="package-item" type="button" data-planned-message="${escapeHtml(copy.office.badgesPlanned)}">
1379
+ <span class="package-item__icon package-item__icon--badge" aria-hidden="true">${locale === "en" ? "BD" : "章"}</span>
1380
+ <span><strong>${escapeHtml(copy.office.currentBadges)}</strong><small>${escapeHtml(copy.office.currentBadgesDescription)}</small></span>
1381
+ <b>${escapeHtml(copy.office.planned)}</b>
1382
+ </button>
1383
+ <div class="today-package__links">
1384
+ <button type="button" data-planned-message="${escapeHtml(copy.office.ticketHousePlanned)}">${escapeHtml(copy.office.enterTicketHouse)}</button>
1385
+ <button type="button" data-planned-message="${escapeHtml(copy.office.badgeWallPlanned)}">${escapeHtml(copy.office.viewBadgeWall)}</button>
1386
+ </div>
1387
+ </aside>
1388
+
1389
+ <aside aria-label="${escapeHtml(copy.office.commandDrawerAria)}">
1390
+ <details class="feature-dock" id="commandDrawer" data-feature-details>
1391
+ <summary class="feature-dock__summary">
1392
+ <span class="feature-dock__title">
1393
+ <span class="feature-dock__terminal" aria-hidden="true">&gt;_</span>
1394
+ <span class="feature-dock__title-text">${escapeHtml(copy.sidebar.features.title)}</span>
1395
+ <span class="feature-dock__compact">${escapeHtml(copy.office.commandDrawerCompact)}</span>
1396
+ </span>
1397
+ <span class="feature-dock__meta">
1398
+ <span class="feature-dock__count">${escapeHtml(featureCountLabel)}</span>
1399
+ <svg class="feature-dock__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>
1400
+ </span>
1401
+ </summary>
1402
+ <div class="feature-dock__body" data-feature-scroll>
1403
+ <p class="feature-dock__description">${escapeHtml(commandDescription)}</p>${featureCommands}
1404
+ <span class="feature-copy-status" data-feature-copy-status role="status" aria-live="polite"></span>
1405
+ </div>
1406
+ </details>
1407
+ </aside>
1408
+
1409
+ <section class="ticket-rail" aria-label="${escapeHtml(copy.receiptAria)}">
1410
+ <div class="paper-bridge" aria-hidden="true"></div>
1411
+ <div class="receipt-toolbar toolbar">
1239
1412
  <nav class="theme-switcher" aria-label="${escapeHtml(copy.themeAria)}">
1240
1413
  <button class="theme-button" type="button" data-theme-value="classic">${escapeHtml(copy.themes.classic)}</button>
1241
1414
  <button class="theme-button" type="button" data-theme-value="diner">${escapeHtml(copy.themes.diner)}</button>
@@ -1247,6 +1420,9 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1247
1420
  </div>
1248
1421
  </div>
1249
1422
 
1423
+ <div class="ticket-sway">
1424
+ <div class="receipt-scope">
1425
+
1250
1426
  <div class="export-sheet" id="receipt-export">
1251
1427
  <article class="paper receipt" aria-label="${escapeHtml(copy.receiptAria)}">
1252
1428
  <header class="brand">
@@ -1299,55 +1475,19 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1299
1475
  <p class="transfer-note">${escapeHtml(copy.transferNote)}</p>
1300
1476
  </section>
1301
1477
  </div>
1302
-
1303
- <p class="privacy">${escapeHtml(copy.privacy)}</p>
1304
- </main>
1305
- <aside class="sidebar" aria-label="${escapeHtml(copy.sidebar.aria)}">
1306
- <div class="sidebar-card">
1307
- <h3 class="sidebar-card__title">
1308
- <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25Z"/></svg>
1309
- ${escapeHtml(copy.sidebar.supportTitle)}
1310
- </h3>
1311
- <p>${escapeHtml(copy.sidebar.supportDescription)}</p>
1312
- <a class="github-star-link" href="${escapeHtml(githubStarPrompt.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(githubStarPrompt.label)}</a>
1313
- </div>
1314
- <div class="sidebar-card">
1315
- <h3 class="sidebar-card__title">
1316
- <svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/></svg>
1317
- ${escapeHtml(copy.sidebar.changelogTitle)}
1318
- </h3>
1319
- <a href="${escapeHtml(changelogUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(copy.sidebar.changelogLink)}</a>
1320
1478
  </div>
1321
- <div class="sidebar-card sidebar-sponsor">
1322
- <span class="sidebar-sponsor__label">${escapeHtml(copy.sidebar.sponsorLabel)}</span>
1323
- <a class="sidebar-sponsor__link" href="${MODELFLARE_URL}" target="_blank" rel="noopener noreferrer">
1324
- <img src="${MODELFLARE_LOGO_DATA_URL}" alt="${escapeHtml(copy.sidebar.sponsorAlt)}" width="40" height="40">
1325
- <span class="sidebar-sponsor__name">ModelFlare<br>modelflare.dev</span>
1326
- </a>
1327
1479
  </div>
1328
- <details class="sidebar-card sidebar-features" data-feature-details>
1329
- <summary class="sidebar-features__summary">
1330
- <span class="sidebar-card__title">
1331
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 17 10 11 4 5"></polyline><line x1="12" x2="20" y1="19" y2="19"></line></svg>
1332
- ${escapeHtml(copy.sidebar.features.title)}
1333
- </span>
1334
- <span class="sidebar-features__meta">
1335
- <span class="sidebar-features__count">${escapeHtml(featureCountLabel)}</span>
1336
- <svg class="sidebar-features__chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"></path></svg>
1337
- </span>
1338
- </summary>
1339
- <div class="sidebar-features__body">
1340
- <p class="sidebar-features__description">${escapeHtml(copy.sidebar.features.description)}</p>${featureCommands}
1341
- <span class="feature-copy-status" data-feature-copy-status role="status" aria-live="polite"></span>
1342
- </div>
1343
- </details>
1344
- </aside>
1345
- </div>
1480
+ <p class="privacy">${escapeHtml(copy.privacy)}</p>
1481
+ </section>
1482
+ </main>
1483
+
1484
+ <div class="toast" id="toast" role="status" aria-live="polite"></div>
1346
1485
  <script>${inlineScript(DOM_TO_IMAGE_SOURCE)}</script>
1347
1486
  <script>
1348
1487
  const exportConfig = ${exportConfig};
1349
1488
  const transferConfig = ${transferConfig};
1350
1489
  const featureConfig = ${featureConfig};
1490
+ const officeConfig = ${officeConfig};
1351
1491
  const themes = new Set(["classic", "diner", "payroll"]);
1352
1492
  const buttons = [...document.querySelectorAll("[data-theme-value]")];
1353
1493
  function applyTheme(theme) {
@@ -1361,17 +1501,126 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1361
1501
  try { savedTheme = localStorage.getItem("codex-work-receipt-theme"); } catch {}
1362
1502
  applyTheme(savedTheme || document.documentElement.dataset.theme);
1363
1503
 
1504
+ const toast = document.getElementById("toast");
1505
+ let toastTimer = null;
1506
+ function showToast(message) {
1507
+ if (!toast) return;
1508
+ clearTimeout(toastTimer);
1509
+ toast.textContent = message;
1510
+ toast.dataset.show = "true";
1511
+ toastTimer = setTimeout(() => { toast.dataset.show = "false"; }, 2600);
1512
+ }
1513
+
1514
+ const sceneModeButtons = [...document.querySelectorAll("[data-scene-mode]")];
1515
+ const officeShiftName = document.getElementById("officeShiftName");
1516
+ const arrivalShiftLabel = document.getElementById("arrivalShiftLabel");
1517
+ const officeSceneSign = document.getElementById("officeSceneSign");
1518
+ function applySceneMode(mode) {
1519
+ const selectedMode = new Set(["auto", "day", "night"]).has(mode) ? mode : "auto";
1520
+ const scene = selectedMode === "auto" ? officeConfig.generatedScene : selectedMode;
1521
+ const shift = officeConfig.shifts[scene];
1522
+ document.documentElement.dataset.scene = scene;
1523
+ document.documentElement.style.colorScheme = scene === "day" ? "light" : "dark";
1524
+ sceneModeButtons.forEach((button) => button.setAttribute("aria-pressed", String(button.dataset.sceneMode === selectedMode)));
1525
+ if (officeShiftName) officeShiftName.textContent = officeConfig.shiftHeader.replace("{shift}", shift);
1526
+ if (arrivalShiftLabel) arrivalShiftLabel.textContent = officeConfig.shiftOnDuty.replace("{shift}", shift);
1527
+ if (officeSceneSign) officeSceneSign.textContent = officeConfig.shiftSign.replace("{shift}", shift);
1528
+ }
1529
+ sceneModeButtons.forEach((button) => button.addEventListener("click", () => {
1530
+ applySceneMode(button.dataset.sceneMode);
1531
+ const message = button.dataset.sceneMode === "auto"
1532
+ ? officeConfig.generatedScene === "day" ? officeConfig.switchedAutoDay : officeConfig.switchedAutoNight
1533
+ : button.dataset.sceneMode === "day" ? officeConfig.switchedDay : officeConfig.switchedNight;
1534
+ showToast(message);
1535
+ }));
1536
+ applySceneMode("auto");
1537
+
1538
+ const windToggle = document.getElementById("windToggle");
1539
+ if (windToggle) {
1540
+ windToggle.addEventListener("click", () => {
1541
+ const paused = document.body.classList.toggle("wind-paused");
1542
+ const label = paused ? officeConfig.windResume : officeConfig.windPause;
1543
+ windToggle.setAttribute("aria-pressed", String(paused));
1544
+ const text = windToggle.querySelector("span:last-child");
1545
+ if (text) text.textContent = label;
1546
+ windToggle.title = label;
1547
+ });
1548
+ }
1549
+
1550
+ const ticketRail = document.querySelector(".ticket-rail");
1364
1551
  const featureDetails = document.querySelector("[data-feature-details]");
1552
+ const featureScroll = document.querySelector("[data-feature-scroll]");
1553
+ const commandDrawerToggle = document.getElementById("commandDrawerToggle");
1554
+ const printerCommandToggle = document.getElementById("printerCommandToggle");
1555
+ function syncCommandDrawerState() {
1556
+ const open = Boolean(featureDetails?.open);
1557
+ commandDrawerToggle?.setAttribute("aria-expanded", String(open));
1558
+ printerCommandToggle?.setAttribute("aria-expanded", String(open));
1559
+ }
1560
+ function toggleCommandDrawer(force) {
1561
+ if (!featureDetails) return;
1562
+ featureDetails.open = typeof force === "boolean" ? force : !featureDetails.open;
1563
+ syncCommandDrawerState();
1564
+ }
1365
1565
  if (featureDetails) {
1366
- try {
1367
- const savedFeatureState = localStorage.getItem("codex-work-receipt-features-open");
1368
- if (savedFeatureState === "true") featureDetails.open = true;
1369
- if (savedFeatureState === "false") featureDetails.open = false;
1370
- } catch {}
1371
- featureDetails.addEventListener("toggle", () => {
1372
- try { localStorage.setItem("codex-work-receipt-features-open", String(featureDetails.open)); } catch {}
1373
- });
1566
+ featureDetails.open = false;
1567
+ featureDetails.addEventListener("toggle", syncCommandDrawerState);
1374
1568
  }
1569
+ commandDrawerToggle?.addEventListener("click", () => toggleCommandDrawer());
1570
+ printerCommandToggle?.addEventListener("click", () => toggleCommandDrawer());
1571
+ syncCommandDrawerState();
1572
+
1573
+ document.addEventListener("pointerdown", (event) => {
1574
+ if (!featureDetails?.open) return;
1575
+ if (featureDetails.contains(event.target) || commandDrawerToggle?.contains(event.target) || printerCommandToggle?.contains(event.target)) return;
1576
+ toggleCommandDrawer(false);
1577
+ });
1578
+ document.addEventListener("keydown", (event) => {
1579
+ if (event.key === "Escape" && featureDetails?.open) {
1580
+ toggleCommandDrawer(false);
1581
+ commandDrawerToggle?.focus();
1582
+ }
1583
+ });
1584
+
1585
+ document.querySelectorAll("[data-planned-message]").forEach((button) => {
1586
+ button.addEventListener("click", () => showToast(button.dataset.plannedMessage || ""));
1587
+ });
1588
+ document.getElementById("workReceiptShortcut")?.addEventListener("click", () => {
1589
+ ticketRail?.scrollTo({ top: 0, behavior: "smooth" });
1590
+ showToast(officeConfig.workReceiptDescription);
1591
+ });
1592
+ document.querySelector(".primary-nav__button.is-active")?.addEventListener("click", () => {
1593
+ ticketRail?.scrollTo({ top: 0, behavior: "smooth" });
1594
+ });
1595
+
1596
+ document.addEventListener("wheel", (event) => {
1597
+ if (event.ctrlKey || Math.abs(event.deltaY) < Math.abs(event.deltaX)) return;
1598
+ if (featureDetails?.contains(event.target)) {
1599
+ event.preventDefault();
1600
+ if (featureDetails.open && featureScroll) featureScroll.scrollBy({ top: event.deltaY, behavior: "auto" });
1601
+ return;
1602
+ }
1603
+ if (!ticketRail || ticketRail.contains(event.target)) return;
1604
+ event.preventDefault();
1605
+ ticketRail.scrollBy({ top: event.deltaY, behavior: "auto" });
1606
+ }, { passive: false });
1607
+
1608
+ document.addEventListener("keydown", (event) => {
1609
+ if (!ticketRail || event.defaultPrevented || event.metaKey || event.ctrlKey || event.altKey) return;
1610
+ const target = event.target instanceof Element ? event.target : null;
1611
+ if (target?.closest("button, a, input, textarea, select, summary, code, [contenteditable='true'], [role='tab'], [data-feature-details]")) return;
1612
+ let distance = null;
1613
+ if (event.key === "ArrowDown") distance = 64;
1614
+ if (event.key === "ArrowUp") distance = -64;
1615
+ if (event.key === "PageDown") distance = ticketRail.clientHeight * .82;
1616
+ if (event.key === "PageUp") distance = -ticketRail.clientHeight * .82;
1617
+ if (event.key === " " || event.code === "Space") distance = ticketRail.clientHeight * (event.shiftKey ? -.82 : .82);
1618
+ if (event.key === "Home") distance = -ticketRail.scrollHeight;
1619
+ if (event.key === "End") distance = ticketRail.scrollHeight;
1620
+ if (distance === null) return;
1621
+ event.preventDefault();
1622
+ ticketRail.scrollBy({ top: distance, behavior: "smooth" });
1623
+ });
1375
1624
 
1376
1625
  const featureTabs = document.querySelector("[data-feature-tabs]");
1377
1626
  if (featureTabs) {