@forwardimpact/pathway 0.4.0 → 0.6.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 (245) hide show
  1. package/bin/{pathway.js → fit-pathway.js} +65 -153
  2. package/package.json +18 -41
  3. package/{app → src}/commands/agent.js +5 -2
  4. package/{app → src}/commands/behaviour.js +1 -1
  5. package/{app → src}/commands/command-factory.js +2 -2
  6. package/{app → src}/commands/discipline.js +1 -1
  7. package/{app → src}/commands/driver.js +2 -2
  8. package/{app → src}/commands/grade.js +2 -2
  9. package/{app → src}/commands/job.js +3 -3
  10. package/{app → src}/commands/serve.js +26 -4
  11. package/{app → src}/commands/site.js +24 -4
  12. package/{app → src}/commands/skill.js +3 -3
  13. package/{app → src}/commands/stage.js +1 -1
  14. package/{app → src}/commands/track.js +2 -2
  15. package/{app → src}/components/card.js +11 -1
  16. package/{app → src}/components/checklist.js +1 -1
  17. package/src/components/code-display.js +153 -0
  18. package/{app → src}/components/comparison-radar.js +1 -1
  19. package/{app → src}/components/detail.js +1 -1
  20. package/src/components/markdown-textarea.js +153 -0
  21. package/{app → src}/components/skill-matrix.js +1 -1
  22. package/{app → src}/css/bundles/app.css +14 -0
  23. package/{app → src}/css/components/badges.css +15 -8
  24. package/{app → src}/css/components/forms.css +23 -13
  25. package/{app → src}/css/components/surfaces.css +49 -3
  26. package/{app → src}/css/components/typography.css +1 -2
  27. package/{app → src}/css/pages/agent-builder.css +11 -102
  28. package/{app → src}/css/pages/detail.css +11 -1
  29. package/{app → src}/css/tokens.css +3 -0
  30. package/{app → src}/formatters/agent/dom.js +26 -71
  31. package/{app → src}/formatters/agent/profile.js +11 -6
  32. package/{app → src}/formatters/behaviour/dom.js +1 -1
  33. package/{app → src}/formatters/discipline/dom.js +1 -1
  34. package/{app → src}/formatters/driver/dom.js +1 -1
  35. package/{app → src}/formatters/grade/dom.js +7 -7
  36. package/{app → src}/formatters/grade/markdown.js +1 -1
  37. package/{app → src}/formatters/interview/dom.js +1 -1
  38. package/{app → src}/formatters/interview/markdown.js +1 -1
  39. package/{app → src}/formatters/interview/shared.js +3 -3
  40. package/{app → src}/formatters/job/description.js +1 -1
  41. package/{app → src}/formatters/job/dom.js +3 -3
  42. package/{app → src}/formatters/job/markdown.js +1 -1
  43. package/{app → src}/formatters/json-ld.js +1 -1
  44. package/{app → src}/formatters/progress/shared.js +3 -3
  45. package/{app → src}/formatters/skill/dom.js +69 -57
  46. package/{app → src}/formatters/skill/markdown.js +1 -1
  47. package/{app → src}/formatters/skill/shared.js +5 -3
  48. package/{app → src}/formatters/stage/microdata.js +2 -2
  49. package/{app → src}/formatters/stage/shared.js +3 -3
  50. package/{app → src}/formatters/tool/shared.js +6 -0
  51. package/{app → src}/formatters/track/dom.js +1 -1
  52. package/{app → src}/formatters/track/markdown.js +1 -1
  53. package/{app → src}/formatters/track/shared.js +4 -1
  54. package/{app → src}/handout-main.js +16 -12
  55. package/src/handout.html +43 -0
  56. package/{app → src}/index.html +23 -2
  57. package/{app → src}/lib/card-mappers.js +28 -1
  58. package/{app → src}/lib/job-cache.js +1 -1
  59. package/{app → src}/lib/render.js +1 -1
  60. package/{app → src}/pages/agent-builder.js +120 -76
  61. package/{app → src}/pages/assessment-results.js +1 -1
  62. package/{app → src}/pages/interview.js +1 -1
  63. package/{app → src}/pages/job-builder.js +1 -1
  64. package/{app → src}/pages/job.js +1 -1
  65. package/{app → src}/pages/landing.js +5 -2
  66. package/{app → src}/pages/self-assessment.js +1 -1
  67. package/{app → src}/pages/skill.js +1 -1
  68. package/{app → src}/pages/stage.js +5 -5
  69. package/{app → src}/pages/tool.js +1 -1
  70. package/{app → src}/slide-main.js +2 -2
  71. package/{app → src}/slides/chapter.js +8 -8
  72. package/{app → src}/slides/index.js +3 -3
  73. package/{app → src}/slides/job.js +1 -1
  74. package/{app → src}/slides/overview.js +9 -9
  75. package/{app → src}/slides/skill.js +1 -0
  76. package/{app → src}/slides.html +16 -1
  77. package/templates/agent.template.md +44 -13
  78. package/templates/job.template.md +14 -20
  79. package/templates/skill.template.md +20 -23
  80. package/LICENSE +0 -201
  81. package/README.md +0 -104
  82. package/app/components/markdown-textarea.js +0 -132
  83. package/app/handout.html +0 -28
  84. package/app/model/agent.js +0 -738
  85. package/app/model/checklist.js +0 -103
  86. package/app/model/derivation.js +0 -766
  87. package/app/model/index-generator.js +0 -65
  88. package/app/model/interview.js +0 -539
  89. package/app/model/job.js +0 -228
  90. package/app/model/levels.js +0 -601
  91. package/app/model/loader.js +0 -599
  92. package/app/model/matching.js +0 -888
  93. package/app/model/modifiers.js +0 -158
  94. package/app/model/profile.js +0 -259
  95. package/app/model/progression.js +0 -507
  96. package/app/model/schema-validation.js +0 -438
  97. package/app/model/validation.js +0 -2130
  98. package/examples/agents/.claude/skills/architecture-design/SKILL.md +0 -130
  99. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +0 -131
  100. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +0 -108
  101. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +0 -142
  102. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +0 -134
  103. package/examples/agents/.claude/skills/sre-practices/SKILL.md +0 -163
  104. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +0 -164
  105. package/examples/agents/.github/agents/se-platform-code.agent.md +0 -132
  106. package/examples/agents/.github/agents/se-platform-plan.agent.md +0 -131
  107. package/examples/agents/.github/agents/se-platform-review.agent.md +0 -136
  108. package/examples/agents/.vscode/settings.json +0 -8
  109. package/examples/behaviours/_index.yaml +0 -8
  110. package/examples/behaviours/outcome_ownership.yaml +0 -43
  111. package/examples/behaviours/polymathic_knowledge.yaml +0 -41
  112. package/examples/behaviours/precise_communication.yaml +0 -39
  113. package/examples/behaviours/relentless_curiosity.yaml +0 -37
  114. package/examples/behaviours/systems_thinking.yaml +0 -40
  115. package/examples/capabilities/_index.yaml +0 -8
  116. package/examples/capabilities/business.yaml +0 -189
  117. package/examples/capabilities/delivery.yaml +0 -303
  118. package/examples/capabilities/people.yaml +0 -68
  119. package/examples/capabilities/reliability.yaml +0 -412
  120. package/examples/capabilities/scale.yaml +0 -378
  121. package/examples/copilot-setup-steps.yaml +0 -25
  122. package/examples/devcontainer.yaml +0 -21
  123. package/examples/disciplines/_index.yaml +0 -6
  124. package/examples/disciplines/data_engineering.yaml +0 -78
  125. package/examples/disciplines/engineering_management.yaml +0 -63
  126. package/examples/disciplines/software_engineering.yaml +0 -78
  127. package/examples/drivers.yaml +0 -202
  128. package/examples/framework.yaml +0 -69
  129. package/examples/grades.yaml +0 -115
  130. package/examples/questions/behaviours/outcome_ownership.yaml +0 -51
  131. package/examples/questions/behaviours/polymathic_knowledge.yaml +0 -47
  132. package/examples/questions/behaviours/precise_communication.yaml +0 -54
  133. package/examples/questions/behaviours/relentless_curiosity.yaml +0 -50
  134. package/examples/questions/behaviours/systems_thinking.yaml +0 -52
  135. package/examples/questions/skills/architecture_design.yaml +0 -53
  136. package/examples/questions/skills/cloud_platforms.yaml +0 -47
  137. package/examples/questions/skills/code_quality.yaml +0 -48
  138. package/examples/questions/skills/data_modeling.yaml +0 -45
  139. package/examples/questions/skills/devops.yaml +0 -46
  140. package/examples/questions/skills/full_stack_development.yaml +0 -47
  141. package/examples/questions/skills/sre_practices.yaml +0 -43
  142. package/examples/questions/skills/stakeholder_management.yaml +0 -48
  143. package/examples/questions/skills/team_collaboration.yaml +0 -42
  144. package/examples/questions/skills/technical_writing.yaml +0 -42
  145. package/examples/self-assessments.yaml +0 -64
  146. package/examples/stages.yaml +0 -131
  147. package/examples/tracks/_index.yaml +0 -5
  148. package/examples/tracks/platform.yaml +0 -49
  149. package/examples/tracks/sre.yaml +0 -48
  150. package/examples/vscode-settings.yaml +0 -17
  151. /package/{app → src}/commands/index.js +0 -0
  152. /package/{app → src}/commands/init.js +0 -0
  153. /package/{app → src}/commands/interview.js +0 -0
  154. /package/{app → src}/commands/progress.js +0 -0
  155. /package/{app → src}/commands/questions.js +0 -0
  156. /package/{app → src}/commands/tool.js +0 -0
  157. /package/{app → src}/components/action-buttons.js +0 -0
  158. /package/{app → src}/components/behaviour-profile.js +0 -0
  159. /package/{app → src}/components/builder.js +0 -0
  160. /package/{app → src}/components/error-page.js +0 -0
  161. /package/{app → src}/components/grid.js +0 -0
  162. /package/{app → src}/components/list.js +0 -0
  163. /package/{app → src}/components/modifier-table.js +0 -0
  164. /package/{app → src}/components/nav.js +0 -0
  165. /package/{app → src}/components/progression-table.js +0 -0
  166. /package/{app → src}/components/radar-chart.js +0 -0
  167. /package/{app → src}/css/base.css +0 -0
  168. /package/{app → src}/css/bundles/handout.css +0 -0
  169. /package/{app → src}/css/bundles/slides.css +0 -0
  170. /package/{app → src}/css/components/buttons.css +0 -0
  171. /package/{app → src}/css/components/layout.css +0 -0
  172. /package/{app → src}/css/components/nav.css +0 -0
  173. /package/{app → src}/css/components/progress.css +0 -0
  174. /package/{app → src}/css/components/states.css +0 -0
  175. /package/{app → src}/css/components/tables.css +0 -0
  176. /package/{app → src}/css/components/utilities.css +0 -0
  177. /package/{app → src}/css/pages/assessment-results.css +0 -0
  178. /package/{app → src}/css/pages/interview-builder.css +0 -0
  179. /package/{app → src}/css/pages/job-builder.css +0 -0
  180. /package/{app → src}/css/pages/landing.css +0 -0
  181. /package/{app → src}/css/pages/lifecycle.css +0 -0
  182. /package/{app → src}/css/pages/progress-builder.css +0 -0
  183. /package/{app → src}/css/pages/self-assessment.css +0 -0
  184. /package/{app → src}/css/reset.css +0 -0
  185. /package/{app → src}/css/views/handout.css +0 -0
  186. /package/{app → src}/css/views/print.css +0 -0
  187. /package/{app → src}/css/views/slide-animations.css +0 -0
  188. /package/{app → src}/css/views/slide-base.css +0 -0
  189. /package/{app → src}/css/views/slide-sections.css +0 -0
  190. /package/{app → src}/css/views/slide-tables.css +0 -0
  191. /package/{app → src}/formatters/agent/skill.js +0 -0
  192. /package/{app → src}/formatters/behaviour/markdown.js +0 -0
  193. /package/{app → src}/formatters/behaviour/microdata.js +0 -0
  194. /package/{app → src}/formatters/behaviour/shared.js +0 -0
  195. /package/{app → src}/formatters/discipline/markdown.js +0 -0
  196. /package/{app → src}/formatters/discipline/microdata.js +0 -0
  197. /package/{app → src}/formatters/discipline/shared.js +0 -0
  198. /package/{app → src}/formatters/driver/microdata.js +0 -0
  199. /package/{app → src}/formatters/driver/shared.js +0 -0
  200. /package/{app → src}/formatters/grade/microdata.js +0 -0
  201. /package/{app → src}/formatters/grade/shared.js +0 -0
  202. /package/{app → src}/formatters/index.js +0 -0
  203. /package/{app → src}/formatters/microdata-shared.js +0 -0
  204. /package/{app → src}/formatters/progress/dom.js +0 -0
  205. /package/{app → src}/formatters/progress/markdown.js +0 -0
  206. /package/{app → src}/formatters/questions/json.js +0 -0
  207. /package/{app → src}/formatters/questions/markdown.js +0 -0
  208. /package/{app → src}/formatters/questions/shared.js +0 -0
  209. /package/{app → src}/formatters/questions/yaml.js +0 -0
  210. /package/{app → src}/formatters/shared.js +0 -0
  211. /package/{app → src}/formatters/skill/microdata.js +0 -0
  212. /package/{app → src}/formatters/stage/dom.js +0 -0
  213. /package/{app → src}/formatters/stage/index.js +0 -0
  214. /package/{app → src}/formatters/track/microdata.js +0 -0
  215. /package/{app → src}/lib/cli-output.js +0 -0
  216. /package/{app → src}/lib/error-boundary.js +0 -0
  217. /package/{app → src}/lib/errors.js +0 -0
  218. /package/{app → src}/lib/form-controls.js +0 -0
  219. /package/{app → src}/lib/markdown.js +0 -0
  220. /package/{app → src}/lib/radar.js +0 -0
  221. /package/{app → src}/lib/reactive.js +0 -0
  222. /package/{app → src}/lib/router-core.js +0 -0
  223. /package/{app → src}/lib/router-pages.js +0 -0
  224. /package/{app → src}/lib/router-slides.js +0 -0
  225. /package/{app → src}/lib/state.js +0 -0
  226. /package/{app → src}/lib/template-loader.js +0 -0
  227. /package/{app → src}/lib/utils.js +0 -0
  228. /package/{app → src}/lib/yaml-loader.js +0 -0
  229. /package/{app → src}/main.js +0 -0
  230. /package/{app → src}/pages/behaviour.js +0 -0
  231. /package/{app → src}/pages/discipline.js +0 -0
  232. /package/{app → src}/pages/driver.js +0 -0
  233. /package/{app → src}/pages/grade.js +0 -0
  234. /package/{app → src}/pages/interview-builder.js +0 -0
  235. /package/{app → src}/pages/progress-builder.js +0 -0
  236. /package/{app → src}/pages/progress.js +0 -0
  237. /package/{app → src}/pages/track.js +0 -0
  238. /package/{app → src}/slides/behaviour.js +0 -0
  239. /package/{app → src}/slides/discipline.js +0 -0
  240. /package/{app → src}/slides/driver.js +0 -0
  241. /package/{app → src}/slides/grade.js +0 -0
  242. /package/{app → src}/slides/interview.js +0 -0
  243. /package/{app → src}/slides/progress.js +0 -0
  244. /package/{app → src}/slides/track.js +0 -0
  245. /package/{app → src}/types.js +0 -0
