@forwardimpact/pathway 0.2.0 → 0.4.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 (62) hide show
  1. package/app/commands/agent.js +20 -20
  2. package/app/commands/index.js +4 -3
  3. package/app/commands/job.js +9 -4
  4. package/app/commands/skill.js +56 -2
  5. package/app/commands/tool.js +112 -0
  6. package/app/components/builder.js +6 -3
  7. package/app/components/checklist.js +6 -4
  8. package/app/components/markdown-textarea.js +132 -0
  9. package/app/css/components/forms.css +45 -0
  10. package/app/css/components/layout.css +12 -0
  11. package/app/css/components/surfaces.css +22 -0
  12. package/app/css/pages/detail.css +50 -0
  13. package/app/css/pages/job-builder.css +0 -42
  14. package/app/formatters/agent/profile.js +61 -120
  15. package/app/formatters/agent/skill.js +48 -60
  16. package/app/formatters/grade/dom.js +2 -4
  17. package/app/formatters/job/description.js +74 -82
  18. package/app/formatters/job/dom.js +45 -179
  19. package/app/formatters/job/markdown.js +17 -13
  20. package/app/formatters/shared.js +65 -2
  21. package/app/formatters/skill/dom.js +57 -2
  22. package/app/formatters/skill/markdown.js +18 -0
  23. package/app/formatters/skill/shared.js +12 -4
  24. package/app/formatters/stage/microdata.js +1 -1
  25. package/app/formatters/stage/shared.js +1 -1
  26. package/app/formatters/tool/shared.js +72 -0
  27. package/app/handout-main.js +7 -7
  28. package/app/handout.html +7 -0
  29. package/app/index.html +10 -3
  30. package/app/lib/card-mappers.js +64 -17
  31. package/app/lib/form-controls.js +64 -1
  32. package/app/lib/render.js +12 -1
  33. package/app/lib/template-loader.js +9 -0
  34. package/app/lib/yaml-loader.js +12 -1
  35. package/app/main.js +4 -0
  36. package/app/model/agent.js +26 -18
  37. package/app/model/derivation.js +3 -3
  38. package/app/model/levels.js +2 -0
  39. package/app/model/loader.js +12 -1
  40. package/app/model/validation.js +74 -8
  41. package/app/pages/agent-builder.js +8 -5
  42. package/app/pages/job.js +28 -4
  43. package/app/pages/landing.js +34 -14
  44. package/app/pages/progress.js +6 -5
  45. package/app/pages/self-assessment.js +10 -8
  46. package/app/pages/skill.js +5 -17
  47. package/app/pages/stage.js +10 -6
  48. package/app/pages/tool.js +50 -0
  49. package/app/slides/index.js +25 -25
  50. package/app/slides.html +7 -0
  51. package/bin/pathway.js +41 -27
  52. package/examples/capabilities/business.yaml +17 -17
  53. package/examples/capabilities/delivery.yaml +51 -36
  54. package/examples/capabilities/reliability.yaml +127 -114
  55. package/examples/capabilities/scale.yaml +38 -36
  56. package/examples/disciplines/engineering_management.yaml +1 -1
  57. package/examples/framework.yaml +12 -0
  58. package/examples/grades.yaml +18 -19
  59. package/examples/self-assessments.yaml +1 -1
  60. package/package.json +1 -1
  61. package/templates/job.template.md +47 -0
  62. package/templates/skill.template.md +31 -12
@@ -7,16 +7,18 @@
7
7
  * All agents are stage-specific. Use --stage for a single stage
8
8
  * or --all-stages (default) for all stages.
9
9
  *
10
+ * By default, outputs to console. Use --output to write files.
11
+ *
10
12
  * Usage:
11
- * npx pathway agent <discipline> [--track=<track>] [--output=PATH] [--preview]
13
+ * npx pathway agent <discipline> [--track=<track>]
12
14
  * npx pathway agent <discipline> --track=<track> --stage=plan
13
- * npx pathway agent <discipline> --track=<track> --all-stages
15
+ * npx pathway agent <discipline> --track=<track> --output=./agents
14
16
  * npx pathway agent --list
