@forwardimpact/pathway 0.22.0 → 0.23.1

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 (78) hide show
  1. package/bin/fit-pathway.js +8 -4
  2. package/package.json +6 -2
  3. package/src/commands/agent.js +6 -3
  4. package/src/commands/behaviour.js +11 -1
  5. package/src/commands/build.js +11 -2
  6. package/src/commands/command-factory.js +4 -2
  7. package/src/commands/dev.js +9 -2
  8. package/src/commands/discipline.js +25 -10
  9. package/src/commands/driver.js +11 -1
  10. package/src/commands/job.js +127 -28
  11. package/src/commands/level.js +25 -3
  12. package/src/commands/skill.js +11 -1
  13. package/src/commands/stage.js +11 -1
  14. package/src/commands/tool.js +6 -3
  15. package/src/commands/track.js +20 -4
  16. package/src/components/card.js +8 -104
  17. package/src/components/comparison-radar.js +1 -1
  18. package/src/components/detail.js +16 -118
  19. package/src/components/error-page.js +8 -68
  20. package/src/components/grid.js +12 -106
  21. package/src/components/list.js +7 -116
  22. package/src/components/nav.js +7 -60
  23. package/src/css/bundles/app.css +25 -21
  24. package/src/css/bundles/handout.css +33 -33
  25. package/src/css/bundles/slides.css +25 -25
  26. package/src/formatters/discipline/markdown.js +16 -1
  27. package/src/formatters/interview/shared.js +3 -3
  28. package/src/formatters/job/description.js +2 -2
  29. package/src/formatters/progress/shared.js +3 -3
  30. package/src/formatters/skill/shared.js +1 -1
  31. package/src/formatters/track/markdown.js +14 -0
  32. package/src/formatters/track/shared.js +1 -1
  33. package/src/handout.html +32 -13
  34. package/src/index.html +32 -13
  35. package/src/lib/error-boundary.js +3 -66
  36. package/src/lib/errors.js +7 -45
  37. package/src/lib/job-cache.js +1 -1
  38. package/src/lib/markdown.js +2 -109
  39. package/src/lib/reactive.js +7 -73
  40. package/src/lib/render.js +49 -197
  41. package/src/lib/router-core.js +2 -156
  42. package/src/lib/router-pages.js +2 -11
  43. package/src/lib/router-slides.js +2 -197
  44. package/src/lib/state.js +14 -63
  45. package/src/lib/utils.js +3 -10
  46. package/src/lib/yaml-loader.js +13 -71
  47. package/src/pages/agent-builder.js +1 -1
  48. package/src/pages/assessment-results.js +1 -1
  49. package/src/pages/job-builder.js +1 -1
  50. package/src/pages/job.js +1 -1
  51. package/src/pages/skill.js +1 -1
  52. package/src/slide-main.js +1 -1
  53. package/src/slides/index.js +1 -1
  54. package/src/slides/job.js +1 -1
  55. package/src/slides/overview.js +1 -1
  56. package/src/slides.html +32 -13
  57. package/src/css/base.css +0 -56
  58. package/src/css/components/badges.css +0 -232
  59. package/src/css/components/buttons.css +0 -101
  60. package/src/css/components/forms.css +0 -191
  61. package/src/css/components/layout.css +0 -218
  62. package/src/css/components/nav.css +0 -206
  63. package/src/css/components/progress.css +0 -166
  64. package/src/css/components/states.css +0 -82
  65. package/src/css/components/surfaces.css +0 -347
  66. package/src/css/components/tables.css +0 -362
  67. package/src/css/components/top-bar.css +0 -180
  68. package/src/css/components/typography.css +0 -121
  69. package/src/css/components/utilities.css +0 -41
  70. package/src/css/pages/detail.css +0 -119
  71. package/src/css/reset.css +0 -50
  72. package/src/css/tokens.css +0 -162
  73. package/src/css/views/handout.css +0 -30
  74. package/src/css/views/print.css +0 -634
  75. package/src/css/views/slide-animations.css +0 -113
  76. package/src/css/views/slide-base.css +0 -331
  77. package/src/css/views/slide-sections.css +0 -597
  78. package/src/css/views/slide-tables.css +0 -275