@@ -14,7 +14,7 @@ import { createEntityCommand } from "./command-factory.js";
14
14
  import { trackToMarkdown } from "../formatters/track/markdown.js";
15
15
  import { sortTracksByName } from "../formatters/track/shared.js";
16
16
  import { formatTable } from "../lib/cli-output.js";
17
- import { getConceptEmoji } from "../model/levels.js";
17
+ import { getConceptEmoji } from "@forwardimpact/schema/levels";
18
18
 
19
19
  /**
20
20
  * Format track summary output
@@ -63,5 +63,5 @@ export const runTrackCommand = createEntityCommand({
63
63
  sortItems: sortTracksByName,
64
64
  formatSummary,
65
65
  formatDetail,
66
- emoji: "🛤️",
66
+ emojiIcon: "🛤️",
67
67
  });
@@ -13,6 +13,7 @@ import { div, h3, p, span } from "../lib/render.js";
13
13
  * @param {HTMLElement[]} [options.badges] - Badges to display
14
14
  * @param {HTMLElement[]} [options.meta] - Meta information
15
15
  * @param {HTMLElement} [options.content] - Additional content
16
+ * @param {HTMLElement} [options.icon] - Icon element to display
16
17
  * @param {string} [options.className] - Additional CSS class
17
18
  * @returns {HTMLElement}
18
19
  */
