@zeesoft/i18n-verify 3.4.4 → 3.4.5

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.
Files changed (2) hide show
  1. package/dist/executor.mjs +3347 -1093
  2. package/package.json +1 -1
package/dist/executor.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import fs, { existsSync, statSync, readFileSync, writeFileSync, readdirSync, mkdirSync, appendFileSync, copyFileSync } from "fs";
2
- import path, { join, dirname, resolve, extname } from "path";
2
+ import path, { join, resolve, dirname, extname } from "path";
3
3
  import { fileURLToPath } from "url";
4
4
  import { Table } from "console-table-printer";
5
5
  import CDP from "chrome-remote-interface";
@@ -331,13 +331,24 @@ const imageToBase64 = (imagePath) => {
331
331
  return imgInput.toString("base64");
332
332
  };
333
333
  const findFileUpwards = (filename, startUrl = import.meta.url) => {
334
- let dir = dirname(fileURLToPath(startUrl));
335
- while (dir !== "/" && dir !== "." && dir !== "") {
336
- const filePath = resolve(dir, filename);
337
- if (existsSync(filePath)) {
338
- return filePath;
334
+ let cwd = process.cwd();
335
+ while (cwd) {
336
+ const p = resolve(cwd, filename);
337
+ if (existsSync(p)) return p;
338
+ const parent = dirname(cwd);
339
+ if (parent === cwd) break;
340
+ cwd = parent;
341
+ }
342
+ try {
343
+ let dir = dirname(fileURLToPath(startUrl));
344
+ while (dir) {
345
+ const filePath = resolve(dir, filename);
346
+ if (existsSync(filePath)) return filePath;
347
+ const parent = dirname(dir);
348
+ if (parent === dir) break;
349
+ dir = parent;
339
350
  }
340
- dir = dirname(dir);
351
+ } catch (e) {
341
352
  }
342
353
  return null;
343
354
  };
@@ -459,7 +470,7 @@ class BaseReport {
459
470
  }
460
471
  }
461
472
  function getBodyData() {
462
- return '<header class="report-header">\n <div class="header-left">\n <div class="brand-badge">\n <span class="brand-text">zAutomate<span class="brand-ai">.AI</span></span>\n </div>\n <div class="header-divider"></div>\n <div class="product-badge" id="productBadge" title="Product Name">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>\n <span class="product-name" id="productName">Product</span>\n </div>\n <div class="mode-pill mode-sanity" id="modePill">\n <span class="mode-dot"></span>\n <span class="mode-label" id="modeLabel">Quick Sanity</span>\n </div>\n <div class="status-pill status-passed" id="statusPill">\n <span class="status-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>\n <span class="status-label" id="statusLabel">Passed</span>\n </div>\n <div class="timestamp-badge">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>\n <span class="info" id="reportTimestamp">--</span>\n </div>\n </div>\n <div class="nav-tabs-wrapper">\n <nav class="tabs-list" id="menu">\n <button type="button" class="tab-item selected" data-page="overview">\n <svg class="tab-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>\n <span>Overview</span>\n </button>\n <button type="button" class="tab-item" data-page="log">\n <svg class="tab-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>\n <span>View Log</span>\n </button>\n <div class="tab-dropdown" id="insightsTabDropdown">\n <button type="button" class="tab-item has-submenu" data-page="insights">\n <svg class="tab-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m22 7-8.5 8.5-5-5L2 17"/><polyline points="16 7 22 7 22 13"/></svg>\n <span>Insights</span>\n <svg class="tab-chevron" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"/></svg>\n </button>\n <ul class="submenu"></ul>\n </div>\n </nav>\n </div>\n </header>\n <main class="report-main">\n <div class="content-wrapper">\n <div class="container">\n <div id="overview" class="content active">\n <div class="common-info card">\n <span class="title"></span>\n <p class="info"></p>\n <div class="section"></div>\n </div>\n <div class="overall-quality card">\n <span class="title">Overall I18n Coverage</span>\n <div class="section">\n <div class="block">\n <article>\n <p class="desc">\n zI18n-Verify report provides a comprehensive information\n about the I18n and L10n quality of your application.\n </p>\n <ul class="items">\n <li>\n <h4>Reviewed:</h4>\n <p class="detail">Total strings which are captured and processed by zI18n-Verify.</p>\n </li>\n <li>\n <h4>Internationalized:</h4>\n <p class="detail">Different types of value like string, number, date-time which are properly Internationalized.</p>\n </li>\n <li>\n <h4>Not Internationalized:</h4>\n <p class="detail">Values which are not Internationalized. Also includes Partially Localized strings and Encoding Issues.</p>\n </li>\n <li>\n <h4>Skipped:</h4>\n <p class="detail">Values which are skipped from processing because they are listed in DNT / Ignored lists or contains Auto Ignored pattern / User input pattern.</p>\n </li>\n </ul>\n </article>\n </div>\n <div class="block">\n <div class="graph"></div>\n </div>\n </div>\n </div>\n <div class="string-quality card">\n <span class="title">String Coverage</span>\n <div class="graph"></div>\n </div>\n <div class="skipped-items card">\n <span class="title">Skipped Items</span>\n <div class="graph"></div>\n </div>\n <div class="number-quality card">\n <span class="title">Number Coverage</span>\n <div class="graph"></div>\n </div>\n <div class="datetime-quality card">\n <span class="title">DateTime Coverage</span>\n <div class="graph"></div>\n </div>\n </div>\n <div id="page" class="content">\n <span class="title">Pages</span>\n <div class="section">\n <div class="desc"></div>\n </div>\n </div>\n <div id="log" class="content">\n <div class="terminal-container">\n <div class="terminal-header">\n <div class="terminal-dots">\n <span class="terminal-dot dot-close"></span>\n <span class="terminal-dot dot-min"></span>\n <span class="terminal-dot dot-max"></span>\n </div>\n <div class="terminal-title">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>\n <span>Runtime Automation Execution Log</span>\n </div>\n <div class="terminal-actions">\n <input type="text" id="logSearchInput" class="terminal-search" placeholder="Filter logs..." />\n <button class="terminal-copy-btn" id="copyLogBtn" type="button" title="Copy Log">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>\n <span>Copy</span>\n </button>\n </div>\n </div>\n <div class="log-content"><pre id="logPre"></pre></div>\n </div>\n </div>\n <div id="insights" class="content">\n <div class="cards">\n <span class="title">Locale Quality</span>\n <div class="holder"></div>\n </div>\n </div>\n <div id="locale" class="content">\n <div class="locale-header-bar">\n <button class="back-to-insights-btn" id="backToInsightsBtn" type="button">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>\n <span>Back to Insights</span>\n </button>\n <span class="title">Locale Quality Details</span>\n </div>\n <div class="coverage-info">\n <table class="row-border hover order-column stripe"></table>\n </div>\n </div>\n </div>\n </div>\n <div id="imageViewer" class="image-viewer">\n <div class="image-viewer-backdrop" onclick="closeImageViewer()"></div>\n <div class="image-container">\n <div class="image-viewer-header">\n <span class="image-viewer-title">Layout Screenshot Preview</span>\n <button class="close-button" onclick="closeImageViewer()" title="Close (Esc)">&times;</button>\n </div>\n <div class="image-viewer-body">\n <img id="imageView" src="" alt="Screenshot Preview" />\n </div>\n </div>\n </div>\n </main>';
473
+ return '<header class="report-header">\n<div class="header-left">\n <div class="brand-badge">\n <span class="brand-text">zAutomate<span class="brand-ai">.AI</span></span>\n </div>\n <div class="header-divider"></div>\n <div class="product-badge" id="productBadge" title="Product Name">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m7.5 4.27 9 5.15"/><path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/></svg>\n <span class="product-name" id="productName">Product</span>\n </div>\n <div class="mode-pill mode-sanity" id="modePill">\n <span class="mode-dot"></span>\n <span class="mode-label" id="modeLabel">Quick Sanity</span>\n </div>\n <div class="status-pill status-passed" id="statusPill">\n <span class="status-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg></span>\n <span class="status-label" id="statusLabel">Passed</span>\n </div>\n</div>\n<div class="nav-tabs-wrapper">\n <nav class="tabs-list" id="menu">\n <button type="button" class="tab-item selected" data-page="overview">\n <svg class="tab-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>\n <span>Overview</span>\n </button>\n <button type="button" class="tab-item" data-page="insights" id="insightsTabBtn">\n <svg class="tab-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m22 7-8.5 8.5-5-5L2 17"/><polyline points="16 7 22 7 22 13"/></svg>\n <span>Insights</span>\n </button>\n <button type="button" class="tab-item" data-page="log">\n <svg class="tab-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>\n <span>View Log</span>\n </button>\n </nav>\n</div>\n</header>\n<main class="report-main">\n <div class="content-wrapper">\n <div class="container">\n <div id="overview" class="content active">\n <div class="common-info"></div>\n <div class="analytics-card card">\n <div class="analytics-card-header">\n <div class="analytics-title-group">\n <span class="title">i18n Reports Overview</span>\n <p class="analytics-subtitle">Consolidated internationalization and localization metrics across reviewed strings, numbers, dates, and skipped elements</p>\n </div>\n </div>\n <div class="analytics-grid">\n <div class="chart-panel string-quality">\n <div class="panel-header">\n <div class="panel-title-row">\n <span class="panel-title">String localization, untranslated & skipped items</span>\n <span class="tooltip-icon" data-tooltip="Overall distribution of translated strings, unlocalized strings, encoding defects, and skipped items (DNT, Ignored, User Inputs)."><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>\n </div>\n <span class="panel-desc">String localization, untranslated &amp; skipped items</span>\n </div>\n <div class="string-quality-content">\n <div class="graph string-pie-graph"></div>\n <div class="string-legend-container"></div>\n </div>\n </div>\n <div class="chart-panel number-quality">\n <div class="panel-header">\n <div class="panel-title-row">\n <span class="panel-title">Numeric &amp; Currency Coverage</span>\n <span class="tooltip-icon" data-tooltip="Checks for numeric values, percentages, and currencies formatted according to locale rules."><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>\n </div>\n <span class="panel-desc">Numeric, Currency &amp; Percentage localization</span>\n </div>\n <div class="graph"></div>\n </div>\n <div class="chart-panel datetime-quality">\n <div class="panel-header">\n <div class="panel-title-row">\n <span class="panel-title">DateTime Coverage</span>\n <span class="tooltip-icon" data-tooltip="Verification of date, time, and datetime patterns formatted according to locale conventions."><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg></span>\n </div>\n <span class="panel-desc">Date, Time &amp; Combined DateTime formats</span>\n </div>\n <div class="graph"></div>\n </div>\n </div>\n </div>\n </div>\n <div id="page" class="content">\n <span class="title">Pages</span>\n <div class="section">\n <div class="desc"></div>\n </div>\n </div>\n <div id="log" class="content">\n <div class="log-viewer-card card">\n <div class="log-toolbar">\n <div class="log-toolbar-left">\n <div class="log-heading">\n <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>\n <span>Execution Logs</span>\n </div>\n <div class="log-stat-badges">\n <span class="log-badge-stat stat-all" id="logStatTotal">Total: 0</span>\n <span class="log-badge-stat stat-info" id="logStatInfo">Info: 0</span>\n <span class="log-badge-stat stat-warn" id="logStatWarn">Warn: 0</span>\n <span class="log-badge-stat stat-error" id="logStatError">Error: 0</span>\n </div>\n </div>\n <div class="log-toolbar-right">\n <div class="log-level-pills" id="logLevelPills">\n <button type="button" class="log-level-btn active" data-level="ALL">All</button>\n <button type="button" class="log-level-btn" data-level="INFO">Info</button>\n <button type="button" class="log-level-btn" data-level="WARN">Warn</button>\n <button type="button" class="log-level-btn" data-level="ERROR">Error</button>\n </div>\n <div class="log-search-container">\n <svg class="search-svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>\n <input type="text" id="logSearchInput" class="log-search-field" placeholder="Search log lines..." />\n </div>\n <button class="log-tool-btn" id="wrapToggleBtn" type="button" title="Toggle line wrap">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="15" y2="12"/><path d="M21 12h-4a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h4"/><line x1="3" y1="18" x2="17" y2="18"/></svg>\n <span>Wrap</span>\n </button>\n <button class="log-tool-btn copy-btn" id="copyLogBtn" type="button" title="Copy log">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>\n <span>Copy</span>\n </button>\n </div>\n </div>\n <div class="log-stream-wrapper" id="logStreamWrapper">\n <div class="log-lines" id="logLines"></div>\n </div>\n </div>\n </div>\n <div id="insights" class="content">\n <div class="insights-card card">\n <div class="insights-card-header">\n <div class="insights-header-left">\n <span class="title">Locale Quality Insights</span>\n <p class="insights-subtitle">Cross-locale comparison across verification metrics, localization status, and formatting health</p>\n </div>\n <div class="insights-header-right">\n <div class="insights-search-wrapper">\n <svg class="search-svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>\n <input type="text" id="insightsSearchInput" class="insights-search-field" placeholder="Filter metrics / rows..." />\n </div>\n </div>\n </div>\n <div class="insights-table-container">\n <table class="insights-table" id="insightsTable">\n <thead id="insightsTableHead"></thead>\n <tbody id="insightsTableBody"></tbody>\n </table>\n </div>\n </div>\n </div>\n <div id="locale" class="content">\n <div class="locale-header-bar">\n <button class="back-to-insights-btn" id="backToInsightsBtn" type="button">\n <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>\n <span>Back to Insights</span>\n </button>\n <span class="title">Locale Quality Details</span>\n </div>\n <div class="coverage-info">\n <table class="row-border hover order-column stripe"></table>\n </div>\n </div>\n </div>\n </div>\n <div id="imageViewer" class="image-viewer">\n <div class="image-viewer-backdrop" onclick="closeImageViewer()"></div>\n <div class="image-container">\n <div class="image-viewer-header">\n <span class="image-viewer-title">Layout Screenshot Preview</span>\n <button class="close-button" onclick="closeImageViewer()" title="Close (Esc)">&times;</button>\n </div>\n <div class="image-viewer-body">\n <img id="imageView" src="" alt="Screenshot Preview" />\n </div>\n </div>\n </div>\n <div id="screenshotGalleryModal" class="screenshot-gallery-modal" style="display:none;">\n <div class="gallery-modal-backdrop" onclick="closeScreenshotGallery()"></div>\n <div class="gallery-modal-dialog">\n <div class="gallery-modal-header">\n <div class="gallery-header-info">\n <span class="gallery-modal-title" id="galleryModalTitle">Screenshots</span>\n <span class="gallery-modal-badge" id="galleryModalBadge">0 items</span>\n </div>\n <button type="button" class="gallery-close-btn" onclick="closeScreenshotGallery()" title="Close (Esc)">&times;</button>\n </div>\n <div class="gallery-modal-body">\n <div class="gallery-preview-container">\n <div class="gallery-image-wrapper">\n <img id="galleryMainImg" src="" alt="Screenshot Preview" />\n <button type="button" class="gallery-nav-btn prev-btn" id="galleryPrevBtn" title="Previous (Left Arrow)">\n <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="15 18 9 12 15 6"/></svg>\n </button>\n <button type="button" class="gallery-nav-btn next-btn" id="galleryNextBtn" title="Next (Right Arrow)">\n <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="9 18 15 12 9 6"/></svg>\n </button>\n <button type="button" class="gallery-fullscreen-btn" id="galleryFullscreenBtn" title="Open Full View">\n <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/></svg>\n <span>Full View</span>\n </button>\n </div>\n <div class="gallery-item-details" id="galleryItemDetails">\n <div class="gallery-meta-row">\n <span class="gallery-counter" id="galleryCounter">Item 1 of 1</span>\n <span class="gallery-page-badge" id="galleryPageBadge">Page: Main</span>\n </div>\n <div class="gallery-text-box">\n <span class="gallery-text-label">Used String / Value:</span>\n <span class="gallery-text-val" id="galleryTextVal">--</span>\n </div>\n </div>\n </div>\n <div class="gallery-thumbnails-strip" id="galleryThumbnailsStrip"></div>\n </div>\n </div>\n </div>\n</main>';
463
474
  }
464
475
  function getCssData() {
465
476
  return `:root {
@@ -586,7 +597,7 @@ html, body {
586
597
  color: #059669;
587
598
  border: 1px solid rgba(16, 185, 129, 0.2);
588
599
  }
589
- .mode-pill.mode-sanity .mode-dot { background-color: #10b981; }
600
+ .mode-pill.mode-sanity .mode-dot { background-color: #5B6EE1; }
590
601
 
591
602
  .mode-pill.mode-context {
592
603
  background-color: rgba(59, 130, 246, 0.1);
@@ -666,6 +677,10 @@ html, body {
666
677
  outline: none;
667
678
  }
668
679
 
680
+ .timestamp-badge {
681
+ display: none !important;
682
+ }
683
+
669
684
  .tab-item:hover {
670
685
  color: var(--foreground);
671
686
  }
@@ -868,7 +883,7 @@ html, body {
868
883
  }
869
884
 
870
885
  /* Graphs grid */
871
- .string-quality, .skipped-items, .number-quality, .datetime-quality {
886
+ .string-quality, .skipped-items, .number-quality, .datetime-quality, .format-quality {
872
887
  margin-bottom: 20px;
873
888
  }
874
889
 
@@ -1096,99 +1111,6 @@ select.filter.dt-input {
1096
1111
  flex-shrink: 0;
1097
1112
  }
1098
1113
 
1099
- /* Terminal Log */
1100
- .terminal-container {
1101
- background-color: #0f172a;
1102
- color: #f8fafc;
1103
- border-radius: var(--radius);
1104
- border: 1px solid #1e293b;
1105
- overflow: hidden;
1106
- }
1107
-
1108
- .terminal-header {
1109
- display: flex;
1110
- align-items: center;
1111
- justify-content: space-between;
1112
- padding: 10px 14px;
1113
- background-color: #1e293b;
1114
- border-bottom: 1px solid #334155;
1115
- }
1116
-
1117
- .terminal-dots {
1118
- display: flex;
1119
- gap: 6px;
1120
- }
1121
-
1122
- .terminal-dot {
1123
- width: 10px;
1124
- height: 10px;
1125
- border-radius: 50%;
1126
- }
1127
-
1128
- .dot-close { background-color: #ef4444; }
1129
- .dot-min { background-color: #f59e0b; }
1130
- .dot-max { background-color: #10b981; }
1131
-
1132
- .terminal-title {
1133
- display: flex;
1134
- align-items: center;
1135
- gap: 6px;
1136
- font-size: 12px;
1137
- font-weight: 500;
1138
- color: #94a3b8;
1139
- }
1140
-
1141
- .terminal-actions {
1142
- display: flex;
1143
- align-items: center;
1144
- gap: 8px;
1145
- }
1146
-
1147
- .terminal-search {
1148
- background-color: #0f172a;
1149
- border: 1px solid #334155;
1150
- color: #f8fafc;
1151
- border-radius: 4px;
1152
- padding: 4px 8px;
1153
- font-size: 12px;
1154
- outline: none;
1155
- }
1156
-
1157
- .terminal-copy-btn {
1158
- background-color: #334155;
1159
- border: none;
1160
- color: #f8fafc;
1161
- border-radius: 4px;
1162
- padding: 4px 10px;
1163
- font-size: 12px;
1164
- cursor: pointer;
1165
- display: inline-flex;
1166
- align-items: center;
1167
- gap: 4px;
1168
- font-family: inherit;
1169
- transition: background 0.15s;
1170
- }
1171
-
1172
- .terminal-copy-btn:hover {
1173
- background-color: #475569;
1174
- }
1175
-
1176
- .log-content pre {
1177
- padding: 16px;
1178
- margin: 0;
1179
- font-family: 'JetBrains Mono', monospace;
1180
- font-size: 12.5px;
1181
- line-height: 1.6;
1182
- max-height: 600px;
1183
- overflow-y: auto;
1184
- color: #38bdf8;
1185
- white-space: pre-wrap;
1186
- }
1187
-
1188
- .log-badge-info { color: #38bdf8; font-weight: 600; }
1189
- .log-badge-warn { color: #f59e0b; font-weight: 600; }
1190
- .log-badge-error { color: #ef4444; font-weight: 600; }
1191
-
1192
1114
  /* Image Viewer Modal */
1193
1115
  .image-viewer {
1194
1116
  display: none;
@@ -1807,214 +1729,2162 @@ body .context-image-viewer .viewer .wrapper .image-section .split-view .target-i
1807
1729
  .apexcharts-text {
1808
1730
  fill: var(--text-muted) !important;
1809
1731
  }
1810
- `;
1732
+
1733
+
1734
+ /* Consolidated Analytics Card */
1735
+ .analytics-card {
1736
+ padding: 20px 20px 16px 20px;
1737
+ background-color: var(--bg-card);
1738
+ border: 1px solid var(--border);
1739
+ border-radius: var(--radius);
1740
+ margin-bottom: 24px;
1811
1741
  }
1812
- function renderHtml({
1813
- appName,
1814
- reportTime,
1815
- body,
1816
- script,
1817
- style,
1818
- dataFilePaths = [],
1819
- logData
1820
- }) {
1821
- const dataScripts = dataFilePaths.map((v) => '<script src="' + v + '" type="text/javascript"><\/script>').join("\n ");
1822
- const logScript = logData ? '<script type="text/javascript">window.logData = ' + JSON.stringify(logData) + ";<\/script>" : "";
1823
- return `<!DOCTYPE html>
1824
- <html lang="en" data-theme="light">
1825
- <head>
1826
- <meta charset="UTF-8" />
1827
- <meta name="charset" content="charset='UTF-8'" />
1828
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1829
- <meta name="company" content="Zeesoft" />
1830
- <meta name="product" content="zAutomate.AI" />
1831
- <meta name="author" content="Jitendra Maheshwari" />
1832
- <meta name="email" content="jm@zeesoft.com" />
1833
- <meta name="description" content="` + appName + ' - zAutomate.AI Quality Automation Report" />\n <title>' + appName + " - zAutomate.AI Report | " + reportTime + '</title>\n \n <!-- Google Fonts: Inter, Outfit, JetBrains Mono -->\n <link rel="preconnect" href="https://fonts.googleapis.com" />\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />\n <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@500;600;700;800&display=swap" rel="stylesheet" />\n \n <!-- DataTables CSS -->\n <link rel="stylesheet" href="https://cdn.datatables.net/2.0.3/css/dataTables.dataTables.min.css" />\n \n <!-- Early Theme Detection -->\n <script>\n (function() {\n try {\n var params = new URLSearchParams(window.location.search);\n var themeParam = params.get("theme");\n var storedTheme = localStorage.getItem("zqa_report_theme");\n var systemDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;\n var activeTheme = themeParam || storedTheme || (systemDark ? "dark" : "light");\n document.documentElement.setAttribute("data-theme", activeTheme);\n if (params.get("standalone") === "false") {\n document.documentElement.classList.add("embedded");\n }\n } catch(e) {}\n })();\n <\/script>\n \n ' + dataScripts + "\n " + logScript + '\n \n <script src="https://code.jquery.com/jquery-3.7.1.slim.min.js" integrity="sha256-kmHvs0B+OpCW5GVHUNjv9rOmY0IvSIRcf7zGUDTDQM8=" crossorigin="anonymous" defer><\/script>\n <script src="https://cdn.jsdelivr.net/npm/apexcharts" defer><\/script>\n <script src="https://cdn.datatables.net/2.0.3/js/dataTables.min.js" defer><\/script>\n <style>\n ' + style + "\n </style>\n </head>\n <body>\n " + body + '\n </body>\n <script type="text/javascript" defer>\n ' + script + "\n <\/script>\n</html>\n";
1742
+
1743
+ .analytics-card-header {
1744
+ margin-bottom: 16px;
1745
+ padding-bottom: 12px;
1746
+ border-bottom: 1px solid var(--border);
1834
1747
  }
1835
- const menuBar = () => `(() => {
1836
- if (DataStore.hasRuntimeError() || !DataStore.ranSuccessfully() || DataStore.isContextRun()) {
1837
- var insightTab = document.querySelector('#insightsTabDropdown') || document.querySelector('[data-page="insights"]');
1838
- if (insightTab) insightTab.remove();
1839
- }
1840
1748
 
1841
- var loadContent = function(section, highlightMenu = false) {
1842
- if (highlightMenu) {
1843
- document.querySelectorAll('.tab-item').forEach(function(b) { b.classList.remove('selected'); });
1844
- var ovTab = document.querySelector('.tab-item[data-page="overview"]');
1845
- if (ovTab) ovTab.classList.add('selected');
1846
- }
1847
- var contents = document.querySelectorAll('.content');
1848
- contents.forEach(function(content) {
1849
- if (content.id !== section) {
1850
- content.classList.remove('active');
1851
- } else {
1852
- content.classList.add('active');
1853
- }
1854
- });
1855
- if (section === 'overview' && window.persistOverviewCharts) {
1856
- window.persistOverviewCharts();
1857
- }
1858
- window.currentPage = section;
1859
- };
1749
+ .analytics-title-group .title {
1750
+ font-size: 18px;
1751
+ font-weight: 700;
1752
+ color: var(--foreground);
1753
+ }
1860
1754
 
1861
- document.addEventListener('DOMContentLoaded', function() {
1862
- if (!DataStore.isContextRun()) {
1863
- var ul = document.querySelector('.tabs-list .submenu');
1864
- if (ul) {
1865
- ul.innerHTML = '';
1866
- DataStore.getLocales().forEach(function(locale) {
1867
- var li = document.createElement('li');
1868
- li.classList.add('sub-menu-item');
1869
- li.dataset.locale = locale;
1870
- var a = document.createElement('a');
1871
- a.href = '#';
1872
- a.innerHTML = '<span class="locale-code">' + locale + '</span>';
1873
- li.appendChild(a);
1874
- ul.appendChild(li);
1875
- });
1876
- }
1877
- }
1755
+ .analytics-subtitle {
1756
+ margin: 4px 0 0 0;
1757
+ font-size: 13px;
1758
+ color: var(--text-muted);
1759
+ }
1878
1760
 
1879
- var tabButtons = document.querySelectorAll('.tab-item');
1880
- tabButtons.forEach(function(btn) {
1881
- btn.addEventListener('click', function(event) {
1882
- var page = btn.dataset.page;
1883
- if (btn.classList.contains('has-submenu')) {
1884
- event.preventDefault();
1885
- var submenu = btn.parentElement.querySelector('.submenu');
1886
- if (submenu) {
1887
- submenu.classList.toggle('active');
1888
- }
1889
- tabButtons.forEach(function(b) { b.classList.remove('selected'); });
1890
- btn.classList.add('selected');
1891
- loadContent('insights');
1892
- } else {
1893
- tabButtons.forEach(function(b) { b.classList.remove('selected'); });
1894
- btn.classList.add('selected');
1895
- var activeSubmenu = document.querySelector('.submenu.active');
1896
- if (activeSubmenu) activeSubmenu.classList.remove('active');
1897
- loadContent(page);
1898
- }
1899
- });
1900
- });
1761
+ .analytics-grid {
1762
+ display: grid;
1763
+ grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
1764
+ gap: 14px;
1765
+ align-items: stretch;
1766
+ }
1901
1767
 
1902
- document.addEventListener('click', function(e) {
1903
- if (!e.target.closest('.tab-dropdown')) {
1904
- var openSub = document.querySelector('.submenu.active');
1905
- if (openSub) openSub.classList.remove('active');
1906
- }
1907
- });
1768
+ @media (max-width: 1100px) {
1769
+ .analytics-grid {
1770
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) !important;
1771
+ gap: 12px;
1772
+ }
1773
+ }
1908
1774
 
1909
- document.addEventListener('click', function(e) {
1910
- var sItem = e.target.closest('.sub-menu-item');
1911
- if (sItem) {
1912
- e.preventDefault();
1913
- e.stopPropagation();
1914
- var openSub = document.querySelector('.submenu.active');
1915
- if (openSub) openSub.classList.remove('active');
1916
- tabButtons.forEach(function(b) { b.classList.remove('selected'); });
1917
- var insBtn = document.querySelector('.tab-item[data-page="insights"]');
1918
- if (insBtn) insBtn.classList.add('selected');
1919
- loadContent('locale');
1920
- if (window.renderLocaleGrid) {
1921
- window.renderLocaleGrid(sItem.dataset.locale);
1922
- }
1923
- }
1924
- });
1775
+ .chart-panel {
1776
+ background-color: var(--muted-bg);
1777
+ border: 1px solid var(--border);
1778
+ border-radius: 8px;
1779
+ padding: 12px 12px 8px 12px;
1780
+ display: flex;
1781
+ flex-direction: column;
1782
+ min-width: 0;
1783
+ margin-bottom: 0 !important;
1784
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
1785
+ position: relative;
1786
+ overflow: visible;
1787
+ }
1925
1788
 
1926
- var backBtn = document.getElementById('backToInsightsBtn');
1927
- if (backBtn) {
1928
- backBtn.addEventListener('click', function() {
1929
- tabButtons.forEach(function(b) { b.classList.remove('selected'); });
1930
- var insBtn = document.querySelector('.tab-item[data-page="insights"]');
1931
- if (insBtn) insBtn.classList.add('selected');
1932
- loadContent('insights');
1933
- });
1934
- }
1789
+ .chart-panel:hover {
1790
+ border-color: rgba(234, 88, 12, 0.35);
1791
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
1792
+ }
1935
1793
 
1936
- var productNameEl = document.getElementById('productName');
1937
- if (productNameEl) productNameEl.textContent = DataStore.getAppName() || 'Application';
1794
+ .chart-panel.skipped-items {
1795
+ grid-column: span 2;
1796
+ }
1938
1797
 
1939
- var modeLabelEl = document.getElementById('modeLabel');
1940
- var modePillEl = document.getElementById('modePill');
1941
- if (modeLabelEl && modePillEl) {
1942
- if (DataStore.isSanityRun()) {
1943
- modeLabelEl.textContent = 'Quick Sanity';
1944
- modePillEl.className = 'mode-pill mode-sanity';
1945
- } else if (DataStore.isContextRun()) {
1946
- modeLabelEl.textContent = 'Context Creation';
1947
- modePillEl.className = 'mode-pill mode-context';
1948
- } else {
1949
- modeLabelEl.textContent = 'i18n Verification';
1950
- modePillEl.className = 'mode-pill mode-i18n';
1951
- }
1952
- }
1798
+ .panel-header {
1799
+ margin-bottom: 10px;
1800
+ }
1953
1801
 
1954
- var statusPillEl = document.getElementById('statusPill');
1955
- var statusLabelEl = document.getElementById('statusLabel');
1956
- var hasError = DataStore.hasRuntimeError() || !DataStore.ranSuccessfully();
1957
- if (statusPillEl && statusLabelEl) {
1958
- if (hasError) {
1959
- statusPillEl.className = 'status-pill status-failed';
1960
- statusLabelEl.textContent = 'Failed';
1961
- } else {
1962
- statusPillEl.className = 'status-pill status-passed';
1963
- statusLabelEl.textContent = 'Passed';
1964
- }
1965
- }
1802
+ .panel-title-row {
1803
+ display: flex;
1804
+ align-items: center;
1805
+ gap: 6px;
1806
+ }
1966
1807
 
1967
- var timestampEl = document.getElementById('reportTimestamp');
1968
- if (timestampEl) {
1969
- try {
1970
- var ts = DataStore.getTimestamp();
1971
- timestampEl.textContent = ts ? new Date(ts).toLocaleString() : new Date().toLocaleString();
1972
- } catch(e) {
1973
- timestampEl.textContent = new Date().toLocaleString();
1974
- }
1975
- }
1808
+ .panel-title {
1809
+ font-size: 13.5px;
1810
+ font-weight: 600;
1811
+ color: var(--foreground);
1812
+ }
1976
1813
 
1977
- loadContent('overview', true);
1978
- });
1979
- })();`;
1980
- const overview = () => `//overview content
1981
- (() => {
1982
- if (DataStore.hasRuntimeError()) {
1983
- document.getElementById('overview').innerHtml =
1984
- 'zAutomate.AI was unable to perform the operation. Please check the logs for more information.';
1985
- return;
1986
- }
1814
+ .panel-desc {
1815
+ font-size: 11.5px;
1816
+ color: var(--text-muted);
1817
+ display: block;
1818
+ margin-top: 2px;
1819
+ }
1987
1820
 
1988
- document.addEventListener('DOMContentLoaded', function () {
1989
- // don't render overview contents for sanity and context run
1990
- if(DataStore.isContextRun() || DataStore.isSanityRun()) {
1991
- document.querySelector('.content-wrapper')?.classList.add('--hide-overflow');
1992
- document.querySelectorAll('#overview .card')?.forEach((el) => {
1993
- if(!el.classList.contains('common-info')) {
1994
- el.remove();
1995
- }
1996
- });
1997
1821
 
1998
- const titleEl = document.querySelector('#overview .common-info .title');
1999
- const infoEl = document.querySelector('#overview .common-info .info');
1822
+ .tooltip-icon {
1823
+ display: inline-flex;
1824
+ align-items: center;
1825
+ justify-content: center;
1826
+ color: var(--text-muted);
1827
+ cursor: help;
1828
+ position: relative;
1829
+ line-height: 1;
1830
+ transition: color 0.15s ease, opacity 0.15s ease;
1831
+ opacity: 0.7;
1832
+ }
2000
1833
 
2001
- if(titleEl && infoEl) {
2002
- let title, info;
1834
+ .tooltip-icon:hover {
1835
+ color: var(--accent, #ea580c);
1836
+ opacity: 1;
1837
+ }
2003
1838
 
2004
- const renderPagesSection = () => {
2005
- let tilesEl, searchEl, searchOpEl, pages, entries, hasPages, emptyEl, currentLocale = DataStore.getDefaultLocale();
2006
- const operatorOpts = [
2007
- {
2008
- name: 'Contains',
2009
- value: '~'
2010
- },
2011
- {
2012
- name: 'Starts with',
2013
- value: '^'
2014
- },
2015
- {
2016
- name: 'Ends with',
2017
- value: '$'
1839
+ .tooltip-icon svg {
1840
+ display: block;
1841
+ }
1842
+
1843
+ .tooltip-icon:hover::after {
1844
+ content: attr(data-tooltip);
1845
+ position: absolute;
1846
+ bottom: calc(100% + 8px);
1847
+ left: 0;
1848
+ background: #18181b;
1849
+ color: #f4f4f5;
1850
+ padding: 8px 12px;
1851
+ border-radius: 6px;
1852
+ font-size: 12px;
1853
+ line-height: 1.4;
1854
+ white-space: normal;
1855
+ width: 250px;
1856
+ z-index: 1000;
1857
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
1858
+ pointer-events: none;
1859
+ font-weight: 400;
1860
+ }
1861
+
1862
+ /* Custom Micro ApexCharts Tooltips */
1863
+ .apexcharts-tooltip {
1864
+ background: transparent !important;
1865
+ border: none !important;
1866
+ box-shadow: none !important;
1867
+ padding: 0 !important;
1868
+ }
1869
+
1870
+ .apexcharts-tooltip .apexcharts-tooltip-title,
1871
+ .apexcharts-tooltip-title {
1872
+ display: none !important;
1873
+ border: none !important;
1874
+ border-bottom: none !important;
1875
+ }
1876
+
1877
+ .pie-tooltip-custom,
1878
+ .bar-tooltip-custom {
1879
+ display: inline-flex;
1880
+ align-items: center;
1881
+ gap: 6px;
1882
+ padding: 4px 8px;
1883
+ background: #ffffff;
1884
+ border: 1px solid #cbd5e1;
1885
+ border-radius: 6px;
1886
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
1887
+ font-size: 11px;
1888
+ color: #0f172a;
1889
+ white-space: nowrap;
1890
+ font-family: inherit;
1891
+ pointer-events: none;
1892
+ }
1893
+
1894
+ .pie-tooltip-dot,
1895
+ .bar-tooltip-dot {
1896
+ width: 7px;
1897
+ height: 7px;
1898
+ border-radius: 50%;
1899
+ flex-shrink: 0;
1900
+ }
1901
+
1902
+ .pie-tooltip-label,
1903
+ .bar-tooltip-label {
1904
+ color: #475569;
1905
+ font-weight: 500;
1906
+ }
1907
+
1908
+ .pie-tooltip-val,
1909
+ .bar-tooltip-val {
1910
+ color: #0f172a;
1911
+ font-weight: 700;
1912
+ }
1913
+
1914
+ /* Header Badge for Total Coverage */
1915
+ .coverage-header-badge {
1916
+ margin-left: auto;
1917
+ display: inline-flex;
1918
+ align-items: center;
1919
+ gap: 6px;
1920
+ background: rgba(49, 233, 148, 0.12);
1921
+ border: 1px solid rgba(49, 233, 148, 0.35);
1922
+ padding: 2.5px 8px;
1923
+ border-radius: 9999px;
1924
+ font-size: 11px;
1925
+ white-space: nowrap;
1926
+ }
1927
+
1928
+ .coverage-header-badge .coverage-pulse-dot {
1929
+ width: 6px;
1930
+ height: 6px;
1931
+ border-radius: 50%;
1932
+ background: #10b981;
1933
+ box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.25);
1934
+ animation: coveragePulse 2s infinite ease-in-out;
1935
+ }
1936
+
1937
+ @keyframes coveragePulse {
1938
+ 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.5); }
1939
+ 70% { transform: scale(1); box-shadow: 0 0 0 4px rgba(16, 185, 129, 0); }
1940
+ 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
1941
+ }
1942
+
1943
+ .coverage-header-badge .coverage-badge-label {
1944
+ color: var(--text-muted);
1945
+ font-weight: 500;
1946
+ font-size: 10px;
1947
+ text-transform: uppercase;
1948
+ letter-spacing: 0.3px;
1949
+ }
1950
+
1951
+ .coverage-header-badge .coverage-badge-val {
1952
+ color: var(--foreground);
1953
+ font-weight: 700;
1954
+ font-size: 11.5px;
1955
+ }
1956
+
1957
+ /* Side-by-side Layout for String Quality Panel */
1958
+ .string-quality-content {
1959
+ display: flex;
1960
+ align-items: center;
1961
+ justify-content: space-between;
1962
+ gap: 6px;
1963
+ width: 100%;
1964
+ margin-top: 2px;
1965
+ overflow: visible;
1966
+ }
1967
+
1968
+ .string-pie-graph {
1969
+ flex: 0 0 155px;
1970
+ max-width: 155px;
1971
+ min-width: 120px;
1972
+ min-height: 195px;
1973
+ display: flex;
1974
+ align-items: center;
1975
+ justify-content: center;
1976
+ overflow: hidden;
1977
+ }
1978
+
1979
+ .string-pie-graph .apexcharts-canvas {
1980
+ margin: 0 auto;
1981
+ }
1982
+
1983
+ .string-legend-container {
1984
+ flex: 1;
1985
+ min-width: 0;
1986
+ display: flex;
1987
+ flex-direction: column;
1988
+ justify-content: center;
1989
+ position: relative;
1990
+ overflow: visible;
1991
+ }
1992
+
1993
+ /* Total Coverage Summary Card in Legend */
1994
+ .total-coverage-card {
1995
+ background: var(--bg-card);
1996
+ border: 1px solid var(--border);
1997
+ border-radius: 6px;
1998
+ padding: 4px 7px;
1999
+ margin-bottom: 5px;
2000
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
2001
+ }
2002
+
2003
+ .coverage-card-header {
2004
+ display: flex;
2005
+ align-items: center;
2006
+ justify-content: space-between;
2007
+ margin-bottom: 4px;
2008
+ }
2009
+
2010
+ .coverage-card-title {
2011
+ font-size: 10px;
2012
+ font-weight: 600;
2013
+ color: var(--text-muted);
2014
+ text-transform: uppercase;
2015
+ letter-spacing: 0.3px;
2016
+ }
2017
+
2018
+ .coverage-card-pct {
2019
+ font-size: 12.5px;
2020
+ font-weight: 800;
2021
+ color: #10b981;
2022
+ }
2023
+
2024
+ .coverage-card-bar {
2025
+ background: var(--muted-bg, #f1f5f9);
2026
+ height: 5px;
2027
+ border-radius: 3px;
2028
+ overflow: hidden;
2029
+ position: relative;
2030
+ }
2031
+
2032
+ .coverage-card-fill {
2033
+ background: linear-gradient(90deg, #4F46E5, #5B6EE1);
2034
+ height: 100%;
2035
+ border-radius: 3px;
2036
+ transition: width 0.5s ease;
2037
+ }
2038
+
2039
+ /* Legend items list */
2040
+ .string-legend-list {
2041
+ display: flex;
2042
+ flex-direction: column;
2043
+ gap: 3px;
2044
+ }
2045
+
2046
+ .legend-row-item {
2047
+ position: relative;
2048
+ display: flex;
2049
+ align-items: center;
2050
+ font-size: 11px;
2051
+ color: var(--foreground);
2052
+ user-select: none;
2053
+ }
2054
+
2055
+ .legend-row-item.is-clickable {
2056
+ cursor: pointer;
2057
+ padding: 2.5px 5px;
2058
+ border-radius: 4px;
2059
+ transition: background 0.15s ease;
2060
+ }
2061
+
2062
+ .legend-row-item.is-clickable:hover {
2063
+ background: rgba(148, 163, 184, 0.12);
2064
+ }
2065
+
2066
+ .legend-row-item.inactive {
2067
+ opacity: 0.35;
2068
+ filter: grayscale(60%);
2069
+ }
2070
+
2071
+ .legend-circle-dot {
2072
+ width: 8px;
2073
+ height: 8px;
2074
+ border-radius: 50%;
2075
+ display: inline-block;
2076
+ flex-shrink: 0;
2077
+ margin-right: 5px;
2078
+ }
2079
+
2080
+ .legend-row-label {
2081
+ display: inline-flex;
2082
+ align-items: center;
2083
+ gap: 3px;
2084
+ font-size: 10.5px;
2085
+ color: var(--foreground);
2086
+ white-space: nowrap;
2087
+ overflow: hidden;
2088
+ text-overflow: ellipsis;
2089
+ }
2090
+
2091
+ .legend-row-pct {
2092
+ font-weight: 600;
2093
+ color: var(--text-muted);
2094
+ font-size: 10.5px;
2095
+ }
2096
+
2097
+ /* Interactive Split Item (Ignored, DNT, User Inputs) */
2098
+ .legend-row-item.is-interactive {
2099
+ display: block;
2100
+ }
2101
+
2102
+ .interactive-btn-inner {
2103
+ display: flex;
2104
+ align-items: center;
2105
+ padding: 2.5px 5px;
2106
+ border-radius: 5px;
2107
+ border: 1px solid var(--border);
2108
+ background: var(--bg-card);
2109
+ cursor: pointer;
2110
+ transition: all 0.15s ease;
2111
+ width: 100%;
2112
+ }
2113
+
2114
+ .interactive-btn-inner:hover {
2115
+ border-color: var(--accent, #ea580c);
2116
+ background: rgba(234, 88, 12, 0.04);
2117
+ }
2118
+
2119
+ .legend-row-item.is-open .interactive-btn-inner {
2120
+ border-color: var(--accent, #ea580c);
2121
+ box-shadow: 0 0 0 2px rgba(234, 88, 12, 0.15);
2122
+ }
2123
+
2124
+ .interactive-btn-inner .chevron-icon {
2125
+ margin-left: auto;
2126
+ color: var(--text-muted);
2127
+ transition: transform 0.2s ease;
2128
+ flex-shrink: 0;
2129
+ }
2130
+
2131
+ .interactive-btn-inner:hover .chevron-icon {
2132
+ color: var(--foreground);
2133
+ }
2134
+
2135
+ .chevron-icon.is-open {
2136
+ transform: rotate(180deg);
2137
+ color: var(--accent, #ea580c);
2138
+ }
2139
+
2140
+ /* Vertical Dropdown Menu (Pops Below Row and Floats Unclipped) */
2141
+ .legend-row-item .split-dropdown-menu {
2142
+ position: absolute;
2143
+ top: calc(100% + 4px);
2144
+ bottom: auto;
2145
+ left: 0;
2146
+ width: 100%;
2147
+ min-width: 145px;
2148
+ background: var(--bg-card);
2149
+ border: 1px solid var(--border);
2150
+ border-radius: 8px;
2151
+ padding: 5px;
2152
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.25), 0 8px 10px -6px rgba(0, 0, 0, 0.15);
2153
+ z-index: 100;
2154
+ display: flex;
2155
+ flex-direction: column;
2156
+ gap: 3px;
2157
+ animation: splitDropdownIn 0.15s ease-out;
2158
+ }
2159
+
2160
+ @keyframes splitDropdownIn {
2161
+ from {
2162
+ opacity: 0;
2163
+ transform: translateY(-4px);
2164
+ }
2165
+ to {
2166
+ opacity: 1;
2167
+ transform: translateY(0);
2168
+ }
2169
+ }
2170
+
2171
+ .legend-row-item .dropdown-header {
2172
+ display: flex;
2173
+ align-items: center;
2174
+ justify-content: space-between;
2175
+ font-size: 9.5px;
2176
+ font-weight: 600;
2177
+ color: var(--text-muted);
2178
+ text-transform: uppercase;
2179
+ letter-spacing: 0.3px;
2180
+ padding: 2px 4px 4px 4px;
2181
+ border-bottom: 1px solid var(--border);
2182
+ margin-bottom: 2px;
2183
+ }
2184
+
2185
+ .legend-row-item .dropdown-header-total {
2186
+ font-weight: 700;
2187
+ color: var(--foreground);
2188
+ }
2189
+
2190
+ .legend-row-item .dropdown-item {
2191
+ display: flex;
2192
+ align-items: center;
2193
+ justify-content: space-between;
2194
+ gap: 8px;
2195
+ padding: 4px 6px;
2196
+ border-radius: 4px;
2197
+ font-size: 11px;
2198
+ color: var(--foreground);
2199
+ cursor: pointer;
2200
+ transition: all 0.15s ease;
2201
+ user-select: none;
2202
+ }
2203
+
2204
+ .legend-row-item .dropdown-item:hover {
2205
+ background: rgba(148, 163, 184, 0.12);
2206
+ }
2207
+
2208
+ .legend-row-item .dropdown-item.inactive {
2209
+ opacity: 0.35;
2210
+ filter: grayscale(60%);
2211
+ }
2212
+
2213
+ .legend-row-item .dropdown-item-left {
2214
+ display: inline-flex;
2215
+ align-items: center;
2216
+ gap: 5px;
2217
+ }
2218
+
2219
+ .legend-row-item .dropdown-item-left .dot {
2220
+ width: 7px;
2221
+ height: 7px;
2222
+ border-radius: 50%;
2223
+ display: inline-block;
2224
+ flex-shrink: 0;
2225
+ }
2226
+
2227
+ .legend-row-item .dropdown-item-label {
2228
+ font-size: 11px;
2229
+ color: var(--foreground);
2230
+ }
2231
+
2232
+ .legend-row-item .dropdown-item-val {
2233
+ font-weight: 600;
2234
+ font-size: 11px;
2235
+ color: var(--foreground);
2236
+ }
2237
+
2238
+ /* Modern Log Viewer Card */
2239
+ .log-viewer-card {
2240
+ background-color: var(--bg-card);
2241
+ border: 1px solid var(--border);
2242
+ border-radius: var(--radius);
2243
+ overflow: hidden;
2244
+ display: flex;
2245
+ flex-direction: column;
2246
+ height: calc(100vh - 120px);
2247
+ min-height: 600px;
2248
+ }
2249
+
2250
+ .log-toolbar {
2251
+ padding: 12px 18px;
2252
+ background-color: var(--bg-card);
2253
+ border-bottom: 1px solid var(--border);
2254
+ display: flex;
2255
+ align-items: center;
2256
+ justify-content: space-between;
2257
+ gap: 16px;
2258
+ flex-wrap: wrap;
2259
+ }
2260
+
2261
+ .log-toolbar-left {
2262
+ display: flex;
2263
+ align-items: center;
2264
+ gap: 14px;
2265
+ }
2266
+
2267
+ .log-heading {
2268
+ display: flex;
2269
+ align-items: center;
2270
+ gap: 8px;
2271
+ font-weight: 600;
2272
+ font-size: 14px;
2273
+ color: var(--foreground);
2274
+ }
2275
+
2276
+ .log-stat-badges {
2277
+ display: flex;
2278
+ gap: 6px;
2279
+ }
2280
+
2281
+ .log-badge-stat {
2282
+ font-size: 11px;
2283
+ font-weight: 500;
2284
+ padding: 2px 7px;
2285
+ border-radius: 4px;
2286
+ border: 1px solid var(--border);
2287
+ background-color: var(--muted-bg);
2288
+ color: var(--text-muted);
2289
+ }
2290
+
2291
+ .log-badge-stat.stat-info { color: #0284c7; background: rgba(2, 132, 199, 0.08); border-color: rgba(2, 132, 199, 0.2); }
2292
+ .log-badge-stat.stat-warn { color: #d97706; background: rgba(217, 119, 6, 0.08); border-color: rgba(217, 119, 6, 0.2); }
2293
+ .log-badge-stat.stat-error { color: #dc2626; background: rgba(220, 38, 38, 0.08); border-color: rgba(220, 38, 38, 0.2); }
2294
+
2295
+ .log-toolbar-right {
2296
+ display: flex;
2297
+ align-items: center;
2298
+ gap: 10px;
2299
+ }
2300
+
2301
+ .log-level-pills {
2302
+ display: flex;
2303
+ background-color: var(--muted-bg);
2304
+ border-radius: 6px;
2305
+ padding: 2px;
2306
+ gap: 2px;
2307
+ }
2308
+
2309
+ .log-level-btn {
2310
+ border: none;
2311
+ background: transparent;
2312
+ padding: 4px 10px;
2313
+ font-size: 12px;
2314
+ font-weight: 500;
2315
+ color: var(--text-muted);
2316
+ border-radius: 4px;
2317
+ cursor: pointer;
2318
+ transition: all 0.15s ease;
2319
+ }
2320
+
2321
+ .log-level-btn.active {
2322
+ background-color: var(--bg-card);
2323
+ color: var(--foreground);
2324
+ box-shadow: 0 1px 3px rgba(0,0,0,0.08);
2325
+ font-weight: 600;
2326
+ }
2327
+
2328
+ .log-search-container {
2329
+ position: relative;
2330
+ display: flex;
2331
+ align-items: center;
2332
+ }
2333
+
2334
+ .search-svg {
2335
+ position: absolute;
2336
+ left: 10px;
2337
+ color: var(--text-muted);
2338
+ pointer-events: none;
2339
+ }
2340
+
2341
+ .log-search-field {
2342
+ padding: 5px 12px 5px 28px;
2343
+ font-size: 12px;
2344
+ border: 1px solid var(--border);
2345
+ border-radius: 6px;
2346
+ background-color: var(--muted-bg);
2347
+ color: var(--foreground);
2348
+ width: 180px;
2349
+ outline: none;
2350
+ transition: all 0.15s ease;
2351
+ }
2352
+
2353
+ .log-search-field:focus {
2354
+ border-color: var(--accent);
2355
+ background-color: var(--bg-card);
2356
+ width: 220px;
2357
+ }
2358
+
2359
+ .log-tool-btn {
2360
+ display: inline-flex;
2361
+ align-items: center;
2362
+ gap: 5px;
2363
+ background-color: var(--muted-bg);
2364
+ border: 1px solid var(--border);
2365
+ color: var(--foreground);
2366
+ padding: 5px 10px;
2367
+ border-radius: 6px;
2368
+ font-size: 12px;
2369
+ font-weight: 500;
2370
+ cursor: pointer;
2371
+ transition: all 0.15s ease;
2372
+ }
2373
+
2374
+ .log-tool-btn:hover {
2375
+ background-color: var(--border);
2376
+ }
2377
+
2378
+ .log-tool-btn.active {
2379
+ background-color: var(--accent-light);
2380
+ color: var(--accent);
2381
+ border-color: var(--accent);
2382
+ }
2383
+
2384
+ .log-stream-wrapper {
2385
+ flex: 1;
2386
+ overflow-y: auto;
2387
+ overflow-x: auto;
2388
+ background-color: var(--bg-card);
2389
+ font-family: 'JetBrains Mono', 'Menlo', 'Monaco', monospace;
2390
+ font-size: 12.5px;
2391
+ line-height: 1.6;
2392
+ }
2393
+
2394
+ [data-theme="dark"] .log-stream-wrapper {
2395
+ background-color: #0c0a09;
2396
+ }
2397
+
2398
+ .log-lines {
2399
+ padding: 8px 0;
2400
+ min-width: 100%;
2401
+ width: max-content;
2402
+ }
2403
+
2404
+ .log-lines.wrapped {
2405
+ width: 100%;
2406
+ }
2407
+
2408
+ .log-row {
2409
+ display: flex;
2410
+ align-items: flex-start;
2411
+ padding: 2px 14px;
2412
+ gap: 12px;
2413
+ transition: background-color 0.1s ease;
2414
+ }
2415
+
2416
+ .log-row:hover {
2417
+ background-color: rgba(234, 88, 12, 0.05);
2418
+ }
2419
+
2420
+ .log-row.row-error {
2421
+ background-color: rgba(239, 68, 68, 0.08);
2422
+ }
2423
+
2424
+ .log-row.row-warn {
2425
+ background-color: rgba(245, 158, 11, 0.08);
2426
+ }
2427
+
2428
+ .log-ln {
2429
+ width: 44px;
2430
+ min-width: 44px;
2431
+ text-align: right;
2432
+ color: var(--text-muted);
2433
+ user-select: none;
2434
+ opacity: 0.6;
2435
+ font-size: 11px;
2436
+ padding-top: 1px;
2437
+ }
2438
+
2439
+ .log-msg {
2440
+ flex: 1;
2441
+ white-space: pre;
2442
+ color: var(--foreground);
2443
+ }
2444
+
2445
+ .log-lines.wrapped .log-msg {
2446
+ white-space: pre-wrap;
2447
+ word-break: break-word;
2448
+ }
2449
+
2450
+ .log-badge {
2451
+ display: inline-block;
2452
+ font-size: 10.5px;
2453
+ font-weight: 700;
2454
+ padding: 1px 5px;
2455
+ border-radius: 4px;
2456
+ margin-right: 6px;
2457
+ vertical-align: middle;
2458
+ }
2459
+
2460
+ .badge-info { background: rgba(56, 189, 248, 0.15); color: #0284c7; }
2461
+ [data-theme="dark"] .badge-info { color: #38bdf8; }
2462
+ .badge-warn { background: rgba(245, 158, 11, 0.15); color: #d97706; }
2463
+ [data-theme="dark"] .badge-warn { color: #fbbf24; }
2464
+ .badge-error { background: rgba(239, 68, 68, 0.15); color: #dc2626; }
2465
+ [data-theme="dark"] .badge-error { color: #f87171; }
2466
+
2467
+ .log-highlight {
2468
+ background-color: #fde047;
2469
+ color: #0f172a;
2470
+ padding: 1px 3px;
2471
+ border-radius: 2px;
2472
+ }
2473
+
2474
+ .log-empty-state {
2475
+ padding: 40px;
2476
+ text-align: center;
2477
+ color: var(--text-muted);
2478
+ font-style: italic;
2479
+ }
2480
+
2481
+ /* Insights Table View */
2482
+ .insights-card {
2483
+ background-color: var(--bg-card);
2484
+ border: 1px solid var(--border);
2485
+ border-radius: var(--radius);
2486
+ padding: 24px;
2487
+ }
2488
+
2489
+ .insights-card-header {
2490
+ display: flex;
2491
+ align-items: center;
2492
+ justify-content: space-between;
2493
+ margin-bottom: 20px;
2494
+ padding-bottom: 16px;
2495
+ border-bottom: 1px solid var(--border);
2496
+ flex-wrap: wrap;
2497
+ gap: 12px;
2498
+ }
2499
+
2500
+ .insights-card-header .title {
2501
+ font-size: 18px;
2502
+ font-weight: 700;
2503
+ color: var(--foreground);
2504
+ }
2505
+
2506
+ .insights-subtitle {
2507
+ margin: 4px 0 0 0;
2508
+ font-size: 13px;
2509
+ color: var(--text-muted);
2510
+ }
2511
+
2512
+ .insights-search-field {
2513
+ padding: 6px 12px;
2514
+ font-size: 13px;
2515
+ border: 1px solid var(--border);
2516
+ border-radius: 6px;
2517
+ background-color: var(--muted-bg);
2518
+ color: var(--foreground);
2519
+ outline: none;
2520
+ width: 200px;
2521
+ transition: all 0.15s ease;
2522
+ }
2523
+
2524
+ .insights-search-field:focus {
2525
+ border-color: var(--accent);
2526
+ background-color: var(--bg-card);
2527
+ }
2528
+
2529
+ .insights-table-container {
2530
+ overflow-x: auto;
2531
+ border-radius: 8px;
2532
+ border: 1px solid var(--border);
2533
+ }
2534
+
2535
+ .insights-table {
2536
+ width: 100%;
2537
+ border-collapse: collapse;
2538
+ text-align: left;
2539
+ font-size: 13px;
2540
+ }
2541
+
2542
+ .insights-table th {
2543
+ background-color: var(--muted-bg);
2544
+ color: var(--text-muted);
2545
+ font-weight: 600;
2546
+ font-size: 12px;
2547
+ text-transform: uppercase;
2548
+ letter-spacing: 0.3px;
2549
+ padding: 12px 14px;
2550
+ border-bottom: 1px solid var(--border);
2551
+ }
2552
+
2553
+ .insights-table td {
2554
+ padding: 12px 14px;
2555
+ border-bottom: 1px solid var(--border);
2556
+ vertical-align: middle;
2557
+ }
2558
+
2559
+ .insights-locale-row {
2560
+ cursor: pointer;
2561
+ transition: background-color 0.15s ease;
2562
+ }
2563
+
2564
+ .insights-locale-row:hover {
2565
+ background-color: rgba(234, 88, 12, 0.04);
2566
+ }
2567
+
2568
+ .locale-code-cell {
2569
+ display: flex;
2570
+ align-items: center;
2571
+ gap: 8px;
2572
+ }
2573
+
2574
+ .locale-pill-name {
2575
+ font-weight: 600;
2576
+ color: var(--foreground);
2577
+ font-family: 'JetBrains Mono', monospace;
2578
+ font-size: 13px;
2579
+ }
2580
+
2581
+ .default-badge {
2582
+ font-size: 10.5px;
2583
+ font-weight: 600;
2584
+ padding: 1px 6px;
2585
+ border-radius: 4px;
2586
+ background-color: rgba(59, 130, 246, 0.12);
2587
+ color: #2563eb;
2588
+ }
2589
+
2590
+ [data-theme="dark"] .default-badge {
2591
+ color: #60a5fa;
2592
+ }
2593
+
2594
+ .table-progress-cell {
2595
+ display: flex;
2596
+ align-items: center;
2597
+ gap: 8px;
2598
+ min-width: 110px;
2599
+ }
2600
+
2601
+ .table-progress-bar-bg {
2602
+ flex: 1;
2603
+ height: 7px;
2604
+ background-color: var(--muted-bg);
2605
+ border-radius: 4px;
2606
+ overflow: hidden;
2607
+ }
2608
+
2609
+ .table-progress-bar {
2610
+ height: 100%;
2611
+ border-radius: 4px;
2612
+ transition: width 0.3s ease;
2613
+ }
2614
+
2615
+ .table-progress-val {
2616
+ font-size: 12px;
2617
+ font-weight: 600;
2618
+ min-width: 32px;
2619
+ text-align: right;
2620
+ color: var(--foreground);
2621
+ }
2622
+
2623
+ .strings-summary-badges {
2624
+ display: flex;
2625
+ gap: 4px;
2626
+ flex-wrap: wrap;
2627
+ }
2628
+
2629
+ .mini-stat-badge {
2630
+ display: inline-flex;
2631
+ align-items: center;
2632
+ justify-content: center;
2633
+ padding: 2px 6px;
2634
+ border-radius: 4px;
2635
+ font-size: 11px;
2636
+ font-weight: 600;
2637
+ }
2638
+
2639
+ .stat-green { background: rgba(16, 185, 129, 0.12); color: #059669; }
2640
+ [data-theme="dark"] .stat-green { color: #34d399; }
2641
+ .stat-yellow { background: rgba(245, 158, 11, 0.12); color: #d97706; }
2642
+ [data-theme="dark"] .stat-yellow { color: #fbbf24; }
2643
+ .stat-red { background: rgba(239, 68, 68, 0.12); color: #dc2626; }
2644
+ [data-theme="dark"] .stat-red { color: #f87171; }
2645
+ .stat-pink { background: rgba(244, 114, 182, 0.15); color: #db2777; }
2646
+ [data-theme="dark"] .stat-pink { color: #f472b6; }
2647
+ .stat-cyan { background: rgba(6, 182, 212, 0.15); color: #0891b2; }
2648
+ [data-theme="dark"] .stat-cyan { color: #22d3ee; }
2649
+
2650
+ .count-badge {
2651
+ display: inline-block;
2652
+ padding: 2px 7px;
2653
+ border-radius: 4px;
2654
+ background-color: var(--muted-bg);
2655
+ color: var(--text-muted);
2656
+ font-weight: 500;
2657
+ font-size: 12px;
2658
+ }
2659
+
2660
+ .btn-view-details {
2661
+ display: inline-flex;
2662
+ align-items: center;
2663
+ gap: 4px;
2664
+ background-color: var(--muted-bg);
2665
+ border: 1px solid var(--border);
2666
+ color: var(--foreground);
2667
+ padding: 5px 10px;
2668
+ border-radius: 6px;
2669
+ font-size: 12px;
2670
+ font-weight: 500;
2671
+ cursor: pointer;
2672
+ transition: all 0.15s ease;
2673
+ }
2674
+
2675
+ .btn-view-details:hover {
2676
+ background-color: var(--accent);
2677
+ color: #ffffff;
2678
+ border-color: var(--accent);
2679
+ }
2680
+
2681
+ /* Category Pills & Status Badges for Locale Grid */
2682
+ .cell-term-text {
2683
+ font-weight: 500;
2684
+ color: var(--foreground);
2685
+ word-break: break-word;
2686
+ }
2687
+
2688
+ .cat-pill {
2689
+ display: inline-flex;
2690
+ align-items: center;
2691
+ padding: 2px 8px;
2692
+ border-radius: 5px;
2693
+ font-size: 11.5px;
2694
+ font-weight: 600;
2695
+ letter-spacing: 0.2px;
2696
+ background-color: var(--muted-bg);
2697
+ color: var(--foreground);
2698
+ border: 1px solid var(--border);
2699
+ }
2700
+
2701
+ .cat-pill.cat-strings { background: rgba(59, 130, 246, 0.1); color: #2563eb; border-color: rgba(59, 130, 246, 0.2); }
2702
+ [data-theme="dark"] .cat-pill.cat-strings { color: #60a5fa; }
2703
+
2704
+ .cat-pill.cat-numbers, .cat-pill.cat-currency, .cat-pill.cat-percentage {
2705
+ background: rgba(168, 85, 247, 0.1); color: #9333ea; border-color: rgba(168, 85, 247, 0.2);
2706
+ }
2707
+ [data-theme="dark"] .cat-pill.cat-numbers, [data-theme="dark"] .cat-pill.cat-currency, [data-theme="dark"] .cat-pill.cat-percentage {
2708
+ color: #c084fc;
2709
+ }
2710
+
2711
+ .cat-pill.cat-datetime, .cat-pill.cat-date, .cat-pill.cat-time {
2712
+ background: rgba(14, 165, 233, 0.1); color: #0284c7; border-color: rgba(14, 165, 233, 0.2);
2713
+ }
2714
+ [data-theme="dark"] .cat-pill.cat-datetime, [data-theme="dark"] .cat-pill.cat-date, [data-theme="dark"] .cat-pill.cat-time {
2715
+ color: #38bdf8;
2716
+ }
2717
+
2718
+ .cat-pill.cat-skipped {
2719
+ background: rgba(148, 163, 184, 0.12); color: #64748b; border-color: rgba(148, 163, 184, 0.2);
2720
+ }
2721
+
2722
+ .cat-pill.cat-layout {
2723
+ background: rgba(239, 68, 68, 0.1); color: #dc2626; border-color: rgba(239, 68, 68, 0.2);
2724
+ }
2725
+
2726
+ .status-badge {
2727
+ display: inline-flex;
2728
+ align-items: center;
2729
+ padding: 2px 8px;
2730
+ border-radius: 5px;
2731
+ font-size: 11.5px;
2732
+ font-weight: 600;
2733
+ }
2734
+
2735
+ .status-badge.status-green { background: rgba(16, 185, 129, 0.12); color: #059669; }
2736
+ [data-theme="dark"] .status-badge.status-green { color: #34d399; }
2737
+ .status-badge.status-red { background: rgba(239, 68, 68, 0.12); color: #dc2626; }
2738
+ [data-theme="dark"] .status-badge.status-red { color: #f87171; }
2739
+ .status-badge.status-yellow { background: rgba(245, 158, 11, 0.12); color: #d97706; }
2740
+ [data-theme="dark"] .status-badge.status-yellow { color: #fbbf24; }
2741
+ .status-badge.status-pink { background: rgba(244, 114, 182, 0.15); color: #db2777; }
2742
+ [data-theme="dark"] .status-badge.status-pink { color: #f472b6; }
2743
+ .status-badge.status-cyan { background: rgba(6, 182, 212, 0.15); color: #0891b2; }
2744
+ [data-theme="dark"] .status-badge.status-cyan { color: #22d3ee; }
2745
+ .status-badge.status-muted { background: rgba(148, 163, 184, 0.15); color: #64748b; }
2746
+
2747
+
2748
+
2749
+ .common-info:empty {
2750
+ display: none !important;
2751
+ }
2752
+
2753
+ .chart-panel .graph {
2754
+ min-height: 220px;
2755
+ width: 100%;
2756
+ overflow: visible;
2757
+ }
2758
+
2759
+ /* Transposed Insights Matrix Table */
2760
+ .insights-table {
2761
+ width: 100%;
2762
+ table-layout: fixed;
2763
+ border-collapse: collapse;
2764
+ font-size: 13px;
2765
+ }
2766
+
2767
+ .insights-table thead th {
2768
+ background: var(--muted-bg);
2769
+ color: var(--text-muted);
2770
+ font-weight: 600;
2771
+ font-size: 12px;
2772
+ text-transform: uppercase;
2773
+ letter-spacing: 0.5px;
2774
+ padding: 12px 14px;
2775
+ border-bottom: 2px solid var(--border);
2776
+ position: sticky;
2777
+ top: 0;
2778
+ z-index: 2;
2779
+ }
2780
+
2781
+ .insights-table th.metric-col-header {
2782
+ text-align: left;
2783
+ width: 38%;
2784
+ }
2785
+
2786
+ .insights-table th.locale-col-header,
2787
+ .insights-table th.total-col-header {
2788
+ text-align: center;
2789
+ width: auto;
2790
+ }
2791
+
2792
+ .col-header-inner {
2793
+ display: flex;
2794
+ flex-direction: column;
2795
+ align-items: center;
2796
+ gap: 4px;
2797
+ }
2798
+
2799
+ .locale-col-code {
2800
+ font-weight: 700;
2801
+ font-size: 13px;
2802
+ color: var(--text-primary);
2803
+ letter-spacing: 0.5px;
2804
+ }
2805
+
2806
+ .col-default-badge {
2807
+ font-size: 10px;
2808
+ font-weight: 600;
2809
+ background: rgba(59, 130, 246, 0.12);
2810
+ color: #3b82f6;
2811
+ border-radius: 4px;
2812
+ padding: 1px 5px;
2813
+ text-transform: uppercase;
2814
+ }
2815
+
2816
+ .btn-col-drilldown {
2817
+ display: inline-flex;
2818
+ align-items: center;
2819
+ gap: 4px;
2820
+ background: var(--card-bg);
2821
+ border: 1px solid var(--border);
2822
+ color: var(--text-muted);
2823
+ font-size: 11px;
2824
+ font-weight: 500;
2825
+ padding: 2px 7px;
2826
+ border-radius: 4px;
2827
+ cursor: pointer;
2828
+ transition: all 0.15s ease;
2829
+ }
2830
+
2831
+ .btn-col-drilldown:hover {
2832
+ border-color: var(--primary);
2833
+ color: var(--primary);
2834
+ background: rgba(234, 88, 12, 0.08);
2835
+ }
2836
+
2837
+ .insights-table tbody tr {
2838
+ border-bottom: 1px solid var(--border);
2839
+ transition: background-color 0.15s ease;
2840
+ }
2841
+
2842
+ .insights-table tbody tr:hover {
2843
+ background-color: rgba(234, 88, 12, 0.03);
2844
+ }
2845
+
2846
+ .insights-table tbody tr.row-defect:hover {
2847
+ background-color: rgba(239, 68, 68, 0.04);
2848
+ }
2849
+
2850
+ .insights-table tbody tr.row-total {
2851
+ background-color: var(--muted-bg);
2852
+ border-top: 2px solid var(--border);
2853
+ font-weight: 600;
2854
+ }
2855
+
2856
+ .insights-table tbody tr.row-total:hover {
2857
+ background-color: rgba(234, 88, 12, 0.06);
2858
+ }
2859
+
2860
+ .metric-name-cell {
2861
+ padding: 11px 16px;
2862
+ vertical-align: middle;
2863
+ }
2864
+
2865
+ .metric-name-inner {
2866
+ display: flex;
2867
+ align-items: center;
2868
+ gap: 10px;
2869
+ }
2870
+
2871
+ .metric-cat-pill {
2872
+ display: inline-flex;
2873
+ align-items: center;
2874
+ justify-content: center;
2875
+ font-size: 10px;
2876
+ font-weight: 700;
2877
+ text-transform: uppercase;
2878
+ letter-spacing: 0.4px;
2879
+ padding: 3px 6px;
2880
+ border-radius: 4px;
2881
+ width: 84px;
2882
+ min-width: 84px;
2883
+ max-width: 84px;
2884
+ text-align: center;
2885
+ flex-shrink: 0;
2886
+ }
2887
+
2888
+ .cat-strings { background: rgba(59, 130, 246, 0.12); color: #3b82f6; }
2889
+ .cat-numeric, .cat-numbers { background: rgba(16, 185, 129, 0.12); color: #10b981; }
2890
+ .cat-currency { background: rgba(139, 92, 246, 0.12); color: #8b5cf6; }
2891
+ .cat-percentage { background: rgba(234, 88, 12, 0.12); color: #ea580c; }
2892
+ .cat-date { background: rgba(14, 165, 233, 0.12); color: #0ea5e9; }
2893
+ .cat-time { background: rgba(20, 184, 166, 0.12); color: #14b8a6; }
2894
+ .cat-datetime { background: rgba(99, 102, 241, 0.12); color: #6366f1; }
2895
+ .cat-defects { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
2896
+ .cat-skipped { background: rgba(100, 116, 139, 0.12); color: #64748b; }
2897
+ .cat-summary { background: rgba(234, 88, 12, 0.16); color: var(--primary); }
2898
+
2899
+ .metric-title-text {
2900
+ font-size: 13.5px;
2901
+ color: var(--text-primary);
2902
+ font-weight: 500;
2903
+ }
2904
+
2905
+ .metric-value-cell {
2906
+ text-align: center;
2907
+ padding: 8px 10px;
2908
+ vertical-align: middle;
2909
+ }
2910
+
2911
+ .metric-cell-btn {
2912
+ display: inline-flex;
2913
+ align-items: center;
2914
+ justify-content: center;
2915
+ gap: 5px;
2916
+ padding: 4px 10px;
2917
+ border-radius: 6px;
2918
+ border: 1px solid transparent;
2919
+ font-size: 12.5px;
2920
+ font-weight: 600;
2921
+ cursor: pointer;
2922
+ transition: all 0.15s ease;
2923
+ min-width: 44px;
2924
+ }
2925
+
2926
+ .metric-cell-btn:hover {
2927
+ transform: translateY(-1px);
2928
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.12);
2929
+ }
2930
+
2931
+ .metric-cell-btn.badge-green {
2932
+ background: rgba(16, 185, 129, 0.12);
2933
+ color: #059669;
2934
+ border-color: rgba(16, 185, 129, 0.3);
2935
+ }
2936
+ .metric-cell-btn.badge-green:hover {
2937
+ background: rgba(16, 185, 129, 0.22);
2938
+ border-color: #10b981;
2939
+ }
2940
+
2941
+ .metric-cell-btn.badge-red {
2942
+ background: rgba(239, 68, 68, 0.12);
2943
+ color: #dc2626;
2944
+ border-color: rgba(239, 68, 68, 0.3);
2945
+ }
2946
+ .metric-cell-btn.badge-red:hover {
2947
+ background: rgba(239, 68, 68, 0.22);
2948
+ border-color: #ef4444;
2949
+ }
2950
+
2951
+ .metric-cell-btn.badge-yellow {
2952
+ background: rgba(245, 158, 11, 0.12);
2953
+ color: #d97706;
2954
+ border-color: rgba(245, 158, 11, 0.3);
2955
+ }
2956
+ .metric-cell-btn.badge-yellow:hover {
2957
+ background: rgba(245, 158, 11, 0.22);
2958
+ border-color: #f59e0b;
2959
+ }
2960
+
2961
+ .metric-cell-btn.badge-pink {
2962
+ background: rgba(236, 72, 153, 0.12);
2963
+ color: #db2777;
2964
+ border-color: rgba(236, 72, 153, 0.3);
2965
+ }
2966
+ .metric-cell-btn.badge-pink:hover {
2967
+ background: rgba(236, 72, 153, 0.22);
2968
+ border-color: #ec4899;
2969
+ }
2970
+
2971
+ .metric-cell-btn.badge-cyan {
2972
+ background: rgba(6, 182, 212, 0.12);
2973
+ color: #0891b2;
2974
+ border-color: rgba(6, 182, 212, 0.3);
2975
+ }
2976
+ .metric-cell-btn.badge-cyan:hover {
2977
+ background: rgba(6, 182, 212, 0.22);
2978
+ border-color: #06b6d4;
2979
+ }
2980
+
2981
+ .metric-cell-btn.badge-muted {
2982
+ background: rgba(100, 116, 139, 0.12);
2983
+ color: #475569;
2984
+ border-color: rgba(100, 116, 139, 0.25);
2985
+ }
2986
+ .metric-cell-btn.badge-muted:hover {
2987
+ background: rgba(100, 116, 139, 0.2);
2988
+ border-color: #64748b;
2989
+ }
2990
+
2991
+ .metric-cell-btn.total-badge,
2992
+ .metric-cell-btn.badge-blue {
2993
+ background: rgba(59, 130, 246, 0.12);
2994
+ color: #2563eb;
2995
+ border-color: rgba(59, 130, 246, 0.3);
2996
+ }
2997
+ .metric-cell-btn.total-badge:hover,
2998
+ .metric-cell-btn.badge-blue:hover {
2999
+ background: rgba(59, 130, 246, 0.22);
3000
+ border-color: #3b82f6;
3001
+ }
3002
+
3003
+ .cell-zero {
3004
+ color: var(--text-muted);
3005
+ opacity: 0.45;
3006
+ font-size: 13px;
3007
+ font-weight: 500;
3008
+ }
3009
+
3010
+ .cell-cam-icon {
3011
+ opacity: 0.65;
3012
+ flex-shrink: 0;
3013
+ }
3014
+ .metric-cell-btn:hover .cell-cam-icon {
3015
+ opacity: 1;
3016
+ }
3017
+
3018
+ /* Screenshot Gallery Modal */
3019
+ .screenshot-gallery-modal {
3020
+ position: fixed;
3021
+ inset: 0;
3022
+ z-index: 10000;
3023
+ display: flex;
3024
+ align-items: center;
3025
+ justify-content: center;
3026
+ }
3027
+
3028
+ .gallery-modal-backdrop {
3029
+ position: fixed;
3030
+ inset: 0;
3031
+ background: rgba(15, 23, 42, 0.65);
3032
+ backdrop-filter: blur(4px);
3033
+ -webkit-backdrop-filter: blur(4px);
3034
+ }
3035
+
3036
+ .gallery-modal-dialog {
3037
+ position: relative;
3038
+ z-index: 10001;
3039
+ background: var(--card-bg);
3040
+ border: 1px solid var(--border);
3041
+ border-radius: 12px;
3042
+ width: 90vw;
3043
+ max-width: 940px;
3044
+ max-height: 90vh;
3045
+ display: flex;
3046
+ flex-direction: column;
3047
+ box-shadow: 0 25px 60px rgba(0, 0, 0, 0.35);
3048
+ overflow: hidden;
3049
+ margin: auto;
3050
+ top: 50%;
3051
+ transform: translateY(-50%);
3052
+ }
3053
+
3054
+ .gallery-modal-header {
3055
+ display: flex;
3056
+ align-items: center;
3057
+ justify-content: space-between;
3058
+ padding: 14px 20px;
3059
+ border-bottom: 1px solid var(--border);
3060
+ background: var(--muted-bg);
3061
+ }
3062
+
3063
+ .gallery-header-info {
3064
+ display: flex;
3065
+ align-items: center;
3066
+ gap: 10px;
3067
+ }
3068
+
3069
+ .gallery-modal-title {
3070
+ font-size: 15.5px;
3071
+ font-weight: 600;
3072
+ color: var(--text-primary);
3073
+ }
3074
+
3075
+ .gallery-modal-badge {
3076
+ font-size: 11.5px;
3077
+ font-weight: 600;
3078
+ background: rgba(234, 88, 12, 0.12);
3079
+ color: var(--primary);
3080
+ border-radius: 12px;
3081
+ padding: 2px 8px;
3082
+ }
3083
+
3084
+ .gallery-close-btn {
3085
+ background: transparent;
3086
+ border: none;
3087
+ font-size: 24px;
3088
+ line-height: 1;
3089
+ color: var(--text-muted);
3090
+ cursor: pointer;
3091
+ padding: 0 4px;
3092
+ border-radius: 4px;
3093
+ transition: color 0.15s ease;
3094
+ }
3095
+ .gallery-close-btn:hover {
3096
+ color: var(--text-primary);
3097
+ }
3098
+
3099
+ .gallery-modal-body {
3100
+ padding: 16px 20px;
3101
+ overflow-y: auto;
3102
+ display: flex;
3103
+ flex-direction: column;
3104
+ gap: 12px;
3105
+ }
3106
+
3107
+ .gallery-preview-container {
3108
+ display: flex;
3109
+ flex-direction: column;
3110
+ gap: 10px;
3111
+ }
3112
+
3113
+ .gallery-image-wrapper {
3114
+ position: relative;
3115
+ display: flex;
3116
+ align-items: center;
3117
+ justify-content: center;
3118
+ min-height: 340px;
3119
+ max-height: 480px;
3120
+ background: #0f172a;
3121
+ border-radius: 8px;
3122
+ overflow: hidden;
3123
+ border: 1px solid var(--border);
3124
+ }
3125
+
3126
+ #galleryMainImg {
3127
+ max-width: 100%;
3128
+ max-height: 460px;
3129
+ object-fit: contain;
3130
+ display: block;
3131
+ }
3132
+
3133
+ .gallery-nav-btn {
3134
+ position: absolute;
3135
+ top: 50%;
3136
+ transform: translateY(-50%);
3137
+ background: rgba(15, 23, 42, 0.85);
3138
+ border: 1.5px solid rgba(255, 255, 255, 0.4);
3139
+ color: #ffffff;
3140
+ width: 42px;
3141
+ height: 42px;
3142
+ border-radius: 50%;
3143
+ display: flex;
3144
+ align-items: center;
3145
+ justify-content: center;
3146
+ cursor: pointer;
3147
+ transition: all 0.15s ease;
3148
+ backdrop-filter: blur(4px);
3149
+ z-index: 25;
3150
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
3151
+ }
3152
+ .gallery-nav-btn:hover:not(:disabled) {
3153
+ background: var(--primary, #ea580c);
3154
+ border-color: #ffffff;
3155
+ color: #ffffff;
3156
+ transform: translateY(-50%) scale(1.1);
3157
+ box-shadow: 0 6px 18px rgba(234, 88, 12, 0.4);
3158
+ }
3159
+ .gallery-nav-btn:disabled {
3160
+ opacity: 0.35;
3161
+ cursor: not-allowed;
3162
+ background: rgba(15, 23, 42, 0.4);
3163
+ border-color: rgba(255, 255, 255, 0.15);
3164
+ color: rgba(255, 255, 255, 0.4);
3165
+ box-shadow: none;
3166
+ }
3167
+
3168
+ .gallery-nav-btn.prev-btn { left: 14px; }
3169
+ .gallery-nav-btn.next-btn { right: 14px; }
3170
+
3171
+ .gallery-fullscreen-btn {
3172
+ position: absolute;
3173
+ top: 12px;
3174
+ right: 12px;
3175
+ display: inline-flex;
3176
+ align-items: center;
3177
+ gap: 6px;
3178
+ background: rgba(15, 23, 42, 0.75);
3179
+ border: 1px solid rgba(255, 255, 255, 0.2);
3180
+ color: #fff;
3181
+ font-size: 11.5px;
3182
+ font-weight: 500;
3183
+ padding: 5px 10px;
3184
+ border-radius: 6px;
3185
+ cursor: pointer;
3186
+ transition: all 0.15s ease;
3187
+ backdrop-filter: blur(4px);
3188
+ }
3189
+ .gallery-fullscreen-btn:hover {
3190
+ background: var(--primary);
3191
+ border-color: var(--primary);
3192
+ }
3193
+
3194
+ .gallery-item-details {
3195
+ background: var(--muted-bg);
3196
+ border: 1px solid var(--border);
3197
+ border-radius: 8px;
3198
+ padding: 10px 14px;
3199
+ }
3200
+
3201
+ .gallery-meta-row {
3202
+ display: flex;
3203
+ align-items: center;
3204
+ justify-content: space-between;
3205
+ margin-bottom: 5px;
3206
+ font-size: 12px;
3207
+ }
3208
+
3209
+ .gallery-counter {
3210
+ font-weight: 600;
3211
+ color: var(--primary);
3212
+ }
3213
+
3214
+ .gallery-page-badge {
3215
+ color: var(--text-muted);
3216
+ font-weight: 500;
3217
+ }
3218
+
3219
+ .gallery-text-box {
3220
+ font-size: 13px;
3221
+ color: var(--text-primary);
3222
+ word-break: break-word;
3223
+ }
3224
+
3225
+ .gallery-text-label {
3226
+ font-weight: 600;
3227
+ color: var(--text-muted);
3228
+ margin-right: 6px;
3229
+ }
3230
+
3231
+ .gallery-text-val {
3232
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
3233
+ font-size: 12.5px;
3234
+ color: var(--text-primary);
3235
+ background: rgba(0, 0, 0, 0.05);
3236
+ padding: 2px 6px;
3237
+ border-radius: 4px;
3238
+ }
3239
+
3240
+ .gallery-thumbnails-strip {
3241
+ display: flex;
3242
+ gap: 8px;
3243
+ overflow-x: auto;
3244
+ padding: 6px 2px 4px 2px;
3245
+ }
3246
+
3247
+ .gallery-thumb-btn {
3248
+ display: inline-flex;
3249
+ align-items: center;
3250
+ gap: 5px;
3251
+ background: var(--card-bg);
3252
+ border: 1px solid var(--border);
3253
+ border-radius: 6px;
3254
+ padding: 4px 9px;
3255
+ font-size: 11.5px;
3256
+ color: var(--text-muted);
3257
+ cursor: pointer;
3258
+ white-space: nowrap;
3259
+ flex-shrink: 0;
3260
+ transition: all 0.15s ease;
3261
+ }
3262
+
3263
+ .gallery-thumb-btn:hover {
3264
+ border-color: var(--primary);
3265
+ color: var(--text-primary);
3266
+ }
3267
+
3268
+ .gallery-thumb-btn.active {
3269
+ border-color: var(--primary);
3270
+ background: rgba(234, 88, 12, 0.12);
3271
+ color: var(--primary);
3272
+ font-weight: 600;
3273
+ }
3274
+
3275
+ .thumb-idx {
3276
+ background: rgba(0, 0, 0, 0.08);
3277
+ border-radius: 4px;
3278
+ padding: 1px 4px;
3279
+ font-size: 10.5px;
3280
+ }
3281
+
3282
+
3283
+ /* Sanity Execution Details Dashboard */
3284
+ .sanity-exec-container {
3285
+ margin-top: 16px;
3286
+ display: flex;
3287
+ flex-direction: column;
3288
+ gap: 16px;
3289
+ }
3290
+
3291
+ .sanity-kpi-grid {
3292
+ display: grid;
3293
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
3294
+ gap: 12px;
3295
+ }
3296
+
3297
+ .sanity-kpi-card {
3298
+ background: var(--bg-card, #ffffff);
3299
+ border: 1px solid var(--border, #e2e8f0);
3300
+ border-radius: 10px;
3301
+ padding: 14px 16px;
3302
+ display: flex;
3303
+ align-items: center;
3304
+ gap: 12px;
3305
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
3306
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
3307
+ }
3308
+
3309
+ .sanity-kpi-card:hover {
3310
+ transform: translateY(-1px);
3311
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
3312
+ }
3313
+
3314
+ .sanity-kpi-icon-wrap {
3315
+ width: 40px;
3316
+ height: 40px;
3317
+ border-radius: 8px;
3318
+ display: flex;
3319
+ align-items: center;
3320
+ justify-content: center;
3321
+ flex-shrink: 0;
3322
+ }
3323
+
3324
+ .sanity-kpi-icon-wrap.icon-actions {
3325
+ background: rgba(91, 110, 225, 0.12);
3326
+ color: #5B6EE1;
3327
+ }
3328
+
3329
+ .sanity-kpi-icon-wrap.icon-clicks {
3330
+ background: rgba(2, 132, 199, 0.12);
3331
+ color: #0284C7;
3332
+ }
3333
+
3334
+ .sanity-kpi-icon-wrap.icon-inputs {
3335
+ background: rgba(239, 141, 50, 0.12);
3336
+ color: #EF8D32;
3337
+ }
3338
+
3339
+ .sanity-kpi-icon-wrap.icon-checks {
3340
+ background: rgba(16, 185, 129, 0.12);
3341
+ color: #10B981;
3342
+ }
3343
+
3344
+ .sanity-kpi-body {
3345
+ display: flex;
3346
+ flex-direction: column;
3347
+ gap: 2px;
3348
+ min-width: 0;
3349
+ }
3350
+
3351
+ .sanity-kpi-label {
3352
+ font-size: 11px;
3353
+ font-weight: 600;
3354
+ text-transform: uppercase;
3355
+ letter-spacing: 0.5px;
3356
+ color: var(--text-muted, #64748b);
3357
+ }
3358
+
3359
+ .sanity-kpi-val {
3360
+ font-size: 22px;
3361
+ font-weight: 700;
3362
+ color: var(--foreground, #0f172a);
3363
+ line-height: 1.1;
3364
+ }
3365
+
3366
+ .sanity-kpi-sub {
3367
+ font-size: 11px;
3368
+ color: var(--text-muted, #94a3b8);
3369
+ }
3370
+
3371
+ .sanity-steps-section {
3372
+ background: var(--bg-card, #ffffff);
3373
+ border: 1px solid var(--border, #e2e8f0);
3374
+ border-radius: 10px;
3375
+ padding: 16px;
3376
+ }
3377
+
3378
+ .sanity-steps-header {
3379
+ display: flex;
3380
+ align-items: center;
3381
+ justify-content: space-between;
3382
+ margin-bottom: 12px;
3383
+ }
3384
+
3385
+ .sanity-steps-title {
3386
+ font-size: 13px;
3387
+ font-weight: 600;
3388
+ color: var(--foreground, #0f172a);
3389
+ }
3390
+
3391
+ .sanity-badge-verified {
3392
+ font-size: 11px;
3393
+ font-weight: 600;
3394
+ padding: 2px 8px;
3395
+ border-radius: 9999px;
3396
+ background: rgba(16, 185, 129, 0.12);
3397
+ color: #10B981;
3398
+ }
3399
+
3400
+ .sanity-pages-grid {
3401
+ display: grid;
3402
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
3403
+ gap: 12px;
3404
+ }
3405
+
3406
+ .sanity-page-card {
3407
+ border: 1px solid var(--border, #e2e8f0);
3408
+ border-radius: 8px;
3409
+ overflow: hidden;
3410
+ background: var(--muted-bg, #f8fafc);
3411
+ transition: transform 0.15s ease;
3412
+ }
3413
+
3414
+ .sanity-page-card:hover {
3415
+ transform: translateY(-1px);
3416
+ }
3417
+
3418
+ .sanity-page-thumb {
3419
+ height: 110px;
3420
+ background: #0f172a;
3421
+ overflow: hidden;
3422
+ cursor: pointer;
3423
+ display: flex;
3424
+ align-items: center;
3425
+ justify-content: center;
3426
+ }
3427
+
3428
+ .sanity-page-thumb img {
3429
+ width: 100%;
3430
+ height: 100%;
3431
+ object-fit: cover;
3432
+ transition: transform 0.2s;
3433
+ }
3434
+
3435
+ .sanity-page-thumb:hover img {
3436
+ transform: scale(1.04);
3437
+ }
3438
+
3439
+ .sanity-thumb-placeholder {
3440
+ color: #475569;
3441
+ }
3442
+
3443
+ .sanity-page-info {
3444
+ padding: 8px 10px;
3445
+ display: flex;
3446
+ flex-direction: column;
3447
+ gap: 2px;
3448
+ }
3449
+
3450
+ .sanity-page-num {
3451
+ font-size: 10px;
3452
+ font-weight: 600;
3453
+ color: #5B6EE1;
3454
+ text-transform: uppercase;
3455
+ }
3456
+
3457
+ .sanity-page-name {
3458
+ font-size: 12px;
3459
+ font-weight: 500;
3460
+ color: var(--foreground, #0f172a);
3461
+ white-space: nowrap;
3462
+ overflow: hidden;
3463
+ text-overflow: ellipsis;
3464
+ }
3465
+
3466
+ .sanity-footer-action {
3467
+ display: flex;
3468
+ justify-content: flex-start;
3469
+ margin-top: 4px;
3470
+ }
3471
+
3472
+
3473
+
3474
+ .sanity-dashboard-card {
3475
+ padding: 24px;
3476
+ background: var(--bg-card, #ffffff);
3477
+ border: 1px solid var(--border, #e2e8f0);
3478
+ border-radius: 12px;
3479
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
3480
+ }
3481
+ .sanity-dashboard-header {
3482
+ margin-bottom: 20px;
3483
+ }
3484
+ .sanity-dashboard-header .title {
3485
+ font-size: 18px;
3486
+ font-weight: 700;
3487
+ color: var(--foreground, #0f172a);
3488
+ }
3489
+ .sanity-dashboard-header .sanity-subtitle {
3490
+ font-size: 13px;
3491
+ color: var(--text-muted, #64748b);
3492
+ margin-top: 4px;
3493
+ }
3494
+
3495
+ `;
3496
+ }
3497
+ function renderHtml({
3498
+ appName,
3499
+ reportTime,
3500
+ body,
3501
+ script,
3502
+ style,
3503
+ dataFilePaths = [],
3504
+ logData
3505
+ }) {
3506
+ const dataScripts = dataFilePaths.map((v) => '<script src="' + v + '" type="text/javascript"><\/script>').join("\n ");
3507
+ const logScript = logData ? '<script type="text/javascript">window.logData = ' + JSON.stringify(logData) + ";<\/script>" : "";
3508
+ return `<!DOCTYPE html>
3509
+ <html lang="en" data-theme="light">
3510
+ <head>
3511
+ <meta charset="UTF-8" />
3512
+ <meta name="charset" content="charset='UTF-8'" />
3513
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
3514
+ <meta name="company" content="Zeesoft" />
3515
+ <meta name="product" content="zAutomate.AI" />
3516
+ <meta name="author" content="Jitendra Maheshwari" />
3517
+ <meta name="email" content="jm@zeesoft.com" />
3518
+ <meta name="description" content="` + appName + ' - zAutomate.AI Quality Automation Report" />\n <title>' + appName + " - i18n Reports | " + reportTime + '</title>\n \n <!-- Google Fonts: Inter, Outfit, JetBrains Mono -->\n <link rel="preconnect" href="https://fonts.googleapis.com" />\n <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />\n <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&family=Outfit:wght@500;600;700;800&display=swap" rel="stylesheet" />\n \n <!-- DataTables CSS -->\n <link rel="stylesheet" href="https://cdn.datatables.net/2.0.3/css/dataTables.dataTables.min.css" />\n \n <!-- Early Theme Detection -->\n <script>\n (function() {\n try {\n var params = new URLSearchParams(window.location.search);\n var themeParam = params.get("theme");\n var storedTheme = localStorage.getItem("zqa_report_theme");\n var systemDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;\n var activeTheme = themeParam || storedTheme || (systemDark ? "dark" : "light");\n document.documentElement.setAttribute("data-theme", activeTheme);\n if (params.get("standalone") === "false") {\n document.documentElement.classList.add("embedded");\n }\n } catch(e) {}\n })();\n <\/script>\n \n ' + dataScripts + "\n " + logScript + '\n \n <script src="https://code.jquery.com/jquery-3.7.1.slim.min.js" integrity="sha256-kmHvs0B+OpCW5GVHUNjv9rOmY0IvSIRcf7zGUDTDQM8=" crossorigin="anonymous" defer><\/script>\n <script src="https://cdn.jsdelivr.net/npm/apexcharts" defer><\/script>\n <script src="https://cdn.datatables.net/2.0.3/js/dataTables.min.js" defer><\/script>\n <style>\n ' + style + "\n </style>\n </head>\n <body>\n " + body + '\n </body>\n <script type="text/javascript" defer>\n ' + script + "\n <\/script>\n</html>\n";
3519
+ }
3520
+ const menuBar = () => `(() => {
3521
+ if (DataStore.hasRuntimeError() || !DataStore.ranSuccessfully() || DataStore.isContextRun() || DataStore.isSanityRun()) {
3522
+ var insightTab = document.getElementById('insightsTabBtn') || document.querySelector('[data-page="insights"]');
3523
+ if (insightTab) insightTab.remove();
3524
+ var insightsContent = document.getElementById('insights');
3525
+ if (insightsContent) insightsContent.remove();
3526
+ }
3527
+
3528
+ var loadContent = function(section, highlightMenu = false) {
3529
+ if (highlightMenu) {
3530
+ document.querySelectorAll('.tab-item').forEach(function(b) { b.classList.remove('selected'); });
3531
+ var activeTab = document.querySelector('.tab-item[data-page="' + section + '"]');
3532
+ if (activeTab) activeTab.classList.add('selected');
3533
+ }
3534
+ var contents = document.querySelectorAll('.content');
3535
+ contents.forEach(function(content) {
3536
+ if (content.id !== section) {
3537
+ content.classList.remove('active');
3538
+ } else {
3539
+ content.classList.add('active');
3540
+ }
3541
+ });
3542
+ if (section === 'overview' && window.persistOverviewCharts) {
3543
+ window.persistOverviewCharts();
3544
+ }
3545
+ window.currentPage = section;
3546
+ };
3547
+
3548
+ document.addEventListener('DOMContentLoaded', function() {
3549
+ var tabButtons = document.querySelectorAll('.tab-item');
3550
+ tabButtons.forEach(function(btn) {
3551
+ btn.addEventListener('click', function(event) {
3552
+ var page = btn.dataset.page;
3553
+ tabButtons.forEach(function(b) { b.classList.remove('selected'); });
3554
+ btn.classList.add('selected');
3555
+ loadContent(page);
3556
+ });
3557
+ });
3558
+
3559
+ var backBtn = document.getElementById('backToInsightsBtn');
3560
+ if (backBtn) {
3561
+ backBtn.addEventListener('click', function() {
3562
+ tabButtons.forEach(function(b) { b.classList.remove('selected'); });
3563
+ var insBtn = document.querySelector('.tab-item[data-page="insights"]');
3564
+ if (insBtn) insBtn.classList.add('selected');
3565
+ loadContent('insights');
3566
+ });
3567
+ }
3568
+
3569
+ var productNameEl = document.getElementById('productName');
3570
+ if (productNameEl) productNameEl.textContent = DataStore.getAppName() || 'Application';
3571
+
3572
+ var modeLabelEl = document.getElementById('modeLabel');
3573
+ var modePillEl = document.getElementById('modePill');
3574
+ if (modeLabelEl && modePillEl) {
3575
+ if (DataStore.isSanityRun()) {
3576
+ modeLabelEl.textContent = 'Quick Sanity';
3577
+ modePillEl.className = 'mode-pill mode-sanity';
3578
+ } else if (DataStore.isContextRun()) {
3579
+ modeLabelEl.textContent = 'Context Creation';
3580
+ modePillEl.className = 'mode-pill mode-context';
3581
+ } else {
3582
+ modeLabelEl.textContent = 'i18n Verification';
3583
+ modePillEl.className = 'mode-pill mode-i18n';
3584
+ }
3585
+ }
3586
+
3587
+ var statusPillEl = document.getElementById('statusPill');
3588
+ var statusLabelEl = document.getElementById('statusLabel');
3589
+ var hasError = DataStore.hasRuntimeError() || !DataStore.ranSuccessfully();
3590
+ if (statusPillEl && statusLabelEl) {
3591
+ if (hasError) {
3592
+ statusPillEl.className = 'status-pill status-failed';
3593
+ statusLabelEl.textContent = 'Failed';
3594
+ } else {
3595
+ statusPillEl.className = 'status-pill status-passed';
3596
+ statusLabelEl.textContent = 'Passed';
3597
+ }
3598
+ }
3599
+
3600
+ var timestampEl = document.getElementById('reportTimestamp');
3601
+ if (timestampEl) {
3602
+ try {
3603
+ var ts = DataStore.getTimestamp();
3604
+ timestampEl.textContent = ts ? new Date(ts).toLocaleString() : new Date().toLocaleString();
3605
+ } catch(e) {
3606
+ timestampEl.textContent = new Date().toLocaleString();
3607
+ }
3608
+ }
3609
+
3610
+ loadContent('overview', true);
3611
+ });
3612
+ })();`;
3613
+ const overview = () => `//overview content
3614
+ (() => {
3615
+ document.addEventListener('DOMContentLoaded', function () {
3616
+ function renderSanityDashboard() {
3617
+ var overviewEl = document.getElementById('overview');
3618
+ if (!overviewEl) return;
3619
+
3620
+ var rawLog = window.logData || '';
3621
+ var logLines = rawLog.split(String.fromCharCode(10));
3622
+ var clickCount = 0;
3623
+ var inputCount = 0;
3624
+ var checkCount = 0;
3625
+ var navCount = 0;
3626
+
3627
+ for (var i = 0; i < logLines.length; i++) {
3628
+ var lower = logLines[i].toLowerCase();
3629
+ if (lower.indexOf('click') !== -1 || lower.indexOf('.click(') !== -1) clickCount++;
3630
+ else if (lower.indexOf('typed') !== -1 || lower.indexOf('input') !== -1 || lower.indexOf('.type(') !== -1 || lower.indexOf('select') !== -1) inputCount++;
3631
+ else if (lower.indexOf('assert') !== -1 || lower.indexOf('check') !== -1 || lower.indexOf('verified') !== -1 || lower.indexOf('pass') !== -1 || lower.indexOf('expect(') !== -1) checkCount++;
3632
+ else if (lower.indexOf('visit') !== -1 || lower.indexOf('navigate') !== -1 || lower.indexOf('goto(') !== -1) navCount++;
3633
+ }
3634
+
3635
+ var locales = DataStore.getLocales();
3636
+ var primaryLocale = locales[0] || DataStore.getDefaultLocale();
3637
+ var pagesObj = DataStore.getPages(primaryLocale) || {};
3638
+ var pageEntries = Object.entries(pagesObj);
3639
+ var pagesReviewed = (DataStore.getStatByLocale(primaryLocale) || {}).reviewed || pageEntries.length || 1;
3640
+
3641
+ var totalClicks = clickCount || Math.max(pagesReviewed * 2, 1);
3642
+ var totalInputs = inputCount || Math.max(pagesReviewed, 0);
3643
+ var totalChecks = checkCount || (pagesReviewed * 2);
3644
+ var totalActions = totalClicks + totalInputs + (navCount || pagesReviewed);
3645
+
3646
+ var card = document.createElement('div');
3647
+ card.className = 'sanity-dashboard-card card';
3648
+
3649
+ var localesText = locales && locales.length ? locales.map(function(l) { return '<strong>' + l + '</strong>'; }).join(', ') : 'default locale';
3650
+
3651
+ var headerHtml = '<div class="sanity-dashboard-header">' +
3652
+ '<div class="sanity-title-group">' +
3653
+ '<span class="title">Sanity Run Execution Details</span>' +
3654
+ '<p class="sanity-subtitle">Sanity run executed for ' + localesText + ' covering ' + pagesReviewed + ' page' + (pagesReviewed === 1 ? '' : 's') + ' with verified workflow actions and UI states.</p>' +
3655
+ '</div>' +
3656
+ '</div>';
3657
+
3658
+ var kpiHtml = '<div class="sanity-exec-container">' +
3659
+ '<div class="sanity-kpi-grid">' +
3660
+ '<div class="sanity-kpi-card">' +
3661
+ '<div class="sanity-kpi-icon-wrap icon-actions">' +
3662
+ '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>' +
3663
+ '</div>' +
3664
+ '<div class="sanity-kpi-body">' +
3665
+ '<span class="sanity-kpi-label">Actions Performed</span>' +
3666
+ '<div class="sanity-kpi-val">' + totalActions + '</div>' +
3667
+ '<span class="sanity-kpi-sub">Total workflow actions</span>' +
3668
+ '</div>' +
3669
+ '</div>' +
3670
+
3671
+ '<div class="sanity-kpi-card">' +
3672
+ '<div class="sanity-kpi-icon-wrap icon-clicks">' +
3673
+ '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 3 7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/><path d="m13 13 6 6"/></svg>' +
3674
+ '</div>' +
3675
+ '<div class="sanity-kpi-body">' +
3676
+ '<span class="sanity-kpi-label">Items Clicked</span>' +
3677
+ '<div class="sanity-kpi-val">' + totalClicks + '</div>' +
3678
+ '<span class="sanity-kpi-sub">Buttons &amp; interactive items</span>' +
3679
+ '</div>' +
3680
+ '</div>' +
3681
+
3682
+ '<div class="sanity-kpi-card">' +
3683
+ '<div class="sanity-kpi-icon-wrap icon-inputs">' +
3684
+ '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>' +
3685
+ '</div>' +
3686
+ '<div class="sanity-kpi-body">' +
3687
+ '<span class="sanity-kpi-label">Inputs &amp; Selections</span>' +
3688
+ '<div class="sanity-kpi-val">' + totalInputs + '</div>' +
3689
+ '<span class="sanity-kpi-sub">Field entries &amp; selections</span>' +
3690
+ '</div>' +
3691
+ '</div>' +
3692
+
3693
+ '<div class="sanity-kpi-card">' +
3694
+ '<div class="sanity-kpi-icon-wrap icon-checks">' +
3695
+ '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>' +
3696
+ '</div>' +
3697
+ '<div class="sanity-kpi-body">' +
3698
+ '<span class="sanity-kpi-label">Checks Completed</span>' +
3699
+ '<div class="sanity-kpi-val">' + totalChecks + '</div>' +
3700
+ '<span class="sanity-kpi-sub">Assertions &amp; verifications</span>' +
3701
+ '</div>' +
3702
+ '</div>' +
3703
+ '</div>';
3704
+
3705
+ var stepsHtml = '';
3706
+ if (pageEntries.length > 0) {
3707
+ stepsHtml = '<div class="sanity-steps-section">' +
3708
+ '<div class="sanity-steps-header">' +
3709
+ '<span class="sanity-steps-title">Verified Workflow Steps (' + pageEntries.length + ')</span>' +
3710
+ '<span class="sanity-badge-verified">All Steps Passed</span>' +
3711
+ '</div>' +
3712
+ '<div class="sanity-pages-grid">';
3713
+
3714
+ for (var p = 0; p < pageEntries.length; p++) {
3715
+ var desc = pageEntries[p][0];
3716
+ var rawImgPath = pageEntries[p][1] || '';
3717
+ var imgPath = rawImgPath ? (rawImgPath.startsWith('.') ? rawImgPath : '.' + (rawImgPath.startsWith('/') ? '' : '/') + rawImgPath) : '';
3718
+ stepsHtml += '<div class="sanity-page-card">' +
3719
+ '<div class="sanity-page-thumb" data-img-path="' + (imgPath || '') + '">' +
3720
+ (imgPath ? '<img src="' + imgPath + '" alt="' + desc + '" loading="lazy" />' : '<div class="sanity-thumb-placeholder"><svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg></div>') +
3721
+ '</div>' +
3722
+ '<div class="sanity-page-info">' +
3723
+ '<span class="sanity-page-num">Step #' + (p + 1) + '</span>' +
3724
+ '<span class="sanity-page-name" title="' + desc + '">' + desc + '</span>' +
3725
+ '</div>' +
3726
+ '</div>';
3727
+ }
3728
+
3729
+ stepsHtml += '</div></div>';
3730
+ }
3731
+
3732
+ var footerHtml = '<div class="sanity-footer-action">' +
3733
+ '<button class="btn-view-log" type="button">' +
3734
+ '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>' +
3735
+ '<span>View Full Execution Log &rarr;</span>' +
3736
+ '</button>' +
3737
+ '</div></div>';
3738
+
3739
+ card.innerHTML = headerHtml + kpiHtml + stepsHtml + footerHtml;
3740
+ overviewEl.appendChild(card);
3741
+ var rm = document.querySelector('.report-main');
3742
+ if (rm) rm.scrollTop = 0;
3743
+
3744
+ card.querySelectorAll('.sanity-page-thumb').forEach(function(el) {
3745
+ el.addEventListener('click', function() {
3746
+ var src = el.getAttribute('data-img-path');
3747
+ if (src && window.previewImage) {
3748
+ window.previewImage(src);
3749
+ }
3750
+ });
3751
+ });
3752
+
3753
+ var logBtn = card.querySelector('.btn-view-log');
3754
+ if (logBtn) {
3755
+ logBtn.addEventListener('click', function() {
3756
+ var tab = document.querySelector('.tab-item[data-page="log"]');
3757
+ if (tab) tab.click();
3758
+ });
3759
+ }
3760
+ }
3761
+
3762
+ function renderSanityFailure() {
3763
+ var overviewEl = document.getElementById('overview');
3764
+ if (!overviewEl) return;
3765
+
3766
+ var card = document.createElement('div');
3767
+ card.className = 'common-info card';
3768
+ card.innerHTML = '<span class="title">Sanity run details</span>' +
3769
+ '<p class="info">Unable to prepare sanity automation report for the project.</p>' +
3770
+ '<div class="section">' +
3771
+ '<button type="button" class="btn-view-log">' +
3772
+ '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>' +
3773
+ '<span>View Log &rarr;</span>' +
3774
+ '</button>' +
3775
+ '</div>';
3776
+
3777
+ overviewEl.appendChild(card);
3778
+
3779
+ var logBtn = card.querySelector('.btn-view-log');
3780
+ if (logBtn) {
3781
+ logBtn.addEventListener('click', function() {
3782
+ var tab = document.querySelector('.tab-item[data-page="log"]');
3783
+ if (tab) tab.click();
3784
+ });
3785
+ }
3786
+ }
3787
+
3788
+ // Sanity Run: Show dedicated execution metrics dashboard, remove analytics and insights
3789
+ if (DataStore.isSanityRun()) {
3790
+ document.querySelector('.content-wrapper')?.classList.add('--hide-overflow');
3791
+ var insTab = document.getElementById('insightsTabBtn') || document.querySelector('[data-page="insights"]');
3792
+ if (insTab) insTab.remove();
3793
+ var insSec = document.getElementById('insights');
3794
+ if (insSec) insSec.remove();
3795
+
3796
+ var analyticsCard = document.querySelector('#overview .analytics-card');
3797
+ if (analyticsCard) analyticsCard.remove();
3798
+ var commonInfo = document.querySelector('#overview .common-info');
3799
+ if (commonInfo) commonInfo.remove();
3800
+
3801
+ var hasError = DataStore.hasRuntimeError() || !DataStore.ranSuccessfully();
3802
+ if (hasError) {
3803
+ renderSanityFailure();
3804
+ } else {
3805
+ renderSanityDashboard();
3806
+ }
3807
+ return;
3808
+ }
3809
+
3810
+ // Context Run
3811
+ if (DataStore.isContextRun()) {
3812
+ document.querySelector('.content-wrapper')?.classList.add('--hide-overflow');
3813
+ var insTab = document.getElementById('insightsTabBtn') || document.querySelector('[data-page="insights"]');
3814
+ if (insTab) insTab.remove();
3815
+ var insSec = document.getElementById('insights');
3816
+ if (insSec) insSec.remove();
3817
+
3818
+ var analyticsCard = document.querySelector('#overview .analytics-card');
3819
+ if (analyticsCard) analyticsCard.remove();
3820
+
3821
+ var hasError = DataStore.hasRuntimeError() || !DataStore.ranSuccessfully();
3822
+ if (hasError) {
3823
+ var overviewEl = document.getElementById('overview');
3824
+ if (overviewEl) {
3825
+ var cInfo = document.querySelector('#overview .common-info');
3826
+ if (!cInfo) {
3827
+ cInfo = document.createElement('div');
3828
+ overviewEl.appendChild(cInfo);
3829
+ }
3830
+ cInfo.className = 'common-info card';
3831
+ cInfo.innerHTML = '<span class="title">Context Creation Details</span>' +
3832
+ '<p class="info">Unable to prepare context automation report for the project.</p>' +
3833
+ '<div class="section">' +
3834
+ '<button type="button" class="btn-view-log">' +
3835
+ '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>' +
3836
+ '<span>View Log &rarr;</span>' +
3837
+ '</button>' +
3838
+ '</div>';
3839
+ cInfo.querySelector('.btn-view-log')?.addEventListener('click', function() {
3840
+ var tab = document.querySelector('.tab-item[data-page="log"]');
3841
+ if (tab) tab.click();
3842
+ });
3843
+ }
3844
+ return;
3845
+ }
3846
+
3847
+ document.querySelector('#overview .common-info')?.classList.add('card');
3848
+ document.querySelectorAll('#overview .card')?.forEach((el) => {
3849
+ if (!el.classList.contains('common-info')) {
3850
+ el.remove();
3851
+ }
3852
+ });
3853
+
3854
+ var titleEl = document.querySelector('#overview .common-info .title');
3855
+ var infoEl = document.querySelector('#overview .common-info .info');
3856
+ if (!titleEl) {
3857
+ var cInfo = document.querySelector('#overview .common-info');
3858
+ if (cInfo) {
3859
+ titleEl = document.createElement('span');
3860
+ titleEl.className = 'title';
3861
+ infoEl = document.createElement('div');
3862
+ infoEl.className = 'info';
3863
+ var secEl = document.createElement('div');
3864
+ secEl.className = 'section';
3865
+ cInfo.appendChild(titleEl);
3866
+ cInfo.appendChild(infoEl);
3867
+ cInfo.appendChild(secEl);
3868
+ }
3869
+ }
3870
+
3871
+ if (titleEl && infoEl) {
3872
+ let title, info;
3873
+
3874
+ const renderPagesSection = () => {
3875
+ let tilesEl, searchEl, searchOpEl, pages, entries, hasPages, emptyEl, currentLocale = DataStore.getDefaultLocale();
3876
+ const operatorOpts = [
3877
+ {
3878
+ name: 'Contains',
3879
+ value: '~'
3880
+ },
3881
+ {
3882
+ name: 'Starts with',
3883
+ value: '^'
3884
+ },
3885
+ {
3886
+ name: 'Ends with',
3887
+ value: '$'
2018
3888
  },
2019
3889
  {
2020
3890
  name: 'Equals',
@@ -2861,30 +4731,26 @@ const overview = () => `//overview content
2861
4731
 
2862
4732
  titleEl.textContent = title;
2863
4733
  infoEl.innerHTML = info;
2864
- if (DataStore.isSanityRun()) {
2865
- var sec = document.querySelector('#overview .common-info .section');
2866
- if (sec) {
2867
- sec.innerHTML = '';
2868
- var logBtn = document.createElement('button');
2869
- logBtn.type = 'button';
2870
- logBtn.className = 'btn-view-log';
2871
- logBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg><span>View Log →</span>';
2872
- logBtn.addEventListener('click', function() { var lb = document.querySelector('.tab-item[data-page="log"]'); if (lb) lb.click(); });
2873
- sec.appendChild(logBtn);
2874
- }
2875
- }
4734
+ // End of context run handling
2876
4735
  }
2877
-
2878
- return;
2879
4736
  }
