@forwardimpact/pathway 0.13.0 → 0.14.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/bin/fit-pathway.js +26 -7
- package/package.json +2 -2
- package/src/commands/build.js +98 -2
- package/src/commands/index.js +1 -0
- package/src/commands/job.js +1 -0
- package/src/commands/stage.js +8 -8
- package/src/commands/update.js +133 -0
- package/src/components/command-prompt.js +85 -0
- package/src/components/nav.js +2 -2
- package/src/components/top-bar.js +97 -0
- package/src/css/bundles/app.css +2 -0
- package/src/css/components/command-prompt.css +98 -0
- package/src/css/components/layout.css +0 -3
- package/src/css/components/nav.css +121 -81
- package/src/css/components/surfaces.css +1 -1
- package/src/css/components/top-bar.css +180 -0
- package/src/css/pages/agent-builder.css +0 -9
- package/src/css/pages/landing.css +4 -0
- package/src/css/pages/lifecycle.css +5 -2
- package/src/css/reset.css +1 -1
- package/src/css/tokens.css +4 -2
- package/src/css/views/slide-base.css +2 -1
- package/src/formatters/agent/dom.js +0 -26
- package/src/formatters/agent/profile.js +13 -7
- package/src/formatters/agent/skill.js +4 -4
- package/src/formatters/stage/dom.js +13 -10
- package/src/formatters/stage/microdata.js +14 -8
- package/src/formatters/stage/shared.js +4 -4
- package/src/index.html +69 -44
- package/src/lib/cli-command.js +145 -0
- package/src/lib/state.js +2 -0
- package/src/main.js +47 -26
- package/src/pages/agent-builder.js +0 -28
- package/src/pages/behaviour.js +3 -1
- package/src/pages/discipline.js +3 -1
- package/src/pages/driver.js +6 -1
- package/src/pages/grade.js +6 -1
- package/src/pages/job.js +1 -0
- package/src/pages/landing.js +7 -0
- package/src/pages/skill.js +9 -2
- package/src/pages/track.js +6 -1
- package/src/slides/job.js +1 -0
- package/templates/agent.template.md +17 -10
- package/templates/install.template.sh +33 -0
- package/templates/skill.template.md +15 -7
package/bin/fit-pathway.js
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import { join, resolve } from "path";
|
|
33
33
|
import { existsSync } from "fs";
|
|
34
|
+
import { homedir } from "os";
|
|
34
35
|
import { loadAllData } from "@forwardimpact/schema/loader";
|
|
35
36
|
import { formatError } from "../src/lib/cli-output.js";
|
|
36
37
|
|
|
@@ -51,6 +52,7 @@ import { runAgentCommand } from "../src/commands/agent.js";
|
|
|
51
52
|
import { runDevCommand } from "../src/commands/dev.js";
|
|
52
53
|
import { runInitCommand } from "../src/commands/init.js";
|
|
53
54
|
import { runBuildCommand } from "../src/commands/build.js";
|
|
55
|
+
import { runUpdateCommand } from "../src/commands/update.js";
|
|
54
56
|
|
|
55
57
|
const COMMANDS = {
|
|
56
58
|
discipline: runDisciplineCommand,
|
|
@@ -86,7 +88,8 @@ GETTING STARTED
|
|
|
86
88
|
|
|
87
89
|
init Create ./data/ with example data
|
|
88
90
|
dev [--port=PORT] Run live development server
|
|
89
|
-
build [--output=PATH]
|
|
91
|
+
build [--output=PATH] [--url=URL] Generate static site + distribution bundle
|
|
92
|
+
update [--url=URL] Update local ~/.fit/pathway/ installation
|
|
90
93
|
|
|
91
94
|
────────────────────────────────────────────────────────────────────────────────
|
|
92
95
|
ENTITY COMMANDS
|
|
@@ -253,6 +256,7 @@ function parseArgs(args) {
|
|
|
253
256
|
path: null,
|
|
254
257
|
// Site command options
|
|
255
258
|
clean: true,
|
|
259
|
+
url: null,
|
|
256
260
|
};
|
|
257
261
|
|
|
258
262
|
for (const arg of args) {
|
|
@@ -308,6 +312,8 @@ function parseArgs(args) {
|
|
|
308
312
|
result.path = arg.slice(7);
|
|
309
313
|
} else if (arg === "--no-clean") {
|
|
310
314
|
result.clean = false;
|
|
315
|
+
} else if (arg.startsWith("--url=")) {
|
|
316
|
+
result.url = arg.slice(6);
|
|
311
317
|
} else if (!arg.startsWith("-")) {
|
|
312
318
|
if (!result.command) {
|
|
313
319
|
result.command = arg;
|
|
@@ -332,9 +338,10 @@ function printHelp() {
|
|
|
332
338
|
* Resolution order:
|
|
333
339
|
* 1. --data=<path> flag (explicit override)
|
|
334
340
|
* 2. PATHWAY_DATA environment variable
|
|
335
|
-
* 3.
|
|
336
|
-
* 4. ./
|
|
337
|
-
* 5.
|
|
341
|
+
* 3. ~/.fit/pathway/data/ (home directory install)
|
|
342
|
+
* 4. ./data/ relative to current working directory
|
|
343
|
+
* 5. ./examples/ relative to current working directory
|
|
344
|
+
* 6. apps/schema/examples/ for monorepo development
|
|
338
345
|
*
|
|
339
346
|
* @param {Object} options - Parsed command options
|
|
340
347
|
* @returns {string} Resolved absolute path to data directory
|
|
@@ -350,19 +357,25 @@ function resolveDataPath(options) {
|
|
|
350
357
|
return resolve(process.env.PATHWAY_DATA);
|
|
351
358
|
}
|
|
352
359
|
|
|
353
|
-
// 3.
|
|
360
|
+
// 3. Home directory install (~/.fit/pathway/data/)
|
|
361
|
+
const homeData = join(homedir(), ".fit", "pathway", "data");
|
|
362
|
+
if (existsSync(homeData)) {
|
|
363
|
+
return homeData;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 4. Current working directory ./data/
|
|
354
367
|
const cwdData = join(process.cwd(), "data");
|
|
355
368
|
if (existsSync(cwdData)) {
|
|
356
369
|
return cwdData;
|
|
357
370
|
}
|
|
358
371
|
|
|
359
|
-
//
|
|
372
|
+
// 5. Current working directory ./examples/
|
|
360
373
|
const cwdExamples = join(process.cwd(), "examples");
|
|
361
374
|
if (existsSync(cwdExamples)) {
|
|
362
375
|
return cwdExamples;
|
|
363
376
|
}
|
|
364
377
|
|
|
365
|
-
//
|
|
378
|
+
// 6. Monorepo: apps/schema/examples/
|
|
366
379
|
const schemaExamples = join(process.cwd(), "apps/schema/examples");
|
|
367
380
|
if (existsSync(schemaExamples)) {
|
|
368
381
|
return schemaExamples;
|
|
@@ -414,6 +427,12 @@ async function main() {
|
|
|
414
427
|
process.exit(0);
|
|
415
428
|
}
|
|
416
429
|
|
|
430
|
+
// Handle update command (re-downloads bundle for local install)
|
|
431
|
+
if (command === "update") {
|
|
432
|
+
await runUpdateCommand({ dataDir, options });
|
|
433
|
+
process.exit(0);
|
|
434
|
+
}
|
|
435
|
+
|
|
417
436
|
const handler = COMMANDS[command];
|
|
418
437
|
|
|
419
438
|
if (!handler) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/pathway",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Career progression web app and CLI for exploring roles and generating agents",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@forwardimpact/schema": "^0.6.0",
|
|
44
|
-
"@forwardimpact/model": "^0.
|
|
44
|
+
"@forwardimpact/model": "^0.8.0",
|
|
45
45
|
"mustache": "^4.2.0",
|
|
46
46
|
"simple-icons": "^16.7.0",
|
|
47
47
|
"yaml": "^2.3.4"
|
package/src/commands/build.js
CHANGED
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates a static site from the Engineering Pathway data.
|
|
5
5
|
* Copies all necessary files (HTML, JS, CSS) and data to an output directory.
|
|
6
|
+
* Optionally generates a distribution bundle (bundle.tar.gz + install.sh)
|
|
7
|
+
* for local installs by individual engineers.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cp,
|
|
12
|
+
mkdir,
|
|
13
|
+
rm,
|
|
14
|
+
access,
|
|
15
|
+
realpath,
|
|
16
|
+
readFile,
|
|
17
|
+
writeFile,
|
|
18
|
+
} from "fs/promises";
|
|
19
|
+
import { readFileSync } from "fs";
|
|
9
20
|
import { join, dirname, relative, resolve } from "path";
|
|
10
21
|
import { fileURLToPath } from "url";
|
|
22
|
+
import { execFileSync } from "child_process";
|
|
23
|
+
import Mustache from "mustache";
|
|
11
24
|
import { generateAllIndexes } from "@forwardimpact/schema/index-generator";
|
|
12
25
|
import { loadFrameworkConfig } from "@forwardimpact/schema/loader";
|
|
13
26
|
|
|
@@ -151,14 +164,97 @@ ${framework.emojiIcon} Generating ${framework.title} static site...
|
|
|
151
164
|
console.log(` ✓ data/ (from ${relative(process.cwd(), dataDir)})`);
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
// Generate distribution bundle if siteUrl is configured
|
|
168
|
+
const siteUrl = options.url || framework.distribution?.siteUrl;
|
|
169
|
+
if (siteUrl) {
|
|
170
|
+
await generateBundle({ outputDir, dataDir, siteUrl, framework });
|
|
171
|
+
}
|
|
172
|
+
|
|
154
173
|
// Show summary
|
|
155
174
|
console.log(`
|
|
156
175
|
✅ Site generated successfully!
|
|
157
176
|
|
|
158
177
|
Output: ${outputDir}
|
|
159
|
-
|
|
178
|
+
${siteUrl ? `\nDistribution:\n ${outputDir}/bundle.tar.gz\n ${outputDir}/install.sh\n` : ""}
|
|
160
179
|
To serve locally:
|
|
161
180
|
cd ${relative(process.cwd(), outputDir) || "."}
|
|
162
181
|
npx serve .
|
|
163
182
|
`);
|
|
164
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Read the pathway package version from package.json
|
|
187
|
+
* @returns {string} Package version
|
|
188
|
+
*/
|
|
189
|
+
function getPathwayVersion() {
|
|
190
|
+
const pkgPath = join(appDir, "..", "package.json");
|
|
191
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
192
|
+
return pkg.version;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate distribution bundle (bundle.tar.gz + install.sh)
|
|
197
|
+
* @param {Object} params
|
|
198
|
+
* @param {string} params.outputDir - Build output directory
|
|
199
|
+
* @param {string} params.dataDir - Source data directory
|
|
200
|
+
* @param {string} params.siteUrl - Base URL for the published site
|
|
201
|
+
* @param {Object} params.framework - Framework configuration
|
|
202
|
+
*/
|
|
203
|
+
async function generateBundle({ outputDir, dataDir, siteUrl, framework }) {
|
|
204
|
+
console.log("📦 Generating distribution bundle...");
|
|
205
|
+
|
|
206
|
+
const version = getPathwayVersion();
|
|
207
|
+
const frameworkTitle = framework.title || "Engineering Pathway";
|
|
208
|
+
|
|
209
|
+
// 1. Create temporary bundle directory
|
|
210
|
+
const bundleDir = join(outputDir, "_bundle");
|
|
211
|
+
await mkdir(bundleDir, { recursive: true });
|
|
212
|
+
|
|
213
|
+
// 2. Generate minimal package.json for the bundle
|
|
214
|
+
const bundlePkg = {
|
|
215
|
+
name: "fit-pathway-local",
|
|
216
|
+
version: version,
|
|
217
|
+
private: true,
|
|
218
|
+
dependencies: {
|
|
219
|
+
"@forwardimpact/pathway": `^${version}`,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
await writeFile(
|
|
223
|
+
join(bundleDir, "package.json"),
|
|
224
|
+
JSON.stringify(bundlePkg, null, 2) + "\n",
|
|
225
|
+
);
|
|
226
|
+
console.log(` ✓ package.json (pathway ^${version})`);
|
|
227
|
+
|
|
228
|
+
// 3. Copy data files into bundle
|
|
229
|
+
await cp(dataDir, join(bundleDir, "data"), {
|
|
230
|
+
recursive: true,
|
|
231
|
+
dereference: true,
|
|
232
|
+
});
|
|
233
|
+
console.log(" ✓ data/");
|
|
234
|
+
|
|
235
|
+
// 4. Create tar.gz from the bundle directory
|
|
236
|
+
execFileSync("tar", [
|
|
237
|
+
"-czf",
|
|
238
|
+
join(outputDir, "bundle.tar.gz"),
|
|
239
|
+
"-C",
|
|
240
|
+
outputDir,
|
|
241
|
+
"_bundle",
|
|
242
|
+
]);
|
|
243
|
+
console.log(" ✓ bundle.tar.gz");
|
|
244
|
+
|
|
245
|
+
// 5. Clean up temporary bundle directory
|
|
246
|
+
await rm(bundleDir, { recursive: true });
|
|
247
|
+
|
|
248
|
+
// 6. Render install.sh from template
|
|
249
|
+
const templatePath = join(appDir, "..", "templates", "install.template.sh");
|
|
250
|
+
const template = await readFile(templatePath, "utf8");
|
|
251
|
+
const installScript = Mustache.render(template, {
|
|
252
|
+
siteUrl: siteUrl.replace(/\/$/, ""),
|
|
253
|
+
version,
|
|
254
|
+
frameworkTitle,
|
|
255
|
+
});
|
|
256
|
+
await writeFile(join(outputDir, "install.sh"), installScript, {
|
|
257
|
+
mode: 0o755,
|
|
258
|
+
});
|
|
259
|
+
console.log(" ✓ install.sh");
|
|
260
|
+
}
|
package/src/commands/index.js
CHANGED
package/src/commands/job.js
CHANGED
package/src/commands/stage.js
CHANGED
|
@@ -60,19 +60,19 @@ function formatDetail(viewAndContext, _framework) {
|
|
|
60
60
|
console.log(formatHeader(`\n${emoji} ${view.name}\n`));
|
|
61
61
|
console.log(`${view.description}\n`);
|
|
62
62
|
|
|
63
|
-
//
|
|
64
|
-
if (view.
|
|
65
|
-
console.log(formatSubheader("
|
|
66
|
-
for (const item of view.
|
|
63
|
+
// Read checklist
|
|
64
|
+
if (view.readChecklist.length > 0) {
|
|
65
|
+
console.log(formatSubheader("Read-Then-Do Checklist\n"));
|
|
66
|
+
for (const item of view.readChecklist) {
|
|
67
67
|
console.log(formatBullet(item, 1));
|
|
68
68
|
}
|
|
69
69
|
console.log();
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
if (view.
|
|
74
|
-
console.log(formatSubheader("
|
|
75
|
-
for (const item of view.
|
|
72
|
+
// Confirm checklist
|
|
73
|
+
if (view.confirmChecklist.length > 0) {
|
|
74
|
+
console.log(formatSubheader("Do-Then-Confirm Checklist\n"));
|
|
75
|
+
for (const item of view.confirmChecklist) {
|
|
76
76
|
console.log(formatBullet(item, 1));
|
|
77
77
|
}
|
|
78
78
|
console.log();
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command
|
|
3
|
+
*
|
|
4
|
+
* Re-downloads the distribution bundle from the published site URL
|
|
5
|
+
* and updates the local ~/.fit/pathway/ installation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { cp, mkdir, rm, readFile, writeFile, access } from "fs/promises";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { execFileSync, execSync } from "child_process";
|
|
12
|
+
import { loadFrameworkConfig } from "@forwardimpact/schema/loader";
|
|
13
|
+
|
|
14
|
+
const INSTALL_DIR = join(homedir(), ".fit", "pathway");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run the update command.
|
|
18
|
+
* Reads siteUrl from the installed framework.yaml, re-downloads the bundle,
|
|
19
|
+
* extracts data, and runs npm install to update dependencies.
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} params - Command parameters
|
|
22
|
+
* @param {string} params.dataDir - Path to data directory (may be the installed one)
|
|
23
|
+
* @param {Object} params.options - Command options
|
|
24
|
+
*/
|
|
25
|
+
export async function runUpdateCommand({ dataDir: _dataDir, options }) {
|
|
26
|
+
const installDataDir = join(INSTALL_DIR, "data");
|
|
27
|
+
|
|
28
|
+
// Verify we have a home-directory installation
|
|
29
|
+
try {
|
|
30
|
+
await access(installDataDir);
|
|
31
|
+
} catch {
|
|
32
|
+
console.error("Error: No local installation found at ~/.fit/pathway/");
|
|
33
|
+
console.error(
|
|
34
|
+
"Install first using the install.sh script from your organization's pathway site.",
|
|
35
|
+
);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Load framework config to get siteUrl
|
|
40
|
+
const framework = await loadFrameworkConfig(installDataDir);
|
|
41
|
+
const siteUrl = options.url || framework.distribution?.siteUrl;
|
|
42
|
+
|
|
43
|
+
if (!siteUrl) {
|
|
44
|
+
console.error(
|
|
45
|
+
"Error: No siteUrl found in ~/.fit/pathway/data/framework.yaml (distribution.siteUrl)",
|
|
46
|
+
);
|
|
47
|
+
console.error("Provide one with --url=<URL> or add it to framework.yaml.");
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const baseUrl = siteUrl.replace(/\/$/, "");
|
|
52
|
+
const bundleName = "bundle.tar.gz";
|
|
53
|
+
|
|
54
|
+
console.log(`\n🔄 Updating from ${baseUrl}...\n`);
|
|
55
|
+
|
|
56
|
+
// 1. Download bundle to temp location
|
|
57
|
+
const tmpDir = join(INSTALL_DIR, "_update_tmp");
|
|
58
|
+
await mkdir(tmpDir, { recursive: true });
|
|
59
|
+
|
|
60
|
+
const tmpBundle = join(tmpDir, bundleName);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
console.log(" Downloading bundle...");
|
|
64
|
+
execFileSync("curl", [
|
|
65
|
+
"-fsSL",
|
|
66
|
+
`${baseUrl}/${bundleName}`,
|
|
67
|
+
"-o",
|
|
68
|
+
tmpBundle,
|
|
69
|
+
]);
|
|
70
|
+
console.log(" ✓ Downloaded");
|
|
71
|
+
|
|
72
|
+
// 2. Extract bundle
|
|
73
|
+
console.log(" Extracting...");
|
|
74
|
+
const extractDir = join(tmpDir, "extracted");
|
|
75
|
+
await mkdir(extractDir, { recursive: true });
|
|
76
|
+
execFileSync("tar", [
|
|
77
|
+
"-xzf",
|
|
78
|
+
tmpBundle,
|
|
79
|
+
"-C",
|
|
80
|
+
extractDir,
|
|
81
|
+
"--strip-components=1",
|
|
82
|
+
]);
|
|
83
|
+
console.log(" ✓ Extracted");
|
|
84
|
+
|
|
85
|
+
// 3. Compare versions
|
|
86
|
+
const newPkgPath = join(extractDir, "package.json");
|
|
87
|
+
const oldPkgPath = join(INSTALL_DIR, "package.json");
|
|
88
|
+
const newPkg = JSON.parse(await readFile(newPkgPath, "utf8"));
|
|
89
|
+
let oldPkg;
|
|
90
|
+
try {
|
|
91
|
+
oldPkg = JSON.parse(await readFile(oldPkgPath, "utf8"));
|
|
92
|
+
} catch {
|
|
93
|
+
oldPkg = { version: "unknown", dependencies: {} };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const oldVersion =
|
|
97
|
+
oldPkg.dependencies?.["@forwardimpact/pathway"] || "unknown";
|
|
98
|
+
const newVersion =
|
|
99
|
+
newPkg.dependencies?.["@forwardimpact/pathway"] || "unknown";
|
|
100
|
+
|
|
101
|
+
// 4. Replace data
|
|
102
|
+
console.log(" Updating data files...");
|
|
103
|
+
await rm(installDataDir, { recursive: true });
|
|
104
|
+
await cp(join(extractDir, "data"), installDataDir, { recursive: true });
|
|
105
|
+
console.log(" ✓ Data updated");
|
|
106
|
+
|
|
107
|
+
// 5. Update package.json if version changed
|
|
108
|
+
if (oldVersion !== newVersion) {
|
|
109
|
+
console.log(` Updating pathway ${oldVersion} → ${newVersion}...`);
|
|
110
|
+
await writeFile(oldPkgPath, JSON.stringify(newPkg, null, 2) + "\n");
|
|
111
|
+
console.log(" ✓ package.json updated");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 6. Run npm install
|
|
115
|
+
console.log(" Installing dependencies...");
|
|
116
|
+
execSync("npm install --production --ignore-scripts --no-audit --no-fund", {
|
|
117
|
+
cwd: INSTALL_DIR,
|
|
118
|
+
stdio: "ignore",
|
|
119
|
+
});
|
|
120
|
+
console.log(" ✓ Dependencies installed");
|
|
121
|
+
|
|
122
|
+
// 7. Report
|
|
123
|
+
console.log(`
|
|
124
|
+
✅ Update complete!
|
|
125
|
+
|
|
126
|
+
Pathway: ${oldVersion === newVersion ? newVersion + " (unchanged)" : oldVersion + " → " + newVersion}
|
|
127
|
+
Data: updated from ${baseUrl}
|
|
128
|
+
`);
|
|
129
|
+
} finally {
|
|
130
|
+
// Clean up temp directory
|
|
131
|
+
await rm(tmpDir, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Prompt Component
|
|
3
|
+
*
|
|
4
|
+
* Reusable terminal-style command display with copy button.
|
|
5
|
+
* Shows a `$` prompt, monospace command text, and a copy-to-clipboard button.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { div, span, button } from "../lib/render.js";
|
|
9
|
+
|
|
10
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create the copy icon SVG (two overlapping rectangles)
|
|
14
|
+
* @returns {SVGSVGElement}
|
|
15
|
+
*/
|
|
16
|
+
function createCopyIcon() {
|
|
17
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
18
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
19
|
+
|
|
20
|
+
const rect = document.createElementNS(SVG_NS, "rect");
|
|
21
|
+
rect.setAttribute("x", "9");
|
|
22
|
+
rect.setAttribute("y", "9");
|
|
23
|
+
rect.setAttribute("width", "13");
|
|
24
|
+
rect.setAttribute("height", "13");
|
|
25
|
+
rect.setAttribute("rx", "2");
|
|
26
|
+
|
|
27
|
+
const path = document.createElementNS(SVG_NS, "path");
|
|
28
|
+
path.setAttribute(
|
|
29
|
+
"d",
|
|
30
|
+
"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1",
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
svg.appendChild(rect);
|
|
34
|
+
svg.appendChild(path);
|
|
35
|
+
return svg;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Copy text to clipboard with fallback for older browsers
|
|
40
|
+
* @param {string} text
|
|
41
|
+
* @param {HTMLButtonElement} btn
|
|
42
|
+
*/
|
|
43
|
+
async function copyToClipboard(text, btn) {
|
|
44
|
+
try {
|
|
45
|
+
await navigator.clipboard.writeText(text);
|
|
46
|
+
} catch {
|
|
47
|
+
const textarea = document.createElement("textarea");
|
|
48
|
+
textarea.value = text;
|
|
49
|
+
textarea.style.position = "fixed";
|
|
50
|
+
textarea.style.opacity = "0";
|
|
51
|
+
document.body.appendChild(textarea);
|
|
52
|
+
textarea.select();
|
|
53
|
+
document.execCommand("copy");
|
|
54
|
+
document.body.removeChild(textarea);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
btn.classList.add("copied");
|
|
58
|
+
btn.setAttribute("aria-label", "Copied!");
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
btn.classList.remove("copied");
|
|
61
|
+
btn.setAttribute("aria-label", "Copy command");
|
|
62
|
+
}, 2000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create a command prompt element with copy button
|
|
67
|
+
* @param {string} command - The command text to display
|
|
68
|
+
* @returns {HTMLElement}
|
|
69
|
+
*/
|
|
70
|
+
export function createCommandPrompt(command) {
|
|
71
|
+
const copyBtn = button({
|
|
72
|
+
className: "command-prompt__copy",
|
|
73
|
+
"aria-label": "Copy command",
|
|
74
|
+
type: "button",
|
|
75
|
+
});
|
|
76
|
+
copyBtn.appendChild(createCopyIcon());
|
|
77
|
+
copyBtn.addEventListener("click", () => copyToClipboard(command, copyBtn));
|
|
78
|
+
|
|
79
|
+
return div(
|
|
80
|
+
{ className: "command-prompt" },
|
|
81
|
+
span({ className: "command-prompt__prompt" }, "$"),
|
|
82
|
+
span({ className: "command-prompt__text" }, command),
|
|
83
|
+
copyBtn,
|
|
84
|
+
);
|
|
85
|
+
}
|
package/src/components/nav.js
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
import { div, a } from "../lib/render.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Update the active navigation link
|
|
8
|
+
* Update the active navigation link in the drawer
|
|
9
9
|
* @param {string} path - Current path
|
|
10
10
|
*/
|
|
11
11
|
export function updateActiveNav(path) {
|
|
12
|
-
const links = document.querySelectorAll("#nav
|
|
12
|
+
const links = document.querySelectorAll("#drawer-nav a");
|
|
13
13
|
links.forEach((link) => {
|
|
14
14
|
const href = link.getAttribute("href").slice(1); // Remove #
|
|
15
15
|
const isActive = path === href || (href !== "/" && path.startsWith(href));
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top Bar Component
|
|
3
|
+
*
|
|
4
|
+
* Fixed bar across the top of the app with:
|
|
5
|
+
* - Sidebar toggle button (left)
|
|
6
|
+
* - CLI command display with copy button (center)
|
|
7
|
+
*
|
|
8
|
+
* Similar to Safari's URL bar with sidebar toggle.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getCliCommand } from "../lib/cli-command.js";
|
|
12
|
+
|
|
13
|
+
/** @type {HTMLElement|null} */
|
|
14
|
+
let commandDisplay = null;
|
|
15
|
+
|
|
16
|
+
/** @type {HTMLButtonElement|null} */
|
|
17
|
+
let copyButton = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Set up the top bar: wire toggle and initial command display.
|
|
21
|
+
* Call after DOM is ready.
|
|
22
|
+
*/
|
|
23
|
+
export function setupTopBar() {
|
|
24
|
+
const app = document.getElementById("app");
|
|
25
|
+
const toggle = document.getElementById("sidebar-toggle");
|
|
26
|
+
const commandEl = document.getElementById("cli-command");
|
|
27
|
+
const copyBtn = document.getElementById("cli-copy");
|
|
28
|
+
|
|
29
|
+
commandDisplay = commandEl;
|
|
30
|
+
copyButton = copyBtn;
|
|
31
|
+
|
|
32
|
+
if (toggle) {
|
|
33
|
+
toggle.addEventListener("click", () => {
|
|
34
|
+
app.classList.toggle("drawer-open");
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (copyBtn) {
|
|
39
|
+
copyBtn.addEventListener("click", handleCopy);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Intercept history.replaceState so CLI command updates when pages
|
|
43
|
+
// change the hash without triggering hashchange (e.g. agent builder)
|
|
44
|
+
const originalReplaceState = history.replaceState.bind(history);
|
|
45
|
+
history.replaceState = (...args) => {
|
|
46
|
+
originalReplaceState(...args);
|
|
47
|
+
updateCommand();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Set initial command
|
|
51
|
+
updateCommand();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Update the CLI command display for the current route.
|
|
56
|
+
* Call on every route change.
|
|
57
|
+
*/
|
|
58
|
+
export function updateCommand() {
|
|
59
|
+
if (!commandDisplay) return;
|
|
60
|
+
const path = window.location.hash.slice(1) || "/";
|
|
61
|
+
commandDisplay.textContent = getCliCommand(path);
|
|
62
|
+
// Reset copy button state
|
|
63
|
+
if (copyButton) {
|
|
64
|
+
copyButton.setAttribute("aria-label", "Copy command");
|
|
65
|
+
copyButton.classList.remove("copied");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Copy the current CLI command to clipboard
|
|
71
|
+
*/
|
|
72
|
+
async function handleCopy() {
|
|
73
|
+
if (!commandDisplay || !copyButton) return;
|
|
74
|
+
const text = commandDisplay.textContent;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await navigator.clipboard.writeText(text);
|
|
78
|
+
copyButton.classList.add("copied");
|
|
79
|
+
copyButton.setAttribute("aria-label", "Copied!");
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
copyButton.classList.remove("copied");
|
|
82
|
+
copyButton.setAttribute("aria-label", "Copy command");
|
|
83
|
+
}, 2000);
|
|
84
|
+
} catch {
|
|
85
|
+
// Fallback for older browsers
|
|
86
|
+
const textarea = document.createElement("textarea");
|
|
87
|
+
textarea.value = text;
|
|
88
|
+
textarea.style.position = "fixed";
|
|
89
|
+
textarea.style.opacity = "0";
|
|
90
|
+
document.body.appendChild(textarea);
|
|
91
|
+
textarea.select();
|
|
92
|
+
document.execCommand("copy");
|
|
93
|
+
document.body.removeChild(textarea);
|
|
94
|
+
copyButton.classList.add("copied");
|
|
95
|
+
setTimeout(() => copyButton.classList.remove("copied"), 2000);
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/css/bundles/app.css
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
@import "../components/progress.css" layer(components);
|
|
25
25
|
@import "../components/states.css" layer(components);
|
|
26
26
|
@import "../components/nav.css" layer(components);
|
|
27
|
+
@import "../components/top-bar.css" layer(components);
|
|
28
|
+
@import "../components/command-prompt.css" layer(components);
|
|
27
29
|
|
|
28
30
|
/* Utilities */
|
|
29
31
|
@import "../components/utilities.css" layer(utilities);
|