@burdenoff/microfe-crewfoundry 2026.707.1 → 2026.707.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/crewfoundry/components/viz/BarChart.js.map +1 -1
  2. package/dist/crewfoundry/components/viz/CertificateCard.js.map +1 -1
  3. package/dist/crewfoundry/components/viz/DonutChart.js.map +1 -1
  4. package/dist/crewfoundry/components/viz/Heatmap.js.map +1 -1
  5. package/dist/crewfoundry/components/viz/LineChart.js.map +1 -1
  6. package/dist/crewfoundry/components/viz/PathSteps.js.map +1 -1
  7. package/dist/crewfoundry/components/viz/RatingStars.js.map +1 -1
  8. package/dist/crewfoundry/components/viz/SegmentedTabs.js.map +1 -1
  9. package/dist/crewfoundry/components/viz/StatCard.js.map +1 -1
  10. package/dist/crewfoundry/components/viz/lib/Avatar.js.map +1 -1
  11. package/dist/crewfoundry/components/viz/lib/CartesianFrame.js.map +1 -1
  12. package/dist/crewfoundry/components/viz/lib/geometry.js.map +1 -1
  13. package/dist/crewfoundry/lib/analytics.js.map +1 -1
  14. package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  15. package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js.map +1 -1
  16. package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
  17. package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
  18. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js +2 -1
  19. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +1 -1
  20. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  21. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
  22. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
  23. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
  24. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
  25. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
  26. package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
  27. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js +8 -4
  28. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
  29. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
  30. package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
  31. package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
  32. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
  33. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
  34. package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
  35. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
  36. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
  37. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
  38. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
  39. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +1 -1
  40. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
  41. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -1
  42. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
  43. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
  44. package/dist/crewfoundry/utils/courseProgress.js.map +1 -1
  45. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"BarChart.js","names":[],"sources":["../../../../src/crewfoundry/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 ? undefined : '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;CAAG;AAGtD,SAAgB,EAAS,EACvB,WACA,YAAS,KACT,oBAAiB,GACjB,cAAW,IACX,gBAAa,IACb,UACA,gBACgB;CAChB,IAAM,IAAU,GAAY,EACtB,IAAU,GAAyB;AAEzC,KAAI,EAAO,WAAW,EACpB,QAAO;CAGT,IAAM,IAAS,EAAO,KAAK,MAAM,EAAE,MAAM,EACnC,IAAM,KAAK,IAAI,GAAG,GAAQ,EAAE,IAAI,GAChC,IAAU,EAAa,EAAO,OAAO;AAG3C,QACE,kBAAC,GAAD;EACU;EACR,OALe,KAAS,gBAAgB,EAAO,OAAO;EAM3C;EACX,MAAM,EAAO,KAAK,MAAM,GAAG,EAAE,MAAM,IAAI,EAAe,EAAE,MAAM,GAAG,CAAC,KAAK,KAAK;aAE1E,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,IAAK;AAC1C,UACE,kBAAC,KAAD,EAAA,UAAA,CACE,kBAAC,QAAD;IACE,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,QAAO;IACP,aAAa;IACb,cAAa;IACb,SAAS;IACT,CAAA,EACD,EAAO,KAAK,GAAO,MAAM;IACxB,IAAM,IAAO,EAAY,EAAM,OAAO,GAAG,GAAK,GAAG,EAAM,EACjD,IAAK,IAAW,IAAO,IAAI,IAAO,GAClC,IAAI,IAAK,IAAW,GACpB,IAAI,IAAa,GACjB,IAAQ,EAAM,UAAU,IAAW,EAAQ,KAAA;AACjD,WACE,kBAAC,KAAD,EAAA,UAAA;KACE,kBAAC,SAAD,EAAA,UAAQ,GAAG,EAAM,MAAM,IAAI,EAAe,EAAM,MAAM,IAAW,CAAA;KACjE,kBAAC,QAAD;MACK;MACA;MACH,OAAO;MACP,QAAQ,KAAK,IAAI,GAAG,EAAK;MACzB,IAAI;MACJ,MAAM;MACN,OAAO;OACL,cAAc;OACd,iBAAiB;OACjB,WAAW,KAAW,IAAU,cAAc;OAC9C,YAAY,IAAU,KAAA,IAAY;OAClC,iBAAiB,IAAU,KAAA,IAAY,GAAG,IAAI,GAAG;OAClD;MACD,CAAA;KACD,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;OACxD;gBAEA,EAAe,EAAM,MAAM;MACvB,CAAA;KAET,kBAAC,QAAD;MACE,GAAG;MACH,GAAG,IAAS;MACZ,YAAW;MACX,UAAU;MACV,MAAK;gBAEJ,EAAM;MACF,CAAA;KACL,EAAA,EA3CI,GAAG,EAAM,MAAM,GAAG,IA2CtB;KAEN,CACA,EAAA,CAAA;;EAGM,CAAA"}
