@forwardimpact/pathway 0.3.0 → 0.5.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 (90) hide show
  1. package/app/commands/agent.js +1 -1
  2. package/app/commands/behaviour.js +1 -1
  3. package/app/commands/command-factory.js +2 -2
  4. package/app/commands/discipline.js +1 -1
  5. package/app/commands/driver.js +1 -1
  6. package/app/commands/grade.js +1 -1
  7. package/app/commands/index.js +4 -3
  8. package/app/commands/serve.js +2 -2
  9. package/app/commands/site.js +22 -2
  10. package/app/commands/skill.js +57 -3
  11. package/app/commands/stage.js +1 -1
  12. package/app/commands/tool.js +112 -0
  13. package/app/commands/track.js +1 -1
  14. package/app/components/card.js +11 -1
  15. package/app/components/checklist.js +6 -4
  16. package/app/components/code-display.js +153 -0
  17. package/app/components/markdown-textarea.js +153 -0
  18. package/app/css/bundles/app.css +14 -0
  19. package/app/css/components/badges.css +15 -8
  20. package/app/css/components/forms.css +55 -0
  21. package/app/css/components/layout.css +12 -0
  22. package/app/css/components/surfaces.css +71 -3
  23. package/app/css/components/typography.css +1 -2
  24. package/app/css/pages/agent-builder.css +11 -102
  25. package/app/css/pages/detail.css +60 -0
  26. package/app/css/pages/job-builder.css +0 -42
  27. package/app/css/tokens.css +3 -0
  28. package/app/formatters/agent/dom.js +26 -71
  29. package/app/formatters/agent/profile.js +67 -10
  30. package/app/formatters/agent/skill.js +48 -6
  31. package/app/formatters/grade/dom.js +6 -6
  32. package/app/formatters/job/description.js +21 -16
  33. package/app/formatters/job/dom.js +9 -70
  34. package/app/formatters/json-ld.js +1 -1
  35. package/app/formatters/shared.js +58 -0
  36. package/app/formatters/skill/dom.js +70 -3
  37. package/app/formatters/skill/markdown.js +18 -0
  38. package/app/formatters/skill/shared.js +14 -4
  39. package/app/formatters/stage/microdata.js +2 -2
  40. package/app/formatters/stage/shared.js +3 -3
  41. package/app/formatters/tool/shared.js +78 -0
  42. package/app/handout-main.js +19 -18
  43. package/app/index.html +16 -3
  44. package/app/lib/card-mappers.js +91 -17
  45. package/app/lib/render.js +4 -0
  46. package/app/lib/yaml-loader.js +12 -1
  47. package/app/main.js +4 -0
  48. package/app/model/agent.js +47 -23
  49. package/app/model/checklist.js +2 -2
  50. package/app/model/derivation.js +5 -5
  51. package/app/model/levels.js +4 -2
  52. package/app/model/loader.js +12 -1
  53. package/app/model/validation.js +77 -11
  54. package/app/pages/agent-builder.js +121 -77
  55. package/app/pages/landing.js +35 -15
  56. package/app/pages/self-assessment.js +7 -5
  57. package/app/pages/skill.js +5 -17
  58. package/app/pages/stage.js +12 -8
  59. package/app/pages/tool.js +50 -0
  60. package/app/slide-main.js +1 -1
  61. package/app/slides/chapter.js +8 -8
  62. package/app/slides/index.js +26 -26
  63. package/app/slides/overview.js +8 -8
  64. package/app/slides/skill.js +1 -0
  65. package/bin/pathway.js +31 -16
  66. package/examples/capabilities/business.yaml +18 -18
  67. package/examples/capabilities/delivery.yaml +54 -37
  68. package/examples/capabilities/people.yaml +1 -1
  69. package/examples/capabilities/reliability.yaml +130 -115
  70. package/examples/capabilities/scale.yaml +39 -37
  71. package/examples/disciplines/engineering_management.yaml +1 -1
  72. package/examples/framework.yaml +21 -9
  73. package/examples/grades.yaml +5 -7
  74. package/examples/self-assessments.yaml +1 -1
  75. package/examples/stages.yaml +18 -10
  76. package/package.json +2 -1
  77. package/templates/agent.template.md +47 -17
  78. package/templates/job.template.md +8 -8
  79. package/templates/skill.template.md +33 -11
  80. package/examples/agents/.claude/skills/architecture-design/SKILL.md +0 -130
  81. package/examples/agents/.claude/skills/cloud-platforms/SKILL.md +0 -131
  82. package/examples/agents/.claude/skills/code-quality-review/SKILL.md +0 -108
  83. package/examples/agents/.claude/skills/devops-cicd/SKILL.md +0 -142
  84. package/examples/agents/.claude/skills/full-stack-development/SKILL.md +0 -134
  85. package/examples/agents/.claude/skills/sre-practices/SKILL.md +0 -163
  86. package/examples/agents/.claude/skills/technical-debt-management/SKILL.md +0 -164
  87. package/examples/agents/.github/agents/se-platform-code.agent.md +0 -132
  88. package/examples/agents/.github/agents/se-platform-plan.agent.md +0 -131
  89. package/examples/agents/.github/agents/se-platform-review.agent.md +0 -136
  90. package/examples/agents/.vscode/settings.json +0 -8
