@burdenoff/microfe-atheletebridge 2026.710.1 → 2026.716.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/atheletebridge/AthleteBridgeRoot.js +50 -53
  2. package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
  3. package/dist/atheletebridge/AthleteBridgeRoutes.d.ts.map +1 -1
  4. package/dist/atheletebridge/AthleteBridgeRoutes.js +29 -25
  5. package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
  6. package/dist/atheletebridge/admin/AdminDashboardPage.d.ts +4 -0
  7. package/dist/atheletebridge/admin/AdminDashboardPage.d.ts.map +1 -0
  8. package/dist/atheletebridge/admin/AdminDashboardPage.js +553 -0
  9. package/dist/atheletebridge/admin/AdminDashboardPage.js.map +1 -0
  10. package/dist/atheletebridge/admin/AdminRoot.d.ts +9 -0
  11. package/dist/atheletebridge/admin/AdminRoot.d.ts.map +1 -0
  12. package/dist/atheletebridge/admin/AdminRoot.js +23 -0
  13. package/dist/atheletebridge/admin/AdminRoot.js.map +1 -0
  14. package/dist/atheletebridge/admin/index.d.ts +11 -0
  15. package/dist/atheletebridge/admin/index.d.ts.map +1 -0
  16. package/dist/atheletebridge/admin/index.js +2 -0
  17. package/dist/atheletebridge/components/DrillVideoLink.js +1 -1
  18. package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -1
  19. package/dist/atheletebridge/components/UserSearchSelect.js +2 -2
  20. package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -1
  21. package/dist/atheletebridge/components/domain.js +3 -3
  22. package/dist/atheletebridge/components/domain.js.map +1 -1
  23. package/dist/atheletebridge/hooks/index.js +1 -0
  24. package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
  25. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +27 -27
  26. package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
  27. package/dist/atheletebridge/i18n.d.ts +26 -0
  28. package/dist/atheletebridge/i18n.d.ts.map +1 -0
  29. package/dist/atheletebridge/i18n.js +14 -0
  30. package/dist/atheletebridge/i18n.js.map +1 -0
  31. package/dist/atheletebridge/index.d.ts +1 -0
  32. package/dist/atheletebridge/index.d.ts.map +1 -1
  33. package/dist/atheletebridge/index.js +3 -0
  34. package/dist/atheletebridge/pages/DashboardPage.d.ts.map +1 -1
  35. package/dist/atheletebridge/pages/DashboardPage.js +166 -144
  36. package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
  37. package/dist/atheletebridge/pages/StandingsPage.d.ts.map +1 -1
  38. package/dist/atheletebridge/pages/StandingsPage.js +98 -92
  39. package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
  40. package/dist/atheletebridge/pages/fixtures/FixturesListPage.d.ts.map +1 -1
  41. package/dist/atheletebridge/pages/fixtures/FixturesListPage.js +6 -4
  42. package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
  43. package/dist/atheletebridge/pages/onboarding/OnboardingPage.d.ts +4 -0
  44. package/dist/atheletebridge/pages/onboarding/OnboardingPage.d.ts.map +1 -0
  45. package/dist/atheletebridge/pages/onboarding/OnboardingPage.js +371 -0
  46. package/dist/atheletebridge/pages/onboarding/OnboardingPage.js.map +1 -0
  47. package/dist/atheletebridge/pages/performance/PerformancePage.d.ts.map +1 -1
  48. package/dist/atheletebridge/pages/performance/PerformancePage.js +42 -41
  49. package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
  50. package/dist/atheletebridge/pages/programs/ProgramsListPage.d.ts.map +1 -1
  51. package/dist/atheletebridge/pages/programs/ProgramsListPage.js +8 -6
  52. package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
  53. package/dist/atheletebridge/pages/sessions/SessionDetailPage.js +1 -1
  54. package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
  55. package/dist/atheletebridge/pages/sessions/SessionsListPage.d.ts.map +1 -1
  56. package/dist/atheletebridge/pages/sessions/SessionsListPage.js +6 -4
  57. package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
  58. package/dist/atheletebridge/pages/teams/TeamsListPage.d.ts.map +1 -1
  59. package/dist/atheletebridge/pages/teams/TeamsListPage.js +7 -5
  60. package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
  61. package/dist/atheletebridge/types/index.d.ts.map +1 -1
  62. package/dist/index.js +5 -3
  63. package/dist/services/createSandboxAssistantTransport.d.ts +8 -7
  64. package/dist/services/createSandboxAssistantTransport.d.ts.map +1 -1
  65. package/dist/services/createSandboxAssistantTransport.js +8 -114
  66. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  67. package/package.json +15 -4
  68. package/dist/generated/wspace-operations.js +0 -59
  69. package/dist/generated/wspace-operations.js.map +0 -1
  70. package/dist/services/assistantApi.d.ts +0 -70
  71. package/dist/services/assistantApi.d.ts.map +0 -1
  72. package/dist/services/assistantApi.js +0 -202
  73. package/dist/services/assistantApi.js.map +0 -1
