This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
The following commit(s) were added to refs/heads/main by this push:
new b886414 fix(layer-dashboards): pin the widget editor in place so it
always opens complete (#77)
b886414 is described below
commit b8864147e9b9cbc58838265f1610c6d1c5275ae5
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Thu Jun 25 09:50:29 2026 +0800
fix(layer-dashboards): pin the widget editor in place so it always opens
complete (#77)
---
CHANGELOG.md | 2 +-
.../admin/layer-templates/LayerDashboardsAdmin.vue | 94 +++++++++++++++-------
2 files changed, 65 insertions(+), 31 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb9234e..d986856 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -37,7 +37,7 @@ The version line is shared by every package in the monorepo
(apps + shared packa
### Layer dashboard editor
-- **The widget editor's move / delete controls stay reachable.** On the Layer
dashboards admin, the per-widget editor's `Up` / `Down` / `Delete` row is now a
pinned footer instead of the last thing in a scrolling panel, and the panel
sizes itself to the visible area — so Delete no longer slips below the fold on
a long form or a short board. Adding a widget also scrolls the new widget into
view, next to the editor that opens for it, instead of leaving it off-screen at
the bottom of the canvas.
+- **The widget editor pins in place beside the canvas and always opens
complete.** On the Layer dashboards admin, clicking a widget — anywhere on the
board, including the bottom rows — now opens the per-widget editor pinned next
to the canvas and fully visible, without scrolling the page (a sticky panel
used to get clipped past the bottom of a tall board, hiding the editor's top or
its `Up` / `Down` / `Delete` row). The move / delete controls sit in a pinned
footer, and the editor tucks [...]
## 0.7.0
diff --git
a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
index 52288fe..41d0666 100644
--- a/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
+++ b/apps/ui/src/features/admin/layer-templates/LayerDashboardsAdmin.vue
@@ -601,28 +601,63 @@ watch(
watch([visibleScopes, activeScope], () => void nextTick(updateScopeScroll));
function onWinResizeScope(): void { updateScopeScroll(); }
-// ── Editor drawer height. The drawer is a sticky sidebar that sits below the
-// card header, so a fixed `100vh - 52px` overshoots (the header offsets the
-// drawer's top) and pushes its pinned footer — Up / Down / Delete — below the
-// fold; worst on a short board you can't scroll far enough to pin. Size it to
-// the live space from wherever it currently sits down to the viewport bottom
-// instead, so the footer is always visible. Scroll listens in the capture
-// phase so the inner content pane's scroll (not just window) re-syncs it.
+// ── Editor drawer position. The drawer edits the selected widget. A sticky
+// drawer gets clipped once you scroll past the bottom of the tall widget
canvas
+// (its containing block), so a bottom-row widget opens with the editor's top
+// off-screen. Pin it to the viewport with `position: fixed` instead,
overlaying
+// the grid column the layout already reserves for it — so it's always fully
+// visible IN PLACE wherever you click in the canvas, without scrolling the
page.
+// JS owns top/left/width/height because the column's x-position tracks the
+// canvas width (re-measured on layout change), while the y-extent is the
+// viewport below the topbar.
const drawerEl = ref<HTMLElement | null>(null);
-function syncDrawerHeight(): void {
+const editorCardEl = ref<HTMLElement | null>(null);
+const DRAWER_COL = 360;
+function positionDrawer(): void {
const el = drawerEl.value;
- if (!el) return;
- el.style.height = `${Math.max(220, window.innerHeight -
el.getBoundingClientRect().top - 8)}px`;
-}
+ const card = editorCardEl.value;
+ const split = el?.closest('.editor-split') as HTMLElement | null;
+ const main = document.querySelector('.sw-main');
+ if (!el || !card || !split || !main) return;
+ // Hide the fixed drawer when the editor card is scrolled out of view, so it
+ // doesn't float over the scope-config cards above it.
+ const cr = card.getBoundingClientRect();
+ if (cr.bottom < 80 || cr.top > window.innerHeight) {
+ el.style.display = 'none';
+ return;
+ }
+ el.style.display = 'flex';
+ const top = Math.max(0, Math.round(main.getBoundingClientRect().top));
+ el.style.top = `${top}px`;
+ el.style.right = 'auto';
+ el.style.left = `${Math.round(split.getBoundingClientRect().right -
DRAWER_COL)}px`;
+ el.style.width = `${DRAWER_COL}px`;
+ el.style.height = `${window.innerHeight - top}px`;
+}
+// Re-place the drawer when the editor card resizes (window resize, sidebar
+// collapse, scope switch) — its right column moves with the canvas width.
+let drawerLayoutObs: ResizeObserver | null = null;
+watch(editorCardEl, (el) => {
+ drawerLayoutObs?.disconnect();
+ drawerLayoutObs = null;
+ if (el && typeof ResizeObserver !== 'undefined') {
+ drawerLayoutObs = new ResizeObserver(() => positionDrawer());
+ drawerLayoutObs.observe(el);
+ }
+});
onMounted(() => {
window.addEventListener('resize', onWinResizeScope);
- window.addEventListener('scroll', syncDrawerHeight, { capture: true,
passive: true });
- window.addEventListener('resize', syncDrawerHeight, { passive: true });
+ window.addEventListener('resize', positionDrawer, { passive: true });
+ // Scroll only toggles the drawer's visibility (its x/width are fixed);
capture
+ // so the inner content pane's scroll fires it too.
+ window.addEventListener('scroll', positionDrawer, { capture: true, passive:
true });
});
onBeforeUnmount(() => {
window.removeEventListener('resize', onWinResizeScope);
- window.removeEventListener('scroll', syncDrawerHeight, { capture: true });
- window.removeEventListener('resize', syncDrawerHeight);
+ window.removeEventListener('resize', positionDrawer);
+ window.removeEventListener('scroll', positionDrawer, { capture: true });
+ drawerLayoutObs?.disconnect();
+ drawerLayoutObs = null;
scopeResizeObs?.disconnect();
scopeResizeObs = null;
});
@@ -710,7 +745,7 @@ function moveWidget(i: number, dir: -1 | 1): void {
const selectedIdx = ref<number | null>(null);
/* Re-fit the drawer to the viewport when it opens / the target widget changes
* (content height differs); the scroll + resize listeners keep it fitted
after. */
-watch(selectedIdx, () => void nextTick(syncDrawerHeight));
+watch(selectedIdx, () => void nextTick(positionDrawer));
/** When the user switches scope or layer we drop the selection so the
* drawer doesn't refer to a widget that no longer exists. */
@@ -3675,7 +3710,7 @@ const namingTest = computed<NamingTestResult>(() => {
</div>
</section>
- <section v-else class="sw-card editor-card">
+ <section v-else ref="editorCardEl" class="sw-card editor-card">
<div class="card-head">
<h4>{{ scopeLabel(activeScope) }} widgets</h4>
<span class="sub">
@@ -5530,19 +5565,18 @@ const namingTest = computed<NamingTestResult>(() => {
flex-direction: column;
background: var(--sw-bg-1);
border-left: 1px solid var(--sw-line);
- /* Sticky within the scrolling main pane (topbar is fixed above it, so
- * top: 0 pins just below it) and full-height, so the editor uses the
- * whole viewport and follows the canvas scroll instead of being a short
- * box stranded at the top of a tall grid cell. `align-self: start`
- * stops the grid from stretching it (which would defeat sticky). */
- position: sticky;
- top: 0;
- align-self: start;
- /* Height is set in JS (syncDrawerHeight) to the live space from the drawer's
- * top to the viewport bottom, so the pinned footer never falls below the
- * fold. This calc is just the pre-JS / no-JS fallback. */
- height: calc(100vh - 96px);
- max-height: calc(100vh - 52px);
+ /* Pinned to the viewport (positionDrawer sets top/left/width/height) so the
+ * editor is always fully visible IN PLACE wherever you click in the canvas,
+ * overlaying the reserved grid column — a sticky drawer clips past the
bottom
+ * of the tall canvas. The values below are a pre-JS fallback, refined on the
+ * next tick. */
+ position: fixed;
+ z-index: 5;
+ top: 44px;
+ right: 28px;
+ width: 360px;
+ height: calc(100vh - 44px);
+ box-shadow: -8px 0 24px -12px rgba(0, 0, 0, 0.5);
}
.drawer-head {
display: flex;