@@ -454,7 +454,7 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
454
454
  const skillFiles = derivedSkills
455
455
  .map((derived) => skillsWithAgent.find((s) => s.id === derived.skillId))
456
456
  .filter((skill) => skill?.agent)
457
- .map((skill) => generateSkillMd(skill));
457
+ .map((skill) => generateSkillMd(skill, data.stages));
458
458
 
459
459
  // Validate all profiles
460
460
  for (const profile of profiles) {
@@ -57,5 +57,5 @@ export const runBehaviourCommand = createEntityCommand({
57
57
  }),
58
58
  formatSummary,
59
59
  formatDetail,
60
- emoji: "🧠",
60
+ emojiIcon: "🧠",
61
61
  });
@@ -24,7 +24,7 @@ import { capitalize } from "../formatters/shared.js";
24
24
  * @param {Function} config.formatDetail - Function to format detail output: (view, framework) => void
25
25
  * @param {Function} [config.sortItems] - Optional function to sort items: (items) => sortedItems
26
26
  * @param {Function} [config.validate] - Optional validation function: (data) => {errors: [], warnings: []}
27
- * @param {string} [config.emoji] - Optional emoji for the entity
27
+ * @param {string} [config.emojiIcon] - Optional emoji for the entity
28
28
  * @returns {Function} Command handler
29
29
  */
30
30
  export function createEntityCommand({
@@ -36,7 +36,7 @@ export function createEntityCommand({
36
36
  formatDetail,
37
37
  sortItems,
38
38
  validate,
39
- _emoji = "",
39
+ _emojiIcon = "",
40
40
  }) {
41
41
  return async function runCommand({ data, args, options }) {
42
42
  const [id] = args;
@@ -54,5 +54,5 @@ export const runDisciplineCommand = createEntityCommand({
54
54
  }),
55
55
  formatSummary,
56
56
  formatDetail,
57
- emoji: "📋",
57
+ emojiIcon: "📋",
58
58
  });
@@ -90,5 +90,5 @@ export const runDriverCommand = createEntityCommand({
90
90
  }),
91
91
  formatSummary,
92
92
  formatDetail,
93
- emoji: "🎯",
93
+ emojiIcon: "🎯",
94
94
  });
@@ -56,5 +56,5 @@ export const runGradeCommand = createEntityCommand({
56
56
  presentDetail: (entity) => entity,
57
57
  formatSummary,
58
58
  formatDetail,
59
- emoji: "📊",
59
+ emojiIcon: "📊",
60
60
  });
@@ -4,13 +4,14 @@
4
4
  * Re-exports all command handlers for convenient importing.
5
5
  */
6
6
 
7
- export { runSkillCommand } from "./skill.js";
8
- export { runBehaviourCommand } from "./behaviour.js";
9
- export { runDriverCommand } from "./driver.js";
10
7
  export { runDisciplineCommand } from "./discipline.js";
11
8
  export { runGradeCommand } from "./grade.js";
12
9
  export { runTrackCommand } from "./track.js";
