@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,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
+ }