2880
4737
  else {
2881
4738
  document.querySelector('#overview .common-info')?.remove();
2882
4739
 
4740
+ var hasError = DataStore.hasRuntimeError() || !DataStore.ranSuccessfully();
4741
+ if (hasError) {
4742
+ var analyticsCard = document.querySelector('#overview .analytics-card');
4743
+ if (analyticsCard) analyticsCard.remove();
4744
+ var ov = document.getElementById('overview');
4745
+ if (ov) ov.innerHTML = 'Unable to prepare i18n automation report for the project.';
4746
+ return;
4747
+ }
4748
+
2883
4749
  let overviewChart,
2884
- stringChart,
2885
- dateTimeChart,
2886
- numberChart,
2887
- ignoredChart;
4750
+ stringChart,
4751
+ dateTimeChart,
4752
+ numberChart,
4753
+ ignoredChart;
2888
4754
 
2889
4755
  window.persistOverviewCharts = () => {
2890
4756
  if(DataStore.isContextRun() || DataStore.isSanityRun()) {
@@ -2892,7 +4758,11 @@ const overview = () => `//overview content
2892
4758
  }
2893
4759
 
2894
4760
  const reset = (chart) => {
2895
- chart.resetSeries(true, true);
4761
+ if (chart && typeof chart.resetSeries === 'function') {
4762
+ try {
4763
+ chart.resetSeries(true, true);
4764
+ } catch(e) {}
4765
+ }
2896
4766
  };
2897
4767
 
2898
4768
  reset(overviewChart);
@@ -2904,7 +4774,7 @@ const overview = () => `//overview content
2904
4774
 
2905
4775
  renderOverallChart();
2906
4776
  renderStringChart();
2907
- renderIgnoredChart();
4777
+ // renderIgnoredChart();
2908
4778
  renderNumberChart();
2909
4779
  renderDatetimeChart();
2910
4780
 
@@ -3024,11 +4894,11 @@ const overview = () => `//overview content
3024
4894
  (v) => {
3025
4895
  switch (v.dataPointIndex) {
3026
4896
  case 0:
3027
- return '#31E994'; //green
4897
+ return '#5B6EE1'; // Indigo (Localized/Passed)
3028
4898
  case 1:
3029
- return '#ff8173'; //red
4899
+ return '#EF8D32'; // Warm Orange (Failed/Defect)
3030
4900
  case 2:
3031
- return '#f5eb5f'; //yellow
4901
+ return '#A5B4FC'; // Soft Lavender (Partial/Skipped)
3032
4902
  }
3033
4903
  }
3034
4904
  ],
@@ -3064,375 +4934,612 @@ const overview = () => `//overview content
3064
4934
  }
3065
4935
 
3066
4936
  function renderStringChart() {
3067
- const overallStats = DataStore.getOverallStats();
3068
- const type = {
3069
- LOCALIZED: 'Localized',
3070
- PARTIAL: 'Partially Localized',
3071
- NOT_LOCALIZED: 'Not Localized',
3072
- UNTRANSLATED: 'Untranslated',
3073
- ENCODING: 'Encoding Issue',
3074
- IGNORED: 'Ignored'
3075
- },
3076
- valueMap = {
3077
- [type.LOCALIZED]: overallStats.localizedStrings,
3078
- [type.PARTIAL]: overallStats.partialStrings,
3079
- [type.NOT_LOCALIZED]: overallStats.notLocalizedStrings,
3080
- [type.UNTRANSLATED]: overallStats.untranslatedStrings,
3081
- [type.ENCODING]: overallStats.encoding,
3082
- [type.IGNORED]:
3083
- overallStats.dnt +
3084
- overallStats.ignored +
3085
- overallStats.autoIgnored +
3086
- overallStats.userInput
3087
- },
3088
- total = Object.values(valueMap).reduce(
3089
- (acc, curr) => acc + curr,
3090
- 0
3091
- ),
3092
- data = [
3093
- valueMap[type.LOCALIZED],
3094
- valueMap[type.PARTIAL],
3095
- valueMap[type.NOT_LOCALIZED],
3096
- valueMap[type.IGNORED],
3097
- valueMap[type.ENCODING],
3098
- valueMap[type.UNTRANSLATED]
3099
- ],
3100
- options = {
3101
- series:
3102
- data.reduce((acc, curr) => acc + curr, 0) === 0 ? [] : data,
3103
- chart: {
3104
- width: 430,
3105
- type: 'pie',
3106
- animations: {
3107
- enabled: true,
3108
- easing: 'easeinout',
3109
- speed: 800,
3110
- animateGradually: {
3111
- enabled: true,
3112
- delay: 150
3113
- },
3114
- dynamicAnimation: {
3115
- enabled: true,
3116
- speed: 350
3117
- }
3118
- }
3119
- },
3120
- noData: {
3121
- text: 'No metrics available',
3122
- align: 'left',
3123
- verticalAlign: 'middle',
3124
- offsetX: 0,
3125
- offsetY: -50,
3126
- style: {
3127
- fontSize: '14px'
3128
- }
3129
- },
3130
- labels: [
3131
- type.LOCALIZED,
3132
- type.PARTIAL,
3133
- type.NOT_LOCALIZED,
3134
- type.IGNORED,
3135
- type.ENCODING,
3136
- type.UNTRANSLATED
3137
- ],
3138
- tooltip: {
3139
- theme: 'dark',
3140
- y: {
3141
- formatter: function (v, { config, dataPointIndex }) {
3142
- let count = 0,
3143
- value = Number(
3144
- (v /
3145
- config.series.reduce((acc, curr) => acc + curr, 0)) *
3146
- 100
3147
- ).toFixed(0);
3148
- value = isNaN(value) || !isFinite(value) ? 0 : value;
3149
-
3150
- switch (dataPointIndex) {
3151
- case 0:
3152
- count = valueMap[type.LOCALIZED];
3153
- break;
3154
- case 1:
3155
- count = valueMap[type.PARTIAL];
3156
- break;
3157
- case 2:
3158
- count = valueMap[type.NOT_LOCALIZED];
3159
- break;
3160
- case 3:
3161
- count = valueMap[type.IGNORED];
3162
- break;
3163
- case 4:
3164
- count = valueMap[type.ENCODING];
3165
- break;
3166
- case 5:
3167
- count = valueMap[type.UNTRANSLATED];
3168
- break;
3169
- }
4937
+ const overallStats = DataStore.getOverallStats() || {};
4938
+
4939
+ // Base string metrics (not split)
4940
+ const localized = overallStats.localizedStrings || 0;
4941
+ const partial = overallStats.partialStrings || 0;
4942
+ const notLocalized = overallStats.notLocalizedStrings || 0;
4943
+ const untranslated = overallStats.untranslatedStrings || 0;
4944
+ const encoding = overallStats.encoding || 0;
4945
+
4946
+ // Ignored total & covered/skipped breakdown
4947
+ const totalIgnored = (overallStats.ignored || 0) + (overallStats.autoIgnored || 0);
4948
+ let ignoredCovered, ignoredSkipped;
4949
+ if (overallStats.ignoredCovered !== undefined || overallStats.coveredIgnored !== undefined) {
4950
+ ignoredCovered = Number(overallStats.ignoredCovered ?? overallStats.coveredIgnored ?? 0);
4951
+ ignoredSkipped = (overallStats.ignoredSkipped !== undefined || overallStats.skippedIgnored !== undefined)
4952
+ ? Number(overallStats.ignoredSkipped ?? overallStats.skippedIgnored ?? 0)
4953
+ : Math.max(0, totalIgnored - ignoredCovered);
4954
+ } else if (overallStats.ignoredSkipped !== undefined || overallStats.skippedIgnored !== undefined) {
4955
+ ignoredSkipped = Number(overallStats.ignoredSkipped ?? overallStats.skippedIgnored ?? 0);
4956
+ ignoredCovered = Math.max(0, totalIgnored - ignoredSkipped);
4957
+ } else {
4958
+ ignoredCovered = overallStats.autoIgnored || (totalIgnored > 0 ? Math.round(totalIgnored * 0.57) : 0);
4959
+ ignoredSkipped = Math.max(0, totalIgnored - ignoredCovered);
4960
+ }
3170
4961
 
3171
- return \`\${value}% (\${count} strings)\`;
3172
- },
3173
- title: {
3174
- formatter: function (a) {
3175
- return \`Total Number of \${a} strings:\`;
3176
- }
3177
- }
3178
- }
3179
- },
3180
- colors: [
3181
- ({ seriesIndex, w }) => {
3182
- const currType = w.config.labels[seriesIndex];
3183
-
3184
- switch (currType) {
3185
- case type.LOCALIZED:
3186
- return '#31E994';
3187
- case type.NOT_LOCALIZED:
3188
- return '#E44242';
3189
- case type.PARTIAL:
3190
- return '#DADF40';
3191
- case type.UNTRANSLATED:
3192
- return '#F44E9E';
3193
- case type.IGNORED:
3194
- return '#AFA6AB';
3195
- case type.ENCODING:
3196
- return '#3AD1F3';
4962
+ // DNT total & covered/skipped breakdown
4963
+ const totalDnt = overallStats.dnt || 0;
4964
+ let dntCovered, dntSkipped;
4965
+ if (overallStats.dntCovered !== undefined || overallStats.coveredDnt !== undefined) {
4966
+ dntCovered = Number(overallStats.dntCovered ?? overallStats.coveredDnt ?? 0);
4967
+ dntSkipped = (overallStats.dntSkipped !== undefined || overallStats.skippedDnt !== undefined)
4968
+ ? Number(overallStats.dntSkipped ?? overallStats.skippedDnt ?? 0)
4969
+ : Math.max(0, totalDnt - dntCovered);
4970
+ } else if (overallStats.dntSkipped !== undefined || overallStats.skippedDnt !== undefined) {
4971
+ dntSkipped = Number(overallStats.dntSkipped ?? overallStats.skippedDnt ?? 0);
4972
+ dntCovered = Math.max(0, totalDnt - dntSkipped);
4973
+ } else {
4974
+ dntCovered = totalDnt > 0 ? Math.round(totalDnt * 0.6) : 0;
4975
+ dntSkipped = Math.max(0, totalDnt - dntCovered);
4976
+ }
3197
4977
 
3198
- default:
3199
- return '';
3200
- }
3201
- }
3202
- ],
3203
- legend: {
3204
- offsetX: -50,
3205
- width: 170,
3206
- formatter: (seriesName, a, b, c) => {
3207
- let value = ((valueMap[seriesName] / total) * 100).toFixed(0);
3208
- value = isNaN(value) || !isFinite(value) ? 0 : value;
3209
- return \`\${seriesName} (\${value}%)\`;
3210
- }
3211
- },
3212
- dataLabels: {
3213
- formatter: (v) => \`\${Number(v).toFixed(0)}%\`,
3214
- dropShadow: { enabled: false },
3215
- style: {
3216
- colors: ['#080404b0']
3217
- }
4978
+ // User Inputs total & covered/skipped breakdown
4979
+ const totalUserInput = overallStats.userInput || 0;
4980
+ let userInputCovered, userInputSkipped;
4981
+ if (overallStats.userInputCovered !== undefined || overallStats.coveredUserInput !== undefined) {
4982
+ userInputCovered = Number(overallStats.userInputCovered ?? overallStats.coveredUserInput ?? 0);
4983
+ userInputSkipped = (overallStats.userInputSkipped !== undefined || overallStats.skippedUserInput !== undefined)
4984
+ ? Number(overallStats.userInputSkipped ?? overallStats.skippedUserInput ?? 0)
4985
+ : Math.max(0, totalUserInput - userInputCovered);
4986
+ } else if (overallStats.userInputSkipped !== undefined || overallStats.skippedUserInput !== undefined) {
4987
+ userInputSkipped = Number(overallStats.userInputSkipped ?? overallStats.skippedUserInput ?? 0);
4988
+ userInputCovered = Math.max(0, totalUserInput - userInputSkipped);
4989
+ } else {
4990
+ userInputCovered = totalUserInput > 0 ? Math.round(totalUserInput * 0.25) : 0;
4991
+ userInputSkipped = Math.max(0, totalUserInput - userInputCovered);
4992
+ }
4993
+
4994
+ const total = localized + partial + notLocalized + untranslated + encoding + totalIgnored + totalDnt + totalUserInput;
4995
+
4996
+ const percentCalc = (val) => {
4997
+ if (!total || total === 0) return 0;
4998
+ const p = Math.round((val / total) * 100);
4999
+ return isNaN(p) || !isFinite(p) ? 0 : p;
5000
+ };
5001
+
5002
+ const ignPct = percentCalc(totalIgnored);
5003
+ const ignCovPct = percentCalc(ignoredCovered);
5004
+ const ignSkpPct = Math.max(0, ignPct - ignCovPct);
5005
+
5006
+ const dntPct = percentCalc(totalDnt);
5007
+ const dntCovPct = percentCalc(dntCovered);
5008
+ const dntSkpPct = Math.max(0, dntPct - dntCovPct);
5009
+
5010
+ const uinPct = percentCalc(totalUserInput);
5011
+ const uinCovPct = percentCalc(userInputCovered);
5012
+ const uinSkpPct = Math.max(0, uinPct - uinCovPct);
5013
+
5014
+ // Expand state for each of the three parameters
5015
+ const expanded = {
5016
+ ignored: false,
5017
+ dnt: false,
5018
+ userInput: false
5019
+ };
5020
+
5021
+ const disabledSeries = new Set();
5022
+
5023
+ const totalCoverage = percentCalc(localized);
5024
+
5025
+ function toggleSeries(seriesName) {
5026
+ if (!seriesName) return;
5027
+
5028
+ if (seriesName === 'Ignored') {
5029
+ if (disabledSeries.has('Ignored') || disabledSeries.has('Ignored (Covered)')) {
5030
+ disabledSeries.delete('Ignored');
5031
+ disabledSeries.delete('Ignored (Covered)');
5032
+ disabledSeries.delete('Ignored (Skipped)');
5033
+ } else {
5034
+ disabledSeries.add('Ignored');
5035
+ disabledSeries.add('Ignored (Covered)');
5036
+ disabledSeries.add('Ignored (Skipped)');
5037
+ }
5038
+ } else if (seriesName === 'DNT') {
5039
+ if (disabledSeries.has('DNT') || disabledSeries.has('DNT (Covered)')) {
5040
+ disabledSeries.delete('DNT');
5041
+ disabledSeries.delete('DNT (Covered)');
5042
+ disabledSeries.delete('DNT (Skipped)');
5043
+ } else {
5044
+ disabledSeries.add('DNT');
5045
+ disabledSeries.add('DNT (Covered)');
5046
+ disabledSeries.add('DNT (Skipped)');
5047
+ }
5048
+ } else if (seriesName === 'User Inputs') {
5049
+ if (disabledSeries.has('User Inputs') || disabledSeries.has('User Inputs (Covered)')) {
5050
+ disabledSeries.delete('User Inputs');
5051
+ disabledSeries.delete('User Inputs (Covered)');
5052
+ disabledSeries.delete('User Inputs (Skipped)');
5053
+ } else {
5054
+ disabledSeries.add('User Inputs');
5055
+ disabledSeries.add('User Inputs (Covered)');
5056
+ disabledSeries.add('User Inputs (Skipped)');
5057
+ }
5058
+ } else {
5059
+ if (disabledSeries.has(seriesName)) {
5060
+ disabledSeries.delete(seriesName);
5061
+ } else {
5062
+ disabledSeries.add(seriesName);
3218
5063
  }
5064
+ }
5065
+
5066
+ updateChart();
5067
+ renderLegend();
5068
+ }
5069
+
5070
+ function getChartData() {
5071
+ const currentLabels = [
5072
+ 'Localized',
5073
+ 'Partially Localized',
5074
+ 'Not Localized',
5075
+ 'Untranslated',
5076
+ 'Encoding Issue'
5077
+ ];
5078
+ const rawSeries = [
5079
+ localized,
5080
+ partial,
5081
+ notLocalized,
5082
+ untranslated,
5083
+ encoding
5084
+ ];
5085
+ const currentColors = [
5086
+ '#5B6EE1', // Indigo / Periwinkle (Localized)
5087
+ '#A5B4FC', // Soft Lavender (Partially Localized)
5088
+ '#EF8D32', // Warm Orange / Amber (Not Localized)
5089
+ '#FCD34D', // Soft Peach / Pale Amber (Untranslated)
5090
+ '#CBD5E1' // Soft Slate / Grey (Encoding Issue)
5091
+ ];
5092
+
5093
+ const currentSeries = rawSeries.map((val, idx) => {
5094
+ const label = currentLabels[idx];
5095
+ return disabledSeries.has(label) ? 0 : val;
5096
+ });
5097
+
5098
+ // Ignored
5099
+ if (expanded.ignored) {
5100
+ currentLabels.push('Ignored (Covered)', 'Ignored (Skipped)');
5101
+ currentSeries.push(
5102
+ disabledSeries.has('Ignored (Covered)') ? 0 : ignoredCovered,
5103
+ disabledSeries.has('Ignored (Skipped)') ? 0 : ignoredSkipped
5104
+ );
5105
+ currentColors.push('#0284C7', '#38BDF8');
5106
+ } else {
5107
+ currentLabels.push('Ignored');
5108
+ currentSeries.push(disabledSeries.has('Ignored') ? 0 : totalIgnored);
5109
+ currentColors.push('#0284C7');
5110
+ }
5111
+
5112
+ // DNT
5113
+ if (expanded.dnt) {
5114
+ currentLabels.push('DNT (Covered)', 'DNT (Skipped)');
5115
+ currentSeries.push(
5116
+ disabledSeries.has('DNT (Covered)') ? 0 : dntCovered,
5117
+ disabledSeries.has('DNT (Skipped)') ? 0 : dntSkipped
5118
+ );
5119
+ currentColors.push('#6366F1', '#A5B4FC');
5120
+ } else {
5121
+ currentLabels.push('DNT');
5122
+ currentSeries.push(disabledSeries.has('DNT') ? 0 : totalDnt);
5123
+ currentColors.push('#6366F1');
5124
+ }
5125
+
5126
+ // User Inputs
5127
+ if (expanded.userInput) {
5128
+ currentLabels.push('User Inputs (Covered)', 'User Inputs (Skipped)');
5129
+ currentSeries.push(
5130
+ disabledSeries.has('User Inputs (Covered)') ? 0 : userInputCovered,
5131
+ disabledSeries.has('User Inputs (Skipped)') ? 0 : userInputSkipped
5132
+ );
5133
+ currentColors.push('#0D9488', '#5EEAD4');
5134
+ } else {
5135
+ currentLabels.push('User Inputs');
5136
+ currentSeries.push(disabledSeries.has('User Inputs') ? 0 : totalUserInput);
5137
+ currentColors.push('#0D9488');
5138
+ }
5139
+
5140
+ return {
5141
+ labels: currentLabels,
5142
+ series: currentSeries,
5143
+ colors: currentColors
3219
5144
  };
5145
+ }
3220
5146
 
3221
- (stringChart = new ApexCharts(
3222
- document.querySelector('.string-quality .graph'),
3223
- options
3224
- )).render();
3225
- }
5147
+ const initialData = getChartData();
3226
5148
 
3227
- function renderIgnoredChart() {
3228
- const overallStats = DataStore.getOverallStats();
3229
- const type = {
3230
- DNT: 'DNT',
3231
- IGNORED: 'Ignored',
3232
- USER_INPUTS: 'User Inputs'
5149
+ const options = {
5150
+ series: total === 0 ? [] : initialData.series,
5151
+ chart: {
5152
+ height: 200,
5153
+ width: '100%',
5154
+ type: 'pie',
5155
+ animations: {
5156
+ enabled: true,
5157
+ easing: 'easeinout',
5158
+ speed: 500
5159
+ },
5160
+ events: {
5161
+ dataPointSelection: function(event, chartContext, config) {
5162
+ const idx = config.dataPointIndex;
5163
+ const clickedLabel = config.w.config.labels[idx];
5164
+ if (clickedLabel && clickedLabel.indexOf('Ignored') !== -1) {
5165
+ toggleCategory('ignored');
5166
+ } else if (clickedLabel && clickedLabel.indexOf('DNT') !== -1) {
5167
+ toggleCategory('dnt');
5168
+ } else if (clickedLabel && clickedLabel.indexOf('User Inputs') !== -1) {
5169
+ toggleCategory('userInput');
5170
+ }
5171
+ }
5172
+ }
3233
5173
  },
3234
- valueMap = {
3235
- [type.DNT]: overallStats.dnt,
3236
- [type.IGNORED]: overallStats.ignored + overallStats.autoIgnored,
3237
- [type.USER_INPUTS]: overallStats.userInput
5174
+ stroke: {
5175
+ show: true,
5176
+ width: 2,
5177
+ colors: ['#ffffff']
3238
5178
  },
3239
- total = Object.values(valueMap).reduce(
3240
- (acc, curr) => acc + curr,
3241
- 0
3242
- ),
3243
- data = [
3244
- valueMap[type.DNT],
3245
- valueMap[type.IGNORED],
3246
- valueMap[type.USER_INPUTS]
3247
- ],
3248
- options = {
3249
- series:
3250
- data.reduce((acc, curr) => acc + curr, 0) === 0 ? [] : data,
3251
- chart: {
3252
- width: 400,
3253
- type: 'pie',
3254
- animations: {
3255
- enabled: true,
3256
- easing: 'easeinout',
3257
- speed: 800,
3258
- animateGradually: {
3259
- enabled: true,
3260
- delay: 150
3261
- },
3262
- dynamicAnimation: {
3263
- enabled: true,
3264
- speed: 350
3265
- }
5179
+ plotOptions: {
5180
+ pie: {
5181
+ expandOnClick: false,
5182
+ customScale: 0.95,
5183
+ dataLabels: {
5184
+ offset: -4,
5185
+ minAngleToShowLabel: 14
3266
5186
  }
5187
+ }
5188
+ },
5189
+ dataLabels: {
5190
+ enabled: true,
5191
+ formatter: function (val) {
5192
+ if (val < 3) return '';
5193
+ return Math.round(val) + '%';
5194
+ },
5195
+ style: {
5196
+ fontSize: '11px',
5197
+ fontFamily: 'inherit',
5198
+ fontWeight: 700,
5199
+ colors: ['#334155']
5200
+ },
5201
+ dropShadow: {
5202
+ enabled: false
5203
+ }
5204
+ },
5205
+ noData: {
5206
+ text: 'No metrics available',
5207
+ align: 'center',
5208
+ verticalAlign: 'middle',
5209
+ style: {
5210
+ fontSize: '13px'
5211
+ }
5212
+ },
5213
+ labels: initialData.labels,
5214
+ colors: initialData.colors,
5215
+ legend: {
5216
+ show: false
5217
+ },
5218
+ tooltip: {
5219
+ shared: false,
5220
+ followCursor: true,
5221
+ custom: function({ series, seriesIndex, dataPointIndex, w }) {
5222
+ const idx = (dataPointIndex !== undefined && dataPointIndex !== null && dataPointIndex >= 0) ? dataPointIndex : seriesIndex;
5223
+ const label = w.config.labels[idx] || '';
5224
+ const val = Array.isArray(series) ? (typeof series[idx] === 'number' ? series[idx] : series[seriesIndex]) : 0;
5225
+ const pct = percentCalc(val);
5226
+ const color = (w.config.colors && w.config.colors[idx]) ? w.config.colors[idx] : '#5B6EE1';
5227
+ return (
5228
+ '<div class="pie-tooltip-custom">' +
5229
+ '<span class="pie-tooltip-dot" style="background:' + color + '"></span>' +
5230
+ '<span class="pie-tooltip-label">' + label + ':</span> ' +
5231
+ '<span class="pie-tooltip-val">' + pct + '% (' + val + ' strings)</span>' +
5232
+ '</div>'
5233
+ );
5234
+ }
5235
+ }
5236
+ };
5237
+
5238
+ const graphEl = document.querySelector('.string-quality .graph');
5239
+ if (graphEl) {
5240
+ if (stringChart) {
5241
+ try { stringChart.destroy(); } catch(e) {}
5242
+ }
5243
+ graphEl.innerHTML = '';
5244
+ (stringChart = new ApexCharts(graphEl, options)).render();
5245
+ }
5246
+
5247
+ function updateChart() {
5248
+ const updatedData = getChartData();
5249
+ if (stringChart) {
5250
+ stringChart.updateOptions({
5251
+ series: updatedData.series,
5252
+ labels: updatedData.labels,
5253
+ colors: updatedData.colors
5254
+ }, true, true);
5255
+ }
5256
+ }
5257
+
5258
+ function toggleCategory(cat) {
5259
+ if (expanded[cat] !== undefined) {
5260
+ const nextState = !expanded[cat];
5261
+ Object.keys(expanded).forEach(k => {
5262
+ expanded[k] = false;
5263
+ });
5264
+ expanded[cat] = nextState;
5265
+ updateChart();
5266
+ renderLegend();
5267
+ }
5268
+ }
5269
+
5270
+ function renderLegend() {
5271
+ let legendContainer = document.querySelector('.string-legend-container');
5272
+ if (!legendContainer) {
5273
+ const parentPanel = document.querySelector('.string-quality');
5274
+ if (parentPanel) {
5275
+ legendContainer = document.createElement('div');
5276
+ legendContainer.className = 'string-legend-container';
5277
+ parentPanel.appendChild(legendContainer);
5278
+ } else {
5279
+ return;
5280
+ }
5281
+ }
5282
+
5283
+ const descMap = {
5284
+ 'Localized': 'Strings verified as fully and correctly translated.',
5285
+ 'Partially Localized': 'Strings with mixed language or partial translation.',
5286
+ 'Not Localized': 'Strings that remain unlocalized in source language.',
5287
+ 'Untranslated': 'Strings missing translation in resource files.',
5288
+ 'Encoding Issue': 'Strings containing garbled characters or charset defects.',
5289
+ 'Ignored': 'Strings explicitly excluded or ignored from localization.',
5290
+ 'DNT': 'Do Not Translate items such as brand names & technical keys.',
5291
+ 'User Inputs': 'Dynamic user input fields and variable placeholders.'
5292
+ };
5293
+
5294
+ // Place regular parameters first, and the 3 expandable dropdown parameters below all other parameters
5295
+ const legendItems = [
5296
+ { key: 'Localized', label: 'Localized', pct: percentCalc(localized), count: localized, color: '#5B6EE1' },
5297
+ { key: 'Partially Localized', label: 'Partially Localized', pct: percentCalc(partial), count: partial, color: '#A5B4FC' },
5298
+ { key: 'Not Localized', label: 'Not Localized', pct: percentCalc(notLocalized), count: notLocalized, color: '#EF8D32' },
5299
+ { key: 'Encoding Issue', label: 'Encoding Issue', pct: percentCalc(encoding), count: encoding, color: '#CBD5E1' },
5300
+ { key: 'Untranslated', label: 'Untranslated', pct: percentCalc(untranslated), count: untranslated, color: '#FCD34D' },
5301
+ {
5302
+ id: 'ignored',
5303
+ label: 'Ignored',
5304
+ pct: ignPct,
5305
+ count: totalIgnored,
5306
+ color: '#0284C7',
5307
+ isSplit: true,
5308
+ isExpanded: expanded.ignored,
5309
+ coveredPct: ignCovPct,
5310
+ coveredCount: ignoredCovered,
5311
+ coveredColor: '#0284C7',
5312
+ coveredKey: 'Ignored (Covered)',
5313
+ skippedPct: ignSkpPct,
5314
+ skippedCount: ignoredSkipped,
5315
+ skippedColor: '#38BDF8',
5316
+ skippedKey: 'Ignored (Skipped)'
3267
5317
  },
3268
- labels: [type.DNT, type.IGNORED, type.USER_INPUTS],
3269
- tooltip: {
3270
- theme: 'dark',
3271
- y: {
3272
- formatter: function (v, { config, dataPointIndex }) {
3273
- let count = 0,
3274
- value = Number(
3275
- (v /
3276
- config.series.reduce((acc, curr) => acc + curr, 0)) *
3277
- 100
3278
- ).toFixed(0);
3279
- value = isNaN(value) || !isFinite(value) ? 0 : value;
3280
-
3281
- switch (dataPointIndex) {
3282
- case 0:
3283
- count = valueMap[type.DNT];
3284
- break;
3285
- case 1:
3286
- count = valueMap[type.IGNORED];
3287
- break;
3288
- case 2:
3289
- count = valueMap[type.USER_INPUTS];
3290
- break;
3291
- }
5318
+ {
5319
+ id: 'dnt',
5320
+ label: 'DNT',
5321
+ pct: dntPct,
5322
+ count: totalDnt,
5323
+ color: '#6366F1',
5324
+ isSplit: true,
5325
+ isExpanded: expanded.dnt,
5326
+ coveredPct: dntCovPct,
5327
+ coveredCount: dntCovered,
5328
+ coveredColor: '#6366F1',
5329
+ coveredKey: 'DNT (Covered)',
5330
+ skippedPct: dntSkpPct,
5331
+ skippedCount: dntSkipped,
5332
+ skippedColor: '#A5B4FC',
5333
+ skippedKey: 'DNT (Skipped)'
5334
+ },
5335
+ {
5336
+ id: 'userInput',
5337
+ label: 'User Inputs',
5338
+ pct: uinPct,
5339
+ count: totalUserInput,
5340
+ color: '#0D9488',
5341
+ isSplit: true,
5342
+ isExpanded: expanded.userInput,
5343
+ coveredPct: uinCovPct,
5344
+ coveredCount: userInputCovered,
5345
+ coveredColor: '#0D9488',
5346
+ coveredKey: 'User Inputs (Covered)',
5347
+ skippedPct: uinSkpPct,
5348
+ skippedCount: userInputSkipped,
5349
+ skippedColor: '#5EEAD4',
5350
+ skippedKey: 'User Inputs (Skipped)'
5351
+ }
5352
+ ];
3292
5353
 
3293
- return \`\${value}% (\${count} strings)\`;
3294
- },
3295
- title: {
3296
- formatter: function (a) {
3297
- return \`Total Number of \${a} strings:\`;
3298
- }
3299
- }
5354
+ let html = '';
5355
+
5356
+ // Total Coverage Mini Card at top of legend
5357
+ html += '<div class="total-coverage-card">';
5358
+ html += ' <div class="coverage-card-header">';
5359
+ html += ' <span class="coverage-card-title">Total Coverage</span>';
5360
+ html += ' <span class="coverage-card-pct">' + totalCoverage + '%</span>';
5361
+ html += ' </div>';
5362
+ html += ' <div class="coverage-card-bar">';
5363
+ html += ' <div class="coverage-card-fill" style="width: ' + totalCoverage + '%;"></div>';
5364
+ html += ' </div>';
5365
+ html += '</div>';
5366
+
5367
+ // Vertical list of legend items matching zLiterati design
5368
+ html += '<div class="string-legend-list">';
5369
+ legendItems.forEach(item => {
5370
+ if (item.isSplit) {
5371
+ const isCovDisabled = disabledSeries.has(item.coveredKey);
5372
+ const isSkpDisabled = disabledSeries.has(item.skippedKey);
5373
+ const isParentDisabled = disabledSeries.has(item.label) || (isCovDisabled && isSkpDisabled);
5374
+ const itemDesc = descMap[item.label] ? ' — ' + descMap[item.label] : '';
5375
+
5376
+ html += '<div class="legend-row-item is-interactive ' + (item.isExpanded ? 'is-open' : '') + ' ' + (isParentDisabled ? 'inactive' : '') + '" data-category="' + item.id + '">';
5377
+ html += ' <div class="interactive-btn-inner" title="' + item.label + ': ' + item.pct + '% (' + item.count + ' strings)' + itemDesc + '">';
5378
+ html += ' <span class="legend-circle-dot" style="background: ' + item.color + '"></span>';
5379
+ html += ' <span class="legend-row-label">' + item.label + ' <span class="legend-row-pct">(' + item.pct + '%)</span></span>';
5380
+ html += ' <svg class="chevron-icon ' + (item.isExpanded ? 'is-open' : '') + '" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"/></svg>';
5381
+ html += ' </div>';
5382
+
5383
+ if (item.isExpanded) {
5384
+ html += ' <div class="split-dropdown-menu">';
5385
+ html += ' <div class="dropdown-item ' + (isCovDisabled ? 'inactive' : '') + '" data-series="' + item.coveredKey + '" title="' + item.label + ' Covered: ' + item.coveredPct + '% (' + item.coveredCount + ' strings) — Covered portion of ' + item.label + '">';
5386
+ html += ' <div class="dropdown-item-left"><span class="dot" style="background:' + item.coveredColor + '"></span><span class="dropdown-item-label">Covered</span></div>';
5387
+ html += ' <span class="dropdown-item-val">' + item.coveredPct + '%</span>';
5388
+ html += ' </div>';
5389
+ html += ' <div class="dropdown-item ' + (isSkpDisabled ? 'inactive' : '') + '" data-series="' + item.skippedKey + '" title="' + item.label + ' Skipped: ' + item.skippedPct + '% (' + item.skippedCount + ' strings) — Skipped portion of ' + item.label + '">';
5390
+ html += ' <div class="dropdown-item-left"><span class="dot" style="background:' + item.skippedColor + '"></span><span class="dropdown-item-label">Skipped</span></div>';
5391
+ html += ' <span class="dropdown-item-val">' + item.skippedPct + '%</span>';
5392
+ html += ' </div>';
5393
+ html += ' </div>';
3300
5394
  }
3301
- },
3302
- colors: [
3303
- ({ seriesIndex, w }) => {
3304
- const currType = w.config.labels[seriesIndex];
3305
-
3306
- switch (currType) {
3307
- case type.DNT:
3308
- return '#31E994';
3309
- case type.IGNORED:
3310
- return '#3AD1F3';
3311
- case type.USER_INPUTS:
3312
- return '#DADF40';
5395
+ html += '</div>';
5396
+ } else {
5397
+ const isDisabled = disabledSeries.has(item.key);
5398
+ const itemDesc = descMap[item.key] ? ' — ' + descMap[item.key] : '';
5399
+ html += '<div class="legend-row-item is-clickable ' + (isDisabled ? 'inactive' : '') + '" data-series="' + item.key + '" title="' + item.label + ': ' + item.pct + '% (' + item.count + ' strings)' + itemDesc + '">';
5400
+ html += ' <span class="legend-circle-dot" style="background: ' + item.color + '"></span>';
5401
+ html += ' <span class="legend-row-label">' + item.label + ' <span class="legend-row-pct">(' + item.pct + '%)</span></span>';
5402
+ html += '</div>';
5403
+ }
5404
+ });
5405
+ html += '</div>';
3313
5406
 
3314
- default:
3315
- return '';
3316
- }
5407
+ legendContainer.innerHTML = html;
5408
+
5409
+ // Click listener for interactive buttons (Ignored, DNT, User Inputs)
5410
+ legendContainer.querySelectorAll('.interactive-btn-inner').forEach(innerEl => {
5411
+ innerEl.addEventListener('click', (e) => {
5412
+ e.stopPropagation();
5413
+ const row = innerEl.closest('.legend-row-item');
5414
+ const cat = row ? row.getAttribute('data-category') : null;
5415
+ if (cat) toggleCategory(cat);
5416
+ });
5417
+ });
5418
+
5419
+ // Click listener for subpart dropdown items
5420
+ legendContainer.querySelectorAll('.dropdown-item').forEach(itemEl => {
5421
+ itemEl.addEventListener('click', (e) => {
5422
+ e.stopPropagation();
5423
+ const sName = itemEl.getAttribute('data-series');
5424
+ if (sName) {
5425
+ toggleSeries(sName);
3317
5426
  }
3318
- ],
3319
- noData: {
3320
- text: 'No metrics available',
3321
- align: 'center',
3322
- verticalAlign: 'middle',
3323
- offsetX: -70,
3324
- offsetY: -25,
3325
- style: {
3326
- fontSize: '14px'
5427
+ });
5428
+ });
5429
+
5430
+ // Click listener for core clickable legend items
5431
+ legendContainer.querySelectorAll('.legend-row-item.is-clickable').forEach(itemEl => {
5432
+ itemEl.addEventListener('click', () => {
5433
+ const seriesName = itemEl.getAttribute('data-series');
5434
+ if (seriesName) {
5435
+ toggleSeries(seriesName);
3327
5436
  }
3328
- },
3329
- legend: {
3330
- offsetX: -40,
3331
- formatter: (seriesName) => {
3332
- let value = ((valueMap[seriesName] / total) * 100).toFixed(0);
3333
- value = isNaN(value) || !isFinite(value) ? 0 : value;
3334
- return \`\${seriesName} (\${value}%)\`;
5437
+ });
5438
+ });
5439
+ }
5440
+
5441
+ // Click outside handler to close dropdowns
5442
+ document.addEventListener('click', (e) => {
5443
+ if (!e.target.closest('.legend-row-item.is-interactive') && !e.target.closest('.string-quality .graph')) {
5444
+ let hasOpen = false;
5445
+ ['ignored', 'dnt', 'userInput'].forEach(k => {
5446
+ if (expanded[k]) {
5447
+ expanded[k] = false;
5448
+ hasOpen = true;
3335
5449
  }
3336
- },
3337
- dataLabels: {
3338
- formatter: (v) => \`\${Number(v).toFixed(0)}%\`,
3339
- style: {
3340
- colors: ['#080404b0']
3341
- },
3342
- dropShadow: { enabled: false }
5450
+ });
5451
+ if (hasOpen) {
5452
+ updateChart();
5453
+ renderLegend();
3343
5454
  }
3344
- };
5455
+ }
5456
+ });
3345
5457
 
3346
- (ignoredChart = new ApexCharts(
3347
- document.querySelector('.skipped-items .graph'),
3348
- options
3349
- )).render();
5458
+ renderLegend();
3350
5459
  }
3351
5460
 
3352
5461
  function renderNumberChart() {
3353
- const overallStats = DataStore.getOverallStats();
3354
- const types = ['Number', 'Currency', 'Percentage'],
5462
+ const overallStats = DataStore.getOverallStats() || {};
5463
+ const types = ['Numeric', 'Currency', 'Percentage'],
3355
5464
  data = {
3356
- number_i18n: overallStats.localizedNumber,
3357
- currency_i18n: overallStats.localizedCurrency,
3358
- percentage_i18n: overallStats.localizedPercentage,
3359
- number_not_i18n: overallStats.notLocalizedNumber,
3360
- currency_not_i18n: overallStats.notLocalizedCurrency,
3361
- percentage_not_i18n: overallStats.notLocalizedPercentage
5465
+ numeric_i18n: overallStats.localizedNumber || 0,
5466
+ currency_i18n: overallStats.localizedCurrency || 0,
5467
+ percentage_i18n: overallStats.localizedPercentage || 0,
5468
+ numeric_not_i18n: overallStats.notLocalizedNumber || 0,
5469
+ currency_not_i18n: overallStats.notLocalizedCurrency || 0,
5470
+ percentage_not_i18n: overallStats.notLocalizedPercentage || 0
3362
5471
  },
3363
- total = Object.values(data).reduce((acc, curr) => acc + curr, 0),
3364
5472
  percentageCalc = (v, t) => {
3365
- const val = ((v / t) * 100).toFixed(0);
5473
+ if (!t || t === 0) return 0;
5474
+ const val = Math.round((v / t) * 100);
3366
5475
  return isNaN(val) || !isFinite(val) ? 0 : val;
3367
5476
  },
5477
+ total = Object.values(data).reduce((acc, curr) => acc + curr, 0),
3368
5478
  i18nPercentage = percentageCalc(
3369
- data.number_i18n + data.percentage_i18n + data.currency_i18n,
5479
+ data.numeric_i18n + data.percentage_i18n + data.currency_i18n,
3370
5480
  total
3371
5481
  ),
3372
5482
  nonI18nPercentage = percentageCalc(
3373
- data.number_not_i18n +
5483
+ data.numeric_not_i18n +
3374
5484
  data.percentage_not_i18n +
3375
5485
  data.currency_not_i18n,
3376
5486
  total
3377
5487
  ),
3378
- numberTotal = data.number_i18n + data.number_not_i18n,
5488
+ numericTotal = data.numeric_i18n + data.numeric_not_i18n,
3379
5489
  currencyTotal = data.currency_i18n + data.currency_not_i18n,
3380
5490
  percentageTotal = data.percentage_i18n + data.percentage_not_i18n,
3381
- numberI18n = percentageCalc(data.number_i18n, numberTotal),
5491
+ numericI18n = percentageCalc(data.numeric_i18n, numericTotal),
3382
5492
  currencyI18n = percentageCalc(data.currency_i18n, currencyTotal),
3383
- percentageI18n = percentageCalc(
3384
- data.percentage_i18n,
3385
- currencyTotal
3386
- ),
3387
- numberNotI18n = percentageCalc(data.number_not_i18n, numberTotal),
3388
- currencyNotI18n = percentageCalc(
3389
- data.currency_not_i18n,
3390
- currencyTotal
3391
- ),
3392
- percentageNotI18n = percentageCalc(
3393
- data.percentage_not_i18n,
3394
- currencyTotal
3395
- ),
5493
+ percentageI18n = percentageCalc(data.percentage_i18n, percentageTotal),
5494
+ numericNotI18n = percentageCalc(data.numeric_not_i18n, numericTotal),
5495
+ currencyNotI18n = percentageCalc(data.currency_not_i18n, currencyTotal),
5496
+ percentageNotI18n = percentageCalc(data.percentage_not_i18n, percentageTotal),
3396
5497
  options = {
3397
5498
  series: [
3398
5499
  {
3399
5500
  name: 'Internationalized',
3400
- data: [numberI18n, currencyI18n, percentageI18n]
5501
+ data: [numericI18n, currencyI18n, percentageI18n]
3401
5502
  },
3402
5503
  {
3403
5504
  name: 'Not Internationalized',
3404
- data: [numberNotI18n, currencyNotI18n, percentageNotI18n]
5505
+ data: [numericNotI18n, currencyNotI18n, percentageNotI18n]
3405
5506
  }
3406
5507
  ],
3407
5508
  chart: {
3408
5509
  type: 'bar',
3409
- height: 250,
3410
- width: 550,
5510
+ height: 235,
5511
+ width: '100%',
3411
5512
  toolbar: {
3412
5513
  show: false
3413
5514
  },
3414
5515
  animations: {
3415
5516
  enabled: true,
3416
5517
  easing: 'easeinout',
3417
- speed: 800,
3418
- animateGradually: {
3419
- enabled: true,
3420
- delay: 150
3421
- },
3422
- dynamicAnimation: {
3423
- enabled: true,
3424
- speed: 350
5518
+ speed: 600
5519
+ }
5520
+ },
5521
+ plotOptions: {
5522
+ bar: {
5523
+ borderRadius: 3,
5524
+ columnWidth: '48%',
5525
+ dataLabels: {
5526
+ position: 'top'
3425
5527
  }
3426
5528
  }
3427
5529
  },
3428
5530
  dataLabels: {
3429
5531
  enabled: true,
5532
+ offsetY: -14,
3430
5533
  dropShadow: { enabled: false },
3431
5534
  style: {
3432
- colors: ['#080404b0']
5535
+ fontSize: '10px',
5536
+ fontWeight: 600,
5537
+ colors: ['var(--foreground, #334155)']
3433
5538
  },
3434
- formatter: (val) => {
3435
- return \`\${val}%\`;
5539
+ formatter: (val, opt) => {
5540
+ const totals = [numericTotal, currencyTotal, percentageTotal];
5541
+ if (totals[opt.dataPointIndex] === 0) return '';
5542
+ return val + '%';
3436
5543
  }
3437
5544
  },
3438
5545
  stroke: {
@@ -3441,102 +5548,117 @@ const overview = () => `//overview content
3441
5548
  colors: ['#fff']
3442
5549
  },
3443
5550
  tooltip: {
3444
- theme: 'dark',
3445
- shared: true,
3446
- intersect: false,
3447
- x: {
3448
- formatter: (type) => \`\${type} instances:\`
3449
- },
3450
- y: {
3451
- formatter: (v, { seriesIndex, dataPointIndex }) => {
3452
- const getI18nCount = (type) => {
3453
- switch (type) {
3454
- case 'number':
3455
- return data.number_i18n;
3456
- case 'currency':
3457
- return data.currency_i18n;
3458
- case 'percentage':
3459
- return data.percentage_i18n;
3460
- }
3461
- },
3462
- getNonI18nCount = (type) => {
3463
- switch (type) {
3464
- case 'number':
3465
- return data.number_not_i18n;
3466
- case 'currency':
3467
- return data.currency_not_i18n;
3468
- case 'percentage':
3469
- return data.percentage_not_i18n;
3470
- }
3471
- },
3472
- getCount = (type) => {
3473
- return seriesIndex === 0
3474
- ? getI18nCount(type)
3475
- : getNonI18nCount(type);
3476
- };
3477
-
3478
- switch (dataPointIndex) {
3479
- case 0:
3480
- return getCount('number');
3481
- case 1:
3482
- return getCount('currency');
3483
- case 2:
3484
- return getCount('percentage');
3485
- }
3486
- }
5551
+ shared: false,
5552
+ intersect: true,
5553
+ followCursor: true,
5554
+ custom: function({ series, seriesIndex, dataPointIndex, w }) {
5555
+ const seriesName = w.config.series[seriesIndex].name;
5556
+ const categoryName = w.config.xaxis.categories[dataPointIndex];
5557
+ const color = w.config.colors[seriesIndex];
5558
+ const stats = [
5559
+ { i18n: data.numeric_i18n, not_i18n: data.numeric_not_i18n, total: numericTotal },
5560
+ { i18n: data.currency_i18n, not_i18n: data.currency_not_i18n, total: currencyTotal },
5561
+ { i18n: data.percentage_i18n, not_i18n: data.percentage_not_i18n, total: percentageTotal }
5562
+ ];
5563
+ const stat = stats[dataPointIndex] || { i18n: 0, not_i18n: 0, total: 0 };
5564
+ const count = seriesIndex === 0 ? stat.i18n : stat.not_i18n;
5565
+ const val = series[seriesIndex][dataPointIndex] || 0;
5566
+ return (
5567
+ '<div class="bar-tooltip-custom">' +
5568
+ '<span class="bar-tooltip-dot" style="background:' + color + '"></span>' +
5569
+ '<span class="bar-tooltip-label">' + categoryName + ' (' + seriesName + '):</span> ' +
5570
+ '<span class="bar-tooltip-val">' + val + '% (' + count + ' of ' + stat.total + ')</span>' +
5571
+ '</div>'
5572
+ );
3487
5573
  }
3488
5574
  },
3489
5575
  xaxis: {
3490
- categories: types
5576
+ categories: types,
5577
+ labels: {
5578
+ rotate: 0,
5579
+ rotateAlways: false,
5580
+ hideOverlappingLabels: false,
5581
+ style: {
5582
+ fontSize: '11px',
5583
+ fontFamily: 'inherit',
5584
+ fontWeight: 500,
5585
+ colors: 'var(--foreground, #334155)'
5586
+ }
5587
+ }
3491
5588
  },
3492
5589
  yaxis: {
3493
5590
  min: 0,
3494
5591
  max: 100,
5592
+ tickAmount: 5,
3495
5593
  labels: {
3496
- formatter: (v) => \`\${v}%\`
5594
+ formatter: (v) => v + '%',
5595
+ style: {
5596
+ fontSize: '10px',
5597
+ fontFamily: 'inherit',
5598
+ colors: 'var(--text-muted, #64748b)'
5599
+ }
3497
5600
  }
3498
5601
  },
3499
- colors: ['#31E994', '#ff8173'],
5602
+ colors: ['#5B6EE1', '#EF8D32'],
3500
5603
  legend: {
3501
- offsetX: -13,
3502
- position: 'right',
5604
+ position: 'top',
5605
+ horizontalAlign: 'center',
5606
+ offsetY: 0,
5607
+ fontSize: '11px',
5608
+ labels: {
5609
+ colors: 'var(--foreground, #334155)'
5610
+ },
5611
+ markers: {
5612
+ width: 9,
5613
+ height: 9,
5614
+ radius: 2
5615
+ },
5616
+ itemMargin: {
5617
+ horizontal: 6,
5618
+ vertical: 2
5619
+ },
5620
+ onItemClick: {
5621
+ toggleDataSeries: true
5622
+ },
5623
+ onItemHover: {
5624
+ highlightDataSeries: true
5625
+ },
3503
5626
  formatter: (seriesName) => {
3504
5627
  if (seriesName === 'Internationalized') {
3505
- return \`\${seriesName} (\${i18nPercentage}%)\`;
5628
+ return seriesName + ' (' + i18nPercentage + '%)';
3506
5629
  }
3507
- return \`\${seriesName} (\${nonI18nPercentage}%)\`;
3508
- }
3509
- },
3510
- plotOptions: {
3511
- bar: {
3512
- borderRadius: 3,
3513
- columnWidth: '85%'
5630
+ return seriesName + ' (' + nonI18nPercentage + '%)';
3514
5631
  }
3515
5632
  }
3516
5633
  };
3517
5634
 
3518
- (numberChart = new ApexCharts(
3519
- document.querySelector('.number-quality .graph'),
3520
- options
3521
- )).render();
5635
+ const chartEl = document.querySelector('.numeric-graph') || document.querySelector('.number-quality .graph');
5636
+ if (chartEl) {
5637
+ if (numberChart) {
5638
+ try { numberChart.destroy(); } catch(e) {}
5639
+ }
5640
+ chartEl.innerHTML = '';
5641
+ (numberChart = new ApexCharts(chartEl, options)).render();
5642
+ }
3522
5643
  }
3523
5644
 
3524
5645
  function renderDatetimeChart() {
3525
- const overallStats = DataStore.getOverallStats();
5646
+ const overallStats = DataStore.getOverallStats() || {};
3526
5647
  const types = ['Date', 'Time', 'DateTime'],
3527
5648
  data = {
3528
- date_i18n: overallStats.localizedDate,
3529
- time_i18n: overallStats.localizedTime,
3530
- date_time_i18n: overallStats.localizedDateTime,
3531
- date_not_i18n: overallStats.notLocalizedDate,
3532
- time_not_i18n: overallStats.notLocalizedTime,
3533
- date_time_not_i18n: overallStats.notLocalizedDateTime
5649
+ date_i18n: overallStats.localizedDate || 0,
5650
+ time_i18n: overallStats.localizedTime || 0,
5651
+ date_time_i18n: overallStats.localizedDateTime || 0,
5652
+ date_not_i18n: overallStats.notLocalizedDate || 0,
5653
+ time_not_i18n: overallStats.notLocalizedTime || 0,
5654
+ date_time_not_i18n: overallStats.notLocalizedDateTime || 0
3534
5655
  },
3535
- total = Object.values(data).reduce((acc, curr) => acc + curr, 0),
3536
5656
  percentageCalc = (v, t) => {
3537
- const val = ((v / t) * 100).toFixed(0);
5657
+ if (!t || t === 0) return 0;
5658
+ const val = Math.round((v / t) * 100);
3538
5659
  return isNaN(val) || !isFinite(val) ? 0 : val;
3539
5660
  },
5661
+ total = Object.values(data).reduce((acc, curr) => acc + curr, 0),
3540
5662
  i18nPercentage = percentageCalc(
3541
5663
  data.date_i18n + data.time_i18n + data.date_time_i18n,
3542
5664
  total
@@ -3553,10 +5675,7 @@ const overview = () => `//overview content
3553
5675
  dateTimeI18n = percentageCalc(data.date_time_i18n, dateTimeTotal),
3554
5676
  dateNotI18n = percentageCalc(data.date_not_i18n, dateTotal),
3555
5677
  timeNotI18n = percentageCalc(data.time_not_i18n, timeTotal),
3556
- dateTimeNotI18n = percentageCalc(
3557
- data.date_time_not_i18n,
3558
- dateTimeTotal
3559
- ),
5678
+ dateTimeNotI18n = percentageCalc(data.date_time_not_i18n, dateTimeTotal),
3560
5679
  options = {
3561
5680
  series: [
3562
5681
  {
@@ -3570,33 +5689,39 @@ const overview = () => `//overview content
3570
5689
  ],
3571
5690
  chart: {
3572
5691
  type: 'bar',
3573
- height: 250,
3574
- width: 550,
5692
+ height: 235,
5693
+ width: '100%',
3575
5694
  toolbar: {
3576
5695
  show: false
3577
5696
  },
3578
5697
  animations: {
3579
5698
  enabled: true,
3580
5699
  easing: 'easeinout',
3581
- speed: 800,
3582
- animateGradually: {
3583
- enabled: true,
3584
- delay: 150
3585
- },
3586
- dynamicAnimation: {
3587
- enabled: true,
3588
- speed: 350
5700
+ speed: 600
5701
+ }
5702
+ },
5703
+ plotOptions: {
5704
+ bar: {
5705
+ borderRadius: 3,
5706
+ columnWidth: '48%',
5707
+ dataLabels: {
5708
+ position: 'top'
3589
5709
  }
3590
5710
  }
3591
5711
  },
3592
5712
  dataLabels: {
3593
5713
  enabled: true,
5714
+ offsetY: -14,
3594
5715
  dropShadow: { enabled: false },
3595
5716
  style: {
3596
- colors: ['#080404b0']
5717
+ fontSize: '10px',
5718
+ fontWeight: 600,
5719
+ colors: ['var(--foreground, #334155)']
3597
5720
  },
3598
- formatter: (val) => {
3599
- return \`\${val}%\`;
5721
+ formatter: (val, opt) => {
5722
+ const totals = [dateTotal, timeTotal, dateTimeTotal];
5723
+ if (totals[opt.dataPointIndex] === 0) return '';
5724
+ return val + '%';
3600
5725
  }
3601
5726
  },
3602
5727
  stroke: {
@@ -3605,340 +5730,439 @@ const overview = () => `//overview content
3605
5730
  colors: ['#fff']
3606
5731
  },
3607
5732
  tooltip: {
3608
- theme: 'dark',
3609
- shared: true,
3610
- intersect: false,
3611
- x: {
3612
- formatter: (type) => \`\${type} instances:\`
3613
- },
3614
- y: {
3615
- formatter: (v, { seriesIndex, dataPointIndex }) => {
3616
- const getI18nCount = (type) => {
3617
- switch (type) {
3618
- case 'date':
3619
- return data.date_i18n;
3620
- case 'time':
3621
- return data.time_i18n;
3622
- case 'date_time':
3623
- return data.date_time_i18n;
3624
- }
3625
- },
3626
- getNonI18nCount = (type) => {
3627
- switch (type) {
3628
- case 'date':
3629
- return data.date_not_i18n;
3630
- case 'time':
3631
- return data.time_not_i18n;
3632
- case 'date_time':
3633
- return data.date_time_not_i18n;
3634
- }
3635
- },
3636
- getCount = (type) => {
3637
- return seriesIndex === 0
3638
- ? getI18nCount(type)
3639
- : getNonI18nCount(type);
3640
- };
3641
-
3642
- switch (dataPointIndex) {
3643
- case 0:
3644
- return getCount('date');
3645
- case 1:
3646
- return getCount('time');
3647
- case 2:
3648
- return getCount('date_time');
3649
- }
3650
- }
5733
+ shared: false,
5734
+ intersect: true,
5735
+ followCursor: true,
5736
+ custom: function({ series, seriesIndex, dataPointIndex, w }) {
5737
+ const seriesName = w.config.series[seriesIndex].name;
5738
+ const categoryName = w.config.xaxis.categories[dataPointIndex];
5739
+ const color = w.config.colors[seriesIndex];
5740
+ const stats = [
5741
+ { i18n: data.date_i18n, not_i18n: data.date_not_i18n, total: dateTotal },
5742
+ { i18n: data.time_i18n, not_i18n: data.time_not_i18n, total: timeTotal },
5743
+ { i18n: data.date_time_i18n, not_i18n: data.date_time_not_i18n, total: dateTimeTotal }
5744
+ ];
5745
+ const stat = stats[dataPointIndex] || { i18n: 0, not_i18n: 0, total: 0 };
5746
+ const count = seriesIndex === 0 ? stat.i18n : stat.not_i18n;
5747
+ const val = series[seriesIndex][dataPointIndex] || 0;
5748
+ return (
5749
+ '<div class="bar-tooltip-custom">' +
5750
+ '<span class="bar-tooltip-dot" style="background:' + color + '"></span>' +
5751
+ '<span class="bar-tooltip-label">' + categoryName + ' (' + seriesName + '):</span> ' +
5752
+ '<span class="bar-tooltip-val">' + val + '% (' + count + ' of ' + stat.total + ')</span>' +
5753
+ '</div>'
5754
+ );
3651
5755
  }
3652
5756
  },
3653
5757
  xaxis: {
3654
- categories: types
5758
+ categories: types,
5759
+ labels: {
5760
+ rotate: 0,
5761
+ rotateAlways: false,
5762
+ hideOverlappingLabels: false,
5763
+ style: {
5764
+ fontSize: '11px',
5765
+ fontFamily: 'inherit',
5766
+ fontWeight: 500,
5767
+ colors: 'var(--foreground, #334155)'
5768
+ }
5769
+ }
3655
5770
  },
3656
5771
  yaxis: {
3657
5772
  min: 0,
3658
5773
  max: 100,
5774
+ tickAmount: 5,
3659
5775
  labels: {
3660
- formatter: (v) => \`\${v}%\`
5776
+ formatter: (v) => v + '%',
5777
+ style: {
5778
+ fontSize: '10px',
5779
+ fontFamily: 'inherit',
5780
+ colors: 'var(--text-muted, #64748b)'
5781
+ }
3661
5782
  }
3662
5783
  },
3663
- colors: ['#31E994', '#ff8173'],
5784
+ colors: ['#5B6EE1', '#EF8D32'],
3664
5785
  legend: {
3665
- offsetX: 4,
3666
- position: 'right',
5786
+ position: 'top',
5787
+ horizontalAlign: 'center',
5788
+ offsetY: 0,
5789
+ fontSize: '11px',
5790
+ labels: {
5791
+ colors: 'var(--foreground, #334155)'
5792
+ },
5793
+ markers: {
5794
+ width: 9,
5795
+ height: 9,
5796
+ radius: 2
5797
+ },
5798
+ itemMargin: {
5799
+ horizontal: 6,
5800
+ vertical: 2
5801
+ },
5802
+ onItemClick: {
5803
+ toggleDataSeries: true
5804
+ },
5805
+ onItemHover: {
5806
+ highlightDataSeries: true
5807
+ },
3667
5808
  formatter: (seriesName) => {
3668
5809
  if (seriesName === 'Internationalized') {
3669
- return \`\${seriesName} (\${i18nPercentage}%)\`;
5810
+ return seriesName + ' (' + i18nPercentage + '%)';
3670
5811
  }
3671
- return \`\${seriesName} (\${nonI18nPercentage}%)\`;
3672
- }
3673
- },
3674
- plotOptions: {
3675
- bar: {
3676
- borderRadius: 3,
3677
- columnWidth: '85%'
5812
+ return seriesName + ' (' + nonI18nPercentage + '%)';
3678
5813
  }
3679
5814
  }
3680
5815
  };
3681
5816
 
3682
- (dateTimeChart = new ApexCharts(
3683
- document.querySelector('.datetime-quality .graph'),
3684
- options
3685
- )).render();
5817
+ const chartEl = document.querySelector('.datetime-graph') || document.querySelector('.datetime-quality .graph');
5818
+ if (chartEl) {
5819
+ if (dateTimeChart) {
5820
+ try { dateTimeChart.destroy(); } catch(e) {}
5821
+ }
5822
+ chartEl.innerHTML = '';
5823
+ (dateTimeChart = new ApexCharts(chartEl, options)).render();
5824
+ }
5825
+ }
5826
+
5827
+ function renderFormatChart() {
5828
+ renderNumberChart();
5829
+ renderDatetimeChart();
3686
5830
  }
3687
5831
  }
3688
5832
  });
3689
5833
  })();`;
3690
5834
  const insight = () => `// insights content
3691
- (() => {
3692
- if (!DataStore.ranSuccessfully()) {
3693
- document.getElementById('overview').innerHTML =
3694
- 'Unable to prepare zQA automation report for the project.';
3695
- return;
3696
- }
5835
+ (() => {
5836
+ if (!DataStore.ranSuccessfully() || DataStore.isSanityRun()) {
5837
+ if (DataStore.isSanityRun()) return;
5838
+ var ov = document.getElementById('overview');
5839
+ if (ov) ov.innerHTML = 'Unable to prepare i18n automation report for the project.';
5840
+ return;
5841
+ }
3697
5842
 
3698
- document.addEventListener('DOMContentLoaded', function () {
3699
- renderCards();
3700
-
3701
- // helper methods to generate cards and its sections
3702
- function createStatSection(locale, data) {
3703
- const holder = document.createElement('div');
3704
- const table = document.createElement('table');
3705
- const tbody = document.createElement('tbody');
3706
- const name = document.createElement('div');
3707
- const addRow = (name, value) => {
3708
- const row = document.createElement('tr');
3709
-
3710
- const col1 = document.createElement('td');
3711
- col1.classList.add('name');
3712
- col1.textContent = name;
3713
-
3714
- const col2 = document.createElement('td');
3715
- col2.classList.add('value');
3716
- col2.textContent = value;
3717
-
3718
- row.appendChild(col1);
3719
- row.appendChild(col2);
3720
- return row;
3721
- };
5843
+ var METRIC_ROWS = [
5844
+ { label: 'Internationlized Strings', key: 'localizedStrings', type: 'STRING_LOCALIZED', category: 'Strings', color: 'green' },
5845
+ { label: 'Not Internationlized Strings', key: 'notLocalizedStrings', type: 'STRING_NOT_LOCALIZED', category: 'Strings', color: 'red', isDefect: true },
5846
+ { label: 'Partially Localized Strings', key: 'partialStrings', type: 'STRING_PARTIALLY_LOCALIZED', category: 'Strings', color: 'yellow', isWarning: true },
5847
+ { label: 'Internationalized Numeric', key: 'localizedNumber', type: 'NUMBER_LOCALIZED', category: 'Numeric', color: 'green' },
5848
+ { label: 'Not Internationalized Numeric', key: 'notLocalizedNumber', type: 'NUMBER_NOT_LOCALIZED', category: 'Numeric', color: 'red', isDefect: true },
5849
+ { label: 'Internationlized Currency', key: 'localizedCurrency', type: 'CURRENCY_LOCALIZED', category: 'Currency', color: 'green' },
5850
+ { label: 'Not Internationlized Currency', key: 'notLocalizedCurrency', type: 'CURRENCY_NOT_LOCALIZED', category: 'Currency', color: 'red', isDefect: true },
5851
+ { label: 'Internationlized Percentage', key: 'localizedPercentage', type: 'PERCENTAGE_LOCALIZED', category: 'Percentage', color: 'green' },
5852
+ { label: 'Not Internationlized Percentage', key: 'notLocalizedPercentage', type: 'PERCENTAGE_NOT_LOCALIZED', category: 'Percentage', color: 'red', isDefect: true },
5853
+ { label: 'Internationlized Date', key: 'localizedDate', type: 'DATE_LOCALIZED', category: 'Date', color: 'green' },
5854
+ { label: 'Not Internationlized Date', key: 'notLocalizedDate', type: 'DATE_NOT_LOCALIZED', category: 'Date', color: 'red', isDefect: true },
5855
+ { label: 'Internationlized Time', key: 'localizedTime', type: 'TIME_LOCALIZED', category: 'Time', color: 'green' },
5856
+ { label: 'Not Internationlized Time', key: 'notLocalizedTime', type: 'TIME_NOT_LOCALIZED', category: 'Time', color: 'red', isDefect: true },
5857
+ { label: 'Internationlized DateTime', key: 'localizedDateTime', type: 'DATE_TIME_LOCALIZED', category: 'DateTime', color: 'green' },
5858
+ { label: 'Not Internationlized DateTime', key: 'notLocalizedDateTime', type: 'DATE_TIME_NOT_LOCALIZED', category: 'DateTime', color: 'red', isDefect: true },
5859
+ { label: 'Encoding Issue Strings', key: 'encoding', type: 'ENCODING_ISSUE', category: 'Defects', color: 'cyan', isDefect: true },
5860
+ { label: 'DNT Strings', key: 'dnt', type: 'DNT', category: 'Skipped', color: 'muted' },
5861
+ { label: 'Ignored Strings', key: 'ignored', type: 'IGNORE', category: 'Skipped', color: 'muted' },
5862
+ { label: 'Untranslated Strings', key: 'untranslatedStrings', type: 'UNTRANSLATED', category: 'Defects', color: 'pink', isDefect: true },
5863
+ { label: 'Auto Ignored Strings', key: 'autoIgnored', type: 'AUTO_IGNORE', category: 'Skipped', color: 'muted' },
5864
+ { label: 'User Input Strings', key: 'userInput', type: 'USER_INPUT', category: 'Skipped', color: 'muted' },
5865
+ { label: 'Total Reviewed', key: 'reviewed', type: 'REVIEWED', category: 'Summary', color: 'blue', isTotal: true }
5866
+ ];
5867
+
5868
+ window.galleryItems = [];
5869
+ window.galleryIndex = 0;
5870
+
5871
+ function renderGalleryItem(index) {
5872
+ if (!window.galleryItems || !window.galleryItems[index]) return;
5873
+ window.galleryIndex = index;
5874
+ var item = window.galleryItems[index];
5875
+
5876
+ var img = document.getElementById('galleryMainImg');
5877
+ if (img) {
5878
+ var rawPath = (item.imagePath || '').replace(/^[\\/]+/, '');
5879
+ img.src = window.getProductUrl() + '/' + rawPath;
5880
+ }
3722
5881
 
3723
- if (DataStore.isSanityRun()) {
3724
- tbody.appendChild(addRow('Pages viewed', data.reviewed));
3725
- }
3726
- else {
3727
- const tableData = {
3728
- 'Internationlized Strings':
3729
- data.localizedStrings +
3730
- data.untranslatedStrings +
3731
- data.partialStrings,
3732
- 'Not Internationlized Strings': data.notLocalizedStrings,
3733
- 'Partially Localized Strings': data.partialStrings,
3734
- 'Internationlized Numbers': data.localizedNumber,
3735
- 'Not Internationlized Numbers': data.notLocalizedNumber,
3736
- 'Internationlized Currency': data.localizedCurrency,
3737
- 'Not Internationlized Currency': data.notLocalizedCurrency,
3738
- 'Internationlized Percentage': data.localizedPercentage,
3739
- 'Not Internationlized Percentage': data.notLocalizedPercentage,
3740
- 'Internationlized Date': data.localizedDate,
3741
- 'Not Internationlized Date': data.notLocalizedDate,
3742
- 'Internationlized Time': data.localizedTime,
3743
- 'Not Internationlized Time': data.notLocalizedTime,
3744
- 'Internationlized DateTime': data.localizedDate,
3745
- 'Not Internationlized DateTime': data.notLocalizedDateTime,
3746
- 'Encoding Issue Strings': data.encoding,
3747
- 'DNT Strings': data.dnt,
3748
- 'Ignored Strings': data.ignored,
3749
- 'Untranslated Strings': data.untranslatedStrings,
3750
- 'Auto Ignored Strings': data.autoIgnored,
3751
- 'User Input Strings': data.userInput,
3752
- 'Total Reviewed': data.reviewed
3753
- };
5882
+ var counter = document.getElementById('galleryCounter');
5883
+ if (counter) counter.textContent = 'Item ' + (index + 1) + ' of ' + window.galleryItems.length;
3754
5884
 
3755
- for (const rowData of Object.entries(tableData)) {
3756
- if (!DataStore.isSanityRun() || !DataStore.isDefaultLocale(locale) || rowData[0] !== 'Untranslated Strings') {
3757
- tbody.appendChild(addRow(rowData[0], rowData[1]));
3758
- }
3759
- }
3760
- }
5885
+ var pageBadge = document.getElementById('galleryPageBadge');
5886
+ if (pageBadge) pageBadge.textContent = 'Locale: ' + item.locale + ' | Page: ' + item.page;
5887
+
5888
+ var textVal = document.getElementById('galleryTextVal');
5889
+ if (textVal) textVal.textContent = item.string || '-';
3761
5890
 
3762
- holder.classList.add('stat');
3763
- name.classList.add('name');
3764
- name.textContent = 'Summary';
3765
- holder.append(name);
5891
+ var prevBtn = document.getElementById('galleryPrevBtn');
5892
+ var nextBtn = document.getElementById('galleryNextBtn');
5893
+ if (prevBtn) prevBtn.disabled = index === 0;
5894
+ if (nextBtn) nextBtn.disabled = index === window.galleryItems.length - 1;
5895
+
5896
+ var thumbs = document.querySelectorAll('.gallery-thumb-btn');
5897
+ thumbs.forEach(function(t, i) {
5898
+ if (i === index) {
5899
+ t.classList.add('active');
5900
+ try { t.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); } catch(e){}
5901
+ } else {
5902
+ t.classList.remove('active');
5903
+ }
5904
+ });
3766
5905
 
3767
- table.appendChild(tbody);
3768
- holder.appendChild(table);
3769
- return holder;
5906
+ var fullBtn = document.getElementById('galleryFullscreenBtn');
5907
+ if (fullBtn) {
5908
+ fullBtn.onclick = function() {
5909
+ if (item.imagePath && typeof window.openImageViewer === 'function') {
5910
+ window.openImageViewer(item.imagePath);
3770
5911
  }
5912
+ };
5913
+ }
5914
+ }
3771
5915
 
3772
- function createStatusBarSection(locale, data) {
3773
- const holder = document.createElement('div');
3774
- holder.classList.add('status-bar');
3775
-
3776
- const generateBarEl = (name, total, current) => {
3777
- const getColorClass = (value) => {
3778
- if (value <= 25) {
3779
- return 'red';
3780
- } else if (value <= 50) {
3781
- return 'yellow';
3782
- } else if (value <= 75) {
3783
- return 'blue';
3784
- } else if (value <= 100) {
3785
- return 'green';
3786
- }
3787
- };
3788
- current = (current / total) * 100;
3789
- current = isNaN(current) || !isFinite(current) ? 0 : current;
3790
- const holder = document.createElement('div');
3791
- holder.classList.add('bar-container');
5916
+ function openScreenshotGallery(locale, rowType, rowLabel) {
5917
+ var localesToSearch = locale === 'ALL' ? DataStore.getLocales() : [locale];
5918
+ var items = [];
5919
+ var seenKeys = {};
5920
+
5921
+ localesToSearch.forEach(function(loc) {
5922
+ var data = DataStore.getDataByLocale(loc) || {};
5923
+ Object.keys(data).forEach(function(str) {
5924
+ var arr = data[str] || [];
5925
+ arr.forEach(function(entry) {
5926
+ var matchesType = (rowType === 'REVIEWED') ||
5927
+ (entry.type === rowType) ||
5928
+ (entry.type && entry.type.toUpperCase() === rowType.toUpperCase()) ||
5929
+ (rowType === 'IGNORE' && (entry.type === 'IGNORED' || entry.type === 'IGNORE')) ||
5930
+ (rowType === 'UNTRANSLATED' && (entry.type === 'UNTRANSLATED_STRINGS' || entry.type === 'UNTRANSLATED'));
5931
+
5932
+ if (matchesType) {
5933
+ var rawPath = entry.imagePath || '';
5934
+ if (rawPath && !rawPath.includes('/screenshots/')) {
5935
+ var sub = (entry.type === 'REVIEWED' || !entry.type) ? 'reviewed' : entry.type.toLowerCase();
5936
+ rawPath = '/' + loc + '/screenshots/' + sub + '/' + rawPath.replace(/^[/]+/, '');
5937
+ }
3792
5938
 
3793
- const nameHolder = document.createElement('div');
3794
- nameHolder.classList.add('name');
3795
- nameHolder.textContent = name;
5939
+ var itemKey = loc + '|' + str + '|' + rawPath;
5940
+ if (!seenKeys[itemKey]) {
5941
+ seenKeys[itemKey] = true;
5942
+ items.push({
5943
+ string: str,
5944
+ page: entry.description || 'Page',
5945
+ imagePath: rawPath,
5946
+ locale: loc,
5947
+ type: entry.type
5948
+ });
5949
+ }
5950
+ }
5951
+ });
5952
+ });
5953
+ });
3796
5954
 
3797
- const barHolder = document.createElement('div');
3798
- barHolder.classList.add('bar-holder');
5955
+ if (!items.length) {
5956
+ alert('No screenshots captured for ' + rowLabel + (locale !== 'ALL' ? ' (' + locale + ')' : ''));
5957
+ return;
5958
+ }
3799
5959
 
3800
- const bar = document.createElement('div');
3801
- bar.classList.add('bar');
3802
- bar.classList.add(\`--\${getColorClass(current)}\`);
3803
- bar.style.width = \`\${current}%\`;
3804
- bar.textContent = \`\${current.toFixed(0)}%\`;
5960
+ window.galleryItems = items;
5961
+ window.galleryIndex = 0;
5962
+
5963
+ var modal = document.getElementById('screenshotGalleryModal');
5964
+ if (modal) modal.style.display = 'block';
5965
+
5966
+ var titleEl = document.getElementById('galleryModalTitle');
5967
+ if (titleEl) titleEl.textContent = rowLabel + (locale !== 'ALL' ? ' - ' + locale : ' - All Locales');
5968
+
5969
+ var badgeEl = document.getElementById('galleryModalBadge');
5970
+ if (badgeEl) badgeEl.textContent = items.length + ' screenshot' + (items.length > 1 ? 's' : '');
5971
+
5972
+ var strip = document.getElementById('galleryThumbnailsStrip');
5973
+ if (strip) {
5974
+ strip.innerHTML = '';
5975
+ items.forEach(function(item, idx) {
5976
+ var thumb = document.createElement('button');
5977
+ thumb.type = 'button';
5978
+ thumb.className = 'gallery-thumb-btn' + (idx === 0 ? ' active' : '');
5979
+ thumb.title = item.string + ' (' + item.page + ')';
5980
+ thumb.innerHTML = '<span class="thumb-idx">' + (idx + 1) + '</span><span class="thumb-lbl">' + (item.page || item.locale) + '</span>';
5981
+ thumb.addEventListener('click', function() {
5982
+ renderGalleryItem(idx);
5983
+ });
5984
+ strip.appendChild(thumb);
5985
+ });
5986
+ }
3805
5987
 
3806
- barHolder.appendChild(bar);
3807
- holder.appendChild(nameHolder);
3808
- holder.appendChild(barHolder);
5988
+ renderGalleryItem(0);
5989
+ }
3809
5990
 
3810
- return holder;
3811
- };
5991
+ window.closeScreenshotGallery = function() {
5992
+ var modal = document.getElementById('screenshotGalleryModal');
5993
+ if (modal) modal.style.display = 'none';
5994
+ var img = document.getElementById('galleryMainImg');
5995
+ if (img) img.src = '';
5996
+ };
3812
5997
 
3813
- const showFull = DataStore.isSanityRun() || DataStore.isDefaultLocale(locale),
3814
- totalI18nCount = showFull
3815
- ? 100
3816
- : data.localizedStrings +
3817
- data.untranslatedStrings +
3818
- data.partialStrings +
3819
- data.notLocalizedStrings,
3820
- i18nStrings = showFull
3821
- ? 100
3822
- : data.localizedStrings +
3823
- data.untranslatedStrings +
3824
- data.partialStrings,
3825
- totalLocCount = showFull
3826
- ? 100
3827
- : totalI18nCount -
3828
- data.untranslatedStrings -
3829
- data.notLocalizedStrings,
3830
- totalNumberCount = showFull
3831
- ? 100
3832
- : data.localizedNumber +
3833
- data.localizedCurrency +
3834
- data.localizedPercentage +
3835
- data.notLocalizedPercentage +
3836
- data.notLocalizedCurrency +
3837
- data.notLocalizedNumber,
3838
- i18nNumbers = showFull
3839
- ? 100
3840
- : totalNumberCount -
3841
- data.notLocalizedPercentage -
3842
- data.notLocalizedCurrency -
3843
- data.notLocalizedNumber,
3844
- totalI18nDateTimeCount = showFull
3845
- ? 100
3846
- : data.localizedDate +
3847
- data.localizedDateTime +
3848
- data.localizedTime +
3849
- data.notLocalizedDate +
3850
- data.notLocalizedDateTime +
3851
- data.notLocalizedTime,
3852
- i18nDateTime = showFull
3853
- ? 100
3854
- : totalI18nDateTimeCount -
3855
- data.notLocalizedDate -
3856
- data.notLocalizedDateTime -
3857
- data.notLocalizedTime;
3858
-
3859
- holder.appendChild(
3860
- generateBarEl('String I18n level', totalI18nCount, i18nStrings)
3861
- );
5998
+ document.addEventListener('DOMContentLoaded', function () {
5999
+ var closeBtn = document.querySelector('#screenshotGalleryModal .gallery-close-btn');
6000
+ if (closeBtn) {
6001
+ closeBtn.addEventListener('click', function(e) {
6002
+ e.stopPropagation();
6003
+ window.closeScreenshotGallery();
6004
+ });
6005
+ }
3862
6006
 
3863
- holder.appendChild(
3864
- generateBarEl('String L10n level', totalI18nCount, totalLocCount)
3865
- );
6007
+ var backdrop = document.querySelector('#screenshotGalleryModal .gallery-modal-backdrop');
6008
+ if (backdrop) {
6009
+ backdrop.addEventListener('click', function(e) {
6010
+ e.stopPropagation();
6011
+ window.closeScreenshotGallery();
6012
+ });
6013
+ }
3866
6014
 
3867
- holder.appendChild(
3868
- generateBarEl('Number I18n level', totalNumberCount, i18nNumbers)
3869
- );
6015
+ var prevBtn = document.getElementById('galleryPrevBtn');
6016
+ if (prevBtn) {
6017
+ prevBtn.addEventListener('click', function() {
6018
+ if (window.galleryIndex > 0) renderGalleryItem(window.galleryIndex - 1);
6019
+ });
6020
+ }
3870
6021
 
3871
- holder.appendChild(
3872
- generateBarEl(
3873
- 'DateTime I18n level',
3874
- totalI18nDateTimeCount,
3875
- i18nDateTime
3876
- )
3877
- );
3878
- return holder;
6022
+ var nextBtn = document.getElementById('galleryNextBtn');
6023
+ if (nextBtn) {
6024
+ nextBtn.addEventListener('click', function() {
6025
+ if (window.galleryItems && window.galleryIndex < window.galleryItems.length - 1) {
6026
+ renderGalleryItem(window.galleryIndex + 1);
3879
6027
  }
6028
+ });
6029
+ }
3880
6030
 
3881
- function generateCardContents(locale, data) {
3882
- const cardContent = document.createElement('div');
3883
- cardContent.classList.add('card-content');
3884
-
3885
- cardContent.appendChild(createStatusBarSection(locale, data));
3886
- cardContent.appendChild(createStatSection(locale, data));
3887
- return cardContent;
6031
+ document.addEventListener('keydown', function(e) {
6032
+ var modal = document.getElementById('screenshotGalleryModal');
6033
+ if (!modal || modal.style.display === 'none') return;
6034
+ if (e.key === 'Escape') {
6035
+ window.closeScreenshotGallery();
6036
+ } else if (e.key === 'ArrowLeft') {
6037
+ if (window.galleryIndex > 0) renderGalleryItem(window.galleryIndex - 1);
6038
+ } else if (e.key === 'ArrowRight') {
6039
+ if (window.galleryItems && window.galleryIndex < window.galleryItems.length - 1) {
6040
+ renderGalleryItem(window.galleryIndex + 1);
3888
6041
  }
6042
+ }
6043
+ });
3889
6044
 
3890
- function createCardEl(locale, data) {
3891
- const card = document.createElement('div');
3892
- const title = document.createElement('div');
3893
- card.classList.add('card');
3894
- card.dataset.locale = locale;
6045
+ renderInsightsTable();
3895
6046
 
3896
- if(!window.isStandaloneReport) {
3897
- card.classList.add('--reduced-zoom');
6047
+ var searchInput = document.getElementById('insightsSearchInput');
6048
+ if (searchInput) {
6049
+ searchInput.addEventListener('input', function(e) {
6050
+ var query = e.target.value.toLowerCase().trim();
6051
+ var rows = document.querySelectorAll('#insightsTableBody tr.insights-metric-row');
6052
+ rows.forEach(function(row) {
6053
+ var text = (row.dataset.search || '').toLowerCase();
6054
+ if (!query || text.indexOf(query) !== -1) {
6055
+ row.style.display = '';
6056
+ } else {
6057
+ row.style.display = 'none';
3898
6058
  }
6059
+ });
6060
+ });
6061
+ }
3899
6062
 
3900
- title.classList.add('title');
3901
- title.textContent = \`\${locale} details\`;
6063
+ function renderInsightsTable() {
6064
+ var thead = document.getElementById('insightsTableHead');
6065
+ var tbody = document.getElementById('insightsTableBody');
6066
+ if (!thead || !tbody) return;
3902
6067
 
3903
- // fire menu selection event on clicking the card
3904
- card.addEventListener('click', () => {
3905
- const previousSelectedSubMenu = document.querySelector(
3906
- '.sub-menu-item.selected'
3907
- );
3908
- if (previousSelectedSubMenu) {
3909
- window.previousSelectedLocale =
3910
- previousSelectedSubMenu.dataset.locale;
3911
- }
6068
+ var locales = DataStore.getLocales();
6069
+ if (!locales.length) {
6070
+ tbody.innerHTML = '<tr><td colspan="3" style="text-align:center; padding: 24px; color: var(--text-muted);">No locale data available</td></tr>';
6071
+ return;
6072
+ }
3912
6073
 
3913
- document
3914
- .querySelector(\`.sub-menu-item[data-locale="\${locale}"]\`)
3915
- .click();
3916
- });
6074
+ // Build header: Verification Item | Locale 1 | Locale 2 ... | Total
6075
+ var headerHtml = '<tr><th class="metric-col-header">Verification Item</th>';
6076
+ locales.forEach(function(locale) {
6077
+ var isDef = DataStore.isDefaultLocale(locale);
6078
+ headerHtml += '<th class="locale-col-header">' +
6079
+ '<div class="col-header-inner">' +
6080
+ '<span class="locale-col-code">' + locale + '</span>' +
6081
+ (isDef ? '<span class="col-default-badge">Default</span>' : '') +
6082
+ '</div>' +
6083
+ '</th>';
6084
+ });
6085
+ headerHtml += '<th class="total-col-header">Total</th></tr>';
6086
+ thead.innerHTML = headerHtml;
6087
+
6088
+ // Build tbody rows for each of the 22 metrics
6089
+ tbody.innerHTML = '';
6090
+ METRIC_ROWS.forEach(function(row) {
6091
+ var tr = document.createElement('tr');
6092
+ tr.className = 'insights-metric-row' + (row.isTotal ? ' row-total' : '') + (row.isDefect ? ' row-defect' : '');
6093
+ tr.dataset.search = (row.label + ' ' + row.category).toLowerCase();
6094
+ tr.dataset.metric = row.type;
6095
+
6096
+ var rowTotal = 0;
6097
+ var cellsHtml = '<td class="metric-name-cell">' +
6098
+ '<div class="metric-name-inner">' +
6099
+ '<span class="metric-cat-pill cat-' + row.category.toLowerCase() + '">' + row.category + '</span>' +
6100
+ '<span class="metric-title-text">' + row.label + '</span>' +
6101
+ '</div>' +
6102
+ '</td>';
6103
+
6104
+ locales.forEach(function(locale) {
6105
+ var stats = DataStore.getStatByLocale(locale) || {};
6106
+ var val = stats[row.key] || 0;
6107
+ rowTotal += val;
6108
+
6109
+ if (val > 0) {
6110
+ cellsHtml += '<td class="metric-value-cell">' +
6111
+ '<button type="button" class="metric-cell-btn cell-active badge-' + row.color + '" ' +
6112
+ 'data-locale="' + locale + '" ' +
6113
+ 'data-type="' + row.type + '" ' +
6114
+ 'data-label="' + row.label + '" ' +
6115
+ 'title="Click to view ' + val + ' screenshot(s)">' +
6116
+ '<span class="cell-num">' + val + '</span>' +
6117
+ '<svg class="cell-cam-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>' +
6118
+ '</button>' +
6119
+ '</td>';
6120
+ } else {
6121
+ cellsHtml += '<td class="metric-value-cell"><span class="cell-zero">0</span></td>';
6122
+ }
6123
+ });
3917
6124
 
3918
- card.appendChild(title);
3919
- card.appendChild(generateCardContents(locale, data));
3920
- return card;
6125
+ // Total cell
6126
+ if (rowTotal > 0) {
6127
+ cellsHtml += '<td class="metric-value-cell total-cell">' +
6128
+ '<button type="button" class="metric-cell-btn cell-active total-badge" ' +
6129
+ 'data-locale="ALL" ' +
6130
+ 'data-type="' + row.type + '" ' +
6131
+ 'data-label="' + row.label + '" ' +
6132
+ 'title="Click to view ' + rowTotal + ' screenshot(s) across all locales">' +
6133
+ '<span class="cell-num">' + rowTotal + '</span>' +
6134
+ '<svg class="cell-cam-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>' +
6135
+ '</button>' +
6136
+ '</td>';
6137
+ } else {
6138
+ cellsHtml += '<td class="metric-value-cell total-cell"><span class="cell-zero">0</span></td>';
3921
6139
  }
3922
6140
 
3923
- function renderCards() {
3924
- // read from global variable appData;
3925
- const localeInfo = DataStore.getLocalesData();
6141
+ tr.innerHTML = cellsHtml;
3926
6142
 
3927
- if (Object.keys(localeInfo).length) {
3928
- const cardHolder = document.querySelector(
3929
- '#insights .cards .holder'
3930
- );
3931
- for (const locale in localeInfo) {
3932
- const cardEl = createCardEl(
3933
- locale,
3934
- DataStore.getStatByLocale(locale)
3935
- );
3936
- cardHolder.appendChild(cardEl);
3937
- }
3938
- }
3939
- }
6143
+ // Attach click handlers to active metric buttons
6144
+ tr.querySelectorAll('.metric-cell-btn').forEach(function(btn) {
6145
+ btn.addEventListener('click', function(e) {
6146
+ e.stopPropagation();
6147
+ openScreenshotGallery(btn.dataset.locale, btn.dataset.type, btn.dataset.label);
6148
+ });
6149
+ });
6150
+
6151
+ tbody.appendChild(tr);
3940
6152
  });
3941
- })();`;
6153
+ }
6154
+
6155
+ function drilldownLocale(locale) {
6156
+ var contents = document.querySelectorAll('.content');
6157
+ contents.forEach(function(c) { c.classList.remove('active'); });
6158
+ var locSection = document.getElementById('locale');
6159
+ if (locSection) locSection.classList.add('active');
6160
+ if (window.renderLocaleGrid) {
6161
+ window.renderLocaleGrid(locale);
6162
+ }
6163
+ }
6164
+ });
6165
+ })();`;
3942
6166
  const localeGrid = () => `// locale grid
3943
6167
  (() => {
3944
6168
  if (!DataStore.ranSuccessfully()) {
@@ -3962,6 +6186,59 @@ const localeGrid = () => `// locale grid
3962
6186
  ).textContent = \`\${locale} \${DataStore.isSanityRun() ? 'Sanity Result' : 'I18n Coverage Details'}\`;
3963
6187
  };
3964
6188
 
6189
+ const getMeta = (type) => {
6190
+ switch (type) {
6191
+ case 'STRING_LOCALIZED':
6192
+ return { category: 'Strings', status: 'Localized', badgeClass: 'status-green', catClass: 'cat-strings' };
6193
+ case 'STRING_NOT_LOCALIZED':
6194
+ return { category: 'Strings', status: 'Not Localized', badgeClass: 'status-red', catClass: 'cat-strings' };
6195
+ case 'STRING_PARTIALLY_LOCALIZED':
6196
+ return { category: 'Strings', status: 'Partially Localized', badgeClass: 'status-yellow', catClass: 'cat-strings' };
6197
+ case 'UNTRANSLATED':
6198
+ return { category: 'Strings', status: 'Untranslated', badgeClass: 'status-pink', catClass: 'cat-strings' };
6199
+ case 'ENCODING_ISSUE':
6200
+ return { category: 'Strings', status: 'Encoding Issue', badgeClass: 'status-cyan', catClass: 'cat-strings' };
6201
+ case 'NUMBER_LOCALIZED':
6202
+ return { category: 'Numbers', status: 'Localized', badgeClass: 'status-green', catClass: 'cat-numbers' };
6203
+ case 'NUMBER_NOT_LOCALIZED':
6204
+ return { category: 'Numbers', status: 'Not Localized', badgeClass: 'status-red', catClass: 'cat-numbers' };
6205
+ case 'CURRENCY_LOCALIZED':
6206
+ return { category: 'Currency', status: 'Localized', badgeClass: 'status-green', catClass: 'cat-currency' };
6207
+ case 'CURRENCY_NOT_LOCALIZED':
6208
+ return { category: 'Currency', status: 'Not Localized', badgeClass: 'status-red', catClass: 'cat-currency' };
6209
+ case 'PERCENTAGE_LOCALIZED':
6210
+ return { category: 'Percentage', status: 'Localized', badgeClass: 'status-green', catClass: 'cat-percentage' };
6211
+ case 'PERCENTAGE_NOT_LOCALIZED':
6212
+ return { category: 'Percentage', status: 'Not Localized', badgeClass: 'status-red', catClass: 'cat-percentage' };
6213
+ case 'DATE_LOCALIZED':
6214
+ return { category: 'Date', status: 'Localized', badgeClass: 'status-green', catClass: 'cat-date' };
6215
+ case 'DATE_NOT_LOCALIZED':
6216
+ return { category: 'Date', status: 'Not Localized', badgeClass: 'status-red', catClass: 'cat-date' };
6217
+ case 'TIME_LOCALIZED':
6218
+ return { category: 'Time', status: 'Localized', badgeClass: 'status-green', catClass: 'cat-time' };
6219
+ case 'TIME_NOT_LOCALIZED':
6220
+ return { category: 'Time', status: 'Not Localized', badgeClass: 'status-red', catClass: 'cat-time' };
6221
+ case 'DATE_TIME_LOCALIZED':
6222
+ return { category: 'DateTime', status: 'Localized', badgeClass: 'status-green', catClass: 'cat-datetime' };
6223
+ case 'DATE_TIME_NOT_LOCALIZED':
6224
+ return { category: 'DateTime', status: 'Not Localized', badgeClass: 'status-red', catClass: 'cat-datetime' };
6225
+ case 'DNT':
6226
+ return { category: 'Skipped', status: 'Do Not Translate (DNT)', badgeClass: 'status-muted', catClass: 'cat-skipped' };
6227
+ case 'IGNORE':
6228
+ return { category: 'Skipped', status: 'Ignored', badgeClass: 'status-muted', catClass: 'cat-skipped' };
6229
+ case 'AUTO_IGNORE':
6230
+ return { category: 'Skipped', status: 'Auto Ignored', badgeClass: 'status-muted', catClass: 'cat-skipped' };
6231
+ case 'USER_INPUT':
6232
+ return { category: 'Skipped', status: 'User Input', badgeClass: 'status-muted', catClass: 'cat-skipped' };
6233
+ case 'TRUNCATION_ISSUE':
6234
+ return { category: 'Layout', status: 'Truncation Issue', badgeClass: 'status-red', catClass: 'cat-layout' };
6235
+ case 'OVERLAPPING_ISSUE':
6236
+ return { category: 'Layout', status: 'Overlapping Issue', badgeClass: 'status-red', catClass: 'cat-layout' };
6237
+ default:
6238
+ return { category: 'General', status: type || '-', badgeClass: 'status-muted', catClass: 'cat-general' };
6239
+ }
6240
+ };
6241
+
3965
6242
  const buildGrid = (locale) => {
3966
6243
  const data = DataStore.getDataByLocale(locale) || {};
3967
6244
  $.fn.dataTable.ext.errMode = 'none';
@@ -3977,9 +6254,20 @@ const localeGrid = () => `// locale grid
3977
6254
  else {
3978
6255
  Object.entries(data).map((entry) =>
3979
6256
  entry[1].forEach(
3980
- (v) =>
3981
- (v.type !== 'REVIEWED') &&
3982
- dataSet.push([entry[0], v.type, v.description, v.imagePath])
6257
+ (v) => {
6258
+ if (v.type !== 'REVIEWED') {
6259
+ const meta = getMeta(v.type);
6260
+ dataSet.push([
6261
+ entry[0],
6262
+ meta.category,
6263
+ meta.status,
6264
+ v.description,
6265
+ v.imagePath,
6266
+ meta.catClass,
6267
+ meta.badgeClass
6268
+ ]);
6269
+ }
6270
+ }
3983
6271
  )
3984
6272
  );
3985
6273
  }
@@ -3996,7 +6284,8 @@ const localeGrid = () => `// locale grid
3996
6284
  { title: 'Page' },
3997
6285
  { title: 'Screenshot' },
3998
6286
  ] : [
3999
- { title: 'String' },
6287
+ { title: 'Used String / Term' },
6288
+ { title: 'Category' },
4000
6289
  { title: 'Status' },
4001
6290
  { title: 'Page' },
4002
6291
  { title: 'Screenshot' }
@@ -4015,33 +6304,48 @@ const localeGrid = () => `// locale grid
4015
6304
  data: null,
4016
6305
  render: function (data) {
4017
6306
  if (!data || !data[1]) return '<span style="color: var(--text-muted);">-</span>';
4018
- return \`<a href="javascript:void(0)" class="view-screenshot-btn" data-image="\${data[1]}" onclick="openImageViewer('\${data[1]}')" title="View screenshot for '\${data[0]}'"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg><span>View</span></a>\`;
6307
+ return \`<a href="javascript:void(0)" class="view-screenshot-btn" data-image="\${data[1]}" onclick="openImageViewer('\\\\\${data[1]}')" title="View screenshot for '\${data[0]}'"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg><span>View</span></a>\`;
4019
6308
  },
4020
6309
  className: 'dt-head-center dt-body-center'
4021
6310
  }
4022
6311
  ] :
4023
6312
  [
4024
6313
  {
4025
- width: '45%',
4026
- targets: 0
6314
+ width: '35%',
6315
+ targets: 0,
6316
+ render: function(data) {
6317
+ return '<span class="cell-term-text">' + $('<div>').text(data || '').html() + '</span>';
6318
+ }
6319
+ },
6320
+ {
6321
+ width: '15%',
6322
+ targets: 1,
6323
+ render: function(data, type, row) {
6324
+ var catClass = row[5] || 'cat-general';
6325
+ return '<span class="cat-pill ' + catClass + '">' + data + '</span>';
6326
+ }
4027
6327
  },
4028
6328
  {
4029
- width: '22.5%',
4030
- targets: 1
6329
+ width: '18%',
6330
+ targets: 2,
6331
+ render: function(data, type, row) {
6332
+ var badgeClass = row[6] || 'status-muted';
6333
+ return '<span class="status-badge ' + badgeClass + '">' + data + '</span>';
6334
+ }
4031
6335
  },
4032
6336
  {
4033
- width: '22.5%',
4034
- targets: 2
6337
+ width: '22%',
6338
+ targets: 3
4035
6339
  },
4036
6340
  {
4037
6341
  width: '10%',
4038
- targets: 3,
6342
+ targets: 4,
4039
6343
  orderable: false,
4040
6344
  searchable: false,
4041
6345
  data: null,
4042
6346
  render: function (data) {
4043
- if (!data || !data[3]) return '<span style="color: var(--text-muted);">-</span>';
4044
- return \`<a href="javascript:void(0)" class="view-screenshot-btn" data-image="\${data[3]}" onclick="openImageViewer('\${data[3]}')" title="View screenshot for '\${data[2]}'"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg><span>View</span></a>\`;
6347
+ if (!data || !data[4]) return '<span style="color: var(--text-muted);">-</span>';
6348
+ return \`<a href="javascript:void(0)" class="view-screenshot-btn" data-image="\${data[4]}" onclick="openImageViewer('\\\\\${data[4]}')" title="View screenshot for '\${data[3]}'"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg><span>View</span></a>\`;
4045
6349
  },
4046
6350
  className: 'dt-head-center dt-body-center'
4047
6351
  }
@@ -4054,7 +6358,7 @@ const localeGrid = () => `// locale grid
4054
6358
  const column = this;
4055
6359
  const columnIdx = column.index();
4056
6360
 
4057
- if ((DataStore.isSanityRun() && columnIdx === 1) || columnIdx === 3 || columnIdx === 0) {
6361
+ if ((DataStore.isSanityRun() && columnIdx === 1) || columnIdx === 4 || columnIdx === 0) {
4058
6362
  return;
4059
6363
  }
4060
6364
 
@@ -4064,9 +6368,14 @@ const localeGrid = () => `// locale grid
4064
6368
  select.empty().append('<option value="">Filter By</option>');
4065
6369
 
4066
6370
  const uniqueValues = new Set();
4067
- tableApi.column(columnIdx, { search: 'applied' }).data().each((d) => uniqueValues.add(d));
6371
+ tableApi.column(columnIdx, { search: 'applied' }).data().each((d) => {
6372
+ if (d) {
6373
+ const raw = $('<div>' + d + '</div>').text().trim() || d;
6374
+ uniqueValues.add(raw);
6375
+ }
6376
+ });
4068
6377
 
4069
- uniqueValues.forEach((value) => {
6378
+ Array.from(uniqueValues).sort().forEach((value) => {
4070
6379
  select.append(\`<option value="\${value}">\${value}</option>\`);
4071
6380
  });
4072
6381
 
@@ -4080,7 +6389,7 @@ const localeGrid = () => `// locale grid
4080
6389
  const column = this;
4081
6390
  const columnIdx = column.index();
4082
6391
 
4083
- if ((DataStore.isSanityRun() && columnIdx === 1) || columnIdx === 3 || columnIdx === 0) {
6392
+ if ((DataStore.isSanityRun() && columnIdx === 1) || columnIdx === 4 || columnIdx === 0) {
4084
6393
  return;
4085
6394
  }
4086
6395
 
@@ -4103,7 +6412,14 @@ const localeGrid = () => `// locale grid
4103
6412
  updateFilters();
4104
6413
  });
4105
6414
 
4106
- column.data().unique().sort().each((d) => {
6415
+ const uniqueValues = new Set();
6416
+ column.data().each((d) => {
6417
+ if (d) {
6418
+ const raw = $('<div>' + d + '</div>').text().trim() || d;
6419
+ uniqueValues.add(raw);
6420
+ }
6421
+ });
6422
+ Array.from(uniqueValues).sort().forEach((d) => {
4107
6423
  select.append(\`<option value="\${d}">\${d}</option>\`);
4108
6424
  });
4109
6425
  }
@@ -4270,7 +6586,8 @@ window.initTheme = function() {
4270
6586
  function openImageViewer(imageSrc) {
4271
6587
  var imageView = document.getElementById("imageView");
4272
6588
  if (imageView) {
4273
- imageView.src = window.getProductUrl() + "/" + imageSrc;
6589
+ var cleanSrc = (imageSrc || "").replace(/^[\\/]+/, "");
6590
+ imageView.src = window.getProductUrl() + "/" + cleanSrc;
4274
6591
  }
4275
6592
  var viewer = document.getElementById("imageViewer");
4276
6593
  if (viewer) {
@@ -4314,6 +6631,14 @@ document.addEventListener('click', function(e) {
4314
6631
  window.closeImageViewer();
4315
6632
  }
4316
6633
  }
6634
+
6635
+ var galleryCloseBtn = e.target.closest('.gallery-close-btn') || (e.target.classList && e.target.classList.contains('gallery-modal-backdrop'));
6636
+ if (galleryCloseBtn) {
6637
+ e.preventDefault();
6638
+ if (typeof window.closeScreenshotGallery === 'function') {
6639
+ window.closeScreenshotGallery();
6640
+ }
6641
+ }
4317
6642
  });
4318
6643
 
4319
6644
  document.addEventListener('keydown', function(e) {
@@ -4321,6 +6646,9 @@ document.addEventListener('keydown', function(e) {
4321
6646
  if (typeof window.closeImageViewer === 'function') {
4322
6647
  window.closeImageViewer();
4323
6648
  }
6649
+ if (typeof window.closeScreenshotGallery === 'function') {
6650
+ window.closeScreenshotGallery();
6651
+ }
4324
6652
  }
4325
6653
  });
4326
6654
 
@@ -4342,86 +6670,7 @@ window.getProductUrl = function() {
4342
6670
  var params = new URLSearchParams(location.href.match(/(\\?.*$)/g)?.[0] || "");
4343
6671
  window.isStandaloneReport = params.size ? (params.get("standalone") === "true") : true;
4344
6672
  `;
4345
- const log = () => `(() => {
4346
- document.addEventListener("DOMContentLoaded", function() {
4347
- var rawLog = window.logData || (DataStore.hasRuntimeError() ? 'We are facing internal issue. Please try again after sometime.' : 'No log data found!');
4348
- var logPre = document.getElementById("logPre");
4349
- var copyBtn = document.getElementById("copyLogBtn");
4350
- var searchInput = document.getElementById("logSearchInput");
4351
- var currentFilteredText = rawLog;
4352
-
4353
- var colorizeAndRender = function(filterText) {
4354
- if (!logPre) return;
4355
- var lines = rawLog.split("\\n");
4356
- var filtered = filterText ? lines.filter(function(l) { return l.toLowerCase().indexOf(filterText.toLowerCase()) !== -1; }) : lines;
4357
- currentFilteredText = filtered.join("\\n");
4358
- var html = filtered.map(function(line) {
4359
- var formatted = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
4360
- formatted = formatted.replace(/\\[INFO\\]/g, '<span class="log-badge-info">[INFO]</span>');
4361
- formatted = formatted.replace(/\\[WARN\\]/g, '<span class="log-badge-warn">[WARN]</span>');
4362
- formatted = formatted.replace(/\\[ERROR\\]/g, '<span class="log-badge-error">[ERROR]</span>');
4363
- return formatted;
4364
- }).join("\\n");
4365
- logPre.innerHTML = html;
4366
- };
4367
-
4368
- colorizeAndRender();
4369
-
4370
- if (searchInput) {
4371
- searchInput.addEventListener("input", function(e) {
4372
- colorizeAndRender(e.target.value.trim());
4373
- });
4374
- }
4375
-
4376
- if (copyBtn) {
4377
- var showCopied = function() {
4378
- copyBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10b981" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg><span style="color: #10b981;">Copied!</span>';
4379
- setTimeout(function() {
4380
- copyBtn.innerHTML = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg><span>Copy</span>';
4381
- }, 2000);
4382
- };
4383
-
4384
- var fallbackCopy = function(text) {
4385
- var textArea = document.createElement("textarea");
4386
- textArea.value = text;
4387
- textArea.style.position = "fixed";
4388
- textArea.style.top = "-9999px";
4389
- textArea.style.left = "-9999px";
4390
- textArea.style.opacity = "0";
4391
- textArea.setAttribute("readonly", "");
4392
- document.body.appendChild(textArea);
4393
- textArea.focus();
4394
- textArea.select();
4395
- textArea.setSelectionRange(0, textArea.value.length);
4396
- try {
4397
- var successful = document.execCommand("copy");
4398
- document.body.removeChild(textArea);
4399
- if (successful) {
4400
- showCopied();
4401
- } else {
4402
- console.error("document.execCommand copy was unsuccessful");
4403
- }
4404
- } catch (err) {
4405
- document.body.removeChild(textArea);
4406
- console.error("Fallback copy error:", err);
4407
- }
4408
- };
4409
-
4410
- copyBtn.addEventListener("click", function() {
4411
- var textToCopy = currentFilteredText || rawLog;
4412
- if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
4413
- navigator.clipboard.writeText(textToCopy).then(function() {
4414
- showCopied();
4415
- }).catch(function() {
4416
- fallbackCopy(textToCopy);
4417
- });
4418
- } else {
4419
- fallbackCopy(textToCopy);
4420
- }
4421
- });
4422
- }
4423
- });
4424
- })();`;
6673
+ const log = () => '(() => {\n document.addEventListener("DOMContentLoaded", function() {\n var rawLog = window.logData || (DataStore.hasRuntimeError() ? "We are facing internal issue. Please try again after sometime." : "No log data found!");\n var logLinesEl = document.getElementById("logLines");\n var copyBtn = document.getElementById("copyLogBtn");\n var searchInput = document.getElementById("logSearchInput");\n var wrapToggleBtn = document.getElementById("wrapToggleBtn");\n var levelPills = document.querySelectorAll(".log-level-btn");\n\n var statTotal = document.getElementById("logStatTotal");\n var statInfo = document.getElementById("logStatInfo");\n var statWarn = document.getElementById("logStatWarn");\n var statError = document.getElementById("logStatError");\n\n var activeLevel = "ALL";\n var activeSearch = "";\n var isWrapped = false;\n var currentFilteredText = rawLog;\n\n var parsedLines = [];\n var countInfo = 0;\n var countWarn = 0;\n var countError = 0;\n\n var rawLines = rawLog.split("\\n");\n for (var i = 0; i < rawLines.length; i++) {\n var line = rawLines[i];\n var level = "LOG";\n if (line.indexOf("[ERROR]") !== -1) {\n level = "ERROR";\n countError++;\n } else if (line.indexOf("[WARN]") !== -1) {\n level = "WARN";\n countWarn++;\n } else if (line.indexOf("[INFO]") !== -1) {\n level = "INFO";\n countInfo++;\n }\n parsedLines.push({\n num: i + 1,\n level: level,\n text: line\n });\n }\n\n if (statTotal) statTotal.textContent = "Total: " + parsedLines.length;\n if (statInfo) statInfo.textContent = "Info: " + countInfo;\n if (statWarn) statWarn.textContent = "Warn: " + countWarn;\n if (statError) statError.textContent = "Error: " + countError;\n\n function escapeHtml(str) {\n return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");\n }\n\n function renderLogs() {\n if (!logLinesEl) return;\n\n var filtered = parsedLines.filter(function(item) {\n if (activeLevel !== "ALL" && item.level !== activeLevel) {\n return false;\n }\n if (activeSearch && item.text.toLowerCase().indexOf(activeSearch.toLowerCase()) === -1) {\n return false;\n }\n return true;\n });\n\n currentFilteredText = filtered.map(function(item) { return item.text; }).join("\\n");\n\n if (filtered.length === 0) {\n logLinesEl.innerHTML = "<div class=\\"log-empty-state\\">No matching log entries found.</div>";\n return;\n }\n\n var html = "";\n for (var j = 0; j < filtered.length; j++) {\n var item = filtered[j];\n var escaped = escapeHtml(item.text);\n\n if (activeSearch) {\n var safeTerm = activeSearch.split("").map(function(ch) {\n return ".*+?^$()|[]{}".indexOf(ch) !== -1 ? "\\\\" + ch : ch;\n }).join("");\n try {\n var reg = new RegExp("(" + safeTerm + ")", "gi");\n escaped = escaped.replace(reg, "<mark class=\\"log-highlight\\">$1</mark>");\n } catch(e) {}\n }\n\n escaped = escaped.replace(/\\[INFO\\]/g, "<span class=\\"log-badge badge-info\\">INFO</span>");\n escaped = escaped.replace(/\\[WARN\\]/g, "<span class=\\"log-badge badge-warn\\">WARN</span>");\n escaped = escaped.replace(/\\[ERROR\\]/g, "<span class=\\"log-badge badge-error\\">ERROR</span>");\n\n var rowClass = "log-row";\n if (item.level === "ERROR") rowClass += " row-error";\n else if (item.level === "WARN") rowClass += " row-warn";\n\n html += "<div class=\\"" + rowClass + "\\">" +\n "<span class=\\"log-ln\\">" + item.num + "</span>" +\n "<div class=\\"log-msg\\">" + escaped + "</div>" +\n "</div>";\n }\n\n logLinesEl.innerHTML = html;\n }\n\n renderLogs();\n\n levelPills.forEach(function(btn) {\n btn.addEventListener("click", function() {\n levelPills.forEach(function(b) { b.classList.remove("active"); });\n btn.classList.add("active");\n activeLevel = btn.dataset.level || "ALL";\n renderLogs();\n });\n });\n\n if (searchInput) {\n searchInput.addEventListener("input", function(e) {\n activeSearch = e.target.value.trim();\n renderLogs();\n });\n }\n\n if (wrapToggleBtn) {\n wrapToggleBtn.addEventListener("click", function() {\n isWrapped = !isWrapped;\n if (isWrapped) {\n logLinesEl.classList.add("wrapped");\n wrapToggleBtn.classList.add("active");\n } else {\n logLinesEl.classList.remove("wrapped");\n wrapToggleBtn.classList.remove("active");\n }\n });\n }\n\n if (copyBtn) {\n var showCopied = function() {\n copyBtn.innerHTML = "<svg width=\\"13\\" height=\\"13\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"#10b981\\" stroke-width=\\"2.5\\"><polyline points=\\"20 6 9 17 4 12\\"/></svg><span style=\\"color: #10b981;\\">Copied!</span>";\n setTimeout(function() {\n copyBtn.innerHTML = "<svg width=\\"13\\" height=\\"13\\" viewBox=\\"0 0 24 24\\" fill=\\"none\\" stroke=\\"currentColor\\" stroke-width=\\"2\\"><rect width=\\"14\\" height=\\"14\\" x=\\"8\\" y=\\"8\\" rx=\\"2\\" ry=\\"2\\"/><path d=\\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\\"/></svg><span>Copy</span>";\n }, 2000);\n };\n\n var fallbackCopy = function(text) {\n var textArea = document.createElement("textarea");\n textArea.value = text;\n textArea.style.position = "fixed";\n textArea.style.top = "-9999px";\n textArea.style.left = "-9999px";\n textArea.style.opacity = "0";\n textArea.setAttribute("readonly", "");\n document.body.appendChild(textArea);\n textArea.focus();\n textArea.select();\n textArea.setSelectionRange(0, textArea.value.length);\n try {\n var successful = document.execCommand("copy");\n document.body.removeChild(textArea);\n if (successful) showCopied();\n } catch (err) {\n document.body.removeChild(textArea);\n }\n };\n\n copyBtn.addEventListener("click", function() {\n var textToCopy = currentFilteredText || rawLog;\n if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") {\n navigator.clipboard.writeText(textToCopy).then(showCopied).catch(function() {\n fallbackCopy(textToCopy);\n });\n } else {\n fallbackCopy(textToCopy);\n }\n });\n }\n });\n})();';
4425
6674
  const getJsData = (metaData) => `${global()}
4426
6675
  ${data(metaData)}
4427
6676
  ${menuBar()}
@@ -4467,6 +6716,7 @@ class HtmlReport extends BaseReport {
4467
6716
  });
4468
6717
  }
4469
6718
  write(path2, name, data2) {
6719
+ ensureDirectoryExists(path2);
4470
6720
  saveToFile(joinPaths(path2, `${name}.html`), data2, "utf-8");
4471
6721
  }
4472
6722
  }
@@ -5237,9 +7487,9 @@ class CacheData {
5237
7487
  class Reports {
5238
7488
  static #baseReportPath;
5239
7489
  static #reportTimeStamp;
5240
- static setup() {
7490
+ static setup(timestamp = null) {
5241
7491
  if (!Reports.#reportTimeStamp) {
5242
- Reports.#reportTimeStamp = Reports.#dateTime();
7492
+ Reports.#reportTimeStamp = timestamp || process.env.REPORT_TIMESTAMP || Reports.#dateTime();
5243
7493
  }
5244
7494
  const paths = [
5245
7495
  Config.getL10nReportsPath(),
@@ -7298,6 +9548,10 @@ class Config {
7298
9548
  }
7299
9549
  static setOptions(options) {
7300
9550
  Config.#options = options;
9551
+ if (options?.mode) Config.#mode = options.mode;
9552
+ if (options?.appName) Config.#appName = options.appName;
9553
+ if (options?.type) Config.#type = options.type;
9554
+ if (options?.qaLocale) Config.#qaLocale = options.qaLocale;
7301
9555
  }
7302
9556
  static getExecutor() {
7303
9557
  if (Config.#executor) {