1
+ {"version":3,"file":"BarChart.js","names":[],"sources":["../../../../src/crewfoundry/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;CAAG;AAGtD,SAAgB,EAAS,EACvB,WACA,YAAS,KACT,oBAAiB,GACjB,cAAW,IACX,gBAAa,IACb,UACA,gBACgB;CAChB,IAAM,IAAU,GAAY,EACtB,IAAU,GAAyB;AAEzC,KAAI,EAAO,WAAW,EACpB,QAAO;CAGT,IAAM,IAAS,EAAO,KAAK,MAAM,EAAE,MAAM,EACnC,IAAM,KAAK,IAAI,GAAG,GAAQ,EAAE,IAAI,GAChC,IAAU,EAAa,EAAO,OAAO;AAG3C,QACE,kBAAC,GAAD;EACU;EACR,OALe,KAAS,gBAAgB,EAAO,OAAO;EAM3C;EACX,MAAM,EAAO,KAAK,MAAM,GAAG,EAAE,MAAM,IAAI,EAAe,EAAE,MAAM,GAAG,CAAC,KAAK,KAAK;aAE1E,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,IAAK;AAC1C,UACE,kBAAC,KAAD,EAAA,UAAA,CACE,kBAAC,QAAD;IACE,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,QAAO;IACP,aAAa;IACb,cAAa;IACb,SAAS;IACT,CAAA,EACD,EAAO,KAAK,GAAO,MAAM;IACxB,IAAM,IAAO,EAAY,EAAM,OAAO,GAAG,GAAK,GAAG,EAAM,EACjD,IAAK,IAAW,IAAO,IAAI,IAAO,GAClC,IAAI,IAAK,IAAW,GACpB,IAAI,IAAa,GACjB,IAAQ,EAAM,UAAU,IAAW,EAAQ,KAAA;AACjD,WACE,kBAAC,KAAD,EAAA,UAAA;KACE,kBAAC,SAAD,EAAA,UAAQ,GAAG,EAAM,MAAM,IAAI,EAAe,EAAM,MAAM,IAAW,CAAA;KACjE,kBAAC,QAAD;MACK;MACA;MACH,OAAO;MACP,QAAQ,KAAK,IAAI,GAAG,EAAK;MACzB,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;OAClD;MACD,CAAA;KACD,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;OACxD;gBAEA,EAAe,EAAM,MAAM;MACvB,CAAA;KAET,kBAAC,QAAD;MACE,GAAG;MACH,GAAG,IAAS;MACZ,YAAW;MACX,UAAU;MACV,MAAK;gBAEJ,EAAM;MACF,CAAA;KACL,EAAA,EA7CI,GAAG,EAAM,MAAM,GAAG,IA6CtB;KAEN,CACA,EAAA,CAAA;;EAGM,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CertificateCard.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/CertificateCard.tsx"],"sourcesContent":["import { Download } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\nimport { starPath } from './lib/geometry';\n\nexport interface CertificateCardProps {\n title: string;\n recipient: string;\n date: string;\n issuer?: string;\n credentialId?: string;\n /** When provided, renders a download button wired to this handler. */\n onDownload?: () => void;\n downloadLabel?: string;\n className?: string;\n}\n\n/** Decorative award seal (hand-drawn SVG, tokenized via currentColor). */\nfunction Seal() {\n return (\n <svg\n viewBox=\"0 0 48 60\"\n width={48}\n height={60}\n aria-hidden=\"true\"\n style={{ color: ACCENT, display: 'block' }}\n >\n <path d=\"M18 38 L18 58 L24 52 L30 58 L30 38 Z\" fill=\"currentColor\" opacity={0.85} />\n <circle cx=\"24\" cy=\"22\" r=\"19\" fill=\"currentColor\" opacity={0.1} />\n <circle cx=\"24\" cy=\"22\" r=\"19\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} />\n <circle cx=\"24\" cy=\"22\" r=\"14\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1} opacity={0.5} />\n <path d={starPath(24, 22, 9, 3.8)} fill=\"currentColor\" />\n </svg>\n );\n}\n\n/** Landscape certificate tile with seal, recipient, date and download affordance. */\nexport function CertificateCard({\n title,\n recipient,\n date,\n issuer,\n credentialId,\n onDownload,\n downloadLabel = 'Download',\n className,\n}: CertificateCardProps) {\n return (\n <div\n className={cn(\n 'relative flex flex-col gap-4 rounded-[var(--radius-card)] border-2 border-border-default bg-bg-surface p-5 shadow-elevation-1',\n 'before:pointer-events-none before:absolute before:inset-1.5 before:rounded-[calc(var(--radius-card)-4px)] before:border before:border-border-subtle before:content-[\"\"]',\n className,\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-muted\">\n Certificate of Completion\n </p>\n <h3 className=\"mt-1 truncate text-lg font-semibold text-text-primary\" title={title}>\n {title}\n </h3>\n </div>\n <Seal />\n </div>\n\n <div className=\"min-w-0\">\n <p className=\"text-xs text-text-muted\">Awarded to</p>\n <p className=\"truncate text-base font-medium text-text-primary\" title={recipient}>\n {recipient}\n </p>\n </div>\n\n <div className=\"flex flex-wrap items-end justify-between gap-3 border-t border-border-subtle pt-3\">\n <div className=\"flex flex-col gap-0.5 text-xs text-text-muted\">\n <span>\n Issued <span className=\"text-text-secondary\">{date}</span>\n {issuer ? (\n <>\n {' · '}\n <span className=\"text-text-secondary\">{issuer}</span>\n </>\n ) : null}\n </span>\n {credentialId ? <span className=\"tabular-nums\">ID: {credentialId}</span> : null}\n </div>\n {onDownload ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onDownload}>\n <Download aria-hidden=\"true\" className=\"h-4 w-4\" />\n {downloadLabel}\n </Button>\n ) : null}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAmBA,SAAS,IAAO;AACd,QACE,kBAAC,OAAD;EACE,SAAQ;EACR,OAAO;EACP,QAAQ;EACR,eAAY;EACZ,OAAO;GAAE,OAAO;GAAQ,SAAS;GAAS;YAL5C;GAOE,kBAAC,QAAD;IAAM,GAAE;IAAuC,MAAK;IAAe,SAAS;IAAQ,CAAA;GACpF,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAe,SAAS;IAAO,CAAA;GACnE,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAO,QAAO;IAAe,aAAa;IAAK,CAAA;GACnF,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAO,QAAO;IAAe,aAAa;IAAG,SAAS;IAAO,CAAA;GACjG,kBAAC,QAAD;IAAM,GAAG,EAAS,IAAI,IAAI,GAAG,IAAI;IAAE,MAAK;IAAiB,CAAA;GACrD;;;AAKV,SAAgB,EAAgB,EAC9B,UACA,cACA,SACA,WACA,iBACA,eACA,mBAAgB,YAChB,gBACuB;AACvB,QACE,kBAAC,OAAD;EACE,WAAW,EACT,iIACA,6KACA,EACD;YALH;GAOE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAA8D;MAEvE,CAAA,EACJ,kBAAC,MAAD;MAAI,WAAU;MAA+D;gBAC1E;MACE,CAAA,CACD;QACN,kBAAC,GAAD,EAAQ,CAAA,CACJ;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAA0B;KAAc,CAAA,EACrD,kBAAC,KAAD;KAAG,WAAU;KAAmD,OAAO;eACpE;KACC,CAAA,CACA;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD,EAAA,UAAA;MAAM;MACG,kBAAC,QAAD;OAAM,WAAU;iBAAuB;OAAY,CAAA;MACzD,IACC,kBAAA,GAAA,EAAA,UAAA,CACG,OACD,kBAAC,QAAD;OAAM,WAAU;iBAAuB;OAAc,CAAA,CACpD,EAAA,CAAA,GACD;MACC,EAAA,CAAA,EACN,IAAe,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CAA+B,QAAK,EAAoB;UAAG,KACvE;QACL,IACC,kBAAC,GAAD;KAAQ,SAAQ;KAAU,MAAK;KAAK,SAAS;eAA7C,CACE,kBAAC,GAAD;MAAU,eAAY;MAAO,WAAU;MAAY,CAAA,EAClD,EACM;SACP,KACA;;GACF"}
1
+ {"version":3,"file":"CertificateCard.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/CertificateCard.tsx"],"sourcesContent":["import { Download } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\nimport { starPath } from './lib/geometry';\n\nexport interface CertificateCardProps {\n title: string;\n recipient: string;\n date: string;\n issuer?: string;\n credentialId?: string;\n /** When provided, renders a download button wired to this handler. */\n onDownload?: () => void;\n downloadLabel?: string;\n className?: string;\n}\n\n/** Decorative award seal (hand-drawn SVG, tokenized via currentColor). */\nfunction Seal() {\n return (\n <svg\n viewBox=\"0 0 48 60\"\n width={48}\n height={60}\n aria-hidden=\"true\"\n style={{ color: ACCENT, display: 'block' }}\n >\n <path d=\"M18 38 L18 58 L24 52 L30 58 L30 38 Z\" fill=\"currentColor\" opacity={0.85} />\n <circle cx=\"24\" cy=\"22\" r=\"19\" fill=\"currentColor\" opacity={0.1} />\n <circle cx=\"24\" cy=\"22\" r=\"19\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} />\n <circle\n cx=\"24\"\n cy=\"22\"\n r=\"14\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1}\n opacity={0.5}\n />\n <path d={starPath(24, 22, 9, 3.8)} fill=\"currentColor\" />\n </svg>\n );\n}\n\n/** Landscape certificate tile with seal, recipient, date and download affordance. */\nexport function CertificateCard({\n title,\n recipient,\n date,\n issuer,\n credentialId,\n onDownload,\n downloadLabel = 'Download',\n className,\n}: CertificateCardProps) {\n return (\n <div\n className={cn(\n 'relative flex flex-col gap-4 rounded-[var(--radius-card)] border-2 border-border-default bg-bg-surface p-5 shadow-elevation-1',\n 'before:pointer-events-none before:absolute before:inset-1.5 before:rounded-[calc(var(--radius-card)-4px)] before:border before:border-border-subtle before:content-[\"\"]',\n className\n )}\n >\n <div className=\"flex items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-xs font-medium uppercase tracking-wide text-text-muted\">\n Certificate of Completion\n </p>\n <h3 className=\"mt-1 truncate text-lg font-semibold text-text-primary\" title={title}>\n {title}\n </h3>\n </div>\n <Seal />\n </div>\n\n <div className=\"min-w-0\">\n <p className=\"text-xs text-text-muted\">Awarded to</p>\n <p className=\"truncate text-base font-medium text-text-primary\" title={recipient}>\n {recipient}\n </p>\n </div>\n\n <div className=\"flex flex-wrap items-end justify-between gap-3 border-t border-border-subtle pt-3\">\n <div className=\"flex flex-col gap-0.5 text-xs text-text-muted\">\n <span>\n Issued <span className=\"text-text-secondary\">{date}</span>\n {issuer ? (\n <>\n {' · '}\n <span className=\"text-text-secondary\">{issuer}</span>\n </>\n ) : null}\n </span>\n {credentialId ? <span className=\"tabular-nums\">ID: {credentialId}</span> : null}\n </div>\n {onDownload ? (\n <Button variant=\"outline\" size=\"sm\" onClick={onDownload}>\n <Download aria-hidden=\"true\" className=\"h-4 w-4\" />\n {downloadLabel}\n </Button>\n ) : null}\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;;AAmBA,SAAS,IAAO;AACd,QACE,kBAAC,OAAD;EACE,SAAQ;EACR,OAAO;EACP,QAAQ;EACR,eAAY;EACZ,OAAO;GAAE,OAAO;GAAQ,SAAS;GAAS;YAL5C;GAOE,kBAAC,QAAD;IAAM,GAAE;IAAuC,MAAK;IAAe,SAAS;IAAQ,CAAA;GACpF,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAe,SAAS;IAAO,CAAA;GACnE,kBAAC,UAAD;IAAQ,IAAG;IAAK,IAAG;IAAK,GAAE;IAAK,MAAK;IAAO,QAAO;IAAe,aAAa;IAAK,CAAA;GACnF,kBAAC,UAAD;IACE,IAAG;IACH,IAAG;IACH,GAAE;IACF,MAAK;IACL,QAAO;IACP,aAAa;IACb,SAAS;IACT,CAAA;GACF,kBAAC,QAAD;IAAM,GAAG,EAAS,IAAI,IAAI,GAAG,IAAI;IAAE,MAAK;IAAiB,CAAA;GACrD;;;AAKV,SAAgB,EAAgB,EAC9B,UACA,cACA,SACA,WACA,iBACA,eACA,mBAAgB,YAChB,gBACuB;AACvB,QACE,kBAAC,OAAD;EACE,WAAW,EACT,iIACA,6KACA,EACD;YALH;GAOE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBAA8D;MAEvE,CAAA,EACJ,kBAAC,MAAD;MAAI,WAAU;MAA+D;gBAC1E;MACE,CAAA,CACD;QACN,kBAAC,GAAD,EAAQ,CAAA,CACJ;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAA0B;KAAc,CAAA,EACrD,kBAAC,KAAD;KAAG,WAAU;KAAmD,OAAO;eACpE;KACC,CAAA,CACA;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD,EAAA,UAAA;MAAM;MACG,kBAAC,QAAD;OAAM,WAAU;iBAAuB;OAAY,CAAA;MACzD,IACC,kBAAA,GAAA,EAAA,UAAA,CACG,OACD,kBAAC,QAAD;OAAM,WAAU;iBAAuB;OAAc,CAAA,CACpD,EAAA,CAAA,GACD;MACC,EAAA,CAAA,EACN,IAAe,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CAA+B,QAAK,EAAoB;UAAG,KACvE;QACL,IACC,kBAAC,GAAD;KAAQ,SAAQ;KAAU,MAAK;KAAK,SAAS;eAA7C,CACE,kBAAC,GAAD;MAAU,eAAY;MAAO,WAAU;MAAY,CAAA,EAClD,EACM;SACP,KACA;;GACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"DonutChart.js","names":[],"sources":["../../../../src/crewfoundry/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(CENTER, CENTER, R_OUTER, rInner, arc.startAngle, arc.endAngle)}\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
+ {"version":3,"file":"DonutChart.js","names":[],"sources":["../../../../src/crewfoundry/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":"Heatmap.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/Heatmap.tsx"],"sourcesContent":["import { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\nimport { clamp } from './lib/geometry';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\n\nexport interface HeatmapCell {\n value: number;\n /** Per-cell max; otherwise the grid-wide max is used. */\n max?: number;\n}\n\nexport interface HeatmapRow {\n label: string;\n cells: HeatmapCell[];\n}\n\nexport interface HeatmapProps {\n rows: HeatmapRow[];\n colLabels?: string[];\n /** Show the intensity legend below the grid. */\n legend?: boolean;\n /** Show the numeric value inside each cell. */\n showValues?: boolean;\n valueFormatter?: (value: number) => string;\n title?: string;\n className?: string;\n}\n\n/** Opacity floor so a non-zero cell is always faintly visible. */\nfunction intensity(value: number, max: number): number {\n if (value <= 0 || max <= 0) {\n return 0;\n }\n return clamp(0.14 + 0.86 * (value / max), 0, 1);\n}\n\n/**\n * Row × column grid with accent-shaded cells. Rendered as a real table (row +\n * column headers) and horizontally scrollable so wide grids never push the page.\n */\nexport function Heatmap({\n rows,\n colLabels,\n legend = true,\n showValues = false,\n valueFormatter = (v) => String(v),\n title,\n className,\n}: HeatmapProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (rows.length === 0) {\n return null;\n }\n\n const gridMax = Math.max(1, ...rows.flatMap((r) => r.cells.map((c) => c.value)));\n const caption = title ?? 'Heatmap';\n const legendStops = [0.14, 0.32, 0.5, 0.68, 0.86, 1];\n\n return (\n <div className={cn('flex flex-col gap-3', className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"border-separate border-spacing-1\" style={{ minWidth: 'min-content' }}>\n <caption className=\"sr-only\">{caption}</caption>\n {colLabels ? (\n <thead>\n <tr>\n <th className=\"w-px\" />\n {colLabels.map((label, i) => (\n <th\n key={`col-${i}`}\n scope=\"col\"\n className=\"px-1 pb-1 text-center text-xs font-medium text-text-muted\"\n >\n {label}\n </th>\n ))}\n </tr>\n </thead>\n ) : null}\n <tbody>\n {rows.map((row, r) => (\n <tr key={`row-${r}`}>\n <th\n scope=\"row\"\n className=\"whitespace-nowrap pr-2 text-right text-xs font-medium text-text-secondary\"\n >\n {row.label}\n </th>\n {row.cells.map((cell, c) => {\n const cellMax = cell.max ?? gridMax;\n const alpha = intensity(cell.value, cellMax);\n const delay = reduced ? 0 : (r + c) * 22;\n return (\n <td key={`cell-${r}-${c}`} className=\"p-0\">\n <div\n className=\"relative flex h-9 w-9 items-center justify-center rounded-[var(--radius-input)] border border-border-subtle text-[11px] font-medium tabular-nums\"\n title={`${row.label}${colLabels?.[c] ? ` · ${colLabels[c]}` : ''}: ${valueFormatter(\n cell.value,\n )}`}\n >\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-0 rounded-[var(--radius-input)]\"\n style={{\n backgroundColor: ACCENT,\n opacity: mounted || reduced ? alpha : 0,\n transition: reduced ? undefined : 'opacity 420ms ease',\n transitionDelay: reduced ? undefined : `${delay}ms`,\n }}\n />\n {showValues && cell.value > 0 ? (\n <span\n className=\"relative\"\n style={{ color: alpha > 0.55 ? 'var(--color-text-inverse)' : 'var(--color-text-secondary)' }}\n >\n {valueFormatter(cell.value)}\n </span>\n ) : null}\n </div>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {legend ? (\n <div className=\"flex items-center gap-2 text-xs text-text-muted\">\n <span>Less</span>\n <span className=\"flex gap-1\">\n {legendStops.map((stop, i) => (\n <span\n key={i}\n aria-hidden=\"true\"\n className=\"h-3 w-3 rounded-[3px] border border-border-subtle\"\n style={{ backgroundColor: ACCENT, opacity: stop }}\n />\n ))}\n </span>\n <span>More</span>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;AA6BA,SAAS,EAAU,GAAe,GAAqB;AAIrD,QAHI,KAAS,KAAK,KAAO,IAChB,IAEF,EAAM,MAAe,IAAQ,IAAhB,KAAsB,GAAG,EAAE;;AAOjD,SAAgB,EAAQ,EACtB,SACA,cACA,YAAS,IACT,gBAAa,IACb,qBAAkB,MAAM,OAAO,EAAE,EACjC,UACA,gBACe;CACf,IAAM,IAAU,GAAY,EACtB,IAAU,GAAyB;AAEzC,KAAI,EAAK,WAAW,EAClB,QAAO;CAGT,IAAM,IAAU,KAAK,IAAI,GAAG,GAAG,EAAK,SAAS,MAAM,EAAE,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,EAC1E,IAAU,KAAS;AAGzB,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,uBAAuB,EAAU;YAApD,CACE,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,SAAD;IAAO,WAAU;IAAmC,OAAO,EAAE,UAAU,eAAe;cAAtF;KACE,kBAAC,WAAD;MAAS,WAAU;gBAAW;MAAkB,CAAA;KAC/C,IACC,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD,EAAA,UAAA,CACE,kBAAC,MAAD,EAAI,WAAU,QAAS,CAAA,EACtB,EAAU,KAAK,GAAO,MACrB,kBAAC,MAAD;MAEE,OAAM;MACN,WAAU;gBAET;MACE,EALE,OAAO,IAKT,CACL,CACC,EAAA,CAAA,EACC,CAAA,GACN;KACJ,kBAAC,SAAD,EAAA,UACG,EAAK,KAAK,GAAK,MACd,kBAAC,MAAD,EAAA,UAAA,CACE,kBAAC,MAAD;MACE,OAAM;MACN,WAAU;gBAET,EAAI;MACF,CAAA,EACJ,EAAI,MAAM,KAAK,GAAM,MAAM;MAC1B,IAAM,IAAU,EAAK,OAAO,GACtB,IAAQ,EAAU,EAAK,OAAO,EAAQ,EACtC,IAAQ,IAAU,KAAK,IAAI,KAAK;AACtC,aACE,kBAAC,MAAD;OAA2B,WAAU;iBACnC,kBAAC,OAAD;QACE,WAAU;QACV,OAAO,GAAG,EAAI,QAAQ,IAAY,KAAK,MAAM,EAAU,OAAO,GAAG,IAAI,EACnE,EAAK,MACN;kBAJH,CAME,kBAAC,QAAD;SACE,eAAY;SACZ,WAAU;SACV,OAAO;UACL,iBAAiB;UACjB,SAAS,KAAW,IAAU,IAAQ;UACtC,YAAY,IAAU,KAAA,IAAY;UAClC,iBAAiB,IAAU,KAAA,IAAY,GAAG,EAAM;UACjD;SACD,CAAA,EACD,KAAc,EAAK,QAAQ,IAC1B,kBAAC,QAAD;SACE,WAAU;SACV,OAAO,EAAE,OAAO,IAAQ,MAAO,8BAA8B,+BAA+B;mBAE3F,EAAe,EAAK,MAAM;SACtB,CAAA,GACL,KACA;;OACH,EA1BI,QAAQ,EAAE,GAAG,IA0BjB;OAEP,CACC,EAAA,EAzCI,OAAO,IAyCX,CACL,EACI,CAAA;KACF;;GACJ,CAAA,EACL,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,QAAD,EAAA,UAAM,QAAW,CAAA;IACjB,kBAAC,QAAD;KAAM,WAAU;eA1EJ;MAAC;MAAM;MAAM;MAAK;MAAM;MAAM;MAAE,CA2E7B,KAAK,GAAM,MACtB,kBAAC,QAAD;MAEE,eAAY;MACZ,WAAU;MACV,OAAO;OAAE,iBAAiB;OAAQ,SAAS;OAAM;MACjD,EAJK,EAIL,CACF;KACG,CAAA;IACP,kBAAC,QAAD,EAAA,UAAM,QAAW,CAAA;IACb;OACJ,KACA"}
1
+ {"version":3,"file":"Heatmap.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/Heatmap.tsx"],"sourcesContent":["import { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\nimport { clamp } from './lib/geometry';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\n\nexport interface HeatmapCell {\n value: number;\n /** Per-cell max; otherwise the grid-wide max is used. */\n max?: number;\n}\n\nexport interface HeatmapRow {\n label: string;\n cells: HeatmapCell[];\n}\n\nexport interface HeatmapProps {\n rows: HeatmapRow[];\n colLabels?: string[];\n /** Show the intensity legend below the grid. */\n legend?: boolean;\n /** Show the numeric value inside each cell. */\n showValues?: boolean;\n valueFormatter?: (value: number) => string;\n title?: string;\n className?: string;\n}\n\n/** Opacity floor so a non-zero cell is always faintly visible. */\nfunction intensity(value: number, max: number): number {\n if (value <= 0 || max <= 0) {\n return 0;\n }\n return clamp(0.14 + 0.86 * (value / max), 0, 1);\n}\n\n/**\n * Row × column grid with accent-shaded cells. Rendered as a real table (row +\n * column headers) and horizontally scrollable so wide grids never push the page.\n */\nexport function Heatmap({\n rows,\n colLabels,\n legend = true,\n showValues = false,\n valueFormatter = (v) => String(v),\n title,\n className,\n}: HeatmapProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (rows.length === 0) {\n return null;\n }\n\n const gridMax = Math.max(1, ...rows.flatMap((r) => r.cells.map((c) => c.value)));\n const caption = title ?? 'Heatmap';\n const legendStops = [0.14, 0.32, 0.5, 0.68, 0.86, 1];\n\n return (\n <div className={cn('flex flex-col gap-3', className)}>\n <div className=\"overflow-x-auto\">\n <table className=\"border-separate border-spacing-1\" style={{ minWidth: 'min-content' }}>\n <caption className=\"sr-only\">{caption}</caption>\n {colLabels ? (\n <thead>\n <tr>\n <th className=\"w-px\" />\n {colLabels.map((label, i) => (\n <th\n key={`col-${i}`}\n scope=\"col\"\n className=\"px-1 pb-1 text-center text-xs font-medium text-text-muted\"\n >\n {label}\n </th>\n ))}\n </tr>\n </thead>\n ) : null}\n <tbody>\n {rows.map((row, r) => (\n <tr key={`row-${r}`}>\n <th\n scope=\"row\"\n className=\"whitespace-nowrap pr-2 text-right text-xs font-medium text-text-secondary\"\n >\n {row.label}\n </th>\n {row.cells.map((cell, c) => {\n const cellMax = cell.max ?? gridMax;\n const alpha = intensity(cell.value, cellMax);\n const delay = reduced ? 0 : (r + c) * 22;\n return (\n <td key={`cell-${r}-${c}`} className=\"p-0\">\n <div\n className=\"relative flex h-9 w-9 items-center justify-center rounded-[var(--radius-input)] border border-border-subtle text-[11px] font-medium tabular-nums\"\n title={`${row.label}${colLabels?.[c] ? ` · ${colLabels[c]}` : ''}: ${valueFormatter(\n cell.value\n )}`}\n >\n <span\n aria-hidden=\"true\"\n className=\"absolute inset-0 rounded-[var(--radius-input)]\"\n style={{\n backgroundColor: ACCENT,\n opacity: mounted || reduced ? alpha : 0,\n transition: reduced ? undefined : 'opacity 420ms ease',\n transitionDelay: reduced ? undefined : `${delay}ms`,\n }}\n />\n {showValues && cell.value > 0 ? (\n <span\n className=\"relative\"\n style={{\n color:\n alpha > 0.55\n ? 'var(--color-text-inverse)'\n : 'var(--color-text-secondary)',\n }}\n >\n {valueFormatter(cell.value)}\n </span>\n ) : null}\n </div>\n </td>\n );\n })}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n {legend ? (\n <div className=\"flex items-center gap-2 text-xs text-text-muted\">\n <span>Less</span>\n <span className=\"flex gap-1\">\n {legendStops.map((stop, i) => (\n <span\n key={i}\n aria-hidden=\"true\"\n className=\"h-3 w-3 rounded-[3px] border border-border-subtle\"\n style={{ backgroundColor: ACCENT, opacity: stop }}\n />\n ))}\n </span>\n <span>More</span>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;;;AA6BA,SAAS,EAAU,GAAe,GAAqB;AAIrD,QAHI,KAAS,KAAK,KAAO,IAChB,IAEF,EAAM,MAAe,IAAQ,IAAhB,KAAsB,GAAG,EAAE;;AAOjD,SAAgB,EAAQ,EACtB,SACA,cACA,YAAS,IACT,gBAAa,IACb,qBAAkB,MAAM,OAAO,EAAE,EACjC,UACA,gBACe;CACf,IAAM,IAAU,GAAY,EACtB,IAAU,GAAyB;AAEzC,KAAI,EAAK,WAAW,EAClB,QAAO;CAGT,IAAM,IAAU,KAAK,IAAI,GAAG,GAAG,EAAK,SAAS,MAAM,EAAE,MAAM,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,EAC1E,IAAU,KAAS;AAGzB,QACE,kBAAC,OAAD;EAAK,WAAW,EAAG,uBAAuB,EAAU;YAApD,CACE,kBAAC,OAAD;GAAK,WAAU;aACb,kBAAC,SAAD;IAAO,WAAU;IAAmC,OAAO,EAAE,UAAU,eAAe;cAAtF;KACE,kBAAC,WAAD;MAAS,WAAU;gBAAW;MAAkB,CAAA;KAC/C,IACC,kBAAC,SAAD,EAAA,UACE,kBAAC,MAAD,EAAA,UAAA,CACE,kBAAC,MAAD,EAAI,WAAU,QAAS,CAAA,EACtB,EAAU,KAAK,GAAO,MACrB,kBAAC,MAAD;MAEE,OAAM;MACN,WAAU;gBAET;MACE,EALE,OAAO,IAKT,CACL,CACC,EAAA,CAAA,EACC,CAAA,GACN;KACJ,kBAAC,SAAD,EAAA,UACG,EAAK,KAAK,GAAK,MACd,kBAAC,MAAD,EAAA,UAAA,CACE,kBAAC,MAAD;MACE,OAAM;MACN,WAAU;gBAET,EAAI;MACF,CAAA,EACJ,EAAI,MAAM,KAAK,GAAM,MAAM;MAC1B,IAAM,IAAU,EAAK,OAAO,GACtB,IAAQ,EAAU,EAAK,OAAO,EAAQ,EACtC,IAAQ,IAAU,KAAK,IAAI,KAAK;AACtC,aACE,kBAAC,MAAD;OAA2B,WAAU;iBACnC,kBAAC,OAAD;QACE,WAAU;QACV,OAAO,GAAG,EAAI,QAAQ,IAAY,KAAK,MAAM,EAAU,OAAO,GAAG,IAAI,EACnE,EAAK,MACN;kBAJH,CAME,kBAAC,QAAD;SACE,eAAY;SACZ,WAAU;SACV,OAAO;UACL,iBAAiB;UACjB,SAAS,KAAW,IAAU,IAAQ;UACtC,YAAY,IAAU,KAAA,IAAY;UAClC,iBAAiB,IAAU,KAAA,IAAY,GAAG,EAAM;UACjD;SACD,CAAA,EACD,KAAc,EAAK,QAAQ,IAC1B,kBAAC,QAAD;SACE,WAAU;SACV,OAAO,EACL,OACE,IAAQ,MACJ,8BACA,+BACP;mBAEA,EAAe,EAAK,MAAM;SACtB,CAAA,GACL,KACA;;OACH,EA/BI,QAAQ,EAAE,GAAG,IA+BjB;OAEP,CACC,EAAA,EA9CI,OAAO,IA8CX,CACL,EACI,CAAA;KACF;;GACJ,CAAA,EACL,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,QAAD,EAAA,UAAM,QAAW,CAAA;IACjB,kBAAC,QAAD;KAAM,WAAU;eA/EJ;MAAC;MAAM;MAAM;MAAK;MAAM;MAAM;MAAE,CAgF7B,KAAK,GAAM,MACtB,kBAAC,QAAD;MAEE,eAAY;MACZ,WAAU;MACV,OAAO;OAAE,iBAAiB;OAAQ,SAAS;OAAM;MACjD,EAJK,EAIL,CACF;KACG,CAAA;IACP,kBAAC,QAAD,EAAA,UAAM,QAAW,CAAA;IACb;OACJ,KACA"}
@@ -1 +1 @@
1
- {"version":3,"file":"LineChart.js","names":[],"sources":["../../../../src/crewfoundry/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 = 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
+ {"version":3,"file":"LineChart.js","names":[],"sources":["../../../../src/crewfoundry/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":"PathSteps.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/PathSteps.tsx"],"sourcesContent":["import { Check, Lock } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\n\nexport type PathStepState = 'done' | 'active' | 'todo' | 'locked';\n\nexport interface PathStep {\n label: string;\n meta?: string;\n state?: PathStepState;\n}\n\nexport interface PathStepsProps {\n steps: PathStep[];\n className?: string;\n}\n\n/**\n * A sequence of course nodes (circles + labels + connectors) for a learning\n * path. Each node carries its own state; the connector into a node is filled\n * once that node is complete. Scrolls horizontally on narrow screens.\n */\nexport function PathSteps({ steps, className }: PathStepsProps) {\n if (steps.length === 0) {\n return null;\n }\n return (\n <ol className={cn('flex items-start overflow-x-auto pb-1', className)} aria-label=\"Learning path\">\n {steps.map((step, i) => {\n const state = step.state ?? 'todo';\n const connectorFilled = state === 'done';\n return (\n <li\n key={`${step.label}-${i}`}\n className=\"flex min-w-[5.5rem] flex-1 flex-col items-center\"\n aria-current={state === 'active' ? 'step' : undefined}\n >\n <div className=\"flex w-full items-center\">\n {i > 0 ? (\n <span\n aria-hidden=\"true\"\n className=\"h-0.5 flex-1 rounded-full\"\n style={{ backgroundColor: connectorFilled ? ACCENT : 'var(--color-border-subtle)' }}\n />\n ) : (\n <span className=\"flex-1\" aria-hidden=\"true\" />\n )}\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-full border-2 text-xs font-semibold transition-colors',\n state === 'done' && 'border-transparent text-text-inverse',\n state === 'active' && 'bg-bg-surface shadow-elevation-1',\n state === 'todo' && 'border-border-default bg-bg-surface text-text-secondary',\n state === 'locked' && 'border-border-subtle bg-bg-sunken text-text-muted',\n )}\n style={\n state === 'done'\n ? { backgroundColor: ACCENT }\n : state === 'active'\n ? { borderColor: ACCENT, color: ACCENT }\n : undefined\n }\n >\n {state === 'done' ? (\n <Check aria-hidden=\"true\" className=\"h-4 w-4\" />\n ) : state === 'locked' ? (\n <Lock aria-hidden=\"true\" className=\"h-3.5 w-3.5\" />\n ) : (\n i + 1\n )}\n </span>\n {i < steps.length - 1 ? (\n <span\n aria-hidden=\"true\"\n className=\"h-0.5 flex-1 rounded-full\"\n style={{\n backgroundColor:\n state === 'done' ? ACCENT : 'var(--color-border-subtle)',\n }}\n />\n ) : (\n <span className=\"flex-1\" aria-hidden=\"true\" />\n )}\n </div>\n <span\n className={cn(\n 'mt-2 max-w-[7rem] truncate px-1 text-center text-xs',\n state === 'locked' ? 'text-text-muted' : 'font-medium text-text-primary',\n )}\n title={step.label}\n >\n {step.label}\n </span>\n {step.meta ? (\n <span className=\"mt-0.5 px-1 text-center text-[11px] text-text-muted\">{step.meta}</span>\n ) : null}\n </li>\n );\n })}\n </ol>\n );\n}\n"],"mappings":";;;;;AAsBA,SAAgB,EAAU,EAAE,UAAO,gBAA6B;AAI9D,QAHI,EAAM,WAAW,IACZ,OAGP,kBAAC,MAAD;EAAI,WAAW,EAAG,yCAAyC,EAAU;EAAE,cAAW;YAC/E,EAAM,KAAK,GAAM,MAAM;GACtB,IAAM,IAAQ,EAAK,SAAS;AAE5B,UACE,kBAAC,MAAD;IAEE,WAAU;IACV,gBAAc,MAAU,WAAW,SAAS,KAAA;cAH9C;KAKE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACG,IAAI,IACH,kBAAC,QAAD;QACE,eAAY;QACZ,WAAU;QACV,OAAO,EAAE,iBAZK,MAAU,SAYoB,IAAS,8BAA8B;QACnF,CAAA,GAEF,kBAAC,QAAD;QAAM,WAAU;QAAS,eAAY;QAAS,CAAA;OAEhD,kBAAC,QAAD;QACE,WAAW,EACT,mHACA,MAAU,UAAU,wCACpB,MAAU,YAAY,oCACtB,MAAU,UAAU,2DACpB,MAAU,YAAY,oDACvB;QACD,OACE,MAAU,SACN,EAAE,iBAAiB,GAAQ,GAC3B,MAAU,WACR;SAAE,aAAa;SAAQ,OAAO;SAAQ,GACtC,KAAA;kBAGP,MAAU,SACT,kBAAC,GAAD;SAAO,eAAY;SAAO,WAAU;SAAY,CAAA,GAC9C,MAAU,WACZ,kBAAC,GAAD;SAAM,eAAY;SAAO,WAAU;SAAgB,CAAA,GAEnD,IAAI;QAED,CAAA;OACN,IAAI,EAAM,SAAS,IAClB,kBAAC,QAAD;QACE,eAAY;QACZ,WAAU;QACV,OAAO,EACL,iBACE,MAAU,SAAS,IAAS,8BAC/B;QACD,CAAA,GAEF,kBAAC,QAAD;QAAM,WAAU;QAAS,eAAY;QAAS,CAAA;OAE5C;;KACN,kBAAC,QAAD;MACE,WAAW,EACT,uDACA,MAAU,WAAW,oBAAoB,gCAC1C;MACD,OAAO,EAAK;gBAEX,EAAK;MACD,CAAA;KACN,EAAK,OACJ,kBAAC,QAAD;MAAM,WAAU;gBAAuD,EAAK;MAAY,CAAA,GACtF;KACD;MA/DE,GAAG,EAAK,MAAM,GAAG,IA+DnB;IAEP;EACC,CAAA"}
1
+ {"version":3,"file":"PathSteps.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/PathSteps.tsx"],"sourcesContent":["import { Check, Lock } from 'lucide-react';\nimport { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\n\nexport type PathStepState = 'done' | 'active' | 'todo' | 'locked';\n\nexport interface PathStep {\n label: string;\n meta?: string;\n state?: PathStepState;\n}\n\nexport interface PathStepsProps {\n steps: PathStep[];\n className?: string;\n}\n\n/**\n * A sequence of course nodes (circles + labels + connectors) for a learning\n * path. Each node carries its own state; the connector into a node is filled\n * once that node is complete. Scrolls horizontally on narrow screens.\n */\nexport function PathSteps({ steps, className }: PathStepsProps) {\n if (steps.length === 0) {\n return null;\n }\n return (\n <ol\n className={cn('flex items-start overflow-x-auto pb-1', className)}\n aria-label=\"Learning path\"\n >\n {steps.map((step, i) => {\n const state = step.state ?? 'todo';\n const connectorFilled = state === 'done';\n return (\n <li\n key={`${step.label}-${i}`}\n className=\"flex min-w-[5.5rem] flex-1 flex-col items-center\"\n aria-current={state === 'active' ? 'step' : undefined}\n >\n <div className=\"flex w-full items-center\">\n {i > 0 ? (\n <span\n aria-hidden=\"true\"\n className=\"h-0.5 flex-1 rounded-full\"\n style={{\n backgroundColor: connectorFilled ? ACCENT : 'var(--color-border-subtle)',\n }}\n />\n ) : (\n <span className=\"flex-1\" aria-hidden=\"true\" />\n )}\n <span\n className={cn(\n 'flex h-9 w-9 shrink-0 items-center justify-center rounded-full border-2 text-xs font-semibold transition-colors',\n state === 'done' && 'border-transparent text-text-inverse',\n state === 'active' && 'bg-bg-surface shadow-elevation-1',\n state === 'todo' && 'border-border-default bg-bg-surface text-text-secondary',\n state === 'locked' && 'border-border-subtle bg-bg-sunken text-text-muted'\n )}\n style={\n state === 'done'\n ? { backgroundColor: ACCENT }\n : state === 'active'\n ? { borderColor: ACCENT, color: ACCENT }\n : undefined\n }\n >\n {state === 'done' ? (\n <Check aria-hidden=\"true\" className=\"h-4 w-4\" />\n ) : state === 'locked' ? (\n <Lock aria-hidden=\"true\" className=\"h-3.5 w-3.5\" />\n ) : (\n i + 1\n )}\n </span>\n {i < steps.length - 1 ? (\n <span\n aria-hidden=\"true\"\n className=\"h-0.5 flex-1 rounded-full\"\n style={{\n backgroundColor: state === 'done' ? ACCENT : 'var(--color-border-subtle)',\n }}\n />\n ) : (\n <span className=\"flex-1\" aria-hidden=\"true\" />\n )}\n </div>\n <span\n className={cn(\n 'mt-2 max-w-[7rem] truncate px-1 text-center text-xs',\n state === 'locked' ? 'text-text-muted' : 'font-medium text-text-primary'\n )}\n title={step.label}\n >\n {step.label}\n </span>\n {step.meta ? (\n <span className=\"mt-0.5 px-1 text-center text-[11px] text-text-muted\">\n {step.meta}\n </span>\n ) : null}\n </li>\n );\n })}\n </ol>\n );\n}\n"],"mappings":";;;;;AAsBA,SAAgB,EAAU,EAAE,UAAO,gBAA6B;AAI9D,QAHI,EAAM,WAAW,IACZ,OAGP,kBAAC,MAAD;EACE,WAAW,EAAG,yCAAyC,EAAU;EACjE,cAAW;YAEV,EAAM,KAAK,GAAM,MAAM;GACtB,IAAM,IAAQ,EAAK,SAAS;AAE5B,UACE,kBAAC,MAAD;IAEE,WAAU;IACV,gBAAc,MAAU,WAAW,SAAS,KAAA;cAH9C;KAKE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACG,IAAI,IACH,kBAAC,QAAD;QACE,eAAY;QACZ,WAAU;QACV,OAAO,EACL,iBAbY,MAAU,SAaa,IAAS,8BAC7C;QACD,CAAA,GAEF,kBAAC,QAAD;QAAM,WAAU;QAAS,eAAY;QAAS,CAAA;OAEhD,kBAAC,QAAD;QACE,WAAW,EACT,mHACA,MAAU,UAAU,wCACpB,MAAU,YAAY,oCACtB,MAAU,UAAU,2DACpB,MAAU,YAAY,oDACvB;QACD,OACE,MAAU,SACN,EAAE,iBAAiB,GAAQ,GAC3B,MAAU,WACR;SAAE,aAAa;SAAQ,OAAO;SAAQ,GACtC,KAAA;kBAGP,MAAU,SACT,kBAAC,GAAD;SAAO,eAAY;SAAO,WAAU;SAAY,CAAA,GAC9C,MAAU,WACZ,kBAAC,GAAD;SAAM,eAAY;SAAO,WAAU;SAAgB,CAAA,GAEnD,IAAI;QAED,CAAA;OACN,IAAI,EAAM,SAAS,IAClB,kBAAC,QAAD;QACE,eAAY;QACZ,WAAU;QACV,OAAO,EACL,iBAAiB,MAAU,SAAS,IAAS,8BAC9C;QACD,CAAA,GAEF,kBAAC,QAAD;QAAM,WAAU;QAAS,eAAY;QAAS,CAAA;OAE5C;;KACN,kBAAC,QAAD;MACE,WAAW,EACT,uDACA,MAAU,WAAW,oBAAoB,gCAC1C;MACD,OAAO,EAAK;gBAEX,EAAK;MACD,CAAA;KACN,EAAK,OACJ,kBAAC,QAAD;MAAM,WAAU;gBACb,EAAK;MACD,CAAA,GACL;KACD;MAlEE,GAAG,EAAK,MAAM,GAAG,IAkEnB;IAEP;EACC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"RatingStars.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/RatingStars.tsx"],"sourcesContent":["import { 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. */\n showValue?: boolean;\n /** Optional review count, e.g. \"(128)\". */\n count?: number;\n /** Star size in pixels. */\n size?: number;\n className?: string;\n}\n\n/** Filled / empty stars for a 0–max rating, with optional value + count. */\nexport function RatingStars({\n value,\n max = 5,\n showValue = false,\n count,\n size = 16,\n className,\n}: RatingStarsProps) {\n const clamped = clamp(value, 0, max);\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 ? STATUS.warning : 'var(--color-border-strong)' }}\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\n className=\"absolute left-0 top-0 overflow-hidden\"\n style={{ width: `${fillPct}%` }}\n >\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 ? (\n <span className=\"text-xs text-text-muted\">({count})</span>\n ) : null}\n </span>\n );\n}\n"],"mappings":";;;;;;AAkBA,SAAgB,EAAY,EAC1B,UACA,SAAM,GACN,eAAY,IACZ,UACA,UAAO,IACP,gBACmB;CACnB,IAAM,IAAU,EAAM,GAAO,GAAG,EAAI,EAC9B,IAAW,IAAU,IAAO,KAC5B,IAAM,KAAK,MAAM,IAAO,IAAK,EAE7B,KAAO,MACX,kBAAC,QAAD;EAAM,WAAU;EAAO,OAAO,EAAE,QAAK;EAAE,eAAY;YAChD,MAAM,KAAK,EAAE,QAAQ,GAAK,GAAG,GAAG,MAC/B,kBAAC,GAAD;GAEE,OAAO;GACP,QAAQ;GACR,aAAa;GACb,OAAO,EAAE,OAAO,IAAS,EAAO,UAAU,8BAA8B;GACxE,MAAM,IAAS,iBAAiB;GAChC,EANK,EAML,CACF;EACG,CAAA;AAGT,QACE,kBAAC,QAAD;EACE,WAAW,EAAG,oCAAoC,EAAU;EAC5D,MAAK;EACL,cAAY,SAAS,EAAQ,UAAU,IAAM,MAAU,KAAA,IAAuC,KAA3B,SAAS,EAAM;YAHpF;GAKE,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACG,EAAI,GAAM,EACX,kBAAC,QAAD;KACE,WAAU;KACV,OAAO,EAAE,OAAO,GAAG,EAAQ,IAAI;eAE9B,EAAI,GAAK;KACL,CAAA,CACF;;GACN,IACC,kBAAC,QAAD;IAAM,WAAU;cACb,EAAQ,QAAQ,EAAE;IACd,CAAA,GACL;GACH,MAAU,KAAA,IAEP,OADF,kBAAC,QAAD;IAAM,WAAU;cAAhB;KAA0C;KAAE;KAAM;KAAQ;;GAEvD"}
1
+ {"version":3,"file":"RatingStars.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/RatingStars.tsx"],"sourcesContent":["import { 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. */\n showValue?: boolean;\n /** Optional review count, e.g. \"(128)\". */\n count?: number;\n /** Star size in pixels. */\n size?: number;\n className?: string;\n}\n\n/** Filled / empty stars for a 0–max rating, with optional value + count. */\nexport function RatingStars({\n value,\n max = 5,\n showValue = false,\n count,\n size = 16,\n className,\n}: RatingStarsProps) {\n const clamped = clamp(value, 0, max);\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 ? STATUS.warning : 'var(--color-border-strong)' }}\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":";;;;;;AAkBA,SAAgB,EAAY,EAC1B,UACA,SAAM,GACN,eAAY,IACZ,UACA,UAAO,IACP,gBACmB;CACnB,IAAM,IAAU,EAAM,GAAO,GAAG,EAAI,EAC9B,IAAW,IAAU,IAAO,KAC5B,IAAM,KAAK,MAAM,IAAO,IAAK,EAE7B,KAAO,MACX,kBAAC,QAAD;EAAM,WAAU;EAAO,OAAO,EAAE,QAAK;EAAE,eAAY;YAChD,MAAM,KAAK,EAAE,QAAQ,GAAK,GAAG,GAAG,MAC/B,kBAAC,GAAD;GAEE,OAAO;GACP,QAAQ;GACR,aAAa;GACb,OAAO,EAAE,OAAO,IAAS,EAAO,UAAU,8BAA8B;GACxE,MAAM,IAAS,iBAAiB;GAChC,EANK,EAML,CACF;EACG,CAAA;AAGT,QACE,kBAAC,QAAD;EACE,WAAW,EAAG,oCAAoC,EAAU;EAC5D,MAAK;EACL,cAAY,SAAS,EAAQ,UAAU,IAAM,MAAU,KAAA,IAAuC,KAA3B,SAAS,EAAM;YAHpF;GAKE,kBAAC,QAAD;IAAM,WAAU;cAAhB,CACG,EAAI,GAAM,EACX,kBAAC,QAAD;KAAM,WAAU;KAAwC,OAAO,EAAE,OAAO,GAAG,EAAQ,IAAI;eACpF,EAAI,GAAK;KACL,CAAA,CACF;;GACN,IACC,kBAAC,QAAD;IAAM,WAAU;cACb,EAAQ,QAAQ,EAAE;IACd,CAAA,GACL;GACH,MAAU,KAAA,IAAyE,OAA7D,kBAAC,QAAD;IAAM,WAAU;cAAhB;KAA0C;KAAE;KAAM;KAAQ;;GAC5E"}
@@ -1 +1 @@
1
- {"version":3,"file":"SegmentedTabs.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/SegmentedTabs.tsx"],"sourcesContent":["import { type KeyboardEvent, type ReactNode, useRef } from 'react';\nimport { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\n\nexport interface SegmentedTab {\n id: string;\n label: ReactNode;\n icon?: ReactNode;\n count?: number;\n /** id of the panel this tab controls (for aria-controls). */\n controls?: string;\n}\n\nexport interface SegmentedTabsProps {\n tabs: SegmentedTab[];\n value: string;\n onChange: (id: string) => void;\n variant?: 'pill' | 'underline';\n /** Stretch tabs to fill the available width. */\n fill?: boolean;\n className?: string;\n 'aria-label'?: string;\n}\n\n/**\n * A controlled pill / underline tab switcher with a proper tablist:\n * roving tab focus, arrow / Home / End navigation, and tokenized styling.\n */\nexport function SegmentedTabs({\n tabs,\n value,\n onChange,\n variant = 'pill',\n fill = false,\n className,\n 'aria-label': ariaLabel,\n}: SegmentedTabsProps) {\n const refs = useRef<(HTMLButtonElement | null)[]>([]);\n\n const focusTab = (index: number): void => {\n const clamped = (index + tabs.length) % tabs.length;\n const tab = tabs[clamped];\n if (tab) {\n onChange(tab.id);\n refs.current[clamped]?.focus();\n }\n };\n\n const handleKey = (event: KeyboardEvent<HTMLButtonElement>, index: number): void => {\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n event.preventDefault();\n focusTab(index + 1);\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n event.preventDefault();\n focusTab(index - 1);\n break;\n case 'Home':\n event.preventDefault();\n focusTab(0);\n break;\n case 'End':\n event.preventDefault();\n focusTab(tabs.length - 1);\n break;\n default:\n break;\n }\n };\n\n const isPill = variant === 'pill';\n\n return (\n <div\n role=\"tablist\"\n aria-label={ariaLabel}\n className={cn(\n 'flex items-center',\n isPill\n ? 'gap-1 rounded-full border border-border-subtle bg-bg-sunken p-1'\n : 'gap-1 border-b border-border-subtle',\n fill && 'w-full',\n className,\n )}\n >\n {tabs.map((tab, i) => {\n const selected = tab.id === value;\n return (\n <button\n key={tab.id}\n ref={(node) => {\n refs.current[i] = node;\n }}\n type=\"button\"\n role=\"tab\"\n aria-selected={selected}\n aria-controls={tab.controls}\n tabIndex={selected ? 0 : -1}\n onClick={() => onChange(tab.id)}\n onKeyDown={(event) => handleKey(event, i)}\n className={cn(\n 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n fill && 'flex-1',\n isPill\n ? cn(\n 'rounded-full px-3 py-1.5',\n selected\n ? 'bg-bg-surface text-text-primary shadow-elevation-1'\n : 'text-text-secondary hover:text-text-primary',\n )\n : cn(\n '-mb-px border-b-2 px-3 py-2',\n selected\n ? 'text-text-primary'\n : 'border-transparent text-text-secondary hover:text-text-primary',\n ),\n )}\n style={!isPill && selected ? { borderColor: ACCENT, color: 'var(--color-text-primary)' } : undefined}\n >\n {tab.icon ? <span aria-hidden=\"true\">{tab.icon}</span> : null}\n {tab.label}\n {tab.count !== undefined ? (\n <span\n className={cn(\n 'inline-flex min-w-5 items-center justify-center rounded-full px-1.5 text-xs tabular-nums',\n selected ? 'bg-bg-sunken text-text-secondary' : 'bg-bg-elevated text-text-muted',\n )}\n >\n {tab.count}\n </span>\n ) : null}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;AA4BA,SAAgB,EAAc,EAC5B,SACA,UACA,aACA,aAAU,QACV,UAAO,IACP,cACA,cAAc,KACO;CACrB,IAAM,IAAO,EAAqC,EAAE,CAAC,EAE/C,KAAY,MAAwB;EACxC,IAAM,KAAW,IAAQ,EAAK,UAAU,EAAK,QACvC,IAAM,EAAK;AACjB,EAAI,MACF,EAAS,EAAI,GAAG,EAChB,EAAK,QAAQ,IAAU,OAAO;IAI5B,KAAa,GAAyC,MAAwB;AAClF,UAAQ,EAAM,KAAd;GACE,KAAK;GACL,KAAK;AAEH,IADA,EAAM,gBAAgB,EACtB,EAAS,IAAQ,EAAE;AACnB;GACF,KAAK;GACL,KAAK;AAEH,IADA,EAAM,gBAAgB,EACtB,EAAS,IAAQ,EAAE;AACnB;GACF,KAAK;AAEH,IADA,EAAM,gBAAgB,EACtB,EAAS,EAAE;AACX;GACF,KAAK;AAEH,IADA,EAAM,gBAAgB,EACtB,EAAS,EAAK,SAAS,EAAE;AACzB;GACF,QACE;;IAIA,IAAS,MAAY;AAE3B,QACE,kBAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,WAAW,EACT,qBACA,IACI,oEACA,uCACJ,KAAQ,UACR,EACD;YAEA,EAAK,KAAK,GAAK,MAAM;GACpB,IAAM,IAAW,EAAI,OAAO;AAC5B,UACE,kBAAC,UAAD;IAEE,MAAM,MAAS;AACb,OAAK,QAAQ,KAAK;;IAEpB,MAAK;IACL,MAAK;IACL,iBAAe;IACf,iBAAe,EAAI;IACnB,UAAU,IAAW,IAAI;IACzB,eAAe,EAAS,EAAI,GAAG;IAC/B,YAAY,MAAU,EAAU,GAAO,EAAE;IACzC,WAAW,EACT,0LACA,KAAQ,UACR,IACI,EACE,4BACA,IACI,uDACA,8CACL,GACD,EACE,+BACA,IACI,sBACA,iEACL,CACN;IACD,OAAO,CAAC,KAAU,IAAW;KAAE,aAAa;KAAQ,OAAO;KAA6B,GAAG,KAAA;cA7B7F;KA+BG,EAAI,OAAO,kBAAC,QAAD;MAAM,eAAY;gBAAQ,EAAI;MAAY,CAAA,GAAG;KACxD,EAAI;KACJ,EAAI,UAAU,KAAA,IASX,OARF,kBAAC,QAAD;MACE,WAAW,EACT,4FACA,IAAW,qCAAqC,iCACjD;gBAEA,EAAI;MACA,CAAA;KAEF;MA1CF,EAAI,GA0CF;IAEX;EACE,CAAA"}
1
+ {"version":3,"file":"SegmentedTabs.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/SegmentedTabs.tsx"],"sourcesContent":["import { type KeyboardEvent, type ReactNode, useRef } from 'react';\nimport { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\n\nexport interface SegmentedTab {\n id: string;\n label: ReactNode;\n icon?: ReactNode;\n count?: number;\n /** id of the panel this tab controls (for aria-controls). */\n controls?: string;\n}\n\nexport interface SegmentedTabsProps {\n tabs: SegmentedTab[];\n value: string;\n onChange: (id: string) => void;\n variant?: 'pill' | 'underline';\n /** Stretch tabs to fill the available width. */\n fill?: boolean;\n className?: string;\n 'aria-label'?: string;\n}\n\n/**\n * A controlled pill / underline tab switcher with a proper tablist:\n * roving tab focus, arrow / Home / End navigation, and tokenized styling.\n */\nexport function SegmentedTabs({\n tabs,\n value,\n onChange,\n variant = 'pill',\n fill = false,\n className,\n 'aria-label': ariaLabel,\n}: SegmentedTabsProps) {\n const refs = useRef<(HTMLButtonElement | null)[]>([]);\n\n const focusTab = (index: number): void => {\n const clamped = (index + tabs.length) % tabs.length;\n const tab = tabs[clamped];\n if (tab) {\n onChange(tab.id);\n refs.current[clamped]?.focus();\n }\n };\n\n const handleKey = (event: KeyboardEvent<HTMLButtonElement>, index: number): void => {\n switch (event.key) {\n case 'ArrowRight':\n case 'ArrowDown':\n event.preventDefault();\n focusTab(index + 1);\n break;\n case 'ArrowLeft':\n case 'ArrowUp':\n event.preventDefault();\n focusTab(index - 1);\n break;\n case 'Home':\n event.preventDefault();\n focusTab(0);\n break;\n case 'End':\n event.preventDefault();\n focusTab(tabs.length - 1);\n break;\n default:\n break;\n }\n };\n\n const isPill = variant === 'pill';\n\n return (\n <div\n role=\"tablist\"\n aria-label={ariaLabel}\n className={cn(\n 'flex items-center',\n isPill\n ? 'gap-1 rounded-full border border-border-subtle bg-bg-sunken p-1'\n : 'gap-1 border-b border-border-subtle',\n fill && 'w-full',\n className\n )}\n >\n {tabs.map((tab, i) => {\n const selected = tab.id === value;\n return (\n <button\n key={tab.id}\n ref={(node) => {\n refs.current[i] = node;\n }}\n type=\"button\"\n role=\"tab\"\n aria-selected={selected}\n aria-controls={tab.controls}\n tabIndex={selected ? 0 : -1}\n onClick={() => onChange(tab.id)}\n onKeyDown={(event) => handleKey(event, i)}\n className={cn(\n 'inline-flex items-center justify-center gap-1.5 whitespace-nowrap text-sm font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n fill && 'flex-1',\n isPill\n ? cn(\n 'rounded-full px-3 py-1.5',\n selected\n ? 'bg-bg-surface text-text-primary shadow-elevation-1'\n : 'text-text-secondary hover:text-text-primary'\n )\n : cn(\n '-mb-px border-b-2 px-3 py-2',\n selected\n ? 'text-text-primary'\n : 'border-transparent text-text-secondary hover:text-text-primary'\n )\n )}\n style={\n !isPill && selected\n ? { borderColor: ACCENT, color: 'var(--color-text-primary)' }\n : undefined\n }\n >\n {tab.icon ? <span aria-hidden=\"true\">{tab.icon}</span> : null}\n {tab.label}\n {tab.count !== undefined ? (\n <span\n className={cn(\n 'inline-flex min-w-5 items-center justify-center rounded-full px-1.5 text-xs tabular-nums',\n selected ? 'bg-bg-sunken text-text-secondary' : 'bg-bg-elevated text-text-muted'\n )}\n >\n {tab.count}\n </span>\n ) : null}\n </button>\n );\n })}\n </div>\n );\n}\n"],"mappings":";;;;;AA4BA,SAAgB,EAAc,EAC5B,SACA,UACA,aACA,aAAU,QACV,UAAO,IACP,cACA,cAAc,KACO;CACrB,IAAM,IAAO,EAAqC,EAAE,CAAC,EAE/C,KAAY,MAAwB;EACxC,IAAM,KAAW,IAAQ,EAAK,UAAU,EAAK,QACvC,IAAM,EAAK;AACjB,EAAI,MACF,EAAS,EAAI,GAAG,EAChB,EAAK,QAAQ,IAAU,OAAO;IAI5B,KAAa,GAAyC,MAAwB;AAClF,UAAQ,EAAM,KAAd;GACE,KAAK;GACL,KAAK;AAEH,IADA,EAAM,gBAAgB,EACtB,EAAS,IAAQ,EAAE;AACnB;GACF,KAAK;GACL,KAAK;AAEH,IADA,EAAM,gBAAgB,EACtB,EAAS,IAAQ,EAAE;AACnB;GACF,KAAK;AAEH,IADA,EAAM,gBAAgB,EACtB,EAAS,EAAE;AACX;GACF,KAAK;AAEH,IADA,EAAM,gBAAgB,EACtB,EAAS,EAAK,SAAS,EAAE;AACzB;GACF,QACE;;IAIA,IAAS,MAAY;AAE3B,QACE,kBAAC,OAAD;EACE,MAAK;EACL,cAAY;EACZ,WAAW,EACT,qBACA,IACI,oEACA,uCACJ,KAAQ,UACR,EACD;YAEA,EAAK,KAAK,GAAK,MAAM;GACpB,IAAM,IAAW,EAAI,OAAO;AAC5B,UACE,kBAAC,UAAD;IAEE,MAAM,MAAS;AACb,OAAK,QAAQ,KAAK;;IAEpB,MAAK;IACL,MAAK;IACL,iBAAe;IACf,iBAAe,EAAI;IACnB,UAAU,IAAW,IAAI;IACzB,eAAe,EAAS,EAAI,GAAG;IAC/B,YAAY,MAAU,EAAU,GAAO,EAAE;IACzC,WAAW,EACT,0LACA,KAAQ,UACR,IACI,EACE,4BACA,IACI,uDACA,8CACL,GACD,EACE,+BACA,IACI,sBACA,iEACL,CACN;IACD,OACE,CAAC,KAAU,IACP;KAAE,aAAa;KAAQ,OAAO;KAA6B,GAC3D,KAAA;cAhCR;KAmCG,EAAI,OAAO,kBAAC,QAAD;MAAM,eAAY;gBAAQ,EAAI;MAAY,CAAA,GAAG;KACxD,EAAI;KACJ,EAAI,UAAU,KAAA,IASX,OARF,kBAAC,QAAD;MACE,WAAW,EACT,4FACA,IAAW,qCAAqC,iCACjD;gBAEA,EAAI;MACA,CAAA;KAEF;MA9CF,EAAI,GA8CF;IAEX;EACE,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"StatCard.js","names":[],"sources":["../../../../src/crewfoundry/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 className={cn('inline-flex items-center gap-0.5 font-medium tabular-nums', toneClass)}>\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;KAAM,WAAW,EAAG,6DAA6D,EAAU;eAA3F;MACG,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"}
1
+ {"version":3,"file":"StatCard.js","names":[],"sources":["../../../../src/crewfoundry/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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Avatar.js","names":[],"sources":["../../../../../src/crewfoundry/components/viz/lib/Avatar.tsx"],"sourcesContent":["import { cn } from '../../../utils/cn';\nimport { getInitials } from './format';\n\nexport interface VizAvatarProps {\n name: string;\n src?: string;\n /** Pixel diameter. */\n size?: number;\n className?: string;\n /** Extra ring around the avatar (used by overlapping stacks). */\n ring?: boolean;\n}\n\n/**\n * Internal avatar primitive: image when available, initials fallback otherwise.\n * Shared by AvatarStack, OrgTree and Timeline. Not exported from the barrel.\n */\nexport function VizAvatar({ name, src, size = 32, className, ring = false }: VizAvatarProps) {\n return (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border-subtle bg-bg-sunken font-medium text-text-secondary',\n ring && 'ring-2 ring-[var(--color-bg-surface)]',\n className,\n )}\n style={{ width: size, height: size, fontSize: Math.round(size * 0.38) }}\n title={name}\n >\n {src ? (\n <img\n src={src}\n alt={name}\n width={size}\n height={size}\n className=\"h-full w-full object-cover\"\n loading=\"lazy\"\n />\n ) : (\n <span aria-label={name}>{getInitials(name)}</span>\n )}\n </span>\n );\n}\n"],"mappings":";;;;AAiBA,SAAgB,EAAU,EAAE,SAAM,QAAK,UAAO,IAAI,cAAW,UAAO,MAAyB;AAC3F,QACE,kBAAC,QAAD;EACE,WAAW,EACT,0JACA,KAAQ,yCACR,EACD;EACD,OAAO;GAAE,OAAO;GAAM,QAAQ;GAAM,UAAU,KAAK,MAAM,IAAO,IAAK;GAAE;EACvE,OAAO;YAEN,IACC,kBAAC,OAAD;GACO;GACL,KAAK;GACL,OAAO;GACP,QAAQ;GACR,WAAU;GACV,SAAQ;GACR,CAAA,GAEF,kBAAC,QAAD;GAAM,cAAY;aAAO,EAAY,EAAK;GAAQ,CAAA;EAE/C,CAAA"}
1
+ {"version":3,"file":"Avatar.js","names":[],"sources":["../../../../../src/crewfoundry/components/viz/lib/Avatar.tsx"],"sourcesContent":["import { cn } from '../../../utils/cn';\nimport { getInitials } from './format';\n\nexport interface VizAvatarProps {\n name: string;\n src?: string;\n /** Pixel diameter. */\n size?: number;\n className?: string;\n /** Extra ring around the avatar (used by overlapping stacks). */\n ring?: boolean;\n}\n\n/**\n * Internal avatar primitive: image when available, initials fallback otherwise.\n * Shared by AvatarStack, OrgTree and Timeline. Not exported from the barrel.\n */\nexport function VizAvatar({ name, src, size = 32, className, ring = false }: VizAvatarProps) {\n return (\n <span\n className={cn(\n 'inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border-subtle bg-bg-sunken font-medium text-text-secondary',\n ring && 'ring-2 ring-[var(--color-bg-surface)]',\n className\n )}\n style={{ width: size, height: size, fontSize: Math.round(size * 0.38) }}\n title={name}\n >\n {src ? (\n <img\n src={src}\n alt={name}\n width={size}\n height={size}\n className=\"h-full w-full object-cover\"\n loading=\"lazy\"\n />\n ) : (\n <span aria-label={name}>{getInitials(name)}</span>\n )}\n </span>\n );\n}\n"],"mappings":";;;;AAiBA,SAAgB,EAAU,EAAE,SAAM,QAAK,UAAO,IAAI,cAAW,UAAO,MAAyB;AAC3F,QACE,kBAAC,QAAD;EACE,WAAW,EACT,0JACA,KAAQ,yCACR,EACD;EACD,OAAO;GAAE,OAAO;GAAM,QAAQ;GAAM,UAAU,KAAK,MAAM,IAAO,IAAK;GAAE;EACvE,OAAO;YAEN,IACC,kBAAC,OAAD;GACO;GACL,KAAK;GACL,OAAO;GACP,QAAQ;GACR,WAAU;GACV,SAAQ;GACR,CAAA,GAEF,kBAAC,QAAD;GAAM,cAAY;aAAO,EAAY,EAAK;GAAQ,CAAA;EAE/C,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"CartesianFrame.js","names":[],"sources":["../../../../../src/crewfoundry/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\n .map((_, i) => i)\n .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
+ {"version":3,"file":"CartesianFrame.js","names":[],"sources":["../../../../../src/crewfoundry/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":"geometry.js","names":[],"sources":["../../../../../src/crewfoundry/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(\n cx: number,\n cy: number,\n radius: number,\n angleDeg: number,\n): 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
+ {"version":3,"file":"geometry.js","names":[],"sources":["../../../../../src/crewfoundry/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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"}