@@ -1,161 +1,7 @@
1
1
  /**
2
2
  * Core Router Factory
3
3
  *
4
- * Pure factory function for hash-based routing with no dependencies.
4
+ * Re-exports from @forwardimpact/libui/router-core.
5
5
  */
6
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
- window.scrollTo(0, 0);
100
- matched.handler(matched.params);
101
- } else {
102
- onNotFound(path);
103
- }
104
- }
105
-
106
- return {
107
- /**
108
- * Register a route
109
- * @param {string} pattern - Route pattern (e.g., '/skills/:id')
110
- * @param {Function} handler - Handler function
111
- */
112
- on(pattern, handler) {
113
- const { regex, paramNames } = parsePattern(pattern);
114
- const wrappedHandler = withErrorBoundary(handler, {
115
- onError,
116
- backPath: "/",
117
- backText: "← Back to Home",
118
- renderErrorFn: renderError,
119
- });
120
- routes.push({ pattern, regex, paramNames, handler: wrappedHandler });
121
- },
122
-
123
- /**
124
- * Navigate to a path
125
- * @param {string} path
126
- */
127
- navigate(path) {
128
- window.location.hash = path;
129
- },
130
-
131
- currentPath,
132
- handleRoute,
133
-
134
- /**
135
- * Start listening for hash changes
136
- */
137
- start() {
138
- hashChangeHandler = () => handleRoute();
139
- window.addEventListener("hashchange", hashChangeHandler);
140
- handleRoute();
141
- },
142
-
143
- /**
144
- * Stop listening for hash changes
145
- */
146
- stop() {
147
- if (hashChangeHandler) {
148
- window.removeEventListener("hashchange", hashChangeHandler);
149
- hashChangeHandler = null;
150
- }
151
- },
152
-
153
- /**
154
- * Get all registered route patterns
155
- * @returns {string[]}
156
- */
157
- patterns() {
158
- return routes.map((r) => r.pattern);
159
- },
160
- };
161
- }
7
+ export { createRouter } from "@forwardimpact/libui/router-core";
@@ -1,16 +1,7 @@
1
1
  /**
2
2
  * Pages Router
3
3
  *
4
- * Router instance for the main app pages.
4
+ * Re-exports from @forwardimpact/libui/router-pages.
5
5
  */
6
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
- }
7
+ export { createPagesRouter } from "@forwardimpact/libui/router-pages";
@@ -1,202 +1,7 @@
1
1
  /**
2
2
  * Slide Router
3
3
  *
4
- * Extended router with navigation state and keyboard controls for slides.
4
+ * Re-exports from @forwardimpact/libui/router-slides.
5
5
  */
6
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
- }
7
+ export { createSlideRouter } from "@forwardimpact/libui/router-slides";
package/src/lib/state.js CHANGED
@@ -1,15 +1,13 @@
1
1
  /**
2
2
  * Application state management
3
+ *
4
+ * Uses generic store from @forwardimpact/libui/state
5
+ * with Pathway-specific state shape and accessors.
3
6
  */
4
7
 
5
- /**
6
- * @typedef {Object} AppState
7
- * @property {Object} data - Loaded data from YAML files
8
- * @property {Object} ui - UI state
9
- */
8
+ import { createStore } from "@forwardimpact/libui/state";
10
9
 
11
- /** @type {AppState} */
12
- const state = {
10
+ const store = createStore({
13
11
  data: {
14
12
  skills: [],
15
13
  behaviours: [],
@@ -35,48 +33,18 @@ const state = {
35
33
  drivers: { search: "" },
36
34
  },
37
35
  },
38
- };
39
-
40
- /** @type {Set<Function>} */
41
- const listeners = new Set();
42
-
43
- /**
44
- * Get the current state
45
- * @returns {AppState}
46
- */
47
- export function getState() {
48
- return state;
49
- }
50
-
51
- /**
52
- * Get a specific path from state
53
- * @param {string} path - Dot-notation path (e.g., 'data.skills')
54
- * @returns {*}
55
- */
56
- export function getStatePath(path) {
57
- return path.split(".").reduce((obj, key) => obj?.[key], state);
58
- }
36
+ });
59
37
 
