@burdenoff/microfe-timecampus 2026.801.1 → 2026.820.2

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 (155) hide show
  1. package/dist/generated/wspace-operations.d.ts +484 -861
  2. package/dist/generated/wspace-operations.d.ts.map +1 -1
  3. package/dist/generated/wspace-operations.js +1377 -1377
  4. package/dist/generated/wspace-operations.js.map +1 -1
  5. package/dist/generated/wspace-types.js.map +1 -1
  6. package/dist/index.js +5 -3
  7. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  8. package/dist/stories/EmptyState.stories.d.ts +0 -1
  9. package/dist/stories/LoadingSkeleton.stories.d.ts +0 -1
  10. package/dist/stories/StatCard.stories.d.ts +0 -1
  11. package/dist/stories/StatusBadge.stories.d.ts +0 -1
  12. package/dist/timecampus/AdminRoot.d.ts +8 -0
  13. package/dist/timecampus/AdminRoot.d.ts.map +1 -0
  14. package/dist/timecampus/AdminRoot.js +12 -0
  15. package/dist/timecampus/AdminRoot.js.map +1 -0
  16. package/dist/timecampus/AdminRoutes.d.ts +3 -0
  17. package/dist/timecampus/AdminRoutes.d.ts.map +1 -0
  18. package/dist/timecampus/AdminRoutes.js +8 -0
  19. package/dist/timecampus/AdminRoutes.js.map +1 -0
  20. package/dist/timecampus/TimeCampusRoot.d.ts +0 -1
  21. package/dist/timecampus/TimeCampusRoot.js +4 -2
  22. package/dist/timecampus/TimeCampusRoot.js.map +1 -1
  23. package/dist/timecampus/TimeCampusRoutes.js.map +1 -1
  24. package/dist/timecampus/components/ChartCard.d.ts +0 -1
  25. package/dist/timecampus/components/ChartCard.js.map +1 -1
  26. package/dist/timecampus/components/DataTable.d.ts +0 -1
  27. package/dist/timecampus/components/EmptyState.d.ts +0 -1
  28. package/dist/timecampus/components/EmptyState.js.map +1 -1
  29. package/dist/timecampus/components/ErrorBoundary.d.ts +0 -1
  30. package/dist/timecampus/components/ErrorBoundary.js.map +1 -1
  31. package/dist/timecampus/components/FilterBar.d.ts +0 -1
  32. package/dist/timecampus/components/LoadingSkeleton.d.ts +0 -1
  33. package/dist/timecampus/components/LoadingSkeleton.js.map +1 -1
  34. package/dist/timecampus/components/Modal.d.ts +0 -1
  35. package/dist/timecampus/components/Modal.js +5 -3
  36. package/dist/timecampus/components/Modal.js.map +1 -1
  37. package/dist/timecampus/components/PageLayout.d.ts +0 -1
  38. package/dist/timecampus/components/PageLayout.js.map +1 -1
  39. package/dist/timecampus/components/StatCard.d.ts +0 -1
  40. package/dist/timecampus/components/StatCard.js.map +1 -1
  41. package/dist/timecampus/components/StatusBadge.d.ts +0 -1
  42. package/dist/timecampus/components/StatusBadge.js.map +1 -1
  43. package/dist/timecampus/components/Toast.d.ts +0 -1
  44. package/dist/timecampus/components/Toast.js.map +1 -1
  45. package/dist/timecampus/components/charts/BalanceWheel.js.map +1 -1
  46. package/dist/timecampus/components/charts/BarsChart.d.ts +0 -1
  47. package/dist/timecampus/components/charts/BarsChart.js.map +1 -1
  48. package/dist/timecampus/components/charts/ChartParts.d.ts +0 -1
  49. package/dist/timecampus/components/charts/ChartParts.js.map +1 -1
  50. package/dist/timecampus/components/charts/DonutChart.d.ts +0 -1
  51. package/dist/timecampus/components/charts/DonutChart.js.map +1 -1
  52. package/dist/timecampus/components/charts/Heatmap.d.ts +0 -1
  53. package/dist/timecampus/components/charts/Heatmap.js +29 -29
  54. package/dist/timecampus/components/charts/Heatmap.js.map +1 -1
  55. package/dist/timecampus/components/charts/ProgressMeter.d.ts +0 -1
  56. package/dist/timecampus/components/charts/ProgressMeter.js +2 -2
  57. package/dist/timecampus/components/charts/ProgressMeter.js.map +1 -1
  58. package/dist/timecampus/components/charts/RadialGauge.d.ts +0 -1
  59. package/dist/timecampus/components/charts/RadialGauge.js +2 -2
  60. package/dist/timecampus/components/charts/RadialGauge.js.map +1 -1
  61. package/dist/timecampus/components/charts/TrendChart.d.ts +0 -1
  62. package/dist/timecampus/components/charts/TrendChart.js.map +1 -1
  63. package/dist/timecampus/components/charts/WeeksGrid.js +2 -2
  64. package/dist/timecampus/components/charts/WeeksGrid.js.map +1 -1
  65. package/dist/timecampus/components/charts/chartTheme.js.map +1 -1
  66. package/dist/timecampus/components/charts/index.js +10 -9
  67. package/dist/timecampus/components/index.js +18 -17
  68. package/dist/timecampus/index.d.ts +3 -1
  69. package/dist/timecampus/index.d.ts.map +1 -1
  70. package/dist/timecampus/index.js +8 -5
  71. package/dist/timecampus/pages/HomePage.d.ts +0 -1
  72. package/dist/timecampus/pages/HomePage.js.map +1 -1
  73. package/dist/timecampus/pages/admin/AdminPage.d.ts +3 -0
  74. package/dist/timecampus/pages/admin/AdminPage.d.ts.map +1 -0
  75. package/dist/timecampus/pages/admin/AdminPage.js +235 -0
  76. package/dist/timecampus/pages/admin/AdminPage.js.map +1 -0
  77. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.d.ts.map +1 -1
  78. package/dist/timecampus/pages/application-usage/ApplicationUsagePage.js.map +1 -1
  79. package/dist/timecampus/pages/client-portal/ClientPortalPage.d.ts.map +1 -1
  80. package/dist/timecampus/pages/client-portal/ClientPortalPage.js.map +1 -1
  81. package/dist/timecampus/pages/dashboard/AnalyticsPage.js.map +1 -1
  82. package/dist/timecampus/pages/dashboard/ReportsPage.js.map +1 -1
  83. package/dist/timecampus/pages/email-tracking/EmailAnalyticsPage.js.map +1 -1
  84. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.d.ts.map +1 -1
  85. package/dist/timecampus/pages/email-tracking/EmailTrackingPage.js.map +1 -1
  86. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.d.ts.map +1 -1
  87. package/dist/timecampus/pages/expense-tracking/ExpenseTrackingPage.js.map +1 -1
  88. package/dist/timecampus/pages/focus-score/FocusScorePage.js.map +1 -1
  89. package/dist/timecampus/pages/geofencing/GeofencingPage.d.ts.map +1 -1
  90. package/dist/timecampus/pages/geofencing/GeofencingPage.js.map +1 -1
  91. package/dist/timecampus/pages/invoicing/InvoicingPage.d.ts.map +1 -1
  92. package/dist/timecampus/pages/invoicing/InvoicingPage.js.map +1 -1
  93. package/dist/timecampus/pages/lifetime/DeadTimeRadarPage.js.map +1 -1
  94. package/dist/timecampus/pages/lifetime/FiniteLifePage.js.map +1 -1
  95. package/dist/timecampus/pages/lifetime/LifeBalancePage.js.map +1 -1
  96. package/dist/timecampus/pages/lifetime/LifeTimelinePage.js.map +1 -1
  97. package/dist/timecampus/pages/lifetime/PersonalCorporatePage.js.map +1 -1
  98. package/dist/timecampus/pages/lifetime/RecommendationsPage.js +1 -1
  99. package/dist/timecampus/pages/lifetime/RecommendationsPage.js.map +1 -1
  100. package/dist/timecampus/pages/lifetime/TimeWellSpentPage.js.map +1 -1
  101. package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js +9 -6
  102. package/dist/timecampus/pages/lifetime/ValuesGoalsPage.js.map +1 -1
  103. package/dist/timecampus/pages/lifetime/domains.d.ts +0 -1
  104. package/dist/timecampus/pages/lifetime/domains.js +1 -1
  105. package/dist/timecampus/pages/lifetime/domains.js.map +1 -1
  106. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.d.ts.map +1 -1
  107. package/dist/timecampus/pages/location-tracking/LocationTrackingPage.js.map +1 -1
  108. package/dist/timecampus/pages/meetings/ActionItemFormModal.d.ts +0 -1
  109. package/dist/timecampus/pages/meetings/ActionItemFormModal.js.map +1 -1
  110. package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js +1 -1
  111. package/dist/timecampus/pages/meetings/MeetingAnalyticsPage.js.map +1 -1
  112. package/dist/timecampus/pages/meetings/MeetingCreatePage.js.map +1 -1
  113. package/dist/timecampus/pages/meetings/MeetingDetailPage.js.map +1 -1
  114. package/dist/timecampus/pages/meetings/MeetingFormModal.d.ts +0 -1
  115. package/dist/timecampus/pages/meetings/MeetingFormModal.js.map +1 -1
  116. package/dist/timecampus/pages/meetings/MeetingsPage.js.map +1 -1
  117. package/dist/timecampus/pages/meetings/ParticipantFormModal.d.ts +0 -1
  118. package/dist/timecampus/pages/meetings/ParticipantFormModal.js.map +1 -1
  119. package/dist/timecampus/pages/pomodoro/PomodoroAnalyticsPage.js.map +1 -1
  120. package/dist/timecampus/pages/pomodoro/PomodoroPage.js.map +1 -1
  121. package/dist/timecampus/pages/pomodoro/PomodoroSettingsPage.js.map +1 -1
  122. package/dist/timecampus/pages/projects/ProjectCreatePage.js.map +1 -1
  123. package/dist/timecampus/pages/projects/ProjectDetailPage.js.map +1 -1
  124. package/dist/timecampus/pages/projects/ProjectsPage.js.map +1 -1
  125. package/dist/timecampus/pages/projects/TasksPage.js.map +1 -1
  126. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.d.ts.map +1 -1
  127. package/dist/timecampus/pages/resource-planning/ResourcePlanningPage.js.map +1 -1
  128. package/dist/timecampus/pages/team/TeamAnalyticsPage.js.map +1 -1
  129. package/dist/timecampus/pages/team/TeamMembersPage.js.map +1 -1
  130. package/dist/timecampus/pages/team/TeamPage.js.map +1 -1
  131. package/dist/timecampus/pages/team-activity/TeamActivityPage.js.map +1 -1
  132. package/dist/timecampus/pages/time/TimeEntriesPage.d.ts.map +1 -1
  133. package/dist/timecampus/pages/time/TimeEntriesPage.js.map +1 -1
  134. package/dist/timecampus/pages/time/TimeTrackingPage.js +1 -1
  135. package/dist/timecampus/pages/time/TimeTrackingPage.js.map +1 -1
  136. package/dist/timecampus/pages/time/TimerPage.js +4 -4
  137. package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
  138. package/dist/timecampus/pages/time-approval/TimeApprovalPage.d.ts.map +1 -1
  139. package/dist/timecampus/pages/time-approval/TimeApprovalPage.js.map +1 -1
  140. package/dist/timecampus/pages/timeline/TimelinePage.js.map +1 -1
  141. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js +1 -1
  142. package/dist/timecampus/pages/work-patterns/WorkPatternsPage.js.map +1 -1
  143. package/dist/timecampus/providers/TimeCampusProvider.d.ts +0 -1
  144. package/dist/timecampus/providers/TimeCampusProvider.d.ts.map +1 -1
  145. package/dist/timecampus/providers/TimeCampusProvider.js +38 -35
  146. package/dist/timecampus/providers/TimeCampusProvider.js.map +1 -1
  147. package/dist/timecampus/store/timecampusStore.js.map +1 -1
  148. package/dist/timecampus/types/index.d.ts +4 -1
  149. package/dist/timecampus/types/index.d.ts.map +1 -1
  150. package/dist/timecampus/utils/helpers.d.ts +0 -1
  151. package/dist/timecampus/utils/helpers.js.map +1 -1
  152. package/dist/types/assistant.d.ts +0 -1
  153. package/dist/utils/pageContext.js +1 -1
  154. package/dist/utils/pageContext.js.map +1 -1
  155. package/package.json +60 -57
