@grafloria/element 0.4.32 → 0.4.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grafloria/element",
3
- "version": "0.4.32",
3
+ "version": "0.4.33",
4
4
  "type": "module",
5
5
  "main": "./src/index.js",
6
6
  "types": "./src/index.d.ts",
@@ -1099,6 +1099,7 @@ export function createDashboardHandle(ctx) {
1099
1099
  return { move, collapse: remaining.length > 0 ? [] : collapseOf(boardId) };
1100
1100
  },
1101
1101
  joinTargets: [...ctx.layoutOf.keys()].filter((id) => { var _a, _b; return id !== containerId && id !== pageId && ((_a = ctx.layoutOf.get(id)) !== null && _a !== void 0 ? _a : (_b = specById.get(id)) === null || _b === void 0 ? void 0 : _b.layout) === 'tabs' && !!model.getGroup(id) && !insidePage(id); }),
1102
+ stripHeight: (targetId) => tabStripReserve(ctx.tabsOf.get(targetId), Math.max(1, liveCount(targetId))),
1102
1103
  stripIndex: (targetId, cx, cy) => {
1103
1104
  const strip = ctx.tabStrips.get(targetId);
1104
1105
  if (!strip)
@@ -567,6 +567,8 @@ export interface TearOutPlan {
567
567
  markDrop(targetId: string | null, index: number | null): void;
568
568
  /** The tab index a CLIENT point means along `targetId`'s strip — null when the point is not over that strip. */
569
569
  stripIndex(targetId: string, clientX: number, clientY: number): number | null;
570
+ /** The strip's height on `targetId`, px — the band above its body. */
571
+ stripHeight(targetId: string): number;
570
572
  /** The commands that REORDER the page to `index` along its own strip. Empty when nothing changes. */
571
573
  reorder(index: number): Command[];
572
574
  /**
@@ -650,17 +652,34 @@ interface AdoptedLeg {
650
652
  x: number;
651
653
  y: number;
652
654
  }): void;
655
+ /** Put the tile at a PRESCRIBED cell (a split's half, a docking band): sized to it, unlocked tiles pushed. Answers whether it sits there. */
656
+ place(cell: {
657
+ x: number;
658
+ y: number;
659
+ w: number;
660
+ h: number;
661
+ }): boolean;
662
+ /** Every other tile's cell before the ghost entered — the layout a row insert translates. */
663
+ baseline(): Map<string, CellRect>;
653
664
  /** Undo the adoption: target board back to its pre-entry layout. */
654
665
  abort(): void;
655
666
  /**
656
667
  * Close the leg for commit: returns the target-side displaced commands, the
657
- * tile's final cell and its projected rect. Null when the tile is somehow
658
- * gone (treat as abort).
668
+ * tile's final cell and its projected rect and the member GROUPS the
669
+ * adoption moved (the node commands skip groups; a dock that pushed sections
670
+ * commits them through these). Null when the tile is somehow gone.
659
671
  */
660
672
  finalize(): {
661
673
  commands: Command[];
662
674
  cell: CellRect;
663
675
  rect: WorldRect;
676
+ groups: Array<{
677
+ id: string;
678
+ cellBefore: CellRect;
679
+ cellAfter: CellRect;
680
+ frameBefore: WorldRect;
681
+ frameAfter: WorldRect;
682
+ }>;
664
683
  } | null;
665
684
  }
666
685
  /**
@@ -2453,6 +2453,34 @@ export function bindDashboardGrid(api, group, options = {}) {
2453
2453
  syncPlaceholder();
2454
2454
  return {
2455
2455
  groupId: group.id,
2456
+ baseline: () => startCells,
2457
+ place: (cell) => {
2458
+ if (!engine.getItem(node.id) && !engine.add({ id: node.id, x: 0, y: engine.rows(), w: cell.w, h: cell.h }))
2459
+ return false;
2460
+ const it = engine.getItem(node.id);
2461
+ if (!it)
2462
+ return false;
2463
+ // Shrink, move, then grow. A resize is clamped at the board's edge
2464
+ // where the ghost SITS, so a cell wider than fits there (a full-width
2465
+ // dock from a ghost parked mid-board) is reached by moving first —
2466
+ // and a cell narrower than the ghost by shrinking first, so the move
2467
+ // itself fits.
2468
+ const w0 = Math.min(it.w, cell.w);
2469
+ const h0 = Math.min(it.h, cell.h);
2470
+ if (w0 !== it.w || h0 !== it.h)
2471
+ engine.resizeCheck(node.id, w0, h0);
2472
+ const moved = engine.getItem(node.id);
2473
+ if (moved && (moved.x !== cell.x || moved.y !== cell.y))
2474
+ engine.moveCheck(node.id, cell.x, cell.y, { gate: false });
2475
+ const now = engine.getItem(node.id);
2476
+ if (now && (now.w !== cell.w || now.h !== cell.h))
2477
+ engine.resizeCheck(node.id, cell.w, cell.h);
2478
+ lastWant = null;
2479
+ project();
2480
+ syncPlaceholder();
2481
+ const at = engine.getItem(node.id);
2482
+ return !!at && at.x === cell.x && at.y === cell.y && at.w === cell.w && at.h === cell.h;
2483
+ },
2456
2484
  move: (w) => {
2457
2485
  const item = engine.getItem(node.id);
2458
2486
  if (!item)
@@ -2506,11 +2534,29 @@ export function bindDashboardGrid(api, group, options = {}) {
2506
2534
  const cell = { x: item.x, y: item.y, w: item.w, h: item.h };
2507
2535
  const rect = cellToRect(item, frame(), geom(), rows());
2508
2536
  const commands = buildCommitCommands(deltasSince(startCells, startGeom, node.id));
2537
+ const groups = [];
2538
+ for (const [id, before] of startCells) {
2539
+ if (!isGroupMember(id))
2540
+ continue;
2541
+ const it = engine.getItem(id);
2542
+ const g0 = startGeom.get(id);
2543
+ if (!it || !g0)
2544
+ continue;
2545
+ if (it.x === before.x && it.y === before.y && it.w === before.w && it.h === before.h)
2546
+ continue;
2547
+ groups.push({
2548
+ id,
2549
+ cellBefore: before,
2550
+ cellAfter: { x: it.x, y: it.y, w: it.w, h: it.h },
2551
+ frameBefore: { x: g0.pos.x, y: g0.pos.y, width: g0.size.width, height: g0.size.height },
2552
+ frameAfter: cellToRect(it, frame(), geom(), rows()),
2553
+ });
2554
+ }
2509
2555
  engine.endGesture();
2510
2556
  adoptedGhostId = null;
2511
2557
  disarmGlideSoon();
2512
2558
  syncPlaceholder();
2513
- return { commands, cell, rect };
2559
+ return { commands, cell, rect, groups };
2514
2560
  },
2515
2561
  };
2516
2562
  };
@@ -3136,69 +3182,374 @@ export function bindDashboardGrid(api, group, options = {}) {
3136
3182
  const rect = api.container.getBoundingClientRect();
3137
3183
  return ((_a = api.viewport) === null || _a === void 0 ? void 0 : _a.clientToWorld) ? api.viewport.clientToWorld(cx, cy, rect) : { x: cx - rect.left, y: cy - rect.top };
3138
3184
  };
3139
- const first = toWorld(ev.clientX, ev.clientY);
3140
3185
  // The board holds the NEW group, not the bare page: it is adopted under the
3141
3186
  // group's id, sized for the page plus its strip, taking the room under the
3142
3187
  // pointer with its top edge at the pointer — the tab is what the pointer
3143
- // holds, the page hangs below it.
3144
- const leg = adopt({ id: plan.arrivingId }, first, plan.size, { fit: 'shrink', anchor: 'top' });
3145
- if (!leg)
3146
- return false;
3188
+ // holds, the page hangs below it. NOT at the press: a ghost in the engine
3189
+ // costs rows, and on a fit board every tile squeezes the moment it enters,
3190
+ // so the ghost enters only when the pointer is over free board space, and
3191
+ // leaves whenever it is over a strip, a group or an edge. A full board that
3192
+ // refuses the ghost still lets the page join, reorder or split.
3193
+ let leg = null;
3194
+ const ensureLeg = (world) => {
3195
+ if (!leg)
3196
+ leg = adopt({ id: plan.arrivingId }, world, plan.size, { fit: 'shrink', anchor: 'top' });
3197
+ return leg;
3198
+ };
3199
+ const naturalSpan = (() => {
3200
+ const sp = sizeToSpan(plan.size.width, plan.size.height, frame(), geom(), rows());
3201
+ return { w: Math.max(1, Math.min(columns, sp.w)), h: Math.max(TEAR_OUT_MIN_ROWS, sp.h) };
3202
+ })();
3203
+ // A DOCKED group takes at most HALF the board — VS Code's edge drop splits
3204
+ // the area in two. A page as tall as the container it left would
3205
+ // otherwise shove the whole dashboard out of view. Half of the board as
3206
+ // it stood at the press, and of what the canvas shows, whichever is less.
3207
+ const dockSpan = (() => {
3208
+ const rows0 = rows();
3209
+ const rect = api.container.getBoundingClientRect();
3210
+ const rh = rowHeightFor(geom(), rows0) + gap;
3211
+ const visible = rect.height > 0 && rh > 0 ? Math.max(1, Math.floor((rect.height + gap) / rh)) : rows0;
3212
+ const halfRows = Math.max(TEAR_OUT_MIN_ROWS, Math.ceil(Math.min(rows0, visible) / 2));
3213
+ const halfCols = Math.max(1, Math.ceil(columns / 2));
3214
+ return { w: Math.min(naturalSpan.w, halfCols), h: Math.min(naturalSpan.h, halfRows) };
3215
+ })();
3147
3216
  tearing = pageId;
3148
3217
  const doc = (_a = api.container.ownerDocument) !== null && _a !== void 0 ? _a : document;
3149
3218
  const chip = doc.createElement('div');
3150
3219
  chip.className = 'axdb-drag-chip axdb-tab-chip';
3151
3220
  chip.textContent = plan.label;
3152
3221
  doc.body.appendChild(chip);
3153
- // Over ANOTHER tab container the page joins it instead of taking a cell:
3154
- // the tile leaves the board (its displaced tiles come home), the target's
3155
- // frame lights up and its strip marks where the tab will go.
3222
+ const moveChip = (cx, cy) => {
3223
+ chip.style.left = `${cx + 6}px`;
3224
+ chip.style.top = `${cy + 6}px`;
3225
+ };
3226
+ moveChip(ev.clientX, ev.clientY);
3227
+ armGlide();
3228
+ api.render();
3156
3229
  const layer = htmlLayer();
3230
+ const area = (g) => {
3231
+ const sz = sizeOf(g);
3232
+ return sz.width * sz.height;
3233
+ };
3157
3234
  const targets = plan.joinTargets
3158
3235
  .map((id) => diagram.getGroup(id))
3159
- .filter((g) => !!g && g.id !== fromGroupId);
3160
- const targetAt = (wx, wy) => { var _a; return (_a = targets.find((t) => worldInsideGroup(t, wx, wy))) !== null && _a !== void 0 ? _a : null; };
3161
- let over = null;
3236
+ .filter((g) => !!g && g.id !== fromGroupId)
3237
+ .sort((a, b) => area(a) - area(b));
3238
+ // GEOMETRY MUST NOT MOVE WITH THE GHOST. On a fit board the rows squeeze
3239
+ // while the ghost occupies some and relax when it leaves, so a target's
3240
+ // frame changes with the very decision being made about it: a join makes
3241
+ // the ghost leave, the frame grows, and the same pointer now reads as a
3242
+ // split. The target and its frame are therefore anchored when the pointer
3243
+ // enters it and kept until it leaves; the board frame is the one at press.
3244
+ let anchor = null;
3245
+ const inRect = (r, wx, wy) => wx >= r.x && wx <= r.x + r.width && wy >= r.y && wy <= r.y + r.height;
3246
+ const targetAt = (wx, wy) => {
3247
+ var _a;
3248
+ if (anchor && inRect(anchor.frame, wx, wy))
3249
+ return anchor.g;
3250
+ const t = (_a = targets.find((x) => worldInsideGroup(x, wx, wy))) !== null && _a !== void 0 ? _a : null;
3251
+ if (t)
3252
+ leg === null || leg === void 0 ? void 0 : leg.leave(); // the ghost leaves first, so the frame anchored is the relaxed one
3253
+ anchor = t ? { g: t, frame: frameOfGroup(t), stripH: plan.stripHeight(t.id) } : null;
3254
+ return t;
3255
+ };
3256
+ // THE BANDS ARE ON THE SCREEN. Dockview measures its container edges on
3257
+ // the element, and so must we: the camera moves during the drag (the
3258
+ // board glides, the ghost adds rows a bounded scroll answers to), so a
3259
+ // band fixed in world space at the press drifts away from the edge the
3260
+ // user sees. The frame is mapped to client space through the live
3261
+ // camera and clipped to the canvas, so a scrolled board's bands sit at
3262
+ // its VISIBLE edges.
3263
+ const ROOT_TOP = 20;
3264
+ const ROOT_BOTTOM = 20;
3265
+ const ROOT_SIDE = 40;
3266
+ const visibleFrame = () => {
3267
+ const rect = api.container.getBoundingClientRect();
3268
+ const o = toWorld(rect.left, rect.top);
3269
+ const u = toWorld(rect.left + 100, rect.top + 100);
3270
+ const sx = 100 / (u.x - o.x || 100);
3271
+ const sy = 100 / (u.y - o.y || 100);
3272
+ const f = frame();
3273
+ const left = rect.left + (f.x - o.x) * sx;
3274
+ const top = rect.top + (f.y - o.y) * sy;
3275
+ const right = left + f.width * sx;
3276
+ const bottom = top + f.height * sy;
3277
+ if (rect.width <= 0 || rect.height <= 0)
3278
+ return { left, top, right, bottom }; // unlaid-out: nothing to clip to
3279
+ return { left: Math.max(left, rect.left), top: Math.max(top, rect.top), right: Math.min(right, rect.right), bottom: Math.min(bottom, rect.bottom) };
3280
+ };
3281
+ const rootAt = (cx, cy) => {
3282
+ // The bands reach a little OUTSIDE the frame too: a pointer that
3283
+ // overshoots the board's top edge by a few pixels means "the top".
3284
+ const v = visibleFrame();
3285
+ if (cx < v.left - ROOT_SIDE || cx > v.right + ROOT_SIDE || cy < v.top - ROOT_TOP || cy > v.bottom + ROOT_BOTTOM)
3286
+ return null;
3287
+ const rows = Math.max(TEAR_OUT_MIN_ROWS, rowsWithout(plan.arrivingId));
3288
+ const w = dockSpan.w;
3289
+ const h = dockSpan.h;
3290
+ if (cy - v.top <= ROOT_TOP)
3291
+ return { kind: 'root', side: 'top', cell: { x: 0, y: 0, w: columns, h } };
3292
+ if (cx - v.left <= ROOT_SIDE)
3293
+ return { kind: 'root', side: 'left', cell: { x: 0, y: 0, w, h: rows } };
3294
+ if (v.right - cx <= ROOT_SIDE)
3295
+ return { kind: 'root', side: 'right', cell: { x: Math.max(0, columns - w), y: 0, w, h: rows } };
3296
+ if (v.bottom - cy <= ROOT_BOTTOM)
3297
+ return { kind: 'root', side: 'bottom', cell: { x: 0, y: rows, w: columns, h } };
3298
+ return null;
3299
+ };
3300
+ const rowsWithout = (id) => {
3301
+ let r = 0;
3302
+ for (const it of engine.getItems())
3303
+ if (it.id !== id)
3304
+ r = Math.max(r, it.y + it.h);
3305
+ return r;
3306
+ };
3307
+ /** A split being previewed: the target shrunk to its half, to be restored when the pointer leaves. */
3308
+ let split = null;
3309
+ const halves = (target, side) => {
3310
+ // The cell to halve is the target's own — not the half it is already
3311
+ // shrunk to while a split is being previewed.
3312
+ const live = engine.getItem(target.id);
3313
+ const it = split && split.id === target.id ? split.before : live;
3314
+ if (!it || !live)
3315
+ return null; // a group on another board: no cell of ours to halve
3316
+ if (side === 'left' || side === 'right') {
3317
+ if (it.w < 2)
3318
+ return null;
3319
+ const a = Math.ceil(it.w / 2);
3320
+ const b = it.w - a;
3321
+ return side === 'right'
3322
+ ? { keep: { x: it.x, y: it.y, w: a, h: it.h }, born: { x: it.x + a, y: it.y, w: b, h: it.h } }
3323
+ : { keep: { x: it.x + b, y: it.y, w: a, h: it.h }, born: { x: it.x, y: it.y, w: b, h: it.h } };
3324
+ }
3325
+ if (it.h < 2 * TEAR_OUT_MIN_ROWS)
3326
+ return null;
3327
+ const a = Math.ceil(it.h / 2);
3328
+ const b = it.h - a;
3329
+ return side === 'bottom'
3330
+ ? { keep: { x: it.x, y: it.y, w: it.w, h: a }, born: { x: it.x, y: it.y + a, w: it.w, h: b } }
3331
+ : { keep: { x: it.x, y: it.y + b, w: it.w, h: a }, born: { x: it.x, y: it.y, w: it.w, h: b } };
3332
+ };
3333
+ const zoneAt = (cx, cy, world) => {
3334
+ const target = targetAt(world.x, world.y);
3335
+ const home = !target && worldInsideGroup(from, world.x, world.y);
3336
+ // A strip is the most precise target there is: anyone's wins outright.
3337
+ if (target) {
3338
+ const idx = plan.stripIndex(target.id, cx, cy);
3339
+ if (idx !== null)
3340
+ return { kind: 'strip', target, index: idx };
3341
+ }
3342
+ else if (home) {
3343
+ const idx = plan.stripIndex(fromGroupId, cx, cy);
3344
+ if (idx !== null)
3345
+ return { kind: 'reorder', index: idx };
3346
+ }
3347
+ // The board's own edges beat whatever sits against them — Dockview's
3348
+ // container edges over its groups — so a group at the top of the board
3349
+ // still leaves the top band to the board.
3350
+ const root = rootAt(cx, cy);
3351
+ if (root)
3352
+ return root;
3353
+ if (target) {
3354
+ const f = anchor && anchor.g === target ? anchor.frame : frameOfGroup(target);
3355
+ const stripH = anchor && anchor.g === target ? anchor.stripH : plan.stripHeight(target.id);
3356
+ const bodyH = Math.max(1, f.height - stripH);
3357
+ const rx = Math.min(1, Math.max(0, (world.x - f.x) / Math.max(1, f.width)));
3358
+ const ry = Math.min(1, Math.max(0, (world.y - f.y - stripH) / bodyH));
3359
+ if (rx >= 1 / 3 && rx <= 2 / 3 && ry >= 1 / 3 && ry <= 2 / 3)
3360
+ return { kind: 'join', target };
3361
+ const d = [['left', rx], ['right', 1 - rx], ['top', ry], ['bottom', 1 - ry]];
3362
+ d.sort((p, q) => p[1] - q[1]);
3363
+ const h = halves(target, d[0][0]);
3364
+ return h ? Object.assign({ kind: 'split', target, side: d[0][0] }, h) : { kind: 'join', target };
3365
+ }
3366
+ if (home)
3367
+ return { kind: 'home' };
3368
+ return { kind: 'board' };
3369
+ };
3370
+ const zoneKey = (z) => JSON.stringify(z, (k, v) => (k === 'target' ? v.id : v));
3371
+ // DOCKING PUSHES SECTIONS. A section is a locked tile so that a widget
3372
+ // never pushes it; a group docked against the board's edge is the one
3373
+ // gesture that must — everything below the top band moves down. For the
3374
+ // preview every member group is unlocked; they are relocked when the
3375
+ // pointer leaves the band, and their moved cells are committed with the
3376
+ // dock.
3377
+ let unlockedGroups = [];
3378
+ const unlockGroups = () => {
3379
+ if (unlockedGroups.length > 0)
3380
+ return;
3381
+ for (const it of engine.getItems()) {
3382
+ if (it.id !== plan.arrivingId && it.locked && isGroupMember(it.id)) {
3383
+ it.locked = false;
3384
+ unlockedGroups.push(it.id);
3385
+ }
3386
+ }
3387
+ };
3388
+ const relockGroups = () => {
3389
+ for (const id of unlockedGroups) {
3390
+ const it = engine.getItem(id);
3391
+ if (it)
3392
+ it.locked = true;
3393
+ }
3394
+ unlockedGroups = [];
3395
+ };
3396
+ const groupCommands = (fin, except) => fin.groups.filter((g) => g.id !== except).map((g) => new SetGroupCellCommand(g.id, g.cellBefore, g.cellAfter, g.frameBefore, g.frameAfter));
3397
+ // A TOP DOCK INSERTS ROWS. The engine's push cascade resolves the band's
3398
+ // collisions one tile at a time and scrambles what stood beneath it (the
3399
+ // demo's KPI row came apart, two of its tiles under the chart). VS Code
3400
+ // shoves the whole area down intact, so after the ghost takes the band
3401
+ // every tile that stood at or below it is put back at its own column,
3402
+ // exactly the band's height lower — a translation of a layout without
3403
+ // overlaps has none — and comes back the same way when the pointer leaves.
3404
+ // The layout translated is the one from BEFORE the ghost entered (the
3405
+ // leg's baseline), not the engine's mid-gesture state: the adoption
3406
+ // itself pushes tiles about, and a snapshot taken after it would carry
3407
+ // that cascade into the insert.
3408
+ let inserted = null;
3409
+ const insertRows = (cell, l) => {
3410
+ if (cell.w < columns)
3411
+ return; // a side dock has no whole rows to insert
3412
+ inserted = l.baseline();
3413
+ for (const [id, c] of inserted) {
3414
+ const it = engine.getItem(id);
3415
+ if (!it)
3416
+ continue;
3417
+ it.x = c.x;
3418
+ it.y = c.y >= cell.y ? c.y + cell.h : c.y;
3419
+ }
3420
+ };
3421
+ const undoInsertRows = () => {
3422
+ if (!inserted)
3423
+ return;
3424
+ leg === null || leg === void 0 ? void 0 : leg.leave(); // the ghost frees its rows first
3425
+ for (const [id, c] of inserted) {
3426
+ const it = engine.getItem(id);
3427
+ if (it) {
3428
+ it.x = c.x;
3429
+ it.y = c.y;
3430
+ }
3431
+ }
3432
+ inserted = null;
3433
+ project();
3434
+ };
3162
3435
  let joinEl = null;
3163
- const showJoin = (g) => {
3164
- if (!layer || g.id === fromGroupId)
3165
- return; // reordering along its own strip: the mark is enough
3436
+ const showOverlay = (r) => {
3437
+ if (!layer)
3438
+ return;
3166
3439
  if (!joinEl) {
3167
3440
  joinEl = doc.createElement('div');
3168
3441
  joinEl.className = 'axdb-join';
3169
3442
  layer.prepend(joinEl);
3170
3443
  }
3171
- const sz = sizeOf(g);
3172
- joinEl.style.left = `${g.position.x}px`;
3173
- joinEl.style.top = `${g.position.y}px`;
3174
- joinEl.style.width = `${sz.width}px`;
3175
- joinEl.style.height = `${sz.height}px`;
3444
+ joinEl.style.left = `${r.x}px`;
3445
+ joinEl.style.top = `${r.y}px`;
3446
+ joinEl.style.width = `${r.width}px`;
3447
+ joinEl.style.height = `${r.height}px`;
3176
3448
  };
3177
- const hideJoin = () => {
3449
+ const hideOverlay = () => {
3178
3450
  joinEl === null || joinEl === void 0 ? void 0 : joinEl.remove();
3179
3451
  joinEl = null;
3180
- plan.markDrop(null, null);
3181
3452
  };
3182
- const setOver = (g, world) => {
3183
- if (g === over)
3453
+ const undoSplitPreview = () => {
3454
+ if (!split)
3184
3455
  return;
3185
- over = g;
3186
- if (g) {
3187
- leg.leave();
3188
- showJoin(g);
3189
- }
3190
- else {
3191
- hideJoin();
3192
- leg.enter(world);
3456
+ const it = engine.getItem(split.id);
3457
+ leg === null || leg === void 0 ? void 0 : leg.leave(); // the born half must be free before the target grows back into it
3458
+ if (it) {
3459
+ if (it.x !== split.before.x || it.y !== split.before.y)
3460
+ engine.moveCheck(split.id, split.before.x, split.before.y, { gate: false });
3461
+ if (it.w !== split.before.w || it.h !== split.before.h)
3462
+ engine.resizeCheck(split.id, split.before.w, split.before.h);
3463
+ it.locked = true;
3193
3464
  }
3465
+ split = null;
3466
+ project();
3194
3467
  };
3195
- const moveChip = (cx, cy) => {
3196
- chip.style.left = `${cx + 6}px`;
3197
- chip.style.top = `${cy + 6}px`;
3468
+ const previewSplit = (z, world) => {
3469
+ const it = engine.getItem(z.target.id);
3470
+ const l = ensureLeg(world);
3471
+ if (!it || !l)
3472
+ return false;
3473
+ split = { id: z.target.id, before: { x: it.x, y: it.y, w: it.w, h: it.h }, frameBefore: frameOfGroup(z.target), keep: z.keep };
3474
+ it.locked = false; // its own gesture for the moment: it may shrink and shift
3475
+ if (it.w !== z.keep.w || it.h !== z.keep.h)
3476
+ engine.resizeCheck(z.target.id, z.keep.w, z.keep.h);
3477
+ const now = engine.getItem(z.target.id);
3478
+ if (now && (now.x !== z.keep.x || now.y !== z.keep.y))
3479
+ engine.moveCheck(z.target.id, z.keep.x, z.keep.y, { gate: false });
3480
+ const ok = l.place(z.born);
3481
+ project();
3482
+ placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove(); // the accent overlay says which half; the grey placeholder under it is noise
3483
+ placeholder = null;
3484
+ return ok;
3485
+ };
3486
+ let zone = { kind: 'board' };
3487
+ let key = zoneKey(zone);
3488
+ const applyZone = (z, world) => {
3489
+ var _a;
3490
+ const k = zoneKey(z);
3491
+ const same = k === key;
3492
+ key = k;
3493
+ zone = z;
3494
+ if (same) {
3495
+ if (z.kind === 'board')
3496
+ (_a = ensureLeg(world)) === null || _a === void 0 ? void 0 : _a.move(world);
3497
+ return;
3498
+ }
3499
+ undoSplitPreview();
3500
+ undoInsertRows();
3501
+ if (z.kind !== 'root')
3502
+ relockGroups();
3503
+ plan.markDrop(null, null);
3504
+ hideOverlay();
3505
+ switch (z.kind) {
3506
+ case 'strip':
3507
+ leg === null || leg === void 0 ? void 0 : leg.leave();
3508
+ showOverlay(frameOfGroup(z.target));
3509
+ plan.markDrop(z.target.id, z.index);
3510
+ break;
3511
+ case 'join':
3512
+ leg === null || leg === void 0 ? void 0 : leg.leave();
3513
+ showOverlay(frameOfGroup(z.target));
3514
+ break;
3515
+ case 'split': {
3516
+ if (!previewSplit(z, world)) {
3517
+ zone = { kind: 'join', target: z.target };
3518
+ key = zoneKey(zone);
3519
+ leg === null || leg === void 0 ? void 0 : leg.leave();
3520
+ showOverlay(frameOfGroup(z.target));
3521
+ break;
3522
+ }
3523
+ showOverlay(cellToRect(z.born, frame(), geom(), rows()));
3524
+ break;
3525
+ }
3526
+ case 'root': {
3527
+ const l = ensureLeg(world);
3528
+ if (l) {
3529
+ unlockGroups();
3530
+ l.place(z.cell);
3531
+ insertRows(z.cell, l);
3532
+ project();
3533
+ placeholder === null || placeholder === void 0 ? void 0 : placeholder.remove();
3534
+ placeholder = null;
3535
+ showOverlay(cellToRect(z.cell, frame(), geom(), rows()));
3536
+ }
3537
+ break;
3538
+ }
3539
+ case 'reorder':
3540
+ leg === null || leg === void 0 ? void 0 : leg.leave();
3541
+ plan.markDrop(fromGroupId, z.index);
3542
+ break;
3543
+ case 'home':
3544
+ leg === null || leg === void 0 ? void 0 : leg.leave();
3545
+ break;
3546
+ case 'board': {
3547
+ const l = ensureLeg(world);
3548
+ l === null || l === void 0 ? void 0 : l.enter(world);
3549
+ break;
3550
+ }
3551
+ }
3198
3552
  };
3199
- moveChip(ev.clientX, ev.clientY);
3200
- armGlide();
3201
- api.render();
3202
3553
  let last = { x: ev.clientX, y: ev.clientY };
3203
3554
  const detach = () => {
3204
3555
  window.removeEventListener('pointermove', onMove, true);
@@ -3206,85 +3557,104 @@ export function bindDashboardGrid(api, group, options = {}) {
3206
3557
  window.removeEventListener('pointercancel', onCancel, true);
3207
3558
  window.removeEventListener('keydown', onKey, true);
3208
3559
  };
3209
- let homeIndex = null;
3210
3560
  const onMove = (e) => {
3211
3561
  if (disposed)
3212
3562
  return detach();
3213
3563
  last = { x: e.clientX, y: e.clientY };
3214
3564
  moveChip(e.clientX, e.clientY);
3215
3565
  const world = toWorld(e.clientX, e.clientY);
3216
- const home = worldInsideGroup(from, world.x, world.y);
3217
- // Over its OWN strip the tab reorders: the strip marks the slot and the
3218
- // tile leaves the board. Over its own body it goes home (a cancel).
3219
- homeIndex = home ? plan.stripIndex(fromGroupId, e.clientX, e.clientY) : null;
3220
- const target = home ? null : targetAt(world.x, world.y);
3221
- setOver(target !== null && target !== void 0 ? target : (homeIndex !== null ? from : null), world);
3222
- chip.classList.toggle('axdb-out', (home && homeIndex === null) || (!target && !home && !worldInsideBoardGrace(world.x, world.y)));
3223
- if (target)
3224
- plan.markDrop(target.id, plan.dropIndex(target.id, e.clientX, e.clientY));
3225
- else if (homeIndex !== null)
3226
- plan.markDrop(fromGroupId, homeIndex);
3227
- else if (!home)
3228
- leg.move(world);
3566
+ const z = zoneAt(e.clientX, e.clientY, world);
3567
+ applyZone(z, world);
3568
+ // Dimmed = a release here does nothing: home, off the board, or a board
3569
+ // that refused the ghost (bounded and full).
3570
+ chip.classList.toggle('axdb-out', zone.kind === 'home' || (zone.kind === 'root' && !leg) || (zone.kind === 'board' && (!leg || !worldInsideBoardGrace(world.x, world.y))));
3229
3571
  api.render();
3230
3572
  };
3573
+ const done = (changed, kind) => {
3574
+ var _a;
3575
+ disarmGlideSoon();
3576
+ enforceBoardHeight();
3577
+ persistLayouts();
3578
+ api.renderNow();
3579
+ (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: kind, kind: 'move', nodeId: pageId, changed });
3580
+ };
3231
3581
  const finish = (commit) => {
3232
- var _a, _b, _c, _d;
3582
+ var _a, _b;
3233
3583
  detach();
3234
3584
  chip.remove();
3235
- hideJoin();
3585
+ hideOverlay();
3586
+ plan.markDrop(null, null);
3236
3587
  tearing = null;
3237
3588
  if (disposed)
3238
3589
  return;
3239
3590
  const world = toWorld(last.x, last.y);
3240
- const home = worldInsideGroup(from, world.x, world.y);
3241
- if (commit && home && homeIndex !== null) {
3242
- // REORDER along its own strip.
3243
- leg.abort();
3244
- const cmds = plan.reorder(homeIndex);
3591
+ const z = commit ? zoneAt(last.x, last.y, world) : { kind: 'home' };
3592
+ if (z.kind !== 'root')
3593
+ undoInsertRows();
3594
+ if (!commit || z.kind === 'home' || (z.kind === 'root' && !ensureLeg(world)) || (z.kind === 'board' && (!ensureLeg(world) || !worldInsideBoardGrace(world.x, world.y)))) {
3595
+ undoSplitPreview();
3596
+ leg === null || leg === void 0 ? void 0 : leg.abort();
3597
+ relockGroups();
3598
+ done(false, 'cancel');
3599
+ return;
3600
+ }
3601
+ if (z.kind === 'reorder') {
3602
+ undoSplitPreview();
3603
+ leg === null || leg === void 0 ? void 0 : leg.abort();
3604
+ const cmds = plan.reorder(z.index);
3245
3605
  const changed = cmds.length > 0 ? execute('Reorder tab', cmds) : false;
3246
- disarmGlideSoon();
3247
- api.renderNow();
3248
- (_a = options.onGesture) === null || _a === void 0 ? void 0 : _a.call(options, { type: changed ? 'commit' : 'cancel', kind: 'move', nodeId: pageId, changed });
3606
+ done(changed, changed ? 'commit' : 'cancel');
3249
3607
  return;
3250
3608
  }
3251
- const target = commit && !home ? targetAt(world.x, world.y) : null;
3252
- if (target) {
3609
+ if (z.kind === 'strip' || z.kind === 'join') {
3253
3610
  // JOIN: no cell on this board — the leg is abandoned (its displaced
3254
3611
  // tiles are already home) and the page becomes the target's tab.
3255
- const index = plan.dropIndex(target.id, last.x, last.y);
3256
- leg.abort();
3257
- const planned = plan.join(target.id, index, group.id);
3258
- const changed = execute('Move tab', [...planned.move, ...planned.collapse]);
3259
- disarmGlideSoon();
3260
- enforceBoardHeight();
3261
- persistLayouts();
3262
- api.renderNow();
3263
- (_b = options.onGesture) === null || _b === void 0 ? void 0 : _b.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
3612
+ undoSplitPreview();
3613
+ const index = z.kind === 'strip' ? z.index : plan.dropIndex(z.target.id, last.x, last.y);
3614
+ leg === null || leg === void 0 ? void 0 : leg.abort();
3615
+ const planned = plan.join(z.target.id, index, group.id);
3616
+ done(execute('Move tab', [...planned.move, ...planned.collapse]), 'commit');
3264
3617
  return;
3265
3618
  }
3266
- if (!commit || home || !worldInsideBoardGrace(world.x, world.y)) {
3267
- leg.abort();
3268
- disarmGlideSoon();
3269
- api.renderNow();
3270
- (_c = options.onGesture) === null || _c === void 0 ? void 0 : _c.call(options, { type: 'cancel', kind: 'move', nodeId: pageId, changed: false });
3619
+ if (z.kind === 'split') {
3620
+ // SPLIT: the target keeps one half of its cell, the page's new group the
3621
+ // other. The preview already holds both in the engine; commit them.
3622
+ if (!split || split.id !== z.target.id || zoneKey(zone) !== zoneKey(z))
3623
+ applyZone(z, world);
3624
+ const fin = (_a = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _a !== void 0 ? _a : null;
3625
+ const it = engine.getItem(z.target.id);
3626
+ const before = split;
3627
+ if (it)
3628
+ it.locked = true;
3629
+ split = null;
3630
+ if (!fin || !before || !it) {
3631
+ leg === null || leg === void 0 ? void 0 : leg.abort();
3632
+ done(false, 'cancel');
3633
+ return;
3634
+ }
3635
+ const keep = { x: it.x, y: it.y, w: it.w, h: it.h };
3636
+ const planned = plan.commands(fin.cell, fin.rect, group.id);
3637
+ const changed = execute('Split group', [
3638
+ ...fin.commands,
3639
+ ...groupCommands(fin, z.target.id),
3640
+ new SetGroupCellCommand(z.target.id, before.before, keep, before.frameBefore, cellToRect(keep, frame(), geom(), rows())),
3641
+ ...planned.move,
3642
+ ...planned.collapse,
3643
+ ]);
3644
+ done(changed, 'commit');
3271
3645
  return;
3272
3646
  }
3273
- const fin = leg.finalize();
3647
+ // ROOT DOCK or a free cell on the board: the leg sits where it will land.
3648
+ if (zoneKey(zone) !== zoneKey(z))
3649
+ applyZone(z, world);
3650
+ const fin = (_b = leg === null || leg === void 0 ? void 0 : leg.finalize()) !== null && _b !== void 0 ? _b : null;
3651
+ relockGroups();
3274
3652
  if (!fin) {
3275
- disarmGlideSoon();
3276
- api.renderNow();
3653
+ done(false, 'cancel');
3277
3654
  return;
3278
3655
  }
3279
3656
  const planned = plan.commands(fin.cell, fin.rect, group.id);
3280
- // The container the page left may have been holding its last page: it
3281
- // goes too (the plan carries the removal's settle).
3282
- const changed = execute('Move tab out', [...fin.commands, ...planned.move, ...planned.collapse]);
3283
- disarmGlideSoon();
3284
- enforceBoardHeight();
3285
- persistLayouts();
3286
- api.renderNow();
3287
- (_d = options.onGesture) === null || _d === void 0 ? void 0 : _d.call(options, { type: 'commit', kind: 'move', nodeId: pageId, changed });
3657
+ done(execute(z.kind === 'root' ? 'Dock tab' : 'Move tab out', [...fin.commands, ...groupCommands(fin), ...planned.move, ...planned.collapse]), 'commit');
3288
3658
  };
3289
3659
  const onUp = () => finish(true);
3290
3660
  const onCancel = () => finish(false);