@veolab/discoverylab 1.4.3 → 1.6.3
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +70 -211
- package/dist/{chunk-HB3YPWF3.js → chunk-5AISGCS4.js} +166 -8
- package/dist/{chunk-CUBQRT5L.js → chunk-HFN6BTVO.js} +110 -1
- package/dist/{chunk-D4FTLCKM.js → chunk-IVX2OSOJ.js} +260 -4
- package/dist/cli.js +158 -27
- package/dist/export/infographic-template.html +254 -0
- package/dist/import-W2JEW254.js +180 -0
- package/dist/index.html +420 -9
- package/dist/index.js +4 -4
- package/dist/infographic-GQAHEOAA.js +183 -0
- package/dist/{server-CZPWQYOI.js → server-W3JQ5RG7.js} +1 -1
- package/dist/{setup-6JJYKKBS.js → setup-F7MGEFIM.js} +4 -1
- package/dist/{tools-Q7OZO732.js → tools-VYFNRUS4.js} +5 -3
- package/dist/visualizations/app-flow-map.html +177 -0
- package/dist/visualizations/device-showcase.html +150 -0
- package/dist/visualizations/flow-diagram.html +181 -0
- package/dist/visualizations/metrics-dashboard.html +263 -0
- package/doc/esvp-protocol.md +116 -0
- package/package.json +3 -3
- package/skills/knowledge-brain/SKILL.md +44 -43
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import "./chunk-R5U7XKVJ.js";
|
|
2
|
+
|
|
3
|
+
// src/core/export/infographic.ts
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { join, dirname, extname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
function findTemplate() {
|
|
8
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const possiblePaths = [
|
|
10
|
+
join(__dir, "infographic-template.html"),
|
|
11
|
+
join(__dir, "..", "export", "infographic-template.html"),
|
|
12
|
+
join(process.cwd(), "dist", "export", "infographic-template.html"),
|
|
13
|
+
join(process.cwd(), "src", "core", "export", "infographic-template.html")
|
|
14
|
+
];
|
|
15
|
+
for (const p of possiblePaths) {
|
|
16
|
+
if (existsSync(p)) return p;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function imageToBase64(imagePath) {
|
|
21
|
+
if (!existsSync(imagePath)) return "";
|
|
22
|
+
const buffer = readFileSync(imagePath);
|
|
23
|
+
const ext = extname(imagePath).toLowerCase();
|
|
24
|
+
const mimeTypes = {
|
|
25
|
+
".png": "image/png",
|
|
26
|
+
".jpg": "image/jpeg",
|
|
27
|
+
".jpeg": "image/jpeg",
|
|
28
|
+
".webp": "image/webp",
|
|
29
|
+
".gif": "image/gif"
|
|
30
|
+
};
|
|
31
|
+
const mime = mimeTypes[ext] || "image/png";
|
|
32
|
+
return `data:${mime};base64,${buffer.toString("base64")}`;
|
|
33
|
+
}
|
|
34
|
+
function stripMarkdown(text) {
|
|
35
|
+
return text.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/_(.+?)_/g, "$1").replace(/`(.+?)`/g, "$1").replace(/^#{1,3}\s+/gm, "").replace(/^-\s+/gm, "").replace(/^\d+\.\s+/gm, "").replace(/\[(.+?)\]\(.*?\)/g, "$1").trim();
|
|
36
|
+
}
|
|
37
|
+
function collectFrameImages(framesDir, videoPath, projectsDir, projectId) {
|
|
38
|
+
const imageExts = /\.(png|jpg|jpeg|webp|gif)$/i;
|
|
39
|
+
const dirs = [
|
|
40
|
+
framesDir,
|
|
41
|
+
...videoPath ? [join(videoPath, "screenshots"), videoPath] : [],
|
|
42
|
+
...projectsDir && projectId ? [
|
|
43
|
+
join(projectsDir, "maestro-recordings", projectId, "screenshots"),
|
|
44
|
+
join(projectsDir, "web-recordings", projectId, "screenshots")
|
|
45
|
+
] : []
|
|
46
|
+
];
|
|
47
|
+
for (const dir of dirs) {
|
|
48
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) continue;
|
|
49
|
+
const files = readdirSync(dir).filter((f) => imageExts.test(f)).sort().map((f) => join(dir, f));
|
|
50
|
+
if (files.length > 0) return files;
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
function buildInfographicData(project, frameFiles, frameOcr, annotations) {
|
|
55
|
+
let flowSteps = [];
|
|
56
|
+
let uiElements = [];
|
|
57
|
+
if (project.aiSummary) {
|
|
58
|
+
const flowMatch = project.aiSummary.match(/## (?:User Flow|Likely User Flow)\n([\s\S]*?)(?=\n##|\n$|$)/);
|
|
59
|
+
if (flowMatch) {
|
|
60
|
+
flowSteps = (flowMatch[1].match(/^\d+\.\s+(.+)$/gm) || []).map((s) => s.replace(/^\d+\.\s+/, ""));
|
|
61
|
+
}
|
|
62
|
+
const uiMatch = project.aiSummary.match(/## (?:UI Elements Found|Key UI Elements)\n([\s\S]*?)(?=\n##|\n$|$)/);
|
|
63
|
+
if (uiMatch) {
|
|
64
|
+
uiElements = (uiMatch[1].match(/^-\s+(.+)$/gm) || []).map((s) => s.replace(/^-\s+/, ""));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const elementsPerFrame = Math.max(1, Math.ceil(uiElements.length / Math.max(frameFiles.length, 1)));
|
|
68
|
+
const hotspotColors = ["#818CF8", "#34D399", "#F59E0B", "#EC4899", "#06B6D4", "#8B5CF6"];
|
|
69
|
+
const hotspotPositions = [
|
|
70
|
+
{ x: 50, y: 8 },
|
|
71
|
+
// top center (nav bar)
|
|
72
|
+
{ x: 15, y: 30 },
|
|
73
|
+
// left middle
|
|
74
|
+
{ x: 85, y: 30 },
|
|
75
|
+
// right middle
|
|
76
|
+
{ x: 50, y: 50 },
|
|
77
|
+
// center
|
|
78
|
+
{ x: 50, y: 85 },
|
|
79
|
+
// bottom center (tab bar)
|
|
80
|
+
{ x: 15, y: 70 }
|
|
81
|
+
// bottom left
|
|
82
|
+
];
|
|
83
|
+
const frames = frameFiles.map((filePath, i) => {
|
|
84
|
+
const rawStepName = annotations?.[i]?.label || flowSteps[i] || `Step ${i + 1}`;
|
|
85
|
+
const stepName = stripMarkdown(rawStepName);
|
|
86
|
+
const ocr = frameOcr[i]?.ocrText || "";
|
|
87
|
+
const rawDesc = flowSteps[i] || ocr.slice(0, 100) || `Screen ${i + 1}`;
|
|
88
|
+
const description = stripMarkdown(rawDesc);
|
|
89
|
+
const hotspots = [];
|
|
90
|
+
const startIdx = i * elementsPerFrame;
|
|
91
|
+
const frameElements = uiElements.slice(startIdx, startIdx + elementsPerFrame).slice(0, 4);
|
|
92
|
+
frameElements.forEach((el, j) => {
|
|
93
|
+
const cleanEl = stripMarkdown(el);
|
|
94
|
+
const pos = hotspotPositions[j % hotspotPositions.length];
|
|
95
|
+
hotspots.push({
|
|
96
|
+
id: String.fromCharCode(65 + j),
|
|
97
|
+
x_percent: pos.x,
|
|
98
|
+
y_percent: pos.y,
|
|
99
|
+
label: cleanEl.slice(0, 20),
|
|
100
|
+
title: cleanEl.slice(0, 40),
|
|
101
|
+
description: cleanEl,
|
|
102
|
+
color: hotspotColors[j % hotspotColors.length]
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
if (hotspots.length === 0 && ocr) {
|
|
106
|
+
const ocrWords = ocr.split(/\s+/).filter((w) => w.length > 3).slice(0, 3);
|
|
107
|
+
ocrWords.forEach((w, j) => {
|
|
108
|
+
const pos = hotspotPositions[j % hotspotPositions.length];
|
|
109
|
+
hotspots.push({
|
|
110
|
+
id: String.fromCharCode(65 + j),
|
|
111
|
+
x_percent: pos.x,
|
|
112
|
+
y_percent: pos.y,
|
|
113
|
+
label: w.slice(0, 15),
|
|
114
|
+
title: w,
|
|
115
|
+
description: `Text found: "${w}"`,
|
|
116
|
+
color: hotspotColors[j % hotspotColors.length]
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
id: `frame-${i}`,
|
|
122
|
+
step_name: stepName,
|
|
123
|
+
description,
|
|
124
|
+
base64: imageToBase64(filePath),
|
|
125
|
+
baseline_status: "not_validated",
|
|
126
|
+
hotspots
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
name: project.marketingTitle || project.name,
|
|
131
|
+
platform: project.platform || "unknown",
|
|
132
|
+
recorded_at: project.createdAt instanceof Date ? project.createdAt.toISOString() : typeof project.createdAt === "string" ? project.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
133
|
+
frames
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function generateInfographicHtmlString(data) {
|
|
137
|
+
const templatePath = findTemplate();
|
|
138
|
+
if (!templatePath) return null;
|
|
139
|
+
let html = readFileSync(templatePath, "utf-8");
|
|
140
|
+
html = html.replace("__TITLE__", data.name);
|
|
141
|
+
const dataJson = JSON.stringify(data);
|
|
142
|
+
html = html.replace(
|
|
143
|
+
"window.FLOW_DATA || { name: 'Flow', frames: [] }",
|
|
144
|
+
`window.FLOW_DATA || ${dataJson}`
|
|
145
|
+
);
|
|
146
|
+
return html;
|
|
147
|
+
}
|
|
148
|
+
function generateInfographicHtml(data, outputPath) {
|
|
149
|
+
try {
|
|
150
|
+
const templatePath = findTemplate();
|
|
151
|
+
if (!templatePath) {
|
|
152
|
+
return { success: false, error: "Infographic template not found" };
|
|
153
|
+
}
|
|
154
|
+
let html = readFileSync(templatePath, "utf-8");
|
|
155
|
+
html = html.replace("__TITLE__", data.name);
|
|
156
|
+
const dataJson = JSON.stringify(data);
|
|
157
|
+
html = html.replace(
|
|
158
|
+
"window.FLOW_DATA || { name: 'Flow', frames: [] }",
|
|
159
|
+
`window.FLOW_DATA || ${dataJson}`
|
|
160
|
+
);
|
|
161
|
+
const outDir = dirname(outputPath);
|
|
162
|
+
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
163
|
+
writeFileSync(outputPath, html, "utf-8");
|
|
164
|
+
const stat = statSync(outputPath);
|
|
165
|
+
return {
|
|
166
|
+
success: true,
|
|
167
|
+
outputPath,
|
|
168
|
+
size: stat.size,
|
|
169
|
+
frameCount: data.frames.length
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: error instanceof Error ? error.message : String(error)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export {
|
|
179
|
+
buildInfographicData,
|
|
180
|
+
collectFrameImages,
|
|
181
|
+
generateInfographicHtml,
|
|
182
|
+
generateInfographicHtmlString
|
|
183
|
+
};
|
|
@@ -2,10 +2,12 @@ import {
|
|
|
2
2
|
setupCheckTool,
|
|
3
3
|
setupInitTool,
|
|
4
4
|
setupInstallTool,
|
|
5
|
+
setupReplayStatusTool,
|
|
5
6
|
setupStatusTool,
|
|
6
7
|
setupTools
|
|
7
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-HFN6BTVO.js";
|
|
8
9
|
import "./chunk-XKX6NBHF.js";
|
|
10
|
+
import "./chunk-GRU332L4.js";
|
|
9
11
|
import "./chunk-6EGBXRDK.js";
|
|
10
12
|
import "./chunk-YYOK2RF7.js";
|
|
11
13
|
import "./chunk-R5U7XKVJ.js";
|
|
@@ -13,6 +15,7 @@ export {
|
|
|
13
15
|
setupCheckTool,
|
|
14
16
|
setupInitTool,
|
|
15
17
|
setupInstallTool,
|
|
18
|
+
setupReplayStatusTool,
|
|
16
19
|
setupStatusTool,
|
|
17
20
|
setupTools
|
|
18
21
|
};
|
|
@@ -107,16 +107,17 @@ import {
|
|
|
107
107
|
uiStatusTool,
|
|
108
108
|
uiTools,
|
|
109
109
|
videoInfoTool
|
|
110
|
-
} from "./chunk-
|
|
110
|
+
} from "./chunk-5AISGCS4.js";
|
|
111
|
+
import "./chunk-34GGYFXX.js";
|
|
111
112
|
import "./chunk-PMCXEA7J.js";
|
|
112
113
|
import {
|
|
113
114
|
setupCheckTool,
|
|
114
115
|
setupInitTool,
|
|
116
|
+
setupReplayStatusTool,
|
|
115
117
|
setupStatusTool,
|
|
116
118
|
setupTools
|
|
117
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-HFN6BTVO.js";
|
|
118
120
|
import "./chunk-XKX6NBHF.js";
|
|
119
|
-
import "./chunk-34GGYFXX.js";
|
|
120
121
|
import "./chunk-7R5YNOXE.js";
|
|
121
122
|
import "./chunk-3ERJNXYM.js";
|
|
122
123
|
import "./chunk-LB3RNE3O.js";
|
|
@@ -217,6 +218,7 @@ export {
|
|
|
217
218
|
projectTools,
|
|
218
219
|
setupCheckTool,
|
|
219
220
|
setupInitTool,
|
|
221
|
+
setupReplayStatusTool,
|
|
220
222
|
setupStatusTool,
|
|
221
223
|
setupTools,
|
|
222
224
|
startRecordingTool,
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>App Flow Map</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
background: #1a1a2e;
|
|
11
|
+
color: #e0e0e0;
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
padding: 32px;
|
|
15
|
+
overflow-x: hidden;
|
|
16
|
+
}
|
|
17
|
+
.map { max-width: 900px; margin: 0 auto; }
|
|
18
|
+
.map-header {
|
|
19
|
+
text-align: center; margin-bottom: 36px;
|
|
20
|
+
opacity: 0; animation: fadeIn 0.6s 0.1s forwards;
|
|
21
|
+
}
|
|
22
|
+
.map-header h1 { font-size: 24px; font-weight: 700; color: #fff; }
|
|
23
|
+
.map-header p { font-size: 13px; color: rgba(255,255,255,0.45); margin-top: 6px; }
|
|
24
|
+
@keyframes fadeIn { to { opacity: 1; } }
|
|
25
|
+
@keyframes slideUp { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } }
|
|
26
|
+
|
|
27
|
+
/* Phase blocks */
|
|
28
|
+
.phase {
|
|
29
|
+
border-radius: 16px;
|
|
30
|
+
padding: 20px;
|
|
31
|
+
margin-bottom: 0;
|
|
32
|
+
opacity: 0;
|
|
33
|
+
animation: slideUp 0.5s forwards;
|
|
34
|
+
position: relative;
|
|
35
|
+
}
|
|
36
|
+
.phase-title { font-size: 16px; font-weight: 700; color: #fff; margin-bottom: 4px; }
|
|
37
|
+
.phase-desc { font-size: 12px; opacity: 0.6; margin-bottom: 14px; }
|
|
38
|
+
|
|
39
|
+
/* Phase color themes */
|
|
40
|
+
.phase-0 { background: rgba(34, 197, 94, 0.12); border: 1px solid rgba(34, 197, 94, 0.25); }
|
|
41
|
+
.phase-0 .phase-title { color: #86efac; }
|
|
42
|
+
.phase-1 { background: rgba(99, 102, 241, 0.12); border: 1px solid rgba(99, 102, 241, 0.25); }
|
|
43
|
+
.phase-1 .phase-title { color: #a5b4fc; }
|
|
44
|
+
.phase-2 { background: rgba(234, 179, 8, 0.12); border: 1px solid rgba(234, 179, 8, 0.25); }
|
|
45
|
+
.phase-2 .phase-title { color: #fde047; }
|
|
46
|
+
.phase-3 { background: rgba(236, 72, 153, 0.12); border: 1px solid rgba(236, 72, 153, 0.25); }
|
|
47
|
+
.phase-3 .phase-title { color: #f9a8d4; }
|
|
48
|
+
.phase-4 { background: rgba(14, 165, 233, 0.12); border: 1px solid rgba(14, 165, 233, 0.25); }
|
|
49
|
+
.phase-4 .phase-title { color: #7dd3fc; }
|
|
50
|
+
|
|
51
|
+
/* Screen cards inside phases */
|
|
52
|
+
.phase-screens {
|
|
53
|
+
display: flex; gap: 10px; overflow-x: auto; padding: 4px 0;
|
|
54
|
+
}
|
|
55
|
+
.screen-card {
|
|
56
|
+
flex-shrink: 0;
|
|
57
|
+
background: rgba(255,255,255,0.06);
|
|
58
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
59
|
+
border-radius: 10px;
|
|
60
|
+
padding: 8px;
|
|
61
|
+
min-width: 130px;
|
|
62
|
+
max-width: 160px;
|
|
63
|
+
transition: transform 0.2s, box-shadow 0.2s;
|
|
64
|
+
}
|
|
65
|
+
.screen-card:hover {
|
|
66
|
+
transform: translateY(-2px);
|
|
67
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
|
68
|
+
}
|
|
69
|
+
.screen-card-img {
|
|
70
|
+
width: 100%; height: 180px;
|
|
71
|
+
border-radius: 6px; overflow: hidden;
|
|
72
|
+
background: #000; margin-bottom: 6px;
|
|
73
|
+
}
|
|
74
|
+
.screen-card-img img { width: 100%; height: 100%; object-fit: contain; }
|
|
75
|
+
.screen-card-label { font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.85); }
|
|
76
|
+
.screen-card-detail { font-size: 10px; color: rgba(255,255,255,0.4); margin-top: 2px; }
|
|
77
|
+
|
|
78
|
+
/* Arrow connector between phases */
|
|
79
|
+
.arrow-connector {
|
|
80
|
+
display: flex; align-items: center; justify-content: center;
|
|
81
|
+
height: 40px;
|
|
82
|
+
opacity: 0; animation: fadeIn 0.3s forwards;
|
|
83
|
+
}
|
|
84
|
+
.arrow-connector svg {
|
|
85
|
+
stroke: rgba(255,255,255,0.2); fill: none; stroke-width: 2;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/* Insight callout */
|
|
89
|
+
.insight {
|
|
90
|
+
margin-top: 6px; padding: 8px 12px;
|
|
91
|
+
background: rgba(255,255,255,0.04);
|
|
92
|
+
border-left: 3px solid rgba(255,255,255,0.15);
|
|
93
|
+
border-radius: 0 8px 8px 0;
|
|
94
|
+
font-size: 11px; color: rgba(255,255,255,0.55);
|
|
95
|
+
line-height: 1.5;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.footer {
|
|
99
|
+
text-align: center; margin-top: 32px;
|
|
100
|
+
font-size: 10px; color: rgba(255,255,255,0.2);
|
|
101
|
+
}
|
|
102
|
+
.model-label {
|
|
103
|
+
position: fixed; bottom: 10px; right: 14px;
|
|
104
|
+
font-size: 9px; color: rgba(255,255,255,0.2);
|
|
105
|
+
background: rgba(0,0,0,0.3);
|
|
106
|
+
padding: 2px 8px; border-radius: 4px;
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
109
|
+
</head>
|
|
110
|
+
<body>
|
|
111
|
+
<div class="map">
|
|
112
|
+
<div class="map-header">
|
|
113
|
+
<h1 id="mapTitle">App Flow Map</h1>
|
|
114
|
+
<p id="mapSubtitle"></p>
|
|
115
|
+
</div>
|
|
116
|
+
<div id="mapContent"></div>
|
|
117
|
+
<div class="footer" id="mapFooter">Generated by DiscoveryLab</div>
|
|
118
|
+
<div class="model-label" id="modelLabel"></div>
|
|
119
|
+
</div>
|
|
120
|
+
<script>
|
|
121
|
+
const DATA = window.__VISUALIZATION_DATA__ || {};
|
|
122
|
+
const phases = DATA.phases || [];
|
|
123
|
+
|
|
124
|
+
if (DATA.title) document.getElementById('mapTitle').textContent = DATA.title;
|
|
125
|
+
if (DATA.subtitle) document.getElementById('mapSubtitle').textContent = DATA.subtitle;
|
|
126
|
+
if (DATA.providerName) document.getElementById('modelLabel').textContent = DATA.providerName;
|
|
127
|
+
|
|
128
|
+
const container = document.getElementById('mapContent');
|
|
129
|
+
let animDelay = 0.3;
|
|
130
|
+
|
|
131
|
+
phases.forEach((phase, phaseIdx) => {
|
|
132
|
+
// Arrow between phases
|
|
133
|
+
if (phaseIdx > 0) {
|
|
134
|
+
const arrow = document.createElement('div');
|
|
135
|
+
arrow.className = 'arrow-connector';
|
|
136
|
+
arrow.style.animationDelay = animDelay + 's';
|
|
137
|
+
arrow.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24"><path d="M12 5v14M5 12l7 7 7-7" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
138
|
+
container.appendChild(arrow);
|
|
139
|
+
animDelay += 0.15;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const phaseEl = document.createElement('div');
|
|
143
|
+
phaseEl.className = `phase phase-${phaseIdx % 5}`;
|
|
144
|
+
phaseEl.style.animationDelay = animDelay + 's';
|
|
145
|
+
|
|
146
|
+
let html = `
|
|
147
|
+
<div class="phase-title">${phase.title || 'Phase ' + (phaseIdx + 1)}</div>
|
|
148
|
+
<div class="phase-desc">${phase.description || ''}</div>
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
// Screen cards
|
|
152
|
+
if (phase.screens && phase.screens.length > 0) {
|
|
153
|
+
html += '<div class="phase-screens">';
|
|
154
|
+
phase.screens.forEach(screen => {
|
|
155
|
+
html += `
|
|
156
|
+
<div class="screen-card">
|
|
157
|
+
${screen.imageUrl ? `<div class="screen-card-img"><img src="${screen.imageUrl}" alt="" loading="lazy"></div>` : ''}
|
|
158
|
+
<div class="screen-card-label">${screen.label || ''}</div>
|
|
159
|
+
${screen.detail ? `<div class="screen-card-detail">${screen.detail}</div>` : ''}
|
|
160
|
+
</div>
|
|
161
|
+
`;
|
|
162
|
+
});
|
|
163
|
+
html += '</div>';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Insight
|
|
167
|
+
if (phase.insight) {
|
|
168
|
+
html += `<div class="insight">${phase.insight}</div>`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
phaseEl.innerHTML = html;
|
|
172
|
+
container.appendChild(phaseEl);
|
|
173
|
+
animDelay += 0.3;
|
|
174
|
+
});
|
|
175
|
+
</script>
|
|
176
|
+
</body>
|
|
177
|
+
</html>
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Device Showcase</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body {
|
|
10
|
+
background: linear-gradient(135deg, #0c0c1d, #1e1e3f, #0c0c1d);
|
|
11
|
+
color: #fff;
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
align-items: center;
|
|
18
|
+
justify-content: center;
|
|
19
|
+
}
|
|
20
|
+
.header { text-align: center; margin-bottom: 32px; z-index: 10; }
|
|
21
|
+
.header h1 { font-size: 28px; font-weight: 700; }
|
|
22
|
+
.header p { font-size: 13px; color: rgba(255,255,255,0.5); margin-top: 4px; }
|
|
23
|
+
.carousel-wrapper {
|
|
24
|
+
perspective: 1200px;
|
|
25
|
+
width: 100%;
|
|
26
|
+
max-width: 800px;
|
|
27
|
+
height: 500px;
|
|
28
|
+
position: relative;
|
|
29
|
+
}
|
|
30
|
+
.carousel {
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
position: relative;
|
|
34
|
+
transform-style: preserve-3d;
|
|
35
|
+
animation: spin 20s linear infinite;
|
|
36
|
+
animation-play-state: running;
|
|
37
|
+
}
|
|
38
|
+
.carousel:hover { animation-play-state: paused; }
|
|
39
|
+
@keyframes spin { to { transform: rotateY(360deg); } }
|
|
40
|
+
.device {
|
|
41
|
+
position: absolute;
|
|
42
|
+
width: 200px;
|
|
43
|
+
left: 50%;
|
|
44
|
+
top: 50%;
|
|
45
|
+
margin-left: -100px;
|
|
46
|
+
margin-top: -200px;
|
|
47
|
+
transform-style: preserve-3d;
|
|
48
|
+
transition: transform 0.5s, box-shadow 0.5s;
|
|
49
|
+
}
|
|
50
|
+
.device-frame {
|
|
51
|
+
background: #111;
|
|
52
|
+
border-radius: 24px;
|
|
53
|
+
padding: 8px;
|
|
54
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.1);
|
|
55
|
+
border: 1px solid rgba(255,255,255,0.08);
|
|
56
|
+
}
|
|
57
|
+
.device-screen {
|
|
58
|
+
border-radius: 16px;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
background: #000;
|
|
61
|
+
}
|
|
62
|
+
.device-screen img {
|
|
63
|
+
width: 100%;
|
|
64
|
+
height: auto;
|
|
65
|
+
display: block;
|
|
66
|
+
}
|
|
67
|
+
.device-label {
|
|
68
|
+
text-align: center;
|
|
69
|
+
font-size: 11px;
|
|
70
|
+
color: rgba(255,255,255,0.5);
|
|
71
|
+
margin-top: 12px;
|
|
72
|
+
}
|
|
73
|
+
.glow {
|
|
74
|
+
position: absolute;
|
|
75
|
+
width: 300px; height: 300px;
|
|
76
|
+
background: radial-gradient(circle, rgba(99,102,241,0.15) 0%, transparent 70%);
|
|
77
|
+
top: 50%; left: 50%;
|
|
78
|
+
transform: translate(-50%, -50%);
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
}
|
|
81
|
+
.footer {
|
|
82
|
+
position: fixed; bottom: 16px;
|
|
83
|
+
font-size: 11px; color: rgba(255,255,255,0.25);
|
|
84
|
+
}
|
|
85
|
+
.nav-dots {
|
|
86
|
+
display: flex; gap: 8px; margin-top: 24px; z-index: 10;
|
|
87
|
+
}
|
|
88
|
+
.nav-dot {
|
|
89
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
90
|
+
background: rgba(255,255,255,0.2);
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
transition: background 0.3s;
|
|
93
|
+
}
|
|
94
|
+
.nav-dot.active { background: #6366f1; }
|
|
95
|
+
</style>
|
|
96
|
+
</head>
|
|
97
|
+
<body>
|
|
98
|
+
<div class="header">
|
|
99
|
+
<h1 id="vizTitle">Device Showcase</h1>
|
|
100
|
+
<p id="vizSubtitle"></p>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="carousel-wrapper">
|
|
103
|
+
<div class="glow"></div>
|
|
104
|
+
<div class="carousel" id="carousel"></div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="nav-dots" id="navDots"></div>
|
|
107
|
+
<div class="footer" id="vizFooter">Generated by DiscoveryLab</div>
|
|
108
|
+
<script>
|
|
109
|
+
const DATA = window.__VISUALIZATION_DATA__ || {};
|
|
110
|
+
const screens = DATA.screens || [];
|
|
111
|
+
|
|
112
|
+
if (DATA.title) document.getElementById('vizTitle').textContent = DATA.title;
|
|
113
|
+
if (DATA.subtitle) document.getElementById('vizSubtitle').textContent = DATA.subtitle;
|
|
114
|
+
|
|
115
|
+
const carousel = document.getElementById('carousel');
|
|
116
|
+
const dotsContainer = document.getElementById('navDots');
|
|
117
|
+
const n = screens.length || 1;
|
|
118
|
+
const angleStep = 360 / n;
|
|
119
|
+
const radius = Math.max(250, n * 60);
|
|
120
|
+
|
|
121
|
+
screens.forEach((screen, i) => {
|
|
122
|
+
const angle = angleStep * i;
|
|
123
|
+
const device = document.createElement('div');
|
|
124
|
+
device.className = 'device';
|
|
125
|
+
device.style.transform = `rotateY(${angle}deg) translateZ(${radius}px)`;
|
|
126
|
+
device.innerHTML = `
|
|
127
|
+
<div class="device-frame">
|
|
128
|
+
<div class="device-screen">
|
|
129
|
+
<img src="${screen.imageUrl}" alt="Screen ${i + 1}" loading="lazy">
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
<div class="device-label">${screen.label || `Screen ${i + 1}`}</div>
|
|
133
|
+
`;
|
|
134
|
+
carousel.appendChild(device);
|
|
135
|
+
|
|
136
|
+
const dot = document.createElement('div');
|
|
137
|
+
dot.className = `nav-dot ${i === 0 ? 'active' : ''}`;
|
|
138
|
+
dot.addEventListener('click', () => {
|
|
139
|
+
carousel.style.animation = 'none';
|
|
140
|
+
carousel.offsetHeight; // reflow
|
|
141
|
+
carousel.style.transform = `rotateY(${-angle}deg)`;
|
|
142
|
+
carousel.style.transition = 'transform 0.8s ease';
|
|
143
|
+
dotsContainer.querySelectorAll('.nav-dot').forEach(d => d.classList.remove('active'));
|
|
144
|
+
dot.classList.add('active');
|
|
145
|
+
});
|
|
146
|
+
dotsContainer.appendChild(dot);
|
|
147
|
+
});
|
|
148
|
+
</script>
|
|
149
|
+
</body>
|
|
150
|
+
</html>
|