@@ -1 +1 @@
1
- {"version":3,"file":"PersonalCorporatePage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/PersonalCorporatePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n Lock,\n Building2,\n Share2,\n ShieldCheck,\n Plug,\n Info,\n ArrowUpRight,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n ToastContainer,\n useToast,\n} from \"../../components\";\nimport { cn } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useMyContextSharingPolicyQuery,\n useMySourceConsentsQuery,\n useMyPersonalDataSummaryQuery,\n useUpdateContextSharingPolicyMutation,\n useGrantSourceConsentMutation,\n useRevokeSourceConsentMutation,\n type MyPersonalDataSummaryQuery,\n type MySourceConsentsQuery,\n} from \"@/generated/wspace-operations\";\nimport {\n lifeDomainLabel,\n formatSecondsHuman,\n isoDaysAgo,\n isoNow,\n} from \"./domains\";\n\ntype PersonalDomainCount =\n MyPersonalDataSummaryQuery[\"myPersonalDataSummary\"][\"byDomain\"][number];\ntype SourceConsent = MySourceConsentsQuery[\"mySourceConsents\"][number];\n\n/** The three aggregate work signals the user can share up to their workspace. */\ntype PolicyKey = \"sustainableHours\" | \"recoveryBand\" | \"workLifeBoundary\";\n\nconst POLICY_TOGGLES: {\n key: PolicyKey;\n label: string;\n description: string;\n}[] = [\n {\n key: \"sustainableHours\",\n label: \"Sustainable hours\",\n description:\n \"A yes/no signal that your working hours sit within a healthy range — never the hours themselves.\",\n },\n {\n key: \"recoveryBand\",\n label: \"Recovery band\",\n description:\n \"Whether you're getting enough recovery between work blocks, as a single band — not what you did to recover.\",\n },\n {\n key: \"workLifeBoundary\",\n label: \"Work-life boundary\",\n description:\n \"Whether work is staying inside the hours you've set aside for it — a boundary flag, nothing more.\",\n },\n];\n\n/** Turn a raw enum/source token like \"SUSTAINABLE_HOURS\" or \"onDevice\" into prose. */\nfunction humanizeToken(raw: string): string {\n const spaced = raw\n .replace(/[_-]+/g, \" \")\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .trim()\n .toLowerCase();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/**\n * A small, token-styled toggle switch. Kept local to this page (per brief) —\n * a headless button with `role=\"switch\"` so it stays accessible and themable.\n * No hover movement: the track only changes colour.\n */\nfunction ToggleSwitch({\n checked,\n onChange,\n disabled = false,\n ariaLabel,\n}: {\n checked: boolean;\n onChange: (next: boolean) => void;\n disabled?: boolean;\n ariaLabel: string;\n}) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={ariaLabel}\n disabled={disabled}\n onClick={() => onChange(!checked)}\n className={cn(\n \"relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition-colors\",\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface\",\n checked\n ? \"border-transparent bg-action-primary-bg\"\n : \"border-border-subtle bg-bg-sunken\",\n disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <span\n className={cn(\n \"inline-block h-4 w-4 transform rounded-full shadow-sm transition-transform motion-reduce:transition-none\",\n checked\n ? \"translate-x-6 bg-action-primary-text\"\n : \"translate-x-1 bg-bg-surface\",\n )}\n />\n </button>\n );\n}\n\nexport function PersonalCorporatePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n\n // Keep the range identity stable so `isoNow()` doesn't trigger refetch loops.\n const range = useMemo(() => ({ from: isoDaysAgo(30), to: isoNow() }), []);\n\n const {\n data: policyData,\n loading: policyLoading,\n error: policyError,\n } = useMyContextSharingPolicyQuery({ skip: !workspaceId });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n refetch: refetchSummary,\n } = useMyPersonalDataSummaryQuery({\n variables: { from: range.from, to: range.to },\n skip: !workspaceId,\n });\n\n const {\n data: consentsData,\n loading: consentsLoading,\n error: consentsError,\n refetch: refetchConsents,\n } = useMySourceConsentsQuery({ skip: !workspaceId });\n\n const [updatePolicy] = useUpdateContextSharingPolicyMutation();\n const [grantConsent] = useGrantSourceConsentMutation();\n const [revokeConsent] = useRevokeSourceConsentMutation();\n\n const [savingPolicyKey, setSavingPolicyKey] = useState<PolicyKey | null>(\n null,\n );\n const [savingSource, setSavingSource] = useState<string | null>(null);\n const [savingOnDeviceSource, setSavingOnDeviceSource] = useState<\n string | null\n >(null);\n\n const policy = policyData?.myContextSharingPolicy;\n const summary = summaryData?.myPersonalDataSummary;\n const consents: SourceConsent[] = consentsData?.mySourceConsents ?? [];\n\n // Biggest slices of private time first — most meaningful at a glance.\n const personalDomains = useMemo<PersonalDomainCount[]>(() => {\n const list = summary?.byDomain ?? [];\n return [...list].sort((a, b) => b.totalSeconds - a.totalSeconds);\n }, [summary]);\n\n const maxDomainSeconds = personalDomains[0]?.totalSeconds ?? 0;\n const sharedAggregates: string[] = summary?.sharedAggregates ?? [];\n\n const pageProps = {\n title: \"Personal & Corporate\",\n description: \"Two contexts, one boundary you control\",\n purpose:\n \"TimeCampus keeps two worlds apart: your private life record, which stays yours, and the workspace you work in. This is where you decide exactly which aggregate work signals — never your personal context — travel up to your employer.\",\n } as const;\n\n const initialLoading =\n !workspaceId ||\n (policyLoading && !policyData) ||\n (summaryLoading && !summaryData);\n\n if (initialLoading) {\n return (\n <PageLayout {...pageProps}>\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <LoadingSkeleton variant=\"card\" count={1} />\n <LoadingSkeleton variant=\"card\" count={1} />\n <LoadingSkeleton variant=\"card\" count={1} />\n </div>\n </PageLayout>\n );\n }\n\n // Both core surfaces failed and we have nothing to show — calm, honest error.\n if (policyError && summaryError && !policy && !summary) {\n return (\n <PageLayout {...pageProps}>\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n We couldn't load your privacy settings right now. Please try again in\n a moment.\n </div>\n </PageLayout>\n );\n }\n\n const handlePolicyToggle = async (key: PolicyKey, next: boolean) => {\n if (!policy) return;\n const label = POLICY_TOGGLES.find((t) => t.key === key)?.label ?? \"signal\";\n setSavingPolicyKey(key);\n try {\n const res = await updatePolicy({\n variables: {\n input: {\n sustainableHours: policy.sustainableHours,\n recoveryBand: policy.recoveryBand,\n workLifeBoundary: policy.workLifeBoundary,\n [key]: next,\n },\n },\n });\n if (res.error) throw new Error(res.error.message);\n // Shared-aggregate list can change with the policy — keep Corporate honest.\n await refetchSummary();\n addToast({\n title: next ? `Now sharing \"${label}\"` : `Stopped sharing \"${label}\"`,\n description:\n \"Your workspace only ever sees this as an aggregate signal.\",\n variant: \"success\",\n });\n } catch {\n addToast({\n title: \"Couldn't update what you share\",\n description: \"Nothing was changed. Please try again.\",\n variant: \"error\",\n });\n } finally {\n setSavingPolicyKey(null);\n }\n };\n\n // BOFF-4202: let the user actually flip onDeviceOnly, not just see it.\n // Re-grants with the same mode + the flipped flag; grantSourceConsent's\n // upsert preserves `granted:true` on this path (only meaningful to toggle\n // on an already-connected source), so this never re-enables a revoked one.\n const handleOnDeviceOnlyToggle = async (\n consent: SourceConsent,\n next: boolean,\n ) => {\n const label = humanizeToken(consent.source);\n setSavingOnDeviceSource(consent.source);\n try {\n const res = await grantConsent({\n variables: {\n source: consent.source,\n mode: consent.mode,\n onDeviceOnly: next,\n },\n });\n if (res.error) throw new Error(res.error.message);\n await refetchConsents();\n addToast({\n title: next\n ? `${label} now on-device only`\n : `${label} on-device-only turned off`,\n description: next\n ? \"Only aggregated app/category/timing data reaches your workspace — no titles, URLs, or other details.\"\n : undefined,\n variant: \"success\",\n });\n } catch {\n addToast({\n title: \"Couldn't update on-device-only\",\n description: \"Nothing was changed. Please try again.\",\n variant: \"error\",\n });\n } finally {\n setSavingOnDeviceSource(null);\n }\n };\n\n const handleConsentToggle = async (consent: SourceConsent, next: boolean) => {\n const label = humanizeToken(consent.source);\n setSavingSource(consent.source);\n try {\n if (next) {\n const res = await grantConsent({\n variables: { source: consent.source },\n });\n if (res.error) throw new Error(res.error.message);\n } else {\n const res = await revokeConsent({\n variables: { source: consent.source },\n });\n if (res.error) throw new Error(res.error.message);\n }\n await refetchConsents();\n addToast({\n title: next ? `${label} connected` : `${label} disconnected`,\n variant: \"success\",\n });\n } catch {\n addToast({\n title: \"Couldn't update this source\",\n description: \"Nothing was changed. Please try again.\",\n variant: \"error\",\n });\n } finally {\n setSavingSource(null);\n }\n };\n\n return (\n <PageLayout {...pageProps}>\n {/* Three contexts: your private record · what you share · the workspace. */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* ── Personal (private to you) ─────────────────────────────────── */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-4 sm:p-6\">\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Lock className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Personal{\" \"}\n <span className=\"font-normal text-text-secondary\">\n (private to you)\n </span>\n </h2>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Everything you capture about your life. Locked to your account —\n it never leaves this side.\n </p>\n </div>\n </div>\n\n {summaryError && !summary ? (\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text\">\n Couldn't load your private data summary right now.\n </div>\n ) : !summary || summary.totalPersonalEvents === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"Nothing private captured yet\"\n description=\"Log your first life entry and it will live here — always private to you.\"\n />\n ) : (\n <div className=\"flex flex-1 flex-col gap-4\">\n <StatCard\n title=\"Private life entries\"\n value={summary.totalPersonalEvents.toLocaleString()}\n icon={<Lock className=\"h-5 w-5\" />}\n description=\"captured in the last 30 days\"\n />\n\n {personalDomains.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No life domains logged in this window yet.\n </p>\n ) : (\n <ul className=\"space-y-2.5\">\n {personalDomains.map((d) => {\n const pct =\n maxDomainSeconds > 0\n ? Math.round((d.totalSeconds / maxDomainSeconds) * 100)\n : 0;\n return (\n <li\n key={d.domain}\n className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-3\"\n >\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {lifeDomainLabel(d.domain)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatSecondsHuman(d.totalSeconds)} ·{\" \"}\n {d.eventCount.toLocaleString()}{\" \"}\n {d.eventCount === 1 ? \"entry\" : \"entries\"}\n </p>\n </div>\n <Lock\n className=\"h-3.5 w-3.5 shrink-0 text-text-muted\"\n aria-label=\"Private to you\"\n />\n </div>\n {/* Data-driven share bar of this window's private time. */}\n <div className=\"mt-2 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg/60\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n )}\n </GlassCard>\n\n {/* ── What you share up to work ─────────────────────────────────── */}\n <div className=\"flex flex-col gap-4\">\n <GlassCard\n treatment=\"insight\"\n glow\n className=\"flex flex-col p-4 sm:p-6\"\n >\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg/40 text-status-info-text\">\n <Share2 className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n What you share up to work\n </h2>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Aggregate signals only. Every switch is off until you turn it\n on.\n </p>\n </div>\n </div>\n\n {policyError && !policy ? (\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text\">\n Couldn't load your sharing settings right now.\n </div>\n ) : (\n <div className=\"space-y-3\">\n {POLICY_TOGGLES.map((toggle) => {\n const checked = policy ? policy[toggle.key] : false;\n const saving = savingPolicyKey === toggle.key;\n return (\n <div\n key={toggle.key}\n className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n {toggle.label}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {toggle.description}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <ToggleSwitch\n checked={checked}\n disabled={saving || !policy}\n ariaLabel={`Share \"${toggle.label}\" with your workspace`}\n onChange={(next) =>\n handlePolicyToggle(toggle.key, next)\n }\n />\n <span\n className={cn(\n \"text-[0.65rem] font-medium uppercase tracking-wide\",\n checked\n ? \"text-status-success-text\"\n : \"text-text-muted\",\n )}\n >\n {saving ? \"Saving…\" : checked ? \"On\" : \"Off\"}\n </span>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </GlassCard>\n\n {/* Strong, unmissable reassurance. */}\n <GlassCard\n treatment=\"metric\"\n className=\"border-status-success-border/40 p-4\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-success-bg/40 text-status-success-text\">\n <ShieldCheck className=\"h-5 w-5\" />\n </div>\n <p className=\"text-sm font-semibold text-text-primary\">\n Your employer never sees your personal context.\n </p>\n </div>\n </GlassCard>\n </div>\n\n {/* ── Corporate (your workspace) ────────────────────────────────── */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-4 sm:p-6\">\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Building2 className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Corporate{\" \"}\n <span className=\"font-normal text-text-secondary\">\n (your workspace)\n </span>\n </h2>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n What the workspace can actually see about you.\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-1 flex-col gap-4\">\n <div className=\"flex items-start gap-2 rounded-lg border border-status-info-border/40 bg-status-info-bg/10 px-3 py-2.5 text-xs text-status-info-text\">\n <Info className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" />\n <span>\n Your workspace only ever receives aggregate work signals —\n coarse yes/no bands, never entries, locations, health or\n timestamps.\n </span>\n </div>\n\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-muted\">\n Aggregate signals visible to work\n </p>\n {sharedAggregates.length === 0 ? (\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-4 text-center\">\n <p className=\"text-sm font-medium text-text-primary\">\n Nothing is shared right now\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Turn on a signal in the middle column and it will appear\n here.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-2\">\n {sharedAggregates.map((agg) => (\n <li\n key={agg}\n className=\"flex items-center justify-between gap-3 rounded-lg border border-border-subtle bg-bg-sunken/40 px-3 py-2.5\"\n >\n <span className=\"flex min-w-0 items-center gap-2 text-sm text-text-primary\">\n <ArrowUpRight className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n <span className=\"truncate\">{humanizeToken(agg)}</span>\n </span>\n <StatusBadge variant=\"success\" label=\"Shared\" dot />\n </li>\n ))}\n </ul>\n )}\n </div>\n\n <p className=\"mt-auto text-xs text-text-muted\">\n This mirrors exactly what you've allowed — change it any time from\n the switches on the left.\n </p>\n </div>\n </GlassCard>\n </div>\n\n {/* ── Connected sources & consent ─────────────────────────────────── */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-4 sm:p-6\">\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Plug className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Connected sources\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Each capture source only feeds your private record once you\n consent — and you can withdraw at any moment.\n </p>\n </div>\n </div>\n\n {consentsError && consents.length === 0 ? (\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text\">\n Couldn't load your connected sources right now.\n </div>\n ) : consentsLoading && consents.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={3} />\n ) : consents.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No sources connected yet\"\n description=\"When you connect a capture source — a calendar, location or health signal — you'll grant and revoke exactly what it may feed here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {consents.map((consent) => {\n const saving = savingSource === consent.source;\n return (\n <GlassCard key={consent.id} treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {humanizeToken(consent.source)}\n </p>\n <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n <StatusBadge\n variant=\"muted\"\n label={humanizeToken(consent.mode)}\n />\n {consent.granted && (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={consent.onDeviceOnly}\n disabled={savingOnDeviceSource === consent.source}\n onClick={() =>\n handleOnDeviceOnlyToggle(\n consent,\n !consent.onDeviceOnly,\n )\n }\n className={cn(\n \"rounded-full border px-2 py-0.5 text-[0.65rem] font-medium uppercase tracking-wide transition-colors\",\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg/50\",\n consent.onDeviceOnly\n ? \"border-status-info-border/60 bg-status-info-bg/40 text-status-info-text\"\n : \"border-border-subtle bg-bg-sunken/40 text-text-muted hover:text-text-secondary\",\n savingOnDeviceSource === consent.source &&\n \"cursor-not-allowed opacity-50\",\n )}\n >\n {savingOnDeviceSource === consent.source\n ? \"Saving…\"\n : consent.onDeviceOnly\n ? \"On-device only\"\n : \"Turn on on-device only\"}\n </button>\n )}\n {consent.retentionDays != null && (\n <StatusBadge\n variant=\"muted\"\n label={`Kept ${consent.retentionDays}d`}\n />\n )}\n </div>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <ToggleSwitch\n checked={consent.granted}\n disabled={saving}\n ariaLabel={`Consent for ${humanizeToken(consent.source)}`}\n onChange={(next) => handleConsentToggle(consent, next)}\n />\n <span\n className={cn(\n \"text-[0.65rem] font-medium uppercase tracking-wide\",\n consent.granted\n ? \"text-status-success-text\"\n : \"text-text-muted\",\n )}\n >\n {saving\n ? \"Saving…\"\n : consent.granted\n ? \"Granted\"\n : \"Revoked\"}\n </span>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </GlassCard>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PersonalCorporatePage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/PersonalCorporatePage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n Lock,\n Building2,\n Share2,\n ShieldCheck,\n Plug,\n Info,\n ArrowUpRight,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n ToastContainer,\n useToast,\n} from \"../../components\";\nimport { cn } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useMyContextSharingPolicyQuery,\n useMySourceConsentsQuery,\n useMyPersonalDataSummaryQuery,\n useUpdateContextSharingPolicyMutation,\n useGrantSourceConsentMutation,\n useRevokeSourceConsentMutation,\n type MyPersonalDataSummaryQuery,\n type MySourceConsentsQuery,\n} from \"@/generated/wspace-operations\";\nimport {\n lifeDomainLabel,\n formatSecondsHuman,\n isoDaysAgo,\n isoNow,\n} from \"./domains\";\n\ntype PersonalDomainCount =\n MyPersonalDataSummaryQuery[\"myPersonalDataSummary\"][\"byDomain\"][number];\ntype SourceConsent = MySourceConsentsQuery[\"mySourceConsents\"][number];\n\n/** The three aggregate work signals the user can share up to their workspace. */\ntype PolicyKey = \"sustainableHours\" | \"recoveryBand\" | \"workLifeBoundary\";\n\nconst POLICY_TOGGLES: {\n key: PolicyKey;\n label: string;\n description: string;\n}[] = [\n {\n key: \"sustainableHours\",\n label: \"Sustainable hours\",\n description:\n \"A yes/no signal that your working hours sit within a healthy range — never the hours themselves.\",\n },\n {\n key: \"recoveryBand\",\n label: \"Recovery band\",\n description:\n \"Whether you're getting enough recovery between work blocks, as a single band — not what you did to recover.\",\n },\n {\n key: \"workLifeBoundary\",\n label: \"Work-life boundary\",\n description:\n \"Whether work is staying inside the hours you've set aside for it — a boundary flag, nothing more.\",\n },\n];\n\n/** Turn a raw enum/source token like \"SUSTAINABLE_HOURS\" or \"onDevice\" into prose. */\nfunction humanizeToken(raw: string): string {\n const spaced = raw\n .replace(/[_-]+/g, \" \")\n .replace(/([a-z0-9])([A-Z])/g, \"$1 $2\")\n .trim()\n .toLowerCase();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/**\n * A small, token-styled toggle switch. Kept local to this page (per brief) —\n * a headless button with `role=\"switch\"` so it stays accessible and themable.\n * No hover movement: the track only changes colour.\n */\nfunction ToggleSwitch({\n checked,\n onChange,\n disabled = false,\n ariaLabel,\n}: {\n checked: boolean;\n onChange: (next: boolean) => void;\n disabled?: boolean;\n ariaLabel: string;\n}) {\n return (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={checked}\n aria-label={ariaLabel}\n disabled={disabled}\n onClick={() => onChange(!checked)}\n className={cn(\n \"relative inline-flex h-6 w-11 shrink-0 items-center rounded-full border transition-colors\",\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg-surface\",\n checked\n ? \"border-transparent bg-action-primary-bg\"\n : \"border-border-subtle bg-bg-sunken\",\n disabled && \"cursor-not-allowed opacity-50\",\n )}\n >\n <span\n className={cn(\n \"inline-block h-4 w-4 transform rounded-full shadow-sm transition-transform motion-reduce:transition-none\",\n checked\n ? \"translate-x-6 bg-action-primary-text\"\n : \"translate-x-1 bg-bg-surface\",\n )}\n />\n </button>\n );\n}\n\nexport function PersonalCorporatePage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n\n // Keep the range identity stable so `isoNow()` doesn't trigger refetch loops.\n const range = useMemo(() => ({ from: isoDaysAgo(30), to: isoNow() }), []);\n\n const {\n data: policyData,\n loading: policyLoading,\n error: policyError,\n } = useMyContextSharingPolicyQuery({ skip: !workspaceId });\n\n const {\n data: summaryData,\n loading: summaryLoading,\n error: summaryError,\n refetch: refetchSummary,\n } = useMyPersonalDataSummaryQuery({\n variables: { from: range.from, to: range.to },\n skip: !workspaceId,\n });\n\n const {\n data: consentsData,\n loading: consentsLoading,\n error: consentsError,\n refetch: refetchConsents,\n } = useMySourceConsentsQuery({ skip: !workspaceId });\n\n const [updatePolicy] = useUpdateContextSharingPolicyMutation();\n const [grantConsent] = useGrantSourceConsentMutation();\n const [revokeConsent] = useRevokeSourceConsentMutation();\n\n const [savingPolicyKey, setSavingPolicyKey] = useState<PolicyKey | null>(\n null,\n );\n const [savingSource, setSavingSource] = useState<string | null>(null);\n const [savingOnDeviceSource, setSavingOnDeviceSource] = useState<\n string | null\n >(null);\n\n const policy = policyData?.myContextSharingPolicy;\n const summary = summaryData?.myPersonalDataSummary;\n const consents: SourceConsent[] = consentsData?.mySourceConsents ?? [];\n\n // Biggest slices of private time first — most meaningful at a glance.\n const personalDomains = useMemo<PersonalDomainCount[]>(() => {\n const list = summary?.byDomain ?? [];\n return [...list].sort((a, b) => b.totalSeconds - a.totalSeconds);\n }, [summary]);\n\n const maxDomainSeconds = personalDomains[0]?.totalSeconds ?? 0;\n const sharedAggregates: string[] = summary?.sharedAggregates ?? [];\n\n const pageProps = {\n title: \"Personal & Corporate\",\n description: \"Two contexts, one boundary you control\",\n purpose:\n \"TimeCampus keeps two worlds apart: your private life record, which stays yours, and the workspace you work in. This is where you decide exactly which aggregate work signals — never your personal context — travel up to your employer.\",\n } as const;\n\n const initialLoading =\n !workspaceId ||\n (policyLoading && !policyData) ||\n (summaryLoading && !summaryData);\n\n if (initialLoading) {\n return (\n <PageLayout {...pageProps}>\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <LoadingSkeleton variant=\"card\" count={1} />\n <LoadingSkeleton variant=\"card\" count={1} />\n <LoadingSkeleton variant=\"card\" count={1} />\n </div>\n </PageLayout>\n );\n }\n\n // Both core surfaces failed and we have nothing to show — calm, honest error.\n if (policyError && summaryError && !policy && !summary) {\n return (\n <PageLayout {...pageProps}>\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n We couldn't load your privacy settings right now. Please try again in\n a moment.\n </div>\n </PageLayout>\n );\n }\n\n const handlePolicyToggle = async (key: PolicyKey, next: boolean) => {\n if (!policy) return;\n const label = POLICY_TOGGLES.find((t) => t.key === key)?.label ?? \"signal\";\n setSavingPolicyKey(key);\n try {\n const res = await updatePolicy({\n variables: {\n input: {\n sustainableHours: policy.sustainableHours,\n recoveryBand: policy.recoveryBand,\n workLifeBoundary: policy.workLifeBoundary,\n [key]: next,\n },\n },\n });\n if (res.error) throw new Error(res.error.message);\n // Shared-aggregate list can change with the policy — keep Corporate honest.\n await refetchSummary();\n addToast({\n title: next ? `Now sharing \"${label}\"` : `Stopped sharing \"${label}\"`,\n description:\n \"Your workspace only ever sees this as an aggregate signal.\",\n variant: \"success\",\n });\n } catch {\n addToast({\n title: \"Couldn't update what you share\",\n description: \"Nothing was changed. Please try again.\",\n variant: \"error\",\n });\n } finally {\n setSavingPolicyKey(null);\n }\n };\n\n // BOFF-4202: let the user actually flip onDeviceOnly, not just see it.\n // Re-grants with the same mode + the flipped flag; grantSourceConsent's\n // upsert preserves `granted:true` on this path (only meaningful to toggle\n // on an already-connected source), so this never re-enables a revoked one.\n const handleOnDeviceOnlyToggle = async (\n consent: SourceConsent,\n next: boolean,\n ) => {\n const label = humanizeToken(consent.source);\n setSavingOnDeviceSource(consent.source);\n try {\n const res = await grantConsent({\n variables: {\n source: consent.source,\n mode: consent.mode,\n onDeviceOnly: next,\n },\n });\n if (res.error) throw new Error(res.error.message);\n await refetchConsents();\n addToast({\n title: next\n ? `${label} now on-device only`\n : `${label} on-device-only turned off`,\n description: next\n ? \"Only aggregated app/category/timing data reaches your workspace — no titles, URLs, or other details.\"\n : undefined,\n variant: \"success\",\n });\n } catch {\n addToast({\n title: \"Couldn't update on-device-only\",\n description: \"Nothing was changed. Please try again.\",\n variant: \"error\",\n });\n } finally {\n setSavingOnDeviceSource(null);\n }\n };\n\n const handleConsentToggle = async (consent: SourceConsent, next: boolean) => {\n const label = humanizeToken(consent.source);\n setSavingSource(consent.source);\n try {\n if (next) {\n const res = await grantConsent({\n variables: { source: consent.source },\n });\n if (res.error) throw new Error(res.error.message);\n } else {\n const res = await revokeConsent({\n variables: { source: consent.source },\n });\n if (res.error) throw new Error(res.error.message);\n }\n await refetchConsents();\n addToast({\n title: next ? `${label} connected` : `${label} disconnected`,\n variant: \"success\",\n });\n } catch {\n addToast({\n title: \"Couldn't update this source\",\n description: \"Nothing was changed. Please try again.\",\n variant: \"error\",\n });\n } finally {\n setSavingSource(null);\n }\n };\n\n return (\n <PageLayout {...pageProps}>\n {/* Three contexts: your private record · what you share · the workspace. */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* ── Personal (private to you) ─────────────────────────────────── */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-4 sm:p-6\">\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Lock className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Personal{\" \"}\n <span className=\"font-normal text-text-secondary\">\n (private to you)\n </span>\n </h2>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Everything you capture about your life. Locked to your account —\n it never leaves this side.\n </p>\n </div>\n </div>\n\n {summaryError && !summary ? (\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text\">\n Couldn't load your private data summary right now.\n </div>\n ) : !summary || summary.totalPersonalEvents === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"Nothing private captured yet\"\n description=\"Log your first life entry and it will live here — always private to you.\"\n />\n ) : (\n <div className=\"flex flex-1 flex-col gap-4\">\n <StatCard\n title=\"Private life entries\"\n value={summary.totalPersonalEvents.toLocaleString()}\n icon={<Lock className=\"h-5 w-5\" />}\n description=\"captured in the last 30 days\"\n />\n\n {personalDomains.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n No life domains logged in this window yet.\n </p>\n ) : (\n <ul className=\"space-y-2.5\">\n {personalDomains.map((d) => {\n const pct =\n maxDomainSeconds > 0\n ? Math.round((d.totalSeconds / maxDomainSeconds) * 100)\n : 0;\n return (\n <li\n key={d.domain}\n className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-3\"\n >\n <div className=\"flex items-center justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {lifeDomainLabel(d.domain)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatSecondsHuman(d.totalSeconds)} ·{\" \"}\n {d.eventCount.toLocaleString()}{\" \"}\n {d.eventCount === 1 ? \"entry\" : \"entries\"}\n </p>\n </div>\n <Lock\n className=\"h-3.5 w-3.5 shrink-0 text-text-muted\"\n aria-label=\"Private to you\"\n />\n </div>\n {/* Data-driven share bar of this window's private time. */}\n <div className=\"mt-2 h-1.5 w-full overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-action-primary-bg/60\"\n style={{ width: `${pct}%` }}\n />\n </div>\n </li>\n );\n })}\n </ul>\n )}\n </div>\n )}\n </GlassCard>\n\n {/* ── What you share up to work ─────────────────────────────────── */}\n <div className=\"flex flex-col gap-4\">\n <GlassCard\n treatment=\"insight\"\n glow\n className=\"flex flex-col p-4 sm:p-6\"\n >\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg/40 text-status-info-text\">\n <Share2 className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n What you share up to work\n </h2>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Aggregate signals only. Every switch is off until you turn it\n on.\n </p>\n </div>\n </div>\n\n {policyError && !policy ? (\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text\">\n Couldn't load your sharing settings right now.\n </div>\n ) : (\n <div className=\"space-y-3\">\n {POLICY_TOGGLES.map((toggle) => {\n const checked = policy ? policy[toggle.key] : false;\n const saving = savingPolicyKey === toggle.key;\n return (\n <div\n key={toggle.key}\n className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-3\"\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n {toggle.label}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {toggle.description}\n </p>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <ToggleSwitch\n checked={checked}\n disabled={saving || !policy}\n ariaLabel={`Share \"${toggle.label}\" with your workspace`}\n onChange={(next) =>\n handlePolicyToggle(toggle.key, next)\n }\n />\n <span\n className={cn(\n \"text-[0.65rem] font-medium uppercase tracking-wide\",\n checked\n ? \"text-status-success-text\"\n : \"text-text-muted\",\n )}\n >\n {saving ? \"Saving…\" : checked ? \"On\" : \"Off\"}\n </span>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n )}\n </GlassCard>\n\n {/* Strong, unmissable reassurance. */}\n <GlassCard\n treatment=\"metric\"\n className=\"border-status-success-border/40 p-4\"\n >\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-success-bg/40 text-status-success-text\">\n <ShieldCheck className=\"h-5 w-5\" />\n </div>\n <p className=\"text-sm font-semibold text-text-primary\">\n Your employer never sees your personal context.\n </p>\n </div>\n </GlassCard>\n </div>\n\n {/* ── Corporate (your workspace) ────────────────────────────────── */}\n <GlassCard treatment=\"glass\" className=\"flex flex-col p-4 sm:p-6\">\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Building2 className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h2 className=\"text-sm font-semibold text-text-primary\">\n Corporate{\" \"}\n <span className=\"font-normal text-text-secondary\">\n (your workspace)\n </span>\n </h2>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n What the workspace can actually see about you.\n </p>\n </div>\n </div>\n\n <div className=\"flex flex-1 flex-col gap-4\">\n <div className=\"flex items-start gap-2 rounded-lg border border-status-info-border/40 bg-status-info-bg/10 px-3 py-2.5 text-xs text-status-info-text\">\n <Info className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" />\n <span>\n Your workspace only ever receives aggregate work signals —\n coarse yes/no bands, never entries, locations, health or\n timestamps.\n </span>\n </div>\n\n <div>\n <p className=\"mb-2 text-xs font-medium uppercase tracking-wide text-text-muted\">\n Aggregate signals visible to work\n </p>\n {sharedAggregates.length === 0 ? (\n <div className=\"rounded-lg border border-border-subtle bg-bg-sunken/40 p-4 text-center\">\n <p className=\"text-sm font-medium text-text-primary\">\n Nothing is shared right now\n </p>\n <p className=\"mt-1 text-xs text-text-secondary\">\n Turn on a signal in the middle column and it will appear\n here.\n </p>\n </div>\n ) : (\n <ul className=\"space-y-2\">\n {sharedAggregates.map((agg) => (\n <li\n key={agg}\n className=\"flex items-center justify-between gap-3 rounded-lg border border-border-subtle bg-bg-sunken/40 px-3 py-2.5\"\n >\n <span className=\"flex min-w-0 items-center gap-2 text-sm text-text-primary\">\n <ArrowUpRight className=\"h-4 w-4 shrink-0 text-status-success-text\" />\n <span className=\"truncate\">{humanizeToken(agg)}</span>\n </span>\n <StatusBadge variant=\"success\" label=\"Shared\" dot />\n </li>\n ))}\n </ul>\n )}\n </div>\n\n <p className=\"mt-auto text-xs text-text-muted\">\n This mirrors exactly what you've allowed — change it any time from\n the switches on the left.\n </p>\n </div>\n </GlassCard>\n </div>\n\n {/* ── Connected sources & consent ─────────────────────────────────── */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-4 sm:p-6\">\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Plug className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Connected sources\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Each capture source only feeds your private record once you\n consent — and you can withdraw at any moment.\n </p>\n </div>\n </div>\n\n {consentsError && consents.length === 0 ? (\n <div className=\"rounded-lg border border-status-error-border/50 bg-status-error-bg/10 p-3 text-xs text-status-error-text\">\n Couldn't load your connected sources right now.\n </div>\n ) : consentsLoading && consents.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={3} />\n ) : consents.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No sources connected yet\"\n description=\"When you connect a capture source — a calendar, location or health signal — you'll grant and revoke exactly what it may feed here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3\">\n {consents.map((consent) => {\n const saving = savingSource === consent.source;\n return (\n <GlassCard key={consent.id} treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {humanizeToken(consent.source)}\n </p>\n <div className=\"mt-1.5 flex flex-wrap items-center gap-1.5\">\n <StatusBadge\n variant=\"muted\"\n label={humanizeToken(consent.mode)}\n />\n {consent.granted && (\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={consent.onDeviceOnly}\n disabled={savingOnDeviceSource === consent.source}\n onClick={() =>\n handleOnDeviceOnlyToggle(\n consent,\n !consent.onDeviceOnly,\n )\n }\n className={cn(\n \"rounded-full border px-2 py-0.5 text-[0.65rem] font-medium uppercase tracking-wide transition-colors\",\n \"focus:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-bg/50\",\n consent.onDeviceOnly\n ? \"border-status-info-border/60 bg-status-info-bg/40 text-status-info-text\"\n : \"border-border-subtle bg-bg-sunken/40 text-text-muted hover:text-text-secondary\",\n savingOnDeviceSource === consent.source &&\n \"cursor-not-allowed opacity-50\",\n )}\n >\n {savingOnDeviceSource === consent.source\n ? \"Saving…\"\n : consent.onDeviceOnly\n ? \"On-device only\"\n : \"Turn on on-device only\"}\n </button>\n )}\n {consent.retentionDays != null && (\n <StatusBadge\n variant=\"muted\"\n label={`Kept ${consent.retentionDays}d`}\n />\n )}\n </div>\n </div>\n <div className=\"flex shrink-0 flex-col items-end gap-1\">\n <ToggleSwitch\n checked={consent.granted}\n disabled={saving}\n ariaLabel={`Consent for ${humanizeToken(consent.source)}`}\n onChange={(next) => handleConsentToggle(consent, next)}\n />\n <span\n className={cn(\n \"text-[0.65rem] font-medium uppercase tracking-wide\",\n consent.granted\n ? \"text-status-success-text\"\n : \"text-text-muted\",\n )}\n >\n {saving\n ? \"Saving…\"\n : consent.granted\n ? \"Granted\"\n : \"Revoked\"}\n </span>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </GlassCard>\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -42,7 +42,7 @@ function V(e) {
42
42
  return e === B.New || e === B.Snoozed;
43
43
  }
