fix: make dashboard widgets closable (#14)
CI / build-and-test (pull_request) Successful in 1m27s

Every GridStackItem now renders a close button next to its drag grip,
mirroring the grip's hover affordance. Closing detaches the item
server-side; grid-stack.ts already unregisters widgets via its
MutationObserver, so the layout is persisted without an extra protocol.

Closable is on by default and can be turned off per item with
setClosable(false); GridStackItem.CloseEvent lets views react.

The GridStackView "remove last" toolbar button is dropped — per-widget
close replaces it, so the view no longer tracks a widget stack. Its
counter now only grows, keeping a closed widget's id from being handed
to a new widget (which would inherit the saved position).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue9ZtWUBQF4SuSHpzZ3zwq
This commit is contained in:
Pit Friedrich
2026-07-26 20:17:05 +02:00
parent 2e2c9aec3b
commit 7d23eef81b
8 changed files with 185 additions and 37 deletions
@@ -176,9 +176,45 @@ vaadin-app-layout::part(content) {
opacity: 1;
}
/* Touch devices never hover — keep the grip permanently visible there. */
/* Close button (GridStackItem#setClosable): same affordance as the grip, in
the outermost corner slot. The grip steps aside only when it is there, so a
non-closable item keeps its grip flush with the corner. */
.dialect-close-button {
position: absolute;
top: 14px;
right: 16px;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 6px;
color: var(--dialect-ink);
opacity: 0;
cursor: pointer;
transition: opacity 120ms ease;
}
.grid-stack-item:has(> .dialect-close-button) > .dialect-drag-handle {
right: 48px;
}
.grid-stack-item:hover > .dialect-close-button,
.dialect-close-button:focus-visible {
opacity: 0.65;
}
.dialect-close-button:hover,
.dialect-close-button:active {
opacity: 1;
color: var(--dialect-primary);
}
/* Touch devices never hover — keep the corner controls permanently visible. */
@media (pointer: coarse) {
.dialect-drag-handle {
.dialect-drag-handle,
.dialect-close-button {
opacity: 0.65;
}
}
@@ -24,15 +24,15 @@ card.registration=Registrierung
card.employees=Mitarbeiter
card.gridstackHint=Bedienung
gridstack.hint=Karten am Griff oben rechts verschieben, an der unteren rechten Ecke die Größe ändern. Das Layout wird im Browser gespeichert und beim nächsten Besuch wiederhergestellt.
gridstack.hint=Karten am Griff oben rechts verschieben, an der unteren rechten Ecke die Größe ändern, mit dem X oben rechts schließen. Das Layout wird im Browser gespeichert und beim nächsten Besuch wiederhergestellt.
gridstack.addWidget=Widget hinzufügen
gridstack.removeWidget=Letztes entfernen
gridstack.reset=Layout zurücksetzen
gridstack.status=Layout geändert {0} Widgets
gridstack.statusInitial=Layout unverändert
gridstack.widget=Widget {0}
gridstack.widgetText=Frei platzierbare Karte.
gridstack.dragHandle=Verschieben
gridstack.close=Schließen
chart.revenueSeries=Umsatz 2026
chart.pointClick=Serie {0}, Punkt {1}
@@ -24,15 +24,15 @@ card.registration=Registration
card.employees=Employees
card.gridstackHint=How it works
gridstack.hint=Move cards with the grip in their top right corner, resize them from the bottom right corner. The layout is stored in your browser and restored on your next visit.
gridstack.hint=Move cards with the grip in their top right corner, resize them from the bottom right corner, close them with the X in the top right corner. The layout is stored in your browser and restored on your next visit.
gridstack.addWidget=Add widget
gridstack.removeWidget=Remove last
gridstack.reset=Reset layout
gridstack.status=Layout changed {0} widgets
gridstack.statusInitial=Layout unchanged
gridstack.widget=Widget {0}
gridstack.widgetText=Freely placeable card.
gridstack.dragHandle=Move
gridstack.close=Close
chart.revenueSeries=Revenue 2026
chart.pointClick=Series {0}, point {1}
@@ -24,15 +24,15 @@ card.registration=Registro
card.employees=Empleados
card.gridstackHint=Cómo funciona
gridstack.hint=Mueve las tarjetas con el asa de la esquina superior derecha y cambia su tamaño desde la esquina inferior derecha. El diseño se guarda en el navegador y se restaura en la próxima visita.
gridstack.hint=Mueve las tarjetas con el asa de la esquina superior derecha, cambia su tamaño desde la esquina inferior derecha y ciérralas con la X de la esquina superior derecha. El diseño se guarda en el navegador y se restaura en la próxima visita.
gridstack.addWidget=Añadir widget
gridstack.removeWidget=Quitar el último
gridstack.reset=Restablecer diseño
gridstack.status=Diseño modificado {0} widgets
gridstack.statusInitial=Diseño sin cambios
gridstack.widget=Widget {0}
gridstack.widgetText=Tarjeta de colocación libre.
gridstack.dragHandle=Mover
gridstack.close=Cerrar
chart.revenueSeries=Ingresos 2026
chart.pointClick=Serie {0}, punto {1}