@forwardimpact/pathway 0.21.0 → 0.22.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 (72) hide show
  1. package/README.md +3 -3
  2. package/bin/fit-pathway.js +22 -22
  3. package/package.json +3 -3
  4. package/src/commands/agent.js +7 -7
  5. package/src/commands/index.js +1 -1
  6. package/src/commands/init.js +1 -1
  7. package/src/commands/interview.js +8 -8
  8. package/src/commands/job.js +19 -19
  9. package/src/commands/level.js +60 -0
  10. package/src/commands/progress.js +20 -20
  11. package/src/commands/questions.js +3 -3
  12. package/src/components/action-buttons.js +3 -3
  13. package/src/components/builder.js +25 -25
  14. package/src/components/comparison-radar.js +3 -3
  15. package/src/components/detail.js +2 -2
  16. package/src/components/grid.js +1 -1
  17. package/src/components/radar-chart.js +3 -3
  18. package/src/components/skill-matrix.js +7 -7
  19. package/src/css/pages/landing.css +5 -5
  20. package/src/formatters/index.js +5 -5
  21. package/src/formatters/interview/shared.js +20 -20
  22. package/src/formatters/job/description.js +17 -17
  23. package/src/formatters/job/dom.js +12 -12
  24. package/src/formatters/job/markdown.js +7 -7
  25. package/src/formatters/json-ld.js +24 -24
  26. package/src/formatters/{grade → level}/dom.js +31 -27
  27. package/src/formatters/{grade → level}/markdown.js +19 -28
  28. package/src/formatters/{grade → level}/microdata.js +28 -38
  29. package/src/formatters/level/shared.js +86 -0
  30. package/src/formatters/progress/markdown.js +2 -2
  31. package/src/formatters/progress/shared.js +48 -48
  32. package/src/formatters/questions/markdown.js +8 -6
  33. package/src/formatters/questions/shared.js +7 -7
  34. package/src/formatters/skill/dom.js +4 -4
  35. package/src/formatters/skill/markdown.js +1 -1
  36. package/src/formatters/skill/microdata.js +3 -3
  37. package/src/formatters/skill/shared.js +2 -2
  38. package/src/handout-main.js +12 -12
  39. package/src/index.html +1 -1
  40. package/src/lib/card-mappers.js +16 -16
  41. package/src/lib/cli-command.js +3 -3
  42. package/src/lib/cli-output.js +2 -2
  43. package/src/lib/job-cache.js +11 -11
  44. package/src/lib/render.js +5 -5
  45. package/src/lib/state.js +2 -2
  46. package/src/lib/yaml-loader.js +9 -9
  47. package/src/main.js +10 -10
  48. package/src/pages/agent-builder.js +11 -11
  49. package/src/pages/assessment-results.js +27 -23
  50. package/src/pages/interview-builder.js +6 -6
  51. package/src/pages/interview.js +8 -8
  52. package/src/pages/job-builder.js +6 -6
  53. package/src/pages/job.js +7 -7
  54. package/src/pages/landing.js +8 -8
  55. package/src/pages/level.js +122 -0
  56. package/src/pages/progress-builder.js +8 -8
  57. package/src/pages/progress.js +74 -74
  58. package/src/pages/self-assessment.js +7 -7
  59. package/src/slide-main.js +22 -22
  60. package/src/slides/chapter.js +4 -4
  61. package/src/slides/index.js +10 -10
  62. package/src/slides/interview.js +2 -2
  63. package/src/slides/job.js +3 -3
  64. package/src/slides/level.js +32 -0
  65. package/src/slides/overview.js +9 -9
  66. package/src/slides/progress.js +13 -13
  67. package/src/types.js +1 -1
  68. package/templates/job.template.md +2 -2
  69. package/src/commands/grade.js +0 -60
  70. package/src/formatters/grade/shared.js +0 -86
  71. package/src/pages/grade.js +0 -122
  72. package/src/slides/grade.js +0 -32
