zefiro 0.8.1 → 0.9.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/dist/cli-6qr9gvkp.js +508 -0
- package/dist/cli-b26q1e27.js +264 -0
- package/dist/cli-g7n2me6z.js +133 -0
- package/dist/cli.js +46 -31
- package/dist/explorer-g6bmbak1.js +10 -0
- package/dist/mcp.js +3836 -26
- package/dist/report-sdtah1f4.js +11 -0
- package/package.json +1 -1
- package/skills/zefiro-copilot/SKILL.md +103 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
// src/browser/url-tracker.ts
|
|
2
|
+
var SKIP_EXTENSIONS = new Set([
|
|
3
|
+
".pdf",
|
|
4
|
+
".zip",
|
|
5
|
+
".csv",
|
|
6
|
+
".xlsx",
|
|
7
|
+
".xls",
|
|
8
|
+
".doc",
|
|
9
|
+
".docx",
|
|
10
|
+
".png",
|
|
11
|
+
".jpg",
|
|
12
|
+
".jpeg",
|
|
13
|
+
".gif",
|
|
14
|
+
".svg",
|
|
15
|
+
".ico",
|
|
16
|
+
".webp",
|
|
17
|
+
".mp3",
|
|
18
|
+
".mp4",
|
|
19
|
+
".wav",
|
|
20
|
+
".avi",
|
|
21
|
+
".mov"
|
|
22
|
+
]);
|
|
23
|
+
var SKIP_PATTERNS = [
|
|
24
|
+
/\/logout/i,
|
|
25
|
+
/\/signout/i,
|
|
26
|
+
/\/api\//i,
|
|
27
|
+
/\/auth\/callback/i,
|
|
28
|
+
/\/oauth/i,
|
|
29
|
+
/javascript:/i,
|
|
30
|
+
/mailto:/i,
|
|
31
|
+
/tel:/i,
|
|
32
|
+
/#$/
|
|
33
|
+
];
|
|
34
|
+
function normalizeUrl(url, baseUrl) {
|
|
35
|
+
try {
|
|
36
|
+
const resolved = new URL(url, baseUrl);
|
|
37
|
+
resolved.hash = "";
|
|
38
|
+
let normalized = resolved.href;
|
|
39
|
+
if (normalized.endsWith("/") && resolved.pathname !== "/") {
|
|
40
|
+
normalized = normalized.slice(0, -1);
|
|
41
|
+
}
|
|
42
|
+
return normalized;
|
|
43
|
+
} catch {
|
|
44
|
+
return url;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function shouldVisit(url, baseUrl, excludePatterns, visited) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = new URL(url);
|
|
50
|
+
const base = new URL(baseUrl);
|
|
51
|
+
if (parsed.origin !== base.origin)
|
|
52
|
+
return false;
|
|
53
|
+
const normalized = normalizeUrl(url, baseUrl);
|
|
54
|
+
if (visited.has(normalized))
|
|
55
|
+
return false;
|
|
56
|
+
const ext = parsed.pathname.match(/\.\w+$/)?.[0]?.toLowerCase();
|
|
57
|
+
if (ext && SKIP_EXTENSIONS.has(ext))
|
|
58
|
+
return false;
|
|
59
|
+
for (const pattern of SKIP_PATTERNS) {
|
|
60
|
+
if (pattern.test(url))
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
for (const pattern of excludePatterns) {
|
|
64
|
+
try {
|
|
65
|
+
if (new RegExp(pattern).test(url))
|
|
66
|
+
return false;
|
|
67
|
+
} catch {
|
|
68
|
+
if (url.includes(pattern))
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function extractHrefsFromElements(elements) {
|
|
78
|
+
return elements.filter((el) => el.role === "link").map((el) => el.ref);
|
|
79
|
+
}
|
|
80
|
+
function urlToSlug(url, baseUrl) {
|
|
81
|
+
try {
|
|
82
|
+
const parsed = new URL(url, baseUrl);
|
|
83
|
+
const path = parsed.pathname;
|
|
84
|
+
if (path === "/" || path === "")
|
|
85
|
+
return "home";
|
|
86
|
+
return path.replace(/^\/|\/$/g, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").toLowerCase();
|
|
87
|
+
} catch {
|
|
88
|
+
return "unknown";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function buildTopology(pages) {
|
|
92
|
+
const root = { path: "/", slug: "root", title: "Application", children: [] };
|
|
93
|
+
const nodeMap = new Map;
|
|
94
|
+
nodeMap.set("/", root);
|
|
95
|
+
const sortedPages = Array.from(pages.values()).filter((p) => p.status === "visited").sort((a, b) => a.depth - b.depth);
|
|
96
|
+
for (const page of sortedPages) {
|
|
97
|
+
const segments = page.path.replace(/^\/|\/$/g, "").split("/").filter(Boolean);
|
|
98
|
+
const node = {
|
|
99
|
+
path: page.path,
|
|
100
|
+
slug: page.slug,
|
|
101
|
+
title: page.title || page.slug,
|
|
102
|
+
children: []
|
|
103
|
+
};
|
|
104
|
+
let parentPath = "/";
|
|
105
|
+
if (segments.length > 1) {
|
|
106
|
+
parentPath = "/" + segments.slice(0, -1).join("/");
|
|
107
|
+
}
|
|
108
|
+
const parent = nodeMap.get(parentPath) ?? root;
|
|
109
|
+
parent.children.push(node);
|
|
110
|
+
nodeMap.set(page.path, node);
|
|
111
|
+
}
|
|
112
|
+
return root.children;
|
|
113
|
+
}
|
|
114
|
+
function topologyToAscii(nodes, prefix = "") {
|
|
115
|
+
const lines = [];
|
|
116
|
+
for (let i = 0;i < nodes.length; i++) {
|
|
117
|
+
const isLast = i === nodes.length - 1;
|
|
118
|
+
const connector = isLast ? "└── " : "├── ";
|
|
119
|
+
const childPrefix = isLast ? " " : "│ ";
|
|
120
|
+
lines.push(`${prefix}${connector}${nodes[i].path} — ${nodes[i].title}`);
|
|
121
|
+
if (nodes[i].children.length > 0) {
|
|
122
|
+
lines.push(topologyToAscii(nodes[i].children, prefix + childPrefix));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return lines.join(`
|
|
126
|
+
`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/browser/report.ts
|
|
130
|
+
function generateSectionMarkdown(page, relatedPages, annotations) {
|
|
131
|
+
const analysis = page.analysis;
|
|
132
|
+
const lines = [];
|
|
133
|
+
const name = analysis?.pageName ?? page.title ?? page.slug;
|
|
134
|
+
lines.push(`# ${name} (\`${page.path}\`)`);
|
|
135
|
+
lines.push("");
|
|
136
|
+
if (analysis?.description) {
|
|
137
|
+
lines.push(`> ${analysis.description}`);
|
|
138
|
+
lines.push("");
|
|
139
|
+
}
|
|
140
|
+
lines.push(`**Route:** \`${page.path}\``);
|
|
141
|
+
if (analysis?.navigation?.tabs?.length) {
|
|
142
|
+
lines.push(`**Tabs:** ${analysis.navigation.tabs.join(", ")}`);
|
|
143
|
+
}
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push("---");
|
|
146
|
+
lines.push("");
|
|
147
|
+
lines.push(``);
|
|
148
|
+
lines.push("");
|
|
149
|
+
if (analysis?.sections) {
|
|
150
|
+
for (const section of analysis.sections) {
|
|
151
|
+
lines.push(`## ${section.name}`);
|
|
152
|
+
lines.push("");
|
|
153
|
+
if (section.elements?.length) {
|
|
154
|
+
const hasRequired = section.elements.some((e) => e.required !== undefined);
|
|
155
|
+
if (hasRequired) {
|
|
156
|
+
lines.push("| Field | Type | Required | Description |");
|
|
157
|
+
lines.push("|-------|------|----------|-------------|");
|
|
158
|
+
for (const el of section.elements) {
|
|
159
|
+
lines.push(`| ${el.name} | ${el.type} | ${el.required ? "Yes" : "No"} | ${el.description} |`);
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
lines.push("| Control | Type | Description |");
|
|
163
|
+
lines.push("|---------|------|-------------|");
|
|
164
|
+
for (const el of section.elements) {
|
|
165
|
+
lines.push(`| ${el.name} | ${el.type} | ${el.description} |`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
if (section.tableColumns?.length) {
|
|
171
|
+
lines.push("### Table Columns");
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push("| Column | Sortable | Description |");
|
|
174
|
+
lines.push("|--------|----------|-------------|");
|
|
175
|
+
for (const col of section.tableColumns) {
|
|
176
|
+
lines.push(`| ${col.name} | ${col.sortable ? "Yes" : "No"} | ${col.description} |`);
|
|
177
|
+
}
|
|
178
|
+
lines.push("");
|
|
179
|
+
}
|
|
180
|
+
lines.push("---");
|
|
181
|
+
lines.push("");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (analysis?.actions?.length) {
|
|
185
|
+
lines.push("## Available Actions");
|
|
186
|
+
lines.push("");
|
|
187
|
+
for (const action of analysis.actions) {
|
|
188
|
+
lines.push(`- ${action}`);
|
|
189
|
+
}
|
|
190
|
+
lines.push("");
|
|
191
|
+
}
|
|
192
|
+
if (analysis?.observations?.length) {
|
|
193
|
+
lines.push("## Key Behaviors");
|
|
194
|
+
lines.push("");
|
|
195
|
+
for (const obs of analysis.observations) {
|
|
196
|
+
lines.push(`- ${obs}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push("");
|
|
199
|
+
}
|
|
200
|
+
const related = relatedPages.filter((p) => p.slug !== page.slug);
|
|
201
|
+
if (related.length > 0) {
|
|
202
|
+
lines.push("## Related Pages");
|
|
203
|
+
lines.push("");
|
|
204
|
+
for (const rp of related) {
|
|
205
|
+
lines.push(`- [${rp.name}](./${rp.slug}.md)`);
|
|
206
|
+
}
|
|
207
|
+
lines.push("");
|
|
208
|
+
}
|
|
209
|
+
if (annotations?.length) {
|
|
210
|
+
lines.push("## Annotations (voice)");
|
|
211
|
+
lines.push("");
|
|
212
|
+
for (const ann of annotations) {
|
|
213
|
+
lines.push(`- "${ann.text}" — ${ann.timestamp}`);
|
|
214
|
+
}
|
|
215
|
+
lines.push("");
|
|
216
|
+
}
|
|
217
|
+
return lines.join(`
|
|
218
|
+
`);
|
|
219
|
+
}
|
|
220
|
+
function generateReadme(state) {
|
|
221
|
+
const lines = [];
|
|
222
|
+
const visited = Array.from(state.pages.values()).filter((p) => p.status === "visited");
|
|
223
|
+
const errors = Array.from(state.pages.values()).filter((p) => p.status === "error");
|
|
224
|
+
lines.push("# Application Map");
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push(`> **Scan Date:** ${new Date(state.startedAt).toISOString().split("T")[0]}`);
|
|
227
|
+
lines.push(`> **URL:** \`${state.baseUrl}\``);
|
|
228
|
+
lines.push(`> **Pages Explored:** ${visited.length}`);
|
|
229
|
+
lines.push("");
|
|
230
|
+
lines.push("---");
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push("## Application Structure");
|
|
233
|
+
lines.push("");
|
|
234
|
+
lines.push("```");
|
|
235
|
+
lines.push(`Application (${state.baseUrl})`);
|
|
236
|
+
if (state.topology.length > 0) {
|
|
237
|
+
lines.push(topologyToAscii(state.topology));
|
|
238
|
+
}
|
|
239
|
+
lines.push("```");
|
|
240
|
+
lines.push("");
|
|
241
|
+
lines.push("---");
|
|
242
|
+
lines.push("");
|
|
243
|
+
lines.push("## Section Reports");
|
|
244
|
+
lines.push("");
|
|
245
|
+
lines.push("| Section | Report | Screenshots |");
|
|
246
|
+
lines.push("|---------|--------|-------------|");
|
|
247
|
+
for (const page of visited) {
|
|
248
|
+
const name = page.analysis?.pageName ?? page.title ?? page.slug;
|
|
249
|
+
lines.push(`| ${name} | [${page.slug}.md](./sections/${page.slug}.md) | [screenshots/${page.slug}/](./screenshots/${page.slug}/) |`);
|
|
250
|
+
}
|
|
251
|
+
lines.push("");
|
|
252
|
+
lines.push("---");
|
|
253
|
+
lines.push("");
|
|
254
|
+
lines.push("## Route Map");
|
|
255
|
+
lines.push("");
|
|
256
|
+
lines.push("| Route | Section | Elements | Links |");
|
|
257
|
+
lines.push("|-------|---------|----------|-------|");
|
|
258
|
+
for (const page of visited) {
|
|
259
|
+
const name = page.analysis?.pageName ?? page.title ?? page.slug;
|
|
260
|
+
lines.push(`| \`${page.path}\` | ${name} | ${page.elements.length} | ${page.outgoingLinks.length} |`);
|
|
261
|
+
}
|
|
262
|
+
lines.push("");
|
|
263
|
+
if (errors.length > 0) {
|
|
264
|
+
lines.push("---");
|
|
265
|
+
lines.push("");
|
|
266
|
+
lines.push("## Errors");
|
|
267
|
+
lines.push("");
|
|
268
|
+
for (const page of errors) {
|
|
269
|
+
lines.push(`- \`${page.url}\`: ${page.error}`);
|
|
270
|
+
}
|
|
271
|
+
lines.push("");
|
|
272
|
+
}
|
|
273
|
+
lines.push("---");
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push("## Screenshot Inventory");
|
|
276
|
+
lines.push("");
|
|
277
|
+
lines.push(`| Section | Count |`);
|
|
278
|
+
lines.push(`|---------|-------|`);
|
|
279
|
+
let total = 0;
|
|
280
|
+
for (const page of visited) {
|
|
281
|
+
lines.push(`| ${page.analysis?.pageName ?? page.slug} | 1 |`);
|
|
282
|
+
total++;
|
|
283
|
+
}
|
|
284
|
+
lines.push(`| **Total** | **${total}** |`);
|
|
285
|
+
lines.push("");
|
|
286
|
+
return lines.join(`
|
|
287
|
+
`);
|
|
288
|
+
}
|
|
289
|
+
function generateIndexHtml(state) {
|
|
290
|
+
const visited = Array.from(state.pages.values()).filter((p) => p.status === "visited");
|
|
291
|
+
const screensData = {};
|
|
292
|
+
for (const page of visited) {
|
|
293
|
+
const name = page.analysis?.pageName ?? page.title ?? page.slug;
|
|
294
|
+
screensData[page.slug] = [{
|
|
295
|
+
src: page.screenshot,
|
|
296
|
+
label: name,
|
|
297
|
+
desc: page.analysis?.description ?? `Page at ${page.path}`
|
|
298
|
+
}];
|
|
299
|
+
}
|
|
300
|
+
const sidebarItems = visited.map((page) => {
|
|
301
|
+
const name = page.analysis?.pageName ?? page.title ?? page.slug;
|
|
302
|
+
return `<div class="sidebar-item${visited[0] === page ? " active" : ""}" data-section="${page.slug}" onclick="showSection('${page.slug}')">
|
|
303
|
+
<span class="icon">\uD83D\uDCC4</span>${name}<span class="count">1</span>
|
|
304
|
+
</div>`;
|
|
305
|
+
}).join(`
|
|
306
|
+
`);
|
|
307
|
+
const sections = visited.map((page) => {
|
|
308
|
+
const name = page.analysis?.pageName ?? page.title ?? page.slug;
|
|
309
|
+
const isFirst = visited[0] === page;
|
|
310
|
+
return `<div id="sec-${page.slug}" class="flow-section${isFirst ? " active" : ""}">
|
|
311
|
+
<div class="flow-title">${name}</div>
|
|
312
|
+
<div class="flow-subtitle">${page.analysis?.description ?? ""}</div>
|
|
313
|
+
<div class="flow-route">${page.path}</div>
|
|
314
|
+
<div class="flow-row">
|
|
315
|
+
<div class="flow-card" onclick="openLightbox('${page.slug}', 0)">
|
|
316
|
+
<div class="card-thumb" style="background-image:url('${page.screenshot}')">
|
|
317
|
+
<span class="tag tag-page">Page</span>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="card-body">
|
|
320
|
+
<div class="card-label">${name}</div>
|
|
321
|
+
<div class="card-desc">${page.analysis?.description ?? ""}</div>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
</div>`;
|
|
326
|
+
}).join(`
|
|
327
|
+
`);
|
|
328
|
+
return `<!DOCTYPE html>
|
|
329
|
+
<html lang="en">
|
|
330
|
+
<head>
|
|
331
|
+
<meta charset="UTF-8">
|
|
332
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
333
|
+
<title>Application Map</title>
|
|
334
|
+
<style>
|
|
335
|
+
:root {
|
|
336
|
+
--bg: #0f1117;
|
|
337
|
+
--surface: #1a1d27;
|
|
338
|
+
--surface2: #242837;
|
|
339
|
+
--border: #2e3348;
|
|
340
|
+
--text: #e1e4ed;
|
|
341
|
+
--text-dim: #8b90a5;
|
|
342
|
+
--accent: #6366f1;
|
|
343
|
+
--accent-glow: rgba(99,102,241,0.25);
|
|
344
|
+
--green: #22c55e;
|
|
345
|
+
--amber: #f59e0b;
|
|
346
|
+
--red: #ef4444;
|
|
347
|
+
--cyan: #06b6d4;
|
|
348
|
+
--pink: #ec4899;
|
|
349
|
+
}
|
|
350
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
351
|
+
body {
|
|
352
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
353
|
+
background: var(--bg); color: var(--text);
|
|
354
|
+
overflow: hidden; height: 100vh;
|
|
355
|
+
}
|
|
356
|
+
.topbar {
|
|
357
|
+
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
|
358
|
+
height: 56px; background: var(--surface);
|
|
359
|
+
border-bottom: 1px solid var(--border);
|
|
360
|
+
display: flex; align-items: center; padding: 0 24px; gap: 16px;
|
|
361
|
+
}
|
|
362
|
+
.topbar h1 { font-size: 16px; font-weight: 600; }
|
|
363
|
+
.topbar .badge {
|
|
364
|
+
font-size: 11px; padding: 3px 10px; border-radius: 20px;
|
|
365
|
+
background: var(--accent); color: #fff; font-weight: 500;
|
|
366
|
+
}
|
|
367
|
+
.topbar .sep { flex: 1; }
|
|
368
|
+
.main { display: flex; height: 100vh; padding-top: 56px; }
|
|
369
|
+
.sidebar {
|
|
370
|
+
width: 260px; min-width: 260px;
|
|
371
|
+
background: var(--surface); border-right: 1px solid var(--border);
|
|
372
|
+
overflow-y: auto; padding: 16px 0;
|
|
373
|
+
}
|
|
374
|
+
.sidebar-item {
|
|
375
|
+
display: flex; align-items: center; gap: 10px;
|
|
376
|
+
padding: 8px 20px; border-radius: 8px; cursor: pointer;
|
|
377
|
+
font-size: 13px; color: var(--text-dim); margin: 0 8px 2px;
|
|
378
|
+
}
|
|
379
|
+
.sidebar-item:hover { background: var(--surface2); color: var(--text); }
|
|
380
|
+
.sidebar-item.active { background: var(--accent-glow); color: var(--accent); font-weight: 500; }
|
|
381
|
+
.sidebar-item .icon { font-size: 16px; width: 20px; text-align: center; }
|
|
382
|
+
.sidebar-item .count {
|
|
383
|
+
margin-left: auto; font-size: 11px; background: var(--surface2);
|
|
384
|
+
padding: 1px 7px; border-radius: 10px; color: var(--text-dim);
|
|
385
|
+
}
|
|
386
|
+
.canvas-area { flex: 1; overflow: hidden; }
|
|
387
|
+
.canvas-scroll { width: 100%; height: 100%; overflow: auto; padding: 40px; }
|
|
388
|
+
.flow-section { display: none; }
|
|
389
|
+
.flow-section.active { display: block; }
|
|
390
|
+
.flow-title { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
|
|
391
|
+
.flow-subtitle { font-size: 14px; color: var(--text-dim); margin-bottom: 8px; }
|
|
392
|
+
.flow-route {
|
|
393
|
+
display: inline-block; font-size: 12px; font-family: monospace;
|
|
394
|
+
background: var(--surface2); padding: 3px 10px; border-radius: 6px;
|
|
395
|
+
color: var(--text-dim); margin-bottom: 24px;
|
|
396
|
+
}
|
|
397
|
+
.flow-row { display: flex; gap: 16px; flex-wrap: wrap; }
|
|
398
|
+
.flow-card {
|
|
399
|
+
width: 320px; background: var(--surface); border: 1px solid var(--border);
|
|
400
|
+
border-radius: 12px; overflow: hidden; cursor: pointer; transition: border-color 0.15s;
|
|
401
|
+
}
|
|
402
|
+
.flow-card:hover { border-color: var(--accent); }
|
|
403
|
+
.card-thumb {
|
|
404
|
+
height: 180px; background-size: cover; background-position: top center;
|
|
405
|
+
position: relative;
|
|
406
|
+
}
|
|
407
|
+
.tag {
|
|
408
|
+
position: absolute; top: 8px; right: 8px;
|
|
409
|
+
font-size: 10px; padding: 2px 8px; border-radius: 4px;
|
|
410
|
+
font-weight: 600; text-transform: uppercase;
|
|
411
|
+
}
|
|
412
|
+
.tag-page { background: var(--accent); color: #fff; }
|
|
413
|
+
.tag-modal { background: var(--pink); color: #fff; }
|
|
414
|
+
.tag-action { background: var(--amber); color: #000; }
|
|
415
|
+
.card-body { padding: 12px 16px; }
|
|
416
|
+
.card-label { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
|
|
417
|
+
.card-desc { font-size: 12px; color: var(--text-dim); }
|
|
418
|
+
/* Lightbox */
|
|
419
|
+
.lightbox {
|
|
420
|
+
display: none; position: fixed; inset: 0; z-index: 200;
|
|
421
|
+
background: rgba(0,0,0,0.9); align-items: center; justify-content: center;
|
|
422
|
+
}
|
|
423
|
+
.lightbox.open { display: flex; }
|
|
424
|
+
.lightbox img { max-width: 90vw; max-height: 80vh; border-radius: 8px; }
|
|
425
|
+
.lb-caption {
|
|
426
|
+
position: fixed; bottom: 40px; left: 50%; transform: translateX(-50%);
|
|
427
|
+
text-align: center; color: var(--text);
|
|
428
|
+
}
|
|
429
|
+
.lb-counter { font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }
|
|
430
|
+
.lb-label { font-size: 16px; font-weight: 600; }
|
|
431
|
+
.lb-close {
|
|
432
|
+
position: fixed; top: 20px; right: 24px; font-size: 24px;
|
|
433
|
+
color: var(--text-dim); cursor: pointer; z-index: 201;
|
|
434
|
+
}
|
|
435
|
+
.lb-nav {
|
|
436
|
+
position: fixed; top: 50%; font-size: 32px; color: var(--text-dim);
|
|
437
|
+
cursor: pointer; padding: 20px; z-index: 201;
|
|
438
|
+
}
|
|
439
|
+
.lb-prev { left: 10px; }
|
|
440
|
+
.lb-next { right: 10px; }
|
|
441
|
+
</style>
|
|
442
|
+
</head>
|
|
443
|
+
<body>
|
|
444
|
+
<div class="topbar">
|
|
445
|
+
<h1>Application Map</h1>
|
|
446
|
+
<span class="badge">Zefiro</span>
|
|
447
|
+
<span class="sep"></span>
|
|
448
|
+
<span style="font-size:12px;color:var(--text-dim)">${visited.length} pages</span>
|
|
449
|
+
</div>
|
|
450
|
+
<div class="main">
|
|
451
|
+
<div class="sidebar">
|
|
452
|
+
${sidebarItems}
|
|
453
|
+
</div>
|
|
454
|
+
<div class="canvas-area">
|
|
455
|
+
<div class="canvas-scroll">
|
|
456
|
+
${sections}
|
|
457
|
+
</div>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="lightbox" id="lightbox" onclick="if(event.target===this)closeLightbox()">
|
|
461
|
+
<span class="lb-close" onclick="closeLightbox()">✕</span>
|
|
462
|
+
<span class="lb-nav lb-prev" onclick="navLightbox(-1)">‹</span>
|
|
463
|
+
<img id="lb-img" src="" alt="">
|
|
464
|
+
<span class="lb-nav lb-next" onclick="navLightbox(1)">›</span>
|
|
465
|
+
<div class="lb-caption">
|
|
466
|
+
<div class="lb-counter" id="lb-counter"></div>
|
|
467
|
+
<div class="lb-label" id="lb-label"></div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
<script>
|
|
471
|
+
const screens = ${JSON.stringify(screensData, null, 2)};
|
|
472
|
+
let lbSection = null, lbIndex = 0;
|
|
473
|
+
function showSection(slug) {
|
|
474
|
+
document.querySelectorAll('.flow-section').forEach(s => s.classList.remove('active'));
|
|
475
|
+
document.querySelectorAll('.sidebar-item').forEach(s => s.classList.remove('active'));
|
|
476
|
+
const sec = document.getElementById('sec-' + slug);
|
|
477
|
+
if (sec) sec.classList.add('active');
|
|
478
|
+
const item = document.querySelector('[data-section="' + slug + '"]');
|
|
479
|
+
if (item) item.classList.add('active');
|
|
480
|
+
}
|
|
481
|
+
function openLightbox(section, index) {
|
|
482
|
+
lbSection = section; lbIndex = index;
|
|
483
|
+
const items = screens[section] || [];
|
|
484
|
+
if (!items[index]) return;
|
|
485
|
+
document.getElementById('lb-img').src = items[index].src;
|
|
486
|
+
document.getElementById('lb-label').textContent = items[index].label;
|
|
487
|
+
document.getElementById('lb-counter').textContent = (index + 1) + ' / ' + items.length;
|
|
488
|
+
document.getElementById('lightbox').classList.add('open');
|
|
489
|
+
}
|
|
490
|
+
function closeLightbox() {
|
|
491
|
+
document.getElementById('lightbox').classList.remove('open');
|
|
492
|
+
}
|
|
493
|
+
function navLightbox(dir) {
|
|
494
|
+
const items = screens[lbSection] || [];
|
|
495
|
+
lbIndex = (lbIndex + dir + items.length) % items.length;
|
|
496
|
+
openLightbox(lbSection, lbIndex);
|
|
497
|
+
}
|
|
498
|
+
document.addEventListener('keydown', e => {
|
|
499
|
+
if (e.key === 'Escape') closeLightbox();
|
|
500
|
+
if (e.key === 'ArrowLeft') navLightbox(-1);
|
|
501
|
+
if (e.key === 'ArrowRight') navLightbox(1);
|
|
502
|
+
});
|
|
503
|
+
</script>
|
|
504
|
+
</body>
|
|
505
|
+
</html>`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export { normalizeUrl, shouldVisit, extractHrefsFromElements, urlToSlug, buildTopology, generateSectionMarkdown, generateReadme, generateIndexHtml };
|