@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,70 @@
1
+ /**
2
+ * Track CLI Command
3
+ *
4
+ * Handles track summary, listing, and detail display in the terminal.
5
+ *
6
+ * Usage:
7
+ * npx pathway track # Summary with stats
8
+ * npx pathway track --list # IDs only (for piping)
9
+ * npx pathway track <id> # Detail view
10
+ * npx pathway track --validate # Validation checks
11
+ */
12
+
13
+ import { createEntityCommand } from "./command-factory.js";
14
+ import { trackToMarkdown } from "../formatters/track/markdown.js";
15
+ import { sortTracksByType } from "../formatters/track/shared.js";
16
+ import { formatTable } from "../lib/cli-output.js";
17
+ import { getConceptEmoji } from "../model/levels.js";
18
+
19
+ /**
20
+ * Format track summary output
21
+ * @param {Array} tracks - Raw track entities (already sorted by type)
22
+ * @param {Object} data - Full data context
23
+ */
24
+ function formatSummary(tracks, data) {
25
+ const { framework } = data;
26
+ const emoji = framework ? getConceptEmoji(framework, "track") : "🛤️";
27
+
28
+ console.log(`\n${emoji} Tracks\n`);
29
+
30
+ const rows = tracks.map((t) => {
31
+ const types = [];
32
+ if (t.isProfessional) types.push("P");
33
+ if (t.isManagement) types.push("M");
34
+ const modCount = Object.keys(t.skillModifiers || {}).length;
35
+ return [t.id, t.name, types.join("/") || "-", modCount];
36
+ });
37
+
38
+ console.log(formatTable(["ID", "Name", "Type", "Modifiers"], rows));
39
+ console.log(`\nTotal: ${tracks.length} tracks`);
40
+ console.log(`\nRun 'npx pathway track --list' for IDs`);
41
+ console.log(`Run 'npx pathway track <id>' for details\n`);
42
+ }
43
+
44
+ /**
45
+ * Format track detail output - receives entity and context as single object
46
+ * @param {Object} viewAndContext - Contains track entity and data context
47
+ * @param {Object} framework - Framework config
48
+ */
49
+ function formatDetail(viewAndContext, framework) {
50
+ const { track, skills, behaviours, disciplines } = viewAndContext;
51
+ console.log(
52
+ trackToMarkdown(track, { skills, behaviours, disciplines, framework }),
53
+ );
54
+ }
55
+
56
+ export const runTrackCommand = createEntityCommand({
57
+ entityName: "track",
58
+ pluralName: "tracks",
59
+ findEntity: (data, id) => data.tracks.find((t) => t.id === id),
60
+ presentDetail: (entity, data) => ({
61
+ track: entity,
62
+ skills: data.skills,
63
+ behaviours: data.behaviours,
64
+ disciplines: data.disciplines,
65
+ }),
66
+ sortItems: sortTracksByType,
67
+ formatSummary,
68
+ formatDetail,
69
+ emoji: "🛤️",
70
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Reusable action button components
3
+ */
4
+
5
+ import { button } from "../lib/render.js";
6
+
7
+ /**
8
+ * Create a navigation button
9
+ * @param {Object} options - Configuration options
10
+ * @param {string} options.label - Button label text
11
+ * @param {string} options.href - Destination URL hash
12
+ * @param {string} [options.variant] - Button variant: 'primary' or 'secondary'
13
+ * @returns {HTMLElement}
14
+ */
15
+ export function createNavButton({ label, href, variant = "primary" }) {
16
+ const btn = button(
17
+ {
18
+ className: `btn btn-${variant}`,
19
+ },
20
+ label,
21
+ );
22
+
23
+ btn.addEventListener("click", () => {
24
+ window.location.hash = href;
25
+ });
26
+
27
+ return btn;
28
+ }
29
+
30
+ /**
31
+ * Create a button to navigate to job builder with a parameter
32
+ * @param {Object} options - Configuration options
33
+ * @param {string} options.paramName - Parameter name (discipline, grade, track)
34
+ * @param {string} options.paramValue - Parameter value (the ID)
35
+ * @param {string} [options.label] - Optional custom label
36
+ * @returns {HTMLElement}
37
+ */
38
+ export function createJobBuilderButton({ paramName, paramValue, label }) {
39
+ const defaultLabels = {
40
+ discipline: "Build Job with this Discipline →",
41
+ grade: "Build Job at this Grade →",
42
+ track: "Build Job with this Track →",
43
+ };
44
+
45
+ return createNavButton({
46
+ label: label || defaultLabels[paramName] || "Build Job →",
47
+ href: `/job-builder?${paramName}=${paramValue}`,
48
+ variant: "primary",
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Create a button to navigate to interview prep with a parameter
54
+ * @param {Object} options - Configuration options
55
+ * @param {string} options.paramName - Parameter name (discipline, grade, track)
56
+ * @param {string} options.paramValue - Parameter value (the ID)
57
+ * @param {string} [options.label] - Optional custom label
58
+ * @returns {HTMLElement}
59
+ */
60
+ export function createInterviewPrepButton({ paramName, paramValue, label }) {
61
+ return createNavButton({
62
+ label: label || "Interview Prep →",
63
+ href: `/interview-prep?${paramName}=${paramValue}`,
64
+ variant: "secondary",
65
+ });
66
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Behaviour profile display component
3
+ */
4
+
5
+ /** @typedef {import('../types.js').BehaviourProfileItem} BehaviourProfileItem */
6
+
7
+ import { div, table, thead, tbody, tr, th, td, a } from "../lib/render.js";
8
+ import { getBehaviourMaturityIndex } from "../lib/render.js";
9
+ import { createLevelCell } from "./detail.js";
10
+ import { truncate } from "../formatters/shared.js";
11
+
12
+ /**
13
+ * Create a behaviour profile table
14
+ * @param {BehaviourProfileItem[]} behaviourProfile - Behaviour profile entries
15
+ * @returns {HTMLElement}
16
+ */
17
+ export function createBehaviourProfile(behaviourProfile) {
18
+ if (!behaviourProfile || behaviourProfile.length === 0) {
19
+ return div({ className: "empty-state" }, "No behaviours in profile");
20
+ }
21
+
22
+ const rows = behaviourProfile.map((behaviour) => {
23
+ const maturityIndex = getBehaviourMaturityIndex(behaviour.maturity);
24
+
25
+ return tr(
26
+ {},
27
+ td(
28
+ {},
29
+ a(
30
+ { href: `#/behaviour/${behaviour.behaviourId}` },
31
+ behaviour.behaviourName,
32
+ ),
33
+ ),
34
+ createLevelCell(maturityIndex, 5, behaviour.maturity),
35
+ td(
36
+ { className: "behaviour-description" },
37
+ truncate(behaviour.maturityDescription, 80),
38
+ ),
39
+ );
40
+ });
41
+
42
+ return div(
43
+ { className: "table-container" },
44
+ table(
45
+ { className: "table matrix-table behaviour-matrix" },
46
+ thead(
47
+ {},
48
+ tr({}, th({}, "Behaviour"), th({}, "Maturity"), th({}, "Description")),
49
+ ),
50
+ tbody({}, ...rows),
51
+ ),
52
+ );
53
+ }
@@ -0,0 +1,341 @@
1
+ /**
2
+ * Builder component for discipline/grade/track selection pages
3
+ */
4
+
5
+ import {
6
+ div,
7
+ h1,
8
+ h2,
9
+ h3,
10
+ p,
11
+ span,
12
+ button,
13
+ label,
14
+ section,
15
+ } from "../lib/render.js";
16
+ import { getState } from "../lib/state.js";
17
+ import { createBadge } from "./card.js";
18
+ import { createSelectWithValue } from "../lib/form-controls.js";
19
+ import { createReactive } from "../lib/reactive.js";
20
+
21
+ /**
22
+ * @typedef {Object} HelpItem
23
+ * @property {string} label - Label for the help item
24
+ * @property {string} text - Description text
25
+ */
26
+
27
+ /**
28
+ * @typedef {Object} BuilderSelection
29
+ * @property {Object} discipline - Selected discipline
30
+ * @property {Object} grade - Selected grade
31
+ * @property {Object} track - Selected track
32
+ */
33
+
34
+ /**
35
+ * @typedef {Object} BuilderConfig
36
+ * @property {string} title - Page title
37
+ * @property {string} description - Page description
38
+ * @property {string} formTitle - Form section title
39
+ * @property {string} emptyPreviewText - Text when nothing selected
40
+ * @property {string} buttonText - Action button text
41
+ * @property {Function} previewPresenter - (selection, data) => previewData
42
+ * @property {Function} detailPath - (selection) => "/path/to/detail"
43
+ * @property {Function} renderPreview - (previewData, selection) => HTMLElement
44
+ * @property {Array<HelpItem>} helpItems - Help section items
45
+ * @property {Object} [labels] - Optional custom labels for selects
46
+ */
47
+
48
+ /**
49
+ * Create a builder page
50
+ * @param {BuilderConfig} config
51
+ * @returns {HTMLElement}
52
+ */
53
+ export function createBuilder({
54
+ title,
55
+ description,
56
+ formTitle,
57
+ emptyPreviewText,
58
+ buttonText,
59
+ previewPresenter,
60
+ detailPath,
61
+ renderPreview,
62
+ helpItems,
63
+ labels = {},
64
+ }) {
65
+ const { data } = getState();
66
+
67
+ // Parse URL params for pre-selection
68
+ const urlParams = new URLSearchParams(
69
+ window.location.hash.split("?")[1] || "",
70
+ );
71
+
72
+ // Create reactive selection state
73
+ const selection = createReactive({
74
+ discipline: urlParams.get("discipline") || "",
75
+ track: urlParams.get("track") || "",
76
+ grade: urlParams.get("grade") || "",
77
+ });
78
+
79
+ const sortedGrades = [...data.grades].sort((a, b) => a.level - b.level);
80
+
81
+ // Create elements that need references
82
+ const previewContainer = div(
83
+ { className: "job-preview", id: "job-preview" },
84
+ p({ className: "text-muted" }, emptyPreviewText),
85
+ );
86
+
87
+ const actionButton = button(
88
+ { className: "btn btn-primary btn-lg", id: "generate-btn", disabled: true },
89
+ buttonText,
90
+ );
91
+
92
+ // Subscribe to selection changes - all updates happen here
93
+ selection.subscribe(({ discipline, track, grade }) => {
94
+ if (!discipline || !track || !grade) {
95
+ previewContainer.innerHTML = "";
96
+ previewContainer.appendChild(
97
+ p({ className: "text-muted" }, emptyPreviewText),
98
+ );
99
+ actionButton.disabled = true;
100
+ return;
101
+ }
102
+
103
+ const disciplineObj = data.disciplines.find((d) => d.id === discipline);
104
+ const trackObj = data.tracks.find((t) => t.id === track);
105
+ const gradeObj = data.grades.find((g) => g.id === grade);
106
+
107
+ if (!disciplineObj || !trackObj || !gradeObj) {
108
+ previewContainer.innerHTML = "";
109
+ previewContainer.appendChild(
110
+ p({ className: "text-muted" }, "Invalid selection. Please try again."),
111
+ );
112
+ actionButton.disabled = true;
113
+ return;
114
+ }
115
+
116
+ const selectionObj = {
117
+ discipline: disciplineObj,
118
+ track: trackObj,
119
+ grade: gradeObj,
120
+ };
121
+ const preview = previewPresenter(selectionObj, data);
122
+
123
+ previewContainer.innerHTML = "";
124
+ previewContainer.appendChild(renderPreview(preview, selectionObj));
125
+ actionButton.disabled = !preview.isValid;
126
+ });
127
+
128
+ // Wire up button
129
+ actionButton.addEventListener("click", () => {
130
+ const { discipline, track, grade } = selection.get();
131
+ window.location.hash = detailPath({ discipline, track, grade });
132
+ });
133
+
134
+ // Build the page
135
+ const page = div(
136
+ { className: "job-builder-page" },
137
+ // Header
138
+ div(
139
+ { className: "page-header" },
140
+ h1({ className: "page-title" }, title),
141
+ p({ className: "page-description" }, description),
142
+ ),
143
+ // Form
144
+ div(
145
+ { className: "job-builder-form" },
146
+ h2({}, formTitle),
147
+ div(
148
+ { className: "auto-grid-sm gap-lg" },
149
+ // Discipline selector
150
+ div(
151
+ { className: "form-group" },
152
+ label({ className: "form-label" }, labels.discipline || "Discipline"),
153
+ createSelectWithValue({
154
+ id: "discipline-select",
155
+ items: data.disciplines,
156
+ initialValue: selection.get().discipline,
157
+ placeholder: "Select a discipline...",
158
+ onChange: (value) => {
159
+ selection.update((prev) => ({ ...prev, discipline: value }));
160
+ },
161
+ getDisplayName: (d) => d.specialization || d.name,
162
+ }),
163
+ ),
164
+ // Track selector
165
+ div(
166
+ { className: "form-group" },
167
+ label({ className: "form-label" }, labels.track || "Track"),
168
+ createSelectWithValue({
169
+ id: "track-select",
170
+ items: data.tracks,
171
+ initialValue: selection.get().track,
172
+ placeholder: "Select a track...",
173
+ onChange: (value) => {
174
+ selection.update((prev) => ({ ...prev, track: value }));
175
+ },
176
+ getDisplayName: (t) => t.name,
177
+ }),
178
+ ),
179
+ // Grade selector
180
+ div(
181
+ { className: "form-group" },
182
+ label({ className: "form-label" }, labels.grade || "Grade"),
183
+ createSelectWithValue({
184
+ id: "grade-select",
185
+ items: sortedGrades,
186
+ initialValue: selection.get().grade,
187
+ placeholder: "Select a grade...",
188
+ onChange: (value) => {
189
+ selection.update((prev) => ({ ...prev, grade: value }));
190
+ },
191
+ getDisplayName: (g) => g.id,
192
+ }),
193
+ ),
194
+ ),
195
+ previewContainer,
196
+ div({ className: "page-actions" }, actionButton),
197
+ ),
198
+ // Help section
199
+ helpItems && createHelpSection(helpItems),
200
+ );
201
+
202
+ // Trigger initial update if preselected
203
+ const initial = selection.get();
204
+ if (initial.discipline || initial.track || initial.grade) {
205
+ setTimeout(() => selection.set(selection.get()), 0);
206
+ }
207
+
208
+ return page;
209
+ }
210
+
211
+ /**
212
+ * Create help section with items
213
+ * @param {Array<HelpItem>} items
214
+ * @returns {HTMLElement}
215
+ */
216
+ function createHelpSection(items) {
217
+ return section(
218
+ { className: "section section-detail" },
219
+ h2({ className: "section-title" }, "How It Works"),
220
+ div(
221
+ { className: "auto-grid-md" },
222
+ ...items.map((item) =>
223
+ div(
224
+ { className: "detail-item" },
225
+ div({ className: "detail-item-label" }, item.label),
226
+ p({}, item.text),
227
+ ),
228
+ ),
229
+ ),
230
+ );
231
+ }
232
+
233
+ /**
234
+ * Create a standard job/interview preview with valid/invalid states
235
+ * @param {Object} preview - Preview data from presenter
236
+ * @returns {HTMLElement}
237
+ */
238
+ export function createStandardPreview(preview) {
239
+ if (!preview.isValid) {
240
+ return div(
241
+ {},
242
+ div(
243
+ {
244
+ className: "job-preview-invalid",
245
+ style:
246
+ "color: var(--danger-color); display: flex; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem;",
247
+ },
248
+ span({}, "✗"),
249
+ span({}, "Invalid combination"),
250
+ ),
251
+ p({ className: "text-muted" }, preview.invalidReason),
252
+ );
253
+ }
254
+
255
+ return div(
256
+ {},
257
+ div(
258
+ { className: "job-preview-valid" },
259
+ span({}, "✓"),
260
+ span({}, "Valid combination"),
261
+ ),
262
+ h3({ className: "job-preview-title" }, preview.title),
263
+ div(
264
+ { className: "card-meta", style: "margin-top: 0.5rem" },
265
+ createBadge(`${preview.totalSkills} skills`, "default"),
266
+ createBadge(`${preview.totalBehaviours} behaviours`, "default"),
267
+ ),
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Create career progress preview with paths info
273
+ * @param {Object} preview - Preview data from presenter
274
+ * @param {Object} selection - Current selection
275
+ * @returns {HTMLElement}
276
+ */
277
+ export function createProgressPreview(preview, selection) {
278
+ if (!preview.isValid) {
279
+ return div(
280
+ { className: "job-preview-content" },
281
+ div(
282
+ { className: "preview-error" },
283
+ span({ className: "preview-error-icon" }, "⚠️"),
284
+ span({}, "This combination is not valid. "),
285
+ span({ className: "text-muted" }, preview.invalidReason),
286
+ ),
287
+ );
288
+ }
289
+
290
+ const { discipline, grade, track } = selection;
291
+
292
+ return div(
293
+ { className: "job-preview-content" },
294
+ div(
295
+ { className: "preview-section" },
296
+ div({ className: "preview-label" }, "Current Role"),
297
+ div({ className: "preview-title" }, preview.title),
298
+ ),
299
+ div(
300
+ { className: "preview-badges" },
301
+ createBadge(discipline.specialization, "discipline"),
302
+ createBadge(grade.id, "grade"),
303
+ createBadge(track.name, "track"),
304
+ ),
305
+ div(
306
+ { className: "preview-section", style: "margin-top: 1rem" },
307
+ div({ className: "preview-label" }, "Progression Paths Available"),
308
+ div(
309
+ { className: "preview-paths" },
310
+ preview.nextGrade
311
+ ? div(
312
+ { className: "path-item" },
313
+ span({ className: "path-icon" }, "📈"),
314
+ span(
315
+ {},
316
+ `Next Grade: ${preview.nextGrade.id} - ${preview.nextGrade.name}`,
317
+ ),
318
+ )
319
+ : div(
320
+ { className: "path-item text-muted" },
321
+ span({ className: "path-icon" }, "🏆"),
322
+ span({}, "You're at the highest grade!"),
323
+ ),
324
+ preview.validTracks.length > 0
325
+ ? div(
326
+ { className: "path-item" },
327
+ span({ className: "path-icon" }, "🔀"),
328
+ span(
329
+ {},
330
+ `${preview.validTracks.length} other track${preview.validTracks.length > 1 ? "s" : ""} to compare`,
331
+ ),
332
+ )
333
+ : div(
334
+ { className: "path-item text-muted" },
335
+ span({ className: "path-icon" }, "—"),
336
+ span({}, "No other valid tracks for this discipline"),
337
+ ),
338
+ ),
339
+ ),
340
+ );
341
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Reusable card component
3
+ */
4
+
5
+ import { div, h3, p, span } from "../lib/render.js";
6
+
7
+ /**
8
+ * Create a card component
9
+ * @param {Object} options
10
+ * @param {string} options.title - Card title
11
+ * @param {string} [options.description] - Card description
12
+ * @param {string} [options.href] - Link destination (makes card clickable)
13
+ * @param {HTMLElement[]} [options.badges] - Badges to display
14
+ * @param {HTMLElement[]} [options.meta] - Meta information
15
+ * @param {HTMLElement} [options.content] - Additional content
16
+ * @param {string} [options.className] - Additional CSS class
17
+ * @returns {HTMLElement}
18
+ */
19
+ export function createCard({
20
+ title,
21
+ description,
22
+ href,
23
+ badges = [],
24
+ meta = [],
25
+ content,
26
+ className = "",
27
+ }) {
28
+ const isClickable = !!href;
29
+
30
+ const cardHeader = div(
31
+ { className: "card-header" },
32
+ h3({ className: "card-title" }, title),
33
+ badges.length > 0 ? div({ className: "card-badges" }, ...badges) : null,
34
+ );
35
+
36
+ const card = div(
37
+ {
38
+ className:
39
+ `card ${isClickable ? "card-clickable" : ""} ${className}`.trim(),
40
+ },
41
+ cardHeader,
42
+ description ? p({ className: "card-description" }, description) : null,
43
+ content || null,
44
+ meta.length > 0 ? div({ className: "card-meta" }, ...meta) : null,
45
+ );
46
+
47
+ if (isClickable) {
48
+ card.addEventListener("click", () => {
49
+ window.location.hash = href;
50
+ });
51
+ }
52
+
53
+ return card;
54
+ }
55
+
56
+ /**
57
+ * Create a stat card for the landing page
58
+ * @param {Object} options
59
+ * @param {number|string} options.value - The stat value
60
+ * @param {string} options.label - The stat label
61
+ * @param {string} [options.href] - Optional link
62
+ * @returns {HTMLElement}
63
+ */
64
+ export function createStatCard({ value, label, href }) {
65
+ const card = div(
66
+ { className: "stat-card" },
67
+ div({ className: "stat-value" }, String(value)),
68
+ div({ className: "stat-label" }, label),
69
+ );
70
+
71
+ if (href) {
72
+ card.style.cursor = "pointer";
73
+ card.addEventListener("click", () => {
74
+ window.location.hash = href;
75
+ });
76
+ }
77
+
78
+ return card;
79
+ }
80
+
81
+ /**
82
+ * Create a badge element
83
+ * @param {string} text - Badge text
84
+ * @param {string} [type] - Badge type (default, primary, secondary, broad, technical, ai, etc.)
85
+ * @returns {HTMLElement}
86
+ */
87
+ export function createBadge(text, type = "default") {
88
+ return span({ className: `badge badge-${type}` }, text);
89
+ }
90
+
91
+ /**
92
+ * Create a tag element
93
+ * @param {string} text
94
+ * @returns {HTMLElement}
95
+ */
96
+ export function createTag(text) {
97
+ return span({ className: "tag" }, text);
98
+ }