@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,411 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job formatting for DOM/web output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
div,
|
|
7
|
+
h1,
|
|
8
|
+
h2,
|
|
9
|
+
p,
|
|
10
|
+
a,
|
|
11
|
+
span,
|
|
12
|
+
button,
|
|
13
|
+
section,
|
|
14
|
+
details,
|
|
15
|
+
summary,
|
|
16
|
+
} from "../../lib/render.js";
|
|
17
|
+
import { createBackLink } from "../../components/nav.js";
|
|
18
|
+
import {
|
|
19
|
+
createDetailSection,
|
|
20
|
+
createExpectationsCard,
|
|
21
|
+
} from "../../components/detail.js";
|
|
22
|
+
import {
|
|
23
|
+
createSkillRadar,
|
|
24
|
+
createBehaviourRadar,
|
|
25
|
+
} from "../../components/radar-chart.js";
|
|
26
|
+
import { createSkillMatrix } from "../../components/skill-matrix.js";
|
|
27
|
+
import { createBehaviourProfile } from "../../components/behaviour-profile.js";
|
|
28
|
+
import { markdownToHtml } from "../../lib/markdown.js";
|
|
29
|
+
import { formatJobDescription } from "./description.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format job detail as DOM elements
|
|
33
|
+
* @param {Object} view - Job detail view from presenter
|
|
34
|
+
* @param {Object} options - Formatting options
|
|
35
|
+
* @param {boolean} [options.showBackLink=true] - Whether to show back navigation link
|
|
36
|
+
* @param {boolean} [options.showTables=true] - Whether to show Skill Matrix, Behaviour Profile, Driver Coverage tables
|
|
37
|
+
* @param {boolean} [options.showJobDescriptionHtml=false] - Whether to show HTML job description (for print)
|
|
38
|
+
* @param {boolean} [options.showJobDescriptionMarkdown=true] - Whether to show copyable markdown section
|
|
39
|
+
* @param {Object} [options.discipline] - Discipline entity for job description
|
|
40
|
+
* @param {Object} [options.grade] - Grade entity for job description
|
|
41
|
+
* @param {Object} [options.track] - Track entity for job description
|
|
42
|
+
* @returns {HTMLElement}
|
|
43
|
+
*/
|
|
44
|
+
export function jobToDOM(view, options = {}) {
|
|
45
|
+
const {
|
|
46
|
+
showBackLink = true,
|
|
47
|
+
showTables = true,
|
|
48
|
+
showJobDescriptionHtml = false,
|
|
49
|
+
showJobDescriptionMarkdown = true,
|
|
50
|
+
discipline,
|
|
51
|
+
grade,
|
|
52
|
+
track,
|
|
53
|
+
} = options;
|
|
54
|
+
|
|
55
|
+
const hasEntities = discipline && grade && track;
|
|
56
|
+
|
|
57
|
+
return div(
|
|
58
|
+
{ className: "job-detail-page" },
|
|
59
|
+
// Header
|
|
60
|
+
div(
|
|
61
|
+
{ className: "page-header" },
|
|
62
|
+
showBackLink
|
|
63
|
+
? createBackLink("/job-builder", "← Back to Job Builder")
|
|
64
|
+
: null,
|
|
65
|
+
h1({ className: "page-title" }, view.title),
|
|
66
|
+
div(
|
|
67
|
+
{ className: "page-description" },
|
|
68
|
+
"Generated from: ",
|
|
69
|
+
a({ href: `#/discipline/${view.disciplineId}` }, view.disciplineName),
|
|
70
|
+
" × ",
|
|
71
|
+
a({ href: `#/grade/${view.gradeId}` }, view.gradeId),
|
|
72
|
+
" × ",
|
|
73
|
+
a({ href: `#/track/${view.trackId}` }, view.trackName),
|
|
74
|
+
),
|
|
75
|
+
),
|
|
76
|
+
|
|
77
|
+
// Expectations card
|
|
78
|
+
view.expectations && Object.keys(view.expectations).length > 0
|
|
79
|
+
? createDetailSection({
|
|
80
|
+
title: "Expectations",
|
|
81
|
+
content: createExpectationsCard(view.expectations),
|
|
82
|
+
})
|
|
83
|
+
: null,
|
|
84
|
+
|
|
85
|
+
// Radar charts
|
|
86
|
+
div(
|
|
87
|
+
{ className: "section auto-grid-lg" },
|
|
88
|
+
createSkillRadar(view.skillMatrix, {
|
|
89
|
+
title: "Skills Radar",
|
|
90
|
+
size: 420,
|
|
91
|
+
}),
|
|
92
|
+
createBehaviourRadar(view.behaviourProfile, {
|
|
93
|
+
title: "Behaviours Radar",
|
|
94
|
+
size: 420,
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
|
|
98
|
+
// Job Description HTML (for print view)
|
|
99
|
+
showJobDescriptionHtml && hasEntities
|
|
100
|
+
? createJobDescriptionHtml({
|
|
101
|
+
job: {
|
|
102
|
+
title: view.title,
|
|
103
|
+
skillMatrix: view.skillMatrix,
|
|
104
|
+
behaviourProfile: view.behaviourProfile,
|
|
105
|
+
expectations: view.expectations,
|
|
106
|
+
derivedResponsibilities: view.derivedResponsibilities,
|
|
107
|
+
},
|
|
108
|
+
discipline,
|
|
109
|
+
grade,
|
|
110
|
+
track,
|
|
111
|
+
})
|
|
112
|
+
: null,
|
|
113
|
+
|
|
114
|
+
// Skill matrix, Behaviour profile, Driver coverage tables
|
|
115
|
+
showTables
|
|
116
|
+
? div(
|
|
117
|
+
{ className: "job-tables-section" },
|
|
118
|
+
createDetailSection({
|
|
119
|
+
title: "Skill Matrix",
|
|
120
|
+
content: createSkillMatrix(view.skillMatrix),
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
// Behaviour profile table
|
|
124
|
+
createDetailSection({
|
|
125
|
+
title: "Behaviour Profile",
|
|
126
|
+
content: createBehaviourProfile(view.behaviourProfile),
|
|
127
|
+
}),
|
|
128
|
+
|
|
129
|
+
// Driver coverage
|
|
130
|
+
view.driverCoverage.length > 0
|
|
131
|
+
? createDetailSection({
|
|
132
|
+
title: "Driver Coverage",
|
|
133
|
+
content: div(
|
|
134
|
+
{},
|
|
135
|
+
p(
|
|
136
|
+
{ className: "text-muted", style: "margin-bottom: 1rem" },
|
|
137
|
+
"How well this job aligns with organizational outcome drivers.",
|
|
138
|
+
),
|
|
139
|
+
createDriverCoverageDisplay(view.driverCoverage),
|
|
140
|
+
),
|
|
141
|
+
})
|
|
142
|
+
: null,
|
|
143
|
+
|
|
144
|
+
// Handoff Checklists
|
|
145
|
+
view.checklists && hasChecklistItems(view.checklists)
|
|
146
|
+
? createDetailSection({
|
|
147
|
+
title: "📋 Handoff Checklists",
|
|
148
|
+
content: createChecklistSections(view.checklists),
|
|
149
|
+
})
|
|
150
|
+
: null,
|
|
151
|
+
)
|
|
152
|
+
: null,
|
|
153
|
+
|
|
154
|
+
// Job Description (copyable markdown)
|
|
155
|
+
showJobDescriptionMarkdown && hasEntities
|
|
156
|
+
? createJobDescriptionSection({
|
|
157
|
+
job: {
|
|
158
|
+
title: view.title,
|
|
159
|
+
skillMatrix: view.skillMatrix,
|
|
160
|
+
behaviourProfile: view.behaviourProfile,
|
|
161
|
+
expectations: view.expectations,
|
|
162
|
+
derivedResponsibilities: view.derivedResponsibilities,
|
|
163
|
+
},
|
|
164
|
+
discipline,
|
|
165
|
+
grade,
|
|
166
|
+
track,
|
|
167
|
+
})
|
|
168
|
+
: null,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create driver coverage display
|
|
174
|
+
*/
|
|
175
|
+
function createDriverCoverageDisplay(coverage) {
|
|
176
|
+
const items = coverage.map((c) => {
|
|
177
|
+
const percentage = Math.round(c.coverage * 100);
|
|
178
|
+
|
|
179
|
+
return div(
|
|
180
|
+
{ className: "driver-coverage-item" },
|
|
181
|
+
div(
|
|
182
|
+
{ className: "driver-coverage-header" },
|
|
183
|
+
a(
|
|
184
|
+
{
|
|
185
|
+
href: `#/driver/${c.id}`,
|
|
186
|
+
className: "driver-coverage-name",
|
|
187
|
+
},
|
|
188
|
+
c.name,
|
|
189
|
+
),
|
|
190
|
+
span({ className: "driver-coverage-score" }, `${percentage}%`),
|
|
191
|
+
),
|
|
192
|
+
div(
|
|
193
|
+
{ className: "progress-bar" },
|
|
194
|
+
div({
|
|
195
|
+
className: "progress-bar-fill",
|
|
196
|
+
style: `width: ${percentage}%; background: ${getScoreColor(c.coverage)}`,
|
|
197
|
+
}),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return div({ className: "driver-coverage" }, ...items);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get color based on score
|
|
207
|
+
*/
|
|
208
|
+
function getScoreColor(score) {
|
|
209
|
+
if (score >= 0.8) return "#10b981"; // Green
|
|
210
|
+
if (score >= 0.5) return "#f59e0b"; // Yellow
|
|
211
|
+
return "#ef4444"; // Red
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check if any checklist has items
|
|
216
|
+
* @param {Object} checklists - Checklists object keyed by handoff type
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
function hasChecklistItems(checklists) {
|
|
220
|
+
for (const items of Object.values(checklists)) {
|
|
221
|
+
if (items && items.length > 0) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Create collapsible checklist sections for all handoffs
|
|
230
|
+
* @param {Object} checklists - Checklists object keyed by handoff type
|
|
231
|
+
* @returns {HTMLElement}
|
|
232
|
+
*/
|
|
233
|
+
function createChecklistSections(checklists) {
|
|
234
|
+
const handoffLabels = {
|
|
235
|
+
plan_to_code: "📋 → 💻 Plan → Code",
|
|
236
|
+
code_to_review: "💻 → ✅ Code → Review",
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const sections = Object.entries(checklists)
|
|
240
|
+
.filter(([_, items]) => items && items.length > 0)
|
|
241
|
+
.map(([handoff, groups]) => {
|
|
242
|
+
const label = handoffLabels[handoff] || handoff;
|
|
243
|
+
const totalItems = groups.reduce((sum, g) => sum + g.items.length, 0);
|
|
244
|
+
|
|
245
|
+
return details(
|
|
246
|
+
{ className: "checklist-section" },
|
|
247
|
+
summary(
|
|
248
|
+
{ className: "checklist-section-header" },
|
|
249
|
+
span({ className: "checklist-section-label" }, label),
|
|
250
|
+
span({ className: "badge badge-default" }, `${totalItems} items`),
|
|
251
|
+
),
|
|
252
|
+
div(
|
|
253
|
+
{ className: "checklist-section-content" },
|
|
254
|
+
...groups.map((group) => createChecklistGroup(group)),
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return div({ className: "checklist-sections" }, ...sections);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Create a checklist group for a capability
|
|
264
|
+
* @param {Object} group - Group with capability, level, and items
|
|
265
|
+
* @returns {HTMLElement}
|
|
266
|
+
*/
|
|
267
|
+
function createChecklistGroup(group) {
|
|
268
|
+
const emoji = group.capability.emoji || "📌";
|
|
269
|
+
const capabilityName = group.capability.name || group.capability.id;
|
|
270
|
+
|
|
271
|
+
return div(
|
|
272
|
+
{ className: "checklist-group" },
|
|
273
|
+
div(
|
|
274
|
+
{ className: "checklist-group-header" },
|
|
275
|
+
span({ className: "checklist-emoji" }, emoji),
|
|
276
|
+
span({ className: "checklist-capability" }, capabilityName),
|
|
277
|
+
span({ className: "badge badge-secondary" }, group.level),
|
|
278
|
+
),
|
|
279
|
+
div(
|
|
280
|
+
{ className: "checklist-items" },
|
|
281
|
+
...group.items.map((item) =>
|
|
282
|
+
div(
|
|
283
|
+
{ className: "checklist-item" },
|
|
284
|
+
span({ className: "checklist-checkbox" }, "☐"),
|
|
285
|
+
span({}, item),
|
|
286
|
+
),
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Create the job description section with copy button
|
|
294
|
+
* @param {Object} params
|
|
295
|
+
* @param {Object} params.job - The job definition
|
|
296
|
+
* @param {Object} params.discipline - The discipline
|
|
297
|
+
* @param {Object} params.grade - The grade
|
|
298
|
+
* @param {Object} params.track - The track
|
|
299
|
+
* @returns {HTMLElement} The job description section element
|
|
300
|
+
*/
|
|
301
|
+
export function createJobDescriptionSection({ job, discipline, grade, track }) {
|
|
302
|
+
const markdown = formatJobDescription({
|
|
303
|
+
job,
|
|
304
|
+
discipline,
|
|
305
|
+
grade,
|
|
306
|
+
track,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const copyButton = button(
|
|
310
|
+
{
|
|
311
|
+
className: "btn btn-primary copy-btn",
|
|
312
|
+
onClick: async () => {
|
|
313
|
+
try {
|
|
314
|
+
await navigator.clipboard.writeText(markdown);
|
|
315
|
+
copyButton.textContent = "✓ Copied!";
|
|
316
|
+
copyButton.classList.add("copied");
|
|
317
|
+
setTimeout(() => {
|
|
318
|
+
copyButton.textContent = "Copy Markdown";
|
|
319
|
+
copyButton.classList.remove("copied");
|
|
320
|
+
}, 2000);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
console.error("Failed to copy:", err);
|
|
323
|
+
copyButton.textContent = "Copy failed";
|
|
324
|
+
setTimeout(() => {
|
|
325
|
+
copyButton.textContent = "Copy Markdown";
|
|
326
|
+
}, 2000);
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
},
|
|
330
|
+
"Copy Markdown",
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
const copyHtmlButton = button(
|
|
334
|
+
{
|
|
335
|
+
className: "btn btn-secondary copy-btn",
|
|
336
|
+
onClick: async () => {
|
|
337
|
+
try {
|
|
338
|
+
const html = markdownToHtml(markdown);
|
|
339
|
+
// Use ClipboardItem with text/html MIME type for rich text pasting in Word
|
|
340
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
341
|
+
const clipboardItem = new ClipboardItem({ "text/html": blob });
|
|
342
|
+
await navigator.clipboard.write([clipboardItem]);
|
|
343
|
+
copyHtmlButton.textContent = "✓ Copied!";
|
|
344
|
+
copyHtmlButton.classList.add("copied");
|
|
345
|
+
setTimeout(() => {
|
|
346
|
+
copyHtmlButton.textContent = "Copy as HTML";
|
|
347
|
+
copyHtmlButton.classList.remove("copied");
|
|
348
|
+
}, 2000);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
console.error("Failed to copy:", err);
|
|
351
|
+
copyHtmlButton.textContent = "Copy failed";
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
copyHtmlButton.textContent = "Copy as HTML";
|
|
354
|
+
}, 2000);
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
"Copy as HTML",
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const textarea = document.createElement("textarea");
|
|
362
|
+
textarea.className = "job-description-textarea";
|
|
363
|
+
textarea.readOnly = true;
|
|
364
|
+
textarea.value = markdown;
|
|
365
|
+
|
|
366
|
+
return createDetailSection({
|
|
367
|
+
title: "Job Description",
|
|
368
|
+
content: div(
|
|
369
|
+
{ className: "job-description-container" },
|
|
370
|
+
div(
|
|
371
|
+
{ className: "job-description-header" },
|
|
372
|
+
p(
|
|
373
|
+
{ className: "text-muted" },
|
|
374
|
+
"Copy this markdown-formatted job description for use in job postings, documentation, or sharing.",
|
|
375
|
+
),
|
|
376
|
+
div({ className: "button-group" }, copyButton, copyHtmlButton),
|
|
377
|
+
),
|
|
378
|
+
textarea,
|
|
379
|
+
),
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Create a print-only HTML version of the job description
|
|
385
|
+
* This is hidden on screen and only visible when printing
|
|
386
|
+
* @param {Object} params
|
|
387
|
+
* @param {Object} params.job - The job definition
|
|
388
|
+
* @param {Object} params.discipline - The discipline
|
|
389
|
+
* @param {Object} params.grade - The grade
|
|
390
|
+
* @param {Object} params.track - The track
|
|
391
|
+
* @returns {HTMLElement} The job description HTML element (print-only)
|
|
392
|
+
*/
|
|
393
|
+
export function createJobDescriptionHtml({ job, discipline, grade, track }) {
|
|
394
|
+
const markdown = formatJobDescription({
|
|
395
|
+
job,
|
|
396
|
+
discipline,
|
|
397
|
+
grade,
|
|
398
|
+
track,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
const html = markdownToHtml(markdown);
|
|
402
|
+
|
|
403
|
+
const container = div({ className: "job-description-print-only" });
|
|
404
|
+
container.innerHTML = html;
|
|
405
|
+
|
|
406
|
+
return section(
|
|
407
|
+
{ className: "section job-description-print-section" },
|
|
408
|
+
h2({ className: "section-title" }, "Job Description"),
|
|
409
|
+
container,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job formatting for markdown/CLI output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
tableToMarkdown,
|
|
7
|
+
objectToMarkdownList,
|
|
8
|
+
formatPercent,
|
|
9
|
+
} from "../shared.js";
|
|
10
|
+
import { formatLevel } from "../../lib/render.js";
|
|
11
|
+
import { formatJobDescription } from "./description.js";
|
|
12
|
+
import { SKILL_LEVEL_ORDER } from "../../model/levels.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format job detail as markdown
|
|
16
|
+
* @param {Object} view - Job detail view from presenter
|
|
17
|
+
* @param {Object} [entities] - Original entities (for job description)
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function jobToMarkdown(view, entities = {}) {
|
|
21
|
+
const lines = [
|
|
22
|
+
`# ${view.title}`,
|
|
23
|
+
"",
|
|
24
|
+
`${view.disciplineName} × ${view.gradeId} × ${view.trackName}`,
|
|
25
|
+
"",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Expectations
|
|
29
|
+
if (view.expectations && Object.keys(view.expectations).length > 0) {
|
|
30
|
+
lines.push("## Expectations", "");
|
|
31
|
+
lines.push(objectToMarkdownList(view.expectations));
|
|
32
|
+
lines.push("");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Skill Matrix - sorted by level descending
|
|
36
|
+
lines.push("## Skill Matrix", "");
|
|
37
|
+
const sortedSkills = [...view.skillMatrix].sort((a, b) => {
|
|
38
|
+
const levelA = SKILL_LEVEL_ORDER.indexOf(a.level);
|
|
39
|
+
const levelB = SKILL_LEVEL_ORDER.indexOf(b.level);
|
|
40
|
+
if (levelB !== levelA) {
|
|
41
|
+
return levelB - levelA;
|
|
42
|
+
}
|
|
43
|
+
return a.skillName.localeCompare(b.skillName);
|
|
44
|
+
});
|
|
45
|
+
const skillRows = sortedSkills.map((s) => [
|
|
46
|
+
s.skillName,
|
|
47
|
+
formatLevel(s.level),
|
|
48
|
+
]);
|
|
49
|
+
lines.push(tableToMarkdown(["Skill", "Level"], skillRows));
|
|
50
|
+
lines.push("");
|
|
51
|
+
|
|
52
|
+
// Behaviour Profile
|
|
53
|
+
lines.push("## Behaviour Profile", "");
|
|
54
|
+
const behaviourRows = view.behaviourProfile.map((b) => [
|
|
55
|
+
b.behaviourName,
|
|
56
|
+
formatLevel(b.maturity),
|
|
57
|
+
]);
|
|
58
|
+
lines.push(tableToMarkdown(["Behaviour", "Maturity"], behaviourRows));
|
|
59
|
+
lines.push("");
|
|
60
|
+
|
|
61
|
+
// Driver Coverage
|
|
62
|
+
if (view.driverCoverage.length > 0) {
|
|
63
|
+
lines.push("## Driver Coverage", "");
|
|
64
|
+
const driverRows = view.driverCoverage.map((d) => [
|
|
65
|
+
d.name,
|
|
66
|
+
formatPercent(d.coverage),
|
|
67
|
+
`${d.skillsCovered}/${d.skillsTotal} skills`,
|
|
68
|
+
`${d.behavioursCovered}/${d.behavioursTotal} behaviours`,
|
|
69
|
+
]);
|
|
70
|
+
lines.push(
|
|
71
|
+
tableToMarkdown(
|
|
72
|
+
["Driver", "Coverage", "Skills", "Behaviours"],
|
|
73
|
+
driverRows,
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
lines.push("");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Job Description (copyable markdown)
|
|
80
|
+
if (entities.discipline && entities.grade && entities.track) {
|
|
81
|
+
lines.push("---", "");
|
|
82
|
+
lines.push("## Job Description", "");
|
|
83
|
+
lines.push("```markdown");
|
|
84
|
+
lines.push(
|
|
85
|
+
formatJobDescription({
|
|
86
|
+
job: {
|
|
87
|
+
title: view.title,
|
|
88
|
+
skillMatrix: view.skillMatrix,
|
|
89
|
+
behaviourProfile: view.behaviourProfile,
|
|
90
|
+
expectations: view.expectations,
|
|
91
|
+
derivedResponsibilities: view.derivedResponsibilities,
|
|
92
|
+
},
|
|
93
|
+
discipline: entities.discipline,
|
|
94
|
+
grade: entities.grade,
|
|
95
|
+
track: entities.track,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
lines.push("```");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress formatting for DOM output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
div,
|
|
7
|
+
heading1,
|
|
8
|
+
heading2,
|
|
9
|
+
p,
|
|
10
|
+
table,
|
|
11
|
+
thead,
|
|
12
|
+
tbody,
|
|
13
|
+
tr,
|
|
14
|
+
th,
|
|
15
|
+
td,
|
|
16
|
+
span,
|
|
17
|
+
} from "../../lib/render.js";
|
|
18
|
+
import { createBackLink } from "../../components/nav.js";
|
|
19
|
+
import { createLevelDots } from "../../components/detail.js";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Format progress detail as DOM elements
|
|
23
|
+
* @param {Object} view - Progress detail view from presenter
|
|
24
|
+
* @param {Object} options - Formatting options
|
|
25
|
+
* @param {boolean} options.showBackLink - Whether to show back navigation link
|
|
26
|
+
* @returns {HTMLElement}
|
|
27
|
+
*/
|
|
28
|
+
export function progressToDOM(view, { showBackLink = true } = {}) {
|
|
29
|
+
return div(
|
|
30
|
+
{ className: "detail-page progress-detail" },
|
|
31
|
+
// Header
|
|
32
|
+
div(
|
|
33
|
+
{ className: "page-header" },
|
|
34
|
+
showBackLink
|
|
35
|
+
? createBackLink("/progress", "← Back to Progress Tracking")
|
|
36
|
+
: null,
|
|
37
|
+
heading1({ className: "page-title" }, "📈 ", view.name),
|
|
38
|
+
p({ className: "page-description" }, view.description),
|
|
39
|
+
),
|
|
40
|
+
|
|
41
|
+
// Skill changes
|
|
42
|
+
view.skillChanges && view.skillChanges.length > 0
|
|
43
|
+
? div(
|
|
44
|
+
{ className: "detail-section" },
|
|
45
|
+
heading2({ className: "section-title" }, "Skill Changes"),
|
|
46
|
+
table(
|
|
47
|
+
{ className: "progression-table" },
|
|
48
|
+
thead(
|
|
49
|
+
{},
|
|
50
|
+
tr(
|
|
51
|
+
{},
|
|
52
|
+
th({}, "Skill"),
|
|
53
|
+
th({}, "Change"),
|
|
54
|
+
th({}, "Expected Level"),
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
tbody(
|
|
58
|
+
{},
|
|
59
|
+
...view.skillChanges.map((change) =>
|
|
60
|
+
tr(
|
|
61
|
+
{},
|
|
62
|
+
td({}, change.skillName),
|
|
63
|
+
td(
|
|
64
|
+
{},
|
|
65
|
+
span(
|
|
66
|
+
{
|
|
67
|
+
className: `modifier modifier-${change.modifier > 0 ? "positive" : "negative"}`,
|
|
68
|
+
},
|
|
69
|
+
change.modifier > 0
|
|
70
|
+
? `+${change.modifier}`
|
|
71
|
+
: String(change.modifier),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
td(
|
|
75
|
+
{},
|
|
76
|
+
createLevelDots(
|
|
77
|
+
change.expectedLevelIndex,
|
|
78
|
+
change.totalLevels,
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
: null,
|
|
87
|
+
|
|
88
|
+
// Behaviour changes
|
|
89
|
+
view.behaviourChanges && view.behaviourChanges.length > 0
|
|
90
|
+
? div(
|
|
91
|
+
{ className: "detail-section" },
|
|
92
|
+
heading2({ className: "section-title" }, "Behaviour Changes"),
|
|
93
|
+
table(
|
|
94
|
+
{ className: "progression-table" },
|
|
95
|
+
thead(
|
|
96
|
+
{},
|
|
97
|
+
tr(
|
|
98
|
+
{},
|
|
99
|
+
th({}, "Behaviour"),
|
|
100
|
+
th({}, "Change"),
|
|
101
|
+
th({}, "Expected Level"),
|
|
102
|
+
),
|
|
103
|
+
),
|
|
104
|
+
tbody(
|
|
105
|
+
{},
|
|
106
|
+
...view.behaviourChanges.map((change) =>
|
|
107
|
+
tr(
|
|
108
|
+
{},
|
|
109
|
+
td({}, change.behaviourName),
|
|
110
|
+
td(
|
|
111
|
+
{},
|
|
112
|
+
span(
|
|
113
|
+
{
|
|
114
|
+
className: `modifier modifier-${change.modifier > 0 ? "positive" : "negative"}`,
|
|
115
|
+
},
|
|
116
|
+
change.modifier > 0
|
|
117
|
+
? `+${change.modifier}`
|
|
118
|
+
: String(change.modifier),
|
|
119
|
+
),
|
|
120
|
+
),
|
|
121
|
+
td(
|
|
122
|
+
{},
|
|
123
|
+
createLevelDots(
|
|
124
|
+
change.expectedLevelIndex,
|
|
125
|
+
change.totalLevels,
|
|
126
|
+
),
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
: null,
|
|
134
|
+
);
|
|
135
|
+
}
|