@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.
- package/LICENSE +201 -0
- package/README.md +104 -0
- package/app/commands/agent.js +430 -0
- package/app/commands/behaviour.js +61 -0
- package/app/commands/command-factory.js +211 -0
- package/app/commands/discipline.js +58 -0
- package/app/commands/driver.js +94 -0
- package/app/commands/grade.js +60 -0
- package/app/commands/index.js +20 -0
- package/app/commands/init.js +67 -0
- package/app/commands/interview.js +68 -0
- package/app/commands/job.js +157 -0
- package/app/commands/progress.js +77 -0
- package/app/commands/questions.js +179 -0
- package/app/commands/serve.js +143 -0
- package/app/commands/site.js +121 -0
- package/app/commands/skill.js +76 -0
- package/app/commands/stage.js +129 -0
- package/app/commands/track.js +70 -0
- package/app/components/action-buttons.js +66 -0
- package/app/components/behaviour-profile.js +53 -0
- package/app/components/builder.js +341 -0
- package/app/components/card.js +98 -0
- package/app/components/checklist.js +145 -0
- package/app/components/comparison-radar.js +237 -0
- package/app/components/detail.js +230 -0
- package/app/components/error-page.js +72 -0
- package/app/components/grid.js +109 -0
- package/app/components/list.js +120 -0
- package/app/components/modifier-table.js +142 -0
- package/app/components/nav.js +64 -0
- package/app/components/progression-table.js +320 -0
- package/app/components/radar-chart.js +102 -0
- package/app/components/skill-matrix.js +97 -0
- package/app/css/base.css +56 -0
- package/app/css/bundles/app.css +40 -0
- package/app/css/bundles/handout.css +43 -0
- package/app/css/bundles/slides.css +40 -0
- package/app/css/components/badges.css +215 -0
- package/app/css/components/buttons.css +101 -0
- package/app/css/components/forms.css +105 -0
- package/app/css/components/layout.css +209 -0
- package/app/css/components/nav.css +166 -0
- package/app/css/components/progress.css +166 -0
- package/app/css/components/states.css +82 -0
- package/app/css/components/surfaces.css +243 -0
- package/app/css/components/tables.css +362 -0
- package/app/css/components/typography.css +122 -0
- package/app/css/components/utilities.css +41 -0
- package/app/css/pages/agent-builder.css +391 -0
- package/app/css/pages/assessment-results.css +453 -0
- package/app/css/pages/detail.css +59 -0
- package/app/css/pages/interview-builder.css +148 -0
- package/app/css/pages/job-builder.css +134 -0
- package/app/css/pages/landing.css +92 -0
- package/app/css/pages/lifecycle.css +118 -0
- package/app/css/pages/progress-builder.css +274 -0
- package/app/css/pages/self-assessment.css +502 -0
- package/app/css/reset.css +50 -0
- package/app/css/tokens.css +153 -0
- package/app/css/views/handout.css +30 -0
- package/app/css/views/print.css +608 -0
- package/app/css/views/slide-animations.css +113 -0
- package/app/css/views/slide-base.css +330 -0
- package/app/css/views/slide-sections.css +597 -0
- package/app/css/views/slide-tables.css +275 -0
- package/app/formatters/agent/dom.js +540 -0
- package/app/formatters/agent/profile.js +133 -0
- package/app/formatters/agent/skill.js +58 -0
- package/app/formatters/behaviour/dom.js +91 -0
- package/app/formatters/behaviour/markdown.js +54 -0
- package/app/formatters/behaviour/shared.js +64 -0
- package/app/formatters/discipline/dom.js +187 -0
- package/app/formatters/discipline/markdown.js +87 -0
- package/app/formatters/discipline/shared.js +131 -0
- package/app/formatters/driver/dom.js +103 -0
- package/app/formatters/driver/shared.js +92 -0
- package/app/formatters/grade/dom.js +208 -0
- package/app/formatters/grade/markdown.js +94 -0
- package/app/formatters/grade/shared.js +86 -0
- package/app/formatters/index.js +50 -0
- package/app/formatters/interview/dom.js +97 -0
- package/app/formatters/interview/markdown.js +66 -0
- package/app/formatters/interview/shared.js +332 -0
- package/app/formatters/job/description.js +176 -0
- package/app/formatters/job/dom.js +411 -0
- package/app/formatters/job/markdown.js +102 -0
- package/app/formatters/progress/dom.js +135 -0
- package/app/formatters/progress/markdown.js +86 -0
- package/app/formatters/progress/shared.js +339 -0
- package/app/formatters/questions/json.js +43 -0
- package/app/formatters/questions/markdown.js +303 -0
- package/app/formatters/questions/shared.js +274 -0
- package/app/formatters/questions/yaml.js +76 -0
- package/app/formatters/shared.js +71 -0
- package/app/formatters/skill/dom.js +168 -0
- package/app/formatters/skill/markdown.js +109 -0
- package/app/formatters/skill/shared.js +125 -0
- package/app/formatters/stage/dom.js +135 -0
- package/app/formatters/stage/index.js +12 -0
- package/app/formatters/stage/shared.js +111 -0
- package/app/formatters/track/dom.js +128 -0
- package/app/formatters/track/markdown.js +105 -0
- package/app/formatters/track/shared.js +181 -0
- package/app/handout-main.js +421 -0
- package/app/handout.html +21 -0
- package/app/index.html +59 -0
- package/app/lib/card-mappers.js +173 -0
- package/app/lib/cli-output.js +270 -0
- package/app/lib/error-boundary.js +70 -0
- package/app/lib/errors.js +49 -0
- package/app/lib/form-controls.js +47 -0
- package/app/lib/job-cache.js +86 -0
- package/app/lib/markdown.js +114 -0
- package/app/lib/radar.js +866 -0
- package/app/lib/reactive.js +77 -0
- package/app/lib/render.js +212 -0
- package/app/lib/router-core.js +160 -0
- package/app/lib/router-pages.js +16 -0
- package/app/lib/router-slides.js +202 -0
- package/app/lib/state.js +148 -0
- package/app/lib/utils.js +14 -0
- package/app/lib/yaml-loader.js +327 -0
- package/app/main.js +213 -0
- package/app/model/agent.js +702 -0
- package/app/model/checklist.js +137 -0
- package/app/model/derivation.js +699 -0
- package/app/model/index-generator.js +71 -0
- package/app/model/interview.js +539 -0
- package/app/model/job.js +222 -0
- package/app/model/levels.js +591 -0
- package/app/model/loader.js +564 -0
- package/app/model/matching.js +858 -0
- package/app/model/modifiers.js +158 -0
- package/app/model/profile.js +266 -0
- package/app/model/progression.js +507 -0
- package/app/model/validation.js +1385 -0
- package/app/pages/agent-builder.js +823 -0
- package/app/pages/assessment-results.js +507 -0
- package/app/pages/behaviour.js +70 -0
- package/app/pages/discipline.js +71 -0
- package/app/pages/driver.js +106 -0
- package/app/pages/grade.js +117 -0
- package/app/pages/interview-builder.js +50 -0
- package/app/pages/interview.js +304 -0
- package/app/pages/job-builder.js +50 -0
- package/app/pages/job.js +58 -0
- package/app/pages/landing.js +305 -0
- package/app/pages/progress-builder.js +58 -0
- package/app/pages/progress.js +495 -0
- package/app/pages/self-assessment.js +729 -0
- package/app/pages/skill.js +113 -0
- package/app/pages/stage.js +231 -0
- package/app/pages/track.js +69 -0
- package/app/slide-main.js +360 -0
- package/app/slides/behaviour.js +38 -0
- package/app/slides/chapter.js +82 -0
- package/app/slides/discipline.js +40 -0
- package/app/slides/driver.js +39 -0
- package/app/slides/grade.js +32 -0
- package/app/slides/index.js +198 -0
- package/app/slides/interview.js +58 -0
- package/app/slides/job.js +55 -0
- package/app/slides/overview.js +126 -0
- package/app/slides/progress.js +83 -0
- package/app/slides/skill.js +40 -0
- package/app/slides/track.js +39 -0
- package/app/slides.html +56 -0
- package/app/types.js +147 -0
- package/bin/pathway.js +489 -0
- package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
- package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
- package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
- package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
- package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
- package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
- package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
- package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
- package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
- package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
- package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
- package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
- package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
- package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
- package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
- package/examples/agents/.vscode/settings.json +8 -0
- package/examples/behaviours/_index.yaml +8 -0
- package/examples/behaviours/outcome_ownership.yaml +44 -0
- package/examples/behaviours/polymathic_knowledge.yaml +42 -0
- package/examples/behaviours/precise_communication.yaml +40 -0
- package/examples/behaviours/relentless_curiosity.yaml +38 -0
- package/examples/behaviours/systems_thinking.yaml +41 -0
- package/examples/capabilities/_index.yaml +8 -0
- package/examples/capabilities/business.yaml +251 -0
- package/examples/capabilities/delivery.yaml +352 -0
- package/examples/capabilities/people.yaml +100 -0
- package/examples/capabilities/reliability.yaml +318 -0
- package/examples/capabilities/scale.yaml +394 -0
- package/examples/disciplines/_index.yaml +5 -0
- package/examples/disciplines/data_engineering.yaml +76 -0
- package/examples/disciplines/software_engineering.yaml +76 -0
- package/examples/drivers.yaml +205 -0
- package/examples/framework.yaml +58 -0
- package/examples/grades.yaml +118 -0
- package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
- package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
- package/examples/questions/behaviours/precise_communication.yaml +55 -0
- package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
- package/examples/questions/behaviours/systems_thinking.yaml +53 -0
- package/examples/questions/skills/architecture_design.yaml +54 -0
- package/examples/questions/skills/cloud_platforms.yaml +48 -0
- package/examples/questions/skills/code_quality.yaml +49 -0
- package/examples/questions/skills/data_modeling.yaml +46 -0
- package/examples/questions/skills/devops.yaml +47 -0
- package/examples/questions/skills/full_stack_development.yaml +48 -0
- package/examples/questions/skills/sre_practices.yaml +44 -0
- package/examples/questions/skills/stakeholder_management.yaml +49 -0
- package/examples/questions/skills/team_collaboration.yaml +43 -0
- package/examples/questions/skills/technical_writing.yaml +43 -0
- package/examples/self-assessments.yaml +66 -0
- package/examples/stages.yaml +76 -0
- package/examples/tracks/_index.yaml +6 -0
- package/examples/tracks/manager.yaml +53 -0
- package/examples/tracks/platform.yaml +54 -0
- package/examples/tracks/sre.yaml +58 -0
- package/examples/vscode-settings.yaml +22 -0
- 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
|
+
}
|