@capillarytech/cap-ui-dev-tools 1.2.0 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capillarytech/cap-ui-dev-tools",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Development tools for Capillary UI projects including webpack hot-reload plugin and CapVision session recording",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -155,6 +155,207 @@ class ReportEnhancer {
155
155
  }
156
156
  }
157
157
 
158
+ /**
159
+ * Generate logo HTML header with toggle button
160
+ * @private
161
+ * @returns {string} Logo HTML
162
+ */
163
+ generateLogoHTML() {
164
+ // Inline SVG logo - CapVision branded logo (icon size)
165
+ const logoSVG = `
166
+ <svg width="120" height="80" viewBox="0 0 1600 800" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="display: inline-block; vertical-align: middle;">
167
+ <defs>
168
+ <!-- Neon gradient used for strokes/fills -->
169
+ <linearGradient id="neonGradient" x1="0" x2="1" y1="0" y2="1">
170
+ <stop offset="0%" stop-color="#00D4FF"/>
171
+ <stop offset="25%" stop-color="#3A7CFF"/>
172
+ <stop offset="50%" stop-color="#9A4BFF"/>
173
+ <stop offset="70%" stop-color="#FF4CA3"/>
174
+ <stop offset="85%" stop-color="#FF8E3C"/>
175
+ <stop offset="100%" stop-color="#FFD800"/>
176
+ </linearGradient>
177
+
178
+ <!-- Wordmark gradient (cool blue) -->
179
+ <linearGradient id="wordGradient" x1="0" x2="1">
180
+ <stop offset="0%" stop-color="#19C6FF"/>
181
+ <stop offset="100%" stop-color="#2EA6FF"/>
182
+ </linearGradient>
183
+
184
+ <!-- Soft inner shadow for icon fill -->
185
+ <linearGradient id="iconFill" x1="0" x2="1">
186
+ <stop offset="0%" stop-color="#061022" stop-opacity="0.0"/>
187
+ <stop offset="100%" stop-color="#061022" stop-opacity="0.08"/>
188
+ </linearGradient>
189
+
190
+ <!-- Stroke style for neon lines -->
191
+ <style type="text/css"><![CDATA[
192
+ .neon { stroke: url(#neonGradient); stroke-width:14; stroke-linecap:square; stroke-linejoin:miter; fill: none; }
193
+ .neon-thin { stroke: url(#neonGradient); stroke-width:8; stroke-linecap:square; stroke-linejoin:miter; fill: none; }
194
+ .eye-fill { fill: none; stroke: url(#neonGradient); stroke-width:12; stroke-linecap:square; stroke-linejoin:miter; }
195
+ .circuit { stroke: url(#neonGradient); stroke-width:10; stroke-linecap:square; fill: none; }
196
+ .accent-dot { fill: url(#neonGradient); }
197
+ .word { font-family: "Inter", "Montserrat", sans-serif; font-weight: 700; font-size:120px; fill: url(#wordGradient); letter-spacing: -2px; }
198
+ .bg-texture { opacity: 0.02; }
199
+ ]]></style>
200
+ </defs>
201
+
202
+ <!-- Dark background (keeps high contrast for presentation and dashboards) -->
203
+ <rect id="bg" x="0" y="0" width="1600" height="800" fill="#060719" />
204
+
205
+ <!-- subtle grid/texture (very faint) -->
206
+ <g class="bg-texture" transform="translate(0,0)">
207
+ <rect x="0" y="0" width="1600" height="800" fill="url(#iconFill)" />
208
+ </g>
209
+
210
+ <!-- LEFT ICON GROUP -->
211
+ <g id="icon" transform="translate(160,210) scale(1.4)">
212
+ <!-- robot-head-ish rounded rectangle -->
213
+ <rect x="0" y="0" rx="8" ry="8" width="220" height="150" class="neon" />
214
+
215
+ <!-- antenna -->
216
+ <line x1="110" y1="-30" x2="110" y2="0" class="neon-thin" />
217
+ <circle cx="110" cy="-36" r="12" class="accent-dot" />
218
+
219
+ <!-- small scan lines / UI fragments on head (to suggest vision/processing) -->
220
+ <path d="M170,30 h40" class="neon-thin" stroke-linecap="square" />
221
+ <path d="M162,54 h30" class="neon-thin" stroke-linecap="square" />
222
+ <path d="M28,28 h-12" class="neon-thin" />
223
+ <rect x="20" y="20" width="6" height="28" rx="1" class="neon-thin" />
224
+
225
+ <!-- Abstract eye -->
226
+ <g transform="translate(10,50)">
227
+ <!-- eye outline -->
228
+ <path d="M40,60 C20,60 0,44 0,30 C0,16 20,0 40,0 C60,0 80,16 80,30 C80,44 60,60 40,60 Z" class="eye-fill"/>
229
+ <!-- iris concentric rings -->
230
+ <circle cx="40" cy="30" r="16" stroke="url(#neonGradient)" stroke-width="8" fill="none" />
231
+ <circle cx="40" cy="30" r="8" stroke="url(#neonGradient)" stroke-width="6" fill="none" />
232
+ <circle cx="40" cy="30" r="3" fill="#04102A"/>
233
+
234
+ <!-- subtle scan ticks inside eye -->
235
+ <path d="M20,30 h6" class="neon-thin" />
236
+ <path d="M66,30 h6" class="neon-thin" />
237
+ <path d="M40,8 v6" class="neon-thin" />
238
+ <path d="M40,52 v6" class="neon-thin" />
239
+ </g>
240
+
241
+ <!-- Circuit-like legs below eye (AI & automation) -->
242
+ <g transform="translate(15,125)">
243
+ <!-- crossing trace lines -->
244
+ <path d="M20,0 L75,60" class="circuit" />
245
+ <path d="M110,0 L58,60" class="circuit" />
246
+
247
+ <!-- branch nodes -->
248
+ <circle cx="20" cy="0" r="7" class="accent-dot" />
249
+ <circle cx="110" cy="0" r="7" class="accent-dot" />
250
+
251
+ <circle cx="75" cy="60" r="8" class="accent-dot" />
252
+ <circle cx="58" cy="60" r="8" class="accent-dot" />
253
+
254
+ <!-- side traces -->
255
+ <path d="M10,18 L35,38" class="circuit" />
256
+ <path d="M120,18 L95,38" class="circuit" />
257
+
258
+ <circle cx="8" cy="18" r="6" class="accent-dot" />
259
+ <circle cx="122" cy="18" r="6" class="accent-dot" />
260
+ </g>
261
+ </g>
262
+
263
+ <!-- WORDMARK (Right side) -->
264
+ <g id="wordmark" transform="translate(530,360)">
265
+ <!-- Main text -->
266
+ <text class="word" x="0" y="30">CapVision</text>
267
+
268
+ <!-- subtle tech underline / focus ring hint under 'Vision' -->
269
+ <path d="M210,52 h160" stroke="url(#wordGradient)" stroke-width="6" stroke-linecap="square" opacity="0.35" />
270
+ <circle cx="360" cy="28" r="10" fill="none" stroke="url(#wordGradient)" stroke-width="4" opacity="0.45" />
271
+ </g>
272
+
273
+ <!-- Accessibility: invisible title/desc -->
274
+ <title>CapVision — cyberpunk AI vision logo</title>
275
+ <desc>Futuristic neon logo composed of an abstract robotic-eye icon and circuit traces left, and a bold CapVision wordmark right. Gradient neon colors and clean strokes create a cyberpunk high-tech look.</desc>
276
+ </svg>
277
+ `.trim();
278
+
279
+ return `
280
+ <tr class="recording-logo-header">
281
+ <td colspan="2">
282
+ <div style="padding: 20px 15px; background: #fff; border-bottom: 2px solid #e9ecef; display: flex; justify-content: space-between; align-items: center;">
283
+ <span id="capvision-toggle-wrapper"
284
+ onclick="toggleCapVisionRecordings()"
285
+ style="cursor: pointer; padding: 10px; user-select: none; background-color: #C8E6C9; border-radius: 3px; display: inline-block; flex: 1; max-width: calc(100% - 150px); margin-right: 20px;">
286
+ <span id="capvision-toggle-icon" class="collapsed" style="font-size: 12px; display: inline-block;"></span>
287
+ </span>
288
+ <div style="text-align: right;">
289
+ ${logoSVG}
290
+ </div>
291
+ </div>
292
+ </td>
293
+ </tr>`;
294
+ }
295
+
296
+ /**
297
+ * Generate wrapper HTML for recordings (table structure)
298
+ * @private
299
+ * @param {string} logoHTML - Logo HTML
300
+ * @param {string} recordingPlayersHTML - Recording players HTML
301
+ * @returns {string} Wrapped HTML
302
+ */
303
+ generateRecordingsWrapper(logoHTML, recordingPlayersHTML) {
304
+ return `
305
+ <table class="table table-bordered" style="margin-top: 20px;">
306
+ <thead>
307
+ ${logoHTML}
308
+ </thead>
309
+ <tbody id="capvision-recordings-container" class="collapsed" style="display: table-row-group;">
310
+ ${recordingPlayersHTML}
311
+ </tbody>
312
+ </table>
313
+ <style>
314
+ #capvision-recordings-container {
315
+ transition: opacity 0.3s ease-in-out;
316
+ }
317
+ #capvision-recordings-container.collapsed {
318
+ display: none !important;
319
+ }
320
+ #capvision-toggle-wrapper {
321
+ transition: background-color 0.2s ease;
322
+ }
323
+ #capvision-toggle-wrapper:hover {
324
+ background-color: #C8E6C9;
325
+ }
326
+ #capvision-toggle-wrapper:hover #capvision-toggle-icon {
327
+ color: #333;
328
+ }
329
+ #capvision-toggle-icon {
330
+ display: inline-block;
331
+ }
332
+ #capvision-toggle-icon::before {
333
+ font-family: "Glyphicons Halflings", "Font Awesome", Arial, sans-serif;
334
+ color: #333;
335
+ }
336
+ #capvision-toggle-icon.collapsed::before {
337
+ content: "\\e113";
338
+ }
339
+ #capvision-toggle-icon:not(.collapsed)::before {
340
+ content: "\\e114";
341
+ }
342
+ </style>
343
+ <script>
344
+ function toggleCapVisionRecordings() {
345
+ const container = document.getElementById('capvision-recordings-container');
346
+ const toggleIcon = document.getElementById('capvision-toggle-icon');
347
+
348
+ if (container.classList.contains('collapsed')) {
349
+ container.classList.remove('collapsed');
350
+ toggleIcon.classList.remove('collapsed');
351
+ } else {
352
+ container.classList.add('collapsed');
353
+ toggleIcon.classList.add('collapsed');
354
+ }
355
+ }
356
+ </script>`;
357
+ }
358
+
158
359
  /**
159
360
  * Add inline recording players to HTML content
160
361
  * @private
@@ -167,17 +368,60 @@ class ReportEnhancer {
167
368
  return htmlContent;
168
369
  }
169
370
 
170
- const testHeaderRegex = /(<tr class="test-header">[\s\S]*?<\/tr>)/;
371
+ // Find table with class "table table-bordered table-suite"
372
+ const tableSuiteRegex = /<table[^>]*class=["'][^"']*table[^"']*table-bordered[^"']*table-suite[^"']*["'][^>]*>/i;
373
+ const match = htmlContent.match(tableSuiteRegex);
374
+
375
+ if (!match) {
376
+ // Table not found, return original content
377
+ return htmlContent;
378
+ }
379
+
380
+ const tableStartIndex = match.index;
381
+ const tableStartTag = match[0];
382
+
383
+ // Find the matching closing </table> tag for this table
384
+ // We need to account for nested tables
385
+ let depth = 1;
386
+ let searchIndex = tableStartIndex + tableStartTag.length;
387
+ let closingTableIndex = -1;
388
+
389
+ while (searchIndex < htmlContent.length && depth > 0) {
390
+ const nextOpen = htmlContent.indexOf('<table', searchIndex);
391
+ const nextClose = htmlContent.indexOf('</table>', searchIndex);
392
+
393
+ if (nextClose === -1) {
394
+ break; // No closing tag found
395
+ }
396
+
397
+ if (nextOpen !== -1 && nextOpen < nextClose) {
398
+ depth++;
399
+ searchIndex = nextOpen + 6;
400
+ } else {
401
+ depth--;
402
+ if (depth === 0) {
403
+ closingTableIndex = nextClose;
404
+ break;
405
+ }
406
+ searchIndex = nextClose + 8;
407
+ }
408
+ }
409
+
410
+ if (closingTableIndex === -1) {
411
+ // If we can't find matching closing tag, return original content
412
+ return htmlContent;
413
+ }
171
414
 
172
- return htmlContent.replace(testHeaderRegex, (match) => {
173
- const recordingPlayersHTML = recordings.map((recording, index) => {
174
- const testName = recording.data.testName || 'Unknown Test';
175
- const recordingNumber = recording.data.recordingNumber || (index + 1);
176
- const displayName = recording.data.testName
177
- ? `${testName} (Recording #${recordingNumber})`
178
- : recording.filename;
415
+ // Generate logo and players HTML
416
+ const logoHTML = this.generateLogoHTML();
417
+ const recordingPlayersHTML = recordings.map((recording, index) => {
418
+ const testName = recording.data.testName || 'Unknown Test';
419
+ const recordingNumber = recording.data.recordingNumber || (index + 1);
420
+ const displayName = recording.data.testName
421
+ ? `${testName} (Recording #${recordingNumber})`
422
+ : recording.filename;
179
423
 
180
- return `
424
+ return `
181
425
  <tr class="test-row recording">
182
426
  <td colspan="2">
183
427
  <div class="recordingWrapper" style="padding: 15px; background: #f8f9fa; margin: 10px 0;">
@@ -195,10 +439,15 @@ class ReportEnhancer {
195
439
  </div>
196
440
  </td>
197
441
  </tr>`;
198
- }).join('');
442
+ }).join('');
443
+
444
+ // Wrap logo and players in a new table and inject after the closing </table> tag
445
+ const recordingsWrapper = this.generateRecordingsWrapper(logoHTML, recordingPlayersHTML);
446
+ const insertPosition = closingTableIndex + 8; // 8 = length of '</table>'
199
447
 
200
- return match + recordingPlayersHTML;
201
- });
448
+ return htmlContent.substring(0, insertPosition) +
449
+ recordingsWrapper +
450
+ htmlContent.substring(insertPosition);
202
451
  }
203
452
 
204
453
  /**
@@ -441,7 +690,7 @@ class ReportEnhancer {
441
690
  // Add player scripts and CSS
442
691
  const capVisionPlayerHTML = this.generateCapVisionPlayerHTML(recordings);
443
692
  const bodyEndIndex = htmlContent.lastIndexOf('</body>');
444
-
693
+
445
694
  if (bodyEndIndex === -1) {
446
695
  console.log('🟡 Could not find </body> tag, appending to end');
447
696
  htmlContent += capVisionPlayerHTML;