10
+ export { runBehaviourCommand } from "./behaviour.js";
11
+ export { runSkillCommand } from "./skill.js";
12
+ export { runDriverCommand } from "./driver.js";
13
13
  export { runStageCommand } from "./stage.js";
14
+ export { runToolCommand } from "./tool.js";
14
15
  export { runJobCommand } from "./job.js";
15
16
  export { runInterviewCommand } from "./interview.js";
16
17
  export { runProgressCommand } from "./progress.js";
@@ -94,7 +94,7 @@ export async function runServeCommand({ dataDir, options }) {
94
94
  framework = await loadFrameworkConfig(dataDir);
95
95
  } catch {
96
96
  // Fallback if framework config fails
97
- framework = { emoji: "🚀", title: "Engineering Pathway" };
97
+ framework = { emojiIcon: "🚀", title: "Engineering Pathway" };
98
98
  }
99
99
 
100
100
  // Generate _index.yaml files before serving
@@ -136,7 +136,7 @@ export async function runServeCommand({ dataDir, options }) {
136
136
 
137
137
  server.listen(port, () => {
138
138
  console.log(`
139
- ${framework.emoji} ${framework.title} running at http://localhost:${port}
139
+ ${framework.emojiIcon} ${framework.title} running at http://localhost:${port}
140
140
  📁 Data directory: ${dataDir}
141
141
 
142
142
  Press Ctrl+C to stop the server.
@@ -38,6 +38,11 @@ const PUBLIC_ASSETS = [
38
38
  "formatters",
39
39
  ];
40
40
 
41
+ /**
42
+ * Files and directories to copy from project root
43
+ */
44
+ const ROOT_ASSETS = ["templates"];
45
+
41
46
  /**
42
47
  * Run the site command
43
48
  * @param {Object} params - Command parameters
@@ -53,11 +58,11 @@ export async function runSiteCommand({ dataDir, options }) {
53
58
  try {
54
59
  framework = await loadFrameworkConfig(dataDir);
55
60
  } catch {
56
- framework = { emoji: "🚀", title: "Engineering Pathway" };
61
+ framework = { emojiIcon: "🚀", title: "Engineering Pathway" };
57
62
  }
58
63
 
59
64
  console.log(`
60
- ${framework.emoji} Generating ${framework.title} static site...
65
+ ${framework.emojiIcon} Generating ${framework.title} static site...
61
66
  `);
62
67
 
63
68
  // Clean output directory if requested
@@ -93,6 +98,21 @@ ${framework.emoji} Generating ${framework.title} static site...
93
98
  }
94
99
  }
95
100
 
101
+ // Copy root assets (templates, etc.)
102
+ const rootDir = join(appDir, "..");
103
+ for (const asset of ROOT_ASSETS) {
104
+ const src = join(rootDir, asset);
105
+ const dest = join(outputDir, asset);
106
+
107
+ try {
108
+ await access(src);
109
+ await cp(src, dest, { recursive: true });
110
+ console.log(` ✓ ${asset}`);
111
+ } catch (err) {
112
+ console.log(` ⚠️ Skipped ${asset}: ${err.message}`);
113
+ }
114
+ }
115
+
96
116
  // Copy data directory (dereference symlinks to copy actual content)
97
117
  console.log("📁 Copying data files...");
98
118
  const dataOutputDir = join(outputDir, "data");
@@ -7,6 +7,7 @@
7
7
  * npx pathway skill # Summary with stats
8
8
  * npx pathway skill --list # IDs only (for piping)
9
9
  * npx pathway skill <id> # Detail view
10
+ * npx pathway skill <id> --agent # Agent SKILL.md output
10
11
  * npx pathway skill --validate # Validation checks
11
12
  */
12
13
 
@@ -14,7 +15,10 @@ import { createEntityCommand } from "./command-factory.js";
14
15
  import { skillToMarkdown } from "../formatters/skill/markdown.js";
15
16
  import { prepareSkillsList } from "../formatters/skill/shared.js";
16
17
  import { getConceptEmoji } from "../model/levels.js";
17
- import { formatTable } from "../lib/cli-output.js";
18
+ import { formatTable, formatError } from "../lib/cli-output.js";
19
+ import { generateSkillMd } from "../model/agent.js";
20
+ import { formatAgentSkill } from "../formatters/agent/skill.js";
21
+ import { loadSkillTemplate } from "../lib/template-loader.js";
18
22
 
19
23
  /**
20
24
  * Format skill summary output
@@ -59,7 +63,29 @@ function formatDetail(viewAndContext, framework) {
59
63
  );
60
64
  }
61
65
 
62
- export const runSkillCommand = createEntityCommand({
66
+ /**
67
+ * Format skill as agent SKILL.md output
68
+ * @param {Object} skill - Skill entity with agent section
69
+ * @param {Array} stages - All stage entities
70
+ * @param {string} dataDir - Path to data directory for template loading
71
+ */
72
+ async function formatAgentDetail(skill, stages, dataDir) {
73
+ if (!skill.agent) {
74
+ console.error(formatError(`Skill '${skill.id}' has no agent section`));
75
+ console.error(`\nSkills with agent support:`);
76
+ console.error(
77
+ ` npx pathway skill --list | xargs -I{} sh -c 'npx pathway skill {} --json | jq -e .skill.agent > /dev/null && echo {}'`,
78
+ );
79
+ process.exit(1);
80
+ }
81
+
82
+ const template = await loadSkillTemplate(dataDir);
83
+ const skillMd = generateSkillMd(skill, stages);
84
+ const output = formatAgentSkill(skillMd, template);
85
+ console.log(output);
86
+ }
87
+
88
+ const baseSkillCommand = createEntityCommand({
63
89
  entityName: "skill",
64
90
  pluralName: "skills",
65
91
  findEntity: (data, id) => data.skills.find((s) => s.id === id),
@@ -72,5 +98,33 @@ export const runSkillCommand = createEntityCommand({
72
98
  }),
73
99
  formatSummary,
74
100
  formatDetail,
75
- emoji: "📚",
101
+ emojiIcon: "📚",
76
102
  });
103
+
104
+ /**
105
+ * Run skill command with --agent support
106
+ * @param {Object} params - Command parameters
107
+ * @param {Object} params.data - Loaded pathway data
108
+ * @param {string[]} params.args - Command arguments
109
+ * @param {Object} params.options - Command options
110
+ * @param {string} params.dataDir - Path to data directory
111
+ */
112
+ export async function runSkillCommand({ data, args, options, dataDir }) {
113
+ // Handle --agent flag for detail view
114
+ if (options.agent && args.length > 0) {
115
+ const [id] = args;
116
+ const skill = data.skills.find((s) => s.id === id);
117
+
118
+ if (!skill) {
119
+ console.error(formatError(`Skill not found: ${id}`));
120
+ console.error(`Available: ${data.skills.map((s) => s.id).join(", ")}`);
121
+ process.exit(1);
122
+ }
123
+
124
+ await formatAgentDetail(skill, data.stages, dataDir);
125
+ return;
126
+ }
127
+
128
+ // Delegate to base command for all other cases
129
+ return baseSkillCommand({ data, args, options, dataDir });
130
+ }
@@ -115,5 +115,5 @@ export const runStageCommand = createEntityCommand({
115
115
  }),
116
116
  formatSummary,
117
117
  formatDetail,
118
- emoji: "🔄",
118
+ emojiIcon: "🔄",
119
119
  });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Tool CLI Command
3
+ *
4
+ * Handles tool summary, listing, and detail display in the terminal.
5
+ *
6
+ * Usage:
7
+ * npx pathway tool # Summary with stats
8
+ * npx pathway tool --list # Tool names only (for piping)
9
+ * npx pathway tool <name> # Detail view for specific tool
10
+ */
11
+
12
+ import { prepareToolsList } from "../formatters/tool/shared.js";
13
+ import {
14
+ formatTable,
15
+ formatHeader,
16
+ formatSubheader,
17
+ } from "../lib/cli-output.js";
18
+
19
+ /**
20
+ * Run tool command
21
+ * @param {Object} params - Command parameters
22
+ * @param {Object} params.data - Loaded pathway data
23
+ * @param {string[]} params.args - Command arguments
24
+ * @param {Object} params.options - Command options
25
+ */
26
+ export async function runToolCommand({ data, args, options }) {
27
+ const [name] = args;
28
+ const { tools, totalCount } = prepareToolsList(data.skills);
29
+
30
+ // --list: Output clean newline-separated tool names for piping
31
+ if (options.list) {
32
+ for (const tool of tools) {
33
+ console.log(tool.name);
34
+ }
35
+ return;
36
+ }
37
+
38
+ // No args: Show summary
39
+ if (!name) {
40
+ if (options.json) {
41
+ console.log(JSON.stringify(tools, null, 2));
42
+ return;
43
+ }
44
+ formatSummary(tools, totalCount);
45
+ return;
46
+ }
47
+
48
+ // With name: Show detail
49
+ const tool = tools.find((t) => t.name.toLowerCase() === name.toLowerCase());
50
+
51
+ if (!tool) {
52
+ console.error(`Tool not found: ${name}`);
53
+ console.error(`Available: ${tools.map((t) => t.name).join(", ")}`);
54
+ process.exit(1);
55
+ }
56
+
57
+ if (options.json) {
58
+ console.log(JSON.stringify(tool, null, 2));
59
+ return;
60
+ }
61
+
62
+ formatDetail(tool);
63
+ }
64
+
65
+ /**
66
+ * Format tool summary output
67
+ * @param {Array} tools - Aggregated tools
68
+ * @param {number} totalCount - Total tool count
69
+ */
70
+ function formatSummary(tools, totalCount) {
71
+ console.log(`\n🔧 Tools\n`);
72
+
73
+ // Show tools sorted by usage count
74
+ const sorted = [...tools].sort((a, b) => b.usages.length - a.usages.length);
75
+ const rows = sorted
76
+ .slice(0, 15)
77
+ .map((t) => [
78
+ t.name,
79
+ t.usages.length,
80
+ t.description.length > 50
81
+ ? t.description.slice(0, 47) + "..."
82
+ : t.description,
83
+ ]);
84
+
85
+ console.log(formatTable(["Tool", "Skills", "Description"], rows));
86
+ console.log(`\nTotal: ${totalCount} tools`);
87
+ if (sorted.length > 15) {
88
+ console.log(`(showing top 15 by usage)`);
89
+ }
90
+ console.log(`\nRun 'npx pathway tool --list' for all tool names`);
91
+ console.log(`Run 'npx pathway tool <name>' for details\n`);
92
+ }
93
+
94
+ /**
95
+ * Format tool detail output
96
+ * @param {Object} tool - Aggregated tool with usages
97
+ */
98
+ function formatDetail(tool) {
99
+ console.log(formatHeader(`\n🔧 ${tool.name}\n`));
100
+ console.log(`${tool.description}\n`);
101
+
102
+ if (tool.url) {
103
+ console.log(`Documentation: ${tool.url}\n`);
104
+ }
105
+
106
+ if (tool.usages.length > 0) {
107
+ console.log(formatSubheader("Used in Skills\n"));
108
+ const rows = tool.usages.map((u) => [u.skillName, u.useWhen]);
109
+ console.log(formatTable(["Skill", "Use When"], rows));
110
+ console.log();
111
+ }
112
+ }
@@ -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
 
@@ -43,7 +43,7 @@ export function createChecklist(checklist, options = {}) {
43
43
  function createChecklistGroup(group, options) {
44
44
  const { interactive, capabilities } = options;
45
45
  const emoji = getCapabilityEmoji(capabilities, group.capability);
46
- const capabilityName = formatCapabilityName(group.capability);
46
+ const capabilityName = formatCapabilityName(group.capability, capabilities);
47
47
 
48
48
  return div(
49
49
  { className: "checklist-group" },
@@ -90,11 +90,13 @@ function createInteractiveCheckbox() {
90
90
 
91
91
  /**
92
92
  * Format capability name for display
93
- * @param {string} capability - Capability ID
93
+ * @param {string} capabilityId - Capability ID
94
+ * @param {Array} capabilities - Capabilities array
94
95
  * @returns {string}
95
96
  */
96
- function formatCapabilityName(capability) {
97
- return capability.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
97
+ function formatCapabilityName(capabilityId, capabilities) {
98
+ const capability = capabilities.find((c) => c.id === capabilityId);
99
+ return capability?.name || capabilityId;
98
100
  }
99
101
 
100
102
  /**
@@ -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
+ }