@@ -23,13 +24,22 @@ export function createCard({
23
24
  badges = [],
24
25
  meta = [],
25
26
  content,
27
+ icon,
26
28
  className = "",
27
29
  }) {
28
30
  const isClickable = !!href;
29
31
 
32
+ const titleContent = icon
33
+ ? div(
34
+ { className: "card-title-with-icon" },
35
+ icon,
36
+ h3({ className: "card-title" }, title),
37
+ )
38
+ : h3({ className: "card-title" }, title);
39
+
30
40
  const cardHeader = div(
31
41
  { className: "card-header" },
32
- h3({ className: "card-title" }, title),
42
+ titleContent,
33
43
  badges.length > 0 ? div({ className: "card-badges" }, ...badges) : null,
34
44
  );
35
45
 
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { div, span, details, summary } from "../lib/render.js";
9
- import { getCapabilityEmoji } from "../model/levels.js";
9
+ import { getCapabilityEmoji } from "@forwardimpact/schema/levels";
10
10
 
11
11
  /**
12
12
  * Create checklist display grouped by capability
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Code Display Component
3
+ *
4
+ * Reusable read-only code block with copy buttons and syntax highlighting.
5
+ * Used for markdown content, agent profiles, skills, and code snippets.
6
+ */
7
+
8
+ /* global Prism */
9
+ import { div, p, span, button } from "../lib/render.js";
10
+
11
+ const COPY_LABEL = "📋 Copy";
12
+ const COPY_HTML_LABEL = "Copy as HTML";
13
+
14
+ /**
15
+ * Create a copy button that copies content to clipboard
16
+ * @param {string} content - The text content to copy
17
+ * @returns {HTMLElement}
18
+ */
19
+ export function createCopyButton(content) {
20
+ const btn = button(
21
+ {
22
+ className: "btn btn-sm copy-btn",
23
+ onClick: async () => {
24
+ try {
25
+ await navigator.clipboard.writeText(content);
26
+ btn.textContent = "✓ Copied!";
27
+ btn.classList.add("copied");
28
+ setTimeout(() => {
29
+ btn.textContent = COPY_LABEL;
30
+ btn.classList.remove("copied");
31
+ }, 2000);
32
+ } catch (err) {
33
+ console.error("Failed to copy:", err);
34
+ btn.textContent = "Copy failed";
35
+ setTimeout(() => {
36
+ btn.textContent = COPY_LABEL;
37
+ }, 2000);
38
+ }
39
+ },
40
+ },
41
+ COPY_LABEL,
42
+ );
43
+ return btn;
44
+ }
45
+
46
+ /**
47
+ * Create a copy button that copies HTML to clipboard (for rich text pasting)
48
+ * @param {string} html - The HTML content to copy
49
+ * @returns {HTMLElement}
50
+ */
51
+ function createCopyHtmlButton(html) {
52
+ const btn = button(
53
+ {
54
+ className: "btn btn-sm btn-secondary copy-btn",
55
+ onClick: async () => {
56
+ try {
57
+ const blob = new Blob([html], { type: "text/html" });
58
+ const clipboardItem = new ClipboardItem({ "text/html": blob });
59
+ await navigator.clipboard.write([clipboardItem]);
60
+ btn.textContent = "✓ Copied!";
61
+ btn.classList.add("copied");
62
+ setTimeout(() => {
63
+ btn.textContent = COPY_HTML_LABEL;
64
+ btn.classList.remove("copied");
65
+ }, 2000);
66
+ } catch (err) {
67
+ console.error("Failed to copy:", err);
68
+ btn.textContent = "Copy failed";
69
+ setTimeout(() => {
70
+ btn.textContent = COPY_HTML_LABEL;
71
+ }, 2000);
72
+ }
73
+ },
74
+ },
75
+ COPY_HTML_LABEL,
76
+ );
77
+ return btn;
78
+ }
79
+
80
+ /**
81
+ * Create a code display component with syntax highlighting and copy button
82
+ * @param {Object} options
83
+ * @param {string} options.content - The code content to display
84
+ * @param {string} [options.language="markdown"] - Language for syntax highlighting
85
+ * @param {string} [options.filename] - Optional filename to display in header
86
+ * @param {string} [options.description] - Optional description text
87
+ * @param {Function} [options.toHtml] - Function to convert content to HTML (enables "Copy as HTML" button)
88
+ * @param {number} [options.minHeight] - Optional minimum height in pixels
89
+ * @param {number} [options.maxHeight] - Optional maximum height in pixels
90
+ * @returns {HTMLElement}
91
+ */
92
+ export function createCodeDisplay({
93
+ content,
94
+ language = "markdown",
95
+ filename,
96
+ description,
97
+ toHtml,
98
+ minHeight,
99
+ maxHeight,
100
+ }) {
101
+ // Create highlighted code block
102
+ const pre = document.createElement("pre");
103
+ pre.className = "code-display";
104
+ if (minHeight) pre.style.minHeight = `${minHeight}px`;
105
+ if (maxHeight) {
106
+ pre.style.maxHeight = `${maxHeight}px`;
107
+ pre.style.overflowY = "auto";
108
+ }
109
+
110
+ const code = document.createElement("code");
111
+ if (language) {
112
+ code.className = `language-${language}`;
113
+ }
114
+ code.textContent = content;
115
+ pre.appendChild(code);
116
+
117
+ // Apply Prism highlighting if available and language specified
118
+ if (language && typeof Prism !== "undefined") {
119
+ Prism.highlightElement(code);
120
+ }
121
+
122
+ // Build header content
123
+ const headerLeft = [];
124
+ if (filename) {
125
+ headerLeft.push(span({ className: "code-display-filename" }, filename));
126
+ }
127
+ if (description) {
128
+ headerLeft.push(p({ className: "text-muted" }, description));
129
+ }
130
+
131
+ // Build buttons
132
+ const buttons = [createCopyButton(content)];
133
+ if (toHtml) {
134
+ buttons.push(createCopyHtmlButton(toHtml(content)));
135
+ }
136
+
137
+ // Only show header if there's content for it
138
+ const hasHeader = headerLeft.length > 0 || buttons.length > 0;
139
+
140
+ return div(
141
+ { className: "code-display-container" },
142
+ hasHeader
143
+ ? div(
144
+ { className: "code-display-header" },
145
+ headerLeft.length > 0
146
+ ? div({ className: "code-display-info" }, ...headerLeft)
147
+ : null,
148
+ div({ className: "button-group" }, ...buttons),
149
+ )
150
+ : null,
151
+ pre,
152
+ );
153
+ }
@@ -13,7 +13,7 @@ import {
13
13
  getBehaviourMaturityIndex,
14
14
  formatLevel,
15
15
  } from "../lib/render.js";
16
- import { getCapabilityIndex } from "../model/levels.js";
16
+ import { getCapabilityIndex } from "@forwardimpact/schema/levels";
17
17
 
18
18
  /**
19
19
  * Create a comparison skill radar chart
@@ -23,7 +23,7 @@ import { formatLevel } from "../lib/render.js";
23
23
  import {
24
24
  SKILL_LEVEL_ORDER,
25
25
  BEHAVIOUR_MATURITY_ORDER,
26
- } from "../model/levels.js";
26
+ } from "@forwardimpact/schema/levels";
27
27
 
28
28
  /**
29
29
  * Create a detail page header
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Code Display Component
3
+ *
4
+ * Reusable read-only code block with copy buttons and syntax highlighting.
5
+ * Used for markdown content, agent profiles, skills, and code snippets.
6
+ */
7
+
8
+ /* global Prism */
9
+ import { div, p, span, button } from "../lib/render.js";
10
+
11
+ const COPY_LABEL = "📋 Copy";
12
+ const COPY_HTML_LABEL = "Copy as HTML";
13
+
14
+ /**
15
+ * Create a copy button that copies content to clipboard
16
+ * @param {string} content - The text content to copy
17
+ * @returns {HTMLElement}
18
+ */
19
+ export function createCopyButton(content) {
20
+ const btn = button(
21
+ {
22
+ className: "btn btn-sm copy-btn",
23
+ onClick: async () => {
24
+ try {
25
+ await navigator.clipboard.writeText(content);
26
+ btn.textContent = "✓ Copied!";
27
+ btn.classList.add("copied");
28
+ setTimeout(() => {
29
+ btn.textContent = COPY_LABEL;
30
+ btn.classList.remove("copied");
31
+ }, 2000);
32
+ } catch (err) {
33
+ console.error("Failed to copy:", err);
34
+ btn.textContent = "Copy failed";
35
+ setTimeout(() => {
36
+ btn.textContent = COPY_LABEL;
37
+ }, 2000);
38
+ }
39
+ },
40
+ },
41
+ COPY_LABEL,
42
+ );
43
+ return btn;
44
+ }
45
+
46
+ /**
47
+ * Create a copy button that copies HTML to clipboard (for rich text pasting)
48
+ * @param {string} html - The HTML content to copy
49
+ * @returns {HTMLElement}
50
+ */
51
+ function createCopyHtmlButton(html) {
52
+ const btn = button(
53
+ {
54
+ className: "btn btn-sm btn-secondary copy-btn",
55
+ onClick: async () => {
56
+ try {
57
+ const blob = new Blob([html], { type: "text/html" });
58
+ const clipboardItem = new ClipboardItem({ "text/html": blob });
59
+ await navigator.clipboard.write([clipboardItem]);
60
+ btn.textContent = "✓ Copied!";
61
+ btn.classList.add("copied");
62
+ setTimeout(() => {
63
+ btn.textContent = COPY_HTML_LABEL;
64
+ btn.classList.remove("copied");
65
+ }, 2000);
66
+ } catch (err) {
67
+ console.error("Failed to copy:", err);
68
+ btn.textContent = "Copy failed";
69
+ setTimeout(() => {
70
+ btn.textContent = COPY_HTML_LABEL;
71
+ }, 2000);
72
+ }
73
+ },
74
+ },
75
+ COPY_HTML_LABEL,
76
+ );
77
+ return btn;
78
+ }
79
+
80
+ /**
81
+ * Create a code display component with syntax highlighting and copy button
82
+ * @param {Object} options
83
+ * @param {string} options.content - The code content to display
84
+ * @param {string} [options.language="markdown"] - Language for syntax highlighting
85
+ * @param {string} [options.filename] - Optional filename to display in header
86
+ * @param {string} [options.description] - Optional description text
87
+ * @param {Function} [options.toHtml] - Function to convert content to HTML (enables "Copy as HTML" button)
88
+ * @param {number} [options.minHeight] - Optional minimum height in pixels
89
+ * @param {number} [options.maxHeight] - Optional maximum height in pixels
90
+ * @returns {HTMLElement}
91
+ */
92
+ export function createCodeDisplay({
93
+ content,
94
+ language = "markdown",
95
+ filename,
96
+ description,
97
+ toHtml,
98
+ minHeight,
99
+ maxHeight,
100
+ }) {
101
+ // Create highlighted code block
102
+ const pre = document.createElement("pre");
103
+ pre.className = "code-display";
104
+ if (minHeight) pre.style.minHeight = `${minHeight}px`;
105
+ if (maxHeight) {
106
+ pre.style.maxHeight = `${maxHeight}px`;
107
+ pre.style.overflowY = "auto";
108
+ }
109
+
110
+ const code = document.createElement("code");
111
+ if (language) {
112
+ code.className = `language-${language}`;
113
+ }
114
+ code.textContent = content;
115
+ pre.appendChild(code);
116
+
117
+ // Apply Prism highlighting if available and language specified
118
+ if (language && typeof Prism !== "undefined") {
119
+ Prism.highlightElement(code);
120
+ }
121
+
122
+ // Build header content
123
+ const headerLeft = [];
124
+ if (filename) {
125
+ headerLeft.push(span({ className: "code-display-filename" }, filename));
126
+ }
127
+ if (description) {
128
+ headerLeft.push(p({ className: "text-muted" }, description));
129
+ }
130
+
131
+ // Build buttons
132
+ const buttons = [createCopyButton(content)];
133
+ if (toHtml) {
134
+ buttons.push(createCopyHtmlButton(toHtml(content)));
135
+ }
136
+
137
+ // Only show header if there's content for it
138
+ const hasHeader = headerLeft.length > 0 || buttons.length > 0;
139
+
140
+ return div(
141
+ { className: "code-display-container" },
142
+ hasHeader
143
+ ? div(
144
+ { className: "code-display-header" },
145
+ headerLeft.length > 0
146
+ ? div({ className: "code-display-info" }, ...headerLeft)
147
+ : null,
148
+ div({ className: "button-group" }, ...buttons),
149
+ )
150
+ : null,
151
+ pre,
152
+ );
153
+ }
@@ -18,7 +18,7 @@ import {
18
18
  import { getSkillLevelIndex } from "../lib/render.js";
19
19
  import { createLevelCell } from "./detail.js";
20
20
  import { createBadge } from "./card.js";
21
- import { SKILL_LEVEL_ORDER } from "../model/levels.js";
21
+ import { SKILL_LEVEL_ORDER } from "@forwardimpact/schema/levels";
22
22
  import { truncate } from "../formatters/shared.js";
23
23
 
24
24
  /**
@@ -38,3 +38,17 @@
38
38
  @import "../pages/self-assessment.css" layer(pages);
39
39
  @import "../pages/assessment-results.css" layer(pages);
40
40
  @import "../pages/progress-builder.css" layer(pages);
41
+
42
+ /**
43
+ * Prism overrides (unlayered to beat Prism's unlayered CSS)
44
+ */
45
+ pre.code-display[class*="language-"] {
46
+ font-family: var(--font-family-mono);
47
+ font-size: var(--font-size-xs);
48
+ background-color: var(--color-surface);
49
+ }
50
+
51
+ pre.code-display > code[class*="language-"] {
52
+ font-family: inherit;
53
+ font-size: inherit;
54
+ }
@@ -5,10 +5,13 @@
5
5
  */
6
6
 
7
7
  @layer components {
8
- /* Base badge */
8
+ /* Base badge - 28px height matches tool icons for consistent row heights */
9
9
  .badge {
10
- display: inline-block;
11
- padding: var(--space-xs) var(--space-sm);
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ height: 28px;
14
+ padding: 0 var(--space-sm);
12
15
  border-radius: var(--radius-md);
13
16
  font-size: var(--font-size-xs);
14
17
  font-weight: 500;
@@ -78,10 +81,13 @@
78
81
  color: #5b21b6;
79
82
  }
80
83
 
81
- /* Level badge base style */
84
+ /* Level badge base style - 28px height matches tool icons */
82
85
  .level-badge {
83
- display: inline-block;
84
- padding: var(--space-xs) var(--space-sm);
86
+ display: inline-flex;
87
+ align-items: center;
88
+ justify-content: center;
89
+ height: 28px;
90
+ padding: 0 var(--space-sm);
85
91
  border-radius: var(--radius-md);
86
92
  font-size: var(--font-size-sm);
87
93
  font-weight: 500;
@@ -142,11 +148,12 @@
142
148
  padding: var(--space-xs) var(--space-sm);
143
149
  }
144
150
 
145
- /* Modifier tags - consolidated definition */
151
+ /* Modifier tags - 28px height matches badges and tool icons */
146
152
  .modifier {
147
153
  display: inline-flex;
148
154
  align-items: center;
149
- padding: var(--space-xs) var(--space-sm);
155
+ height: 28px;
156
+ padding: 0 var(--space-sm);
150
157
  border-radius: var(--radius-sm);
151
158
  font-size: var(--font-size-sm);
152
159
  font-weight: 600;
@@ -103,14 +103,14 @@
103
103
  color: var(--color-primary);
104
104
  }
105
105
 
106
- /* Markdown display - read-only code block with copy buttons */
107
- .markdown-textarea-container {
106
+ /* Code display - unified component for code/markdown with copy buttons */
107
+ .code-display-container {
108
108
  display: flex;
109
109
  flex-direction: column;
110
- gap: var(--space-md);
110
+ gap: var(--space-sm);
111
111
  }
112
112
 
113
- .markdown-textarea-header {
113
+ .code-display-header {
114
114
  display: flex;
115
115
  justify-content: space-between;
116
116
  align-items: center;
@@ -118,21 +118,32 @@
118
118
  gap: var(--space-md);
119
119
  }
120
120
 
121
- .markdown-textarea-header .text-muted {
122
- margin: 0;
121
+ .code-display-info {
122
+ display: flex;
123
+ flex-direction: column;
124
+ gap: var(--space-xs);
123
125
  flex: 1;
124
126
  min-width: 200px;
125
127
  }
126
128
 
127
- .markdown-display {
129
+ .code-display-info .text-muted {
130
+ margin: 0;
131
+ }
132
+
133
+ .code-display-filename {
134
+ font-family: var(--font-family-mono);
135
+ font-size: var(--font-size-xs);
136
+ color: var(--color-text-muted);
137
+ }
138
+
139
+ .code-display {
128
140
  width: 100%;
129
141
  margin: 0;
130
142
  padding: var(--space-md);
131
- font-family:
132
- ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
133
- font-size: var(--font-size-sm);
143
+ font-family: var(--font-family-mono);
144
+ font-size: var(--font-size-xs);
134
145
  line-height: 1.6;
135
- background-color: var(--color-surface) !important;
146
+ background-color: var(--color-surface);
136
147
  border: 1px solid var(--color-border);
137
148
  border-radius: var(--radius-md);
138
149
  overflow: auto;
@@ -141,10 +152,9 @@
141
152
  word-wrap: break-word;
142
153
  }
143
154
 
144
- .markdown-display code {
155
+ .code-display code {
145
156
  background: transparent;
146
157
  padding: 0;
147
- font-size: inherit;
148
158
  color: inherit;
149
159
  }
150
160
  }
@@ -40,6 +40,30 @@
40
40
  font-size: var(--font-size-lg);
41
41
  }
42
42
 
43
+ .card-title-with-icon {
44
+ display: flex;
45
+ align-items: center;
46
+ gap: var(--space-sm);
47
+ }
48
+
49
+ .card-title-with-icon .card-title {
50
+ margin: 0;
51
+ }
52
+
53
+ /* Tool icons */
54
+ .tool-icon {
55
+ width: 28px;
56
+ height: 28px;
57
+ flex-shrink: 0;
58
+ }
59
+
60
+ /* Tool name cell with icon */
61
+ .tool-name-cell {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: var(--space-sm);
65
+ }
66
+
43
67
  .card-description {
44
68
  color: var(--color-text-muted);
45
69
  margin: 0;
@@ -122,7 +146,7 @@
122
146
  .section-title {
123
147
  font-size: var(--font-size-xl);
124
148
  font-weight: 600;
125
- margin: 0 0 var(--space-sm);
149
+ margin: 0 0 var(--space-md);
126
150
  color: var(--color-text);
127
151
  }
128
152
 
@@ -209,11 +233,33 @@
209
233
  font-weight: 500;
210
234
  }
211
235
 
236
+ /*
237
+ * List items: 60px row height = 16px padding-top + 28px content + 16px padding-bottom
238
+ * Uses bottom-border only for 1px gaps between items (like table rows)
239
+ * Left/right borders and rounded corners on first/last items match table styling
240
+ */
212
241
  .list-item {
213
- padding: var(--space-sm);
242
+ display: flex;
243
+ align-items: center;
244
+ gap: var(--space-sm);
245
+ min-height: 60px;
246
+ padding: var(--space-md) var(--space-lg);
214
247
  background: var(--color-surface);
215
- border-radius: var(--radius-md);
216
248
  border: 1px solid var(--color-border);
249
+ border-top: none;
250
+ }
251
+
252
+ /* First list-item in a sequence (handles heading before list items) */
253
+ .list-item:first-child,
254
+ :not(.list-item) + .list-item {
255
+ border-top: 1px solid var(--color-border);
256
+ border-top-left-radius: var(--radius-lg);
257
+ border-top-right-radius: var(--radius-lg);
258
+ }
259
+
260
+ .list-item:last-child {
261
+ border-bottom-left-radius: var(--radius-lg);
262
+ border-bottom-right-radius: var(--radius-lg);
217
263
  }
218
264
 
219
265
  /* Accent borders */
@@ -111,8 +111,7 @@
111
111
 
112
112
  /* Code inline */
113
113
  .code-inline {
114
- font-family:
115
- ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
114
+ font-family: var(--font-family-mono);
116
115
  font-size: 0.9em;
117
116
  background: var(--color-bg);
118
117
  padding: 0.1em 0.4em;