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