@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.
Files changed (227) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +104 -0
  3. package/app/commands/agent.js +430 -0
  4. package/app/commands/behaviour.js +61 -0
  5. package/app/commands/command-factory.js +211 -0
  6. package/app/commands/discipline.js +58 -0
  7. package/app/commands/driver.js +94 -0
  8. package/app/commands/grade.js +60 -0
  9. package/app/commands/index.js +20 -0
  10. package/app/commands/init.js +67 -0
  11. package/app/commands/interview.js +68 -0
  12. package/app/commands/job.js +157 -0
  13. package/app/commands/progress.js +77 -0
  14. package/app/commands/questions.js +179 -0
  15. package/app/commands/serve.js +143 -0
  16. package/app/commands/site.js +121 -0
  17. package/app/commands/skill.js +76 -0
  18. package/app/commands/stage.js +129 -0
  19. package/app/commands/track.js +70 -0
  20. package/app/components/action-buttons.js +66 -0
  21. package/app/components/behaviour-profile.js +53 -0
  22. package/app/components/builder.js +341 -0
  23. package/app/components/card.js +98 -0
  24. package/app/components/checklist.js +145 -0
  25. package/app/components/comparison-radar.js +237 -0
  26. package/app/components/detail.js +230 -0
  27. package/app/components/error-page.js +72 -0
  28. package/app/components/grid.js +109 -0
  29. package/app/components/list.js +120 -0
  30. package/app/components/modifier-table.js +142 -0
  31. package/app/components/nav.js +64 -0
  32. package/app/components/progression-table.js +320 -0
  33. package/app/components/radar-chart.js +102 -0
  34. package/app/components/skill-matrix.js +97 -0
  35. package/app/css/base.css +56 -0
  36. package/app/css/bundles/app.css +40 -0
  37. package/app/css/bundles/handout.css +43 -0
  38. package/app/css/bundles/slides.css +40 -0
  39. package/app/css/components/badges.css +215 -0
  40. package/app/css/components/buttons.css +101 -0
  41. package/app/css/components/forms.css +105 -0
  42. package/app/css/components/layout.css +209 -0
  43. package/app/css/components/nav.css +166 -0
  44. package/app/css/components/progress.css +166 -0
  45. package/app/css/components/states.css +82 -0
  46. package/app/css/components/surfaces.css +243 -0
  47. package/app/css/components/tables.css +362 -0
  48. package/app/css/components/typography.css +122 -0
  49. package/app/css/components/utilities.css +41 -0
  50. package/app/css/pages/agent-builder.css +391 -0
  51. package/app/css/pages/assessment-results.css +453 -0
  52. package/app/css/pages/detail.css +59 -0
  53. package/app/css/pages/interview-builder.css +148 -0
  54. package/app/css/pages/job-builder.css +134 -0
  55. package/app/css/pages/landing.css +92 -0
  56. package/app/css/pages/lifecycle.css +118 -0
  57. package/app/css/pages/progress-builder.css +274 -0
  58. package/app/css/pages/self-assessment.css +502 -0
  59. package/app/css/reset.css +50 -0
  60. package/app/css/tokens.css +153 -0
  61. package/app/css/views/handout.css +30 -0
  62. package/app/css/views/print.css +608 -0
  63. package/app/css/views/slide-animations.css +113 -0
  64. package/app/css/views/slide-base.css +330 -0
  65. package/app/css/views/slide-sections.css +597 -0
  66. package/app/css/views/slide-tables.css +275 -0
  67. package/app/formatters/agent/dom.js +540 -0
  68. package/app/formatters/agent/profile.js +133 -0
  69. package/app/formatters/agent/skill.js +58 -0
  70. package/app/formatters/behaviour/dom.js +91 -0
  71. package/app/formatters/behaviour/markdown.js +54 -0
  72. package/app/formatters/behaviour/shared.js +64 -0
  73. package/app/formatters/discipline/dom.js +187 -0
  74. package/app/formatters/discipline/markdown.js +87 -0
  75. package/app/formatters/discipline/shared.js +131 -0
  76. package/app/formatters/driver/dom.js +103 -0
  77. package/app/formatters/driver/shared.js +92 -0
  78. package/app/formatters/grade/dom.js +208 -0
  79. package/app/formatters/grade/markdown.js +94 -0
  80. package/app/formatters/grade/shared.js +86 -0
  81. package/app/formatters/index.js +50 -0
  82. package/app/formatters/interview/dom.js +97 -0
  83. package/app/formatters/interview/markdown.js +66 -0
  84. package/app/formatters/interview/shared.js +332 -0
  85. package/app/formatters/job/description.js +176 -0
  86. package/app/formatters/job/dom.js +411 -0
  87. package/app/formatters/job/markdown.js +102 -0
  88. package/app/formatters/progress/dom.js +135 -0
  89. package/app/formatters/progress/markdown.js +86 -0
  90. package/app/formatters/progress/shared.js +339 -0
  91. package/app/formatters/questions/json.js +43 -0
  92. package/app/formatters/questions/markdown.js +303 -0
  93. package/app/formatters/questions/shared.js +274 -0
  94. package/app/formatters/questions/yaml.js +76 -0
  95. package/app/formatters/shared.js +71 -0
  96. package/app/formatters/skill/dom.js +168 -0
  97. package/app/formatters/skill/markdown.js +109 -0
  98. package/app/formatters/skill/shared.js +125 -0
  99. package/app/formatters/stage/dom.js +135 -0
  100. package/app/formatters/stage/index.js +12 -0
  101. package/app/formatters/stage/shared.js +111 -0
  102. package/app/formatters/track/dom.js +128 -0
  103. package/app/formatters/track/markdown.js +105 -0
  104. package/app/formatters/track/shared.js +181 -0
  105. package/app/handout-main.js +421 -0
  106. package/app/handout.html +21 -0
  107. package/app/index.html +59 -0
  108. package/app/lib/card-mappers.js +173 -0
  109. package/app/lib/cli-output.js +270 -0
  110. package/app/lib/error-boundary.js +70 -0
  111. package/app/lib/errors.js +49 -0
  112. package/app/lib/form-controls.js +47 -0
  113. package/app/lib/job-cache.js +86 -0
  114. package/app/lib/markdown.js +114 -0
  115. package/app/lib/radar.js +866 -0
  116. package/app/lib/reactive.js +77 -0
  117. package/app/lib/render.js +212 -0
  118. package/app/lib/router-core.js +160 -0
  119. package/app/lib/router-pages.js +16 -0
  120. package/app/lib/router-slides.js +202 -0
  121. package/app/lib/state.js +148 -0
  122. package/app/lib/utils.js +14 -0
  123. package/app/lib/yaml-loader.js +327 -0
  124. package/app/main.js +213 -0
  125. package/app/model/agent.js +702 -0
  126. package/app/model/checklist.js +137 -0
  127. package/app/model/derivation.js +699 -0
  128. package/app/model/index-generator.js +71 -0
  129. package/app/model/interview.js +539 -0
  130. package/app/model/job.js +222 -0
  131. package/app/model/levels.js +591 -0
  132. package/app/model/loader.js +564 -0
  133. package/app/model/matching.js +858 -0
  134. package/app/model/modifiers.js +158 -0
  135. package/app/model/profile.js +266 -0
  136. package/app/model/progression.js +507 -0
  137. package/app/model/validation.js +1385 -0
  138. package/app/pages/agent-builder.js +823 -0
  139. package/app/pages/assessment-results.js +507 -0
  140. package/app/pages/behaviour.js +70 -0
  141. package/app/pages/discipline.js +71 -0
  142. package/app/pages/driver.js +106 -0
  143. package/app/pages/grade.js +117 -0
  144. package/app/pages/interview-builder.js +50 -0
  145. package/app/pages/interview.js +304 -0
  146. package/app/pages/job-builder.js +50 -0
  147. package/app/pages/job.js +58 -0
  148. package/app/pages/landing.js +305 -0
  149. package/app/pages/progress-builder.js +58 -0
  150. package/app/pages/progress.js +495 -0
  151. package/app/pages/self-assessment.js +729 -0
  152. package/app/pages/skill.js +113 -0
  153. package/app/pages/stage.js +231 -0
  154. package/app/pages/track.js +69 -0
  155. package/app/slide-main.js +360 -0
  156. package/app/slides/behaviour.js +38 -0
  157. package/app/slides/chapter.js +82 -0
  158. package/app/slides/discipline.js +40 -0
  159. package/app/slides/driver.js +39 -0
  160. package/app/slides/grade.js +32 -0
  161. package/app/slides/index.js +198 -0
  162. package/app/slides/interview.js +58 -0
  163. package/app/slides/job.js +55 -0
  164. package/app/slides/overview.js +126 -0
  165. package/app/slides/progress.js +83 -0
  166. package/app/slides/skill.js +40 -0
  167. package/app/slides/track.js +39 -0
  168. package/app/slides.html +56 -0
  169. package/app/types.js +147 -0
  170. package/bin/pathway.js +489 -0
  171. package/examples/agents/.claude/skills/architecture-design/SKILL.md +88 -0
  172. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +90 -0
  173. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +67 -0
  174. package/examples/agents/.claude/skills/data-modeling/SKILL.md +99 -0
  175. package/examples/agents/.claude/skills/developer-experience/SKILL.md +99 -0
  176. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +96 -0
  177. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +90 -0
  178. package/examples/agents/.claude/skills/knowledge-management/SKILL.md +100 -0
  179. package/examples/agents/.claude/skills/pattern-generalization/SKILL.md +102 -0
  180. package/examples/agents/.claude/skills/sre-practices/SKILL.md +117 -0
  181. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +123 -0
  182. package/examples/agents/.claude/skills/technical-writing/SKILL.md +129 -0
  183. package/examples/agents/.github/agents/se-platform-code.agent.md +181 -0
  184. package/examples/agents/.github/agents/se-platform-plan.agent.md +178 -0
  185. package/examples/agents/.github/agents/se-platform-review.agent.md +113 -0
  186. package/examples/agents/.vscode/settings.json +8 -0
  187. package/examples/behaviours/_index.yaml +8 -0
  188. package/examples/behaviours/outcome_ownership.yaml +44 -0
  189. package/examples/behaviours/polymathic_knowledge.yaml +42 -0
  190. package/examples/behaviours/precise_communication.yaml +40 -0
  191. package/examples/behaviours/relentless_curiosity.yaml +38 -0
  192. package/examples/behaviours/systems_thinking.yaml +41 -0
  193. package/examples/capabilities/_index.yaml +8 -0
  194. package/examples/capabilities/business.yaml +251 -0
  195. package/examples/capabilities/delivery.yaml +352 -0
  196. package/examples/capabilities/people.yaml +100 -0
  197. package/examples/capabilities/reliability.yaml +318 -0
  198. package/examples/capabilities/scale.yaml +394 -0
  199. package/examples/disciplines/_index.yaml +5 -0
  200. package/examples/disciplines/data_engineering.yaml +76 -0
  201. package/examples/disciplines/software_engineering.yaml +76 -0
  202. package/examples/drivers.yaml +205 -0
  203. package/examples/framework.yaml +58 -0
  204. package/examples/grades.yaml +118 -0
  205. package/examples/questions/behaviours/outcome_ownership.yaml +52 -0
  206. package/examples/questions/behaviours/polymathic_knowledge.yaml +48 -0
  207. package/examples/questions/behaviours/precise_communication.yaml +55 -0
  208. package/examples/questions/behaviours/relentless_curiosity.yaml +51 -0
  209. package/examples/questions/behaviours/systems_thinking.yaml +53 -0
  210. package/examples/questions/skills/architecture_design.yaml +54 -0
  211. package/examples/questions/skills/cloud_platforms.yaml +48 -0
  212. package/examples/questions/skills/code_quality.yaml +49 -0
  213. package/examples/questions/skills/data_modeling.yaml +46 -0
  214. package/examples/questions/skills/devops.yaml +47 -0
  215. package/examples/questions/skills/full_stack_development.yaml +48 -0
  216. package/examples/questions/skills/sre_practices.yaml +44 -0
  217. package/examples/questions/skills/stakeholder_management.yaml +49 -0
  218. package/examples/questions/skills/team_collaboration.yaml +43 -0
  219. package/examples/questions/skills/technical_writing.yaml +43 -0
  220. package/examples/self-assessments.yaml +66 -0
  221. package/examples/stages.yaml +76 -0
  222. package/examples/tracks/_index.yaml +6 -0
  223. package/examples/tracks/manager.yaml +53 -0
  224. package/examples/tracks/platform.yaml +54 -0
  225. package/examples/tracks/sre.yaml +58 -0
  226. package/examples/vscode-settings.yaml +22 -0
  227. 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
+ }