@burdenoff/microfe-planmagnet 2026.930.22 → 2026.930.24

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 (68) hide show
  1. package/dist/index.js +6 -5
  2. package/dist/planmagnet/PlanMagnetRoot.d.ts.map +1 -1
  3. package/dist/planmagnet/PlanMagnetRoot.js +154 -147
  4. package/dist/planmagnet/PlanMagnetRoot.js.map +1 -1
  5. package/dist/planmagnet/PlanMagnetRoutes.d.ts.map +1 -1
  6. package/dist/planmagnet/PlanMagnetRoutes.js +66 -62
  7. package/dist/planmagnet/PlanMagnetRoutes.js.map +1 -1
  8. package/dist/planmagnet/controls/FeatureControls.d.ts +11 -0
  9. package/dist/planmagnet/controls/FeatureControls.d.ts.map +1 -1
  10. package/dist/planmagnet/controls/FeatureControls.js +305 -135
  11. package/dist/planmagnet/controls/FeatureControls.js.map +1 -1
  12. package/dist/planmagnet/controls/Pickers.d.ts +18 -0
  13. package/dist/planmagnet/controls/Pickers.d.ts.map +1 -1
  14. package/dist/planmagnet/controls/Pickers.js +78 -1
  15. package/dist/planmagnet/controls/Pickers.js.map +1 -1
  16. package/dist/planmagnet/controls/PrioritizationControls.d.ts +72 -0
  17. package/dist/planmagnet/controls/PrioritizationControls.d.ts.map +1 -0
  18. package/dist/planmagnet/controls/PrioritizationControls.js +246 -0
  19. package/dist/planmagnet/controls/PrioritizationControls.js.map +1 -0
  20. package/dist/planmagnet/dependency/DependencyGraph.d.ts.map +1 -1
  21. package/dist/planmagnet/dependency/DependencyGraph.js +5 -2
  22. package/dist/planmagnet/dependency/DependencyGraph.js.map +1 -1
  23. package/dist/planmagnet/dependency/GanttChart.d.ts.map +1 -1
  24. package/dist/planmagnet/dependency/GanttChart.js +11 -5
  25. package/dist/planmagnet/dependency/GanttChart.js.map +1 -1
  26. package/dist/planmagnet/index.d.ts +1 -0
  27. package/dist/planmagnet/index.d.ts.map +1 -1
  28. package/dist/planmagnet/index.js +1 -0
  29. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.d.ts.map +1 -1
  30. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js +131 -125
  31. package/dist/planmagnet/pages/ProductsFeatureDetailPagePage.js.map +1 -1
  32. package/dist/planmagnet/pages/ProductsFeaturesPagePage.d.ts.map +1 -1
  33. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js +183 -99
  34. package/dist/planmagnet/pages/ProductsFeaturesPagePage.js.map +1 -1
  35. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.d.ts.map +1 -1
  36. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js +113 -97
  37. package/dist/planmagnet/pages/ProductsNewFeaturePagePage.js.map +1 -1
  38. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts +2 -0
  39. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.d.ts.map +1 -0
  40. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js +363 -0
  41. package/dist/planmagnet/pages/ProductsPrioritizationPagePage.js.map +1 -0
  42. package/dist/planmagnet/pages/ProductsProductDetailPagePage.d.ts.map +1 -1
  43. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js +96 -79
  44. package/dist/planmagnet/pages/ProductsProductDetailPagePage.js.map +1 -1
  45. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.d.ts.map +1 -1
  46. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js +133 -123
  47. package/dist/planmagnet/pages/ProductsRoadmapDetailPagePage.js.map +1 -1
  48. package/dist/planmagnet/pages/analytics/StatusFlowCard.js +5 -2
  49. package/dist/planmagnet/pages/analytics/StatusFlowCard.js.map +1 -1
  50. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js +5 -2
  51. package/dist/planmagnet/pages/analytics/WorkBreakdownCard.js.map +1 -1
  52. package/dist/planmagnet/product/ProductControls.d.ts +33 -0
  53. package/dist/planmagnet/product/ProductControls.d.ts.map +1 -1
  54. package/dist/planmagnet/product/ProductControls.js +593 -268
  55. package/dist/planmagnet/product/ProductControls.js.map +1 -1
  56. package/dist/planmagnet/product/routes.d.ts +10 -0
  57. package/dist/planmagnet/product/routes.d.ts.map +1 -0
  58. package/dist/planmagnet/product/routes.js +6 -0
  59. package/dist/planmagnet/product/routes.js.map +1 -0
  60. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts +10 -0
  61. package/dist/planmagnet/roadmaps/RoadmapControls.d.ts.map +1 -1
  62. package/dist/planmagnet/roadmaps/RoadmapControls.js +235 -184
  63. package/dist/planmagnet/roadmaps/RoadmapControls.js.map +1 -1
  64. package/dist/planmagnet/utils/statusColor.d.ts +8 -9
  65. package/dist/planmagnet/utils/statusColor.d.ts.map +1 -1
  66. package/dist/planmagnet/utils/statusColor.js +8 -40
  67. package/dist/planmagnet/utils/statusColor.js.map +1 -1
  68. package/package.json +1 -1
