@forwardimpact/pathway 0.17.0 → 0.17.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forwardimpact/pathway",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Career progression web app and CLI for exploring roles and generating agent teams",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,106 @@
1
+ /**
2
+ * File Card Component
3
+ *
4
+ * Unified card component for displaying one or more files with collapsible
5
+ * code panes. Replaces separate agent-card and skill-card patterns with a
6
+ * single bordered surface containing a header and accordion file panes.
7
+ *
8
+ * One component, one DOM pattern, one set of styles.
9
+ */
10
+
11
+ /* global Prism */
12
+ import { div, span } from "../lib/render.js";
13
+ import { createCopyButton } from "./code-display.js";
14
+
15
+ /**
16
+ * @typedef {Object} FileDescriptor
17
+ * @property {string} filename - Display filename for the pane summary
18
+ * @property {string} content - File content to display
19
+ * @property {string} [language="markdown"] - Syntax highlighting language
20
+ */
21
+
22
+ /**
23
+ * Create a file card with header and collapsible file panes.
24
+ *
25
+ * Agent card = createFileCard with 1 file (1 pane, open).
26
+ * Skill card = createFileCard with 1–3 files (accordion).
27
+ *
28
+ * @param {Object} options
29
+ * @param {Array<HTMLElement|string>} options.header - Elements for the card header
30
+ * @param {FileDescriptor[]} options.files - Files to display as collapsible panes
31
+ * @param {number} [options.maxHeight=300] - Max code block height in px
32
+ * @param {number} [options.openIndex=0] - Initially open pane (-1 = all closed)
33
+ * @returns {HTMLElement}
34
+ */
35
+ export function createFileCard({
36
+ header,
37
+ files,
38
+ maxHeight = 300,
39
+ openIndex = 0,
40
+ }) {
41
+ const card = div(
42
+ { className: "file-card" },
43
+ div({ className: "file-card-header" }, ...header),
44
+ );
45
+
46
+ /** @type {HTMLDetailsElement[]} */
47
+ const panes = [];
48
+
49
+ for (let i = 0; i < files.length; i++) {
50
+ const file = files[i];
51
+
52
+ const details = document.createElement("details");
53
+ details.className = "file-card-pane";
54
+ if (i === openIndex) details.open = true;
55
+
56
+ const summary = document.createElement("summary");
57
+ summary.appendChild(
58
+ span({ className: "file-card-filename" }, file.filename),
59
+ );
60
+ summary.appendChild(
61
+ div({ className: "button-group" }, createCopyButton(file.content)),
62
+ );
63
+ details.appendChild(summary);
64
+
65
+ // Lazy-render code block on first open
66
+ let rendered = false;
67
+ const renderCode = () => {
68
+ if (rendered) return;
69
+ rendered = true;
70
+ const pre = document.createElement("pre");
71
+ pre.className = "code-display";
72
+ if (maxHeight) {
73
+ pre.style.maxHeight = `${maxHeight}px`;
74
+ pre.style.overflowY = "auto";
75
+ }
76
+ const code = document.createElement("code");
77
+ code.className = `language-${file.language || "markdown"}`;
78
+ code.textContent = file.content;
79
+ pre.appendChild(code);
80
+ if (typeof Prism !== "undefined") Prism.highlightElement(code);
81
+ details.appendChild(pre);
82
+ };
83
+
84
+ if (i === openIndex) renderCode();
85
+ else
86
+ details.addEventListener("toggle", () => {
87
+ if (details.open) renderCode();
88
+ });
89
+
90
+ panes.push(details);
91
+ card.appendChild(details);
92
+ }
93
+
94
+ // Accordion: opening one pane closes others
95
+ for (const pane of panes) {
96
+ pane.addEventListener("toggle", () => {
97
+ if (pane.open) {
98
+ for (const other of panes) {
99
+ if (other !== pane) other.open = false;
100
+ }
101
+ }
102
+ });
103
+ }
104
+
105
+ return card;
106
+ }
@@ -27,6 +27,7 @@
27
27
  @import "../components/top-bar.css" layer(components);
28
28
  @import "../components/command-prompt.css" layer(components);
29
29
  @import "../components/skill-file-viewer.css" layer(components);
30
+ @import "../components/file-card.css" layer(components);
30
31
 
31
32
  /* Utilities */
32
33
  @import "../components/utilities.css" layer(utilities);
