@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,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive state utilities for local component state
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @template T
|
|
7
|
+
* @typedef {Object} Reactive
|
|
8
|
+
* @property {() => T} get - Get current value
|
|
9
|
+
* @property {(value: T) => void} set - Set new value
|
|
10
|
+
* @property {(fn: (prev: T) => T) => void} update - Update value with function
|
|
11
|
+
* @property {(fn: (value: T) => void) => () => void} subscribe - Subscribe to changes
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a reactive state container
|
|
16
|
+
* @template T
|
|
17
|
+
* @param {T} initial - Initial value
|
|
18
|
+
* @returns {Reactive<T>}
|
|
19
|
+
*/
|
|
20
|
+
export function createReactive(initial) {
|
|
21
|
+
let state = initial;
|
|
22
|
+
const subscribers = new Set();
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
get: () => state,
|
|
26
|
+
|
|
27
|
+
set: (next) => {
|
|
28
|
+
state = next;
|
|
29
|
+
subscribers.forEach((fn) => fn(state));
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
update: (fn) => {
|
|
33
|
+
state = fn(state);
|
|
34
|
+
subscribers.forEach((sub) => sub(state));
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
subscribe: (fn) => {
|
|
38
|
+
subscribers.add(fn);
|
|
39
|
+
return () => subscribers.delete(fn);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a computed value that updates when dependencies change
|
|
46
|
+
* @template T
|
|
47
|
+
* @param {() => T} compute - Computation function
|
|
48
|
+
* @param {Reactive<*>[]} deps - Dependencies
|
|
49
|
+
* @returns {Reactive<T>}
|
|
50
|
+
*/
|
|
51
|
+
export function createComputed(compute, deps) {
|
|
52
|
+
const computed = createReactive(compute());
|
|
53
|
+
|
|
54
|
+
deps.forEach((dep) => {
|
|
55
|
+
dep.subscribe(() => {
|
|
56
|
+
computed.set(compute());
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return computed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Bind a reactive value to an element attribute
|
|
65
|
+
* @template T
|
|
66
|
+
* @param {Reactive<T>} reactive - Reactive state
|
|
67
|
+
* @param {HTMLElement} element - Element to bind
|
|
68
|
+
* @param {string} attribute - Attribute name
|
|
69
|
+
* @param {(value: T) => *} [transform] - Transform function
|
|
70
|
+
*/
|
|
71
|
+
export function bind(reactive, element, attribute, transform = (v) => v) {
|
|
72
|
+
const update = (value) => {
|
|
73
|
+
element[attribute] = transform(value);
|
|
74
|
+
};
|
|
75
|
+
update(reactive.get());
|
|
76
|
+
reactive.subscribe(update);
|
|
77
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM rendering utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
SKILL_LEVEL_ORDER,
|
|
7
|
+
BEHAVIOUR_MATURITY_ORDER,
|
|
8
|
+
} from "../model/levels.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the main content container
|
|
12
|
+
* @returns {HTMLElement}
|
|
13
|
+
*/
|
|
14
|
+
export function getContainer() {
|
|
15
|
+
return document.getElementById("app-content");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Clear and render content to the main container
|
|
20
|
+
* @param {HTMLElement|string} content
|
|
21
|
+
*/
|
|
22
|
+
export function render(content) {
|
|
23
|
+
const container = getContainer();
|
|
24
|
+
container.innerHTML = "";
|
|
25
|
+
|
|
26
|
+
if (typeof content === "string") {
|
|
27
|
+
container.innerHTML = content;
|
|
28
|
+
} else if (content instanceof HTMLElement) {
|
|
29
|
+
container.appendChild(content);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create an element with attributes and children
|
|
35
|
+
* @param {string} tag - HTML tag name
|
|
36
|
+
* @param {Object} [attrs] - Attributes and properties
|
|
37
|
+
* @param {...(HTMLElement|string)} children - Child elements or text
|
|
38
|
+
* @returns {HTMLElement}
|
|
39
|
+
*/
|
|
40
|
+
export function createElement(tag, attrs = {}, ...children) {
|
|
41
|
+
const element = document.createElement(tag);
|
|
42
|
+
|
|
43
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
44
|
+
if (key === "className") {
|
|
45
|
+
element.className = value;
|
|
46
|
+
} else if (key === "style" && typeof value === "object") {
|
|
47
|
+
Object.assign(element.style, value);
|
|
48
|
+
} else if (key.startsWith("on") && typeof value === "function") {
|
|
49
|
+
element.addEventListener(key.slice(2).toLowerCase(), value);
|
|
50
|
+
} else if (key === "dataset" && typeof value === "object") {
|
|
51
|
+
Object.assign(element.dataset, value);
|
|
52
|
+
} else if (typeof value === "boolean") {
|
|
53
|
+
// Handle boolean attributes - only set if true, skip if false
|
|
54
|
+
if (value) {
|
|
55
|
+
element.setAttribute(key, "");
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
element.setAttribute(key, value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const child of children) {
|
|
63
|
+
if (child == null || child === false) continue;
|
|
64
|
+
if (typeof child === "string" || typeof child === "number") {
|
|
65
|
+
element.appendChild(document.createTextNode(String(child)));
|
|
66
|
+
} else if (child instanceof HTMLElement) {
|
|
67
|
+
element.appendChild(child);
|
|
68
|
+
} else if (Array.isArray(child)) {
|
|
69
|
+
child.forEach((c) => {
|
|
70
|
+
if (c == null || c === false) return;
|
|
71
|
+
if (c instanceof HTMLElement) {
|
|
72
|
+
element.appendChild(c);
|
|
73
|
+
} else if (typeof c === "string" || typeof c === "number") {
|
|
74
|
+
element.appendChild(document.createTextNode(String(c)));
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return element;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Shorthand element creators
|
|
84
|
+
export const div = (attrs, ...children) =>
|
|
85
|
+
createElement("div", attrs, ...children);
|
|
86
|
+
export const span = (attrs, ...children) =>
|
|
87
|
+
createElement("span", attrs, ...children);
|
|
88
|
+
export const h1 = (attrs, ...children) =>
|
|
89
|
+
createElement("h1", attrs, ...children);
|
|
90
|
+
export const h2 = (attrs, ...children) =>
|
|
91
|
+
createElement("h2", attrs, ...children);
|
|
92
|
+
export const h3 = (attrs, ...children) =>
|
|
93
|
+
createElement("h3", attrs, ...children);
|
|
94
|
+
export const h4 = (attrs, ...children) =>
|
|
95
|
+
createElement("h4", attrs, ...children);
|
|
96
|
+
export const p = (attrs, ...children) => createElement("p", attrs, ...children);
|
|
97
|
+
export const a = (attrs, ...children) => createElement("a", attrs, ...children);
|
|
98
|
+
export const ul = (attrs, ...children) =>
|
|
99
|
+
createElement("ul", attrs, ...children);
|
|
100
|
+
export const li = (attrs, ...children) =>
|
|
101
|
+
createElement("li", attrs, ...children);
|
|
102
|
+
export const table = (attrs, ...children) =>
|
|
103
|
+
createElement("table", attrs, ...children);
|
|
104
|
+
export const thead = (attrs, ...children) =>
|
|
105
|
+
createElement("thead", attrs, ...children);
|
|
106
|
+
export const tbody = (attrs, ...children) =>
|
|
107
|
+
createElement("tbody", attrs, ...children);
|
|
108
|
+
export const tr = (attrs, ...children) =>
|
|
109
|
+
createElement("tr", attrs, ...children);
|
|
110
|
+
export const th = (attrs, ...children) =>
|
|
111
|
+
createElement("th", attrs, ...children);
|
|
112
|
+
export const td = (attrs, ...children) =>
|
|
113
|
+
createElement("td", attrs, ...children);
|
|
114
|
+
export const button = (attrs, ...children) =>
|
|
115
|
+
createElement("button", attrs, ...children);
|
|
116
|
+
export const input = (attrs) => createElement("input", attrs);
|
|
117
|
+
export const select = (attrs, ...children) =>
|
|
118
|
+
createElement("select", attrs, ...children);
|
|
119
|
+
export const option = (attrs, ...children) =>
|
|
120
|
+
createElement("option", attrs, ...children);
|
|
121
|
+
export const label = (attrs, ...children) =>
|
|
122
|
+
createElement("label", attrs, ...children);
|
|
123
|
+
export const form = (attrs, ...children) =>
|
|
124
|
+
createElement("form", attrs, ...children);
|
|
125
|
+
export const section = (attrs, ...children) =>
|
|
126
|
+
createElement("section", attrs, ...children);
|
|
127
|
+
export const article = (attrs, ...children) =>
|
|
128
|
+
createElement("article", attrs, ...children);
|
|
129
|
+
export const header = (attrs, ...children) =>
|
|
130
|
+
createElement("header", attrs, ...children);
|
|
131
|
+
export const footer = (attrs, ...children) =>
|
|
132
|
+
createElement("footer", attrs, ...children);
|
|
133
|
+
export const nav = (attrs, ...children) =>
|
|
134
|
+
createElement("nav", attrs, ...children);
|
|
135
|
+
export const main = (attrs, ...children) =>
|
|
136
|
+
createElement("main", attrs, ...children);
|
|
137
|
+
export const details = (attrs, ...children) =>
|
|
138
|
+
createElement("details", attrs, ...children);
|
|
139
|
+
export const summary = (attrs, ...children) =>
|
|
140
|
+
createElement("summary", attrs, ...children);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Semantic heading aliases that match heading levels
|
|
144
|
+
* Use these instead of h1/h2/h3 for clarity in slides and formatters
|
|
145
|
+
*/
|
|
146
|
+
export const heading1 = h1;
|
|
147
|
+
export const heading2 = h2;
|
|
148
|
+
export const heading3 = h3;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a fragment from multiple elements
|
|
152
|
+
* @param {...HTMLElement} children
|
|
153
|
+
* @returns {DocumentFragment}
|
|
154
|
+
*/
|
|
155
|
+
export function fragment(...children) {
|
|
156
|
+
const frag = document.createDocumentFragment();
|
|
157
|
+
children.forEach((child) => {
|
|
158
|
+
if (child instanceof HTMLElement) {
|
|
159
|
+
frag.appendChild(child);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return frag;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Show a loading state
|
|
167
|
+
*/
|
|
168
|
+
export function showLoading() {
|
|
169
|
+
render(
|
|
170
|
+
div(
|
|
171
|
+
{ className: "loading" },
|
|
172
|
+
div({ className: "loading-spinner" }),
|
|
173
|
+
p({}, "Loading..."),
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Show an error message
|
|
180
|
+
* @param {string} message
|
|
181
|
+
*/
|
|
182
|
+
export function showError(message) {
|
|
183
|
+
render(div({ className: "error-message" }, h2({}, "Error"), p({}, message)));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Format a skill level or behaviour maturity for display
|
|
188
|
+
* @param {string} value - The level/maturity value
|
|
189
|
+
* @returns {string}
|
|
190
|
+
*/
|
|
191
|
+
export function formatLevel(value) {
|
|
192
|
+
if (!value) return "";
|
|
193
|
+
return value.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get the index for a skill level (1-5)
|
|
198
|
+
* @param {string} level
|
|
199
|
+
* @returns {number}
|
|
200
|
+
*/
|
|
201
|
+
export function getSkillLevelIndex(level) {
|
|
202
|
+
return SKILL_LEVEL_ORDER.indexOf(level) + 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get the index for a behaviour maturity (1-5)
|
|
207
|
+
* @param {string} maturity
|
|
208
|
+
* @returns {number}
|
|
209
|
+
*/
|
|
210
|
+
export function getBehaviourMaturityIndex(maturity) {
|
|
211
|
+
return BEHAVIOUR_MATURITY_ORDER.indexOf(maturity) + 1;
|
|
212
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Router Factory
|
|
3
|
+
*
|
|
4
|
+
* Pure factory function for hash-based routing with no dependencies.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { withErrorBoundary } from "./error-boundary.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} Route
|
|
11
|
+
* @property {string} pattern - Route pattern with :params
|
|
12
|
+
* @property {RegExp} regex - Compiled pattern
|
|
13
|
+
* @property {string[]} paramNames - Extracted param names
|
|
14
|
+
* @property {Function} handler - Route handler
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} RouteMatch
|
|
19
|
+
* @property {Function} handler - Matched handler
|
|
20
|
+
* @property {Object} params - Extracted parameters
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} Router
|
|
25
|
+
* @property {(pattern: string, handler: Function) => void} on - Register route
|
|
26
|
+
* @property {(path: string) => void} navigate - Navigate to path
|
|
27
|
+
* @property {() => string} currentPath - Get current hash path
|
|
28
|
+
* @property {() => void} handleRoute - Process current route
|
|
29
|
+
* @property {() => void} start - Begin listening for hash changes
|
|
30
|
+
* @property {() => void} stop - Stop listening for hash changes
|
|
31
|
+
* @property {() => string[]} patterns - Get registered patterns
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse route pattern into regex and param names
|
|
36
|
+
* @param {string} pattern
|
|
37
|
+
* @returns {{ regex: RegExp, paramNames: string[] }}
|
|
38
|
+
*/
|
|
39
|
+
function parsePattern(pattern) {
|
|
40
|
+
const paramNames = [];
|
|
41
|
+
const regexStr = pattern
|
|
42
|
+
.replace(/:([^/]+)/g, (_, name) => {
|
|
43
|
+
paramNames.push(name);
|
|
44
|
+
return "([^/]+)";
|
|
45
|
+
})
|
|
46
|
+
.replace(/\//g, "\\/");
|
|
47
|
+
return {
|
|
48
|
+
regex: new RegExp(`^${regexStr}$`),
|
|
49
|
+
paramNames,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a router instance
|
|
55
|
+
* @param {{ onNotFound?: (path: string) => void, onError?: (error: Error) => void, renderError?: (title: string, message: string) => void }} options
|
|
56
|
+
* @returns {Router}
|
|
57
|
+
*/
|
|
58
|
+
export function createRouter(options = {}) {
|
|
59
|
+
const { onNotFound = () => {}, onError, renderError } = options;
|
|
60
|
+
/** @type {Route[]} */
|
|
61
|
+
const routes = [];
|
|
62
|
+
let hashChangeHandler = null;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Match a path to a route
|
|
66
|
+
* @param {string} path
|
|
67
|
+
* @returns {RouteMatch|null}
|
|
68
|
+
*/
|
|
69
|
+
function matchRoute(path) {
|
|
70
|
+
for (const route of routes) {
|
|
71
|
+
const match = path.match(route.regex);
|
|
72
|
+
if (match) {
|
|
73
|
+
const params = {};
|
|
74
|
+
route.paramNames.forEach((name, i) => {
|
|
75
|
+
params[name] = decodeURIComponent(match[i + 1]);
|
|
76
|
+
});
|
|
77
|
+
return { handler: route.handler, params };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get current path from hash (including query string)
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
function currentPath() {
|
|
88
|
+
return window.location.hash.slice(1) || "/";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Handle the current route
|
|
93
|
+
*/
|
|
94
|
+
function handleRoute() {
|
|
95
|
+
const fullPath = currentPath();
|
|
96
|
+
const path = fullPath.split("?")[0];
|
|
97
|
+
const matched = matchRoute(path);
|
|
98
|
+
if (matched) {
|
|
99
|
+
matched.handler(matched.params);
|
|
100
|
+
} else {
|
|
101
|
+
onNotFound(path);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
/**
|
|
107
|
+
* Register a route
|
|
108
|
+
* @param {string} pattern - Route pattern (e.g., '/skills/:id')
|
|
109
|
+
* @param {Function} handler - Handler function
|
|
110
|
+
*/
|
|
111
|
+
on(pattern, handler) {
|
|
112
|
+
const { regex, paramNames } = parsePattern(pattern);
|
|
113
|
+
const wrappedHandler = withErrorBoundary(handler, {
|
|
114
|
+
onError,
|
|
115
|
+
backPath: "/",
|
|
116
|
+
backText: "← Back to Home",
|
|
117
|
+
renderErrorFn: renderError,
|
|
118
|
+
});
|
|
119
|
+
routes.push({ pattern, regex, paramNames, handler: wrappedHandler });
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Navigate to a path
|
|
124
|
+
* @param {string} path
|
|
125
|
+
*/
|
|
126
|
+
navigate(path) {
|
|
127
|
+
window.location.hash = path;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
currentPath,
|
|
131
|
+
handleRoute,
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Start listening for hash changes
|
|
135
|
+
*/
|
|
136
|
+
start() {
|
|
137
|
+
hashChangeHandler = () => handleRoute();
|
|
138
|
+
window.addEventListener("hashchange", hashChangeHandler);
|
|
139
|
+
handleRoute();
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Stop listening for hash changes
|
|
144
|
+
*/
|
|
145
|
+
stop() {
|
|
146
|
+
if (hashChangeHandler) {
|
|
147
|
+
window.removeEventListener("hashchange", hashChangeHandler);
|
|
148
|
+
hashChangeHandler = null;
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get all registered route patterns
|
|
154
|
+
* @returns {string[]}
|
|
155
|
+
*/
|
|
156
|
+
patterns() {
|
|
157
|
+
return routes.map((r) => r.pattern);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pages Router
|
|
3
|
+
*
|
|
4
|
+
* Router instance for the main app pages.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRouter } from "./router-core.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create the pages router for the main app
|
|
11
|
+
* @param {{ onNotFound?: (path: string) => void }} options
|
|
12
|
+
* @returns {import('./router-core.js').Router}
|
|
13
|
+
*/
|
|
14
|
+
export function createPagesRouter(options = {}) {
|
|
15
|
+
return createRouter(options);
|
|
16
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slide Router
|
|
3
|
+
*
|
|
4
|
+
* Extended router with navigation state and keyboard controls for slides.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createRouter } from "./router-core.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} SlideRouter
|
|
11
|
+
* @property {(pattern: string, handler: Function) => void} on - Register route
|
|
12
|
+
* @property {(path: string) => void} navigate - Navigate to path
|
|
13
|
+
* @property {() => string} currentPath - Get current hash path
|
|
14
|
+
* @property {() => void} handleRoute - Process current route
|
|
15
|
+
* @property {() => void} start - Begin listening for hash changes
|
|
16
|
+
* @property {() => void} stop - Stop listening for hash changes
|
|
17
|
+
* @property {() => string[]} patterns - Get registered patterns
|
|
18
|
+
* @property {(paths: string[]) => void} setSlideOrder - Define navigation order
|
|
19
|
+
* @property {() => void} next - Navigate to next slide
|
|
20
|
+
* @property {() => void} prev - Navigate to previous slide
|
|
21
|
+
* @property {() => void} home - Navigate to index
|
|
22
|
+
* @property {() => number} currentIndex - Current position in order
|
|
23
|
+
* @property {() => number} totalSlides - Total slide count
|
|
24
|
+
* @property {() => void} startKeyboardNav - Enable keyboard shortcuts
|
|
25
|
+
* @property {() => void} stopKeyboardNav - Disable keyboard shortcuts
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a slide router with navigation capabilities
|
|
30
|
+
* @param {{ onNotFound?: (path: string) => void, renderError?: (title: string, message: string) => void }} options
|
|
31
|
+
* @returns {SlideRouter}
|
|
32
|
+
*/
|
|
33
|
+
export function createSlideRouter(options = {}) {
|
|
34
|
+
const router = createRouter(options);
|
|
35
|
+
let slideOrder = [];
|
|
36
|
+
let chapterBoundaries = [];
|
|
37
|
+
let keyHandler = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Find current position in slide order
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
function findCurrentIndex() {
|
|
44
|
+
const path = router.currentPath();
|
|
45
|
+
return slideOrder.indexOf(path);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Navigate to slide at given index
|
|
50
|
+
* @param {number} index
|
|
51
|
+
*/
|
|
52
|
+
function navigateToIndex(index) {
|
|
53
|
+
if (index >= 0 && index < slideOrder.length) {
|
|
54
|
+
router.navigate(slideOrder[index]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const slideRouter = {
|
|
59
|
+
// Expose core router methods
|
|
60
|
+
on: router.on,
|
|
61
|
+
navigate: router.navigate,
|
|
62
|
+
currentPath: router.currentPath,
|
|
63
|
+
handleRoute: router.handleRoute,
|
|
64
|
+
start: router.start,
|
|
65
|
+
stop: router.stop,
|
|
66
|
+
patterns: router.patterns,
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Define the slide navigation order
|
|
70
|
+
* @param {string[]} paths
|
|
71
|
+
* @param {number[]} boundaries - Indices where chapters start
|
|
72
|
+
*/
|
|
73
|
+
setSlideOrder(paths, boundaries = []) {
|
|
74
|
+
slideOrder = paths;
|
|
75
|
+
chapterBoundaries = boundaries;
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Navigate to next slide
|
|
80
|
+
*/
|
|
81
|
+
next() {
|
|
82
|
+
const idx = findCurrentIndex();
|
|
83
|
+
navigateToIndex(idx + 1);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Navigate to previous slide
|
|
88
|
+
*/
|
|
89
|
+
prev() {
|
|
90
|
+
const idx = findCurrentIndex();
|
|
91
|
+
navigateToIndex(idx - 1);
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Navigate to previous chapter
|
|
96
|
+
*/
|
|
97
|
+
prevChapter() {
|
|
98
|
+
const idx = findCurrentIndex();
|
|
99
|
+
// Find the previous chapter boundary before current position
|
|
100
|
+
const prevBoundary = chapterBoundaries.filter((b) => b < idx).pop();
|
|
101
|
+
if (prevBoundary !== undefined) {
|
|
102
|
+
navigateToIndex(prevBoundary);
|
|
103
|
+
} else if (idx > 0) {
|
|
104
|
+
navigateToIndex(0);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Navigate to next chapter
|
|
110
|
+
*/
|
|
111
|
+
nextChapter() {
|
|
112
|
+
const idx = findCurrentIndex();
|
|
113
|
+
// Find the next chapter boundary after current position
|
|
114
|
+
const nextBoundary = chapterBoundaries.find((b) => b > idx);
|
|
115
|
+
if (nextBoundary !== undefined) {
|
|
116
|
+
navigateToIndex(nextBoundary);
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Navigate to first slide (index)
|
|
122
|
+
*/
|
|
123
|
+
home() {
|
|
124
|
+
navigateToIndex(0);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get current slide index
|
|
129
|
+
* @returns {number}
|
|
130
|
+
*/
|
|
131
|
+
currentIndex() {
|
|
132
|
+
return findCurrentIndex();
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get total number of slides
|
|
137
|
+
* @returns {number}
|
|
138
|
+
*/
|
|
139
|
+
totalSlides() {
|
|
140
|
+
return slideOrder.length;
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Start keyboard navigation
|
|
145
|
+
*/
|
|
146
|
+
startKeyboardNav() {
|
|
147
|
+
keyHandler = (e) => {
|
|
148
|
+
// Ignore if typing in an input
|
|
149
|
+
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
switch (e.key) {
|
|
154
|
+
case "ArrowRight":
|
|
155
|
+
case " ":
|
|
156
|
+
case "PageDown":
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
slideRouter.next();
|
|
159
|
+
break;
|
|
160
|
+
case "ArrowLeft":
|
|
161
|
+
case "PageUp":
|
|
162
|
+
e.preventDefault();
|
|
163
|
+
slideRouter.prev();
|
|
164
|
+
break;
|
|
165
|
+
case "ArrowDown":
|
|
166
|
+
e.preventDefault();
|
|
167
|
+
slideRouter.nextChapter();
|
|
168
|
+
break;
|
|
169
|
+
case "ArrowUp":
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
slideRouter.prevChapter();
|
|
172
|
+
break;
|
|
173
|
+
case "Home":
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
slideRouter.home();
|
|
176
|
+
break;
|
|
177
|
+
case "End":
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
navigateToIndex(slideOrder.length - 1);
|
|
180
|
+
break;
|
|
181
|
+
case "Escape":
|
|
182
|
+
e.preventDefault();
|
|
183
|
+
slideRouter.home();
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
document.addEventListener("keydown", keyHandler);
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Stop keyboard navigation
|
|
192
|
+
*/
|
|
193
|
+
stopKeyboardNav() {
|
|
194
|
+
if (keyHandler) {
|
|
195
|
+
document.removeEventListener("keydown", keyHandler);
|
|
196
|
+
keyHandler = null;
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
return slideRouter;
|
|
202
|
+
}
|