@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,145 @@
1
+ /**
2
+ * Checklist component
3
+ *
4
+ * Displays derived checklists grouped by capability.
5
+ * Used on job pages to show handoff checklists.
6
+ */
7
+
8
+ import { div, span, details, summary } from "../lib/render.js";
9
+ import { getCapabilityEmoji } from "../model/levels.js";
10
+
11
+ /**
12
+ * Create checklist display grouped by capability
13
+ * @param {Array<{capability: string, items: string[]}>} checklist - Checklist groups
14
+ * @param {Object} options - Display options
15
+ * @param {boolean} [options.interactive=false] - Whether checkboxes are interactive
16
+ * @param {Array} [options.capabilities] - Capabilities array for emoji lookup
17
+ * @returns {HTMLElement}
18
+ */
19
+ export function createChecklist(checklist, options = {}) {
20
+ const { interactive = false, capabilities = [] } = options;
21
+
22
+ if (!checklist || checklist.length === 0) {
23
+ return div(
24
+ { className: "checklist-empty text-muted" },
25
+ "No checklist items for this transition.",
26
+ );
27
+ }
28
+
29
+ return div(
30
+ { className: "checklist" },
31
+ ...checklist.map((group) =>
32
+ createChecklistGroup(group, { interactive, capabilities }),
33
+ ),
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Create a single checklist group for a capability
39
+ * @param {Object} group - Group with capability and items
40
+ * @param {Object} options - Display options
41
+ * @returns {HTMLElement}
42
+ */
43
+ function createChecklistGroup(group, options) {
44
+ const { interactive, capabilities } = options;
45
+ const emoji = getCapabilityEmoji(capabilities, group.capability);
46
+ const capabilityName = formatCapabilityName(group.capability);
47
+
48
+ return div(
49
+ { className: "checklist-group" },
50
+ div(
51
+ { className: "checklist-group-header" },
52
+ span({ className: "checklist-emoji" }, emoji),
53
+ span({ className: "checklist-capability" }, capabilityName),
54
+ span(
55
+ { className: "checklist-count badge badge-default" },
56
+ `${group.items.length}`,
57
+ ),
58
+ ),
59
+ div(
60
+ { className: "checklist-items" },
61
+ ...group.items.map((item) => createChecklistItem(item, { interactive })),
62
+ ),
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Create a single checklist item
68
+ * @param {string} item - Checklist item text
69
+ * @param {Object} options - Display options
70
+ * @returns {HTMLElement}
71
+ */
72
+ function createChecklistItem(item, { interactive }) {
73
+ const checkbox = interactive
74
+ ? createInteractiveCheckbox()
75
+ : span({ className: "checklist-checkbox" }, "☐");
76
+
77
+ return div({ className: "checklist-item" }, checkbox, span({}, item));
78
+ }
79
+
80
+ /**
81
+ * Create an interactive checkbox element
82
+ * @returns {HTMLElement}
83
+ */
84
+ function createInteractiveCheckbox() {
85
+ const input = document.createElement("input");
86
+ input.type = "checkbox";
87
+ input.className = "checklist-checkbox-input";
88
+ return input;
89
+ }
90
+
91
+ /**
92
+ * Format capability name for display
93
+ * @param {string} capability - Capability ID
94
+ * @returns {string}
95
+ */
96
+ function formatCapabilityName(capability) {
97
+ return capability.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
98
+ }
99
+
100
+ /**
101
+ * Create collapsible checklist sections for multiple handoffs
102
+ * @param {Object} checklists - Object with handoff types as keys
103
+ * @param {Object} options - Display options
104
+ * @param {Array} [options.capabilities] - Capabilities for emoji lookup
105
+ * @returns {HTMLElement}
106
+ */
107
+ export function createChecklistSections(checklists, options = {}) {
108
+ const { capabilities = [] } = options;
109
+ const handoffLabels = {
110
+ plan_to_code: "📋 → 💻 Plan → Code",
111
+ code_to_review: "💻 → ✅ Code → Review",
112
+ };
113
+
114
+ const sections = Object.entries(checklists)
115
+ .filter(([_, items]) => items && items.length > 0)
116
+ .map(([handoff, items]) => {
117
+ const label = handoffLabels[handoff] || handoff;
118
+ const totalItems = items.reduce((sum, g) => sum + g.items.length, 0);
119
+
120
+ return details(
121
+ { className: "checklist-section" },
122
+ summary(
123
+ { className: "checklist-section-header" },
124
+ span({ className: "checklist-section-label" }, label),
125
+ span(
126
+ { className: "checklist-section-count badge badge-default" },
127
+ `${totalItems} items`,
128
+ ),
129
+ ),
130
+ div(
131
+ { className: "checklist-section-content" },
132
+ createChecklist(items, { capabilities }),
133
+ ),
134
+ );
135
+ });
136
+
137
+ if (sections.length === 0) {
138
+ return div(
139
+ { className: "checklist-empty text-muted" },
140
+ "No checklists available for this role.",
141
+ );
142
+ }
143
+
144
+ return div({ className: "checklist-sections" }, ...sections);
145
+ }
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Comparison radar chart component
3
+ * Displays two overlaid radar charts for comparing current vs target levels
4
+ */
5
+
6
+ /** @typedef {import('../types.js').SkillMatrixItem} SkillMatrixItem */
7
+ /** @typedef {import('../types.js').BehaviourProfileItem} BehaviourProfileItem */
8
+
9
+ import { ComparisonRadarChart } from "../lib/radar.js";
10
+ import { div, h3 } from "../lib/render.js";
11
+ import {
12
+ getSkillLevelIndex,
13
+ getBehaviourMaturityIndex,
14
+ formatLevel,
15
+ } from "../lib/render.js";
16
+ import { getCapabilityIndex } from "../model/levels.js";
17
+
18
+ /**
19
+ * Create a comparison skill radar chart
20
+ * @param {SkillMatrixItem[]} currentMatrix - Current skill matrix entries
21
+ * @param {SkillMatrixItem[]} targetMatrix - Target skill matrix entries
22
+ * @param {Object} [options]
23
+ * @returns {HTMLElement}
24
+ */
25
+ export function createComparisonSkillRadar(
26
+ currentMatrix,
27
+ targetMatrix,
28
+ options = {},
29
+ ) {
30
+ const container = div(
31
+ { className: "radar-container comparison-radar" },
32
+ h3({ className: "radar-title" }, options.title || "Skills Comparison"),
33
+ div(
34
+ { className: "radar-legend" },
35
+ div(
36
+ { className: "legend-item" },
37
+ div({ className: "legend-color", style: "background: #3b82f6" }),
38
+ options.currentLabel || "Current",
39
+ ),
40
+ div(
41
+ { className: "legend-item" },
42
+ div({ className: "legend-color", style: "background: #10b981" }),
43
+ options.targetLabel || "Target",
44
+ ),
45
+ ),
46
+ div({
47
+ className: "radar-chart-wrapper",
48
+ id: `skill-comparison-${Date.now()}`,
49
+ }),
50
+ );
51
+
52
+ // Render chart after container is in DOM
53
+ setTimeout(() => {
54
+ const wrapper = container.querySelector(".radar-chart-wrapper");
55
+ if (!wrapper || !currentMatrix || currentMatrix.length === 0) return;
56
+
57
+ // Build aligned data arrays that include all skills from both matrices
58
+ // This handles new skills (in target but not current) and removed skills (in current but not target)
59
+ const allSkillIds = new Set([
60
+ ...currentMatrix.map((s) => s.skillId),
61
+ ...targetMatrix.map((s) => s.skillId),
62
+ ]);
63
+
64
+ // Build skill entries with capability info for sorting
65
+ const skillEntries = [];
66
+ for (const skillId of allSkillIds) {
67
+ const currentSkill = currentMatrix.find((s) => s.skillId === skillId);
68
+ const targetSkill = targetMatrix.find((s) => s.skillId === skillId);
69
+ const capability =
70
+ currentSkill?.capability || targetSkill?.capability || "";
71
+ const skillName = currentSkill?.skillName || targetSkill?.skillName;
72
+
73
+ skillEntries.push({
74
+ skillId,
75
+ skillName,
76
+ capability,
77
+ currentSkill,
78
+ targetSkill,
79
+ });
80
+ }
81
+
82
+ // Sort by capability order, then by skill name within capability
83
+ skillEntries.sort((a, b) => {
84
+ const capDiff =
85
+ getCapabilityIndex(a.capability) - getCapabilityIndex(b.capability);
86
+ if (capDiff !== 0) return capDiff;
87
+ return a.skillName.localeCompare(b.skillName);
88
+ });
89
+
90
+ const currentData = [];
91
+ const targetData = [];
92
+
93
+ for (const entry of skillEntries) {
94
+ const { skillName, currentSkill, targetSkill } = entry;
95
+
96
+ currentData.push({
97
+ label: skillName,
98
+ value: currentSkill ? getSkillLevelIndex(currentSkill.level) : 0,
99
+ maxValue: 5,
100
+ description: currentSkill
101
+ ? `${formatLevel(currentSkill.type)} - ${formatLevel(currentSkill.level)}`
102
+ : "Not required",
103
+ });
104
+
105
+ targetData.push({
106
+ label: skillName,
107
+ value: targetSkill ? getSkillLevelIndex(targetSkill.level) : 0,
108
+ maxValue: 5,
109
+ description: targetSkill
110
+ ? `${formatLevel(targetSkill.type)} - ${formatLevel(targetSkill.level)}`
111
+ : "Not required",
112
+ });
113
+ }
114
+
115
+ const chart = new ComparisonRadarChart({
116
+ container: wrapper,
117
+ currentData,
118
+ targetData,
119
+ options: {
120
+ levels: 5,
121
+ currentColor: options.currentColor || "#3b82f6",
122
+ targetColor: options.targetColor || "#10b981",
123
+ size: options.size || 400,
124
+ showLabels: true,
125
+ showTooltips: true,
126
+ },
127
+ });
128
+
129
+ chart.render();
130
+ }, 0);
131
+
132
+ return container;
133
+ }
134
+
135
+ /**
136
+ * Create a comparison behaviour radar chart
137
+ * @param {BehaviourProfileItem[]} currentProfile - Current behaviour profile entries
138
+ * @param {BehaviourProfileItem[]} targetProfile - Target behaviour profile entries
139
+ * @param {Object} [options]
140
+ * @returns {HTMLElement}
141
+ */
142
+ export function createComparisonBehaviourRadar(
143
+ currentProfile,
144
+ targetProfile,
145
+ options = {},
146
+ ) {
147
+ const container = div(
148
+ { className: "radar-container comparison-radar" },
149
+ h3({ className: "radar-title" }, options.title || "Behaviours Comparison"),
150
+ div(
151
+ { className: "radar-legend" },
152
+ div(
153
+ { className: "legend-item" },
154
+ div({ className: "legend-color", style: "background: #3b82f6" }),
155
+ options.currentLabel || "Current",
156
+ ),
157
+ div(
158
+ { className: "legend-item" },
159
+ div({ className: "legend-color", style: "background: #10b981" }),
160
+ options.targetLabel || "Target",
161
+ ),
162
+ ),
163
+ div({
164
+ className: "radar-chart-wrapper",
165
+ id: `behaviour-comparison-${Date.now()}`,
166
+ }),
167
+ );
168
+
169
+ // Render chart after container is in DOM
170
+ setTimeout(() => {
171
+ const wrapper = container.querySelector(".radar-chart-wrapper");
172
+ if (!wrapper || !currentProfile || currentProfile.length === 0) return;
173
+
174
+ // Build aligned data arrays that include all behaviours from both profiles
175
+ // This handles new behaviours (in target but not current) and removed behaviours (in current but not target)
176
+ const allBehaviourIds = new Set([
177
+ ...currentProfile.map((b) => b.behaviourId),
178
+ ...targetProfile.map((b) => b.behaviourId),
179
+ ]);
180
+
181
+ const currentData = [];
182
+ const targetData = [];
183
+
184
+ for (const behaviourId of allBehaviourIds) {
185
+ const currentBehaviour = currentProfile.find(
186
+ (b) => b.behaviourId === behaviourId,
187
+ );
188
+ const targetBehaviour = targetProfile.find(
189
+ (b) => b.behaviourId === behaviourId,
190
+ );
191
+
192
+ // Use whichever behaviour entry exists for the label
193
+ const behaviourName =
194
+ currentBehaviour?.behaviourName || targetBehaviour?.behaviourName;
195
+
196
+ currentData.push({
197
+ label: behaviourName,
198
+ value: currentBehaviour
199
+ ? getBehaviourMaturityIndex(currentBehaviour.maturity)
200
+ : 0,
201
+ maxValue: 5,
202
+ description: currentBehaviour
203
+ ? `${formatLevel(currentBehaviour.maturity)}`
204
+ : "Not required",
205
+ });
206
+
207
+ targetData.push({
208
+ label: behaviourName,
209
+ value: targetBehaviour
210
+ ? getBehaviourMaturityIndex(targetBehaviour.maturity)
211
+ : 0,
212
+ maxValue: 5,
213
+ description: targetBehaviour
214
+ ? `${formatLevel(targetBehaviour.maturity)}`
215
+ : "Not required",
216
+ });
217
+ }
218
+
219
+ const chart = new ComparisonRadarChart({
220
+ container: wrapper,
221
+ currentData,
222
+ targetData,
223
+ options: {
224
+ levels: 5,
225
+ currentColor: options.currentColor || "#3b82f6",
226
+ targetColor: options.targetColor || "#10b981",
227
+ size: options.size || 400,
228
+ showLabels: true,
229
+ showTooltips: true,
230
+ },
231
+ });
232
+
233
+ chart.render();
234
+ }, 0);
235
+
236
+ return container;
237
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Reusable detail view component
3
+ */
4
+
5
+ import {
6
+ div,
7
+ h1,
8
+ h2,
9
+ p,
10
+ a,
11
+ span,
12
+ table,
13
+ thead,
14
+ tbody,
15
+ tr,
16
+ th,
17
+ td,
18
+ section,
19
+ } from "../lib/render.js";
20
+ import { createBackLink } from "./nav.js";
21
+ import { createTag } from "./card.js";
22
+ import { formatLevel } from "../lib/render.js";
23
+ import {
24
+ SKILL_LEVEL_ORDER,
25
+ BEHAVIOUR_MATURITY_ORDER,
26
+ } from "../model/levels.js";
27
+
28
+ /**
29
+ * Create a detail page header
30
+ * @param {Object} options
31
+ * @param {string} options.title
32
+ * @param {string} [options.description]
33
+ * @param {string} options.backLink - Path to go back to
34
+ * @param {string} [options.backText] - Back link text
35
+ * @param {HTMLElement[]} [options.badges]
36
+ * @param {HTMLElement[]} [options.actions] - Action buttons
37
+ * @returns {HTMLElement}
38
+ */
39
+ export function createDetailHeader({
40
+ title,
41
+ description,
42
+ backLink,
43
+ backText = "← Back to list",
44
+ badges = [],
45
+ actions = [],
46
+ }) {
47
+ return div(
48
+ { className: "page-header" },
49
+ createBackLink(backLink, backText),
50
+ div(
51
+ { className: "card-header" },
52
+ h1({ className: "page-title" }, title),
53
+ badges.length > 0 ? div({ className: "page-meta" }, ...badges) : null,
54
+ ),
55
+ description ? p({ className: "page-description" }, description) : null,
56
+ actions.length > 0 ? div({ className: "page-actions" }, ...actions) : null,
57
+ );
58
+ }
59
+
60
+ /**
61
+ * Create a detail section
62
+ * @param {Object} options
63
+ * @param {string} options.title
64
+ * @param {HTMLElement} options.content
65
+ * @returns {HTMLElement}
66
+ */
67
+ export function createDetailSection({ title, content }) {
68
+ return section(
69
+ { className: "section section-detail" },
70
+ h2({ className: "section-title" }, title),
71
+ content,
72
+ );
73
+ }
74
+
75
+ /**
76
+ * Create a level descriptions table
77
+ * @param {Object} descriptions - Level descriptions object
78
+ * @param {string} [type='skill'] - 'skill' or 'behaviour'
79
+ * @returns {HTMLElement}
80
+ */
81
+ export function createLevelTable(descriptions, type = "skill") {
82
+ const levels =
83
+ type === "skill" ? SKILL_LEVEL_ORDER : BEHAVIOUR_MATURITY_ORDER;
84
+
85
+ const levelLabels = Object.fromEntries(
86
+ levels.map((level, index) => [level, String(index + 1)]),
87
+ );
88
+
89
+ const maxLevels = levels.length;
90
+
91
+ const rows = levels.map((level) => {
92
+ const description = descriptions?.[level] || "—";
93
+ const levelIndex = parseInt(levelLabels[level]);
94
+ return tr(
95
+ {},
96
+ createLevelCell(levelIndex, maxLevels, level),
97
+ td({}, description),
98
+ );
99
+ });
100
+
101
+ return div(
102
+ { className: "table-container" },
103
+ table(
104
+ { className: "table levels-table" },
105
+ thead({}, tr({}, th({}, "Level"), th({}, "Description"))),
106
+ tbody({}, ...rows),
107
+ ),
108
+ );
109
+ }
110
+
111
+ /**
112
+ * Create level dots indicator
113
+ * @param {number} level - Current level (1-based)
114
+ * @param {number} maxLevel - Maximum level
115
+ * @returns {HTMLElement}
116
+ */
117
+ export function createLevelDots(level, maxLevel) {
118
+ const dots = [];
119
+ for (let i = 1; i <= maxLevel; i++) {
120
+ const dot = div({
121
+ className: `level-dot ${i <= level ? "filled level-" + i : ""}`,
122
+ });
123
+ dots.push(dot);
124
+ }
125
+ return div({ className: "level-bar" }, ...dots);
126
+ }
127
+
128
+ /**
129
+ * Create a level cell with dots and label
130
+ * @param {number} levelIndex - Current level (1-based index)
131
+ * @param {number} maxLevels - Maximum levels
132
+ * @param {string} levelName - Level name to display
133
+ * @returns {HTMLElement}
134
+ */
135
+ export function createLevelCell(levelIndex, maxLevels, levelName) {
136
+ return td(
137
+ { className: "level-cell" },
138
+ createLevelDots(levelIndex, maxLevels),
139
+ span({ className: "level-label" }, formatLevel(levelName)),
140
+ );
141
+ }
142
+
143
+ /**
144
+ * Create an empty level cell (for gained/lost states)
145
+ * @returns {HTMLElement}
146
+ */
147
+ export function createEmptyLevelCell() {
148
+ return td(
149
+ { className: "level-cell" },
150
+ span({ className: "level-label text-muted" }, "—"),
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Create a links list
156
+ * @param {Array<{id: string, name: string}>} items
157
+ * @param {string} basePath - Base path for links (e.g., '/skill')
158
+ * @param {string} [emptyMessage='None']
159
+ * @param {Function} [getDisplayName] - Optional function to get display name
160
+ * @returns {HTMLElement}
161
+ */
162
+ export function createLinksList(
163
+ items,
164
+ basePath,
165
+ emptyMessage = "None",
166
+ getDisplayName,
167
+ ) {
168
+ if (!items || items.length === 0) {
169
+ return p({ className: "text-muted" }, emptyMessage);
170
+ }
171
+
172
+ const displayFn = getDisplayName || ((item) => item.name);
173
+ const links = items.map((item) =>
174
+ a({ href: `#${basePath}/${item.id}` }, displayFn(item)),
175
+ );
176
+
177
+ return div({ className: "links-list" }, ...links);
178
+ }
179
+
180
+ /**
181
+ * Create a tags list
182
+ * @param {string[]} tags
183
+ * @param {string} [emptyMessage='None']
184
+ * @returns {HTMLElement}
185
+ */
186
+ export function createTagsList(tags, emptyMessage = "None") {
187
+ if (!tags || tags.length === 0) {
188
+ return p({ className: "text-muted" }, emptyMessage);
189
+ }
190
+
191
+ return div({ className: "tags" }, ...tags.map((tag) => createTag(tag)));
192
+ }
193
+
194
+ /**
195
+ * Create a detail grid item
196
+ * @param {string} label
197
+ * @param {string|HTMLElement} value
198
+ * @returns {HTMLElement}
199
+ */
200
+ export function createDetailItem(label, value) {
201
+ const valueEl =
202
+ typeof value === "string"
203
+ ? div({ className: "detail-item-value" }, value)
204
+ : value;
205
+
206
+ return div(
207
+ { className: "detail-item" },
208
+ div({ className: "detail-item-label" }, label),
209
+ valueEl,
210
+ );
211
+ }
212
+
213
+ /**
214
+ * Create an expectations card
215
+ * @param {Object} expectations
216
+ * @returns {HTMLElement}
217
+ */
218
+ export function createExpectationsCard(expectations) {
219
+ if (!expectations) return null;
220
+
221
+ const items = Object.entries(expectations).map(([key, value]) =>
222
+ div(
223
+ { className: "expectation-item" },
224
+ div({ className: "expectation-label" }, formatLevel(key)),
225
+ div({ className: "expectation-value" }, value),
226
+ ),
227
+ );
228
+
229
+ return div({ className: "auto-grid-sm" }, ...items);
230
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Error page components
3
+ */
4
+
5
+ import { render, div, h1, p, a } from "../lib/render.js";
6
+
7
+ /**
8
+ * Render a not found error page
9
+ * @param {Object} options - Configuration options
10
+ * @param {string} options.entityType - Type of entity not found (e.g., 'Skill', 'Behaviour')
11
+ * @param {string} options.entityId - ID that was not found
12
+ * @param {string} options.backPath - Path to navigate back to
13
+ * @param {string} options.backText - Text for back link
14
+ */
15
+ export function renderNotFound({ entityType, entityId, backPath, backText }) {
16
+ render(
17
+ div(
18
+ { className: "error-message" },
19
+ h1({}, `${entityType} Not Found`),
20
+ p({}, `No ${entityType.toLowerCase()} found with ID: ${entityId}`),
21
+ a({ href: `#${backPath}` }, backText),
22
+ ),
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Create a not found error element (without rendering)
28
+ * @param {Object} options - Configuration options
29
+ * @param {string} options.entityType - Type of entity not found
30
+ * @param {string} options.entityId - ID that was not found
31
+ * @param {string} options.backPath - Path to navigate back to
32
+ * @param {string} options.backText - Text for back link
33
+ * @returns {HTMLElement}
34
+ */
35
+ export function createNotFound({ entityType, entityId, backPath, backText }) {
36
+ return div(
37
+ { className: "error-message" },
38
+ h1({}, `${entityType} Not Found`),
39
+ p({}, `No ${entityType.toLowerCase()} found with ID: ${entityId}`),
40
+ a({ href: `#${backPath}` }, backText),
41
+ );
42
+ }
43
+
44
+ /**
45
+ * Create an invalid state error element
46
+ * @param {Object} options - Configuration options
47
+ * @param {string} options.title - Error title
48
+ * @param {string} options.message - Error message
49
+ * @param {string} options.backPath - Path to navigate back to
50
+ * @param {string} options.backText - Text for back link
51
+ * @returns {HTMLElement}
52
+ */
53
+ export function createErrorMessage({ title, message, backPath, backText }) {
54
+ return div(
55
+ { className: "error-message" },
56
+ h1({}, title),
57
+ p({}, message),
58
+ a({ href: `#${backPath}` }, backText),
59
+ );
60
+ }
61
+
62
+ /**
63
+ * Render an invalid state error page
64
+ * @param {Object} options - Configuration options
65
+ * @param {string} options.title - Error title
66
+ * @param {string} options.message - Error message
67
+ * @param {string} options.backPath - Path to navigate back to
68
+ * @param {string} options.backText - Text for back link
69
+ */
70
+ export function renderError({ title, message, backPath, backText }) {
71
+ render(createErrorMessage({ title, message, backPath, backText }));
72
+ }