15
17
  *
16
18
  * Examples:
17
19
  * npx pathway agent software_engineering --track=platform
18
20
  * npx pathway agent software_engineering --track=platform --stage=plan
19
- * npx pathway agent software_engineering --track=platform --preview
21
+ * npx pathway agent software_engineering --track=platform --output=./agents
20
22
  */
21
23
 
22
24
  import { writeFile, mkdir, readFile } from "fs/promises";
@@ -32,10 +34,7 @@ import {
32
34
  deriveAgentSkills,
33
35
  generateSkillMd,
34
36
  } from "../model/agent.js";
35
- import {
36
- formatAgentProfile,
37
- formatAgentProfileForCli,
38
- } from "../formatters/agent/profile.js";
37
+ import { formatAgentProfile } from "../formatters/agent/profile.js";
39
38
  import { formatAgentSkill } from "../formatters/agent/skill.js";
40
39
  import { formatError, formatSuccess } from "../lib/cli-output.js";
41
40
  import {
@@ -410,14 +409,15 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
410
409
  process.exit(1);
411
410
  }
412
411
 
413
- // Preview or write
414
- if (options.preview) {
415
- console.log(formatAgentProfileForCli(profile));
412
+ // Load template
413
+ const agentTemplate = await loadAgentTemplate(dataDir);
414
+
415
+ // Output to console (default) or write to files (with --output)
416
+ if (!options.output) {
417
+ console.log(formatAgentProfile(profile, agentTemplate));
416
418
  return;
417
419
  }
418
420
 
419
- // Load templates only when writing files
420
- const agentTemplate = await loadAgentTemplate(dataDir);
421
421
  await writeProfile(profile, baseDir, agentTemplate);
422
422
  await generateVSCodeSettings(baseDir, agentData.vscodeSettings);
423
423
  await generateDevcontainer(
@@ -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) {
@@ -484,19 +484,19 @@ export async function runAgentCommand({ data, args, options, dataDir }) {
484
484
  }
485
485
  }
486
486
 
487
- // Preview or write
488
- if (options.preview) {
487
+ // Load templates
488
+ const agentTemplate = await loadAgentTemplate(dataDir);
489
+ const skillTemplate = await loadSkillTemplate(dataDir);
490
+
491
+ // Output to console (default) or write to files (with --output)
492
+ if (!options.output) {
489
493
  for (const profile of profiles) {
490
- console.log(formatAgentProfileForCli(profile));
494
+ console.log(formatAgentProfile(profile, agentTemplate));
491
495
  console.log("\n---\n");
492
496
  }
493
497
  return;
494
498
  }
495
499
 
496
- // Load templates only when writing files
497
- const agentTemplate = await loadAgentTemplate(dataDir);
498
- const skillTemplate = await loadSkillTemplate(dataDir);
499
-
500
500
  for (const profile of profiles) {
501
501
  await writeProfile(profile, baseDir, agentTemplate);
502
502
  }
@@ -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";
@@ -20,15 +20,17 @@ import {
20
20
  deriveChecklist,
21
21
  formatChecklistMarkdown,
22
22
  } from "../model/checklist.js";
23
+ import { loadJobTemplate } from "../lib/template-loader.js";
23
24
 
24
25
  /**
25
26
  * Format job output
26
27
  * @param {Object} view - Presenter view
27
28
  * @param {Object} _options - Command options
28
29
  * @param {Object} entities - Original entities
30
+ * @param {string} jobTemplate - Mustache template for job description
29
31
  */
30
- function formatJob(view, _options, entities) {
31
- console.log(jobToMarkdown(view, entities));
32
+ function formatJob(view, _options, entities, jobTemplate) {
33
+ console.log(jobToMarkdown(view, entities, jobTemplate));
32
34
  }
33
35
 
34
36
  /**
@@ -37,8 +39,9 @@ function formatJob(view, _options, entities) {
37
39
  * @param {Object} params.data - All loaded data
38
40
  * @param {string[]} params.args - Command arguments
39
41
  * @param {Object} params.options - Command options
42
+ * @param {string} params.dataDir - Path to data directory
40
43
  */
41
- export async function runJobCommand({ data, args, options }) {
44
+ export async function runJobCommand({ data, args, options, dataDir }) {
42
45
  const jobs = generateAllJobs({
43
46
  disciplines: data.disciplines,
44
47
  grades: data.grades,
@@ -167,5 +170,7 @@ export async function runJobCommand({ data, args, options }) {
167
170
  return;
168
171
  }
169
172
 
170
- formatJob(view, options, { discipline, grade, track });
173
+ // Load job template for description formatting
174
+ const jobTemplate = await loadJobTemplate(dataDir);
175
+ formatJob(view, options, { discipline, grade, track }, jobTemplate);
171
176
  }
@@ -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),
@@ -74,3 +100,31 @@ export const runSkillCommand = createEntityCommand({
74
100
  formatDetail,
75
101
  emoji: "📚",
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
+ }
@@ -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
+ }
@@ -17,7 +17,10 @@ import {
17
17
  } from "../lib/render.js";
18
18
  import { getState } from "../lib/state.js";
19
19
  import { createBadge } from "./card.js";
20
- import { createSelectWithValue } from "../lib/form-controls.js";
20
+ import {
21
+ createSelectWithValue,
22
+ createDisciplineSelect,
23
+ } from "../lib/form-controls.js";
21
24
  import { createReactive } from "../lib/reactive.js";
22
25
 
23
26
  /**
@@ -213,9 +216,9 @@ export function createBuilder({
213
216
  div(
214
217
  { className: "form-group" },
215
218
  label({ className: "form-label" }, labels.discipline || "Discipline"),
216
- createSelectWithValue({
219
+ createDisciplineSelect({
217
220
  id: "discipline-select",
218
- items: data.disciplines,
221
+ disciplines: data.disciplines,
219
222
  initialValue: selection.get().discipline,
220
223
  placeholder: "Select a discipline...",
221
224
  onChange: (value) => {
@@ -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,132 @@
1
+ /**
2
+ * Markdown Textarea Component
3
+ *
4
+ * Reusable read-only textarea with copy buttons for displaying markdown content.
5
+ * Used by job descriptions and skill implementation patterns.
6
+ */
7
+
8
+ /* global Prism */
9
+ import { div, p, button } from "../lib/render.js";
10
+
11
+ /**
12
+ * Create a copy button that copies content to clipboard
13
+ * @param {string} content - The text content to copy
14
+ * @param {string} label - Button label text
15
+ * @param {string} [className="btn btn-primary"] - Button class
16
+ * @returns {HTMLElement}
17
+ */
18
+ export function createCopyButton(
19
+ content,
20
+ label,
21
+ className = "btn btn-primary",
22
+ ) {
23
+ const btn = button(
24
+ {
25
+ className: `${className} copy-btn`,
26
+ onClick: async () => {
27
+ try {
28
+ await navigator.clipboard.writeText(content);
29
+ btn.textContent = "✓ Copied!";
30
+ btn.classList.add("copied");
31
+ setTimeout(() => {
32
+ btn.textContent = label;
33
+ btn.classList.remove("copied");
34
+ }, 2000);
35
+ } catch (err) {
36
+ console.error("Failed to copy:", err);
37
+ btn.textContent = "Copy failed";
38
+ setTimeout(() => {
39
+ btn.textContent = label;
40
+ }, 2000);
41
+ }
42
+ },
43
+ },
44
+ label,
45
+ );
46
+ return btn;
47
+ }
48
+
49
+ /**
50
+ * Create a copy button that copies HTML to clipboard (for rich text pasting)
51
+ * @param {string} html - The HTML content to copy
52
+ * @param {string} label - Button label text
53
+ * @returns {HTMLElement}
54
+ */
55
+ export function createCopyHtmlButton(html, label) {
56
+ const btn = button(
57
+ {
58
+ className: "btn btn-secondary copy-btn",
59
+ onClick: async () => {
60
+ try {
61
+ const blob = new Blob([html], { type: "text/html" });
62
+ const clipboardItem = new ClipboardItem({ "text/html": blob });
63
+ await navigator.clipboard.write([clipboardItem]);
64
+ btn.textContent = "✓ Copied!";
65
+ btn.classList.add("copied");
66
+ setTimeout(() => {
67
+ btn.textContent = label;
68
+ btn.classList.remove("copied");
69
+ }, 2000);
70
+ } catch (err) {
71
+ console.error("Failed to copy:", err);
72
+ btn.textContent = "Copy failed";
73
+ setTimeout(() => {
74
+ btn.textContent = label;
75
+ }, 2000);
76
+ }
77
+ },
78
+ },
79
+ label,
80
+ );
81
+ return btn;
82
+ }
83
+
84
+ /**
85
+ * Create a markdown textarea with copy buttons
86
+ * @param {Object} options
87
+ * @param {string} options.markdown - The markdown content to display
88
+ * @param {string} [options.description] - Optional description text above the textarea
89
+ * @param {string} [options.copyLabel="Copy Markdown"] - Label for the copy button
90
+ * @param {Function} [options.toHtml] - Optional function to convert markdown to HTML for rich copy
91
+ * @param {string} [options.copyHtmlLabel="Copy as HTML"] - Label for the HTML copy button
92
+ * @param {number} [options.minHeight=300] - Minimum height in pixels
93
+ * @returns {HTMLElement}
94
+ */
95
+ export function createMarkdownTextarea({
96
+ markdown,
97
+ description,
98
+ copyLabel = "Copy Markdown",
99
+ toHtml,
100
+ copyHtmlLabel = "Copy as HTML",
101
+ minHeight = 300,
102
+ }) {
103
+ // Create highlighted code block
104
+ const pre = document.createElement("pre");
105
+ pre.className = "markdown-display";
106
+ pre.style.minHeight = `${minHeight}px`;
107
+
108
+ const code = document.createElement("code");
109
+ code.className = "language-markdown";
110
+ code.textContent = markdown;
111
+ pre.appendChild(code);
112
+
113
+ // Apply Prism highlighting if available
114
+ if (typeof Prism !== "undefined") {
115
+ Prism.highlightElement(code);
116
+ }
117
+
118
+ const buttons = [createCopyButton(markdown, copyLabel)];
119
+ if (toHtml) {
120
+ buttons.push(createCopyHtmlButton(toHtml(markdown), copyHtmlLabel));
121
+ }
122
+
123
+ return div(
124
+ { className: "markdown-textarea-container" },
125
+ div(
126
+ { className: "markdown-textarea-header" },
127
+ description ? p({ className: "text-muted" }, description) : null,
128
+ div({ className: "button-group" }, ...buttons),
129
+ ),
130
+ pre,
131
+ );
132
+ }
@@ -102,4 +102,49 @@
102
102
  background: var(--color-primary-light, rgba(37, 99, 235, 0.1));
103
103
  color: var(--color-primary);
104
104
  }
105
+
106
+ /* Markdown display - read-only code block with copy buttons */
107
+ .markdown-textarea-container {
108
+ display: flex;
109
+ flex-direction: column;
110
+ gap: var(--space-md);
111
+ }
112
+
113
+ .markdown-textarea-header {
114
+ display: flex;
115
+ justify-content: space-between;
116
+ align-items: center;
117
+ flex-wrap: wrap;
118
+ gap: var(--space-md);
119
+ }
120
+
121
+ .markdown-textarea-header .text-muted {
122
+ margin: 0;
123
+ flex: 1;
124
+ min-width: 200px;
125
+ }
126
+
127
+ .markdown-display {
128
+ width: 100%;
129
+ margin: 0;
130
+ padding: var(--space-md);
131
+ font-family:
132
+ ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
133
+ font-size: var(--font-size-sm);
134
+ line-height: 1.6;
135
+ background-color: var(--color-surface) !important;
136
+ border: 1px solid var(--color-border);
137
+ border-radius: var(--radius-md);
138
+ overflow: auto;
139
+ color: var(--color-text);
140
+ white-space: pre-wrap;
141
+ word-wrap: break-word;
142
+ }
143
+
144
+ .markdown-display code {
145
+ background: transparent;
146
+ padding: 0;
147
+ font-size: inherit;
148
+ color: inherit;
149
+ }
105
150
  }
@@ -95,6 +95,10 @@
95
95
  grid-template-columns: repeat(3, 1fr);
96
96
  }
97
97
 
98
+ .grid-4 {
99
+ grid-template-columns: repeat(4, 1fr);
100
+ }
101
+
98
102
  .grid-6 {
99
103
  grid-template-columns: repeat(6, 1fr);
100
104
  }
@@ -174,6 +178,10 @@
174
178
  grid-template-columns: repeat(2, 1fr);
175
179
  }
176
180
 
181
+ .grid-4 {
182
+ grid-template-columns: repeat(2, 1fr);
183
+ }
184
+
177
185
  .grid-6 {
178
186
  grid-template-columns: repeat(3, 1fr);
179
187
  }
@@ -198,6 +206,10 @@
198
206
  grid-template-columns: 1fr;
199
207
  }
