@burdenoff/microfe-crewfoundry 2026.704.6 → 2026.704.7

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 (148) hide show
  1. package/dist/crewfoundry/CrewFoundryRoutes.js +51 -43
  2. package/dist/crewfoundry/CrewFoundryRoutes.js.map +1 -1
  3. package/dist/crewfoundry/components/forms/formValues.js +5 -2
  4. package/dist/crewfoundry/components/forms/formValues.js.map +1 -1
  5. package/dist/crewfoundry/components/viz/AllocationBar.js +68 -0
  6. package/dist/crewfoundry/components/viz/AllocationBar.js.map +1 -0
  7. package/dist/crewfoundry/components/viz/AreaChart.js +102 -0
  8. package/dist/crewfoundry/components/viz/AreaChart.js.map +1 -0
  9. package/dist/crewfoundry/components/viz/AvatarStack.js +36 -0
  10. package/dist/crewfoundry/components/viz/AvatarStack.js.map +1 -0
  11. package/dist/crewfoundry/components/viz/BarChart.js +83 -0
  12. package/dist/crewfoundry/components/viz/BarChart.js.map +1 -0
  13. package/dist/crewfoundry/components/viz/CertificateCard.js +118 -0
  14. package/dist/crewfoundry/components/viz/CertificateCard.js.map +1 -0
  15. package/dist/crewfoundry/components/viz/DonutChart.js +109 -0
  16. package/dist/crewfoundry/components/viz/DonutChart.js.map +1 -0
  17. package/dist/crewfoundry/components/viz/HBarFunnel.js +54 -0
  18. package/dist/crewfoundry/components/viz/HBarFunnel.js.map +1 -0
  19. package/dist/crewfoundry/components/viz/Heatmap.js +91 -0
  20. package/dist/crewfoundry/components/viz/Heatmap.js.map +1 -0
  21. package/dist/crewfoundry/components/viz/KanbanBoard.js +50 -0
  22. package/dist/crewfoundry/components/viz/KanbanBoard.js.map +1 -0
  23. package/dist/crewfoundry/components/viz/LineChart.js +123 -0
  24. package/dist/crewfoundry/components/viz/LineChart.js.map +1 -0
  25. package/dist/crewfoundry/components/viz/OrgTree.js +83 -0
  26. package/dist/crewfoundry/components/viz/OrgTree.js.map +1 -0
  27. package/dist/crewfoundry/components/viz/PathSteps.js +68 -0
  28. package/dist/crewfoundry/components/viz/PathSteps.js.map +1 -0
  29. package/dist/crewfoundry/components/viz/ProgressRing.js +68 -0
  30. package/dist/crewfoundry/components/viz/ProgressRing.js.map +1 -0
  31. package/dist/crewfoundry/components/viz/RatingStars.js +51 -0
  32. package/dist/crewfoundry/components/viz/RatingStars.js.map +1 -0
  33. package/dist/crewfoundry/components/viz/SegmentedTabs.js +69 -0
  34. package/dist/crewfoundry/components/viz/SegmentedTabs.js.map +1 -0
  35. package/dist/crewfoundry/components/viz/SkillBar.js +3 -0
  36. package/dist/crewfoundry/components/viz/Sparkline.js +74 -0
  37. package/dist/crewfoundry/components/viz/Sparkline.js.map +1 -0
  38. package/dist/crewfoundry/components/viz/StatCard.js +60 -0
  39. package/dist/crewfoundry/components/viz/StatCard.js.map +1 -0
  40. package/dist/crewfoundry/components/viz/StepProgress.js +3 -0
  41. package/dist/crewfoundry/components/viz/Timeline.js +66 -0
  42. package/dist/crewfoundry/components/viz/Timeline.js.map +1 -0
  43. package/dist/crewfoundry/components/viz/index.js +23 -0
  44. package/dist/crewfoundry/components/viz/lib/Avatar.js +30 -0
  45. package/dist/crewfoundry/components/viz/lib/Avatar.js.map +1 -0
  46. package/dist/crewfoundry/components/viz/lib/CartesianFrame.js +119 -0
  47. package/dist/crewfoundry/components/viz/lib/CartesianFrame.js.map +1 -0
  48. package/dist/crewfoundry/components/viz/lib/ResponsiveSvg.js +35 -0
  49. package/dist/crewfoundry/components/viz/lib/ResponsiveSvg.js.map +1 -0
  50. package/dist/crewfoundry/components/viz/lib/format.js +21 -0
  51. package/dist/crewfoundry/components/viz/lib/format.js.map +1 -0
  52. package/dist/crewfoundry/components/viz/lib/geometry.js +69 -0
  53. package/dist/crewfoundry/components/viz/lib/geometry.js.map +1 -0
  54. package/dist/crewfoundry/components/viz/lib/hooks.js +39 -0
  55. package/dist/crewfoundry/components/viz/lib/hooks.js.map +1 -0
  56. package/dist/crewfoundry/components/viz/lib/palette.js +27 -0
  57. package/dist/crewfoundry/components/viz/lib/palette.js.map +1 -0
  58. package/dist/crewfoundry/lib/analytics.js +323 -0
  59. package/dist/crewfoundry/lib/analytics.js.map +1 -0
  60. package/dist/crewfoundry/lib/companyFinance.js +16 -0
  61. package/dist/crewfoundry/lib/companyFinance.js.map +1 -0
  62. package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js +226 -283
  63. package/dist/crewfoundry/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  64. package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js +107 -120
  65. package/dist/crewfoundry/pages/AnalyticsEmployeeAnalyticsPagePage.js.map +1 -1
  66. package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js +122 -162
  67. package/dist/crewfoundry/pages/AnalyticsFinancialAnalyticsPagePage.js.map +1 -1
  68. package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js +121 -158
  69. package/dist/crewfoundry/pages/AnalyticsHiringAnalyticsPagePage.js.map +1 -1
  70. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js +316 -211
  71. package/dist/crewfoundry/pages/CommunicationsAnnouncementsPagePage.js.map +1 -1
  72. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js +75 -27
  73. package/dist/crewfoundry/pages/CommunicationsMessagesPagePage.js.map +1 -1
  74. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js +167 -165
  75. package/dist/crewfoundry/pages/CompanyClientManagementPagePage.js.map +1 -1
  76. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js +168 -109
  77. package/dist/crewfoundry/pages/CompanyVendorManagementPagePage.js.map +1 -1
  78. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +401 -290
  79. package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
  80. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js +84 -75
  81. package/dist/crewfoundry/pages/HiringApplicationsPagePage.js.map +1 -1
  82. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js +368 -174
  83. package/dist/crewfoundry/pages/HiringCandidatesPagePage.js.map +1 -1
  84. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +236 -201
  85. package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
  86. package/dist/crewfoundry/pages/HiringInterviewsPagePage.js +369 -196
  87. package/dist/crewfoundry/pages/HiringInterviewsPagePage.js.map +1 -1
  88. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +269 -270
  89. package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
  90. package/dist/crewfoundry/pages/HiringJobsPagePage.js +210 -110
  91. package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
  92. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js +216 -178
  93. package/dist/crewfoundry/pages/HrDepartmentsPagePage.js.map +1 -1
  94. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js +385 -0
  95. package/dist/crewfoundry/pages/HrEmployeeProfilePagePage.js.map +1 -0
  96. package/dist/crewfoundry/pages/HrEmployeesPagePage.js +189 -130
  97. package/dist/crewfoundry/pages/HrEmployeesPagePage.js.map +1 -1
  98. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js +397 -241
  99. package/dist/crewfoundry/pages/HrLeaveManagementPagePage.js.map +1 -1
  100. package/dist/crewfoundry/pages/HrPayrollPagePage.js +262 -147
  101. package/dist/crewfoundry/pages/HrPayrollPagePage.js.map +1 -1
  102. package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js +345 -167
  103. package/dist/crewfoundry/pages/HrTimeTrackingPagePage.js.map +1 -1
  104. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js +98 -24
  105. package/dist/crewfoundry/pages/LearningCertificatesPagePage.js.map +1 -1
  106. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js +1 -1
  107. package/dist/crewfoundry/pages/LearningCourseDetailsPagePage.js.map +1 -1
  108. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js +289 -108
  109. package/dist/crewfoundry/pages/LearningCourseLearningPagePage.js.map +1 -1
  110. package/dist/crewfoundry/pages/LearningCoursesPagePage.js +221 -114
  111. package/dist/crewfoundry/pages/LearningCoursesPagePage.js.map +1 -1
  112. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js +134 -135
  113. package/dist/crewfoundry/pages/LearningLearningPathDetailPagePage.js.map +1 -1
  114. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js +148 -97
  115. package/dist/crewfoundry/pages/LearningLearningPathsPagePage.js.map +1 -1
  116. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js +264 -157
  117. package/dist/crewfoundry/pages/LearningMyLearningPagePage.js.map +1 -1
  118. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js +85 -93
  119. package/dist/crewfoundry/pages/MarketplaceFreelancerProfilePagePage.js.map +1 -1
  120. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +211 -122
  121. package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
  122. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js +94 -54
  123. package/dist/crewfoundry/pages/MarketplaceMarketplaceServicesPagePage.js.map +1 -1
  124. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js +289 -140
  125. package/dist/crewfoundry/pages/MarketplaceServiceDetailsPagePage.js.map +1 -1
  126. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js +169 -79
  127. package/dist/crewfoundry/pages/PaymentsInvoicesPagePage.js.map +1 -1
  128. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +296 -125
  129. package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
  130. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js +187 -0
  131. package/dist/crewfoundry/pages/ProjectsResourcingPagePage.js.map +1 -0
  132. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js +140 -78
  133. package/dist/crewfoundry/pages/TeamsTeamDetailPagePage.js.map +1 -1
  134. package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js +93 -43
  135. package/dist/crewfoundry/pages/TeamsTeamDirectoryPagePage.js.map +1 -1
  136. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +250 -98
  137. package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
  138. package/dist/crewfoundry/store/crewfoundryStore.js +1 -1
  139. package/dist/crewfoundry/store/crewfoundryStore.js.map +1 -1
  140. package/dist/crewfoundry/utils/cn.js +10 -0
  141. package/dist/crewfoundry/utils/cn.js.map +1 -0
  142. package/dist/crewfoundry/utils/courseProgress.js +46 -0
  143. package/dist/crewfoundry/utils/courseProgress.js.map +1 -0
  144. package/dist/node_modules/clsx/dist/clsx.js +18 -0
  145. package/dist/node_modules/clsx/dist/clsx.js.map +1 -0
  146. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +1716 -0
  147. package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js.map +1 -0
  148. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AreaChart.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/AreaChart.tsx"],"sourcesContent":["import { ACCENT } from './lib/palette';\nimport { CartesianFrame } from './lib/CartesianFrame';\nimport { buildAreaPath, buildLinePath } from './lib/geometry';\nimport { formatCompact } from './lib/format';\nimport { useMounted, usePrefersReducedMotion, useVizId } from './lib/hooks';\nimport type { LabeledValue } from './lib/types';\n\nexport interface AreaChartProps {\n series: LabeledValue[];\n /** Pixel height of the plot. */\n height?: number;\n /** Formats axis + tooltip values. Defaults to compact notation. */\n valueFormatter?: (value: number) => string;\n /** Accessible title; defaults to a generic description. */\n title?: string;\n className?: string;\n}\n\n/**\n * Gradient-fill area chart with x/y axes, faint grid and a pointer/keyboard\n * hover readout. Draws itself in on mount unless reduced-motion is requested.\n */\nexport function AreaChart({\n series,\n height = 200,\n valueFormatter = formatCompact,\n title,\n className,\n}: AreaChartProps) {\n const gradientId = useVizId('area-grad');\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 labels = series.map((s) => s.label);\n const min = Math.min(...values);\n const max = Math.max(...values);\n const span = max - min || Math.abs(max) || 1;\n const domain: [number, number] = [min - span * 0.12, max + span * 0.12];\n const chartTitle = title ?? `Area chart of ${series.length} points`;\n\n return (\n <CartesianFrame\n labels={labels}\n domain={domain}\n height={height}\n title={chartTitle}\n formatValue={valueFormatter}\n className={className}\n tooltip={(i) => (\n <span>\n <span className=\"text-text-secondary\">{labels[i]}: </span>\n <span className=\"font-semibold tabular-nums\">{valueFormatter(values[i])}</span>\n </span>\n )}\n >\n {({ px, py, plotBottom, activeIndex }) => {\n const points = values.map((v, i) => ({ x: px(i), y: py(v) }));\n const line = buildLinePath(points);\n const area = buildAreaPath(points, plotBottom);\n const last = points[points.length - 1];\n return (\n <g style={{ color: ACCENT }}>\n <defs>\n <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor=\"currentColor\" stopOpacity={0.34} />\n <stop offset=\"100%\" stopColor=\"currentColor\" stopOpacity={0.02} />\n </linearGradient>\n </defs>\n <path\n d={area}\n fill={`url(#${gradientId})`}\n stroke=\"none\"\n style={{\n opacity: mounted || reduced ? 1 : 0,\n transition: reduced ? undefined : 'opacity 700ms ease',\n }}\n />\n <path\n d={line}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n pathLength={1}\n style={{\n strokeDasharray: 1,\n strokeDashoffset: mounted || reduced ? 0 : 1,\n transition: reduced ? undefined : 'stroke-dashoffset 900ms ease',\n }}\n />\n <circle cx={last.x} cy={last.y} r={3.5} fill=\"currentColor\" />\n {activeIndex !== null ? (\n <g>\n <circle\n cx={px(activeIndex)}\n cy={py(values[activeIndex])}\n r={7}\n fill=\"currentColor\"\n opacity={0.16}\n />\n <circle\n cx={px(activeIndex)}\n cy={py(values[activeIndex])}\n r={3.5}\n fill=\"currentColor\"\n stroke=\"var(--color-bg-surface)\"\n strokeWidth={1.5}\n />\n </g>\n ) : null}\n </g>\n );\n }}\n </CartesianFrame>\n );\n}\n"],"mappings":";;;;;;;AAsBA,SAAgB,EAAU,EACxB,WACA,YAAS,KACT,oBAAiB,GACjB,UACA,gBACiB;CACjB,IAAM,IAAa,EAAS,YAAY,EAClC,IAAU,GAAY,EACtB,IAAU,GAAyB;AAEzC,KAAI,EAAO,WAAW,EACpB,QAAO;CAGT,IAAM,IAAS,EAAO,KAAK,MAAM,EAAE,MAAM,EACnC,IAAS,EAAO,KAAK,MAAM,EAAE,MAAM,EACnC,IAAM,KAAK,IAAI,GAAG,EAAO,EACzB,IAAM,KAAK,IAAI,GAAG,EAAO,EACzB,IAAO,IAAM,KAAO,KAAK,IAAI,EAAI,IAAI;AAI3C,QACE,kBAAC,GAAD;EACU;EACA,QANqB,CAAC,IAAM,IAAO,KAAM,IAAM,IAAO,IAAK;EAO3D;EACR,OAPe,KAAS,iBAAiB,EAAO,OAAO;EAQvD,aAAa;EACF;EACX,UAAU,MACR,kBAAC,QAAD,EAAA,UAAA,CACE,kBAAC,QAAD;GAAM,WAAU;aAAhB,CAAuC,EAAO,IAAG,KAAS;MAC1D,kBAAC,QAAD;GAAM,WAAU;aAA8B,EAAe,EAAO,GAAG;GAAQ,CAAA,CAC1E,EAAA,CAAA;aAGP,EAAE,OAAI,OAAI,eAAY,qBAAkB;GACxC,IAAM,IAAS,EAAO,KAAK,GAAG,OAAO;IAAE,GAAG,EAAG,EAAE;IAAE,GAAG,EAAG,EAAE;IAAE,EAAE,EACvD,IAAO,EAAc,EAAO,EAC5B,IAAO,EAAc,GAAQ,EAAW,EACxC,IAAO,EAAO,EAAO,SAAS;AACpC,UACE,kBAAC,KAAD;IAAG,OAAO,EAAE,OAAO,GAAQ;cAA3B;KACE,kBAAC,QAAD,EAAA,UACE,kBAAC,kBAAD;MAAgB,IAAI;MAAY,IAAG;MAAI,IAAG;MAAI,IAAG;MAAI,IAAG;gBAAxD,CACE,kBAAC,QAAD;OAAM,QAAO;OAAK,WAAU;OAAe,aAAa;OAAQ,CAAA,EAChE,kBAAC,QAAD;OAAM,QAAO;OAAO,WAAU;OAAe,aAAa;OAAQ,CAAA,CACnD;SACZ,CAAA;KACP,kBAAC,QAAD;MACK;MACH,MAAM,QAAQ,EAAW;MACzB,QAAO;MACP,OAAO;OACL,SAAS,KAAW,IAAU,IAAI;OAClC,YAAY,IAAU,KAAA,IAAY;OACnC;MACD,CAAA;KACF,kBAAC,QAAD;MACE,GAAG;MACH,MAAK;MACL,QAAO;MACP,aAAa;MACb,eAAc;MACd,gBAAe;MACf,cAAa;MACb,YAAY;MACZ,OAAO;OACL,iBAAiB;OACjB,kBAAkB,KAAW,IAAU,IAAI;OAC3C,YAAY,IAAU,KAAA,IAAY;OACnC;MACD,CAAA;KACF,kBAAC,UAAD;MAAQ,IAAI,EAAK;MAAG,IAAI,EAAK;MAAG,GAAG;MAAK,MAAK;MAAiB,CAAA;KAC7D,MAAgB,OAkBb,OAjBF,kBAAC,KAAD,EAAA,UAAA,CACE,kBAAC,UAAD;MACE,IAAI,EAAG,EAAY;MACnB,IAAI,EAAG,EAAO,GAAa;MAC3B,GAAG;MACH,MAAK;MACL,SAAS;MACT,CAAA,EACF,kBAAC,UAAD;MACE,IAAI,EAAG,EAAY;MACnB,IAAI,EAAG,EAAO,GAAa;MAC3B,GAAG;MACH,MAAK;MACL,QAAO;MACP,aAAa;MACb,CAAA,CACA,EAAA,CAAA;KAEJ;;;EAGO,CAAA"}
@@ -0,0 +1,36 @@
1
+ import { cn as e } from "../../utils/cn.js";
2
+ import { VizAvatar as t } from "./lib/Avatar.js";
3
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
+ //#region src/crewfoundry/components/viz/AvatarStack.tsx
5
+ function i({ people: i, max: a = 5, size: o = 32, className: s }) {
6
+ if (i.length === 0) return null;
7
+ let c = i.slice(0, a), l = i.length - c.length, u = Math.round(o * .32);
8
+ return /* @__PURE__ */ r("div", {
9
+ className: e("flex items-center", s),
10
+ role: "img",
11
+ "aria-label": `${i.length} people: ${i.map((e) => e.name).join(", ")}`,
12
+ children: [c.map((e, r) => /* @__PURE__ */ n("span", {
13
+ style: { marginLeft: r === 0 ? 0 : -u },
14
+ children: /* @__PURE__ */ n(t, {
15
+ name: e.name,
16
+ src: e.avatarUrl,
17
+ size: o,
18
+ ring: !0
19
+ })
20
+ }, `${e.name}-${r}`)), l > 0 ? /* @__PURE__ */ r("span", {
21
+ className: "inline-flex items-center justify-center rounded-full border border-border-subtle bg-bg-elevated font-medium text-text-secondary ring-2 ring-[var(--color-bg-surface)]",
22
+ style: {
23
+ width: o,
24
+ height: o,
25
+ marginLeft: -u,
26
+ fontSize: Math.round(o * .34)
27
+ },
28
+ "aria-hidden": "true",
29
+ children: ["+", l]
30
+ }) : null]
31
+ });
32
+ }
33
+ //#endregion
34
+ export { i as AvatarStack };
35
+
36
+ //# sourceMappingURL=AvatarStack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AvatarStack.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/AvatarStack.tsx"],"sourcesContent":["import { cn } from '../../utils/cn';\nimport { VizAvatar } from './lib/Avatar';\nimport type { Person } from './lib/types';\n\nexport interface AvatarStackProps {\n people: Person[];\n /** Maximum avatars shown before collapsing into a \"+N\" chip. */\n max?: number;\n /** Pixel diameter of each avatar. */\n size?: number;\n className?: string;\n}\n\n/** Overlapping avatars with a \"+N\" overflow chip. */\nexport function AvatarStack({ people, max = 5, size = 32, className }: AvatarStackProps) {\n if (people.length === 0) {\n return null;\n }\n const shown = people.slice(0, max);\n const overflow = people.length - shown.length;\n const overlap = Math.round(size * 0.32);\n\n return (\n <div\n className={cn('flex items-center', className)}\n role=\"img\"\n aria-label={`${people.length} people: ${people.map((p) => p.name).join(', ')}`}\n >\n {shown.map((person, i) => (\n <span key={`${person.name}-${i}`} style={{ marginLeft: i === 0 ? 0 : -overlap }}>\n <VizAvatar name={person.name} src={person.avatarUrl} size={size} ring />\n </span>\n ))}\n {overflow > 0 ? (\n <span\n className=\"inline-flex items-center justify-center rounded-full border border-border-subtle bg-bg-elevated font-medium text-text-secondary ring-2 ring-[var(--color-bg-surface)]\"\n style={{\n width: size,\n height: size,\n marginLeft: -overlap,\n fontSize: Math.round(size * 0.34),\n }}\n aria-hidden=\"true\"\n >\n +{overflow}\n </span>\n ) : null}\n </div>\n );\n}\n"],"mappings":";;;;AAcA,SAAgB,EAAY,EAAE,WAAQ,SAAM,GAAG,UAAO,IAAI,gBAA+B;AACvF,KAAI,EAAO,WAAW,EACpB,QAAO;CAET,IAAM,IAAQ,EAAO,MAAM,GAAG,EAAI,EAC5B,IAAW,EAAO,SAAS,EAAM,QACjC,IAAU,KAAK,MAAM,IAAO,IAAK;AAEvC,QACE,kBAAC,OAAD;EACE,WAAW,EAAG,qBAAqB,EAAU;EAC7C,MAAK;EACL,cAAY,GAAG,EAAO,OAAO,WAAW,EAAO,KAAK,MAAM,EAAE,KAAK,CAAC,KAAK,KAAK;YAH9E,CAKG,EAAM,KAAK,GAAQ,MAClB,kBAAC,QAAD;GAAkC,OAAO,EAAE,YAAY,MAAM,IAAI,IAAI,CAAC,GAAS;aAC7E,kBAAC,GAAD;IAAW,MAAM,EAAO;IAAM,KAAK,EAAO;IAAiB;IAAM,MAAA;IAAO,CAAA;GACnE,EAFI,GAAG,EAAO,KAAK,GAAG,IAEtB,CACP,EACD,IAAW,IACV,kBAAC,QAAD;GACE,WAAU;GACV,OAAO;IACL,OAAO;IACP,QAAQ;IACR,YAAY,CAAC;IACb,UAAU,KAAK,MAAM,IAAO,IAAK;IAClC;GACD,eAAY;aARd,CASC,KACG,EACG;OACL,KACA"}
@@ -0,0 +1,83 @@
1
+ import { chartPalette as e } from "./lib/palette.js";
2
+ import { useMounted as t, usePrefersReducedMotion as n } from "./lib/hooks.js";
3
+ import { ResponsiveSvg as r } from "./lib/ResponsiveSvg.js";
4
+ import { scaleLinear as i } from "./lib/geometry.js";
5
+ import { formatCompact as a } from "./lib/format.js";
6
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
7
+ //#region src/crewfoundry/components/viz/BarChart.tsx
8
+ var c = {
9
+ top: 20,
10
+ bottom: 26,
11
+ left: 8,
12
+ right: 8
13
+ };
14
+ function l({ series: l, height: u = 220, valueFormatter: d = a, colorful: f = !1, hideValues: p = !1, title: m, className: h }) {
15
+ let g = t(), _ = n();
16
+ if (l.length === 0) return null;
17
+ let v = l.map((e) => e.value), y = Math.max(...v, 0) || 1, b = e(l.length);
18
+ return /* @__PURE__ */ o(r, {
19
+ height: u,
20
+ title: m ?? `Bar chart of ${l.length} categories`,
21
+ className: h,
22
+ desc: l.map((e) => `${e.label}: ${d(e.value)}`).join(", "),
23
+ children: ({ width: e }) => {
24
+ let t = c.left, n = e - c.right, r = c.top, a = u - c.bottom, m = a - r, h = (n - t) / l.length, v = Math.min(56, h * .62);
25
+ return /* @__PURE__ */ s("g", { children: [/* @__PURE__ */ o("line", {
26
+ x1: t,
27
+ x2: n,
28
+ y1: a,
29
+ y2: a,
30
+ stroke: "var(--color-chart-axis)",
31
+ strokeWidth: 1,
32
+ vectorEffect: "non-scaling-stroke",
33
+ opacity: .5
34
+ }), l.map((e, n) => {
35
+ let r = i(e.value, 0, y, 0, m), c = t + h * n + h / 2, l = c - v / 2, x = a - r, S = e.color ?? (f ? b[n] : "var(--color-accent-primary)");
36
+ return /* @__PURE__ */ s("g", { children: [
37
+ /* @__PURE__ */ o("title", { children: `${e.label}: ${d(e.value)}` }),
38
+ /* @__PURE__ */ o("rect", {
39
+ x: l,
40
+ y: x,
41
+ width: v,
42
+ height: Math.max(0, r),
43
+ rx: 4,
44
+ fill: S,
45
+ style: {
46
+ transformBox: "fill-box",
47
+ transformOrigin: "bottom",
48
+ transform: g || _ ? "scaleY(1)" : "scaleY(0)",
49
+ transition: _ ? void 0 : "transform 620ms cubic-bezier(0.22,1,0.36,1)",
50
+ transitionDelay: _ ? void 0 : `${n * 45}ms`
51
+ }
52
+ }),
53
+ p ? null : /* @__PURE__ */ o("text", {
54
+ x: c,
55
+ y: x - 6,
56
+ textAnchor: "middle",
57
+ fontSize: 10,
58
+ fontWeight: 600,
59
+ fill: "var(--color-text-secondary)",
60
+ style: {
61
+ opacity: g || _ ? 1 : 0,
62
+ transition: _ ? void 0 : "opacity 400ms ease",
63
+ transitionDelay: _ ? void 0 : `${n * 45 + 220}ms`
64
+ },
65
+ children: d(e.value)
66
+ }),
67
+ /* @__PURE__ */ o("text", {
68
+ x: c,
69
+ y: u - 8,
70
+ textAnchor: "middle",
71
+ fontSize: 10,
72
+ fill: "var(--color-chart-axis)",
73
+ children: e.label
74
+ })
75
+ ] }, `${e.label}-${n}`);
76
+ })] });
77
+ }
78
+ });
79
+ }
80
+ //#endregion
81
+ export { l as BarChart };
82
+
83
+ //# sourceMappingURL=BarChart.js.map
@@ -0,0 +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"}
@@ -0,0 +1,118 @@
1
+ import { ACCENT as e } from "./lib/palette.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { starPath as n } from "./lib/geometry.js";
4
+ import { Download as r } from "lucide-react";
5
+ import { Fragment as i, jsx as a, jsxs as o } from "react/jsx-runtime";
6
+ import { Button as s } from "@burdenoff/fe-libs/ui";
7
+ //#region src/crewfoundry/components/viz/CertificateCard.tsx
8
+ function c() {
9
+ return /* @__PURE__ */ o("svg", {
10
+ viewBox: "0 0 48 60",
11
+ width: 48,
12
+ height: 60,
13
+ "aria-hidden": "true",
14
+ style: {
15
+ color: e,
16
+ display: "block"
17
+ },
18
+ children: [
19
+ /* @__PURE__ */ a("path", {
20
+ d: "M18 38 L18 58 L24 52 L30 58 L30 38 Z",
21
+ fill: "currentColor",
22
+ opacity: .85
23
+ }),
24
+ /* @__PURE__ */ a("circle", {
25
+ cx: "24",
26
+ cy: "22",
27
+ r: "19",
28
+ fill: "currentColor",
29
+ opacity: .1
30
+ }),
31
+ /* @__PURE__ */ a("circle", {
32
+ cx: "24",
33
+ cy: "22",
34
+ r: "19",
35
+ fill: "none",
36
+ stroke: "currentColor",
37
+ strokeWidth: 2
38
+ }),
39
+ /* @__PURE__ */ a("circle", {
40
+ cx: "24",
41
+ cy: "22",
42
+ r: "14",
43
+ fill: "none",
44
+ stroke: "currentColor",
45
+ strokeWidth: 1,
46
+ opacity: .5
47
+ }),
48
+ /* @__PURE__ */ a("path", {
49
+ d: n(24, 22, 9, 3.8),
50
+ fill: "currentColor"
51
+ })
52
+ ]
53
+ });
54
+ }
55
+ function l({ title: e, recipient: n, date: l, issuer: u, credentialId: d, onDownload: f, downloadLabel: p = "Download", className: m }) {
56
+ return /* @__PURE__ */ o("div", {
57
+ className: t("relative flex flex-col gap-4 rounded-[var(--radius-card)] border-2 border-border-default bg-bg-surface p-5 shadow-elevation-1", "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-[\"\"]", m),
58
+ children: [
59
+ /* @__PURE__ */ o("div", {
60
+ className: "flex items-start justify-between gap-3",
61
+ children: [/* @__PURE__ */ o("div", {
62
+ className: "min-w-0",
63
+ children: [/* @__PURE__ */ a("p", {
64
+ className: "text-xs font-medium uppercase tracking-wide text-text-muted",
65
+ children: "Certificate of Completion"
66
+ }), /* @__PURE__ */ a("h3", {
67
+ className: "mt-1 truncate text-lg font-semibold text-text-primary",
68
+ title: e,
69
+ children: e
70
+ })]
71
+ }), /* @__PURE__ */ a(c, {})]
72
+ }),
73
+ /* @__PURE__ */ o("div", {
74
+ className: "min-w-0",
75
+ children: [/* @__PURE__ */ a("p", {
76
+ className: "text-xs text-text-muted",
77
+ children: "Awarded to"
78
+ }), /* @__PURE__ */ a("p", {
79
+ className: "truncate text-base font-medium text-text-primary",
80
+ title: n,
81
+ children: n
82
+ })]
83
+ }),
84
+ /* @__PURE__ */ o("div", {
85
+ className: "flex flex-wrap items-end justify-between gap-3 border-t border-border-subtle pt-3",
86
+ children: [/* @__PURE__ */ o("div", {
87
+ className: "flex flex-col gap-0.5 text-xs text-text-muted",
88
+ children: [/* @__PURE__ */ o("span", { children: [
89
+ "Issued ",
90
+ /* @__PURE__ */ a("span", {
91
+ className: "text-text-secondary",
92
+ children: l
93
+ }),
94
+ u ? /* @__PURE__ */ o(i, { children: [" · ", /* @__PURE__ */ a("span", {
95
+ className: "text-text-secondary",
96
+ children: u
97
+ })] }) : null
98
+ ] }), d ? /* @__PURE__ */ o("span", {
99
+ className: "tabular-nums",
100
+ children: ["ID: ", d]
101
+ }) : null]
102
+ }), f ? /* @__PURE__ */ o(s, {
103
+ variant: "outline",
104
+ size: "sm",
105
+ onClick: f,
106
+ children: [/* @__PURE__ */ a(r, {
107
+ "aria-hidden": "true",
108
+ className: "h-4 w-4"
109
+ }), p]
110
+ }) : null]
111
+ })
112
+ ]
113
+ });
114
+ }
115
+ //#endregion
116
+ export { l as CertificateCard };
117
+
118
+ //# sourceMappingURL=CertificateCard.js.map
@@ -0,0 +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"}
@@ -0,0 +1,109 @@
1
+ import { chartPalette as e } from "./lib/palette.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { useMounted as n, usePrefersReducedMotion as r } from "./lib/hooks.js";
4
+ import { donutSegmentPath as i } from "./lib/geometry.js";
5
+ import { formatCompact as a, toPercent as o } from "./lib/format.js";
6
+ import { jsx as s, jsxs as c } from "react/jsx-runtime";
7
+ //#region src/crewfoundry/components/viz/DonutChart.tsx
8
+ var l = 100, u = l / 2, d = 46;
9
+ function f({ segments: f, centerLabel: p, centerCaption: m, size: h = 168, thickness: g = .28, valueFormatter: _ = a, showLegend: v = !0, title: y, className: b }) {
10
+ let x = n(), S = r();
11
+ if (f.length === 0) return null;
12
+ let C = f.reduce((e, t) => e + Math.max(0, t.value), 0), w = e(f.length), T = d * (1 - g), E = y ?? `Donut chart of ${f.length} segments`, D = f.map((e) => C > 0 ? Math.max(0, e.value) / C : 0), O = D.map((e, t) => D.slice(0, t).reduce((e, t) => e + t, 0)), k = f.map((e, t) => {
13
+ let n = D[t], r = O[t] * 360, i = Math.min((O[t] + n) * 360, 359.999);
14
+ return {
15
+ segment: e,
16
+ color: e.color ?? w[t],
17
+ startAngle: r,
18
+ endAngle: i,
19
+ fraction: n,
20
+ isFull: n >= .9999
21
+ };
22
+ });
23
+ return /* @__PURE__ */ c("div", {
24
+ className: t("flex flex-wrap items-center gap-5", b),
25
+ children: [/* @__PURE__ */ c("div", {
26
+ className: "relative shrink-0",
27
+ style: {
28
+ width: h,
29
+ height: h,
30
+ maxWidth: "100%"
31
+ },
32
+ children: [/* @__PURE__ */ c("svg", {
33
+ viewBox: `0 0 ${l} ${l}`,
34
+ width: h,
35
+ height: h,
36
+ role: "img",
37
+ "aria-label": E,
38
+ style: {
39
+ display: "block",
40
+ maxWidth: "100%",
41
+ height: "auto"
42
+ },
43
+ children: [
44
+ /* @__PURE__ */ s("title", { children: E }),
45
+ /* @__PURE__ */ s("desc", { children: f.map((e) => `${e.label}: ${_(e.value)} (${o(e.value, C)}%)`).join(", ") }),
46
+ /* @__PURE__ */ s("g", {
47
+ style: {
48
+ transformOrigin: "center",
49
+ transform: x || S ? "scale(1)" : "scale(0.86)",
50
+ transition: S ? void 0 : "transform 520ms cubic-bezier(0.22,1,0.36,1)"
51
+ },
52
+ children: k.map((e, t) => {
53
+ let n = {
54
+ fill: e.color,
55
+ style: {
56
+ opacity: x || S ? 1 : 0,
57
+ transition: S ? void 0 : "opacity 460ms ease",
58
+ transitionDelay: S ? void 0 : `${t * 70}ms`
59
+ }
60
+ };
61
+ return e.isFull ? /* @__PURE__ */ s("path", {
62
+ d: `M ${u} ${u - d} A ${d} ${d} 0 1 0 ${u} ${u + d} A ${d} ${d} 0 1 0 ${u} ${u - d} M ${u} ${u - T} A ${T} ${T} 0 1 1 ${u} ${u + T} A ${T} ${T} 0 1 1 ${u} ${u - T} Z`,
63
+ fillRule: "evenodd",
64
+ ...n
65
+ }, `${e.segment.label}-${t}`) : /* @__PURE__ */ s("path", {
66
+ d: i(u, u, d, T, e.startAngle, e.endAngle),
67
+ ...n,
68
+ children: /* @__PURE__ */ s("title", { children: `${e.segment.label}: ${_(e.segment.value)}` })
69
+ }, `${e.segment.label}-${t}`);
70
+ })
71
+ })
72
+ ]
73
+ }), p !== void 0 || m !== void 0 ? /* @__PURE__ */ c("div", {
74
+ className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center text-center",
75
+ children: [/* @__PURE__ */ s("span", {
76
+ className: "text-xl font-semibold leading-none text-text-primary",
77
+ children: p
78
+ }), m === void 0 ? null : /* @__PURE__ */ s("span", {
79
+ className: "mt-1 text-xs text-text-muted",
80
+ children: m
81
+ })]
82
+ }) : null]
83
+ }), v ? /* @__PURE__ */ s("ul", {
84
+ className: "flex min-w-[8rem] flex-1 flex-col gap-1.5 text-sm",
85
+ children: k.map((e, t) => /* @__PURE__ */ c("li", {
86
+ className: "flex items-center gap-2",
87
+ children: [
88
+ /* @__PURE__ */ s("span", {
89
+ "aria-hidden": "true",
90
+ className: "inline-block h-2.5 w-2.5 shrink-0 rounded-full",
91
+ style: { backgroundColor: e.color }
92
+ }),
93
+ /* @__PURE__ */ s("span", {
94
+ className: "flex-1 truncate text-text-secondary",
95
+ children: e.segment.label
96
+ }),
97
+ /* @__PURE__ */ c("span", {
98
+ className: "tabular-nums text-text-muted",
99
+ children: [o(e.segment.value, C), "%"]
100
+ })
101
+ ]
102
+ }, `${e.segment.label}-${t}`))
103
+ }) : null]
104
+ });
105
+ }
106
+ //#endregion
107
+ export { f as DonutChart };
108
+
109
+ //# sourceMappingURL=DonutChart.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DonutChart.js","names":[],"sources":["../../../../src/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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"}
@@ -0,0 +1,54 @@
1
+ import { ACCENT as e } from "./lib/palette.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { useMounted as n, usePrefersReducedMotion as r } from "./lib/hooks.js";
4
+ import { formatCompact as i, toPercent as a } from "./lib/format.js";
5
+ import { jsx as o, jsxs as s } from "react/jsx-runtime";
6
+ //#region src/crewfoundry/components/viz/HBarFunnel.tsx
7
+ function c({ stages: c, valueFormatter: l = i, showConversion: u = !0, className: d }) {
8
+ let f = n(), p = r();
9
+ if (c.length === 0) return null;
10
+ let m = Math.max(c[0]?.value ?? 0, 1);
11
+ return /* @__PURE__ */ o("ol", {
12
+ className: t("flex flex-col gap-2", d),
13
+ children: c.map((t, n) => {
14
+ let r = a(t.value, m, 1), i = n === 0 ? 100 : a(t.value, c[n - 1].value, 1), d = 1 - n / Math.max(c.length, 2) * .62;
15
+ return /* @__PURE__ */ s("li", {
16
+ className: "flex flex-col gap-1",
17
+ children: [/* @__PURE__ */ s("div", {
18
+ className: "flex items-baseline justify-between text-sm",
19
+ children: [/* @__PURE__ */ o("span", {
20
+ className: "font-medium text-text-primary",
21
+ children: t.label
22
+ }), /* @__PURE__ */ s("span", {
23
+ className: "tabular-nums text-text-secondary",
24
+ children: [l(t.value), u && n > 0 ? /* @__PURE__ */ s("span", {
25
+ className: "ml-2 text-xs text-text-muted",
26
+ children: [i, "%"]
27
+ }) : null]
28
+ })]
29
+ }), /* @__PURE__ */ o("div", {
30
+ className: "h-7 w-full overflow-hidden rounded-[var(--radius-input)] bg-bg-sunken",
31
+ role: "progressbar",
32
+ "aria-valuenow": Math.round(r),
33
+ "aria-valuemin": 0,
34
+ "aria-valuemax": 100,
35
+ "aria-label": `${t.label}: ${l(t.value)}`,
36
+ children: /* @__PURE__ */ o("div", {
37
+ className: "h-full rounded-[var(--radius-input)]",
38
+ style: {
39
+ width: f || p ? `${r}%` : "0%",
40
+ backgroundColor: e,
41
+ opacity: d,
42
+ transition: p ? void 0 : "width 720ms cubic-bezier(0.22,1,0.36,1)",
43
+ transitionDelay: p ? void 0 : `${n * 70}ms`
44
+ }
45
+ })
46
+ })]
47
+ }, `${t.label}-${n}`);
48
+ })
49
+ });
50
+ }
51
+ //#endregion
52
+ export { c as HBarFunnel };
53
+
54
+ //# sourceMappingURL=HBarFunnel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HBarFunnel.js","names":[],"sources":["../../../../src/crewfoundry/components/viz/HBarFunnel.tsx"],"sourcesContent":["import { cn } from '../../utils/cn';\nimport { ACCENT } from './lib/palette';\nimport { formatCompact, toPercent } from './lib/format';\nimport { useMounted, usePrefersReducedMotion } from './lib/hooks';\nimport type { LabeledValue } from './lib/types';\n\nexport interface HBarFunnelProps {\n /** Ordered funnel stages, largest first. */\n stages: LabeledValue[];\n valueFormatter?: (value: number) => string;\n /** Show conversion % relative to the first stage. */\n showConversion?: boolean;\n className?: string;\n}\n\n/**\n * Horizontal descending funnel. Each stage renders as a bar whose width is\n * proportional to the top-of-funnel value; the accent fades down the funnel so\n * the drop-off reads at a glance. Built with CSS bars for crisp, real text.\n */\nexport function HBarFunnel({\n stages,\n valueFormatter = formatCompact,\n showConversion = true,\n className,\n}: HBarFunnelProps) {\n const mounted = useMounted();\n const reduced = usePrefersReducedMotion();\n\n if (stages.length === 0) {\n return null;\n }\n\n const top = Math.max(stages[0]?.value ?? 0, 1);\n\n return (\n <ol className={cn('flex flex-col gap-2', className)}>\n {stages.map((stage, i) => {\n const pct = toPercent(stage.value, top, 1);\n const conversion = i === 0 ? 100 : toPercent(stage.value, stages[i - 1].value, 1);\n const fillOpacity = 1 - (i / Math.max(stages.length, 2)) * 0.62;\n return (\n <li key={`${stage.label}-${i}`} className=\"flex flex-col gap-1\">\n <div className=\"flex items-baseline justify-between text-sm\">\n <span className=\"font-medium text-text-primary\">{stage.label}</span>\n <span className=\"tabular-nums text-text-secondary\">\n {valueFormatter(stage.value)}\n {showConversion && i > 0 ? (\n <span className=\"ml-2 text-xs text-text-muted\">{conversion}%</span>\n ) : null}\n </span>\n </div>\n <div\n className=\"h-7 w-full overflow-hidden rounded-[var(--radius-input)] bg-bg-sunken\"\n role=\"progressbar\"\n aria-valuenow={Math.round(pct)}\n aria-valuemin={0}\n aria-valuemax={100}\n aria-label={`${stage.label}: ${valueFormatter(stage.value)}`}\n >\n <div\n className=\"h-full rounded-[var(--radius-input)]\"\n style={{\n width: mounted || reduced ? `${pct}%` : '0%',\n backgroundColor: ACCENT,\n opacity: fillOpacity,\n transition: reduced ? undefined : 'width 720ms cubic-bezier(0.22,1,0.36,1)',\n transitionDelay: reduced ? undefined : `${i * 70}ms`,\n }}\n />\n </div>\n </li>\n );\n })}\n </ol>\n );\n}\n"],"mappings":";;;;;;AAoBA,SAAgB,EAAW,EACzB,WACA,oBAAiB,GACjB,oBAAiB,IACjB,gBACkB;CAClB,IAAM,IAAU,GAAY,EACtB,IAAU,GAAyB;AAEzC,KAAI,EAAO,WAAW,EACpB,QAAO;CAGT,IAAM,IAAM,KAAK,IAAI,EAAO,IAAI,SAAS,GAAG,EAAE;AAE9C,QACE,kBAAC,MAAD;EAAI,WAAW,EAAG,uBAAuB,EAAU;YAChD,EAAO,KAAK,GAAO,MAAM;GACxB,IAAM,IAAM,EAAU,EAAM,OAAO,GAAK,EAAE,EACpC,IAAa,MAAM,IAAI,MAAM,EAAU,EAAM,OAAO,EAAO,IAAI,GAAG,OAAO,EAAE,EAC3E,IAAc,IAAK,IAAI,KAAK,IAAI,EAAO,QAAQ,EAAE,GAAI;AAC3D,UACE,kBAAC,MAAD;IAAgC,WAAU;cAA1C,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,QAAD;MAAM,WAAU;gBAAiC,EAAM;MAAa,CAAA,EACpE,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACG,EAAe,EAAM,MAAM,EAC3B,KAAkB,IAAI,IACrB,kBAAC,QAAD;OAAM,WAAU;iBAAhB,CAAgD,GAAW,IAAQ;WACjE,KACC;QACH;QACN,kBAAC,OAAD;KACE,WAAU;KACV,MAAK;KACL,iBAAe,KAAK,MAAM,EAAI;KAC9B,iBAAe;KACf,iBAAe;KACf,cAAY,GAAG,EAAM,MAAM,IAAI,EAAe,EAAM,MAAM;eAE1D,kBAAC,OAAD;MACE,WAAU;MACV,OAAO;OACL,OAAO,KAAW,IAAU,GAAG,EAAI,KAAK;OACxC,iBAAiB;OACjB,SAAS;OACT,YAAY,IAAU,KAAA,IAAY;OAClC,iBAAiB,IAAU,KAAA,IAAY,GAAG,IAAI,GAAG;OAClD;MACD,CAAA;KACE,CAAA,CACH;MA7BI,GAAG,EAAM,MAAM,GAAG,IA6BtB;IAEP;EACC,CAAA"}
@@ -0,0 +1,91 @@
1
+ import { ACCENT as e } from "./lib/palette.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { useMounted as n, usePrefersReducedMotion as r } from "./lib/hooks.js";
4
+ import { clamp as i } from "./lib/geometry.js";
5
+ import { jsx as a, jsxs as o } from "react/jsx-runtime";
6
+ //#region src/crewfoundry/components/viz/Heatmap.tsx
7
+ function s(e, t) {
8
+ return e <= 0 || t <= 0 ? 0 : i(.14 + e / t * .86, 0, 1);
9
+ }
10
+ function c({ rows: i, colLabels: c, legend: l = !0, showValues: u = !1, valueFormatter: d = (e) => String(e), title: f, className: p }) {
11
+ let m = n(), h = r();
12
+ if (i.length === 0) return null;
13
+ let g = Math.max(1, ...i.flatMap((e) => e.cells.map((e) => e.value))), _ = f ?? "Heatmap";
14
+ return /* @__PURE__ */ o("div", {
15
+ className: t("flex flex-col gap-3", p),
16
+ children: [/* @__PURE__ */ a("div", {
17
+ className: "overflow-x-auto",
18
+ children: /* @__PURE__ */ o("table", {
19
+ className: "border-separate border-spacing-1",
20
+ style: { minWidth: "min-content" },
21
+ children: [
22
+ /* @__PURE__ */ a("caption", {
23
+ className: "sr-only",
24
+ children: _
25
+ }),
26
+ c ? /* @__PURE__ */ a("thead", { children: /* @__PURE__ */ o("tr", { children: [/* @__PURE__ */ a("th", { className: "w-px" }), c.map((e, t) => /* @__PURE__ */ a("th", {
27
+ scope: "col",
28
+ className: "px-1 pb-1 text-center text-xs font-medium text-text-muted",
29
+ children: e
30
+ }, `col-${t}`))] }) }) : null,
31
+ /* @__PURE__ */ a("tbody", { children: i.map((t, n) => /* @__PURE__ */ o("tr", { children: [/* @__PURE__ */ a("th", {
32
+ scope: "row",
33
+ className: "whitespace-nowrap pr-2 text-right text-xs font-medium text-text-secondary",
34
+ children: t.label
35
+ }), t.cells.map((r, i) => {
36
+ let l = r.max ?? g, f = s(r.value, l), p = h ? 0 : (n + i) * 22;
37
+ return /* @__PURE__ */ a("td", {
38
+ className: "p-0",
39
+ children: /* @__PURE__ */ o("div", {
40
+ 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",
41
+ title: `${t.label}${c?.[i] ? ` · ${c[i]}` : ""}: ${d(r.value)}`,
42
+ children: [/* @__PURE__ */ a("span", {
43
+ "aria-hidden": "true",
44
+ className: "absolute inset-0 rounded-[var(--radius-input)]",
45
+ style: {
46
+ backgroundColor: e,
47
+ opacity: m || h ? f : 0,
48
+ transition: h ? void 0 : "opacity 420ms ease",
49
+ transitionDelay: h ? void 0 : `${p}ms`
50
+ }
51
+ }), u && r.value > 0 ? /* @__PURE__ */ a("span", {
52
+ className: "relative",
53
+ style: { color: f > .55 ? "var(--color-text-inverse)" : "var(--color-text-secondary)" },
54
+ children: d(r.value)
55
+ }) : null]
56
+ })
57
+ }, `cell-${n}-${i}`);
58
+ })] }, `row-${n}`)) })
59
+ ]
60
+ })
61
+ }), l ? /* @__PURE__ */ o("div", {
62
+ className: "flex items-center gap-2 text-xs text-text-muted",
63
+ children: [
64
+ /* @__PURE__ */ a("span", { children: "Less" }),
65
+ /* @__PURE__ */ a("span", {
66
+ className: "flex gap-1",
67
+ children: [
68
+ .14,
69
+ .32,
70
+ .5,
71
+ .68,
72
+ .86,
73
+ 1
74
+ ].map((t, n) => /* @__PURE__ */ a("span", {
75
+ "aria-hidden": "true",
76
+ className: "h-3 w-3 rounded-[3px] border border-border-subtle",
77
+ style: {
78
+ backgroundColor: e,
79
+ opacity: t
80
+ }
81
+ }, n))
82
+ }),
83
+ /* @__PURE__ */ a("span", { children: "More" })
84
+ ]
85
+ }) : null]
86
+ });
87
+ }
88
+ //#endregion
89
+ export { c as Heatmap };
90
+
91
+ //# sourceMappingURL=Heatmap.js.map
@@ -0,0 +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"}