@burdenoff/microfe-planmagnet 2026.930.21 → 2026.930.23

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 (127) hide show
  1. package/dist/node_modules/lodash/_arrayEach.js +14 -0
  2. package/dist/node_modules/lodash/_arrayEach.js.map +1 -0
  3. package/dist/node_modules/lodash/_assignValue.js +17 -0
  4. package/dist/node_modules/lodash/_assignValue.js.map +1 -0
  5. package/dist/node_modules/lodash/_baseAssign.js +16 -0
  6. package/dist/node_modules/lodash/_baseAssign.js.map +1 -0
  7. package/dist/node_modules/lodash/_baseAssignIn.js +16 -0
  8. package/dist/node_modules/lodash/_baseAssignIn.js.map +1 -0
  9. package/dist/node_modules/lodash/_baseClone.js +64 -0
  10. package/dist/node_modules/lodash/_baseClone.js.map +1 -0
  11. package/dist/node_modules/lodash/_baseCreate.js +21 -0
  12. package/dist/node_modules/lodash/_baseCreate.js.map +1 -0
  13. package/dist/node_modules/lodash/_baseIsMap.js +16 -0
  14. package/dist/node_modules/lodash/_baseIsMap.js.map +1 -0
  15. package/dist/node_modules/lodash/_baseIsSet.js +16 -0
  16. package/dist/node_modules/lodash/_baseIsSet.js.map +1 -0
  17. package/dist/node_modules/lodash/_baseKeysIn.js +20 -0
  18. package/dist/node_modules/lodash/_baseKeysIn.js.map +1 -0
  19. package/dist/node_modules/lodash/_baseSum.js +17 -0
  20. package/dist/node_modules/lodash/_baseSum.js.map +1 -0
  21. package/dist/node_modules/lodash/_baseUnset.js +26 -0
  22. package/dist/node_modules/lodash/_baseUnset.js.map +1 -0
  23. package/dist/node_modules/lodash/_cloneArrayBuffer.js +16 -0
  24. package/dist/node_modules/lodash/_cloneArrayBuffer.js.map +1 -0
  25. package/dist/node_modules/lodash/_cloneBuffer.js +17 -0
  26. package/dist/node_modules/lodash/_cloneBuffer.js.map +1 -0
  27. package/dist/node_modules/lodash/_cloneDataView.js +16 -0
  28. package/dist/node_modules/lodash/_cloneDataView.js.map +1 -0
  29. package/dist/node_modules/lodash/_cloneRegExp.js +15 -0
  30. package/dist/node_modules/lodash/_cloneRegExp.js.map +1 -0
  31. package/dist/node_modules/lodash/_cloneSymbol.js +15 -0
  32. package/dist/node_modules/lodash/_cloneSymbol.js.map +1 -0
  33. package/dist/node_modules/lodash/_cloneTypedArray.js +16 -0
  34. package/dist/node_modules/lodash/_cloneTypedArray.js.map +1 -0
  35. package/dist/node_modules/lodash/_copyArray.js +15 -0
  36. package/dist/node_modules/lodash/_copyArray.js.map +1 -0
  37. package/dist/node_modules/lodash/_copyObject.js +22 -0
  38. package/dist/node_modules/lodash/_copyObject.js.map +1 -0
  39. package/dist/node_modules/lodash/_copySymbols.js +16 -0
  40. package/dist/node_modules/lodash/_copySymbols.js.map +1 -0
  41. package/dist/node_modules/lodash/_copySymbolsIn.js +16 -0
  42. package/dist/node_modules/lodash/_copySymbolsIn.js.map +1 -0
  43. package/dist/node_modules/lodash/_customOmitClone.js +15 -0
  44. package/dist/node_modules/lodash/_customOmitClone.js.map +1 -0
  45. package/dist/node_modules/lodash/_flatRest.js +17 -0
  46. package/dist/node_modules/lodash/_flatRest.js.map +1 -0
  47. package/dist/node_modules/lodash/_getAllKeysIn.js +17 -0
  48. package/dist/node_modules/lodash/_getAllKeysIn.js.map +1 -0
  49. package/dist/node_modules/lodash/_getSymbolsIn.js +18 -0
  50. package/dist/node_modules/lodash/_getSymbolsIn.js.map +1 -0
  51. package/dist/node_modules/lodash/_initCloneArray.js +15 -0
  52. package/dist/node_modules/lodash/_initCloneArray.js.map +1 -0
  53. package/dist/node_modules/lodash/_initCloneByTag.js +40 -0
  54. package/dist/node_modules/lodash/_initCloneByTag.js.map +1 -0
  55. package/dist/node_modules/lodash/_initCloneObject.js +17 -0
  56. package/dist/node_modules/lodash/_initCloneObject.js.map +1 -0
  57. package/dist/node_modules/lodash/_nativeKeysIn.js +15 -0
  58. package/dist/node_modules/lodash/_nativeKeysIn.js.map +1 -0
  59. package/dist/node_modules/lodash/_parent.js +16 -0
  60. package/dist/node_modules/lodash/_parent.js.map +1 -0
  61. package/dist/node_modules/lodash/flatten.js +15 -0
  62. package/dist/node_modules/lodash/flatten.js.map +1 -0
  63. package/dist/node_modules/lodash/isMap.js +14 -0
  64. package/dist/node_modules/lodash/isMap.js.map +1 -0
  65. package/dist/node_modules/lodash/isSet.js +14 -0
  66. package/dist/node_modules/lodash/isSet.js.map +1 -0
  67. package/dist/node_modules/lodash/keysIn.js +17 -0
  68. package/dist/node_modules/lodash/keysIn.js.map +1 -0
  69. package/dist/node_modules/lodash/maxBy.js +17 -0
  70. package/dist/node_modules/lodash/maxBy.js.map +1 -0
  71. package/dist/node_modules/lodash/omit.js +28 -0
  72. package/dist/node_modules/lodash/omit.js.map +1 -0
  73. package/dist/node_modules/lodash/sumBy.js +16 -0
  74. package/dist/node_modules/lodash/sumBy.js.map +1 -0
  75. package/dist/node_modules/recharts/es6/chart/Sankey.js +556 -0
  76. package/dist/node_modules/recharts/es6/chart/Sankey.js.map +1 -0
  77. package/dist/node_modules/recharts/es6/chart/Treemap.js +545 -0
  78. package/dist/node_modules/recharts/es6/chart/Treemap.js.map +1 -0
  79. package/dist/node_modules/recharts/es6/shape/Polygon.js +112 -0
  80. package/dist/node_modules/recharts/es6/shape/Polygon.js.map +1 -0
  81. package/dist/node_modules/recharts/es6/util/Constants.js +31 -0
  82. package/dist/node_modules/recharts/es6/util/Constants.js.map +1 -0
  83. package/dist/node_modules/recharts/es6/util/ReactUtils.js +15 -10
  84. package/dist/node_modules/recharts/es6/util/ReactUtils.js.map +1 -1
  85. package/dist/planmagnet/controls/AutomationRuleControls.js +10 -10
  86. package/dist/planmagnet/controls/CompetitorControls.js +7 -7
  87. package/dist/planmagnet/controls/FeatureControls.js +38 -38
  88. package/dist/planmagnet/controls/ProgramControls.js +8 -8
  89. package/dist/planmagnet/dependency/DependencyGraph.d.ts.map +1 -1
  90. package/dist/planmagnet/dependency/DependencyGraph.js +13 -10
  91. package/dist/planmagnet/dependency/DependencyGraph.js.map +1 -1
  92. package/dist/planmagnet/dependency/DependencyPanel.js +13 -13
  93. package/dist/planmagnet/dependency/GanttChart.d.ts.map +1 -1
  94. package/dist/planmagnet/dependency/GanttChart.js +11 -5
  95. package/dist/planmagnet/dependency/GanttChart.js.map +1 -1
  96. package/dist/planmagnet/pages/ActivityActivityPagePage.js +16 -16
  97. package/dist/planmagnet/pages/ActivityActivityPagePage.js.map +1 -1
  98. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.d.ts.map +1 -1
  99. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js +91 -73
  100. package/dist/planmagnet/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
  101. package/dist/planmagnet/pages/AutomationAutomationRulesPagePage.js +6 -6
  102. package/dist/planmagnet/pages/AutomationCreateAutomationRulePagePage.js +4 -4
  103. package/dist/planmagnet/pages/DashboardDashboardPagePage.js +11 -11
  104. package/dist/planmagnet/pages/DashboardsProjectManagerPagePage.js +5 -5
  105. package/dist/planmagnet/pages/DashboardsTeamUtilizationPagePage.js +5 -5
  106. package/dist/planmagnet/pages/IntegrationsNewSyncPagePage.js +6 -6
  107. package/dist/planmagnet/pages/IntegrationsNewSyncPagePage.js.map +1 -1
  108. package/dist/planmagnet/pages/IntegrationsSyncsPagePage.js +8 -8
  109. package/dist/planmagnet/pages/ProjectsBudgetsPagePage.js +10 -10
  110. package/dist/planmagnet/pages/ProjectsCriticalPathPagePage.js +6 -6
  111. package/dist/planmagnet/pages/ProjectsEditProjectPagePage.js +5 -5
  112. package/dist/planmagnet/pages/ProjectsPlanningPagePage.js +8 -8
  113. package/dist/planmagnet/pages/SprintsStartSprintPagePage.js +4 -4
  114. package/dist/planmagnet/pages/analytics/StatusFlowCard.d.ts +8 -0
  115. package/dist/planmagnet/pages/analytics/StatusFlowCard.d.ts.map +1 -0
  116. package/dist/planmagnet/pages/analytics/StatusFlowCard.js +234 -0
  117. package/dist/planmagnet/pages/analytics/StatusFlowCard.js.map +1 -0
  118. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.d.ts +6 -0
  119. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.d.ts.map +1 -0
  120. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +367 -0
  121. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js.map +1 -0
  122. package/dist/planmagnet/roadmaps/RoadmapControls.js +17 -17
  123. package/dist/planmagnet/utils/statusColor.d.ts +8 -9
  124. package/dist/planmagnet/utils/statusColor.d.ts.map +1 -1
  125. package/dist/planmagnet/utils/statusColor.js +8 -40
  126. package/dist/planmagnet/utils/statusColor.js.map +1 -1
  127. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WorkBreakdownCard.js","names":[],"sources":["../../../../src/planmagnet/pages/analytics/WorkBreakdownCard.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { gql } from '@apollo/client';\nimport { useQuery } from '@apollo/client/react';\nimport { Card, CardContent, CardHeader, CardTitle } from '@burdenoff/fe-libs/ui';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { LayoutGrid } from 'lucide-react';\nimport { ResponsiveContainer, Tooltip, Treemap } from 'recharts';\nimport { usePlanMagnetNavigate } from '../../providers/PlanMagnetProvider';\nimport { rowLinkProps } from '../../utils/rowNavigation';\nimport { isForbidden } from '../../utils/graphqlErrors';\nimport { truncate } from '../../dependency/DependencyGraph';\n\n/**\n * ANR-24: work breakdown Treemap (wspace-planmagnet-svc#65 `planmagnetWorkBreakdown`) — live (not date-ranged;\n * see the field's own doc comment) open/done counts grouped project -> epic, with a \"No epic\" bucket per\n * project for items an epic rollup doesn't reach (see PlanmagnetNoEpicWorkBreakdown's doc comment: direct\n * children only, same as PlanmagnetEpicWorkBreakdown — a sub-task nested two levels under an epic falls here\n * too, not under its grandparent epic).\n *\n * recharts' Treemap computes a node's own size as the SUM of its children's sizes when `node.children` is set\n * (see `computeNode` in recharts' source) — so each project's own cell size, and its done-ratio color, both\n * come from summing this card's own visualized epic + \"No epic\" leaves, not from the server's separate\n * project-level totals (which include every live item in the project, not only the ones a leaf here\n * represents — see PlanmagnetProjectWorkBreakdown's doc comment). What the treemap draws and what the table\n * below it lists are therefore always the same numbers.\n */\n\ntype BreakdownMetric = 'items' | 'points';\n\nconst GET_WORK_BREAKDOWN = gql`\n query GetAnalyticsWorkBreakdown($workspaceId: ID!, $projectIds: [ID!]) {\n planmagnetWorkBreakdown(workspaceId: $workspaceId, projectIds: $projectIds) {\n truncatedProjects\n projects {\n projectId\n projectName\n truncatedEpics\n epics {\n epicId\n epicTitle\n openCount\n doneCount\n openPoints\n donePoints\n }\n noEpic {\n openCount\n doneCount\n openPoints\n donePoints\n }\n }\n }\n }\n`;\n\ninterface EpicBreakdown {\n epicId: string;\n epicTitle: string;\n openCount: number;\n doneCount: number;\n openPoints: number;\n donePoints: number;\n}\n\ninterface NoEpicBreakdown {\n openCount: number;\n doneCount: number;\n openPoints: number;\n donePoints: number;\n}\n\ninterface ProjectBreakdown {\n projectId: string;\n projectName: string;\n truncatedEpics: boolean;\n epics: EpicBreakdown[];\n noEpic: NoEpicBreakdown;\n}\n\ninterface WorkBreakdownData {\n planmagnetWorkBreakdown: {\n truncatedProjects: boolean;\n projects: ProjectBreakdown[];\n };\n}\n\ninterface CountsLike {\n openCount: number;\n doneCount: number;\n openPoints: number;\n donePoints: number;\n}\n\nfunction metricTotals(c: CountsLike, metric: BreakdownMetric): { size: number; done: number } {\n return metric === 'items'\n ? { size: c.openCount + c.doneCount, done: c.doneCount }\n : { size: c.openPoints + c.donePoints, done: c.donePoints };\n}\n\nfunction doneRatioOf(c: CountsLike, metric: BreakdownMetric): number | null {\n const { size, done } = metricTotals(c, metric);\n return size > 0 ? done / size : null;\n}\n\ninterface BreakdownLeafDatum extends CountsLike {\n name: string;\n size: number;\n doneRatio: number | null;\n /** null = not an individually openable work item (the project-group frame, or the \"No epic\" bucket). */\n epicId: string | null;\n isNoEpic: boolean;\n isHeader: boolean;\n projectId: string;\n projectName: string;\n}\n\ninterface BreakdownGroupDatum {\n name: string;\n projectId: string;\n children: BreakdownLeafDatum[];\n}\n\n/** A label cell reserves this share of each project's own area (never less than HEADER_MIN_SIZE). Small relative\n * to its real epic siblings so it barely affects the epics' own visual weight, but consistently sized so every\n * project's header reads the same size. */\nconst HEADER_FRACTION = 0.14;\nconst HEADER_MIN_SIZE = 1;\n\nfunction buildBreakdownGroups(\n projects: ProjectBreakdown[],\n metric: BreakdownMetric,\n noEpicLabel: string\n): BreakdownGroupDatum[] {\n const groups: BreakdownGroupDatum[] = [];\n\n for (const project of projects) {\n const epicLeaves: BreakdownLeafDatum[] = project.epics\n .map((epic) => ({\n name: epic.epicTitle,\n size: metricTotals(epic, metric).size,\n doneRatio: doneRatioOf(epic, metric),\n epicId: epic.epicId,\n isNoEpic: false,\n isHeader: false,\n projectId: project.projectId,\n projectName: project.projectName,\n openCount: epic.openCount,\n doneCount: epic.doneCount,\n openPoints: epic.openPoints,\n donePoints: epic.donePoints,\n }))\n .filter((leaf) => leaf.size > 0);\n\n const noEpicSize = metricTotals(project.noEpic, metric).size;\n if (noEpicSize > 0) {\n epicLeaves.push({\n name: noEpicLabel,\n size: noEpicSize,\n doneRatio: doneRatioOf(project.noEpic, metric),\n epicId: null,\n isNoEpic: true,\n isHeader: false,\n projectId: project.projectId,\n projectName: project.projectName,\n openCount: project.noEpic.openCount,\n doneCount: project.noEpic.doneCount,\n openPoints: project.noEpic.openPoints,\n donePoints: project.noEpic.donePoints,\n });\n }\n\n const totalSize = epicLeaves.reduce((sum, leaf) => sum + leaf.size, 0);\n if (totalSize <= 0) continue;\n\n // A small leading child in a list of much-larger siblings reliably becomes its own single-item row under\n // squarify's row-packing (recharts' Treemap layout): adding the next, much bigger sibling to that same row\n // would immediately worsen the row's aspect ratio, so squarify closes the row after just this one item —\n // which a single-item row always stretches to the full width (or height) of its parent rect. That makes\n // this synthetic \"header\" leaf a reliable label strip across the top (or side) of each project's own area,\n // without needing a second, hand-rolled layout pass. See WorkBreakdownCell's `isHeader` branch for the\n // render side of this.\n const headerLeaf: BreakdownLeafDatum = {\n name: project.projectName,\n size: Math.max(Math.round(totalSize * HEADER_FRACTION), HEADER_MIN_SIZE),\n doneRatio: null,\n epicId: null,\n isNoEpic: false,\n isHeader: true,\n projectId: project.projectId,\n projectName: project.projectName,\n openCount: 0,\n doneCount: 0,\n openPoints: 0,\n donePoints: 0,\n };\n\n groups.push({\n name: project.projectName,\n projectId: project.projectId,\n children: [headerLeaf, ...epicLeaves],\n });\n }\n\n return groups;\n}\n\nfunction ratioToFillOpacity(doneRatio: number | null): number {\n // Sequential, single-hue encoding on DONE's own chart color (the same hue statusVisual.ts assigns to the DONE\n // status everywhere else in this app): lighter = mostly open, darker = mostly done. Never fully transparent,\n // so a 0%-done cell still reads as a populated cell rather than empty space — and the exact ratio is always\n // available in the tooltip and the table below, so this fill is a supplementary channel, never the only way\n // to read it.\n if (doneRatio === null) return 0.12;\n return 0.18 + doneRatio * 0.72;\n}\n\ninterface WorkBreakdownCellProps {\n x?: number;\n y?: number;\n width?: number;\n height?: number;\n depth?: number;\n name?: string;\n isHeader?: boolean;\n isNoEpic?: boolean;\n epicId?: string | null;\n doneRatio?: number | null;\n onOpenEpic: (epicId: string) => void;\n noEpicLabel: string;\n}\n\nfunction WorkBreakdownCell(props: WorkBreakdownCellProps) {\n const {\n x = 0,\n y = 0,\n width = 0,\n height = 0,\n depth = 0,\n name = '',\n isHeader = false,\n isNoEpic = false,\n epicId = null,\n doneRatio = null,\n onOpenEpic,\n noEpicLabel,\n } = props;\n\n if (width <= 0 || height <= 0) return null;\n\n if (depth === 0) {\n // recharts wraps the ENTIRE `data` array (every project combined) in one synthetic root at depth 0\n // (see `computeNode` / `getDerivedStateFromProps` in recharts' own Treemap source) — this fires exactly\n // once for the whole chart area, never once per project. It has no boundary of its own to draw (a rect\n // here would be one border around every project combined, not a per-project frame), so render nothing.\n return null;\n }\n\n if (depth === 1) {\n // The real per-project frame: recharts assigns depth 1 to each top-level entry of `data` — i.e. one\n // `BreakdownGroupDatum` per project — so this branch fires once per project, at that project's own\n // bounds. Drawn first (recharts renders a non-leaf node's own content, then its children, in that\n // order), so the header + epic leaves at depth 2 paint on top of it within the same bounds.\n // Never filled: fill is reserved for done-ratio below, so the frame reads purely as a boundary.\n return (\n <rect\n x={x}\n y={y}\n width={width}\n height={height}\n rx={4}\n className=\"fill-none stroke-border\"\n strokeWidth={1}\n />\n );\n }\n\n const label = isNoEpic ? noEpicLabel : name;\n const showLabel = width > 28 && height > 14;\n const fitChars = Math.max(1, Math.floor((width - 8) / 6));\n const displayLabel = truncate(label, fitChars);\n\n if (isHeader) {\n return (\n <g>\n <rect x={x} y={y} width={width} height={height} className=\"fill-muted\" />\n {showLabel && (\n <text\n x={x + 4}\n y={y + height / 2}\n dy=\"0.32em\"\n className=\"fill-foreground text-[11px] font-medium\"\n >\n {displayLabel}\n </text>\n )}\n </g>\n );\n }\n\n const cellRect = (\n <rect\n x={x}\n y={y}\n width={width}\n height={height}\n className=\"stroke-background\"\n style={{ fill: 'var(--color-chart-2)', fillOpacity: ratioToFillOpacity(doneRatio) }}\n strokeWidth={2}\n />\n );\n const cellLabel = showLabel && (\n <text x={x + 4} y={y + height / 2} dy=\"0.32em\" className=\"fill-foreground text-[11px]\">\n {displayLabel}\n </text>\n );\n\n if (!epicId) {\n // \"No epic\" bucket — an aggregate of several work items, not one item, so there is nothing to navigate to.\n return (\n <g>\n {cellRect}\n {cellLabel}\n </g>\n );\n }\n\n const accessibleLabel = `${name} — ${Math.round((doneRatio ?? 0) * 100)}% done`;\n return (\n <g\n {...rowLinkProps<SVGGElement>(() => onOpenEpic(epicId), accessibleLabel)}\n className=\"cursor-pointer focus:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary focus-visible:outline-offset-2\"\n >\n <title>{accessibleLabel}</title>\n {cellRect}\n {cellLabel}\n </g>\n );\n}\n\nexport function WorkBreakdownCard({\n workspaceId,\n projectId,\n}: {\n workspaceId?: string | null;\n /** ANR-09 project scope. Omitted/undefined = every project (up to 50 — see truncatedProjects below). */\n projectId?: string;\n}) {\n const { t } = useI18n();\n const navigate = usePlanMagnetNavigate();\n const [metric, setMetric] = useState<BreakdownMetric>('items');\n const { data, loading, error } = useQuery<WorkBreakdownData>(GET_WORK_BREAKDOWN, {\n variables: { workspaceId, projectIds: projectId ? [projectId] : undefined },\n skip: !workspaceId,\n });\n\n const noEpicLabel = t('planmagnet.analytics.workBreakdown.noEpic', 'No epic');\n const breakdown = data?.planmagnetWorkBreakdown;\n const projects = breakdown?.projects ?? [];\n const groups = buildBreakdownGroups(projects, metric, noEpicLabel);\n const truncatedProjectNames = projects.filter((p) => p.truncatedEpics).map((p) => p.projectName);\n\n function openEpic(epicId: string) {\n navigate(`/tasks/task-detail-page?taskId=${encodeURIComponent(epicId)}`);\n }\n\n return (\n <Card data-testid=\"work-breakdown-card\">\n <CardHeader>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <CardTitle className=\"flex items-center gap-2\">\n <LayoutGrid className=\"h-4 w-4\" />\n {t('planmagnet.analytics.workBreakdown.title', 'Work breakdown')}\n </CardTitle>\n <div\n className=\"flex items-center gap-1 rounded-md border p-0.5\"\n role=\"group\"\n aria-label={t('planmagnet.analytics.workBreakdown.metricLabel', 'Size by')}\n data-testid=\"viz-breakdown-metric\"\n >\n {(['items', 'points'] as const).map((m) => (\n <button\n key={m}\n type=\"button\"\n aria-pressed={metric === m}\n onClick={() => setMetric(m)}\n className={`px-2 py-1 text-xs rounded ${\n metric === m\n ? 'bg-primary text-primary-foreground'\n : 'text-muted-foreground hover:bg-muted'\n }`}\n >\n {m === 'items'\n ? t('planmagnet.analytics.workBreakdown.items', 'Items')\n : t('planmagnet.analytics.workBreakdown.points', 'Story points')}\n </button>\n ))}\n </div>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.workBreakdown.subtitle',\n 'Live open/done work, by project and epic — current state, not limited by the date range above.'\n )}\n </p>\n </CardHeader>\n <CardContent>\n {loading && !data ? (\n <div className=\"flex justify-center py-12\">\n <div className=\"animate-spin h-6 w-6 border-2 border-primary border-t-transparent rounded-full\" />\n </div>\n ) : error ? (\n <p className=\"text-sm text-status-error-text\">\n {isForbidden(error)\n ? t(\n 'planmagnet.analytics.workBreakdown.forbidden',\n 'You do not have access to the work breakdown for this scope.'\n )\n : `${t('planmagnet.analytics.workBreakdown.error', 'Work breakdown could not be loaded')}: ${error.message}`}\n </p>\n ) : groups.length === 0 ? (\n <p className=\"text-sm text-muted-foreground py-8 text-center\">\n {t(\n 'planmagnet.analytics.workBreakdown.empty',\n 'No live work items found for this scope.'\n )}\n </p>\n ) : (\n <div className=\"space-y-3\">\n {breakdown?.truncatedProjects && (\n <p className=\"text-xs text-status-warning-text\">\n {t(\n 'planmagnet.analytics.workBreakdown.truncatedProjects',\n 'Showing the first 50 projects with live work. Narrow by project above to see a specific one.'\n )}\n </p>\n )}\n {truncatedProjectNames.length > 0 && (\n <p className=\"text-xs text-status-warning-text\">\n {t(\n 'planmagnet.analytics.workBreakdown.truncatedEpics',\n 'Showing at most 200 epics for: {{projects}}.',\n { projects: truncatedProjectNames.join(', ') }\n )}\n </p>\n )}\n\n <div data-testid=\"viz-work-breakdown\">\n <ResponsiveContainer width=\"100%\" height={360}>\n <Treemap\n data={groups}\n dataKey=\"size\"\n type=\"flat\"\n aspectRatio={4 / 3}\n content={<WorkBreakdownCell onOpenEpic={openEpic} noEpicLabel={noEpicLabel} />}\n >\n <Tooltip\n content={({ active, payload }) => {\n if (!active || !payload || payload.length === 0) return null;\n // Treemap hands back our own leaf datum directly as `payload.payload` (recharts types it\n // as `any`); narrowed here, once.\n const node = payload[0]?.payload as BreakdownLeafDatum | undefined;\n if (!node || node.isHeader) return null;\n const label = node.isNoEpic ? noEpicLabel : node.name;\n return (\n <div className=\"rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs shadow-md\">\n <p className=\"font-medium text-popover-foreground\">{label}</p>\n {!node.isNoEpic && (\n <p className=\"text-muted-foreground\">{node.projectName}</p>\n )}\n <p className=\"mt-1\">\n {t('planmagnet.analytics.workBreakdown.tooltipOpen', 'Open')}:{' '}\n {node.openCount} ({node.openPoints}{' '}\n {t('planmagnet.analytics.workBreakdown.pts', 'pts')}) ·{' '}\n {t('planmagnet.analytics.workBreakdown.tooltipDone', 'Done')}:{' '}\n {node.doneCount} ({node.donePoints}{' '}\n {t('planmagnet.analytics.workBreakdown.pts', 'pts')})\n </p>\n <p>\n {t('planmagnet.analytics.workBreakdown.doneRatio', 'Done ratio')}:{' '}\n {node.doneRatio === null ? '—' : `${Math.round(node.doneRatio * 100)}%`}\n </p>\n </div>\n );\n }}\n />\n </Treemap>\n </ResponsiveContainer>\n </div>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.workBreakdown.legend',\n 'Shade = share done (lighter → more open, darker → more done). Click an epic to open it.'\n )}\n </p>\n\n {/* Accessible table alternative — every group/epic the treemap draws, in the same data. */}\n <table className=\"w-full text-sm\" data-testid=\"viz-work-breakdown-table\">\n <caption className=\"mb-2 text-left text-xs text-muted-foreground\">\n {t(\n 'planmagnet.analytics.workBreakdown.tableCaption',\n 'Work breakdown by project and epic'\n )}\n </caption>\n <thead>\n <tr className=\"text-left text-xs text-muted-foreground\">\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.analytics.workBreakdown.project', 'Project')}\n </th>\n <th className=\"py-1 pr-2 font-normal\">\n {t('planmagnet.analytics.workBreakdown.epic', 'Epic')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.analytics.workBreakdown.tooltipOpen', 'Open')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.analytics.workBreakdown.tooltipDone', 'Done')}\n </th>\n <th className=\"py-1 text-right font-normal\">\n {t('planmagnet.analytics.workBreakdown.doneRatio', 'Done ratio')}\n </th>\n </tr>\n </thead>\n <tbody>\n {groups.flatMap((g) =>\n g.children\n .filter((leaf) => !leaf.isHeader)\n .map((leaf) => {\n const epicId = leaf.epicId;\n const open = metric === 'items' ? leaf.openCount : leaf.openPoints;\n const done = metric === 'items' ? leaf.doneCount : leaf.donePoints;\n return (\n <tr key={`${g.projectId}-${epicId ?? 'no-epic'}`} className=\"border-t\">\n <td className=\"py-1.5 pr-2\">{g.name}</td>\n <td className=\"py-1.5 pr-2\">\n {epicId ? (\n <button\n type=\"button\"\n className=\"text-left text-primary hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary\"\n onClick={() => openEpic(epicId)}\n >\n {leaf.name}\n </button>\n ) : (\n noEpicLabel\n )}\n </td>\n <td className=\"py-1.5 text-right tabular-nums\">{open}</td>\n <td className=\"py-1.5 text-right tabular-nums\">{done}</td>\n <td className=\"py-1.5 text-right tabular-nums\">\n {leaf.doneRatio === null ? '—' : `${Math.round(leaf.doneRatio * 100)}%`}\n </td>\n </tr>\n );\n })\n )}\n </tbody>\n </table>\n </div>\n )}\n </CardContent>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -2,8 +2,8 @@ import { usePlanMagnetNavigate as e } from "../providers/PlanMagnetProvider.js";
2
2
  import { LEAVES_PAGE_CONTEXT as t, throwOnError as n } from "../utils/mutationResult.js";
3
3
  import { ConfirmButton as r, ErrorLine as i } from "../controls/ConfirmButton.js";
4
4
  import { enumLabel as a } from "../controls/WorkItemControls.js";
5
- import { detailPath as o } from "../utils/rowNavigation.js";
6
- import { mutationErrorMessage as s } from "../utils/graphqlErrors.js";
5
+ import { mutationErrorMessage as o } from "../utils/graphqlErrors.js";
6
+ import { detailPath as s } from "../utils/rowNavigation.js";
7
7
  import { endBeforeStart as c, parseIntField as l } from "../utils/formValidation.js";
8
8
  import { useState as u } from "react";
9
9
  import { BarChart3 as d, Pencil as f, Plus as p, Trash2 as m } from "lucide-react";
@@ -66,7 +66,7 @@ var O = [
66
66
  }
67
67
  `, I = (e) => e ? e.slice(0, 10) : "";
68
68
  function L({ roadmapId: e, item: t, lanes: r }) {
69
- let o = {
69
+ let s = {
70
70
  title: t?.title ?? "",
71
71
  description: t?.description ?? "",
72
72
  status: t?.status ?? "PLANNED",
@@ -77,7 +77,7 @@ function L({ roadmapId: e, item: t, lanes: r }) {
77
77
  confidence: t?.confidence == null ? "" : String(t.confidence),
78
78
  riskLevel: t?.riskLevel ?? "",
79
79
  riskNote: t?.riskNote ?? ""
80
- }, [d, m] = u(!1), [h, P] = u(o), [F, L] = u(null), R = {
80
+ }, [d, m] = u(!1), [h, P] = u(s), [F, L] = u(null), R = {
81
81
  refetchQueries: j,
82
82
  awaitRefetchQueries: !0
83
83
  }, [z, B] = g(M, R), [V, H] = g(N, R), U = B.loading || H.loading, W = c(h.startDate, h.endDate), G = l(h.confidence, {
@@ -94,13 +94,13 @@ function L({ roadmapId: e, item: t, lanes: r }) {
94
94
  try {
95
95
  if (t) {
96
96
  let e = {
97
- ...h.startDate !== o.startDate && { startDate: h.startDate || null },
98
- ...h.endDate !== o.endDate && { endDate: h.endDate || null }
97
+ ...h.startDate !== s.startDate && { startDate: h.startDate || null },
98
+ ...h.endDate !== s.endDate && { endDate: h.endDate || null }
99
99
  }, i = {
100
- ...h.horizon !== o.horizon && { horizon: h.horizon || null },
101
- ...h.confidence !== o.confidence && { confidence: G.value ?? null },
102
- ...h.riskLevel !== o.riskLevel && { riskLevel: h.riskLevel || null },
103
- ...h.riskNote.trim() !== o.riskNote.trim() && { riskNote: h.riskNote.trim() || null }
100
+ ...h.horizon !== s.horizon && { horizon: h.horizon || null },
101
+ ...h.confidence !== s.confidence && { confidence: G.value ?? null },
102
+ ...h.riskLevel !== s.riskLevel && { riskLevel: h.riskLevel || null },
103
+ ...h.riskNote.trim() !== s.riskNote.trim() && { riskNote: h.riskNote.trim() || null }
104
104
  };
105
105
  await n(V({ variables: {
106
106
  id: t.id,
@@ -119,16 +119,16 @@ function L({ roadmapId: e, item: t, lanes: r }) {
119
119
  confidence: G.value,
120
120
  riskLevel: h.riskLevel || void 0,
121
121
  riskNote: h.riskNote.trim() || void 0
122
- } } })), P(o);
122
+ } } })), P(s);
123
123
  m(!1);
124
124
  } catch (e) {
125
- L(s(e, "The item could not be saved."));
125
+ L(o(e, "The item could not be saved."));
126
126
  }
127
127
  }, Q = "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm";
128
128
  return /* @__PURE__ */ v(x, {
129
129
  open: d,
130
130
  onOpenChange: (e) => {
131
- m(e), e ? P(o) : L(null);
131
+ m(e), e ? P(s) : L(null);
132
132
  },
133
133
  children: [/* @__PURE__ */ _(D, {
134
134
  asChild: !0,
@@ -375,7 +375,7 @@ function R({ id: e, title: t }) {
375
375
  }), s && /* @__PURE__ */ _(i, { message: s })]
376
376
  });
377
377
  }
378
- function z({ roadmapId: a, name: s }) {
378
+ function z({ roadmapId: a, name: o }) {
379
379
  let c = e(), [l, p] = g(F, {
380
380
  refetchQueries: ["ListRoadmapsPage", "ListRoadmaps"],
381
381
  context: t
@@ -396,21 +396,21 @@ function z({ roadmapId: a, name: s }) {
396
396
  size: "sm",
397
397
  variant: "outline",
398
398
  "data-testid": "roadmap-edit",
399
- onClick: () => c(o("/products/edit-roadmap-page", a)),
399
+ onClick: () => c(s("/products/edit-roadmap-page", a)),
400
400
  children: [/* @__PURE__ */ _(f, { className: "mr-1 h-4 w-4" }), "Edit"]
401
401
  }),
402
402
  /* @__PURE__ */ v(y, {
403
403
  size: "sm",
404
404
  variant: "outline",
405
405
  "data-testid": "roadmap-analytics",
406
- onClick: () => c(o("/products/roadmap-analytics-page", a)),
406
+ onClick: () => c(s("/products/roadmap-analytics-page", a)),
407
407
  children: [/* @__PURE__ */ _(d, { className: "mr-1 h-4 w-4" }), "Analytics"]
408
408
  }),
409
409
  /* @__PURE__ */ _(r, {
410
410
  label: "Delete",
411
411
  variant: "outline",
412
412
  icon: /* @__PURE__ */ _(m, { className: "mr-1 h-4 w-4" }),
413
- title: `Delete ${s}?`,
413
+ title: `Delete ${o}?`,
414
414
  description: "The roadmap and its items are deleted. This cannot be undone here.",
415
415
  confirmLabel: "Delete roadmap",
416
416
  destructive: !0,
@@ -4,21 +4,20 @@
4
4
  * re-assigned from whichever statuses happen to appear in a given project/graph — a categorical channel keeps the
5
5
  * same color for the same identity everywhere it shows up.
6
6
  *
7
- * Values are complete literal Tailwind class names (never built with a template string): Tailwind's build scans
8
- * source text for whole class-name tokens, so `` `fill-${status}` `` would never be picked up and would ship
9
- * invisible/unstyled in production while every check here still passes.
7
+ * Values are CSS custom-property references to fe-libs' chart token ramp (`var(--color-chart-N)`), applied through
8
+ * inline `style` (`fill`, `stroke`, `backgroundColor`) — NOT Tailwind utilities. The host apps' Tailwind themes do
9
+ * not register `--color-chart-*`, so chart utility classes are never generated: measured in prod 2026-09-30, no
10
+ * such rule existed in the served CSS and every chart mark rendered black. The token variables themselves are
11
+ * always defined (fe-libs tokens.css), so referencing them directly works in every host.
10
12
  *
11
13
  * Colors come from fe-libs' chart token ramp (`chart-1`..`chart-12`), not the `status-*` tokens — those are
12
14
  * reserved for good/warning/serious/critical severity (see `dev/skills` dataviz guidance) and workflow status is
13
15
  * an identity dimension, not a severity one.
14
16
  */
15
17
  export interface StatusVisual {
16
- /** Fill class for the node/bar body. */
17
- fill: string;
18
- /** Stroke class matching `fill`, for outlines and edge/line marks of the same status. */
19
- stroke: string;
20
- /** Solid swatch class for a small legend dot. */
21
- swatch: string;
18
+ /** The status's chart color as a CSS value (`var(--color-chart-N)`) — use as an SVG `fill`/`stroke` or a legend
19
+ * swatch's `backgroundColor` through inline style. */
20
+ color: string;
22
21
  }
23
22
  export declare function statusVisual(status: string): StatusVisual;
24
23
  /** Every status with an assigned visual, in the fixed legend order. */
@@ -1 +1 @@
1
- {"version":3,"file":"statusColor.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/statusColor.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;CAChB;AAoBD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAEzD;AAED,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAuB,CAAC"}
1
+ {"version":3,"file":"statusColor.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/utils/statusColor.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,YAAY;IAC3B;0DACsD;IACtD,KAAK,EAAE,MAAM,CAAC;CACf;AAgBD,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAEzD;AAED,uEAAuE;AACvE,eAAO,MAAM,mBAAmB,EAAE,SAAS,MAAM,EAAuB,CAAC"}
@@ -1,46 +1,14 @@
1
1
  import { WORK_ITEM_STATUSES as e } from "../controls/WorkItemControls.js";
2
2
  //#region src/planmagnet/utils/statusColor.ts
3
3
  var t = {
4
- BACKLOG: {
5
- fill: "fill-chart-6",
6
- stroke: "stroke-chart-6",
7
- swatch: "bg-chart-6"
8
- },
9
- TODO: {
10
- fill: "fill-chart-1",
11
- stroke: "stroke-chart-1",
12
- swatch: "bg-chart-1"
13
- },
14
- IN_PROGRESS: {
15
- fill: "fill-chart-4",
16
- stroke: "stroke-chart-4",
17
- swatch: "bg-chart-4"
18
- },
19
- IN_REVIEW: {
20
- fill: "fill-chart-3",
21
- stroke: "stroke-chart-3",
22
- swatch: "bg-chart-3"
23
- },
24
- TESTING: {
25
- fill: "fill-chart-9",
26
- stroke: "stroke-chart-9",
27
- swatch: "bg-chart-9"
28
- },
29
- DONE: {
30
- fill: "fill-chart-2",
31
- stroke: "stroke-chart-2",
32
- swatch: "bg-chart-2"
33
- },
34
- CANCELLED: {
35
- fill: "fill-chart-10",
36
- stroke: "stroke-chart-10",
37
- swatch: "bg-chart-10"
38
- }
39
- }, n = {
40
- fill: "fill-chart-12",
41
- stroke: "stroke-chart-12",
42
- swatch: "bg-chart-12"
43
- };
4
+ BACKLOG: { color: "var(--color-chart-6)" },
5
+ TODO: { color: "var(--color-chart-1)" },
6
+ IN_PROGRESS: { color: "var(--color-chart-4)" },
7
+ IN_REVIEW: { color: "var(--color-chart-3)" },
8
+ TESTING: { color: "var(--color-chart-9)" },
9
+ DONE: { color: "var(--color-chart-2)" },
10
+ CANCELLED: { color: "var(--color-chart-10)" }
11
+ }, n = { color: "var(--color-chart-12)" };
44
12
  function r(e) {
45
13
  return t[e] ?? n;
46
14
  }
@@ -1 +1 @@
1
- {"version":3,"file":"statusColor.js","names":[],"sources":["../../../src/planmagnet/utils/statusColor.ts"],"sourcesContent":["import { WORK_ITEM_STATUSES } from '../controls/WorkItemControls';\n\n/**\n * One fixed chart color per WorkItemStatus (PDM-27 dependency graph, ANR-24 Gantt): both visualizations color by\n * status, so they share this single mapping rather than each re-deriving one. The order is FIXED and never\n * re-assigned from whichever statuses happen to appear in a given project/graph — a categorical channel keeps the\n * same color for the same identity everywhere it shows up.\n *\n * Values are complete literal Tailwind class names (never built with a template string): Tailwind's build scans\n * source text for whole class-name tokens, so `` `fill-${status}` `` would never be picked up and would ship\n * invisible/unstyled in production while every check here still passes.\n *\n * Colors come from fe-libs' chart token ramp (`chart-1`..`chart-12`), not the `status-*` tokens — those are\n * reserved for good/warning/serious/critical severity (see `dev/skills` dataviz guidance) and workflow status is\n * an identity dimension, not a severity one.\n */\nexport interface StatusVisual {\n /** Fill class for the node/bar body. */\n fill: string;\n /** Stroke class matching `fill`, for outlines and edge/line marks of the same status. */\n stroke: string;\n /** Solid swatch class for a small legend dot. */\n swatch: string;\n}\n\nconst STATUS_VISUALS: Record<(typeof WORK_ITEM_STATUSES)[number], StatusVisual> = {\n BACKLOG: { fill: 'fill-chart-6', stroke: 'stroke-chart-6', swatch: 'bg-chart-6' },\n TODO: { fill: 'fill-chart-1', stroke: 'stroke-chart-1', swatch: 'bg-chart-1' },\n IN_PROGRESS: { fill: 'fill-chart-4', stroke: 'stroke-chart-4', swatch: 'bg-chart-4' },\n IN_REVIEW: { fill: 'fill-chart-3', stroke: 'stroke-chart-3', swatch: 'bg-chart-3' },\n TESTING: { fill: 'fill-chart-9', stroke: 'stroke-chart-9', swatch: 'bg-chart-9' },\n DONE: { fill: 'fill-chart-2', stroke: 'stroke-chart-2', swatch: 'bg-chart-2' },\n CANCELLED: { fill: 'fill-chart-10', stroke: 'stroke-chart-10', swatch: 'bg-chart-10' },\n};\n\n/** A status the API returns that this map doesn't know yet (see WorkItemStatusCount's own doc comment on the\n * same risk) falls back to this rather than throwing or rendering unstyled. */\nconst FALLBACK_VISUAL: StatusVisual = {\n fill: 'fill-chart-12',\n stroke: 'stroke-chart-12',\n swatch: 'bg-chart-12',\n};\n\nexport function statusVisual(status: string): StatusVisual {\n return (STATUS_VISUALS as Record<string, StatusVisual>)[status] ?? FALLBACK_VISUAL;\n}\n\n/** Every status with an assigned visual, in the fixed legend order. */\nexport const STATUS_LEGEND_ORDER: readonly string[] = WORK_ITEM_STATUSES;\n"],"mappings":";;AAyBA,IAAM,IAA4E;CAChF,SAAS;EAAE,MAAM;EAAgB,QAAQ;EAAkB,QAAQ;EAAc;CACjF,MAAM;EAAE,MAAM;EAAgB,QAAQ;EAAkB,QAAQ;EAAc;CAC9E,aAAa;EAAE,MAAM;EAAgB,QAAQ;EAAkB,QAAQ;EAAc;CACrF,WAAW;EAAE,MAAM;EAAgB,QAAQ;EAAkB,QAAQ;EAAc;CACnF,SAAS;EAAE,MAAM;EAAgB,QAAQ;EAAkB,QAAQ;EAAc;CACjF,MAAM;EAAE,MAAM;EAAgB,QAAQ;EAAkB,QAAQ;EAAc;CAC9E,WAAW;EAAE,MAAM;EAAiB,QAAQ;EAAmB,QAAQ;EAAe;CACvF,EAIK,IAAgC;CACpC,MAAM;CACN,QAAQ;CACR,QAAQ;CACT;AAED,SAAgB,EAAa,GAA8B;AACzD,QAAQ,EAAgD,MAAW;;AAIrE,IAAa,IAAyC"}
1
+ {"version":3,"file":"statusColor.js","names":[],"sources":["../../../src/planmagnet/utils/statusColor.ts"],"sourcesContent":["import { WORK_ITEM_STATUSES } from '../controls/WorkItemControls';\n\n/**\n * One fixed chart color per WorkItemStatus (PDM-27 dependency graph, ANR-24 Gantt): both visualizations color by\n * status, so they share this single mapping rather than each re-deriving one. The order is FIXED and never\n * re-assigned from whichever statuses happen to appear in a given project/graph — a categorical channel keeps the\n * same color for the same identity everywhere it shows up.\n *\n * Values are CSS custom-property references to fe-libs' chart token ramp (`var(--color-chart-N)`), applied through\n * inline `style` (`fill`, `stroke`, `backgroundColor`) — NOT Tailwind utilities. The host apps' Tailwind themes do\n * not register `--color-chart-*`, so chart utility classes are never generated: measured in prod 2026-09-30, no\n * such rule existed in the served CSS and every chart mark rendered black. The token variables themselves are\n * always defined (fe-libs tokens.css), so referencing them directly works in every host.\n *\n * Colors come from fe-libs' chart token ramp (`chart-1`..`chart-12`), not the `status-*` tokens — those are\n * reserved for good/warning/serious/critical severity (see `dev/skills` dataviz guidance) and workflow status is\n * an identity dimension, not a severity one.\n */\nexport interface StatusVisual {\n /** The status's chart color as a CSS value (`var(--color-chart-N)`) — use as an SVG `fill`/`stroke` or a legend\n * swatch's `backgroundColor` through inline style. */\n color: string;\n}\n\nconst STATUS_VISUALS: Record<(typeof WORK_ITEM_STATUSES)[number], StatusVisual> = {\n BACKLOG: { color: 'var(--color-chart-6)' },\n TODO: { color: 'var(--color-chart-1)' },\n IN_PROGRESS: { color: 'var(--color-chart-4)' },\n IN_REVIEW: { color: 'var(--color-chart-3)' },\n TESTING: { color: 'var(--color-chart-9)' },\n DONE: { color: 'var(--color-chart-2)' },\n CANCELLED: { color: 'var(--color-chart-10)' },\n};\n\n/** A status the API returns that this map doesn't know yet (see WorkItemStatusCount's own doc comment on the\n * same risk) falls back to this rather than throwing or rendering unstyled. */\nconst FALLBACK_VISUAL: StatusVisual = { color: 'var(--color-chart-12)' };\n\nexport function statusVisual(status: string): StatusVisual {\n return (STATUS_VISUALS as Record<string, StatusVisual>)[status] ?? FALLBACK_VISUAL;\n}\n\n/** Every status with an assigned visual, in the fixed legend order. */\nexport const STATUS_LEGEND_ORDER: readonly string[] = WORK_ITEM_STATUSES;\n"],"mappings":";;AAwBA,IAAM,IAA4E;CAChF,SAAS,EAAE,OAAO,wBAAwB;CAC1C,MAAM,EAAE,OAAO,wBAAwB;CACvC,aAAa,EAAE,OAAO,wBAAwB;CAC9C,WAAW,EAAE,OAAO,wBAAwB;CAC5C,SAAS,EAAE,OAAO,wBAAwB;CAC1C,MAAM,EAAE,OAAO,wBAAwB;CACvC,WAAW,EAAE,OAAO,yBAAyB;CAC9C,EAIK,IAAgC,EAAE,OAAO,yBAAyB;AAExE,SAAgB,EAAa,GAA8B;AACzD,QAAQ,EAAgD,MAAW;;AAIrE,IAAa,IAAyC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-planmagnet",
3
- "version": "2026.930.21",
3
+ "version": "2026.930.23",
4
4
  "description": "Burdenoff PlanMagnet Microfrontend",
5
5
  "type": "module",
6
6
  "files": [