@forwardimpact/pathway 0.21.0 → 0.23.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/README.md +3 -3
- package/bin/fit-pathway.js +22 -22
- package/package.json +4 -3
- package/src/commands/agent.js +14 -10
- package/src/commands/behaviour.js +11 -1
- package/src/commands/build.js +11 -2
- package/src/commands/command-factory.js +4 -2
- package/src/commands/dev.js +9 -2
- package/src/commands/discipline.js +19 -2
- package/src/commands/driver.js +11 -1
- package/src/commands/index.js +1 -1
- package/src/commands/init.js +1 -1
- package/src/commands/interview.js +8 -8
- package/src/commands/job.js +41 -28
- package/src/commands/level.js +76 -0
- package/src/commands/progress.js +20 -20
- package/src/commands/questions.js +3 -3
- package/src/commands/skill.js +11 -1
- package/src/commands/stage.js +11 -1
- package/src/commands/tool.js +4 -3
- package/src/commands/track.js +11 -1
- package/src/components/action-buttons.js +3 -3
- package/src/components/builder.js +25 -25
- package/src/components/card.js +8 -104
- package/src/components/comparison-radar.js +4 -4
- package/src/components/detail.js +18 -120
- package/src/components/error-page.js +8 -68
- package/src/components/grid.js +12 -106
- package/src/components/list.js +7 -116
- package/src/components/nav.js +7 -60
- package/src/components/radar-chart.js +3 -3
- package/src/components/skill-matrix.js +7 -7
- package/src/css/bundles/app.css +25 -21
- package/src/css/bundles/handout.css +33 -33
- package/src/css/bundles/slides.css +25 -25
- package/src/css/pages/landing.css +5 -5
- package/src/formatters/index.js +5 -5
- package/src/formatters/interview/shared.js +23 -23
- package/src/formatters/job/description.js +18 -18
- package/src/formatters/job/dom.js +12 -12
- package/src/formatters/job/markdown.js +7 -7
- package/src/formatters/json-ld.js +24 -24
- package/src/formatters/{grade → level}/dom.js +31 -27
- package/src/formatters/{grade → level}/markdown.js +19 -28
- package/src/formatters/{grade → level}/microdata.js +28 -38
- package/src/formatters/level/shared.js +86 -0
- package/src/formatters/progress/markdown.js +2 -2
- package/src/formatters/progress/shared.js +51 -51
- package/src/formatters/questions/markdown.js +8 -6
- package/src/formatters/questions/shared.js +7 -7
- package/src/formatters/skill/dom.js +4 -4
- package/src/formatters/skill/markdown.js +1 -1
- package/src/formatters/skill/microdata.js +3 -3
- package/src/formatters/skill/shared.js +3 -3
- package/src/formatters/track/shared.js +1 -1
- package/src/handout-main.js +12 -12
- package/src/handout.html +32 -13
- package/src/index.html +33 -14
- package/src/lib/card-mappers.js +16 -16
- package/src/lib/cli-command.js +3 -3
- package/src/lib/cli-output.js +2 -2
- package/src/lib/error-boundary.js +3 -66
- package/src/lib/errors.js +7 -45
- package/src/lib/job-cache.js +12 -12
- package/src/lib/markdown.js +2 -109
- package/src/lib/reactive.js +7 -73
- package/src/lib/render.js +53 -201
- package/src/lib/router-core.js +2 -156
- package/src/lib/router-pages.js +2 -11
- package/src/lib/router-slides.js +2 -197
- package/src/lib/state.js +16 -65
- package/src/lib/utils.js +3 -10
- package/src/lib/yaml-loader.js +22 -80
- package/src/main.js +10 -10
- package/src/pages/agent-builder.js +12 -12
- package/src/pages/assessment-results.js +28 -24
- package/src/pages/interview-builder.js +6 -6
- package/src/pages/interview.js +8 -8
- package/src/pages/job-builder.js +7 -7
- package/src/pages/job.js +8 -8
- package/src/pages/landing.js +8 -8
- package/src/pages/level.js +122 -0
- package/src/pages/progress-builder.js +8 -8
- package/src/pages/progress.js +74 -74
- package/src/pages/self-assessment.js +7 -7
- package/src/pages/skill.js +1 -1
- package/src/slide-main.js +23 -23
- package/src/slides/chapter.js +4 -4
- package/src/slides/index.js +11 -11
- package/src/slides/interview.js +2 -2
- package/src/slides/job.js +4 -4
- package/src/slides/level.js +32 -0
- package/src/slides/overview.js +10 -10
- package/src/slides/progress.js +13 -13
- package/src/slides.html +32 -13
- package/src/types.js +1 -1
- package/templates/job.template.md +2 -2
- package/src/commands/grade.js +0 -60
- package/src/css/base.css +0 -56
- package/src/css/components/badges.css +0 -232
- package/src/css/components/buttons.css +0 -101
- package/src/css/components/forms.css +0 -191
- package/src/css/components/layout.css +0 -218
- package/src/css/components/nav.css +0 -206
- package/src/css/components/progress.css +0 -166
- package/src/css/components/states.css +0 -82
- package/src/css/components/surfaces.css +0 -347
- package/src/css/components/tables.css +0 -362
- package/src/css/components/top-bar.css +0 -180
- package/src/css/components/typography.css +0 -121
- package/src/css/components/utilities.css +0 -41
- package/src/css/pages/detail.css +0 -119
- package/src/css/reset.css +0 -50
- package/src/css/tokens.css +0 -162
- package/src/css/views/handout.css +0 -30
- package/src/css/views/print.css +0 -634
- package/src/css/views/slide-animations.css +0 -113
- package/src/css/views/slide-base.css +0 -331
- package/src/css/views/slide-sections.css +0 -597
- package/src/css/views/slide-tables.css +0 -275
- package/src/formatters/grade/shared.js +0 -86
- package/src/pages/grade.js +0 -122
- package/src/slides/grade.js +0 -32
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Level CLI Command
|
|
3
|
+
*
|
|
4
|
+
* Handles level summary, listing, and detail display in the terminal.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx pathway level # Summary with stats
|
|
8
|
+
* npx pathway level --list # IDs only (for piping)
|
|
9
|
+
* npx pathway level <id> # Detail view
|
|
10
|
+
* npx pathway level --validate # Validation checks
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createEntityCommand } from "./command-factory.js";
|
|
14
|
+
import { levelToMarkdown } from "../formatters/level/markdown.js";
|
|
15
|
+
import { formatTable } from "../lib/cli-output.js";
|
|
16
|
+
import { getConceptEmoji } from "@forwardimpact/map/levels";
|
|
17
|
+
import { capitalize } from "../formatters/shared.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Format level list item for --list output
|
|
21
|
+
* @param {Object} level - Level entity
|
|
22
|
+
* @returns {string} Formatted list line
|
|
23
|
+
*/
|
|
24
|
+
function formatListItem(level) {
|
|
25
|
+
return `${level.id}, ${level.professionalTitle || level.id}, ${level.managementTitle || level.id}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Format level summary output
|
|
30
|
+
* @param {Array} levels - Raw level entities
|
|
31
|
+
* @param {Object} data - Full data context
|
|
32
|
+
*/
|
|
33
|
+
function formatSummary(levels, data) {
|
|
34
|
+
const { framework } = data;
|
|
35
|
+
const emoji = framework ? getConceptEmoji(framework, "level") : "📊";
|
|
36
|
+
|
|
37
|
+
console.log(`\n${emoji} Levels\n`);
|
|
38
|
+
|
|
39
|
+
const rows = levels.map((g) => [
|
|
40
|
+
g.id,
|
|
41
|
+
g.professionalTitle || g.displayName || g.id,
|
|
42
|
+
g.managementTitle || "-",
|
|
43
|
+
g.typicalExperienceRange || "-",
|
|
44
|
+
capitalize(g.baseSkillProficiencies?.primary || "-"),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
console.log(
|
|
48
|
+
formatTable(
|
|
49
|
+
["ID", "Professional Title", "Management Title", "Experience", "Primary Level"],
|
|
50
|
+
rows,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
console.log(`\nTotal: ${levels.length} levels`);
|
|
54
|
+
console.log(`\nRun 'npx pathway level --list' for IDs and titles`);
|
|
55
|
+
console.log(`Run 'npx pathway level <id>' for details\n`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format level detail output
|
|
60
|
+
* @param {Object} level - Raw level entity
|
|
61
|
+
* @param {Object} framework - Framework config
|
|
62
|
+
*/
|
|
63
|
+
function formatDetail(level, framework) {
|
|
64
|
+
console.log(levelToMarkdown(level, framework));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const runLevelCommand = createEntityCommand({
|
|
68
|
+
entityName: "level",
|
|
69
|
+
pluralName: "levels",
|
|
70
|
+
findEntity: (data, id) => data.levels.find((g) => g.id === id),
|
|
71
|
+
presentDetail: (entity) => entity,
|
|
72
|
+
formatSummary,
|
|
73
|
+
formatDetail,
|
|
74
|
+
formatListItem,
|
|
75
|
+
emojiIcon: "📊",
|
|
76
|
+
});
|
package/src/commands/progress.js
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
* Shows career progression analysis in the terminal.
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* npx pathway progress <discipline> <
|
|
8
|
-
* npx pathway progress <discipline> <
|
|
9
|
-
* npx pathway progress <discipline> <
|
|
7
|
+
* npx pathway progress <discipline> <level> # Progress for trackless job
|
|
8
|
+
* npx pathway progress <discipline> <level> --track=<track> # Progress with track
|
|
9
|
+
* npx pathway progress <discipline> <from_level> --compare=<to_level> # Compare levels
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createCompositeCommand } from "./command-factory.js";
|
|
13
13
|
import {
|
|
14
14
|
prepareProgressDetail,
|
|
15
|
-
|
|
15
|
+
getDefaultTargetLevel,
|
|
16
16
|
} from "../formatters/progress/shared.js";
|
|
17
17
|
import { progressToMarkdown } from "../formatters/progress/markdown.js";
|
|
18
18
|
|
|
@@ -26,53 +26,53 @@ function formatProgress(view) {
|
|
|
26
26
|
|
|
27
27
|
export const runProgressCommand = createCompositeCommand({
|
|
28
28
|
commandName: "progress",
|
|
29
|
-
requiredArgs: ["discipline_id", "
|
|
29
|
+
requiredArgs: ["discipline_id", "level_id"],
|
|
30
30
|
findEntities: (data, args, options) => {
|
|
31
31
|
const discipline = data.disciplines.find((d) => d.id === args[0]);
|
|
32
|
-
const
|
|
32
|
+
const level = data.levels.find((g) => g.id === args[1]);
|
|
33
33
|
const track = options.track
|
|
34
34
|
? data.tracks.find((t) => t.id === options.track)
|
|
35
35
|
: null;
|
|
36
36
|
|
|
37
|
-
let
|
|
37
|
+
let targetLevel;
|
|
38
38
|
if (options.compare) {
|
|
39
|
-
|
|
40
|
-
if (!
|
|
41
|
-
console.error(`Target
|
|
39
|
+
targetLevel = data.levels.find((g) => g.id === options.compare);
|
|
40
|
+
if (!targetLevel) {
|
|
41
|
+
console.error(`Target level not found: ${options.compare}`);
|
|
42
42
|
process.exit(1);
|
|
43
43
|
}
|
|
44
44
|
} else {
|
|
45
|
-
|
|
46
|
-
if (!
|
|
47
|
-
console.error("No next
|
|
45
|
+
targetLevel = getDefaultTargetLevel(level, data.levels);
|
|
46
|
+
if (!targetLevel) {
|
|
47
|
+
console.error("No next level available for progression.");
|
|
48
48
|
process.exit(1);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
return { discipline,
|
|
52
|
+
return { discipline, level, track, targetLevel };
|
|
53
53
|
},
|
|
54
54
|
validateEntities: (entities, _data, options) => {
|
|
55
55
|
if (!entities.discipline) {
|
|
56
56
|
return `Discipline not found`;
|
|
57
57
|
}
|
|
58
|
-
if (!entities.
|
|
59
|
-
return `
|
|
58
|
+
if (!entities.level) {
|
|
59
|
+
return `Level not found`;
|
|
60
60
|
}
|
|
61
61
|
if (options.track && !entities.track) {
|
|
62
62
|
return `Track not found: ${options.track}`;
|
|
63
63
|
}
|
|
64
|
-
if (!entities.
|
|
65
|
-
return `Target
|
|
64
|
+
if (!entities.targetLevel) {
|
|
65
|
+
return `Target level not found`;
|
|
66
66
|
}
|
|
67
67
|
return null;
|
|
68
68
|
},
|
|
69
69
|
presenter: (entities, data) =>
|
|
70
70
|
prepareProgressDetail({
|
|
71
71
|
fromDiscipline: entities.discipline,
|
|
72
|
-
|
|
72
|
+
fromLevel: entities.level,
|
|
73
73
|
fromTrack: entities.track,
|
|
74
74
|
toDiscipline: entities.discipline,
|
|
75
|
-
|
|
75
|
+
toLevel: entities.targetLevel,
|
|
76
76
|
toTrack: entities.track,
|
|
77
77
|
skills: data.skills,
|
|
78
78
|
behaviours: data.behaviours,
|
|
@@ -49,7 +49,7 @@ function showQuestionsSummary(data) {
|
|
|
49
49
|
console.log(`\n❓ Questions\n`);
|
|
50
50
|
|
|
51
51
|
// Skill questions by level
|
|
52
|
-
const
|
|
52
|
+
const skillProficiencies = [
|
|
53
53
|
"awareness",
|
|
54
54
|
"foundational",
|
|
55
55
|
"working",
|
|
@@ -57,10 +57,10 @@ function showQuestionsSummary(data) {
|
|
|
57
57
|
"expert",
|
|
58
58
|
];
|
|
59
59
|
const roleTypes = ["professionalQuestions", "managementQuestions"];
|
|
60
|
-
const skillRows =
|
|
60
|
+
const skillRows = skillProficiencies.map((level) => {
|
|
61
61
|
let count = 0;
|
|
62
62
|
for (const skill of skills) {
|
|
63
|
-
const sq = questions.
|
|
63
|
+
const sq = questions.skillProficiencies?.[skill.id];
|
|
64
64
|
if (sq) {
|
|
65
65
|
for (const roleType of roleTypes) {
|
|
66
66
|
count += (sq[roleType]?.[level] || []).length;
|
package/src/commands/skill.js
CHANGED
|
@@ -16,7 +16,7 @@ import { skillToMarkdown } from "../formatters/skill/markdown.js";
|
|
|
16
16
|
import { prepareSkillsList } from "../formatters/skill/shared.js";
|
|
17
17
|
import { getConceptEmoji } from "@forwardimpact/map/levels";
|
|
18
18
|
import { formatTable, formatError } from "../lib/cli-output.js";
|
|
19
|
-
import { generateSkillMarkdown } from "@forwardimpact/
|
|
19
|
+
import { generateSkillMarkdown } from "@forwardimpact/libskill/agent";
|
|
20
20
|
import { formatAgentSkill } from "../formatters/agent/skill.js";
|
|
21
21
|
import { loadSkillTemplate } from "../lib/template-loader.js";
|
|
22
22
|
|
|
@@ -85,6 +85,15 @@ async function formatAgentDetail(skill, stages, dataDir) {
|
|
|
85
85
|
console.log(output);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Format skill list item for --list output
|
|
90
|
+
* @param {Object} skill - Skill entity
|
|
91
|
+
* @returns {string} Formatted list line
|
|
92
|
+
*/
|
|
93
|
+
function formatListItem(skill) {
|
|
94
|
+
return `${skill.id}, ${skill.name}, ${skill.capability || "-"}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
const baseSkillCommand = createEntityCommand({
|
|
89
98
|
entityName: "skill",
|
|
90
99
|
pluralName: "skills",
|
|
@@ -98,6 +107,7 @@ const baseSkillCommand = createEntityCommand({
|
|
|
98
107
|
}),
|
|
99
108
|
formatSummary,
|
|
100
109
|
formatDetail,
|
|
110
|
+
formatListItem,
|
|
101
111
|
emojiIcon: "📚",
|
|
102
112
|
});
|
|
103
113
|
|
package/src/commands/stage.js
CHANGED
|
@@ -21,6 +21,15 @@ import {
|
|
|
21
21
|
formatBullet,
|
|
22
22
|
} from "../lib/cli-output.js";
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Format stage list item for --list output
|
|
26
|
+
* @param {Object} stage - Stage entity
|
|
27
|
+
* @returns {string} Formatted list line
|
|
28
|
+
*/
|
|
29
|
+
function formatListItem(stage) {
|
|
30
|
+
return `${stage.id}, ${stage.name}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
/**
|
|
25
34
|
* Format stage summary output
|
|
26
35
|
* @param {Array} stages - Raw stage entities
|
|
@@ -43,7 +52,7 @@ function formatSummary(stages, _data) {
|
|
|
43
52
|
|
|
44
53
|
console.log(formatTable(["ID", "Name", "Mode", "Tools", "Handoffs"], rows));
|
|
45
54
|
console.log(`\nTotal: ${stages.length} stages`);
|
|
46
|
-
console.log(`\nRun 'npx pathway stage --list' for IDs`);
|
|
55
|
+
console.log(`\nRun 'npx pathway stage --list' for IDs and names`);
|
|
47
56
|
console.log(`Run 'npx pathway stage <id>' for details\n`);
|
|
48
57
|
}
|
|
49
58
|
|
|
@@ -115,5 +124,6 @@ export const runStageCommand = createEntityCommand({
|
|
|
115
124
|
}),
|
|
116
125
|
formatSummary,
|
|
117
126
|
formatDetail,
|
|
127
|
+
formatListItem,
|
|
118
128
|
emojiIcon: "🔄",
|
|
119
129
|
});
|
package/src/commands/tool.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* npx pathway tool <name> # Detail view for specific tool
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { truncate } from "../formatters/shared.js";
|
|
12
13
|
import { prepareToolsList } from "../formatters/tool/shared.js";
|
|
13
14
|
import {
|
|
14
15
|
formatTable,
|
|
@@ -27,10 +28,10 @@ export async function runToolCommand({ data, args, options }) {
|
|
|
27
28
|
const [name] = args;
|
|
28
29
|
const { tools, totalCount } = prepareToolsList(data.skills);
|
|
29
30
|
|
|
30
|
-
// --list: Output
|
|
31
|
+
// --list: Output descriptive comma-separated tool lines for piping
|
|
31
32
|
if (options.list) {
|
|
32
33
|
for (const tool of tools) {
|
|
33
|
-
console.log(tool.name);
|
|
34
|
+
console.log(`${tool.name}, ${truncate(tool.description, 60)}`);
|
|
34
35
|
}
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
@@ -87,7 +88,7 @@ function formatSummary(tools, totalCount) {
|
|
|
87
88
|
if (sorted.length > 15) {
|
|
88
89
|
console.log(`(showing top 15 by usage)`);
|
|
89
90
|
}
|
|
90
|
-
console.log(`\nRun 'npx pathway tool --list' for all tool names`);
|
|
91
|
+
console.log(`\nRun 'npx pathway tool --list' for all tool names and descriptions`);
|
|
91
92
|
console.log(`Run 'npx pathway tool <name>' for details\n`);
|
|
92
93
|
}
|
|
93
94
|
|
package/src/commands/track.js
CHANGED
|
@@ -16,6 +16,15 @@ import { sortTracksByName } from "../formatters/track/shared.js";
|
|
|
16
16
|
import { formatTable } from "../lib/cli-output.js";
|
|
17
17
|
import { getConceptEmoji } from "@forwardimpact/map/levels";
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Format track list item for --list output
|
|
21
|
+
* @param {Object} track - Track entity
|
|
22
|
+
* @returns {string} Formatted list line
|
|
23
|
+
*/
|
|
24
|
+
function formatListItem(track) {
|
|
25
|
+
return `${track.id}, ${track.name}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
/**
|
|
20
29
|
* Format track summary output
|
|
21
30
|
* @param {Array} tracks - Raw track entities
|
|
@@ -34,7 +43,7 @@ function formatSummary(tracks, data) {
|
|
|
34
43
|
|
|
35
44
|
console.log(formatTable(["ID", "Name", "Modifiers"], rows));
|
|
36
45
|
console.log(`\nTotal: ${tracks.length} tracks`);
|
|
37
|
-
console.log(`\nRun 'npx pathway track --list' for IDs`);
|
|
46
|
+
console.log(`\nRun 'npx pathway track --list' for IDs and names`);
|
|
38
47
|
console.log(`Run 'npx pathway track <id>' for details\n`);
|
|
39
48
|
}
|
|
40
49
|
|
|
@@ -63,5 +72,6 @@ export const runTrackCommand = createEntityCommand({
|
|
|
63
72
|
sortItems: sortTracksByName,
|
|
64
73
|
formatSummary,
|
|
65
74
|
formatDetail,
|
|
75
|
+
formatListItem,
|
|
66
76
|
emojiIcon: "🛤️",
|
|
67
77
|
});
|
|
@@ -30,7 +30,7 @@ export function createNavButton({ label, href, variant = "primary" }) {
|
|
|
30
30
|
/**
|
|
31
31
|
* Create a button to navigate to job builder with a parameter
|
|
32
32
|
* @param {Object} options - Configuration options
|
|
33
|
-
* @param {string} options.paramName - Parameter name (discipline,
|
|
33
|
+
* @param {string} options.paramName - Parameter name (discipline, level, track)
|
|
34
34
|
* @param {string} options.paramValue - Parameter value (the ID)
|
|
35
35
|
* @param {string} [options.label] - Optional custom label
|
|
36
36
|
* @returns {HTMLElement}
|
|
@@ -38,7 +38,7 @@ export function createNavButton({ label, href, variant = "primary" }) {
|
|
|
38
38
|
export function createJobBuilderButton({ paramName, paramValue, label }) {
|
|
39
39
|
const defaultLabels = {
|
|
40
40
|
discipline: "Build Job with this Discipline →",
|
|
41
|
-
|
|
41
|
+
level: "Build Job at this Level →",
|
|
42
42
|
track: "Build Job with this Track →",
|
|
43
43
|
};
|
|
44
44
|
|
|
@@ -52,7 +52,7 @@ export function createJobBuilderButton({ paramName, paramValue, label }) {
|
|
|
52
52
|
/**
|
|
53
53
|
* Create a button to navigate to interview prep with a parameter
|
|
54
54
|
* @param {Object} options - Configuration options
|
|
55
|
-
* @param {string} options.paramName - Parameter name (discipline,
|
|
55
|
+
* @param {string} options.paramName - Parameter name (discipline, level, track)
|
|
56
56
|
* @param {string} options.paramValue - Parameter value (the ID)
|
|
57
57
|
* @param {string} [options.label] - Optional custom label
|
|
58
58
|
* @returns {HTMLElement}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Builder component for discipline/
|
|
2
|
+
* Builder component for discipline/level/track selection pages
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import {
|
|
@@ -32,7 +32,7 @@ import { createReactive } from "../lib/reactive.js";
|
|
|
32
32
|
/**
|
|
33
33
|
* @typedef {Object} BuilderSelection
|
|
34
34
|
* @property {Object} discipline - Selected discipline
|
|
35
|
-
* @property {Object}
|
|
35
|
+
* @property {Object} level - Selected level
|
|
36
36
|
* @property {Object} track - Selected track
|
|
37
37
|
*/
|
|
38
38
|
|
|
@@ -78,10 +78,10 @@ export function createBuilder({
|
|
|
78
78
|
const selection = createReactive({
|
|
79
79
|
discipline: urlParams.get("discipline") || "",
|
|
80
80
|
track: urlParams.get("track") || "",
|
|
81
|
-
|
|
81
|
+
level: urlParams.get("level") || "",
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
-
const
|
|
84
|
+
const sortedLevels = [...data.levels].sort((a, b) => a.level - b.level);
|
|
85
85
|
|
|
86
86
|
// Create elements that need references
|
|
87
87
|
const previewContainer = div(
|
|
@@ -155,9 +155,9 @@ export function createBuilder({
|
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
// Subscribe to selection changes - all updates happen here
|
|
158
|
-
selection.subscribe(({ discipline, track,
|
|
159
|
-
// Track is now optional - only discipline and
|
|
160
|
-
if (!discipline || !
|
|
158
|
+
selection.subscribe(({ discipline, track, level }) => {
|
|
159
|
+
// Track is now optional - only discipline and level are required
|
|
160
|
+
if (!discipline || !level) {
|
|
161
161
|
previewContainer.innerHTML = "";
|
|
162
162
|
previewContainer.appendChild(
|
|
163
163
|
p({ className: "text-muted" }, emptyPreviewText),
|
|
@@ -168,9 +168,9 @@ export function createBuilder({
|
|
|
168
168
|
|
|
169
169
|
const disciplineObj = data.disciplines.find((d) => d.id === discipline);
|
|
170
170
|
const trackObj = track ? data.tracks.find((t) => t.id === track) : null;
|
|
171
|
-
const
|
|
171
|
+
const levelObj = data.levels.find((g) => g.id === level);
|
|
172
172
|
|
|
173
|
-
if (!disciplineObj || !
|
|
173
|
+
if (!disciplineObj || !levelObj) {
|
|
174
174
|
previewContainer.innerHTML = "";
|
|
175
175
|
previewContainer.appendChild(
|
|
176
176
|
p({ className: "text-muted" }, "Invalid selection. Please try again."),
|
|
@@ -182,7 +182,7 @@ export function createBuilder({
|
|
|
182
182
|
const selectionObj = {
|
|
183
183
|
discipline: disciplineObj,
|
|
184
184
|
track: trackObj,
|
|
185
|
-
|
|
185
|
+
level: levelObj,
|
|
186
186
|
};
|
|
187
187
|
const preview = previewPresenter(selectionObj, data);
|
|
188
188
|
|
|
@@ -193,8 +193,8 @@ export function createBuilder({
|
|
|
193
193
|
|
|
194
194
|
// Wire up button
|
|
195
195
|
actionButton.addEventListener("click", () => {
|
|
196
|
-
const { discipline, track,
|
|
197
|
-
window.location.hash = detailPath({ discipline, track,
|
|
196
|
+
const { discipline, track, level } = selection.get();
|
|
197
|
+
window.location.hash = detailPath({ discipline, track, level });
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
// Build the page
|
|
@@ -234,17 +234,17 @@ export function createBuilder({
|
|
|
234
234
|
getDisplayName: (d) => d.specialization || d.name,
|
|
235
235
|
}),
|
|
236
236
|
),
|
|
237
|
-
//
|
|
237
|
+
// Level selector (second)
|
|
238
238
|
div(
|
|
239
239
|
{ className: "form-group" },
|
|
240
|
-
label({ className: "form-label" }, labels.
|
|
240
|
+
label({ className: "form-label" }, labels.level || "Level"),
|
|
241
241
|
createSelectWithValue({
|
|
242
|
-
id: "
|
|
243
|
-
items:
|
|
244
|
-
initialValue: selection.get().
|
|
245
|
-
placeholder: "Select a
|
|
242
|
+
id: "level-select",
|
|
243
|
+
items: sortedLevels,
|
|
244
|
+
initialValue: selection.get().level,
|
|
245
|
+
placeholder: "Select a level...",
|
|
246
246
|
onChange: (value) => {
|
|
247
|
-
selection.update((prev) => ({ ...prev,
|
|
247
|
+
selection.update((prev) => ({ ...prev, level: value }));
|
|
248
248
|
},
|
|
249
249
|
getDisplayName: (g) => g.id,
|
|
250
250
|
}),
|
|
@@ -284,7 +284,7 @@ export function createBuilder({
|
|
|
284
284
|
|
|
285
285
|
// Trigger initial update if preselected
|
|
286
286
|
const initial = selection.get();
|
|
287
|
-
if (initial.discipline || initial.track || initial.
|
|
287
|
+
if (initial.discipline || initial.track || initial.level) {
|
|
288
288
|
setTimeout(() => selection.set(selection.get()), 0);
|
|
289
289
|
}
|
|
290
290
|
|
|
@@ -370,12 +370,12 @@ export function createProgressPreview(preview, selection) {
|
|
|
370
370
|
);
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
const { discipline,
|
|
373
|
+
const { discipline, level, track } = selection;
|
|
374
374
|
|
|
375
375
|
// Build badges array - track is optional
|
|
376
376
|
const badges = [
|
|
377
377
|
createBadge(discipline.specialization, "discipline"),
|
|
378
|
-
createBadge(
|
|
378
|
+
createBadge(level.id, "level"),
|
|
379
379
|
];
|
|
380
380
|
if (track) {
|
|
381
381
|
badges.push(createBadge(track.name, "track"));
|
|
@@ -394,19 +394,19 @@ export function createProgressPreview(preview, selection) {
|
|
|
394
394
|
div({ className: "preview-label" }, "Progression Paths Available"),
|
|
395
395
|
div(
|
|
396
396
|
{ className: "preview-paths" },
|
|
397
|
-
preview.
|
|
397
|
+
preview.nextLevel
|
|
398
398
|
? div(
|
|
399
399
|
{ className: "path-item" },
|
|
400
400
|
span({ className: "path-icon" }, "📈"),
|
|
401
401
|
span(
|
|
402
402
|
{},
|
|
403
|
-
`Next
|
|
403
|
+
`Next Level: ${preview.nextLevel.id} - ${preview.nextLevel.name}`,
|
|
404
404
|
),
|
|
405
405
|
)
|
|
406
406
|
: div(
|
|
407
407
|
{ className: "path-item text-muted" },
|
|
408
408
|
span({ className: "path-icon" }, "🏆"),
|
|
409
|
-
span({}, "You're at the highest
|
|
409
|
+
span({}, "You're at the highest level!"),
|
|
410
410
|
),
|
|
411
411
|
preview.validTracks.length > 0
|
|
412
412
|
? div(
|
package/src/components/card.js
CHANGED
|
@@ -1,108 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Reusable card component
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from @forwardimpact/libui/components/card.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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 {HTMLElement} [options.icon] - Icon element to display
|
|
17
|
-
* @param {string} [options.className] - Additional CSS class
|
|
18
|
-
* @returns {HTMLElement}
|
|
19
|
-
*/
|
|
20
|
-
export function createCard({
|
|
21
|
-
title,
|
|
22
|
-
description,
|
|
23
|
-
href,
|
|
24
|
-
badges = [],
|
|
25
|
-
meta = [],
|
|
26
|
-
content,
|
|
27
|
-
icon,
|
|
28
|
-
className = "",
|
|
29
|
-
}) {
|
|
30
|
-
const isClickable = !!href;
|
|
31
|
-
|
|
32
|
-
const titleContent = icon
|
|
33
|
-
? div(
|
|
34
|
-
{ className: "card-title-with-icon" },
|
|
35
|
-
icon,
|
|
36
|
-
h3({ className: "card-title" }, title),
|
|
37
|
-
)
|
|
38
|
-
: h3({ className: "card-title" }, title);
|
|
39
|
-
|
|
40
|
-
const cardHeader = div(
|
|
41
|
-
{ className: "card-header" },
|
|
42
|
-
titleContent,
|
|
43
|
-
badges.length > 0 ? div({ className: "card-badges" }, ...badges) : null,
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const card = div(
|
|
47
|
-
{
|
|
48
|
-
className:
|
|
49
|
-
`card ${isClickable ? "card-clickable" : ""} ${className}`.trim(),
|
|
50
|
-
},
|
|
51
|
-
cardHeader,
|
|
52
|
-
description ? p({ className: "card-description" }, description) : null,
|
|
53
|
-
content || null,
|
|
54
|
-
meta.length > 0 ? div({ className: "card-meta" }, ...meta) : null,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
if (isClickable) {
|
|
58
|
-
card.addEventListener("click", () => {
|
|
59
|
-
window.location.hash = href;
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return card;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Create a stat card for the landing page
|
|
68
|
-
* @param {Object} options
|
|
69
|
-
* @param {number|string} options.value - The stat value
|
|
70
|
-
* @param {string} options.label - The stat label
|
|
71
|
-
* @param {string} [options.href] - Optional link
|
|
72
|
-
* @returns {HTMLElement}
|
|
73
|
-
*/
|
|
74
|
-
export function createStatCard({ value, label, href }) {
|
|
75
|
-
const card = div(
|
|
76
|
-
{ className: "stat-card" },
|
|
77
|
-
div({ className: "stat-value" }, String(value)),
|
|
78
|
-
div({ className: "stat-label" }, label),
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
if (href) {
|
|
82
|
-
card.style.cursor = "pointer";
|
|
83
|
-
card.addEventListener("click", () => {
|
|
84
|
-
window.location.hash = href;
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return card;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Create a badge element
|
|
93
|
-
* @param {string} text - Badge text
|
|
94
|
-
* @param {string} [type] - Badge type (default, primary, secondary, broad, technical, ai, etc.)
|
|
95
|
-
* @returns {HTMLElement}
|
|
96
|
-
*/
|
|
97
|
-
export function createBadge(text, type = "default") {
|
|
98
|
-
return span({ className: `badge badge-${type}` }, text);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Create a tag element
|
|
103
|
-
* @param {string} text
|
|
104
|
-
* @returns {HTMLElement}
|
|
105
|
-
*/
|
|
106
|
-
export function createTag(text) {
|
|
107
|
-
return span({ className: "info-tag" }, text);
|
|
108
|
-
}
|
|
7
|
+
export {
|
|
8
|
+
createCard,
|
|
9
|
+
createStatCard,
|
|
10
|
+
createBadge,
|
|
11
|
+
createTag,
|
|
12
|
+
} from "@forwardimpact/libui/components/card";
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
import { ComparisonRadarChart } from "../lib/radar.js";
|
|
10
10
|
import { div, h3 } from "../lib/render.js";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
getSkillProficiencyIndex,
|
|
13
13
|
getBehaviourMaturityIndex,
|
|
14
14
|
formatLevel,
|
|
15
15
|
} from "../lib/render.js";
|
|
16
|
-
import { compareByCapability } from "@forwardimpact/
|
|
16
|
+
import { compareByCapability } from "@forwardimpact/libskill/policies";
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Create a comparison skill radar chart
|
|
@@ -97,7 +97,7 @@ export function createComparisonSkillRadar(
|
|
|
97
97
|
|
|
98
98
|
currentData.push({
|
|
99
99
|
label: skillName,
|
|
100
|
-
value: currentSkill ?
|
|
100
|
+
value: currentSkill ? getSkillProficiencyIndex(currentSkill.level) : 0,
|
|
101
101
|
maxValue: 5,
|
|
102
102
|
description: currentSkill
|
|
103
103
|
? `${formatLevel(currentSkill.type)} - ${formatLevel(currentSkill.level)}`
|
|
@@ -106,7 +106,7 @@ export function createComparisonSkillRadar(
|
|
|
106
106
|
|
|
107
107
|
targetData.push({
|
|
108
108
|
label: skillName,
|
|
109
|
-
value: targetSkill ?
|
|
109
|
+
value: targetSkill ? getSkillProficiencyIndex(targetSkill.level) : 0,
|
|
110
110
|
maxValue: 5,
|
|
111
111
|
description: targetSkill
|
|
112
112
|
? `${formatLevel(targetSkill.type)} - ${formatLevel(targetSkill.level)}`
|