@burdenoff/microfe-atheletebridge 2026.705.2 → 2026.706.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atheletebridge/AthleteBridgeRoot.js +55 -39
- package/dist/atheletebridge/AthleteBridgeRoot.js.map +1 -1
- package/dist/atheletebridge/AthleteBridgeRoutes.js +18 -6
- package/dist/atheletebridge/AthleteBridgeRoutes.js.map +1 -1
- package/dist/atheletebridge/components/Crest.js +70 -0
- package/dist/atheletebridge/components/Crest.js.map +1 -0
- package/dist/atheletebridge/components/FixtureCard.js +8 -72
- package/dist/atheletebridge/components/FixtureCard.js.map +1 -1
- package/dist/atheletebridge/components/MatchupRow.js +74 -0
- package/dist/atheletebridge/components/MatchupRow.js.map +1 -0
- package/dist/atheletebridge/components/MetricsCard.js +61 -21
- package/dist/atheletebridge/components/MetricsCard.js.map +1 -1
- package/dist/atheletebridge/components/ProgramCard.js +75 -43
- package/dist/atheletebridge/components/ProgramCard.js.map +1 -1
- package/dist/atheletebridge/components/Scoreboard.js +163 -0
- package/dist/atheletebridge/components/Scoreboard.js.map +1 -0
- package/dist/atheletebridge/components/SessionCard.js +64 -41
- package/dist/atheletebridge/components/SessionCard.js.map +1 -1
- package/dist/atheletebridge/components/SessionStatusTimeline.js +80 -0
- package/dist/atheletebridge/components/SessionStatusTimeline.js.map +1 -0
- package/dist/atheletebridge/components/StatusBadge.js +5 -5
- package/dist/atheletebridge/components/StatusBadge.js.map +1 -1
- package/dist/atheletebridge/components/TeamCard.js +65 -35
- package/dist/atheletebridge/components/TeamCard.js.map +1 -1
- package/dist/atheletebridge/components/UserAvatar.js +33 -0
- package/dist/atheletebridge/components/UserAvatar.js.map +1 -0
- package/dist/atheletebridge/components/domain.js +5 -2
- package/dist/atheletebridge/components/domain.js.map +1 -1
- package/dist/atheletebridge/components/viz/AvatarStack.js +2 -0
- package/dist/atheletebridge/components/viz/BarChart.js +83 -0
- package/dist/atheletebridge/components/viz/BarChart.js.map +1 -0
- package/dist/atheletebridge/components/viz/DonutChart.js +109 -0
- package/dist/atheletebridge/components/viz/DonutChart.js.map +1 -0
- package/dist/atheletebridge/components/viz/LineChart.js +123 -0
- package/dist/atheletebridge/components/viz/LineChart.js.map +1 -0
- package/dist/atheletebridge/components/viz/ProgressRing.js +6 -0
- package/dist/atheletebridge/components/viz/ProgressRing.js.map +1 -0
- package/dist/atheletebridge/components/viz/RatingStars.js +92 -0
- package/dist/atheletebridge/components/viz/RatingStars.js.map +1 -0
- package/dist/atheletebridge/components/viz/Sparkline.js +74 -0
- package/dist/atheletebridge/components/viz/Sparkline.js.map +1 -0
- package/dist/atheletebridge/components/viz/StatCard.js +60 -0
- package/dist/atheletebridge/components/viz/StatCard.js.map +1 -0
- package/dist/atheletebridge/components/viz/StepProgress.js +3 -0
- package/dist/atheletebridge/components/viz/Timeline.js +66 -0
- package/dist/atheletebridge/components/viz/Timeline.js.map +1 -0
- package/dist/atheletebridge/components/viz/index.js +13 -0
- package/dist/atheletebridge/components/viz/lib/Avatar.js +1 -0
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.js +119 -0
- package/dist/atheletebridge/components/viz/lib/CartesianFrame.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js +35 -0
- package/dist/atheletebridge/components/viz/lib/ResponsiveSvg.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/format.js +17 -0
- package/dist/atheletebridge/components/viz/lib/format.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/geometry.js +61 -0
- package/dist/atheletebridge/components/viz/lib/geometry.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/hooks.js +39 -0
- package/dist/atheletebridge/components/viz/lib/hooks.js.map +1 -0
- package/dist/atheletebridge/components/viz/lib/palette.js +27 -0
- package/dist/atheletebridge/components/viz/lib/palette.js.map +1 -0
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js +369 -163
- package/dist/atheletebridge/hooks/useAthleteBridgeApi.js.map +1 -1
- package/dist/atheletebridge/pages/DashboardPage.js +182 -117
- package/dist/atheletebridge/pages/DashboardPage.js.map +1 -1
- package/dist/atheletebridge/pages/StandingsPage.js +119 -95
- package/dist/atheletebridge/pages/StandingsPage.js.map +1 -1
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js +372 -0
- package/dist/atheletebridge/pages/analytics/CoachAnalyticsPage.js.map +1 -0
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js +511 -0
- package/dist/atheletebridge/pages/athletes/AthleteProfilePage.js.map +1 -0
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js +120 -131
- package/dist/atheletebridge/pages/fixtures/FixtureDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js +264 -176
- package/dist/atheletebridge/pages/fixtures/FixtureFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js +84 -47
- package/dist/atheletebridge/pages/fixtures/FixturesListPage.js.map +1 -1
- package/dist/atheletebridge/pages/performance/PerformancePage.js +432 -281
- package/dist/atheletebridge/pages/performance/PerformancePage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js +187 -123
- package/dist/atheletebridge/pages/programs/ProgramDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js +56 -39
- package/dist/atheletebridge/pages/programs/ProgramsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js +267 -139
- package/dist/atheletebridge/pages/sessions/SessionDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js +253 -162
- package/dist/atheletebridge/pages/sessions/SessionFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js +38 -36
- package/dist/atheletebridge/pages/sessions/SessionsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js +437 -189
- package/dist/atheletebridge/pages/teams/TeamDetailPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamFormPage.js +368 -177
- package/dist/atheletebridge/pages/teams/TeamFormPage.js.map +1 -1
- package/dist/atheletebridge/pages/teams/TeamsListPage.js +3 -0
- package/dist/atheletebridge/pages/teams/TeamsListPage.js.map +1 -1
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js +298 -0
- package/dist/atheletebridge/pages/tournaments/TournamentBracketPage.js.map +1 -0
- package/dist/atheletebridge/utils/identity.js +31 -0
- package/dist/atheletebridge/utils/identity.js.map +1 -0
- package/dist/atheletebridge/utils/timeAgo.js +20 -0
- package/dist/atheletebridge/utils/timeAgo.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { chartPalette as t } from "./lib/palette.js";
|
|
3
|
+
import { useMounted as n, usePrefersReducedMotion as r } from "./lib/hooks.js";
|
|
4
|
+
import { donutSegmentPath as i } from "./lib/geometry.js";
|
|
5
|
+
import { formatCompact as a, toPercent as o } from "./lib/format.js";
|
|
6
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
7
|
+
//#region src/atheletebridge/components/viz/DonutChart.tsx
|
|
8
|
+
var l = 100, u = l / 2, d = 46;
|
|
9
|
+
function f({ segments: f, centerLabel: p, centerCaption: m, size: h = 168, thickness: g = .28, valueFormatter: _ = a, showLegend: v = !0, title: y, className: b }) {
|
|
10
|
+
let x = n(), S = r();
|
|
11
|
+
if (f.length === 0) return null;
|
|
12
|
+
let C = f.reduce((e, t) => e + Math.max(0, t.value), 0), w = t(f.length), T = d * (1 - g), E = y ?? `Donut chart of ${f.length} segments`, D = f.map((e) => C > 0 ? Math.max(0, e.value) / C : 0), O = D.map((e, t) => D.slice(0, t).reduce((e, t) => e + t, 0)), k = f.map((e, t) => {
|
|
13
|
+
let n = D[t], r = O[t] * 360, i = Math.min((O[t] + n) * 360, 359.999);
|
|
14
|
+
return {
|
|
15
|
+
segment: e,
|
|
16
|
+
color: e.color ?? w[t],
|
|
17
|
+
startAngle: r,
|
|
18
|
+
endAngle: i,
|
|
19
|
+
fraction: n,
|
|
20
|
+
isFull: n >= .9999
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
return /* @__PURE__ */ c("div", {
|
|
24
|
+
className: e("flex flex-wrap items-center gap-5", b),
|
|
25
|
+
children: [/* @__PURE__ */ c("div", {
|
|
26
|
+
className: "relative shrink-0",
|
|
27
|
+
style: {
|
|
28
|
+
width: h,
|
|
29
|
+
height: h,
|
|
30
|
+
maxWidth: "100%"
|
|
31
|
+
},
|
|
32
|
+
children: [/* @__PURE__ */ c("svg", {
|
|
33
|
+
viewBox: `0 0 ${l} ${l}`,
|
|
34
|
+
width: h,
|
|
35
|
+
height: h,
|
|
36
|
+
role: "img",
|
|
37
|
+
"aria-label": E,
|
|
38
|
+
style: {
|
|
39
|
+
display: "block",
|
|
40
|
+
maxWidth: "100%",
|
|
41
|
+
height: "auto"
|
|
42
|
+
},
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ s("title", { children: E }),
|
|
45
|
+
/* @__PURE__ */ s("desc", { children: f.map((e) => `${e.label}: ${_(e.value)} (${o(e.value, C)}%)`).join(", ") }),
|
|
46
|
+
/* @__PURE__ */ s("g", {
|
|
47
|
+
style: {
|
|
48
|
+
transformOrigin: "center",
|
|
49
|
+
transform: x || S ? "scale(1)" : "scale(0.86)",
|
|
50
|
+
transition: S ? void 0 : "transform 520ms cubic-bezier(0.22,1,0.36,1)"
|
|
51
|
+
},
|
|
52
|
+
children: k.map((e, t) => {
|
|
53
|
+
let n = {
|
|
54
|
+
fill: e.color,
|
|
55
|
+
style: {
|
|
56
|
+
opacity: x || S ? 1 : 0,
|
|
57
|
+
transition: S ? void 0 : "opacity 460ms ease",
|
|
58
|
+
transitionDelay: S ? void 0 : `${t * 70}ms`
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return e.isFull ? /* @__PURE__ */ s("path", {
|
|
62
|
+
d: `M ${u} ${u - d} A ${d} ${d} 0 1 0 ${u} ${u + d} 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
|
+
fillRule: "evenodd",
|
|
64
|
+
...n
|
|
65
|
+
}, `${e.segment.label}-${t}`) : /* @__PURE__ */ s("path", {
|
|
66
|
+
d: i(u, u, d, T, e.startAngle, e.endAngle),
|
|
67
|
+
...n,
|
|
68
|
+
children: /* @__PURE__ */ s("title", { children: `${e.segment.label}: ${_(e.segment.value)}` })
|
|
69
|
+
}, `${e.segment.label}-${t}`);
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
]
|
|
73
|
+
}), p !== void 0 || m !== void 0 ? /* @__PURE__ */ c("div", {
|
|
74
|
+
className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center",
|
|
75
|
+
children: [/* @__PURE__ */ s("span", {
|
|
76
|
+
className: "text-xl font-semibold leading-none text-text-primary",
|
|
77
|
+
children: p
|
|
78
|
+
}), m === void 0 ? null : /* @__PURE__ */ s("span", {
|
|
79
|
+
className: "mt-1 text-xs text-text-muted",
|
|
80
|
+
children: m
|
|
81
|
+
})]
|
|
82
|
+
}) : null]
|
|
83
|
+
}), v ? /* @__PURE__ */ s("ul", {
|
|
84
|
+
className: "flex min-w-[8rem] flex-1 flex-col gap-1.5 text-sm",
|
|
85
|
+
children: k.map((e, t) => /* @__PURE__ */ c("li", {
|
|
86
|
+
className: "flex items-center gap-2",
|
|
87
|
+
children: [
|
|
88
|
+
/* @__PURE__ */ s("span", {
|
|
89
|
+
"aria-hidden": "true",
|
|
90
|
+
className: "inline-block h-2.5 w-2.5 shrink-0 rounded-full",
|
|
91
|
+
style: { backgroundColor: e.color }
|
|
92
|
+
}),
|
|
93
|
+
/* @__PURE__ */ s("span", {
|
|
94
|
+
className: "flex-1 truncate text-text-secondary",
|
|
95
|
+
children: e.segment.label
|
|
96
|
+
}),
|
|
97
|
+
/* @__PURE__ */ c("span", {
|
|
98
|
+
className: "tabular-nums text-text-muted",
|
|
99
|
+
children: [o(e.segment.value, C), "%"]
|
|
100
|
+
})
|
|
101
|
+
]
|
|
102
|
+
}, `${e.segment.label}-${t}`))
|
|
103
|
+
}) : null]
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
export { f as DonutChart };
|
|
108
|
+
|
|
109
|
+
//# sourceMappingURL=DonutChart.js.map
|
|
@@ -0,0 +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,GAAY,EACtB,IAAU,GAAyB;AAEzC,KAAI,EAAS,WAAW,EACtB,QAAO;CAGT,IAAM,IAAQ,EAAS,QAAQ,GAAK,MAAM,IAAM,KAAK,IAAI,GAAG,EAAE,MAAM,EAAE,EAAE,EAClE,IAAU,EAAa,EAAS,OAAO,EACvC,IAAS,KAAW,IAAI,IACxB,IAAa,KAAS,kBAAkB,EAAS,OAAO,YAExD,IAAY,EAAS,KAAK,MAAO,IAAQ,IAAI,KAAK,IAAI,GAAG,EAAE,MAAM,GAAG,IAAQ,EAAG,EAC/E,IAAS,EAAU,KAAK,GAAG,MAAM,EAAU,MAAM,GAAG,EAAE,CAAC,QAAQ,GAAG,MAAM,IAAI,GAAG,EAAE,CAAC,EAClF,IAAO,EAAS,KAAK,GAAS,MAAM;EACxC,IAAM,IAAW,EAAU,IACrB,IAAa,EAAO,KAAK,KACzB,IAAW,KAAK,KAAK,EAAO,KAAK,KAAY,KAAK,QAAQ;AAEhE,SAAO;GAAE;GAAS,OADJ,EAAQ,SAAS,EAAQ;GACd;GAAY;GAAU;GAAU,QAAQ,KAAY;GAAQ;GACrF;AAEF,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,qCAAqC,EAAU;YAAlE,CACE,kBAAC,OAAD;GAAK,WAAU;GAAoB,OAAO;IAAE,OAAO;IAAM,QAAQ;IAAM,UAAU;IAAQ;aAAzF,CACE,kBAAC,OAAD;IACE,SAAS,OAAO,EAAK,GAAG;IACxB,OAAO;IACP,QAAQ;IACR,MAAK;IACL,cAAY;IACZ,OAAO;KAAE,SAAS;KAAS,UAAU;KAAQ,QAAQ;KAAQ;cAN/D;KAQE,kBAAC,SAAD,EAAA,UAAQ,GAAmB,CAAA;KAC3B,kBAAC,QAAD,EAAA,UACG,EACE,KAAK,MAAM,GAAG,EAAE,MAAM,IAAI,EAAe,EAAE,MAAM,CAAC,IAAI,EAAU,EAAE,OAAO,EAAM,CAAC,IAAI,CACpF,KAAK,KAAK,EACR,CAAA;KACP,kBAAC,KAAD;MACE,OAAO;OACL,iBAAiB;OACjB,WAAW,KAAW,IAAU,aAAa;OAC7C,YAAY,IAAU,KAAA,IAAY;OACnC;gBAEA,EAAK,KAAK,GAAK,MAAM;OACpB,IAAM,IAAS;QACb,MAAM,EAAI;QACV,OAAO;SACL,SAAS,KAAW,IAAU,IAAI;SAClC,YAAY,IAAU,KAAA,IAAY;SAClC,iBAAiB,IAAU,KAAA,IAAY,GAAG,IAAI,GAAG;SAClD;QACF;AAiBD,cAhBI,EAAI,SAEJ,kBAAC,QAAD;QAEE,GAAG,KAAK,EAAO,GAAG,IAAS,EAAQ,KAAK,EAAQ,GAAG,EAAQ,SAAS,EAAO,GACzE,IAAS,EACV,KAAK,EAAQ,GAAG,EAAQ,SAAS,EAAO,GAAG,IAAS,EAAQ,KAAK,EAAO,GACvE,IAAS,EACV,KAAK,EAAO,GAAG,EAAO,SAAS,EAAO,GAAG,IAAS,EAAO,KAAK,EAAO,GAAG,EAAO,SAAS,EAAO,GAC9F,IAAS,EACV;QACD,UAAS;QACT,GAAI;QACJ,EAVK,GAAG,EAAI,QAAQ,MAAM,GAAG,IAU7B,GAIJ,kBAAC,QAAD;QAEE,GAAG,EACD,GACA,GACA,GACA,GACA,EAAI,YACJ,EAAI,SACL;QACD,GAAI;kBAEJ,kBAAC,SAAD,EAAA,UAAQ,GAAG,EAAI,QAAQ,MAAM,IAAI,EAAe,EAAI,QAAQ,MAAM,IAAW,CAAA;QACxE,EAZA,GAAG,EAAI,QAAQ,MAAM,GAAG,IAYxB;QAET;MACA,CAAA;KACA;OACL,MAAgB,KAAA,KAAa,MAAkB,KAAA,IAC9C,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eACb;KACI,CAAA,EACN,MAAkB,KAAA,IAEf,OADF,kBAAC,QAAD;KAAM,WAAU;eAAgC;KAAqB,CAAA,CAEnE;QACJ,KACA;MACL,IACC,kBAAC,MAAD;GAAI,WAAU;aACX,EAAK,KAAK,GAAK,MACd,kBAAC,MAAD;IAAsC,WAAU;cAAhD;KACE,kBAAC,QAAD;MACE,eAAY;MACZ,WAAU;MACV,OAAO,EAAE,iBAAiB,EAAI,OAAO;MACrC,CAAA;KACF,kBAAC,QAAD;MAAM,WAAU;gBAAuC,EAAI,QAAQ;MAAa,CAAA;KAChF,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,EAAU,EAAI,QAAQ,OAAO,EAAM,EAAC,IAChC;;KACJ;MAVI,GAAG,EAAI,QAAQ,MAAM,GAAG,IAU5B,CACL;GACC,CAAA,GACH,KACA"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { chartPalette as e } from "./lib/palette.js";
|
|
2
|
+
import { useMounted as t, usePrefersReducedMotion as n } from "./lib/hooks.js";
|
|
3
|
+
import { buildLinePath as r } from "./lib/geometry.js";
|
|
4
|
+
import { CartesianFrame as i } from "./lib/CartesianFrame.js";
|
|
5
|
+
import { formatCompact as a } from "./lib/format.js";
|
|
6
|
+
import { Fragment as o, jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
7
|
+
//#region src/atheletebridge/components/viz/LineChart.tsx
|
|
8
|
+
function l({ series: l, compare: u, seriesLabel: d = "Series", compareLabel: f = "Compare", height: p = 200, valueFormatter: m = a, title: h, className: g }) {
|
|
9
|
+
let _ = t(), v = n();
|
|
10
|
+
if (l.length === 0) return null;
|
|
11
|
+
let y = l.map((e) => e.label), b = l.map((e) => e.value), x = Array.isArray(u) && u.length > 0, S = x ? [...b, ...u] : b, C = Math.min(...S), w = Math.max(...S), T = w - C || Math.abs(w) || 1, E = [C - T * .12, w + T * .12], [D, O] = e(2), k = h ?? `Line chart of ${l.length} points`, A = {
|
|
12
|
+
strokeDasharray: 1,
|
|
13
|
+
strokeDashoffset: _ || v ? 0 : 1,
|
|
14
|
+
transition: v ? void 0 : "stroke-dashoffset 900ms ease"
|
|
15
|
+
};
|
|
16
|
+
return /* @__PURE__ */ c("div", {
|
|
17
|
+
className: g,
|
|
18
|
+
children: [x ? /* @__PURE__ */ c("div", {
|
|
19
|
+
className: "mb-2 flex flex-wrap items-center gap-4 text-xs text-text-secondary",
|
|
20
|
+
children: [/* @__PURE__ */ c("span", {
|
|
21
|
+
className: "inline-flex items-center gap-1.5",
|
|
22
|
+
children: [/* @__PURE__ */ s("span", {
|
|
23
|
+
"aria-hidden": "true",
|
|
24
|
+
className: "inline-block h-2 w-3 rounded-full",
|
|
25
|
+
style: { backgroundColor: D }
|
|
26
|
+
}), d]
|
|
27
|
+
}), /* @__PURE__ */ c("span", {
|
|
28
|
+
className: "inline-flex items-center gap-1.5",
|
|
29
|
+
children: [/* @__PURE__ */ s("span", {
|
|
30
|
+
"aria-hidden": "true",
|
|
31
|
+
className: "inline-block h-2 w-3 rounded-full",
|
|
32
|
+
style: { backgroundColor: O }
|
|
33
|
+
}), f]
|
|
34
|
+
})]
|
|
35
|
+
}) : null, /* @__PURE__ */ s(i, {
|
|
36
|
+
labels: y,
|
|
37
|
+
domain: E,
|
|
38
|
+
height: p,
|
|
39
|
+
title: k,
|
|
40
|
+
formatValue: m,
|
|
41
|
+
tooltip: (e) => /* @__PURE__ */ c("span", {
|
|
42
|
+
className: "flex flex-col gap-0.5",
|
|
43
|
+
children: [
|
|
44
|
+
/* @__PURE__ */ s("span", {
|
|
45
|
+
className: "text-text-secondary",
|
|
46
|
+
children: y[e]
|
|
47
|
+
}),
|
|
48
|
+
/* @__PURE__ */ c("span", {
|
|
49
|
+
className: "font-semibold tabular-nums",
|
|
50
|
+
style: { color: D },
|
|
51
|
+
children: [
|
|
52
|
+
d,
|
|
53
|
+
": ",
|
|
54
|
+
m(b[e])
|
|
55
|
+
]
|
|
56
|
+
}),
|
|
57
|
+
x && u ? /* @__PURE__ */ c("span", {
|
|
58
|
+
className: "font-semibold tabular-nums",
|
|
59
|
+
style: { color: O },
|
|
60
|
+
children: [
|
|
61
|
+
f,
|
|
62
|
+
": ",
|
|
63
|
+
m(u[e])
|
|
64
|
+
]
|
|
65
|
+
}) : null
|
|
66
|
+
]
|
|
67
|
+
}),
|
|
68
|
+
children: ({ px: e, py: t, activeIndex: n }) => {
|
|
69
|
+
let i = b.map((n, r) => ({
|
|
70
|
+
x: e(r),
|
|
71
|
+
y: t(n)
|
|
72
|
+
})), a = x && u ? u.map((n, r) => ({
|
|
73
|
+
x: e(r),
|
|
74
|
+
y: t(n)
|
|
75
|
+
})) : [];
|
|
76
|
+
return /* @__PURE__ */ c("g", { children: [
|
|
77
|
+
a.length > 0 ? /* @__PURE__ */ s("path", {
|
|
78
|
+
d: r(a),
|
|
79
|
+
fill: "none",
|
|
80
|
+
stroke: O,
|
|
81
|
+
strokeWidth: 2,
|
|
82
|
+
strokeLinecap: "round",
|
|
83
|
+
strokeLinejoin: "round",
|
|
84
|
+
strokeOpacity: .85,
|
|
85
|
+
vectorEffect: "non-scaling-stroke",
|
|
86
|
+
pathLength: 1,
|
|
87
|
+
style: A
|
|
88
|
+
}) : null,
|
|
89
|
+
/* @__PURE__ */ s("path", {
|
|
90
|
+
d: r(i),
|
|
91
|
+
fill: "none",
|
|
92
|
+
stroke: D,
|
|
93
|
+
strokeWidth: 2.25,
|
|
94
|
+
strokeLinecap: "round",
|
|
95
|
+
strokeLinejoin: "round",
|
|
96
|
+
vectorEffect: "non-scaling-stroke",
|
|
97
|
+
pathLength: 1,
|
|
98
|
+
style: A
|
|
99
|
+
}),
|
|
100
|
+
n === null ? null : /* @__PURE__ */ c(o, { children: [/* @__PURE__ */ s("circle", {
|
|
101
|
+
cx: e(n),
|
|
102
|
+
cy: t(b[n]),
|
|
103
|
+
r: 3.5,
|
|
104
|
+
fill: D,
|
|
105
|
+
stroke: "var(--color-bg-surface)",
|
|
106
|
+
strokeWidth: 1.5
|
|
107
|
+
}), x && u ? /* @__PURE__ */ s("circle", {
|
|
108
|
+
cx: e(n),
|
|
109
|
+
cy: t(u[n]),
|
|
110
|
+
r: 3.5,
|
|
111
|
+
fill: O,
|
|
112
|
+
stroke: "var(--color-bg-surface)",
|
|
113
|
+
strokeWidth: 1.5
|
|
114
|
+
}) : null] })
|
|
115
|
+
] });
|
|
116
|
+
}
|
|
117
|
+
})]
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
export { l as LineChart };
|
|
122
|
+
|
|
123
|
+
//# sourceMappingURL=LineChart.js.map
|
|
@@ -0,0 +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":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProgressRing.js","names":[],"sources":["../../../../src/atheletebridge/components/viz/ProgressRing.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\nimport { clamp } from './lib/geometry';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\n\nexport interface ProgressRingProps {\n value: number;\n max?: number;\n /** Center content. Defaults to the rounded value (e.g. \"4.6\" or \"72%\"). */\n label?: ReactNode;\n /** Small caption under the center label. */\n caption?: ReactNode;\n size?: number;\n /** Stroke thickness in viewBox units. */\n thickness?: number;\n /** Tokenized arc color; defaults to the product accent. */\n color?: string;\n title?: string;\n className?: string;\n}\n\nconst VIEW = 100;\nconst CENTER = VIEW / 2;\n\n/** Circular gauge for a score or percentage, with an animated sweep. */\nexport function ProgressRing({\n value,\n max = 100,\n label,\n caption,\n size = 132,\n thickness = 10,\n color = ACCENT,\n title,\n className,\n}: ProgressRingProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n const radius = CENTER - thickness / 2;\n const circumference = 2 * Math.PI * radius;\n const fraction = clamp(max === 0 ? 0 : value / max, 0, 1);\n const target = circumference * (1 - fraction);\n const offset = mounted || reduced ? target : circumference;\n const ringTitle = title ?? `Progress: ${Math.round(fraction * 100)} percent`;\n\n return (\n <div\n className={cn('relative inline-flex shrink-0', className)}\n style={{ width: size, height: size, maxWidth: '100%' }}\n >\n <svg\n viewBox={`0 0 ${VIEW} ${VIEW}`}\n width={size}\n height={size}\n role=\"img\"\n aria-label={ringTitle}\n style={{ display: 'block', maxWidth: '100%', height: 'auto' }}\n >\n <title>{ringTitle}</title>\n <circle\n cx={CENTER}\n cy={CENTER}\n r={radius}\n fill=\"none\"\n stroke=\"var(--color-chart-grid)\"\n strokeWidth={thickness}\n opacity={0.7}\n />\n <circle\n cx={CENTER}\n cy={CENTER}\n r={radius}\n fill=\"none\"\n stroke={color}\n strokeWidth={thickness}\n strokeLinecap=\"round\"\n strokeDasharray={circumference}\n strokeDashoffset={offset}\n transform={`rotate(-90 ${CENTER} ${CENTER})`}\n style={{\n transition: reduced ? undefined : 'stroke-dashoffset 900ms cubic-bezier(0.22,1,0.36,1)',\n }}\n />\n </svg>\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center\">\n <span className=\"text-2xl font-semibold leading-none text-text-primary\">\n {label ?? Math.round(fraction * 100)}\n </span>\n {caption !== undefined ? (\n <span className=\"mt-1 text-xs text-text-muted\">{caption}</span>\n ) : null}\n </div>\n </div>\n );\n}\n"],"mappings":""}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { STATUS as t } from "./lib/palette.js";
|
|
3
|
+
import { clamp as n } from "./lib/geometry.js";
|
|
4
|
+
import { useState as r } from "react";
|
|
5
|
+
import { Star as i } from "lucide-react";
|
|
6
|
+
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
7
|
+
//#region src/atheletebridge/components/viz/RatingStars.tsx
|
|
8
|
+
var s = t.warning, c = "var(--color-border-strong)";
|
|
9
|
+
function l({ value: t, max: l = 5, showValue: u = !1, count: d, size: f = 16, onChange: p, showLabels: m = !1, ariaLabel: h, className: g }) {
|
|
10
|
+
let _ = n(t, 0, l), [v, y] = r(null);
|
|
11
|
+
if (p) {
|
|
12
|
+
let t = v ?? _, r = Math.round(f * .35);
|
|
13
|
+
return /* @__PURE__ */ a("div", {
|
|
14
|
+
role: "radiogroup",
|
|
15
|
+
"aria-label": h ?? `Rate out of ${l}`,
|
|
16
|
+
className: e("inline-flex items-start", g),
|
|
17
|
+
style: { gap: r },
|
|
18
|
+
onKeyDown: (e) => {
|
|
19
|
+
e.key === "ArrowRight" || e.key === "ArrowUp" ? (e.preventDefault(), p(n(Math.round(_) + 1, 1, l))) : e.key === "ArrowLeft" || e.key === "ArrowDown" ? (e.preventDefault(), p(n(Math.round(_) - 1, 1, l))) : e.key === "Home" ? (e.preventDefault(), p(1)) : e.key === "End" && (e.preventDefault(), p(l));
|
|
20
|
+
},
|
|
21
|
+
onMouseLeave: () => y(null),
|
|
22
|
+
children: Array.from({ length: l }, (n, r) => {
|
|
23
|
+
let l = r + 1, u = l <= t;
|
|
24
|
+
return /* @__PURE__ */ o("button", {
|
|
25
|
+
type: "button",
|
|
26
|
+
role: "radio",
|
|
27
|
+
"aria-checked": Math.round(_) === l,
|
|
28
|
+
"aria-label": `${l} ${l === 1 ? "star" : "stars"}`,
|
|
29
|
+
tabIndex: Math.round(_) === l || _ < 1 && r === 0 ? 0 : -1,
|
|
30
|
+
onClick: () => p(l),
|
|
31
|
+
onMouseEnter: () => y(l),
|
|
32
|
+
onFocus: () => y(l),
|
|
33
|
+
onBlur: () => y(null),
|
|
34
|
+
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)]",
|
|
35
|
+
children: [/* @__PURE__ */ a(i, {
|
|
36
|
+
width: f,
|
|
37
|
+
height: f,
|
|
38
|
+
strokeWidth: 1.5,
|
|
39
|
+
style: { color: u ? s : c },
|
|
40
|
+
fill: u ? "currentColor" : "none"
|
|
41
|
+
}), m ? /* @__PURE__ */ a("span", {
|
|
42
|
+
className: e("mt-1 text-xs tabular-nums", u ? "text-text-secondary" : "text-text-muted"),
|
|
43
|
+
children: l
|
|
44
|
+
}) : null]
|
|
45
|
+
}, l);
|
|
46
|
+
})
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
let b = _ / l * 100, x = Math.round(f * .15), S = (e) => /* @__PURE__ */ a("span", {
|
|
50
|
+
className: "flex",
|
|
51
|
+
style: { gap: x },
|
|
52
|
+
"aria-hidden": "true",
|
|
53
|
+
children: Array.from({ length: l }, (t, n) => /* @__PURE__ */ a(i, {
|
|
54
|
+
width: f,
|
|
55
|
+
height: f,
|
|
56
|
+
strokeWidth: 1.5,
|
|
57
|
+
style: { color: e ? s : c },
|
|
58
|
+
fill: e ? "currentColor" : "none"
|
|
59
|
+
}, n))
|
|
60
|
+
});
|
|
61
|
+
return /* @__PURE__ */ o("span", {
|
|
62
|
+
className: e("inline-flex items-center gap-1.5", g),
|
|
63
|
+
role: "img",
|
|
64
|
+
"aria-label": `Rated ${_} out of ${l}${d === void 0 ? "" : ` from ${d} reviews`}`,
|
|
65
|
+
children: [
|
|
66
|
+
/* @__PURE__ */ o("span", {
|
|
67
|
+
className: "relative inline-flex",
|
|
68
|
+
children: [S(!1), /* @__PURE__ */ a("span", {
|
|
69
|
+
className: "absolute left-0 top-0 overflow-hidden",
|
|
70
|
+
style: { width: `${b}%` },
|
|
71
|
+
children: S(!0)
|
|
72
|
+
})]
|
|
73
|
+
}),
|
|
74
|
+
u ? /* @__PURE__ */ a("span", {
|
|
75
|
+
className: "text-sm font-medium tabular-nums text-text-secondary",
|
|
76
|
+
children: _.toFixed(1)
|
|
77
|
+
}) : null,
|
|
78
|
+
d === void 0 ? null : /* @__PURE__ */ o("span", {
|
|
79
|
+
className: "text-xs text-text-muted",
|
|
80
|
+
children: [
|
|
81
|
+
"(",
|
|
82
|
+
d,
|
|
83
|
+
")"
|
|
84
|
+
]
|
|
85
|
+
})
|
|
86
|
+
]
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { l as RatingStars };
|
|
91
|
+
|
|
92
|
+
//# sourceMappingURL=RatingStars.js.map
|
|
@@ -0,0 +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":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ACCENT as e } from "./lib/palette.js";
|
|
2
|
+
import { useMounted as t, usePrefersReducedMotion as n, useVizId as r } from "./lib/hooks.js";
|
|
3
|
+
import { ResponsiveSvg as i } from "./lib/ResponsiveSvg.js";
|
|
4
|
+
import { buildAreaPath as a, buildLinePath as o, pointsFromValues as s } from "./lib/geometry.js";
|
|
5
|
+
import { jsx as c, jsxs as l } from "react/jsx-runtime";
|
|
6
|
+
//#region src/atheletebridge/components/viz/Sparkline.tsx
|
|
7
|
+
var u = 3;
|
|
8
|
+
function d({ data: d, variant: f = "line", height: p = 36, label: m, className: h }) {
|
|
9
|
+
let g = r("spark-grad"), _ = t(), v = n();
|
|
10
|
+
return d.length === 0 ? null : /* @__PURE__ */ c(i, {
|
|
11
|
+
height: p,
|
|
12
|
+
title: m ?? `Trend of ${d.length} points`,
|
|
13
|
+
minWidth: 96,
|
|
14
|
+
className: h,
|
|
15
|
+
children: ({ width: t }) => {
|
|
16
|
+
let n = s(d, t, p, u), r = o(n), i = a(n, p - u), m = n[n.length - 1];
|
|
17
|
+
return /* @__PURE__ */ l("g", {
|
|
18
|
+
style: { color: e },
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ c("defs", { children: /* @__PURE__ */ l("linearGradient", {
|
|
21
|
+
id: g,
|
|
22
|
+
x1: "0",
|
|
23
|
+
y1: "0",
|
|
24
|
+
x2: "0",
|
|
25
|
+
y2: "1",
|
|
26
|
+
children: [/* @__PURE__ */ c("stop", {
|
|
27
|
+
offset: "0%",
|
|
28
|
+
stopColor: "currentColor",
|
|
29
|
+
stopOpacity: .28
|
|
30
|
+
}), /* @__PURE__ */ c("stop", {
|
|
31
|
+
offset: "100%",
|
|
32
|
+
stopColor: "currentColor",
|
|
33
|
+
stopOpacity: 0
|
|
34
|
+
})]
|
|
35
|
+
}) }),
|
|
36
|
+
f === "area" ? /* @__PURE__ */ c("path", {
|
|
37
|
+
d: i,
|
|
38
|
+
fill: `url(#${g})`,
|
|
39
|
+
stroke: "none",
|
|
40
|
+
style: {
|
|
41
|
+
opacity: _ || v ? 1 : 0,
|
|
42
|
+
transition: v ? void 0 : "opacity 700ms ease"
|
|
43
|
+
}
|
|
44
|
+
}) : null,
|
|
45
|
+
/* @__PURE__ */ c("path", {
|
|
46
|
+
d: r,
|
|
47
|
+
fill: "none",
|
|
48
|
+
stroke: "currentColor",
|
|
49
|
+
strokeWidth: 2,
|
|
50
|
+
strokeLinecap: "round",
|
|
51
|
+
strokeLinejoin: "round",
|
|
52
|
+
vectorEffect: "non-scaling-stroke",
|
|
53
|
+
pathLength: 1,
|
|
54
|
+
style: {
|
|
55
|
+
strokeDasharray: 1,
|
|
56
|
+
strokeDashoffset: _ || v ? 0 : 1,
|
|
57
|
+
transition: v ? void 0 : "stroke-dashoffset 900ms ease"
|
|
58
|
+
}
|
|
59
|
+
}),
|
|
60
|
+
/* @__PURE__ */ c("circle", {
|
|
61
|
+
cx: m.x,
|
|
62
|
+
cy: m.y,
|
|
63
|
+
r: 2.6,
|
|
64
|
+
fill: "currentColor"
|
|
65
|
+
})
|
|
66
|
+
]
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
export { d as Sparkline };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=Sparkline.js.map
|
|
@@ -0,0 +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,aAAa,EACnC,IAAU,GAAY,EACtB,IAAU,GAAyB;AAQzC,QANI,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,EAAI,EACnD,IAAW,EAAc,EAAO,EAChC,IAAW,EAAc,GAAQ,IAAS,EAAI,EAC9C,IAAO,EAAO,EAAO,SAAS;AACpC,UACE,kBAAC,KAAD;IAAG,OAAO,EAAE,OAAO,GAAQ;cAA3B;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;OAAQ,CAAA,EAChE,kBAAC,QAAD;OAAM,QAAO;OAAO,WAAU;OAAe,aAAa;OAAK,CAAA,CAChD;SACZ,CAAA;KACN,MAAY,SACX,kBAAC,QAAD;MACE,GAAG;MACH,MAAM,QAAQ,EAAW;MACzB,QAAO;MACP,OAAO;OACL,SAAS,KAAW,IAAU,IAAI;OAClC,YAAY,IAAU,KAAA,IAAY;OACnC;MACD,CAAA,GACA;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;OACnC;MACD,CAAA;KACF,kBAAC,UAAD;MAAQ,IAAI,EAAK;MAAG,IAAI,EAAK;MAAG,GAAG;MAAK,MAAK;MAAiB,CAAA;KAC5D;;;EAGM,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { Sparkline as t } from "./Sparkline.js";
|
|
3
|
+
import { ArrowDownRight as n, ArrowUpRight as r } from "lucide-react";
|
|
4
|
+
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
5
|
+
//#region src/atheletebridge/components/viz/StatCard.tsx
|
|
6
|
+
function o({ label: o, value: s, delta: c, deltaSuffix: l = "%", deltaLabel: u, invertDelta: d = !1, icon: f, sparkline: p, sparklineVariant: m = "area", className: h }) {
|
|
7
|
+
let g = typeof c == "number", _ = d ? (c ?? 0) < 0 : (c ?? 0) > 0, v = (c ?? 0) === 0, y = (c ?? 0) > 0, b = v ? "text-text-muted" : _ ? "text-status-success-text" : "text-status-error-text";
|
|
8
|
+
return /* @__PURE__ */ a("div", {
|
|
9
|
+
className: e("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", h),
|
|
10
|
+
children: [
|
|
11
|
+
/* @__PURE__ */ a("div", {
|
|
12
|
+
className: "flex items-start justify-between gap-2",
|
|
13
|
+
children: [/* @__PURE__ */ i("span", {
|
|
14
|
+
className: "text-sm font-medium text-text-secondary",
|
|
15
|
+
children: o
|
|
16
|
+
}), f ? /* @__PURE__ */ i("span", {
|
|
17
|
+
className: "text-text-muted",
|
|
18
|
+
children: f
|
|
19
|
+
}) : null]
|
|
20
|
+
}),
|
|
21
|
+
/* @__PURE__ */ i("div", {
|
|
22
|
+
className: "flex items-end justify-between gap-3",
|
|
23
|
+
children: /* @__PURE__ */ i("span", {
|
|
24
|
+
className: "text-3xl font-semibold leading-none tracking-tight tabular-nums text-text-primary",
|
|
25
|
+
children: s
|
|
26
|
+
})
|
|
27
|
+
}),
|
|
28
|
+
g ? /* @__PURE__ */ a("div", {
|
|
29
|
+
className: "flex items-center gap-1.5 text-sm",
|
|
30
|
+
children: [/* @__PURE__ */ a("span", {
|
|
31
|
+
className: e("inline-flex items-center gap-0.5 font-medium tabular-nums", b),
|
|
32
|
+
children: [
|
|
33
|
+
v ? null : i(y ? r : n, {
|
|
34
|
+
"aria-hidden": "true",
|
|
35
|
+
className: "h-4 w-4"
|
|
36
|
+
}),
|
|
37
|
+
y ? "+" : "",
|
|
38
|
+
c,
|
|
39
|
+
l
|
|
40
|
+
]
|
|
41
|
+
}), u ? /* @__PURE__ */ i("span", {
|
|
42
|
+
className: "text-text-muted",
|
|
43
|
+
children: u
|
|
44
|
+
}) : null]
|
|
45
|
+
}) : null,
|
|
46
|
+
p && p.length > 1 ? /* @__PURE__ */ i("div", {
|
|
47
|
+
className: "mt-1",
|
|
48
|
+
children: /* @__PURE__ */ i(t, {
|
|
49
|
+
data: p,
|
|
50
|
+
variant: m,
|
|
51
|
+
label: `${o} trend`
|
|
52
|
+
})
|
|
53
|
+
}) : null
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { o as StatCard };
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=StatCard.js.map
|
|
@@ -0,0 +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;AAEN,QACE,kBAAC,OAAD;EACE,WAAW,EACT,6KACA,EACD;YAJH;GAME,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAA2C;KAAa,CAAA,EACvE,IAAO,kBAAC,QAAD;KAAM,WAAU;eAAmB;KAAY,CAAA,GAAG,KACtD;;GACN,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,QAAD;KAAM,WAAU;eACb;KACI,CAAA;IACH,CAAA;GACL,IACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KACE,WAAW,EAAG,6DAA6D,EAAU;eADvF;MAGG,IAAY,OACX,EADkB,IACjB,IAEA,GAFD;OAAc,eAAY;OAAO,WAAU;OAAY,CAEE;MAE1D,IAAS,MAAM;MACf;MACA;MACI;QACN,IAAa,kBAAC,QAAD;KAAM,WAAU;eAAmB;KAAkB,CAAA,GAAG,KAClE;QACJ;GACH,KAAa,EAAU,SAAS,IAC/B,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KAAW,MAAM;KAAW,SAAS;KAAkB,OAAO,GAAG,EAAM;KAAW,CAAA;IAC9E,CAAA,GACJ;GACA"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { cn as e } from "../../utils/cn.js";
|
|
2
|
+
import { ACCENT as t, STATUS as n } from "./lib/palette.js";
|
|
3
|
+
import { jsx as r, jsxs as i } from "react/jsx-runtime";
|
|
4
|
+
//#region src/atheletebridge/components/viz/Timeline.tsx
|
|
5
|
+
function a(e) {
|
|
6
|
+
switch (e) {
|
|
7
|
+
case "success":
|
|
8
|
+
case "warning":
|
|
9
|
+
case "danger":
|
|
10
|
+
case "info": return n[e];
|
|
11
|
+
case "muted": return "var(--color-text-muted)";
|
|
12
|
+
default: return t;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function o({ items: t, className: n }) {
|
|
16
|
+
return t.length === 0 ? null : /* @__PURE__ */ r("ol", {
|
|
17
|
+
className: e("flex flex-col", n),
|
|
18
|
+
children: t.map((n, o) => {
|
|
19
|
+
let s = o === t.length - 1, c = a(n.tone ?? "accent");
|
|
20
|
+
return /* @__PURE__ */ i("li", {
|
|
21
|
+
className: "flex gap-3",
|
|
22
|
+
children: [/* @__PURE__ */ i("div", {
|
|
23
|
+
className: "flex flex-col items-center",
|
|
24
|
+
children: [/* @__PURE__ */ r("span", {
|
|
25
|
+
className: "mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border-2",
|
|
26
|
+
style: {
|
|
27
|
+
borderColor: c,
|
|
28
|
+
color: c
|
|
29
|
+
},
|
|
30
|
+
children: n.icon ?? /* @__PURE__ */ r("span", {
|
|
31
|
+
"aria-hidden": "true",
|
|
32
|
+
className: "h-2 w-2 rounded-full",
|
|
33
|
+
style: { backgroundColor: c }
|
|
34
|
+
})
|
|
35
|
+
}), s ? null : /* @__PURE__ */ r("span", {
|
|
36
|
+
"aria-hidden": "true",
|
|
37
|
+
className: "w-px flex-1",
|
|
38
|
+
style: {
|
|
39
|
+
backgroundColor: "var(--color-border-subtle)",
|
|
40
|
+
minHeight: 12
|
|
41
|
+
}
|
|
42
|
+
})]
|
|
43
|
+
}), /* @__PURE__ */ i("div", {
|
|
44
|
+
className: e("flex-1", s ? "pb-0" : "pb-5"),
|
|
45
|
+
children: [/* @__PURE__ */ i("div", {
|
|
46
|
+
className: "flex flex-wrap items-baseline justify-between gap-x-3",
|
|
47
|
+
children: [/* @__PURE__ */ r("p", {
|
|
48
|
+
className: "text-sm font-medium text-text-primary",
|
|
49
|
+
children: n.title
|
|
50
|
+
}), n.time ? /* @__PURE__ */ r("span", {
|
|
51
|
+
className: "shrink-0 text-xs text-text-muted",
|
|
52
|
+
children: n.time
|
|
53
|
+
}) : null]
|
|
54
|
+
}), n.meta ? /* @__PURE__ */ r("div", {
|
|
55
|
+
className: "mt-0.5 text-sm text-text-secondary",
|
|
56
|
+
children: n.meta
|
|
57
|
+
}) : null]
|
|
58
|
+
})]
|
|
59
|
+
}, n.id ?? o);
|
|
60
|
+
})
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
export { o as Timeline };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=Timeline.js.map
|
|
@@ -0,0 +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;AAC5C,SAAQ,GAAR;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,OACH,QAAO,EAAO;EAChB,KAAK,QACH,QAAO;EACT,QACE,QAAO;;;AAKb,SAAgB,EAAS,EAAE,UAAO,gBAA4B;AAI5D,QAHI,EAAM,WAAW,IACZ,OAGP,kBAAC,MAAD;EAAI,WAAW,EAAG,iBAAiB,EAAU;YAC1C,EAAM,KAAK,GAAM,MAAM;GACtB,IAAM,IAAS,MAAM,EAAM,SAAS,GAC9B,IAAQ,EAAS,EAAK,QAAQ,SAAS;AAC7C,UACE,kBAAC,MAAD;IAAuB,WAAU;cAAjC,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MACE,WAAU;MACV,OAAO;OAAE,aAAa;OAAO;OAAO;gBAEnC,EAAK,QACJ,kBAAC,QAAD;OACE,eAAY;OACZ,WAAU;OACV,OAAO,EAAE,iBAAiB,GAAO;OACjC,CAAA;MAEC,CAAA,EACN,IAAS,OACR,kBAAC,QAAD;MACE,eAAY;MACZ,WAAU;MACV,OAAO;OAAE,iBAAiB;OAA8B,WAAW;OAAI;MACvE,CAAA,CAEA;QACN,kBAAC,OAAD;KAAK,WAAW,EAAG,UAAU,IAAS,SAAS,OAAO;eAAtD,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,KAAD;OAAG,WAAU;iBAAyC,EAAK;OAAU,CAAA,EACpE,EAAK,OACJ,kBAAC,QAAD;OAAM,WAAU;iBAAoC,EAAK;OAAY,CAAA,GACnE,KACA;SACL,EAAK,OACJ,kBAAC,OAAD;MAAK,WAAU;gBAAsC,EAAK;MAAW,CAAA,GACnE,KACA;OACH;MAjCI,EAAK,MAAM,EAiCf;IAEP;EACC,CAAA"}
|