@nightkatana/kronosys-app 1.0.0-beta.21 → 1.0.0-beta.22

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.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/app/changelog/page.tsx +87 -19
  3. package/app/globals.css +10 -8
  4. package/app/guide/page.tsx +71 -34
  5. package/app/implementation/page.tsx +70 -60
  6. package/app/licenses/page.tsx +79 -47
  7. package/app/logs/page.tsx +103 -47
  8. package/app/page.tsx +104 -169
  9. package/app/reporting/page.tsx +1918 -1436
  10. package/app/settings/page.tsx +66 -44
  11. package/components/KronosysPayloadProvider.tsx +19 -5
  12. package/components/dashboard/AppShellHeaderKronoFocus.tsx +78 -0
  13. package/components/dashboard/AppShellHeaderToolbarLayout.tsx +36 -0
  14. package/components/dashboard/AppShellHeaderUtilityRibbon.tsx +19 -0
  15. package/components/dashboard/AppShellHeaderWallClock.tsx +23 -17
  16. package/components/dashboard/AppShellRouteNav.tsx +336 -209
  17. package/components/dashboard/AppShellToolbarCommandCenter.tsx +225 -0
  18. package/components/dashboard/AppShellToolbarRouteNav.tsx +204 -0
  19. package/components/dashboard/DashboardCommandCenter.tsx +119 -30
  20. package/components/dashboard/KronoFocusPanel.tsx +287 -260
  21. package/components/dashboard/LanguageMenu.tsx +23 -7
  22. package/components/dashboard/PageRefreshButton.tsx +42 -16
  23. package/components/dashboard/ReportingTour.tsx +20 -2
  24. package/components/dashboard/SessionListPanel.tsx +4 -4
  25. package/components/dashboard/ThemeToggle.tsx +4 -3
  26. package/components/dashboard/useAnchoredFloatingPortalStyle.ts +9 -2
  27. package/components/dashboard/useKronoFocusLiveSeconds.ts +4 -2
  28. package/lib/appShellHeaderClasses.ts +22 -3
  29. package/lib/appShellToolbarChrome.ts +112 -0
  30. package/lib/appShellToolbarDeferredIntents.ts +112 -0
  31. package/lib/appShellToolbarSessionSlices.ts +67 -0
  32. package/lib/dashboardCopy.ts +78 -29
  33. package/lib/dashboardQuickSearch.ts +37 -6
  34. package/lib/dashboardUrlSession.ts +36 -0
  35. package/lib/generatedUserChangelog.ts +14 -0
  36. package/lib/implementationNotes.ts +18 -14
  37. package/lib/reportingAggregate.ts +68 -9
  38. package/lib/reportingMetricHelp.ts +8 -8
  39. package/lib/reportingStrings.ts +118 -9
  40. package/lib/reportingTagWeekBreakdown.ts +55 -13
  41. package/lib/settingsCopy.ts +6 -7
  42. package/lib/userGuideCopy.ts +29 -26
  43. package/package.json +7 -5
  44. package/server/db.ts +6 -4
  45. package/server/dbSchema.ts +2 -2
  46. package/components/dashboard/AppShellCommandCenterPlaceholder.tsx +0 -17
@@ -48,11 +48,13 @@ import { AppVersionStamp } from "@/components/dashboard/AppVersionStamp";
48
48
  import {
49
49
  appShellHeaderClassName,
50
50
  appShellHeaderTitleMetaRowClassName,
51
- appShellHeaderToolbarClassName,
52
51
  } from "@/lib/appShellHeaderClasses";
53
- import { AppShellCommandCenterPlaceholder } from "@/components/dashboard/AppShellCommandCenterPlaceholder";
52
+ import { AppShellHeaderToolbarLayout } from "@/components/dashboard/AppShellHeaderToolbarLayout";
53
+ import { AppShellToolbarCommandCenter } from "@/components/dashboard/AppShellToolbarCommandCenter";
54
54
  import { AppShellHeaderSessionMeta } from "@/components/dashboard/AppShellHeaderSessionMeta";
55
55
  import { AppShellHeaderWallClock } from "@/components/dashboard/AppShellHeaderWallClock";
56
+ import { AppShellHeaderKronoFocus } from "@/components/dashboard/AppShellHeaderKronoFocus";
57
+ import { AppShellHeaderUtilityRibbon } from "@/components/dashboard/AppShellHeaderUtilityRibbon";
56
58
  import { dashboardStrings, type Lang } from "@/lib/dashboardCopy";
