@burdenoff/microfe-atheletebridge 2026.720.1 → 2026.720.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/AthleteBridgeRoot.js.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/admin/AdminDashboardPage.js.map +1 -1
- package/dist/atheletebridge/admin/AdminRoot.js.map +1 -1
- package/dist/atheletebridge/components/AthleteSelect.js.map +1 -1
- package/dist/atheletebridge/components/Crest.js.map +1 -1
- package/dist/atheletebridge/components/CursorPager.js.map +1 -1
- package/dist/atheletebridge/components/DrillVideoLink.js.map +1 -1
- package/dist/atheletebridge/components/EmptyState.js.map +1 -1
- package/dist/atheletebridge/components/FilterSelect.js.map +1 -1
- package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
- package/dist/atheletebridge/components/LoadingState.js.map +1 -1
- package/dist/atheletebridge/components/MatchupRow.js.map +1 -1
- package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
- package/dist/atheletebridge/components/PageLayout.js.map +1 -1
- package/dist/atheletebridge/components/ProgramCard.js.map +1 -1
- package/dist/atheletebridge/components/RouteAnalytics.js.map +1 -1
- package/dist/atheletebridge/components/Scoreboard.js.map +1 -1
- package/dist/atheletebridge/components/SessionCard.js.map +1 -1
- package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -1
- package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.js.map +1 -1
- package/dist/atheletebridge/components/UserAvatar.js.map +1 -1
- package/dist/atheletebridge/components/UserSearchSelect.js.map +1 -1
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts +9 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.d.ts.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js +118 -0
- package/dist/atheletebridge/components/availability/AvailabilityRosterPanel.js.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts +9 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.d.ts.map +1 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js +88 -0
- package/dist/atheletebridge/components/availability/AvailabilityRsvpControl.js.map +1 -0
- package/dist/atheletebridge/components/domain.js.map +1 -1
- package/dist/atheletebridge/components/form.js.map +1 -1
- package/dist/atheletebridge/components/viz/BarChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/DonutChart.js +1 -1
- package/dist/atheletebridge/components/viz/DonutChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/LineChart.js.map +1 -1
- package/dist/atheletebridge/components/viz/RatingStars.js.map +1 -1
- package/dist/atheletebridge/components/viz/Sparkline.js.map +1 -1
- package/dist/atheletebridge/components/viz/StatCard.js.map +1 -1
- package/dist/atheletebridge/components/viz/Timeline.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/format.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/geometry.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/hooks.js.map +1 -1
- package/dist/atheletebridge/components/viz/lib/palette.js.map +1 -1
- package/dist/atheletebridge/context/AthleteBridgeContext.js.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts +18 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.d.ts.map +1 -1
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +71 -33
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/i18n.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -1
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.d.ts.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +87 -75
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
- package/dist/atheletebridge/pages/onboarding/OnboardingPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js.map +1 -1
- package/dist/atheletebridge/store/atheletebridgeStore.js.map +1 -1
- package/dist/atheletebridge/types/index.d.ts +27 -0
- package/dist/atheletebridge/types/index.d.ts.map +1 -1
- package/dist/atheletebridge/utils/cn.js.map +1 -1
- package/dist/atheletebridge/utils/datetime.js.map +1 -1
- package/dist/atheletebridge/utils/formatters.js.map +1 -1
- package/dist/atheletebridge/utils/identity.js.map +1 -1
- package/dist/atheletebridge/utils/navigation.js.map +1 -1
- package/dist/atheletebridge/utils/pageContext.js.map +1 -1
- package/dist/atheletebridge/utils/timeAgo.js.map +1 -1
- package/dist/atheletebridge/utils/url.js.map +1 -1
- package/dist/node_modules/clsx/dist/clsx.js.map +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1 -1
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -1
- package/dist/services/createSandboxAssistantTransport.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AvailabilityRsvpControl.js","names":[],"sources":["../../../../src/atheletebridge/components/availability/AvailabilityRsvpControl.tsx"],"sourcesContent":["/**\n * Availability RSVP Control\n * @module atheletebridge/components/availability\n *\n * Lets the signed-in team member respond Yes / No / Maybe to a fixture or\n * session invitation (BOFF-4134). Resolves \"my\" `AthMember` row for the team\n * by matching `currentUser.id` against the roster's `userId`, then reads /\n * writes through `athListAvailability` / `athSetAvailability`.\n *\n * Renders nothing if the signed-in user has no roster membership on this\n * team — RSVP only makes sense for people actually on the roster.\n */\nimport { useMemo, useState } from 'react';\nimport { CheckCircle2, XCircle, HelpCircle, Loader2 } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useAthleteBridge } from '../../context/AthleteBridgeContext';\nimport {\n useAvailability,\n useSetAvailability,\n useTeamMembers,\n} from '../../hooks/useAthleteBridgeApi';\nimport type { AthAvailabilityResponse } from '../../types';\nimport { cn } from '../../utils/cn';\n\ninterface AvailabilityRsvpControlProps {\n teamId: string;\n fixtureId?: string;\n sessionId?: string;\n className?: string;\n}\n\nconst RESPONSE_OPTIONS: Array<{\n value: Exclude<AthAvailabilityResponse, 'NO_RESPONSE'>;\n label: string;\n icon: typeof CheckCircle2;\n activeClass: string;\n}> = [\n {\n value: 'YES',\n label: \"I'm in\",\n icon: CheckCircle2,\n activeClass: 'bg-status-success-bg-subtle text-status-success-text ring-status-success-border',\n },\n {\n value: 'MAYBE',\n label: 'Maybe',\n icon: HelpCircle,\n activeClass: 'bg-status-warning-bg-subtle text-status-warning-text ring-status-warning-border',\n },\n {\n value: 'NO',\n label: \"Can't make it\",\n icon: XCircle,\n activeClass: 'bg-status-error-bg-subtle text-status-error-text ring-status-error-border',\n },\n];\n\nexport function AvailabilityRsvpControl({\n teamId,\n fixtureId,\n sessionId,\n className,\n}: AvailabilityRsvpControlProps) {\n const { currentUser } = useAthleteBridge();\n const members = useTeamMembers(teamId);\n const availability = useAvailability({ fixtureId, sessionId });\n const setAvailability = useSetAvailability();\n const [error, setError] = useState<string | null>(null);\n\n const myMember = useMemo(\n () => members.data?.find((m) => m.userId === currentUser?.id) ?? null,\n [members.data, currentUser?.id]\n );\n\n const myResponse = useMemo(\n () => availability.data?.find((a) => a.memberId === myMember?.id)?.response ?? 'NO_RESPONSE',\n [availability.data, myMember?.id]\n );\n\n // No signed-in user, or they are not on this team's roster — nothing to RSVP with.\n if (!currentUser?.id || (members.isSuccess && !myMember)) {\n return null;\n }\n\n const isLoading = members.isLoading || availability.isLoading;\n const isBusy = setAvailability.isPending;\n\n const respond = async (response: Exclude<AthAvailabilityResponse, 'NO_RESPONSE'>) => {\n if (!myMember) return;\n setError(null);\n try {\n await setAvailability.mutateAsync({\n memberId: myMember.id,\n fixtureId,\n sessionId,\n response,\n });\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to save your response.');\n }\n };\n\n return (\n <GlassCard treatment=\"metric\" className={cn('p-4', className)}>\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-muted\">\n Your availability\n </p>\n {isLoading ? (\n <div className=\"mt-3 flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" /> Loading…\n </div>\n ) : (\n <div className=\"mt-3 flex flex-wrap gap-2\">\n {RESPONSE_OPTIONS.map(({ value, label, icon: Icon, activeClass }) => {\n const isActive = myResponse === value;\n return (\n <button\n key={value}\n type=\"button\"\n disabled={isBusy}\n onClick={() => void respond(value)}\n aria-pressed={isActive}\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium ring-1 ring-inset transition-colors disabled:opacity-60',\n isActive\n ? activeClass\n : 'bg-bg-sunken text-text-secondary ring-border-seam hover:bg-bg-surface'\n )}\n >\n {isBusy && setAvailability.variables?.response === value ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Icon className=\"h-4 w-4\" aria-hidden=\"true\" />\n )}\n {label}\n </button>\n );\n })}\n </div>\n )}\n {error && <p className=\"mt-2 text-sm text-status-error-text\">{error}</p>}\n </GlassCard>\n );\n}\n"],"mappings":";;;;;;;;AA+BA,IAAM,IAKD;CACH;EACE,OAAO;EACP,OAAO;EACP,MAAM;EACN,aAAa;CACf;CACA;EACE,OAAO;EACP,OAAO;EACP,MAAM;EACN,aAAa;CACf;CACA;EACE,OAAO;EACP,OAAO;EACP,MAAM;EACN,aAAa;CACf;AACF;AAEA,SAAgB,EAAwB,EACtC,WACA,cACA,cACA,gBAC+B;CAC/B,IAAM,EAAE,mBAAgB,EAAiB,GACnC,IAAU,EAAe,CAAM,GAC/B,IAAe,EAAgB;EAAE;EAAW;CAAU,CAAC,GACvD,IAAkB,EAAmB,GACrC,CAAC,GAAO,KAAY,EAAwB,IAAI,GAEhD,IAAW,QACT,EAAQ,MAAM,MAAM,MAAM,EAAE,WAAW,GAAa,EAAE,KAAK,MACjE,CAAC,EAAQ,MAAM,GAAa,EAAE,CAChC,GAEM,IAAa,QACX,EAAa,MAAM,MAAM,MAAM,EAAE,aAAa,GAAU,EAAE,GAAG,YAAY,eAC/E,CAAC,EAAa,MAAM,GAAU,EAAE,CAClC;CAGA,IAAI,CAAC,GAAa,MAAO,EAAQ,aAAa,CAAC,GAC7C,OAAO;CAGT,IAAM,IAAY,EAAQ,aAAa,EAAa,WAC9C,IAAS,EAAgB,WAEzB,IAAU,OAAO,MAA8D;EAC9E,OACL;KAAS,IAAI;GACb,IAAI;IACF,MAAM,EAAgB,YAAY;KAChC,UAAU,EAAS;KACnB;KACA;KACA;IACF,CAAC;GACH,SAAS,GAAK;IACZ,EAAS,aAAe,QAAQ,EAAI,UAAU,+BAA+B;GAC/E;EAVa;CAWf;CAEA,OACE,kBAAC,GAAD;EAAW,WAAU;EAAS,WAAW,EAAG,OAAO,CAAS;YAA5D;GACE,kBAAC,KAAD;IAAG,WAAU;cAA8D;GAExE,CAAA;GACF,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,GAAC,WACzC;QAEL,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAiB,KAAK,EAAE,UAAO,UAAO,MAAM,GAAM,qBAAkB;KACnE,IAAM,IAAW,MAAe;KAChC,OACE,kBAAC,UAAD;MAEE,MAAK;MACL,UAAU;MACV,eAAe,KAAK,EAAQ,CAAK;MACjC,gBAAc;MACd,WAAW,EACT,qIACA,IACI,IACA,uEACN;gBAXF,CAaG,KAAU,EAAgB,WAAW,aAAa,IACjD,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,IAE3C,kBAAC,GAAD;OAAM,WAAU;OAAU,eAAY;MAAQ,CAAA,GAE/C,CACK;QAlBD,CAkBC;IAEZ,CAAC;GACE,CAAA;GAEN,KAAS,kBAAC,KAAD;IAAG,WAAU;cAAuC;GAAS,CAAA;EAC9D;;AAEf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"domain.js","names":[],"sources":["../../../src/atheletebridge/components/domain.ts"],"sourcesContent":["/**\n * Domain display helpers\n * @module atheletebridge/components\n *\n * Centralized, type-safe labels and icon mappings for the AthleteBridge\n * enums so list/detail/form pages render them consistently.\n */\n\nimport {\n Activity,\n Dumbbell,\n Trophy,\n Volleyball,\n Waves,\n Gamepad2,\n Footprints,\n type LucideIcon,\n} from 'lucide-react';\nimport type {\n AthFixtureType,\n AthMemberRole,\n AthMetricType,\n AthProgramLevel,\n AthSessionType,\n AthSport,\n} from '../types';\n\nexport const SPORT_ICONS: Record<AthSport, LucideIcon> = {\n ATHLETICS: Footprints,\n BASKETBALL: Volleyball,\n CRICKET: Trophy,\n ESPORTS: Gamepad2,\n FOOTBALL: Volleyball,\n OTHER: Activity,\n SWIMMING: Waves,\n TENNIS: Dumbbell,\n};\n\nexport const SPORT_LABELS: Record<AthSport, string> = {\n ATHLETICS: 'Athletics',\n BASKETBALL: 'Basketball',\n CRICKET: 'Cricket',\n ESPORTS: 'Esports',\n FOOTBALL: 'Football',\n OTHER: 'Other',\n SWIMMING: 'Swimming',\n TENNIS: 'Tennis',\n};\n\nexport const PROGRAM_LEVEL_LABELS: Record<AthProgramLevel, string> = {\n BEGINNER: 'Beginner',\n INTERMEDIATE: 'Intermediate',\n ADVANCED: 'Advanced',\n ELITE: 'Elite',\n};\n\nexport const MEMBER_ROLE_LABELS: Record<AthMemberRole, string> = {\n PLAYER: 'Player',\n CAPTAIN: 'Captain',\n COACH: 'Coach',\n ASSISTANT_COACH: 'Assistant Coach',\n PHYSIO: 'Physio',\n MANAGER: 'Manager',\n};\n\nexport const FIXTURE_TYPE_LABELS: Record<AthFixtureType, string> = {\n FRIENDLY: 'Friendly',\n LEAGUE: 'League',\n CUP: 'Cup',\n TOURNAMENT: 'Tournament',\n SCRIMMAGE: 'Scrimmage',\n};\n\nexport const SESSION_TYPE_LABELS: Record<AthSessionType, string> = {\n IN_PERSON: 'In Person',\n ONLINE: 'Online',\n HYBRID: 'Hybrid',\n};\n\nexport const METRIC_TYPE_LABELS: Record<AthMetricType, string> = {\n SPEED: 'Speed',\n ENDURANCE: 'Endurance',\n STRENGTH: 'Strength',\n AGILITY: 'Agility',\n ACCURACY: 'Accuracy',\n WEIGHT: 'Weight',\n HEART_RATE: 'Heart Rate',\n CUSTOM: 'Custom',\n};\n\nexport const SPORT_OPTIONS = Object.keys(SPORT_LABELS) as AthSport[];\nexport const PROGRAM_LEVEL_OPTIONS = Object.keys(PROGRAM_LEVEL_LABELS) as AthProgramLevel[];\nexport const MEMBER_ROLE_OPTIONS = Object.keys(MEMBER_ROLE_LABELS) as AthMemberRole[];\nexport const FIXTURE_TYPE_OPTIONS = Object.keys(FIXTURE_TYPE_LABELS) as AthFixtureType[];\nexport const SESSION_TYPE_OPTIONS = Object.keys(SESSION_TYPE_LABELS) as AthSessionType[];\n\n/** Humanize an enum value for display (fallback when no explicit label exists). */\nexport function humanizeEnum(value: string): string {\n return value\n .replace(/_/g, ' ')\n .toLowerCase()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n"],"mappings":";;AA2BA,IAAa,IAA4C;CACvD,WAAW;CACX,YAAY;CACZ,SAAS;CACT,SAAS;CACT,UAAU;CACV,OAAO;CACP,UAAU;CACV,QAAQ;
|
|
1
|
+
{"version":3,"file":"domain.js","names":[],"sources":["../../../src/atheletebridge/components/domain.ts"],"sourcesContent":["/**\n * Domain display helpers\n * @module atheletebridge/components\n *\n * Centralized, type-safe labels and icon mappings for the AthleteBridge\n * enums so list/detail/form pages render them consistently.\n */\n\nimport {\n Activity,\n Dumbbell,\n Trophy,\n Volleyball,\n Waves,\n Gamepad2,\n Footprints,\n type LucideIcon,\n} from 'lucide-react';\nimport type {\n AthFixtureType,\n AthMemberRole,\n AthMetricType,\n AthProgramLevel,\n AthSessionType,\n AthSport,\n} from '../types';\n\nexport const SPORT_ICONS: Record<AthSport, LucideIcon> = {\n ATHLETICS: Footprints,\n BASKETBALL: Volleyball,\n CRICKET: Trophy,\n ESPORTS: Gamepad2,\n FOOTBALL: Volleyball,\n OTHER: Activity,\n SWIMMING: Waves,\n TENNIS: Dumbbell,\n};\n\nexport const SPORT_LABELS: Record<AthSport, string> = {\n ATHLETICS: 'Athletics',\n BASKETBALL: 'Basketball',\n CRICKET: 'Cricket',\n ESPORTS: 'Esports',\n FOOTBALL: 'Football',\n OTHER: 'Other',\n SWIMMING: 'Swimming',\n TENNIS: 'Tennis',\n};\n\nexport const PROGRAM_LEVEL_LABELS: Record<AthProgramLevel, string> = {\n BEGINNER: 'Beginner',\n INTERMEDIATE: 'Intermediate',\n ADVANCED: 'Advanced',\n ELITE: 'Elite',\n};\n\nexport const MEMBER_ROLE_LABELS: Record<AthMemberRole, string> = {\n PLAYER: 'Player',\n CAPTAIN: 'Captain',\n COACH: 'Coach',\n ASSISTANT_COACH: 'Assistant Coach',\n PHYSIO: 'Physio',\n MANAGER: 'Manager',\n};\n\nexport const FIXTURE_TYPE_LABELS: Record<AthFixtureType, string> = {\n FRIENDLY: 'Friendly',\n LEAGUE: 'League',\n CUP: 'Cup',\n TOURNAMENT: 'Tournament',\n SCRIMMAGE: 'Scrimmage',\n};\n\nexport const SESSION_TYPE_LABELS: Record<AthSessionType, string> = {\n IN_PERSON: 'In Person',\n ONLINE: 'Online',\n HYBRID: 'Hybrid',\n};\n\nexport const METRIC_TYPE_LABELS: Record<AthMetricType, string> = {\n SPEED: 'Speed',\n ENDURANCE: 'Endurance',\n STRENGTH: 'Strength',\n AGILITY: 'Agility',\n ACCURACY: 'Accuracy',\n WEIGHT: 'Weight',\n HEART_RATE: 'Heart Rate',\n CUSTOM: 'Custom',\n};\n\nexport const SPORT_OPTIONS = Object.keys(SPORT_LABELS) as AthSport[];\nexport const PROGRAM_LEVEL_OPTIONS = Object.keys(PROGRAM_LEVEL_LABELS) as AthProgramLevel[];\nexport const MEMBER_ROLE_OPTIONS = Object.keys(MEMBER_ROLE_LABELS) as AthMemberRole[];\nexport const FIXTURE_TYPE_OPTIONS = Object.keys(FIXTURE_TYPE_LABELS) as AthFixtureType[];\nexport const SESSION_TYPE_OPTIONS = Object.keys(SESSION_TYPE_LABELS) as AthSessionType[];\n\n/** Humanize an enum value for display (fallback when no explicit label exists). */\nexport function humanizeEnum(value: string): string {\n return value\n .replace(/_/g, ' ')\n .toLowerCase()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n}\n"],"mappings":";;AA2BA,IAAa,IAA4C;CACvD,WAAW;CACX,YAAY;CACZ,SAAS;CACT,SAAS;CACT,UAAU;CACV,OAAO;CACP,UAAU;CACV,QAAQ;AACV,GAEa,IAAyC;CACpD,WAAW;CACX,YAAY;CACZ,SAAS;CACT,SAAS;CACT,UAAU;CACV,OAAO;CACP,UAAU;CACV,QAAQ;AACV,GAEa,IAAwD;CACnE,UAAU;CACV,cAAc;CACd,UAAU;CACV,OAAO;AACT,GAEa,IAAoD;CAC/D,QAAQ;CACR,SAAS;CACT,OAAO;CACP,iBAAiB;CACjB,QAAQ;CACR,SAAS;AACX,GAEa,IAAsD;CACjE,UAAU;CACV,QAAQ;CACR,KAAK;CACL,YAAY;CACZ,WAAW;AACb,GAEa,IAAsD;CACjE,WAAW;CACX,QAAQ;CACR,QAAQ;AACV,GAEa,IAAoD;CAC/D,OAAO;CACP,WAAW;CACX,UAAU;CACV,SAAS;CACT,UAAU;CACV,QAAQ;CACR,YAAY;CACZ,QAAQ;AACV,GAEa,IAAgB,OAAO,KAAK,CAAY,GAExC,IAAsB,OAAO,KAAK,CAAkB,GACpD,IAAuB,OAAO,KAAK,CAAmB;AAInE,SAAgB,EAAa,GAAuB;CAClD,OAAO,EACJ,QAAQ,MAAM,GAAG,EACjB,YAAY,EACZ,QAAQ,UAAU,MAAM,EAAE,YAAY,CAAC;AAC5C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"form.js","names":[],"sources":["../../../src/atheletebridge/components/form.tsx"],"sourcesContent":["/**\n * Form primitives\n * @module atheletebridge/components\n *\n * Thin, semantic-token form building blocks shared by the create/edit pages.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface FormFieldProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n children: ReactNode;\n className?: string;\n}\n\nexport const FormField: FC<FormFieldProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n children,\n className,\n}) => (\n <div className={cn('flex flex-col gap-1.5', className)}>\n <label htmlFor={htmlFor} className=\"text-sm font-medium text-text-primary\">\n {label}\n {required && <span className=\"ml-0.5 text-status-error-text\">*</span>}\n </label>\n {children}\n {hint && !error && <p className=\"text-xs text-text-secondary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n);\n\nexport const fieldInputClass = cn(\n 'w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary',\n 'placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]'\n);\n"],"mappings":";;;AAoBA,IAAa,KAAiC,EAC5C,UACA,YACA,aACA,SACA,UACA,aACA,mBAEA,kBAAC,OAAD;CAAK,WAAW,EAAG,yBAAyB,
|
|
1
|
+
{"version":3,"file":"form.js","names":[],"sources":["../../../src/atheletebridge/components/form.tsx"],"sourcesContent":["/**\n * Form primitives\n * @module atheletebridge/components\n *\n * Thin, semantic-token form building blocks shared by the create/edit pages.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { cn } from '../utils/cn';\n\ninterface FormFieldProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n children: ReactNode;\n className?: string;\n}\n\nexport const FormField: FC<FormFieldProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n children,\n className,\n}) => (\n <div className={cn('flex flex-col gap-1.5', className)}>\n <label htmlFor={htmlFor} className=\"text-sm font-medium text-text-primary\">\n {label}\n {required && <span className=\"ml-0.5 text-status-error-text\">*</span>}\n </label>\n {children}\n {hint && !error && <p className=\"text-xs text-text-secondary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n);\n\nexport const fieldInputClass = cn(\n 'w-full rounded-lg border border-border-seam bg-bg-surface px-3 py-2 text-sm text-text-primary',\n 'placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]'\n);\n"],"mappings":";;;AAoBA,IAAa,KAAiC,EAC5C,UACA,YACA,aACA,SACA,UACA,aACA,mBAEA,kBAAC,OAAD;CAAK,WAAW,EAAG,yBAAyB,CAAS;WAArD;EACE,kBAAC,SAAD;GAAgB;GAAS,WAAU;aAAnC,CACG,GACA,KAAY,kBAAC,QAAD;IAAM,WAAU;cAAgC;GAAO,CAAA,CAC/D;;EACN;EACA,KAAQ,CAAC,KAAS,kBAAC,KAAD;GAAG,WAAU;aAA+B;EAAQ,CAAA;EACtE,KAAS,kBAAC,KAAD;GAAG,WAAU;aAAkC;EAAS,CAAA;CAC/D;IAGM,IAAkB,EAC7B,iGACA,sGACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BarChart.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/BarChart.tsx"],"sourcesContent":["import { ACCENT, chartPalette } from './lib/palette';\nimport { ResponsiveSvg } from './lib/ResponsiveSvg';\nimport { scaleLinear } from './lib/geometry';\nimport { formatCompact } from './lib/format';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\nimport type { Segment } from './lib/types';\n\nexport interface BarChartProps {\n /** Bars to render. A `color` on a datum overrides the palette. */\n series: Segment[];\n height?: number;\n valueFormatter?: (value: number) => string;\n /** Color each bar from the categorical palette instead of a single accent. */\n colorful?: boolean;\n /** Hide the value label above each bar. */\n hideValues?: boolean;\n title?: string;\n className?: string;\n}\n\nconst PAD = { top: 20, bottom: 26, left: 8, right: 8 };\n\n/** Vertical bar chart with value labels and an animated grow-in. */\nexport function BarChart({\n series,\n height = 220,\n valueFormatter = formatCompact,\n colorful = false,\n hideValues = false,\n title,\n className,\n}: BarChartProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (series.length === 0) {\n return null;\n }\n\n const values = series.map((s) => s.value);\n const max = Math.max(...values, 0) || 1;\n const palette = chartPalette(series.length);\n const chartTitle = title ?? `Bar chart of ${series.length} categories`;\n\n return (\n <ResponsiveSvg\n height={height}\n title={chartTitle}\n className={className}\n desc={series.map((s) => `${s.label}: ${valueFormatter(s.value)}`).join(', ')}\n >\n {({ width }) => {\n const plotLeft = PAD.left;\n const plotRight = width - PAD.right;\n const plotTop = PAD.top;\n const plotBottom = height - PAD.bottom;\n const plotH = plotBottom - plotTop;\n const slot = (plotRight - plotLeft) / series.length;\n const barWidth = Math.min(56, slot * 0.62);\n return (\n <g>\n <line\n x1={plotLeft}\n x2={plotRight}\n y1={plotBottom}\n y2={plotBottom}\n stroke=\"var(--color-chart-axis)\"\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n opacity={0.5}\n />\n {series.map((datum, i) => {\n const barH = scaleLinear(datum.value, 0, max, 0, plotH);\n const cx = plotLeft + slot * i + slot / 2;\n const x = cx - barWidth / 2;\n const y = plotBottom - barH;\n const color = datum.color ?? (colorful ? palette[i] : ACCENT);\n return (\n <g key={`${datum.label}-${i}`}>\n <title>{`${datum.label}: ${valueFormatter(datum.value)}`}</title>\n <rect\n x={x}\n y={y}\n width={barWidth}\n height={Math.max(0, barH)}\n rx={4}\n fill={color}\n style={{\n transformBox: 'fill-box',\n transformOrigin: 'bottom',\n transform: mounted || reduced ? 'scaleY(1)' : 'scaleY(0)',\n transition: reduced\n ? undefined\n : 'transform 620ms cubic-bezier(0.22,1,0.36,1)',\n transitionDelay: reduced ? undefined : `${i * 45}ms`,\n }}\n />\n {hideValues ? null : (\n <text\n x={cx}\n y={y - 6}\n textAnchor=\"middle\"\n fontSize={10}\n fontWeight={600}\n fill=\"var(--color-text-secondary)\"\n style={{\n opacity: mounted || reduced ? 1 : 0,\n transition: reduced ? undefined : 'opacity 400ms ease',\n transitionDelay: reduced ? undefined : `${i * 45 + 220}ms`,\n }}\n >\n {valueFormatter(datum.value)}\n </text>\n )}\n <text\n x={cx}\n y={height - 8}\n textAnchor=\"middle\"\n fontSize={10}\n fill=\"var(--color-chart-axis)\"\n >\n {datum.label}\n </text>\n </g>\n );\n })}\n </g>\n );\n }}\n </ResponsiveSvg>\n );\n}\n"],"mappings":";;;;;;;AAoBA,IAAM,IAAM;CAAE,KAAK;CAAI,QAAQ;CAAI,MAAM;CAAG,OAAO;
|
|
1
|
+
{"version":3,"file":"BarChart.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/BarChart.tsx"],"sourcesContent":["import { ACCENT, chartPalette } from './lib/palette';\nimport { ResponsiveSvg } from './lib/ResponsiveSvg';\nimport { scaleLinear } from './lib/geometry';\nimport { formatCompact } from './lib/format';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\nimport type { Segment } from './lib/types';\n\nexport interface BarChartProps {\n /** Bars to render. A `color` on a datum overrides the palette. */\n series: Segment[];\n height?: number;\n valueFormatter?: (value: number) => string;\n /** Color each bar from the categorical palette instead of a single accent. */\n colorful?: boolean;\n /** Hide the value label above each bar. */\n hideValues?: boolean;\n title?: string;\n className?: string;\n}\n\nconst PAD = { top: 20, bottom: 26, left: 8, right: 8 };\n\n/** Vertical bar chart with value labels and an animated grow-in. */\nexport function BarChart({\n series,\n height = 220,\n valueFormatter = formatCompact,\n colorful = false,\n hideValues = false,\n title,\n className,\n}: BarChartProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (series.length === 0) {\n return null;\n }\n\n const values = series.map((s) => s.value);\n const max = Math.max(...values, 0) || 1;\n const palette = chartPalette(series.length);\n const chartTitle = title ?? `Bar chart of ${series.length} categories`;\n\n return (\n <ResponsiveSvg\n height={height}\n title={chartTitle}\n className={className}\n desc={series.map((s) => `${s.label}: ${valueFormatter(s.value)}`).join(', ')}\n >\n {({ width }) => {\n const plotLeft = PAD.left;\n const plotRight = width - PAD.right;\n const plotTop = PAD.top;\n const plotBottom = height - PAD.bottom;\n const plotH = plotBottom - plotTop;\n const slot = (plotRight - plotLeft) / series.length;\n const barWidth = Math.min(56, slot * 0.62);\n return (\n <g>\n <line\n x1={plotLeft}\n x2={plotRight}\n y1={plotBottom}\n y2={plotBottom}\n stroke=\"var(--color-chart-axis)\"\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n opacity={0.5}\n />\n {series.map((datum, i) => {\n const barH = scaleLinear(datum.value, 0, max, 0, plotH);\n const cx = plotLeft + slot * i + slot / 2;\n const x = cx - barWidth / 2;\n const y = plotBottom - barH;\n const color = datum.color ?? (colorful ? palette[i] : ACCENT);\n return (\n <g key={`${datum.label}-${i}`}>\n <title>{`${datum.label}: ${valueFormatter(datum.value)}`}</title>\n <rect\n x={x}\n y={y}\n width={barWidth}\n height={Math.max(0, barH)}\n rx={4}\n fill={color}\n style={{\n transformBox: 'fill-box',\n transformOrigin: 'bottom',\n transform: mounted || reduced ? 'scaleY(1)' : 'scaleY(0)',\n transition: reduced\n ? undefined\n : 'transform 620ms cubic-bezier(0.22,1,0.36,1)',\n transitionDelay: reduced ? undefined : `${i * 45}ms`,\n }}\n />\n {hideValues ? null : (\n <text\n x={cx}\n y={y - 6}\n textAnchor=\"middle\"\n fontSize={10}\n fontWeight={600}\n fill=\"var(--color-text-secondary)\"\n style={{\n opacity: mounted || reduced ? 1 : 0,\n transition: reduced ? undefined : 'opacity 400ms ease',\n transitionDelay: reduced ? undefined : `${i * 45 + 220}ms`,\n }}\n >\n {valueFormatter(datum.value)}\n </text>\n )}\n <text\n x={cx}\n y={height - 8}\n textAnchor=\"middle\"\n fontSize={10}\n fill=\"var(--color-chart-axis)\"\n >\n {datum.label}\n </text>\n </g>\n );\n })}\n </g>\n );\n }}\n </ResponsiveSvg>\n );\n}\n"],"mappings":";;;;;;;AAoBA,IAAM,IAAM;CAAE,KAAK;CAAI,QAAQ;CAAI,MAAM;CAAG,OAAO;AAAE;AAGrD,SAAgB,EAAS,EACvB,WACA,YAAS,KACT,oBAAiB,GACjB,cAAW,IACX,gBAAa,IACb,UACA,gBACgB;CAChB,IAAM,IAAU,EAAW,GACrB,IAAU,EAAwB;CAExC,IAAI,EAAO,WAAW,GACpB,OAAO;CAGT,IAAM,IAAS,EAAO,KAAK,MAAM,EAAE,KAAK,GAClC,IAAM,KAAK,IAAI,GAAG,GAAQ,CAAC,KAAK,GAChC,IAAU,EAAa,EAAO,MAAM;CAG1C,OACE,kBAAC,GAAD;EACU;EACR,OALe,KAAS,gBAAgB,EAAO,OAAO;EAM3C;EACX,MAAM,EAAO,KAAK,MAAM,GAAG,EAAE,MAAM,IAAI,EAAe,EAAE,KAAK,GAAG,EAAE,KAAK,IAAI;aAEzE,EAAE,eAAY;GACd,IAAM,IAAW,EAAI,MACf,IAAY,IAAQ,EAAI,OACxB,IAAU,EAAI,KACd,IAAa,IAAS,EAAI,QAC1B,IAAQ,IAAa,GACrB,KAAQ,IAAY,KAAY,EAAO,QACvC,IAAW,KAAK,IAAI,IAAI,IAAO,GAAI;GACzC,OACE,kBAAC,KAAD,EAAA,UAAA,CACE,kBAAC,QAAD;IACE,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,QAAO;IACP,aAAa;IACb,cAAa;IACb,SAAS;GACV,CAAA,GACA,EAAO,KAAK,GAAO,MAAM;IACxB,IAAM,IAAO,EAAY,EAAM,OAAO,GAAG,GAAK,GAAG,CAAK,GAChD,IAAK,IAAW,IAAO,IAAI,IAAO,GAClC,IAAI,IAAK,IAAW,GACpB,IAAI,IAAa,GACjB,IAAQ,EAAM,UAAU,IAAW,EAAQ,KAAA;IACjD,OACE,kBAAC,KAAD,EAAA,UAAA;KACE,kBAAC,SAAD,EAAA,UAAQ,GAAG,EAAM,MAAM,IAAI,EAAe,EAAM,KAAK,IAAW,CAAA;KAChE,kBAAC,QAAD;MACK;MACA;MACH,OAAO;MACP,QAAQ,KAAK,IAAI,GAAG,CAAI;MACxB,IAAI;MACJ,MAAM;MACN,OAAO;OACL,cAAc;OACd,iBAAiB;OACjB,WAAW,KAAW,IAAU,cAAc;OAC9C,YAAY,IACR,KAAA,IACA;OACJ,iBAAiB,IAAU,KAAA,IAAY,GAAG,IAAI,GAAG;MACnD;KACD,CAAA;KACA,IAAa,OACZ,kBAAC,QAAD;MACE,GAAG;MACH,GAAG,IAAI;MACP,YAAW;MACX,UAAU;MACV,YAAY;MACZ,MAAK;MACL,OAAO;OACL,SAAS,KAAW,IAAU,IAAI;OAClC,YAAY,IAAU,KAAA,IAAY;OAClC,iBAAiB,IAAU,KAAA,IAAY,GAAG,IAAI,KAAK,IAAI;MACzD;gBAEC,EAAe,EAAM,KAAK;KACvB,CAAA;KAER,kBAAC,QAAD;MACE,GAAG;MACH,GAAG,IAAS;MACZ,YAAW;MACX,UAAU;MACV,MAAK;gBAEJ,EAAM;KACH,CAAA;IACL,EAAA,GA7CK,GAAG,EAAM,MAAM,GAAG,GA6CvB;GAEP,CAAC,CACA,EAAA,CAAA;EAEP;CACa,CAAA;AAEnB"}
|
|
@@ -59,7 +59,7 @@ function f({ segments: f, centerLabel: p, centerCaption: m, size: h = 168, thick
|
|
|
59
59
|
}
|
|
60
60
|
};
|
|
61
61
|
return e.isFull ? /* @__PURE__ */ s("path", {
|
|
62
|
-
d: `M ${u} ${u - d} A ${d} ${d} 0 1 0 ${u}
|
|
62
|
+
d: `M ${u} ${u - d} A ${d} ${d} 0 1 0 ${u} 96 A ${d} ${d} 0 1 0 ${u} ${u - d} M ${u} ${u - T} A ${T} ${T} 0 1 1 ${u} ${u + T} A ${T} ${T} 0 1 1 ${u} ${u - T} Z`,
|
|
63
63
|
fillRule: "evenodd",
|
|
64
64
|
...n
|
|
65
65
|
}, `${e.segment.label}-${t}`) : /* @__PURE__ */ s("path", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DonutChart.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/DonutChart.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\nimport { chartPalette } from './lib/palette';\nimport { donutSegmentPath } from './lib/geometry';\nimport { formatCompact, toPercent } from './lib/format';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\nimport type { Segment } from './lib/types';\n\nexport interface DonutChartProps {\n segments: Segment[];\n centerLabel?: ReactNode;\n /** Small caption under the center label. */\n centerCaption?: ReactNode;\n /** Pixel diameter (scales down responsively). */\n size?: number;\n /** Ring thickness as a fraction of the radius (0–1). */\n thickness?: number;\n valueFormatter?: (value: number) => string;\n showLegend?: boolean;\n title?: string;\n className?: string;\n}\n\nconst VIEW = 100;\nconst CENTER = VIEW / 2;\nconst R_OUTER = 46;\n\n/** Donut chart with a center label and an optional legend. */\nexport function DonutChart({\n segments,\n centerLabel,\n centerCaption,\n size = 168,\n thickness = 0.28,\n valueFormatter = formatCompact,\n showLegend = true,\n title,\n className,\n}: DonutChartProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (segments.length === 0) {\n return null;\n }\n\n const total = segments.reduce((acc, s) => acc + Math.max(0, s.value), 0);\n const palette = chartPalette(segments.length);\n const rInner = R_OUTER * (1 - thickness);\n const chartTitle = title ?? `Donut chart of ${segments.length} segments`;\n\n const fractions = segments.map((s) => (total > 0 ? Math.max(0, s.value) / total : 0));\n const starts = fractions.map((_, i) => fractions.slice(0, i).reduce((a, b) => a + b, 0));\n const arcs = segments.map((segment, i) => {\n const fraction = fractions[i];\n const startAngle = starts[i] * 360;\n const endAngle = Math.min((starts[i] + fraction) * 360, 359.999);\n const color = segment.color ?? palette[i];\n return { segment, color, startAngle, endAngle, fraction, isFull: fraction >= 0.9999 };\n });\n\n return (\n <div className={cn('flex flex-wrap items-center gap-5', className)}>\n <div className=\"relative shrink-0\" style={{ width: size, height: size, maxWidth: '100%' }}>\n <svg\n viewBox={`0 0 ${VIEW} ${VIEW}`}\n width={size}\n height={size}\n role=\"img\"\n aria-label={chartTitle}\n style={{ display: 'block', maxWidth: '100%', height: 'auto' }}\n >\n <title>{chartTitle}</title>\n <desc>\n {segments\n .map((s) => `${s.label}: ${valueFormatter(s.value)} (${toPercent(s.value, total)}%)`)\n .join(', ')}\n </desc>\n <g\n style={{\n transformOrigin: 'center',\n transform: mounted || reduced ? 'scale(1)' : 'scale(0.86)',\n transition: reduced ? undefined : 'transform 520ms cubic-bezier(0.22,1,0.36,1)',\n }}\n >\n {arcs.map((arc, i) => {\n const shared = {\n fill: arc.color,\n style: {\n opacity: mounted || reduced ? 1 : 0,\n transition: reduced ? undefined : 'opacity 460ms ease',\n transitionDelay: reduced ? undefined : `${i * 70}ms`,\n },\n } as const;\n if (arc.isFull) {\n return (\n <path\n key={`${arc.segment.label}-${i}`}\n d={`M ${CENTER} ${CENTER - R_OUTER} A ${R_OUTER} ${R_OUTER} 0 1 0 ${CENTER} ${\n CENTER + R_OUTER\n } A ${R_OUTER} ${R_OUTER} 0 1 0 ${CENTER} ${CENTER - R_OUTER} M ${CENTER} ${\n CENTER - rInner\n } A ${rInner} ${rInner} 0 1 1 ${CENTER} ${CENTER + rInner} A ${rInner} ${rInner} 0 1 1 ${CENTER} ${\n CENTER - rInner\n } Z`}\n fillRule=\"evenodd\"\n {...shared}\n />\n );\n }\n return (\n <path\n key={`${arc.segment.label}-${i}`}\n d={donutSegmentPath(\n CENTER,\n CENTER,\n R_OUTER,\n rInner,\n arc.startAngle,\n arc.endAngle\n )}\n {...shared}\n >\n <title>{`${arc.segment.label}: ${valueFormatter(arc.segment.value)}`}</title>\n </path>\n );\n })}\n </g>\n </svg>\n {centerLabel !== undefined || centerCaption !== undefined ? (\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\">\n <span className=\"text-xl font-semibold leading-none text-text-primary\">\n {centerLabel}\n </span>\n {centerCaption !== undefined ? (\n <span className=\"mt-1 text-xs text-text-muted\">{centerCaption}</span>\n ) : null}\n </div>\n ) : null}\n </div>\n {showLegend ? (\n <ul className=\"flex min-w-[8rem] flex-1 flex-col gap-1.5 text-sm\">\n {arcs.map((arc, i) => (\n <li key={`${arc.segment.label}-${i}`} className=\"flex items-center gap-2\">\n <span\n aria-hidden=\"true\"\n className=\"inline-block h-2.5 w-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: arc.color }}\n />\n <span className=\"flex-1 truncate text-text-secondary\">{arc.segment.label}</span>\n <span className=\"tabular-nums text-text-muted\">\n {toPercent(arc.segment.value, total)}%\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;AAuBA,IAAM,IAAO,KACP,IAAS,IAAO,GAChB,IAAU;AAGhB,SAAgB,EAAW,EACzB,aACA,gBACA,kBACA,UAAO,KACP,eAAY,KACZ,oBAAiB,GACjB,gBAAa,IACb,UACA,gBACkB;CAClB,IAAM,IAAU,
|
|
1
|
+
{"version":3,"file":"DonutChart.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/DonutChart.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\nimport { chartPalette } from './lib/palette';\nimport { donutSegmentPath } from './lib/geometry';\nimport { formatCompact, toPercent } from './lib/format';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\nimport type { Segment } from './lib/types';\n\nexport interface DonutChartProps {\n segments: Segment[];\n centerLabel?: ReactNode;\n /** Small caption under the center label. */\n centerCaption?: ReactNode;\n /** Pixel diameter (scales down responsively). */\n size?: number;\n /** Ring thickness as a fraction of the radius (0–1). */\n thickness?: number;\n valueFormatter?: (value: number) => string;\n showLegend?: boolean;\n title?: string;\n className?: string;\n}\n\nconst VIEW = 100;\nconst CENTER = VIEW / 2;\nconst R_OUTER = 46;\n\n/** Donut chart with a center label and an optional legend. */\nexport function DonutChart({\n segments,\n centerLabel,\n centerCaption,\n size = 168,\n thickness = 0.28,\n valueFormatter = formatCompact,\n showLegend = true,\n title,\n className,\n}: DonutChartProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (segments.length === 0) {\n return null;\n }\n\n const total = segments.reduce((acc, s) => acc + Math.max(0, s.value), 0);\n const palette = chartPalette(segments.length);\n const rInner = R_OUTER * (1 - thickness);\n const chartTitle = title ?? `Donut chart of ${segments.length} segments`;\n\n const fractions = segments.map((s) => (total > 0 ? Math.max(0, s.value) / total : 0));\n const starts = fractions.map((_, i) => fractions.slice(0, i).reduce((a, b) => a + b, 0));\n const arcs = segments.map((segment, i) => {\n const fraction = fractions[i];\n const startAngle = starts[i] * 360;\n const endAngle = Math.min((starts[i] + fraction) * 360, 359.999);\n const color = segment.color ?? palette[i];\n return { segment, color, startAngle, endAngle, fraction, isFull: fraction >= 0.9999 };\n });\n\n return (\n <div className={cn('flex flex-wrap items-center gap-5', className)}>\n <div className=\"relative shrink-0\" style={{ width: size, height: size, maxWidth: '100%' }}>\n <svg\n viewBox={`0 0 ${VIEW} ${VIEW}`}\n width={size}\n height={size}\n role=\"img\"\n aria-label={chartTitle}\n style={{ display: 'block', maxWidth: '100%', height: 'auto' }}\n >\n <title>{chartTitle}</title>\n <desc>\n {segments\n .map((s) => `${s.label}: ${valueFormatter(s.value)} (${toPercent(s.value, total)}%)`)\n .join(', ')}\n </desc>\n <g\n style={{\n transformOrigin: 'center',\n transform: mounted || reduced ? 'scale(1)' : 'scale(0.86)',\n transition: reduced ? undefined : 'transform 520ms cubic-bezier(0.22,1,0.36,1)',\n }}\n >\n {arcs.map((arc, i) => {\n const shared = {\n fill: arc.color,\n style: {\n opacity: mounted || reduced ? 1 : 0,\n transition: reduced ? undefined : 'opacity 460ms ease',\n transitionDelay: reduced ? undefined : `${i * 70}ms`,\n },\n } as const;\n if (arc.isFull) {\n return (\n <path\n key={`${arc.segment.label}-${i}`}\n d={`M ${CENTER} ${CENTER - R_OUTER} A ${R_OUTER} ${R_OUTER} 0 1 0 ${CENTER} ${\n CENTER + R_OUTER\n } A ${R_OUTER} ${R_OUTER} 0 1 0 ${CENTER} ${CENTER - R_OUTER} M ${CENTER} ${\n CENTER - rInner\n } A ${rInner} ${rInner} 0 1 1 ${CENTER} ${CENTER + rInner} A ${rInner} ${rInner} 0 1 1 ${CENTER} ${\n CENTER - rInner\n } Z`}\n fillRule=\"evenodd\"\n {...shared}\n />\n );\n }\n return (\n <path\n key={`${arc.segment.label}-${i}`}\n d={donutSegmentPath(\n CENTER,\n CENTER,\n R_OUTER,\n rInner,\n arc.startAngle,\n arc.endAngle\n )}\n {...shared}\n >\n <title>{`${arc.segment.label}: ${valueFormatter(arc.segment.value)}`}</title>\n </path>\n );\n })}\n </g>\n </svg>\n {centerLabel !== undefined || centerCaption !== undefined ? (\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\">\n <span className=\"text-xl font-semibold leading-none text-text-primary\">\n {centerLabel}\n </span>\n {centerCaption !== undefined ? (\n <span className=\"mt-1 text-xs text-text-muted\">{centerCaption}</span>\n ) : null}\n </div>\n ) : null}\n </div>\n {showLegend ? (\n <ul className=\"flex min-w-[8rem] flex-1 flex-col gap-1.5 text-sm\">\n {arcs.map((arc, i) => (\n <li key={`${arc.segment.label}-${i}`} className=\"flex items-center gap-2\">\n <span\n aria-hidden=\"true\"\n className=\"inline-block h-2.5 w-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: arc.color }}\n />\n <span className=\"flex-1 truncate text-text-secondary\">{arc.segment.label}</span>\n <span className=\"tabular-nums text-text-muted\">\n {toPercent(arc.segment.value, total)}%\n </span>\n </li>\n ))}\n </ul>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LineChart.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/LineChart.tsx"],"sourcesContent":["import { chartPalette } from './lib/palette';\nimport { CartesianFrame } from './lib/CartesianFrame';\nimport { buildLinePath } from './lib/geometry';\nimport { formatCompact } from './lib/format';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\nimport type { LabeledValue } from './lib/types';\n\nexport interface LineChartProps {\n /** Primary series (its labels drive the x-axis). */\n series: LabeledValue[];\n /** Optional second series, aligned to the primary series by index. */\n compare?: number[];\n seriesLabel?: string;\n compareLabel?: string;\n height?: number;\n valueFormatter?: (value: number) => string;\n title?: string;\n className?: string;\n}\n\n/**\n * Multi-point line chart (trend / attrition) with an optional second series,\n * shared axes + hover, and an animated draw-in.\n */\nexport function LineChart({\n series,\n compare,\n seriesLabel = 'Series',\n compareLabel = 'Compare',\n height = 200,\n valueFormatter = formatCompact,\n title,\n className,\n}: LineChartProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (series.length === 0) {\n return null;\n }\n\n const labels = series.map((s) => s.label);\n const primary = series.map((s) => s.value);\n const hasCompare = Array.isArray(compare) && compare.length > 0;\n const all = hasCompare ? [...primary, ...compare] : primary;\n const min = Math.min(...all);\n const max = Math.max(...all);\n const span = max - min || Math.abs(max) || 1;\n const domain: [number, number] = [min - span * 0.12, max + span * 0.12];\n const [primaryColor, compareColor] = chartPalette(2);\n const chartTitle = title ?? `Line chart of ${series.length} points`;\n\n const drawStyle = {\n strokeDasharray: 1,\n strokeDashoffset: mounted || reduced ? 0 : 1,\n transition: reduced ? undefined : 'stroke-dashoffset 900ms ease',\n } as const;\n\n return (\n <div className={className}>\n {hasCompare ? (\n <div className=\"mb-2 flex flex-wrap items-center gap-4 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className=\"inline-block h-2 w-3 rounded-full\"\n style={{ backgroundColor: primaryColor }}\n />\n {seriesLabel}\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className=\"inline-block h-2 w-3 rounded-full\"\n style={{ backgroundColor: compareColor }}\n />\n {compareLabel}\n </span>\n </div>\n ) : null}\n <CartesianFrame\n labels={labels}\n domain={domain}\n height={height}\n title={chartTitle}\n formatValue={valueFormatter}\n tooltip={(i) => (\n <span className=\"flex flex-col gap-0.5\">\n <span className=\"text-text-secondary\">{labels[i]}</span>\n <span className=\"font-semibold tabular-nums\" style={{ color: primaryColor }}>\n {seriesLabel}: {valueFormatter(primary[i])}\n </span>\n {hasCompare && compare ? (\n <span className=\"font-semibold tabular-nums\" style={{ color: compareColor }}>\n {compareLabel}: {valueFormatter(compare[i])}\n </span>\n ) : null}\n </span>\n )}\n >\n {({ px, py, activeIndex }) => {\n const primaryPoints = primary.map((v, i) => ({ x: px(i), y: py(v) }));\n const comparePoints =\n hasCompare && compare ? compare.map((v, i) => ({ x: px(i), y: py(v) })) : [];\n return (\n <g>\n {comparePoints.length > 0 ? (\n <path\n d={buildLinePath(comparePoints)}\n fill=\"none\"\n stroke={compareColor}\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeOpacity={0.85}\n vectorEffect=\"non-scaling-stroke\"\n pathLength={1}\n style={drawStyle}\n />\n ) : null}\n <path\n d={buildLinePath(primaryPoints)}\n fill=\"none\"\n stroke={primaryColor}\n strokeWidth={2.25}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n pathLength={1}\n style={drawStyle}\n />\n {activeIndex !== null ? (\n <>\n <circle\n cx={px(activeIndex)}\n cy={py(primary[activeIndex])}\n r={3.5}\n fill={primaryColor}\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={1.5}\n />\n {hasCompare && compare ? (\n <circle\n cx={px(activeIndex)}\n cy={py(compare[activeIndex])}\n r={3.5}\n fill={compareColor}\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={1.5}\n />\n ) : null}\n </>\n ) : null}\n </g>\n );\n }}\n </CartesianFrame>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAwBA,SAAgB,EAAU,EACxB,WACA,YACA,iBAAc,UACd,kBAAe,WACf,YAAS,KACT,oBAAiB,GACjB,UACA,gBACiB;CACjB,IAAM,IAAU,
|
|
1
|
+
{"version":3,"file":"LineChart.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/LineChart.tsx"],"sourcesContent":["import { chartPalette } from './lib/palette';\nimport { CartesianFrame } from './lib/CartesianFrame';\nimport { buildLinePath } from './lib/geometry';\nimport { formatCompact } from './lib/format';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\nimport type { LabeledValue } from './lib/types';\n\nexport interface LineChartProps {\n /** Primary series (its labels drive the x-axis). */\n series: LabeledValue[];\n /** Optional second series, aligned to the primary series by index. */\n compare?: number[];\n seriesLabel?: string;\n compareLabel?: string;\n height?: number;\n valueFormatter?: (value: number) => string;\n title?: string;\n className?: string;\n}\n\n/**\n * Multi-point line chart (trend / attrition) with an optional second series,\n * shared axes + hover, and an animated draw-in.\n */\nexport function LineChart({\n series,\n compare,\n seriesLabel = 'Series',\n compareLabel = 'Compare',\n height = 200,\n valueFormatter = formatCompact,\n title,\n className,\n}: LineChartProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (series.length === 0) {\n return null;\n }\n\n const labels = series.map((s) => s.label);\n const primary = series.map((s) => s.value);\n const hasCompare = Array.isArray(compare) && compare.length > 0;\n const all = hasCompare ? [...primary, ...compare] : primary;\n const min = Math.min(...all);\n const max = Math.max(...all);\n const span = max - min || Math.abs(max) || 1;\n const domain: [number, number] = [min - span * 0.12, max + span * 0.12];\n const [primaryColor, compareColor] = chartPalette(2);\n const chartTitle = title ?? `Line chart of ${series.length} points`;\n\n const drawStyle = {\n strokeDasharray: 1,\n strokeDashoffset: mounted || reduced ? 0 : 1,\n transition: reduced ? undefined : 'stroke-dashoffset 900ms ease',\n } as const;\n\n return (\n <div className={className}>\n {hasCompare ? (\n <div className=\"mb-2 flex flex-wrap items-center gap-4 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className=\"inline-block h-2 w-3 rounded-full\"\n style={{ backgroundColor: primaryColor }}\n />\n {seriesLabel}\n </span>\n <span className=\"inline-flex items-center gap-1.5\">\n <span\n aria-hidden=\"true\"\n className=\"inline-block h-2 w-3 rounded-full\"\n style={{ backgroundColor: compareColor }}\n />\n {compareLabel}\n </span>\n </div>\n ) : null}\n <CartesianFrame\n labels={labels}\n domain={domain}\n height={height}\n title={chartTitle}\n formatValue={valueFormatter}\n tooltip={(i) => (\n <span className=\"flex flex-col gap-0.5\">\n <span className=\"text-text-secondary\">{labels[i]}</span>\n <span className=\"font-semibold tabular-nums\" style={{ color: primaryColor }}>\n {seriesLabel}: {valueFormatter(primary[i])}\n </span>\n {hasCompare && compare ? (\n <span className=\"font-semibold tabular-nums\" style={{ color: compareColor }}>\n {compareLabel}: {valueFormatter(compare[i])}\n </span>\n ) : null}\n </span>\n )}\n >\n {({ px, py, activeIndex }) => {\n const primaryPoints = primary.map((v, i) => ({ x: px(i), y: py(v) }));\n const comparePoints =\n hasCompare && compare ? compare.map((v, i) => ({ x: px(i), y: py(v) })) : [];\n return (\n <g>\n {comparePoints.length > 0 ? (\n <path\n d={buildLinePath(comparePoints)}\n fill=\"none\"\n stroke={compareColor}\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeOpacity={0.85}\n vectorEffect=\"non-scaling-stroke\"\n pathLength={1}\n style={drawStyle}\n />\n ) : null}\n <path\n d={buildLinePath(primaryPoints)}\n fill=\"none\"\n stroke={primaryColor}\n strokeWidth={2.25}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n pathLength={1}\n style={drawStyle}\n />\n {activeIndex !== null ? (\n <>\n <circle\n cx={px(activeIndex)}\n cy={py(primary[activeIndex])}\n r={3.5}\n fill={primaryColor}\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={1.5}\n />\n {hasCompare && compare ? (\n <circle\n cx={px(activeIndex)}\n cy={py(compare[activeIndex])}\n r={3.5}\n fill={compareColor}\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={1.5}\n />\n ) : null}\n </>\n ) : null}\n </g>\n );\n }}\n </CartesianFrame>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RatingStars.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/RatingStars.tsx"],"sourcesContent":["import { type KeyboardEvent, useState } from 'react';\nimport { Star } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { STATUS } from './lib/palette';\nimport { clamp } from './lib/geometry';\n\nexport interface RatingStarsProps {\n value: number;\n max?: number;\n /** Show the numeric value next to the stars (read-only mode only). */\n showValue?: boolean;\n /** Optional review count, e.g. \"(128)\". */\n count?: number;\n /** Star size in pixels. */\n size?: number;\n /**\n * When provided the control becomes interactive: an accessible radiogroup of\n * whole-star buttons with hover/focus states. Omit for a read-only display\n * (which supports fractional fill).\n */\n onChange?: (value: number) => void;\n /** Show 1..max number labels under each star (interactive mode). */\n showLabels?: boolean;\n /** Accessible group label for the interactive control. */\n ariaLabel?: string;\n className?: string;\n}\n\n/** Tokenized arc/star color — warm status hue for filled, faint border for empty. */\nconst FILLED = STATUS.warning;\nconst EMPTY = 'var(--color-border-strong)';\n\n/**\n * Star rating. Read-only by default (supports fractional fill + value/count);\n * pass `onChange` for an interactive, keyboard-operable whole-star picker whose\n * fill uses accent-teal / warning tokens (never a raw color).\n */\nexport function RatingStars({\n value,\n max = 5,\n showValue = false,\n count,\n size = 16,\n onChange,\n showLabels = false,\n ariaLabel,\n className,\n}: RatingStarsProps) {\n const clamped = clamp(value, 0, max);\n\n // ── Interactive picker ─────────────────────────────────────────────────────\n const [hover, setHover] = useState<number | null>(null);\n\n if (onChange) {\n const shown = hover ?? clamped;\n const gap = Math.round(size * 0.35);\n const handleKey = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {\n event.preventDefault();\n onChange(clamp(Math.round(clamped) + 1, 1, max));\n } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {\n event.preventDefault();\n onChange(clamp(Math.round(clamped) - 1, 1, max));\n } else if (event.key === 'Home') {\n event.preventDefault();\n onChange(1);\n } else if (event.key === 'End') {\n event.preventDefault();\n onChange(max);\n }\n };\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel ?? `Rate out of ${max}`}\n className={cn('inline-flex items-start', className)}\n style={{ gap }}\n onKeyDown={handleKey}\n onMouseLeave={() => setHover(null)}\n >\n {Array.from({ length: max }, (_, i) => {\n const starValue = i + 1;\n const filled = starValue <= shown;\n return (\n <button\n key={starValue}\n type=\"button\"\n role=\"radio\"\n aria-checked={Math.round(clamped) === starValue}\n aria-label={`${starValue} ${starValue === 1 ? 'star' : 'stars'}`}\n tabIndex={Math.round(clamped) === starValue || (clamped < 1 && i === 0) ? 0 : -1}\n onClick={() => onChange(starValue)}\n onMouseEnter={() => setHover(starValue)}\n onFocus={() => setHover(starValue)}\n onBlur={() => setHover(null)}\n className=\"flex flex-col items-center rounded-md p-0.5 transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Star\n width={size}\n height={size}\n strokeWidth={1.5}\n style={{ color: filled ? FILLED : EMPTY }}\n fill={filled ? 'currentColor' : 'none'}\n />\n {showLabels ? (\n <span\n className={cn(\n 'mt-1 text-xs tabular-nums',\n filled ? 'text-text-secondary' : 'text-text-muted'\n )}\n >\n {starValue}\n </span>\n ) : null}\n </button>\n );\n })}\n </div>\n );\n }\n\n // ── Read-only display (fractional fill) ────────────────────────────────────\n const fillPct = (clamped / max) * 100;\n const gap = Math.round(size * 0.15);\n\n const row = (filled: boolean) => (\n <span className=\"flex\" style={{ gap }} aria-hidden=\"true\">\n {Array.from({ length: max }, (_, i) => (\n <Star\n key={i}\n width={size}\n height={size}\n strokeWidth={1.5}\n style={{ color: filled ? FILLED : EMPTY }}\n fill={filled ? 'currentColor' : 'none'}\n />\n ))}\n </span>\n );\n\n return (\n <span\n className={cn('inline-flex items-center gap-1.5', className)}\n role=\"img\"\n aria-label={`Rated ${clamped} out of ${max}${count !== undefined ? ` from ${count} reviews` : ''}`}\n >\n <span className=\"relative inline-flex\">\n {row(false)}\n <span className=\"absolute left-0 top-0 overflow-hidden\" style={{ width: `${fillPct}%` }}>\n {row(true)}\n </span>\n </span>\n {showValue ? (\n <span className=\"text-sm font-medium tabular-nums text-text-secondary\">\n {clamped.toFixed(1)}\n </span>\n ) : null}\n {count !== undefined ? <span className=\"text-xs text-text-muted\">({count})</span> : null}\n </span>\n );\n}\n"],"mappings":";;;;;;;AA6BA,IAAM,IAAS,EAAO,SAChB,IAAQ;AAOd,SAAgB,EAAY,EAC1B,UACA,SAAM,GACN,eAAY,IACZ,UACA,UAAO,IACP,aACA,gBAAa,IACb,cACA,gBACmB;CACnB,IAAM,IAAU,EAAM,GAAO,GAAG,
|
|
1
|
+
{"version":3,"file":"RatingStars.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/RatingStars.tsx"],"sourcesContent":["import { type KeyboardEvent, useState } from 'react';\nimport { Star } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { STATUS } from './lib/palette';\nimport { clamp } from './lib/geometry';\n\nexport interface RatingStarsProps {\n value: number;\n max?: number;\n /** Show the numeric value next to the stars (read-only mode only). */\n showValue?: boolean;\n /** Optional review count, e.g. \"(128)\". */\n count?: number;\n /** Star size in pixels. */\n size?: number;\n /**\n * When provided the control becomes interactive: an accessible radiogroup of\n * whole-star buttons with hover/focus states. Omit for a read-only display\n * (which supports fractional fill).\n */\n onChange?: (value: number) => void;\n /** Show 1..max number labels under each star (interactive mode). */\n showLabels?: boolean;\n /** Accessible group label for the interactive control. */\n ariaLabel?: string;\n className?: string;\n}\n\n/** Tokenized arc/star color — warm status hue for filled, faint border for empty. */\nconst FILLED = STATUS.warning;\nconst EMPTY = 'var(--color-border-strong)';\n\n/**\n * Star rating. Read-only by default (supports fractional fill + value/count);\n * pass `onChange` for an interactive, keyboard-operable whole-star picker whose\n * fill uses accent-teal / warning tokens (never a raw color).\n */\nexport function RatingStars({\n value,\n max = 5,\n showValue = false,\n count,\n size = 16,\n onChange,\n showLabels = false,\n ariaLabel,\n className,\n}: RatingStarsProps) {\n const clamped = clamp(value, 0, max);\n\n // ── Interactive picker ─────────────────────────────────────────────────────\n const [hover, setHover] = useState<number | null>(null);\n\n if (onChange) {\n const shown = hover ?? clamped;\n const gap = Math.round(size * 0.35);\n const handleKey = (event: KeyboardEvent<HTMLDivElement>) => {\n if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {\n event.preventDefault();\n onChange(clamp(Math.round(clamped) + 1, 1, max));\n } else if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {\n event.preventDefault();\n onChange(clamp(Math.round(clamped) - 1, 1, max));\n } else if (event.key === 'Home') {\n event.preventDefault();\n onChange(1);\n } else if (event.key === 'End') {\n event.preventDefault();\n onChange(max);\n }\n };\n return (\n <div\n role=\"radiogroup\"\n aria-label={ariaLabel ?? `Rate out of ${max}`}\n className={cn('inline-flex items-start', className)}\n style={{ gap }}\n onKeyDown={handleKey}\n onMouseLeave={() => setHover(null)}\n >\n {Array.from({ length: max }, (_, i) => {\n const starValue = i + 1;\n const filled = starValue <= shown;\n return (\n <button\n key={starValue}\n type=\"button\"\n role=\"radio\"\n aria-checked={Math.round(clamped) === starValue}\n aria-label={`${starValue} ${starValue === 1 ? 'star' : 'stars'}`}\n tabIndex={Math.round(clamped) === starValue || (clamped < 1 && i === 0) ? 0 : -1}\n onClick={() => onChange(starValue)}\n onMouseEnter={() => setHover(starValue)}\n onFocus={() => setHover(starValue)}\n onBlur={() => setHover(null)}\n className=\"flex flex-col items-center rounded-md p-0.5 transition-transform hover:scale-110 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <Star\n width={size}\n height={size}\n strokeWidth={1.5}\n style={{ color: filled ? FILLED : EMPTY }}\n fill={filled ? 'currentColor' : 'none'}\n />\n {showLabels ? (\n <span\n className={cn(\n 'mt-1 text-xs tabular-nums',\n filled ? 'text-text-secondary' : 'text-text-muted'\n )}\n >\n {starValue}\n </span>\n ) : null}\n </button>\n );\n })}\n </div>\n );\n }\n\n // ── Read-only display (fractional fill) ────────────────────────────────────\n const fillPct = (clamped / max) * 100;\n const gap = Math.round(size * 0.15);\n\n const row = (filled: boolean) => (\n <span className=\"flex\" style={{ gap }} aria-hidden=\"true\">\n {Array.from({ length: max }, (_, i) => (\n <Star\n key={i}\n width={size}\n height={size}\n strokeWidth={1.5}\n style={{ color: filled ? FILLED : EMPTY }}\n fill={filled ? 'currentColor' : 'none'}\n />\n ))}\n </span>\n );\n\n return (\n <span\n className={cn('inline-flex items-center gap-1.5', className)}\n role=\"img\"\n aria-label={`Rated ${clamped} out of ${max}${count !== undefined ? ` from ${count} reviews` : ''}`}\n >\n <span className=\"relative inline-flex\">\n {row(false)}\n <span className=\"absolute left-0 top-0 overflow-hidden\" style={{ width: `${fillPct}%` }}>\n {row(true)}\n </span>\n </span>\n {showValue ? (\n <span className=\"text-sm font-medium tabular-nums text-text-secondary\">\n {clamped.toFixed(1)}\n </span>\n ) : null}\n {count !== undefined ? <span className=\"text-xs text-text-muted\">({count})</span> : null}\n </span>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Sparkline.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/Sparkline.tsx"],"sourcesContent":["import { ACCENT } from './lib/palette';\nimport { ResponsiveSvg } from './lib/ResponsiveSvg';\nimport { buildAreaPath, buildLinePath, pointsFromValues } from './lib/geometry';\nimport { useMounted, usePrefersReducedMotion, useVizId } from './lib/hooks';\n\nexport interface SparklineProps {\n /** Ordered trend values (oldest first). */\n data: number[];\n variant?: 'line' | 'area';\n /** Pixel height of the sparkline. */\n height?: number;\n /** Accessible label; defaults to a generic trend description. */\n label?: string;\n className?: string;\n}\n\nconst PAD = 3;\n\n/**\n * Tiny inline trend line/area for KPI cards. Draws itself in on mount\n * (unless reduced-motion is requested) and emphasizes the final point.\n */\nexport function Sparkline({\n data,\n variant = 'line',\n height = 36,\n label,\n className,\n}: SparklineProps) {\n const gradientId = useVizId('spark-grad');\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (data.length === 0) {\n return null;\n }\n\n const title = label ?? `Trend of ${data.length} points`;\n\n return (\n <ResponsiveSvg height={height} title={title} minWidth={96} className={className}>\n {({ width }) => {\n const points = pointsFromValues(data, width, height, PAD);\n const linePath = buildLinePath(points);\n const areaPath = buildAreaPath(points, height - PAD);\n const last = points[points.length - 1];\n return (\n <g style={{ color: ACCENT }}>\n <defs>\n <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor=\"currentColor\" stopOpacity={0.28} />\n <stop offset=\"100%\" stopColor=\"currentColor\" stopOpacity={0} />\n </linearGradient>\n </defs>\n {variant === 'area' ? (\n <path\n d={areaPath}\n fill={`url(#${gradientId})`}\n stroke=\"none\"\n style={{\n opacity: mounted || reduced ? 1 : 0,\n transition: reduced ? undefined : 'opacity 700ms ease',\n }}\n />\n ) : null}\n <path\n d={linePath}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n pathLength={1}\n style={{\n strokeDasharray: 1,\n strokeDashoffset: mounted || reduced ? 0 : 1,\n transition: reduced ? undefined : 'stroke-dashoffset 900ms ease',\n }}\n />\n <circle cx={last.x} cy={last.y} r={2.6} fill=\"currentColor\" />\n </g>\n );\n }}\n </ResponsiveSvg>\n );\n}\n"],"mappings":";;;;;;AAgBA,IAAM,IAAM;AAMZ,SAAgB,EAAU,EACxB,SACA,aAAU,QACV,YAAS,IACT,UACA,gBACiB;CACjB,IAAM,IAAa,EAAS,
|
|
1
|
+
{"version":3,"file":"Sparkline.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/Sparkline.tsx"],"sourcesContent":["import { ACCENT } from './lib/palette';\nimport { ResponsiveSvg } from './lib/ResponsiveSvg';\nimport { buildAreaPath, buildLinePath, pointsFromValues } from './lib/geometry';\nimport { useMounted, usePrefersReducedMotion, useVizId } from './lib/hooks';\n\nexport interface SparklineProps {\n /** Ordered trend values (oldest first). */\n data: number[];\n variant?: 'line' | 'area';\n /** Pixel height of the sparkline. */\n height?: number;\n /** Accessible label; defaults to a generic trend description. */\n label?: string;\n className?: string;\n}\n\nconst PAD = 3;\n\n/**\n * Tiny inline trend line/area for KPI cards. Draws itself in on mount\n * (unless reduced-motion is requested) and emphasizes the final point.\n */\nexport function Sparkline({\n data,\n variant = 'line',\n height = 36,\n label,\n className,\n}: SparklineProps) {\n const gradientId = useVizId('spark-grad');\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (data.length === 0) {\n return null;\n }\n\n const title = label ?? `Trend of ${data.length} points`;\n\n return (\n <ResponsiveSvg height={height} title={title} minWidth={96} className={className}>\n {({ width }) => {\n const points = pointsFromValues(data, width, height, PAD);\n const linePath = buildLinePath(points);\n const areaPath = buildAreaPath(points, height - PAD);\n const last = points[points.length - 1];\n return (\n <g style={{ color: ACCENT }}>\n <defs>\n <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor=\"currentColor\" stopOpacity={0.28} />\n <stop offset=\"100%\" stopColor=\"currentColor\" stopOpacity={0} />\n </linearGradient>\n </defs>\n {variant === 'area' ? (\n <path\n d={areaPath}\n fill={`url(#${gradientId})`}\n stroke=\"none\"\n style={{\n opacity: mounted || reduced ? 1 : 0,\n transition: reduced ? undefined : 'opacity 700ms ease',\n }}\n />\n ) : null}\n <path\n d={linePath}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n pathLength={1}\n style={{\n strokeDasharray: 1,\n strokeDashoffset: mounted || reduced ? 0 : 1,\n transition: reduced ? undefined : 'stroke-dashoffset 900ms ease',\n }}\n />\n <circle cx={last.x} cy={last.y} r={2.6} fill=\"currentColor\" />\n </g>\n );\n }}\n </ResponsiveSvg>\n );\n}\n"],"mappings":";;;;;;AAgBA,IAAM,IAAM;AAMZ,SAAgB,EAAU,EACxB,SACA,aAAU,QACV,YAAS,IACT,UACA,gBACiB;CACjB,IAAM,IAAa,EAAS,YAAY,GAClC,IAAU,EAAW,GACrB,IAAU,EAAwB;CAQxC,OANI,EAAK,WAAW,IACX,OAMP,kBAAC,GAAD;EAAuB;EAAe,OAH1B,KAAS,YAAY,EAAK,OAAO;EAGA,UAAU;EAAe;aAClE,EAAE,eAAY;GACd,IAAM,IAAS,EAAiB,GAAM,GAAO,GAAQ,CAAG,GAClD,IAAW,EAAc,CAAM,GAC/B,IAAW,EAAc,GAAQ,IAAS,CAAG,GAC7C,IAAO,EAAO,EAAO,SAAS;GACpC,OACE,kBAAC,KAAD;IAAG,OAAO,EAAE,OAAO,EAAO;cAA1B;KACE,kBAAC,QAAD,EAAA,UACE,kBAAC,kBAAD;MAAgB,IAAI;MAAY,IAAG;MAAI,IAAG;MAAI,IAAG;MAAI,IAAG;gBAAxD,CACE,kBAAC,QAAD;OAAM,QAAO;OAAK,WAAU;OAAe,aAAa;MAAO,CAAA,GAC/D,kBAAC,QAAD;OAAM,QAAO;OAAO,WAAU;OAAe,aAAa;MAAI,CAAA,CAChD;QACZ,CAAA;KACL,MAAY,SACX,kBAAC,QAAD;MACE,GAAG;MACH,MAAM,QAAQ,EAAW;MACzB,QAAO;MACP,OAAO;OACL,SAAS,KAAW,IAAU,IAAI;OAClC,YAAY,IAAU,KAAA,IAAY;MACpC;KACD,CAAA,IACC;KACJ,kBAAC,QAAD;MACE,GAAG;MACH,MAAK;MACL,QAAO;MACP,aAAa;MACb,eAAc;MACd,gBAAe;MACf,cAAa;MACb,YAAY;MACZ,OAAO;OACL,iBAAiB;OACjB,kBAAkB,KAAW,IAAU,IAAI;OAC3C,YAAY,IAAU,KAAA,IAAY;MACpC;KACD,CAAA;KACD,kBAAC,UAAD;MAAQ,IAAI,EAAK;MAAG,IAAI,EAAK;MAAG,GAAG;MAAK,MAAK;KAAgB,CAAA;IAC5D;;EAEP;CACa,CAAA;AAEnB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatCard.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/StatCard.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { ArrowDownRight, ArrowUpRight } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { Sparkline } from './Sparkline';\n\nexport interface StatCardProps {\n label: string;\n value: ReactNode;\n /** Signed change; positive renders success (unless `invertDelta`). */\n delta?: number;\n /** Unit appended to the delta (default \"%\"). */\n deltaSuffix?: string;\n /** Context line under the delta, e.g. \"vs last month\". */\n deltaLabel?: string;\n /** When true (attrition, cost) a negative delta is the good outcome. */\n invertDelta?: boolean;\n icon?: ReactNode;\n /** Optional trend to render as a footer sparkline. */\n sparkline?: number[];\n sparklineVariant?: 'line' | 'area';\n className?: string;\n}\n\n/** KPI tile: label, big tabular value, optional signed delta and sparkline. */\nexport function StatCard({\n label,\n value,\n delta,\n deltaSuffix = '%',\n deltaLabel,\n invertDelta = false,\n icon,\n sparkline,\n sparklineVariant = 'area',\n className,\n}: StatCardProps) {\n const hasDelta = typeof delta === 'number';\n const positiveIsGood = invertDelta ? (delta ?? 0) < 0 : (delta ?? 0) > 0;\n const isNeutral = (delta ?? 0) === 0;\n const rising = (delta ?? 0) > 0;\n\n const toneClass = isNeutral\n ? 'text-text-muted'\n : positiveIsGood\n ? 'text-status-success-text'\n : 'text-status-error-text';\n\n return (\n <div\n className={cn(\n 'flex flex-col gap-3 rounded-[var(--radius-card)] border border-border-subtle bg-bg-surface p-5 shadow-elevation-1 transition-shadow duration-200 hover:shadow-elevation-2',\n className\n )}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-secondary\">{label}</span>\n {icon ? <span className=\"text-text-muted\">{icon}</span> : null}\n </div>\n <div className=\"flex items-end justify-between gap-3\">\n <span className=\"text-3xl font-semibold leading-none tracking-tight tabular-nums text-text-primary\">\n {value}\n </span>\n </div>\n {hasDelta ? (\n <div className=\"flex items-center gap-1.5 text-sm\">\n <span\n className={cn('inline-flex items-center gap-0.5 font-medium tabular-nums', toneClass)}\n >\n {isNeutral ? null : rising ? (\n <ArrowUpRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n ) : (\n <ArrowDownRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n )}\n {rising ? '+' : ''}\n {delta}\n {deltaSuffix}\n </span>\n {deltaLabel ? <span className=\"text-text-muted\">{deltaLabel}</span> : null}\n </div>\n ) : null}\n {sparkline && sparkline.length > 1 ? (\n <div className=\"mt-1\">\n <Sparkline data={sparkline} variant={sparklineVariant} label={`${label} trend`} />\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;AAwBA,SAAgB,EAAS,EACvB,UACA,UACA,UACA,iBAAc,KACd,eACA,iBAAc,IACd,SACA,cACA,sBAAmB,QACnB,gBACgB;CAChB,IAAM,IAAW,OAAO,KAAU,UAC5B,IAAiB,KAAe,KAAS,KAAK,KAAK,KAAS,KAAK,GACjE,KAAa,KAAS,OAAO,GAC7B,KAAU,KAAS,KAAK,GAExB,IAAY,IACd,oBACA,IACE,6BACA;
|
|
1
|
+
{"version":3,"file":"StatCard.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/StatCard.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { ArrowDownRight, ArrowUpRight } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { Sparkline } from './Sparkline';\n\nexport interface StatCardProps {\n label: string;\n value: ReactNode;\n /** Signed change; positive renders success (unless `invertDelta`). */\n delta?: number;\n /** Unit appended to the delta (default \"%\"). */\n deltaSuffix?: string;\n /** Context line under the delta, e.g. \"vs last month\". */\n deltaLabel?: string;\n /** When true (attrition, cost) a negative delta is the good outcome. */\n invertDelta?: boolean;\n icon?: ReactNode;\n /** Optional trend to render as a footer sparkline. */\n sparkline?: number[];\n sparklineVariant?: 'line' | 'area';\n className?: string;\n}\n\n/** KPI tile: label, big tabular value, optional signed delta and sparkline. */\nexport function StatCard({\n label,\n value,\n delta,\n deltaSuffix = '%',\n deltaLabel,\n invertDelta = false,\n icon,\n sparkline,\n sparklineVariant = 'area',\n className,\n}: StatCardProps) {\n const hasDelta = typeof delta === 'number';\n const positiveIsGood = invertDelta ? (delta ?? 0) < 0 : (delta ?? 0) > 0;\n const isNeutral = (delta ?? 0) === 0;\n const rising = (delta ?? 0) > 0;\n\n const toneClass = isNeutral\n ? 'text-text-muted'\n : positiveIsGood\n ? 'text-status-success-text'\n : 'text-status-error-text';\n\n return (\n <div\n className={cn(\n 'flex flex-col gap-3 rounded-[var(--radius-card)] border border-border-subtle bg-bg-surface p-5 shadow-elevation-1 transition-shadow duration-200 hover:shadow-elevation-2',\n className\n )}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"text-sm font-medium text-text-secondary\">{label}</span>\n {icon ? <span className=\"text-text-muted\">{icon}</span> : null}\n </div>\n <div className=\"flex items-end justify-between gap-3\">\n <span className=\"text-3xl font-semibold leading-none tracking-tight tabular-nums text-text-primary\">\n {value}\n </span>\n </div>\n {hasDelta ? (\n <div className=\"flex items-center gap-1.5 text-sm\">\n <span\n className={cn('inline-flex items-center gap-0.5 font-medium tabular-nums', toneClass)}\n >\n {isNeutral ? null : rising ? (\n <ArrowUpRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n ) : (\n <ArrowDownRight aria-hidden=\"true\" className=\"h-4 w-4\" />\n )}\n {rising ? '+' : ''}\n {delta}\n {deltaSuffix}\n </span>\n {deltaLabel ? <span className=\"text-text-muted\">{deltaLabel}</span> : null}\n </div>\n ) : null}\n {sparkline && sparkline.length > 1 ? (\n <div className=\"mt-1\">\n <Sparkline data={sparkline} variant={sparklineVariant} label={`${label} trend`} />\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;AAwBA,SAAgB,EAAS,EACvB,UACA,UACA,UACA,iBAAc,KACd,eACA,iBAAc,IACd,SACA,cACA,sBAAmB,QACnB,gBACgB;CAChB,IAAM,IAAW,OAAO,KAAU,UAC5B,IAAiB,KAAe,KAAS,KAAK,KAAK,KAAS,KAAK,GACjE,KAAa,KAAS,OAAO,GAC7B,KAAU,KAAS,KAAK,GAExB,IAAY,IACd,oBACA,IACE,6BACA;CAEN,OACE,kBAAC,OAAD;EACE,WAAW,EACT,6KACA,CACF;YAJF;GAME,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAA2C;IAAY,CAAA,GACtE,IAAO,kBAAC,QAAD;KAAM,WAAU;eAAmB;IAAW,CAAA,IAAI,IACvD;;GACL,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,QAAD;KAAM,WAAU;eACb;IACG,CAAA;GACH,CAAA;GACJ,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KACE,WAAW,EAAG,6DAA6D,CAAS;eADtF;MAGG,IAAY,OACX,EADkB,IACjB,IAEA,GAFD;OAAc,eAAY;OAAO,WAAU;MAAW,CAEE;MAEzD,IAAS,MAAM;MACf;MACA;KACG;QACL,IAAa,kBAAC,QAAD;KAAM,WAAU;eAAmB;IAAiB,CAAA,IAAI,IACnE;QACH;GACH,KAAa,EAAU,SAAS,IAC/B,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAW,MAAM;KAAW,SAAS;KAAkB,OAAO,GAAG,EAAM;IAAU,CAAA;GAC9E,CAAA,IACH;EACD;;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Timeline.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/Timeline.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\nimport { ACCENT, STATUS, type StatusTone } from './lib/palette';\n\nexport type TimelineTone = 'accent' | StatusTone | 'muted';\n\nexport interface TimelineItem {\n id?: string;\n title: ReactNode;\n meta?: ReactNode;\n time?: ReactNode;\n /** Colors the node dot. Defaults to accent. */\n tone?: TimelineTone;\n /** Optional icon rendered inside the node. */\n icon?: ReactNode;\n}\n\nexport interface TimelineProps {\n items: TimelineItem[];\n className?: string;\n}\n\nfunction dotColor(tone: TimelineTone): string {\n switch (tone) {\n case 'success':\n case 'warning':\n case 'danger':\n case 'info':\n return STATUS[tone];\n case 'muted':\n return 'var(--color-text-muted)';\n default:\n return ACCENT;\n }\n}\n\n/** Vertical activity feed: node dot + connector + title, meta and time. */\nexport function Timeline({ items, className }: TimelineProps) {\n if (items.length === 0) {\n return null;\n }\n return (\n <ol className={cn('flex flex-col', className)}>\n {items.map((item, i) => {\n const isLast = i === items.length - 1;\n const color = dotColor(item.tone ?? 'accent');\n return (\n <li key={item.id ?? i} className=\"flex gap-3\">\n <div className=\"flex flex-col items-center\">\n <span\n className=\"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2\"\n style={{ borderColor: color, color }}\n >\n {item.icon ?? (\n <span\n aria-hidden=\"true\"\n className=\"h-2 w-2 rounded-full\"\n style={{ backgroundColor: color }}\n />\n )}\n </span>\n {isLast ? null : (\n <span\n aria-hidden=\"true\"\n className=\"w-px flex-1\"\n style={{ backgroundColor: 'var(--color-border-subtle)', minHeight: 12 }}\n />\n )}\n </div>\n <div className={cn('flex-1', isLast ? 'pb-0' : 'pb-5')}>\n <div className=\"flex flex-wrap items-baseline justify-between gap-x-3\">\n <p className=\"text-sm font-medium text-text-primary\">{item.title}</p>\n {item.time ? (\n <span className=\"shrink-0 text-xs text-text-muted\">{item.time}</span>\n ) : null}\n </div>\n {item.meta ? (\n <div className=\"mt-0.5 text-sm text-text-secondary\">{item.meta}</div>\n ) : null}\n </div>\n </li>\n );\n })}\n </ol>\n );\n}\n"],"mappings":";;;;AAsBA,SAAS,EAAS,GAA4B;
|
|
1
|
+
{"version":3,"file":"Timeline.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/Timeline.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\nimport { ACCENT, STATUS, type StatusTone } from './lib/palette';\n\nexport type TimelineTone = 'accent' | StatusTone | 'muted';\n\nexport interface TimelineItem {\n id?: string;\n title: ReactNode;\n meta?: ReactNode;\n time?: ReactNode;\n /** Colors the node dot. Defaults to accent. */\n tone?: TimelineTone;\n /** Optional icon rendered inside the node. */\n icon?: ReactNode;\n}\n\nexport interface TimelineProps {\n items: TimelineItem[];\n className?: string;\n}\n\nfunction dotColor(tone: TimelineTone): string {\n switch (tone) {\n case 'success':\n case 'warning':\n case 'danger':\n case 'info':\n return STATUS[tone];\n case 'muted':\n return 'var(--color-text-muted)';\n default:\n return ACCENT;\n }\n}\n\n/** Vertical activity feed: node dot + connector + title, meta and time. */\nexport function Timeline({ items, className }: TimelineProps) {\n if (items.length === 0) {\n return null;\n }\n return (\n <ol className={cn('flex flex-col', className)}>\n {items.map((item, i) => {\n const isLast = i === items.length - 1;\n const color = dotColor(item.tone ?? 'accent');\n return (\n <li key={item.id ?? i} className=\"flex gap-3\">\n <div className=\"flex flex-col items-center\">\n <span\n className=\"mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2\"\n style={{ borderColor: color, color }}\n >\n {item.icon ?? (\n <span\n aria-hidden=\"true\"\n className=\"h-2 w-2 rounded-full\"\n style={{ backgroundColor: color }}\n />\n )}\n </span>\n {isLast ? null : (\n <span\n aria-hidden=\"true\"\n className=\"w-px flex-1\"\n style={{ backgroundColor: 'var(--color-border-subtle)', minHeight: 12 }}\n />\n )}\n </div>\n <div className={cn('flex-1', isLast ? 'pb-0' : 'pb-5')}>\n <div className=\"flex flex-wrap items-baseline justify-between gap-x-3\">\n <p className=\"text-sm font-medium text-text-primary\">{item.title}</p>\n {item.time ? (\n <span className=\"shrink-0 text-xs text-text-muted\">{item.time}</span>\n ) : null}\n </div>\n {item.meta ? (\n <div className=\"mt-0.5 text-sm text-text-secondary\">{item.meta}</div>\n ) : null}\n </div>\n </li>\n );\n })}\n </ol>\n );\n}\n"],"mappings":";;;;AAsBA,SAAS,EAAS,GAA4B;CAC5C,QAAQ,GAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,QACH,OAAO,EAAO;EAChB,KAAK,SACH,OAAO;EACT,SACE,OAAO;CACX;AACF;AAGA,SAAgB,EAAS,EAAE,UAAO,gBAA4B;CAI5D,OAHI,EAAM,WAAW,IACZ,OAGP,kBAAC,MAAD;EAAI,WAAW,EAAG,iBAAiB,CAAS;YACzC,EAAM,KAAK,GAAM,MAAM;GACtB,IAAM,IAAS,MAAM,EAAM,SAAS,GAC9B,IAAQ,EAAS,EAAK,QAAQ,QAAQ;GAC5C,OACE,kBAAC,MAAD;IAAuB,WAAU;cAAjC,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MACE,WAAU;MACV,OAAO;OAAE,aAAa;OAAO;MAAM;gBAElC,EAAK,QACJ,kBAAC,QAAD;OACE,eAAY;OACZ,WAAU;OACV,OAAO,EAAE,iBAAiB,EAAM;MACjC,CAAA;KAEC,CAAA,GACL,IAAS,OACR,kBAAC,QAAD;MACE,eAAY;MACZ,WAAU;MACV,OAAO;OAAE,iBAAiB;OAA8B,WAAW;MAAG;KACvE,CAAA,CAEA;QACL,kBAAC,OAAD;KAAK,WAAW,EAAG,UAAU,IAAS,SAAS,MAAM;eAArD,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,KAAD;OAAG,WAAU;iBAAyC,EAAK;MAAS,CAAA,GACnE,EAAK,OACJ,kBAAC,QAAD;OAAM,WAAU;iBAAoC,EAAK;MAAW,CAAA,IAClE,IACD;SACJ,EAAK,OACJ,kBAAC,OAAD;MAAK,WAAU;gBAAsC,EAAK;KAAU,CAAA,IAClE,IACD;MACH;MAjCK,EAAK,MAAM,CAiChB;EAER,CAAC;CACC,CAAA;AAER"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CartesianFrame.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/CartesianFrame.tsx"],"sourcesContent":["import { type KeyboardEvent, type MouseEvent, type ReactNode, useState } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { CHART_AXIS, CHART_GRID } from './palette';\nimport { clamp, scaleLinear } from './geometry';\nimport { useElementWidth } from './hooks';\n\nexport interface CartesianContext {\n width: number;\n height: number;\n plotLeft: number;\n plotRight: number;\n plotTop: number;\n plotBottom: number;\n activeIndex: number | null;\n /** X pixel for a data index. */\n px: (index: number) => number;\n /** Y pixel for a data value. */\n py: (value: number) => number;\n}\n\ninterface Padding {\n left: number;\n right: number;\n top: number;\n bottom: number;\n}\n\nexport interface CartesianFrameProps {\n labels: string[];\n /** Y domain [low, high]. */\n domain: [number, number];\n height: number;\n title: string;\n desc?: string;\n formatValue: (value: number) => string;\n /** Number of grid intervals (lines = intervals + 1). */\n yTicks?: number;\n padding?: Partial<Padding>;\n tooltip?: (index: number) => ReactNode;\n className?: string;\n children: (ctx: CartesianContext) => ReactNode;\n}\n\nconst DEFAULT_PADDING: Padding = { left: 40, right: 14, top: 14, bottom: 26 };\nconst MAX_X_LABELS = 7;\n\n/**\n * Shared cartesian chart chrome: faint grid, tokenized axes, and pointer +\n * keyboard driven hover with an accessible tooltip. The series marks are\n * supplied via the render-prop `children`, which receives pixel scales.\n */\nexport function CartesianFrame({\n labels,\n domain,\n height,\n title,\n desc,\n formatValue,\n yTicks = 4,\n padding,\n tooltip,\n className,\n children,\n}: CartesianFrameProps) {\n const [ref, measured] = useElementWidth<HTMLDivElement>();\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const width = measured > 0 ? measured : 480;\n\n const pad = { ...DEFAULT_PADDING, ...padding };\n const n = labels.length;\n const [lo, hi] = domain;\n const plotLeft = pad.left;\n const plotRight = width - pad.right;\n const plotTop = pad.top;\n const plotBottom = height - pad.bottom;\n\n const px = (index: number): number =>\n n <= 1 ? (plotLeft + plotRight) / 2 : plotLeft + ((plotRight - plotLeft) * index) / (n - 1);\n const py = (value: number): number =>\n plotBottom - scaleLinear(value, lo, hi, 0, plotBottom - plotTop);\n\n const gridValues = Array.from({ length: yTicks + 1 }, (_, k) => lo + ((hi - lo) * k) / yTicks);\n const xStep = Math.max(1, Math.ceil(n / MAX_X_LABELS));\n const xLabelIndexes = labels.map((_, i) => i).filter((i) => i % xStep === 0 || i === n - 1);\n\n const setNearest = (clientX: number): void => {\n const el = ref.current;\n if (!el || n === 0) {\n return;\n }\n const rect = el.getBoundingClientRect();\n const x = clientX - rect.left;\n let best = 0;\n let bestDistance = Infinity;\n for (let i = 0; i < n; i += 1) {\n const distance = Math.abs(px(i) - x);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = i;\n }\n }\n setActiveIndex(best);\n };\n\n const handleMove = (event: MouseEvent<HTMLDivElement>): void => setNearest(event.clientX);\n const handleLeave = (): void => setActiveIndex(null);\n const handleKey = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (n === 0) {\n return;\n }\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n event.preventDefault();\n const base = activeIndex ?? (event.key === 'ArrowRight' ? -1 : n);\n setActiveIndex(clamp(base + (event.key === 'ArrowRight' ? 1 : -1), 0, n - 1));\n } else if (event.key === 'Home') {\n event.preventDefault();\n setActiveIndex(0);\n } else if (event.key === 'End') {\n event.preventDefault();\n setActiveIndex(n - 1);\n } else if (event.key === 'Escape') {\n setActiveIndex(null);\n }\n };\n\n const ctx: CartesianContext = {\n width,\n height,\n plotLeft,\n plotRight,\n plotTop,\n plotBottom,\n activeIndex,\n px,\n py,\n };\n\n return (\n <div\n ref={ref}\n className={cn(\n 'relative w-full rounded-[var(--radius-card)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n tabIndex={0}\n role=\"img\"\n aria-label={title}\n onMouseMove={handleMove}\n onMouseLeave={handleLeave}\n onBlur={handleLeave}\n onKeyDown={handleKey}\n >\n <svg\n viewBox={`0 0 ${width} ${height}`}\n width=\"100%\"\n height={height}\n preserveAspectRatio=\"none\"\n style={{ display: 'block', overflow: 'visible' }}\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <title>{title}</title>\n {desc ? <desc>{desc}</desc> : null}\n {gridValues.map((value, k) => (\n <line\n key={`grid-${k}`}\n x1={plotLeft}\n x2={plotRight}\n y1={py(value)}\n y2={py(value)}\n stroke={CHART_GRID}\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n opacity={0.6}\n />\n ))}\n {gridValues.map((value, k) => (\n <text\n key={`ylabel-${k}`}\n x={plotLeft - 7}\n y={py(value)}\n textAnchor=\"end\"\n dominantBaseline=\"central\"\n fontSize={10}\n fill={CHART_AXIS}\n >\n {formatValue(value)}\n </text>\n ))}\n {xLabelIndexes.map((i) => (\n <text\n key={`xlabel-${i}`}\n x={px(i)}\n y={height - 8}\n textAnchor=\"middle\"\n fontSize={10}\n fill={CHART_AXIS}\n >\n {labels[i]}\n </text>\n ))}\n {activeIndex !== null ? (\n <line\n x1={px(activeIndex)}\n x2={px(activeIndex)}\n y1={plotTop}\n y2={plotBottom}\n stroke={CHART_AXIS}\n strokeWidth={1}\n strokeDasharray=\"3 3\"\n vectorEffect=\"non-scaling-stroke\"\n opacity={0.7}\n />\n ) : null}\n {children(ctx)}\n </svg>\n {activeIndex !== null && tooltip ? (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"pointer-events-none absolute z-10 -translate-x-1/2 rounded-[var(--radius-input)] border border-border-subtle bg-bg-elevated px-2 py-1 text-xs text-text-primary shadow-elevation-2\"\n style={{\n left: clamp(px(activeIndex), 48, Math.max(48, width - 48)),\n top: plotTop + 2,\n }}\n >\n {tooltip(activeIndex)}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;;AA2CA,IAAM,IAA2B;CAAE,MAAM;CAAI,OAAO;CAAI,KAAK;CAAI,QAAQ;
|
|
1
|
+
{"version":3,"file":"CartesianFrame.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/CartesianFrame.tsx"],"sourcesContent":["import { type KeyboardEvent, type MouseEvent, type ReactNode, useState } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { CHART_AXIS, CHART_GRID } from './palette';\nimport { clamp, scaleLinear } from './geometry';\nimport { useElementWidth } from './hooks';\n\nexport interface CartesianContext {\n width: number;\n height: number;\n plotLeft: number;\n plotRight: number;\n plotTop: number;\n plotBottom: number;\n activeIndex: number | null;\n /** X pixel for a data index. */\n px: (index: number) => number;\n /** Y pixel for a data value. */\n py: (value: number) => number;\n}\n\ninterface Padding {\n left: number;\n right: number;\n top: number;\n bottom: number;\n}\n\nexport interface CartesianFrameProps {\n labels: string[];\n /** Y domain [low, high]. */\n domain: [number, number];\n height: number;\n title: string;\n desc?: string;\n formatValue: (value: number) => string;\n /** Number of grid intervals (lines = intervals + 1). */\n yTicks?: number;\n padding?: Partial<Padding>;\n tooltip?: (index: number) => ReactNode;\n className?: string;\n children: (ctx: CartesianContext) => ReactNode;\n}\n\nconst DEFAULT_PADDING: Padding = { left: 40, right: 14, top: 14, bottom: 26 };\nconst MAX_X_LABELS = 7;\n\n/**\n * Shared cartesian chart chrome: faint grid, tokenized axes, and pointer +\n * keyboard driven hover with an accessible tooltip. The series marks are\n * supplied via the render-prop `children`, which receives pixel scales.\n */\nexport function CartesianFrame({\n labels,\n domain,\n height,\n title,\n desc,\n formatValue,\n yTicks = 4,\n padding,\n tooltip,\n className,\n children,\n}: CartesianFrameProps) {\n const [ref, measured] = useElementWidth<HTMLDivElement>();\n const [activeIndex, setActiveIndex] = useState<number | null>(null);\n const width = measured > 0 ? measured : 480;\n\n const pad = { ...DEFAULT_PADDING, ...padding };\n const n = labels.length;\n const [lo, hi] = domain;\n const plotLeft = pad.left;\n const plotRight = width - pad.right;\n const plotTop = pad.top;\n const plotBottom = height - pad.bottom;\n\n const px = (index: number): number =>\n n <= 1 ? (plotLeft + plotRight) / 2 : plotLeft + ((plotRight - plotLeft) * index) / (n - 1);\n const py = (value: number): number =>\n plotBottom - scaleLinear(value, lo, hi, 0, plotBottom - plotTop);\n\n const gridValues = Array.from({ length: yTicks + 1 }, (_, k) => lo + ((hi - lo) * k) / yTicks);\n const xStep = Math.max(1, Math.ceil(n / MAX_X_LABELS));\n const xLabelIndexes = labels.map((_, i) => i).filter((i) => i % xStep === 0 || i === n - 1);\n\n const setNearest = (clientX: number): void => {\n const el = ref.current;\n if (!el || n === 0) {\n return;\n }\n const rect = el.getBoundingClientRect();\n const x = clientX - rect.left;\n let best = 0;\n let bestDistance = Infinity;\n for (let i = 0; i < n; i += 1) {\n const distance = Math.abs(px(i) - x);\n if (distance < bestDistance) {\n bestDistance = distance;\n best = i;\n }\n }\n setActiveIndex(best);\n };\n\n const handleMove = (event: MouseEvent<HTMLDivElement>): void => setNearest(event.clientX);\n const handleLeave = (): void => setActiveIndex(null);\n const handleKey = (event: KeyboardEvent<HTMLDivElement>): void => {\n if (n === 0) {\n return;\n }\n if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {\n event.preventDefault();\n const base = activeIndex ?? (event.key === 'ArrowRight' ? -1 : n);\n setActiveIndex(clamp(base + (event.key === 'ArrowRight' ? 1 : -1), 0, n - 1));\n } else if (event.key === 'Home') {\n event.preventDefault();\n setActiveIndex(0);\n } else if (event.key === 'End') {\n event.preventDefault();\n setActiveIndex(n - 1);\n } else if (event.key === 'Escape') {\n setActiveIndex(null);\n }\n };\n\n const ctx: CartesianContext = {\n width,\n height,\n plotLeft,\n plotRight,\n plotTop,\n plotBottom,\n activeIndex,\n px,\n py,\n };\n\n return (\n <div\n ref={ref}\n className={cn(\n 'relative w-full rounded-[var(--radius-card)] outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n className\n )}\n tabIndex={0}\n role=\"img\"\n aria-label={title}\n onMouseMove={handleMove}\n onMouseLeave={handleLeave}\n onBlur={handleLeave}\n onKeyDown={handleKey}\n >\n <svg\n viewBox={`0 0 ${width} ${height}`}\n width=\"100%\"\n height={height}\n preserveAspectRatio=\"none\"\n style={{ display: 'block', overflow: 'visible' }}\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <title>{title}</title>\n {desc ? <desc>{desc}</desc> : null}\n {gridValues.map((value, k) => (\n <line\n key={`grid-${k}`}\n x1={plotLeft}\n x2={plotRight}\n y1={py(value)}\n y2={py(value)}\n stroke={CHART_GRID}\n strokeWidth={1}\n vectorEffect=\"non-scaling-stroke\"\n opacity={0.6}\n />\n ))}\n {gridValues.map((value, k) => (\n <text\n key={`ylabel-${k}`}\n x={plotLeft - 7}\n y={py(value)}\n textAnchor=\"end\"\n dominantBaseline=\"central\"\n fontSize={10}\n fill={CHART_AXIS}\n >\n {formatValue(value)}\n </text>\n ))}\n {xLabelIndexes.map((i) => (\n <text\n key={`xlabel-${i}`}\n x={px(i)}\n y={height - 8}\n textAnchor=\"middle\"\n fontSize={10}\n fill={CHART_AXIS}\n >\n {labels[i]}\n </text>\n ))}\n {activeIndex !== null ? (\n <line\n x1={px(activeIndex)}\n x2={px(activeIndex)}\n y1={plotTop}\n y2={plotBottom}\n stroke={CHART_AXIS}\n strokeWidth={1}\n strokeDasharray=\"3 3\"\n vectorEffect=\"non-scaling-stroke\"\n opacity={0.7}\n />\n ) : null}\n {children(ctx)}\n </svg>\n {activeIndex !== null && tooltip ? (\n <div\n role=\"status\"\n aria-live=\"polite\"\n className=\"pointer-events-none absolute z-10 -translate-x-1/2 rounded-[var(--radius-input)] border border-border-subtle bg-bg-elevated px-2 py-1 text-xs text-text-primary shadow-elevation-2\"\n style={{\n left: clamp(px(activeIndex), 48, Math.max(48, width - 48)),\n top: plotTop + 2,\n }}\n >\n {tooltip(activeIndex)}\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResponsiveSvg.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/ResponsiveSvg.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { useElementWidth } from './hooks';\n\nexport interface ResponsiveSvgDims {\n width: number;\n height: number;\n}\n\nexport interface ResponsiveSvgProps {\n /** Fixed pixel height; width is measured from the container. */\n height: number;\n /** Accessible name announced for the chart. */\n title: string;\n /** Optional longer description linked via <desc>. */\n desc?: string;\n className?: string;\n /** Width assumed before the container has been measured. */\n minWidth?: number;\n /** Allow marks (endpoint halos, labels) to paint outside the viewBox. */\n overflowVisible?: boolean;\n children: (dims: ResponsiveSvgDims) => ReactNode;\n}\n\n/**\n * A width-measuring SVG frame. Because the viewBox matches the rendered pixel\n * box 1:1, circles stay circular and strokes stay crisp at any width, while the\n * chart still fills its container and reflows on resize.\n */\nexport function ResponsiveSvg({\n height,\n title,\n desc,\n className,\n minWidth = 320,\n overflowVisible = true,\n children,\n}: ResponsiveSvgProps) {\n const [ref, measured] = useElementWidth<HTMLDivElement>();\n const width = measured > 0 ? measured : minWidth;\n return (\n <div ref={ref} className={cn('w-full', className)}>\n <svg\n viewBox={`0 0 ${width} ${height}`}\n width=\"100%\"\n height={height}\n preserveAspectRatio=\"none\"\n role=\"img\"\n aria-label={title}\n style={{ display: 'block', overflow: overflowVisible ? 'visible' : 'hidden' }}\n >\n <title>{title}</title>\n {desc ? <desc>{desc}</desc> : null}\n {children({ width, height })}\n </svg>\n </div>\n );\n}\n"],"mappings":";;;;AA6BA,SAAgB,EAAc,EAC5B,WACA,UACA,SACA,cACA,cAAW,KACX,qBAAkB,IAClB,eACqB;CACrB,IAAM,CAAC,GAAK,KAAY,
|
|
1
|
+
{"version":3,"file":"ResponsiveSvg.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/ResponsiveSvg.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../../../utils/cn';\nimport { useElementWidth } from './hooks';\n\nexport interface ResponsiveSvgDims {\n width: number;\n height: number;\n}\n\nexport interface ResponsiveSvgProps {\n /** Fixed pixel height; width is measured from the container. */\n height: number;\n /** Accessible name announced for the chart. */\n title: string;\n /** Optional longer description linked via <desc>. */\n desc?: string;\n className?: string;\n /** Width assumed before the container has been measured. */\n minWidth?: number;\n /** Allow marks (endpoint halos, labels) to paint outside the viewBox. */\n overflowVisible?: boolean;\n children: (dims: ResponsiveSvgDims) => ReactNode;\n}\n\n/**\n * A width-measuring SVG frame. Because the viewBox matches the rendered pixel\n * box 1:1, circles stay circular and strokes stay crisp at any width, while the\n * chart still fills its container and reflows on resize.\n */\nexport function ResponsiveSvg({\n height,\n title,\n desc,\n className,\n minWidth = 320,\n overflowVisible = true,\n children,\n}: ResponsiveSvgProps) {\n const [ref, measured] = useElementWidth<HTMLDivElement>();\n const width = measured > 0 ? measured : minWidth;\n return (\n <div ref={ref} className={cn('w-full', className)}>\n <svg\n viewBox={`0 0 ${width} ${height}`}\n width=\"100%\"\n height={height}\n preserveAspectRatio=\"none\"\n role=\"img\"\n aria-label={title}\n style={{ display: 'block', overflow: overflowVisible ? 'visible' : 'hidden' }}\n >\n <title>{title}</title>\n {desc ? <desc>{desc}</desc> : null}\n {children({ width, height })}\n </svg>\n </div>\n );\n}\n"],"mappings":";;;;AA6BA,SAAgB,EAAc,EAC5B,WACA,UACA,SACA,cACA,cAAW,KACX,qBAAkB,IAClB,eACqB;CACrB,IAAM,CAAC,GAAK,KAAY,EAAgC,GAClD,IAAQ,IAAW,IAAI,IAAW;CACxC,OACE,kBAAC,OAAD;EAAU;EAAK,WAAW,EAAG,UAAU,CAAS;YAC9C,kBAAC,OAAD;GACE,SAAS,OAAO,EAAM,GAAG;GACzB,OAAM;GACE;GACR,qBAAoB;GACpB,MAAK;GACL,cAAY;GACZ,OAAO;IAAE,SAAS;IAAS,UAAU,IAAkB,YAAY;GAAS;aAP9E;IASE,kBAAC,SAAD,EAAA,UAAQ,EAAa,CAAA;IACpB,IAAO,kBAAC,QAAD,EAAA,UAAO,EAAW,CAAA,IAAI;IAC7B,EAAS;KAAE;KAAO;IAAO,CAAC;GACxB;;CACF,CAAA;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/format.ts"],"sourcesContent":["/**\n * Pure formatting helpers for the visualization kit. No React / DOM.\n */\n\nfunction trimDecimal(n: number): string {\n return (Math.round(n * 10) / 10).toString();\n}\n\n/** Compact number formatting: 1200 -> \"1.2k\", 3_400_000 -> \"3.4M\". */\nexport function formatCompact(value: number): string {\n const abs = Math.abs(value);\n if (abs >= 1_000_000_000) {\n return `${trimDecimal(value / 1_000_000_000)}B`;\n }\n if (abs >= 1_000_000) {\n return `${trimDecimal(value / 1_000_000)}M`;\n }\n if (abs >= 1_000) {\n return `${trimDecimal(value / 1_000)}k`;\n }\n return String(value);\n}\n\n/** Value as a percentage of a total, rounded to `digits` decimals (0 when total is 0). */\nexport function toPercent(value: number, total: number, digits = 0): number {\n if (total === 0) {\n return 0;\n }\n const factor = 10 ** digits;\n return Math.round((value / total) * 100 * factor) / factor;\n}\n\n/** Up-to-two-letter initials from a display name. */\nexport function getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) {\n return '?';\n }\n if (parts.length === 1) {\n return parts[0].slice(0, 2).toUpperCase();\n }\n return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();\n}\n"],"mappings":";AAIA,SAAS,EAAY,GAAmB;
|
|
1
|
+
{"version":3,"file":"format.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/format.ts"],"sourcesContent":["/**\n * Pure formatting helpers for the visualization kit. No React / DOM.\n */\n\nfunction trimDecimal(n: number): string {\n return (Math.round(n * 10) / 10).toString();\n}\n\n/** Compact number formatting: 1200 -> \"1.2k\", 3_400_000 -> \"3.4M\". */\nexport function formatCompact(value: number): string {\n const abs = Math.abs(value);\n if (abs >= 1_000_000_000) {\n return `${trimDecimal(value / 1_000_000_000)}B`;\n }\n if (abs >= 1_000_000) {\n return `${trimDecimal(value / 1_000_000)}M`;\n }\n if (abs >= 1_000) {\n return `${trimDecimal(value / 1_000)}k`;\n }\n return String(value);\n}\n\n/** Value as a percentage of a total, rounded to `digits` decimals (0 when total is 0). */\nexport function toPercent(value: number, total: number, digits = 0): number {\n if (total === 0) {\n return 0;\n }\n const factor = 10 ** digits;\n return Math.round((value / total) * 100 * factor) / factor;\n}\n\n/** Up-to-two-letter initials from a display name. */\nexport function getInitials(name: string): string {\n const parts = name.trim().split(/\\s+/).filter(Boolean);\n if (parts.length === 0) {\n return '?';\n }\n if (parts.length === 1) {\n return parts[0].slice(0, 2).toUpperCase();\n }\n return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();\n}\n"],"mappings":";AAIA,SAAS,EAAY,GAAmB;CACtC,QAAQ,KAAK,MAAM,IAAI,EAAE,IAAI,IAAI,SAAS;AAC5C;AAGA,SAAgB,EAAc,GAAuB;CACnD,IAAM,IAAM,KAAK,IAAI,CAAK;CAU1B,OATI,KAAO,MACF,GAAG,EAAY,IAAQ,GAAa,EAAE,KAE3C,KAAO,MACF,GAAG,EAAY,IAAQ,GAAS,EAAE,KAEvC,KAAO,MACF,GAAG,EAAY,IAAQ,GAAK,EAAE,KAEhC,OAAO,CAAK;AACrB;AAGA,SAAgB,EAAU,GAAe,GAAe,IAAS,GAAW;CAC1E,IAAI,MAAU,GACZ,OAAO;CAET,IAAM,IAAS,MAAM;CACrB,OAAO,KAAK,MAAO,IAAQ,IAAS,MAAM,CAAM,IAAI;AACtD"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"geometry.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/geometry.ts"],"sourcesContent":["/**\n * Pure geometry / scaling helpers for the CrewFoundry visualization kit.\n * No React, no DOM, no side effects — safe to unit test in isolation.\n */\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** Clamp a value into an inclusive range. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\n/** Sum of a numeric list (0 for empty). */\nexport function sum(values: readonly number[]): number {\n return values.reduce((acc, v) => acc + v, 0);\n}\n\n/** Min/max of a list. Returns [0, 0] for an empty list. */\nexport function extent(values: readonly number[]): [number, number] {\n if (values.length === 0) {\n return [0, 0];\n }\n let min = values[0];\n let max = values[0];\n for (const v of values) {\n if (v < min) {\n min = v;\n }\n if (v > max) {\n max = v;\n }\n }\n return [min, max];\n}\n\n/** Extent that always includes a zero baseline, so bars/areas anchor sensibly. */\nexport function extentWithZero(values: readonly number[]): [number, number] {\n const [min, max] = extent(values);\n return [Math.min(0, min), Math.max(0, max)];\n}\n\n/**\n * Linear map of `value` from a domain onto a range. Degenerate domains\n * (min === max) map to the range midpoint instead of dividing by zero.\n */\nexport function scaleLinear(\n value: number,\n domainMin: number,\n domainMax: number,\n rangeMin: number,\n rangeMax: number\n): number {\n if (domainMax === domainMin) {\n return (rangeMin + rangeMax) / 2;\n }\n const t = (value - domainMin) / (domainMax - domainMin);\n return rangeMin + t * (rangeMax - rangeMin);\n}\n\n/** Round to 2 decimals — keeps generated path strings compact. */\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n\n/**\n * Evenly space a list of y-values across a box and invert Y (SVG origin is\n * top-left, so larger values render higher).\n */\nexport function pointsFromValues(\n values: readonly number[],\n width: number,\n height: number,\n padding = 0,\n domain?: [number, number]\n): Point[] {\n const n = values.length;\n if (n === 0) {\n return [];\n }\n const [dMin, dMax] = domain ?? extentWithZero(values);\n const innerW = Math.max(0, width - padding * 2);\n const innerH = Math.max(0, height - padding * 2);\n return values.map((v, i) => {\n const x = padding + (n === 1 ? innerW / 2 : (innerW * i) / (n - 1));\n const y = padding + innerH - scaleLinear(v, dMin, dMax, 0, innerH);\n return { x: round(x), y: round(y) };\n });\n}\n\n/** Build an SVG polyline `d` from points. */\nexport function buildLinePath(points: readonly Point[]): string {\n if (points.length === 0) {\n return '';\n }\n return points.map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`).join(' ');\n}\n\n/** Build a closed area `d` (line dropped to a baseline and closed). */\nexport function buildAreaPath(points: readonly Point[], baselineY: number): string {\n if (points.length === 0) {\n return '';\n }\n const line = buildLinePath(points);\n const first = points[0];\n const last = points[points.length - 1];\n return `${line} L${round(last.x)},${round(baselineY)} L${round(first.x)},${round(baselineY)} Z`;\n}\n\n/** SVG path for an n-pointed star centered at (cx, cy). */\nexport function starPath(\n cx: number,\n cy: number,\n outerRadius: number,\n innerRadius: number,\n points = 5\n): string {\n const total = points * 2;\n const coords: Point[] = [];\n for (let i = 0; i < total; i += 1) {\n const radius = i % 2 === 0 ? outerRadius : innerRadius;\n coords.push(polarToCartesian(cx, cy, radius, (i * 360) / total));\n }\n return `${coords.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')} Z`;\n}\n\n/** Convert a polar coordinate (degrees, clockwise from 12 o'clock) to cartesian. */\nexport function polarToCartesian(cx: number, cy: number, radius: number, angleDeg: number): Point {\n const a = ((angleDeg - 90) * Math.PI) / 180;\n return { x: round(cx + radius * Math.cos(a)), y: round(cy + radius * Math.sin(a)) };\n}\n\n/**\n * Annular (donut) segment path between two angles, clockwise from 12 o'clock.\n * For a full 360° sweep the caller should draw a ring instead (this path\n * degenerates when start and end coincide).\n */\nexport function donutSegmentPath(\n cx: number,\n cy: number,\n rOuter: number,\n rInner: number,\n startAngle: number,\n endAngle: number\n): string {\n const startOuter = polarToCartesian(cx, cy, rOuter, endAngle);\n const endOuter = polarToCartesian(cx, cy, rOuter, startAngle);\n const startInner = polarToCartesian(cx, cy, rInner, endAngle);\n const endInner = polarToCartesian(cx, cy, rInner, startAngle);\n const largeArc = endAngle - startAngle > 180 ? 1 : 0;\n return [\n `M${startOuter.x},${startOuter.y}`,\n `A${rOuter},${rOuter} 0 ${largeArc} 0 ${endOuter.x},${endOuter.y}`,\n `L${endInner.x},${endInner.y}`,\n `A${rInner},${rInner} 0 ${largeArc} 1 ${startInner.x},${startInner.y}`,\n 'Z',\n ].join(' ');\n}\n"],"mappings":";AAWA,SAAgB,EAAM,GAAe,GAAa,GAAqB;
|
|
1
|
+
{"version":3,"file":"geometry.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/geometry.ts"],"sourcesContent":["/**\n * Pure geometry / scaling helpers for the CrewFoundry visualization kit.\n * No React, no DOM, no side effects — safe to unit test in isolation.\n */\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** Clamp a value into an inclusive range. */\nexport function clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\n/** Sum of a numeric list (0 for empty). */\nexport function sum(values: readonly number[]): number {\n return values.reduce((acc, v) => acc + v, 0);\n}\n\n/** Min/max of a list. Returns [0, 0] for an empty list. */\nexport function extent(values: readonly number[]): [number, number] {\n if (values.length === 0) {\n return [0, 0];\n }\n let min = values[0];\n let max = values[0];\n for (const v of values) {\n if (v < min) {\n min = v;\n }\n if (v > max) {\n max = v;\n }\n }\n return [min, max];\n}\n\n/** Extent that always includes a zero baseline, so bars/areas anchor sensibly. */\nexport function extentWithZero(values: readonly number[]): [number, number] {\n const [min, max] = extent(values);\n return [Math.min(0, min), Math.max(0, max)];\n}\n\n/**\n * Linear map of `value` from a domain onto a range. Degenerate domains\n * (min === max) map to the range midpoint instead of dividing by zero.\n */\nexport function scaleLinear(\n value: number,\n domainMin: number,\n domainMax: number,\n rangeMin: number,\n rangeMax: number\n): number {\n if (domainMax === domainMin) {\n return (rangeMin + rangeMax) / 2;\n }\n const t = (value - domainMin) / (domainMax - domainMin);\n return rangeMin + t * (rangeMax - rangeMin);\n}\n\n/** Round to 2 decimals — keeps generated path strings compact. */\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n\n/**\n * Evenly space a list of y-values across a box and invert Y (SVG origin is\n * top-left, so larger values render higher).\n */\nexport function pointsFromValues(\n values: readonly number[],\n width: number,\n height: number,\n padding = 0,\n domain?: [number, number]\n): Point[] {\n const n = values.length;\n if (n === 0) {\n return [];\n }\n const [dMin, dMax] = domain ?? extentWithZero(values);\n const innerW = Math.max(0, width - padding * 2);\n const innerH = Math.max(0, height - padding * 2);\n return values.map((v, i) => {\n const x = padding + (n === 1 ? innerW / 2 : (innerW * i) / (n - 1));\n const y = padding + innerH - scaleLinear(v, dMin, dMax, 0, innerH);\n return { x: round(x), y: round(y) };\n });\n}\n\n/** Build an SVG polyline `d` from points. */\nexport function buildLinePath(points: readonly Point[]): string {\n if (points.length === 0) {\n return '';\n }\n return points.map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`).join(' ');\n}\n\n/** Build a closed area `d` (line dropped to a baseline and closed). */\nexport function buildAreaPath(points: readonly Point[], baselineY: number): string {\n if (points.length === 0) {\n return '';\n }\n const line = buildLinePath(points);\n const first = points[0];\n const last = points[points.length - 1];\n return `${line} L${round(last.x)},${round(baselineY)} L${round(first.x)},${round(baselineY)} Z`;\n}\n\n/** SVG path for an n-pointed star centered at (cx, cy). */\nexport function starPath(\n cx: number,\n cy: number,\n outerRadius: number,\n innerRadius: number,\n points = 5\n): string {\n const total = points * 2;\n const coords: Point[] = [];\n for (let i = 0; i < total; i += 1) {\n const radius = i % 2 === 0 ? outerRadius : innerRadius;\n coords.push(polarToCartesian(cx, cy, radius, (i * 360) / total));\n }\n return `${coords.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ')} Z`;\n}\n\n/** Convert a polar coordinate (degrees, clockwise from 12 o'clock) to cartesian. */\nexport function polarToCartesian(cx: number, cy: number, radius: number, angleDeg: number): Point {\n const a = ((angleDeg - 90) * Math.PI) / 180;\n return { x: round(cx + radius * Math.cos(a)), y: round(cy + radius * Math.sin(a)) };\n}\n\n/**\n * Annular (donut) segment path between two angles, clockwise from 12 o'clock.\n * For a full 360° sweep the caller should draw a ring instead (this path\n * degenerates when start and end coincide).\n */\nexport function donutSegmentPath(\n cx: number,\n cy: number,\n rOuter: number,\n rInner: number,\n startAngle: number,\n endAngle: number\n): string {\n const startOuter = polarToCartesian(cx, cy, rOuter, endAngle);\n const endOuter = polarToCartesian(cx, cy, rOuter, startAngle);\n const startInner = polarToCartesian(cx, cy, rInner, endAngle);\n const endInner = polarToCartesian(cx, cy, rInner, startAngle);\n const largeArc = endAngle - startAngle > 180 ? 1 : 0;\n return [\n `M${startOuter.x},${startOuter.y}`,\n `A${rOuter},${rOuter} 0 ${largeArc} 0 ${endOuter.x},${endOuter.y}`,\n `L${endInner.x},${endInner.y}`,\n `A${rInner},${rInner} 0 ${largeArc} 1 ${startInner.x},${startInner.y}`,\n 'Z',\n ].join(' ');\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hooks.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/hooks.ts"],"sourcesContent":["import { type RefObject, useEffect, useLayoutEffect, useRef, useState } from 'react';\n\nconst canUseDOM = typeof window !== 'undefined';\nconst useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;\n\n/**\n * Tracks the user's reduced-motion preference. Components use this to skip\n * entrance / draw-in transitions entirely rather than merely shortening them.\n */\nexport function usePrefersReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false);\n useEffect(() => {\n if (!canUseDOM || typeof window.matchMedia !== 'function') {\n return;\n }\n const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n const update = () => setReduced(mq.matches);\n update();\n mq.addEventListener('change', update);\n return () => mq.removeEventListener('change', update);\n }, []);\n return reduced;\n}\n\n/** Becomes true after the first paint — drives one-shot enter transitions. */\nexport function useMounted(): boolean {\n const [mounted, setMounted] = useState(false);\n useEffect(() => {\n const raf = requestAnimationFrame(() => setMounted(true));\n return () => cancelAnimationFrame(raf);\n }, []);\n return mounted;\n}\n\nlet uidCounter = 0;\n\n/** Stable, SVG-safe unique id (no colons) for gradient / clip / aria references. */\nexport function useVizId(prefix = 'viz'): string {\n const [id] = useState(() => {\n uidCounter += 1;\n return `${prefix}-${uidCounter}`;\n });\n return id;\n}\n\n/**\n * Observes the width of an element so SVG charts can render a 1:1 viewBox\n * (no aspect-ratio distortion) and reflow on resize. Falls back gracefully\n * where ResizeObserver is unavailable.\n */\nexport function useElementWidth<T extends HTMLElement>(): [RefObject<T | null>, number] {\n const ref = useRef<T>(null);\n const [width, setWidth] = useState(0);\n useIsomorphicLayoutEffect(() => {\n const el = ref.current;\n if (!el) {\n return;\n }\n setWidth(el.clientWidth);\n if (typeof ResizeObserver === 'undefined') {\n return;\n }\n const ro = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (typeof measured === 'number') {\n setWidth(measured);\n }\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n return [ref, width];\n}\n"],"mappings":";;AAEA,IAAM,IAAY,OAAO,SAAW,KAC9B,IAA4B,IAAY,IAAkB;AAMhE,SAAgB,IAAmC;CACjD,IAAM,CAAC,GAAS,KAAc,EAAS,
|
|
1
|
+
{"version":3,"file":"hooks.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/hooks.ts"],"sourcesContent":["import { type RefObject, useEffect, useLayoutEffect, useRef, useState } from 'react';\n\nconst canUseDOM = typeof window !== 'undefined';\nconst useIsomorphicLayoutEffect = canUseDOM ? useLayoutEffect : useEffect;\n\n/**\n * Tracks the user's reduced-motion preference. Components use this to skip\n * entrance / draw-in transitions entirely rather than merely shortening them.\n */\nexport function usePrefersReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false);\n useEffect(() => {\n if (!canUseDOM || typeof window.matchMedia !== 'function') {\n return;\n }\n const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n const update = () => setReduced(mq.matches);\n update();\n mq.addEventListener('change', update);\n return () => mq.removeEventListener('change', update);\n }, []);\n return reduced;\n}\n\n/** Becomes true after the first paint — drives one-shot enter transitions. */\nexport function useMounted(): boolean {\n const [mounted, setMounted] = useState(false);\n useEffect(() => {\n const raf = requestAnimationFrame(() => setMounted(true));\n return () => cancelAnimationFrame(raf);\n }, []);\n return mounted;\n}\n\nlet uidCounter = 0;\n\n/** Stable, SVG-safe unique id (no colons) for gradient / clip / aria references. */\nexport function useVizId(prefix = 'viz'): string {\n const [id] = useState(() => {\n uidCounter += 1;\n return `${prefix}-${uidCounter}`;\n });\n return id;\n}\n\n/**\n * Observes the width of an element so SVG charts can render a 1:1 viewBox\n * (no aspect-ratio distortion) and reflow on resize. Falls back gracefully\n * where ResizeObserver is unavailable.\n */\nexport function useElementWidth<T extends HTMLElement>(): [RefObject<T | null>, number] {\n const ref = useRef<T>(null);\n const [width, setWidth] = useState(0);\n useIsomorphicLayoutEffect(() => {\n const el = ref.current;\n if (!el) {\n return;\n }\n setWidth(el.clientWidth);\n if (typeof ResizeObserver === 'undefined') {\n return;\n }\n const ro = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (typeof measured === 'number') {\n setWidth(measured);\n }\n });\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n return [ref, width];\n}\n"],"mappings":";;AAEA,IAAM,IAAY,OAAO,SAAW,KAC9B,IAA4B,IAAY,IAAkB;AAMhE,SAAgB,IAAmC;CACjD,IAAM,CAAC,GAAS,KAAc,EAAS,EAAK;CAW5C,OAVA,QAAgB;EACd,IAAI,CAAC,KAAa,OAAO,OAAO,cAAe,YAC7C;EAEF,IAAM,IAAK,OAAO,WAAW,kCAAkC,GACzD,UAAe,EAAW,EAAG,OAAO;EAG1C,OAFA,EAAO,GACP,EAAG,iBAAiB,UAAU,CAAM,SACvB,EAAG,oBAAoB,UAAU,CAAM;CACtD,GAAG,CAAC,CAAC,GACE;AACT;AAGA,SAAgB,IAAsB;CACpC,IAAM,CAAC,GAAS,KAAc,EAAS,EAAK;CAK5C,OAJA,QAAgB;EACd,IAAM,IAAM,4BAA4B,EAAW,EAAI,CAAC;EACxD,aAAa,qBAAqB,CAAG;CACvC,GAAG,CAAC,CAAC,GACE;AACT;AAEA,IAAI,IAAa;AAGjB,SAAgB,EAAS,IAAS,OAAe;CAC/C,IAAM,CAAC,KAAM,SACX,KAAc,GACP,GAAG,EAAO,GAAG,IACrB;CACD,OAAO;AACT;AAOA,SAAgB,IAAwE;CACtF,IAAM,IAAM,EAAU,IAAI,GACpB,CAAC,GAAO,KAAY,EAAS,CAAC;CAmBpC,OAlBA,QAAgC;EAC9B,IAAM,IAAK,EAAI;EAKf,IAJI,CAAC,MAGL,EAAS,EAAG,WAAW,GACnB,OAAO,iBAAmB,MAC5B;EAEF,IAAM,IAAK,IAAI,gBAAgB,MAAY;GACzC,IAAM,IAAW,EAAQ,IAAI,YAAY;GACzC,AAAI,OAAO,KAAa,YACtB,EAAS,CAAQ;EAErB,CAAC;EAED,OADA,EAAG,QAAQ,CAAE,SACA,EAAG,WAAW;CAC7B,GAAG,CAAC,CAAC,GACE,CAAC,GAAK,CAAK;AACpB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"palette.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/palette.ts"],"sourcesContent":["/**\n * Tokenized color palette for the visualization kit.\n *\n * Every color here is a reference to a fe-libs semantic CSS variable, so the\n * kit themes automatically (light / dark / product themes) and never ships a\n * raw hex. The single \"accent\" used for highlights and single-series charts is\n * the product theme's primary action color — the app is NOT green; whatever the\n * active theme resolves `--color-accent-primary` to is what renders.\n *\n * Tints/shades are produced by SVG opacity (fill-opacity / stop-opacity) at the\n * call site, never by emitting a second raw color.\n */\n\n/** Primary highlight — resolves to the product theme's primary action color. */\nexport const ACCENT = 'var(--color-accent-primary)';\n\n/** Faint chart chrome (theme-aware). */\nexport const CHART_GRID = 'var(--color-chart-grid)';\nexport const CHART_AXIS = 'var(--color-chart-axis)';\n\n/** Semantic status hues — saturated and readable in both light and dark. */\nexport const STATUS = {\n success: 'var(--color-status-success-text)',\n warning: 'var(--color-status-warning-text)',\n danger: 'var(--color-status-error-text)',\n info: 'var(--color-status-info-text)',\n} as const;\n\nexport type StatusTone = keyof typeof STATUS;\n\n/**\n * Ordered categorical palette, led by the product accent and drawn entirely\n * from theme-aware semantic tokens. Used to color multi-series charts.\n */\nconst SERIES_TOKENS: readonly string[] = [\n ACCENT,\n 'var(--color-accent-blue)',\n 'var(--color-accent-teal)',\n 'var(--color-accent-purple)',\n 'var(--color-accent-indigo)',\n STATUS.warning,\n STATUS.info,\n STATUS.success,\n 'var(--color-chart-6)',\n 'var(--color-chart-7)',\n 'var(--color-chart-9)',\n 'var(--color-chart-12)',\n];\n\n/**\n * A tokenized palette of `count` colors, cycling the semantic series list.\n * Returns CSS-variable strings (never raw colors).\n */\nexport function chartPalette(count: number): string[] {\n if (count <= 0) {\n return [];\n }\n return Array.from({ length: count }, (_, i) => SERIES_TOKENS[i % SERIES_TOKENS.length]);\n}\n"],"mappings":";AAcA,IAAa,IAAS,+BAGT,IAAa,2BACb,IAAa,2BAGb,IAAS;CACpB,SAAS;CACT,SAAS;CACT,QAAQ;CACR,MAAM;
|
|
1
|
+
{"version":3,"file":"palette.js","names":[],"sources":["../../../../../src/atheletebridge/components/viz/lib/palette.ts"],"sourcesContent":["/**\n * Tokenized color palette for the visualization kit.\n *\n * Every color here is a reference to a fe-libs semantic CSS variable, so the\n * kit themes automatically (light / dark / product themes) and never ships a\n * raw hex. The single \"accent\" used for highlights and single-series charts is\n * the product theme's primary action color — the app is NOT green; whatever the\n * active theme resolves `--color-accent-primary` to is what renders.\n *\n * Tints/shades are produced by SVG opacity (fill-opacity / stop-opacity) at the\n * call site, never by emitting a second raw color.\n */\n\n/** Primary highlight — resolves to the product theme's primary action color. */\nexport const ACCENT = 'var(--color-accent-primary)';\n\n/** Faint chart chrome (theme-aware). */\nexport const CHART_GRID = 'var(--color-chart-grid)';\nexport const CHART_AXIS = 'var(--color-chart-axis)';\n\n/** Semantic status hues — saturated and readable in both light and dark. */\nexport const STATUS = {\n success: 'var(--color-status-success-text)',\n warning: 'var(--color-status-warning-text)',\n danger: 'var(--color-status-error-text)',\n info: 'var(--color-status-info-text)',\n} as const;\n\nexport type StatusTone = keyof typeof STATUS;\n\n/**\n * Ordered categorical palette, led by the product accent and drawn entirely\n * from theme-aware semantic tokens. Used to color multi-series charts.\n */\nconst SERIES_TOKENS: readonly string[] = [\n ACCENT,\n 'var(--color-accent-blue)',\n 'var(--color-accent-teal)',\n 'var(--color-accent-purple)',\n 'var(--color-accent-indigo)',\n STATUS.warning,\n STATUS.info,\n STATUS.success,\n 'var(--color-chart-6)',\n 'var(--color-chart-7)',\n 'var(--color-chart-9)',\n 'var(--color-chart-12)',\n];\n\n/**\n * A tokenized palette of `count` colors, cycling the semantic series list.\n * Returns CSS-variable strings (never raw colors).\n */\nexport function chartPalette(count: number): string[] {\n if (count <= 0) {\n return [];\n }\n return Array.from({ length: count }, (_, i) => SERIES_TOKENS[i % SERIES_TOKENS.length]);\n}\n"],"mappings":";AAcA,IAAa,IAAS,+BAGT,IAAa,2BACb,IAAa,2BAGb,IAAS;CACpB,SAAS;CACT,SAAS;CACT,QAAQ;CACR,MAAM;AACR,GAQM,IAAmC;CACvC;CACA;CACA;CACA;CACA;CACA,EAAO;CACP,EAAO;CACP,EAAO;CACP;CACA;CACA;CACA;AACF;AAMA,SAAgB,EAAa,GAAyB;CAIpD,OAHI,KAAS,IACJ,CAAC,IAEH,MAAM,KAAK,EAAE,QAAQ,EAAM,IAAI,GAAG,MAAM,EAAc,IAAI,EAAc,OAAO;AACxF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AthleteBridgeContext.js","names":[],"sources":["../../../src/atheletebridge/context/AthleteBridgeContext.tsx"],"sourcesContent":["/**\n * AthleteBridge Context Provider\n * @module atheletebridge/context\n *\n * The provider exposes the host-supplied props (basePath, workspaceId,\n * tenantId, apiGatewayUrl, authToken, currentUser, entity-select callbacks) plus\n * path/navigation helpers. The Root mounts this with the exact props the shell\n * passes; the export name/signature is part of the host contract.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { AthleteBridgeRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\n// ============================================================================\n// Context\n// ============================================================================\n\ninterface AthleteBridgeContextValue extends Omit<\n AthleteBridgeRootProps,\n 'registerNavItems' | 'registerFooterItems' | 'navigate'\n> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode) */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path */\n getPath: (relativePath: string) => string;\n}\n\nconst AthleteBridgeContext = createContext<AthleteBridgeContextValue | null>(null);\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useAthleteBridge(): AthleteBridgeContextValue {\n const context = useContext(AthleteBridgeContext);\n if (!context) {\n throw new Error('useAthleteBridge must be used within a AthleteBridgeProvider');\n }\n return context;\n}\n\n// ============================================================================\n// Provider Component\n// ============================================================================\n\ntype ProviderProps = Omit<AthleteBridgeRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\n/** Provider for standalone mode (has access to MemoryRouter) */\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n const fullPath = joinPath(effectiveBasePath, relativePath);\n routerNavigate(fullPath);\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<AthleteBridgeContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <AthleteBridgeContext.Provider value={contextValue}>{children}</AthleteBridgeContext.Provider>\n );\n};\n\n/** Provider for integrated mode (shell provides navigate) */\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n}) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n const fullPath = joinPath(effectiveBasePath, relativePath);\n navigate(fullPath);\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<AthleteBridgeContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <AthleteBridgeContext.Provider value={contextValue}>{children}</AthleteBridgeContext.Provider>\n );\n};\n\nexport const AthleteBridgeProvider: FC<PropsWithChildren<AthleteBridgeRootProps>> = (props) => {\n const { children, navigate } = props;\n // Standalone mode: no navigate function provided (MFE manages its own router)\n // Integrated mode: navigate function provided by shell (shell provides router)\n const isStandalone = !navigate;\n\n // Standalone mode: wrap in MemoryRouter\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n // Integrated mode: shell provides router\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AAiCA,IAAM,IAAuB,EAAgD,
|
|
1
|
+
{"version":3,"file":"AthleteBridgeContext.js","names":[],"sources":["../../../src/atheletebridge/context/AthleteBridgeContext.tsx"],"sourcesContent":["/**\n * AthleteBridge Context Provider\n * @module atheletebridge/context\n *\n * The provider exposes the host-supplied props (basePath, workspaceId,\n * tenantId, apiGatewayUrl, authToken, currentUser, entity-select callbacks) plus\n * path/navigation helpers. The Root mounts this with the exact props the shell\n * passes; the export name/signature is part of the host contract.\n */\n\nimport type { FC, PropsWithChildren } from 'react';\nimport { createContext, useContext, useMemo, useCallback } from 'react';\nimport { MemoryRouter, useNavigate as useRouterNavigate } from 'react-router-dom';\nimport type { AthleteBridgeRootProps } from '../types';\nimport { joinPath } from '../utils/navigation';\n\n// ============================================================================\n// Context\n// ============================================================================\n\ninterface AthleteBridgeContextValue extends Omit<\n AthleteBridgeRootProps,\n 'registerNavItems' | 'registerFooterItems' | 'navigate'\n> {\n isStandalone: boolean;\n /** Shell's navigate function (only available in integrated mode) */\n navigate?: (path: string) => void;\n /** Navigate to a path relative to basePath */\n navigateTo: (relativePath: string) => void;\n /** Get the full path for a relative path */\n getPath: (relativePath: string) => string;\n}\n\nconst AthleteBridgeContext = createContext<AthleteBridgeContextValue | null>(null);\n\n// ============================================================================\n// Hook\n// ============================================================================\n\nexport function useAthleteBridge(): AthleteBridgeContextValue {\n const context = useContext(AthleteBridgeContext);\n if (!context) {\n throw new Error('useAthleteBridge must be used within a AthleteBridgeProvider');\n }\n return context;\n}\n\n// ============================================================================\n// Provider Component\n// ============================================================================\n\ntype ProviderProps = Omit<AthleteBridgeRootProps, 'registerNavItems' | 'registerFooterItems'> & {\n isStandalone: boolean;\n};\n\n/** Provider for standalone mode (has access to MemoryRouter) */\nconst StandaloneProviderInner: FC<PropsWithChildren<ProviderProps>> = ({\n children,\n basePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n}) => {\n const routerNavigate = useRouterNavigate();\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n const fullPath = joinPath(effectiveBasePath, relativePath);\n routerNavigate(fullPath);\n },\n [effectiveBasePath, routerNavigate]\n );\n\n const contextValue = useMemo<AthleteBridgeContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate: undefined,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n isStandalone: true,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <AthleteBridgeContext.Provider value={contextValue}>{children}</AthleteBridgeContext.Provider>\n );\n};\n\n/** Provider for integrated mode (shell provides navigate) */\nconst IntegratedProviderInner: FC<\n PropsWithChildren<ProviderProps & { navigate: (path: string) => void }>\n> = ({\n children,\n basePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n}) => {\n const effectiveBasePath = basePath || '/';\n\n const getPath = useCallback(\n (relativePath: string) => joinPath(effectiveBasePath, relativePath),\n [effectiveBasePath]\n );\n\n const navigateTo = useCallback(\n (relativePath: string) => {\n const fullPath = joinPath(effectiveBasePath, relativePath);\n navigate(fullPath);\n },\n [effectiveBasePath, navigate]\n );\n\n const contextValue = useMemo<AthleteBridgeContextValue>(\n () => ({\n basePath: effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n isStandalone: false,\n navigateTo,\n getPath,\n }),\n [\n effectiveBasePath,\n navigate,\n currentUser,\n tenantId,\n workspaceId,\n apiGatewayUrl,\n authToken,\n onTeamSelect,\n onFixtureSelect,\n navigateTo,\n getPath,\n ]\n );\n\n return (\n <AthleteBridgeContext.Provider value={contextValue}>{children}</AthleteBridgeContext.Provider>\n );\n};\n\nexport const AthleteBridgeProvider: FC<PropsWithChildren<AthleteBridgeRootProps>> = (props) => {\n const { children, navigate } = props;\n // Standalone mode: no navigate function provided (MFE manages its own router)\n // Integrated mode: navigate function provided by shell (shell provides router)\n const isStandalone = !navigate;\n\n // Standalone mode: wrap in MemoryRouter\n if (isStandalone) {\n return (\n <MemoryRouter>\n <StandaloneProviderInner {...props} isStandalone={true}>\n {children}\n </StandaloneProviderInner>\n </MemoryRouter>\n );\n }\n\n // Integrated mode: shell provides router\n return (\n <IntegratedProviderInner {...props} navigate={navigate} isStandalone={false}>\n {children}\n </IntegratedProviderInner>\n );\n};\n"],"mappings":";;;;;AAiCA,IAAM,IAAuB,EAAgD,IAAI;AAMjF,SAAgB,IAA8C;CAC5D,IAAM,IAAU,EAAW,CAAoB;CAC/C,IAAI,CAAC,GACH,MAAU,MAAM,8DAA8D;CAEhF,OAAO;AACT;AAWA,IAAM,KAAiE,EACrE,aACA,aACA,gBACA,aACA,gBACA,kBACA,cACA,iBACA,yBACI;CACJ,IAAM,IAAiB,EAAkB,GACnC,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,CAAY,GAClE,CAAC,CAAiB,CACpB,GAEM,IAAa,GAChB,MAAyB;EAExB,EADiB,EAAS,GAAmB,CAC9B,CAAQ;CACzB,GACA,CAAC,GAAmB,CAAc,CACpC,GAEM,IAAe,SACZ;EACL,UAAU;EACV,UAAU,KAAA;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,OACE,kBAAC,EAAqB,UAAtB;EAA+B,OAAO;EAAe;CAAwC,CAAA;AAEjG,GAGM,KAED,EACH,aACA,aACA,aACA,gBACA,aACA,gBACA,kBACA,cACA,iBACA,yBACI;CACJ,IAAM,IAAoB,KAAY,KAEhC,IAAU,GACb,MAAyB,EAAS,GAAmB,CAAY,GAClE,CAAC,CAAiB,CACpB,GAEM,IAAa,GAChB,MAAyB;EAExB,EADiB,EAAS,GAAmB,CACpC,CAAQ;CACnB,GACA,CAAC,GAAmB,CAAQ,CAC9B,GAEM,IAAe,SACZ;EACL,UAAU;EACV;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,cAAc;EACd;EACA;CACF,IACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,OACE,kBAAC,EAAqB,UAAtB;EAA+B,OAAO;EAAe;CAAwC,CAAA;AAEjG,GAEa,KAAwE,MAAU;CAC7F,IAAM,EAAE,aAAU,gBAAa;CAiB/B,OAdsB,IAepB,kBAAC,GAAD;EAAyB,GAAI;EAAiB;EAAU,cAAc;EACnE;CACsB,CAAA,IAZvB,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;EAAyB,GAAI;EAAO,cAAc;EAC/C;CACsB,CAAA,EACb,CAAA;AAUpB"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AthCompleteSessionInput, AthCreateFixtureInput, AthCreateProgramInput, AthCreateTeamInput, AthAddMemberInput, AthAddDrillInput, AthBookSessionInput, AthDrill, AthFixture, AthFixtureConnection, AthFixtureStatus, AthLogWorkoutInput, AthMember, AthMemberStatus, AthPerformanceMetric, AthProgram, AthProgramConnection, AthProgramLevel, AthProgramStatus, AthRecordMetricInput, AthRecordResultInput, AthSession, AthSessionConnection, AthSessionStatus, AthSport, AthStanding, AthTeam, AthTeamConnection, AthTeamStatus, AthUpdateProgramInput, AthUpdateTeamInput, AthWorkoutLog, AthWorkoutLogConnection, PaginationInput } from '../types';
|
|
1
|
+
import { AthAvailability, AthCompleteSessionInput, AthCreateFixtureInput, AthCreateProgramInput, AthCreateTeamInput, AthAddMemberInput, AthAddDrillInput, AthBookSessionInput, AthDrill, AthFixture, AthFixtureConnection, AthFixtureStatus, AthLogWorkoutInput, AthMember, AthMemberStatus, AthPerformanceMetric, AthProgram, AthProgramConnection, AthProgramLevel, AthProgramStatus, AthRecordMetricInput, AthRecordResultInput, AthSession, AthSessionConnection, AthSessionStatus, AthSetAvailabilityInput, AthSport, AthStanding, AthTeam, AthTeamConnection, AthTeamStatus, AthUpdateProgramInput, AthUpdateTeamInput, AthWorkoutLog, AthWorkoutLogConnection, PaginationInput } from '../types';
|
|
2
2
|
|
|
3
3
|
export interface TeamFilters {
|
|
4
4
|
sport?: AthSport;
|
|
@@ -73,6 +73,23 @@ export declare const useWorkoutLogs: (filters?: WorkoutLogFilters) => import('@t
|
|
|
73
73
|
export declare const usePerformanceMetrics: (athleteId: string | undefined) => import('@tanstack/react-query').UseQueryResult<NoInfer<AthPerformanceMetric[]>, Error>;
|
|
74
74
|
export declare const useLogWorkout: () => import('@tanstack/react-query').UseMutationResult<AthWorkoutLog, Error, AthLogWorkoutInput, unknown>;
|
|
75
75
|
export declare const useRecordMetric: () => import('@tanstack/react-query').UseMutationResult<AthPerformanceMetric, Error, AthRecordMetricInput, unknown>;
|
|
76
|
+
export interface AvailabilityFilters {
|
|
77
|
+
fixtureId?: string;
|
|
78
|
+
sessionId?: string;
|
|
79
|
+
}
|
|
80
|
+
/** Lists availability responses for a fixture OR a session. */
|
|
81
|
+
export declare const useAvailability: (filters: AvailabilityFilters) => import('@tanstack/react-query').UseQueryResult<NoInfer<AthAvailability[]>, Error>;
|
|
82
|
+
export declare const useSetAvailability: () => import('@tanstack/react-query').UseMutationResult<AthAvailability, Error, AthSetAvailabilityInput, unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* Nudges roster members who have not yet responded. Publishes a domain event
|
|
85
|
+
* for a future notification consumer to deliver — it does not itself send
|
|
86
|
+
* push/email (no delivery channel is wired up yet). Returns the number of
|
|
87
|
+
* members nudged.
|
|
88
|
+
*/
|
|
89
|
+
export declare const useRemindAvailability: () => import('@tanstack/react-query').UseMutationResult<number, Error, {
|
|
90
|
+
fixtureId?: string;
|
|
91
|
+
sessionId?: string;
|
|
92
|
+
}, unknown>;
|
|
76
93
|
/**
|
|
77
94
|
* Upload a file to wspace-files-svc via the standard three-step flow
|
|
78
95
|
* (initiateUpload → presigned PUT → completeUpload) and resolve the new file id,
|