codex-work-receipt 0.10.0 → 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 {
@@ -719,10 +762,15 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
719
762
  from { opacity: 0; transform: translateY(-3px); }
720
763
  to { opacity: 1; transform: translateY(0); }
721
764
  }
722
- @media (min-width: 1021px) {
765
+ @media (min-width: 1120px) {
766
+ .sidebar {
767
+ align-items: center;
768
+ }
769
+ .sidebar-card {
770
+ width: 100%;
771
+ }
723
772
  .sidebar-features[open] {
724
- width: 560px;
725
- margin-left: -380px;
773
+ width: clamp(360px, calc(100vw - 880px), 560px);
726
774
  }
727
775
  }
728
776
  @media (prefers-reduced-motion: reduce) {
@@ -1169,7 +1217,7 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1169
1217
  line-height: 1.6;
1170
1218
  text-align: center;
1171
1219
  }
1172
- @media (max-width: 1020px) {
1220
+ @media (max-width: 1119px) {
1173
1221
  .layout {
1174
1222
  grid-template-columns: 1fr;
1175
1223
  max-width: 540px;
@@ -1186,7 +1234,30 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1186
1234
  flex: 1 1 160px;
1187
1235
  min-width: 0;
1188
1236
  }
1189
- .sidebar-features { flex-basis: 100%; }
1237
+ .sidebar-features {
1238
+ width: 100%;
1239
+ max-width: 100%;
1240
+ margin-inline: 0;
1241
+ flex-basis: 100%;
1242
+ }
1243
+ .feature-tabs__list {
1244
+ display: grid;
1245
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1246
+ overflow: visible;
1247
+ }
1248
+ .feature-tab {
1249
+ width: 100%;
1250
+ min-width: 0;
1251
+ white-space: normal;
1252
+ }
1253
+ }
1254
+ @media (max-width: 560px) {
1255
+ .feature-panel ul { grid-template-columns: 1fr; }
1256
+ .feature-command:nth-child(2) {
1257
+ margin-top: 10px;
1258
+ padding-top: 10px;
1259
+ border-top: 1px dashed var(--feature-soft-line);
1260
+ }
1190
1261
  }
1191
1262
  @media (max-width: 420px) {
1192
1263
  .layout { padding-inline: 10px; }
@@ -1201,19 +1272,143 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1201
1272
  .structure-grid { grid-template-columns: 1fr; }
1202
1273
  .sidebar { flex-direction: column; }
1203
1274
  .sidebar-card { flex-basis: auto; }
1204
- .feature-panel ul { grid-template-columns: 1fr; }
1205
- .feature-command:nth-child(2) {
1206
- margin-top: 10px;
1207
- padding-top: 10px;
1208
- border-top: 1px dashed var(--feature-soft-line);
1209
- }
1210
1275
  }
1276
+ ${OFFICE_STYLE_SOURCE}
1211
1277
  </style>
1212
1278
  </head>
1213
1279
  <body>
1214
- <div class="layout">
1215
- <main class="page">
1216
- <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">
1217
1412
  <nav class="theme-switcher" aria-label="${escapeHtml(copy.themeAria)}">
1218
1413
  <button class="theme-button" type="button" data-theme-value="classic">${escapeHtml(copy.themes.classic)}</button>
1219
1414
  <button class="theme-button" type="button" data-theme-value="diner">${escapeHtml(copy.themes.diner)}</button>
@@ -1225,6 +1420,9 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1225
1420
  </div>
1226
1421
  </div>
1227
1422
 
1423
+ <div class="ticket-sway">
1424
+ <div class="receipt-scope">
1425
+
1228
1426
  <div class="export-sheet" id="receipt-export">
1229
1427
  <article class="paper receipt" aria-label="${escapeHtml(copy.receiptAria)}">
1230
1428
  <header class="brand">
@@ -1277,55 +1475,19 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1277
1475
  <p class="transfer-note">${escapeHtml(copy.transferNote)}</p>
1278
1476
  </section>
1279
1477
  </div>
1280
-
1281
- <p class="privacy">${escapeHtml(copy.privacy)}</p>
1282
- </main>
1283
- <aside class="sidebar" aria-label="${escapeHtml(copy.sidebar.aria)}">
1284
- <div class="sidebar-card">
1285
- <h3 class="sidebar-card__title">
1286
- <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>
1287
- ${escapeHtml(copy.sidebar.supportTitle)}
1288
- </h3>
1289
- <p>${escapeHtml(copy.sidebar.supportDescription)}</p>
1290
- <a class="github-star-link" href="${escapeHtml(githubStarPrompt.url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(githubStarPrompt.label)}</a>
1291
- </div>
1292
- <div class="sidebar-card">
1293
- <h3 class="sidebar-card__title">
1294
- <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>
1295
- ${escapeHtml(copy.sidebar.changelogTitle)}
1296
- </h3>
1297
- <a href="${escapeHtml(changelogUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(copy.sidebar.changelogLink)}</a>
1298
1478
  </div>
1299
- <div class="sidebar-card sidebar-sponsor">
1300
- <span class="sidebar-sponsor__label">${escapeHtml(copy.sidebar.sponsorLabel)}</span>
1301
- <a class="sidebar-sponsor__link" href="${MODELFLARE_URL}" target="_blank" rel="noopener noreferrer">
1302
- <img src="${MODELFLARE_LOGO_DATA_URL}" alt="${escapeHtml(copy.sidebar.sponsorAlt)}" width="40" height="40">
1303
- <span class="sidebar-sponsor__name">ModelFlare<br>modelflare.dev</span>
1304
- </a>
1305
1479
  </div>
1306
- <details class="sidebar-card sidebar-features" data-feature-details>
1307
- <summary class="sidebar-features__summary">
1308
- <span class="sidebar-card__title">
1309
- <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>
1310
- ${escapeHtml(copy.sidebar.features.title)}
1311
- </span>
1312
- <span class="sidebar-features__meta">
1313
- <span class="sidebar-features__count">${escapeHtml(featureCountLabel)}</span>
1314
- <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>
1315
- </span>
1316
- </summary>
1317
- <div class="sidebar-features__body">
1318
- <p class="sidebar-features__description">${escapeHtml(copy.sidebar.features.description)}</p>${featureCommands}
1319
- <span class="feature-copy-status" data-feature-copy-status role="status" aria-live="polite"></span>
1320
- </div>
1321
- </details>
1322
- </aside>
1323
- </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>
1324
1485
  <script>${inlineScript(DOM_TO_IMAGE_SOURCE)}</script>
1325
1486
  <script>
1326
1487
  const exportConfig = ${exportConfig};
1327
1488
  const transferConfig = ${transferConfig};
1328
1489
  const featureConfig = ${featureConfig};
1490
+ const officeConfig = ${officeConfig};
1329
1491
  const themes = new Set(["classic", "diner", "payroll"]);
1330
1492
  const buttons = [...document.querySelectorAll("[data-theme-value]")];
1331
1493
  function applyTheme(theme) {
@@ -1339,17 +1501,126 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1339
1501
  try { savedTheme = localStorage.getItem("codex-work-receipt-theme"); } catch {}
1340
1502
  applyTheme(savedTheme || document.documentElement.dataset.theme);
1341
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");
1342
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
+ }
1343
1565
  if (featureDetails) {
1344
- try {
1345
- const savedFeatureState = localStorage.getItem("codex-work-receipt-features-open");
1346
- if (savedFeatureState === "true") featureDetails.open = true;
1347
- if (savedFeatureState === "false") featureDetails.open = false;
1348
- } catch {}
1349
- featureDetails.addEventListener("toggle", () => {
1350
- try { localStorage.setItem("codex-work-receipt-features-open", String(featureDetails.open)); } catch {}
1351
- });
1566
+ featureDetails.open = false;
1567
+ featureDetails.addEventListener("toggle", syncCommandDrawerState);
1352
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
+ });
1353
1624
 
1354
1625
  const featureTabs = document.querySelector("[data-feature-tabs]");
1355
1626
  if (featureTabs) {