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