Each widget is as tall as its content, never taller than its saved height (where it scrolls). The dashboard is capped at 2400px instead of 1400px. Past 12 columns' worth of width it adds columns (one per 110px, up to 24) and places widgets again in reading order at their saved widths, so more sit side by side instead of stretching. Edit mode shows and saves the 12-column layout, so a layout doesn't depend on the screen it was edited on. The share view uses the same layout code (static/dash-layout.js). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
5.0 KiB
HTML
104 lines
5.0 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ dashboard.name }} — Steward</title>
|
|
<link rel="icon" type="image/svg+xml" href="{{ url_for('static', filename='logo/steward-mark.svg') }}">
|
|
<script src="{{ url_for('static', filename='vendor/htmx-2.0.4.min.js') }}"></script>
|
|
<script src="{{ url_for('static', filename='vendor/chart-4.4.4.umd.min.js') }}"></script>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='fonts/fonts.css') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='steward.css') }}">
|
|
<script src="{{ url_for('static', filename='chart-colors.js') }}"></script>
|
|
<script defer src="{{ url_for('static', filename='dash-cells.js') }}"></script>
|
|
<script defer src="{{ url_for('static', filename='dash-layout.js') }}"></script>
|
|
{# The palette, type and widget classes come from steward.css like every
|
|
other page; only the read-only page's own frame and its grid live here. #}
|
|
<style>
|
|
.share-header { background: var(--bg-card); padding: 0 1.5rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; border-bottom: 1px solid var(--border-mid); min-height: 48px; }
|
|
.share-name { font-family: var(--fs-font-display); font-weight: 500; font-size: 1.15rem; color: var(--gold); overflow-wrap: anywhere; }
|
|
.share-main { max-width: 2400px; }
|
|
/* The same layout as the signed-in dashboard: rows 70px, gaps 1rem, and
|
|
static/dash-layout.js sets the columns and each cell's place and height. */
|
|
.dash-grid { display: grid; grid-template-columns: repeat(12, 1fr); grid-auto-rows: 70px; gap: 1rem; }
|
|
.dash-grid > .dash-cell { margin-bottom: 0; min-width: 0; overflow: hidden; display: flex; flex-direction: column; }
|
|
.dash-cell .dash-cell-body { flex: 1; min-height: 0; overflow: auto; }
|
|
@media (max-width: 820px) {
|
|
.share-main { padding: 1rem; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<header class="share-header">
|
|
<span class="share-name">{{ dashboard.name }}</span>
|
|
<span style="font-size:0.78rem;color:var(--text-muted);">Steward · read-only view</span>
|
|
</header>
|
|
|
|
<main class="share-main">
|
|
{% if widgets %}
|
|
<div class="dash-grid">
|
|
{% for item in widgets %}
|
|
{% set gw = item.db %}
|
|
<div class="card ping-card dash-cell" data-id="{{ gw.id }}"
|
|
data-x="{{ gw.grid_x }}" data-y="{{ gw.grid_y }}" data-w="{{ gw.grid_w }}" data-h="{{ gw.grid_h }}"
|
|
style="grid-column:{{ gw.grid_x + 1 }} / span {{ gw.grid_w }};grid-row:{{ gw.grid_y + 1 }} / span {{ gw.grid_h }};">
|
|
<div class="widget-header">
|
|
<span class="widget-title" title="{{ item.title }}">{{ item.title }}</span>
|
|
<span class="widget-asof" aria-live="polite"></span>
|
|
</div>
|
|
{# Pass share token as &s= param so require_role allows through #}
|
|
<div class="dash-cell-body" id="widget-{{ gw.id }}"
|
|
hx-get="{{ item.hx_url }}&s={{ raw_token }}"
|
|
hx-trigger="load, retry{% if item.defn.poll %}, every {{ item.defn.poll_seconds or poll_interval }}s{% endif %}"
|
|
hx-request='{"timeout": 20000}'
|
|
data-poll="{{ '1' if item.defn.poll else '0' }}"
|
|
hx-swap="innerHTML"><div class="cell-state">Loading…</div></div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">This dashboard has no widgets.</div>
|
|
{% endif %}
|
|
</main>
|
|
|
|
<script>
|
|
/* Fit each cell to its content and place it for this screen's column count,
|
|
as on the signed-in dashboard (static/dash-layout.js). */
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var L = window.StewardDashLayout, grid = document.querySelector('.dash-grid');
|
|
if (!L || !grid) return;
|
|
var ROW_PX = 70, GAP_PX = 16;
|
|
function cells() { return Array.prototype.slice.call(grid.querySelectorAll('.dash-cell')); }
|
|
function relayout() {
|
|
var cols = L.columnsFor(grid.clientWidth);
|
|
var pos = L.pack(cells().map(function (el) {
|
|
var s = L.saved(el); s.id = el.dataset.id; s.h = el._fitH || s.h; return s;
|
|
}), cols);
|
|
grid.style.gridTemplateColumns = 'repeat(' + cols + ', 1fr)';
|
|
cells().forEach(function (el) {
|
|
var p = pos[el.dataset.id];
|
|
el.style.gridColumn = (p.x + 1) + ' / span ' + p.w;
|
|
el.style.gridRow = (p.y + 1) + ' / span ' + p.h;
|
|
});
|
|
}
|
|
var pending = null;
|
|
function fit(el) {
|
|
var rows = L.fitRows(el, ROW_PX + GAP_PX, GAP_PX, L.saved(el).h);
|
|
if (rows !== el._fitH) { el._fitH = rows; clearTimeout(pending); pending = setTimeout(relayout, 60); }
|
|
}
|
|
document.body.addEventListener('htmx:afterSettle', function (e) {
|
|
var el = e.detail.target && e.detail.target.closest && e.detail.target.closest('.dash-cell');
|
|
if (el) fit(el);
|
|
});
|
|
window.addEventListener('resize', function () {
|
|
clearTimeout(window._dashResize);
|
|
window._dashResize = setTimeout(function () { relayout(); cells().forEach(fit); }, 150);
|
|
});
|
|
relayout();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|