@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.
- package/app/commands/agent.js +20 -20
- package/app/commands/index.js +4 -3
- package/app/commands/job.js +9 -4
- package/app/commands/skill.js +56 -2
- package/app/commands/tool.js +112 -0
- package/app/components/builder.js +6 -3
- package/app/components/checklist.js +6 -4
- package/app/components/markdown-textarea.js +132 -0
- package/app/css/components/forms.css +45 -0
- package/app/css/components/layout.css +12 -0
- package/app/css/components/surfaces.css +22 -0
- package/app/css/pages/detail.css +50 -0
- package/app/css/pages/job-builder.css +0 -42
- package/app/formatters/agent/profile.js +61 -120
- package/app/formatters/agent/skill.js +48 -60
- package/app/formatters/grade/dom.js +2 -4
- package/app/formatters/job/description.js +74 -82
- package/app/formatters/job/dom.js +45 -179
- package/app/formatters/job/markdown.js +17 -13
- package/app/formatters/shared.js +65 -2
- package/app/formatters/skill/dom.js +57 -2
- package/app/formatters/skill/markdown.js +18 -0
- package/app/formatters/skill/shared.js +12 -4
- package/app/formatters/stage/microdata.js +1 -1
- package/app/formatters/stage/shared.js +1 -1
- package/app/formatters/tool/shared.js +72 -0
- package/app/handout-main.js +7 -7
- package/app/handout.html +7 -0
- package/app/index.html +10 -3
- package/app/lib/card-mappers.js +64 -17
- package/app/lib/form-controls.js +64 -1
- package/app/lib/render.js +12 -1
- package/app/lib/template-loader.js +9 -0
- package/app/lib/yaml-loader.js +12 -1
- package/app/main.js +4 -0
- package/app/model/agent.js +26 -18
- package/app/model/derivation.js +3 -3
- package/app/model/levels.js +2 -0
- package/app/model/loader.js +12 -1
- package/app/model/validation.js +74 -8
- package/app/pages/agent-builder.js +8 -5
- package/app/pages/job.js +28 -4
- package/app/pages/landing.js +34 -14
- package/app/pages/progress.js +6 -5
- package/app/pages/self-assessment.js +10 -8
- package/app/pages/skill.js +5 -17
- package/app/pages/stage.js +10 -6
- package/app/pages/tool.js +50 -0
- package/app/slides/index.js +25 -25
- package/app/slides.html +7 -0
- package/bin/pathway.js +41 -27
- package/examples/capabilities/business.yaml +17 -17
- package/examples/capabilities/delivery.yaml +51 -36
- package/examples/capabilities/reliability.yaml +127 -114
- package/examples/capabilities/scale.yaml +38 -36
- package/examples/disciplines/engineering_management.yaml +1 -1
- package/examples/framework.yaml +12 -0
- package/examples/grades.yaml +18 -19
- package/examples/self-assessments.yaml +1 -1
- package/package.json +1 -1
- package/templates/job.template.md +47 -0
- package/templates/skill.template.md +31 -12
package/app/commands/agent.js
CHANGED
|
@@ -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>]
|
|
13
|
+
* npx pathway agent <discipline> [--track=<track>]
|
|
12
14
|
* npx pathway agent <discipline> --track=<track> --stage=plan
|
|
13
|
-
* npx pathway agent <discipline> --track=<track> --
|
|
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 --
|
|
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
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
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
|
-
//
|
|
488
|
-
|
|
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(
|
|
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
|
}
|
package/app/commands/index.js
CHANGED
|
@@ -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";
|
package/app/commands/job.js
CHANGED
|
@@ -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
|
-
|
|
173
|
+
// Load job template for description formatting
|
|
174
|
+
const jobTemplate = await loadJobTemplate(dataDir);
|
|
175
|
+
formatJob(view, options, { discipline, grade, track }, jobTemplate);
|
|
171
176
|
}
|
package/app/commands/skill.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
219
|
+
createDisciplineSelect({
|
|
217
220
|
id: "discipline-select",
|
|
218
|
-
|
|
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}
|
|
93
|
+
* @param {string} capabilityId - Capability ID
|
|
94
|
+
* @param {Array} capabilities - Capabilities array
|
|
94
95
|
* @returns {string}
|
|
95
96
|
*/
|
|
96
|
-
function formatCapabilityName(
|
|
97
|
-
|
|
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);
|
package/app/css/pages/detail.css
CHANGED
|
@@ -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
|
}
|