Select a file from the left to view its contents.
")
diff --git a/FlashXTest/backend/Webview/modern_static/generator.py b/FlashXTest/backend/Webview/modern_static/generator.py
index caa5bfc..250aea4 100644
--- a/FlashXTest/backend/Webview/modern_static/generator.py
+++ b/FlashXTest/backend/Webview/modern_static/generator.py
@@ -68,8 +68,19 @@ def generate_flashx_testview(
output_dir.mkdir(parents=True, exist_ok=False)
+ # Read CSS once; embed inline in every page so files are self-contained
+ pkg_dir = Path(__file__).parent
+ style_src = pkg_dir / "style.css"
+ css_content = style_src.read_text(encoding="utf-8") if style_src.is_file() else ""
+
+ # Copy JS assets (still needed as external files)
+ js_src = pkg_dir / "js"
+ if js_src.is_dir():
+ dst_js = output_dir / "js"
+ shutil.copytree(js_src, dst_js, dirs_exist_ok=True)
+
# Write overview index.html
- index_html = generate_html(board, sites, invocations_sorted, inv_dir_lookup)
+ index_html = generate_html(board, sites, invocations_sorted, inv_dir_lookup, css_content)
(output_dir / "index.html").write_text(index_html, encoding="utf-8")
# Write site-combined invocation pages
@@ -77,22 +88,10 @@ def generate_flashx_testview(
inv_index_dir.mkdir(parents=True, exist_ok=True)
for inv_name in invocations_sorted:
combined = generate_combined_invocation_page(
- inv_name, inv_dir_lookup.get(inv_name, {}), sites
+ inv_name, inv_dir_lookup.get(inv_name, {}), sites, css_content
)
(inv_index_dir / f"{inv_name}.html").write_text(combined, encoding="utf-8")
- # Copy shared CSS and JS assets
- pkg_dir = Path(__file__).parent
- # style.css
- style_src = pkg_dir / "style.css"
- if style_src.is_file():
- shutil.copy(style_src, output_dir / "style.css")
- # js assets
- js_src = pkg_dir / "js"
- if js_src.is_dir():
- dst_js = output_dir / "js"
- shutil.copytree(js_src, dst_js, dirs_exist_ok=True)
-
# Generate build pages (frameset, left/right)
for site_path in site_paths:
site_output_dir = output_dir / site_path.name
@@ -105,17 +104,17 @@ def generate_flashx_testview(
build_output_dir.mkdir(parents=True, exist_ok=True)
# Frameset
(build_output_dir / "frameset.html").write_text(
- generate_build_page_frameset(site_path.name, inv_dir.name, b.name),
+ generate_build_page_frameset(site_path.name, inv_dir.name, b.name, css_content),
encoding="utf-8",
)
# Left frame
(build_output_dir / "leftframe.html").write_text(
- generate_left_frame_html(site_path.name, inv_dir.name, b.name, b),
+ generate_left_frame_html(site_path.name, inv_dir.name, b.name, b, build_output_dir, css_content),
encoding="utf-8",
)
# Right frame
(build_output_dir / "rightframe.html").write_text(
- generate_right_frame_html(site_path.name, inv_dir.name, b.name),
+ generate_right_frame_html(site_path.name, inv_dir.name, b.name, css_content),
encoding="utf-8",
)
diff --git a/FlashXTest/backend/Webview/modern_static/html_utils.py b/FlashXTest/backend/Webview/modern_static/html_utils.py
index 11bfb46..199e56e 100644
--- a/FlashXTest/backend/Webview/modern_static/html_utils.py
+++ b/FlashXTest/backend/Webview/modern_static/html_utils.py
@@ -9,24 +9,27 @@ def zebra_row_class(idx: int) -> str:
return "row-alt" if idx % 2 else "row"
+def _css_tag(css_content: str) -> str:
+ return f" "
+
+
def page_header(
title: str,
- css_href: str,
+ css_content: str,
base_target: Optional[str] = None,
body_class: Optional[str] = None,
) -> List[str]:
- """Return common HTML header lines including DOCTYPE, head, and opening body.
- Optionally set ')
lines.append(' ')
lines.append('
')
@@ -32,33 +35,37 @@ def generate_html(
lines.append(f"
{html.escape(title)}
")
- # Start the table with fixed layout
+ # Search bar to filter invocation rows by name
+ lines.append('
')
+ lines.append(
+ ''
+ )
+ lines.append("
")
+
lines.append('
')
- # header row
+ # Header row
lines.append(" ")
lines.append(' | Invocation | ')
for site in sites:
lines.append(f" {html.escape(site)} | ")
lines.append("
")
- # body rows
+
+ # Body rows
for idx, inv_name in enumerate(invocations):
lines.append(f' ')
- # dynamic hover for invocation
inv_href = f"invocations/{html.escape(inv_name)}.html"
- # header/body for tooltip
- # Build execution summary across all sites
- # Collect all builds for this invocation
- builds = [] # list of (site, build_path)
+ # Build tooltip summary
+ builds = []
for site in sites:
- inv_paths = inv_dir_lookup.get(inv_name, {})
- site_inv = inv_paths.get(site)
+ site_inv = inv_dir_lookup.get(inv_name, {}).get(site)
if site_inv and site_inv.is_dir():
for b in site_inv.iterdir():
if b.is_dir():
builds.append((site, b))
total_tests = len(builds)
- failing = [] # list of (site, build_name, exit_msg)
+ failing = []
for site, b in builds:
status, exit_msg = parse_build_status(b)
if status.colour != "green":
@@ -68,12 +75,13 @@ def generate_html(
else:
header_text = f"All {total_tests} tests completed successfully"
header_js = json.dumps(header_text)
- body_lines = [f"{name} - {msg}" for _, name, msg in failing]
- body_js = json.dumps("
".join(body_lines))
+ body_js = json.dumps("
".join(f"{name} - {msg}" for _, name, msg in failing))
+
lines.append(
- f' "
- + f"{html.escape(inv_name)} | "
+ f' '
+ f'"
+ f"{html.escape(inv_name)} | "
)
for site in sites:
@@ -81,8 +89,17 @@ def generate_html(
if status is None:
lines.append(" | ")
else:
- lines.append(f' {status.emoji} | ')
+ sort_val = _STATUS_SORT.get(status.colour, "9")
+ lines.append(
+ f' '
+ f"{status.emoji} | "
+ )
lines.append("
")
+
lines.append("
")
+
+ # Search filter script
+ lines.append('')
+
lines.extend(page_footer())
return "\n".join(lines)
diff --git a/FlashXTest/backend/Webview/modern_static/invocation_page.py b/FlashXTest/backend/Webview/modern_static/invocation_page.py
index 4dfadb7..b378c53 100644
--- a/FlashXTest/backend/Webview/modern_static/invocation_page.py
+++ b/FlashXTest/backend/Webview/modern_static/invocation_page.py
@@ -9,62 +9,73 @@
from .html_utils import zebra_row_class, page_header, page_footer
from .status import parse_build_status
+# Maps status colour to a numeric sort value so dropdowns produce a
+# consistent order (worst → best: red=0, yellow=1, green=2).
+_STATUS_SORT = {"red": "0", "yellow": "1", "green": "2"}
+
def _generate_site_table_header(site: str) -> List[str]:
- """A helper function to generate the header for a site table."""
- content = [
+ """Generate the filter controls header for one site section."""
+ return [
'", # close site-header
]
- return content
def generate_combined_invocation_page(
- inv_name: str, site_dirs: Dict[str, Path], site_order: List[str]
+ inv_name: str, site_dirs: Dict[str, Path], site_order: List[str], css_content: str
) -> str:
- """Produce a page that lists all sites for one invocation with build tables."""
+ """Produce a page listing all sites for one invocation with filterable build tables."""
sections: List[str] = []
for site in site_order:
inv_dir = site_dirs.get(site)
if not inv_dir:
continue
- # Section per site with heading and its own filter controls
- sections.append('
')
- # Generate the header for the site table
+ sections.append('
')
sections.extend(_generate_site_table_header(site))
- # Build table with fixed layout and column classes
sections.append('
')
sections.append(" ")
sections.append(' | Build | ')
sections.append(' Status | ')
sections.append(' Summary | ')
sections.append("
")
+
builds = sorted(
[d for d in inv_dir.iterdir() if d.is_dir()], key=lambda p: p.name
)
for idx, b in enumerate(builds):
status, exit_msg = parse_build_status(b)
- # link to detailed build frameset page
- rel_link = f"../{html.escape(site)}/{html.escape(inv_name)}/{html.escape(b.name)}/frameset.html"
- # Determine error types for filtering
+ rel_link = (
+ f"../{html.escape(site)}/{html.escape(inv_name)}"
+ f"/{html.escape(b.name)}/frameset.html"
+ )
+ # Determine error types for the type-filter dropdown
err_types = []
lm = exit_msg.lower()
if "setup" in lm:
@@ -76,31 +87,38 @@ def generate_combined_invocation_page(
if "testing" in lm:
err_types.append("testing")
types_str = " ".join(err_types)
- # Table row with data attributes and column classes
+ sort_val = _STATUS_SORT.get(status.colour, "9")
+
sections.extend(
[
- f' ',
- f' | {html.escape(b.name)} | ',
- f' {status.emoji} | ',
+ f'
',
+ f' | '
+ f''
+ f"{html.escape(b.name)} | ",
+ f' {status.emoji} | ',
f' {html.escape(exit_msg)} | ',
"
",
]
)
+
sections.append("
")
- # Close the site section
- sections.append("
")
+ # "No results" message shown by JS when all rows are filtered out
+ sections.append('
No builds match the selected filters.
')
+ sections.append("
") # close site-section
title = f"Invocation {inv_name}"
- header = page_header(title, css_href="../style.css")
- # Page content: heading, back link, filters, site tables, and init script
+ header = page_header(title, css_content)
content = [
f"
{html.escape(title)}
",
'
← Back to overview
',
*sections,
- "",
'',
- "",
+ "",
]
footer = page_footer()
-
return "\n".join(header + content + footer)
diff --git a/FlashXTest/backend/Webview/modern_static/js/invocation_filter.js b/FlashXTest/backend/Webview/modern_static/js/invocation_filter.js
index 5dfa576..90fc90c 100644
--- a/FlashXTest/backend/Webview/modern_static/js/invocation_filter.js
+++ b/FlashXTest/backend/Webview/modern_static/js/invocation_filter.js
@@ -1,36 +1,62 @@
/**
* invocation_filter.js
- * Client-side filtering for per-site build tables.
+ * Client-side filtering for per-site build tables using the Status and Error
+ * Type dropdown selects. Each .site-section card is handled independently.
*/
-(function() {
+(function () {
+ 'use strict';
+
function initInvocationFilter() {
- // For each site section, bind filters to its table
var sections = document.querySelectorAll('.site-section');
- sections.forEach(function(section) {
+
+ sections.forEach(function (section) {
var statusFilter = section.querySelector('.status-filter');
- var typeFilter = section.querySelector('.type-filter');
- var table = section.querySelector('table.inv-table');
+ var typeFilter = section.querySelector('.type-filter');
+ var table = section.querySelector('table.inv-table');
+ var noResultsMsg = section.querySelector('.no-results-msg');
+
if (!statusFilter || !typeFilter || !table) return;
+
function applyFilter() {
var status = statusFilter.value;
- var type = typeFilter.value;
- var rows = table.querySelectorAll('tr[data-status]');
- rows.forEach(function(row) {
- var rowStatus = row.getAttribute('data-status');
- var rowTypes = row.getAttribute('data-errortypes') || '';
- var types = rowTypes.split(' ').filter(function(t) { return t; });
- var statusMatch = (status === 'all' || rowStatus === status);
- var typeMatch = (type === 'all' || types.indexOf(type) !== -1);
- row.style.display = (statusMatch && typeMatch) ? '' : 'none';
+ var type = typeFilter.value;
+ var rows = table.querySelectorAll('tr[data-status]');
+ var visibleCount = 0;
+ var altToggle = 0;
+
+ rows.forEach(function (row) {
+ var rowStatus = row.getAttribute('data-status') || '';
+ var rowTypes = (row.getAttribute('data-errortypes') || '').split(' ')
+ .filter(function (t) { return t; });
+
+ var statusOk = (status === 'all' || rowStatus === status);
+ var typeOk = (type === 'all' || rowTypes.indexOf(type) !== -1);
+ var show = statusOk && typeOk;
+
+ row.style.display = show ? '' : 'none';
+
+ if (show) {
+ // Re-apply zebra stripes to visible rows only
+ row.classList.remove('row', 'row-alt');
+ row.classList.add(altToggle % 2 === 0 ? 'row' : 'row-alt');
+ altToggle++;
+ visibleCount++;
+ }
});
+
+ // Show "no results" message when every row is filtered out
+ if (noResultsMsg) {
+ noResultsMsg.style.display = (visibleCount === 0) ? 'block' : 'none';
+ }
}
- // Bind change events
+
statusFilter.addEventListener('change', applyFilter);
typeFilter.addEventListener('change', applyFilter);
- // Initial filtering
+
+ // Apply on load so the initial selection is respected
applyFilter();
});
}
- // Expose init function
+
window.initInvocationFilter = initInvocationFilter;
-})();
+}());
diff --git a/FlashXTest/backend/Webview/modern_static/js/table_sort.js b/FlashXTest/backend/Webview/modern_static/js/table_sort.js
new file mode 100644
index 0000000..dc93403
--- /dev/null
+++ b/FlashXTest/backend/Webview/modern_static/js/table_sort.js
@@ -0,0 +1,96 @@
+/**
+ * table_sort.js
+ * Click-to-sort for th[data-sortable] headers and text search on the index page.
+ */
+(function () {
+ 'use strict';
+
+ /* Return the sort key for a cell: data-sort-value attr, then trimmed text. */
+ function cellKey(cell) {
+ var v = cell.getAttribute('data-sort-value');
+ return (v !== null ? v : (cell.textContent || '')).trim().toLowerCase();
+ }
+
+ /* Re-apply zebra stripes after sorting; skip hidden rows. */
+ function restripe(rows) {
+ var vis = 0;
+ rows.forEach(function (row) {
+ row.classList.remove('row', 'row-alt');
+ if (row.style.display !== 'none') {
+ row.classList.add(vis % 2 === 0 ? 'row' : 'row-alt');
+ vis++;
+ }
+ });
+ }
+
+ function sortTable(th) {
+ var table = th.closest('table');
+ if (!table) return;
+
+ var headers = Array.from(th.parentElement.children);
+ var colIdx = headers.indexOf(th);
+ var prevDir = th.getAttribute('aria-sort');
+ var dir = prevDir === 'ascending' ? 'descending' : 'ascending';
+
+ headers.forEach(function (h) { h.removeAttribute('aria-sort'); });
+ th.setAttribute('aria-sort', dir);
+
+ var rows = Array.from(table.querySelectorAll('tr')).filter(function (r) {
+ return r.classList.contains('row') || r.classList.contains('row-alt');
+ });
+
+ rows.sort(function (a, b) {
+ var ak = cellKey(a.children[colIdx] || {});
+ var bk = cellKey(b.children[colIdx] || {});
+ var cmp = ak < bk ? -1 : ak > bk ? 1 : 0;
+ return dir === 'ascending' ? cmp : -cmp;
+ });
+
+ var parent = table.querySelector('tbody') || table;
+ rows.forEach(function (row) { parent.appendChild(row); });
+ restripe(rows);
+ }
+
+ /* Wire up all sortable headers in the document. */
+ function initTableSort() {
+ document.querySelectorAll('th[data-sortable]').forEach(function (th) {
+ th.addEventListener('click', function () { sortTable(th); });
+ });
+ }
+
+ /* Live text search on the index page invocation list. */
+ function initInvocationSearch() {
+ var input = document.getElementById('invocationSearch');
+ if (!input) return;
+ input.addEventListener('input', function () {
+ var q = input.value.trim().toLowerCase();
+ var rows = document.querySelectorAll(
+ '.index-table tr.row, .index-table tr.row-alt'
+ );
+ var vis = 0;
+ rows.forEach(function (row) {
+ var text = (row.textContent || '').toLowerCase();
+ var show = !q || text.indexOf(q) !== -1;
+ row.style.display = show ? '' : 'none';
+ if (show) {
+ row.classList.remove('row', 'row-alt');
+ row.classList.add(vis % 2 === 0 ? 'row' : 'row-alt');
+ vis++;
+ }
+ });
+ });
+ }
+
+ window.initTableSort = initTableSort;
+ window.initInvocationSearch = initInvocationSearch;
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', function () {
+ initTableSort();
+ initInvocationSearch();
+ });
+ } else {
+ initTableSort();
+ initInvocationSearch();
+ }
+}());
diff --git a/FlashXTest/backend/Webview/modern_static/status.py b/FlashXTest/backend/Webview/modern_static/status.py
index 13579dc..9ccb57c 100644
--- a/FlashXTest/backend/Webview/modern_static/status.py
+++ b/FlashXTest/backend/Webview/modern_static/status.py
@@ -3,7 +3,7 @@
"""
from pathlib import Path
-from typing import List
+from typing import List, Tuple
class InvocationStatus:
@@ -68,7 +68,7 @@ def classify_invocation(inv_dir: Path) -> InvocationStatus:
return InvocationStatus(colour=colour)
-def parse_build_status(build_dir: Path) -> tuple[InvocationStatus, str]:
+def parse_build_status(build_dir: Path) -> Tuple[InvocationStatus, str]:
"""Return (InvocationStatus, exit_msg) for a single build directory."""
errors_file = build_dir / "errors"
if not errors_file.exists():
diff --git a/FlashXTest/backend/Webview/modern_static/style.css b/FlashXTest/backend/Webview/modern_static/style.css
index 2a59707..11455f7 100644
--- a/FlashXTest/backend/Webview/modern_static/style.css
+++ b/FlashXTest/backend/Webview/modern_static/style.css
@@ -1,115 +1,336 @@
-/* Hovering tooltip for index.html */
-.stats-window {
- font-size: 0.8rem;
- visibility: hidden;
- position: absolute;
- z-index: 1000;
- border: 1px solid #ccc;
- background: #fff;
- padding: 2px;
+/* =============================================
+ FlashXTest Webview – style.css
+ ============================================= */
+
+/* -- Variables -------------------------------- */
+:root {
+ --primary: #1565a0;
+ --primary-dark: #0d4c7a;
+ --primary-light: #e3f0fa;
+ --bg: #f0f4f8;
+ --surface: #ffffff;
+ --border: #d0d7de;
+ --text: #1f2329;
+ --text-muted: #57606a;
+ --green-bg: #d4edda;
+ --green-text: #155724;
+ --yellow-bg: #fff3cd;
+ --yellow-text: #856404;
+ --red-bg: #f8d7da;
+ --red-text: #721c24;
+ --radius: 6px;
+ --shadow: 0 1px 4px rgba(0, 0, 0, .10);
+ --shadow-md: 0 2px 8px rgba(0, 0, 0, .14);
}
-.stats-header {
- font-weight: bold;
- color: #fff;
- margin-bottom: 4px;
- background: #006699;
+
+/* -- Reset / base ----------------------------- */
+*, *::before, *::after { box-sizing: border-box; }
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ font-size: 15px;
+ line-height: 1.5;
+ color: var(--text);
+ background: var(--bg);
+ margin: 0;
+ padding: 1.5rem 2rem;
}
-.stats-body {
+
+h1 { color: var(--primary); margin: 0 0 0.75rem; font-size: 1.6rem; }
+h2 { color: var(--primary); margin: 0 0 0.5rem; font-size: 1.2rem; }
+h3 { color: var(--text-muted); margin: 0.6rem 0 0.3rem; font-size: 1rem; }
+
+a { color: var(--primary); text-decoration: none; }
+a:hover { text-decoration: underline; color: var(--primary-dark); }
+
+p { margin: 0.5rem 0; }
+
+/* -- Toolbar (search bar row) ----------------- */
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+ flex-wrap: wrap;
}
-body {
- font-family: sans-serif;
- margin: 2rem;
+.search-bar {
+ font-size: 0.875rem;
+ font-family: inherit;
+ color: var(--text);
+ padding: 6px 10px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--surface);
+ width: 280px;
+ transition: border-color 0.15s, box-shadow 0.15s;
}
-h1 {
- color: #006699;
+.search-bar::placeholder { color: var(--text-muted); }
+.search-bar:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(21, 101, 160, .15);
}
+
+/* -- Tables ----------------------------------- */
table {
width: 100%;
border-collapse: collapse;
- font-size: 0.9rem;
+ font-size: 0.875rem;
+ background: var(--surface);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+ overflow: hidden;
}
+
th, td {
- border: 1px solid #ccc;
- padding: 4px 8px;
+ border: 1px solid var(--border);
+ padding: 7px 10px;
text-align: center;
}
+
th {
- background: #006699;
+ background: var(--primary);
color: #fff;
+ font-weight: 600;
+ letter-spacing: 0.02em;
+ border-color: var(--primary-dark);
+ white-space: nowrap;
+ position: sticky;
+ top: 0;
+ z-index: 2;
}
-/* for alternating row color */
-.row-alt {
- background: #f7f7f7;
+/* zebra rows */
+.row-alt { background: #f6f9fc; }
+
+/* subtle row hover */
+tr.row:hover td,
+tr.row-alt:hover td {
+ background: var(--primary-light);
+ transition: background 0.1s;
}
-/* make link anchor fill entire cell */
+
+/* link cells – anchor fills the full cell */
+.col-invocation,
+.col-build {
+ text-align: left;
+ padding: 0;
+}
+
+.col-summary { text-align: left; }
+
.cell-link {
display: block;
- width: 100%;
- height: 100%;
+ padding: 7px 10px;
+ color: var(--primary);
+ transition: background 0.12s;
+}
+.cell-link:hover {
+ background: var(--primary-light);
+ text-decoration: none;
+ color: var(--primary-dark);
}
-.green { background: #c4f0c4; }
-.yellow { background: #fff8c4; }
-.red { background: #f7c4c4; }
+/* -- Status colours --------------------------- */
+.green { background: var(--green-bg); color: var(--green-text); font-weight: 600; }
+.yellow { background: var(--yellow-bg); color: var(--yellow-text); font-weight: 600; }
+.red { background: var(--red-bg); color: var(--red-text); font-weight: 600; }
-/* Index page */
-.index-table {
- table-layout: fixed;
- width: 100%;
-}
+/* -- Index page ------------------------------- */
+.index-table { table-layout: fixed; }
+.index-table .col-invocation { width: 70%; }
-.index-table .col-invocation {
- width: 80%;
+/* -- Invocation page: site section card ------- */
+.site-section {
+ background: var(--surface);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow-md);
+ padding: 1rem 1.25rem;
+ margin-bottom: 1.5rem;
}
-/* Invocation page */
-.filters {
- margin-left: auto;
+/* tables inside a card: no extra shadow/radius */
+.site-section table {
+ box-shadow: none;
+ border-radius: 0;
+ overflow: visible;
}
+/* -- Site header + filter row ----------------- */
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
- margin-bottom: 0.5em;
+ margin-bottom: 0.75rem;
+ flex-wrap: wrap;
+ gap: 0.5rem;
}
-.site-header h2 {
- margin: 0;
- padding: 0;
+.site-header h2 { margin: 0; }
+
+.filters {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ flex-wrap: wrap;
}
-.inv-table {
- table-layout: fixed;
- width: 100%;
+.filters label {
+ font-size: 0.85rem;
+ color: var(--text-muted);
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ white-space: nowrap;
}
-.inv-table .col-build {
- width: 50%;
+
+/* -- Select wrapper: CSS-only custom arrow ---- */
+.select-wrap {
+ position: relative;
+ display: inline-block;
}
-.inv-table .col-status {
- width: 5%;
+
+/* The CSS triangle arrow */
+.select-wrap::after {
+ content: '';
+ position: absolute;
+ right: 9px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 0;
+ height: 0;
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: 6px solid var(--text-muted);
+ pointer-events: none;
}
-.inv-table .col-summary {
- width: 45%;
+
+.select-wrap select {
+ font-size: 0.85rem;
+ font-family: inherit;
+ color: var(--text);
+ padding: 5px 30px 5px 9px;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ background: var(--surface);
+ /* Remove native arrow so only our CSS one is visible */
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ cursor: pointer;
+ transition: border-color 0.12s, box-shadow 0.12s;
+ min-width: 110px;
}
-/* Build page */
+.select-wrap select:hover { border-color: var(--primary); }
+.select-wrap select:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(21, 101, 160, .15);
+}
+
+/* -- "No results" banner ---------------------- */
+.no-results-msg {
+ display: none;
+ padding: 0.6rem 1rem;
+ font-size: 0.85rem;
+ color: var(--text-muted);
+ font-style: italic;
+}
+
+/* -- Invocation table ------------------------- */
+.inv-table { table-layout: fixed; }
+.inv-table .col-build { width: 50%; }
+.inv-table .col-status { width: 6%; min-width: 50px; }
+.inv-table .col-summary { width: 44%; }
+
+/* -- Build page – left frame ------------------ */
+.left-frame {
+ font-size: 0.875rem;
+ padding: 0.75rem 1rem;
+}
-/* left frame in build detail */
.left-frame h1 {
- font-size: 1.2em;
- margin: 0.5em 0;
+ font-size: 1.1rem;
+ margin: 0.3rem 0 0.5rem;
+ color: var(--primary);
+ border-bottom: 1px solid var(--border);
+ padding-bottom: 0.3rem;
}
+
.left-frame h2 {
- font-size: 1.1em;
- margin: 0.4em 0;
+ font-size: 0.95rem;
+ margin: 0.8rem 0 0.25rem;
+ color: var(--primary);
}
+
.left-frame h3 {
- font-size: 1em;
- margin: 0.3em 0;
+ font-size: 0.85rem;
+ margin: 0.6rem 0 0.15rem;
+ color: var(--text-muted);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+
+.left-frame ul {
+ margin: 0.15rem 0 0.5rem 1.1rem;
+ padding: 0;
}
-/* right frame in build detail */
+
+.left-frame li { margin: 0.2rem 0; }
+
+.left-frame a {
+ color: var(--primary);
+ font-size: 0.85rem;
+}
+
+.left-frame a:hover { text-decoration: underline; }
+
+.left-frame pre {
+ font-size: 0.8rem;
+ margin: 0.25rem 0 0.4rem;
+ color: var(--text-muted);
+ white-space: pre-wrap;
+ background: var(--bg);
+ border-radius: 3px;
+ padding: 4px 6px;
+}
+
+/* -- Build page – right frame ----------------- */
.right-frame {
+ padding: 1.5rem 2rem;
+ font-size: 0.9rem;
+ color: var(--text-muted);
+ font-style: italic;
+}
+
+/* -- Hover tooltip (index page) --------------- */
+.stats-window {
+ font-size: 0.8rem;
+ visibility: hidden;
+ position: absolute;
+ z-index: 1000;
+ border: 1px solid var(--border);
+ background: var(--surface);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow-md);
+ padding: 0;
+ min-width: 200px;
+ max-width: 340px;
+ overflow: hidden;
+}
+
+.stats-header {
+ font-weight: 600;
+ color: #fff;
+ background: var(--primary);
+ padding: 5px 10px;
+}
+
+.stats-body {
+ padding: 6px 10px;
+ color: var(--text);
+ line-height: 1.6;
}