200
208
 
209
+ .grid-4 {
210
+ grid-template-columns: repeat(2, 1fr);
211
+ }
212
+
201
213
  .grid-6 {
202
214
  grid-template-columns: repeat(2, 1fr);
203
215
  }
@@ -53,6 +53,28 @@
53
53
  margin-top: var(--space-md);
54
54
  }
55
55
 
56
+ /* Tool skills list (inside cards) */
57
+ .tool-skills-list {
58
+ list-style: none;
59
+ padding: 0;
60
+ margin: var(--space-sm) 0 0 0;
61
+ font-size: var(--font-size-sm);
62
+ }
63
+
64
+ .tool-skills-list li {
65
+ padding: var(--space-xs) 0;
66
+ }
67
+
68
+ .tool-skills-list a {
69
+ color: var(--color-text-secondary);
70
+ text-decoration: none;
71
+ }
72
+
73
+ .tool-skills-list a:hover {
74
+ color: var(--color-primary);
75
+ text-decoration: underline;
76
+ }
77
+
56
78
  /* Stat cards */
57
79
  .stat-card {
58
80
  background: var(--color-surface);
@@ -56,4 +56,54 @@
56
56
  align-items: center;
57
57
  gap: var(--space-xs);
58
58
  }
59
+
60
+ /* Tools table (for skill detail page) */
61
+ .tools-table {
62
+ width: 100%;
63
+ border-collapse: separate;
64
+ border-spacing: 0;
65
+ margin-top: var(--space-md);
66
+ border: 1px solid var(--color-border);
67
+ border-radius: var(--radius-lg);
68
+ overflow: hidden;
69
+ background: var(--color-surface);
70
+ }
71
+
72
+ .tools-table th,
73
+ .tools-table td {
74
+ padding: var(--space-md) var(--space-lg);
75
+ text-align: left;
76
+ border-bottom: 1px solid var(--color-border);
77
+ }
78
+
79
+ .tools-table th {
80
+ font-weight: 600;
81
+ font-size: var(--font-size-sm);
82
+ color: var(--color-text-secondary);
83
+ background: var(--color-bg);
84
+ text-transform: uppercase;
85
+ letter-spacing: 0.025em;
86
+ }
87
+
88
+ .tools-table th:first-child {
89
+ width: 200px;
90
+ }
91
+
92
+ .tools-table tbody tr:last-child td {
93
+ border-bottom: none;
94
+ }
95
+
96
+ .tools-table tbody tr:hover {
97
+ background: var(--color-bg);
98
+ }
99
+
100
+ /* See all link */
101
+ .see-all-link {
102
+ margin-top: var(--space-md);
103
+ }
104
+
105
+ .see-all-link a {
106
+ color: var(--color-primary);
107
+ font-size: var(--font-size-sm);
108
+ }
59
109
  }