codex-work-receipt 0.9.0 → 0.10.1

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;
588
+ }
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;
558
596
  }
559
- .feature-group ul {
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;
@@ -651,10 +719,15 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
651
719
  from { opacity: 0; transform: translateY(-3px); }
652
720
  to { opacity: 1; transform: translateY(0); }
653
721
  }
654
- @media (min-width: 1021px) {
722
+ @media (min-width: 1120px) {
723
+ .sidebar {
724
+ align-items: center;
725
+ }
726
+ .sidebar-card {
727
+ width: 100%;
728
+ }
655
729
  .sidebar-features[open] {
656
- width: 300px;
657
- margin-left: -120px;
730
+ width: clamp(360px, calc(100vw - 880px), 560px);
658
731
  }
659
732
  }
660
733
  @media (prefers-reduced-motion: reduce) {
@@ -1101,7 +1174,7 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1101
1174
  line-height: 1.6;
1102
1175
  text-align: center;
1103
1176
  }
1104
- @media (max-width: 1020px) {
1177
+ @media (max-width: 1119px) {
1105
1178
  .layout {
1106
1179
  grid-template-columns: 1fr;
1107
1180
  max-width: 540px;
@@ -1118,7 +1191,30 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1118
1191
  flex: 1 1 160px;
1119
1192
  min-width: 0;
1120
1193
  }
1121
- .sidebar-features { flex-basis: 100%; }
1194
+ .sidebar-features {
1195
+ width: 100%;
1196
+ max-width: 100%;
1197
+ margin-inline: 0;
1198
+ flex-basis: 100%;
1199
+ }
1200
+ .feature-tabs__list {
1201
+ display: grid;
1202
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1203
+ overflow: visible;
1204
+ }
1205
+ .feature-tab {
1206
+ width: 100%;
1207
+ min-width: 0;
1208
+ white-space: normal;
1209
+ }
1210
+ }
1211
+ @media (max-width: 560px) {
1212
+ .feature-panel ul { grid-template-columns: 1fr; }
1213
+ .feature-command:nth-child(2) {
1214
+ margin-top: 10px;
1215
+ padding-top: 10px;
1216
+ border-top: 1px dashed var(--feature-soft-line);
1217
+ }
1122
1218
  }
1123
1219
  @media (max-width: 420px) {
1124
1220
  .layout { padding-inline: 10px; }
@@ -1277,6 +1373,44 @@ export function renderHtml({ record, dataQrDataUrl = null, miniProgramCodeDataUr
1277
1373
  });
1278
1374
  }
1279
1375
 
1376
+ const featureTabs = document.querySelector("[data-feature-tabs]");
1377
+ if (featureTabs) {
1378
+ const tabs = [...featureTabs.querySelectorAll("[data-feature-tab]")];
1379
+ const panels = [...featureTabs.querySelectorAll("[data-feature-panel]")];
1380
+ const availableTabs = new Set(tabs.map((tab) => tab.dataset.featureTab));
1381
+ function activateFeatureTab(value, { focus = false } = {}) {
1382
+ const selected = availableTabs.has(value) ? value : tabs[0]?.dataset.featureTab;
1383
+ tabs.forEach((tab) => {
1384
+ const active = tab.dataset.featureTab === selected;
1385
+ tab.setAttribute("aria-selected", String(active));
1386
+ tab.tabIndex = active ? 0 : -1;
1387
+ if (active && focus) {
1388
+ tab.focus();
1389
+ tab.scrollIntoView({ block: "nearest", inline: "nearest" });
1390
+ }
1391
+ });
1392
+ panels.forEach((panel) => { panel.hidden = panel.dataset.featurePanel !== selected; });
1393
+ try { localStorage.setItem("codex-work-receipt-feature-tab", selected); } catch {}
1394
+ }
1395
+ let savedFeatureTab = null;
1396
+ try { savedFeatureTab = localStorage.getItem("codex-work-receipt-feature-tab"); } catch {}
1397
+ featureTabs.dataset.ready = "true";
1398
+ activateFeatureTab(savedFeatureTab || tabs[0]?.dataset.featureTab);
1399
+ tabs.forEach((tab, index) => {
1400
+ tab.addEventListener("click", () => activateFeatureTab(tab.dataset.featureTab));
1401
+ tab.addEventListener("keydown", (event) => {
1402
+ let nextIndex = null;
1403
+ if (event.key === "ArrowRight") nextIndex = (index + 1) % tabs.length;
1404
+ if (event.key === "ArrowLeft") nextIndex = (index - 1 + tabs.length) % tabs.length;
1405
+ if (event.key === "Home") nextIndex = 0;
1406
+ if (event.key === "End") nextIndex = tabs.length - 1;
1407
+ if (nextIndex === null) return;
1408
+ event.preventDefault();
1409
+ activateFeatureTab(tabs[nextIndex].dataset.featureTab, { focus: true });
1410
+ });
1411
+ });
1412
+ }
1413
+
1280
1414
  function fallbackCopyText(value) {
1281
1415
  const textarea = document.createElement("textarea");
1282
1416
  textarea.value = value;