44
44
  function ne() {
45
- return new Date(Date.now() + 7 * 864e5).toISOString();
45
+ return new Date(Date.now() + 6048e5).toISOString();
46
46
  }
47
47
  var H = (e) => Math.min(1, Math.max(0, e)), U = (e) => `${Math.round(H(e) * 100)}%`;
48
48
  function W(e) {
@@ -1 +1 @@
1
- {"version":3,"file":"RecommendationsPage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/RecommendationsPage.tsx"],"sourcesContent":["import { useMemo, useState, type ComponentType } from \"react\";\nimport {\n Sparkles,\n Check,\n CheckCircle2,\n X,\n Clock,\n ShieldCheck,\n Lightbulb,\n Briefcase,\n Moon,\n HeartPulse,\n Users,\n GraduationCap,\n Coffee,\n Bus,\n Hourglass,\n Home,\n Heart,\n Handshake,\n Wallet,\n Circle,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n ToastContainer,\n useToast,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport { TimeCampusLifeDomain } from \"@/generated/wspace-types\";\nimport {\n useMyRecommendationsQuery,\n useGenerateMyRecommendationsMutation,\n useAcceptRecommendationMutation,\n useDismissRecommendationMutation,\n useSnoozeRecommendationMutation,\n type RecommendationFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { lifeDomainLabel } from \"./domains\";\n\ntype RecItem = RecommendationFieldsFragment;\n\n// ---------------------------------------------------------------------------\n// Recommendation states are stored as free strings on the backend\n// (NEW / ACCEPTED / DISMISSED / SNOOZED / EXPIRED — see the svc schema). We\n// segment the feed by them; unknown states simply don't match a tab.\n// ---------------------------------------------------------------------------\nconst REC_STATE = {\n New: \"NEW\",\n Snoozed: \"SNOOZED\",\n Accepted: \"ACCEPTED\",\n Dismissed: \"DISMISSED\",\n Expired: \"EXPIRED\",\n} as const;\n\nconst SEGMENTS = [\n { key: REC_STATE.New, label: \"New\" },\n { key: REC_STATE.Snoozed, label: \"Snoozed\" },\n { key: REC_STATE.Accepted, label: \"Accepted\" },\n { key: REC_STATE.Dismissed, label: \"Dismissed\" },\n] as const;\n\ntype SegmentKey = (typeof SEGMENTS)[number][\"key\"];\n\n/** A NEW or SNOOZED recommendation is still actionable (accept / snooze / dismiss). */\nfunction isActionable(state: string): boolean {\n return state === REC_STATE.New || state === REC_STATE.Snoozed;\n}\n\n/** Snooze a week out — long enough to be non-nagging, short enough to resurface. */\nfunction snoozeUntilIso(): string {\n return new Date(Date.now() + 7 * 86_400_000).toISOString();\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\nconst asPercent = (n: number) => `${Math.round(clamp01(n) * 100)}%`;\n\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n });\n}\n\n/** Humanise a machine \"kind\" (e.g. FOCUS_DROP → \"Focus Drop\") for the eyebrow. */\nfunction humaniseKind(kind: string): string {\n return kind\n .toLowerCase()\n .split(/[_\\s]+/)\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n}\n\nconst DOMAIN_ICON: Record<\n TimeCampusLifeDomain,\n ComponentType<{ className?: string }>\n> = {\n [TimeCampusLifeDomain.Work]: Briefcase,\n [TimeCampusLifeDomain.Sleep]: Moon,\n [TimeCampusLifeDomain.Health]: HeartPulse,\n [TimeCampusLifeDomain.Family]: Users,\n [TimeCampusLifeDomain.Learning]: GraduationCap,\n [TimeCampusLifeDomain.Leisure]: Coffee,\n [TimeCampusLifeDomain.Commute]: Bus,\n [TimeCampusLifeDomain.Waiting]: Hourglass,\n [TimeCampusLifeDomain.Chores]: Home,\n [TimeCampusLifeDomain.SelfCare]: Heart,\n [TimeCampusLifeDomain.Social]: Handshake,\n [TimeCampusLifeDomain.FinanceAdmin]: Wallet,\n [TimeCampusLifeDomain.Reflection]: Sparkles,\n [TimeCampusLifeDomain.Other]: Circle,\n};\n\nfunction recIcon(\n domain?: TimeCampusLifeDomain | null,\n): ComponentType<{ className?: string }> {\n if (!domain) return Lightbulb;\n return DOMAIN_ICON[domain] ?? Lightbulb;\n}\n\ntype BadgeTone = \"success\" | \"warning\" | \"muted\" | \"info\";\n\nfunction stateBadge(state: string): { variant: BadgeTone; label: string } {\n switch (state) {\n case REC_STATE.Accepted:\n return { variant: \"success\", label: \"Accepted\" };\n case REC_STATE.Snoozed:\n return { variant: \"warning\", label: \"Snoozed\" };\n case REC_STATE.Dismissed:\n return { variant: \"muted\", label: \"Dismissed\" };\n case REC_STATE.Expired:\n return { variant: \"muted\", label: \"Expired\" };\n default:\n return { variant: \"info\", label: \"New\" };\n }\n}\n\nconst primaryBtn =\n \"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\";\nconst neutralBtn =\n \"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-1.5 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\";\nconst chipClass =\n \"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\";\n\n// ---------------------------------------------------------------------------\n// Recommendation card\n// ---------------------------------------------------------------------------\ninterface RecommendationCardProps {\n rec: RecItem;\n busy: boolean;\n onAccept: (id: string) => void;\n onSnooze: (id: string) => void;\n onDismiss: (id: string) => void;\n}\n\nfunction RecommendationCard({\n rec,\n busy,\n onAccept,\n onSnooze,\n onDismiss,\n}: RecommendationCardProps) {\n const Icon = recIcon(rec.domain);\n const actionable = isActionable(rec.state);\n const badge = stateBadge(rec.state);\n\n return (\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-3 p-4 sm:p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Icon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {humaniseKind(rec.kind)}\n </p>\n <h3 className=\"mt-0.5 text-base font-semibold text-text-primary\">\n {rec.title}\n </h3>\n </div>\n </div>\n {rec.confidence != null && (\n <StatusBadge\n variant=\"info\"\n label={`${asPercent(rec.confidence)} confident`}\n className=\"shrink-0\"\n />\n )}\n </div>\n\n <p className=\"text-sm text-text-secondary\">{rec.rationale}</p>\n\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {rec.domain && (\n <span className={chipClass}>{lifeDomainLabel(rec.domain)}</span>\n )}\n {rec.valueAlignment != null && (\n <span className={chipClass}>\n <ShieldCheck className=\"h-3 w-3\" />\n Aligns with your values · {asPercent(rec.valueAlignment)}\n </span>\n )}\n {rec.state === REC_STATE.Snoozed && rec.snoozeUntil && (\n <span className={chipClass}>\n <Clock className=\"h-3 w-3\" />\n Snoozed until {formatDate(rec.snoozeUntil)}\n </span>\n )}\n </div>\n\n {actionable ? (\n <div className=\"flex flex-wrap items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => onAccept(rec.id)}\n disabled={busy}\n className={primaryBtn}\n >\n <Check className=\"h-4 w-4\" />\n Accept\n </button>\n <button\n type=\"button\"\n onClick={() => onSnooze(rec.id)}\n disabled={busy}\n className={neutralBtn}\n >\n <Clock className=\"h-4 w-4\" />\n Snooze\n </button>\n <button\n type=\"button\"\n onClick={() => onDismiss(rec.id)}\n disabled={busy}\n className={neutralBtn}\n >\n <X className=\"h-4 w-4\" />\n Dismiss\n </button>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 pt-1\">\n <StatusBadge variant={badge.variant} label={badge.label} />\n <span className=\"text-xs text-text-secondary\">\n {formatDate(rec.createdAt)}\n </span>\n </div>\n )}\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Page\n// ---------------------------------------------------------------------------\nfunction segmentEmptyCopy(segment: SegmentKey): {\n title: string;\n description: string;\n} {\n switch (segment) {\n case REC_STATE.Snoozed:\n return {\n title: \"Nothing snoozed\",\n description:\n \"Suggestions you snooze for later will wait here quietly until they're worth another look.\",\n };\n case REC_STATE.Accepted:\n return {\n title: \"Nothing accepted yet\",\n description:\n \"When you accept a suggestion it moves here, so you can see the changes you chose to make.\",\n };\n case REC_STATE.Dismissed:\n return {\n title: \"Nothing dismissed\",\n description:\n \"Suggestions you dismiss are kept here — TimeCampus learns from them and won't keep nagging.\",\n };\n default:\n return {\n title: \"You're all caught up\",\n description:\n \"No new suggestions right now. Generate again after a few more days of tracked time.\",\n };\n }\n}\n\nexport function RecommendationsPage() {\n const { workspaceId } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n const [segment, setSegment] = useState<SegmentKey>(REC_STATE.New);\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const { data, loading, error, refetch } = useMyRecommendationsQuery({\n variables: { state: null },\n skip: !workspaceId,\n });\n\n const [generate, { loading: generating }] =\n useGenerateMyRecommendationsMutation();\n const [acceptRec] = useAcceptRecommendationMutation();\n const [dismissRec] = useDismissRecommendationMutation();\n const [snoozeRec] = useSnoozeRecommendationMutation();\n\n const recommendations: RecItem[] = useMemo(\n () => data?.myRecommendations ?? [],\n [data?.myRecommendations],\n );\n\n const counts = useMemo(() => {\n const map: Record<string, number> = {};\n for (const rec of recommendations) {\n map[rec.state] = (map[rec.state] ?? 0) + 1;\n }\n return map;\n }, [recommendations]);\n\n const visible = useMemo(\n () => recommendations.filter((rec) => rec.state === segment),\n [recommendations, segment],\n );\n\n const newCount = counts[REC_STATE.New] ?? 0;\n const acceptedCount = counts[REC_STATE.Accepted] ?? 0;\n const snoozedCount = counts[REC_STATE.Snoozed] ?? 0;\n\n const handleGenerate = async () => {\n try {\n const res = await generate();\n const generated = res.data?.generateMyRecommendations ?? [];\n addToast({\n title:\n generated.length > 0\n ? `${generated.length} suggestion${generated.length === 1 ? \"\" : \"s\"} ready`\n : \"No new suggestions\",\n description:\n generated.length > 0\n ? \"Fresh, values-aware ideas are in your New feed.\"\n : \"You're all caught up — nothing new to suggest right now.\",\n variant: generated.length > 0 ? \"success\" : \"info\",\n });\n setSegment(REC_STATE.New);\n await refetch();\n } catch {\n addToast({\n title: \"Couldn't generate recommendations\",\n description: \"Please try again in a moment.\",\n variant: \"error\",\n });\n }\n };\n\n const handleAccept = async (id: string) => {\n setBusyId(id);\n try {\n await acceptRec({ variables: { id } });\n addToast({ title: \"Accepted — nice\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Couldn't accept that one\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleSnooze = async (id: string) => {\n setBusyId(id);\n try {\n await snoozeRec({ variables: { id, until: snoozeUntilIso() } });\n addToast({\n title: \"Snoozed for a week\",\n description: \"It'll resurface later — no nagging in between.\",\n variant: \"info\",\n });\n await refetch();\n } catch {\n addToast({ title: \"Couldn't snooze that one\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleDismiss = async (id: string) => {\n setBusyId(id);\n try {\n await dismissRec({ variables: { id } });\n addToast({ title: \"Dismissed\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Couldn't dismiss that one\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const generateButton = (\n <button\n type=\"button\"\n onClick={handleGenerate}\n disabled={generating || !workspaceId}\n className={primaryBtn}\n >\n <Sparkles className=\"h-4 w-4\" />\n {generating ? \"Generating…\" : \"Generate recommendations\"}\n </button>\n );\n\n const purpose =\n \"Gentle, values-aware nudges built from your real timeline — protect a focus block, \" +\n \"reclaim a commute, guard family time. Accept the ones that fit, snooze the rest, and \" +\n \"dismiss what doesn't. TimeCampus never optimises for more output — only for time that \" +\n \"matches what you value.\";\n\n const footerNote = (\n <GlassCard treatment=\"insight\" className=\"mt-6 p-4 sm:p-5\">\n <div className=\"flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg text-status-info-text\">\n <ShieldCheck className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Never optimises for more output.\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Every suggestion protects something you value — rest, focus,\n relationships — not your throughput. Snooze or dismiss freely;\n TimeCampus takes the hint and won't keep nagging.\n </p>\n </div>\n </div>\n </GlassCard>\n );\n\n if (!workspaceId || loading) {\n return (\n <PageLayout\n title=\"Recommendations\"\n description=\"Values-aware suggestions from your real timeline\"\n purpose={purpose}\n actions={generateButton}\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Recommendations\"\n description=\"Values-aware suggestions from your real timeline\"\n purpose={purpose}\n actions={generateButton}\n >\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Couldn&apos;t load your recommendations right now. Please try again in\n a moment.\n </div>\n </PageLayout>\n );\n }\n\n const hasAny = recommendations.length > 0;\n const emptyCopy = segmentEmptyCopy(segment);\n\n return (\n <PageLayout\n title=\"Recommendations\"\n description=\"Values-aware suggestions from your real timeline\"\n purpose={purpose}\n actions={generateButton}\n >\n {!hasAny ? (\n <>\n <EmptyState\n illustration=\"empty-data\"\n title=\"No recommendations yet\"\n description=\"TimeCampus turns your real timeline into gentle, values-aware suggestions — like protecting a focus block or reclaiming a commute. Generate your first set to see what it notices.\"\n action={generateButton}\n />\n {footerNote}\n </>\n ) : (\n <div className=\"space-y-6\">\n {/* Summary of where each suggestion stands — from the query only. */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <StatCard\n featured\n title=\"New for you\"\n value={String(newCount)}\n icon={<Sparkles className=\"h-5 w-5\" />}\n description=\"Awaiting your call\"\n />\n <StatCard\n title=\"Accepted\"\n value={String(acceptedCount)}\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n description=\"Changes you chose to make\"\n />\n <StatCard\n title=\"Snoozed\"\n value={String(snoozedCount)}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"Waiting for a better moment\"\n />\n </div>\n\n {/* Segmented control by state. */}\n <div className=\"overflow-x-auto\">\n <div\n className=\"inline-flex items-center rounded-lg border border-border-subtle bg-bg-sunken/50 p-0.5\"\n role=\"tablist\"\n aria-label=\"Filter recommendations by state\"\n >\n {SEGMENTS.map((seg) => {\n const active = seg.key === segment;\n const count = counts[seg.key] ?? 0;\n return (\n <button\n key={seg.key}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => setSegment(seg.key)}\n className={\n active\n ? \"flex items-center gap-1.5 rounded-md bg-bg-surface px-3 py-1.5 text-sm font-medium text-text-primary shadow-sm\"\n : \"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-text-secondary transition-colors hover:text-text-primary\"\n }\n >\n {seg.label}\n <span\n className={\n active\n ? \"rounded-full bg-action-primary-bg/15 px-1.5 text-xs font-semibold text-primary\"\n : \"rounded-full bg-bg-sunken px-1.5 text-xs font-semibold text-text-secondary\"\n }\n >\n {count}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* The feed for the active segment. */}\n {visible.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={emptyCopy.title}\n description={emptyCopy.description}\n action={segment === REC_STATE.New ? generateButton : undefined}\n />\n ) : (\n <div className=\"space-y-4\">\n {visible.map((rec) => (\n <RecommendationCard\n key={rec.id}\n rec={rec}\n busy={busyId === rec.id}\n onAccept={handleAccept}\n onSnooze={handleSnooze}\n onDismiss={handleDismiss}\n />\n ))}\n </div>\n )}\n\n {footerNote}\n </div>\n )}\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"RecommendationsPage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/RecommendationsPage.tsx"],"sourcesContent":["import { useMemo, useState, type ComponentType } from \"react\";\nimport {\n Sparkles,\n Check,\n CheckCircle2,\n X,\n Clock,\n ShieldCheck,\n Lightbulb,\n Briefcase,\n Moon,\n HeartPulse,\n Users,\n GraduationCap,\n Coffee,\n Bus,\n Hourglass,\n Home,\n Heart,\n Handshake,\n Wallet,\n Circle,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport {\n PageLayout,\n StatCard,\n StatusBadge,\n EmptyState,\n LoadingSkeleton,\n ToastContainer,\n useToast,\n} from \"../../components\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport { TimeCampusLifeDomain } from \"@/generated/wspace-types\";\nimport {\n useMyRecommendationsQuery,\n useGenerateMyRecommendationsMutation,\n useAcceptRecommendationMutation,\n useDismissRecommendationMutation,\n useSnoozeRecommendationMutation,\n type RecommendationFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { lifeDomainLabel } from \"./domains\";\n\ntype RecItem = RecommendationFieldsFragment;\n\n// ---------------------------------------------------------------------------\n// Recommendation states are stored as free strings on the backend\n// (NEW / ACCEPTED / DISMISSED / SNOOZED / EXPIRED — see the svc schema). We\n// segment the feed by them; unknown states simply don't match a tab.\n// ---------------------------------------------------------------------------\nconst REC_STATE = {\n New: \"NEW\",\n Snoozed: \"SNOOZED\",\n Accepted: \"ACCEPTED\",\n Dismissed: \"DISMISSED\",\n Expired: \"EXPIRED\",\n} as const;\n\nconst SEGMENTS = [\n { key: REC_STATE.New, label: \"New\" },\n { key: REC_STATE.Snoozed, label: \"Snoozed\" },\n { key: REC_STATE.Accepted, label: \"Accepted\" },\n { key: REC_STATE.Dismissed, label: \"Dismissed\" },\n] as const;\n\ntype SegmentKey = (typeof SEGMENTS)[number][\"key\"];\n\n/** A NEW or SNOOZED recommendation is still actionable (accept / snooze / dismiss). */\nfunction isActionable(state: string): boolean {\n return state === REC_STATE.New || state === REC_STATE.Snoozed;\n}\n\n/** Snooze a week out — long enough to be non-nagging, short enough to resurface. */\nfunction snoozeUntilIso(): string {\n return new Date(Date.now() + 7 * 86_400_000).toISOString();\n}\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\nconst asPercent = (n: number) => `${Math.round(clamp01(n) * 100)}%`;\n\nfunction formatDate(iso: string): string {\n return new Date(iso).toLocaleDateString(undefined, {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n });\n}\n\n/** Humanise a machine \"kind\" (e.g. FOCUS_DROP → \"Focus Drop\") for the eyebrow. */\nfunction humaniseKind(kind: string): string {\n return kind\n .toLowerCase()\n .split(/[_\\s]+/)\n .filter(Boolean)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(\" \");\n}\n\nconst DOMAIN_ICON: Record<\n TimeCampusLifeDomain,\n ComponentType<{ className?: string }>\n> = {\n [TimeCampusLifeDomain.Work]: Briefcase,\n [TimeCampusLifeDomain.Sleep]: Moon,\n [TimeCampusLifeDomain.Health]: HeartPulse,\n [TimeCampusLifeDomain.Family]: Users,\n [TimeCampusLifeDomain.Learning]: GraduationCap,\n [TimeCampusLifeDomain.Leisure]: Coffee,\n [TimeCampusLifeDomain.Commute]: Bus,\n [TimeCampusLifeDomain.Waiting]: Hourglass,\n [TimeCampusLifeDomain.Chores]: Home,\n [TimeCampusLifeDomain.SelfCare]: Heart,\n [TimeCampusLifeDomain.Social]: Handshake,\n [TimeCampusLifeDomain.FinanceAdmin]: Wallet,\n [TimeCampusLifeDomain.Reflection]: Sparkles,\n [TimeCampusLifeDomain.Other]: Circle,\n};\n\nfunction recIcon(\n domain?: TimeCampusLifeDomain | null,\n): ComponentType<{ className?: string }> {\n if (!domain) return Lightbulb;\n return DOMAIN_ICON[domain] ?? Lightbulb;\n}\n\ntype BadgeTone = \"success\" | \"warning\" | \"muted\" | \"info\";\n\nfunction stateBadge(state: string): { variant: BadgeTone; label: string } {\n switch (state) {\n case REC_STATE.Accepted:\n return { variant: \"success\", label: \"Accepted\" };\n case REC_STATE.Snoozed:\n return { variant: \"warning\", label: \"Snoozed\" };\n case REC_STATE.Dismissed:\n return { variant: \"muted\", label: \"Dismissed\" };\n case REC_STATE.Expired:\n return { variant: \"muted\", label: \"Expired\" };\n default:\n return { variant: \"info\", label: \"New\" };\n }\n}\n\nconst primaryBtn =\n \"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\";\nconst neutralBtn =\n \"inline-flex items-center gap-1.5 rounded-md border border-border-subtle px-3 py-1.5 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\";\nconst chipClass =\n \"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\";\n\n// ---------------------------------------------------------------------------\n// Recommendation card\n// ---------------------------------------------------------------------------\ninterface RecommendationCardProps {\n rec: RecItem;\n busy: boolean;\n onAccept: (id: string) => void;\n onSnooze: (id: string) => void;\n onDismiss: (id: string) => void;\n}\n\nfunction RecommendationCard({\n rec,\n busy,\n onAccept,\n onSnooze,\n onDismiss,\n}: RecommendationCardProps) {\n const Icon = recIcon(rec.domain);\n const actionable = isActionable(rec.state);\n const badge = stateBadge(rec.state);\n\n return (\n <GlassCard treatment=\"glass\" className=\"flex flex-col gap-3 p-4 sm:p-5\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"flex min-w-0 items-center gap-3\">\n <span className=\"flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Icon className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs font-medium uppercase tracking-wide text-text-secondary\">\n {humaniseKind(rec.kind)}\n </p>\n <h3 className=\"mt-0.5 text-base font-semibold text-text-primary\">\n {rec.title}\n </h3>\n </div>\n </div>\n {rec.confidence != null && (\n <StatusBadge\n variant=\"info\"\n label={`${asPercent(rec.confidence)} confident`}\n className=\"shrink-0\"\n />\n )}\n </div>\n\n <p className=\"text-sm text-text-secondary\">{rec.rationale}</p>\n\n <div className=\"flex flex-wrap items-center gap-1.5\">\n {rec.domain && (\n <span className={chipClass}>{lifeDomainLabel(rec.domain)}</span>\n )}\n {rec.valueAlignment != null && (\n <span className={chipClass}>\n <ShieldCheck className=\"h-3 w-3\" />\n Aligns with your values · {asPercent(rec.valueAlignment)}\n </span>\n )}\n {rec.state === REC_STATE.Snoozed && rec.snoozeUntil && (\n <span className={chipClass}>\n <Clock className=\"h-3 w-3\" />\n Snoozed until {formatDate(rec.snoozeUntil)}\n </span>\n )}\n </div>\n\n {actionable ? (\n <div className=\"flex flex-wrap items-center gap-2 pt-1\">\n <button\n type=\"button\"\n onClick={() => onAccept(rec.id)}\n disabled={busy}\n className={primaryBtn}\n >\n <Check className=\"h-4 w-4\" />\n Accept\n </button>\n <button\n type=\"button\"\n onClick={() => onSnooze(rec.id)}\n disabled={busy}\n className={neutralBtn}\n >\n <Clock className=\"h-4 w-4\" />\n Snooze\n </button>\n <button\n type=\"button\"\n onClick={() => onDismiss(rec.id)}\n disabled={busy}\n className={neutralBtn}\n >\n <X className=\"h-4 w-4\" />\n Dismiss\n </button>\n </div>\n ) : (\n <div className=\"flex items-center justify-between gap-2 pt-1\">\n <StatusBadge variant={badge.variant} label={badge.label} />\n <span className=\"text-xs text-text-secondary\">\n {formatDate(rec.createdAt)}\n </span>\n </div>\n )}\n </GlassCard>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Page\n// ---------------------------------------------------------------------------\nfunction segmentEmptyCopy(segment: SegmentKey): {\n title: string;\n description: string;\n} {\n switch (segment) {\n case REC_STATE.Snoozed:\n return {\n title: \"Nothing snoozed\",\n description:\n \"Suggestions you snooze for later will wait here quietly until they're worth another look.\",\n };\n case REC_STATE.Accepted:\n return {\n title: \"Nothing accepted yet\",\n description:\n \"When you accept a suggestion it moves here, so you can see the changes you chose to make.\",\n };\n case REC_STATE.Dismissed:\n return {\n title: \"Nothing dismissed\",\n description:\n \"Suggestions you dismiss are kept here — TimeCampus learns from them and won't keep nagging.\",\n };\n default:\n return {\n title: \"You're all caught up\",\n description:\n \"No new suggestions right now. Generate again after a few more days of tracked time.\",\n };\n }\n}\n\nexport function RecommendationsPage() {\n const { workspaceId } = useTimeCampus();\n const { toasts, addToast, dismissToast } = useToast();\n const [segment, setSegment] = useState<SegmentKey>(REC_STATE.New);\n const [busyId, setBusyId] = useState<string | null>(null);\n\n const { data, loading, error, refetch } = useMyRecommendationsQuery({\n variables: { state: null },\n skip: !workspaceId,\n });\n\n const [generate, { loading: generating }] =\n useGenerateMyRecommendationsMutation();\n const [acceptRec] = useAcceptRecommendationMutation();\n const [dismissRec] = useDismissRecommendationMutation();\n const [snoozeRec] = useSnoozeRecommendationMutation();\n\n const recommendations: RecItem[] = useMemo(\n () => data?.myRecommendations ?? [],\n [data?.myRecommendations],\n );\n\n const counts = useMemo(() => {\n const map: Record<string, number> = {};\n for (const rec of recommendations) {\n map[rec.state] = (map[rec.state] ?? 0) + 1;\n }\n return map;\n }, [recommendations]);\n\n const visible = useMemo(\n () => recommendations.filter((rec) => rec.state === segment),\n [recommendations, segment],\n );\n\n const newCount = counts[REC_STATE.New] ?? 0;\n const acceptedCount = counts[REC_STATE.Accepted] ?? 0;\n const snoozedCount = counts[REC_STATE.Snoozed] ?? 0;\n\n const handleGenerate = async () => {\n try {\n const res = await generate();\n const generated = res.data?.generateMyRecommendations ?? [];\n addToast({\n title:\n generated.length > 0\n ? `${generated.length} suggestion${generated.length === 1 ? \"\" : \"s\"} ready`\n : \"No new suggestions\",\n description:\n generated.length > 0\n ? \"Fresh, values-aware ideas are in your New feed.\"\n : \"You're all caught up — nothing new to suggest right now.\",\n variant: generated.length > 0 ? \"success\" : \"info\",\n });\n setSegment(REC_STATE.New);\n await refetch();\n } catch {\n addToast({\n title: \"Couldn't generate recommendations\",\n description: \"Please try again in a moment.\",\n variant: \"error\",\n });\n }\n };\n\n const handleAccept = async (id: string) => {\n setBusyId(id);\n try {\n await acceptRec({ variables: { id } });\n addToast({ title: \"Accepted — nice\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Couldn't accept that one\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleSnooze = async (id: string) => {\n setBusyId(id);\n try {\n await snoozeRec({ variables: { id, until: snoozeUntilIso() } });\n addToast({\n title: \"Snoozed for a week\",\n description: \"It'll resurface later — no nagging in between.\",\n variant: \"info\",\n });\n await refetch();\n } catch {\n addToast({ title: \"Couldn't snooze that one\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const handleDismiss = async (id: string) => {\n setBusyId(id);\n try {\n await dismissRec({ variables: { id } });\n addToast({ title: \"Dismissed\", variant: \"success\" });\n await refetch();\n } catch {\n addToast({ title: \"Couldn't dismiss that one\", variant: \"error\" });\n } finally {\n setBusyId(null);\n }\n };\n\n const generateButton = (\n <button\n type=\"button\"\n onClick={handleGenerate}\n disabled={generating || !workspaceId}\n className={primaryBtn}\n >\n <Sparkles className=\"h-4 w-4\" />\n {generating ? \"Generating…\" : \"Generate recommendations\"}\n </button>\n );\n\n const purpose =\n \"Gentle, values-aware nudges built from your real timeline — protect a focus block, \" +\n \"reclaim a commute, guard family time. Accept the ones that fit, snooze the rest, and \" +\n \"dismiss what doesn't. TimeCampus never optimises for more output — only for time that \" +\n \"matches what you value.\";\n\n const footerNote = (\n <GlassCard treatment=\"insight\" className=\"mt-6 p-4 sm:p-5\">\n <div className=\"flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg text-status-info-text\">\n <ShieldCheck className=\"h-5 w-5\" />\n </span>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Never optimises for more output.\n </h3>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Every suggestion protects something you value — rest, focus,\n relationships — not your throughput. Snooze or dismiss freely;\n TimeCampus takes the hint and won't keep nagging.\n </p>\n </div>\n </div>\n </GlassCard>\n );\n\n if (!workspaceId || loading) {\n return (\n <PageLayout\n title=\"Recommendations\"\n description=\"Values-aware suggestions from your real timeline\"\n purpose={purpose}\n actions={generateButton}\n >\n <LoadingSkeleton variant=\"list\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout\n title=\"Recommendations\"\n description=\"Values-aware suggestions from your real timeline\"\n purpose={purpose}\n actions={generateButton}\n >\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n Couldn&apos;t load your recommendations right now. Please try again in\n a moment.\n </div>\n </PageLayout>\n );\n }\n\n const hasAny = recommendations.length > 0;\n const emptyCopy = segmentEmptyCopy(segment);\n\n return (\n <PageLayout\n title=\"Recommendations\"\n description=\"Values-aware suggestions from your real timeline\"\n purpose={purpose}\n actions={generateButton}\n >\n {!hasAny ? (\n <>\n <EmptyState\n illustration=\"empty-data\"\n title=\"No recommendations yet\"\n description=\"TimeCampus turns your real timeline into gentle, values-aware suggestions — like protecting a focus block or reclaiming a commute. Generate your first set to see what it notices.\"\n action={generateButton}\n />\n {footerNote}\n </>\n ) : (\n <div className=\"space-y-6\">\n {/* Summary of where each suggestion stands — from the query only. */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <StatCard\n featured\n title=\"New for you\"\n value={String(newCount)}\n icon={<Sparkles className=\"h-5 w-5\" />}\n description=\"Awaiting your call\"\n />\n <StatCard\n title=\"Accepted\"\n value={String(acceptedCount)}\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n description=\"Changes you chose to make\"\n />\n <StatCard\n title=\"Snoozed\"\n value={String(snoozedCount)}\n icon={<Clock className=\"h-5 w-5\" />}\n description=\"Waiting for a better moment\"\n />\n </div>\n\n {/* Segmented control by state. */}\n <div className=\"overflow-x-auto\">\n <div\n className=\"inline-flex items-center rounded-lg border border-border-subtle bg-bg-sunken/50 p-0.5\"\n role=\"tablist\"\n aria-label=\"Filter recommendations by state\"\n >\n {SEGMENTS.map((seg) => {\n const active = seg.key === segment;\n const count = counts[seg.key] ?? 0;\n return (\n <button\n key={seg.key}\n type=\"button\"\n role=\"tab\"\n aria-selected={active}\n onClick={() => setSegment(seg.key)}\n className={\n active\n ? \"flex items-center gap-1.5 rounded-md bg-bg-surface px-3 py-1.5 text-sm font-medium text-text-primary shadow-sm\"\n : \"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-sm font-medium text-text-secondary transition-colors hover:text-text-primary\"\n }\n >\n {seg.label}\n <span\n className={\n active\n ? \"rounded-full bg-action-primary-bg/15 px-1.5 text-xs font-semibold text-primary\"\n : \"rounded-full bg-bg-sunken px-1.5 text-xs font-semibold text-text-secondary\"\n }\n >\n {count}\n </span>\n </button>\n );\n })}\n </div>\n </div>\n\n {/* The feed for the active segment. */}\n {visible.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={emptyCopy.title}\n description={emptyCopy.description}\n action={segment === REC_STATE.New ? generateButton : undefined}\n />\n ) : (\n <div className=\"space-y-4\">\n {visible.map((rec) => (\n <RecommendationCard\n key={rec.id}\n rec={rec}\n busy={busyId === rec.id}\n onAccept={handleAccept}\n onSnooze={handleSnooze}\n onDismiss={handleDismiss}\n />\n ))}\n </div>\n )}\n\n {footerNote}\n </div>\n )}\n\n <ToastContainer toasts={toasts} onDismiss={dismissToast} />\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TimeWellSpentPage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/TimeWellSpentPage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n Heart,\n Clock,\n Radar,\n Layers,\n Compass,\n Scale,\n Info,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n ChartCard,\n RadialGauge,\n ProgressMeter,\n StatusBadge,\n type MeterTone,\n} from \"../../components\";\nimport { cn } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useMyTimeWellSpentQuery,\n type MyTimeWellSpentQuery,\n} from \"@/generated/wspace-operations\";\nimport {\n lifeDomainLabel,\n secondsToHours,\n formatSecondsHuman,\n isoDaysAgo,\n isoNow,\n} from \"./domains\";\n\ntype DomainAlignment =\n MyTimeWellSpentQuery[\"myTimeWellSpent\"][\"byDomain\"][number];\n\n/** Range presets for the score window. */\nconst RANGE_OPTIONS = [\n { label: \"7 days\", value: 7 },\n { label: \"30 days\", value: 30 },\n] as const;\n\n/** Below this fraction of the window accounted for, the score is only a rough estimate. */\nconst LOW_COVERAGE = 0.5;\n\ntype AlignLevel = \"aligned\" | \"slight\" | \"off\";\n\n/** Classify a domain's alignment from |actualShare − desiredShare|. */\nfunction alignLevel(gap: number): AlignLevel {\n const magnitude = Math.abs(gap);\n if (magnitude <= 0.05) return \"aligned\";\n if (magnitude <= 0.12) return \"slight\";\n return \"off\";\n}\n\nconst BADGE_VARIANT: Record<AlignLevel, \"success\" | \"warning\" | \"error\"> = {\n aligned: \"success\",\n slight: \"warning\",\n off: \"error\",\n};\n\nconst METER_TONE: Record<AlignLevel, MeterTone> = {\n aligned: \"success\",\n slight: \"warning\",\n off: \"danger\",\n};\n\n/** Directional, human badge label. `gap > 0` means more time than intended. */\nfunction alignmentLabel(gap: number, level: AlignLevel): string {\n if (level === \"aligned\") return \"On track\";\n const pct = Math.round(Math.abs(gap) * 100);\n return gap > 0 ? `${pct}% over` : `${pct}% under`;\n}\n\nexport function TimeWellSpentPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [days, setDays] = useState<number>(30);\n\n // Memoise the window so `isoNow()` doesn't change identity every render and\n // trigger endless refetches; the query re-runs only when `days` changes.\n const range = useMemo(\n () => ({ from: isoDaysAgo(days), to: isoNow() }),\n [days],\n );\n\n const { data, loading, error } = useMyTimeWellSpentQuery({\n variables: { from: range.from, to: range.to },\n skip: !workspaceId,\n });\n\n const wellSpent = data?.myTimeWellSpent;\n\n // Biggest slices of intended-or-actual time first, so what matters most —\n // and where you're most out of balance — surfaces at the top.\n const domains = useMemo<DomainAlignment[]>(() => {\n const list = wellSpent?.byDomain ?? [];\n return [...list].sort(\n (a, b) =>\n Math.max(b.desiredShare, b.actualShare) -\n Math.max(a.desiredShare, a.actualShare),\n );\n }, [wellSpent]);\n\n const rangeSelector = (\n <div className=\"flex items-center rounded-lg border border-border-subtle bg-bg-sunken/50 p-0.5\">\n {RANGE_OPTIONS.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => setDays(opt.value)}\n className={cn(\n \"rounded-md px-3 py-1 text-xs font-medium transition-colors\",\n days === opt.value\n ? \"bg-bg-surface text-text-primary shadow-sm\"\n : \"text-text-secondary hover:text-text-primary\",\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n );\n\n const pageProps = {\n title: \"Time Well Spent\",\n description: \"How closely your time matches what you value\",\n purpose:\n \"Time Well Spent scores how well your actual time matches what you've said matters to you — your declared values and balance targets. It rewards alignment, not output, so a calm, values-aligned day can score higher than a frantic one.\",\n actions: rangeSelector,\n } as const;\n\n if (!workspaceId || loading) {\n return (\n <PageLayout {...pageProps}>\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout {...pageProps}>\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n We couldn't load your Time-Well-Spent score right now. Please try\n again in a moment.\n </div>\n </PageLayout>\n );\n }\n\n if (!wellSpent) {\n return (\n <PageLayout {...pageProps}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"Nothing to score yet\"\n description=\"Once you start capturing your time, your alignment score will appear here.\"\n />\n </PageLayout>\n );\n }\n\n const { score, hasDeclaredValues, coverageConfidence, coveredSeconds } =\n wellSpent;\n\n // No declared values/targets → the score is undefined by design. Invite the\n // first meaningful action instead of showing a hollow gauge.\n if (!hasDeclaredValues) {\n return (\n <PageLayout {...pageProps}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"Declare what you value first\"\n description=\"Time Well Spent needs to know what matters to you. Head to the Values & Goals page to declare your life values or balance targets — then this score will show how closely your real time matches them.\"\n />\n </PageLayout>\n );\n }\n\n // Values are declared, but the window has no accounted time to compare.\n if (coveredSeconds <= 0) {\n return (\n <PageLayout {...pageProps}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"No time logged in this range\"\n description=\"Log your first life entry — or connect a source that captures your time — and we'll score how it aligns with what you value.\"\n />\n </PageLayout>\n );\n }\n\n const scorePct = score != null ? Math.round(score * 100) : 0;\n const coveragePct = Math.round(coverageConfidence * 100);\n const coverageLow = coverageConfidence < LOW_COVERAGE;\n\n return (\n <PageLayout {...pageProps}>\n {/* Hero: the score gauge alongside the framing note + honest stats. */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n title=\"Your alignment score\"\n description={`Last ${days} days`}\n actions={<Compass className=\"h-4 w-4 text-text-secondary\" />}\n className=\"lg:col-span-1\"\n >\n <div className=\"flex flex-col items-center\">\n <div className=\"mx-auto w-full max-w-sm\">\n <RadialGauge\n value={scorePct}\n min={0}\n max={100}\n label=\"Time Well Spent\"\n sublabel=\"alignment with what you value\"\n thresholds={{ warning: 70, danger: 45 }}\n height={260}\n ariaLabel={`Time Well Spent score: ${scorePct} out of 100`}\n />\n </div>\n <p className=\"mt-1 text-xs font-medium text-text-muted\">\n Not a productivity score\n </p>\n {coverageLow && (\n <div className=\"mt-3 flex items-start gap-2 rounded-lg border border-status-warning-border/40 bg-status-warning-bg/10 px-3 py-2 text-xs text-status-warning-text\">\n <Info className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" />\n <span>\n Only {coveragePct}% of this period is accounted for, so treat\n this as a rough estimate. Capturing more of your day sharpens\n it.\n </span>\n </div>\n )}\n </div>\n </ChartCard>\n\n <div className=\"space-y-4 sm:space-y-6 lg:col-span-2\">\n {/* Prominent framing note. */}\n <GlassCard\n treatment=\"insight\"\n className=\"border-status-info-border/40 p-4 sm:p-5\"\n >\n <div className=\"flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg/40 text-status-info-text\">\n <Heart className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n This measures alignment with what you value — not\n productivity.\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n A restful, values-aligned day scores high. A busy day that\n touched nothing you care about scores low.\n </p>\n </div>\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <StatCard\n title=\"Time accounted for\"\n value={formatSecondsHuman(coveredSeconds)}\n icon={<Clock className=\"h-5 w-5\" />}\n description={`${secondsToHours(coveredSeconds)} hrs over ${days} days`}\n iconBgColor=\"bg-status-info-bg text-status-info-text\"\n />\n <StatCard\n title=\"Coverage confidence\"\n value={`${coveragePct}%`}\n icon={<Radar className=\"h-5 w-5\" />}\n description=\"of the period logged\"\n />\n <StatCard\n title=\"Life domains\"\n value={String(domains.length)}\n icon={<Layers className=\"h-5 w-5\" />}\n description=\"with time or a target\"\n />\n </div>\n </div>\n </div>\n\n {/* Per-domain alignment: actual share vs what you intended. */}\n <GlassCard treatment=\"glass\" glow className=\"mt-6 p-4 sm:p-6\">\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Scale className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Where your time aligns\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Your actual share of accounted time versus what you intended, by\n life domain.\n </p>\n </div>\n </div>\n\n {domains.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No domain breakdown yet\"\n description=\"As your time is captured across life domains, each one's alignment with your intentions will appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {domains.map((d) => {\n const level = alignLevel(d.gap);\n const actualPct = Math.round(d.actualShare * 100);\n const desiredPct = Math.round(d.desiredShare * 100);\n const actualHuman = formatSecondsHuman(\n d.actualShare * coveredSeconds,\n );\n return (\n <GlassCard key={d.domain} treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {lifeDomainLabel(d.domain)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {actualHuman} logged\n </p>\n </div>\n <StatusBadge\n variant={BADGE_VARIANT[level]}\n label={alignmentLabel(d.gap, level)}\n />\n </div>\n <ProgressMeter\n className=\"mt-3\"\n value={actualPct}\n max={100}\n tone={METER_TONE[level]}\n showValue={false}\n size=\"md\"\n />\n <div className=\"mt-1.5 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">\n You:{\" \"}\n <span className=\"tabular-nums text-text-primary\">\n {actualPct}%\n </span>\n </span>\n <span className=\"text-text-muted\">\n Intended:{\" \"}\n <span className=\"tabular-nums text-text-secondary\">\n {desiredPct}%\n </span>\n </span>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TimeWellSpentPage.js","names":[],"sources":["../../../../src/timecampus/pages/lifetime/TimeWellSpentPage.tsx"],"sourcesContent":["import { useMemo, useState } from \"react\";\nimport {\n Heart,\n Clock,\n Radar,\n Layers,\n Compass,\n Scale,\n Info,\n} from \"lucide-react\";\nimport { GlassCard } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { StatCard } from \"../../components/StatCard\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport {\n ChartCard,\n RadialGauge,\n ProgressMeter,\n StatusBadge,\n type MeterTone,\n} from \"../../components\";\nimport { cn } from \"../../utils/helpers\";\nimport { useTimeCampus } from \"../../providers/TimeCampusProvider\";\nimport {\n useMyTimeWellSpentQuery,\n type MyTimeWellSpentQuery,\n} from \"@/generated/wspace-operations\";\nimport {\n lifeDomainLabel,\n secondsToHours,\n formatSecondsHuman,\n isoDaysAgo,\n isoNow,\n} from \"./domains\";\n\ntype DomainAlignment =\n MyTimeWellSpentQuery[\"myTimeWellSpent\"][\"byDomain\"][number];\n\n/** Range presets for the score window. */\nconst RANGE_OPTIONS = [\n { label: \"7 days\", value: 7 },\n { label: \"30 days\", value: 30 },\n] as const;\n\n/** Below this fraction of the window accounted for, the score is only a rough estimate. */\nconst LOW_COVERAGE = 0.5;\n\ntype AlignLevel = \"aligned\" | \"slight\" | \"off\";\n\n/** Classify a domain's alignment from |actualShare − desiredShare|. */\nfunction alignLevel(gap: number): AlignLevel {\n const magnitude = Math.abs(gap);\n if (magnitude <= 0.05) return \"aligned\";\n if (magnitude <= 0.12) return \"slight\";\n return \"off\";\n}\n\nconst BADGE_VARIANT: Record<AlignLevel, \"success\" | \"warning\" | \"error\"> = {\n aligned: \"success\",\n slight: \"warning\",\n off: \"error\",\n};\n\nconst METER_TONE: Record<AlignLevel, MeterTone> = {\n aligned: \"success\",\n slight: \"warning\",\n off: \"danger\",\n};\n\n/** Directional, human badge label. `gap > 0` means more time than intended. */\nfunction alignmentLabel(gap: number, level: AlignLevel): string {\n if (level === \"aligned\") return \"On track\";\n const pct = Math.round(Math.abs(gap) * 100);\n return gap > 0 ? `${pct}% over` : `${pct}% under`;\n}\n\nexport function TimeWellSpentPage() {\n const { workspaceId = \"\" } = useTimeCampus();\n const [days, setDays] = useState<number>(30);\n\n // Memoise the window so `isoNow()` doesn't change identity every render and\n // trigger endless refetches; the query re-runs only when `days` changes.\n const range = useMemo(\n () => ({ from: isoDaysAgo(days), to: isoNow() }),\n [days],\n );\n\n const { data, loading, error } = useMyTimeWellSpentQuery({\n variables: { from: range.from, to: range.to },\n skip: !workspaceId,\n });\n\n const wellSpent = data?.myTimeWellSpent;\n\n // Biggest slices of intended-or-actual time first, so what matters most —\n // and where you're most out of balance — surfaces at the top.\n const domains = useMemo<DomainAlignment[]>(() => {\n const list = wellSpent?.byDomain ?? [];\n return [...list].sort(\n (a, b) =>\n Math.max(b.desiredShare, b.actualShare) -\n Math.max(a.desiredShare, a.actualShare),\n );\n }, [wellSpent]);\n\n const rangeSelector = (\n <div className=\"flex items-center rounded-lg border border-border-subtle bg-bg-sunken/50 p-0.5\">\n {RANGE_OPTIONS.map((opt) => (\n <button\n key={opt.value}\n type=\"button\"\n onClick={() => setDays(opt.value)}\n className={cn(\n \"rounded-md px-3 py-1 text-xs font-medium transition-colors\",\n days === opt.value\n ? \"bg-bg-surface text-text-primary shadow-sm\"\n : \"text-text-secondary hover:text-text-primary\",\n )}\n >\n {opt.label}\n </button>\n ))}\n </div>\n );\n\n const pageProps = {\n title: \"Time Well Spent\",\n description: \"How closely your time matches what you value\",\n purpose:\n \"Time Well Spent scores how well your actual time matches what you've said matters to you — your declared values and balance targets. It rewards alignment, not output, so a calm, values-aligned day can score higher than a frantic one.\",\n actions: rangeSelector,\n } as const;\n\n if (!workspaceId || loading) {\n return (\n <PageLayout {...pageProps}>\n <LoadingSkeleton variant=\"card\" count={4} />\n </PageLayout>\n );\n }\n\n if (error) {\n return (\n <PageLayout {...pageProps}>\n <div className=\"rounded-xl border border-status-error-border/50 bg-status-error-bg/10 p-4 text-sm text-status-error-text\">\n We couldn't load your Time-Well-Spent score right now. Please try\n again in a moment.\n </div>\n </PageLayout>\n );\n }\n\n if (!wellSpent) {\n return (\n <PageLayout {...pageProps}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"Nothing to score yet\"\n description=\"Once you start capturing your time, your alignment score will appear here.\"\n />\n </PageLayout>\n );\n }\n\n const { score, hasDeclaredValues, coverageConfidence, coveredSeconds } =\n wellSpent;\n\n // No declared values/targets → the score is undefined by design. Invite the\n // first meaningful action instead of showing a hollow gauge.\n if (!hasDeclaredValues) {\n return (\n <PageLayout {...pageProps}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"Declare what you value first\"\n description=\"Time Well Spent needs to know what matters to you. Head to the Values & Goals page to declare your life values or balance targets — then this score will show how closely your real time matches them.\"\n />\n </PageLayout>\n );\n }\n\n // Values are declared, but the window has no accounted time to compare.\n if (coveredSeconds <= 0) {\n return (\n <PageLayout {...pageProps}>\n <EmptyState\n illustration=\"empty-data\"\n title=\"No time logged in this range\"\n description=\"Log your first life entry — or connect a source that captures your time — and we'll score how it aligns with what you value.\"\n />\n </PageLayout>\n );\n }\n\n const scorePct = score != null ? Math.round(score * 100) : 0;\n const coveragePct = Math.round(coverageConfidence * 100);\n const coverageLow = coverageConfidence < LOW_COVERAGE;\n\n return (\n <PageLayout {...pageProps}>\n {/* Hero: the score gauge alongside the framing note + honest stats. */}\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <ChartCard\n title=\"Your alignment score\"\n description={`Last ${days} days`}\n actions={<Compass className=\"h-4 w-4 text-text-secondary\" />}\n className=\"lg:col-span-1\"\n >\n <div className=\"flex flex-col items-center\">\n <div className=\"mx-auto w-full max-w-sm\">\n <RadialGauge\n value={scorePct}\n min={0}\n max={100}\n label=\"Time Well Spent\"\n sublabel=\"alignment with what you value\"\n thresholds={{ warning: 70, danger: 45 }}\n height={260}\n ariaLabel={`Time Well Spent score: ${scorePct} out of 100`}\n />\n </div>\n <p className=\"mt-1 text-xs font-medium text-text-muted\">\n Not a productivity score\n </p>\n {coverageLow && (\n <div className=\"mt-3 flex items-start gap-2 rounded-lg border border-status-warning-border/40 bg-status-warning-bg/10 px-3 py-2 text-xs text-status-warning-text\">\n <Info className=\"mt-0.5 h-3.5 w-3.5 shrink-0\" />\n <span>\n Only {coveragePct}% of this period is accounted for, so treat\n this as a rough estimate. Capturing more of your day sharpens\n it.\n </span>\n </div>\n )}\n </div>\n </ChartCard>\n\n <div className=\"space-y-4 sm:space-y-6 lg:col-span-2\">\n {/* Prominent framing note. */}\n <GlassCard\n treatment=\"insight\"\n className=\"border-status-info-border/40 p-4 sm:p-5\"\n >\n <div className=\"flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-status-info-bg/40 text-status-info-text\">\n <Heart className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n This measures alignment with what you value — not\n productivity.\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n A restful, values-aligned day scores high. A busy day that\n touched nothing you care about scores low.\n </p>\n </div>\n </div>\n </GlassCard>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <StatCard\n title=\"Time accounted for\"\n value={formatSecondsHuman(coveredSeconds)}\n icon={<Clock className=\"h-5 w-5\" />}\n description={`${secondsToHours(coveredSeconds)} hrs over ${days} days`}\n iconBgColor=\"bg-status-info-bg text-status-info-text\"\n />\n <StatCard\n title=\"Coverage confidence\"\n value={`${coveragePct}%`}\n icon={<Radar className=\"h-5 w-5\" />}\n description=\"of the period logged\"\n />\n <StatCard\n title=\"Life domains\"\n value={String(domains.length)}\n icon={<Layers className=\"h-5 w-5\" />}\n description=\"with time or a target\"\n />\n </div>\n </div>\n </div>\n\n {/* Per-domain alignment: actual share vs what you intended. */}\n <GlassCard treatment=\"glass\" glow className=\"mt-6 p-4 sm:p-6\">\n <div className=\"mb-4 flex items-start gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-primary\">\n <Scale className=\"h-5 w-5\" />\n </div>\n <div className=\"min-w-0\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Where your time aligns\n </h3>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n Your actual share of accounted time versus what you intended, by\n life domain.\n </p>\n </div>\n </div>\n\n {domains.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No domain breakdown yet\"\n description=\"As your time is captured across life domains, each one's alignment with your intentions will appear here.\"\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {domains.map((d) => {\n const level = alignLevel(d.gap);\n const actualPct = Math.round(d.actualShare * 100);\n const desiredPct = Math.round(d.desiredShare * 100);\n const actualHuman = formatSecondsHuman(\n d.actualShare * coveredSeconds,\n );\n return (\n <GlassCard key={d.domain} treatment=\"metric\" className=\"p-4\">\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {lifeDomainLabel(d.domain)}\n </p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {actualHuman} logged\n </p>\n </div>\n <StatusBadge\n variant={BADGE_VARIANT[level]}\n label={alignmentLabel(d.gap, level)}\n />\n </div>\n <ProgressMeter\n className=\"mt-3\"\n value={actualPct}\n max={100}\n tone={METER_TONE[level]}\n showValue={false}\n size=\"md\"\n />\n <div className=\"mt-1.5 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">\n You:{\" \"}\n <span className=\"tabular-nums text-text-primary\">\n {actualPct}%\n </span>\n </span>\n <span className=\"text-text-muted\">\n Intended:{\" \"}\n <span className=\"tabular-nums text-text-secondary\">\n {desiredPct}%\n </span>\n </span>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </GlassCard>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -497,12 +497,15 @@ function M() {
497
497
  }),
498
498
  /* @__PURE__ */ y("div", {
499
499
  className: "mt-2 flex flex-wrap gap-2",
500
- children: a.map((e) => /* @__PURE__ */ y("button", {
501
- type: "button",
502
- onClick: () => We(e),
503
- className: B.domains.includes(e) ? "rounded-full bg-action-primary-bg px-3 py-1 text-xs font-medium text-action-primary-text" : "rounded-full border border-border-subtle px-3 py-1 text-xs font-medium text-text-secondary hover:bg-bg-sunken/50",
504
- children: o(e)
505
- }, e))
500
+ children: a.map((e) => {
501
+ let t = B.domains.includes(e);
502
+ return /* @__PURE__ */ y("button", {
503
+ type: "button",
504
+ onClick: () => We(e),
505
+ className: t ? "rounded-full bg-action-primary-bg px-3 py-1 text-xs font-medium text-action-primary-text" : "rounded-full border border-border-subtle px-3 py-1 text-xs font-medium text-text-secondary hover:bg-bg-sunken/50",
506
+ children: o(e)
507
+ }, e);
508
+ })
506
509
  })
507
510
  ] }),
508
511
  /* @__PURE__ */ b("label", {