package/src/slide-main.js CHANGED
@@ -18,7 +18,7 @@ import { renderSkillSlide } from "./slides/skill.js";
18
18
  import { renderBehaviourSlide } from "./slides/behaviour.js";
19
19
  import { renderDriverSlide } from "./slides/driver.js";
20
20
  import { renderDisciplineSlide } from "./slides/discipline.js";
21
- import { renderGradeSlide } from "./slides/grade.js";
21
+ import { renderLevelSlide } from "./slides/level.js";
22
22
  import { renderTrackSlide } from "./slides/track.js";
23
23
  import { renderJobSlide } from "./slides/job.js";
24
24
  import { renderInterviewSlide } from "./slides/interview.js";
@@ -140,9 +140,9 @@ function setupRoutes() {
140
140
  });
141
141
  });
142
142
 
143
- // Grades
144
- router.on("/grade/:id", (params) => {
145
- renderGradeSlide({ render: renderSlide, data: getState().data, params });
143
+ // Levels
144
+ router.on("/level/:id", (params) => {
145
+ renderLevelSlide({ render: renderSlide, data: getState().data, params });
146
146
  });
147
147
 
148
148
  // Tracks
@@ -150,12 +150,12 @@ function setupRoutes() {
150
150
  renderTrackSlide({ render: renderSlide, data: getState().data, params });
151
151
  });
152
152
 
153
- // Jobs - new format: discipline/grade/track (track optional)
154
- router.on("/job/:discipline/:grade/:track", (params) => {
153
+ // Jobs - new format: discipline/level/track (track optional)
154
+ router.on("/job/:discipline/:level/:track", (params) => {
155
155
  renderJobSlide({ render: renderSlide, data: getState().data, params });
156
156
  });
157
157
 
158
- router.on("/job/:discipline/:grade", (params) => {
158
+ router.on("/job/:discipline/:level", (params) => {
159
159
  renderJobSlide({
160
160
  render: renderSlide,
161
161
  data: getState().data,
@@ -163,8 +163,8 @@ function setupRoutes() {
163
163
  });
164
164
  });
165
165
 
166
- // Interviews - new format: discipline/grade/track (track optional)
167
- router.on("/interview/:discipline/:grade/:track", (params) => {
166
+ // Interviews - new format: discipline/level/track (track optional)
167
+ router.on("/interview/:discipline/:level/:track", (params) => {
168
168
  renderInterviewSlide({
169
169
  render: renderSlide,
170
170
  data: getState().data,
@@ -172,7 +172,7 @@ function setupRoutes() {
172
172
  });
173
173
  });
174
174
 
175
- router.on("/interview/:discipline/:grade", (params) => {
175
+ router.on("/interview/:discipline/:level", (params) => {
176
176
  renderInterviewSlide({
177
177
  render: renderSlide,
178
178
  data: getState().data,
@@ -180,12 +180,12 @@ function setupRoutes() {
180
180
  });
181
181
  });
182
182
 
183
- // Progress - new format: discipline/grade/track (track optional)
184
- router.on("/progress/:discipline/:grade/:track", (params) => {
183
+ // Progress - new format: discipline/level/track (track optional)
184
+ router.on("/progress/:discipline/:level/:track", (params) => {
185
185
  renderProgressSlide({ render: renderSlide, data: getState().data, params });
186
186
  });
187
187
 
188
- router.on("/progress/:discipline/:grade", (params) => {
188
+ router.on("/progress/:discipline/:level", (params) => {
189
189
  renderProgressSlide({
190
190
  render: renderSlide,
191
191
  data: getState().data,
@@ -211,15 +211,15 @@ function buildSlideOrder(data) {
211
211
  data.disciplines.forEach((d) => order.push(`/discipline/${d.id}`));
212
212
  }
213
213
 
214
- // Grades (moved before Tracks)
215
- if (data.grades && data.grades.length > 0) {
214
+ // Levels (moved before Tracks)
215
+ if (data.levels && data.levels.length > 0) {
216
216
  boundaries.push(order.length);
217
- order.push("/chapter/grade");
218
- order.push("/overview/grade");
219
- data.grades.forEach((g) => order.push(`/grade/${g.id}`));
217
+ order.push("/chapter/level");
218
+ order.push("/overview/level");
219
+ data.levels.forEach((g) => order.push(`/level/${g.id}`));
220
220
  }
221
221
 
222
- // Tracks (moved after Grades)
222
+ // Tracks (moved after Levels)
223
223
  if (data.tracks && data.tracks.length > 0) {
224
224
  boundaries.push(order.length);
225
225
  order.push("/chapter/track");
@@ -254,7 +254,7 @@ function buildSlideOrder(data) {
254
254
  // Jobs
255
255
  const jobs = generateAllJobs({
256
256
  disciplines: data.disciplines,
257
- grades: data.grades,
257
+ levels: data.levels,
258
258
  tracks: data.tracks,
259
259
  skills: data.skills,
260
260
  behaviours: data.behaviours,
@@ -267,8 +267,8 @@ function buildSlideOrder(data) {
267
267
  jobs.forEach((job) =>
268
268
  order.push(
269
269
  job.track
270
- ? `/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
271
- : `/job/${job.discipline.id}/${job.grade.id}`,
270
+ ? `/job/${job.discipline.id}/${job.level.id}/${job.track.id}`
271
+ : `/job/${job.discipline.id}/${job.level.id}`,
272
272
  ),
273
273
  );
274
274
  }
@@ -38,10 +38,10 @@ export function renderChapterSlide({ render, data, params }) {
38
38
  emojiIcon: framework.entityDefinitions.discipline.emojiIcon,
39
39
  description: framework.entityDefinitions.discipline.description,
40
40
  },
41
- grade: {
42
- title: framework.entityDefinitions.grade.title,
43
- emojiIcon: framework.entityDefinitions.grade.emojiIcon,
44
- description: framework.entityDefinitions.grade.description,
41
+ level: {
42
+ title: framework.entityDefinitions.level.title,
43
+ emojiIcon: framework.entityDefinitions.level.emojiIcon,
44
+ description: framework.entityDefinitions.level.description,
45
45
  },
46
46
  track: {
47
47
  title: framework.entityDefinitions.track.title,
@@ -55,25 +55,25 @@ export function renderSlideIndex({ render, data }) {
55
55
  ),
56
56
  ),
57
57
 
58
- // Grades
58
+ // Levels
59
59
  div(
60
60
  { className: "slide-section" },
61
61
  a(
62
- { href: "#/overview/grade" },
62
+ { href: "#/overview/level" },
63
63
  heading2(
64
64
  { className: "slide-section-title" },
65
- `${getConceptEmoji(data.framework, "grade")} `,
66
- span({ className: "gradient-text" }, "Grades"),
65
+ `${getConceptEmoji(data.framework, "level")} `,
66
+ span({ className: "gradient-text" }, "Levels"),
67
67
  ),
68
68
  ),
69
69
  ul(
70
70
  { className: "related-list" },
71
- ...data.grades.map((grade) =>
71
+ ...data.levels.map((level) =>
72
72
  li(
73
73
  {},
74
74
  a(
75
- { href: `#/grade/${grade.id}` },
76
- `${grade.id} - ${grade.professionalTitle}`,
75
+ { href: `#/level/${level.id}` },
76
+ `${level.id} - ${level.professionalTitle}`,
77
77
  ),
78
78
  ),
79
79
  ),
@@ -174,7 +174,7 @@ export function renderSlideIndex({ render, data }) {
174
174
  { className: "related-list" },
175
175
  ...generateAllJobs({
176
176
  disciplines: data.disciplines,
177
- grades: data.grades,
177
+ levels: data.levels,
178
178
  tracks: data.tracks,
179
179
  skills: data.skills,
180
180
  behaviours: data.behaviours,
@@ -185,8 +185,8 @@ export function renderSlideIndex({ render, data }) {
185
185
  a(
186
186
  {
187
187
  href: job.track
188
- ? `#/job/${job.discipline.id}/${job.grade.id}/${job.track.id}`
189
- : `#/job/${job.discipline.id}/${job.grade.id}`,
188
+ ? `#/job/${job.discipline.id}/${job.level.id}/${job.track.id}`
189
+ : `#/job/${job.discipline.id}/${job.level.id}`,
190
190
  },
191
191
  job.title,
192
192
  ),
@@ -20,7 +20,7 @@ import { interviewToDOM } from "../formatters/index.js";
20
20
  */
21
21
  export function renderInterviewSlide({ render, data, params }) {
22
22
  const discipline = data.disciplines.find((d) => d.id === params.discipline);
23
- const grade = data.grades.find((g) => g.id === params.grade);
23
+ const level = data.levels.find((g) => g.id === params.level);
24
24
  const track = data.tracks.find((t) => t.id === params.track);
25
25
 
26
26
  // Get interview type from URL query or default to full
@@ -29,7 +29,7 @@ export function renderInterviewSlide({ render, data, params }) {
29
29
 
30
30
  const view = prepareInterviewDetail({
31
31
  discipline,
32
- grade,
32
+ level,
33
33
  track,
34
34
  skills: data.skills,
35
35
  behaviours: data.behaviours,
package/src/slides/job.js CHANGED
@@ -17,12 +17,12 @@ import { jobToDOM } from "../formatters/index.js";
17
17
  */
18
18
  export function renderJobSlide({ render, data, params }) {
19
19
  const discipline = data.disciplines.find((d) => d.id === params.discipline);
20
- const grade = data.grades.find((g) => g.id === params.grade);
20
+ const level = data.levels.find((g) => g.id === params.level);
21
21
  const track = data.tracks.find((t) => t.id === params.track);
22
22
 
23
23
  const view = prepareJobDetail({
24
24
  discipline,
25
- grade,
25
+ level,
26
26
  track,
27
27
  skills: data.skills,
28
28
  behaviours: data.behaviours,
@@ -49,7 +49,7 @@ export function renderJobSlide({ render, data, params }) {
49
49
  showJobDescriptionHtml: true,
50
50
  showJobDescriptionMarkdown: false,
51
51
  discipline,
52
- grade,
52
+ level,
53
53
  track,
54
54
  }),
55
55
  );
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Level Slide View
3
+ *
4
+ * Printer-friendly view of a level.
5
+ */
6
+
7
+ import { div, h1, p } from "../lib/render.js";
8
+ import { levelToDOM } from "../formatters/index.js";
9
+
10
+ /**
11
+ * Render level slide
12
+ * @param {Object} params
13
+ * @param {Function} params.render
14
+ * @param {Object} params.data
15
+ * @param {Object} params.params
16
+ */
17
+ export function renderLevelSlide({ render, data, params }) {
18
+ const level = data.levels.find((g) => g.id === params.id);
19
+
20
+ if (!level) {
21
+ render(
22
+ div(
23
+ { className: "slide-error" },
24
+ h1({}, "Level Not Found"),
25
+ p({}, `No level found with ID: ${params.id}`),
26
+ ),
27
+ );
28
+ return;
29
+ }
30
+
31
+ render(levelToDOM(level, { framework: data.framework, showBackLink: false }));
32
+ }
@@ -12,7 +12,7 @@ import {
12
12
  skillToCardConfig,
13
13
  behaviourToCardConfig,
14
14
  driverToCardConfig,
15
- gradeToCardConfig,
15
+ levelToCardConfig,
16
16
  trackToCardConfig,
17
17
  jobToCardConfig,
18
18
  } from "../lib/card-mappers.js";
@@ -20,7 +20,7 @@ import { prepareDisciplinesList } from "../formatters/discipline/shared.js";
20
20
  import { prepareSkillsList } from "../formatters/skill/shared.js";
21
21
  import { prepareBehavioursList } from "../formatters/behaviour/shared.js";
22
22
  import { prepareDriversList } from "../formatters/driver/shared.js";
23
- import { prepareGradesList } from "../formatters/grade/shared.js";
23
+ import { prepareLevelsList } from "../formatters/level/shared.js";
24
24
  import { prepareTracksList } from "../formatters/track/shared.js";
25
25
  import { generateAllJobs } from "@forwardimpact/libpathway/derivation";
26
26
 
@@ -92,12 +92,12 @@ export function renderOverviewSlide({ render, data, params }) {
92
92
  mapper: disciplineToCardConfig,
93
93
  isGrouped: true,
94
94
  },
95
- grade: {
96
- title: framework.entityDefinitions.grade.title,
97
- emojiIcon: framework.entityDefinitions.grade.emojiIcon,
98
- description: framework.entityDefinitions.grade.description,
99
- entities: prepareGradesList(data.grades).items,
100
- mapper: gradeToCardConfig,
95
+ level: {
96
+ title: framework.entityDefinitions.level.title,
97
+ emojiIcon: framework.entityDefinitions.level.emojiIcon,
98
+ description: framework.entityDefinitions.level.description,
99
+ entities: prepareLevelsList(data.levels).items,
100
+ mapper: levelToCardConfig,
101
101
  },
102
102
  track: {
103
103
  title: framework.entityDefinitions.track.title,
@@ -112,7 +112,7 @@ export function renderOverviewSlide({ render, data, params }) {
112
112
  description: framework.entityDefinitions.job.description,
113
113
  entities: generateAllJobs({
114
114
  disciplines: data.disciplines,
115
- grades: data.grades,
115
+ levels: data.levels,
116
116
  tracks: data.tracks,
117
117
  skills: data.skills,
118
118
  behaviours: data.behaviours,
@@ -7,7 +7,7 @@
7
7
  import { div, h1, p } from "../lib/render.js";
8
8
  import {
9
9
  prepareProgressDetail,
10
- getDefaultTargetGrade,
10
+ getDefaultTargetLevel,
11
11
  } from "../formatters/progress/shared.js";
12
12
  import { progressToDOM } from "../formatters/index.js";
13
13
 
@@ -20,12 +20,12 @@ import { progressToDOM } from "../formatters/index.js";
20
20
  */
21
21
  export function renderProgressSlide({ render, data, params }) {
22
22
  const discipline = data.disciplines.find((d) => d.id === params.discipline);
23
- const grade = data.grades.find((g) => g.id === params.grade);
23
+ const level = data.levels.find((g) => g.id === params.level);
24
24
  const track = params.track
25
25
  ? data.tracks.find((t) => t.id === params.track)
26
26
  : null;
27
27
 
28
- if (!discipline || !grade) {
28
+ if (!discipline || !level) {
29
29
  render(
30
30
  div(
31
31
  { className: "slide-error" },
@@ -36,23 +36,23 @@ export function renderProgressSlide({ render, data, params }) {
36
36
  return;
37
37
  }
38
38
 
39
- // Get compare grade from URL query or default to next grade
39
+ // Get compare level from URL query or default to next level
40
40
  const urlParams = new URLSearchParams(window.location.search);
41
- const compareGradeId = urlParams.get("compare");
41
+ const compareLevelId = urlParams.get("compare");
42
42
 
43
- let targetGrade;
44
- if (compareGradeId) {
45
- targetGrade = data.grades.find((g) => g.id === compareGradeId);
43
+ let targetLevel;
44
+ if (compareLevelId) {
45
+ targetLevel = data.levels.find((g) => g.id === compareLevelId);
46
46
  } else {
47
- targetGrade = getDefaultTargetGrade(grade, data.grades);
47
+ targetLevel = getDefaultTargetLevel(level, data.levels);
48
48
  }
49
49
 
50
- if (!targetGrade) {
50
+ if (!targetLevel) {
51
51
  render(
52
52
  div(
53
53
  { className: "slide-error" },
54
54
  h1({}, "No Progression Available"),
55
- p({}, "No next grade available for this role."),
55
+ p({}, "No next level available for this role."),
56
56
  ),
57
57
  );
58
58
  return;
@@ -60,10 +60,10 @@ export function renderProgressSlide({ render, data, params }) {
60
60
 
61
61
  const view = prepareProgressDetail({
62
62
  fromDiscipline: discipline,
63
- fromGrade: grade,
63
+ fromLevel: level,
64
64
  fromTrack: track,
65
65
  toDiscipline: discipline,
66
- toGrade: targetGrade,
66
+ toLevel: targetLevel,
67
67
  toTrack: track,
68
68
  skills: data.skills,
69
69
  behaviours: data.behaviours,
package/src/types.js CHANGED
@@ -19,7 +19,7 @@
19
19
  * @property {boolean} [humanOnly] - Whether this skill requires human presence
20
20
  * @property {'primary'|'secondary'|'tertiary'} type - Skill type in this role
21
21
  * @property {string} level - Level ID (e.g., "advanced", "expert")
22
- * @property {string} levelDescription - Human-readable level description
22
+ * @property {string} proficiencyDescription - Human-readable level description
23
23
  */
24
24
 
25
25
  // ============================================================================
@@ -1,6 +1,6 @@
1
1
  # {{{title}}}
2
2
 
3
- - **Level:** {{{gradeId}}}
3
+ - **Level:** {{{levelId}}}
4
4
  - **Experience:** {{{typicalExperienceRange}}}
5
5
  {{#hasTrack}}- **Track:** {{{trackName}}}
6
6
  {{/hasTrack}}
@@ -34,7 +34,7 @@
34
34
  {{{responsibilityDescription}}}:
35
35
 
36
36
  {{#skills}}
37
- - **{{{skillName}}}:** {{{levelDescription}}}
37
+ - **{{{skillName}}}:** {{{proficiencyDescription}}}
38
38
  {{/skills}}
39
39
  {{/capabilitySkills}}
40
40
  {{/hasCapabilitySkills}}
@@ -1,60 +0,0 @@
1
- /**
2
- * Grade CLI Command
3
- *
4
- * Handles grade summary, listing, and detail display in the terminal.
5
- *
6
- * Usage:
7
- * npx pathway grade # Summary with stats
8
- * npx pathway grade --list # IDs only (for piping)
9
- * npx pathway grade <id> # Detail view
10
- * npx pathway grade --validate # Validation checks
11
- */
12
-
13
- import { createEntityCommand } from "./command-factory.js";
14
- import { gradeToMarkdown } from "../formatters/grade/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 grade summary output
21
- * @param {Array} grades - Raw grade entities
22
- * @param {Object} data - Full data context
23
- */
24
- function formatSummary(grades, data) {
25
- const { framework } = data;
26
- const emoji = framework ? getConceptEmoji(framework, "grade") : "📊";
27
-
28
- console.log(`\n${emoji} Grades\n`);
29
-
30
- const rows = grades.map((g) => [
31
- g.id,
32
- g.displayName || g.id,
33
- g.typicalExperienceRange || "-",
34
- capitalize(g.baseSkillLevels?.primary || "-"),
35
- ]);
36
-
37
- console.log(formatTable(["ID", "Name", "Experience", "Primary Level"], rows));
38
- console.log(`\nTotal: ${grades.length} grades`);
39
- console.log(`\nRun 'npx pathway grade --list' for IDs`);
40
- console.log(`Run 'npx pathway grade <id>' for details\n`);
41
- }
42
-
43
- /**
44
- * Format grade detail output
45
- * @param {Object} grade - Raw grade entity
46
- * @param {Object} framework - Framework config
47
- */
48
- function formatDetail(grade, framework) {
49
- console.log(gradeToMarkdown(grade, framework));
50
- }
51
-
52
- export const runGradeCommand = createEntityCommand({
53
- entityName: "grade",
54
- pluralName: "grades",
55
- findEntity: (data, id) => data.grades.find((g) => g.id === id),
56
- presentDetail: (entity) => entity,
57
- formatSummary,
58
- formatDetail,
59
- emojiIcon: "📊",
60
- });
@@ -1,86 +0,0 @@
1
- /**
2
- * Grade presentation helpers
3
- *
4
- * Shared utilities for formatting grade data across DOM and markdown outputs.
5
- */
6
-
7
- /**
8
- * Get grade display name (shows both professional and management titles)
9
- * @param {Object} grade
10
- * @returns {string}
11
- */
12
- export function getGradeDisplayName(grade) {
13
- if (grade.professionalTitle && grade.managementTitle) {
14
- if (grade.professionalTitle === grade.managementTitle) {
15
- return grade.professionalTitle;
16
- }
17
- return `${grade.professionalTitle} / ${grade.managementTitle}`;
18
- }
19
- return grade.professionalTitle || grade.managementTitle || grade.id;
20
- }
21
-
22
- /**
23
- * @typedef {Object} GradeListItem
24
- * @property {string} id
25
- * @property {string} displayName
26
- * @property {number} ordinalRank
27
- * @property {string|null} typicalExperienceRange
28
- * @property {Object} baseSkillLevels
29
- * @property {string|null} impactScope
30
- */
31
-
32
- /**
33
- * Transform grades for list view
34
- * @param {Array} grades - Raw grade entities
35
- * @returns {{ items: GradeListItem[] }}
36
- */
37
- export function prepareGradesList(grades) {
38
- const sortedGrades = [...grades].sort(
39
- (a, b) => a.ordinalRank - b.ordinalRank,
40
- );
41
-
42
- const items = sortedGrades.map((grade) => ({
43
- id: grade.id,
44
- displayName: getGradeDisplayName(grade),
45
- ordinalRank: grade.ordinalRank,
46
- typicalExperienceRange: grade.typicalExperienceRange || null,
47
- baseSkillLevels: grade.baseSkillLevels || {},
48
- impactScope: grade.expectations?.impactScope || null,
49
- }));
50
-
51
- return { items };
52
- }
53
-
54
- /**
55
- * @typedef {Object} GradeDetailView
56
- * @property {string} id
57
- * @property {string} displayName
58
- * @property {string} professionalTitle
59
- * @property {string} managementTitle
60
- * @property {number} ordinalRank
61
- * @property {string|null} typicalExperienceRange
62
- * @property {Object} baseSkillLevels
63
- * @property {Object} baseBehaviourMaturity
64
- * @property {Object} expectations
65
- */
66
-
67
- /**
68
- * Transform grade for detail view
69
- * @param {Object} grade - Raw grade entity
70
- * @returns {GradeDetailView|null}
71
- */
72
- export function prepareGradeDetail(grade) {
73
- if (!grade) return null;
74
-
75
- return {
76
- id: grade.id,
77
- displayName: getGradeDisplayName(grade),
78
- professionalTitle: grade.professionalTitle || null,
79
- managementTitle: grade.managementTitle || null,
80
- ordinalRank: grade.ordinalRank,
81
- typicalExperienceRange: grade.typicalExperienceRange || null,
82
- baseSkillLevels: grade.baseSkillLevels || {},
83
- baseBehaviourMaturity: grade.baseBehaviourMaturity || {},
84
- expectations: grade.expectations || {},
85
- };
86
- }
@@ -1,122 +0,0 @@
1
- /**
2
- * Grades pages
3
- */
4
-
5
- import { render, div, h1, h3, p, formatLevel } from "../lib/render.js";
6
- import { getState } from "../lib/state.js";
7
- import { createBadge } from "../components/card.js";
8
- import { renderNotFound } from "../components/error-page.js";
9
- import { prepareGradesList } from "../formatters/grade/shared.js";
10
- import { gradeToDOM } from "../formatters/grade/dom.js";
11
- import { getConceptEmoji } from "@forwardimpact/map/levels";
12
-
13
- /**
14
- * Render grades list page
15
- */
16
- export function renderGradesList() {
17
- const { data } = getState();
18
- const { framework } = data;
19
- const gradeEmoji = getConceptEmoji(framework, "grade");
20
-
21
- // Transform data for list view
22
- const { items } = prepareGradesList(data.grades);
23
-
24
- const page = div(
25
- { className: "grades-page" },
26
- // Header
27
- div(
28
- { className: "page-header" },
29
- h1(
30
- { className: "page-title" },
31
- `${gradeEmoji} ${framework.entityDefinitions.grade.title}`,
32
- ),
33
- p(
34
- { className: "page-description" },
35
- framework.entityDefinitions.grade.description.trim(),
36
- ),
37
- ),
38
-
39
- // Grades timeline
40
- div(
41
- { className: "grades-timeline" },
42
- ...items.map((grade) => createGradeTimelineItem(grade)),
43
- ),
44
- );
45
-
46
- render(page);
47
- }
48
-
49
- /**
50
- * Create a grade timeline item
51
- * @param {Object} grade
52
- * @returns {HTMLElement}
53
- */
54
- function createGradeTimelineItem(grade) {
55
- const item = div(
56
- { className: "grade-timeline-item" },
57
- div({ className: "grade-level-marker" }, String(grade.ordinalRank)),
58
- div(
59
- { className: "grade-timeline-content card card-clickable" },
60
- div(
61
- { className: "card-header" },
62
- h3({ className: "card-title" }, grade.displayName),
63
- createBadge(grade.id, "default"),
64
- ),
65
- grade.typicalExperienceRange
66
- ? p(
67
- { className: "text-muted", style: "margin: 0.25rem 0" },
68
- `${grade.typicalExperienceRange} experience`,
69
- )
70
- : null,
71
- div(
72
- { className: "card-meta", style: "margin-top: 0.5rem" },
73
- createBadge(
74
- `Primary: ${formatLevel(grade.baseSkillLevels?.primary)}`,
75
- "primary",
76
- ),
77
- createBadge(
78
- `Secondary: ${formatLevel(grade.baseSkillLevels?.secondary)}`,
79
- "secondary",
80
- ),
81
- createBadge(
82
- `Broad: ${formatLevel(grade.baseSkillLevels?.broad)}`,
83
- "broad",
84
- ),
85
- ),
86
- grade.scope
87
- ? p(
88
- { className: "card-description", style: "margin-top: 0.75rem" },
89
- `Scope: ${grade.scope}`,
90
- )
91
- : null,
92
- ),
93
- );
94
-
95
- item.querySelector(".card").addEventListener("click", () => {
96
- window.location.hash = `/grade/${grade.id}`;
97
- });
98
-
99
- return item;
100
- }
101
-
102
- /**
103
- * Render grade detail page
104
- * @param {Object} params - Route params
105
- */
106
- export function renderGradeDetail(params) {
107
- const { data } = getState();
108
- const grade = data.grades.find((g) => g.id === params.id);
109
-
110
- if (!grade) {
111
- renderNotFound({
112
- entityType: "Grade",
113
- entityId: params.id,
114
- backPath: "/grade",
115
- backText: "← Back to Grades",
116
- });
117
- return;
118
- }
119
-
120
- // Use DOM formatter - it handles transformation internally
121
- render(gradeToDOM(grade, { framework: data.framework }));
122
- }