@@ -0,0 +1,109 @@
1
+ /**
2
+ * File Card
3
+ *
4
+ * Unified card surface for displaying files with collapsible panes.
5
+ * Single bordered surface — no nested borders from code-display or wrappers.
6
+ */
7
+
8
+ @layer components {
9
+ /* The card — single bordered surface */
10
+ .file-card {
11
+ border: 1px solid var(--color-border);
12
+ border-radius: var(--radius-md);
13
+ background: var(--color-surface);
14
+ overflow: hidden;
15
+ }
16
+
17
+ /* Card header — always visible */
18
+ .file-card-header {
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ gap: var(--space-sm);
23
+ padding: var(--space-sm) var(--space-md);
24
+ border-bottom: 1px solid var(--color-border);
25
+ }
26
+
27
+ .file-card-header h3 {
28
+ margin: 0;
29
+ font-size: var(--font-size-md);
30
+ }
31
+
32
+ /* File panes — flush inside card, no chrome */
33
+ .file-card-pane + .file-card-pane {
34
+ border-top: 1px solid var(--color-border);
35
+ }
36
+
37
+ .file-card-pane > summary {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: var(--space-sm);
41
+ padding: var(--space-sm) var(--space-md);
42
+ cursor: pointer;
43
+ list-style: none;
44
+ }
45
+
46
+ .file-card-pane > summary::-webkit-details-marker {
47
+ display: none;
48
+ }
49
+
50
+ .file-card-pane > summary::before {
51
+ content: "▸";
52
+ font-size: var(--font-size-xs);
53
+ color: var(--color-text-muted);
54
+ flex-shrink: 0;
55
+ }
56
+
57
+ .file-card-pane[open] > summary::before {
58
+ content: "▾";
59
+ }
60
+
61
+ .file-card-filename {
62
+ flex: 1;
63
+ min-width: 0;
64
+ overflow: hidden;
65
+ text-overflow: ellipsis;
66
+ white-space: nowrap;
67
+ font-family: var(--font-family-mono);
68
+ font-size: var(--font-size-xs);
69
+ color: var(--color-text-muted);
70
+ }
71
+
72
+ .file-card-pane > summary .button-group {
73
+ flex-shrink: 0;
74
+ }
75
+
76
+ /* Code block inside file card — no border, no radius, no bg */
77
+ .file-card-pane .code-display {
78
+ border: none;
79
+ border-radius: 0;
80
+ background: transparent;
81
+ margin: 0;
82
+ }
83
+
84
+ /* Header element helpers */
85
+ .file-card-emoji {
86
+ font-size: 1.5rem;
87
+ }
88
+
89
+ .file-card-title {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: var(--space-sm);
93
+ }
94
+
95
+ .file-card-name {
96
+ font-weight: 600;
97
+ font-size: var(--font-size-sm);
98
+ }
99
+
100
+ .file-card-badge {
101
+ font-size: var(--font-size-xs);
102
+ color: var(--color-text-muted);
103
+ background: var(--color-bg);
104
+ border: 1px solid var(--color-border);
105
+ border-radius: var(--radius-sm);
106
+ padding: 0.1em 0.5em;
107
+ white-space: nowrap;
108
+ }
109
+ }
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Agent Builder Page Styles
3
3
  *
4
- * Agent builder form, preview, and card components.
4
+ * Agent builder form, preview, and card grid layout.
5
+ * Card surfaces are in components/file-card.css.
5
6
  */
6
7
 
7
8
  @layer pages {
@@ -32,11 +33,9 @@
32
33
  gap: var(--space-xl);
33
34
  }
34
35
 
36
+ /* Agent section — spacing container, no surface */
35
37
  .agent-section {
36
- background: var(--color-surface);
37
- border-radius: var(--radius-lg);
38
- padding: var(--space-lg);
39
- box-shadow: var(--shadow-md);
38
+ margin-bottom: var(--space-lg);
40
39
  }
41
40
 
42
41
  .agent-section .section-header {
@@ -50,13 +49,6 @@
50
49
  margin: 0;
51
50
  }
52
51
 
53
- .agent-section .filename {
54
- font-family: var(--font-family-mono);
55
- font-size: var(--font-size-xs);
56
- color: var(--color-text-muted);
57
- margin-bottom: var(--space-sm);
58
- }
59
-
60
52
  /* Agent cards grid */
61
53
  .agent-cards-grid {
62
54
  display: grid;
@@ -69,51 +61,6 @@
69
61
  max-width: 800px;
70
62
  }
71
63
 
72
- .agent-card {
73
- border: 1px solid var(--color-border);
74
- border-radius: var(--radius-lg);
75
- background: var(--color-bg);
76
- overflow: hidden;
77
- }
78
-
79
- .agent-card-header {
80
- display: flex;
81
- justify-content: space-between;
82
- align-items: flex-start;
83
- padding: var(--space-md);
84
- background: var(--color-surface);
85
- border-bottom: 1px solid var(--color-border);
86
- gap: var(--space-sm);
87
- }
88
-
89
- .agent-card-title {
90
- display: flex;
91
- align-items: center;
92
- gap: var(--space-sm);
93
- flex-wrap: wrap;
94
- }
95
-
96
- .agent-card-title h3 {
97
- margin: 0;
98
- font-size: var(--font-size-md);
99
- }
100
-
101
- .agent-card-emoji {
102
- font-size: 1.5rem;
103
- }
104
-
105
- .agent-card-preview {
106
- padding: var(--space-md);
107
- }
108
-
109
- .agent-card-preview .code-display-pane {
110
- margin: 0;
111
- }
112
-
113
- .agent-card-preview .code-display {
114
- max-height: 400px;
115
- }
116
-
117
64
  /* Skill cards grid */
