@forwardimpact/pathway 0.1.0

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 (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. package/package.json +68 -0
@@ -0,0 +1,495 @@
1
+ /**
2
+ * Career progress detail page
3
+ * Shows skill and behaviour progression comparison across discipline × grade × track
4
+ */
5
+
6
+ import { render, div, h1, h2, p, a, label, section } from "../lib/render.js";
7
+ import { getState } from "../lib/state.js";
8
+ import { createBackLink } from "../components/nav.js";
9
+ import { createStatCard } from "../components/card.js";
10
+ import {
11
+ createComparisonSkillRadar,
12
+ createComparisonBehaviourRadar,
13
+ } from "../components/comparison-radar.js";
14
+ import { createProgressionTable } from "../components/progression-table.js";
15
+ import { renderError } from "../components/error-page.js";
16
+ import { createSelectWithValue } from "../lib/form-controls.js";
17
+ import {
18
+ prepareCurrentJob,
19
+ prepareCustomProgression,
20
+ getDefaultTargetGrade,
21
+ isValidCombination,
22
+ } from "../formatters/progress/shared.js";
23
+
24
+ /**
25
+ * Render career progress detail page
26
+ * @param {Object} params - Route params
27
+ */
28
+ export function renderProgressDetail(params) {
29
+ const { discipline: disciplineId, track: trackId, grade: gradeId } = params;
30
+ const { data } = getState();
31
+
32
+ // Find the components
33
+ const discipline = data.disciplines.find((d) => d.id === disciplineId);
34
+ const track = data.tracks.find((t) => t.id === trackId);
35
+ const grade = data.grades.find((g) => g.id === gradeId);
36
+
37
+ if (!discipline || !track || !grade) {
38
+ renderError({
39
+ title: "Role Not Found",
40
+ message: "Invalid role combination. One or more components are missing.",
41
+ backPath: "/career-progress",
42
+ backText: "← Back to Career Progress",
43
+ });
44
+ return;
45
+ }
46
+
47
+ // Prepare current job view
48
+ const currentJobView = prepareCurrentJob({
49
+ discipline,
50
+ grade,
51
+ track,
52
+ skills: data.skills,
53
+ behaviours: data.behaviours,
54
+ });
55
+
56
+ if (!currentJobView) {
57
+ renderError({
58
+ title: "Invalid Combination",
59
+ message: "This discipline, track, and grade combination is not valid.",
60
+ backPath: "/career-progress",
61
+ backText: "← Back to Career Progress",
62
+ });
63
+ return;
64
+ }
65
+
66
+ // Find next grade for default comparison
67
+ const nextGrade = getDefaultTargetGrade(grade, data.grades);
68
+
69
+ const page = div(
70
+ { className: "progress-detail-page" },
71
+ // Header
72
+ div(
73
+ { className: "page-header" },
74
+ createBackLink("/career-progress", "← Back to Career Progress"),
75
+ h1({ className: "page-title" }, "Career Progress"),
76
+ div(
77
+ { className: "page-description" },
78
+ "Current role: ",
79
+ a({ href: `#/discipline/${discipline.id}` }, discipline.specialization),
80
+ " × ",
81
+ a({ href: `#/grade/${grade.id}` }, grade.id),
82
+ " × ",
83
+ a({ href: `#/track/${track.id}` }, track.name),
84
+ ),
85
+ ),
86
+
87
+ // Current role summary
88
+ section(
89
+ { className: "section section-detail" },
90
+ h2({ className: "section-title" }, `📍 Current: ${currentJobView.title}`),
91
+ div(
92
+ { className: "grid grid-3" },
93
+ createStatCard({
94
+ value: currentJobView.skillCount,
95
+ label: "Skills",
96
+ }),
97
+ createStatCard({
98
+ value: currentJobView.behaviourCount,
99
+ label: "Behaviours",
100
+ }),
101
+ createStatCard({
102
+ value: currentJobView.primarySkillCount,
103
+ label: "Primary Skills",
104
+ }),
105
+ ),
106
+ ),
107
+
108
+ // Comparison selectors section
109
+ createComparisonSelectorsSection({
110
+ discipline,
111
+ currentGrade: grade,
112
+ currentTrack: track,
113
+ currentJobView,
114
+ nextGrade,
115
+ data,
116
+ }),
117
+
118
+ // Actions
119
+ div(
120
+ { className: "page-actions", style: "margin-top: 2rem" },
121
+ a(
122
+ {
123
+ href: `#/job/${disciplineId}/${trackId}/${gradeId}`,
124
+ className: "btn btn-secondary",
125
+ },
126
+ "View Full Job Definition",
127
+ ),
128
+ ),
129
+ );
130
+
131
+ render(page);
132
+ }
133
+
134
+ /**
135
+ * Create the comparison selectors section
136
+ * Defaults to same discipline, same track, next grade up
137
+ * @param {Object} params
138
+ * @param {Object} params.discipline - Current discipline
139
+ * @param {Object} params.currentGrade - Current grade
140
+ * @param {Object} params.currentTrack - Current track
141
+ * @param {Object} params.currentJobView - Current job view from presenter
142
+ * @param {Object|null} params.nextGrade - Next grade (for default selection)
143
+ * @param {Object} params.data - Full data object with disciplines, grades, tracks, skills, behaviours
144
+ * @returns {HTMLElement}
145
+ */
146
+ function createComparisonSelectorsSection({
147
+ discipline,
148
+ currentGrade,
149
+ currentTrack,
150
+ currentJobView,
151
+ nextGrade,
152
+ data,
153
+ }) {
154
+ // Create a container for dynamic comparison results
155
+ const comparisonResultsContainer = div({
156
+ className: "comparison-results",
157
+ id: "comparison-results",
158
+ });
159
+
160
+ // State to track current selections - default to same discipline, same track, next grade
161
+ let selectedDisciplineId = discipline.id;
162
+ let selectedGradeId = nextGrade?.id || "";
163
+ let selectedTrackId = currentTrack.id;
164
+
165
+ // Get available options based on selected discipline
166
+ function getAvailableOptions(disciplineId) {
167
+ const selectedDisc = data.disciplines.find((d) => d.id === disciplineId);
168
+ if (!selectedDisc) return { grades: [], tracks: [] };
169
+
170
+ const validGrades = [];
171
+ const validTracks = new Set();
172
+
173
+ for (const grade of data.grades) {
174
+ for (const track of data.tracks) {
175
+ if (isValidCombination({ discipline: selectedDisc, grade, track })) {
176
+ if (!validGrades.find((g) => g.id === grade.id)) {
177
+ validGrades.push(grade);
178
+ }
179
+ validTracks.add(track.id);
180
+ }
181
+ }
182
+ }
183
+
184
+ return {
185
+ grades: validGrades.sort((a, b) => a.level - b.level),
186
+ tracks: data.tracks
187
+ .filter((t) => validTracks.has(t.id))
188
+ .sort((a, b) => a.name.localeCompare(b.name)),
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Update the comparison results based on current selections
194
+ */
195
+ function updateComparison() {
196
+ // Clear previous results
197
+ comparisonResultsContainer.innerHTML = "";
198
+
199
+ if (!selectedDisciplineId || !selectedGradeId || !selectedTrackId) {
200
+ comparisonResultsContainer.appendChild(
201
+ div(
202
+ { className: "comparison-placeholder" },
203
+ p(
204
+ { className: "text-muted" },
205
+ "Select a discipline, track, and grade to see the comparison.",
206
+ ),
207
+ ),
208
+ );
209
+ return;
210
+ }
211
+
212
+ const targetDiscipline = data.disciplines.find(
213
+ (d) => d.id === selectedDisciplineId,
214
+ );
215
+ const targetGrade = data.grades.find((g) => g.id === selectedGradeId);
216
+ const targetTrack = data.tracks.find((t) => t.id === selectedTrackId);
217
+
218
+ if (!targetDiscipline || !targetGrade || !targetTrack) {
219
+ return;
220
+ }
221
+
222
+ // Check if comparing to same role
223
+ if (
224
+ targetDiscipline.id === discipline.id &&
225
+ targetGrade.id === currentGrade.id &&
226
+ targetTrack.id === currentTrack.id
227
+ ) {
228
+ comparisonResultsContainer.appendChild(
229
+ div(
230
+ { className: "comparison-placeholder" },
231
+ p(
232
+ { className: "text-muted" },
233
+ "Select a different role to compare with your current role.",
234
+ ),
235
+ ),
236
+ );
237
+ return;
238
+ }
239
+
240
+ // Use formatter shared module to analyze the progression
241
+ const progressionView = prepareCustomProgression({
242
+ discipline,
243
+ currentGrade,
244
+ currentTrack,
245
+ targetDiscipline,
246
+ targetGrade,
247
+ targetTrack,
248
+ skills: data.skills,
249
+ behaviours: data.behaviours,
250
+ });
251
+
252
+ if (!progressionView) {
253
+ comparisonResultsContainer.appendChild(
254
+ div(
255
+ { className: "comparison-error" },
256
+ p({ className: "text-muted" }, "This combination is not valid."),
257
+ ),
258
+ );
259
+ return;
260
+ }
261
+
262
+ const { skillChanges, behaviourChanges, summary, target } = progressionView;
263
+
264
+ // Build flat comparison result sections
265
+ const result = div(
266
+ { className: "comparison-result" },
267
+
268
+ // Summary stats
269
+ div(
270
+ { className: "grid grid-6" },
271
+ summary.skillsGained > 0
272
+ ? createStatCard({ value: summary.skillsGained, label: "New Skills" })
273
+ : null,
274
+ createStatCard({
275
+ value: summary.skillsUp,
276
+ label: "Skills to Grow",
277
+ }),
278
+ summary.skillsDown > 0
279
+ ? createStatCard({
280
+ value: summary.skillsDown,
281
+ label: "Skills Decrease",
282
+ })
283
+ : null,
284
+ summary.skillsLost > 0
285
+ ? createStatCard({
286
+ value: summary.skillsLost,
287
+ label: "Skills Removed",
288
+ })
289
+ : null,
290
+ createStatCard({
291
+ value: summary.behavioursUp,
292
+ label: "Behaviours to Mature",
293
+ }),
294
+ summary.behavioursDown > 0
295
+ ? createStatCard({
296
+ value: summary.behavioursDown,
297
+ label: "Behaviours Decrease",
298
+ })
299
+ : null,
300
+ ),
301
+
302
+ // Comparison radars
303
+ div(
304
+ { className: "section auto-grid-lg" },
305
+ createComparisonSkillRadar(
306
+ currentJobView.skillMatrix,
307
+ target.skillMatrix,
308
+ {
309
+ title: "Skills Comparison",
310
+ currentLabel: `Current (${currentGrade.id})`,
311
+ targetLabel: `Target (${targetGrade.id})`,
312
+ size: 400,
313
+ },
314
+ ),
315
+ createComparisonBehaviourRadar(
316
+ currentJobView.behaviourProfile,
317
+ target.behaviourProfile,
318
+ {
319
+ title: "Behaviours Comparison",
320
+ currentLabel: `Current (${currentGrade.id})`,
321
+ targetLabel: `Target (${targetGrade.id})`,
322
+ size: 400,
323
+ },
324
+ ),
325
+ ),
326
+
327
+ // Skill changes section
328
+ section(
329
+ { className: "section section-detail" },
330
+ h2({ className: "section-title" }, "Skill Changes"),
331
+ createProgressionTable(skillChanges, "skill"),
332
+ ),
333
+
334
+ // Behaviour changes section
335
+ section(
336
+ { className: "section section-detail" },
337
+ h2({ className: "section-title" }, "Behaviour Changes"),
338
+ createProgressionTable(behaviourChanges, "behaviour"),
339
+ ),
340
+
341
+ // Link to target job
342
+ div(
343
+ { className: "page-actions" },
344
+ a(
345
+ {
346
+ href: `#/job/${targetDiscipline.id}/${targetTrack.id}/${targetGrade.id}`,
347
+ className: "btn btn-secondary",
348
+ },
349
+ `View ${targetGrade.id} ${targetTrack.name} Job Definition →`,
350
+ ),
351
+ ),
352
+ );
353
+
354
+ comparisonResultsContainer.appendChild(result);
355
+ }
356
+
357
+ // Get initial available options
358
+ let availableOptions = getAvailableOptions(selectedDisciplineId);
359
+
360
+ // References to select elements for updating
361
+ let gradeSelectEl = null;
362
+ let trackSelectEl = null;
363
+
364
+ /**
365
+ * Update grade and track selectors when discipline changes
366
+ */
367
+ function updateSelectorsForDiscipline(newDisciplineId) {
368
+ availableOptions = getAvailableOptions(newDisciplineId);
369
+
370
+ // Update grade selector
371
+ if (gradeSelectEl) {
372
+ gradeSelectEl.innerHTML = "";
373
+ const placeholderOpt = document.createElement("option");
374
+ placeholderOpt.value = "";
375
+ placeholderOpt.textContent = "Select grade...";
376
+ gradeSelectEl.appendChild(placeholderOpt);
377
+
378
+ for (const grade of availableOptions.grades) {
379
+ const opt = document.createElement("option");
380
+ opt.value = grade.id;
381
+ opt.textContent = grade.id;
382
+ gradeSelectEl.appendChild(opt);
383
+ }
384
+
385
+ // Try to keep current selection if valid
386
+ if (availableOptions.grades.find((g) => g.id === selectedGradeId)) {
387
+ gradeSelectEl.value = selectedGradeId;
388
+ } else {
389
+ selectedGradeId = "";
390
+ gradeSelectEl.value = "";
391
+ }
392
+ }
393
+
394
+ // Update track selector
395
+ if (trackSelectEl) {
396
+ trackSelectEl.innerHTML = "";
397
+ const placeholderOpt = document.createElement("option");
398
+ placeholderOpt.value = "";
399
+ placeholderOpt.textContent = "Select track...";
400
+ trackSelectEl.appendChild(placeholderOpt);
401
+
402
+ for (const track of availableOptions.tracks) {
403
+ const opt = document.createElement("option");
404
+ opt.value = track.id;
405
+ opt.textContent = track.name;
406
+ trackSelectEl.appendChild(opt);
407
+ }
408
+
409
+ // Try to keep current selection if valid
410
+ if (availableOptions.tracks.find((t) => t.id === selectedTrackId)) {
411
+ trackSelectEl.value = selectedTrackId;
412
+ } else {
413
+ selectedTrackId = "";
414
+ trackSelectEl.value = "";
415
+ }
416
+ }
417
+ }
418
+
419
+ // Create grade and track selects with stored references
420
+ gradeSelectEl = createSelectWithValue({
421
+ id: "compare-grade-select",
422
+ items: availableOptions.grades,
423
+ initialValue: selectedGradeId,
424
+ placeholder: "Select grade...",
425
+ getDisplayName: (g) => g.id,
426
+ onChange: (value) => {
427
+ selectedGradeId = value;
428
+ updateComparison();
429
+ },
430
+ });
431
+
432
+ trackSelectEl = createSelectWithValue({
433
+ id: "compare-track-select",
434
+ items: availableOptions.tracks,
435
+ initialValue: selectedTrackId,
436
+ placeholder: "Select track...",
437
+ getDisplayName: (t) => t.name,
438
+ onChange: (value) => {
439
+ selectedTrackId = value;
440
+ updateComparison();
441
+ },
442
+ });
443
+
444
+ // Trigger initial comparison if we have defaults
445
+ if (selectedGradeId && selectedTrackId) {
446
+ // Use setTimeout to ensure DOM is ready
447
+ setTimeout(() => updateComparison(), 0);
448
+ }
449
+
450
+ // Create the section with selectors
451
+ return section(
452
+ { className: "section section-detail" },
453
+ h2({ className: "section-title" }, "📈 Compare Progression"),
454
+ p(
455
+ { className: "text-muted", style: "margin-bottom: 1rem" },
456
+ "Compare your current role with another discipline, grade, or track combination.",
457
+ ),
458
+
459
+ // Selector row
460
+ div(
461
+ { className: "comparison-selectors" },
462
+ div(
463
+ { className: "form-group" },
464
+ label({ for: "compare-discipline-select" }, "Target Discipline"),
465
+ createSelectWithValue({
466
+ id: "compare-discipline-select",
467
+ items: data.disciplines.sort((a, b) =>
468
+ a.specialization.localeCompare(b.specialization),
469
+ ),
470
+ initialValue: selectedDisciplineId,
471
+ placeholder: "Select discipline...",
472
+ getDisplayName: (d) => d.specialization,
473
+ onChange: (value) => {
474
+ selectedDisciplineId = value;
475
+ updateSelectorsForDiscipline(value);
476
+ updateComparison();
477
+ },
478
+ }),
479
+ ),
480
+ div(
481
+ { className: "form-group" },
482
+ label({ for: "compare-grade-select" }, "Target Grade"),
483
+ gradeSelectEl,
484
+ ),
485
+ div(
486
+ { className: "form-group" },
487
+ label({ for: "compare-track-select" }, "Target Track"),
488
+ trackSelectEl,
489
+ ),
490
+ ),
491
+
492
+ // Placeholder for results
493
+ comparisonResultsContainer,
494
+ );
495
+ }