@@ -0,0 +1,246 @@
1
+ import { ErrorLine as e } from "./ConfirmButton.js";
2
+ import { parseIntField as t } from "../utils/formValidation.js";
3
+ import { jsx as n, jsxs as r } from "react/jsx-runtime";
4
+ import { Badge as i, Input as a } from "@burdenoff/fe-libs/ui";
5
+ //#region src/planmagnet/controls/PrioritizationControls.tsx
6
+ var o = [
7
+ "MUST",
8
+ "SHOULD",
9
+ "COULD",
10
+ "WONT"
11
+ ], s = [
12
+ "BASIC",
13
+ "PERFORMANCE",
14
+ "DELIGHTER",
15
+ "INDIFFERENT",
16
+ "REVERSE"
17
+ ], c = [
18
+ "QUICK_WIN",
19
+ "MAJOR_PROJECT",
20
+ "FILL_IN",
21
+ "TIME_SINK"
22
+ ], l = {
23
+ MUST: {
24
+ label: "Must",
25
+ hint: "Non-negotiable — the release does not ship without it.",
26
+ variant: "default"
27
+ },
28
+ SHOULD: {
29
+ label: "Should",
30
+ hint: "Important, but the release can still ship without it.",
31
+ variant: "info"
32
+ },
33
+ COULD: {
34
+ label: "Could",
35
+ hint: "Nice to have if there is time.",
36
+ variant: "secondary"
37
+ },
38
+ WONT: {
39
+ label: "Won't",
40
+ hint: "Explicitly out of scope this time around.",
41
+ variant: "outline"
42
+ }
43
+ }, u = {
44
+ BASIC: {
45
+ label: "Basic",
46
+ hint: "Expected — its absence causes dissatisfaction, its presence goes unnoticed.",
47
+ variant: "secondary"
48
+ },
49
+ PERFORMANCE: {
50
+ label: "Performance",
51
+ hint: "Satisfaction scales with how well it is delivered.",
52
+ variant: "info"
53
+ },
54
+ DELIGHTER: {
55
+ label: "Delighter",
56
+ hint: "Unexpected — its absence is not missed, its presence delights.",
57
+ variant: "success"
58
+ },
59
+ INDIFFERENT: {
60
+ label: "Indifferent",
61
+ hint: "Users do not care either way.",
62
+ variant: "outline"
63
+ },
64
+ REVERSE: {
65
+ label: "Reverse",
66
+ hint: "Some users prefer this NOT be there.",
67
+ variant: "destructive"
68
+ }
69
+ }, d = {
70
+ QUICK_WIN: {
71
+ label: "Quick win",
72
+ hint: "High value, low effort (value ≥ 6, effort ≤ 5) — do these first.",
73
+ variant: "success"
74
+ },
75
+ MAJOR_PROJECT: {
76
+ label: "Major project",
77
+ hint: "High value, high effort (value ≥ 6, effort ≥ 6) — plan and staff deliberately.",
78
+ variant: "info"
79
+ },
80
+ FILL_IN: {
81
+ label: "Fill-in",
82
+ hint: "Low value, low effort (value ≤ 5, effort ≤ 5) — fine as filler work.",
83
+ variant: "secondary"
84
+ },
85
+ TIME_SINK: {
86
+ label: "Time sink",
87
+ hint: "Low value, high effort (value ≤ 5, effort ≥ 6) — avoid or deprioritize.",
88
+ variant: "destructive"
89
+ }
90
+ }, f = (e) => e && e in l ? l[e].label : "Not set", p = (e) => e && e in u ? u[e].label : "Not set";
91
+ function m({ quadrant: e, hideWhenUnscored: t = !1, className: r }) {
92
+ if (!e) return t ? null : /* @__PURE__ */ n(i, {
93
+ variant: "outline",
94
+ className: r,
95
+ "data-testid": "prio-quadrant-badge",
96
+ children: "Unscored"
97
+ });
98
+ let a = e in d ? d[e] : null;
99
+ return /* @__PURE__ */ n(i, {
100
+ variant: a?.variant ?? "outline",
101
+ className: r,
102
+ "data-testid": "prio-quadrant-badge",
103
+ children: a?.label ?? e
104
+ });
105
+ }
106
+ var h = {
107
+ moscow: "",
108
+ kano: "",
109
+ valueScore: "",
110
+ effortScore: ""
111
+ }, g = "mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60";
112
+ function _({ idPrefix: i, values: c, onChange: d, disabled: f = !1 }) {
113
+ let p = t(c.valueScore, {
114
+ min: 1,
115
+ max: 10
116
+ }), m = t(c.effortScore, {
117
+ min: 1,
118
+ max: 10
119
+ });
120
+ return /* @__PURE__ */ r("div", {
121
+ className: "space-y-3",
122
+ "data-testid": "prio-fields",
123
+ children: [/* @__PURE__ */ r("div", {
124
+ className: "grid grid-cols-2 gap-3",
125
+ children: [/* @__PURE__ */ r("div", { children: [
126
+ /* @__PURE__ */ n("label", {
127
+ htmlFor: `${i}-moscow`,
128
+ className: "text-sm font-medium",
129
+ children: "MoSCoW"
130
+ }),
131
+ /* @__PURE__ */ r("select", {
132
+ id: `${i}-moscow`,
133
+ "data-testid": "prio-field-moscow",
134
+ className: g,
135
+ value: c.moscow,
136
+ disabled: f,
137
+ onChange: (e) => d({
138
+ ...c,
139
+ moscow: e.target.value
140
+ }),
141
+ children: [/* @__PURE__ */ n("option", {
142
+ value: "",
143
+ children: "Not set"
144
+ }), o.map((e) => /* @__PURE__ */ n("option", {
145
+ value: e,
146
+ children: l[e].label
147
+ }, e))]
148
+ }),
149
+ /* @__PURE__ */ n("p", {
150
+ className: "mt-1 text-xs text-muted-foreground",
151
+ children: c.moscow && c.moscow in l ? l[c.moscow].hint : "Must/Should/Could/Won't for this release."
152
+ })
153
+ ] }), /* @__PURE__ */ r("div", { children: [
154
+ /* @__PURE__ */ n("label", {
155
+ htmlFor: `${i}-kano`,
156
+ className: "text-sm font-medium",
157
+ children: "Kano"
158
+ }),
159
+ /* @__PURE__ */ r("select", {
160
+ id: `${i}-kano`,
161
+ "data-testid": "prio-field-kano",
162
+ className: g,
163
+ value: c.kano,
164
+ disabled: f,
165
+ onChange: (e) => d({
166
+ ...c,
167
+ kano: e.target.value
168
+ }),
169
+ children: [/* @__PURE__ */ n("option", {
170
+ value: "",
171
+ children: "Not set"
172
+ }), s.map((e) => /* @__PURE__ */ n("option", {
173
+ value: e,
174
+ children: u[e].label
175
+ }, e))]
176
+ }),
177
+ /* @__PURE__ */ n("p", {
178
+ className: "mt-1 text-xs text-muted-foreground",
179
+ children: c.kano && c.kano in u ? u[c.kano].hint : "How this shapes customer satisfaction."
180
+ })
181
+ ] })]
182
+ }), /* @__PURE__ */ r("div", {
183
+ className: "grid grid-cols-2 gap-3",
184
+ children: [/* @__PURE__ */ r("div", { children: [
185
+ /* @__PURE__ */ n("label", {
186
+ htmlFor: `${i}-value-score`,
187
+ className: "text-sm font-medium",
188
+ children: "Value score (1-10)"
189
+ }),
190
+ /* @__PURE__ */ n(a, {
191
+ id: `${i}-value-score`,
192
+ "data-testid": "prio-field-value-score",
193
+ type: "number",
194
+ min: 1,
195
+ max: 10,
196
+ className: "mt-1",
197
+ placeholder: "Not set",
198
+ value: c.valueScore,
199
+ "aria-invalid": p.invalid,
200
+ disabled: f,
201
+ onChange: (e) => d({
202
+ ...c,
203
+ valueScore: e.target.value
204
+ })
205
+ }),
206
+ p.invalid && /* @__PURE__ */ n(e, { message: "Value score is a whole number from 1 to 10." })
207
+ ] }), /* @__PURE__ */ r("div", { children: [
208
+ /* @__PURE__ */ n("label", {
209
+ htmlFor: `${i}-effort-score`,
210
+ className: "text-sm font-medium",
211
+ children: "Effort score (1-10)"
212
+ }),
213
+ /* @__PURE__ */ n(a, {
214
+ id: `${i}-effort-score`,
215
+ "data-testid": "prio-field-effort-score",
216
+ type: "number",
217
+ min: 1,
218
+ max: 10,
219
+ className: "mt-1",
220
+ placeholder: "Not set",
221
+ value: c.effortScore,
222
+ "aria-invalid": m.invalid,
223
+ disabled: f,
224
+ onChange: (e) => d({
225
+ ...c,
226
+ effortScore: e.target.value
227
+ })
228
+ }),
229
+ m.invalid && /* @__PURE__ */ n(e, { message: "Effort score is a whole number from 1 to 10." })
230
+ ] })]
231
+ })]
232
+ });
233
+ }
234
+ function v(e) {
235
+ return !t(e.valueScore, {
236
+ min: 1,
237
+ max: 10
238
+ }).invalid && !t(e.effortScore, {
239
+ min: 1,
240
+ max: 10
241
+ }).invalid;
242
+ }
243
+ //#endregion
244
+ export { h as EMPTY_PRIORITIZATION_FORM, u as KANO_META, s as KANO_VALUES, l as MOSCOW_META, o as MOSCOW_VALUES, _ as PrioritizationScoreFields, d as QUADRANT_META, c as QUADRANT_VALUES, m as QuadrantBadge, p as kanoLabel, f as moscowLabel, v as prioritizationScoresValid };
245
+
246
+ //# sourceMappingURL=PrioritizationControls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PrioritizationControls.js","names":[],"sources":["../../../src/planmagnet/controls/PrioritizationControls.tsx"],"sourcesContent":["import { Badge, Input } from '@burdenoff/fe-libs/ui';\nimport { parseIntField } from '../utils/formValidation';\nimport { ErrorLine } from './ConfirmButton';\n\n/**\n * Prioritization (PRM-27): MoSCoW, Kano and Value-vs-Effort scoring shared by the feature request create/edit\n * form, the feature detail page, and the roadmap initiative (RoadmapItem) edit dialog — one presentational form\n * fragment plus the badge that shows the server-computed Value-vs-Effort quadrant, so the three surfaces (and the\n * prioritization page's own item cards) render it identically instead of three near-copies drifting apart.\n *\n * The quadrant itself is never computed client-side — `FeatureRequest.valueEffortQuadrant` /\n * `RoadmapItem.valueEffortQuadrant` are server-derived (and not stored), so `QuadrantBadge` only ever renders a\n * value that came back from a query or a mutation response.\n */\n\nexport const MOSCOW_VALUES = ['MUST', 'SHOULD', 'COULD', 'WONT'] as const;\nexport type MoscowValue = (typeof MOSCOW_VALUES)[number];\n\nexport const KANO_VALUES = ['BASIC', 'PERFORMANCE', 'DELIGHTER', 'INDIFFERENT', 'REVERSE'] as const;\nexport type KanoValue = (typeof KANO_VALUES)[number];\n\nexport const QUADRANT_VALUES = ['QUICK_WIN', 'MAJOR_PROJECT', 'FILL_IN', 'TIME_SINK'] as const;\nexport type QuadrantValue = (typeof QUADRANT_VALUES)[number];\n\ntype BadgeVariant =\n | 'default'\n | 'success'\n | 'warning'\n | 'info'\n | 'secondary'\n | 'destructive'\n | 'outline';\n\nexport const MOSCOW_META: Record<\n MoscowValue,\n { label: string; hint: string; variant: BadgeVariant }\n> = {\n MUST: {\n label: 'Must',\n hint: 'Non-negotiable — the release does not ship without it.',\n variant: 'default',\n },\n SHOULD: {\n label: 'Should',\n hint: 'Important, but the release can still ship without it.',\n variant: 'info',\n },\n COULD: { label: 'Could', hint: 'Nice to have if there is time.', variant: 'secondary' },\n WONT: { label: \"Won't\", hint: 'Explicitly out of scope this time around.', variant: 'outline' },\n};\n\nexport const KANO_META: Record<KanoValue, { label: string; hint: string; variant: BadgeVariant }> =\n {\n BASIC: {\n label: 'Basic',\n hint: 'Expected — its absence causes dissatisfaction, its presence goes unnoticed.',\n variant: 'secondary',\n },\n PERFORMANCE: {\n label: 'Performance',\n hint: 'Satisfaction scales with how well it is delivered.',\n variant: 'info',\n },\n DELIGHTER: {\n label: 'Delighter',\n hint: 'Unexpected — its absence is not missed, its presence delights.',\n variant: 'success',\n },\n INDIFFERENT: {\n label: 'Indifferent',\n hint: 'Users do not care either way.',\n variant: 'outline',\n },\n REVERSE: {\n label: 'Reverse',\n hint: 'Some users prefer this NOT be there.',\n variant: 'destructive',\n },\n };\n\nexport const QUADRANT_META: Record<\n QuadrantValue,\n { label: string; hint: string; variant: BadgeVariant }\n> = {\n QUICK_WIN: {\n label: 'Quick win',\n hint: 'High value, low effort (value ≥ 6, effort ≤ 5) — do these first.',\n variant: 'success',\n },\n MAJOR_PROJECT: {\n label: 'Major project',\n hint: 'High value, high effort (value ≥ 6, effort ≥ 6) — plan and staff deliberately.',\n variant: 'info',\n },\n FILL_IN: {\n label: 'Fill-in',\n hint: 'Low value, low effort (value ≤ 5, effort ≤ 5) — fine as filler work.',\n variant: 'secondary',\n },\n TIME_SINK: {\n label: 'Time sink',\n hint: 'Low value, high effort (value ≤ 5, effort ≥ 6) — avoid or deprioritize.',\n variant: 'destructive',\n },\n};\n\nexport const moscowLabel = (value: string | null | undefined): string =>\n value && value in MOSCOW_META ? MOSCOW_META[value as MoscowValue].label : 'Not set';\nexport const kanoLabel = (value: string | null | undefined): string =>\n value && value in KANO_META ? KANO_META[value as KanoValue].label : 'Not set';\nexport const quadrantLabel = (value: string | null | undefined): string =>\n value && value in QUADRANT_META ? QUADRANT_META[value as QuadrantValue].label : 'Unscored';\n\n/**\n * The Value-vs-Effort quadrant, read back from the server (`FeatureRequest.valueEffortQuadrant` /\n * `RoadmapItem.valueEffortQuadrant` / a `PlanmagnetPrioritizationBucket.bucket`) — never computed here. Renders\n * an \"Unscored\" badge when null (one or both of valueScore/effortScore is not set yet) unless `hideWhenUnscored`.\n */\nexport function QuadrantBadge({\n quadrant,\n hideWhenUnscored = false,\n className,\n}: {\n quadrant: string | null | undefined;\n hideWhenUnscored?: boolean;\n className?: string;\n}) {\n if (!quadrant) {\n if (hideWhenUnscored) return null;\n return (\n <Badge variant=\"outline\" className={className} data-testid=\"prio-quadrant-badge\">\n Unscored\n </Badge>\n );\n }\n const meta = quadrant in QUADRANT_META ? QUADRANT_META[quadrant as QuadrantValue] : null;\n return (\n <Badge\n variant={meta?.variant ?? 'outline'}\n className={className}\n data-testid=\"prio-quadrant-badge\"\n >\n {meta?.label ?? quadrant}\n </Badge>\n );\n}\n\nexport interface PrioritizationFormValues {\n /** '' = not set */\n moscow: string;\n /** '' = not set */\n kano: string;\n /** '' = not set, else a whole number 1-10 as text */\n valueScore: string;\n /** '' = not set, else a whole number 1-10 as text */\n effortScore: string;\n}\n\nexport const EMPTY_PRIORITIZATION_FORM: PrioritizationFormValues = {\n moscow: '',\n kano: '',\n valueScore: '',\n effortScore: '',\n};\n\nconst selectClass =\n 'mt-1 w-full rounded-md border border-input bg-background px-3 py-2 text-sm disabled:opacity-60';\n\n/**\n * The four prioritization inputs (PRM-27): MoSCoW and Kano selects (each with a short explanation of the\n * selected value), and 1-10 value/effort number inputs. Fully controlled; clearing a select or number input back\n * to empty is how a caller sends `null` (cleared) rather than omitting the field. Shared by the feature request\n * create/edit form, the feature detail page, and the roadmap initiative edit dialog.\n */\nexport function PrioritizationScoreFields({\n idPrefix,\n values,\n onChange,\n disabled = false,\n}: {\n idPrefix: string;\n values: PrioritizationFormValues;\n onChange: (next: PrioritizationFormValues) => void;\n disabled?: boolean;\n}) {\n const value = parseIntField(values.valueScore, { min: 1, max: 10 });\n const effort = parseIntField(values.effortScore, { min: 1, max: 10 });\n\n return (\n <div className=\"space-y-3\" data-testid=\"prio-fields\">\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-moscow`} className=\"text-sm font-medium\">\n MoSCoW\n </label>\n <select\n id={`${idPrefix}-moscow`}\n data-testid=\"prio-field-moscow\"\n className={selectClass}\n value={values.moscow}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, moscow: e.target.value })}\n >\n <option value=\"\">Not set</option>\n {MOSCOW_VALUES.map((v) => (\n <option key={v} value={v}>\n {MOSCOW_META[v].label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {values.moscow && values.moscow in MOSCOW_META\n ? MOSCOW_META[values.moscow as MoscowValue].hint\n : \"Must/Should/Could/Won't for this release.\"}\n </p>\n </div>\n <div>\n <label htmlFor={`${idPrefix}-kano`} className=\"text-sm font-medium\">\n Kano\n </label>\n <select\n id={`${idPrefix}-kano`}\n data-testid=\"prio-field-kano\"\n className={selectClass}\n value={values.kano}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, kano: e.target.value })}\n >\n <option value=\"\">Not set</option>\n {KANO_VALUES.map((v) => (\n <option key={v} value={v}>\n {KANO_META[v].label}\n </option>\n ))}\n </select>\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {values.kano && values.kano in KANO_META\n ? KANO_META[values.kano as KanoValue].hint\n : 'How this shapes customer satisfaction.'}\n </p>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <label htmlFor={`${idPrefix}-value-score`} className=\"text-sm font-medium\">\n Value score (1-10)\n </label>\n <Input\n id={`${idPrefix}-value-score`}\n data-testid=\"prio-field-value-score\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.valueScore}\n aria-invalid={value.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, valueScore: e.target.value })}\n />\n {value.invalid && <ErrorLine message=\"Value score is a whole number from 1 to 10.\" />}\n </div>\n <div>\n <label htmlFor={`${idPrefix}-effort-score`} className=\"text-sm font-medium\">\n Effort score (1-10)\n </label>\n <Input\n id={`${idPrefix}-effort-score`}\n data-testid=\"prio-field-effort-score\"\n type=\"number\"\n min={1}\n max={10}\n className=\"mt-1\"\n placeholder=\"Not set\"\n value={values.effortScore}\n aria-invalid={effort.invalid}\n disabled={disabled}\n onChange={(e) => onChange({ ...values, effortScore: e.target.value })}\n />\n {effort.invalid && <ErrorLine message=\"Effort score is a whole number from 1 to 10.\" />}\n </div>\n </div>\n </div>\n );\n}\n\n/** True when neither of the two score fields holds an out-of-range value (empty is always valid — it means \"clear\"). */\nexport function prioritizationScoresValid(values: PrioritizationFormValues): boolean {\n return (\n !parseIntField(values.valueScore, { min: 1, max: 10 }).invalid &&\n !parseIntField(values.effortScore, { min: 1, max: 10 }).invalid\n );\n}\n"],"mappings":";;;;;AAeA,IAAa,IAAgB;CAAC;CAAQ;CAAU;CAAS;CAAO,EAGnD,IAAc;CAAC;CAAS;CAAe;CAAa;CAAe;CAAU,EAG7E,IAAkB;CAAC;CAAa;CAAiB;CAAW;CAAY,EAYxE,IAGT;CACF,MAAM;EACJ,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,QAAQ;EACN,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,OAAO;EAAE,OAAO;EAAS,MAAM;EAAkC,SAAS;EAAa;CACvF,MAAM;EAAE,OAAO;EAAS,MAAM;EAA6C,SAAS;EAAW;CAChG,EAEY,IACX;CACE,OAAO;EACL,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,aAAa;EACX,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,WAAW;EACT,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,aAAa;EACX,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,SAAS;EACP,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACF,EAEU,IAGT;CACF,WAAW;EACT,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,eAAe;EACb,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,SAAS;EACP,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACD,WAAW;EACT,OAAO;EACP,MAAM;EACN,SAAS;EACV;CACF,EAEY,KAAe,MAC1B,KAAS,KAAS,IAAc,EAAY,GAAsB,QAAQ,WAC/D,KAAa,MACxB,KAAS,KAAS,IAAY,EAAU,GAAoB,QAAQ;AAStE,SAAgB,EAAc,EAC5B,aACA,sBAAmB,IACnB,gBAKC;AACD,KAAI,CAAC,EAEH,QADI,IAAyB,OAE3B,kBAAC,GAAD;EAAO,SAAQ;EAAqB;EAAW,eAAY;YAAsB;EAEzE,CAAA;CAGZ,IAAM,IAAO,KAAY,IAAgB,EAAc,KAA6B;AACpF,QACE,kBAAC,GAAD;EACE,SAAS,GAAM,WAAW;EACf;EACX,eAAY;YAEX,GAAM,SAAS;EACV,CAAA;;AAeZ,IAAa,IAAsD;CACjE,QAAQ;CACR,MAAM;CACN,YAAY;CACZ,aAAa;CACd,EAEK,IACJ;AAQF,SAAgB,EAA0B,EACxC,aACA,WACA,aACA,cAAW,MAMV;CACD,IAAM,IAAQ,EAAc,EAAO,YAAY;EAAE,KAAK;EAAG,KAAK;EAAI,CAAC,EAC7D,IAAS,EAAc,EAAO,aAAa;EAAE,KAAK;EAAG,KAAK;EAAI,CAAC;AAErE,QACE,kBAAC,OAAD;EAAK,WAAU;EAAY,eAAY;YAAvC,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KAAO,SAAS,GAAG,EAAS;KAAU,WAAU;eAAsB;KAE9D,CAAA;IACR,kBAAC,UAAD;KACE,IAAI,GAAG,EAAS;KAChB,eAAY;KACZ,WAAW;KACX,OAAO,EAAO;KACJ;KACV,WAAW,MAAM,EAAS;MAAE,GAAG;MAAQ,QAAQ,EAAE,OAAO;MAAO,CAAC;eANlE,CAQE,kBAAC,UAAD;MAAQ,OAAM;gBAAG;MAAgB,CAAA,EAChC,EAAc,KAAK,MAClB,kBAAC,UAAD;MAAgB,OAAO;gBACpB,EAAY,GAAG;MACT,EAFI,EAEJ,CACT,CACK;;IACT,kBAAC,KAAD;KAAG,WAAU;eACV,EAAO,UAAU,EAAO,UAAU,IAC/B,EAAY,EAAO,QAAuB,OAC1C;KACF,CAAA;IACA,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KAAO,SAAS,GAAG,EAAS;KAAQ,WAAU;eAAsB;KAE5D,CAAA;IACR,kBAAC,UAAD;KACE,IAAI,GAAG,EAAS;KAChB,eAAY;KACZ,WAAW;KACX,OAAO,EAAO;KACJ;KACV,WAAW,MAAM,EAAS;MAAE,GAAG;MAAQ,MAAM,EAAE,OAAO;MAAO,CAAC;eANhE,CAQE,kBAAC,UAAD;MAAQ,OAAM;gBAAG;MAAgB,CAAA,EAChC,EAAY,KAAK,MAChB,kBAAC,UAAD;MAAgB,OAAO;gBACpB,EAAU,GAAG;MACP,EAFI,EAEJ,CACT,CACK;;IACT,kBAAC,KAAD;KAAG,WAAU;eACV,EAAO,QAAQ,EAAO,QAAQ,IAC3B,EAAU,EAAO,MAAmB,OACpC;KACF,CAAA;IACA,EAAA,CAAA,CACF;MACN,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KAAO,SAAS,GAAG,EAAS;KAAe,WAAU;eAAsB;KAEnE,CAAA;IACR,kBAAC,GAAD;KACE,IAAI,GAAG,EAAS;KAChB,eAAY;KACZ,MAAK;KACL,KAAK;KACL,KAAK;KACL,WAAU;KACV,aAAY;KACZ,OAAO,EAAO;KACd,gBAAc,EAAM;KACV;KACV,WAAW,MAAM,EAAS;MAAE,GAAG;MAAQ,YAAY,EAAE,OAAO;MAAO,CAAC;KACpE,CAAA;IACD,EAAM,WAAW,kBAAC,GAAD,EAAW,SAAQ,+CAAgD,CAAA;IACjF,EAAA,CAAA,EACN,kBAAC,OAAD,EAAA,UAAA;IACE,kBAAC,SAAD;KAAO,SAAS,GAAG,EAAS;KAAgB,WAAU;eAAsB;KAEpE,CAAA;IACR,kBAAC,GAAD;KACE,IAAI,GAAG,EAAS;KAChB,eAAY;KACZ,MAAK;KACL,KAAK;KACL,KAAK;KACL,WAAU;KACV,aAAY;KACZ,OAAO,EAAO;KACd,gBAAc,EAAO;KACX;KACV,WAAW,MAAM,EAAS;MAAE,GAAG;MAAQ,aAAa,EAAE,OAAO;MAAO,CAAC;KACrE,CAAA;IACD,EAAO,WAAW,kBAAC,GAAD,EAAW,SAAQ,gDAAiD,CAAA;IACnF,EAAA,CAAA,CACF;KACF;;;AAKV,SAAgB,EAA0B,GAA2C;AACnF,QACE,CAAC,EAAc,EAAO,YAAY;EAAE,KAAK;EAAG,KAAK;EAAI,CAAC,CAAC,WACvD,CAAC,EAAc,EAAO,aAAa;EAAE,KAAK;EAAG,KAAK;EAAI,CAAC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"DependencyGraph.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/dependency/DependencyGraph.tsx"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,6FAA6F;IAC7F,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,2FAA2F;IAC3F,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,wFAAwF;IACxF,SAAS,EAAE,MAAM,CAAC;CACnB;AAWD,uGAAuG;AACvG,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAaxD;AAED,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAMjE;AA0CD,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,KAAK,EACL,eAAe,EACf,eAAe,EACf,cAAc,EACd,YAAY,EACZ,SAAS,GACV,EAAE,oBAAoB,2CA0MtB"}
1
+ {"version":3,"file":"DependencyGraph.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/dependency/DependencyGraph.tsx"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AAEH,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,mBAAmB;IAClC,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,8BAA8B;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,6FAA6F;IAC7F,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,2FAA2F;IAC3F,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,wFAAwF;IACxF,SAAS,EAAE,MAAM,CAAC;CACnB;AAWD,uGAAuG;AACvG,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAaxD;AAED,wBAAgB,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAMjE;AA0CD,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,KAAK,EACL,eAAe,EACf,eAAe,EACf,cAAc,EACd,YAAY,EACZ,SAAS,GACV,EAAE,oBAAoB,2CA6MtB"}
@@ -186,7 +186,7 @@ function y({ nodes: y, edges: b, criticalNodeIds: x, criticalEdgeIds: S, onActiv
186
186
  cx: 16,
187
187
  cy: c / 2,
188
188
  r: f,
189
- className: u.fill
189
+ style: { fill: u.color }
190
190
  }),
191
191
  /* @__PURE__ */ a("text", {
192
192
  x: 28,
@@ -210,7 +210,10 @@ function y({ nodes: y, edges: b, criticalNodeIds: x, criticalEdgeIds: S, onActiv
210
210
  className: "flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-input px-3 py-2 text-[11px] text-muted-foreground",
211
211
  children: [D.map((t) => /* @__PURE__ */ o("span", {
212
212
  className: "inline-flex items-center gap-1.5",
213
- children: [/* @__PURE__ */ a("span", { className: `h-2.5 w-2.5 rounded-full ${n(t).swatch}` }), e(t)]
213
+ children: [/* @__PURE__ */ a("span", {
214
+ className: "h-2.5 w-2.5 rounded-full",
215
+ style: { backgroundColor: n(t).color }
216
+ }), e(t)]
214
217
  }, t)), x && x.size > 0 && /* @__PURE__ */ o("span", {
215
218
  className: "inline-flex items-center gap-1.5",
216
219
  children: [/* @__PURE__ */ a("span", { className: "h-2.5 w-2.5 rounded-full border-2 border-primary bg-background" }), "Critical path"]
@@ -1 +1 @@
1
- {"version":3,"file":"DependencyGraph.js","names":[],"sources":["../../../src/planmagnet/dependency/DependencyGraph.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { rowLinkProps } from '../utils/rowNavigation';\nimport { statusVisual, STATUS_LEGEND_ORDER } from '../utils/statusColor';\n\n/**\n * Reusable SVG dependency graph (PDM-27). Nodes are work items, edges are `WorkItemDependency` rows, laid out in\n * layers left-to-right by longest-path topological order. Two mount points share this component:\n * - the critical-path page's \"Graph\" view (the whole project — see useProjectWorkItemsGraph.ts)\n * - the task dependency panel's one-hop neighbourhood graph (this item, its predecessors, its successors)\n *\n * The existing text list stays the accessible/no-JS-heavy fallback at every call site — this component is an\n * additional view, never a replacement for it.\n */\n\nexport interface DependencyGraphNode {\n id: string;\n key: string | null;\n title: string | null;\n status: string;\n}\n\nexport interface DependencyGraphEdge {\n /** The WorkItemDependency's own id — stable per edge, used as the React key. */\n id: string;\n predecessorId: string;\n successorId: string;\n /** A DependencyType value. */\n type: string;\n lagMinutes: number | null;\n}\n\nexport interface DependencyGraphProps {\n nodes: DependencyGraphNode[];\n edges: DependencyGraphEdge[];\n /** Node ids on the critical path (CPM chain) — emphasised with a thicker primary outline. */\n criticalNodeIds?: ReadonlySet<string>;\n /** Edge ids that connect two consecutive critical-path nodes — emphasised the same way. */\n criticalEdgeIds?: ReadonlySet<string>;\n onActivateNode: (nodeId: string) => void;\n /** Shown instead of the graph when `nodes` is empty. */\n emptyMessage: string;\n /** Accessible name for the graph region — should say what project/item it scopes to. */\n ariaLabel: string;\n}\n\nconst NODE_WIDTH = 180;\nconst NODE_HEIGHT = 56;\nconst LAYER_GAP_X = 72;\nconst NODE_GAP_Y = 16;\nconst PADDING = 20;\nconst CHIP_R = 4;\nconst TITLE_MAX_CHARS = 24;\nconst KEY_MAX_CHARS = 16;\n\n/** Shared with GanttChart.tsx, which draws the same dependency edges between bars instead of nodes. */\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) return text;\n return `${text.slice(0, Math.max(1, maxChars - 1))}…`;\n}\n\nexport function dependencyTypeLabel(type: string): string {\n switch (type) {\n case 'FINISH_TO_START':\n return 'Finish to start';\n case 'START_TO_START':\n return 'Start to start';\n case 'FINISH_TO_FINISH':\n return 'Finish to finish';\n case 'START_TO_FINISH':\n return 'Start to finish';\n default:\n return enumLabel(type);\n }\n}\n\nexport function lagLabel(lagMinutes: number | null): string | null {\n if (!lagMinutes) return null;\n const days = lagMinutes / (60 * 24);\n const verb = lagMinutes > 0 ? 'lag' : 'lead';\n if (Number.isInteger(days)) return `${Math.abs(days)}d ${verb}`;\n return `${Math.abs(Math.round((lagMinutes / 60) * 10) / 10)}h ${verb}`;\n}\n\n/**\n * Longest-path layering with a cycle guard. Every node ends up on the layer one past the FURTHEST-along of its\n * (in-graph) predecessors — never merely the first predecessor visited — so an edge never points backward across\n * layers except when a cycle forces it (the server refuses cycles; this is defensive, not the expected path).\n *\n * `resolving` tracks ids currently on the call stack: a predecessor already being resolved (a back-edge closing a\n * cycle) is treated as layer -1 (contributing 0) rather than recursed into again, so this always terminates —\n * every id is visited at most once per still-active ancestor chain, bounded by `nodeIds.length`.\n */\nfunction computeLayers(\n nodeIds: string[],\n edges: { predecessorId: string; successorId: string }[]\n): Map<string, number> {\n const predecessorsOf = new Map<string, string[]>(nodeIds.map((id) => [id, []]));\n for (const e of edges) {\n const preds = predecessorsOf.get(e.successorId);\n if (preds && predecessorsOf.has(e.predecessorId)) preds.push(e.predecessorId);\n }\n\n const layer = new Map<string, number>();\n const resolving = new Set<string>();\n\n function resolve(id: string, depth: number): number {\n const cached = layer.get(id);\n if (cached !== undefined) return cached;\n if (resolving.has(id) || depth > nodeIds.length) return -1;\n resolving.add(id);\n const preds = predecessorsOf.get(id) ?? [];\n let max = -1;\n for (const p of preds) max = Math.max(max, resolve(p, depth + 1));\n resolving.delete(id);\n const result = max + 1;\n layer.set(id, result);\n return result;\n }\n\n for (const id of nodeIds) resolve(id, 0);\n return layer;\n}\n\nexport function DependencyGraph({\n nodes,\n edges,\n criticalNodeIds,\n criticalEdgeIds,\n onActivateNode,\n emptyMessage,\n ariaLabel,\n}: DependencyGraphProps) {\n const layout = useMemo(() => {\n const nodeIds = nodes.map((n) => n.id);\n const nodeSet = new Set(nodeIds);\n // Cross-scope edges (e.g. a dependency on a work item outside this project, or outside this item's one-hop\n // neighbourhood) have no node to draw — drop them rather than crash on a missing endpoint.\n const inScopeEdges = edges.filter(\n (e) => nodeSet.has(e.predecessorId) && nodeSet.has(e.successorId)\n );\n const layerOf = computeLayers(nodeIds, inScopeEdges);\n\n const byLayer = new Map<number, string[]>();\n for (const id of nodeIds) {\n const l = layerOf.get(id) ?? 0;\n const list = byLayer.get(l);\n if (list) list.push(id);\n else byLayer.set(l, [id]);\n }\n const layerCount = byLayer.size === 0 ? 1 : Math.max(...byLayer.keys()) + 1;\n const maxPerLayer = Math.max(1, ...Array.from(byLayer.values(), (l) => l.length));\n\n const position = new Map<string, { x: number; y: number }>();\n for (const [l, ids] of byLayer) {\n const x = PADDING + l * (NODE_WIDTH + LAYER_GAP_X);\n // Centered vertically within the tallest layer, so a short layer doesn't hug the top.\n const layerHeight = ids.length * NODE_HEIGHT + (ids.length - 1) * NODE_GAP_Y;\n const fullHeight = maxPerLayer * NODE_HEIGHT + (maxPerLayer - 1) * NODE_GAP_Y;\n const yOffset = PADDING + Math.max(0, (fullHeight - layerHeight) / 2);\n ids.forEach((id, i) => {\n position.set(id, { x, y: yOffset + i * (NODE_HEIGHT + NODE_GAP_Y) });\n });\n }\n\n const width = PADDING * 2 + layerCount * NODE_WIDTH + Math.max(0, layerCount - 1) * LAYER_GAP_X;\n const height =\n PADDING * 2 + maxPerLayer * NODE_HEIGHT + Math.max(0, maxPerLayer - 1) * NODE_GAP_Y;\n\n return { position, inScopeEdges, width, height };\n }, [nodes, edges]);\n\n const presentStatuses = useMemo(\n () => STATUS_LEGEND_ORDER.filter((s) => nodes.some((n) => n.status === s)),\n [nodes]\n );\n\n if (nodes.length === 0) {\n return (\n <div data-testid=\"dependency-graph\" className=\"p-6 text-center text-sm text-muted-foreground\">\n {emptyMessage}\n </div>\n );\n }\n\n const nodesById = new Map(nodes.map((n) => [n.id, n]));\n\n return (\n <div data-testid=\"dependency-graph\" className=\"rounded-md border border-input bg-background\">\n <p className=\"sr-only\">\n Dependency graph with {nodes.length} work item{nodes.length === 1 ? '' : 's'} and{' '}\n {layout.inScopeEdges.length} dependenc{layout.inScopeEdges.length === 1 ? 'y' : 'ies'}.\n {criticalNodeIds && criticalNodeIds.size > 0\n ? ` ${criticalNodeIds.size} item${criticalNodeIds.size === 1 ? ' is' : 's are'} on the critical path.`\n : ''}{' '}\n The list above/below gives the same information as text.\n </p>\n <div className=\"overflow-x-auto p-3\">\n {/* No role=\"img\" here: each node below is its own focusable role=\"button\" descendant, and \"img\"\n semantics would tell assistive tech to treat the whole subtree as a single flattened image. */}\n <svg\n aria-label={ariaLabel}\n width={layout.width}\n height={layout.height}\n viewBox={`0 0 ${layout.width} ${layout.height}`}\n className=\"block\"\n >\n <defs>\n <marker\n id=\"dependency-graph-arrow\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n className=\"text-border-strong\"\n >\n <path d=\"M0,0 L10,5 L0,10 z\" fill=\"currentColor\" />\n </marker>\n <marker\n id=\"dependency-graph-arrow-critical\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n className=\"text-primary\"\n >\n <path d=\"M0,0 L10,5 L0,10 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n\n {layout.inScopeEdges.map((edge) => {\n const from = layout.position.get(edge.predecessorId);\n const to = layout.position.get(edge.successorId);\n if (!from || !to) return null;\n const x1 = from.x + NODE_WIDTH;\n const y1 = from.y + NODE_HEIGHT / 2;\n const x2 = to.x;\n const y2 = to.y + NODE_HEIGHT / 2;\n const midX = (x1 + x2) / 2;\n const critical = criticalEdgeIds?.has(edge.id) ?? false;\n const typeLabel =\n edge.type === 'FINISH_TO_START' ? null : dependencyTypeLabel(edge.type);\n const lag = lagLabel(edge.lagMinutes);\n const label = [typeLabel, lag].filter((v): v is string => !!v).join(' · ');\n const predecessor = nodesById.get(edge.predecessorId);\n const successor = nodesById.get(edge.successorId);\n return (\n <g key={edge.id} data-testid=\"dependency-graph-edge\">\n <title>\n {predecessor?.key ?? edge.predecessorId} {dependencyTypeLabel(edge.type)}{' '}\n {successor?.key ?? edge.successorId}\n {lag ? ` (${lag})` : ''}\n </title>\n <path\n d={`M ${x1},${y1} C ${midX},${y1} ${midX},${y2} ${x2},${y2}`}\n fill=\"none\"\n strokeWidth={critical ? 2.5 : 1.5}\n className={critical ? 'text-primary' : 'text-border-strong'}\n stroke=\"currentColor\"\n markerEnd={`url(#${critical ? 'dependency-graph-arrow-critical' : 'dependency-graph-arrow'})`}\n />\n {label && (\n <text\n x={midX}\n y={(y1 + y2) / 2 - 4}\n textAnchor=\"middle\"\n className=\"fill-muted-foreground text-[9px]\"\n >\n {label}\n </text>\n )}\n </g>\n );\n })}\n\n {nodes.map((node) => {\n const pos = layout.position.get(node.id);\n if (!pos) return null;\n const critical = criticalNodeIds?.has(node.id) ?? false;\n const visual = statusVisual(node.status);\n const title = node.title ?? 'Untitled work item';\n const keyLabel = node.key ?? '';\n const accessibleLabel = `${keyLabel ? `${keyLabel} — ` : ''}${title} (${enumLabel(node.status)})${critical ? ', on critical path' : ''}`;\n // ROW_LINK_CLASS (in the rowLinkProps spread below) rings via box-shadow, which SVG elements\n // outside a <foreignObject> don't paint — the explicit className after the spread overrides it\n // with an outline-based focus style, which they do.\n return (\n <g\n key={node.id}\n data-testid=\"dependency-graph-node\"\n transform={`translate(${pos.x}, ${pos.y})`}\n {...rowLinkProps<SVGGElement>(() => onActivateNode(node.id), 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 <rect\n width={NODE_WIDTH}\n height={NODE_HEIGHT}\n rx={8}\n className={`fill-background ${critical ? 'stroke-primary' : 'stroke-input'}`}\n strokeWidth={critical ? 2 : 1}\n />\n <circle cx={16} cy={NODE_HEIGHT / 2} r={CHIP_R} className={visual.fill} />\n <text x={28} y={22} className=\"fill-muted-foreground text-[10px] font-medium\">\n {truncate(keyLabel, KEY_MAX_CHARS)}\n </text>\n <text x={28} y={38} className=\"fill-foreground text-[11px] font-medium\">\n {truncate(title, TITLE_MAX_CHARS)}\n </text>\n </g>\n );\n })}\n </svg>\n </div>\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-input px-3 py-2 text-[11px] text-muted-foreground\">\n {presentStatuses.map((status) => (\n <span key={status} className=\"inline-flex items-center gap-1.5\">\n <span className={`h-2.5 w-2.5 rounded-full ${statusVisual(status).swatch}`} />\n {enumLabel(status)}\n </span>\n ))}\n {criticalNodeIds && criticalNodeIds.size > 0 && (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full border-2 border-primary bg-background\" />\n Critical path\n </span>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"DependencyGraph.js","names":[],"sources":["../../../src/planmagnet/dependency/DependencyGraph.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { rowLinkProps } from '../utils/rowNavigation';\nimport { statusVisual, STATUS_LEGEND_ORDER } from '../utils/statusColor';\n\n/**\n * Reusable SVG dependency graph (PDM-27). Nodes are work items, edges are `WorkItemDependency` rows, laid out in\n * layers left-to-right by longest-path topological order. Two mount points share this component:\n * - the critical-path page's \"Graph\" view (the whole project — see useProjectWorkItemsGraph.ts)\n * - the task dependency panel's one-hop neighbourhood graph (this item, its predecessors, its successors)\n *\n * The existing text list stays the accessible/no-JS-heavy fallback at every call site — this component is an\n * additional view, never a replacement for it.\n */\n\nexport interface DependencyGraphNode {\n id: string;\n key: string | null;\n title: string | null;\n status: string;\n}\n\nexport interface DependencyGraphEdge {\n /** The WorkItemDependency's own id — stable per edge, used as the React key. */\n id: string;\n predecessorId: string;\n successorId: string;\n /** A DependencyType value. */\n type: string;\n lagMinutes: number | null;\n}\n\nexport interface DependencyGraphProps {\n nodes: DependencyGraphNode[];\n edges: DependencyGraphEdge[];\n /** Node ids on the critical path (CPM chain) — emphasised with a thicker primary outline. */\n criticalNodeIds?: ReadonlySet<string>;\n /** Edge ids that connect two consecutive critical-path nodes — emphasised the same way. */\n criticalEdgeIds?: ReadonlySet<string>;\n onActivateNode: (nodeId: string) => void;\n /** Shown instead of the graph when `nodes` is empty. */\n emptyMessage: string;\n /** Accessible name for the graph region — should say what project/item it scopes to. */\n ariaLabel: string;\n}\n\nconst NODE_WIDTH = 180;\nconst NODE_HEIGHT = 56;\nconst LAYER_GAP_X = 72;\nconst NODE_GAP_Y = 16;\nconst PADDING = 20;\nconst CHIP_R = 4;\nconst TITLE_MAX_CHARS = 24;\nconst KEY_MAX_CHARS = 16;\n\n/** Shared with GanttChart.tsx, which draws the same dependency edges between bars instead of nodes. */\nexport function truncate(text: string, maxChars: number): string {\n if (text.length <= maxChars) return text;\n return `${text.slice(0, Math.max(1, maxChars - 1))}…`;\n}\n\nexport function dependencyTypeLabel(type: string): string {\n switch (type) {\n case 'FINISH_TO_START':\n return 'Finish to start';\n case 'START_TO_START':\n return 'Start to start';\n case 'FINISH_TO_FINISH':\n return 'Finish to finish';\n case 'START_TO_FINISH':\n return 'Start to finish';\n default:\n return enumLabel(type);\n }\n}\n\nexport function lagLabel(lagMinutes: number | null): string | null {\n if (!lagMinutes) return null;\n const days = lagMinutes / (60 * 24);\n const verb = lagMinutes > 0 ? 'lag' : 'lead';\n if (Number.isInteger(days)) return `${Math.abs(days)}d ${verb}`;\n return `${Math.abs(Math.round((lagMinutes / 60) * 10) / 10)}h ${verb}`;\n}\n\n/**\n * Longest-path layering with a cycle guard. Every node ends up on the layer one past the FURTHEST-along of its\n * (in-graph) predecessors — never merely the first predecessor visited — so an edge never points backward across\n * layers except when a cycle forces it (the server refuses cycles; this is defensive, not the expected path).\n *\n * `resolving` tracks ids currently on the call stack: a predecessor already being resolved (a back-edge closing a\n * cycle) is treated as layer -1 (contributing 0) rather than recursed into again, so this always terminates —\n * every id is visited at most once per still-active ancestor chain, bounded by `nodeIds.length`.\n */\nfunction computeLayers(\n nodeIds: string[],\n edges: { predecessorId: string; successorId: string }[]\n): Map<string, number> {\n const predecessorsOf = new Map<string, string[]>(nodeIds.map((id) => [id, []]));\n for (const e of edges) {\n const preds = predecessorsOf.get(e.successorId);\n if (preds && predecessorsOf.has(e.predecessorId)) preds.push(e.predecessorId);\n }\n\n const layer = new Map<string, number>();\n const resolving = new Set<string>();\n\n function resolve(id: string, depth: number): number {\n const cached = layer.get(id);\n if (cached !== undefined) return cached;\n if (resolving.has(id) || depth > nodeIds.length) return -1;\n resolving.add(id);\n const preds = predecessorsOf.get(id) ?? [];\n let max = -1;\n for (const p of preds) max = Math.max(max, resolve(p, depth + 1));\n resolving.delete(id);\n const result = max + 1;\n layer.set(id, result);\n return result;\n }\n\n for (const id of nodeIds) resolve(id, 0);\n return layer;\n}\n\nexport function DependencyGraph({\n nodes,\n edges,\n criticalNodeIds,\n criticalEdgeIds,\n onActivateNode,\n emptyMessage,\n ariaLabel,\n}: DependencyGraphProps) {\n const layout = useMemo(() => {\n const nodeIds = nodes.map((n) => n.id);\n const nodeSet = new Set(nodeIds);\n // Cross-scope edges (e.g. a dependency on a work item outside this project, or outside this item's one-hop\n // neighbourhood) have no node to draw — drop them rather than crash on a missing endpoint.\n const inScopeEdges = edges.filter(\n (e) => nodeSet.has(e.predecessorId) && nodeSet.has(e.successorId)\n );\n const layerOf = computeLayers(nodeIds, inScopeEdges);\n\n const byLayer = new Map<number, string[]>();\n for (const id of nodeIds) {\n const l = layerOf.get(id) ?? 0;\n const list = byLayer.get(l);\n if (list) list.push(id);\n else byLayer.set(l, [id]);\n }\n const layerCount = byLayer.size === 0 ? 1 : Math.max(...byLayer.keys()) + 1;\n const maxPerLayer = Math.max(1, ...Array.from(byLayer.values(), (l) => l.length));\n\n const position = new Map<string, { x: number; y: number }>();\n for (const [l, ids] of byLayer) {\n const x = PADDING + l * (NODE_WIDTH + LAYER_GAP_X);\n // Centered vertically within the tallest layer, so a short layer doesn't hug the top.\n const layerHeight = ids.length * NODE_HEIGHT + (ids.length - 1) * NODE_GAP_Y;\n const fullHeight = maxPerLayer * NODE_HEIGHT + (maxPerLayer - 1) * NODE_GAP_Y;\n const yOffset = PADDING + Math.max(0, (fullHeight - layerHeight) / 2);\n ids.forEach((id, i) => {\n position.set(id, { x, y: yOffset + i * (NODE_HEIGHT + NODE_GAP_Y) });\n });\n }\n\n const width = PADDING * 2 + layerCount * NODE_WIDTH + Math.max(0, layerCount - 1) * LAYER_GAP_X;\n const height =\n PADDING * 2 + maxPerLayer * NODE_HEIGHT + Math.max(0, maxPerLayer - 1) * NODE_GAP_Y;\n\n return { position, inScopeEdges, width, height };\n }, [nodes, edges]);\n\n const presentStatuses = useMemo(\n () => STATUS_LEGEND_ORDER.filter((s) => nodes.some((n) => n.status === s)),\n [nodes]\n );\n\n if (nodes.length === 0) {\n return (\n <div data-testid=\"dependency-graph\" className=\"p-6 text-center text-sm text-muted-foreground\">\n {emptyMessage}\n </div>\n );\n }\n\n const nodesById = new Map(nodes.map((n) => [n.id, n]));\n\n return (\n <div data-testid=\"dependency-graph\" className=\"rounded-md border border-input bg-background\">\n <p className=\"sr-only\">\n Dependency graph with {nodes.length} work item{nodes.length === 1 ? '' : 's'} and{' '}\n {layout.inScopeEdges.length} dependenc{layout.inScopeEdges.length === 1 ? 'y' : 'ies'}.\n {criticalNodeIds && criticalNodeIds.size > 0\n ? ` ${criticalNodeIds.size} item${criticalNodeIds.size === 1 ? ' is' : 's are'} on the critical path.`\n : ''}{' '}\n The list above/below gives the same information as text.\n </p>\n <div className=\"overflow-x-auto p-3\">\n {/* No role=\"img\" here: each node below is its own focusable role=\"button\" descendant, and \"img\"\n semantics would tell assistive tech to treat the whole subtree as a single flattened image. */}\n <svg\n aria-label={ariaLabel}\n width={layout.width}\n height={layout.height}\n viewBox={`0 0 ${layout.width} ${layout.height}`}\n className=\"block\"\n >\n <defs>\n <marker\n id=\"dependency-graph-arrow\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n className=\"text-border-strong\"\n >\n <path d=\"M0,0 L10,5 L0,10 z\" fill=\"currentColor\" />\n </marker>\n <marker\n id=\"dependency-graph-arrow-critical\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n className=\"text-primary\"\n >\n <path d=\"M0,0 L10,5 L0,10 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n\n {layout.inScopeEdges.map((edge) => {\n const from = layout.position.get(edge.predecessorId);\n const to = layout.position.get(edge.successorId);\n if (!from || !to) return null;\n const x1 = from.x + NODE_WIDTH;\n const y1 = from.y + NODE_HEIGHT / 2;\n const x2 = to.x;\n const y2 = to.y + NODE_HEIGHT / 2;\n const midX = (x1 + x2) / 2;\n const critical = criticalEdgeIds?.has(edge.id) ?? false;\n const typeLabel =\n edge.type === 'FINISH_TO_START' ? null : dependencyTypeLabel(edge.type);\n const lag = lagLabel(edge.lagMinutes);\n const label = [typeLabel, lag].filter((v): v is string => !!v).join(' · ');\n const predecessor = nodesById.get(edge.predecessorId);\n const successor = nodesById.get(edge.successorId);\n return (\n <g key={edge.id} data-testid=\"dependency-graph-edge\">\n <title>\n {predecessor?.key ?? edge.predecessorId} {dependencyTypeLabel(edge.type)}{' '}\n {successor?.key ?? edge.successorId}\n {lag ? ` (${lag})` : ''}\n </title>\n <path\n d={`M ${x1},${y1} C ${midX},${y1} ${midX},${y2} ${x2},${y2}`}\n fill=\"none\"\n strokeWidth={critical ? 2.5 : 1.5}\n className={critical ? 'text-primary' : 'text-border-strong'}\n stroke=\"currentColor\"\n markerEnd={`url(#${critical ? 'dependency-graph-arrow-critical' : 'dependency-graph-arrow'})`}\n />\n {label && (\n <text\n x={midX}\n y={(y1 + y2) / 2 - 4}\n textAnchor=\"middle\"\n className=\"fill-muted-foreground text-[9px]\"\n >\n {label}\n </text>\n )}\n </g>\n );\n })}\n\n {nodes.map((node) => {\n const pos = layout.position.get(node.id);\n if (!pos) return null;\n const critical = criticalNodeIds?.has(node.id) ?? false;\n const visual = statusVisual(node.status);\n const title = node.title ?? 'Untitled work item';\n const keyLabel = node.key ?? '';\n const accessibleLabel = `${keyLabel ? `${keyLabel} — ` : ''}${title} (${enumLabel(node.status)})${critical ? ', on critical path' : ''}`;\n // ROW_LINK_CLASS (in the rowLinkProps spread below) rings via box-shadow, which SVG elements\n // outside a <foreignObject> don't paint — the explicit className after the spread overrides it\n // with an outline-based focus style, which they do.\n return (\n <g\n key={node.id}\n data-testid=\"dependency-graph-node\"\n transform={`translate(${pos.x}, ${pos.y})`}\n {...rowLinkProps<SVGGElement>(() => onActivateNode(node.id), 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 <rect\n width={NODE_WIDTH}\n height={NODE_HEIGHT}\n rx={8}\n className={`fill-background ${critical ? 'stroke-primary' : 'stroke-input'}`}\n strokeWidth={critical ? 2 : 1}\n />\n <circle cx={16} cy={NODE_HEIGHT / 2} r={CHIP_R} style={{ fill: visual.color }} />\n <text x={28} y={22} className=\"fill-muted-foreground text-[10px] font-medium\">\n {truncate(keyLabel, KEY_MAX_CHARS)}\n </text>\n <text x={28} y={38} className=\"fill-foreground text-[11px] font-medium\">\n {truncate(title, TITLE_MAX_CHARS)}\n </text>\n </g>\n );\n })}\n </svg>\n </div>\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-input px-3 py-2 text-[11px] text-muted-foreground\">\n {presentStatuses.map((status) => (\n <span key={status} className=\"inline-flex items-center gap-1.5\">\n <span\n className=\"h-2.5 w-2.5 rounded-full\"\n style={{ backgroundColor: statusVisual(status).color }}\n />\n {enumLabel(status)}\n </span>\n ))}\n {criticalNodeIds && criticalNodeIds.size > 0 && (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full border-2 border-primary bg-background\" />\n Critical path\n </span>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"GanttChart.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/dependency/GanttChart.tsx"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,eAAe,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,2GAA2G;IAC3G,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,4EAA4E;IAC5E,KAAK,CAAC,EAAE,IAAI,CAAC;CACd;AAsED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,eAAe,EACf,IAAI,EACJ,aAAa,EACb,KAAK,GACN,EAAE,eAAe,2CA6SjB"}
1
+ {"version":3,"file":"GanttChart.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/dependency/GanttChart.tsx"],"names":[],"mappings":"AAKA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,eAAe,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,2GAA2G;IAC3G,eAAe,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACtC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,aAAa,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,4EAA4E;IAC5E,KAAK,CAAC,EAAE,IAAI,CAAC;CACd;AAsED,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,eAAe,EACf,IAAI,EACJ,aAAa,EACb,KAAK,GACN,EAAE,eAAe,2CAmTjB"}
@@ -200,13 +200,14 @@ function T({ items: b, criticalIds: T, criticalEdgeIds: E, zoom: D, onActivateRo
200
200
  x2: A.x(e.day),
201
201
  y1: 0,
202
202
  y2: A.plotHeight,
203
- className: "text-chart-grid",
203
+ style: { color: "var(--color-chart-grid)" },
204
204
  stroke: "currentColor",
205
205
  strokeWidth: 1
206
206
  }), /* @__PURE__ */ s("text", {
207
207
  x: A.x(e.day) + 3,
208
208
  y: p - 10,
209
- className: "fill-chart-axis text-[10px]",
209
+ className: "text-[10px]",
210
+ style: { fill: "var(--color-chart-axis)" },
210
211
  children: e.label
211
212
  })] }, e.day)),
212
213
  /* @__PURE__ */ s("line", {
@@ -250,7 +251,8 @@ function T({ items: b, criticalIds: T, criticalEdgeIds: E, zoom: D, onActivateRo
250
251
  width: h,
251
252
  height: h,
252
253
  transform: `rotate(45 ${e} ${i})`,
253
- className: `${o.fill} ${a ? "stroke-primary" : "stroke-input"}`,
254
+ className: a ? "stroke-primary" : "stroke-input",
255
+ style: { fill: o.color },
254
256
  strokeWidth: a ? 2 : 1
255
257
  })]
256
258
  }, t.item.id);
@@ -264,7 +266,8 @@ function T({ items: b, criticalIds: T, criticalEdgeIds: E, zoom: D, onActivateRo
264
266
  width: _,
265
267
  height: m,
266
268
  rx: 4,
267
- className: `${o.fill} ${a ? "stroke-primary" : "stroke-input"}`,
269
+ className: a ? "stroke-primary" : "stroke-input",
270
+ style: { fill: o.color },
268
271
  strokeWidth: a ? 2 : 1
269
272
  })]
270
273
  }, t.item.id);
@@ -278,7 +281,10 @@ function T({ items: b, criticalIds: T, criticalEdgeIds: E, zoom: D, onActivateRo
278
281
  children: [
279
282
  j.map((t) => /* @__PURE__ */ c("span", {
280
283
  className: "inline-flex items-center gap-1.5",
281
- children: [/* @__PURE__ */ s("span", { className: `h-2.5 w-2.5 rounded-full ${n(t).swatch}` }), e(t)]
284
+ children: [/* @__PURE__ */ s("span", {
285
+ className: "h-2.5 w-2.5 rounded-full",
286
+ style: { backgroundColor: n(t).color }
287
+ }), e(t)]
282
288
  }, t)),
283
289
  /* @__PURE__ */ c("span", {
284
290
  className: "inline-flex items-center gap-1.5",
@@ -1 +1 @@
1
- {"version":3,"file":"GanttChart.js","names":[],"sources":["../../../src/planmagnet/dependency/GanttChart.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { statusVisual, STATUS_LEGEND_ORDER } from '../utils/statusColor';\nimport { dependencyTypeLabel, lagLabel, truncate } from './DependencyGraph';\n\n/**\n * Gantt timeline (ANR-24) for one project's work items. Plain SVG (recharts has no Gantt/timeline mark) — see\n * ProjectsGanttPagePage.tsx for data fetching, project choice, and the week/month zoom control this renders to.\n *\n * SCHEDULE SOURCE: `WorkItem` has no persisted start date in the current schema (`graphql/wspace.graphql` —\n * only `dueDate` and `estimateMinutes`). A bar's start is therefore DERIVED, backward from the due date, as\n * `dueDate - estimateMinutes`: an item with a due date AND a positive estimate draws as a bar; an item with a\n * due date but no (or a zero/negative) estimate draws as a milestone diamond, since there is no span to show; an\n * item with no due date at all has no anchor to place it on the axis and is counted in the unscheduled note\n * instead. This is called out again in the delivery notes — it is a real deviation from a literal \"has its own\n * start date\" reading, forced by the schema as it stands today.\n */\n\nexport interface GanttDependency {\n id: string;\n successorId: string;\n type: string;\n lagMinutes: number | null;\n}\n\nexport interface GanttWorkItem {\n id: string;\n key: string;\n title: string;\n status: string;\n dueDate: string | null;\n estimateMinutes: number | null;\n dependencies: GanttDependency[];\n}\n\nexport interface GanttChartProps {\n items: GanttWorkItem[];\n /** Work item ids on the critical path — their bars/diamonds are emphasised. */\n criticalIds?: ReadonlySet<string>;\n /** WorkItemDependency ids connecting two CONSECUTIVE critical-path items — their arrows are emphasised. */\n criticalEdgeIds?: ReadonlySet<string>;\n zoom: 'week' | 'month';\n onActivateRow: (id: string) => void;\n /** Injectable for predictable output; defaults to the real current time. */\n today?: Date;\n}\n\nconst MS_PER_DAY = 86_400_000;\nconst LABEL_COL_WIDTH = 224;\nconst ROW_HEIGHT = 28;\nconst ROW_GAP = 8;\nconst ROW_STEP = ROW_HEIGHT + ROW_GAP;\nconst AXIS_HEIGHT = 28;\nconst BAR_HEIGHT = 18;\nconst DIAMOND_SIZE = 12;\nconst SVG_PADDING = 12;\nconst DOMAIN_PAD_DAYS = 3;\n\nconst ZOOM_PX_PER_DAY: Record<GanttChartProps['zoom'], number> = { week: 28, month: 6 };\n\nfunction utcDayNumber(d: Date): number {\n return Math.floor(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) / MS_PER_DAY);\n}\n\nfunction dayNumberFromIso(iso: string): number {\n return utcDayNumber(new Date(iso));\n}\n\nfunction formatShortDate(dayNum: number): string {\n const d = new Date(dayNum * MS_PER_DAY);\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' });\n}\n\nfunction formatMonth(dayNum: number): string {\n const d = new Date(dayNum * MS_PER_DAY);\n return d.toLocaleDateString(undefined, { month: 'short', year: 'numeric', timeZone: 'UTC' });\n}\n\ntype ScheduledRow =\n | {\n kind: 'bar';\n item: GanttWorkItem;\n startDay: number;\n dueDay: number;\n sortDay: number;\n }\n | {\n kind: 'milestone';\n item: GanttWorkItem;\n dueDay: number;\n sortDay: number;\n };\n\nfunction scheduleItem(item: GanttWorkItem): ScheduledRow | null {\n if (!item.dueDate) return null;\n const dueDay = dayNumberFromIso(item.dueDate);\n if (item.estimateMinutes && item.estimateMinutes > 0) {\n const estimateDays = Math.max(1, Math.ceil(item.estimateMinutes / (60 * 24)));\n const startDay = dueDay - (estimateDays - 1);\n return { kind: 'bar', item, startDay, dueDay, sortDay: startDay };\n }\n return { kind: 'milestone', item, dueDay, sortDay: dueDay };\n}\n\n/**\n * Anchor x (in fractional day units, fed straight into `x()`) for one end of a row's mark. Must match the\n * geometry the row actually renders at: a bar spans a full day-width per day, so its true right edge sits one\n * day past `dueDay` (see `x2` at the bar render site below), and a milestone diamond is centered half a\n * day-width past `dueDay` (see `cx` at the milestone render site below) — not at `dueDay` itself either end.\n */\nfunction anchorDay(row: ScheduledRow, end: 'start' | 'end'): number {\n if (row.kind === 'milestone') return row.dueDay + 0.5;\n return end === 'start' ? row.startDay : row.dueDay + 1;\n}\n\nexport function GanttChart({\n items,\n criticalIds,\n criticalEdgeIds,\n zoom,\n onActivateRow,\n today,\n}: GanttChartProps) {\n const layout = useMemo(() => {\n const scheduled: ScheduledRow[] = [];\n const unscheduled: GanttWorkItem[] = [];\n for (const item of items) {\n const row = scheduleItem(item);\n if (row) scheduled.push(row);\n else unscheduled.push(item);\n }\n scheduled.sort((a, b) => a.sortDay - b.sortDay || a.item.key.localeCompare(b.item.key));\n\n const rowByItemId = new Map(scheduled.map((r, i) => [r.item.id, { row: r, index: i }]));\n const todayDay = utcDayNumber(today ?? new Date());\n\n const days = scheduled.flatMap((r) => (r.kind === 'bar' ? [r.startDay, r.dueDay] : [r.dueDay]));\n days.push(todayDay);\n const domainStart = Math.min(...days) - DOMAIN_PAD_DAYS;\n const domainEnd = Math.max(...days) + DOMAIN_PAD_DAYS;\n const pxPerDay = ZOOM_PX_PER_DAY[zoom];\n const x = (day: number) => SVG_PADDING + (day - domainStart) * pxPerDay;\n\n const plotWidth = SVG_PADDING * 2 + (domainEnd - domainStart + 1) * pxPerDay;\n const plotHeight = AXIS_HEIGHT + Math.max(1, scheduled.length) * ROW_STEP + SVG_PADDING;\n\n const ticks: { day: number; label: string }[] = [];\n for (let d = domainStart; d <= domainEnd; d++) {\n const date = new Date(d * MS_PER_DAY);\n if (zoom === 'week' && date.getUTCDay() === 1)\n ticks.push({ day: d, label: formatShortDate(d) });\n if (zoom === 'month' && date.getUTCDate() === 1)\n ticks.push({ day: d, label: formatMonth(d) });\n }\n\n const arrows = scheduled.flatMap(({ item: predecessorItem }) => {\n const from = rowByItemId.get(predecessorItem.id);\n if (!from) return [];\n return predecessorItem.dependencies.flatMap((dep) => {\n const to = rowByItemId.get(dep.successorId);\n if (!to) return [];\n const fromEnd =\n dep.type === 'START_TO_START' || dep.type === 'START_TO_FINISH' ? 'start' : 'end';\n const toEnd =\n dep.type === 'FINISH_TO_FINISH' || dep.type === 'START_TO_FINISH' ? 'end' : 'start';\n return [\n {\n id: dep.id,\n type: dep.type,\n lagMinutes: dep.lagMinutes,\n x1: x(anchorDay(from.row, fromEnd)),\n y1: AXIS_HEIGHT + from.index * ROW_STEP + ROW_HEIGHT / 2,\n x2: x(anchorDay(to.row, toEnd)),\n y2: AXIS_HEIGHT + to.index * ROW_STEP + ROW_HEIGHT / 2,\n },\n ];\n });\n });\n\n return {\n scheduled,\n unscheduled,\n todayX: x(todayDay),\n ticks,\n arrows,\n plotWidth,\n plotHeight,\n x,\n };\n }, [items, zoom, today]);\n\n const presentStatuses = useMemo(\n () => STATUS_LEGEND_ORDER.filter((s) => items.some((i) => i.status === s)),\n [items]\n );\n\n return (\n <div className=\"rounded-md border border-input bg-background\" data-testid=\"gantt-chart\">\n <p className=\"sr-only\">\n Gantt timeline with {layout.scheduled.length} scheduled work item\n {layout.scheduled.length === 1 ? '' : 's'}\n {layout.unscheduled.length > 0\n ? ` and ${layout.unscheduled.length} unscheduled item${layout.unscheduled.length === 1 ? '' : 's'} listed below the chart`\n : ''}\n . Row labels open the task detail page.\n </p>\n <div className=\"flex\">\n <div style={{ width: LABEL_COL_WIDTH }} className=\"shrink-0 border-r border-input\">\n <div style={{ height: AXIS_HEIGHT }} className=\"border-b border-input\" />\n {layout.scheduled.map((row) => {\n const critical = criticalIds?.has(row.item.id) ?? false;\n return (\n <div\n key={row.item.id}\n data-testid=\"gantt-row\"\n style={{ height: ROW_STEP }}\n className=\"flex items-center px-2\"\n >\n <button\n type=\"button\"\n onClick={() => onActivateRow(row.item.id)}\n className={`min-w-0 truncate text-left text-xs hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary ${critical ? 'font-semibold text-primary' : 'text-foreground'}`}\n title={`${row.item.key} ${row.item.title}`}\n >\n {row.item.key} <span className=\"font-normal\">{truncate(row.item.title, 28)}</span>\n </button>\n </div>\n );\n })}\n {layout.scheduled.length === 0 && (\n <p className=\"px-2 py-3 text-xs text-muted-foreground\">No scheduled work items.</p>\n )}\n </div>\n\n <div className=\"overflow-x-auto\">\n <svg\n aria-label=\"Project timeline\"\n width={layout.plotWidth}\n height={layout.plotHeight}\n viewBox={`0 0 ${layout.plotWidth} ${layout.plotHeight}`}\n className=\"block\"\n >\n <defs>\n <marker\n id=\"gantt-arrow\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n className=\"text-border-strong\"\n >\n <path d=\"M0,0 L10,5 L0,10 z\" fill=\"currentColor\" />\n </marker>\n <marker\n id=\"gantt-arrow-critical\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n className=\"text-primary\"\n >\n <path d=\"M0,0 L10,5 L0,10 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n\n {layout.ticks.map((tick) => (\n <g key={tick.day}>\n <line\n x1={layout.x(tick.day)}\n x2={layout.x(tick.day)}\n y1={0}\n y2={layout.plotHeight}\n className=\"text-chart-grid\"\n stroke=\"currentColor\"\n strokeWidth={1}\n />\n <text\n x={layout.x(tick.day) + 3}\n y={AXIS_HEIGHT - 10}\n className=\"fill-chart-axis text-[10px]\"\n >\n {tick.label}\n </text>\n </g>\n ))}\n\n <line\n x1={layout.todayX}\n x2={layout.todayX}\n y1={0}\n y2={layout.plotHeight}\n className=\"text-status-info-bg\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeDasharray=\"4 3\"\n />\n <text\n x={layout.todayX + 4}\n y={12}\n className=\"fill-status-info-text text-[10px] font-medium\"\n >\n Today\n </text>\n\n {layout.arrows.map((arrow) => {\n const midX = (arrow.x1 + arrow.x2) / 2;\n const critical = criticalEdgeIds?.has(arrow.id) ?? false;\n return (\n <path\n key={arrow.id}\n data-testid=\"gantt-arrow\"\n d={`M ${arrow.x1},${arrow.y1} C ${midX},${arrow.y1} ${midX},${arrow.y2} ${arrow.x2},${arrow.y2}`}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={critical ? 2 : 1.25}\n className={critical ? 'text-primary' : 'text-border-strong'}\n markerEnd={`url(#${critical ? 'gantt-arrow-critical' : 'gantt-arrow'})`}\n >\n <title>\n {dependencyTypeLabel(arrow.type)}\n {lagLabel(arrow.lagMinutes) ? ` (${lagLabel(arrow.lagMinutes)})` : ''}\n </title>\n </path>\n );\n })}\n\n {layout.scheduled.map((row, index) => {\n const y = AXIS_HEIGHT + index * ROW_STEP + ROW_HEIGHT / 2;\n const critical = criticalIds?.has(row.item.id) ?? false;\n const visual = statusVisual(row.item.status);\n const label = `${row.item.key} — ${row.item.title} (${enumLabel(row.item.status)})${critical ? ', on critical path' : ''}`;\n if (row.kind === 'milestone') {\n const cx = layout.x(row.dueDay) + ZOOM_PX_PER_DAY[zoom] / 2;\n const r = DIAMOND_SIZE / 2;\n return (\n <g key={row.item.id} data-testid=\"gantt-bar\">\n <title>{label} — milestone</title>\n <rect\n x={cx - r}\n y={y - r}\n width={DIAMOND_SIZE}\n height={DIAMOND_SIZE}\n transform={`rotate(45 ${cx} ${y})`}\n className={`${visual.fill} ${critical ? 'stroke-primary' : 'stroke-input'}`}\n strokeWidth={critical ? 2 : 1}\n />\n </g>\n );\n }\n const x1 = layout.x(row.startDay);\n const x2 = layout.x(row.dueDay) + ZOOM_PX_PER_DAY[zoom];\n const width = Math.max(ZOOM_PX_PER_DAY[zoom], x2 - x1);\n return (\n <g key={row.item.id} data-testid=\"gantt-bar\">\n <title>{label}</title>\n <rect\n x={x1}\n y={y - BAR_HEIGHT / 2}\n width={width}\n height={BAR_HEIGHT}\n rx={4}\n className={`${visual.fill} ${critical ? 'stroke-primary' : 'stroke-input'}`}\n strokeWidth={critical ? 2 : 1}\n />\n </g>\n );\n })}\n </svg>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-input px-3 py-2 text-[11px] text-muted-foreground\">\n {presentStatuses.map((status) => (\n <span key={status} className=\"inline-flex items-center gap-1.5\">\n <span className={`h-2.5 w-2.5 rounded-full ${statusVisual(status).swatch}`} />\n {enumLabel(status)}\n </span>\n ))}\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rotate-45 border-2 border-muted-foreground bg-background\" />\n Milestone\n </span>\n {criticalIds && criticalIds.size > 0 && (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full border-2 border-primary bg-background\" />\n Critical path\n </span>\n )}\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"h-2.5 w-0.5 bg-status-info-bg\" />\n Today\n </span>\n </div>\n\n {layout.unscheduled.length > 0 && (\n <div\n data-testid=\"gantt-unscheduled\"\n className=\"border-t border-input px-3 py-2 text-xs text-muted-foreground\"\n >\n <p className=\"font-medium text-foreground\">\n {layout.unscheduled.length} item{layout.unscheduled.length === 1 ? '' : 's'} without a\n due date {layout.unscheduled.length === 1 ? \"isn't\" : \"aren't\"} shown above:\n </p>\n <div className=\"mt-1 flex flex-wrap gap-x-3 gap-y-1\">\n {layout.unscheduled.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => onActivateRow(item.id)}\n className=\"hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary\"\n >\n {item.key}\n </button>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"GanttChart.js","names":[],"sources":["../../../src/planmagnet/dependency/GanttChart.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { enumLabel } from '../controls/WorkItemControls';\nimport { statusVisual, STATUS_LEGEND_ORDER } from '../utils/statusColor';\nimport { dependencyTypeLabel, lagLabel, truncate } from './DependencyGraph';\n\n/**\n * Gantt timeline (ANR-24) for one project's work items. Plain SVG (recharts has no Gantt/timeline mark) — see\n * ProjectsGanttPagePage.tsx for data fetching, project choice, and the week/month zoom control this renders to.\n *\n * SCHEDULE SOURCE: `WorkItem` has no persisted start date in the current schema (`graphql/wspace.graphql` —\n * only `dueDate` and `estimateMinutes`). A bar's start is therefore DERIVED, backward from the due date, as\n * `dueDate - estimateMinutes`: an item with a due date AND a positive estimate draws as a bar; an item with a\n * due date but no (or a zero/negative) estimate draws as a milestone diamond, since there is no span to show; an\n * item with no due date at all has no anchor to place it on the axis and is counted in the unscheduled note\n * instead. This is called out again in the delivery notes — it is a real deviation from a literal \"has its own\n * start date\" reading, forced by the schema as it stands today.\n */\n\nexport interface GanttDependency {\n id: string;\n successorId: string;\n type: string;\n lagMinutes: number | null;\n}\n\nexport interface GanttWorkItem {\n id: string;\n key: string;\n title: string;\n status: string;\n dueDate: string | null;\n estimateMinutes: number | null;\n dependencies: GanttDependency[];\n}\n\nexport interface GanttChartProps {\n items: GanttWorkItem[];\n /** Work item ids on the critical path — their bars/diamonds are emphasised. */\n criticalIds?: ReadonlySet<string>;\n /** WorkItemDependency ids connecting two CONSECUTIVE critical-path items — their arrows are emphasised. */\n criticalEdgeIds?: ReadonlySet<string>;\n zoom: 'week' | 'month';\n onActivateRow: (id: string) => void;\n /** Injectable for predictable output; defaults to the real current time. */\n today?: Date;\n}\n\nconst MS_PER_DAY = 86_400_000;\nconst LABEL_COL_WIDTH = 224;\nconst ROW_HEIGHT = 28;\nconst ROW_GAP = 8;\nconst ROW_STEP = ROW_HEIGHT + ROW_GAP;\nconst AXIS_HEIGHT = 28;\nconst BAR_HEIGHT = 18;\nconst DIAMOND_SIZE = 12;\nconst SVG_PADDING = 12;\nconst DOMAIN_PAD_DAYS = 3;\n\nconst ZOOM_PX_PER_DAY: Record<GanttChartProps['zoom'], number> = { week: 28, month: 6 };\n\nfunction utcDayNumber(d: Date): number {\n return Math.floor(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) / MS_PER_DAY);\n}\n\nfunction dayNumberFromIso(iso: string): number {\n return utcDayNumber(new Date(iso));\n}\n\nfunction formatShortDate(dayNum: number): string {\n const d = new Date(dayNum * MS_PER_DAY);\n return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' });\n}\n\nfunction formatMonth(dayNum: number): string {\n const d = new Date(dayNum * MS_PER_DAY);\n return d.toLocaleDateString(undefined, { month: 'short', year: 'numeric', timeZone: 'UTC' });\n}\n\ntype ScheduledRow =\n | {\n kind: 'bar';\n item: GanttWorkItem;\n startDay: number;\n dueDay: number;\n sortDay: number;\n }\n | {\n kind: 'milestone';\n item: GanttWorkItem;\n dueDay: number;\n sortDay: number;\n };\n\nfunction scheduleItem(item: GanttWorkItem): ScheduledRow | null {\n if (!item.dueDate) return null;\n const dueDay = dayNumberFromIso(item.dueDate);\n if (item.estimateMinutes && item.estimateMinutes > 0) {\n const estimateDays = Math.max(1, Math.ceil(item.estimateMinutes / (60 * 24)));\n const startDay = dueDay - (estimateDays - 1);\n return { kind: 'bar', item, startDay, dueDay, sortDay: startDay };\n }\n return { kind: 'milestone', item, dueDay, sortDay: dueDay };\n}\n\n/**\n * Anchor x (in fractional day units, fed straight into `x()`) for one end of a row's mark. Must match the\n * geometry the row actually renders at: a bar spans a full day-width per day, so its true right edge sits one\n * day past `dueDay` (see `x2` at the bar render site below), and a milestone diamond is centered half a\n * day-width past `dueDay` (see `cx` at the milestone render site below) — not at `dueDay` itself either end.\n */\nfunction anchorDay(row: ScheduledRow, end: 'start' | 'end'): number {\n if (row.kind === 'milestone') return row.dueDay + 0.5;\n return end === 'start' ? row.startDay : row.dueDay + 1;\n}\n\nexport function GanttChart({\n items,\n criticalIds,\n criticalEdgeIds,\n zoom,\n onActivateRow,\n today,\n}: GanttChartProps) {\n const layout = useMemo(() => {\n const scheduled: ScheduledRow[] = [];\n const unscheduled: GanttWorkItem[] = [];\n for (const item of items) {\n const row = scheduleItem(item);\n if (row) scheduled.push(row);\n else unscheduled.push(item);\n }\n scheduled.sort((a, b) => a.sortDay - b.sortDay || a.item.key.localeCompare(b.item.key));\n\n const rowByItemId = new Map(scheduled.map((r, i) => [r.item.id, { row: r, index: i }]));\n const todayDay = utcDayNumber(today ?? new Date());\n\n const days = scheduled.flatMap((r) => (r.kind === 'bar' ? [r.startDay, r.dueDay] : [r.dueDay]));\n days.push(todayDay);\n const domainStart = Math.min(...days) - DOMAIN_PAD_DAYS;\n const domainEnd = Math.max(...days) + DOMAIN_PAD_DAYS;\n const pxPerDay = ZOOM_PX_PER_DAY[zoom];\n const x = (day: number) => SVG_PADDING + (day - domainStart) * pxPerDay;\n\n const plotWidth = SVG_PADDING * 2 + (domainEnd - domainStart + 1) * pxPerDay;\n const plotHeight = AXIS_HEIGHT + Math.max(1, scheduled.length) * ROW_STEP + SVG_PADDING;\n\n const ticks: { day: number; label: string }[] = [];\n for (let d = domainStart; d <= domainEnd; d++) {\n const date = new Date(d * MS_PER_DAY);\n if (zoom === 'week' && date.getUTCDay() === 1)\n ticks.push({ day: d, label: formatShortDate(d) });\n if (zoom === 'month' && date.getUTCDate() === 1)\n ticks.push({ day: d, label: formatMonth(d) });\n }\n\n const arrows = scheduled.flatMap(({ item: predecessorItem }) => {\n const from = rowByItemId.get(predecessorItem.id);\n if (!from) return [];\n return predecessorItem.dependencies.flatMap((dep) => {\n const to = rowByItemId.get(dep.successorId);\n if (!to) return [];\n const fromEnd =\n dep.type === 'START_TO_START' || dep.type === 'START_TO_FINISH' ? 'start' : 'end';\n const toEnd =\n dep.type === 'FINISH_TO_FINISH' || dep.type === 'START_TO_FINISH' ? 'end' : 'start';\n return [\n {\n id: dep.id,\n type: dep.type,\n lagMinutes: dep.lagMinutes,\n x1: x(anchorDay(from.row, fromEnd)),\n y1: AXIS_HEIGHT + from.index * ROW_STEP + ROW_HEIGHT / 2,\n x2: x(anchorDay(to.row, toEnd)),\n y2: AXIS_HEIGHT + to.index * ROW_STEP + ROW_HEIGHT / 2,\n },\n ];\n });\n });\n\n return {\n scheduled,\n unscheduled,\n todayX: x(todayDay),\n ticks,\n arrows,\n plotWidth,\n plotHeight,\n x,\n };\n }, [items, zoom, today]);\n\n const presentStatuses = useMemo(\n () => STATUS_LEGEND_ORDER.filter((s) => items.some((i) => i.status === s)),\n [items]\n );\n\n return (\n <div className=\"rounded-md border border-input bg-background\" data-testid=\"gantt-chart\">\n <p className=\"sr-only\">\n Gantt timeline with {layout.scheduled.length} scheduled work item\n {layout.scheduled.length === 1 ? '' : 's'}\n {layout.unscheduled.length > 0\n ? ` and ${layout.unscheduled.length} unscheduled item${layout.unscheduled.length === 1 ? '' : 's'} listed below the chart`\n : ''}\n . Row labels open the task detail page.\n </p>\n <div className=\"flex\">\n <div style={{ width: LABEL_COL_WIDTH }} className=\"shrink-0 border-r border-input\">\n <div style={{ height: AXIS_HEIGHT }} className=\"border-b border-input\" />\n {layout.scheduled.map((row) => {\n const critical = criticalIds?.has(row.item.id) ?? false;\n return (\n <div\n key={row.item.id}\n data-testid=\"gantt-row\"\n style={{ height: ROW_STEP }}\n className=\"flex items-center px-2\"\n >\n <button\n type=\"button\"\n onClick={() => onActivateRow(row.item.id)}\n className={`min-w-0 truncate text-left text-xs hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary ${critical ? 'font-semibold text-primary' : 'text-foreground'}`}\n title={`${row.item.key} ${row.item.title}`}\n >\n {row.item.key} <span className=\"font-normal\">{truncate(row.item.title, 28)}</span>\n </button>\n </div>\n );\n })}\n {layout.scheduled.length === 0 && (\n <p className=\"px-2 py-3 text-xs text-muted-foreground\">No scheduled work items.</p>\n )}\n </div>\n\n <div className=\"overflow-x-auto\">\n <svg\n aria-label=\"Project timeline\"\n width={layout.plotWidth}\n height={layout.plotHeight}\n viewBox={`0 0 ${layout.plotWidth} ${layout.plotHeight}`}\n className=\"block\"\n >\n <defs>\n <marker\n id=\"gantt-arrow\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n className=\"text-border-strong\"\n >\n <path d=\"M0,0 L10,5 L0,10 z\" fill=\"currentColor\" />\n </marker>\n <marker\n id=\"gantt-arrow-critical\"\n viewBox=\"0 0 10 10\"\n refX=\"9\"\n refY=\"5\"\n markerWidth=\"6\"\n markerHeight=\"6\"\n orient=\"auto-start-reverse\"\n className=\"text-primary\"\n >\n <path d=\"M0,0 L10,5 L0,10 z\" fill=\"currentColor\" />\n </marker>\n </defs>\n\n {layout.ticks.map((tick) => (\n <g key={tick.day}>\n <line\n x1={layout.x(tick.day)}\n x2={layout.x(tick.day)}\n y1={0}\n y2={layout.plotHeight}\n style={{ color: 'var(--color-chart-grid)' }}\n stroke=\"currentColor\"\n strokeWidth={1}\n />\n <text\n x={layout.x(tick.day) + 3}\n y={AXIS_HEIGHT - 10}\n className=\"text-[10px]\"\n style={{ fill: 'var(--color-chart-axis)' }}\n >\n {tick.label}\n </text>\n </g>\n ))}\n\n <line\n x1={layout.todayX}\n x2={layout.todayX}\n y1={0}\n y2={layout.plotHeight}\n className=\"text-status-info-bg\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeDasharray=\"4 3\"\n />\n <text\n x={layout.todayX + 4}\n y={12}\n className=\"fill-status-info-text text-[10px] font-medium\"\n >\n Today\n </text>\n\n {layout.arrows.map((arrow) => {\n const midX = (arrow.x1 + arrow.x2) / 2;\n const critical = criticalEdgeIds?.has(arrow.id) ?? false;\n return (\n <path\n key={arrow.id}\n data-testid=\"gantt-arrow\"\n d={`M ${arrow.x1},${arrow.y1} C ${midX},${arrow.y1} ${midX},${arrow.y2} ${arrow.x2},${arrow.y2}`}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={critical ? 2 : 1.25}\n className={critical ? 'text-primary' : 'text-border-strong'}\n markerEnd={`url(#${critical ? 'gantt-arrow-critical' : 'gantt-arrow'})`}\n >\n <title>\n {dependencyTypeLabel(arrow.type)}\n {lagLabel(arrow.lagMinutes) ? ` (${lagLabel(arrow.lagMinutes)})` : ''}\n </title>\n </path>\n );\n })}\n\n {layout.scheduled.map((row, index) => {\n const y = AXIS_HEIGHT + index * ROW_STEP + ROW_HEIGHT / 2;\n const critical = criticalIds?.has(row.item.id) ?? false;\n const visual = statusVisual(row.item.status);\n const label = `${row.item.key} — ${row.item.title} (${enumLabel(row.item.status)})${critical ? ', on critical path' : ''}`;\n if (row.kind === 'milestone') {\n const cx = layout.x(row.dueDay) + ZOOM_PX_PER_DAY[zoom] / 2;\n const r = DIAMOND_SIZE / 2;\n return (\n <g key={row.item.id} data-testid=\"gantt-bar\">\n <title>{label} — milestone</title>\n <rect\n x={cx - r}\n y={y - r}\n width={DIAMOND_SIZE}\n height={DIAMOND_SIZE}\n transform={`rotate(45 ${cx} ${y})`}\n className={critical ? 'stroke-primary' : 'stroke-input'}\n style={{ fill: visual.color }}\n strokeWidth={critical ? 2 : 1}\n />\n </g>\n );\n }\n const x1 = layout.x(row.startDay);\n const x2 = layout.x(row.dueDay) + ZOOM_PX_PER_DAY[zoom];\n const width = Math.max(ZOOM_PX_PER_DAY[zoom], x2 - x1);\n return (\n <g key={row.item.id} data-testid=\"gantt-bar\">\n <title>{label}</title>\n <rect\n x={x1}\n y={y - BAR_HEIGHT / 2}\n width={width}\n height={BAR_HEIGHT}\n rx={4}\n className={critical ? 'stroke-primary' : 'stroke-input'}\n style={{ fill: visual.color }}\n strokeWidth={critical ? 2 : 1}\n />\n </g>\n );\n })}\n </svg>\n </div>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-1 border-t border-input px-3 py-2 text-[11px] text-muted-foreground\">\n {presentStatuses.map((status) => (\n <span key={status} className=\"inline-flex items-center gap-1.5\">\n <span\n className=\"h-2.5 w-2.5 rounded-full\"\n style={{ backgroundColor: statusVisual(status).color }}\n />\n {enumLabel(status)}\n </span>\n ))}\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rotate-45 border-2 border-muted-foreground bg-background\" />\n Milestone\n </span>\n {criticalIds && criticalIds.size > 0 && (\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"h-2.5 w-2.5 rounded-full border-2 border-primary bg-background\" />\n Critical path\n </span>\n )}\n <span className=\"inline-flex items-center gap-1.5\">\n <span className=\"h-2.5 w-0.5 bg-status-info-bg\" />\n Today\n </span>\n </div>\n\n {layout.unscheduled.length > 0 && (\n <div\n data-testid=\"gantt-unscheduled\"\n className=\"border-t border-input px-3 py-2 text-xs text-muted-foreground\"\n >\n <p className=\"font-medium text-foreground\">\n {layout.unscheduled.length} item{layout.unscheduled.length === 1 ? '' : 's'} without a\n due date {layout.unscheduled.length === 1 ? \"isn't\" : \"aren't\"} shown above:\n </p>\n <div className=\"mt-1 flex flex-wrap gap-x-3 gap-y-1\">\n {layout.unscheduled.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => onActivateRow(item.id)}\n className=\"hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-primary\"\n >\n {item.key}\n </button>\n ))}\n </div>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
@@ -4,4 +4,5 @@ export type { PlanMagnetRootProps, NavItem, FooterItem } from './types';
4
4
  export { usePlanMagnet, usePlanMagnetNavigate } from './providers/PlanMagnetProvider';
5
5
  export { usePlanMagnetStore } from './store/planmagnetStore';
6
6
  export { INTEGRATIONS_SYNCS_PATH, INTEGRATIONS_NEW_SYNC_PATH, INTEGRATIONS_SYNC_DETAIL_PATH, integrationsSyncDetailPath, } from './integrations/routes';
7
+ export { PRIORITIZATION_PATH } from './product/routes';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/planmagnet/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/planmagnet/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAClE,YAAY,EAAE,mBAAmB,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AACtF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,6BAA6B,EAC7B,0BAA0B,GAC3B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import "./providers/PlanMagnetProvider.js";
2
+ import "./product/routes.js";
2
3
  import "./PlanMagnetRoot.js";
3
4
  import "./admin/PlanMagnetAdminRoot.js";
4
5
  import "./store/planmagnetStore.js";
@@ -1 +1 @@
1
- {"version":3,"file":"ProductsFeatureDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsFeatureDetailPagePage.tsx"],"names":[],"mappings":"AAoFA,wBAAgB,6BAA6B,4CAyQ5C"}
1
+ {"version":3,"file":"ProductsFeatureDetailPagePage.d.ts","sourceRoot":"","sources":["../../../src/planmagnet/pages/ProductsFeatureDetailPagePage.tsx"],"names":[],"mappings":"AA6FA,wBAAgB,6BAA6B,4CA4Q5C"}