@veolab/discoverylab 1.4.3 → 1.4.4

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.
@@ -12,7 +12,7 @@
12
12
  "name": "discoverylab",
13
13
  "source": ".",
14
14
  "description": "AI-powered app testing & marketing asset generator. Record mobile/web apps, run automated tests with Maestro & Playwright, and generate professional screenshots, GIFs, and test reports.",
15
- "version": "1.4.3",
15
+ "version": "1.4.4",
16
16
  "author": {
17
17
  "name": "Anderson Melo"
18
18
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "discoverylab",
3
3
  "description": "AI-powered app testing & marketing asset generator. Record mobile/web apps, run automated tests with Maestro & Playwright, and generate professional screenshots, GIFs, and test reports.",
4
- "version": "1.4.3",
4
+ "version": "1.4.4",
5
5
  "author": {
6
6
  "name": "Anderson Melo",
7
7
  "email": "anderson.90@gmail.com"
@@ -5804,11 +5804,17 @@ Rules:
5804
5804
  default:
5805
5805
  return c.json({ error: `Unknown template: ${templateId}` }, 400);
5806
5806
  }
5807
- const templatePath = join6(__dirname, "..", "core", "visualizations", "templates", `${templateId}.html`);
5808
5807
  const possiblePaths = [
5809
- templatePath,
5810
- join6(__dirname, "..", "..", "src", "core", "visualizations", "templates", `${templateId}.html`),
5808
+ join6(__dirname, "..", "visualizations", `${templateId}.html`),
5809
+ // dist/visualizations/
5810
+ join6(__dirname, "visualizations", `${templateId}.html`),
5811
+ // dist/visualizations/ (alt)
5812
+ join6(__dirname, "..", "core", "visualizations", "templates", `${templateId}.html`),
5813
+ // dist/core/...
5814
+ join6(process.cwd(), "dist", "visualizations", `${templateId}.html`),
5815
+ // cwd/dist/visualizations/
5811
5816
  join6(process.cwd(), "src", "core", "visualizations", "templates", `${templateId}.html`)
5817
+ // dev: src/
5812
5818
  ];
5813
5819
  let htmlContent = "";
5814
5820
  for (const p of possiblePaths) {
package/dist/cli.js CHANGED
@@ -389,7 +389,7 @@ program.command("serve").alias("server").description("Start the DiscoveryLab web
389
389
  console.log(chalk.cyan("\n DiscoveryLab"));
390
390
  console.log(chalk.gray(" AI-powered app testing & evidence generator\n"));
391
391
  try {
392
- const { startServer } = await import("./server-CZPWQYOI.js");
392
+ const { startServer } = await import("./server-QFNKZCOJ.js");
393
393
  await startServer(port);
394
394
  console.log(chalk.green(` Server running at http://localhost:${port}`));
395
395
  console.log(chalk.gray(" Press Ctrl+C to stop\n"));
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  startServer,
4
4
  stopServer
5
- } from "./chunk-D4FTLCKM.js";
5
+ } from "./chunk-2UUMLAVR.js";
6
6
  import {
7
7
  analyzeTools,
8
8
  canvasTools,
@@ -3,7 +3,7 @@ import {
3
3
  getServerPort,
4
4
  startServer,
5
5
  stopServer
6
- } from "./chunk-D4FTLCKM.js";
6
+ } from "./chunk-2UUMLAVR.js";
7
7
  import "./chunk-34GGYFXX.js";
8
8
  import "./chunk-6GK5K6CS.js";
9
9
  import "./chunk-7R5YNOXE.js";
@@ -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>
@@ -0,0 +1,181 @@
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>Flow Diagram</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ background: linear-gradient(160deg, #0f0f23, #1a1a3e);
11
+ color: #fff;
12
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
13
+ min-height: 100vh;
14
+ display: flex;
15
+ flex-direction: column;
16
+ }
17
+ .container {
18
+ flex: 1;
19
+ display: flex;
20
+ flex-direction: column;
21
+ padding: 24px;
22
+ max-width: 100%;
23
+ overflow: hidden;
24
+ }
25
+ .header { text-align: center; margin-bottom: 20px; flex-shrink: 0; }
26
+ .header h1 { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
27
+ .header p { font-size: 13px; color: rgba(255,255,255,0.5); }
28
+ .flow {
29
+ flex: 1;
30
+ display: flex;
31
+ flex-direction: row;
32
+ align-items: center;
33
+ justify-content: center;
34
+ gap: 0;
35
+ padding: 10px 0;
36
+ min-height: 0;
37
+ }
38
+ .flow.vertical {
39
+ flex-direction: column;
40
+ overflow-y: auto;
41
+ overflow-x: hidden;
42
+ }
43
+ .step {
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: center;
47
+ flex-shrink: 1;
48
+ min-width: 0;
49
+ max-width: 220px;
50
+ opacity: 0;
51
+ transform: translateY(20px);
52
+ animation: fadeInUp 0.5s forwards;
53
+ /* delay set dynamically via JS for correct sequential order */
54
+ }
55
+ @keyframes fadeInUp {
56
+ to { opacity: 1; transform: translateY(0); }
57
+ }
58
+ .step-badge {
59
+ width: 28px; height: 28px; border-radius: 50%;
60
+ background: #6366f1; color: #fff;
61
+ display: flex; align-items: center; justify-content: center;
62
+ font-size: 12px; font-weight: 700;
63
+ margin-bottom: 8px;
64
+ box-shadow: 0 3px 10px rgba(99,102,241,0.4);
65
+ flex-shrink: 0;
66
+ }
67
+ .step-screenshot {
68
+ width: 100%;
69
+ max-height: 55vh;
70
+ border-radius: 10px;
71
+ overflow: hidden;
72
+ box-shadow: 0 6px 20px rgba(0,0,0,0.3);
73
+ border: 1px solid rgba(255,255,255,0.1);
74
+ transition: transform 0.3s, box-shadow 0.3s;
75
+ flex-shrink: 1;
76
+ }
77
+ .step-screenshot:hover {
78
+ transform: scale(1.03);
79
+ box-shadow: 0 10px 30px rgba(99,102,241,0.25);
80
+ }
81
+ .step-screenshot img {
82
+ width: 100%;
83
+ height: auto;
84
+ display: block;
85
+ max-height: 55vh;
86
+ object-fit: contain;
87
+ }
88
+ .step-label {
89
+ font-size: 11px;
90
+ color: rgba(255,255,255,0.7);
91
+ margin-top: 8px;
92
+ text-align: center;
93
+ max-width: 100%;
94
+ overflow: hidden;
95
+ text-overflow: ellipsis;
96
+ white-space: nowrap;
97
+ flex-shrink: 0;
98
+ }
99
+ .arrow {
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ flex-shrink: 0;
104
+ padding: 0 4px;
105
+ opacity: 0;
106
+ animation: fadeInArrow 0.3s forwards;
107
+ /* delay set dynamically via JS */
108
+ }
109
+ @keyframes fadeInArrow { to { opacity: 1; } }
110
+ .arrow svg {
111
+ width: 20px; height: 20px;
112
+ stroke: rgba(255,255,255,0.35);
113
+ fill: none;
114
+ stroke-width: 2;
115
+ }
116
+ .flow.vertical .arrow { transform: rotate(90deg); }
117
+ .footer {
118
+ text-align: center;
119
+ padding: 12px;
120
+ font-size: 10px;
121
+ color: rgba(255,255,255,0.25);
122
+ flex-shrink: 0;
123
+ }
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <div class="container">
128
+ <div class="header">
129
+ <h1 id="vizTitle">App Flow</h1>
130
+ <p id="vizSubtitle"></p>
131
+ </div>
132
+ <div class="flow" id="flowContainer"></div>
133
+ <div class="footer" id="vizFooter">Generated by DiscoveryLab</div>
134
+ </div>
135
+ <script>
136
+ const DATA = window.__VISUALIZATION_DATA__ || {};
137
+ const steps = DATA.steps || [];
138
+ const direction = DATA.direction || 'horizontal';
139
+ const count = steps.length;
140
+
141
+ if (DATA.title) document.getElementById('vizTitle').textContent = DATA.title;
142
+ if (DATA.subtitle) document.getElementById('vizSubtitle').textContent = DATA.subtitle;
143
+
144
+ const container = document.getElementById('flowContainer');
145
+ if (direction === 'vertical') container.classList.add('vertical');
146
+
147
+ // Auto-size steps based on count and viewport
148
+ const maxStepWidth = Math.min(220, Math.floor((window.innerWidth - 80 - (count - 1) * 28) / count));
149
+
150
+ // Sequential animation: step1 → arrow1 → step2 → arrow2 → step3 ...
151
+ const baseDelay = 0.2; // seconds
152
+ const stepDuration = 0.3; // time between each element appearing
153
+
154
+ steps.forEach((step, i) => {
155
+ const elementIndex = i * 2; // step0=0, arrow0=1, step1=2, arrow1=3, step2=4...
156
+
157
+ if (i > 0) {
158
+ const arrowIndex = (i * 2) - 1;
159
+ const arrow = document.createElement('div');
160
+ arrow.className = 'arrow';
161
+ arrow.style.animationDelay = (baseDelay + arrowIndex * stepDuration) + 's';
162
+ arrow.innerHTML = '<svg viewBox="0 0 24 24"><path d="M5 12h14M12 5l7 7-7 7" stroke-linecap="round" stroke-linejoin="round"/></svg>';
163
+ container.appendChild(arrow);
164
+ }
165
+
166
+ const el = document.createElement('div');
167
+ el.className = 'step';
168
+ el.style.maxWidth = maxStepWidth + 'px';
169
+ el.style.animationDelay = (baseDelay + elementIndex * stepDuration) + 's';
170
+ el.innerHTML = `
171
+ <div class="step-badge">${step.number || i + 1}</div>
172
+ <div class="step-screenshot">
173
+ <img src="${step.imageUrl}" alt="Step ${i + 1}">
174
+ </div>
175
+ <div class="step-label">${step.label || ''}</div>
176
+ `;
177
+ container.appendChild(el);
178
+ });
179
+ </script>
180
+ </body>
181
+ </html>
@@ -0,0 +1,263 @@
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>Metrics Dashboard</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ background: #0f0f1a;
11
+ color: #fff;
12
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
13
+ min-height: 100vh;
14
+ padding: 24px;
15
+ }
16
+ .dashboard { max-width: 960px; margin: 0 auto; }
17
+ .header { margin-bottom: 24px; }
18
+ .header h1 { font-size: 22px; font-weight: 700; }
19
+ .header p { font-size: 13px; color: rgba(255,255,255,0.4); margin-top: 4px; }
20
+
21
+ /* Metrics cards */
22
+ .metrics-grid {
23
+ display: grid;
24
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
25
+ gap: 12px;
26
+ margin-bottom: 24px;
27
+ }
28
+ .metric-card {
29
+ background: rgba(255,255,255,0.04);
30
+ border: 1px solid rgba(255,255,255,0.08);
31
+ border-radius: 12px;
32
+ padding: 16px;
33
+ opacity: 0;
34
+ transform: scale(0.95);
35
+ animation: popIn 0.4s forwards;
36
+ }
37
+ .metric-card:nth-child(1) { animation-delay: 0.1s; }
38
+ .metric-card:nth-child(2) { animation-delay: 0.2s; }
39
+ .metric-card:nth-child(3) { animation-delay: 0.3s; }
40
+ .metric-card:nth-child(4) { animation-delay: 0.4s; }
41
+ @keyframes popIn { to { opacity: 1; transform: scale(1); } }
42
+ .metric-value {
43
+ font-size: 32px; font-weight: 800;
44
+ background: linear-gradient(135deg, #6366f1, #a78bfa);
45
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
46
+ background-clip: text;
47
+ }
48
+ .metric-label { font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 2px; }
49
+
50
+ /* Two-column layout for charts + screens */
51
+ .content-grid {
52
+ display: grid;
53
+ grid-template-columns: 1fr 1fr;
54
+ gap: 16px;
55
+ margin-bottom: 24px;
56
+ }
57
+ @media (max-width: 700px) { .content-grid { grid-template-columns: 1fr; } }
58
+
59
+ .section-card {
60
+ background: rgba(255,255,255,0.03);
61
+ border: 1px solid rgba(255,255,255,0.06);
62
+ border-radius: 12px;
63
+ padding: 16px;
64
+ }
65
+ .section-title { font-size: 13px; font-weight: 600; margin-bottom: 12px; color: rgba(255,255,255,0.8); }
66
+
67
+ /* Bar chart */
68
+ .bar-chart { display: flex; flex-direction: column; gap: 6px; }
69
+ .bar-row {
70
+ display: grid; grid-template-columns: 90px 1fr 32px;
71
+ gap: 8px; align-items: center;
72
+ opacity: 0; animation: slideIn 0.5s forwards;
73
+ }
74
+ .bar-row:nth-child(1) { animation-delay: 0.5s; }
75
+ .bar-row:nth-child(2) { animation-delay: 0.6s; }
76
+ .bar-row:nth-child(3) { animation-delay: 0.7s; }
77
+ .bar-row:nth-child(4) { animation-delay: 0.8s; }
78
+ @keyframes slideIn { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: translateX(0); } }
79
+ .bar-label { font-size: 10px; color: rgba(255,255,255,0.6); text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
80
+ .bar-track { height: 20px; background: rgba(255,255,255,0.06); border-radius: 5px; overflow: hidden; }
81
+ .bar-fill { height: 100%; border-radius: 5px; background: linear-gradient(90deg, #6366f1, #818cf8); transition: width 1s ease; width: 0; }
82
+ .bar-value { font-size: 11px; color: rgba(255,255,255,0.7); font-weight: 600; }
83
+
84
+ /* Screen thumbnails strip */
85
+ .screens-strip {
86
+ display: flex;
87
+ gap: 8px;
88
+ overflow-x: auto;
89
+ padding: 4px 0;
90
+ }
91
+ .screen-thumb {
92
+ flex-shrink: 0;
93
+ width: 80px;
94
+ border-radius: 8px;
95
+ overflow: hidden;
96
+ border: 1px solid rgba(255,255,255,0.1);
97
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
98
+ opacity: 0;
99
+ animation: thumbIn 0.4s forwards;
100
+ position: relative;
101
+ transition: transform 0.2s;
102
+ }
103
+ .screen-thumb:hover { transform: scale(1.08); }
104
+ .screen-thumb img { width: 100%; height: auto; display: block; }
105
+ .screen-thumb-label {
106
+ position: absolute; bottom: 0; left: 0; right: 0;
107
+ background: linear-gradient(transparent, rgba(0,0,0,0.8));
108
+ padding: 12px 4px 4px;
109
+ font-size: 8px; color: rgba(255,255,255,0.8);
110
+ text-align: center;
111
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
112
+ }
113
+ .screen-thumb:nth-child(1) { animation-delay: 0.3s; }
114
+ .screen-thumb:nth-child(2) { animation-delay: 0.4s; }
115
+ .screen-thumb:nth-child(3) { animation-delay: 0.5s; }
116
+ .screen-thumb:nth-child(4) { animation-delay: 0.6s; }
117
+ .screen-thumb:nth-child(5) { animation-delay: 0.7s; }
118
+ .screen-thumb:nth-child(6) { animation-delay: 0.8s; }
119
+ @keyframes thumbIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
120
+
121
+ /* Flow steps with thumbnails */
122
+ .flow-steps { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
123
+ .flow-step {
124
+ display: flex; align-items: center; gap: 10px;
125
+ padding: 8px 10px;
126
+ background: rgba(99,102,241,0.06);
127
+ border: 1px solid rgba(99,102,241,0.12);
128
+ border-radius: 10px;
129
+ opacity: 0;
130
+ animation: fadeIn 0.3s forwards;
131
+ }
132
+ @keyframes fadeIn { to { opacity: 1; } }
133
+ .flow-step-num {
134
+ width: 22px; height: 22px; border-radius: 50%;
135
+ background: #6366f1; color: #fff;
136
+ display: flex; align-items: center; justify-content: center;
137
+ font-size: 10px; font-weight: 700; flex-shrink: 0;
138
+ }
139
+ .flow-step-thumb {
140
+ width: 36px; height: 64px; border-radius: 4px;
141
+ overflow: hidden; flex-shrink: 0;
142
+ border: 1px solid rgba(255,255,255,0.1);
143
+ }
144
+ .flow-step-thumb img { width: 100%; height: 100%; object-fit: cover; }
145
+ .flow-step-label { font-size: 11px; color: rgba(255,255,255,0.7); flex: 1; }
146
+
147
+ .footer {
148
+ text-align: center; margin-top: 24px;
149
+ font-size: 10px; color: rgba(255,255,255,0.2);
150
+ }
151
+ </style>
152
+ </head>
153
+ <body>
154
+ <div class="dashboard">
155
+ <div class="header">
156
+ <h1 id="vizTitle">Analysis Dashboard</h1>
157
+ <p id="vizSubtitle"></p>
158
+ </div>
159
+
160
+ <div class="metrics-grid" id="metricsGrid"></div>
161
+
162
+ <!-- Screen thumbnails strip -->
163
+ <div class="section-card" style="margin-bottom: 16px;" id="screensSection">
164
+ <div class="section-title">Screens Captured</div>
165
+ <div class="screens-strip" id="screensStrip"></div>
166
+ </div>
167
+
168
+ <div class="content-grid">
169
+ <!-- Left: Bar chart -->
170
+ <div class="section-card">
171
+ <div class="section-title">UI Elements Distribution</div>
172
+ <div class="bar-chart" id="barChart"></div>
173
+ </div>
174
+
175
+ <!-- Right: Flow steps with thumbnails -->
176
+ <div class="section-card">
177
+ <div class="section-title">User Flow</div>
178
+ <div class="flow-steps" id="flowSteps"></div>
179
+ </div>
180
+ </div>
181
+
182
+ <div class="footer" id="vizFooter">Generated by DiscoveryLab</div>
183
+ </div>
184
+ <script>
185
+ const DATA = window.__VISUALIZATION_DATA__ || {};
186
+ const screens = DATA.screens || [];
187
+
188
+ if (DATA.title) document.getElementById('vizTitle').textContent = DATA.title;
189
+ if (DATA.subtitle) document.getElementById('vizSubtitle').textContent = DATA.subtitle;
190
+
191
+ // Metrics cards
192
+ const metrics = DATA.metrics || [
193
+ { value: DATA.screenCount || 0, label: 'Screens Analyzed' },
194
+ { value: DATA.flowSteps || 0, label: 'Flow Steps' },
195
+ { value: DATA.uiElements || 0, label: 'UI Elements' },
196
+ { value: DATA.platform || 'N/A', label: 'Platform' },
197
+ ];
198
+ const metricsGrid = document.getElementById('metricsGrid');
199
+ metrics.forEach(m => {
200
+ const card = document.createElement('div');
201
+ card.className = 'metric-card';
202
+ card.innerHTML = `
203
+ <div class="metric-value">${m.value}</div>
204
+ <div class="metric-label">${m.label}</div>
205
+ `;
206
+ metricsGrid.appendChild(card);
207
+ });
208
+
209
+ // Screen thumbnails strip
210
+ const screensStrip = document.getElementById('screensStrip');
211
+ const screensSection = document.getElementById('screensSection');
212
+ if (screens.length > 0) {
213
+ screens.forEach((screen, i) => {
214
+ const el = document.createElement('div');
215
+ el.className = 'screen-thumb';
216
+ el.style.animationDelay = (0.3 + i * 0.1) + 's';
217
+ el.innerHTML = `
218
+ <img src="${screen.imageUrl}" alt="Screen ${i + 1}">
219
+ <div class="screen-thumb-label">${screen.label || 'Screen ' + (i + 1)}</div>
220
+ `;
221
+ screensStrip.appendChild(el);
222
+ });
223
+ } else {
224
+ screensSection.style.display = 'none';
225
+ }
226
+
227
+ // Bar chart
228
+ const categories = DATA.categories || [];
229
+ const barChart = document.getElementById('barChart');
230
+ const maxVal = Math.max(...categories.map(c => c.count), 1);
231
+ categories.forEach(cat => {
232
+ const pct = Math.round((cat.count / maxVal) * 100);
233
+ const row = document.createElement('div');
234
+ row.className = 'bar-row';
235
+ row.innerHTML = `
236
+ <div class="bar-label" title="${cat.name}">${cat.name}</div>
237
+ <div class="bar-track"><div class="bar-fill" style="width: 0%"></div></div>
238
+ <div class="bar-value">${cat.count}</div>
239
+ `;
240
+ barChart.appendChild(row);
241
+ requestAnimationFrame(() => {
242
+ setTimeout(() => { row.querySelector('.bar-fill').style.width = pct + '%'; }, 800);
243
+ });
244
+ });
245
+
246
+ // Flow steps with thumbnails
247
+ const steps = DATA.steps || [];
248
+ const flowSteps = document.getElementById('flowSteps');
249
+ steps.forEach((step, i) => {
250
+ const screen = screens[i];
251
+ const el = document.createElement('div');
252
+ el.className = 'flow-step';
253
+ el.style.animationDelay = (0.8 + i * 0.1) + 's';
254
+ el.innerHTML = `
255
+ <div class="flow-step-num">${i + 1}</div>
256
+ ${screen ? `<div class="flow-step-thumb"><img src="${screen.imageUrl}" alt=""></div>` : ''}
257
+ <div class="flow-step-label">${step.label || step}</div>
258
+ `;
259
+ flowSteps.appendChild(el);
260
+ });
261
+ </script>
262
+ </body>
263
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@veolab/discoverylab",
3
- "version": "1.4.3",
3
+ "version": "1.4.4",
4
4
  "description": "AI-powered app testing & evidence generator - Claude Code Plugin",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "scripts": {
12
12
  "dev": "tsx watch src/cli.ts serve",
13
- "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean && cp src/web/index.html dist/index.html && node scripts/build-host-runtime.mjs --best-effort",
13
+ "build": "tsup src/index.ts src/cli.ts --format esm --dts --clean && cp src/web/index.html dist/index.html && mkdir -p dist/visualizations && cp src/core/visualizations/templates/*.html dist/visualizations/ && node scripts/build-host-runtime.mjs --best-effort",
14
14
  "build:host-runtime": "node scripts/build-host-runtime.mjs",
15
15
  "pack:local": "npm run build && npm run build:host-runtime && node scripts/verify-host-runtime-bundle.mjs && npm pack",
16
16
  "prepack": "npm run build && npm run build:host-runtime && node scripts/verify-host-runtime-bundle.mjs",
@@ -68,7 +68,7 @@
68
68
  "claude-plugin": {
69
69
  "name": "DiscoveryLab",
70
70
  "description": "AI-powered app testing & evidence generator",
71
- "version": "1.4.3",
71
+ "version": "1.4.4",
72
72
  "tools": [
73
73
  "dlab.capture.screen",
74
74
  "dlab.capture.emulator",