118
65
  .skill-cards-grid {
119
66
  display: grid;
@@ -121,50 +68,6 @@
121
68
  gap: var(--space-md);
122
69
  }
123
70
 
124
- .skill-card {
125
- border: 1px solid var(--color-border);
126
- border-radius: var(--radius-md);
127
- background: var(--color-bg);
128
- overflow: hidden;
129
- }
130
-
131
- .skill-card-header {
132
- display: flex;
133
- justify-content: space-between;
134
- align-items: center;
135
- padding: var(--space-sm) var(--space-md);
136
- background: var(--color-surface);
137
- border-bottom: 1px solid var(--color-border);
138
- }
139
-
140
- .skill-card-name {
141
- font-weight: 600;
142
- font-size: var(--font-size-sm);
143
- }
144
-
145
- .skill-card-preview {
146
- padding: var(--space-sm) var(--space-md) var(--space-md);
147
- }
148
-
149
- .skill-card-preview .code-display-pane {
150
- margin: 0;
151
- }
152
-
153
- .skill-card-preview .code-display {
154
- max-height: 300px;
155
- }
156
-
157
- /* Skill card file count badge */
158
- .skill-card-badge {
159
- font-size: var(--font-size-xs);
160
- color: var(--color-text-muted);
161
- background: var(--color-surface);
162
- border: 1px solid var(--color-border);
163
- border-radius: var(--radius-sm);
164
- padding: 0.1em 0.5em;
165
- white-space: nowrap;
166
- }
167
-
168
71
  .skills-list {
169
72
  display: flex;
170
73
  flex-direction: column;
@@ -42,8 +42,7 @@ import {
42
42
  formatInstallScript,
43
43
  formatReference,
44
44
  } from "../formatters/agent/skill.js";
45
- import { createCodeDisplay } from "../components/code-display.js";
46
- import { createSkillFileViewer } from "../components/skill-file-viewer.js";
45
+ import { createFileCard } from "../components/file-card.js";
47
46
  import { createToolkitTable } from "../formatters/toolkit/dom.js";
48
47
  import { createDetailSection } from "../components/detail.js";
49
48
 
@@ -537,9 +536,24 @@ function createAllStagesPreview(context) {
537
536
  ),
538
537
  div(
539
538
  { className: "agent-cards-grid" },
540
- ...stageAgents.map(({ stage, profile }) =>
541
- createAgentCard(stage, profile, stages, templates.agent),
542
- ),
539
+ ...stageAgents.map(({ stage, profile }) => {
540
+ const content = formatAgentProfile(profile, templates.agent);
541
+ const stageEmoji = getStageEmoji(stages, stage.id);
542
+ return createFileCard({
543
+ header: [
544
+ span({ className: "file-card-emoji" }, stageEmoji),
545
+ h3({}, `${stage.name} Agent`),
546
+ ],
547
+ files: [
548
+ {
549
+ filename: profile.filename,
550
+ content,
551
+ language: "markdown",
552
+ },
553
+ ],
554
+ maxHeight: 400,
555
+ });
556
+ }),
543
557
  ),
544
558
  ),
545
559
 
