feat(quick-260916-dyv): Bearbeiten-Schalter unten rechts (Rand oben 28 px), ganze Kachel als Griff mit Kopfleiste, cancel-Selektor, kein Ueberlappen beim Ablegen
- page.tsx: feste Aktionsleiste `fixed bottom-6 right-6 z-20` mit "Widget
hinzufuegen" (nur Bearbeitungsmodus) links neben dem Stift/Haekchen; Block
oben rechts und mt-8-Wrapper entfernt, Grid direkt im Container p-2
(12 + 8 + 8 = 28 px statt 60 px); page.test.tsx NEU mit 3 Tests
- widget-wrapper.tsx: Karte ist im Bearbeitungsmodus der Griff (cursor-grab),
20-px-Overlay-Kopfleiste mit Griff-Symbol und Tooltip dragHint, Loesch-Knopf
in der Kopfleiste mit data-no-drag; Rumpf h-full unveraendert (Hoehenkette)
- dashboard-grid.tsx: WIDGET_DRAG_HANDLE_SELECTOR, WIDGET_DRAG_CANCEL_SELECTOR
(input, textarea, select, button, a, [contenteditable], [data-no-drag],
.widgetNoDrag), threshold 3; FREE_PLACEMENT_COMPACTOR = noCompactor +
preventCollision: true (freie Platzierung bleibt, Commit c8f3361)
- dashboard-grid.test.tsx: Mock per importOriginal (echter noCompactor),
Tests 6-8 (dragConfig-Pin, Compactor-Pin, cancel/handle-Semantik im DOM)
- edit-mode-toggle.tsx: schwebend (shadow-lg, inaktiv border + bg-card)
- de.json/en.json: widgets.dragHint
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018N9CD3ebPKm1b32bPpBknY
This commit is contained in:
@@ -15,7 +15,14 @@ interface WidgetWrapperProps {
|
||||
|
||||
/**
|
||||
* Wrapper card for all dashboard widgets.
|
||||
* In edit mode: shows drag handle (top) and delete button (top-right).
|
||||
*
|
||||
* quick-260916-dyv: Im Bearbeitungsmodus ist die GANZE Karte der Griff
|
||||
* (`widget-drag-handle`, `cursor-grab`). Eine 20 px hohe Kopfleiste mit
|
||||
* Griff-Symbol liegt als Overlay ueber dem oberen Kachelrand — nur optischer
|
||||
* Hinweis (Tooltip `widgets.dragHint`), kein eigener Griff. Der Loesch-Knopf
|
||||
* sitzt rechts in dieser Kopfleiste und traegt `data-no-drag`; welche Ziele
|
||||
* KEIN Ziehen starten (Eingabefelder, Knoepfe, Links, [data-no-drag],
|
||||
* .widgetNoDrag), entscheidet der cancel-Selektor in dashboard-grid.tsx.
|
||||
* Renders the actual widget component via WIDGET_REGISTRY lookup.
|
||||
*/
|
||||
export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperProps) {
|
||||
@@ -23,46 +30,69 @@ export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperPro
|
||||
const definition = WIDGET_REGISTRY[widget.widgetType as WidgetType];
|
||||
const WidgetComponent = definition?.component;
|
||||
|
||||
const cardClassName = isEditMode
|
||||
? 'widget-drag-handle relative h-full w-full cursor-grab overflow-hidden rounded-lg border border-primary/40 bg-card shadow-sm active:cursor-grabbing'
|
||||
: 'relative h-full w-full overflow-hidden rounded-lg border border-primary/20 bg-card shadow-sm';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative h-full w-full overflow-hidden rounded-lg border border-primary/20 bg-card shadow-sm"
|
||||
className={cardClassName}
|
||||
data-widget-id={widget.id}
|
||||
role="article"
|
||||
aria-label={definition ? t(definition.nameKey) : widget.widgetType}
|
||||
>
|
||||
{/* Edit mode: drag handle */}
|
||||
{/* Edit mode: Overlay-Kopfleiste (Griff-Hinweis) mit Loesch-Knopf rechts.
|
||||
Bewusst `absolute` und NICHT im Fluss: der Rumpf bleibt h-full, die
|
||||
Hoehenkette Karte -> Rumpf bleibt definit (cqh loest weiter auf). */}
|
||||
{isEditMode && (
|
||||
<div className="widget-drag-handle flex h-[6px] w-full cursor-grab items-center justify-center bg-muted active:cursor-grabbing">
|
||||
<div className="h-[2px] w-8 rounded-full bg-muted-foreground/40" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit mode: delete button */}
|
||||
{isEditMode && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove(widget.id);
|
||||
}}
|
||||
className="absolute right-1 top-1 z-10 flex h-6 w-6 items-center justify-center rounded-full bg-card text-muted-foreground transition-colors hover:bg-destructive hover:text-destructive-foreground"
|
||||
aria-label={t('deleteTooltip')}
|
||||
title={t('deleteTooltip')}
|
||||
<div
|
||||
className="absolute inset-x-0 top-0 z-10 flex h-5 items-center justify-center bg-muted/70"
|
||||
title={t('dragHint')}
|
||||
data-testid="widget-drag-head"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="currentColor"
|
||||
className="text-muted-foreground"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
<circle cx="5" cy="9" r="1.5" />
|
||||
<circle cx="12" cy="9" r="1.5" />
|
||||
<circle cx="19" cy="9" r="1.5" />
|
||||
<circle cx="5" cy="15" r="1.5" />
|
||||
<circle cx="12" cy="15" r="1.5" />
|
||||
<circle cx="19" cy="15" r="1.5" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemove(widget.id);
|
||||
}}
|
||||
className="absolute right-0.5 top-0 flex h-5 w-5 items-center justify-center rounded-full bg-card text-muted-foreground transition-colors hover:bg-destructive hover:text-destructive-foreground"
|
||||
aria-label={t('deleteTooltip')}
|
||||
title={t('deleteTooltip')}
|
||||
data-no-drag=""
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Widget body.
|
||||
@@ -70,8 +100,10 @@ export function WidgetWrapper({ widget, isEditMode, onRemove }: WidgetWrapperPro
|
||||
damit die Widget-Inhalte per cqw/cqh mit der Kachel skalieren. Braucht eine
|
||||
definite Hoehe — die kommt ueber h-full aus der Karte, die das RGL-Element
|
||||
mit Pixelhoehe fuellt. Steht am Rumpf statt an der Karte, weil die Karte
|
||||
im Bearbeitungsmodus zusaetzlich den Griff traegt. */}
|
||||
<div className={`@container-size h-full ${isEditMode ? 'pt-0' : ''}`}>
|
||||
im Bearbeitungsmodus zusaetzlich den Griff traegt.
|
||||
quick-260916-dyv: Die Kopfleiste liegt als Overlay ueber dem Rumpf und
|
||||
aendert die Hoehenkette nicht. */}
|
||||
<div className="@container-size h-full">
|
||||
{WidgetComponent ? (
|
||||
<WidgetComponent
|
||||
instanceId={widget.id}
|
||||
|
||||
Reference in New Issue
Block a user