@burdenoff/microfe-atheletebridge 2026.827.1 → 2026.829.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.
- package/dist/atheletebridge/components/LoadingState.d.ts.map +1 -1
- package/dist/atheletebridge/components/LoadingState.js +39 -32
- package/dist/atheletebridge/components/LoadingState.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.d.ts.map +1 -1
- package/dist/atheletebridge/components/TeamCard.js +45 -49
- package/dist/atheletebridge/components/TeamCard.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js +56 -24
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js +58 -56
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +110 -106
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js +80 -71
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.js +244 -210
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js +197 -148
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +169 -143
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js +77 -72
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js +104 -86
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js +54 -49
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js +286 -267
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js +185 -180
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.js +70 -74
- package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n *\n * A rolling date-range window (\"Last 7 / 28 / 90 days\", or All time) scopes\n * everything below it: which readings a tile is built from, what its delta is\n * measured against (\"vs previous 7 days\" really means the preceding 7 days,\n * not simply the preceding reading), and which workouts the history lists.\n * The window is applied client-side — see WINDOW_FETCH_LIMIT for why, and for\n * the coverage check that stops a truncated page reading as a complete one.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n CalendarRange,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { FilterSelect, type FilterOption } from '../../components/FilterSelect';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport { useAbTranslate, type TranslateFn } from '../../i18n';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/**\n * How many workout rows to pull when a bounded window is active.\n *\n * Neither `athListWorkoutLogs` nor `athPerformanceMetrics` accepts a date-range\n * argument — the only filters on the composed schema are `athleteId` /\n * `programId` / `type` — so the window is applied client-side over a bounded\n * page. Both list fields are ordered newest-first by the svc (`performedAt` /\n * `recordedAt` desc) and every window here ends today, so the rows a window\n * needs are always the newest rows. The svc clamps `limit` to its own ceiling,\n * so this is a request rather than a guarantee: `windowCovered` below checks\n * whether the page actually reached back past the window's start and the UI\n * says so when it did not.\n */\nconst WINDOW_FETCH_LIMIT = 100;\n\n/**\n * Metric history depth, so a window has prior readings to compare against.\n * Hoisted so the same object identity is reused across renders.\n */\nconst METRIC_PAGINATION = { limit: 100 } as const;\n\n/** Rolling window presets. `''` (the FilterSelect \"all\" option) means no window. */\ntype RangeKey = '7d' | '28d' | '90d';\n\nconst RANGE_KEYS: RangeKey[] = ['7d', '28d', '90d'];\nconst RANGE_DAYS: Record<RangeKey, number> = { '7d': 7, '28d': 28, '90d': 90 };\n\n/** A resolved window of real time, inclusive at both ends. */\ninterface DateWindow {\n start: number;\n end: number;\n}\n\n/**\n * Resolve a preset into whole local calendar days ending with today — \"last 7\n * days\" is today plus the six days before it, which is what the range caption\n * claims. Anchoring on the day rather than on the instant of selection means a\n * reading taken moments after the window was chosen still falls inside it,\n * while a workout scheduled for tomorrow still falls outside. `setDate` is used\n * for the subtraction so a DST boundary cannot shift the start off midnight.\n */\nconst resolveWindow = (key: RangeKey): DateWindow => {\n const endOfToday = new Date();\n endOfToday.setHours(23, 59, 59, 999);\n const startOfWindow = new Date(endOfToday);\n startOfWindow.setHours(0, 0, 0, 0);\n startOfWindow.setDate(startOfWindow.getDate() - (RANGE_DAYS[key] - 1));\n return { start: startOfWindow.getTime(), end: endOfToday.getTime() };\n};\n\n/** Milliseconds for an ISO timestamp; NaN for an unparseable one. */\nconst msOf = (iso: string): number => new Date(iso).getTime();\n\n/** Date-only variant of the shared `formatDate` (drops the time part). */\nconst formatDay = (ms: number): string =>\n formatDate(new Date(ms), { hour: undefined, minute: undefined, hour12: undefined });\n\n/** e.g. \"5 Jul 2026 – 11 Jul 2026\". */\nconst windowLabel = (range: DateWindow): string =>\n `${formatDay(range.start)} – ${formatDay(range.end)}`;\n\n/**\n * Preset name. Written as literal `tr(key, fallback)` calls because the app's\n * i18n extractor reads the call site's AST and cannot resolve variables.\n */\nconst rangeName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr('atheletebridge.pages.performance.window.last7', 'Last 7 days');\n case '28d':\n return tr('atheletebridge.pages.performance.window.last28', 'Last 28 days');\n case '90d':\n return tr('atheletebridge.pages.performance.window.last90', 'Last 90 days');\n }\n};\n\n/** What a windowed metric delta is measured against. */\nconst comparisonName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr('atheletebridge.pages.performance.window.vsLast7', 'vs previous 7 days');\n case '28d':\n return tr('atheletebridge.pages.performance.window.vsLast28', 'vs previous 28 days');\n case '90d':\n return tr('atheletebridge.pages.performance.window.vsLast90', 'vs previous 90 days');\n }\n};\n\n/** Why a metric tile has no delta to show. */\nconst noComparisonName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior7',\n 'No reading in the previous 7 days'\n );\n case '28d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior28',\n 'No reading in the previous 28 days'\n );\n case '90d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior90',\n 'No reading in the previous 90 days'\n );\n }\n};\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title=\"Performance\"\n description=\"Log workouts, record metrics and track an athlete's progress over time.\"\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div\n data-tour=\"performance-athlete-picker\"\n className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\"\n >\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title=\"Pick an athlete to begin\"\n description=\"Choose a team and an athlete above to log workouts, record metrics and review their history.\"\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n}\n\n/** Copy for a tile's delta line, which depends on the active window. */\ninterface DeltaLabels {\n /** What the delta is measured against, e.g. \"vs previous 7 days\". */\n comparison: string;\n /** Shown when there is nothing to compare against. */\n noComparison: string;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const tr = useAbTranslate();\n const [page, setPage] = useState(0);\n const [rangeKey, setRangeKey] = useState<RangeKey | ''>('');\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n // Resolved once per selection, so the window is stable across re-renders\n // (a bare Date.now() in render would invalidate every memo below).\n const range = useMemo<DateWindow | null>(\n () => (rangeKey === '' ? null : resolveWindow(rangeKey)),\n [rangeKey]\n );\n\n // With a window the whole (bounded) newest-first page is pulled and both the\n // filtering and the paging happen here; without one the svc pages as before.\n const logs = useWorkoutLogs({\n athleteId,\n pagination: range\n ? { limit: WINDOW_FETCH_LIMIT }\n : { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId, METRIC_PAGINATION);\n\n const fetchedLogs = useMemo(() => logs.data?.items ?? [], [logs.data]);\n const fetchedHasMore = logs.data?.pageInfo.hasMore ?? false;\n\n const windowLogs = useMemo(() => {\n if (!range) {\n return fetchedLogs;\n }\n return fetchedLogs.filter((log) => {\n const at = msOf(log.performedAt);\n return at >= range.start && at <= range.end;\n });\n }, [fetchedLogs, range]);\n\n // Clamp the page against the windowed set so switching athlete (or a refetch\n // that shrinks the window) cannot strand the pager on an empty page.\n const lastPage = range ? Math.max(0, Math.ceil(windowLogs.length / PAGE_SIZE) - 1) : page;\n const pageIndex = Math.min(page, lastPage);\n const logItems = range\n ? windowLogs.slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE)\n : windowLogs;\n const hasMore = range ? windowLogs.length > (pageIndex + 1) * PAGE_SIZE : fetchedHasMore;\n\n /**\n * Whether the fetched page actually reaches back past the window's start.\n * Rows are `performedAt` desc, so the last fetched row is the oldest one we\n * hold: if there are more rows on the server AND that oldest row is still\n * newer than the window start, in-window workouts are missing and the list\n * must not be presented as complete.\n */\n const windowCovered = useMemo(() => {\n if (!range || !fetchedHasMore) {\n return true;\n }\n const oldest = fetchedLogs[fetchedLogs.length - 1];\n return oldest != null && msOf(oldest.performedAt) <= range.start;\n }, [range, fetchedHasMore, fetchedLogs]);\n\n const rangeOptions = useMemo<FilterOption<RangeKey>[]>(\n () =>\n RANGE_KEYS.map((key) => ({\n value: key,\n label: `${rangeName(key, tr)} · ${windowLabel(resolveWindow(key))}`,\n })),\n [tr]\n );\n\n const deltaLabels = useMemo<DeltaLabels>(\n () =>\n rangeKey === ''\n ? {\n comparison: tr('atheletebridge.pages.performance.window.vsPrevious', 'vs previous'),\n noComparison: tr(\n 'atheletebridge.pages.performance.window.firstReading',\n 'First reading'\n ),\n }\n : {\n comparison: comparisonName(rangeKey, tr),\n noComparison: noComparisonName(rangeKey, tr),\n },\n [rangeKey, tr]\n );\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]): MetricSeries | null => {\n const asc = [...list].sort((a, b) => msOf(a.recordedAt) - msOf(b.recordedAt));\n // Readings inside the window drive the headline, sparkline and range.\n const inWindow = range\n ? asc.filter((m) => {\n const at = msOf(m.recordedAt);\n return at >= range.start && at <= range.end;\n })\n : asc;\n if (inWindow.length === 0) {\n return null;\n }\n const values = inWindow.map((m) => m.value);\n const newest = inWindow[inWindow.length - 1];\n\n // The comparison reading is the latest one in the window immediately\n // BEFORE this one (equal length), so \"vs previous 7 days\" really is the\n // previous 7 days. Without a window it stays the preceding reading.\n let previous: number | null = null;\n if (range) {\n const priorStart = range.start - (range.end - range.start + 1);\n const prior = asc.filter((m) => {\n const at = msOf(m.recordedAt);\n return at >= priorStart && at < range.start;\n });\n previous = prior.length > 0 ? prior[prior.length - 1].value : null;\n } else {\n previous = asc.length > 1 ? asc[asc.length - 2].value : null;\n }\n\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n };\n })\n .filter((entry): entry is MetricSeries => entry !== null)\n .sort((a, b) => msOf(b.recordedAt) - msOf(a.recordedAt));\n }, [metrics.data, range]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: 'Workout',\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: 'Duration',\n priority: 'secondary',\n mobileLabel: 'Duration',\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: 'RPE',\n priority: 'secondary',\n mobileLabel: 'RPE',\n cell: (log) =>\n log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>,\n },\n {\n key: 'date',\n header: 'Date',\n priority: 'secondary',\n mobileLabel: 'Date',\n cell: (log) => (\n <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n priority: 'primary',\n mobileLabel: 'Status',\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: 'Notes',\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n []\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n Tracking <span className=\"font-medium text-text-primary\">{displayName || 'athlete'}</span>\n {range ? (\n <>\n {' · '}\n <span className=\"tabular-nums text-text-muted\">{windowLabel(range)}</span>\n </>\n ) : null}\n </p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {/* Date-range window — scopes the metric tiles, their deltas and the\n workout history below to a rolling period. */}\n <span className=\"flex items-center gap-2\" data-tour=\"performance-date-range\">\n <CalendarRange className=\"h-4 w-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <FilterSelect<RangeKey>\n value={rangeKey}\n onChange={(next) => {\n setRangeKey(next);\n setPage(0);\n }}\n options={rangeOptions}\n allLabel={tr('atheletebridge.pages.performance.window.allTime', 'All time')}\n ariaLabel={tr('atheletebridge.pages.performance.window.ariaLabel', 'Date range')}\n />\n </span>\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Gauge className=\"h-4 w-4\" /> Record metric\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Performance metrics</h2>\n {metrics.isLoading ? (\n <LoadingState label=\"Loading metrics…\" />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title={\n range\n ? tr(\n 'atheletebridge.pages.performance.metrics.emptyWindowTitle',\n 'No metrics in this window'\n )\n : 'No metrics recorded'\n }\n description={\n range\n ? tr(\n 'atheletebridge.pages.performance.metrics.emptyWindowDescription',\n 'Nothing was recorded for this athlete in the selected period. Widen the date range, or record a metric.'\n )\n : 'Record a metric — speed, strength, endurance and more — to start charting progress.'\n }\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" /> Record metric\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} labels={deltaLabels} tr={tr} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Workout history</h2>\n {logs.isLoading ? (\n <LoadingState label=\"Loading workouts…\" />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title={\n range\n ? tr(\n 'atheletebridge.pages.performance.workouts.emptyWindowTitle',\n 'No workouts in this window'\n )\n : 'No workouts logged'\n }\n description={\n range\n ? tr(\n 'atheletebridge.pages.performance.workouts.emptyWindowDescription',\n 'Nothing was logged for this athlete in the selected period. Widen the date range, or log a workout.'\n )\n : \"Log a workout to build this athlete's training history.\"\n }\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" /> Log workout\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={`Workout history for ${displayName || 'athlete'}`}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={pageIndex}\n hasMore={hasMore}\n onPrev={() => setPage(Math.max(0, pageIndex - 1))}\n onNext={() => setPage(pageIndex + 1)}\n />\n </>\n )}\n {!windowCovered ? (\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'atheletebridge.pages.performance.workouts.windowTruncated',\n 'Only the most recent page of workouts was searched, and it does not reach back to the start of this window — older sessions in the range are not listed.'\n )}\n </p>\n ) : null}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" /> Log workout\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({\n metric,\n labels,\n tr,\n}: {\n metric: MetricSeries;\n labels: DeltaLabels;\n tr: TranslateFn;\n}) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {METRIC_TYPE_LABELS[metric.type]}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta against the active comparison period — direction shown neutrally\n (no good/bad claim), and absent rather than guessed when there is no\n reading to compare against. */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null ? (\n <span className=\"text-text-muted\">{labels.noComparison}</span>\n ) : delta === 0 ? (\n <span className=\"text-text-muted\">\n {tr('atheletebridge.pages.performance.window.noChange', 'No change')}{' '}\n {labels.comparison}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">{labels.comparison}</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError('A workout title is required.');\n return;\n }\n if (!performedAt) {\n setError('A performed date and time is required.');\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to log workout.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Log workout{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField label=\"Title\" htmlFor=\"workoutTitle\" required>\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Lower-body strength\"\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Status\" htmlFor=\"workoutStatus\">\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {s.charAt(0) + s.slice(1).toLowerCase()}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Performed at\" htmlFor=\"workoutPerformedAt\" required>\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField label=\"Duration (min)\" htmlFor=\"workoutDuration\">\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField label=\"RPE (1–10)\" htmlFor=\"workoutRpe\">\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField label=\"Total load\" htmlFor=\"workoutLoad\">\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Notes\" htmlFor=\"workoutNotes\">\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder=\"How did the session go?\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n Log workout\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError('A metric label is required.');\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError('A numeric value is required.');\n return;\n }\n if (!recordedAt) {\n setError('A recorded date and time is required.');\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to record metric.');\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n Record metric{displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField label=\"Type\" htmlFor=\"metricType\">\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {METRIC_TYPE_LABELS[t]}\n </option>\n ))}\n </select>\n </FormField>\n <FormField label=\"Recorded at\" htmlFor=\"metricRecordedAt\" required>\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField label=\"Label\" htmlFor=\"metricLabel\" required>\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder=\"e.g. 40m sprint\"\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField label=\"Value\" htmlFor=\"metricValue\" required>\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder=\"e.g. 4.8\"\n />\n </FormField>\n <FormField label=\"Unit\" htmlFor=\"metricUnit\">\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder=\"e.g. s, kg, bpm\"\n />\n </FormField>\n </div>\n <FormField label=\"Source\" htmlFor=\"metricSource\" hint=\"Optional — device, test or coach.\">\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder=\"e.g. GPS vest\"\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n Record metric\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PerformancePage.js","names":[],"sources":["../../../../src/atheletebridge/pages/performance/PerformancePage.tsx"],"sourcesContent":["/**\n * Performance\n * @module atheletebridge/pages/performance\n *\n * The coach's athlete-performance workbench: pick an athlete from a team roster,\n * then log workouts (athLogWorkout) and record performance metrics\n * (athRecordMetric), and review that athlete's workout history\n * (athListWorkoutLogs, offset-paginated) and metrics.\n *\n * Elevated to the concept bar: each measured metric (type + label) becomes a\n * hero stat tile with its big latest value, a real recordedAt-ordered trend\n * sparkline, a min→max range scale and a signed delta-vs-previous — all derived\n * from the real time-series (nothing fabricated). Workout history renders as a\n * responsive table that reflows to cards on mobile, with a thumb-reachable\n * sticky \"Log workout\" action.\n *\n * A rolling date-range window (\"Last 7 / 28 / 90 days\", or All time) scopes\n * everything below it: which readings a tile is built from, what its delta is\n * measured against (\"vs previous 7 days\" really means the preceding 7 days,\n * not simply the preceding reading), and which workouts the history lists.\n * The window is applied client-side — see WINDOW_FETCH_LIMIT for why, and for\n * the coverage check that stops a truncated page reading as a complete one.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\n CalendarRange,\n Dumbbell,\n Gauge,\n HeartPulse,\n Loader2,\n Move,\n Plus,\n Save,\n Scale,\n Target,\n Users,\n Zap,\n type LucideIcon,\n} from 'lucide-react';\nimport {\n GlassCard,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogFooter,\n ResponsiveTable,\n type ResponsiveTableColumn,\n} from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { AthleteSelect, type AthleteSelectValue } from '../../components/AthleteSelect';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingState, ErrorState } from '../../components/LoadingState';\nimport { CursorPager } from '../../components/CursorPager';\nimport { FilterSelect, type FilterOption } from '../../components/FilterSelect';\nimport { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\nimport { useAbTranslate, type TranslateFn } from '../../i18n';\nimport {\n useWorkoutLogs,\n usePerformanceMetrics,\n useLogWorkout,\n useRecordMetric,\n} from '../../hooks/useAthleteBridgeApi';\nimport { formatDate, formatDuration } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type {\n AthMetricType,\n AthPerformanceMetric,\n AthWorkoutLog,\n AthWorkoutStatus,\n} from '../../types';\n\nconst PAGE_SIZE = 10;\n\nconst WORKOUT_STATUSES: AthWorkoutStatus[] = ['COMPLETED', 'PLANNED', 'SKIPPED'];\nconst METRIC_TYPES = Object.keys(METRIC_TYPE_LABELS) as AthMetricType[];\n\n/**\n * How many workout rows to pull when a bounded window is active.\n *\n * Neither `athListWorkoutLogs` nor `athPerformanceMetrics` accepts a date-range\n * argument — the only filters on the composed schema are `athleteId` /\n * `programId` / `type` — so the window is applied client-side over a bounded\n * page. Both list fields are ordered newest-first by the svc (`performedAt` /\n * `recordedAt` desc) and every window here ends today, so the rows a window\n * needs are always the newest rows. The svc clamps `limit` to its own ceiling,\n * so this is a request rather than a guarantee: `windowCovered` below checks\n * whether the page actually reached back past the window's start and the UI\n * says so when it did not.\n */\nconst WINDOW_FETCH_LIMIT = 100;\n\n/**\n * Metric history depth, so a window has prior readings to compare against.\n * Hoisted so the same object identity is reused across renders.\n */\nconst METRIC_PAGINATION = { limit: 100 } as const;\n\n/** Rolling window presets. `''` (the FilterSelect \"all\" option) means no window. */\ntype RangeKey = '7d' | '28d' | '90d';\n\nconst RANGE_KEYS: RangeKey[] = ['7d', '28d', '90d'];\nconst RANGE_DAYS: Record<RangeKey, number> = { '7d': 7, '28d': 28, '90d': 90 };\n\n/** A resolved window of real time, inclusive at both ends. */\ninterface DateWindow {\n start: number;\n end: number;\n}\n\n/**\n * Resolve a preset into whole local calendar days ending with today — \"last 7\n * days\" is today plus the six days before it, which is what the range caption\n * claims. Anchoring on the day rather than on the instant of selection means a\n * reading taken moments after the window was chosen still falls inside it,\n * while a workout scheduled for tomorrow still falls outside. `setDate` is used\n * for the subtraction so a DST boundary cannot shift the start off midnight.\n */\nconst resolveWindow = (key: RangeKey): DateWindow => {\n const endOfToday = new Date();\n endOfToday.setHours(23, 59, 59, 999);\n const startOfWindow = new Date(endOfToday);\n startOfWindow.setHours(0, 0, 0, 0);\n startOfWindow.setDate(startOfWindow.getDate() - (RANGE_DAYS[key] - 1));\n return { start: startOfWindow.getTime(), end: endOfToday.getTime() };\n};\n\n/** Milliseconds for an ISO timestamp; NaN for an unparseable one. */\nconst msOf = (iso: string): number => new Date(iso).getTime();\n\n/** Date-only variant of the shared `formatDate` (drops the time part). */\nconst formatDay = (ms: number): string =>\n formatDate(new Date(ms), { hour: undefined, minute: undefined, hour12: undefined });\n\n/** e.g. \"5 Jul 2026 – 11 Jul 2026\". */\nconst windowLabel = (range: DateWindow): string =>\n `${formatDay(range.start)} – ${formatDay(range.end)}`;\n\n/**\n * Preset name. Written as literal `tr(key, fallback)` calls because the app's\n * i18n extractor reads the call site's AST and cannot resolve variables.\n */\nconst rangeName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr('atheletebridge.pages.performance.window.last7', 'Last 7 days');\n case '28d':\n return tr('atheletebridge.pages.performance.window.last28', 'Last 28 days');\n case '90d':\n return tr('atheletebridge.pages.performance.window.last90', 'Last 90 days');\n }\n};\n\n/** What a windowed metric delta is measured against. */\nconst comparisonName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr('atheletebridge.pages.performance.window.vsLast7', 'vs previous 7 days');\n case '28d':\n return tr('atheletebridge.pages.performance.window.vsLast28', 'vs previous 28 days');\n case '90d':\n return tr('atheletebridge.pages.performance.window.vsLast90', 'vs previous 90 days');\n }\n};\n\n/** Why a metric tile has no delta to show. */\nconst noComparisonName = (key: RangeKey, tr: TranslateFn): string => {\n switch (key) {\n case '7d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior7',\n 'No reading in the previous 7 days'\n );\n case '28d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior28',\n 'No reading in the previous 28 days'\n );\n case '90d':\n return tr(\n 'atheletebridge.pages.performance.window.noPrior90',\n 'No reading in the previous 90 days'\n );\n }\n};\n\n/** Brand gradient CTA — token-only (no raw colors). */\nconst GRADIENT_CTA =\n 'inline-flex items-center justify-center gap-1.5 rounded-lg px-4 py-2 text-sm font-semibold text-action-primary-text shadow-elevation-1 transition-[filter,box-shadow] hover:shadow-elevation-2 hover:brightness-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] bg-[linear-gradient(120deg,var(--color-action-primary-bg),color-mix(in_oklab,var(--color-action-primary-bg)_62%,var(--color-accent-purple)),var(--color-action-primary-bg))]';\n\n/** Per-metric-type icon for the stat tiles. */\nconst METRIC_ICON: Record<AthMetricType, LucideIcon> = {\n SPEED: Zap,\n ENDURANCE: Activity,\n STRENGTH: Dumbbell,\n AGILITY: Move,\n ACCURACY: Target,\n WEIGHT: Scale,\n HEART_RATE: HeartPulse,\n CUSTOM: Gauge,\n};\n\nconst numOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseInt(t, 10);\n return Number.isNaN(n) ? undefined : n;\n};\n\nconst floatOrUndefined = (value: string): number | undefined => {\n const t = value.trim();\n if (!t) return undefined;\n const n = Number.parseFloat(t);\n return Number.isNaN(n) ? undefined : n;\n};\n\n/** Current local time as a `datetime-local` value (`YYYY-MM-DDTHH:mm`). */\nconst nowLocal = (): string => {\n const d = new Date();\n const local = new Date(d.getTime() - d.getTimezoneOffset() * 60_000);\n return local.toISOString().slice(0, 16);\n};\n\n/** Trim trailing zeros from a fixed-precision number for compact display. */\nconst fmtNum = (n: number): string => {\n const abs = Math.abs(n);\n const s = abs >= 100 ? n.toFixed(0) : n.toFixed(2);\n return s.replace(/\\.?0+$/, '');\n};\n\nexport function PerformancePage() {\n const tr = useAbTranslate();\n const [athlete, setAthlete] = useState<AthleteSelectValue>({ athleteId: '', displayName: '' });\n\n return (\n <PageLayout\n title={tr('atheletebridge.pages.performance.title', 'Performance')}\n description={tr(\n 'atheletebridge.pages.performance.description',\n \"Log workouts, record metrics and track an athlete's progress over time.\"\n )}\n >\n {/* Athlete picker — sticks to the top of the scroll area as an insight panel */}\n <div\n data-tour=\"performance-athlete-picker\"\n className=\"sticky top-0 z-20 -mx-6 mb-8 bg-bg-canvas/80 px-6 pb-4 pt-1 backdrop-blur supports-[backdrop-filter]:bg-bg-canvas/60\"\n >\n <GlassCard treatment=\"insight\" className=\"p-5 sm:p-6\">\n <AthleteSelect\n athleteId={athlete.athleteId}\n onSelect={setAthlete}\n idPrefix=\"perf-athlete\"\n />\n </GlassCard>\n </div>\n\n {athlete.athleteId ? (\n <AthletePerformance athleteId={athlete.athleteId} displayName={athlete.displayName} />\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Users className=\"h-10 w-10\" />}\n title={tr(\n 'atheletebridge.pages.performance.pickAthleteTitle',\n 'Pick an athlete to begin'\n )}\n description={tr(\n 'atheletebridge.pages.performance.pickAthleteHint',\n 'Choose a team and an athlete above to log workouts, record metrics and review their history.'\n )}\n />\n )}\n </PageLayout>\n );\n}\n\ninterface AthletePerformanceProps {\n athleteId: string;\n displayName: string;\n}\n\n/** A single measured metric (type + label) collapsed into a real time-series. */\ninterface MetricSeries {\n key: string;\n type: AthMetricType;\n label: string;\n unit: string | null;\n latest: number;\n previous: number | null;\n min: number;\n max: number;\n /** Values oldest → newest (for the trend sparkline). */\n series: number[];\n recordedAt: string;\n}\n\n/** Copy for a tile's delta line, which depends on the active window. */\ninterface DeltaLabels {\n /** What the delta is measured against, e.g. \"vs previous 7 days\". */\n comparison: string;\n /** Shown when there is nothing to compare against. */\n noComparison: string;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const tr = useAbTranslate();\n const [page, setPage] = useState(0);\n const [rangeKey, setRangeKey] = useState<RangeKey | ''>('');\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n // Resolved once per selection, so the window is stable across re-renders\n // (a bare Date.now() in render would invalidate every memo below).\n const range = useMemo<DateWindow | null>(\n () => (rangeKey === '' ? null : resolveWindow(rangeKey)),\n [rangeKey]\n );\n\n // With a window the whole (bounded) newest-first page is pulled and both the\n // filtering and the paging happen here; without one the svc pages as before.\n const logs = useWorkoutLogs({\n athleteId,\n pagination: range\n ? { limit: WINDOW_FETCH_LIMIT }\n : { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId, METRIC_PAGINATION);\n\n const fetchedLogs = useMemo(() => logs.data?.items ?? [], [logs.data]);\n const fetchedHasMore = logs.data?.pageInfo.hasMore ?? false;\n\n const windowLogs = useMemo(() => {\n if (!range) {\n return fetchedLogs;\n }\n return fetchedLogs.filter((log) => {\n const at = msOf(log.performedAt);\n return at >= range.start && at <= range.end;\n });\n }, [fetchedLogs, range]);\n\n // Clamp the page against the windowed set so switching athlete (or a refetch\n // that shrinks the window) cannot strand the pager on an empty page.\n const lastPage = range ? Math.max(0, Math.ceil(windowLogs.length / PAGE_SIZE) - 1) : page;\n const pageIndex = Math.min(page, lastPage);\n const logItems = range\n ? windowLogs.slice(pageIndex * PAGE_SIZE, (pageIndex + 1) * PAGE_SIZE)\n : windowLogs;\n const hasMore = range ? windowLogs.length > (pageIndex + 1) * PAGE_SIZE : fetchedHasMore;\n\n /**\n * Whether the fetched page actually reaches back past the window's start.\n * Rows are `performedAt` desc, so the last fetched row is the oldest one we\n * hold: if there are more rows on the server AND that oldest row is still\n * newer than the window start, in-window workouts are missing and the list\n * must not be presented as complete.\n */\n const windowCovered = useMemo(() => {\n if (!range || !fetchedHasMore) {\n return true;\n }\n const oldest = fetchedLogs[fetchedLogs.length - 1];\n return oldest != null && msOf(oldest.performedAt) <= range.start;\n }, [range, fetchedHasMore, fetchedLogs]);\n\n const rangeOptions = useMemo<FilterOption<RangeKey>[]>(\n () =>\n RANGE_KEYS.map((key) => ({\n value: key,\n label: `${rangeName(key, tr)} · ${windowLabel(resolveWindow(key))}`,\n })),\n [tr]\n );\n\n const deltaLabels = useMemo<DeltaLabels>(\n () =>\n rangeKey === ''\n ? {\n comparison: tr('atheletebridge.pages.performance.window.vsPrevious', 'vs previous'),\n noComparison: tr(\n 'atheletebridge.pages.performance.window.firstReading',\n 'First reading'\n ),\n }\n : {\n comparison: comparisonName(rangeKey, tr),\n noComparison: noComparisonName(rangeKey, tr),\n },\n [rangeKey, tr]\n );\n\n // Group by (type + label) so every distinct measurement is its own trend.\n const metricSeries = useMemo<MetricSeries[]>(() => {\n const groups = new Map<string, AthPerformanceMetric[]>();\n for (const m of metrics.data ?? []) {\n const key = `${m.type}::${m.label}`;\n const list = groups.get(key) ?? [];\n list.push(m);\n groups.set(key, list);\n }\n return Array.from(groups.entries())\n .map(([key, list]): MetricSeries | null => {\n const asc = [...list].sort((a, b) => msOf(a.recordedAt) - msOf(b.recordedAt));\n // Readings inside the window drive the headline, sparkline and range.\n const inWindow = range\n ? asc.filter((m) => {\n const at = msOf(m.recordedAt);\n return at >= range.start && at <= range.end;\n })\n : asc;\n if (inWindow.length === 0) {\n return null;\n }\n const values = inWindow.map((m) => m.value);\n const newest = inWindow[inWindow.length - 1];\n\n // The comparison reading is the latest one in the window immediately\n // BEFORE this one (equal length), so \"vs previous 7 days\" really is the\n // previous 7 days. Without a window it stays the preceding reading.\n let previous: number | null = null;\n if (range) {\n const priorStart = range.start - (range.end - range.start + 1);\n const prior = asc.filter((m) => {\n const at = msOf(m.recordedAt);\n return at >= priorStart && at < range.start;\n });\n previous = prior.length > 0 ? prior[prior.length - 1].value : null;\n } else {\n previous = asc.length > 1 ? asc[asc.length - 2].value : null;\n }\n\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n };\n })\n .filter((entry): entry is MetricSeries => entry !== null)\n .sort((a, b) => msOf(b.recordedAt) - msOf(a.recordedAt));\n }, [metrics.data, range]);\n\n const columns: ResponsiveTableColumn<AthWorkoutLog>[] = useMemo(\n () => [\n {\n key: 'workout',\n header: tr('atheletebridge.pages.performance.column.workout', 'Workout'),\n priority: 'primary',\n cell: (log) => {\n const Icon = workoutIcon(log.title);\n return (\n <div className=\"flex items-center gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n </span>\n <span className=\"min-w-0 truncate font-medium text-text-primary\">{log.title}</span>\n </div>\n );\n },\n },\n {\n key: 'duration',\n header: tr('atheletebridge.pages.performance.column.duration', 'Duration'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.duration', 'Duration'),\n cell: (log) =>\n log.durationMin != null ? (\n <span className=\"tabular-nums text-text-secondary\">\n {formatDuration(log.durationMin)}\n </span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n {\n key: 'rpe',\n header: tr('atheletebridge.pages.performance.column.rpe', 'RPE'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.rpe', 'RPE'),\n cell: (log) =>\n log.rpe != null ? <RpeChip rpe={log.rpe} /> : <span className=\"text-text-muted\">—</span>,\n },\n {\n key: 'date',\n header: tr('atheletebridge.pages.performance.column.date', 'Date'),\n priority: 'secondary',\n mobileLabel: tr('atheletebridge.pages.performance.column.date', 'Date'),\n cell: (log) => (\n <span className=\"tabular-nums text-text-secondary\">{formatDate(log.performedAt)}</span>\n ),\n },\n {\n key: 'status',\n header: tr('atheletebridge.pages.performance.column.status', 'Status'),\n priority: 'primary',\n mobileLabel: tr('atheletebridge.pages.performance.column.status', 'Status'),\n cell: (log) => <StatusBadge status={log.status} />,\n },\n {\n key: 'notes',\n header: tr('atheletebridge.pages.performance.column.notes', 'Notes'),\n priority: 'hidden-on-mobile',\n cell: (log) =>\n log.notes ? (\n <span className=\"line-clamp-2 max-w-[22ch] text-text-secondary\">{log.notes}</span>\n ) : (\n <span className=\"text-text-muted\">—</span>\n ),\n },\n ],\n [tr]\n );\n\n return (\n <div className=\"space-y-8 pb-24 sm:pb-0\">\n {/* Actions */}\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <p className=\"text-sm text-text-secondary\">\n {tr('atheletebridge.pages.performance.tracking', 'Tracking')}{' '}\n <span className=\"font-medium text-text-primary\">\n {displayName || tr('atheletebridge.pages.performance.athleteFallback', 'athlete')}\n </span>\n {range ? (\n <>\n {' · '}\n <span className=\"tabular-nums text-text-muted\">{windowLabel(range)}</span>\n </>\n ) : null}\n </p>\n <div className=\"flex flex-wrap items-center gap-2\">\n {/* Date-range window — scopes the metric tiles, their deltas and the\n workout history below to a rolling period. */}\n <span className=\"flex items-center gap-2\" data-tour=\"performance-date-range\">\n <CalendarRange className=\"h-4 w-4 shrink-0 text-text-muted\" aria-hidden=\"true\" />\n <FilterSelect<RangeKey>\n value={rangeKey}\n onChange={(next) => {\n setRangeKey(next);\n setPage(0);\n }}\n options={rangeOptions}\n allLabel={tr('atheletebridge.pages.performance.window.allTime', 'All time')}\n ariaLabel={tr('atheletebridge.pages.performance.window.ariaLabel', 'Date range')}\n />\n </span>\n <button\n type=\"button\"\n onClick={() => setMetricOpen(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Gauge className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.performance.recordMetric', 'Record metric')}\n </button>\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'hidden sm:inline-flex')}\n >\n <Dumbbell className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </div>\n </div>\n\n {/* Metrics — hero stat tiles with real trend sparklines */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.performance.metricsTitle', 'Performance metrics')}\n </h2>\n {metrics.isLoading ? (\n <LoadingState\n label={tr('atheletebridge.pages.performance.loadingMetrics', 'Loading metrics…')}\n />\n ) : metrics.isError ? (\n <ErrorState onRetry={() => void metrics.refetch()} />\n ) : metricSeries.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Gauge className=\"h-10 w-10\" />}\n title={\n range\n ? tr(\n 'atheletebridge.pages.performance.metrics.emptyWindowTitle',\n 'No metrics in this window'\n )\n : tr('atheletebridge.pages.performance.metrics.emptyTitle', 'No metrics recorded')\n }\n description={\n range\n ? tr(\n 'atheletebridge.pages.performance.metrics.emptyWindowDescription',\n 'Nothing was recorded for this athlete in the selected period. Widen the date range, or record a metric.'\n )\n : tr(\n 'atheletebridge.pages.performance.noMetricsDescription',\n 'Record a metric — speed, strength, endurance and more — to start charting progress.'\n )\n }\n action={\n <button type=\"button\" onClick={() => setMetricOpen(true)} className={GRADIENT_CTA}>\n <Plus className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.performance.recordMetricAction', 'Record metric')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {metricSeries.map((m) => (\n <MetricTile key={m.key} metric={m} labels={deltaLabels} tr={tr} />\n ))}\n </div>\n )}\n </section>\n\n {/* Workout logs */}\n <section>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {tr('atheletebridge.pages.performance.workoutsTitle', 'Workout history')}\n </h2>\n {logs.isLoading ? (\n <LoadingState\n label={tr('atheletebridge.pages.performance.loadingWorkouts', 'Loading workouts…')}\n />\n ) : logs.isError ? (\n <ErrorState onRetry={() => void logs.refetch()} />\n ) : logItems.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Activity className=\"h-10 w-10\" />}\n title={\n range\n ? tr(\n 'atheletebridge.pages.performance.workouts.emptyWindowTitle',\n 'No workouts in this window'\n )\n : tr('atheletebridge.pages.performance.workouts.emptyTitle', 'No workouts logged')\n }\n description={\n range\n ? tr(\n 'atheletebridge.pages.performance.workouts.emptyWindowDescription',\n 'Nothing was logged for this athlete in the selected period. Widen the date range, or log a workout.'\n )\n : tr(\n 'atheletebridge.pages.performance.noWorkoutsDescription',\n \"Log a workout to build this athlete's training history.\"\n )\n }\n action={\n <button type=\"button\" onClick={() => setLogOpen(true)} className={GRADIENT_CTA}>\n <Dumbbell className=\"h-4 w-4\" />{' '}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n }\n />\n ) : (\n <>\n <GlassCard treatment=\"metric\" className=\"overflow-hidden p-1 sm:p-2\">\n <ResponsiveTable\n columns={columns}\n data={logItems}\n rowKey={(log) => log.id}\n caption={tr(\n 'atheletebridge.pages.performance.workouts.caption',\n 'Workout history for {{name}}'\n ).replace(\n '{{name}}',\n displayName || tr('atheletebridge.pages.performance.athleteFallback', 'athlete')\n )}\n />\n </GlassCard>\n <CursorPager\n className=\"mt-6\"\n page={pageIndex}\n hasMore={hasMore}\n onPrev={() => setPage(Math.max(0, pageIndex - 1))}\n onNext={() => setPage(pageIndex + 1)}\n />\n </>\n )}\n {!windowCovered ? (\n <p className=\"mt-3 text-xs text-text-muted\">\n {tr(\n 'atheletebridge.pages.performance.workouts.windowTruncated',\n 'Only the most recent page of workouts was searched, and it does not reach back to the start of this window — older sessions in the range are not listed.'\n )}\n </p>\n ) : null}\n </section>\n\n {/* Mobile sticky Log workout — thumb-reachable primary action */}\n <div className=\"sticky bottom-0 z-30 -mx-6 border-t border-border-seam bg-bg-canvas/85 px-6 pb-[max(env(safe-area-inset-bottom),0.75rem)] pt-3 backdrop-blur sm:hidden\">\n <button\n type=\"button\"\n onClick={() => setLogOpen(true)}\n className={cn(GRADIENT_CTA, 'w-full py-3 text-base')}\n >\n <Plus className=\"h-5 w-5\" />{' '}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </div>\n\n <LogWorkoutDialog\n open={logOpen}\n onOpenChange={setLogOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n <RecordMetricDialog\n open={metricOpen}\n onOpenChange={setMetricOpen}\n athleteId={athleteId}\n displayName={displayName}\n />\n </div>\n );\n}\n\n/** Pick a workout row icon from the (real) title text. Cosmetic only. */\nfunction workoutIcon(title: string): LucideIcon {\n const t = title.toLowerCase();\n if (/strength|lift|squat|deadlift|bench|1rm|weight/.test(t)) return Dumbbell;\n if (/speed|sprint|agility|accel|split/.test(t)) return Zap;\n if (/endurance|aerobic|cardio|run|beep|yo-?yo/.test(t)) return Activity;\n if (/recovery|mobility|stretch|rest/.test(t)) return HeartPulse;\n if (/accuracy|shoot|target/.test(t)) return Target;\n return Dumbbell;\n}\n\n/** Bordered RPE chip, color-scaled by session intensity (semantic tokens). */\nfunction RpeChip({ rpe }: { rpe: number }) {\n const tone =\n rpe >= 8\n ? 'border-status-error-border text-status-error-text'\n : rpe >= 5\n ? 'border-status-warning-border text-status-warning-text'\n : 'border-border-seam text-text-secondary';\n return (\n <span\n className={cn(\n 'inline-flex h-7 min-w-[1.75rem] items-center justify-center rounded-md border px-1.5 text-sm font-semibold tabular-nums',\n tone\n )}\n >\n {rpe}\n </span>\n );\n}\n\n/** Hero stat tile for one measured metric: big value, trend sparkline, range. */\nfunction MetricTile({\n metric,\n labels,\n tr,\n}: {\n metric: MetricSeries;\n labels: DeltaLabels;\n tr: TranslateFn;\n}) {\n const Icon = METRIC_ICON[metric.type];\n const delta = metric.previous == null ? null : metric.latest - metric.previous;\n const hasRange = metric.max > metric.min;\n const pct = hasRange\n ? Math.round(((metric.latest - metric.min) / (metric.max - metric.min)) * 100)\n : 100;\n const unitSuffix = metric.unit ? ` ${metric.unit}` : '';\n\n const metricTypeLabel = (t: AthMetricType): string => {\n switch (t) {\n case 'SPEED':\n return tr('atheletebridge.metricType.speed', 'Speed');\n case 'ENDURANCE':\n return tr('atheletebridge.metricType.endurance', 'Endurance');\n case 'STRENGTH':\n return tr('atheletebridge.metricType.strength', 'Strength');\n case 'AGILITY':\n return tr('atheletebridge.metricType.agility', 'Agility');\n case 'ACCURACY':\n return tr('atheletebridge.metricType.accuracy', 'Accuracy');\n case 'WEIGHT':\n return tr('atheletebridge.metricType.weight', 'Weight');\n case 'HEART_RATE':\n return tr('atheletebridge.metricType.heartRate', 'Heart Rate');\n case 'CUSTOM':\n return tr('atheletebridge.metricType.custom', 'Custom');\n }\n };\n\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col gap-3 p-5\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-accent-teal)]\">\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n {metricTypeLabel(metric.type)}\n </span>\n <span className=\"text-xs text-text-muted\">{formatDate(metric.recordedAt)}</span>\n </div>\n\n <div className=\"flex items-end justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"truncate text-sm text-text-secondary\">{metric.label}</p>\n <p className=\"mt-0.5 text-3xl font-bold leading-none tracking-tight tabular-nums text-text-primary\">\n {fmtNum(metric.latest)}\n {metric.unit ? (\n <span className=\"ml-1 text-base font-medium text-text-secondary\">{metric.unit}</span>\n ) : null}\n </p>\n </div>\n {metric.series.length > 1 ? (\n <div className=\"w-24 shrink-0 sm:w-28\">\n <Sparkline\n data={metric.series}\n variant=\"area\"\n height={40}\n label={`${metric.label} trend`}\n />\n </div>\n ) : null}\n </div>\n\n {/* Range scale: min → latest position → max (from the athlete's own series) */}\n {hasRange ? (\n <div>\n <span\n className=\"block h-1.5 overflow-hidden rounded-full bg-bg-sunken\"\n aria-hidden=\"true\"\n >\n <span\n className=\"block h-1.5 rounded-full bg-[var(--color-accent-teal)]\"\n style={{ width: `${Math.max(4, Math.min(100, pct))}%` }}\n />\n </span>\n <div className=\"mt-1 flex justify-between text-[0.7rem] tabular-nums text-text-muted\">\n <span>{fmtNum(metric.min)}</span>\n <span>{fmtNum((metric.min + metric.max) / 2)}</span>\n <span>{fmtNum(metric.max)}</span>\n </div>\n </div>\n ) : null}\n\n {/* Delta against the active comparison period — direction shown neutrally\n (no good/bad claim), and absent rather than guessed when there is no\n reading to compare against. */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null ? (\n <span className=\"text-text-muted\">{labels.noComparison}</span>\n ) : delta === 0 ? (\n <span className=\"text-text-muted\">\n {tr('atheletebridge.pages.performance.window.noChange', 'No change')}{' '}\n {labels.comparison}\n </span>\n ) : (\n <>\n <span className=\"inline-flex items-center gap-0.5 font-medium tabular-nums text-text-secondary\">\n {delta > 0 ? (\n <ArrowUp className=\"h-4 w-4\" aria-hidden=\"true\" />\n ) : (\n <ArrowDown className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {fmtNum(Math.abs(delta))}\n {unitSuffix}\n </span>\n <span className=\"text-text-muted\">{labels.comparison}</span>\n </>\n )}\n </div>\n </GlassCard>\n );\n}\n\ninterface DialogProps {\n open: boolean;\n onOpenChange: (open: boolean) => void;\n athleteId: string;\n displayName: string;\n}\n\nfunction LogWorkoutDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const tr = useAbTranslate();\n const logWorkout = useLogWorkout();\n const [title, setTitle] = useState('');\n const [status, setStatus] = useState<AthWorkoutStatus>('COMPLETED');\n const [performedAt, setPerformedAt] = useState(nowLocal);\n const [durationMin, setDurationMin] = useState('');\n const [rpe, setRpe] = useState('');\n const [totalLoad, setTotalLoad] = useState('');\n const [notes, setNotes] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setTitle('');\n setStatus('COMPLETED');\n setPerformedAt(nowLocal());\n setDurationMin('');\n setRpe('');\n setTotalLoad('');\n setNotes('');\n setError(null);\n };\n\n const statusLabel = (s: AthWorkoutStatus): string => {\n switch (s) {\n case 'COMPLETED':\n return tr('atheletebridge.workoutStatus.completed', 'Completed');\n case 'PLANNED':\n return tr('atheletebridge.workoutStatus.planned', 'Planned');\n case 'SKIPPED':\n return tr('atheletebridge.workoutStatus.skipped', 'Skipped');\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!title.trim()) {\n setError(\n tr('atheletebridge.pages.performance.error.titleRequired', 'A workout title is required.')\n );\n return;\n }\n if (!performedAt) {\n setError(\n tr(\n 'atheletebridge.pages.performance.error.performedAtRequired',\n 'A performed date and time is required.'\n )\n );\n return;\n }\n try {\n await logWorkout.mutateAsync({\n athleteId,\n title: title.trim(),\n status,\n performedAt: new Date(performedAt).toISOString(),\n durationMin: numOrUndefined(durationMin),\n rpe: numOrUndefined(rpe),\n totalLoad: floatOrUndefined(totalLoad),\n notes: notes.trim() || undefined,\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('atheletebridge.pages.performance.error.logFailed', 'Failed to log workout.')\n );\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('atheletebridge.pages.performance.logWorkoutDialogTitle', 'Log workout')}\n {displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.title', 'Title')}\n htmlFor=\"workoutTitle\"\n required\n >\n <input\n id=\"workoutTitle\"\n className={fieldInputClass}\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.placeholder.title',\n 'e.g. Lower-body strength'\n )}\n />\n </FormField>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.status', 'Status')}\n htmlFor=\"workoutStatus\"\n >\n <select\n id=\"workoutStatus\"\n className={fieldInputClass}\n value={status}\n onChange={(e) => setStatus(e.target.value as AthWorkoutStatus)}\n >\n {WORKOUT_STATUSES.map((s) => (\n <option key={s} value={s}>\n {statusLabel(s)}\n </option>\n ))}\n </select>\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.performedAt', 'Performed at')}\n htmlFor=\"workoutPerformedAt\"\n required\n >\n <input\n id=\"workoutPerformedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={performedAt}\n onChange={(e) => setPerformedAt(e.target.value)}\n />\n </FormField>\n </div>\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.durationMin', 'Duration (min)')}\n htmlFor=\"workoutDuration\"\n >\n <input\n id=\"workoutDuration\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n />\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.rpe', 'RPE (1–10)')}\n htmlFor=\"workoutRpe\"\n >\n <input\n id=\"workoutRpe\"\n inputMode=\"numeric\"\n className={fieldInputClass}\n value={rpe}\n onChange={(e) => setRpe(e.target.value)}\n />\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.totalLoad', 'Total load')}\n htmlFor=\"workoutLoad\"\n >\n <input\n id=\"workoutLoad\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={totalLoad}\n onChange={(e) => setTotalLoad(e.target.value)}\n />\n </FormField>\n </div>\n <FormField\n label={tr('atheletebridge.pages.performance.label.notes', 'Notes')}\n htmlFor=\"workoutNotes\"\n >\n <textarea\n id=\"workoutNotes\"\n rows={2}\n className={fieldInputClass}\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.notesPlaceholder',\n 'How did the session go?'\n )}\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={logWorkout.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {logWorkout.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.performance.logWorkout', 'Log workout')}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n\nfunction RecordMetricDialog({ open, onOpenChange, athleteId, displayName }: DialogProps) {\n const tr = useAbTranslate();\n const recordMetric = useRecordMetric();\n const [type, setType] = useState<AthMetricType>('CUSTOM');\n const [label, setLabel] = useState('');\n const [value, setValue] = useState('');\n const [unit, setUnit] = useState('');\n const [source, setSource] = useState('');\n const [recordedAt, setRecordedAt] = useState(nowLocal);\n const [error, setError] = useState<string | null>(null);\n\n const reset = () => {\n setType('CUSTOM');\n setLabel('');\n setValue('');\n setUnit('');\n setSource('');\n setRecordedAt(nowLocal());\n setError(null);\n };\n\n const metricTypeLabel = (t: AthMetricType): string => {\n switch (t) {\n case 'SPEED':\n return tr('atheletebridge.metricType.speed', 'Speed');\n case 'ENDURANCE':\n return tr('atheletebridge.metricType.endurance', 'Endurance');\n case 'STRENGTH':\n return tr('atheletebridge.metricType.strength', 'Strength');\n case 'AGILITY':\n return tr('atheletebridge.metricType.agility', 'Agility');\n case 'ACCURACY':\n return tr('atheletebridge.metricType.accuracy', 'Accuracy');\n case 'WEIGHT':\n return tr('atheletebridge.metricType.weight', 'Weight');\n case 'HEART_RATE':\n return tr('atheletebridge.metricType.heartRate', 'Heart Rate');\n case 'CUSTOM':\n return tr('atheletebridge.metricType.custom', 'Custom');\n }\n };\n\n const handleSubmit = async (e: React.FormEvent) => {\n e.preventDefault();\n setError(null);\n if (!label.trim()) {\n setError(\n tr(\n 'atheletebridge.pages.performance.error.metricLabelRequired',\n 'A metric label is required.'\n )\n );\n return;\n }\n const numericValue = floatOrUndefined(value);\n if (numericValue === undefined) {\n setError(\n tr(\n 'atheletebridge.pages.performance.error.metricValueRequired',\n 'A numeric value is required.'\n )\n );\n return;\n }\n if (!recordedAt) {\n setError(\n tr(\n 'atheletebridge.pages.performance.error.recordedAtRequired',\n 'A recorded date and time is required.'\n )\n );\n return;\n }\n try {\n await recordMetric.mutateAsync({\n athleteId,\n type,\n label: label.trim(),\n value: numericValue,\n unit: unit.trim() || undefined,\n source: source.trim() || undefined,\n recordedAt: new Date(recordedAt).toISOString(),\n });\n reset();\n onOpenChange(false);\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('atheletebridge.pages.performance.error.recordFailed', 'Failed to record metric.')\n );\n }\n };\n\n return (\n <ResponsiveDialog open={open} onOpenChange={onOpenChange}>\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>\n {tr('atheletebridge.pages.performance.recordMetricDialogTitle', 'Record metric')}\n {displayName ? ` — ${displayName}` : ''}\n </ResponsiveDialogTitle>\n </ResponsiveDialogHeader>\n <form onSubmit={handleSubmit} className=\"space-y-4 px-1\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.metricType', 'Metric type')}\n htmlFor=\"metricType\"\n >\n <select\n id=\"metricType\"\n className={fieldInputClass}\n value={type}\n onChange={(e) => setType(e.target.value as AthMetricType)}\n >\n {METRIC_TYPES.map((t) => (\n <option key={t} value={t}>\n {metricTypeLabel(t)}\n </option>\n ))}\n </select>\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.recordedAt', 'Recorded at')}\n htmlFor=\"metricRecordedAt\"\n required\n >\n <input\n id=\"metricRecordedAt\"\n type=\"datetime-local\"\n className={fieldInputClass}\n value={recordedAt}\n onChange={(e) => setRecordedAt(e.target.value)}\n />\n </FormField>\n </div>\n <FormField\n label={tr('atheletebridge.pages.performance.label.metricLabel', 'Label')}\n htmlFor=\"metricLabel\"\n required\n >\n <input\n id=\"metricLabel\"\n className={fieldInputClass}\n value={label}\n onChange={(e) => setLabel(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.placeholder.metricLabel',\n 'e.g. 40m sprint'\n )}\n />\n </FormField>\n <div className=\"grid grid-cols-2 gap-4\">\n <FormField\n label={tr('atheletebridge.pages.performance.label.value', 'Value')}\n htmlFor=\"metricValue\"\n required\n >\n <input\n id=\"metricValue\"\n inputMode=\"decimal\"\n className={fieldInputClass}\n value={value}\n onChange={(e) => setValue(e.target.value)}\n placeholder={tr('atheletebridge.pages.performance.placeholder.value', 'e.g. 4.8')}\n />\n </FormField>\n <FormField\n label={tr('atheletebridge.pages.performance.label.unit', 'Unit')}\n htmlFor=\"metricUnit\"\n >\n <input\n id=\"metricUnit\"\n className={fieldInputClass}\n value={unit}\n onChange={(e) => setUnit(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.placeholder.unit',\n 'e.g. s, kg, bpm'\n )}\n />\n </FormField>\n </div>\n <FormField\n label={tr('atheletebridge.pages.performance.label.source', 'Source')}\n htmlFor=\"metricSource\"\n hint={tr(\n 'atheletebridge.pages.performance.hint.source',\n 'Optional — device, test or coach.'\n )}\n >\n <input\n id=\"metricSource\"\n className={fieldInputClass}\n value={source}\n onChange={(e) => setSource(e.target.value)}\n placeholder={tr(\n 'atheletebridge.pages.performance.placeholder.source',\n 'e.g. GPS vest'\n )}\n />\n </FormField>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n <ResponsiveDialogFooter>\n <button\n type=\"button\"\n onClick={() => onOpenChange(false)}\n className=\"rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={recordMetric.isPending}\n className={cn(GRADIENT_CTA, 'disabled:opacity-60')}\n >\n {recordMetric.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Plus className=\"h-4 w-4\" />\n )}\n {tr('atheletebridge.pages.performance.recordMetric', 'Record metric')}\n </button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA+EA,IAAM,IAAY,IAEZ,IAAuC;CAAC;CAAa;CAAW;AAAS,GACzE,IAAe,OAAO,KAAK,CAAkB,GAe7C,KAAqB,KAMrB,KAAoB,EAAE,OAAO,IAAI,GAKjC,KAAyB;CAAC;CAAM;CAAO;AAAK,GAC5C,IAAuC;CAAE,MAAM;CAAG,OAAO;CAAI,OAAO;AAAG,GAgBvE,MAAiB,MAA8B;CACnD,IAAM,oBAAa,IAAI,KAAK;CAC5B,EAAW,SAAS,IAAI,IAAI,IAAI,GAAG;CACnC,IAAM,IAAgB,IAAI,KAAK,CAAU;CAGzC,OAFA,EAAc,SAAS,GAAG,GAAG,GAAG,CAAC,GACjC,EAAc,QAAQ,EAAc,QAAQ,KAAK,EAAW,KAAO,EAAE,GAC9D;EAAE,OAAO,EAAc,QAAQ;EAAG,KAAK,EAAW,QAAQ;CAAE;AACrE,GAGM,KAAQ,MAAwB,IAAI,KAAK,CAAG,EAAE,QAAQ,GAGtD,MAAa,MACjB,EAAW,IAAI,KAAK,CAAE,GAAG;CAAE,MAAM,KAAA;CAAW,QAAQ,KAAA;CAAW,QAAQ,KAAA;AAAU,CAAC,GAG9E,MAAe,MACnB,GAAG,GAAU,EAAM,KAAK,EAAE,KAAK,GAAU,EAAM,GAAG,KAM9C,MAAa,GAAe,MAA4B;CAC5D,QAAQ,GAAR;EACE,KAAK,MACH,OAAO,EAAG,iDAAiD,aAAa;EAC1E,KAAK,OACH,OAAO,EAAG,kDAAkD,cAAc;EAC5E,KAAK,OACH,OAAO,EAAG,kDAAkD,cAAc;CAC9E;AACF,GAGM,MAAkB,GAAe,MAA4B;CACjE,QAAQ,GAAR;EACE,KAAK,MACH,OAAO,EAAG,mDAAmD,oBAAoB;EACnF,KAAK,OACH,OAAO,EAAG,oDAAoD,qBAAqB;EACrF,KAAK,OACH,OAAO,EAAG,oDAAoD,qBAAqB;CACvF;AACF,GAGM,MAAoB,GAAe,MAA4B;CACnE,QAAQ,GAAR;EACE,KAAK,MACH,OAAO,EACL,oDACA,mCACF;EACF,KAAK,OACH,OAAO,EACL,qDACA,oCACF;EACF,KAAK,OACH,OAAO,EACL,qDACA,oCACF;CACJ;AACF,GAGM,IACJ,ieAGI,KAAiD;CACrD,OAAO;CACP,WAAW;CACX,UAAU;CACV,SAAS;CACT,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,QAAQ;AACV,GAEM,MAAkB,MAAsC;CAC5D,IAAM,IAAI,EAAM,KAAK;CACrB,IAAI,CAAC,GAAG;CACR,IAAM,IAAI,OAAO,SAAS,GAAG,EAAE;CAC/B,OAAO,OAAO,MAAM,CAAC,IAAI,KAAA,IAAY;AACvC,GAEM,MAAoB,MAAsC;CAC9D,IAAM,IAAI,EAAM,KAAK;CACrB,IAAI,CAAC,GAAG;CACR,IAAM,IAAI,OAAO,WAAW,CAAC;CAC7B,OAAO,OAAO,MAAM,CAAC,IAAI,KAAA,IAAY;AACvC,GAGM,UAAyB;CAC7B,IAAM,oBAAI,IAAI,KAAK;CAEnB,wBAAO,IADW,KAAK,EAAE,QAAQ,IAAI,EAAE,kBAAkB,IAAI,GACtD,GAAM,YAAY,EAAE,MAAM,GAAG,EAAE;AACxC,GAGM,KAAU,OACF,KAAK,IAAI,CACX,KAAO,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,GACxC,QAAQ,UAAU,EAAE;AAG/B,SAAgB,KAAkB;CAChC,IAAM,IAAK,EAAe,GACpB,CAAC,GAAS,KAAc,EAA6B;EAAE,WAAW;EAAI,aAAa;CAAG,CAAC;CAE7F,OACE,kBAAC,GAAD;EACE,OAAO,EAAG,0CAA0C,aAAa;EACjE,aAAa,EACX,gDACA,yEACF;YALF,CAQE,kBAAC,OAAD;GACE,aAAU;GACV,WAAU;aAEV,kBAAC,GAAD;IAAW,WAAU;IAAU,WAAU;cACvC,kBAAC,GAAD;KACE,WAAW,EAAQ;KACnB,UAAU;KACV,UAAS;IACV,CAAA;GACQ,CAAA;EACR,CAAA,GAEJ,EAAQ,YACP,kBAAC,IAAD;GAAoB,WAAW,EAAQ;GAAW,aAAa,EAAQ;EAAc,CAAA,IAErF,kBAAC,GAAD;GACE,cAAa;GACb,MAAM,kBAAC,GAAD,EAAO,WAAU,YAAa,CAAA;GACpC,OAAO,EACL,qDACA,0BACF;GACA,aAAa,EACX,oDACA,8FACF;EACD,CAAA,CAEO;;AAEhB;AA8BA,SAAS,GAAmB,EAAE,cAAW,kBAAwC;CAC/E,IAAM,IAAK,EAAe,GACpB,CAAC,GAAM,KAAW,EAAS,CAAC,GAC5B,CAAC,GAAU,KAAe,EAAwB,EAAE,GACpD,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,CAAC,GAAY,KAAiB,EAAS,EAAK,GAI5C,IAAQ,QACL,MAAa,KAAK,OAAO,GAAc,CAAQ,GACtD,CAAC,CAAQ,CACX,GAIM,IAAO,EAAe;EAC1B;EACA,YAAY,IACR,EAAE,OAAO,GAAmB,IAC5B;GAAE,OAAO;GAAW,QAAQ,IAAO;EAAU;CACnD,CAAC,GACK,IAAU,EAAsB,GAAW,EAAiB,GAE5D,IAAc,QAAc,EAAK,MAAM,SAAS,CAAC,GAAG,CAAC,EAAK,IAAI,CAAC,GAC/D,IAAiB,EAAK,MAAM,SAAS,WAAW,IAEhD,IAAa,QACZ,IAGE,EAAY,QAAQ,MAAQ;EACjC,IAAM,IAAK,EAAK,EAAI,WAAW;EAC/B,OAAO,KAAM,EAAM,SAAS,KAAM,EAAM;CAC1C,CAAC,IALQ,GAMR,CAAC,GAAa,CAAK,CAAC,GAIjB,IAAW,IAAQ,KAAK,IAAI,GAAG,KAAK,KAAK,EAAW,SAAS,CAAS,IAAI,CAAC,IAAI,GAC/E,IAAY,KAAK,IAAI,GAAM,CAAQ,GACnC,IAAW,IACb,EAAW,MAAM,IAAY,IAAY,IAAY,KAAK,CAAS,IACnE,GACE,IAAU,IAAQ,EAAW,UAAU,IAAY,KAAK,IAAY,GASpE,IAAgB,QAAc;EAClC,IAAI,CAAC,KAAS,CAAC,GACb,OAAO;EAET,IAAM,IAAS,EAAY,EAAY,SAAS;EAChD,OAAO,KAAU,QAAQ,EAAK,EAAO,WAAW,KAAK,EAAM;CAC7D,GAAG;EAAC;EAAO;EAAgB;CAAW,CAAC,GAEjC,IAAe,QAEjB,GAAW,KAAK,OAAS;EACvB,OAAO;EACP,OAAO,GAAG,GAAU,GAAK,CAAE,EAAE,KAAK,GAAY,GAAc,CAAG,CAAC;CAClE,EAAE,GACJ,CAAC,CAAE,CACL,GAEM,IAAc,QAEhB,MAAa,KACT;EACE,YAAY,EAAG,sDAAsD,aAAa;EAClF,cAAc,EACZ,wDACA,eACF;CACF,IACA;EACE,YAAY,GAAe,GAAU,CAAE;EACvC,cAAc,GAAiB,GAAU,CAAE;CAC7C,GACN,CAAC,GAAU,CAAE,CACf,GAGM,IAAe,QAA8B;EACjD,IAAM,oBAAS,IAAI,IAAoC;EACvD,KAAK,IAAM,KAAK,EAAQ,QAAQ,CAAC,GAAG;GAClC,IAAM,IAAM,GAAG,EAAE,KAAK,IAAI,EAAE,SACtB,IAAO,EAAO,IAAI,CAAG,KAAK,CAAC;GAEjC,AADA,EAAK,KAAK,CAAC,GACX,EAAO,IAAI,GAAK,CAAI;EACtB;EACA,OAAO,MAAM,KAAK,EAAO,QAAQ,CAAC,EAC/B,KAAK,CAAC,GAAK,OAA+B;GACzC,IAAM,IAAM,CAAC,GAAG,CAAI,EAAE,MAAM,GAAG,MAAM,EAAK,EAAE,UAAU,IAAI,EAAK,EAAE,UAAU,CAAC,GAEtE,IAAW,IACb,EAAI,QAAQ,MAAM;IAChB,IAAM,IAAK,EAAK,EAAE,UAAU;IAC5B,OAAO,KAAM,EAAM,SAAS,KAAM,EAAM;GAC1C,CAAC,IACD;GACJ,IAAI,EAAS,WAAW,GACtB,OAAO;GAET,IAAM,IAAS,EAAS,KAAK,MAAM,EAAE,KAAK,GACpC,IAAS,EAAS,EAAS,SAAS,IAKtC,IAA0B;GAC9B,IAAI,GAAO;IACT,IAAM,IAAa,EAAM,SAAS,EAAM,MAAM,EAAM,QAAQ,IACtD,IAAQ,EAAI,QAAQ,MAAM;KAC9B,IAAM,IAAK,EAAK,EAAE,UAAU;KAC5B,OAAO,KAAM,KAAc,IAAK,EAAM;IACxC,CAAC;IACD,IAAW,EAAM,SAAS,IAAI,EAAM,EAAM,SAAS,GAAG,QAAQ;GAChE,OACE,IAAW,EAAI,SAAS,IAAI,EAAI,EAAI,SAAS,GAAG,QAAQ;GAG1D,OAAO;IACL;IACA,MAAM,EAAO;IACb,OAAO,EAAO;IACd,MAAM,EAAO;IACb,QAAQ,EAAO;IACf;IACA,KAAK,KAAK,IAAI,GAAG,CAAM;IACvB,KAAK,KAAK,IAAI,GAAG,CAAM;IACvB,QAAQ;IACR,YAAY,EAAO;GACrB;EACF,CAAC,EACA,QAAQ,MAAiC,MAAU,IAAI,EACvD,MAAM,GAAG,MAAM,EAAK,EAAE,UAAU,IAAI,EAAK,EAAE,UAAU,CAAC;CAC3D,GAAG,CAAC,EAAQ,MAAM,CAAK,CAAC,GAElB,IAAkD,QAChD;EACJ;GACE,KAAK;GACL,QAAQ,EAAG,mDAAmD,SAAS;GACvE,UAAU;GACV,OAAO,MAGH,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eACd,kBAJO,GAAY,EAAI,KAItB,GAAD;MAAM,WAAU;MAAU,eAAY;KAAQ,CAAA;IAC1C,CAAA,GACN,kBAAC,QAAD;KAAM,WAAU;eAAkD,EAAI;IAAY,CAAA,CAC/E;;EAGX;EACA;GACE,KAAK;GACL,QAAQ,EAAG,oDAAoD,UAAU;GACzE,UAAU;GACV,aAAa,EAAG,oDAAoD,UAAU;GAC9E,OAAO,MACL,EAAI,eAAe,OAKjB,kBAAC,QAAD;IAAM,WAAU;cAAkB;GAAO,CAAA,IAJzC,kBAAC,QAAD;IAAM,WAAU;cACb,EAAe,EAAI,WAAW;GAC3B,CAAA;EAIZ;EACA;GACE,KAAK;GACL,QAAQ,EAAG,+CAA+C,KAAK;GAC/D,UAAU;GACV,aAAa,EAAG,+CAA+C,KAAK;GACpE,OAAO,MACL,EAAI,OAAO,OAAmC,kBAAC,QAAD;IAAM,WAAU;cAAkB;GAAO,CAAA,IAArE,kBAAC,IAAD,EAAS,KAAK,EAAI,IAAM,CAAA;EAC9C;EACA;GACE,KAAK;GACL,QAAQ,EAAG,gDAAgD,MAAM;GACjE,UAAU;GACV,aAAa,EAAG,gDAAgD,MAAM;GACtE,OAAO,MACL,kBAAC,QAAD;IAAM,WAAU;cAAoC,EAAW,EAAI,WAAW;GAAQ,CAAA;EAE1F;EACA;GACE,KAAK;GACL,QAAQ,EAAG,kDAAkD,QAAQ;GACrE,UAAU;GACV,aAAa,EAAG,kDAAkD,QAAQ;GAC1E,OAAO,MAAQ,kBAAC,GAAD,EAAa,QAAQ,EAAI,OAAS,CAAA;EACnD;EACA;GACE,KAAK;GACL,QAAQ,EAAG,iDAAiD,OAAO;GACnE,UAAU;GACV,OAAO,MACL,EAAI,QACF,kBAAC,QAAD;IAAM,WAAU;cAAiD,EAAI;GAAY,CAAA,IAEjF,kBAAC,QAAD;IAAM,WAAU;cAAkB;GAAO,CAAA;EAE/C;CACF,GACA,CAAC,CAAE,CACL;CAEA,OACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAb;MACG,EAAG,6CAA6C,UAAU;MAAG;MAC9D,kBAAC,QAAD;OAAM,WAAU;iBACb,KAAe,EAAG,oDAAoD,SAAS;MAC5E,CAAA;MACL,IACC,kBAAA,GAAA,EAAA,UAAA,CACG,OACD,kBAAC,QAAD;OAAM,WAAU;iBAAgC,GAAY,CAAK;MAAQ,CAAA,CACzE,EAAA,CAAA,IACA;KACH;QACH,kBAAC,OAAD;KAAK,WAAU;eAAf;MAGE,kBAAC,QAAD;OAAM,WAAU;OAA0B,aAAU;iBAApD,CACE,kBAAC,GAAD;QAAe,WAAU;QAAmC,eAAY;OAAQ,CAAA,GAChF,kBAAC,GAAD;QACE,OAAO;QACP,WAAW,MAAS;SAElB,AADA,EAAY,CAAI,GAChB,EAAQ,CAAC;QACX;QACA,SAAS;QACT,UAAU,EAAG,mDAAmD,UAAU;QAC1E,WAAW,EAAG,qDAAqD,YAAY;OAChF,CAAA,CACG;;MACN,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAc,EAAI;OACjC,WAAU;iBAHZ;QAKE,kBAAC,GAAD,EAAO,WAAU,UAAW,CAAA;QAAE;QAC7B,EAAG,iDAAiD,eAAe;OAC9D;;MACR,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,EAAI;OAC9B,WAAW,EAAG,GAAc,uBAAuB;iBAHrD;QAKE,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;QAAE;QAChC,EAAG,+CAA+C,aAAa;OAC1D;;KACL;MACF;;GAGL,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,MAAD;IAAI,WAAU;cACX,EAAG,iDAAiD,qBAAqB;GACxE,CAAA,GACH,EAAQ,YACP,kBAAC,GAAD,EACE,OAAO,EAAG,mDAAmD,kBAAkB,EAChF,CAAA,IACC,EAAQ,UACV,kBAAC,GAAD,EAAY,eAAe,KAAK,EAAQ,QAAQ,EAAI,CAAA,IAClD,EAAa,WAAW,IAC1B,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAO,WAAU,YAAa,CAAA;IACpC,OACE,IACI,EACE,6DACA,2BACF,IACA,EAAG,uDAAuD,qBAAqB;IAErF,aACE,IACI,EACE,mEACA,yGACF,IACA,EACE,yDACA,qFACF;IAEN,QACE,kBAAC,UAAD;KAAQ,MAAK;KAAS,eAAe,EAAc,EAAI;KAAG,WAAW;eAArE;MACE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;MAAE;MAC5B,EAAG,uDAAuD,eAAe;KACpE;;GAEX,CAAA,IAED,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAa,KAAK,MACjB,kBAAC,IAAD;KAAwB,QAAQ;KAAG,QAAQ;KAAiB;IAAK,GAAhD,EAAE,GAA8C,CAClE;GACE,CAAA,CAEA,EAAA,CAAA;GAGT,kBAAC,WAAD,EAAA,UAAA;IACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAG,kDAAkD,iBAAiB;IACrE,CAAA;IACH,EAAK,YACJ,kBAAC,GAAD,EACE,OAAO,EAAG,oDAAoD,mBAAmB,EAClF,CAAA,IACC,EAAK,UACP,kBAAC,GAAD,EAAY,eAAe,KAAK,EAAK,QAAQ,EAAI,CAAA,IAC/C,EAAS,WAAW,IACtB,kBAAC,GAAD;KACE,cAAa;KACb,MAAM,kBAAC,GAAD,EAAU,WAAU,YAAa,CAAA;KACvC,OACE,IACI,EACE,8DACA,4BACF,IACA,EAAG,wDAAwD,oBAAoB;KAErF,aACE,IACI,EACE,oEACA,qGACF,IACA,EACE,0DACA,yDACF;KAEN,QACE,kBAAC,UAAD;MAAQ,MAAK;MAAS,eAAe,EAAW,EAAI;MAAG,WAAW;gBAAlE;OACE,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;OAAE;OAChC,EAAG,+CAA+C,aAAa;MAC1D;;IAEX,CAAA,IAED,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;KAAW,WAAU;KAAS,WAAU;eACtC,kBAAC,IAAD;MACW;MACT,MAAM;MACN,SAAS,MAAQ,EAAI;MACrB,SAAS,EACP,qDACA,8BACF,EAAE,QACA,YACA,KAAe,EAAG,oDAAoD,SAAS,CACjF;KACD,CAAA;IACQ,CAAA,GACX,kBAAC,GAAD;KACE,WAAU;KACV,MAAM;KACG;KACT,cAAc,EAAQ,KAAK,IAAI,GAAG,IAAY,CAAC,CAAC;KAChD,cAAc,EAAQ,IAAY,CAAC;IACpC,CAAA,CACD,EAAA,CAAA;IAEF,IAOE,OANF,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,6DACA,0JACF;IACC,CAAA;GAEE,EAAA,CAAA;GAGT,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAW,EAAI;KAC9B,WAAW,EAAG,GAAc,uBAAuB;eAHrD;MAKE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA;MAAE;MAC5B,EAAG,+CAA+C,aAAa;KAC1D;;GACL,CAAA;GAEL,kBAAC,IAAD;IACE,MAAM;IACN,cAAc;IACH;IACE;GACd,CAAA;GACD,kBAAC,IAAD;IACE,MAAM;IACN,cAAc;IACH;IACE;GACd,CAAA;EACE;;AAET;AAGA,SAAS,GAAY,GAA2B;CAC9C,IAAM,IAAI,EAAM,YAAY;CAM5B,OALI,gDAAgD,KAAK,CAAC,IAAU,IAChE,mCAAmC,KAAK,CAAC,IAAU,IACnD,2CAA2C,KAAK,CAAC,IAAU,IAC3D,iCAAiC,KAAK,CAAC,IAAU,IACjD,wBAAwB,KAAK,CAAC,IAAU,IACrC;AACT;AAGA,SAAS,GAAQ,EAAE,UAAwB;CAOzC,OACE,kBAAC,QAAD;EACE,WAAW,EACT,2HARJ,KAAO,IACH,sDACA,KAAO,IACL,0DACA,wCAMJ;YAEC;CACG,CAAA;AAEV;AAGA,SAAS,GAAW,EAClB,WACA,WACA,SAKC;CACD,IAAM,IAAO,GAAY,EAAO,OAC1B,IAAQ,EAAO,YAAY,OAAO,OAAO,EAAO,SAAS,EAAO,UAChE,IAAW,EAAO,MAAM,EAAO,KAC/B,IAAM,IACR,KAAK,OAAQ,EAAO,SAAS,EAAO,QAAQ,EAAO,MAAM,EAAO,OAAQ,GAAG,IAC3E,KACE,IAAa,EAAO,OAAO,IAAI,EAAO,SAAS;CAuBrD,OACE,kBAAC,GAAD;EAAW,WAAU;EAAS,WAAU;YAAxC;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAAhB,CACE,kBAAC,GAAD;MAAM,WAAU;MAAU,eAAY;KAAQ,CAAA,KAzB7B,MAA6B;MACpD,QAAQ,GAAR;OACE,KAAK,SACH,OAAO,EAAG,mCAAmC,OAAO;OACtD,KAAK,aACH,OAAO,EAAG,uCAAuC,WAAW;OAC9D,KAAK,YACH,OAAO,EAAG,sCAAsC,UAAU;OAC5D,KAAK,WACH,OAAO,EAAG,qCAAqC,SAAS;OAC1D,KAAK,YACH,OAAO,EAAG,sCAAsC,UAAU;OAC5D,KAAK,UACH,OAAO,EAAG,oCAAoC,QAAQ;OACxD,KAAK,cACH,OAAO,EAAG,uCAAuC,YAAY;OAC/D,KAAK,UACH,OAAO,EAAG,oCAAoC,QAAQ;MAC1D;KACF,GAOyB,EAAO,IAAI,CACxB;QACN,kBAAC,QAAD;KAAM,WAAU;eAA2B,EAAW,EAAO,UAAU;IAAQ,CAAA,CAC5E;;GAEL,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAAwC,EAAO;KAAS,CAAA,GACrE,kBAAC,KAAD;MAAG,WAAU;gBAAb,CACG,EAAO,EAAO,MAAM,GACpB,EAAO,OACN,kBAAC,QAAD;OAAM,WAAU;iBAAkD,EAAO;MAAW,CAAA,IAClF,IACH;OACA;QACJ,EAAO,OAAO,SAAS,IACtB,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MACE,MAAM,EAAO;MACb,SAAQ;MACR,QAAQ;MACR,OAAO,GAAG,EAAO,MAAM;KACxB,CAAA;IACE,CAAA,IACH,IACD;;GAGJ,IACC,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,QAAD;IACE,WAAU;IACV,eAAY;cAEZ,kBAAC,QAAD;KACE,WAAU;KACV,OAAO,EAAE,OAAO,GAAG,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,CAAG,CAAC,EAAE,GAAG;IACvD,CAAA;GACG,CAAA,GACN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,QAAD,EAAA,UAAO,EAAO,EAAO,GAAG,EAAQ,CAAA;KAChC,kBAAC,QAAD,EAAA,UAAO,GAAQ,EAAO,MAAM,EAAO,OAAO,CAAC,EAAQ,CAAA;KACnD,kBAAC,QAAD,EAAA,UAAO,EAAO,EAAO,GAAG,EAAQ,CAAA;IAC7B;KACF,EAAA,CAAA,IACH;GAKJ,kBAAC,OAAD;IAAK,WAAU;cACZ,KAAS,OACR,kBAAC,QAAD;KAAM,WAAU;eAAmB,EAAO;IAAmB,CAAA,IAC3D,MAAU,IACZ,kBAAC,QAAD;KAAM,WAAU;eAAhB;MACG,EAAG,oDAAoD,WAAW;MAAG;MACrE,EAAO;KACJ;SAEN,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,QAAD;KAAM,WAAU;eAAhB;MAEI,EADD,IAAQ,IACN,IAEA,GAFD;OAAS,WAAU;OAAU,eAAY;MAAQ,CAEE;MAEpD,EAAO,KAAK,IAAI,CAAK,CAAC;MACtB;KACG;QACN,kBAAC,QAAD;KAAM,WAAU;eAAmB,EAAO;IAAiB,CAAA,CAC3D,EAAA,CAAA;GAED,CAAA;EACI;;AAEf;AASA,SAAS,GAAiB,EAAE,SAAM,iBAAc,cAAW,kBAA4B;CACrF,IAAM,IAAK,EAAe,GACpB,IAAa,EAAc,GAC3B,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAQ,KAAa,EAA2B,WAAW,GAC5D,CAAC,GAAa,KAAkB,EAAS,CAAQ,GACjD,CAAC,GAAa,KAAkB,EAAS,EAAE,GAC3C,CAAC,GAAK,KAAU,EAAS,EAAE,GAC3B,CAAC,GAAW,KAAgB,EAAS,EAAE,GACvC,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAO,KAAY,EAAwB,IAAI,GAEhD,UAAc;EAQlB,AAPA,EAAS,EAAE,GACX,EAAU,WAAW,GACrB,EAAe,EAAS,CAAC,GACzB,EAAe,EAAE,GACjB,EAAO,EAAE,GACT,EAAa,EAAE,GACf,EAAS,EAAE,GACX,EAAS,IAAI;CACf,GAEM,KAAe,MAAgC;EACnD,QAAQ,GAAR;GACE,KAAK,aACH,OAAO,EAAG,0CAA0C,WAAW;GACjE,KAAK,WACH,OAAO,EAAG,wCAAwC,SAAS;GAC7D,KAAK,WACH,OAAO,EAAG,wCAAwC,SAAS;EAC/D;CACF;CA0CA,OACE,kBAAC,GAAD;EAAwB;EAAoB;YAC1C,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA,CACG,EAAG,0DAA0D,aAAa,GAC1E,IAAc,MAAM,MAAgB,EAChB,EAAA,CAAA,EACD,CAAA,GACxB,kBAAC,QAAD;GAAM,UAAU,OAjDM,MAAuB;IAGjD,IAFA,EAAE,eAAe,GACjB,EAAS,IAAI,GACT,CAAC,EAAM,KAAK,GAAG;KACjB,EACE,EAAG,wDAAwD,8BAA8B,CAC3F;KACA;IACF;IACA,IAAI,CAAC,GAAa;KAChB,EACE,EACE,8DACA,wCACF,CACF;KACA;IACF;IACA,IAAI;KAYF,AAXA,MAAM,EAAW,YAAY;MAC3B;MACA,OAAO,EAAM,KAAK;MAClB;MACA,aAAa,IAAI,KAAK,CAAW,EAAE,YAAY;MAC/C,aAAa,GAAe,CAAW;MACvC,KAAK,GAAe,CAAG;MACvB,WAAW,GAAiB,CAAS;MACrC,OAAO,EAAM,KAAK,KAAK,KAAA;KACzB,CAAC,GACD,EAAM,GACN,EAAa,EAAK;IACpB,SAAS,GAAK;KACZ,EACE,aAAe,QACX,EAAI,UACJ,EAAG,oDAAoD,wBAAwB,CACrF;IACF;GACF;GAWoC,WAAU;aAAxC;IACE,kBAAC,GAAD;KACE,OAAO,EAAG,gDAAgD,OAAO;KACjE,SAAQ;KACR,UAAA;eAEA,kBAAC,SAAD;MACE,IAAG;MACH,WAAW;MACX,OAAO;MACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;MACxC,aAAa,EACX,sDACA,0BACF;KACD,CAAA;IACQ,CAAA;IACX,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,OAAO,EAAG,iDAAiD,QAAQ;MACnE,SAAQ;gBAER,kBAAC,UAAD;OACE,IAAG;OACH,WAAW;OACX,OAAO;OACP,WAAW,MAAM,EAAU,EAAE,OAAO,KAAyB;iBAE5D,EAAiB,KAAK,MACrB,kBAAC,UAAD;QAAgB,OAAO;kBACpB,EAAY,CAAC;OACR,GAFK,CAEL,CACT;MACK,CAAA;KACC,CAAA,GACX,kBAAC,GAAD;MACE,OAAO,EAAG,sDAAsD,cAAc;MAC9E,SAAQ;MACR,UAAA;gBAEA,kBAAC,SAAD;OACE,IAAG;OACH,MAAK;OACL,WAAW;OACX,OAAO;OACP,WAAW,MAAM,EAAe,EAAE,OAAO,KAAK;MAC/C,CAAA;KACQ,CAAA,CACR;;IACL,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OACE,OAAO,EAAG,sDAAsD,gBAAgB;OAChF,SAAQ;iBAER,kBAAC,SAAD;QACE,IAAG;QACH,WAAU;QACV,WAAW;QACX,OAAO;QACP,WAAW,MAAM,EAAe,EAAE,OAAO,KAAK;OAC/C,CAAA;MACQ,CAAA;MACX,kBAAC,GAAD;OACE,OAAO,EAAG,8CAA8C,YAAY;OACpE,SAAQ;iBAER,kBAAC,SAAD;QACE,IAAG;QACH,WAAU;QACV,WAAW;QACX,OAAO;QACP,WAAW,MAAM,EAAO,EAAE,OAAO,KAAK;OACvC,CAAA;MACQ,CAAA;MACX,kBAAC,GAAD;OACE,OAAO,EAAG,oDAAoD,YAAY;OAC1E,SAAQ;iBAER,kBAAC,SAAD;QACE,IAAG;QACH,WAAU;QACV,WAAW;QACX,OAAO;QACP,WAAW,MAAM,EAAa,EAAE,OAAO,KAAK;OAC7C,CAAA;MACQ,CAAA;KACR;;IACL,kBAAC,GAAD;KACE,OAAO,EAAG,gDAAgD,OAAO;KACjE,SAAQ;eAER,kBAAC,YAAD;MACE,IAAG;MACH,MAAM;MACN,WAAW;MACX,OAAO;MACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;MACxC,aAAa,EACX,qDACA,yBACF;KACD,CAAA;IACQ,CAAA;IACV,KAAS,kBAAC,KAAD;KAAG,WAAU;eAAkC;IAAS,CAAA;IAClE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAa,EAAK;KACjC,WAAU;eAET,EAAG,iBAAiB,QAAQ;IACvB,CAAA,GACR,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,EAAW;KACrB,WAAW,EAAG,GAAc,qBAAqB;eAHnD,CAKG,EAAW,YACV,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,IAE3C,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAE5B,EAAG,+CAA+C,aAAa,CAC1D;MACc,EAAA,CAAA;GACpB;IACiB,EAAA,CAAA;CACT,CAAA;AAEtB;AAEA,SAAS,GAAmB,EAAE,SAAM,iBAAc,cAAW,kBAA4B;CACvF,IAAM,IAAK,EAAe,GACpB,IAAe,EAAgB,GAC/B,CAAC,GAAM,KAAW,EAAwB,QAAQ,GAClD,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAO,KAAY,EAAS,EAAE,GAC/B,CAAC,GAAM,KAAW,EAAS,EAAE,GAC7B,CAAC,GAAQ,KAAa,EAAS,EAAE,GACjC,CAAC,GAAY,KAAiB,EAAS,CAAQ,GAC/C,CAAC,GAAO,KAAY,EAAwB,IAAI,GAEhD,UAAc;EAOlB,AANA,EAAQ,QAAQ,GAChB,EAAS,EAAE,GACX,EAAS,EAAE,GACX,EAAQ,EAAE,GACV,EAAU,EAAE,GACZ,EAAc,EAAS,CAAC,GACxB,EAAS,IAAI;CACf,GAEM,KAAmB,MAA6B;EACpD,QAAQ,GAAR;GACE,KAAK,SACH,OAAO,EAAG,mCAAmC,OAAO;GACtD,KAAK,aACH,OAAO,EAAG,uCAAuC,WAAW;GAC9D,KAAK,YACH,OAAO,EAAG,sCAAsC,UAAU;GAC5D,KAAK,WACH,OAAO,EAAG,qCAAqC,SAAS;GAC1D,KAAK,YACH,OAAO,EAAG,sCAAsC,UAAU;GAC5D,KAAK,UACH,OAAO,EAAG,oCAAoC,QAAQ;GACxD,KAAK,cACH,OAAO,EAAG,uCAAuC,YAAY;GAC/D,KAAK,UACH,OAAO,EAAG,oCAAoC,QAAQ;EAC1D;CACF;CAsDA,OACE,kBAAC,GAAD;EAAwB;EAAoB;YAC1C,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD,EAAA,UAAA,CACG,EAAG,4DAA4D,eAAe,GAC9E,IAAc,MAAM,MAAgB,EAChB,EAAA,CAAA,EACD,CAAA,GACxB,kBAAC,QAAD;GAAM,UAAU,OA7DM,MAAuB;IAGjD,IAFA,EAAE,eAAe,GACjB,EAAS,IAAI,GACT,CAAC,EAAM,KAAK,GAAG;KACjB,EACE,EACE,8DACA,6BACF,CACF;KACA;IACF;IACA,IAAM,IAAe,GAAiB,CAAK;IAC3C,IAAI,MAAiB,KAAA,GAAW;KAC9B,EACE,EACE,8DACA,8BACF,CACF;KACA;IACF;IACA,IAAI,CAAC,GAAY;KACf,EACE,EACE,6DACA,uCACF,CACF;KACA;IACF;IACA,IAAI;KAWF,AAVA,MAAM,EAAa,YAAY;MAC7B;MACA;MACA,OAAO,EAAM,KAAK;MAClB,OAAO;MACP,MAAM,EAAK,KAAK,KAAK,KAAA;MACrB,QAAQ,EAAO,KAAK,KAAK,KAAA;MACzB,YAAY,IAAI,KAAK,CAAU,EAAE,YAAY;KAC/C,CAAC,GACD,EAAM,GACN,EAAa,EAAK;IACpB,SAAS,GAAK;KACZ,EACE,aAAe,QACX,EAAI,UACJ,EAAG,uDAAuD,0BAA0B,CAC1F;IACF;GACF;GAWoC,WAAU;aAAxC;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,OAAO,EAAG,qDAAqD,aAAa;MAC5E,SAAQ;gBAER,kBAAC,UAAD;OACE,IAAG;OACH,WAAW;OACX,OAAO;OACP,WAAW,MAAM,EAAQ,EAAE,OAAO,KAAsB;iBAEvD,EAAa,KAAK,MACjB,kBAAC,UAAD;QAAgB,OAAO;kBACpB,EAAgB,CAAC;OACZ,GAFK,CAEL,CACT;MACK,CAAA;KACC,CAAA,GACX,kBAAC,GAAD;MACE,OAAO,EAAG,qDAAqD,aAAa;MAC5E,SAAQ;MACR,UAAA;gBAEA,kBAAC,SAAD;OACE,IAAG;OACH,MAAK;OACL,WAAW;OACX,OAAO;OACP,WAAW,MAAM,EAAc,EAAE,OAAO,KAAK;MAC9C,CAAA;KACQ,CAAA,CACR;;IACL,kBAAC,GAAD;KACE,OAAO,EAAG,sDAAsD,OAAO;KACvE,SAAQ;KACR,UAAA;eAEA,kBAAC,SAAD;MACE,IAAG;MACH,WAAW;MACX,OAAO;MACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;MACxC,aAAa,EACX,4DACA,iBACF;KACD,CAAA;IACQ,CAAA;IACX,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACE,OAAO,EAAG,gDAAgD,OAAO;MACjE,SAAQ;MACR,UAAA;gBAEA,kBAAC,SAAD;OACE,IAAG;OACH,WAAU;OACV,WAAW;OACJ;OACP,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;OACxC,aAAa,EAAG,sDAAsD,UAAU;MACjF,CAAA;KACQ,CAAA,GACX,kBAAC,GAAD;MACE,OAAO,EAAG,+CAA+C,MAAM;MAC/D,SAAQ;gBAER,kBAAC,SAAD;OACE,IAAG;OACH,WAAW;OACX,OAAO;OACP,WAAW,MAAM,EAAQ,EAAE,OAAO,KAAK;OACvC,aAAa,EACX,qDACA,iBACF;MACD,CAAA;KACQ,CAAA,CACR;;IACL,kBAAC,GAAD;KACE,OAAO,EAAG,iDAAiD,QAAQ;KACnE,SAAQ;KACR,MAAM,EACJ,gDACA,mCACF;eAEA,kBAAC,SAAD;MACE,IAAG;MACH,WAAW;MACX,OAAO;MACP,WAAW,MAAM,EAAU,EAAE,OAAO,KAAK;MACzC,aAAa,EACX,uDACA,eACF;KACD,CAAA;IACQ,CAAA;IACV,KAAS,kBAAC,KAAD;KAAG,WAAU;eAAkC;IAAS,CAAA;IAClE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,UAAD;KACE,MAAK;KACL,eAAe,EAAa,EAAK;KACjC,WAAU;eAET,EAAG,iBAAiB,QAAQ;IACvB,CAAA,GACR,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,EAAa;KACvB,WAAW,EAAG,GAAc,qBAAqB;eAHnD,CAKG,EAAa,YACZ,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,IAE3C,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,GAE5B,EAAG,iDAAiD,eAAe,CAC9D;MACc,EAAA,CAAA;GACpB;IACiB,EAAA,CAAA;CACT,CAAA;AAEtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProgramDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"ProgramDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/programs/ProgramDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAkFH,wBAAgB,iBAAiB,gCAuchC"}
|