diff --git a/src/admin/web/app/dashboard-inline-layout.js b/src/admin/web/app/dashboard-inline-layout.js index cc0d384..b1b7c11 100644 --- a/src/admin/web/app/dashboard-inline-layout.js +++ b/src/admin/web/app/dashboard-inline-layout.js @@ -79,7 +79,8 @@ window.rustProxyInlineLayout = (() => { const explicit = draft.heights?.[node.dataset.dashboardWidget] !== undefined; const heightValue = node.querySelector(".overview-inline-height-value"); if (heightValue) { - heightValue.textContent = `↕${rows.span}${explicit ? "" : " A"}`; + heightValue.textContent = `↕${rows.span}`; + heightValue.classList.toggle("is-auto", !explicit); heightValue.title = explicit ? t("overviewLayoutHeightFixed") : t("overviewLayoutHeightAuto"); } const lower = node.querySelector(".overview-inline-lower"); @@ -161,7 +162,9 @@ window.rustProxyInlineLayout = (() => { control("−", `${t("overviewLayoutLower")}: ${title}`, "overview-inline-lower", () => resizeHeight(id, -1)), heightValue, control("+", `${t("overviewLayoutTaller")}: ${title}`, "overview-inline-taller", () => resizeHeight(id, 1))); - bar.append(grip, dashboardWidgetTypeBadge(id), sizeControls, heightControls, + const name = el("span", title, "overview-inline-name"); + name.title = title; + bar.append(grip, name, dashboardWidgetTypeBadge(id), sizeControls, heightControls, control("←", `${t("moveUp")}: ${title}`, "overview-inline-up", () => nudge(id, -1)), control("→", `${t("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)), control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id))); @@ -197,6 +200,8 @@ window.rustProxyInlineLayout = (() => { if (modalButton) modalButton.disabled = false; if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden], original.sizes, original.heights); draft = original = null; + // Recompute from scratch so the saved view is exactly what was arranged. + window.rustProxyOverviewRows?.flush?.(); } async function begin() { if (draft || opening || saving || $("overview-presets-dialog")?.open) return; diff --git a/src/admin/web/app/overview-rows.js b/src/admin/web/app/overview-rows.js index bf6b41d..dde4aec 100644 --- a/src/admin/web/app/overview-rows.js +++ b/src/admin/web/app/overview-rows.js @@ -35,12 +35,17 @@ window.rustProxyOverviewRows = (() => { lastWidth = grid.clientWidth; const items = cards().filter(node => !node.hidden && node.getClientRects().length); if (!items.length) return; - grid.classList.add("is-measuring-rows"); + // Measure as the saved (locked) dashboard: editing styles move a few + // controls inside cards, which must not change the layout being edited. + const editing = grid.classList.contains("is-unlocked"); + if (editing) grid.classList.remove("is-unlocked"); + grid.classList.add("has-row-units", "is-measuring-rows"); // Bounding boxes include CSS zoom (Settings → Interface scale); grid tracks, // gaps and the row unit are in CSS pixels, so divide measurements by it. const zoom = zoomOf(); const natural = items.map(node => node.getBoundingClientRect().height / zoom); grid.classList.remove("is-measuring-rows"); + if (editing) grid.classList.add("is-unlocked"); const gap = parseFloat(getComputedStyle(grid).rowGap) || 0; const isStat = node => node.classList.contains("stat"); // The unit is half of the smallest stat card, so every card ends within a diff --git a/src/admin/web/css/30-overview.css b/src/admin/web/css/30-overview.css index 5feef7e..d52214a 100644 --- a/src/admin/web/css/30-overview.css +++ b/src/admin/web/css/30-overview.css @@ -604,7 +604,7 @@ html[data-dashboard-density=ultra] #panel-overview .stat-cache-outcomes .cache-m color: var(--text); } -#panel-overview .dashboard-overview-grid.is-unlocked .stat-with-action .stat-corner-action { +#panel-overview .dashboard-overview-grid.is-unlocked:not(.has-row-units) .stat-with-action .stat-corner-action { top: 44px; } @@ -626,7 +626,7 @@ html[data-dashboard-density=ultra] #panel-overview .stat-cache-outcomes .cache-m white-space: nowrap; } -#panel-overview .dashboard-overview-grid.is-unlocked .backend-latency-stat .backend-health-origins { +#panel-overview .dashboard-overview-grid.is-unlocked:not(.has-row-units) .backend-latency-stat .backend-health-origins { top: 44px; } @@ -1243,9 +1243,95 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline { position: relative; outline: 1px dashed var(--accent); outline-offset: -2px; +} + +/* Legacy grouped layout: reserve room for the editing toolbar. With row units the + toolbar overlays the card header instead, so the unlocked grid has exactly the + geometry of the saved one (what you arrange is what you get). */ +#panel-overview .dashboard-overview-grid.is-unlocked:not(.has-row-units)>[data-dashboard-widget] { padding-top: 40px !important; } +/* Row-unit editing toolbar: a slim opaque header (handle, card name, hide) that + expands to a second line with width / height / move controls on hover or focus. */ +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-toolbar { + top: 2px; + left: 2px; + right: 2px; + flex-wrap: wrap; + align-items: center; + gap: 3px; + padding: 3px; + overflow: visible; + background: var(--panel2, var(--panel)); + border-bottom: 1px solid var(--line); + border-radius: var(--rp-radius-control, 0); + box-shadow: 0 4px 10px var(--shadow-mid, #0006); +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-toolbar button { + min-width: 22px; + min-height: 22px; + padding: 1px 5px; + font-size: 11px; +} + +#panel-overview .overview-inline-name { + display: none; +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-name { + display: block; + flex: 1 1 0; + min-width: 0; + overflow: hidden; + color: var(--text); + font: 600 11px var(--sans); + text-overflow: ellipsis; + white-space: nowrap; +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-toolbar .overview-inline-hide { + order: 1; + margin-left: 0; +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-toolbar :is(.overview-widget-type, .overview-inline-width, .overview-inline-up, .overview-inline-down) { + display: none; + order: 2; +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units>[data-dashboard-widget]:is(:hover, :focus-within) .overview-inline-toolbar :is(.overview-inline-width, .overview-inline-up, .overview-inline-down) { + display: inline-flex; +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units>[data-dashboard-widget]:is(:hover, :focus-within) .overview-inline-toolbar::after { + content: ""; + order: 1; + flex-basis: 100%; + height: 0; +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units>[data-dashboard-widget]:is(:hover, :focus-within) .overview-inline-toolbar .overview-inline-height[hidden] { + display: none; +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-width-value { + min-width: 28px; + font-size: 10px; +} + +/* Automatic height (fits the content) is shown muted; a fixed height in full colour. */ +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-height-value.is-auto { + color: var(--muted); +} + +#panel-overview .dashboard-overview-grid.is-unlocked.has-row-units .overview-inline-width + .overview-inline-width { + margin-left: 4px; + padding-left: 6px; + border-left: 1px solid var(--line); +} + #panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget]:hover, #panel-overview .dashboard-overview-grid.is-unlocked>[data-dashboard-widget]:focus-within { outline-style: solid; @@ -2391,7 +2477,7 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over /* Compact stat cards: a card made only of title, value and one secondary line shows that line beside the value (right-aligned) instead of on a line of its own. */ -#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small))):has(>small)) { +#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small, .overview-inline-toolbar))):has(>small)) { display: grid; grid-template-columns: minmax(0, 1fr) auto; column-gap: 10px; @@ -2399,16 +2485,16 @@ html[data-dashboard-density] #panel-overview .stat :is(.stat-chart-legend, .over align-content: start; } -#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small))):has(>small))>span:first-child { +#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small, .overview-inline-toolbar))):has(>small))>span:first-child { grid-column: 1 / -1; } -#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small))):has(>small))>strong { +#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small, .overview-inline-toolbar))):has(>small))>strong { grid-column: 1; min-width: 0; } -#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small))):has(>small))>small { +#panel-overview .stats-grid>.stat:is(:not(.stat-donut, .stat-meter, .stat-with-action, .stat-chart):not(:has(>:not(span, strong, small, .overview-inline-toolbar))):has(>small))>small { grid-column: 2; max-width: 14em; margin: 0;