@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,729 @@
1
+ /**
2
+ * Self-assessment wizard page
3
+ * A step-by-step interface for users to assess their skills and behaviours
4
+ */
5
+
6
+ import {
7
+ render,
8
+ div,
9
+ h1,
10
+ h2,
11
+ h3,
12
+ h4,
13
+ p,
14
+ span,
15
+ button,
16
+ a,
17
+ } from "../lib/render.js";
18
+ import { getState } from "../lib/state.js";
19
+ import { createBadge } from "../components/card.js";
20
+ import { createSelectWithValue } from "../lib/form-controls.js";
21
+ import {
22
+ SKILL_LEVEL_ORDER,
23
+ BEHAVIOUR_MATURITY_ORDER,
24
+ groupSkillsByCapability,
25
+ CAPABILITY_ORDER,
26
+ getCapabilityEmoji,
27
+ getConceptEmoji,
28
+ } from "../model/levels.js";
29
+ import { formatLevel } from "../lib/render.js";
30
+
31
+ /**
32
+ * Assessment state stored in memory
33
+ * @type {{skills: Object, behaviours: Object, discipline: string|null, currentStep: number}}
34
+ */
35
+ let assessmentState = {
36
+ skills: {},
37
+ behaviours: {},
38
+ discipline: null,
39
+ currentStep: 0,
40
+ };
41
+
42
+ /**
43
+ * Reset assessment state
44
+ */
45
+ export function resetAssessment() {
46
+ assessmentState = {
47
+ skills: {},
48
+ behaviours: {},
49
+ discipline: null,
50
+ currentStep: 0,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Get current assessment state
56
+ * @returns {Object}
57
+ */
58
+ export function getAssessmentState() {
59
+ return assessmentState;
60
+ }
61
+
62
+ /**
63
+ * Get steps for the wizard
64
+ * @param {Object} data - App data
65
+ * @returns {Array<{id: string, name: string, icon: string, type: string, items?: Array}>}
66
+ */
67
+ function getWizardSteps(data) {
68
+ const { framework } = data;
69
+ const skillsByCapability = groupSkillsByCapability(data.skills);
70
+ const steps = [
71
+ {
72
+ id: "intro",
73
+ name: "Start",
74
+ icon: getConceptEmoji(framework, "driver"),
75
+ type: "intro",
76
+ },
77
+ ];
78
+
79
+ // Add a step for each non-empty skill capability
80
+ for (const capability of CAPABILITY_ORDER) {
81
+ const skills = skillsByCapability[capability];
82
+ if (skills && skills.length > 0) {
83
+ steps.push({
84
+ id: `skills-${capability}`,
85
+ name: formatCapability(capability),
86
+ icon: getCapabilityEmoji(data.capabilities, capability),
87
+ type: "skills",
88
+ capability: capability,
89
+ items: skills,
90
+ });
91
+ }
92
+ }
93
+
94
+ // Add behaviours step
95
+ steps.push({
96
+ id: "behaviours",
97
+ name: "Behaviours",
98
+ icon: getConceptEmoji(framework, "behaviour"),
99
+ type: "behaviours",
100
+ items: data.behaviours,
101
+ });
102
+
103
+ // Add results step
104
+ steps.push({
105
+ id: "results",
106
+ name: "Results",
107
+ icon: getConceptEmoji(framework, "grade"),
108
+ type: "results",
109
+ });
110
+
111
+ return steps;
112
+ }
113
+
114
+ /**
115
+ * Format capability name for display
116
+ * @param {string} capability
117
+ * @returns {string}
118
+ */
119
+ function formatCapability(capability) {
120
+ return capability.charAt(0).toUpperCase() + capability.slice(1);
121
+ }
122
+
123
+ /**
124
+ * Calculate progress percentage
125
+ * @param {Object} data - App data
126
+ * @returns {number}
127
+ */
128
+ function calculateProgress(data) {
129
+ const totalSkills = data.skills.length;
130
+ const totalBehaviours = data.behaviours.length;
131
+ const totalItems = totalSkills + totalBehaviours;
132
+
133
+ if (totalItems === 0) return 0;
134
+
135
+ const assessedSkills = Object.keys(assessmentState.skills).length;
136
+ const assessedBehaviours = Object.keys(assessmentState.behaviours).length;
137
+ const assessedItems = assessedSkills + assessedBehaviours;
138
+
139
+ return Math.round((assessedItems / totalItems) * 100);
140
+ }
141
+
142
+ /**
143
+ * Render the self-assessment wizard
144
+ */
145
+ export function renderSelfAssessment() {
146
+ const { data } = getState();
147
+ const steps = getWizardSteps(data);
148
+ const currentStep = Math.min(assessmentState.currentStep, steps.length - 1);
149
+ const step = steps[currentStep];
150
+
151
+ const page = div(
152
+ { className: "self-assessment-page" },
153
+ // Header
154
+ div(
155
+ { className: "page-header" },
156
+ h1({ className: "page-title" }, "Self-Assessment"),
157
+ p(
158
+ { className: "page-description" },
159
+ "Assess your skills and behaviours to find matching roles and identify development opportunities.",
160
+ ),
161
+ ),
162
+
163
+ // Progress bar
164
+ createProgressBar(data, steps, currentStep),
165
+
166
+ // Step content
167
+ div(
168
+ { className: "assessment-content", id: "assessment-content" },
169
+ renderStepContent(step, data),
170
+ ),
171
+
172
+ // Navigation buttons
173
+ createNavigationButtons(steps, currentStep),
174
+ );
175
+
176
+ render(page);
177
+ }
178
+
179
+ /**
180
+ * Create progress bar with step indicators
181
+ * @param {Object} data - App data
182
+ * @param {Array} steps - Wizard steps
183
+ * @param {number} currentStep - Current step index
184
+ * @returns {HTMLElement}
185
+ */
186
+ function createProgressBar(data, steps, currentStep) {
187
+ const progress = calculateProgress(data);
188
+
189
+ return div(
190
+ { className: "assessment-progress" },
191
+ // Progress percentage
192
+ div(
193
+ { className: "progress-header" },
194
+ span({ className: "progress-label" }, `${progress}% Complete`),
195
+ span(
196
+ { className: "progress-stats" },
197
+ `${Object.keys(assessmentState.skills).length}/${data.skills.length} skills, ` +
198
+ `${Object.keys(assessmentState.behaviours).length}/${data.behaviours.length} behaviours`,
199
+ ),
200
+ ),
201
+ // Progress bar
202
+ div(
203
+ { className: "progress-bar" },
204
+ div({ className: "progress-bar-fill", style: `width: ${progress}%` }),
205
+ ),
206
+ // Step indicators
207
+ div(
208
+ { className: "step-indicators" },
209
+ ...steps.map((step, index) =>
210
+ div(
211
+ {
212
+ className: `step-indicator ${index === currentStep ? "active" : ""} ${index < currentStep ? "completed" : ""}`,
213
+ onClick: () => {
214
+ // Allow jumping to any step except results (unless assessment is complete)
215
+ if (index < steps.length - 1 || calculateProgress(data) >= 50) {
216
+ assessmentState.currentStep = index;
217
+ renderSelfAssessment();
218
+ }
219
+ },
220
+ },
221
+ span({ className: "step-icon" }, step.icon),
222
+ span({ className: "step-name" }, step.name),
223
+ ),
224
+ ),
225
+ ),
226
+ );
227
+ }
228
+
229
+ /**
230
+ * Render content for the current step
231
+ * @param {Object} step - Current step configuration
232
+ * @param {Object} data - App data
233
+ * @returns {HTMLElement}
234
+ */
235
+ function renderStepContent(step, data) {
236
+ switch (step.type) {
237
+ case "intro":
238
+ return renderIntroStep(data);
239
+ case "skills":
240
+ return renderSkillsStep(step, data);
241
+ case "behaviours":
242
+ return renderBehavioursStep(step, data);
243
+ case "results":
244
+ return renderResultsPreview(data);
245
+ default:
246
+ return div({}, "Unknown step");
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Render introduction step
252
+ * @param {Object} data - App data
253
+ * @returns {HTMLElement}
254
+ */
255
+ function renderIntroStep(data) {
256
+ return div(
257
+ { className: "assessment-step assessment-intro" },
258
+ div(
259
+ { className: "intro-card" },
260
+ h2({}, "Welcome to the Self-Assessment"),
261
+ p(
262
+ {},
263
+ "This assessment helps you understand your current skill levels and behaviours, " +
264
+ "then matches you with suitable roles in the organization.",
265
+ ),
266
+
267
+ div(
268
+ { className: "intro-info" },
269
+ div(
270
+ { className: "info-item" },
271
+ span(
272
+ { className: "info-icon" },
273
+ getConceptEmoji(data.framework, "skill"),
274
+ ),
275
+ div(
276
+ {},
277
+ h4({}, `${data.skills.length} Skills`),
278
+ p({}, "Across " + CAPABILITY_ORDER.length + " capabilities"),
279
+ ),
280
+ ),
281
+ div(
282
+ { className: "info-item" },
283
+ span(
284
+ { className: "info-icon" },
285
+ getConceptEmoji(data.framework, "behaviour"),
286
+ ),
287
+ div(
288
+ {},
289
+ h4({}, `${data.behaviours.length} Behaviours`),
290
+ p({}, "Key mindsets and ways of working"),
291
+ ),
292
+ ),
293
+ div(
294
+ { className: "info-item" },
295
+ span({ className: "info-icon" }, "⏱️"),
296
+ div({}, h4({}, "10-15 Minutes"), p({}, "Complete at your own pace")),
297
+ ),
298
+ ),
299
+
300
+ // Optional discipline filter
301
+ div(
302
+ { className: "discipline-filter" },
303
+ h3({}, "Optional: Focus on a Discipline"),
304
+ p(
305
+ { className: "text-muted" },
306
+ "Select a discipline to highlight which skills are most relevant for that role. " +
307
+ "You can still assess all skills.",
308
+ ),
309
+ createSelectWithValue({
310
+ id: "discipline-filter-select",
311
+ items: data.disciplines,
312
+ initialValue: assessmentState.discipline || "",
313
+ placeholder: "Select discipline",
314
+ onChange: (value) => {
315
+ assessmentState.discipline = value || null;
316
+ },
317
+ getDisplayName: (d) => d.specialization,
318
+ }),
319
+ ),
320
+
321
+ div(
322
+ { className: "intro-tips" },
323
+ h3({}, "Tips for Accurate Self-Assessment"),
324
+ div(
325
+ { className: "auto-grid-sm" },
326
+ createTipCard(
327
+ "🎯",
328
+ "Be Honest",
329
+ "Rate yourself where you genuinely are, not where you aspire to be.",
330
+ ),
331
+ createTipCard(
332
+ "📚",
333
+ "Read Descriptions",
334
+ "Hover over levels to see detailed descriptions for each.",
335
+ ),
336
+ createTipCard(
337
+ "⏭️",
338
+ "Skip if Unsure",
339
+ "You can leave items unrated and come back later.",
340
+ ),
341
+ createTipCard(
342
+ "💾",
343
+ "Auto-Saved",
344
+ "Your progress is kept while you navigate between steps.",
345
+ ),
346
+ ),
347
+ ),
348
+ ),
349
+ );
350
+ }
351
+
352
+ /**
353
+ * Create a tip card
354
+ * @param {string} icon
355
+ * @param {string} title
356
+ * @param {string} text
357
+ * @returns {HTMLElement}
358
+ */
359
+ function createTipCard(icon, title, text) {
360
+ return div(
361
+ { className: "tip-card" },
362
+ span({ className: "tip-icon" }, icon),
363
+ h4({}, title),
364
+ p({}, text),
365
+ );
366
+ }
367
+
368
+ /**
369
+ * Render skills assessment step
370
+ * @param {Object} step - Step configuration with capability and items
371
+ * @param {Object} data - App data
372
+ * @returns {HTMLElement}
373
+ */
374
+ function renderSkillsStep(step, data) {
375
+ const { capability, items } = step;
376
+ const selectedDiscipline = assessmentState.discipline
377
+ ? data.disciplines.find((d) => d.id === assessmentState.discipline)
378
+ : null;
379
+
380
+ // Determine skill relevance if a discipline is selected
381
+ const getSkillRelevance = (skill) => {
382
+ if (!selectedDiscipline) return null;
383
+ if (selectedDiscipline.coreSkills?.includes(skill.id)) return "primary";
384
+ if (selectedDiscipline.supportingSkills?.includes(skill.id))
385
+ return "secondary";
386
+ if (selectedDiscipline.broadSkills?.includes(skill.id)) return "broad";
387
+ return null;
388
+ };
389
+
390
+ // Sort items: relevant skills first
391
+ const sortedItems = [...items].sort((a, b) => {
392
+ const relevanceA = getSkillRelevance(a);
393
+ const relevanceB = getSkillRelevance(b);
394
+ const order = { primary: 0, secondary: 1, broad: 2 };
395
+
396
+ if (relevanceA && !relevanceB) return -1;
397
+ if (!relevanceA && relevanceB) return 1;
398
+ if (relevanceA && relevanceB) {
399
+ return (order[relevanceA] ?? 3) - (order[relevanceB] ?? 3);
400
+ }
401
+ return a.name.localeCompare(b.name);
402
+ });
403
+
404
+ const assessedCount = items.filter(
405
+ (item) => assessmentState.skills[item.id],
406
+ ).length;
407
+
408
+ return div(
409
+ { className: "assessment-step" },
410
+ div(
411
+ { className: "step-header" },
412
+ h2(
413
+ {},
414
+ span({ className: "step-header-icon" }, step.icon),
415
+ ` ${formatCapability(capability)} Skills`,
416
+ ),
417
+ span(
418
+ { className: "step-progress" },
419
+ `${assessedCount}/${items.length} rated`,
420
+ ),
421
+ ),
422
+
423
+ selectedDiscipline &&
424
+ div(
425
+ { className: "discipline-context" },
426
+ span({}, `Showing relevance for: `),
427
+ span(
428
+ { className: "discipline-name" },
429
+ selectedDiscipline.specialization,
430
+ ),
431
+ ),
432
+
433
+ div(
434
+ { className: "assessment-items" },
435
+ ...sortedItems.map((skill) =>
436
+ createSkillAssessmentItem(skill, getSkillRelevance(skill)),
437
+ ),
438
+ ),
439
+ );
440
+ }
441
+
442
+ /**
443
+ * Create a skill assessment item
444
+ * @param {Object} skill - Skill data
445
+ * @param {string|null} relevance - Skill relevance for selected discipline
446
+ * @returns {HTMLElement}
447
+ */
448
+ function createSkillAssessmentItem(skill, relevance) {
449
+ const currentLevel = assessmentState.skills[skill.id];
450
+
451
+ return div(
452
+ {
453
+ className: `assessment-item ${currentLevel ? "assessed" : ""} ${relevance ? `relevance-${relevance}` : ""}`,
454
+ },
455
+ div(
456
+ { className: "assessment-item-header" },
457
+ div(
458
+ { className: "assessment-item-title" },
459
+ a({ href: `#/skill/${skill.id}` }, skill.name),
460
+ relevance && createBadge(relevance, relevance),
461
+ ),
462
+ currentLevel &&
463
+ span({ className: "current-level-badge" }, formatLevel(currentLevel)),
464
+ ),
465
+
466
+ p({ className: "assessment-item-description" }, skill.description),
467
+
468
+ div(
469
+ { className: "level-selector" },
470
+ ...SKILL_LEVEL_ORDER.map((level, index) =>
471
+ createLevelButton(skill, level, index, "skill"),
472
+ ),
473
+ // Clear button
474
+ button(
475
+ {
476
+ className: "level-clear-btn",
477
+ title: "Clear selection",
478
+ onClick: () => {
479
+ delete assessmentState.skills[skill.id];
480
+ renderSelfAssessment();
481
+ },
482
+ },
483
+ "✕",
484
+ ),
485
+ ),
486
+ );
487
+ }
488
+
489
+ /**
490
+ * Create a level selection button
491
+ * @param {Object} item - Skill or behaviour
492
+ * @param {string} level - Level value
493
+ * @param {number} index - Level index
494
+ * @param {string} type - 'skill' or 'behaviour'
495
+ * @returns {HTMLElement}
496
+ */
497
+ function createLevelButton(item, level, index, type) {
498
+ const stateKey = type === "skill" ? "skills" : "behaviours";
499
+ const currentLevel = assessmentState[stateKey][item.id];
500
+ const isSelected = currentLevel === level;
501
+ const levelDescriptions =
502
+ type === "skill" ? item.levelDescriptions : item.maturityDescriptions;
503
+ const description = levelDescriptions?.[level] || "";
504
+
505
+ return button(
506
+ {
507
+ className: `level-btn level-${index + 1} ${isSelected ? "selected" : ""}`,
508
+ title: `${formatLevel(level)}: ${description}`,
509
+ onClick: () => {
510
+ assessmentState[stateKey][item.id] = level;
511
+ renderSelfAssessment();
512
+ },
513
+ },
514
+ span({ className: "level-btn-number" }, String(index + 1)),
515
+ span({ className: "level-btn-name" }, formatLevel(level)),
516
+ );
517
+ }
518
+
519
+ /**
520
+ * Render behaviours assessment step
521
+ * @param {Object} step - Step configuration
522
+ * @param {Object} data - App data
523
+ * @returns {HTMLElement}
524
+ */
525
+ function renderBehavioursStep(step, data) {
526
+ const { items } = step;
527
+ const assessedCount = items.filter(
528
+ (item) => assessmentState.behaviours[item.id],
529
+ ).length;
530
+
531
+ return div(
532
+ { className: "assessment-step" },
533
+ div(
534
+ { className: "step-header" },
535
+ h2(
536
+ {},
537
+ span(
538
+ { className: "step-header-icon" },
539
+ getConceptEmoji(data.framework, "behaviour"),
540
+ ),
541
+ " Behaviours",
542
+ ),
543
+ span(
544
+ { className: "step-progress" },
545
+ `${assessedCount}/${items.length} rated`,
546
+ ),
547
+ ),
548
+
549
+ p(
550
+ { className: "step-intro" },
551
+ "Behaviours describe how you approach work—your mindsets and ways of working. " +
552
+ "These are equally important as technical skills.",
553
+ ),
554
+
555
+ div(
556
+ { className: "assessment-items" },
557
+ ...items.map((behaviour) => createBehaviourAssessmentItem(behaviour)),
558
+ ),
559
+ );
560
+ }
561
+
562
+ /**
563
+ * Create a behaviour assessment item
564
+ * @param {Object} behaviour - Behaviour data
565
+ * @returns {HTMLElement}
566
+ */
567
+ function createBehaviourAssessmentItem(behaviour) {
568
+ const currentLevel = assessmentState.behaviours[behaviour.id];
569
+
570
+ return div(
571
+ { className: `assessment-item ${currentLevel ? "assessed" : ""}` },
572
+ div(
573
+ { className: "assessment-item-header" },
574
+ div(
575
+ { className: "assessment-item-title" },
576
+ a({ href: `#/behaviour/${behaviour.id}` }, behaviour.name),
577
+ ),
578
+ currentLevel &&
579
+ span({ className: "current-level-badge" }, formatLevel(currentLevel)),
580
+ ),
581
+
582
+ p({ className: "assessment-item-description" }, behaviour.description),
583
+
584
+ div(
585
+ { className: "level-selector" },
586
+ ...BEHAVIOUR_MATURITY_ORDER.map((level, index) =>
587
+ createLevelButton(behaviour, level, index, "behaviour"),
588
+ ),
589
+ // Clear button
590
+ button(
591
+ {
592
+ className: "level-clear-btn",
593
+ title: "Clear selection",
594
+ onClick: () => {
595
+ delete assessmentState.behaviours[behaviour.id];
596
+ renderSelfAssessment();
597
+ },
598
+ },
599
+ "✕",
600
+ ),
601
+ ),
602
+ );
603
+ }
604
+
605
+ /**
606
+ * Render results preview before navigating to full results
607
+ * @param {Object} data - App data
608
+ * @returns {HTMLElement}
609
+ */
610
+ function renderResultsPreview(data) {
611
+ const progress = calculateProgress(data);
612
+ const skillCount = Object.keys(assessmentState.skills).length;
613
+ const behaviourCount = Object.keys(assessmentState.behaviours).length;
614
+
615
+ if (progress < 20) {
616
+ return div(
617
+ { className: "assessment-step results-preview" },
618
+ div(
619
+ { className: "results-incomplete" },
620
+ h2({}, "Complete More of the Assessment"),
621
+ p(
622
+ {},
623
+ "Please complete at least 20% of the assessment to see job matches. " +
624
+ `You've currently assessed ${skillCount} skills and ${behaviourCount} behaviours.`,
625
+ ),
626
+ div(
627
+ { className: "progress-summary" },
628
+ div(
629
+ { className: "progress-bar large" },
630
+ div({
631
+ className: "progress-bar-fill",
632
+ style: `width: ${progress}%`,
633
+ }),
634
+ ),
635
+ span({}, `${progress}% complete`),
636
+ ),
637
+ ),
638
+ );
639
+ }
640
+
641
+ return div(
642
+ { className: "assessment-step results-preview" },
643
+ div(
644
+ { className: "results-ready" },
645
+ h2({}, "🎉 Assessment Complete!"),
646
+ p({}, "Great work! You're ready to see your job matches."),
647
+
648
+ div(
649
+ { className: "results-summary" },
650
+ div(
651
+ { className: "summary-stat" },
652
+ span({ className: "summary-value" }, String(skillCount)),
653
+ span({ className: "summary-label" }, "Skills Assessed"),
654
+ ),
655
+ div(
656
+ { className: "summary-stat" },
657
+ span({ className: "summary-value" }, String(behaviourCount)),
658
+ span({ className: "summary-label" }, "Behaviours Assessed"),
659
+ ),
660
+ div(
661
+ { className: "summary-stat" },
662
+ span({ className: "summary-value" }, `${progress}%`),
663
+ span({ className: "summary-label" }, "Complete"),
664
+ ),
665
+ ),
666
+
667
+ div(
668
+ { className: "results-actions" },
669
+ button(
670
+ {
671
+ className: "btn btn-primary btn-lg",
672
+ onClick: () => {
673
+ // Navigate to results page
674
+ window.location.hash = "/self-assessment/results";
675
+ },
676
+ },
677
+ "View My Job Matches →",
678
+ ),
679
+ ),
680
+ ),
681
+ );
682
+ }
683
+
684
+ /**
685
+ * Create navigation buttons for the wizard
686
+ * @param {Array} steps - Wizard steps
687
+ * @param {number} currentStep - Current step index
688
+ * @returns {HTMLElement}
689
+ */
690
+ function createNavigationButtons(steps, currentStep) {
691
+ const isFirstStep = currentStep === 0;
692
+ const isLastStep = currentStep === steps.length - 1;
693
+
694
+ return div(
695
+ { className: "assessment-navigation" },
696
+ button(
697
+ {
698
+ className: "btn btn-secondary",
699
+ disabled: isFirstStep,
700
+ onClick: () => {
701
+ if (!isFirstStep) {
702
+ assessmentState.currentStep = currentStep - 1;
703
+ renderSelfAssessment();
704
+ }
705
+ },
706
+ },
707
+ "← Previous",
708
+ ),
709
+
710
+ span(
711
+ { className: "step-counter" },
712
+ `Step ${currentStep + 1} of ${steps.length}`,
713
+ ),
714
+
715
+ button(
716
+ {
717
+ className: "btn btn-primary",
718
+ disabled: isLastStep,
719
+ onClick: () => {
720
+ if (!isLastStep) {
721
+ assessmentState.currentStep = currentStep + 1;
722
+ renderSelfAssessment();
723
+ }
724
+ },
725
+ },
726
+ "Next →",
727
+ ),
728
+ );
729
+ }