57
59
  import {
58
60
  reportingPresetDay,
@@ -65,7 +67,7 @@ import {
65
67
  ReportingPageTocDesktop,
66
68
  ReportingPageTocMobile,
67
69
  } from "@/components/dashboard/ReportingPageToc";
68
- import { reportingStrings } from "@/lib/reportingStrings";
70
+ import { reportingNav, reportingStrings } from "@/lib/reportingStrings";
69
71
  import { trackCodeMetricsFromCfg } from "@/lib/usageProfile";
70
72
  import {
71
73
  addDaysYmd,
@@ -85,7 +87,7 @@ import { ReportingTour } from "@/components/dashboard/ReportingTour";
85
87
  import { ScrollToTopFab } from "@/components/dashboard/ScrollToTopFab";
86
88
  import { ThemeToggle } from "@/components/dashboard/ThemeToggle";
87
89
  import { PageRefreshButton } from "@/components/dashboard/PageRefreshButton";
88
- import { AppShellRouteNav } from "@/components/dashboard/AppShellRouteNav";
90
+ import { AppShellToolbarRouteNav } from "@/components/dashboard/AppShellToolbarRouteNav";
89
91
  import {
90
92
  calendarDateKeyInTimeZone,
91
93
  readDashboardTimeZoneFromCfg,
@@ -102,6 +104,8 @@ import {
102
104
  } from "@/components/dashboard/TaskSessionLiveCard";
103
105
  import { useReportingInteractionState } from "@/components/dashboard/useReportingInteractionState";
104
106
 
107
+ type ReportingMainViewTab = "billing" | "rhythm" | "advanced";
108
+
105
109
  type LiveShape = { language?: string };
106
110
  type ReportingTaskInspectScope = {
107
111
  kind: "tag" | "project";
@@ -177,7 +181,7 @@ function reportingArchivedExcludedRichText(
177
181
  return (
178
182
  <>
179
183
  {template.slice(0, i)}
180
- <strong className="font-semibold text-amber-50 tabular-nums">
184
+ <strong className="font-semibold text-amber-900 tabular-nums dark:text-amber-50">
181
185
  {label}
182
186
  </strong>
183
187
  {template.slice(i + marker.length)}
@@ -268,7 +272,7 @@ function StackedTaskBars({
268
272
  className="flex w-8 shrink-0 flex-col items-center gap-1"
269
273
  >
270
274
  <div
271
- className="flex h-[100px] w-full flex-col justify-end rounded-t bg-zinc-800/80"
275
+ className="flex h-[100px] w-full flex-col justify-end rounded-t bg-zinc-200/90 dark:bg-zinc-800/80"
272
276
  title={`${label}: ${b} / ${a}`}
273
277
  >
274
278
  {donePx > 0 ? (
@@ -415,7 +419,7 @@ function ReportingFilteredBadge({
415
419
  }
416
420
  return (
417
421
  <span
418
- className="inline-flex shrink-0 items-center rounded border border-amber-500/35 bg-amber-950/40 px-1.5 py-px text-[0.65rem] font-semibold uppercase tracking-wide text-amber-200/90"
422
+ className="inline-flex shrink-0 items-center rounded border border-amber-400/50 bg-amber-100/90 px-1.5 py-px text-[0.65rem] font-semibold uppercase tracking-wide text-amber-900 dark:border-amber-500/35 dark:bg-amber-950/40 dark:text-amber-200/90"
419
423
  title={titleText}
420
424
  >
421
425
  {label}
@@ -432,6 +436,10 @@ function ReportingContent() {
432
436
  const [dateFrom, setDateFrom] = useState("");
433
437
  const [dateTo, setDateTo] = useState("");
434
438
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
439
+ /** Grille « temps par projet » : `work` = jetons @ (défaut) ; `personal` = temps `!` (`personalProject`). */
440
+ const [projectMinutesScope, setProjectMinutesScope] = useState<
441
+ "work" | "personal"
442
+ >("work");
435
443
  const [workspaceSnapBusy, setWorkspaceSnapBusy] = useState(false);
436
444
  const {
437
445
  weekStartsOn,
@@ -453,6 +461,8 @@ function ReportingContent() {
453
461
  portalReady,
454
462
  taskInspectModalRef,
455
463
  } = useReportingInteractionState<ReportingTaskInspectScope>();
464
+ const [reportingViewTab, setReportingViewTab] =
465
+ useState<ReportingMainViewTab>("billing");
456
466
 
457
467
  useEffect(() => {
458
468
  if (!taskInspectScope || !portalReady) {
@@ -559,6 +569,14 @@ function ReportingContent() {
559
569
  return await refresh({ routerInvalidate: true });
560
570
  }, [refresh]);
561
571
 
572
+ const postHeaderAction = useCallback(
573
+ async (body: Record<string, unknown>) => {
574
+ await postKronosysAction(body);
575
+ await refresh({ routerInvalidate: true });
576
+ },
577
+ [refresh],
578
+ );
579
+
562
580
  const reportLocale = lang === "fr" ? "fr-CA" : "en-CA";
563
581
  const tagDescriptions = (payload?.tagDescriptions ?? {}) as Record<
564
582
  string,
@@ -738,6 +756,7 @@ function ReportingContent() {
738
756
  dateToFilter,
739
757
  reportTimeZone,
740
758
  taskDefaultTagBucketEnabled,
759
+ projectMinutesScope,
741
760
  );
742
761
  }, [
743
762
  mergedSessions,
@@ -746,8 +765,14 @@ function ReportingContent() {
746
765
  dateToFilter,
747
766
  reportTimeZone,
748
767
  taskDefaultTagBucketEnabled,
768
+ projectMinutesScope,
749
769
  ]);
750
770
 
771
+ const projectNameCellToneClass =
772
+ projectMinutesScope === "personal"
773
+ ? "text-rose-800 dark:text-rose-200/90"
774
+ : "text-sky-800 dark:text-sky-200/90";
775
+
751
776
  const projectWeekCalendarRows = useMemo(
752
777
  () => buildProjectWeekCalendarRows(projectTaskMinutesByDay, weekStartsOn),
753
778
  [projectTaskMinutesByDay, weekStartsOn],
@@ -947,6 +972,13 @@ function ReportingContent() {
947
972
  if (!projectKeyLower) {
948
973
  continue;
949
974
  }
975
+ const tagLedgerPersonal = row.tagKey.trim().toLowerCase().startsWith("!");
976
+ if (projectMinutesScope === "work" && tagLedgerPersonal) {
977
+ continue;
978
+ }
979
+ if (projectMinutesScope === "personal" && !tagLedgerPersonal) {
980
+ continue;
981
+ }
950
982
  const bucketKey = `${row.weekStart}:::${projectKeyLower}`;
951
983
  const list = byWeekProject.get(bucketKey) ?? [];
952
984
  list.push(row);
@@ -961,7 +993,7 @@ function ReportingContent() {
961
993
  );
962
994
  }
963
995
  return byWeekProject;
964
- }, [tagWeekCalendarRowsVisible]);
996
+ }, [tagWeekCalendarRowsVisible, projectMinutesScope]);
965
997
 
966
998
  const projectCalendarWeekGroups = useMemo(() => {
967
999
  const byWeek = new Map<string, ProjectWeekCalendarRow[]>();
@@ -1132,7 +1164,7 @@ function ReportingContent() {
1132
1164
  return (
1133
1165
  <button
1134
1166
  type="button"
1135
- className={`${className} underline decoration-dotted underline-offset-2 hover:text-violet-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-400/60`}
1167
+ className={`${className} underline decoration-dotted underline-offset-2 hover:text-violet-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-violet-400/60 dark:hover:text-violet-200`}
1136
1168
  onClick={(event) => {
1137
1169
  const rect = event.currentTarget.getBoundingClientRect();
1138
1170
  openTaskInspectScope(scope, {
@@ -1334,6 +1366,66 @@ function ReportingContent() {
1334
1366
  tagWeekCalendarRows.length > 0 ||
1335
1367
  projectWeekCalendarRows.length > 0);
1336
1368
 
1369
+ const syncReportingTourTab = useCallback(
1370
+ (step: number) => {
1371
+ if (!reportingTourOpen) {
1372
+ return;
1373
+ }
1374
+ if (!hasReportingChartData) {
1375
+ if (step === 2) {
1376
+ setReportingViewTab("advanced");
1377
+ }
1378
+ return;
1379
+ }
1380
+ if (step <= 1) {
1381
+ setReportingViewTab("billing");
1382
+ } else if (step === 2) {
1383
+ setReportingViewTab("advanced");
1384
+ } else if (step === 3) {
1385
+ setReportingViewTab("rhythm");
1386
+ } else if (step <= 5) {
1387
+ setReportingViewTab("billing");
1388
+ }
1389
+ },
1390
+ [reportingTourOpen, hasReportingChartData],
1391
+ );
1392
+
1393
+ useEffect(() => {
1394
+ const applyHash = () => {
1395
+ if (typeof window === "undefined") {
1396
+ return;
1397
+ }
1398
+ const id = window.location.hash.slice(1);
1399
+ if (!id) {
1400
+ return;
1401
+ }
1402
+ const toAdvanced = new Set([
1403
+ "report-workspace-snapshot",
1404
+ "report-summary-kpis",
1405
+ "report-closure-by-kind",
1406
+ "report-loc-metrics",
1407
+ "report-daily-table",
1408
+ ]);
1409
+ const toRhythm = new Set([
1410
+ "report-chart-sessions",
1411
+ "report-chart-tasks",
1412
+ "report-chart-task-time",
1413
+ "report-chart-session-wall",
1414
+ ]);
1415
+ const toBilling = new Set(["report-tag-time", "report-projects"]);
1416
+ if (toAdvanced.has(id)) {
1417
+ setReportingViewTab("advanced");
1418
+ } else if (toRhythm.has(id)) {
1419
+ setReportingViewTab("rhythm");
1420
+ } else if (toBilling.has(id)) {
1421
+ setReportingViewTab("billing");
1422
+ }
1423
+ };
1424
+ applyHash();
1425
+ window.addEventListener("hashchange", applyHash);
1426
+ return () => window.removeEventListener("hashchange", applyHash);
1427
+ }, []);
1428
+
1337
1429
  useEffect(() => {
1338
1430
  if (!payload || !agg) {
1339
1431
  return;
@@ -1348,44 +1440,59 @@ function ReportingContent() {
1348
1440
  }
1349
1441
  }, [payload, agg, searchParams, stripReportingTourReplayParam]);
1350
1442
 
1351
- const reportingTocEntries = (() => {
1443
+ const reportingTocEntries = useMemo(() => {
1352
1444
  if (!payload) {
1353
1445
  return [] as { id: string; label: string }[];
1354
1446
  }
1355
1447
  const rows: { id: string; label: string }[] = [
1356
1448
  { id: "report-filters", label: t.filtersTitle },
1357
1449
  ];
1358
- if (trackCodeMetrics) {
1359
- rows.push({
1360
- id: "report-workspace-snapshot",
1361
- label: t.workspaceSnapshotTitle,
1362
- });
1363
- }
1364
1450
  if (!agg) {
1451
+ if (trackCodeMetrics) {
1452
+ rows.push({
1453
+ id: "report-workspace-snapshot",
1454
+ label: t.workspaceSnapshotTitle,
1455
+ });
1456
+ }
1365
1457
  return rows;
1366
1458
  }
1367
- rows.push({ id: "report-summary-kpis", label: t.tocSummaryKpis });
1368
- rows.push({ id: "report-closure-by-kind", label: t.tocClosureBreakdown });
1369
- if (trackCodeMetrics) {
1370
- rows.push({ id: "report-loc-metrics", label: t.tocLocSection });
1371
- }
1372
- if (!hasReportingChartData) {
1373
- return rows;
1374
- }
1375
- if (navigableWeekStarts.length > 1) {
1459
+ rows.push({ id: "reporting-view-tabs", label: t.tocViewTabs });
1460
+ if (hasReportingChartData && navigableWeekStarts.length > 1) {
1376
1461
  rows.push({ id: "report-week-nav", label: t.tocWeekNav });
1377
1462
  }
1378
1463
  rows.push(
1379
- { id: "report-chart-sessions", label: t.chartSessionsPerDay },
1380
- { id: "report-chart-tasks", label: t.chartTasksByStatusPerDay },
1381
- { id: "report-chart-task-time", label: t.chartTaskTimePerDay },
1382
- { id: "report-chart-session-wall", label: t.chartSessionWallPerDay },
1383
- { id: "report-daily-table", label: t.tocDailyTable },
1384
1464
  { id: "report-tag-time", label: t.tocTagTimeSection },
1385
1465
  { id: "report-projects", label: t.projectSectionTitle },
1386
1466
  );
1467
+ if (hasReportingChartData) {
1468
+ rows.push(
1469
+ { id: "report-chart-sessions", label: t.chartSessionsPerDay },
1470
+ { id: "report-chart-tasks", label: t.chartTasksByStatusPerDay },
1471
+ { id: "report-chart-task-time", label: t.chartTaskTimePerDay },
1472
+ { id: "report-chart-session-wall", label: t.chartSessionWallPerDay },
1473
+ { id: "report-daily-table", label: t.tocDailyTable },
1474
+ );
1475
+ }
1476
+ rows.push(
1477
+ { id: "report-summary-kpis", label: t.tocSummaryKpis },
1478
+ { id: "report-closure-by-kind", label: t.tocClosureBreakdown },
1479
+ );
1480
+ if (trackCodeMetrics) {
1481
+ rows.push({ id: "report-loc-metrics", label: t.tocLocSection });
1482
+ rows.push({
1483
+ id: "report-workspace-snapshot",
1484
+ label: t.workspaceSnapshotTitle,
1485
+ });
1486
+ }
1387
1487
  return rows;
1388
- })();
1488
+ }, [
1489
+ payload,
1490
+ agg,
1491
+ trackCodeMetrics,
1492
+ hasReportingChartData,
1493
+ navigableWeekStarts.length,
1494
+ t,
1495
+ ]);
1389
1496
 
1390
1497
  return (
1391
1498
  <div className="min-h-screen bg-zinc-100 text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100">
@@ -1414,46 +1521,61 @@ function ReportingContent() {
1414
1521
  </div>
1415
1522
  <AppShellHeaderSessionMeta payload={payload} dt={dt} />
1416
1523
  </div>
1417
- <div className="flex w-full justify-end">
1418
- <div className={appShellHeaderToolbarClassName}>
1419
- <AppShellHeaderWallClock lang={lang} dt={dt} />
1420
- <AppShellCommandCenterPlaceholder />
1421
- <AppShellRouteNav
1524
+ <AppShellHeaderToolbarLayout
1525
+ leading={
1526
+ <>
1527
+ <AppShellHeaderWallClock lang={lang} dt={dt} />
1528
+ <AppShellHeaderKronoFocus
1529
+ payload={payload}
1530
+ dt={dt}
1531
+ post={postHeaderAction}
1532
+ />
1533
+ <AppShellToolbarCommandCenter
1534
+ dt={dt}
1535
+ lang={lang}
1536
+ dashboardSessionNavId={dashboardSessionNavId}
1537
+ onManualRefresh={handleManualRefresh}
1538
+ />
1539
+ </>
1540
+ }
1541
+ nav={
1542
+ <AppShellToolbarRouteNav
1422
1543
  current="reporting"
1423
- labels={{
1424
- dashboard: t.dashboard,
1425
- reporting: t.reporting,
1426
- settings: t.settings,
1427
- logs: t.logs,
1428
- guide: t.guide,
1429
- }}
1544
+ labels={reportingNav(lang)}
1430
1545
  navAriaLabel={dt.appShellRouteNavAria}
1431
1546
  dashboardSessionId={dashboardSessionNavId}
1432
- reserveGlobalPauseSlot
1433
- />
1434
- <ThemeToggle lang={lang} />
1435
- <PageRefreshButton
1436
- title={dt.pageRefreshTitle}
1437
- ariaLabel={dt.pageRefreshAriaLabel}
1438
- inlineMessages={{
1439
- loading: dt.pageRefreshProgressLabel,
1440
- success: dt.pageRefreshDoneToast,
1441
- error: dt.pageRefreshFailedToast,
1442
- }}
1443
- onRefresh={handleManualRefresh}
1444
- />
1445
- <LanguageMenu
1446
1547
  lang={lang}
1447
- labelEn="English"
1448
- labelFr="Français"
1449
- menuHeading={lang === "fr" ? "Langue" : "Language"}
1450
- triggerAriaLabel={
1451
- lang === "fr" ? "Langue de l’interface" : "Interface language"
1452
- }
1453
- onSelect={(next) => void postLang(next)}
1548
+ dt={dt}
1454
1549
  />
1455
- </div>
1456
- </div>
1550
+ }
1551
+ trailing={
1552
+ <AppShellHeaderUtilityRibbon
1553
+ ariaLabel={dt.appShellUtilityToolbarGroupAria}
1554
+ >
1555
+ <ThemeToggle lang={lang} />
1556
+ <PageRefreshButton
1557
+ title={dt.pageRefreshTitle}
1558
+ ariaLabel={dt.pageRefreshAriaLabel}
1559
+ inlineMessages={{
1560
+ loading: dt.pageRefreshProgressLabel,
1561
+ success: dt.pageRefreshDoneToast,
1562
+ error: dt.pageRefreshFailedToast,
1563
+ }}
1564
+ onRefresh={handleManualRefresh}
1565
+ />
1566
+ <LanguageMenu
1567
+ lang={lang}
1568
+ labelEn="English"
1569
+ labelFr="Français"
1570
+ menuHeading={lang === "fr" ? "Langue" : "Language"}
1571
+ triggerAriaLabel={
1572
+ lang === "fr" ? "Langue de l’interface" : "Interface language"
1573
+ }
1574
+ onSelect={(next) => void postLang(next)}
1575
+ />
1576
+ </AppShellHeaderUtilityRibbon>
1577
+ }
1578
+ />
1457
1579
  </header>
1458
1580
 
1459
1581
  <div className="w-full px-5 py-8 sm:px-8 lg:px-10 xl:px-12">
@@ -1483,12 +1605,14 @@ function ReportingContent() {
1483
1605
 
1484
1606
  {error && (
1485
1607
  <div
1486
- className="mb-8 rounded-lg border border-red-900/60 bg-red-950/40 px-4 py-3 text-sm text-red-100"
1608
+ className="mb-8 rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-900 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-100"
1487
1609
  role="alert"
1488
1610
  >
1489
- <strong className="block text-red-200">API</strong>
1611
+ <strong className="block text-red-800 dark:text-red-200">
1612
+ API
1613
+ </strong>
1490
1614
  {headerApiError}
1491
- <pre className="mt-2 overflow-x-auto text-xs text-red-200/80">
1615
+ <pre className="mt-2 overflow-x-auto text-xs text-red-800/90 dark:text-red-200/80">
1492
1616
  {error}
1493
1617
  </pre>
1494
1618
  </div>
@@ -1511,10 +1635,10 @@ function ReportingContent() {
1511
1635
  />
1512
1636
  <section
1513
1637
  id="report-filters"
1514
- className="mb-10 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
1638
+ className="mb-10 scroll-mt-28 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
1515
1639
  >
1516
1640
  <div className="flex flex-wrap items-center gap-2 gap-y-2">
1517
- <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
1641
+ <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
1518
1642
  {t.filtersTitle}
1519
1643
  </h2>
1520
1644
  <InlineMetricHelpTrigger
@@ -1555,7 +1679,7 @@ function ReportingContent() {
1555
1679
  type="date"
1556
1680
  value={dateFrom}
1557
1681
  onChange={(e) => setDateFrom(e.target.value)}
1558
- className="rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-zinc-100"
1682
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 shadow-inner shadow-zinc-900/[0.03] dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:shadow-none"
1559
1683
  />
1560
1684
  </label>
1561
1685
  <label className="flex flex-col gap-1 text-xs text-zinc-500">
@@ -1564,12 +1688,12 @@ function ReportingContent() {
1564
1688
  type="date"
1565
1689
  value={dateTo}
1566
1690
  onChange={(e) => setDateTo(e.target.value)}
1567
- className="rounded-lg border border-zinc-700 bg-zinc-950 px-3 py-2 text-sm text-zinc-100"
1691
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-900 shadow-inner shadow-zinc-900/[0.03] dark:border-zinc-700 dark:bg-zinc-950 dark:text-zinc-100 dark:shadow-none"
1568
1692
  />
1569
1693
  </label>
1570
1694
  <button
1571
1695
  type="button"
1572
- className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800"
1696
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-zinc-800"
1573
1697
  onClick={() => {
1574
1698
  const p = reportingPresetDay();
1575
1699
  setDateFrom(p.from);
@@ -1580,7 +1704,7 @@ function ReportingContent() {
1580
1704
  </button>
1581
1705
  <button
1582
1706
  type="button"
1583
- className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800"
1707
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-zinc-800"
1584
1708
  onClick={() => {
1585
1709
  setDateFrom("");
1586
1710
  setDateTo("");
@@ -1600,7 +1724,7 @@ function ReportingContent() {
1600
1724
  <div className="flex flex-wrap gap-2">
1601
1725
  <button
1602
1726
  type="button"
1603
- className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800"
1727
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:border-zinc-400 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:border-zinc-500 dark:hover:bg-zinc-800"
1604
1728
  onClick={() => {
1605
1729
  const p = reportingPresetDay();
1606
1730
  setDateFrom(p.from);
@@ -1611,7 +1735,7 @@ function ReportingContent() {
1611
1735
  </button>
1612
1736
  <button
1613
1737
  type="button"
1614
- className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800"
1738
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:border-zinc-400 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:border-zinc-500 dark:hover:bg-zinc-800"
1615
1739
  onClick={() => {
1616
1740
  const p = reportingPresetWeek();
1617
1741
  setDateFrom(p.from);
@@ -1622,7 +1746,7 @@ function ReportingContent() {
1622
1746
  </button>
1623
1747
  <button
1624
1748
  type="button"
1625
- className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800"
1749
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:border-zinc-400 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:border-zinc-500 dark:hover:bg-zinc-800"
1626
1750
  onClick={() => {
1627
1751
  const p = reportingPresetMonth();
1628
1752
  setDateFrom(p.from);
@@ -1633,7 +1757,7 @@ function ReportingContent() {
1633
1757
  </button>
1634
1758
  <button
1635
1759
  type="button"
1636
- className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800"
1760
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:border-zinc-400 hover:bg-zinc-100 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:border-zinc-500 dark:hover:bg-zinc-800"
1637
1761
  onClick={() => {
1638
1762
  const p = reportingPresetYear();
1639
1763
  setDateFrom(p.from);
@@ -1669,8 +1793,8 @@ function ReportingContent() {
1669
1793
  onClick={() => toggleTag(tag)}
1670
1794
  className={`rounded-full border px-2.5 py-1 text-xs font-medium transition-colors ${
1671
1795
  on
1672
- ? "border-violet-500 bg-violet-600/30 text-violet-100"
1673
- : "border-zinc-600 bg-zinc-950 text-zinc-400 hover:border-zinc-500"
1796
+ ? "border-violet-500 bg-violet-200/90 text-violet-900 dark:bg-violet-600/30 dark:text-violet-100"
1797
+ : "border-zinc-300 bg-white text-zinc-600 hover:border-zinc-400 hover:bg-zinc-50 dark:border-zinc-600 dark:bg-zinc-950 dark:text-zinc-400 dark:hover:border-zinc-500"
1674
1798
  }`}
1675
1799
  >
1676
1800
  {formatTagDisplay(tag)}
@@ -1684,7 +1808,7 @@ function ReportingContent() {
1684
1808
  <button
1685
1809
  type="button"
1686
1810
  disabled={!reportingFiltersActive}
1687
- className="rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:border-zinc-500 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-40"
1811
+ className="rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:border-zinc-400 hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:border-zinc-500 dark:hover:bg-zinc-800"
1688
1812
  onClick={() => {
1689
1813
  setDateFrom("");
1690
1814
  setDateTo("");
@@ -1696,14 +1820,14 @@ function ReportingContent() {
1696
1820
  </div>
1697
1821
  </section>
1698
1822
 
1699
- {trackCodeMetrics ? (
1823
+ {trackCodeMetrics && !agg ? (
1700
1824
  <section
1701
1825
  id="report-workspace-snapshot"
1702
- className="mb-10 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
1826
+ className="mb-10 scroll-mt-28 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
1703
1827
  >
1704
1828
  <div className="flex flex-wrap items-start justify-between gap-3">
1705
1829
  <div className="flex min-w-0 max-w-full items-center gap-1">
1706
- <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
1830
+ <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
1707
1831
  {t.workspaceSnapshotTitle}
1708
1832
  </h2>
1709
1833
  <InlineMetricHelpTrigger
@@ -1714,7 +1838,7 @@ function ReportingContent() {
1714
1838
  <button
1715
1839
  type="button"
1716
1840
  disabled={workspaceSnapBusy}
1717
- className="shrink-0 rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-300 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-50"
1841
+ className="shrink-0 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-zinc-800"
1718
1842
  onClick={() => void refreshWorkspaceSnapshot()}
1719
1843
  >
1720
1844
  {workspaceSnapBusy
@@ -1755,7 +1879,7 @@ function ReportingContent() {
1755
1879
  body={t.metricHelpWsTotalLinesBody}
1756
1880
  />
1757
1881
  </div>
1758
- <div className="tabular-nums text-lg font-semibold text-zinc-100">
1882
+ <div className="tabular-nums text-lg font-semibold text-zinc-900 dark:text-zinc-100">
1759
1883
  {ws.totalLines}
1760
1884
  </div>
1761
1885
  </div>
@@ -1769,7 +1893,7 @@ function ReportingContent() {
1769
1893
  body={t.metricHelpWsFileCountBody}
1770
1894
  />
1771
1895
  </div>
1772
- <div className="tabular-nums text-lg font-semibold text-zinc-100">
1896
+ <div className="tabular-nums text-lg font-semibold text-zinc-900 dark:text-zinc-100">
1773
1897
  {ws.fileCount}
1774
1898
  </div>
1775
1899
  </div>
@@ -1779,7 +1903,7 @@ function ReportingContent() {
1779
1903
  ) : (
1780
1904
  <div className="mt-4 overflow-x-auto">
1781
1905
  <table className="w-full min-w-[22rem] text-left text-sm">
1782
- <thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
1906
+ <thead className="border-b border-zinc-200 text-xs uppercase text-zinc-500 dark:border-zinc-800">
1783
1907
  <tr>
1784
1908
  <th className="py-2 pr-3 font-medium">
1785
1909
  <div className="flex min-h-5 items-center gap-0.5">
@@ -1823,17 +1947,17 @@ function ReportingContent() {
1823
1947
  {ws.byLanguage.map((row) => (
1824
1948
  <tr
1825
1949
  key={row.languageId}
1826
- className="border-b border-zinc-800/80 last:border-0"
1950
+ className="border-b border-zinc-200/90 last:border-0 dark:border-zinc-800/80"
1827
1951
  >
1828
1952
  <td className="py-2 pr-3 font-mono text-xs text-zinc-400">
1829
1953
  {row.languageId}
1830
1954
  </td>
1831
- <td className="py-2 pr-3 tabular-nums text-zinc-200">
1955
+ <td className="py-2 pr-3 tabular-nums text-zinc-800 dark:text-zinc-200">
1832
1956
  {row.lines}
1833
1957
  </td>
1834
1958
  <td className="py-2">
1835
1959
  <div className="flex items-center gap-2">
1836
- <div className="h-2 min-w-[4rem] flex-1 overflow-hidden rounded bg-zinc-800">
1960
+ <div className="h-2 min-w-[4rem] flex-1 overflow-hidden rounded bg-zinc-200 dark:bg-zinc-800">
1837
1961
  <div
1838
1962
  className="h-full rounded bg-sky-600/85"
1839
1963
  style={{
@@ -1859,7 +1983,7 @@ function ReportingContent() {
1859
1983
  );
1860
1984
  }
1861
1985
  return (
1862
- <p className="mt-4 text-sm text-amber-200/90">
1986
+ <p className="mt-4 text-sm text-amber-800 dark:text-amber-200/90">
1863
1987
  {ws.reason === "no_workspace"
1864
1988
  ? t.workspaceSnapshotNoWorkspace
1865
1989
  : ws.reason === "empty"
@@ -1871,7 +1995,7 @@ function ReportingContent() {
1871
1995
  {archivedExcludedTaskMinutes > 1e-9 ? (
1872
1996
  <div
1873
1997
  role="status"
1874
- className="rounded-lg border border-amber-600/45 bg-amber-950/30 px-3 py-2.5 text-sm leading-relaxed text-amber-100/95 dark:border-amber-500/40"
1998
+ className="rounded-lg border border-amber-500/40 bg-amber-50 px-3 py-2.5 text-sm leading-relaxed text-amber-950 dark:border-amber-500/40 dark:bg-amber-950/30 dark:text-amber-100/95"
1875
1999
  >
1876
2000
  {reportingArchivedExcludedRichText(
1877
2001
  t.reportingArchivedExcludedAside,
@@ -1884,259 +2008,156 @@ function ReportingContent() {
1884
2008
 
1885
2009
  {agg && (
1886
2010
  <>
1887
- <section
1888
- id="report-summary-kpis"
1889
- className="mb-10 scroll-mt-28 flex flex-col gap-3"
2011
+ <div
2012
+ id="reporting-view-tabs"
2013
+ className="mb-8 flex flex-wrap gap-2 border-b border-zinc-200 pb-4 dark:border-zinc-700/80"
2014
+ role="tablist"
2015
+ aria-label={t.reportingViewTabGroupAria}
1890
2016
  >
1891
- <div className="flex flex-wrap items-center gap-2">
1892
- <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
1893
- {t.tocSummaryKpis}
1894
- </h2>
1895
- <ReportingFilteredBadge
1896
- active={reportingFiltersActive}
1897
- label={t.sectionFilteredBadge}
1898
- titleText={t.sectionFilteredBadgeTitle}
1899
- />
1900
- </div>
1901
- <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
1902
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
1903
- <div className="flex min-h-5 items-center gap-0.5">
1904
- <span className="text-xs uppercase text-zinc-500">
1905
- {t.summarySessionsInRange}
1906
- </span>
1907
- <InlineMetricHelpTrigger
1908
- ariaLabel={t.metricHelpSessionsAria}
1909
- body={t.metricHelpSessionsBody}
1910
- />
1911
- </div>
1912
- <div className="mt-1 text-2xl font-semibold tabular-nums">
1913
- {agg.sessionCountContributing}
1914
- </div>
1915
- </div>
1916
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
1917
- <div className="flex min-h-5 items-center gap-0.5">
1918
- <span className="text-xs uppercase text-zinc-500">
1919
- {t.summaryTaskEvents}
1920
- </span>
1921
- <InlineMetricHelpTrigger
1922
- ariaLabel={t.metricHelpTaskRowsAria}
1923
- body={t.metricHelpTaskRowsBody}
1924
- />
1925
- </div>
1926
- <div className="mt-1 text-2xl font-semibold tabular-nums">
1927
- {agg.taskCountContributing}
1928
- </div>
1929
- </div>
1930
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
1931
- <div className="flex min-h-5 items-center gap-0.5">
1932
- <span className="text-xs uppercase text-zinc-500">
1933
- {t.summaryKronoFocusCompleted}
1934
- </span>
1935
- <InlineMetricHelpTrigger
1936
- align="end"
1937
- ariaLabel={t.metricHelpKronoFocusCompletedAria}
1938
- body={t.metricHelpKronoFocusCompletedBody}
1939
- />
1940
- </div>
1941
- <div className="mt-1 text-2xl font-semibold tabular-nums">
1942
- {agg.kronoFocusSessionsCompleted}
1943
- </div>
1944
- </div>
1945
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
1946
- <div className="flex min-h-5 items-center gap-0.5">
1947
- <span className="text-xs uppercase text-zinc-500">
1948
- {t.summaryKronoFocusTasksUsed}
1949
- </span>
1950
- <InlineMetricHelpTrigger
1951
- ariaLabel={t.metricHelpKronoFocusTasksUsedAria}
1952
- body={t.metricHelpKronoFocusTasksUsedBody}
1953
- />
1954
- </div>
1955
- <div className="mt-1 text-2xl font-semibold tabular-nums">
1956
- {agg.kronoFocusTasksUsedCount}
1957
- </div>
1958
- </div>
1959
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
1960
- <div className="flex min-h-5 items-center gap-0.5">
1961
- <span className="text-xs uppercase text-zinc-500">
1962
- {t.summaryKronoFocusCycles}
1963
- </span>
1964
- <InlineMetricHelpTrigger
1965
- ariaLabel={t.metricHelpKronoFocusCyclesAria}
1966
- body={t.metricHelpKronoFocusCyclesBody}
1967
- />
1968
- </div>
1969
- <div className="mt-1 text-2xl font-semibold tabular-nums">
1970
- {agg.kronoFocusTaskCyclesSum}
1971
- </div>
1972
- </div>
1973
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
1974
- <div className="flex min-h-5 items-center gap-0.5">
1975
- <span className="text-xs uppercase text-zinc-500">
1976
- {t.summaryTaskTimeRecorded}
1977
- </span>
1978
- <InlineMetricHelpTrigger
1979
- align="end"
1980
- ariaLabel={t.metricHelpTaskTimeAria}
1981
- body={t.metricHelpTaskTimeBody}
1982
- />
1983
- </div>
1984
- <div className="mt-1 text-2xl font-semibold tabular-nums">
1985
- {formatDuration(agg.taskMinutesTotal)}
1986
- </div>
1987
- </div>
1988
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
1989
- <div className="flex min-h-5 items-center gap-0.5">
1990
- <span className="text-xs uppercase text-zinc-500">
1991
- {t.summarySessionCoding}
1992
- </span>
1993
- <InlineMetricHelpTrigger
1994
- ariaLabel={t.metricHelpSessionCodingAria}
1995
- body={t.metricHelpSessionCodingBody}
1996
- />
1997
- </div>
1998
- <div className="mt-1 text-2xl font-semibold tabular-nums">
1999
- {formatDuration(agg.sessionCodingMinutesTotal)}
2000
- </div>
2001
- </div>
2002
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
2003
- <div className="flex min-h-5 items-center gap-0.5">
2004
- <span className="text-xs uppercase text-zinc-500">
2005
- {t.summarySessionActive}
2006
- </span>
2007
- <InlineMetricHelpTrigger
2008
- ariaLabel={t.metricHelpSessionActiveAria}
2009
- body={t.metricHelpSessionActiveBody}
2010
- />
2011
- </div>
2012
- <div className="mt-1 text-2xl font-semibold tabular-nums">
2013
- {formatDuration(agg.sessionActiveMinutesTotal)}
2014
- </div>
2015
- </div>
2016
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
2017
- <div className="flex min-h-5 items-center gap-0.5">
2018
- <span className="text-xs uppercase text-zinc-500">
2019
- {t.summarySessionWallClock}
2020
- </span>
2021
- <InlineMetricHelpTrigger
2022
- align="end"
2023
- ariaLabel={t.metricHelpSessionWallSummaryAria}
2024
- body={t.metricHelpSessionWallSummaryBody}
2025
- />
2026
- </div>
2027
- <div className="mt-1 text-2xl font-semibold tabular-nums">
2028
- {formatDuration(agg.sessionWallClockMinutesTotal)}
2029
- </div>
2030
- </div>
2031
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
2032
- <div className="flex min-h-5 items-center gap-0.5">
2033
- <span className="text-xs uppercase text-zinc-500">
2034
- {t.summaryAssiduityWithReference}
2035
- </span>
2036
- <InlineMetricHelpTrigger
2037
- align="end"
2038
- ariaLabel={t.metricHelpAssiduityRefAria}
2039
- body={t.metricHelpAssiduityRefBody}
2040
- />
2041
- </div>
2042
- <div className="mt-1 text-2xl font-semibold tabular-nums">
2043
- {agg.assiduityReferenceSessionCount}
2044
- </div>
2045
- </div>
2046
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
2047
- <div className="flex min-h-5 items-center gap-0.5">
2048
- <span className="text-xs uppercase text-zinc-500">
2049
- {t.summaryAssiduityLateSessions}
2050
- </span>
2051
- <InlineMetricHelpTrigger
2052
- align="end"
2053
- ariaLabel={t.metricHelpAssiduityLateCountAria}
2054
- body={t.metricHelpAssiduityLateCountBody}
2055
- />
2056
- </div>
2057
- <div className="mt-1 text-2xl font-semibold tabular-nums">
2058
- {agg.assiduityLateSessionCount}
2059
- </div>
2060
- </div>
2061
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
2062
- <div className="flex min-h-5 items-center gap-0.5">
2063
- <span className="text-xs uppercase text-zinc-500">
2064
- {t.summaryAssiduityLateTotal}
2065
- </span>
2066
- <InlineMetricHelpTrigger
2067
- align="end"
2068
- ariaLabel={t.metricHelpAssiduityLateTotalAria}
2069
- body={t.metricHelpAssiduityLateTotalBody}
2070
- />
2071
- </div>
2072
- <div className="mt-1 text-2xl font-semibold tabular-nums">
2073
- {formatDuration(agg.assiduityLateMinutesTotal)}
2074
- </div>
2075
- </div>
2076
- <div className="rounded-xl border border-zinc-800 bg-zinc-900/50 p-4">
2077
- <div className="flex min-h-5 items-center gap-0.5">
2078
- <span className="text-xs uppercase text-zinc-500">
2079
- {t.summaryAssiduityAvgLateWhenLate}
2080
- </span>
2081
- <InlineMetricHelpTrigger
2082
- align="end"
2083
- ariaLabel={t.metricHelpAssiduityAvgLateAria}
2084
- body={t.metricHelpAssiduityAvgLateBody}
2085
- />
2086
- </div>
2087
- <div className="mt-1 text-2xl font-semibold tabular-nums">
2088
- {agg.assiduityAverageLateMinutesWhenLate == null
2089
- ? "—"
2090
- : formatDuration(
2091
- agg.assiduityAverageLateMinutesWhenLate,
2092
- )}
2093
- </div>
2094
- </div>
2095
- </div>
2096
- <div
2097
- id="report-closure-by-kind"
2098
- className="mt-4 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4"
2017
+ <button
2018
+ type="button"
2019
+ role="tab"
2020
+ id="reporting-tab-billing"
2021
+ aria-selected={reportingViewTab === "billing"}
2022
+ className={`rounded-lg border px-3 py-2 text-sm font-medium transition ${
2023
+ reportingViewTab === "billing"
2024
+ ? "border-violet-500 bg-violet-100 text-violet-900 dark:bg-violet-950/40 dark:text-violet-100"
2025
+ : "border-zinc-300 bg-zinc-50 text-zinc-600 hover:border-zinc-400 hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-600 dark:bg-zinc-900/60 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200"
2026
+ }`}
2027
+ onClick={() => setReportingViewTab("billing")}
2028
+ >
2029
+ {t.reportingViewTabBilling}
2030
+ </button>
2031
+ <button
2032
+ type="button"
2033
+ role="tab"
2034
+ id="reporting-tab-rhythm"
2035
+ aria-selected={reportingViewTab === "rhythm"}
2036
+ className={`rounded-lg border px-3 py-2 text-sm font-medium transition ${
2037
+ reportingViewTab === "rhythm"
2038
+ ? "border-violet-500 bg-violet-100 text-violet-900 dark:bg-violet-950/40 dark:text-violet-100"
2039
+ : "border-zinc-300 bg-zinc-50 text-zinc-600 hover:border-zinc-400 hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-600 dark:bg-zinc-900/60 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200"
2040
+ }`}
2041
+ onClick={() => setReportingViewTab("rhythm")}
2042
+ >
2043
+ {t.reportingViewTabRhythm}
2044
+ </button>
2045
+ <button
2046
+ type="button"
2047
+ role="tab"
2048
+ id="reporting-tab-advanced"
2049
+ aria-selected={reportingViewTab === "advanced"}
2050
+ className={`rounded-lg border px-3 py-2 text-sm font-medium transition ${
2051
+ reportingViewTab === "advanced"
2052
+ ? "border-violet-500 bg-violet-100 text-violet-900 dark:bg-violet-950/40 dark:text-violet-100"
2053
+ : "border-zinc-300 bg-zinc-50 text-zinc-600 hover:border-zinc-400 hover:bg-zinc-100 hover:text-zinc-900 dark:border-zinc-600 dark:bg-zinc-900/60 dark:text-zinc-400 dark:hover:border-zinc-500 dark:hover:text-zinc-200"
2054
+ }`}
2055
+ onClick={() => setReportingViewTab("advanced")}
2056
+ >
2057
+ {t.reportingViewTabAdvanced}
2058
+ </button>
2059
+ </div>
2060
+ {hasReportingChartData && navigableWeekStarts.length > 1 ? (
2061
+ <section
2062
+ id="report-week-nav"
2063
+ className="mb-8 scroll-mt-28 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-5"
2064
+ aria-label={t.weekNavAriaLabel}
2099
2065
  >
2100
- <div className="mb-3 flex flex-wrap items-center gap-2">
2101
- <h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
2102
- {t.closureBreakdownTitle}
2103
- </h3>
2066
+ <div className="mb-2 flex flex-wrap items-center gap-2">
2067
+ <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
2068
+ {t.tocWeekNav}
2069
+ </h2>
2104
2070
  <ReportingFilteredBadge
2105
2071
  active={reportingFiltersActive}
2106
2072
  label={t.sectionFilteredBadge}
2107
2073
  titleText={t.sectionFilteredBadgeTitle}
2108
2074
  />
2109
- <InlineMetricHelpTrigger
2110
- ariaLabel={t.metricHelpClosureBreakdownAria}
2111
- body={t.metricHelpClosureBreakdownBody}
2112
- />
2113
2075
  </div>
2114
- <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
2115
- {REPORTING_SESSION_CLOSURE_DISPLAY_ORDER.map(
2116
- (closureKey) => {
2117
- const count =
2118
- agg.sessionCountByClosureKind[closureKey] ?? 0;
2119
- return (
2120
- <div
2121
- key={closureKey}
2122
- className="rounded-lg border border-zinc-800/80 bg-zinc-950/40 p-3"
2123
- >
2124
- <div className="text-xs uppercase text-zinc-500">
2125
- {reportingClosureKindLabels[closureKey]}
2126
- </div>
2127
- <div className="mt-1 text-xl font-semibold tabular-nums text-zinc-100">
2128
- {count}
2129
- </div>
2130
- </div>
2131
- );
2132
- },
2133
- )}
2076
+ <div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between">
2077
+ <p className="max-w-xl text-xs text-zinc-500">
2078
+ {t.weekNavHelpHint}
2079
+ </p>
2080
+ <div className="flex flex-wrap items-center justify-center gap-2 sm:justify-end">
2081
+ <InlineMetricHelpTrigger
2082
+ ariaLabel={t.metricHelpWeekNavAria}
2083
+ body={t.metricHelpWeekNavBody}
2084
+ />
2085
+ <button
2086
+ type="button"
2087
+ className="inline-flex items-center gap-1 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-800 hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-200 dark:hover:bg-zinc-800"
2088
+ disabled={chartWeekNavIndexSafe <= 0}
2089
+ aria-label={t.weekNavPrev}
2090
+ onClick={() =>
2091
+ setChartWeekNavIndex((i) => {
2092
+ const cur =
2093
+ i < 0 ? navigableWeekStarts.length - 1 : i;
2094
+ return Math.max(0, cur - 1);
2095
+ })
2096
+ }
2097
+ >
2098
+ <ChevronLeft
2099
+ className="h-4 w-4 shrink-0"
2100
+ aria-hidden
2101
+ />
2102
+ {t.weekNavPrev}
2103
+ </button>
2104
+ <span className="min-w-[10rem] px-2 text-center text-sm font-medium tabular-nums text-zinc-900 dark:text-zinc-100">
2105
+ {formatWeekRangeLabel(
2106
+ navigableWeekStarts[chartWeekNavIndexSafe],
2107
+ reportLocale,
2108
+ )}
2109
+ </span>
2110
+ <button
2111
+ type="button"
2112
+ className="inline-flex items-center gap-1 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-800 hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-40 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-200 dark:hover:bg-zinc-800"
2113
+ disabled={
2114
+ chartWeekNavIndexSafe >=
2115
+ navigableWeekStarts.length - 1
2116
+ }
2117
+ aria-label={t.weekNavNext}
2118
+ onClick={() =>
2119
+ setChartWeekNavIndex((i) => {
2120
+ const cur =
2121
+ i < 0 ? navigableWeekStarts.length - 1 : i;
2122
+ return Math.min(
2123
+ navigableWeekStarts.length - 1,
2124
+ cur + 1,
2125
+ );
2126
+ })
2127
+ }
2128
+ >
2129
+ {t.weekNavNext}
2130
+ <ChevronRight
2131
+ className="h-4 w-4 shrink-0"
2132
+ aria-hidden
2133
+ />
2134
+ </button>
2135
+ </div>
2134
2136
  </div>
2135
- </div>
2137
+ </section>
2138
+ ) : null}
2139
+ {!hasReportingChartData ? (
2140
+ <p className="mb-6 text-sm text-zinc-500">
2141
+ {t.noRowsInRange}
2142
+ </p>
2143
+ ) : null}
2144
+ <div
2145
+ id="report-view-panel-billing"
2146
+ role="tabpanel"
2147
+ aria-labelledby="reporting-tab-billing"
2148
+ className={
2149
+ reportingViewTab !== "billing"
2150
+ ? "hidden"
2151
+ : "mb-10 space-y-6"
2152
+ }
2153
+ >
2154
+ <p className="text-sm leading-relaxed text-zinc-600 dark:text-zinc-400">
2155
+ {t.reportingViewBillingIntro}
2156
+ </p>
2136
2157
  {archivedExcludedTaskMinutes > 1e-9 ? (
2137
2158
  <div
2138
2159
  role="status"
2139
- className="rounded-lg border border-amber-600/45 bg-amber-950/30 px-3 py-2.5 text-sm leading-relaxed text-amber-100/95 dark:border-amber-500/40"
2160
+ className="rounded-lg border border-amber-500/40 bg-amber-50 px-3 py-2.5 text-sm leading-relaxed text-amber-950 dark:border-amber-500/40 dark:bg-amber-950/30 dark:text-amber-100/95"
2140
2161
  >
2141
2162
  {reportingArchivedExcludedRichText(
2142
2163
  t.reportingArchivedExcludedAside,
@@ -2144,76 +2165,29 @@ function ReportingContent() {
2144
2165
  )}
2145
2166
  </div>
2146
2167
  ) : null}
2147
- </section>
2148
-
2149
- {trackCodeMetrics ? (
2150
- <section
2151
- id="report-loc-metrics"
2152
- className="mb-10 scroll-mt-28 space-y-4 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
2153
- >
2154
- <div className="flex flex-wrap items-center gap-2">
2155
- <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
2156
- {t.tocLocSection}
2157
- </h2>
2158
- <ReportingFilteredBadge
2159
- active={reportingFiltersActive}
2160
- label={t.sectionFilteredBadge}
2161
- titleText={t.sectionFilteredBadgeTitle}
2168
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:max-w-md">
2169
+ <div className="flex min-h-5 items-center gap-0.5">
2170
+ <span className="text-xs uppercase text-zinc-500">
2171
+ {t.summaryTaskTimeRecorded}
2172
+ </span>
2173
+ <InlineMetricHelpTrigger
2174
+ ariaLabel={t.metricHelpTaskTimeAria}
2175
+ body={t.metricHelpTaskTimeBody}
2162
2176
  />
2163
2177
  </div>
2164
- <p className="text-xs text-zinc-500">
2165
- {t.locMetricsHint}
2166
- </p>
2167
- <div className="grid gap-3 sm:grid-cols-3">
2168
- <div className="rounded-lg border border-zinc-800/80 bg-zinc-950/40 p-3">
2169
- <div className="flex min-h-5 items-center gap-0.5">
2170
- <span className="text-xs uppercase text-zinc-500">
2171
- {t.summaryLinesWrittenTotal}
2172
- </span>
2173
- <InlineMetricHelpTrigger
2174
- ariaLabel={t.metricHelpLinesTotalAria}
2175
- body={t.metricHelpLinesTotalBody}
2176
- />
2177
- </div>
2178
- <div className="mt-1 text-xl font-semibold tabular-nums text-zinc-100">
2179
- {agg.linesWrittenTotalSum}
2180
- </div>
2181
- </div>
2182
- <div className="rounded-lg border border-zinc-800/80 bg-zinc-950/40 p-3">
2183
- <div className="flex min-h-5 items-center gap-0.5">
2184
- <span className="text-xs uppercase text-zinc-500">
2185
- {t.summaryLinesWrittenHuman}
2186
- </span>
2187
- <InlineMetricHelpTrigger
2188
- ariaLabel={t.metricHelpLinesHumanAria}
2189
- body={t.metricHelpLinesHumanBody}
2190
- />
2191
- </div>
2192
- <div className="mt-1 text-xl font-semibold tabular-nums text-emerald-400/90">
2193
- {agg.linesWrittenHumanSum}
2194
- </div>
2195
- </div>
2196
- <div className="rounded-lg border border-zinc-800/80 bg-zinc-950/40 p-3">
2197
- <div className="flex min-h-5 items-center gap-0.5">
2198
- <span className="text-xs uppercase text-zinc-500">
2199
- {t.summaryLinesWrittenAi}
2200
- </span>
2201
- <InlineMetricHelpTrigger
2202
- align="end"
2203
- ariaLabel={t.metricHelpLinesAiAria}
2204
- body={t.metricHelpLinesAiBody}
2205
- />
2206
- </div>
2207
- <div className="mt-1 text-xl font-semibold tabular-nums text-violet-400/90">
2208
- {agg.linesWrittenAiSum}
2209
- </div>
2210
- </div>
2178
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
2179
+ {formatDuration(agg.taskMinutesTotal)}
2211
2180
  </div>
2212
- <div className="grid gap-6 lg:grid-cols-2">
2213
- <div>
2181
+ </div>
2182
+ {hasReportingChartData ? (
2183
+ <>
2184
+ <section
2185
+ id="report-tag-time"
2186
+ className="mt-10 scroll-mt-28 space-y-8 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
2187
+ >
2214
2188
  <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2215
- <h3 className="text-sm font-semibold text-zinc-200">
2216
- {t.locByLanguageSectionTitle}
2189
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
2190
+ {t.tagTimeSectionTitle}
2217
2191
  </h3>
2218
2192
  <ReportingFilteredBadge
2219
2193
  active={reportingFiltersActive}
@@ -2221,679 +2195,166 @@ function ReportingContent() {
2221
2195
  titleText={t.sectionFilteredBadgeTitle}
2222
2196
  />
2223
2197
  <InlineMetricHelpTrigger
2224
- ariaLabel={t.metricHelpLocByLangTitleAria}
2225
- body={t.metricHelpLocByLangTitleBody}
2198
+ ariaLabel={t.metricHelpTagTimeSectionAria}
2199
+ body={t.metricHelpTagTimeSectionBody}
2226
2200
  />
2227
2201
  </div>
2228
- {agg.locByLanguageMerged.length === 0 ? (
2229
- <p className="mt-3 text-sm text-zinc-500">—</p>
2230
- ) : (
2231
- <table className="mt-3 w-full min-w-[12rem] text-left text-sm">
2232
- <thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
2233
- <tr>
2234
- <th className="py-2 pr-3 font-medium">
2235
- <div className="flex min-h-5 items-center gap-0.5">
2236
- <span>{t.locByLanguageColLang}</span>
2237
- <InlineMetricHelpTrigger
2238
- ariaLabel={
2239
- t.metricHelpAggLocColLangAria
2240
- }
2241
- body={t.metricHelpAggLocColLangBody}
2242
- />
2243
- </div>
2244
- </th>
2245
- <th className="py-2 font-medium tabular-nums">
2246
- <div className="flex min-h-5 items-center gap-0.5">
2247
- <span>{t.locByLanguageColLines}</span>
2248
- <InlineMetricHelpTrigger
2249
- align="end"
2250
- ariaLabel={
2251
- t.metricHelpAggLocColLinesAria
2252
- }
2253
- body={t.metricHelpAggLocColLinesBody}
2254
- />
2255
- </div>
2256
- </th>
2257
- </tr>
2258
- </thead>
2259
- <tbody>
2260
- {agg.locByLanguageMerged.map(([lang, n]) => (
2261
- <tr
2262
- key={lang}
2263
- className="border-b border-zinc-800/80 last:border-0"
2264
- >
2265
- <td className="py-2 pr-3 font-mono text-xs text-zinc-400">
2266
- {lang}
2267
- </td>
2268
- <td className="py-2 tabular-nums text-zinc-200">
2269
- {n}
2270
- </td>
2271
- </tr>
2272
- ))}
2273
- </tbody>
2274
- </table>
2275
- )}
2276
- </div>
2277
- <div>
2278
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2279
- <h3 className="text-sm font-semibold text-zinc-200">
2280
- {t.codingSignalsSectionTitle}
2281
- </h3>
2282
- <ReportingFilteredBadge
2283
- active={reportingFiltersActive}
2284
- label={t.sectionFilteredBadge}
2285
- titleText={t.sectionFilteredBadgeTitle}
2286
- />
2287
- <InlineMetricHelpTrigger
2288
- ariaLabel={t.metricHelpCodingSignalsTitleAria}
2289
- body={t.metricHelpCodingSignalsTitleBody}
2290
- />
2291
- </div>
2292
- {agg.codingSignalsByLanguageMerged.length === 0 ? (
2293
- <p className="mt-3 text-sm text-zinc-500">—</p>
2294
- ) : (
2295
- <table className="mt-3 w-full min-w-[12rem] text-left text-sm">
2296
- <thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
2297
- <tr>
2298
- <th className="py-2 pr-3 font-medium">
2299
- <div className="flex min-h-5 items-center gap-0.5">
2300
- <span>{t.codingSignalsColLang}</span>
2301
- <InlineMetricHelpTrigger
2302
- ariaLabel={
2303
- t.metricHelpAggSigColLangAria
2304
- }
2305
- body={t.metricHelpAggSigColLangBody}
2306
- />
2307
- </div>
2308
- </th>
2309
- <th className="py-2 font-medium tabular-nums">
2310
- <div className="flex min-h-5 items-center gap-0.5">
2311
- <span>{t.codingSignalsColCount}</span>
2312
- <InlineMetricHelpTrigger
2313
- align="end"
2314
- ariaLabel={
2315
- t.metricHelpAggSigColCountAria
2316
- }
2317
- body={t.metricHelpAggSigColCountBody}
2318
- />
2319
- </div>
2320
- </th>
2321
- </tr>
2322
- </thead>
2323
- <tbody>
2324
- {agg.codingSignalsByLanguageMerged.map(
2325
- ([lang, n]) => (
2326
- <tr
2327
- key={lang}
2328
- className="border-b border-zinc-800/80 last:border-0"
2329
- >
2330
- <td className="py-2 pr-3 font-mono text-xs text-zinc-400">
2331
- {lang}
2332
- </td>
2333
- <td className="py-2 tabular-nums text-zinc-200">
2334
- {n}
2335
- </td>
2336
- </tr>
2337
- ),
2338
- )}
2339
- </tbody>
2340
- </table>
2341
- )}
2342
- </div>
2343
- </div>
2344
- </section>
2345
- ) : null}
2346
-
2347
- {!hasReportingChartData ? (
2348
- <p className="text-sm text-zinc-500">{t.noRowsInRange}</p>
2349
- ) : (
2350
- <>
2351
- {navigableWeekStarts.length > 1 ? (
2352
- <section
2353
- id="report-week-nav"
2354
- className="mb-8 scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-5"
2355
- aria-label={t.weekNavAriaLabel}
2356
- >
2357
- <div className="mb-2 flex flex-wrap items-center gap-2">
2358
- <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-400">
2359
- {t.tocWeekNav}
2360
- </h2>
2361
- <ReportingFilteredBadge
2362
- active={reportingFiltersActive}
2363
- label={t.sectionFilteredBadge}
2364
- titleText={t.sectionFilteredBadgeTitle}
2365
- />
2366
- </div>
2367
- <div className="flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-between">
2368
- <p className="max-w-xl text-xs text-zinc-500">
2369
- {t.weekNavHelpHint}
2370
- </p>
2371
- <div className="flex flex-wrap items-center justify-center gap-2 sm:justify-end">
2372
- <InlineMetricHelpTrigger
2373
- ariaLabel={t.metricHelpWeekNavAria}
2374
- body={t.metricHelpWeekNavBody}
2375
- />
2376
- <button
2377
- type="button"
2378
- className="inline-flex items-center gap-1 rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-200 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-40"
2379
- disabled={chartWeekNavIndexSafe <= 0}
2380
- aria-label={t.weekNavPrev}
2381
- onClick={() =>
2382
- setChartWeekNavIndex((i) => {
2383
- const cur =
2384
- i < 0
2385
- ? navigableWeekStarts.length - 1
2386
- : i;
2387
- return Math.max(0, cur - 1);
2388
- })
2389
- }
2390
- >
2391
- <ChevronLeft
2392
- className="h-4 w-4 shrink-0"
2393
- aria-hidden
2394
- />
2395
- {t.weekNavPrev}
2396
- </button>
2397
- <span className="min-w-[10rem] px-2 text-center text-sm font-medium tabular-nums text-zinc-100">
2398
- {formatWeekRangeLabel(
2399
- navigableWeekStarts[chartWeekNavIndexSafe],
2400
- reportLocale,
2401
- )}
2402
- </span>
2403
- <button
2404
- type="button"
2405
- className="inline-flex items-center gap-1 rounded-lg border border-zinc-600 px-3 py-2 text-sm text-zinc-200 hover:bg-zinc-800 disabled:cursor-not-allowed disabled:opacity-40"
2406
- disabled={
2407
- chartWeekNavIndexSafe >=
2408
- navigableWeekStarts.length - 1
2409
- }
2410
- aria-label={t.weekNavNext}
2411
- onClick={() =>
2412
- setChartWeekNavIndex((i) => {
2413
- const cur =
2414
- i < 0
2415
- ? navigableWeekStarts.length - 1
2416
- : i;
2417
- return Math.min(
2418
- navigableWeekStarts.length - 1,
2419
- cur + 1,
2420
- );
2421
- })
2422
- }
2423
- >
2424
- {t.weekNavNext}
2425
- <ChevronRight
2426
- className="h-4 w-4 shrink-0"
2427
- aria-hidden
2428
- />
2429
- </button>
2430
- </div>
2431
- </div>
2432
- </section>
2433
- ) : null}
2434
- <div className="mb-10 grid grid-cols-1 gap-4 md:grid-cols-2">
2435
- <section
2436
- id="report-chart-sessions"
2437
- className="scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
2438
- >
2439
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2440
- <h3 className="text-sm font-semibold text-zinc-200">
2441
- {t.chartSessionsPerDay}
2442
- </h3>
2443
- <ReportingFilteredBadge
2444
- active={reportingFiltersActive}
2445
- label={t.sectionFilteredBadge}
2446
- titleText={t.sectionFilteredBadgeTitle}
2447
- />
2448
- <InlineMetricHelpTrigger
2449
- ariaLabel={t.metricHelpChartSessionsAria}
2450
- body={t.metricHelpChartSessionsBody}
2451
- />
2452
- </div>
2453
- <div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
2454
- <span>{t.legendSessions}</span>
2455
- <InlineMetricHelpTrigger
2456
- ariaLabel={t.metricHelpLegendSessionsAria}
2457
- body={t.metricHelpLegendSessionsBody}
2458
- />
2459
- </div>
2460
- <div className="mt-4">
2461
- <MiniBars
2462
- days={reportingDayKeys}
2463
- values={agg.sessionsByDay}
2464
- max={peak}
2465
- className="bg-violet-500/90"
2466
- undatedLabel={t.undatedLabel}
2467
- />
2468
- </div>
2469
- </section>
2202
+ <p className="text-xs text-zinc-500">
2203
+ {t.tagTimeSectionHint}
2204
+ </p>
2470
2205
 
2471
- <section
2472
- id="report-chart-tasks"
2473
- className="scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
2474
- >
2475
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2476
- <h3 className="text-sm font-semibold text-zinc-200">
2477
- {t.chartTasksByStatusPerDay}
2478
- </h3>
2479
- <ReportingFilteredBadge
2480
- active={reportingFiltersActive}
2481
- label={t.sectionFilteredBadge}
2482
- titleText={t.sectionFilteredBadgeTitle}
2483
- />
2484
- <InlineMetricHelpTrigger
2485
- ariaLabel={t.metricHelpChartTasksAria}
2486
- body={t.metricHelpChartTasksBody}
2487
- />
2488
- </div>
2489
- <div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-zinc-500">
2490
- <div className="inline-flex items-center gap-1">
2491
- <span className="mr-0.5 inline-block h-2 w-2 shrink-0 rounded-sm bg-emerald-600" />
2492
- {t.legendDone}
2206
+ <div id="report-tag-time-week" className="space-y-4">
2207
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2208
+ <h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
2209
+ {t.tagTimeByWeekTitle}
2210
+ </h4>
2493
2211
  <InlineMetricHelpTrigger
2494
- ariaLabel={t.metricHelpLegendDoneAria}
2495
- body={t.metricHelpLegendDoneBody}
2212
+ ariaLabel={t.metricHelpTagTimeWeekTableAria}
2213
+ body={t.metricHelpTagTimeWeekTableBody}
2496
2214
  />
2497
- </div>
2498
- <div className="inline-flex items-center gap-1">
2499
- <span className="mr-0.5 inline-block h-2 w-2 shrink-0 rounded-sm bg-amber-500" />
2500
- {t.legendActive}
2501
2215
  <InlineMetricHelpTrigger
2502
- ariaLabel={t.metricHelpLegendActiveAria}
2503
- body={t.metricHelpLegendActiveBody}
2216
+ ariaLabel={t.metricHelpTagWeekCalendarAria}
2217
+ body={t.metricHelpTagWeekCalendarBody}
2504
2218
  />
2505
2219
  </div>
2506
- </div>
2507
- <div className="mt-4">
2508
- <StackedTaskBars
2509
- days={reportingDayKeys}
2510
- done={agg.tasksByDayDone}
2511
- active={agg.tasksByDayActive}
2512
- max={peakTasks}
2513
- undatedLabel={t.undatedLabel}
2514
- />
2515
- </div>
2516
- </section>
2517
-
2518
- <section
2519
- id="report-chart-task-time"
2520
- className="scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
2521
- >
2522
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2523
- <h3 className="text-sm font-semibold text-zinc-200">
2524
- {t.chartTaskTimePerDay}
2525
- </h3>
2526
- <ReportingFilteredBadge
2527
- active={reportingFiltersActive}
2528
- label={t.sectionFilteredBadge}
2529
- titleText={t.sectionFilteredBadgeTitle}
2530
- />
2531
- <InlineMetricHelpTrigger
2532
- ariaLabel={t.metricHelpChartTaskTimeAria}
2533
- body={t.metricHelpChartTaskTimeBody}
2534
- />
2535
- </div>
2536
- <div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
2537
- <span>{t.summaryTaskTimeRecorded}</span>
2538
- <InlineMetricHelpTrigger
2539
- ariaLabel={t.metricHelpTaskTimeAria}
2540
- body={t.metricHelpTaskTimeBody}
2541
- />
2542
- </div>
2543
- <div className="mt-4">
2544
- <MiniBars
2545
- days={reportingDayKeys}
2546
- values={agg.taskMinutesByDay}
2547
- max={peakTaskMinutes}
2548
- className="bg-sky-500/90"
2549
- undatedLabel={t.undatedLabel}
2550
- valueTitle={(m) => formatDuration(m)}
2551
- />
2552
- </div>
2553
- </section>
2554
-
2555
- <section
2556
- id="report-chart-session-wall"
2557
- className="scroll-mt-28 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
2558
- >
2559
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2560
- <h3 className="text-sm font-semibold text-zinc-200">
2561
- {t.chartSessionWallPerDay}
2562
- </h3>
2563
- <ReportingFilteredBadge
2564
- active={reportingFiltersActive}
2565
- label={t.sectionFilteredBadge}
2566
- titleText={t.sectionFilteredBadgeTitle}
2567
- />
2568
- <InlineMetricHelpTrigger
2569
- ariaLabel={t.metricHelpChartSessionWallAria}
2570
- body={t.metricHelpChartSessionWallBody}
2571
- />
2572
- </div>
2573
- <div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
2574
- <span>{t.summarySessionWallClock}</span>
2575
- <InlineMetricHelpTrigger
2576
- ariaLabel={t.metricHelpSessionWallSummaryAria}
2577
- body={t.metricHelpSessionWallSummaryBody}
2578
- />
2579
- </div>
2580
- <div className="mt-4">
2581
- <MiniBars
2582
- days={reportingDayKeys}
2583
- values={agg.sessionWallClockMinutesByDay}
2584
- max={peakSessionWallMinutes}
2585
- className="bg-fuchsia-500/85"
2586
- undatedLabel={t.undatedLabel}
2587
- valueTitle={(m) => formatDuration(m)}
2588
- />
2589
- </div>
2590
- </section>
2591
- </div>
2592
2220
 
2593
- <section
2594
- id="report-daily-table"
2595
- className="scroll-mt-28 overflow-x-auto rounded-xl border border-zinc-800 bg-zinc-900/50"
2596
- >
2597
- <div className="flex flex-wrap items-center gap-2 border-b border-zinc-800 px-4 py-3">
2598
- <h3 className="text-sm font-semibold text-zinc-200">
2599
- {t.tocDailyTable}
2600
- </h3>
2601
- <ReportingFilteredBadge
2602
- active={reportingFiltersActive}
2603
- label={t.sectionFilteredBadge}
2604
- titleText={t.sectionFilteredBadgeTitle}
2605
- />
2606
- </div>
2607
- <table className="w-full min-w-[48rem] text-left text-sm">
2608
- <thead className="border-b border-zinc-800 text-xs uppercase text-zinc-500">
2609
- <tr>
2610
- <th className="px-4 py-3 font-medium">
2611
- <div className="flex min-h-5 items-center gap-0.5">
2612
- <span>{t.tableDay}</span>
2613
- <InlineMetricHelpTrigger
2614
- ariaLabel={t.metricHelpTblDayAria}
2615
- body={t.metricHelpTblDayBody}
2616
- />
2617
- </div>
2618
- </th>
2619
- <th className="px-4 py-3 font-medium tabular-nums">
2620
- <div className="flex min-h-5 items-center gap-0.5">
2621
- <span>{t.tableSessions}</span>
2622
- <InlineMetricHelpTrigger
2623
- ariaLabel={t.metricHelpTblSessionsAria}
2624
- body={t.metricHelpTblSessionsBody}
2625
- />
2626
- </div>
2627
- </th>
2628
- <th className="px-4 py-3 font-medium tabular-nums">
2629
- <div className="flex min-h-5 items-center gap-0.5">
2630
- <span>{t.tableSessionWall}</span>
2221
+ <fieldset className="rounded-lg border border-zinc-200/90 bg-zinc-50/90 dark:border-zinc-800/90 dark:bg-zinc-950/30 p-3">
2222
+ <legend className="px-1 text-xs text-zinc-500">
2223
+ <span className="inline-flex flex-wrap items-center gap-1">
2224
+ {t.tagWeekStartsOnLegend}
2631
2225
  <InlineMetricHelpTrigger
2632
- ariaLabel={t.metricHelpTblSessionWallAria}
2633
- body={t.metricHelpTblSessionWallBody}
2634
- />
2635
- </div>
2636
- </th>
2637
- <th className="px-4 py-3 font-medium tabular-nums">
2638
- <div className="flex min-h-5 items-center gap-0.5">
2639
- <span>{t.tableDone}</span>
2640
- <InlineMetricHelpTrigger
2641
- ariaLabel={t.metricHelpTblDoneAria}
2642
- body={t.metricHelpTblDoneBody}
2643
- />
2644
- </div>
2645
- </th>
2646
- <th className="px-4 py-3 font-medium tabular-nums">
2647
- <div className="flex min-h-5 items-center gap-0.5">
2648
- <span>{t.tableActive}</span>
2649
- <InlineMetricHelpTrigger
2650
- align="end"
2651
- ariaLabel={t.metricHelpTblActiveAria}
2652
- body={t.metricHelpTblActiveBody}
2653
- />
2654
- </div>
2655
- </th>
2656
- <th className="px-4 py-3 font-medium tabular-nums">
2657
- <div className="flex min-h-5 items-center gap-0.5">
2658
- <span>{t.tableTaskTime}</span>
2659
- <InlineMetricHelpTrigger
2660
- ariaLabel={t.metricHelpTblTaskTimeAria}
2661
- body={t.metricHelpTblTaskTimeBody}
2662
- />
2663
- </div>
2664
- </th>
2665
- <th className="px-4 py-3 font-medium tabular-nums">
2666
- <div className="flex min-h-5 items-center gap-0.5">
2667
- <span>{t.tableTaskTimeNonConcurrent}</span>
2668
- </div>
2669
- </th>
2670
- <th className="px-4 py-3 font-medium tabular-nums">
2671
- <div className="flex min-h-5 items-center gap-0.5">
2672
- <span>{t.tableSessionCoding}</span>
2673
- <InlineMetricHelpTrigger
2674
- align="end"
2675
- ariaLabel={t.metricHelpTblSessionCodingAria}
2676
- body={t.metricHelpTblSessionCodingBody}
2226
+ ariaLabel={t.metricHelpTagWeekStartsOnAria}
2227
+ body={t.metricHelpTagWeekStartsOnBody}
2677
2228
  />
2678
- </div>
2679
- </th>
2680
- </tr>
2681
- </thead>
2682
- <tbody>
2683
- {reportingDayKeys.map((d) => (
2684
- <tr
2685
- key={d}
2686
- className="border-b border-zinc-800/80 last:border-0"
2229
+ </span>
2230
+ </legend>
2231
+ <div
2232
+ className="mt-2 flex flex-wrap gap-2"
2233
+ role="radiogroup"
2234
+ aria-label={t.tagWeekStartsOnLegend}
2687
2235
  >
2688
- <td className="px-4 py-2.5 text-zinc-200">
2689
- {dayLabel(d, t.undatedLabel)}
2690
- </td>
2691
- <td className="px-4 py-2.5 tabular-nums text-zinc-300">
2692
- {agg.sessionsByDay[d] ?? 0}
2693
- </td>
2694
- <td className="px-4 py-2.5 tabular-nums text-fuchsia-300/90">
2695
- {formatMinutesCell(
2696
- agg.sessionWallClockMinutesByDay[d],
2697
- )}
2698
- </td>
2699
- <td className="px-4 py-2.5 tabular-nums text-emerald-400/90">
2700
- {agg.tasksByDayDone[d] ?? 0}
2701
- </td>
2702
- <td className="px-4 py-2.5 tabular-nums text-amber-400/90">
2703
- {agg.tasksByDayActive[d] ?? 0}
2704
- </td>
2705
- <td className="px-4 py-2.5 tabular-nums text-sky-300/90">
2706
- {formatMinutesCell(agg.taskMinutesByDay[d])}
2707
- </td>
2708
- <td className="px-4 py-2.5 tabular-nums text-cyan-300/90">
2709
- {formatMinutesCell(
2710
- agg.nonConcurrentTaskMinutesByDay[d],
2711
- )}
2712
- </td>
2713
- <td className="px-4 py-2.5 tabular-nums text-zinc-300">
2714
- {formatMinutesCell(
2715
- agg.sessionCodingMinutesByDay[d],
2716
- )}
2717
- </td>
2718
- </tr>
2719
- ))}
2720
- </tbody>
2721
- </table>
2722
- </section>
2723
-
2724
- <section
2725
- id="report-tag-time"
2726
- className="mt-10 scroll-mt-28 space-y-8 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
2727
- >
2728
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2729
- <h3 className="text-sm font-semibold text-zinc-200">
2730
- {t.tagTimeSectionTitle}
2731
- </h3>
2732
- <ReportingFilteredBadge
2733
- active={reportingFiltersActive}
2734
- label={t.sectionFilteredBadge}
2735
- titleText={t.sectionFilteredBadgeTitle}
2736
- />
2737
- <InlineMetricHelpTrigger
2738
- ariaLabel={t.metricHelpTagTimeSectionAria}
2739
- body={t.metricHelpTagTimeSectionBody}
2740
- />
2741
- </div>
2742
- <p className="text-xs text-zinc-500">
2743
- {t.tagTimeSectionHint}
2744
- </p>
2745
-
2746
- <div id="report-tag-time-week" className="space-y-4">
2747
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2748
- <h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-400">
2749
- {t.tagTimeByWeekTitle}
2750
- </h4>
2751
- <InlineMetricHelpTrigger
2752
- ariaLabel={t.metricHelpTagTimeWeekTableAria}
2753
- body={t.metricHelpTagTimeWeekTableBody}
2754
- />
2755
- <InlineMetricHelpTrigger
2756
- ariaLabel={t.metricHelpTagWeekCalendarAria}
2757
- body={t.metricHelpTagWeekCalendarBody}
2758
- />
2759
- </div>
2760
-
2761
- <fieldset className="rounded-lg border border-zinc-800/90 bg-zinc-950/30 p-3">
2762
- <legend className="px-1 text-xs text-zinc-500">
2763
- <span className="inline-flex flex-wrap items-center gap-1">
2764
- {t.tagWeekStartsOnLegend}
2765
- <InlineMetricHelpTrigger
2766
- ariaLabel={t.metricHelpTagWeekStartsOnAria}
2767
- body={t.metricHelpTagWeekStartsOnBody}
2768
- />
2769
- </span>
2770
- </legend>
2771
- <div
2772
- className="mt-2 flex flex-wrap gap-2"
2773
- role="radiogroup"
2774
- aria-label={t.tagWeekStartsOnLegend}
2775
- >
2776
- {(
2777
- [
2778
- ["monday", t.weekStartsMonday],
2779
- ["sunday", t.weekStartsSunday],
2780
- ["saturday", t.weekStartsSaturday],
2781
- ] as const
2782
- ).map(([value, label]) => (
2783
- <button
2784
- key={value}
2785
- type="button"
2786
- role="radio"
2787
- aria-checked={weekStartsOn === value}
2788
- onClick={() => persistWeekStartsOn(value)}
2789
- className={`rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${
2790
- weekStartsOn === value
2791
- ? "border-violet-500/80 bg-violet-950/50 text-violet-200"
2792
- : "border-zinc-700 bg-zinc-900/60 text-zinc-400 hover:border-zinc-600 hover:text-zinc-200"
2793
- }`}
2794
- >
2795
- {label}
2796
- </button>
2797
- ))}
2798
- </div>
2799
- </fieldset>
2236
+ {(
2237
+ [
2238
+ ["monday", t.weekStartsMonday],
2239
+ ["sunday", t.weekStartsSunday],
2240
+ ["saturday", t.weekStartsSaturday],
2241
+ ] as const
2242
+ ).map(([value, label]) => (
2243
+ <button
2244
+ key={value}
2245
+ type="button"
2246
+ role="radio"
2247
+ aria-checked={weekStartsOn === value}
2248
+ onClick={() => persistWeekStartsOn(value)}
2249
+ className={`rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${
2250
+ weekStartsOn === value
2251
+ ? "border-violet-500/80 bg-violet-100 text-violet-900 dark:bg-violet-950/50 dark:text-violet-200"
2252
+ : "border-zinc-300 bg-white text-zinc-600 hover:border-zinc-400 hover:bg-zinc-50 hover:text-zinc-900 dark:border-zinc-700 dark:bg-zinc-900/60 dark:text-zinc-400 dark:hover:border-zinc-600 dark:hover:text-zinc-200"
2253
+ }`}
2254
+ >
2255
+ {label}
2256
+ </button>
2257
+ ))}
2258
+ </div>
2259
+ </fieldset>
2800
2260
 
2801
- {tagWeekCalendarRows.length === 0 ? (
2802
- <p className="text-sm text-zinc-500">—</p>
2803
- ) : tagWeekCalendarRowsVisible.length === 0 ? (
2804
- <div className="space-y-2 text-sm text-zinc-500">
2805
- <p>{t.weekNavNoTagDataThisWeek}</p>
2806
- {archivedExcludedTaskMinutes > 1e-9 ? (
2807
- <p className="text-amber-200/90">
2808
- {reportingArchivedExcludedRichText(
2809
- t.reportingArchivedExcludedAside,
2810
- archivedExcludedTaskMinutes,
2811
- )}
2812
- </p>
2813
- ) : null}
2814
- </div>
2815
- ) : (
2816
- <div className="space-y-8">
2817
- {tagCalendarWeekGroups.map(
2818
- ({ weekStart, rows }) => (
2819
- <div key={weekStart}>
2820
- <div className="mb-2 flex flex-wrap items-center gap-2">
2821
- <p className="text-sm font-medium text-zinc-300">
2822
- {formatWeekRangeLabel(
2823
- weekStart,
2824
- reportLocale,
2825
- )}
2826
- </p>
2827
- <InlineMetricHelpTrigger
2828
- ariaLabel={
2829
- t.metricHelpTagTimeColWeekAria
2830
- }
2831
- body={t.metricHelpTagTimeColWeekBody}
2832
- />
2833
- </div>
2834
- <div className="overflow-x-auto rounded-lg border border-zinc-800/80">
2835
- <table className="w-full min-w-[28rem] text-left text-sm">
2836
- <thead className="border-b border-zinc-800 bg-zinc-950/40 text-xs uppercase text-zinc-500">
2837
- <tr>
2838
- <th className="px-2 py-2 pl-3 font-medium">
2839
- <div className="flex min-h-5 items-center gap-0.5">
2840
- <span>{t.tagTimeColTag}</span>
2841
- <InlineMetricHelpTrigger
2842
- ariaLabel={
2843
- t.metricHelpTagTimeColTagAria
2844
- }
2845
- body={
2846
- t.metricHelpTagTimeColTagBody
2847
- }
2848
- />
2849
- </div>
2850
- </th>
2851
- {weekdayDateColumnHeaders(
2852
- weekStart,
2853
- reportLocale,
2854
- ).map((col) => (
2855
- <th
2856
- key={col.dateKey}
2857
- className="px-1 py-2 text-center font-medium normal-case"
2858
- title={col.dateKey}
2859
- >
2860
- <div className="flex flex-col items-center gap-0.5 leading-tight">
2861
- <span className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500">
2862
- {col.weekdayShort}
2863
- </span>
2864
- <span className="text-[0.7rem] font-medium tabular-nums text-zinc-300">
2865
- {col.calendarDateShort}
2261
+ {tagWeekCalendarRows.length === 0 ? (
2262
+ <p className="text-sm text-zinc-500">—</p>
2263
+ ) : tagWeekCalendarRowsVisible.length === 0 ? (
2264
+ <div className="space-y-2 text-sm text-zinc-500">
2265
+ <p>{t.weekNavNoTagDataThisWeek}</p>
2266
+ {archivedExcludedTaskMinutes > 1e-9 ? (
2267
+ <p className="text-amber-800 dark:text-amber-200/90">
2268
+ {reportingArchivedExcludedRichText(
2269
+ t.reportingArchivedExcludedAside,
2270
+ archivedExcludedTaskMinutes,
2271
+ )}
2272
+ </p>
2273
+ ) : null}
2274
+ </div>
2275
+ ) : (
2276
+ <div className="space-y-8">
2277
+ {tagCalendarWeekGroups.map(
2278
+ ({ weekStart, rows }) => (
2279
+ <div key={weekStart}>
2280
+ <div className="mb-2 flex flex-wrap items-center gap-2">
2281
+ <p className="text-sm font-medium text-zinc-800 dark:text-zinc-300">
2282
+ {formatWeekRangeLabel(
2283
+ weekStart,
2284
+ reportLocale,
2285
+ )}
2286
+ </p>
2287
+ <InlineMetricHelpTrigger
2288
+ ariaLabel={
2289
+ t.metricHelpTagTimeColWeekAria
2290
+ }
2291
+ body={t.metricHelpTagTimeColWeekBody}
2292
+ />
2293
+ </div>
2294
+ <div className="overflow-x-auto rounded-lg border border-zinc-200/80 dark:border-zinc-800/80">
2295
+ <table className="w-full min-w-[28rem] text-left text-sm">
2296
+ <thead className="border-b border-zinc-200 bg-zinc-100/80 dark:border-zinc-800 dark:bg-zinc-950/40 text-xs uppercase text-zinc-500">
2297
+ <tr>
2298
+ <th className="px-2 py-2 pl-3 font-medium">
2299
+ <div className="flex min-h-5 items-center gap-0.5">
2300
+ <span>{t.tagTimeColTag}</span>
2301
+ <InlineMetricHelpTrigger
2302
+ ariaLabel={
2303
+ t.metricHelpTagTimeColTagAria
2304
+ }
2305
+ body={
2306
+ t.metricHelpTagTimeColTagBody
2307
+ }
2308
+ />
2309
+ </div>
2310
+ </th>
2311
+ {weekdayDateColumnHeaders(
2312
+ weekStart,
2313
+ reportLocale,
2314
+ ).map((col) => (
2315
+ <th
2316
+ key={col.dateKey}
2317
+ className="px-1 py-2 text-center font-medium normal-case"
2318
+ title={col.dateKey}
2319
+ >
2320
+ <div className="flex flex-col items-center gap-0.5 leading-tight">
2321
+ <span className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500">
2322
+ {col.weekdayShort}
2323
+ </span>
2324
+ <span className="text-[0.7rem] font-medium tabular-nums text-zinc-700 dark:text-zinc-300">
2325
+ {col.calendarDateShort}
2326
+ </span>
2327
+ </div>
2328
+ </th>
2329
+ ))}
2330
+ <th className="px-2 py-2 pr-3 text-center font-medium">
2331
+ <div className="flex min-h-5 items-center justify-center gap-0.5">
2332
+ <span>
2333
+ {t.tagWeekSumColumn}
2866
2334
  </span>
2335
+ <InlineMetricHelpTrigger
2336
+ align="end"
2337
+ ariaLabel={
2338
+ t.metricHelpTagTimeColMinutesAria
2339
+ }
2340
+ body={
2341
+ t.metricHelpTagTimeColMinutesBody
2342
+ }
2343
+ />
2867
2344
  </div>
2868
2345
  </th>
2869
- ))}
2870
- <th className="px-2 py-2 pr-3 text-center font-medium">
2871
- <div className="flex min-h-5 items-center justify-center gap-0.5">
2872
- <span>
2873
- {t.tagWeekSumColumn}
2874
- </span>
2875
- <InlineMetricHelpTrigger
2876
- align="end"
2877
- ariaLabel={
2878
- t.metricHelpTagTimeColMinutesAria
2879
- }
2880
- body={
2881
- t.metricHelpTagTimeColMinutesBody
2882
- }
2883
- />
2884
- </div>
2885
- </th>
2886
- </tr>
2887
- </thead>
2888
- <tbody>
2889
- {buildTagWeekDisplayBlocks(rows).map(
2890
- (block) => {
2346
+ </tr>
2347
+ </thead>
2348
+ <tbody>
2349
+ {buildTagWeekDisplayBlocks(
2350
+ rows,
2351
+ ).map((block) => {
2891
2352
  if (block.kind === "leaf") {
2892
2353
  const row = block.row;
2893
2354
  return (
2894
2355
  <tr
2895
2356
  key={`${weekStart}\0${row.tagKey}`}
2896
- className="border-b border-zinc-800/70 last:border-0"
2357
+ className="border-b border-zinc-200/90 last:border-0 dark:border-zinc-800/70"
2897
2358
  >
2898
2359
  <ReportingTagNameCell
2899
2360
  tagKey={row.tagKey}
@@ -2907,7 +2368,7 @@ function ReportingContent() {
2907
2368
  descriptions={
2908
2369
  tagDescriptions
2909
2370
  }
2910
- className="px-2 py-2 pl-3 text-violet-200/90"
2371
+ className="px-2 py-2 pl-3 text-violet-800 dark:text-violet-200/90"
2911
2372
  />
2912
2373
  {row.slots.map(
2913
2374
  (mins, i) => {
@@ -2919,7 +2380,7 @@ function ReportingContent() {
2919
2380
  return (
2920
2381
  <td
2921
2382
  key={dateKey}
2922
- className="px-1 py-2 text-center tabular-nums text-zinc-200"
2383
+ className="px-1 py-2 text-center tabular-nums text-zinc-800 dark:text-zinc-200"
2923
2384
  title={dateKey}
2924
2385
  >
2925
2386
  {mins > 0
@@ -2940,14 +2401,14 @@ function ReportingContent() {
2940
2401
  t.undatedLabel,
2941
2402
  ),
2942
2403
  },
2943
- "text-zinc-200",
2404
+ "text-zinc-800 dark:text-zinc-200",
2944
2405
  )
2945
2406
  : "—"}
2946
2407
  </td>
2947
2408
  );
2948
2409
  },
2949
2410
  )}
2950
- <td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
2411
+ <td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-950 dark:text-zinc-100">
2951
2412
  {renderReportingDurationButton(
2952
2413
  row.total,
2953
2414
  {
@@ -2965,13 +2426,13 @@ function ReportingContent() {
2965
2426
  reportLocale,
2966
2427
  ),
2967
2428
  },
2968
- "text-zinc-100",
2429
+ "text-zinc-950 dark:text-zinc-100",
2969
2430
  )}
2970
2431
  </td>
2971
2432
  </tr>
2972
2433
  );
2973
2434
  }
2974
- const rollupKey = `${weekStart}:::${block.projectKeyLower}`;
2435
+ const rollupKey = `${weekStart}:::${block.rollupStableKey}`;
2975
2436
  const open =
2976
2437
  tagWeekRollupOpenKeys.has(
2977
2438
  rollupKey,
@@ -2983,8 +2444,8 @@ function ReportingContent() {
2983
2444
  ) ?? "";
2984
2445
  return (
2985
2446
  <Fragment key={rollupKey}>
2986
- <tr className="border-b border-zinc-800/70 bg-zinc-900/40">
2987
- <td className="px-2 py-2 pl-3 align-top text-violet-200/90">
2447
+ <tr className="border-b border-zinc-200/90 bg-zinc-100/70 dark:border-zinc-800/70 dark:bg-zinc-900/40">
2448
+ <td className="px-2 py-2 pl-3 align-top text-violet-800 dark:text-violet-200/90">
2988
2449
  <div className="flex items-start gap-1.5">
2989
2450
  <button
2990
2451
  type="button"
@@ -2996,7 +2457,7 @@ function ReportingContent() {
2996
2457
  aria-label={
2997
2458
  t.tagWeekScopedRollupToggleAria
2998
2459
  }
2999
- className="mt-0.5 shrink-0 rounded p-0.5 text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
2460
+ className="mt-0.5 shrink-0 rounded p-0.5 text-zinc-500 transition hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
3000
2461
  onClick={() =>
3001
2462
  toggleTagWeekRollup(
3002
2463
  rollupKey,
@@ -3016,7 +2477,6 @@ function ReportingContent() {
3016
2477
  <div className="min-w-0">
3017
2478
  <div className="flex flex-wrap items-baseline gap-x-1.5 font-medium">
3018
2479
  <span>
3019
- @
3020
2480
  {
3021
2481
  block.displayProject
3022
2482
  }
@@ -3048,7 +2508,7 @@ function ReportingContent() {
3048
2508
  return (
3049
2509
  <td
3050
2510
  key={dateKey}
3051
- className="px-1 py-2 text-center tabular-nums text-zinc-200"
2511
+ className="px-1 py-2 text-center tabular-nums text-zinc-800 dark:text-zinc-200"
3052
2512
  title={dateKey}
3053
2513
  >
3054
2514
  {mins > 0
@@ -3056,7 +2516,8 @@ function ReportingContent() {
3056
2516
  mins,
3057
2517
  {
3058
2518
  kind: "project",
3059
- title: `@${block.displayProject}`,
2519
+ title:
2520
+ block.displayProject,
3060
2521
  dayKey:
3061
2522
  dateKey,
3062
2523
  projectKey:
@@ -3067,19 +2528,20 @@ function ReportingContent() {
3067
2528
  t.undatedLabel,
3068
2529
  ),
3069
2530
  },
3070
- "text-zinc-200",
2531
+ "text-zinc-800 dark:text-zinc-200",
3071
2532
  )
3072
2533
  : "—"}
3073
2534
  </td>
3074
2535
  );
3075
2536
  },
3076
2537
  )}
3077
- <td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
2538
+ <td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-950 dark:text-zinc-100">
3078
2539
  {renderReportingDurationButton(
3079
2540
  block.parentTotal,
3080
2541
  {
3081
2542
  kind: "project",
3082
- title: `@${block.displayProject}`,
2543
+ title:
2544
+ block.displayProject,
3083
2545
  weekStart:
3084
2546
  block.weekStart,
3085
2547
  projectKey:
@@ -3091,7 +2553,7 @@ function ReportingContent() {
3091
2553
  reportLocale,
3092
2554
  ),
3093
2555
  },
3094
- "text-zinc-100",
2556
+ "text-zinc-950 dark:text-zinc-100",
3095
2557
  )}
3096
2558
  </td>
3097
2559
  </tr>
@@ -3100,7 +2562,7 @@ function ReportingContent() {
3100
2562
  (child) => (
3101
2563
  <tr
3102
2564
  key={`${weekStart}\0${child.tagKey}`}
3103
- className="border-b border-zinc-800/55 bg-zinc-950/30"
2565
+ className="border-b border-zinc-200/80 bg-zinc-50/90 dark:border-zinc-800/55 dark:bg-zinc-950/30"
3104
2566
  >
3105
2567
  <ReportingTagNameCell
3106
2568
  tagKey={
@@ -3116,7 +2578,7 @@ function ReportingContent() {
3116
2578
  descriptions={
3117
2579
  tagDescriptions
3118
2580
  }
3119
- className="px-2 py-2 pl-10 text-violet-200/85"
2581
+ className="px-2 py-2 pl-10 text-violet-800 dark:text-violet-200/85"
3120
2582
  />
3121
2583
  {child.slots.map(
3122
2584
  (mins, i) => {
@@ -3130,7 +2592,7 @@ function ReportingContent() {
3130
2592
  key={
3131
2593
  dateKey
3132
2594
  }
3133
- className="px-1 py-2 text-center tabular-nums text-zinc-300/95"
2595
+ className="px-1 py-2 text-center tabular-nums text-zinc-700 dark:text-zinc-300/95"
3134
2596
  title={
3135
2597
  dateKey
3136
2598
  }
@@ -3153,14 +2615,14 @@ function ReportingContent() {
3153
2615
  t.undatedLabel,
3154
2616
  ),
3155
2617
  },
3156
- "text-zinc-300/95",
2618
+ "text-zinc-700 dark:text-zinc-300/95",
3157
2619
  )
3158
2620
  : "—"}
3159
2621
  </td>
3160
2622
  );
3161
2623
  },
3162
2624
  )}
3163
- <td className="px-2 py-2 pr-3 text-center tabular-nums font-medium text-zinc-200">
2625
+ <td className="px-2 py-2 pr-3 text-center tabular-nums font-medium text-zinc-800 dark:text-zinc-200">
3164
2626
  {renderReportingDurationButton(
3165
2627
  child.total,
3166
2628
  {
@@ -3179,7 +2641,7 @@ function ReportingContent() {
3179
2641
  reportLocale,
3180
2642
  ),
3181
2643
  },
3182
- "text-zinc-200",
2644
+ "text-zinc-800 dark:text-zinc-200",
3183
2645
  )}
3184
2646
  </td>
3185
2647
  </tr>
@@ -3188,379 +2650,1398 @@ function ReportingContent() {
3188
2650
  : null}
3189
2651
  </Fragment>
3190
2652
  );
3191
- },
3192
- )}
3193
- </tbody>
3194
- </table>
2653
+ })}
2654
+ </tbody>
2655
+ </table>
2656
+ </div>
3195
2657
  </div>
3196
- </div>
3197
- ),
3198
- )}
2658
+ ),
2659
+ )}
2660
+ </div>
2661
+ )}
2662
+ </div>
2663
+ </section>
2664
+
2665
+ <section
2666
+ id="report-projects"
2667
+ className="mt-10 scroll-mt-28 space-y-4 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
2668
+ >
2669
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2670
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
2671
+ {t.projectSectionTitle}
2672
+ </h3>
2673
+ <ReportingFilteredBadge
2674
+ active={reportingFiltersActive}
2675
+ label={t.sectionFilteredBadge}
2676
+ titleText={t.sectionFilteredBadgeTitle}
2677
+ />
2678
+ <InlineMetricHelpTrigger
2679
+ ariaLabel={t.metricHelpProjectTitleAria}
2680
+ body={t.metricHelpProjectTitleBody}
2681
+ />
2682
+ </div>
2683
+ <p className="text-xs text-zinc-500">
2684
+ {t.projectTableHint}
2685
+ </p>
2686
+
2687
+ <fieldset className="rounded-lg border border-zinc-200/90 bg-zinc-50/90 dark:border-zinc-800/90 dark:bg-zinc-950/30 p-3">
2688
+ <legend className="px-1 text-xs text-zinc-500">
2689
+ {t.projectScopeLegend}
2690
+ </legend>
2691
+ <div
2692
+ className="mt-2 flex flex-wrap gap-2"
2693
+ role="radiogroup"
2694
+ aria-label={t.projectScopeGroupAria}
2695
+ >
2696
+ <button
2697
+ type="button"
2698
+ role="radio"
2699
+ aria-checked={projectMinutesScope === "work"}
2700
+ onClick={() => setProjectMinutesScope("work")}
2701
+ className={`rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${
2702
+ projectMinutesScope === "work"
2703
+ ? "border-violet-500/80 bg-violet-100 text-violet-900 dark:bg-violet-950/50 dark:text-violet-200"
2704
+ : "border-zinc-300 bg-white text-zinc-600 hover:border-zinc-400 hover:bg-zinc-50 hover:text-zinc-900 dark:border-zinc-700 dark:bg-zinc-900/60 dark:text-zinc-400 dark:hover:border-zinc-600 dark:hover:text-zinc-200"
2705
+ }`}
2706
+ >
2707
+ {t.projectScopeWork}
2708
+ </button>
2709
+ <button
2710
+ type="button"
2711
+ role="radio"
2712
+ aria-checked={
2713
+ projectMinutesScope === "personal"
2714
+ }
2715
+ onClick={() =>
2716
+ setProjectMinutesScope("personal")
2717
+ }
2718
+ className={`rounded-md border px-3 py-1.5 text-xs font-medium transition-colors ${
2719
+ projectMinutesScope === "personal"
2720
+ ? "border-rose-500/70 bg-rose-100 text-rose-900 dark:bg-rose-950/40 dark:text-rose-200"
2721
+ : "border-zinc-300 bg-white text-zinc-600 hover:border-zinc-400 hover:bg-zinc-50 hover:text-zinc-900 dark:border-zinc-700 dark:bg-zinc-900/60 dark:text-zinc-400 dark:hover:border-zinc-600 dark:hover:text-zinc-200"
2722
+ }`}
2723
+ >
2724
+ {t.projectScopePersonal}
2725
+ </button>
3199
2726
  </div>
3200
- )}
3201
- </div>
3202
- </section>
2727
+ </fieldset>
2728
+
2729
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
2730
+ <h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
2731
+ {t.projectWeeklyCalendarTitle}
2732
+ </h4>
2733
+ <InlineMetricHelpTrigger
2734
+ ariaLabel={t.metricHelpProjectCalendarAria}
2735
+ body={t.metricHelpProjectCalendarBody}
2736
+ />
2737
+ </div>
3203
2738
 
2739
+ {projectWeekCalendarRows.length === 0 ? (
2740
+ <p className="text-sm text-zinc-500">—</p>
2741
+ ) : projectWeekCalendarRowsVisible.length === 0 ? (
2742
+ <div className="space-y-2 text-sm text-zinc-500">
2743
+ <p>{t.weekNavNoProjectDataThisWeek}</p>
2744
+ {archivedExcludedTaskMinutes > 1e-9 ? (
2745
+ <p className="text-amber-800 dark:text-amber-200/90">
2746
+ {reportingArchivedExcludedRichText(
2747
+ t.reportingArchivedExcludedAside,
2748
+ archivedExcludedTaskMinutes,
2749
+ )}
2750
+ </p>
2751
+ ) : null}
2752
+ </div>
2753
+ ) : (
2754
+ <div className="space-y-8">
2755
+ {projectCalendarWeekGroups.map(
2756
+ ({ weekStart, rows }) => (
2757
+ <div key={weekStart}>
2758
+ <div className="mb-2 flex flex-wrap items-center gap-2">
2759
+ <p className="text-sm font-medium text-zinc-800 dark:text-zinc-300">
2760
+ {formatWeekRangeLabel(
2761
+ weekStart,
2762
+ reportLocale,
2763
+ )}
2764
+ </p>
2765
+ <InlineMetricHelpTrigger
2766
+ ariaLabel={
2767
+ t.metricHelpTagTimeColWeekAria
2768
+ }
2769
+ body={t.metricHelpTagTimeColWeekBody}
2770
+ />
2771
+ </div>
2772
+ <div className="overflow-x-auto rounded-lg border border-zinc-200/80 dark:border-zinc-800/80">
2773
+ <table className="w-full min-w-[28rem] text-left text-sm">
2774
+ <thead className="border-b border-zinc-200 bg-zinc-100/80 dark:border-zinc-800 dark:bg-zinc-950/40 text-xs uppercase text-zinc-500">
2775
+ <tr>
2776
+ <th className="px-2 py-2 pl-3 font-medium">
2777
+ <div className="flex min-h-5 items-center gap-0.5">
2778
+ <span>
2779
+ {t.projectColProject}
2780
+ </span>
2781
+ <InlineMetricHelpTrigger
2782
+ ariaLabel={
2783
+ t.metricHelpProjectColProjAria
2784
+ }
2785
+ body={
2786
+ t.metricHelpProjectColProjBody
2787
+ }
2788
+ />
2789
+ </div>
2790
+ </th>
2791
+ {weekdayDateColumnHeaders(
2792
+ weekStart,
2793
+ reportLocale,
2794
+ ).map((col) => (
2795
+ <th
2796
+ key={`proj-${col.dateKey}`}
2797
+ className="px-1 py-2 text-center font-medium normal-case"
2798
+ title={col.dateKey}
2799
+ >
2800
+ <div className="flex flex-col items-center gap-0.5 leading-tight">
2801
+ <span className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500">
2802
+ {col.weekdayShort}
2803
+ </span>
2804
+ <span className="text-[0.7rem] font-medium tabular-nums text-zinc-700 dark:text-zinc-300">
2805
+ {col.calendarDateShort}
2806
+ </span>
2807
+ </div>
2808
+ </th>
2809
+ ))}
2810
+ <th className="px-2 py-2 pr-3 text-center font-medium">
2811
+ <div className="flex min-h-5 items-center justify-center gap-0.5">
2812
+ <span>
2813
+ {t.tagWeekSumColumn}
2814
+ </span>
2815
+ <InlineMetricHelpTrigger
2816
+ align="end"
2817
+ ariaLabel={
2818
+ t.metricHelpProjectColTimeAria
2819
+ }
2820
+ body={
2821
+ t.metricHelpProjectColTimeBody
2822
+ }
2823
+ />
2824
+ </div>
2825
+ </th>
2826
+ </tr>
2827
+ </thead>
2828
+ <tbody>
2829
+ {rows.map((row) => {
2830
+ const projectKeyLower =
2831
+ normalizeProjectKey(
2832
+ row.projectKey,
2833
+ ).toLowerCase();
2834
+ const breakdownKey = `${weekStart}:::${projectKeyLower}`;
2835
+ const childTagRows =
2836
+ projectScopedTagRowsByWeekProject.get(
2837
+ breakdownKey,
2838
+ ) ?? [];
2839
+ const canExpand =
2840
+ childTagRows.length > 0;
2841
+ const isOpen =
2842
+ canExpand &&
2843
+ projectWeekTagBreakdownOpenKeys.has(
2844
+ breakdownKey,
2845
+ );
2846
+ const rowTitle =
2847
+ row.displayProject ||
2848
+ row.projectKey;
2849
+ const rowProjectDescription =
2850
+ reportingProjectDescriptionLine(
2851
+ row.projectKey,
2852
+ projectDescriptions,
2853
+ ) ?? "";
2854
+ return (
2855
+ <Fragment
2856
+ key={`${weekStart}\0${row.projectKey}`}
2857
+ >
2858
+ <tr className="border-b border-zinc-200/90 last:border-0 dark:border-zinc-800/70">
2859
+ {canExpand ? (
2860
+ <td
2861
+ className={`align-top px-2 py-2 pl-3 ${projectNameCellToneClass}`}
2862
+ >
2863
+ <div className="flex items-start gap-1.5">
2864
+ <button
2865
+ type="button"
2866
+ aria-expanded={isOpen}
2867
+ aria-label={
2868
+ t.tagWeekScopedRollupToggleAria
2869
+ }
2870
+ className="mt-0.5 shrink-0 rounded p-0.5 text-zinc-500 transition hover:bg-zinc-200 hover:text-zinc-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
2871
+ onClick={() =>
2872
+ toggleProjectWeekTagBreakdown(
2873
+ breakdownKey,
2874
+ )
2875
+ }
2876
+ >
2877
+ <ChevronRight
2878
+ className={`size-4 transition-transform duration-200 ${
2879
+ isOpen
2880
+ ? "rotate-90"
2881
+ : ""
2882
+ }`}
2883
+ strokeWidth={2}
2884
+ aria-hidden
2885
+ />
2886
+ </button>
2887
+ <div className="min-w-0">
2888
+ <div>
2889
+ {row.projectKey ===
2890
+ ""
2891
+ ? t.projectUnassigned
2892
+ : row.displayProject ||
2893
+ row.projectKey}
2894
+ </div>
2895
+ {rowProjectDescription ? (
2896
+ <p className="mt-1 max-w-[min(22rem,55vw)] whitespace-pre-line text-[0.65rem] font-normal leading-snug text-zinc-500">
2897
+ {
2898
+ rowProjectDescription
2899
+ }
2900
+ </p>
2901
+ ) : null}
2902
+ <p className="mt-1 text-[0.65rem] font-normal tabular-nums text-zinc-500">
2903
+ {
2904
+ childTagRows.length
2905
+ }{" "}
2906
+ {lang === "fr"
2907
+ ? "étiquette(s) liée(s)"
2908
+ : "linked tag(s)"}
2909
+ </p>
2910
+ </div>
2911
+ </div>
2912
+ </td>
2913
+ ) : (
2914
+ <ReportingProjectNameCell
2915
+ projectKey={
2916
+ row.projectKey
2917
+ }
2918
+ displayLabel={
2919
+ row.displayProject ||
2920
+ row.projectKey
2921
+ }
2922
+ unassignedLabel={
2923
+ t.projectUnassigned
2924
+ }
2925
+ descriptions={
2926
+ projectDescriptions
2927
+ }
2928
+ className={`px-2 py-2 pl-3 ${projectNameCellToneClass}`}
2929
+ />
2930
+ )}
2931
+ {row.slots.map((mins, i) => {
2932
+ const dateKey = addDaysYmd(
2933
+ row.weekStart,
2934
+ i,
2935
+ );
2936
+ return (
2937
+ <td
2938
+ key={dateKey}
2939
+ className="px-1 py-2 text-center tabular-nums text-zinc-800 dark:text-zinc-200"
2940
+ title={dateKey}
2941
+ >
2942
+ {mins > 0
2943
+ ? renderReportingDurationButton(
2944
+ mins,
2945
+ {
2946
+ kind: "project",
2947
+ title: rowTitle,
2948
+ dayKey: dateKey,
2949
+ projectKey:
2950
+ row.projectKey,
2951
+ sourceLabel:
2952
+ dayLabel(
2953
+ dateKey,
2954
+ t.undatedLabel,
2955
+ ),
2956
+ },
2957
+ "text-zinc-800 dark:text-zinc-200",
2958
+ )
2959
+ : "—"}
2960
+ </td>
2961
+ );
2962
+ })}
2963
+ <td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-950 dark:text-zinc-100">
2964
+ {renderReportingDurationButton(
2965
+ row.total,
2966
+ {
2967
+ kind: "project",
2968
+ title: rowTitle,
2969
+ weekStart:
2970
+ row.weekStart,
2971
+ projectKey:
2972
+ row.projectKey,
2973
+ sourceLabel:
2974
+ weekRangeLabelForSource(
2975
+ row.weekStart,
2976
+ lang,
2977
+ reportLocale,
2978
+ ),
2979
+ },
2980
+ "text-zinc-950 dark:text-zinc-100",
2981
+ )}
2982
+ </td>
2983
+ </tr>
2984
+ {isOpen
2985
+ ? childTagRows.map(
2986
+ (child) => (
2987
+ <tr
2988
+ key={`${weekStart}\0${row.projectKey}\0${child.tagKey}`}
2989
+ className="border-b border-zinc-200/80 bg-zinc-50/90 dark:border-zinc-800/55 dark:bg-zinc-950/30"
2990
+ >
2991
+ <ReportingTagNameCell
2992
+ tagKey={
2993
+ child.tagKey
2994
+ }
2995
+ displayLabel={
2996
+ child.displayTag ||
2997
+ child.tagKey
2998
+ }
2999
+ untaggedLabel={
3000
+ t.tagTimeUntagged
3001
+ }
3002
+ descriptions={
3003
+ tagDescriptions
3004
+ }
3005
+ className="px-2 py-2 pl-10 text-violet-800 dark:text-violet-200/85"
3006
+ />
3007
+ {child.slots.map(
3008
+ (mins, i) => {
3009
+ const dateKey =
3010
+ addDaysYmd(
3011
+ child.weekStart,
3012
+ i,
3013
+ );
3014
+ return (
3015
+ <td
3016
+ key={dateKey}
3017
+ className="px-1 py-2 text-center tabular-nums text-zinc-700 dark:text-zinc-300/95"
3018
+ title={
3019
+ dateKey
3020
+ }
3021
+ >
3022
+ {mins > 0
3023
+ ? renderReportingDurationButton(
3024
+ mins,
3025
+ {
3026
+ kind: "tag",
3027
+ title:
3028
+ child.displayTag ||
3029
+ child.tagKey,
3030
+ dayKey:
3031
+ dateKey,
3032
+ tagKey:
3033
+ child.tagKey,
3034
+ sourceLabel:
3035
+ dayLabel(
3036
+ dateKey,
3037
+ t.undatedLabel,
3038
+ ),
3039
+ },
3040
+ "text-zinc-700 dark:text-zinc-300/95",
3041
+ )
3042
+ : "—"}
3043
+ </td>
3044
+ );
3045
+ },
3046
+ )}
3047
+ <td className="px-2 py-2 pr-3 text-center tabular-nums font-medium text-zinc-800 dark:text-zinc-200">
3048
+ {renderReportingDurationButton(
3049
+ child.total,
3050
+ {
3051
+ kind: "tag",
3052
+ title:
3053
+ child.displayTag ||
3054
+ child.tagKey,
3055
+ weekStart:
3056
+ child.weekStart,
3057
+ tagKey:
3058
+ child.tagKey,
3059
+ sourceLabel:
3060
+ weekRangeLabelForSource(
3061
+ child.weekStart,
3062
+ lang,
3063
+ reportLocale,
3064
+ ),
3065
+ },
3066
+ "text-zinc-800 dark:text-zinc-200",
3067
+ )}
3068
+ </td>
3069
+ </tr>
3070
+ ),
3071
+ )
3072
+ : null}
3073
+ </Fragment>
3074
+ );
3075
+ })}
3076
+ </tbody>
3077
+ </table>
3078
+ </div>
3079
+ </div>
3080
+ ),
3081
+ )}
3082
+ </div>
3083
+ )}
3084
+ </section>
3085
+ </>
3086
+ ) : null}
3087
+ </div>
3088
+ <div
3089
+ id="report-view-panel-rhythm"
3090
+ role="tabpanel"
3091
+ aria-labelledby="reporting-tab-rhythm"
3092
+ className={
3093
+ reportingViewTab !== "rhythm"
3094
+ ? "hidden"
3095
+ : "mb-10 space-y-6"
3096
+ }
3097
+ >
3098
+ {hasReportingChartData ? (
3099
+ <>
3100
+ <div className="mb-10 grid grid-cols-1 gap-4 md:grid-cols-2">
3101
+ <section
3102
+ id="report-chart-sessions"
3103
+ className="scroll-mt-28 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
3104
+ >
3105
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
3106
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
3107
+ {t.chartSessionsPerDay}
3108
+ </h3>
3109
+ <ReportingFilteredBadge
3110
+ active={reportingFiltersActive}
3111
+ label={t.sectionFilteredBadge}
3112
+ titleText={t.sectionFilteredBadgeTitle}
3113
+ />
3114
+ <InlineMetricHelpTrigger
3115
+ ariaLabel={t.metricHelpChartSessionsAria}
3116
+ body={t.metricHelpChartSessionsBody}
3117
+ />
3118
+ </div>
3119
+ <div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
3120
+ <span>{t.legendSessions}</span>
3121
+ <InlineMetricHelpTrigger
3122
+ ariaLabel={t.metricHelpLegendSessionsAria}
3123
+ body={t.metricHelpLegendSessionsBody}
3124
+ />
3125
+ </div>
3126
+ <div className="mt-4">
3127
+ <MiniBars
3128
+ days={reportingDayKeys}
3129
+ values={agg.sessionsByDay}
3130
+ max={peak}
3131
+ className="bg-violet-500/90"
3132
+ undatedLabel={t.undatedLabel}
3133
+ />
3134
+ </div>
3135
+ </section>
3136
+
3137
+ <section
3138
+ id="report-chart-tasks"
3139
+ className="scroll-mt-28 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
3140
+ >
3141
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
3142
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
3143
+ {t.chartTasksByStatusPerDay}
3144
+ </h3>
3145
+ <ReportingFilteredBadge
3146
+ active={reportingFiltersActive}
3147
+ label={t.sectionFilteredBadge}
3148
+ titleText={t.sectionFilteredBadgeTitle}
3149
+ />
3150
+ <InlineMetricHelpTrigger
3151
+ ariaLabel={t.metricHelpChartTasksAria}
3152
+ body={t.metricHelpChartTasksBody}
3153
+ />
3154
+ </div>
3155
+ <div className="mt-2 flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-zinc-500">
3156
+ <div className="inline-flex items-center gap-1">
3157
+ <span className="mr-0.5 inline-block h-2 w-2 shrink-0 rounded-sm bg-emerald-600" />
3158
+ {t.legendDone}
3159
+ <InlineMetricHelpTrigger
3160
+ ariaLabel={t.metricHelpLegendDoneAria}
3161
+ body={t.metricHelpLegendDoneBody}
3162
+ />
3163
+ </div>
3164
+ <div className="inline-flex items-center gap-1">
3165
+ <span className="mr-0.5 inline-block h-2 w-2 shrink-0 rounded-sm bg-amber-500" />
3166
+ {t.legendActive}
3167
+ <InlineMetricHelpTrigger
3168
+ ariaLabel={t.metricHelpLegendActiveAria}
3169
+ body={t.metricHelpLegendActiveBody}
3170
+ />
3171
+ </div>
3172
+ </div>
3173
+ <div className="mt-4">
3174
+ <StackedTaskBars
3175
+ days={reportingDayKeys}
3176
+ done={agg.tasksByDayDone}
3177
+ active={agg.tasksByDayActive}
3178
+ max={peakTasks}
3179
+ undatedLabel={t.undatedLabel}
3180
+ />
3181
+ </div>
3182
+ </section>
3183
+
3184
+ <section
3185
+ id="report-chart-task-time"
3186
+ className="scroll-mt-28 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
3187
+ >
3188
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
3189
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
3190
+ {t.chartTaskTimePerDay}
3191
+ </h3>
3192
+ <ReportingFilteredBadge
3193
+ active={reportingFiltersActive}
3194
+ label={t.sectionFilteredBadge}
3195
+ titleText={t.sectionFilteredBadgeTitle}
3196
+ />
3197
+ <InlineMetricHelpTrigger
3198
+ ariaLabel={t.metricHelpChartTaskTimeAria}
3199
+ body={t.metricHelpChartTaskTimeBody}
3200
+ />
3201
+ </div>
3202
+ <div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
3203
+ <span>{t.summaryTaskTimeRecorded}</span>
3204
+ <InlineMetricHelpTrigger
3205
+ ariaLabel={t.metricHelpTaskTimeAria}
3206
+ body={t.metricHelpTaskTimeBody}
3207
+ />
3208
+ </div>
3209
+ <div className="mt-4">
3210
+ <MiniBars
3211
+ days={reportingDayKeys}
3212
+ values={agg.taskMinutesByDay}
3213
+ max={peakTaskMinutes}
3214
+ className="bg-sky-500/90"
3215
+ undatedLabel={t.undatedLabel}
3216
+ valueTitle={(m) => formatDuration(m)}
3217
+ />
3218
+ </div>
3219
+ </section>
3220
+
3221
+ <section
3222
+ id="report-chart-session-wall"
3223
+ className="scroll-mt-28 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
3224
+ >
3225
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
3226
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
3227
+ {t.chartSessionWallPerDay}
3228
+ </h3>
3229
+ <ReportingFilteredBadge
3230
+ active={reportingFiltersActive}
3231
+ label={t.sectionFilteredBadge}
3232
+ titleText={t.sectionFilteredBadgeTitle}
3233
+ />
3234
+ <InlineMetricHelpTrigger
3235
+ ariaLabel={t.metricHelpChartSessionWallAria}
3236
+ body={t.metricHelpChartSessionWallBody}
3237
+ />
3238
+ </div>
3239
+ <div className="mt-1 flex flex-wrap items-center gap-1 text-xs text-zinc-500">
3240
+ <span>{t.summarySessionWallClock}</span>
3241
+ <InlineMetricHelpTrigger
3242
+ ariaLabel={t.metricHelpSessionWallSummaryAria}
3243
+ body={t.metricHelpSessionWallSummaryBody}
3244
+ />
3245
+ </div>
3246
+ <div className="mt-4">
3247
+ <MiniBars
3248
+ days={reportingDayKeys}
3249
+ values={agg.sessionWallClockMinutesByDay}
3250
+ max={peakSessionWallMinutes}
3251
+ className="bg-fuchsia-500/85"
3252
+ undatedLabel={t.undatedLabel}
3253
+ valueTitle={(m) => formatDuration(m)}
3254
+ />
3255
+ </div>
3256
+ </section>
3257
+ </div>
3258
+ </>
3259
+ ) : null}
3260
+ </div>
3261
+ <div
3262
+ id="report-view-panel-advanced"
3263
+ role="tabpanel"
3264
+ aria-labelledby="reporting-tab-advanced"
3265
+ className={
3266
+ reportingViewTab !== "advanced"
3267
+ ? "hidden"
3268
+ : "mb-10 space-y-10"
3269
+ }
3270
+ >
3271
+ {trackCodeMetrics ? (
3272
+ <>
3273
+ <section
3274
+ id="report-workspace-snapshot"
3275
+ className="mb-10 scroll-mt-28 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
3276
+ >
3277
+ <div className="flex flex-wrap items-start justify-between gap-3">
3278
+ <div className="flex min-w-0 max-w-full items-center gap-1">
3279
+ <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
3280
+ {t.workspaceSnapshotTitle}
3281
+ </h2>
3282
+ <InlineMetricHelpTrigger
3283
+ ariaLabel={t.metricHelpWorkspaceTitleAria}
3284
+ body={t.metricHelpWorkspaceTitleBody}
3285
+ />
3286
+ </div>
3287
+ <button
3288
+ type="button"
3289
+ disabled={workspaceSnapBusy}
3290
+ className="shrink-0 rounded-lg border border-zinc-300 bg-white px-3 py-2 text-sm text-zinc-700 hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50 dark:border-zinc-600 dark:bg-transparent dark:text-zinc-300 dark:hover:bg-zinc-800"
3291
+ onClick={() => void refreshWorkspaceSnapshot()}
3292
+ >
3293
+ {workspaceSnapBusy
3294
+ ? t.workspaceSnapshotRefreshing
3295
+ : t.workspaceSnapshotRefresh}
3296
+ </button>
3297
+ </div>
3298
+ <p className="mt-2 text-xs text-zinc-500">
3299
+ {t.workspaceSnapshotIntro}
3300
+ </p>
3301
+ {(() => {
3302
+ const ws = payload.workspaceCodeSnapshot as
3303
+ | WorkspaceCodeSnapshotPayload
3304
+ | undefined;
3305
+ if (!ws) {
3306
+ return (
3307
+ <p className="mt-4 text-sm text-zinc-500">
3308
+ {t.workspaceSnapshotRefreshHint}
3309
+ </p>
3310
+ );
3311
+ }
3312
+ if (ws.ok) {
3313
+ return (
3314
+ <>
3315
+ <p className="mt-2 text-xs text-zinc-500">
3316
+ {ws.source === "git"
3317
+ ? t.workspaceSnapshotSourceGit
3318
+ : t.workspaceSnapshotSourceWalk}
3319
+ </p>
3320
+ <div className="mt-3 flex flex-wrap gap-6 text-sm">
3321
+ <div>
3322
+ <div className="flex min-h-5 items-center gap-0.5">
3323
+ <span className="text-xs uppercase text-zinc-500">
3324
+ {t.workspaceSnapshotTotalLines}
3325
+ </span>
3326
+ <InlineMetricHelpTrigger
3327
+ ariaLabel={
3328
+ t.metricHelpWsTotalLinesAria
3329
+ }
3330
+ body={t.metricHelpWsTotalLinesBody}
3331
+ />
3332
+ </div>
3333
+ <div className="tabular-nums text-lg font-semibold text-zinc-900 dark:text-zinc-100">
3334
+ {ws.totalLines}
3335
+ </div>
3336
+ </div>
3337
+ <div>
3338
+ <div className="flex min-h-5 items-center gap-0.5">
3339
+ <span className="text-xs uppercase text-zinc-500">
3340
+ {t.workspaceSnapshotFileCount}
3341
+ </span>
3342
+ <InlineMetricHelpTrigger
3343
+ ariaLabel={
3344
+ t.metricHelpWsFileCountAria
3345
+ }
3346
+ body={t.metricHelpWsFileCountBody}
3347
+ />
3348
+ </div>
3349
+ <div className="tabular-nums text-lg font-semibold text-zinc-900 dark:text-zinc-100">
3350
+ {ws.fileCount}
3351
+ </div>
3352
+ </div>
3353
+ </div>
3354
+ {ws.byLanguage.length === 0 ? (
3355
+ <p className="mt-4 text-sm text-zinc-500">
3356
+
3357
+ </p>
3358
+ ) : (
3359
+ <div className="mt-4 overflow-x-auto">
3360
+ <table className="w-full min-w-[22rem] text-left text-sm">
3361
+ <thead className="border-b border-zinc-200 text-xs uppercase text-zinc-500 dark:border-zinc-800">
3362
+ <tr>
3363
+ <th className="py-2 pr-3 font-medium">
3364
+ <div className="flex min-h-5 items-center gap-0.5">
3365
+ <span>
3366
+ {t.workspaceSnapshotColLang}
3367
+ </span>
3368
+ <InlineMetricHelpTrigger
3369
+ ariaLabel={
3370
+ t.metricHelpWsColLangAria
3371
+ }
3372
+ body={
3373
+ t.metricHelpWsColLangBody
3374
+ }
3375
+ />
3376
+ </div>
3377
+ </th>
3378
+ <th className="py-2 pr-3 font-medium tabular-nums">
3379
+ <div className="flex min-h-5 items-center gap-0.5">
3380
+ <span>
3381
+ {t.workspaceSnapshotColLines}
3382
+ </span>
3383
+ <InlineMetricHelpTrigger
3384
+ ariaLabel={
3385
+ t.metricHelpWsColLinesAria
3386
+ }
3387
+ body={
3388
+ t.metricHelpWsColLinesBody
3389
+ }
3390
+ />
3391
+ </div>
3392
+ </th>
3393
+ <th className="py-2 font-medium">
3394
+ <div className="flex min-h-5 items-center gap-0.5">
3395
+ <span>
3396
+ {
3397
+ t.workspaceSnapshotColPercent
3398
+ }
3399
+ </span>
3400
+ <InlineMetricHelpTrigger
3401
+ align="end"
3402
+ ariaLabel={
3403
+ t.metricHelpWsColPercentAria
3404
+ }
3405
+ body={
3406
+ t.metricHelpWsColPercentBody
3407
+ }
3408
+ />
3409
+ </div>
3410
+ </th>
3411
+ </tr>
3412
+ </thead>
3413
+ <tbody>
3414
+ {ws.byLanguage.map((row) => (
3415
+ <tr
3416
+ key={row.languageId}
3417
+ className="border-b border-zinc-200/90 last:border-0 dark:border-zinc-800/80"
3418
+ >
3419
+ <td className="py-2 pr-3 font-mono text-xs text-zinc-400">
3420
+ {row.languageId}
3421
+ </td>
3422
+ <td className="py-2 pr-3 tabular-nums text-zinc-800 dark:text-zinc-200">
3423
+ {row.lines}
3424
+ </td>
3425
+ <td className="py-2">
3426
+ <div className="flex items-center gap-2">
3427
+ <div className="h-2 min-w-[4rem] flex-1 overflow-hidden rounded bg-zinc-200 dark:bg-zinc-800">
3428
+ <div
3429
+ className="h-full rounded bg-sky-600/85"
3430
+ style={{
3431
+ width: `${Math.min(
3432
+ 100,
3433
+ row.percent,
3434
+ )}%`,
3435
+ }}
3436
+ />
3437
+ </div>
3438
+ <span className="w-14 shrink-0 tabular-nums text-zinc-400">
3439
+ {row.percent.toFixed(1)}%
3440
+ </span>
3441
+ </div>
3442
+ </td>
3443
+ </tr>
3444
+ ))}
3445
+ </tbody>
3446
+ </table>
3447
+ </div>
3448
+ )}
3449
+ </>
3450
+ );
3451
+ }
3452
+ return (
3453
+ <p className="mt-4 text-sm text-amber-800 dark:text-amber-200/90">
3454
+ {ws.reason === "no_workspace"
3455
+ ? t.workspaceSnapshotNoWorkspace
3456
+ : ws.reason === "empty"
3457
+ ? ws.message || t.workspaceSnapshotEmpty
3458
+ : ws.message || t.workspaceSnapshotError}
3459
+ </p>
3460
+ );
3461
+ })()}
3462
+ </section>
3463
+ </>
3464
+ ) : null}
3465
+ <section
3466
+ id="report-summary-kpis"
3467
+ className="mb-10 scroll-mt-28 flex flex-col gap-3"
3468
+ >
3469
+ <div className="flex flex-wrap items-center gap-2">
3470
+ <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
3471
+ {t.tocSummaryKpis}
3472
+ </h2>
3473
+ <ReportingFilteredBadge
3474
+ active={reportingFiltersActive}
3475
+ label={t.sectionFilteredBadge}
3476
+ titleText={t.sectionFilteredBadgeTitle}
3477
+ />
3478
+ </div>
3479
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
3480
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3481
+ <div className="flex min-h-5 items-center gap-0.5">
3482
+ <span className="text-xs uppercase text-zinc-500">
3483
+ {t.summarySessionsInRange}
3484
+ </span>
3485
+ <InlineMetricHelpTrigger
3486
+ ariaLabel={t.metricHelpSessionsAria}
3487
+ body={t.metricHelpSessionsBody}
3488
+ />
3489
+ </div>
3490
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3491
+ {agg.sessionCountContributing}
3492
+ </div>
3493
+ </div>
3494
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3495
+ <div className="flex min-h-5 items-center gap-0.5">
3496
+ <span className="text-xs uppercase text-zinc-500">
3497
+ {t.summaryTaskEvents}
3498
+ </span>
3499
+ <InlineMetricHelpTrigger
3500
+ ariaLabel={t.metricHelpTaskRowsAria}
3501
+ body={t.metricHelpTaskRowsBody}
3502
+ />
3503
+ </div>
3504
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3505
+ {agg.taskCountContributing}
3506
+ </div>
3507
+ </div>
3508
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3509
+ <div className="flex min-h-5 items-center gap-0.5">
3510
+ <span className="text-xs uppercase text-zinc-500">
3511
+ {t.summaryKronoFocusCompleted}
3512
+ </span>
3513
+ <InlineMetricHelpTrigger
3514
+ align="end"
3515
+ ariaLabel={t.metricHelpKronoFocusCompletedAria}
3516
+ body={t.metricHelpKronoFocusCompletedBody}
3517
+ />
3518
+ </div>
3519
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3520
+ {agg.kronoFocusSessionsCompleted}
3521
+ </div>
3522
+ </div>
3523
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3524
+ <div className="flex min-h-5 items-center gap-0.5">
3525
+ <span className="text-xs uppercase text-zinc-500">
3526
+ {t.summaryKronoFocusTasksUsed}
3527
+ </span>
3528
+ <InlineMetricHelpTrigger
3529
+ ariaLabel={t.metricHelpKronoFocusTasksUsedAria}
3530
+ body={t.metricHelpKronoFocusTasksUsedBody}
3531
+ />
3532
+ </div>
3533
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3534
+ {agg.kronoFocusTasksUsedCount}
3535
+ </div>
3536
+ </div>
3537
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3538
+ <div className="flex min-h-5 items-center gap-0.5">
3539
+ <span className="text-xs uppercase text-zinc-500">
3540
+ {t.summaryKronoFocusCycles}
3541
+ </span>
3542
+ <InlineMetricHelpTrigger
3543
+ ariaLabel={t.metricHelpKronoFocusCyclesAria}
3544
+ body={t.metricHelpKronoFocusCyclesBody}
3545
+ />
3546
+ </div>
3547
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3548
+ {agg.kronoFocusTaskCyclesSum}
3549
+ </div>
3550
+ </div>
3551
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3552
+ <div className="flex min-h-5 items-center gap-0.5">
3553
+ <span className="text-xs uppercase text-zinc-500">
3554
+ {t.summaryTaskTimeRecorded}
3555
+ </span>
3556
+ <InlineMetricHelpTrigger
3557
+ align="end"
3558
+ ariaLabel={t.metricHelpTaskTimeAria}
3559
+ body={t.metricHelpTaskTimeBody}
3560
+ />
3561
+ </div>
3562
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3563
+ {formatDuration(agg.taskMinutesTotal)}
3564
+ </div>
3565
+ </div>
3566
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3567
+ <div className="flex min-h-5 items-center gap-0.5">
3568
+ <span className="text-xs uppercase text-zinc-500">
3569
+ {t.summarySessionCoding}
3570
+ </span>
3571
+ <InlineMetricHelpTrigger
3572
+ ariaLabel={t.metricHelpSessionCodingAria}
3573
+ body={t.metricHelpSessionCodingBody}
3574
+ />
3575
+ </div>
3576
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3577
+ {formatDuration(agg.sessionCodingMinutesTotal)}
3578
+ </div>
3579
+ </div>
3580
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3581
+ <div className="flex min-h-5 items-center gap-0.5">
3582
+ <span className="text-xs uppercase text-zinc-500">
3583
+ {t.summarySessionActive}
3584
+ </span>
3585
+ <InlineMetricHelpTrigger
3586
+ ariaLabel={t.metricHelpSessionActiveAria}
3587
+ body={t.metricHelpSessionActiveBody}
3588
+ />
3589
+ </div>
3590
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3591
+ {formatDuration(agg.sessionActiveMinutesTotal)}
3592
+ </div>
3593
+ </div>
3594
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3595
+ <div className="flex min-h-5 items-center gap-0.5">
3596
+ <span className="text-xs uppercase text-zinc-500">
3597
+ {t.summarySessionWallClock}
3598
+ </span>
3599
+ <InlineMetricHelpTrigger
3600
+ align="end"
3601
+ ariaLabel={t.metricHelpSessionWallSummaryAria}
3602
+ body={t.metricHelpSessionWallSummaryBody}
3603
+ />
3604
+ </div>
3605
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3606
+ {formatDuration(agg.sessionWallClockMinutesTotal)}
3607
+ </div>
3608
+ </div>
3609
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3610
+ <div className="flex min-h-5 items-center gap-0.5">
3611
+ <span className="text-xs uppercase text-zinc-500">
3612
+ {t.summaryAssiduityWithReference}
3613
+ </span>
3614
+ <InlineMetricHelpTrigger
3615
+ align="end"
3616
+ ariaLabel={t.metricHelpAssiduityRefAria}
3617
+ body={t.metricHelpAssiduityRefBody}
3618
+ />
3619
+ </div>
3620
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3621
+ {agg.assiduityReferenceSessionCount}
3622
+ </div>
3623
+ </div>
3624
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3625
+ <div className="flex min-h-5 items-center gap-0.5">
3626
+ <span className="text-xs uppercase text-zinc-500">
3627
+ {t.summaryAssiduityLateSessions}
3628
+ </span>
3629
+ <InlineMetricHelpTrigger
3630
+ align="end"
3631
+ ariaLabel={t.metricHelpAssiduityLateCountAria}
3632
+ body={t.metricHelpAssiduityLateCountBody}
3633
+ />
3634
+ </div>
3635
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3636
+ {agg.assiduityLateSessionCount}
3637
+ </div>
3638
+ </div>
3639
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3640
+ <div className="flex min-h-5 items-center gap-0.5">
3641
+ <span className="text-xs uppercase text-zinc-500">
3642
+ {t.summaryAssiduityLateTotal}
3643
+ </span>
3644
+ <InlineMetricHelpTrigger
3645
+ align="end"
3646
+ ariaLabel={t.metricHelpAssiduityLateTotalAria}
3647
+ body={t.metricHelpAssiduityLateTotalBody}
3648
+ />
3649
+ </div>
3650
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3651
+ {formatDuration(agg.assiduityLateMinutesTotal)}
3652
+ </div>
3653
+ </div>
3654
+ <div className="rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4">
3655
+ <div className="flex min-h-5 items-center gap-0.5">
3656
+ <span className="text-xs uppercase text-zinc-500">
3657
+ {t.summaryAssiduityAvgLateWhenLate}
3658
+ </span>
3659
+ <InlineMetricHelpTrigger
3660
+ align="end"
3661
+ ariaLabel={t.metricHelpAssiduityAvgLateAria}
3662
+ body={t.metricHelpAssiduityAvgLateBody}
3663
+ />
3664
+ </div>
3665
+ <div className="mt-1 text-2xl font-semibold tabular-nums">
3666
+ {agg.assiduityAverageLateMinutesWhenLate == null
3667
+ ? "—"
3668
+ : formatDuration(
3669
+ agg.assiduityAverageLateMinutesWhenLate,
3670
+ )}
3671
+ </div>
3672
+ </div>
3673
+ </div>
3674
+ <div
3675
+ id="report-closure-by-kind"
3676
+ className="mt-4 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4"
3677
+ >
3678
+ <div className="mb-3 flex flex-wrap items-center gap-2">
3679
+ <h3 className="text-xs font-semibold uppercase tracking-wide text-zinc-500">
3680
+ {t.closureBreakdownTitle}
3681
+ </h3>
3682
+ <ReportingFilteredBadge
3683
+ active={reportingFiltersActive}
3684
+ label={t.sectionFilteredBadge}
3685
+ titleText={t.sectionFilteredBadgeTitle}
3686
+ />
3687
+ <InlineMetricHelpTrigger
3688
+ ariaLabel={t.metricHelpClosureBreakdownAria}
3689
+ body={t.metricHelpClosureBreakdownBody}
3690
+ />
3691
+ </div>
3692
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5">
3693
+ {REPORTING_SESSION_CLOSURE_DISPLAY_ORDER.map(
3694
+ (closureKey) => {
3695
+ const count =
3696
+ agg.sessionCountByClosureKind[closureKey] ?? 0;
3697
+ return (
3698
+ <div
3699
+ key={closureKey}
3700
+ className="rounded-lg border border-zinc-200/80 bg-zinc-100/70 dark:border-zinc-800/80 dark:bg-zinc-950/40 p-3"
3701
+ >
3702
+ <div className="text-xs uppercase text-zinc-500">
3703
+ {reportingClosureKindLabels[closureKey]}
3704
+ </div>
3705
+ <div className="mt-1 text-xl font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
3706
+ {count}
3707
+ </div>
3708
+ </div>
3709
+ );
3710
+ },
3711
+ )}
3712
+ </div>
3713
+ </div>
3714
+ </section>
3715
+ {trackCodeMetrics ? (
3204
3716
  <section
3205
- id="report-projects"
3206
- className="mt-10 scroll-mt-28 space-y-4 rounded-xl border border-zinc-800 bg-zinc-900/50 p-4 sm:p-6"
3717
+ id="report-loc-metrics"
3718
+ className="mb-10 scroll-mt-28 space-y-4 rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none p-4 sm:p-6"
3207
3719
  >
3208
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
3209
- <h3 className="text-sm font-semibold text-zinc-200">
3210
- {t.projectSectionTitle}
3211
- </h3>
3720
+ <div className="flex flex-wrap items-center gap-2">
3721
+ <h2 className="text-sm font-semibold uppercase tracking-wide text-zinc-600 dark:text-zinc-400">
3722
+ {t.tocLocSection}
3723
+ </h2>
3212
3724
  <ReportingFilteredBadge
3213
3725
  active={reportingFiltersActive}
3214
3726
  label={t.sectionFilteredBadge}
3215
3727
  titleText={t.sectionFilteredBadgeTitle}
3216
3728
  />
3217
- <InlineMetricHelpTrigger
3218
- ariaLabel={t.metricHelpProjectTitleAria}
3219
- body={t.metricHelpProjectTitleBody}
3220
- />
3221
3729
  </div>
3222
3730
  <p className="text-xs text-zinc-500">
3223
- {t.projectTableHint}
3731
+ {t.locMetricsHint}
3224
3732
  </p>
3225
-
3226
- <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
3227
- <h4 className="text-xs font-semibold uppercase tracking-wide text-zinc-400">
3228
- {t.projectWeeklyCalendarTitle}
3229
- </h4>
3230
- <InlineMetricHelpTrigger
3231
- ariaLabel={t.metricHelpProjectCalendarAria}
3232
- body={t.metricHelpProjectCalendarBody}
3233
- />
3733
+ <div className="grid gap-3 sm:grid-cols-3">
3734
+ <div className="rounded-lg border border-zinc-200/80 bg-zinc-100/70 dark:border-zinc-800/80 dark:bg-zinc-950/40 p-3">
3735
+ <div className="flex min-h-5 items-center gap-0.5">
3736
+ <span className="text-xs uppercase text-zinc-500">
3737
+ {t.summaryLinesWrittenTotal}
3738
+ </span>
3739
+ <InlineMetricHelpTrigger
3740
+ ariaLabel={t.metricHelpLinesTotalAria}
3741
+ body={t.metricHelpLinesTotalBody}
3742
+ />
3743
+ </div>
3744
+ <div className="mt-1 text-xl font-semibold tabular-nums text-zinc-900 dark:text-zinc-100">
3745
+ {agg.linesWrittenTotalSum}
3746
+ </div>
3747
+ </div>
3748
+ <div className="rounded-lg border border-zinc-200/80 bg-zinc-100/70 dark:border-zinc-800/80 dark:bg-zinc-950/40 p-3">
3749
+ <div className="flex min-h-5 items-center gap-0.5">
3750
+ <span className="text-xs uppercase text-zinc-500">
3751
+ {t.summaryLinesWrittenHuman}
3752
+ </span>
3753
+ <InlineMetricHelpTrigger
3754
+ ariaLabel={t.metricHelpLinesHumanAria}
3755
+ body={t.metricHelpLinesHumanBody}
3756
+ />
3757
+ </div>
3758
+ <div className="mt-1 text-xl font-semibold tabular-nums text-emerald-400/90">
3759
+ {agg.linesWrittenHumanSum}
3760
+ </div>
3761
+ </div>
3762
+ <div className="rounded-lg border border-zinc-200/80 bg-zinc-100/70 dark:border-zinc-800/80 dark:bg-zinc-950/40 p-3">
3763
+ <div className="flex min-h-5 items-center gap-0.5">
3764
+ <span className="text-xs uppercase text-zinc-500">
3765
+ {t.summaryLinesWrittenAi}
3766
+ </span>
3767
+ <InlineMetricHelpTrigger
3768
+ align="end"
3769
+ ariaLabel={t.metricHelpLinesAiAria}
3770
+ body={t.metricHelpLinesAiBody}
3771
+ />
3772
+ </div>
3773
+ <div className="mt-1 text-xl font-semibold tabular-nums text-violet-400/90">
3774
+ {agg.linesWrittenAiSum}
3775
+ </div>
3776
+ </div>
3234
3777
  </div>
3235
-
3236
- {projectWeekCalendarRows.length === 0 ? (
3237
- <p className="text-sm text-zinc-500">—</p>
3238
- ) : projectWeekCalendarRowsVisible.length === 0 ? (
3239
- <div className="space-y-2 text-sm text-zinc-500">
3240
- <p>{t.weekNavNoProjectDataThisWeek}</p>
3241
- {archivedExcludedTaskMinutes > 1e-9 ? (
3242
- <p className="text-amber-200/90">
3243
- {reportingArchivedExcludedRichText(
3244
- t.reportingArchivedExcludedAside,
3245
- archivedExcludedTaskMinutes,
3246
- )}
3247
- </p>
3248
- ) : null}
3778
+ <div className="grid gap-6 lg:grid-cols-2">
3779
+ <div>
3780
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
3781
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
3782
+ {t.locByLanguageSectionTitle}
3783
+ </h3>
3784
+ <ReportingFilteredBadge
3785
+ active={reportingFiltersActive}
3786
+ label={t.sectionFilteredBadge}
3787
+ titleText={t.sectionFilteredBadgeTitle}
3788
+ />
3789
+ <InlineMetricHelpTrigger
3790
+ ariaLabel={t.metricHelpLocByLangTitleAria}
3791
+ body={t.metricHelpLocByLangTitleBody}
3792
+ />
3793
+ </div>
3794
+ {agg.locByLanguageMerged.length === 0 ? (
3795
+ <p className="mt-3 text-sm text-zinc-500">—</p>
3796
+ ) : (
3797
+ <table className="mt-3 w-full min-w-[12rem] text-left text-sm">
3798
+ <thead className="border-b border-zinc-200 text-xs uppercase text-zinc-500 dark:border-zinc-800">
3799
+ <tr>
3800
+ <th className="py-2 pr-3 font-medium">
3801
+ <div className="flex min-h-5 items-center gap-0.5">
3802
+ <span>{t.locByLanguageColLang}</span>
3803
+ <InlineMetricHelpTrigger
3804
+ ariaLabel={
3805
+ t.metricHelpAggLocColLangAria
3806
+ }
3807
+ body={t.metricHelpAggLocColLangBody}
3808
+ />
3809
+ </div>
3810
+ </th>
3811
+ <th className="py-2 font-medium tabular-nums">
3812
+ <div className="flex min-h-5 items-center gap-0.5">
3813
+ <span>{t.locByLanguageColLines}</span>
3814
+ <InlineMetricHelpTrigger
3815
+ align="end"
3816
+ ariaLabel={
3817
+ t.metricHelpAggLocColLinesAria
3818
+ }
3819
+ body={t.metricHelpAggLocColLinesBody}
3820
+ />
3821
+ </div>
3822
+ </th>
3823
+ </tr>
3824
+ </thead>
3825
+ <tbody>
3826
+ {agg.locByLanguageMerged.map(([lang, n]) => (
3827
+ <tr
3828
+ key={lang}
3829
+ className="border-b border-zinc-200/90 last:border-0 dark:border-zinc-800/80"
3830
+ >
3831
+ <td className="py-2 pr-3 font-mono text-xs text-zinc-400">
3832
+ {lang}
3833
+ </td>
3834
+ <td className="py-2 tabular-nums text-zinc-800 dark:text-zinc-200">
3835
+ {n}
3836
+ </td>
3837
+ </tr>
3838
+ ))}
3839
+ </tbody>
3840
+ </table>
3841
+ )}
3249
3842
  </div>
3250
- ) : (
3251
- <div className="space-y-8">
3252
- {projectCalendarWeekGroups.map(
3253
- ({ weekStart, rows }) => (
3254
- <div key={weekStart}>
3255
- <div className="mb-2 flex flex-wrap items-center gap-2">
3256
- <p className="text-sm font-medium text-zinc-300">
3257
- {formatWeekRangeLabel(
3258
- weekStart,
3259
- reportLocale,
3260
- )}
3261
- </p>
3262
- <InlineMetricHelpTrigger
3263
- ariaLabel={t.metricHelpTagTimeColWeekAria}
3264
- body={t.metricHelpTagTimeColWeekBody}
3265
- />
3266
- </div>
3267
- <div className="overflow-x-auto rounded-lg border border-zinc-800/80">
3268
- <table className="w-full min-w-[28rem] text-left text-sm">
3269
- <thead className="border-b border-zinc-800 bg-zinc-950/40 text-xs uppercase text-zinc-500">
3270
- <tr>
3271
- <th className="px-2 py-2 pl-3 font-medium">
3272
- <div className="flex min-h-5 items-center gap-0.5">
3273
- <span>{t.projectColProject}</span>
3274
- <InlineMetricHelpTrigger
3275
- ariaLabel={
3276
- t.metricHelpProjectColProjAria
3277
- }
3278
- body={
3279
- t.metricHelpProjectColProjBody
3280
- }
3281
- />
3282
- </div>
3283
- </th>
3284
- {weekdayDateColumnHeaders(
3285
- weekStart,
3286
- reportLocale,
3287
- ).map((col) => (
3288
- <th
3289
- key={`proj-${col.dateKey}`}
3290
- className="px-1 py-2 text-center font-medium normal-case"
3291
- title={col.dateKey}
3292
- >
3293
- <div className="flex flex-col items-center gap-0.5 leading-tight">
3294
- <span className="text-[0.65rem] font-semibold uppercase tracking-wide text-zinc-500">
3295
- {col.weekdayShort}
3296
- </span>
3297
- <span className="text-[0.7rem] font-medium tabular-nums text-zinc-300">
3298
- {col.calendarDateShort}
3299
- </span>
3300
- </div>
3301
- </th>
3302
- ))}
3303
- <th className="px-2 py-2 pr-3 text-center font-medium">
3304
- <div className="flex min-h-5 items-center justify-center gap-0.5">
3305
- <span>{t.tagWeekSumColumn}</span>
3306
- <InlineMetricHelpTrigger
3307
- align="end"
3308
- ariaLabel={
3309
- t.metricHelpProjectColTimeAria
3310
- }
3311
- body={
3312
- t.metricHelpProjectColTimeBody
3313
- }
3314
- />
3315
- </div>
3316
- </th>
3317
- </tr>
3318
- </thead>
3319
- <tbody>
3320
- {rows.map((row) => {
3321
- const projectKeyLower =
3322
- normalizeProjectKey(
3323
- row.projectKey,
3324
- ).toLowerCase();
3325
- const breakdownKey = `${weekStart}:::${projectKeyLower}`;
3326
- const childTagRows =
3327
- projectScopedTagRowsByWeekProject.get(
3328
- breakdownKey,
3329
- ) ?? [];
3330
- const canExpand =
3331
- childTagRows.length > 0;
3332
- const isOpen =
3333
- canExpand &&
3334
- projectWeekTagBreakdownOpenKeys.has(
3335
- breakdownKey,
3336
- );
3337
- const rowTitle =
3338
- row.displayProject ||
3339
- row.projectKey;
3340
- const rowProjectDescription =
3341
- reportingProjectDescriptionLine(
3342
- row.projectKey,
3343
- projectDescriptions,
3344
- ) ?? "";
3345
- return (
3346
- <Fragment
3347
- key={`${weekStart}\0${row.projectKey}`}
3348
- >
3349
- <tr className="border-b border-zinc-800/70 last:border-0">
3350
- {canExpand ? (
3351
- <td className="align-top px-2 py-2 pl-3 text-sky-200/90">
3352
- <div className="flex items-start gap-1.5">
3353
- <button
3354
- type="button"
3355
- aria-expanded={isOpen}
3356
- aria-label={
3357
- t.tagWeekScopedRollupToggleAria
3358
- }
3359
- className="mt-0.5 shrink-0 rounded p-0.5 text-zinc-400 transition hover:bg-zinc-800 hover:text-zinc-200"
3360
- onClick={() =>
3361
- toggleProjectWeekTagBreakdown(
3362
- breakdownKey,
3363
- )
3364
- }
3365
- >
3366
- <ChevronRight
3367
- className={`size-4 transition-transform duration-200 ${
3368
- isOpen
3369
- ? "rotate-90"
3370
- : ""
3371
- }`}
3372
- strokeWidth={2}
3373
- aria-hidden
3374
- />
3375
- </button>
3376
- <div className="min-w-0">
3377
- <div>
3378
- {row.projectKey === ""
3379
- ? t.projectUnassigned
3380
- : row.displayProject ||
3381
- row.projectKey}
3382
- </div>
3383
- {rowProjectDescription ? (
3384
- <p className="mt-1 max-w-[min(22rem,55vw)] whitespace-pre-line text-[0.65rem] font-normal leading-snug text-zinc-500">
3385
- {
3386
- rowProjectDescription
3387
- }
3388
- </p>
3389
- ) : null}
3390
- <p className="mt-1 text-[0.65rem] font-normal tabular-nums text-zinc-500">
3391
- {childTagRows.length}{" "}
3392
- {lang === "fr"
3393
- ? "étiquette(s) liée(s)"
3394
- : "linked tag(s)"}
3395
- </p>
3396
- </div>
3397
- </div>
3398
- </td>
3399
- ) : (
3400
- <ReportingProjectNameCell
3401
- projectKey={row.projectKey}
3402
- displayLabel={
3403
- row.displayProject ||
3404
- row.projectKey
3405
- }
3406
- unassignedLabel={
3407
- t.projectUnassigned
3408
- }
3409
- descriptions={
3410
- projectDescriptions
3411
- }
3412
- className="px-2 py-2 pl-3 text-sky-200/90"
3413
- />
3414
- )}
3415
- {row.slots.map((mins, i) => {
3416
- const dateKey = addDaysYmd(
3417
- row.weekStart,
3418
- i,
3419
- );
3420
- return (
3421
- <td
3422
- key={dateKey}
3423
- className="px-1 py-2 text-center tabular-nums text-zinc-200"
3424
- title={dateKey}
3425
- >
3426
- {mins > 0
3427
- ? renderReportingDurationButton(
3428
- mins,
3429
- {
3430
- kind: "project",
3431
- title: rowTitle,
3432
- dayKey: dateKey,
3433
- projectKey:
3434
- row.projectKey,
3435
- sourceLabel:
3436
- dayLabel(
3437
- dateKey,
3438
- t.undatedLabel,
3439
- ),
3440
- },
3441
- "text-zinc-200",
3442
- )
3443
- : "—"}
3444
- </td>
3445
- );
3446
- })}
3447
- <td className="px-2 py-2 pr-3 text-center tabular-nums font-semibold text-zinc-100">
3448
- {renderReportingDurationButton(
3449
- row.total,
3450
- {
3451
- kind: "project",
3452
- title: rowTitle,
3453
- weekStart: row.weekStart,
3454
- projectKey:
3455
- row.projectKey,
3456
- sourceLabel:
3457
- weekRangeLabelForSource(
3458
- row.weekStart,
3459
- lang,
3460
- reportLocale,
3461
- ),
3462
- },
3463
- "text-zinc-100",
3464
- )}
3465
- </td>
3466
- </tr>
3467
- {isOpen
3468
- ? childTagRows.map((child) => (
3469
- <tr
3470
- key={`${weekStart}\0${row.projectKey}\0${child.tagKey}`}
3471
- className="border-b border-zinc-800/55 bg-zinc-950/30"
3472
- >
3473
- <ReportingTagNameCell
3474
- tagKey={child.tagKey}
3475
- displayLabel={
3476
- child.displayTag ||
3477
- child.tagKey
3478
- }
3479
- untaggedLabel={
3480
- t.tagTimeUntagged
3481
- }
3482
- descriptions={
3483
- tagDescriptions
3484
- }
3485
- className="px-2 py-2 pl-10 text-violet-200/85"
3486
- />
3487
- {child.slots.map(
3488
- (mins, i) => {
3489
- const dateKey =
3490
- addDaysYmd(
3491
- child.weekStart,
3492
- i,
3493
- );
3494
- return (
3495
- <td
3496
- key={dateKey}
3497
- className="px-1 py-2 text-center tabular-nums text-zinc-300/95"
3498
- title={dateKey}
3499
- >
3500
- {mins > 0
3501
- ? renderReportingDurationButton(
3502
- mins,
3503
- {
3504
- kind: "tag",
3505
- title:
3506
- child.displayTag ||
3507
- child.tagKey,
3508
- dayKey:
3509
- dateKey,
3510
- tagKey:
3511
- child.tagKey,
3512
- sourceLabel:
3513
- dayLabel(
3514
- dateKey,
3515
- t.undatedLabel,
3516
- ),
3517
- },
3518
- "text-zinc-300/95",
3519
- )
3520
- : "—"}
3521
- </td>
3522
- );
3523
- },
3524
- )}
3525
- <td className="px-2 py-2 pr-3 text-center tabular-nums font-medium text-zinc-200">
3526
- {renderReportingDurationButton(
3527
- child.total,
3528
- {
3529
- kind: "tag",
3530
- title:
3531
- child.displayTag ||
3532
- child.tagKey,
3533
- weekStart:
3534
- child.weekStart,
3535
- tagKey:
3536
- child.tagKey,
3537
- sourceLabel:
3538
- weekRangeLabelForSource(
3539
- child.weekStart,
3540
- lang,
3541
- reportLocale,
3542
- ),
3543
- },
3544
- "text-zinc-200",
3545
- )}
3546
- </td>
3547
- </tr>
3548
- ))
3549
- : null}
3550
- </Fragment>
3551
- );
3552
- })}
3553
- </tbody>
3554
- </table>
3555
- </div>
3556
- </div>
3557
- ),
3843
+ <div>
3844
+ <div className="flex min-h-6 flex-wrap items-center gap-x-2 gap-y-1">
3845
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
3846
+ {t.codingSignalsSectionTitle}
3847
+ </h3>
3848
+ <ReportingFilteredBadge
3849
+ active={reportingFiltersActive}
3850
+ label={t.sectionFilteredBadge}
3851
+ titleText={t.sectionFilteredBadgeTitle}
3852
+ />
3853
+ <InlineMetricHelpTrigger
3854
+ ariaLabel={t.metricHelpCodingSignalsTitleAria}
3855
+ body={t.metricHelpCodingSignalsTitleBody}
3856
+ />
3857
+ </div>
3858
+ {agg.codingSignalsByLanguageMerged.length === 0 ? (
3859
+ <p className="mt-3 text-sm text-zinc-500">—</p>
3860
+ ) : (
3861
+ <table className="mt-3 w-full min-w-[12rem] text-left text-sm">
3862
+ <thead className="border-b border-zinc-200 text-xs uppercase text-zinc-500 dark:border-zinc-800">
3863
+ <tr>
3864
+ <th className="py-2 pr-3 font-medium">
3865
+ <div className="flex min-h-5 items-center gap-0.5">
3866
+ <span>{t.codingSignalsColLang}</span>
3867
+ <InlineMetricHelpTrigger
3868
+ ariaLabel={
3869
+ t.metricHelpAggSigColLangAria
3870
+ }
3871
+ body={t.metricHelpAggSigColLangBody}
3872
+ />
3873
+ </div>
3874
+ </th>
3875
+ <th className="py-2 font-medium tabular-nums">
3876
+ <div className="flex min-h-5 items-center gap-0.5">
3877
+ <span>{t.codingSignalsColCount}</span>
3878
+ <InlineMetricHelpTrigger
3879
+ align="end"
3880
+ ariaLabel={
3881
+ t.metricHelpAggSigColCountAria
3882
+ }
3883
+ body={t.metricHelpAggSigColCountBody}
3884
+ />
3885
+ </div>
3886
+ </th>
3887
+ </tr>
3888
+ </thead>
3889
+ <tbody>
3890
+ {agg.codingSignalsByLanguageMerged.map(
3891
+ ([lang, n]) => (
3892
+ <tr
3893
+ key={lang}
3894
+ className="border-b border-zinc-200/90 last:border-0 dark:border-zinc-800/80"
3895
+ >
3896
+ <td className="py-2 pr-3 font-mono text-xs text-zinc-400">
3897
+ {lang}
3898
+ </td>
3899
+ <td className="py-2 tabular-nums text-zinc-800 dark:text-zinc-200">
3900
+ {n}
3901
+ </td>
3902
+ </tr>
3903
+ ),
3904
+ )}
3905
+ </tbody>
3906
+ </table>
3558
3907
  )}
3559
3908
  </div>
3560
- )}
3909
+ </div>
3561
3910
  </section>
3562
- </>
3563
- )}
3911
+ ) : null}
3912
+ {hasReportingChartData ? (
3913
+ <section
3914
+ id="report-daily-table"
3915
+ className="scroll-mt-28 overflow-x-auto rounded-xl border border-zinc-200 bg-white/95 shadow-sm shadow-zinc-900/[0.06] dark:border-zinc-800 dark:bg-zinc-900/50 dark:shadow-none"
3916
+ >
3917
+ <div className="flex flex-wrap items-center gap-2 border-b border-zinc-200 px-4 py-3 dark:border-zinc-800">
3918
+ <h3 className="text-sm font-semibold text-zinc-900 dark:text-zinc-200">
3919
+ {t.tocDailyTable}
3920
+ </h3>
3921
+ <ReportingFilteredBadge
3922
+ active={reportingFiltersActive}
3923
+ label={t.sectionFilteredBadge}
3924
+ titleText={t.sectionFilteredBadgeTitle}
3925
+ />
3926
+ </div>
3927
+ <table className="w-full min-w-[48rem] text-left text-sm">
3928
+ <thead className="border-b border-zinc-200 text-xs uppercase text-zinc-500 dark:border-zinc-800">
3929
+ <tr>
3930
+ <th className="px-4 py-3 font-medium">
3931
+ <div className="flex min-h-5 items-center gap-0.5">
3932
+ <span>{t.tableDay}</span>
3933
+ <InlineMetricHelpTrigger
3934
+ ariaLabel={t.metricHelpTblDayAria}
3935
+ body={t.metricHelpTblDayBody}
3936
+ />
3937
+ </div>
3938
+ </th>
3939
+ <th className="px-4 py-3 font-medium tabular-nums">
3940
+ <div className="flex min-h-5 items-center gap-0.5">
3941
+ <span>{t.tableSessions}</span>
3942
+ <InlineMetricHelpTrigger
3943
+ ariaLabel={t.metricHelpTblSessionsAria}
3944
+ body={t.metricHelpTblSessionsBody}
3945
+ />
3946
+ </div>
3947
+ </th>
3948
+ <th className="px-4 py-3 font-medium tabular-nums">
3949
+ <div className="flex min-h-5 items-center gap-0.5">
3950
+ <span>{t.tableSessionWall}</span>
3951
+ <InlineMetricHelpTrigger
3952
+ ariaLabel={t.metricHelpTblSessionWallAria}
3953
+ body={t.metricHelpTblSessionWallBody}
3954
+ />
3955
+ </div>
3956
+ </th>
3957
+ <th className="px-4 py-3 font-medium tabular-nums">
3958
+ <div className="flex min-h-5 items-center gap-0.5">
3959
+ <span>{t.tableDone}</span>
3960
+ <InlineMetricHelpTrigger
3961
+ ariaLabel={t.metricHelpTblDoneAria}
3962
+ body={t.metricHelpTblDoneBody}
3963
+ />
3964
+ </div>
3965
+ </th>
3966
+ <th className="px-4 py-3 font-medium tabular-nums">
3967
+ <div className="flex min-h-5 items-center gap-0.5">
3968
+ <span>{t.tableActive}</span>
3969
+ <InlineMetricHelpTrigger
3970
+ align="end"
3971
+ ariaLabel={t.metricHelpTblActiveAria}
3972
+ body={t.metricHelpTblActiveBody}
3973
+ />
3974
+ </div>
3975
+ </th>
3976
+ <th className="px-4 py-3 font-medium tabular-nums">
3977
+ <div className="flex min-h-5 items-center gap-0.5">
3978
+ <span>{t.tableTaskTime}</span>
3979
+ <InlineMetricHelpTrigger
3980
+ ariaLabel={t.metricHelpTblTaskTimeAria}
3981
+ body={t.metricHelpTblTaskTimeBody}
3982
+ />
3983
+ </div>
3984
+ </th>
3985
+ <th className="px-4 py-3 font-medium tabular-nums">
3986
+ <div className="flex min-h-5 items-center gap-0.5">
3987
+ <span>{t.tableTaskTimeNonConcurrent}</span>
3988
+ </div>
3989
+ </th>
3990
+ <th className="px-4 py-3 font-medium tabular-nums">
3991
+ <div className="flex min-h-5 items-center gap-0.5">
3992
+ <span>{t.tableSessionCoding}</span>
3993
+ <InlineMetricHelpTrigger
3994
+ align="end"
3995
+ ariaLabel={t.metricHelpTblSessionCodingAria}
3996
+ body={t.metricHelpTblSessionCodingBody}
3997
+ />
3998
+ </div>
3999
+ </th>
4000
+ </tr>
4001
+ </thead>
4002
+ <tbody>
4003
+ {reportingDayKeys.map((d) => (
4004
+ <tr
4005
+ key={d}
4006
+ className="border-b border-zinc-200/90 last:border-0 dark:border-zinc-800/80"
4007
+ >
4008
+ <td className="px-4 py-2.5 text-zinc-900 dark:text-zinc-200">
4009
+ {dayLabel(d, t.undatedLabel)}
4010
+ </td>
4011
+ <td className="px-4 py-2.5 tabular-nums text-zinc-700 dark:text-zinc-300">
4012
+ {agg.sessionsByDay[d] ?? 0}
4013
+ </td>
4014
+ <td className="px-4 py-2.5 tabular-nums text-fuchsia-800 dark:text-fuchsia-300/90">
4015
+ {formatMinutesCell(
4016
+ agg.sessionWallClockMinutesByDay[d],
4017
+ )}
4018
+ </td>
4019
+ <td className="px-4 py-2.5 tabular-nums text-emerald-800 dark:text-emerald-400/90">
4020
+ {agg.tasksByDayDone[d] ?? 0}
4021
+ </td>
4022
+ <td className="px-4 py-2.5 tabular-nums text-amber-800 dark:text-amber-400/90">
4023
+ {agg.tasksByDayActive[d] ?? 0}
4024
+ </td>
4025
+ <td className="px-4 py-2.5 tabular-nums text-sky-800 dark:text-sky-300/90">
4026
+ {formatMinutesCell(agg.taskMinutesByDay[d])}
4027
+ </td>
4028
+ <td className="px-4 py-2.5 tabular-nums text-cyan-800 dark:text-cyan-300/90">
4029
+ {formatMinutesCell(
4030
+ agg.nonConcurrentTaskMinutesByDay[d],
4031
+ )}
4032
+ </td>
4033
+ <td className="px-4 py-2.5 tabular-nums text-zinc-700 dark:text-zinc-300">
4034
+ {formatMinutesCell(
4035
+ agg.sessionCodingMinutesByDay[d],
4036
+ )}
4037
+ </td>
4038
+ </tr>
4039
+ ))}
4040
+ </tbody>
4041
+ </table>
4042
+ </section>
4043
+ ) : null}
4044
+ </div>
3564
4045
  </>
3565
4046
  )}
3566
4047
  </div>
@@ -3614,7 +4095,7 @@ function ReportingContent() {
3614
4095
  ) : null}
3615
4096
  <div
3616
4097
  ref={taskInspectModalRef}
3617
- className="max-h-[72vh] overflow-hidden rounded-xl border border-zinc-700 bg-zinc-900 shadow-2xl"
4098
+ className="max-h-[72vh] overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-2xl dark:border-zinc-700 dark:bg-zinc-900"
3618
4099
  style={
3619
4100
  taskInspectPlacement
3620
4101
  ? {
@@ -3634,15 +4115,15 @@ function ReportingContent() {
3634
4115
  }
3635
4116
  }
3636
4117
  >
3637
- <div className="flex items-center justify-between gap-3 border-b border-zinc-700 px-4 py-3">
4118
+ <div className="flex items-center justify-between gap-3 border-b border-zinc-200 px-4 py-3 dark:border-zinc-700">
3638
4119
  <div className="min-w-0">
3639
- <h2 className="min-w-0 truncate text-sm font-semibold text-zinc-100 sm:text-base">
4120
+ <h2 className="min-w-0 truncate text-sm font-semibold text-zinc-900 sm:text-base dark:text-zinc-100">
3640
4121
  {lang === "fr"
3641
4122
  ? `Détails des tâches - ${taskInspectScope.title}`
3642
4123
  : `Task details - ${taskInspectScope.title}`}
3643
4124
  </h2>
3644
4125
  {taskInspectScope.sourceLabel ? (
3645
- <p className="mt-0.5 truncate text-[0.7rem] text-zinc-400">
4126
+ <p className="mt-0.5 truncate text-[0.7rem] text-zinc-600 dark:text-zinc-400">
3646
4127
  {lang === "fr"
3647
4128
  ? `Source : ${
3648
4129
  taskInspectScope.kind === "tag"
@@ -3659,7 +4140,7 @@ function ReportingContent() {
3659
4140
  </div>
3660
4141
  <button
3661
4142
  type="button"
3662
- className="rounded border border-zinc-600 px-2 py-1 text-xs text-zinc-200 hover:bg-zinc-800"
4143
+ className="rounded border border-zinc-300 px-2 py-1 text-xs text-zinc-800 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
3663
4144
  onClick={() => {
3664
4145
  setTaskInspectScope(null);
3665
4146
  setTaskInspectAnchor(null);
@@ -3698,7 +4179,7 @@ function ReportingContent() {
3698
4179
  ))}
3699
4180
  </div>
3700
4181
  ) : (
3701
- <p className="text-sm text-zinc-400">
4182
+ <p className="text-sm text-zinc-600 dark:text-zinc-400">
3702
4183
  {lang === "fr"
3703
4184
  ? "Aucune tâche correspondante pour ce segment de temps."
3704
4185
  : "No matching tasks for this time segment."}
@@ -3713,6 +4194,7 @@ function ReportingContent() {
3713
4194
  <ReportingTour
3714
4195
  open={reportingTourOpen}
3715
4196
  onOpenChange={setReportingTourOpen}
4197
+ onStepChange={syncReportingTourTab}
3716
4198
  dt={dt}
3717
4199
  hasReportingChartData={hasReportingChartData}
3718
4200
  />