Files
ever-gauzy/apps
joel kalema 6ceaccc4db Fix/custom dashboard drag and drop (#10269)
* fix(dashboard): assign both grid axes when re-flowing a canvas layout

`packLayout` only ever corrected `y`, leaving `x` exactly where it was. The
canvas derives reading order from `(y, x)`, so reordering two widgets that
shared a row was a silent no-op: the sort read the untouched `x` values back
and restored the original order.

`flowLayout` assigns both coordinates from the order of the array, which is
what the builder actually models -- widgets are appended, reordered by drag and
resized from a menu, never placed at an arbitrary column. It is the sparse
packing algorithm from CSS Grid 8.5 step 4, because the canvas now renders its
cells with spans only and lets the browser place them; the two have to agree or
the persisted `x`/`y` would describe a layout nobody sees. Verified against a
real browser across four layouts, including ragged heights.

Sparse rather than dense on purpose: dense back-fills earlier gaps, which would
let a widget render before one that precedes it in the array, and the index CDK
reports for a drop is a position in that array.

Also adds `readingOrder`, and the pure drop-point geometry (`isPointInRect`,
`dropIndexAtPoint`) the canvas uses to resolve a release that landed between
cells. `packLayout` is untouched: `normalizeLayout` still depends on it.

* test(dashboard): cover canvas flow layout and drop-point geometry

Locks in the two contracts that are easy to break without noticing:

- `flowLayout` must pack exactly like CSS grid auto-placement, because the
  browser is what actually positions the cells. The ragged-heights case is the
  one that catches drift -- `d` tucks in beside the tall `a` at column 4 rather
  than starting a fresh row.
- A move between two widgets on the same row must survive the reading-order
  sort that follows it. That is the regression that made drag-and-drop look
  dead.

Plus the drop-point geometry: below everything, in a gutter, on the next row,
off either edge, and the half-open edge rule that stops two neighbouring cells
both claiming a boundary pixel.

* chore(i18n): add widget height labels and update the builder drag copy

`HEIGHT` / `HEIGHT_ROWS` back the new height ladder in the widget menu.

The drag copy no longer points at the grip: the whole card is the drag surface
now, and the rail is the keyboard control, so the hint and the handle label say
that instead.

Only `en.json` carries these -- the rest of the builder strings are English-only
too, and the other locales fall back.

* fix(dashboard): make canvas drag-and-drop actually move widgets

Dragging a widget did nothing at all, in any direction.

`cdkDropListOrientation="mixed"` selects CDK's `MixedSortStrategy`, whose
`sort()` shows the new position by MOVING THE PLACEHOLDER NODE between cells
(`overlapElement.after(current)`) and does nothing else. Every cell was pinned
with `grid-column: x+1 / span w`, so DOM order had no bearing on where anything
rendered: CDK was reordering nodes inside a grid that ignored node order.
Measured in a browser -- moving the first cell to the end left every widget's
pixel position identical.

Cells now declare spans only and the grid auto-places them in DOM order, so the
sort is visible and the placeholder shows the real destination.

Three further fixes:

- `cdkDragHandle` is gone. With it, only the 16px rail could start a drag and
  grabbing the card -- what everyone tries -- did nothing. The whole cell is the
  drag surface; the kebab still opens on click because CDK needs 5px of movement
  before treating a press as a drag. The rail stays as the arrow-key control,
  which is the only way to rearrange without a mouse.
- A release over the canvas' empty space is resolved from the drop point.
  CDK only re-sorts while the pointer is over another cell, so dropping into a
  gap, the ragged space beside a tall widget, or the run-off below the last row
  silently kept the index of the last cell crossed -- dragging a widget to the
  bottom put it back near where it started.
- A release off the canvas changes nothing. CDK leaves a rejected or stray item
  at whatever index it sorted to on the way past, so flicking a widget towards
  the palette (which refuses it) quietly reordered the canvas. The canvas rect
  is measured in the handler rather than read from `isPointerOverContainer`,
  which CDK answers from a rect cached at drag start -- a layout shift mid-drag
  leaves it tens of pixels stale and rejects drops made well inside.

Also drops the dead `_normalizePlacementHeight`, which was a no-op today but
would have squashed any widget declaring `minSize.h === 1` to one row on load,
and swaps CDK's default preview (a full clone of the cell -- a large translucent
card covering the slot being aimed at, with a blank cloned canvas) for a chip.