60
- /**
61
- * Update state at a specific path
62
- * @param {string} path - Dot-notation path
63
- * @param {*} value - New value
64
- */
65
- export function updateState(path, value) {
66
- const keys = path.split(".");
67
- const lastKey = keys.pop();
68
- const target = keys.reduce((obj, key) => obj[key], state);
69
- target[lastKey] = value;
70
- notifyListeners();
71
- }
38
+ export const { getState, getStatePath, updateState, subscribe } = store;
72
39
 
73
40
  /**
74
41
  * Merge data into state
75
42
  * @param {Object} data - Data to merge
76
43
  */
77
44
  export function setData(data) {
45
+ const state = getState();
78
46
  Object.assign(state.data, data, { loaded: true, error: null });
79
- notifyListeners();
47
+ updateState("data", state.data);
80
48
  }
81
49
 
82
50
  /**
@@ -84,25 +52,7 @@ export function setData(data) {
84
52
  * @param {Error} error
85
53
  */
86
54
  export function setError(error) {
87
- state.data.error = error.message;
88
- notifyListeners();
89
- }
90
-
91
- /**
92
- * Subscribe to state changes
93
- * @param {Function} listener
94
- * @returns {Function} Unsubscribe function
95
- */
96
- export function subscribe(listener) {
97
- listeners.add(listener);
98
- return () => listeners.delete(listener);
99
- }
100
-
101
- /**
102
- * Notify all listeners of state change
103
- */
104
- function notifyListeners() {
105
- listeners.forEach((listener) => listener(state));
55
+ updateState("data.error", error.message);
106
56
  }
107
57
 
108
58
  /**
@@ -112,9 +62,10 @@ function notifyListeners() {
112
62
  * @param {*} value - Filter value
113
63
  */
114
64
  export function setFilter(entity, filterKey, value) {
65
+ const state = getState();
115
66
  if (state.ui.filters[entity]) {
116
67
  state.ui.filters[entity][filterKey] = value;
117
- notifyListeners();
68
+ updateState("ui.filters", state.ui.filters);
118
69
  }
119
70
  }
120
71
 
@@ -124,7 +75,7 @@ export function setFilter(entity, filterKey, value) {
124
75
  * @returns {Object}
125
76
  */
126
77
  export function getFilters(entity) {
127
- return state.ui.filters[entity] || {};
78
+ return getState().ui.filters[entity] || {};
128
79
  }
129
80
 
130
81
  /**
@@ -140,7 +91,7 @@ export function getFilters(entity) {
140
91
  * @returns {Branding}
141
92
  */
142
93
  export function getBranding() {
143
- const { framework } = state.data;
94
+ const { framework } = getState().data;
144
95
  return {
145
96
  title: framework.title || "Engineering Pathway",
146
97
  tag: framework.tag || "#BenchTools",
package/src/lib/utils.js CHANGED
@@ -1,14 +1,7 @@
1
1
  /**
2
2
  * General utility functions
3
+ *
4
+ * Re-exports from @forwardimpact/libui/utils.
3
5
  */
4
6
 
5
- /**
6
- * Get an array of items by their IDs
7
- * @param {Array} items - Array of items with id property
8
- * @param {string[]} ids - Array of IDs to find
9
- * @returns {Array} - Found items, filtered to remove nulls
10
- */
11
- export function getItemsByIds(items, ids) {
12
- if (!ids) return [];
13
- return ids.map((id) => items.find((item) => item.id === id)).filter(Boolean);
14
- }
7
+ export { getItemsByIds } from "@forwardimpact/libui/utils";