codex-work-receipt 0.9.0 → 0.10.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.
@@ -7,6 +7,7 @@ import { formatDate, formatDuration, formatNumber, formatTime } from "../lib/tim
7
7
  import {
8
8
  buildCompensation,
9
9
  DEFAULT_LOCALE,
10
+ getCustomSummaryNotice,
10
11
  getReceiptCopy,
11
12
  getRollingSummaryNotice,
12
13
  getScopeLabel,
@@ -183,7 +184,7 @@ function renderInsights(record, copy, locale) {
183
184
 
184
185
  function renderFeatureCommands(featureCopy, locale) {
185
186
  const languageArgument = locale === "en" ? " --lang en" : "";
186
- return featureCopy.groups.map((group) => {
187
+ const tabs = featureCopy.groups.map((group, groupIndex) => {
187
188
  const commands = group.commands.map((item) => {
188
189
  const command = `npx codex-work-receipt@latest ${item.args}${languageArgument}`;
189
190
  return `
@@ -198,13 +199,22 @@ function renderFeatureCommands(featureCopy, locale) {
198
199
  </div>
199
200
  </li>`;
200
201
  }).join("");
201
- return `
202
- <section class="feature-group">
203
- <h4>${escapeHtml(group.title)}</h4>
202
+ const tabId = `feature-tab-${group.id}`;
203
+ const panelId = `feature-panel-${group.id}`;
204
+ return {
205
+ tab: `<button id="${tabId}" class="feature-tab" type="button" role="tab" aria-selected="${groupIndex === 0 ? "true" : "false"}" aria-controls="${panelId}" tabindex="${groupIndex === 0 ? "0" : "-1"}" data-feature-tab="${escapeHtml(group.id)}">${escapeHtml(group.title)}<span>${formatNumber(group.commands.length, locale)}</span></button>`,
206
+ panel: `
207
+ <section id="${panelId}" class="feature-panel" role="tabpanel" aria-labelledby="${tabId}" data-feature-panel="${escapeHtml(group.id)}">
204
208
  <ul>${commands}
205
209
  </ul>
206
- </section>`;
207
- }).join("");
210
+ </section>`,
211
+ };
212
+ });
213
+ return `
214
+ <div class="feature-tabs" data-feature-tabs>
215
+ <div class="feature-tabs__list" role="tablist" aria-label="${escapeHtml(featureCopy.tabAria)}">${tabs.map((item) => item.tab).join("")}</div>
216
+ <div class="feature-tabs__panels">${tabs.map((item) => item.panel).join("")}</div>
217
+ </div>`;
208
218
  }
209
219
 
210
220
  export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUrl = null, transferFile = null }) {
@@ -218,11 +228,17 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
218
228
  const displayDate = formatDate(endAt, timezone, locale);
219
229
  const rangeStartDate = record.period.range_start_date || formatDateKey(record.period.start_at.slice(0, 10), "zh-CN").replaceAll("/", "-");
220
230
  const rangeEndDate = record.period.range_end_date || formatDateKey(record.period.end_at.slice(0, 10), "zh-CN").replaceAll("/", "-");
221
- const isCalendarScope = new Set(["today", "last-7-days", "this-week"]).has(record.source.scope);
231
+ const isCalendarScope = new Set(["today", "last-7-days", "this-week"]).has(record.source.scope)
232
+ || (record.source.scope === "custom-range" && record.source.range_kind === "calendar-days");
222
233
  const spansMultipleDates = rangeStartDate !== rangeEndDate;
223
- const scopeLabel = getScopeLabel(record.source.scope, locale, record.source.hours);
234
+ const scopeLabel = getScopeLabel(record.source.scope, locale, record.source.hours, {
235
+ rangeKind: record.source.range_kind,
236
+ filterKind: record.source.filter_kind,
237
+ });
224
238
  const rollingSummaryNotice = record.source.scope === "last-hours"
225
239
  ? getRollingSummaryNotice(locale, record.source.hours)
240
+ : record.source.scope === "custom-range" && record.source.range_kind === "exact-time"
241
+ ? getCustomSummaryNotice(locale)
226
242
  : null;
227
243
  const dateRange = spansMultipleDates
228
244
  ? `${formatDateKey(rangeStartDate, locale)}—${formatDateKey(rangeEndDate, locale)}`
@@ -542,30 +558,82 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
542
558
  margin: 10px 0 12px;
543
559
  color: var(--feature-muted);
544
560
  }
545
- .feature-group + .feature-group {
546
- margin-top: 14px;
547
- padding-top: 13px;
548
- border-top: 1px solid var(--feature-soft-line);
561
+ .feature-tabs__list {
562
+ display: flex;
563
+ gap: 5px;
564
+ margin: 0 -2px 12px;
565
+ padding: 2px;
566
+ overflow-x: auto;
567
+ scrollbar-width: thin;
568
+ scrollbar-color: #b8b0a4 transparent;
549
569
  }
550
- .feature-group h4 {
551
- margin: 0 0 8px;
570
+ .feature-tab {
571
+ display: inline-flex;
572
+ align-items: center;
573
+ justify-content: center;
574
+ gap: 5px;
575
+ min-height: 34px;
576
+ padding: 7px 10px;
577
+ flex: 0 0 auto;
578
+ border: 1px solid var(--feature-line);
579
+ border-radius: 5px;
580
+ background: var(--feature-command);
552
581
  color: var(--feature-muted);
582
+ cursor: pointer;
553
583
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "PingFang SC", sans-serif;
554
- font-size: 9px;
584
+ font-size: 10px;
555
585
  font-weight: 700;
556
- letter-spacing: .08em;
557
- text-transform: uppercase;
586
+ line-height: 1.2;
587
+ white-space: nowrap;
558
588
  }
559
- .feature-group ul {
589
+ .feature-tab span {
590
+ min-width: 17px;
591
+ padding: 1px 4px;
592
+ border-radius: 999px;
593
+ background: rgba(40, 38, 32, .08);
594
+ font-size: 8px;
595
+ text-align: center;
596
+ }
597
+ .feature-tab[aria-selected="true"] {
598
+ border-color: var(--feature-ink);
599
+ background: var(--feature-ink);
600
+ color: #fff;
601
+ }
602
+ .feature-tab[aria-selected="true"] span { background: rgba(255, 255, 255, .18); }
603
+ .feature-tab:focus-visible {
604
+ outline: 2px solid #81786c;
605
+ outline-offset: 2px;
606
+ }
607
+ .feature-tabs[data-ready="true"] .feature-panel[hidden] { display: none; }
608
+ .feature-panel + .feature-panel {
609
+ margin-top: 14px;
610
+ padding-top: 13px;
611
+ border-top: 1px solid var(--feature-soft-line);
612
+ }
613
+ .feature-tabs[data-ready="true"] .feature-panel + .feature-panel {
614
+ margin-top: 0;
615
+ padding-top: 0;
616
+ border-top: 0;
617
+ }
618
+ .feature-panel ul {
619
+ display: grid;
620
+ grid-template-columns: repeat(2, minmax(0, 1fr));
621
+ column-gap: 12px;
560
622
  margin: 0;
561
623
  padding: 0;
562
624
  list-style: none;
563
625
  }
564
- .feature-command + .feature-command {
626
+ .feature-command {
627
+ min-width: 0;
565
628
  margin-top: 10px;
566
629
  padding-top: 10px;
567
630
  border-top: 1px dashed var(--feature-soft-line);
568
631
  }
632
+ .feature-command:nth-child(-n + 2) {
633
+ margin-top: 0;
634
+ padding-top: 0;
635
+ border-top: 0;
636
+ }
569
637
  .feature-command__name {
570
638
  display: block;
571
639
  margin-bottom: 5px;
@@ -653,8 +721,8 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
653
721
  }
654
722
  @media (min-width: 1021px) {
655
723
  .sidebar-features[open] {
656
- width: 300px;
657
- margin-left: -120px;
724
+ width: 560px;
725
+ margin-left: -380px;
658
726
  }
659
727
  }
660
728
  @media (prefers-reduced-motion: reduce) {
@@ -1133,6 +1201,12 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1133
1201
  .structure-grid { grid-template-columns: 1fr; }
1134
1202
  .sidebar { flex-direction: column; }
1135
1203
  .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
+ }
1136
1210
  }
1137
1211
  </style>
1138
1212
  </head>
@@ -1277,6 +1351,44 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1277
1351
  });
1278
1352
  }
1279
1353
 
1354
+ const featureTabs = document.querySelector("[data-feature-tabs]");
1355
+ if (featureTabs) {
1356
+ const tabs = [...featureTabs.querySelectorAll("[data-feature-tab]")];
1357
+ const panels = [...featureTabs.querySelectorAll("[data-feature-panel]")];
1358
+ const availableTabs = new Set(tabs.map((tab) => tab.dataset.featureTab));
1359
+ function activateFeatureTab(value, { focus = false } = {}) {
1360
+ const selected = availableTabs.has(value) ? value : tabs[0]?.dataset.featureTab;
1361
+ tabs.forEach((tab) => {
1362
+ const active = tab.dataset.featureTab === selected;
1363
+ tab.setAttribute("aria-selected", String(active));
1364
+ tab.tabIndex = active ? 0 : -1;
1365
+ if (active && focus) {
1366
+ tab.focus();
1367
+ tab.scrollIntoView({ block: "nearest", inline: "nearest" });
1368
+ }
1369
+ });
1370
+ panels.forEach((panel) => { panel.hidden = panel.dataset.featurePanel !== selected; });
1371
+ try { localStorage.setItem("codex-work-receipt-feature-tab", selected); } catch {}
1372
+ }
1373
+ let savedFeatureTab = null;
1374
+ try { savedFeatureTab = localStorage.getItem("codex-work-receipt-feature-tab"); } catch {}
1375
+ featureTabs.dataset.ready = "true";
1376
+ activateFeatureTab(savedFeatureTab || tabs[0]?.dataset.featureTab);
1377
+ tabs.forEach((tab, index) => {
1378
+ tab.addEventListener("click", () => activateFeatureTab(tab.dataset.featureTab));
1379
+ tab.addEventListener("keydown", (event) => {
1380
+ let nextIndex = null;
1381
+ if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
1382
+ if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
1383
+ if (event.key === "Home") nextIndex = 0;
1384
+ if (event.key === "End") nextIndex = tabs.length - 1;
1385
+ if (nextIndex === null) return;
1386
+ event.preventDefault();
1387
+ activateFeatureTab(tabs[nextIndex].dataset.featureTab, { focus: true });
1388
+ });
1389
+ });
1390
+ }
1391
+
1280
1392
  function fallbackCopyText(value) {
1281
1393
  const textarea = document.createElement("textarea");
1282
1394
  textarea.value = value;