@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,109 @@
1
+ /**
2
+ * Grid components and utilities
3
+ *
4
+ * This module provides reusable grid components that wrap the CSS grid utilities.
5
+ * All grids use the unified CSS classes: auto-grid-xs, auto-grid-sm, auto-grid-md, auto-grid-lg
6
+ *
7
+ * Grid sizes:
8
+ * - xs: 150px min column width (compact stats, small cards)
9
+ * - sm: 200px min column width (form controls, medium cards)
10
+ * - md: 300px min column width (detail items, content cards)
11
+ * - lg: 400px min column width (large content like radar charts)
12
+ */
13
+
14
+ import { div } from "../lib/render.js";
15
+
16
+ /**
17
+ * Grid size options
18
+ * @typedef {'xs' | 'sm' | 'md' | 'lg'} GridSize
19
+ */
20
+
21
+ /**
22
+ * Gap size options
23
+ * @typedef {'sm' | 'md' | 'lg' | 'xl'} GapSize
24
+ */
25
+
26
+ /**
27
+ * Create an auto-fit grid container
28
+ * @param {GridSize} size - Grid size variant (xs, sm, md, lg)
29
+ * @param {HTMLElement[]} children - Child elements to place in grid
30
+ * @param {Object} [options] - Optional configuration
31
+ * @param {string} [options.className] - Additional CSS classes
32
+ * @param {GapSize} [options.gap] - Override default gap (sm, md, lg, xl)
33
+ * @returns {HTMLElement}
34
+ */
35
+ export function createAutoGrid(size, children, options = {}) {
36
+ const { className = "", gap } = options;
37
+ const classes = [`auto-grid-${size}`, gap ? `gap-${gap}` : "", className]
38
+ .filter(Boolean)
39
+ .join(" ");
40
+
41
+ return div({ className: classes }, ...children);
42
+ }
43
+
44
+ /**
45
+ * Create a fixed-column grid container
46
+ * @param {2 | 3 | 6} columns - Number of columns
47
+ * @param {HTMLElement[]} children - Child elements to place in grid
48
+ * @param {Object} [options] - Optional configuration
49
+ * @param {string} [options.className] - Additional CSS classes
50
+ * @returns {HTMLElement}
51
+ */
52
+ export function createFixedGrid(columns, children, options = {}) {
53
+ const { className = "" } = options;
54
+ const classes = ["grid", `grid-${columns}`, className]
55
+ .filter(Boolean)
56
+ .join(" ");
57
+
58
+ return div({ className: classes }, ...children);
59
+ }
60
+
61
+ /**
62
+ * Create a grid for form selectors (discipline/grade/track dropdowns)
63
+ * Uses auto-grid-sm (200px min)
64
+ * @param {HTMLElement[]} children - Form control elements
65
+ * @returns {HTMLElement}
66
+ */
67
+ export function createSelectorGrid(children) {
68
+ return createAutoGrid("sm", children, { gap: "lg" });
69
+ }
70
+
71
+ /**
72
+ * Create a grid for detail items (key-value pairs)
73
+ * Uses auto-grid-md (300px min)
74
+ * @param {HTMLElement[]} children - Detail item elements
75
+ * @returns {HTMLElement}
76
+ */
77
+ export function createDetailGrid(children) {
78
+ return createAutoGrid("md", children);
79
+ }
80
+
81
+ /**
82
+ * Create a grid for radar charts or large content
83
+ * Uses auto-grid-lg (400px min)
84
+ * @param {HTMLElement[]} children - Large content elements
85
+ * @returns {HTMLElement}
86
+ */
87
+ export function createRadarGrid(children) {
88
+ return createAutoGrid("lg", children);
89
+ }
90
+
91
+ /**
92
+ * Create a grid for compact stats
93
+ * Uses auto-grid-xs (150px min)
94
+ * @param {HTMLElement[]} children - Stat elements
95
+ * @returns {HTMLElement}
96
+ */
97
+ export function createStatsGrid(children) {
98
+ return createAutoGrid("xs", children);
99
+ }
100
+
101
+ /**
102
+ * Create a grid for card-like items (tips, expectations)
103
+ * Uses auto-grid-sm (200px min)
104
+ * @param {HTMLElement[]} children - Card elements
105
+ * @returns {HTMLElement}
106
+ */
107
+ export function createCardGrid(children) {
108
+ return createAutoGrid("sm", children);
109
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Reusable list component
3
+ */
4
+
5
+ import { div, h2, input, select, option } from "../lib/render.js";
6
+ import { createCard, createBadge } from "./card.js";
7
+
8
+ /**
9
+ * Create a search/filter bar
10
+ * @param {Object} options
11
+ * @param {Function} options.onSearch - Search callback
12
+ * @param {Array<{value: string, label: string}>} [options.filterOptions] - Filter dropdown options
13
+ * @param {Function} [options.onFilter] - Filter callback
14
+ * @param {string} [options.searchPlaceholder='Search...']
15
+ * @param {string} [options.filterPlaceholder='All']
16
+ * @returns {HTMLElement}
17
+ */
18
+ export function createSearchBar({
19
+ onSearch,
20
+ filterOptions,
21
+ onFilter,
22
+ searchPlaceholder = "Search...",
23
+ filterPlaceholder = "All",
24
+ }) {
25
+ const searchInput = input({
26
+ type: "text",
27
+ className: "form-input",
28
+ placeholder: searchPlaceholder,
29
+ });
30
+
31
+ searchInput.addEventListener("input", (e) => {
32
+ onSearch(e.target.value);
33
+ });
34
+
35
+ const children = [searchInput];
36
+
37
+ if (filterOptions && onFilter) {
38
+ const filterSelect = select(
39
+ { className: "form-select" },
40
+ option({ value: "" }, filterPlaceholder),
41
+ ...filterOptions.map((opt) => option({ value: opt.value }, opt.label)),
42
+ );
43
+
44
+ filterSelect.addEventListener("change", (e) => {
45
+ onFilter(e.target.value);
46
+ });
47
+
48
+ children.push(filterSelect);
49
+ }
50
+
51
+ return div({ className: "search-bar" }, ...children);
52
+ }
53
+
54
+ /**
55
+ * Create a list of cards
56
+ * @param {Array} items - Items to render
57
+ * @param {Function} renderItem - Function to render each item as a card config
58
+ * @param {string} [emptyMessage='No items found']
59
+ * @returns {HTMLElement}
60
+ */
61
+ export function createCardList(
62
+ items,
63
+ renderItem,
64
+ emptyMessage = "No items found",
65
+ ) {
66
+ if (!items || items.length === 0) {
67
+ return div(
68
+ { className: "empty-state" },
69
+ div({ className: "empty-message" }, emptyMessage),
70
+ );
71
+ }
72
+
73
+ const cards = items.map((item) => {
74
+ const config = renderItem(item);
75
+ return createCard(config);
76
+ });
77
+
78
+ return div({ className: "grid grid-3" }, ...cards);
79
+ }
80
+
81
+ /**
82
+ * Create a grouped list (like skills by capability)
83
+ * @param {Object} groups - Object with group names as keys and arrays as values
84
+ * @param {Function} renderItem - Function to render each item
85
+ * @param {Function} [renderGroupHeader] - Function to render group header
86
+ * @returns {HTMLElement}
87
+ */
88
+ export function createGroupedList(groups, renderItem, renderGroupHeader) {
89
+ const sections = Object.entries(groups).map(([groupName, items]) => {
90
+ const header = renderGroupHeader
91
+ ? renderGroupHeader(groupName, items.length)
92
+ : div(
93
+ { className: "capability-header" },
94
+ h2({ className: "capability-title" }, formatGroupName(groupName)),
95
+ createBadge(`${items.length}`, "default"),
96
+ );
97
+
98
+ const cards = items.map((item) => {
99
+ const config = renderItem(item);
100
+ return createCard(config);
101
+ });
102
+
103
+ return div(
104
+ { className: "capability-group" },
105
+ header,
106
+ div({ className: "grid grid-3" }, ...cards),
107
+ );
108
+ });
109
+
110
+ return div({ className: "grouped-list" }, ...sections);
111
+ }
112
+
113
+ /**
114
+ * Format a group name for display
115
+ * @param {string} name
116
+ * @returns {string}
117
+ */
118
+ function formatGroupName(name) {
119
+ return name.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
120
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Modifier table component for displaying skill/behaviour modifiers
3
+ */
4
+
5
+ import {
6
+ div,
7
+ span,
8
+ table,
9
+ thead,
10
+ tbody,
11
+ tr,
12
+ th,
13
+ td,
14
+ a,
15
+ } from "../lib/render.js";
16
+ import { createBadge } from "./card.js";
17
+
18
+ /**
19
+ * Create a modifier badge based on value
20
+ * @param {number} modifier - The modifier value
21
+ * @returns {HTMLElement}
22
+ */
23
+ function createModifierBadge(modifier) {
24
+ if (modifier > 0) {
25
+ return createBadge(`+${modifier}`, "broad");
26
+ } else if (modifier < 0) {
27
+ return createBadge(`${modifier}`, "negative");
28
+ }
29
+ return createBadge(`${modifier}`, "secondary");
30
+ }
31
+
32
+ /**
33
+ * Create a table displaying modifiers for skills or behaviours
34
+ * @param {Object} options - Configuration options
35
+ * @param {Array} options.modifiers - Array of {id, name, modifier} objects
36
+ * @param {string} options.basePath - Base path for links (e.g., '/skill', '/behaviour')
37
+ * @param {string} options.itemLabel - Label for the first column (e.g., 'Skill', 'Behaviour')
38
+ * @returns {HTMLElement}
39
+ */
40
+ export function createModifierTable({ modifiers, basePath, itemLabel }) {
41
+ const rows = modifiers.map((m) =>
42
+ tr(
43
+ {},
44
+ td({}, a({ href: `#${basePath}/${m.id}` }, m.name)),
45
+ td({}, createModifierBadge(m.modifier)),
46
+ ),
47
+ );
48
+
49
+ return div(
50
+ { className: "table-container" },
51
+ table(
52
+ { className: "table modifier-table" },
53
+ thead({}, tr({}, th({}, itemLabel), th({}, "Modifier"))),
54
+ tbody({}, ...rows),
55
+ ),
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Create a behaviour modifiers table
61
+ * @param {Array} modifiers - Array of {id, name, modifier} objects
62
+ * @returns {HTMLElement}
63
+ */
64
+ export function createBehaviourModifierTable(modifiers) {
65
+ return createModifierTable({
66
+ modifiers,
67
+ basePath: "/behaviour",
68
+ itemLabel: "Behaviour",
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Create a skill modifiers table
74
+ * @param {Array} modifiers - Array of {id, name, modifier} objects
75
+ * @returns {HTMLElement}
76
+ */
77
+ export function createSkillModifierTable(modifiers) {
78
+ return createModifierTable({
79
+ modifiers,
80
+ basePath: "/skill",
81
+ itemLabel: "Skill",
82
+ });
83
+ }
84
+
85
+ /**
86
+ * Create a skill modifiers table with capability support
87
+ * Used for tracks which can have capability-level modifiers with nested skill links
88
+ * @param {Array} modifiers - Array of {id, name, modifier, isCapability?, skills?} objects
89
+ * @returns {HTMLElement}
90
+ */
91
+ export function createSkillModifierTableWithCapabilities(modifiers) {
92
+ const rows = modifiers.map((m) => {
93
+ if (m.isCapability && m.skills) {
94
+ // Capability row: show capability name (not linked) with nested skill list
95
+ const skillLinks = m.skills.map((skill, index) => {
96
+ if (index < m.skills.length - 1) {
97
+ return span({}, a({ href: `#/skill/${skill.id}` }, skill.name), ", ");
98
+ }
99
+ return a({ href: `#/skill/${skill.id}` }, skill.name);
100
+ });
101
+
102
+ return tr(
103
+ {},
104
+ td(
105
+ {},
106
+ div({ style: "font-weight: 500;" }, m.name),
107
+ div(
108
+ {
109
+ style:
110
+ "font-size: 0.85em; color: var(--text-secondary); margin-top: 0.25rem;",
111
+ },
112
+ ...skillLinks,
113
+ ),
114
+ ),
115
+ td({}, createModifierBadge(m.modifier)),
116
+ );
117
+ } else if (m.isCapability) {
118
+ // Capability row without nested skills: show simple text
119
+ return tr(
120
+ {},
121
+ td({}, `All ${m.name} skills`),
122
+ td({}, createModifierBadge(m.modifier)),
123
+ );
124
+ } else {
125
+ // Individual skill row: show linked skill name
126
+ return tr(
127
+ {},
128
+ td({}, a({ href: `#/skill/${m.id}` }, m.name)),
129
+ td({}, createModifierBadge(m.modifier)),
130
+ );
131
+ }
132
+ });
133
+
134
+ return div(
135
+ { className: "table-container" },
136
+ table(
137
+ { className: "table modifier-table" },
138
+ thead({}, tr({}, th({}, "Skill"), th({}, "Modifier"))),
139
+ tbody({}, ...rows),
140
+ ),
141
+ );
142
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Navigation component helpers
3
+ */
4
+
5
+ import { div, a } from "../lib/render.js";
6
+
7
+ /**
8
+ * Update the active navigation link
9
+ * @param {string} path - Current path
10
+ */
11
+ export function updateActiveNav(path) {
12
+ const links = document.querySelectorAll("#nav-links a");
13
+ links.forEach((link) => {
14
+ const href = link.getAttribute("href").slice(1); // Remove #
15
+ const isActive = path === href || (href !== "/" && path.startsWith(href));
16
+ link.classList.toggle("active", isActive);
17
+ });
18
+ }
19
+
20
+ /**
21
+ * Create a back link
22
+ * @param {string} href - Link destination
23
+ * @param {string} [text='Back'] - Link text
24
+ * @returns {HTMLElement}
25
+ */
26
+ export function createBackLink(href, text = "← Back") {
27
+ return a({ href: `#${href}`, className: "back-link" }, text);
28
+ }
29
+
30
+ /**
31
+ * Create breadcrumbs
32
+ * @param {Array<{label: string, href?: string}>} items
33
+ * @returns {HTMLElement}
34
+ */
35
+ export function createBreadcrumbs(items) {
36
+ const crumbs = items.map((item, index) => {
37
+ const isLast = index === items.length - 1;
38
+ if (isLast || !item.href) {
39
+ return span({ className: "breadcrumb-item" }, item.label);
40
+ }
41
+ return a(
42
+ { href: `#${item.href}`, className: "breadcrumb-item" },
43
+ item.label,
44
+ );
45
+ });
46
+
47
+ const separator = " / ";
48
+ const children = [];
49
+ crumbs.forEach((crumb, i) => {
50
+ children.push(crumb);
51
+ if (i < crumbs.length - 1) {
52
+ children.push(document.createTextNode(separator));
53
+ }
54
+ });
55
+
56
+ return div({ className: "breadcrumbs" }, ...children);
57
+ }
58
+
59
+ function span(attrs, text) {
60
+ const el = document.createElement("span");
61
+ if (attrs.className) el.className = attrs.className;
62
+ if (text) el.textContent = text;
63
+ return el;
64
+ }