Verified with a real CDK harness driven by Playwright: 17 scenarios covering
horizontal, vertical up and down, to the start and end, ragged rows, gaps, dead
space, palette drops, rejected drops, no-op drags and a scrolled page.

* fix(dashboard): stop canvas rows inflating and tighten the grid

`align-content` behaves as `stretch` on a grid and `grid-auto-rows`' `auto`
maximum is stretchable, so the rows expanded to fill `min-height`: a single
two-row widget on a fresh canvas grew to the full 14rem. `align-content: start`
keeps rows at their own size, and the space left over becomes the run-off the
drop-point fallback reads as "put it at the end".

`min-height` now applies only while editing, where it is that drop target. A
read-only canvas has nothing to drop, and reserving 14rem under two small
widgets was just empty page.

Density: the row unit goes 64px -> 52px and the gap 1rem -> 0.75rem, so a
six-row widget is 372px rather than 464px. Widget bodies scroll, so a tighter
unit costs a little scrolling inside a big widget and buys the whole canvas
fitting on one screen.

The grab rail is visible from the moment edit mode starts -- at `opacity: 0`
the only affordance for rearranging was hovering a strip nobody knew was there
-- and the cell reserves a gutter for it so it sits beside the card instead of
over the first 14px of its content.

Drops the sibling-transform transition: the mixed sort strategy re-orders nodes
and never transforms siblings, so it was dead.

* fix(dashboard): auto-scroll the dashboard while dragging a widget

This host element is the dashboard's scroll surface -- `dashboard.component.scss`
gives `router-outlet ~ *` the `overflow-y: auto`, because the app layout's own
scroller is clamped so the header and footer stay put.

`CdkDropList` only auto-scrolls the viewport and ancestors registered with
`ScrollDispatcher`, which is what `cdkScrollable` does. Without it, dragging a
widget towards the bottom of a canvas taller than the window stopped at the
fold: the page never scrolled, so the lower half was unreachable by drag.

* style(dashboard): match the page spacing to the canvas grid

The page stacked its heading, hint bar, tab strip and body on a 1rem rhythm
wrapped around a 0.75rem grid, which read as a loose frame round a tight
canvas. Same gap on both now, and the heading loses a quarter rem it was not
using.

* feat(dashboard): show a drop slot when dragging a widget from the palette

CDK carries the placeholder into whichever list the pointer is over, so the
default one -- a clone of the palette row -- was being dropped into the canvas
GRID as a full-width list entry.

The slot is styled in the palette's own sheet rather than the canvas': CDK
renders it from this component's template, so it carries this component's style
attribute and a canvas-scoped rule would never match it. Its grid spans are
inert in the palette's own block list and match the most common widget
footprint, since the real one is only known once the drop resolves the widget's
`defaultSize`.

* feat(dashboard): let a widget's height be resized from its menu

The menu offered width only, so a widget arrived at whatever `defaultSize` its
author picked and the only way to stop one eating half the canvas was to remove
it.

`resized` now carries either dimension and the menu shows two ladders. A ladder
rather than every value in range -- a chart allowing 3 to 10 rows would put
eight near-identical buttons in the menu -- but both of the widget's declared
bounds are always included, so the shortest and tallest size it supports are
always reachable.

* style(dashboard): tighten widget card chrome

The header and body paddings are the frame's fixed overhead: paid by every card
on the canvas at every size, and they consumed around 2.4rem of a two-row widget
before its content got a pixel. Tight here so the grid can be tight.

Also renames the menu's width ladder classes to serve both axes.

* fixed the AI comments

* fixed the AI comments
2026-09-22 17:55:55 +02:00
..
2026-04-01 23:49:57 +02:00
2026-02-20 09:52:34 +01:00