@@ -550,7 +564,7 @@ function createAllStagesPreview(context) {
550
564
  skillFiles.length > 0
551
565
  ? div(
552
566
  { className: "skill-cards-grid" },
553
- ...skillFiles.map((skill) => createSkillCard(skill, templates)),
567
+ ...skillFiles.map((skill) => buildSkillFileCard(skill, templates)),
554
568
  )
555
569
  : p(
556
570
  { className: "text-muted" },
@@ -591,20 +605,6 @@ function createSingleStagePreview(context, stage) {
591
605
  agentIndex,
592
606
  } = context;
593
607
 
594
- // Derive stage agent
595
- const derived = deriveStageAgent({
596
- discipline: humanDiscipline,
597
- track: humanTrack,
598
- stage,
599
- grade,
600
- skills,
601
- behaviours,
602
- agentBehaviours,
603
- agentDiscipline,
604
- agentTrack,
605
- stages,
606
- });
607
-
608
608
  const profile = generateStageAgentProfile({
609
609
  discipline: humanDiscipline,
610
610
  track: humanTrack,
@@ -656,7 +656,24 @@ function createSingleStagePreview(context, stage) {
656
656
  h2({}, "Agent"),
657
657
  div(
658
658
  { className: "agent-cards-grid single" },
659
- createAgentCard(stage, profile, stages, templates.agent, derived),
659
+ (() => {
660
+ const content = formatAgentProfile(profile, templates.agent);
661
+ const stageEmoji = getStageEmoji(stages, stage.id);
662
+ return createFileCard({
663
+ header: [
664
+ span({ className: "file-card-emoji" }, stageEmoji),
665
+ h3({}, `${stage.name} Agent`),
666
+ ],
667
+ files: [
668
+ {
669
+ filename: profile.filename,
670
+ content,
671
+ language: "markdown",
672
+ },
673
+ ],
674
+ maxHeight: 400,
675
+ });
676
+ })(),
660
677
  ),
661
678
  ),
662
679
 
@@ -667,7 +684,7 @@ function createSingleStagePreview(context, stage) {
667
684
  skillFiles.length > 0
668
685
  ? div(
669
686
  { className: "skill-cards-grid" },
670
- ...skillFiles.map((skill) => createSkillCard(skill, templates)),
687
+ ...skillFiles.map((skill) => buildSkillFileCard(skill, templates)),
671
688
  )
672
689
  : p(
673
690
  { className: "text-muted" },
@@ -686,57 +703,18 @@ function createSingleStagePreview(context, stage) {
686
703
  }
687
704
 
688
705
  /**
689
- * Create an agent card for a stage
690
- * @param {Object} stage - Stage object
691
- * @param {Object} profile - Generated profile
692
- * @param {Array} stages - All stages for emoji lookup
693
- * @param {string} agentTemplate - Mustache template for agent profile
694
- * @param {Object} [_derived] - Optional derived agent data for extra info
695
- * @returns {HTMLElement}
696
- */
697
- function createAgentCard(stage, profile, stages, agentTemplate, _derived) {
698
- const content = formatAgentProfile(profile, agentTemplate);
699
- const stageEmoji = getStageEmoji(stages, stage.id);
700
-
701
- const card = div(
702
- { className: "agent-card" },
703
- div(
704
- { className: "agent-card-header" },
705
- div(
706
- { className: "agent-card-title" },
707
- span({ className: "agent-card-emoji" }, stageEmoji),
708
- h3({}, `${stage.name} Agent`),
709
- ),
710
- ),
711
- div(
712
- { className: "agent-card-preview" },
713
- createCodeDisplay({
714
- content,
715
- filename: profile.filename,
716
- maxHeight: 400,
717
- open: true,
718
- }),
719
- ),
720
- );
721
-
722
- return card;
723
- }
724
-
725
- /**
726
- * Create a skill card with tabbed file viewer
706
+ * Build a file card for a skill with 1–3 file panes (accordion).
727
707
  * @param {Object} skill - Skill with frontmatter and body
728
708
  * @param {{skill: string, install: string, reference: string}} templates - Mustache templates
729
709
  * @returns {HTMLElement}
730
710
  */
731
- function createSkillCard(skill, templates) {
711
+ function buildSkillFileCard(skill, templates) {
732
712
  const content = formatAgentSkill(skill, templates.skill);
733
- const filename = `${skill.dirname}/SKILL.md`;
734
713
 
735
- // Build files array for the tabbed viewer
736
- /** @type {import('../components/skill-file-viewer.js').SkillFile[]} */
714
+ /** @type {import('../components/file-card.js').FileDescriptor[]} */
737
715
  const files = [
738
716
  {
739
- filename,
717
+ filename: `${skill.dirname}/SKILL.md`,
740
718
  content,
741
719
  language: "markdown",
742
720
  },
@@ -758,25 +736,20 @@ function createSkillCard(skill, templates) {
758
736
  });
759
737
  }
760
738
 
761
- // Count total files for badge
762
- const fileCount = files.length;
763
739
  const headerChildren = [
764
- span({ className: "skill-card-name" }, skill.frontmatter.name),
740
+ span({ className: "file-card-name" }, skill.frontmatter.name),
765
741
  ];
766
- if (fileCount > 1) {
742
+ if (files.length > 1) {
767
743
  headerChildren.push(
768
- span({ className: "skill-card-badge" }, `${fileCount} files`),
744
+ span({ className: "file-card-badge" }, `${files.length} files`),
769
745
  );
770
746
  }
771
747
 
772
- return div(
773
- { className: "skill-card" },
774
- div({ className: "skill-card-header" }, ...headerChildren),
775
- div(
776
- { className: "skill-card-preview" },
777
- createSkillFileViewer({ files, maxHeight: 300 }),
778
- ),
779
- );
748
+ return createFileCard({
749
+ header: headerChildren,
750
+ files,
751
+ maxHeight: 300,
752
+ });
780
753
  }
781
754
 
782
755
  /**