@@ -8,8 +8,8 @@ import { formatDate as a, formatDuration as o } from "../../utils/formatters.js"
8
8
  import { EmptyState as s } from "../../components/EmptyState.js";
9
9
  import { ErrorState as c, LoadingState as l } from "../../components/LoadingState.js";
10
10
  import { useLogWorkout as u, usePerformanceMetrics as d, useRecordMetric as f, useWorkoutLogs as p } from "../../hooks/useAthleteBridgeApi.js";
11
- import { FormField as m, fieldInputClass as h } from "../../components/form.js";
12
- import { CursorPager as g } from "../../components/CursorPager.js";
11
+ import { CursorPager as m } from "../../components/CursorPager.js";
12
+ import { FormField as h, fieldInputClass as g } from "../../components/form.js";
13
13
  import { AthleteSelect as _ } from "../../components/AthleteSelect.js";
14
14
  import { useMemo as v, useState as y } from "react";
15
15
  import { Activity as b, ArrowDown as x, ArrowUp as S, Dumbbell as C, Gauge as w, HeartPulse as T, Loader2 as E, Move as D, Plus as O, Save as k, Scale as A, Target as j, Users as M, Zap as N } from "lucide-react";
@@ -52,6 +52,7 @@ function $() {
52
52
  title: "Performance",
53
53
  description: "Log workouts, record metrics and track an athlete's progress over time.",
54
54
  children: [/* @__PURE__ */ F("div", {
55
+ "data-tour": "performance-athlete-picker",
55
56
  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",
56
57
  children: /* @__PURE__ */ F(L, {
57
58
  treatment: "insight",
@@ -74,7 +75,7 @@ function $() {
74
75
  });
75
76
  }
76
77
  function ee({ athleteId: t, displayName: n }) {
77
- let [i, u] = y(0), [f, m] = y(!1), [h, _] = y(!1), x = p({
78
+ let [i, u] = y(0), [f, h] = y(!1), [g, _] = y(!1), x = p({
78
79
  athleteId: t,
79
80
  pagination: {
80
81
  limit: W,
@@ -194,7 +195,7 @@ function ee({ athleteId: t, displayName: n }) {
194
195
  children: [/* @__PURE__ */ F(w, { className: "h-4 w-4" }), " Record metric"]
195
196
  }), /* @__PURE__ */ I("button", {
196
197
  type: "button",
197
- onClick: () => m(!0),
198
+ onClick: () => h(!0),
198
199
  className: e(q, "hidden sm:inline-flex"),
199
200
  children: [/* @__PURE__ */ F(C, { className: "h-4 w-4" }), " Log workout"]
200
201
  })]
@@ -228,7 +229,7 @@ function ee({ athleteId: t, displayName: n }) {
228
229
  description: "Log a workout to build this athlete's training history.",
229
230
  action: /* @__PURE__ */ I("button", {
230
231
  type: "button",
231
- onClick: () => m(!0),
232
+ onClick: () => h(!0),
232
233
  className: q,
233
234
  children: [/* @__PURE__ */ F(C, { className: "h-4 w-4" }), " Log workout"]
234
235
  })
@@ -241,7 +242,7 @@ function ee({ athleteId: t, displayName: n }) {
241
242
  rowKey: (e) => e.id,
242
243
  caption: `Workout history for ${n || "athlete"}`
243
244
  })
244
- }), /* @__PURE__ */ F(g, {
245
+ }), /* @__PURE__ */ F(m, {
245
246
  className: "mt-6",
246
247
  page: i,
247
248
  hasMore: E,
@@ -252,19 +253,19 @@ function ee({ athleteId: t, displayName: n }) {
252
253
  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",
253
254
  children: /* @__PURE__ */ I("button", {
254
255
  type: "button",
255
- onClick: () => m(!0),
256
+ onClick: () => h(!0),
256
257
  className: e(q, "w-full py-3 text-base"),
257
258
  children: [/* @__PURE__ */ F(O, { className: "h-5 w-5" }), " Log workout"]
258
259
  })
259
260
  }),
260
261
  /* @__PURE__ */ F(ie, {
261
262
  open: f,
262
- onOpenChange: m,
263
+ onOpenChange: h,
263
264
  athleteId: t,
264
265
  displayName: n
265
266
  }),
266
267
  /* @__PURE__ */ F(ae, {
267
- open: h,
268
+ open: g,
268
269
  onOpenChange: _,
269
270
  athleteId: t,
270
271
  displayName: n
@@ -364,8 +365,8 @@ function re({ metric: e }) {
364
365
  });
365
366
  }
366
367
  function ie({ open: t, onOpenChange: n, athleteId: r, displayName: i }) {
367
- let a = u(), [o, s] = y(""), [c, l] = y("COMPLETED"), [d, f] = y(Z), [p, g] = y(""), [_, v] = y(""), [b, x] = y(""), [S, C] = y(""), [w, T] = y(null), D = () => {
368
- s(""), l("COMPLETED"), f(Z()), g(""), v(""), x(""), C(""), T(null);
368
+ let a = u(), [o, s] = y(""), [c, l] = y("COMPLETED"), [d, f] = y(Z), [p, m] = y(""), [_, v] = y(""), [b, x] = y(""), [S, C] = y(""), [w, T] = y(null), D = () => {
369
+ s(""), l("COMPLETED"), f(Z()), m(""), v(""), x(""), C(""), T(null);
369
370
  };
370
371
  return /* @__PURE__ */ F(R, {
371
372
  open: t,
@@ -397,13 +398,13 @@ function ie({ open: t, onOpenChange: n, athleteId: r, displayName: i }) {
397
398
  },
398
399
  className: "space-y-4 px-1",
399
400
  children: [
400
- /* @__PURE__ */ F(m, {
401
+ /* @__PURE__ */ F(h, {
401
402
  label: "Title",
402
403
  htmlFor: "workoutTitle",
403
404
  required: !0,
404
405
  children: /* @__PURE__ */ F("input", {
405
406
  id: "workoutTitle",
406
- className: h,
407
+ className: g,
407
408
  value: o,
408
409
  onChange: (e) => s(e.target.value),
409
410
  placeholder: "e.g. Lower-body strength"
@@ -411,12 +412,12 @@ function ie({ open: t, onOpenChange: n, athleteId: r, displayName: i }) {
411
412
  }),
412
413
  /* @__PURE__ */ I("div", {
413
414
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
414
- children: [/* @__PURE__ */ F(m, {
415
+ children: [/* @__PURE__ */ F(h, {
415
416
  label: "Status",
416
417
  htmlFor: "workoutStatus",
417
418
  children: /* @__PURE__ */ F("select", {
418
419
  id: "workoutStatus",
419
- className: h,
420
+ className: g,
420
421
  value: c,
421
422
  onChange: (e) => l(e.target.value),
422
423
  children: G.map((e) => /* @__PURE__ */ F("option", {
@@ -424,14 +425,14 @@ function ie({ open: t, onOpenChange: n, athleteId: r, displayName: i }) {
424
425
  children: e.charAt(0) + e.slice(1).toLowerCase()
425
426
  }, e))
426
427
  })
427
- }), /* @__PURE__ */ F(m, {
428
+ }), /* @__PURE__ */ F(h, {
428
429
  label: "Performed at",
429
430
  htmlFor: "workoutPerformedAt",
430
431
  required: !0,
431
432
  children: /* @__PURE__ */ F("input", {
432
433
  id: "workoutPerformedAt",
433
434
  type: "datetime-local",
434
- className: h,
435
+ className: g,
435
436
  value: d,
436
437
  onChange: (e) => f(e.target.value)
437
438
  })
@@ -440,48 +441,48 @@ function ie({ open: t, onOpenChange: n, athleteId: r, displayName: i }) {
440
441
  /* @__PURE__ */ I("div", {
441
442
  className: "grid grid-cols-1 gap-4 sm:grid-cols-3",
442
443
  children: [
443
- /* @__PURE__ */ F(m, {
444
+ /* @__PURE__ */ F(h, {
444
445
  label: "Duration (min)",
445
446
  htmlFor: "workoutDuration",
446
447
  children: /* @__PURE__ */ F("input", {
447
448
  id: "workoutDuration",
448
449
  inputMode: "numeric",
449
- className: h,
450
+ className: g,
450
451
  value: p,
451
- onChange: (e) => g(e.target.value)
452
+ onChange: (e) => m(e.target.value)
452
453
  })
453
454
  }),
454
- /* @__PURE__ */ F(m, {
455
+ /* @__PURE__ */ F(h, {
455
456
  label: "RPE (1–10)",
456
457
  htmlFor: "workoutRpe",
457
458
  children: /* @__PURE__ */ F("input", {
458
459
  id: "workoutRpe",
459
460
  inputMode: "numeric",
460
- className: h,
461
+ className: g,
461
462
  value: _,
462
463
  onChange: (e) => v(e.target.value)
463
464
  })
464
465
  }),
465
- /* @__PURE__ */ F(m, {
466
+ /* @__PURE__ */ F(h, {
466
467
  label: "Total load",
467
468
  htmlFor: "workoutLoad",
468
469
  children: /* @__PURE__ */ F("input", {
469
470
  id: "workoutLoad",
470
471
  inputMode: "decimal",
471
- className: h,
472
+ className: g,
472
473
  value: b,
473
474
  onChange: (e) => x(e.target.value)
474
475
  })
475
476
  })
476
477
  ]
477
478
  }),
478
- /* @__PURE__ */ F(m, {
479
+ /* @__PURE__ */ F(h, {
479
480
  label: "Notes",
480
481
  htmlFor: "workoutNotes",
481
482
  children: /* @__PURE__ */ F("textarea", {
482
483
  id: "workoutNotes",
483
484
  rows: 2,
484
- className: h,
485
+ className: g,
485
486
  value: S,
486
487
  onChange: (e) => C(e.target.value),
487
488
  placeholder: "How did the session go?"
@@ -507,7 +508,7 @@ function ie({ open: t, onOpenChange: n, athleteId: r, displayName: i }) {
507
508
  });
508
509
  }
509
510
  function ae({ open: t, onOpenChange: n, athleteId: r, displayName: a }) {
510
- let o = f(), [s, c] = y("CUSTOM"), [l, u] = y(""), [d, p] = y(""), [g, _] = y(""), [v, b] = y(""), [x, S] = y(Z), [C, w] = y(null), T = () => {
511
+ let o = f(), [s, c] = y("CUSTOM"), [l, u] = y(""), [d, p] = y(""), [m, _] = y(""), [v, b] = y(""), [x, S] = y(Z), [C, w] = y(null), T = () => {
511
512
  c("CUSTOM"), u(""), p(""), _(""), b(""), S(Z()), w(null);
512
513
  };
513
514
  return /* @__PURE__ */ F(R, {
@@ -534,7 +535,7 @@ function ae({ open: t, onOpenChange: n, athleteId: r, displayName: a }) {
534
535
  type: s,
535
536
  label: l.trim(),
536
537
  value: t,
537
- unit: g.trim() || void 0,
538
+ unit: m.trim() || void 0,
538
539
  source: v.trim() || void 0,
539
540
  recordedAt: new Date(x).toISOString()
540
541
  }), T(), n(!1);
@@ -546,12 +547,12 @@ function ae({ open: t, onOpenChange: n, athleteId: r, displayName: a }) {
546
547
  children: [
547
548
  /* @__PURE__ */ I("div", {
548
549
  className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
549
- children: [/* @__PURE__ */ F(m, {
550
+ children: [/* @__PURE__ */ F(h, {
550
551
  label: "Type",
551
552
  htmlFor: "metricType",
552
553
  children: /* @__PURE__ */ F("select", {
553
554
  id: "metricType",
554
- className: h,
555
+ className: g,
555
556
  value: s,
556
557
  onChange: (e) => c(e.target.value),
557
558
  children: K.map((e) => /* @__PURE__ */ F("option", {
@@ -559,26 +560,26 @@ function ae({ open: t, onOpenChange: n, athleteId: r, displayName: a }) {
559
560
  children: i[e]
560
561
  }, e))
561
562
  })
562
- }), /* @__PURE__ */ F(m, {
563
+ }), /* @__PURE__ */ F(h, {
563
564
  label: "Recorded at",
564
565
  htmlFor: "metricRecordedAt",
565
566
  required: !0,
566
567
  children: /* @__PURE__ */ F("input", {
567
568
  id: "metricRecordedAt",
568
569
  type: "datetime-local",
569
- className: h,
570
+ className: g,
570
571
  value: x,
571
572
  onChange: (e) => S(e.target.value)
572
573
  })
573
574
  })]
574
575
  }),
575
- /* @__PURE__ */ F(m, {
576
+ /* @__PURE__ */ F(h, {
576
577
  label: "Label",
577
578
  htmlFor: "metricLabel",
578
579
  required: !0,
579
580
  children: /* @__PURE__ */ F("input", {
580
581
  id: "metricLabel",
581
- className: h,
582
+ className: g,
582
583
  value: l,
583
584
  onChange: (e) => u(e.target.value),
584
585
  placeholder: "e.g. 40m sprint"
@@ -586,37 +587,37 @@ function ae({ open: t, onOpenChange: n, athleteId: r, displayName: a }) {
586
587
  }),
587
588
  /* @__PURE__ */ I("div", {
588
589
  className: "grid grid-cols-2 gap-4",
589
- children: [/* @__PURE__ */ F(m, {
590
+ children: [/* @__PURE__ */ F(h, {
590
591
  label: "Value",
591
592
  htmlFor: "metricValue",
592
593
  required: !0,
593
594
  children: /* @__PURE__ */ F("input", {
594
595
  id: "metricValue",
595
596
  inputMode: "decimal",
596
- className: h,
597
+ className: g,
597
598
  value: d,
598
599
  onChange: (e) => p(e.target.value),
599
600
  placeholder: "e.g. 4.8"
600
601
  })
601
- }), /* @__PURE__ */ F(m, {
602
+ }), /* @__PURE__ */ F(h, {
602
603
  label: "Unit",
603
604
  htmlFor: "metricUnit",
604
605
  children: /* @__PURE__ */ F("input", {
605
606
  id: "metricUnit",
606
- className: h,
607
- value: g,
607
+ className: g,
608
+ value: m,
608
609
  onChange: (e) => _(e.target.value),
609
610
  placeholder: "e.g. s, kg, bpm"
610
611
  })
611
612
  })]
612
613
  }),
613
- /* @__PURE__ */ F(m, {
614
+ /* @__PURE__ */ F(h, {
614
615
  label: "Source",
615
616
  htmlFor: "metricSource",
616
617
  hint: "Optional — device, test or coach.",
617
618
  children: /* @__PURE__ */ F("input", {
618
619
  id: "metricSource",
619
- className: h,
620
+ className: g,
620
621
  value: v,
621
622
  onChange: (e) => b(e.target.value),
622
623
  placeholder: "e.g. GPS vest"
@@ -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\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\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 { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\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/** 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 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 <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 count: number;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const [page, setPage] = useState(0);\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n const logs = useWorkoutLogs({\n athleteId,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId);\n\n const logItems = logs.data?.items ?? [];\n const hasMore = logs.data?.pageInfo.hasMore ?? false;\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]) => {\n const asc = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const values = asc.map((m) => m.value);\n const newest = asc[asc.length - 1];\n const prior = asc.length > 1 ? asc[asc.length - 2] : null;\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous: prior ? prior.value : null,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n count: asc.length,\n };\n })\n .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());\n }, [metrics.data]);\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 </p>\n <div className=\"flex flex-wrap gap-2\">\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=\"No metrics recorded\"\n description=\"Record a metric — speed, strength, endurance and more — to start charting progress.\"\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} />\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=\"No workouts logged\"\n description=\"Log a workout to build this athlete's training history.\"\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={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\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({ metric }: { metric: MetricSeries }) {\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 vs previous reading — direction shown neutrally (no good/bad claim) */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null || delta === 0 ? (\n <span className=\"text-text-muted\">\n {metric.count > 1 ? 'No change vs previous' : 'First reading'}\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\">vs previous</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\nimport { useMemo, useState } from 'react';\nimport {\n Activity,\n ArrowDown,\n ArrowUp,\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 { Sparkline } from '../../components/viz';\nimport { FormField, fieldInputClass } from '../../components/form';\nimport { METRIC_TYPE_LABELS } from '../../components/domain';\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/** 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 count: number;\n}\n\nfunction AthletePerformance({ athleteId, displayName }: AthletePerformanceProps) {\n const [page, setPage] = useState(0);\n const [logOpen, setLogOpen] = useState(false);\n const [metricOpen, setMetricOpen] = useState(false);\n\n const logs = useWorkoutLogs({\n athleteId,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n const metrics = usePerformanceMetrics(athleteId);\n\n const logItems = logs.data?.items ?? [];\n const hasMore = logs.data?.pageInfo.hasMore ?? false;\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]) => {\n const asc = [...list].sort(\n (a, b) => new Date(a.recordedAt).getTime() - new Date(b.recordedAt).getTime()\n );\n const values = asc.map((m) => m.value);\n const newest = asc[asc.length - 1];\n const prior = asc.length > 1 ? asc[asc.length - 2] : null;\n return {\n key,\n type: newest.type,\n label: newest.label,\n unit: newest.unit,\n latest: newest.value,\n previous: prior ? prior.value : null,\n min: Math.min(...values),\n max: Math.max(...values),\n series: values,\n recordedAt: newest.recordedAt,\n count: asc.length,\n };\n })\n .sort((a, b) => new Date(b.recordedAt).getTime() - new Date(a.recordedAt).getTime());\n }, [metrics.data]);\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 </p>\n <div className=\"flex flex-wrap gap-2\">\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=\"No metrics recorded\"\n description=\"Record a metric — speed, strength, endurance and more — to start charting progress.\"\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} />\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=\"No workouts logged\"\n description=\"Log a workout to build this athlete's training history.\"\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={page}\n hasMore={hasMore}\n onPrev={() => setPage((p) => Math.max(0, p - 1))}\n onNext={() => setPage((p) => p + 1)}\n />\n </>\n )}\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({ metric }: { metric: MetricSeries }) {\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 vs previous reading — direction shown neutrally (no good/bad claim) */}\n <div className=\"mt-auto flex items-center gap-1.5 text-sm\">\n {delta == null || delta === 0 ? (\n <span className=\"text-text-muted\">\n {metric.count > 1 ? 'No change vs previous' : 'First reading'}\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\">vs previous</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 +1 @@
1
- {"version":3,"file":"ProgramsListPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/programs/ProgramsListPage.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAiCH,wBAAgB,gBAAgB,gCA6I/B"}
1
+ {"version":3,"file":"ProgramsListPage.d.ts","sourceRoot":"","sources":["../../../../src/atheletebridge/pages/programs/ProgramsListPage.tsx"],"names":[],"mappings":"AAAA;;;GAGG;AAiCH,wBAAgB,gBAAgB,gCA8I/B"}
@@ -4,8 +4,8 @@ import { PROGRAM_LEVEL_LABELS as n, SPORT_LABELS as r } from "../../components/d
4
4
  import { EmptyState as i } from "../../components/EmptyState.js";
5
5
  import { ErrorState as a, LoadingState as o } from "../../components/LoadingState.js";
6
6
  import { usePrograms as s } from "../../hooks/useAthleteBridgeApi.js";
7
- import { FilterSelect as c } from "../../components/FilterSelect.js";
8
- import { CursorPager as l } from "../../components/CursorPager.js";
7
+ import { CursorPager as c } from "../../components/CursorPager.js";
8
+ import { FilterSelect as l } from "../../components/FilterSelect.js";
9
9
  import { ProgramCard as u } from "../../components/ProgramCard.js";
10
10
  import { useState as d } from "react";
11
11
  import { Dumbbell as f, Plus as p, Search as m } from "lucide-react";
@@ -48,11 +48,13 @@ function S() {
48
48
  gradientTitle: !0,
49
49
  actions: /* @__PURE__ */ _("button", {
50
50
  type: "button",
51
+ "data-tour": "programs-create",
51
52
  onClick: () => n("/programs/new"),
52
53
  className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90",
53
54
  children: [/* @__PURE__ */ g(p, { className: "h-4 w-4" }), " New program"]
54
55
  }),
55
56
  children: [/* @__PURE__ */ _("div", {
57
+ "data-tour": "programs-filters",
56
58
  className: "mb-6 flex flex-wrap items-center gap-3",
57
59
  children: [
58
60
  /* @__PURE__ */ _("div", {
@@ -69,7 +71,7 @@ function S() {
69
71
  className: "w-full rounded-lg border border-border-seam bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]"
70
72
  })]
71
73
  }),
72
- /* @__PURE__ */ g(c, {
74
+ /* @__PURE__ */ g(l, {
73
75
  ariaLabel: "Filter by sport",
74
76
  allLabel: "All sports",
75
77
  value: r,
@@ -78,7 +80,7 @@ function S() {
78
80
  },
79
81
  options: y
80
82
  }),
81
- /* @__PURE__ */ g(c, {
83
+ /* @__PURE__ */ g(l, {
82
84
  ariaLabel: "Filter by level",
83
85
  allLabel: "All levels",
84
86
  value: C,
@@ -87,7 +89,7 @@ function S() {
87
89
  },
88
90
  options: b
89
91
  }),
90
- /* @__PURE__ */ g(c, {
92
+ /* @__PURE__ */ g(l, {
91
93
  ariaLabel: "Filter by status",
92
94
  allLabel: "All statuses",
93
95
  value: T,
@@ -115,7 +117,7 @@ function S() {
115
117
  drillCount: e.drills?.length,
116
118
  onClick: () => n(`/programs/${e.id}`)
117
119
  }, e.id))
118
- }), /* @__PURE__ */ g(l, {
120
+ }), /* @__PURE__ */ g(c, {
119
121
  className: "mt-6",
120
122
  page: k,
121
123
  hasMore: F,