@tutti-os/workbench-surface 0.0.49 → 0.0.51

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/dist/index.d.ts CHANGED
@@ -451,6 +451,10 @@ interface WorkbenchHostLaunchResult {
451
451
  payload?: unknown;
452
452
  type: string;
453
453
  } | null;
454
+ cascadeOffset?: {
455
+ x: number;
456
+ y: number;
457
+ };
454
458
  defaultFrame?: WorkbenchFrame;
455
459
  displayMode?: WorkbenchDisplayMode;
456
460
  dockEntryId?: string;
package/dist/index.js CHANGED
@@ -5270,7 +5270,14 @@ function createWorkbenchSnapshotFromState(state, options = {}) {
5270
5270
 
5271
5271
  // src/core/placement.ts
5272
5272
  var WORKBENCH_WINDOW_CASCADE_OFFSET = 28;
5273
+ var WORKBENCH_WINDOW_CASCADE_FALLBACK_LIMIT = 12;
5274
+ var WORKBENCH_WINDOW_CASCADE_POSITION_TOLERANCE = 4;
5273
5275
  function resolveWorkbenchCascadedRect(input) {
5276
+ const hasCustomCascadeOffset = input.cascadeOffset !== void 0;
5277
+ const cascadeOffset = input.cascadeOffset ?? {
5278
+ x: WORKBENCH_WINDOW_CASCADE_OFFSET,
5279
+ y: WORKBENCH_WINDOW_CASCADE_OFFSET
5280
+ };
5274
5281
  const activeNode = input.existingNodes.find(
5275
5282
  (node) => node.id === input.currentNodeStack.at(-1)
5276
5283
  ) ?? input.existingNodes.at(-1);
@@ -5282,18 +5289,120 @@ function resolveWorkbenchCascadedRect(input) {
5282
5289
  input.sizeConstraints
5283
5290
  );
5284
5291
  }
5292
+ const preferredCandidate = createWorkbenchCascadeCandidate({
5293
+ input,
5294
+ x: activeNode.frame.x + cascadeOffset.x,
5295
+ y: activeNode.frame.y + cascadeOffset.y
5296
+ });
5297
+ if ((!hasCustomCascadeOffset || input.existingNodes.length <= 1) && !workbenchCascadeCandidateMatchesExisting(preferredCandidate, input)) {
5298
+ return preferredCandidate;
5299
+ }
5300
+ const candidateOrigins = [
5301
+ {
5302
+ x: activeNode.frame.x + cascadeOffset.x,
5303
+ y: activeNode.frame.y + cascadeOffset.y
5304
+ },
5305
+ {
5306
+ x: activeNode.frame.x - cascadeOffset.x,
5307
+ y: activeNode.frame.y + cascadeOffset.y
5308
+ },
5309
+ {
5310
+ x: activeNode.frame.x + cascadeOffset.x,
5311
+ y: activeNode.frame.y - cascadeOffset.y
5312
+ },
5313
+ {
5314
+ x: activeNode.frame.x - cascadeOffset.x,
5315
+ y: activeNode.frame.y - cascadeOffset.y
5316
+ }
5317
+ ];
5318
+ for (let index = 1; index <= WORKBENCH_WINDOW_CASCADE_FALLBACK_LIMIT; index += 1) {
5319
+ candidateOrigins.push(
5320
+ {
5321
+ x: input.preferredFrame.x + cascadeOffset.x * index,
5322
+ y: input.preferredFrame.y + cascadeOffset.y * index
5323
+ },
5324
+ {
5325
+ x: input.preferredFrame.x - cascadeOffset.x * index,
5326
+ y: input.preferredFrame.y + cascadeOffset.y * index
5327
+ },
5328
+ {
5329
+ x: input.preferredFrame.x + cascadeOffset.x * index,
5330
+ y: input.preferredFrame.y - cascadeOffset.y * index
5331
+ },
5332
+ {
5333
+ x: input.preferredFrame.x - cascadeOffset.x * index,
5334
+ y: input.preferredFrame.y - cascadeOffset.y * index
5335
+ }
5336
+ );
5337
+ }
5338
+ return resolveBestWorkbenchCascadeCandidate({
5339
+ candidateOrigins,
5340
+ fallbackFrame: preferredCandidate,
5341
+ input
5342
+ });
5343
+ }
5344
+ function createWorkbenchCascadeCandidate(input) {
5285
5345
  return clampWorkbenchRect(
5286
5346
  {
5287
- x: activeNode.frame.x + WORKBENCH_WINDOW_CASCADE_OFFSET,
5288
- y: activeNode.frame.y + WORKBENCH_WINDOW_CASCADE_OFFSET,
5289
- width: input.preferredFrame.width,
5290
- height: input.preferredFrame.height
5347
+ x: input.x,
5348
+ y: input.y,
5349
+ width: input.input.preferredFrame.width,
5350
+ height: input.input.preferredFrame.height
5291
5351
  },
5292
- input.surfaceSize,
5293
- input.constraints,
5294
- input.sizeConstraints
5352
+ input.input.surfaceSize,
5353
+ input.input.constraints,
5354
+ input.input.sizeConstraints
5295
5355
  );
5296
5356
  }
5357
+ function workbenchCascadeCandidateMatchesExisting(candidate, input) {
5358
+ return input.existingNodes.some((node) => {
5359
+ return Math.abs(node.frame.x - candidate.x) <= WORKBENCH_WINDOW_CASCADE_POSITION_TOLERANCE && Math.abs(node.frame.y - candidate.y) <= WORKBENCH_WINDOW_CASCADE_POSITION_TOLERANCE;
5360
+ });
5361
+ }
5362
+ function resolveBestWorkbenchCascadeCandidate(input) {
5363
+ let bestFrame = null;
5364
+ let bestScore = Number.POSITIVE_INFINITY;
5365
+ const seen = /* @__PURE__ */ new Set();
5366
+ for (const candidate of input.candidateOrigins) {
5367
+ const frame = createWorkbenchCascadeCandidate({
5368
+ input: input.input,
5369
+ x: candidate.x,
5370
+ y: candidate.y
5371
+ });
5372
+ const key = `${frame.x}:${frame.y}:${frame.width}:${frame.height}`;
5373
+ if (seen.has(key)) {
5374
+ continue;
5375
+ }
5376
+ seen.add(key);
5377
+ const score = scoreWorkbenchCascadeCandidate(frame, input.input);
5378
+ if (score < bestScore) {
5379
+ bestFrame = frame;
5380
+ bestScore = score;
5381
+ }
5382
+ }
5383
+ return bestFrame ?? input.fallbackFrame;
5384
+ }
5385
+ function scoreWorkbenchCascadeCandidate(candidate, input) {
5386
+ const overlapArea = input.existingNodes.reduce((total, node) => {
5387
+ return total + workbenchFrameIntersectionArea(candidate, node.frame);
5388
+ }, 0);
5389
+ const positionCollisionPenalty = workbenchCascadeCandidateMatchesExisting(
5390
+ candidate,
5391
+ input
5392
+ ) ? candidate.width * candidate.height : 0;
5393
+ return overlapArea + positionCollisionPenalty;
5394
+ }
5395
+ function workbenchFrameIntersectionArea(a, b) {
5396
+ const width = Math.max(
5397
+ 0,
5398
+ Math.min(a.x + a.width, b.x + b.width) - Math.max(a.x, b.x)
5399
+ );
5400
+ const height = Math.max(
5401
+ 0,
5402
+ Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y)
5403
+ );
5404
+ return width * height;
5405
+ }
5297
5406
 
5298
5407
  // src/host/nodeIdentity.ts
5299
5408
  function createWorkbenchHostProjectedNodeId(input) {
@@ -5405,6 +5514,7 @@ function resolveWorkbenchLaunchedHostNodeFrame(input) {
5405
5514
  existingSameTypeNodes.map((node) => node.id)
5406
5515
  );
5407
5516
  return resolveWorkbenchCascadedRect({
5517
+ cascadeOffset: input.result.cascadeOffset,
5408
5518
  currentNodeStack: input.currentState.nodeStack.filter(
5409
5519
  (nodeId) => existingSameTypeNodeIds.has(nodeId)
5410
5520
  ),
@@ -5421,6 +5531,7 @@ function resolveWorkbenchLaunchedHostNodeFrame(input) {
5421
5531
  });
5422
5532
  }
5423
5533
  return resolveWorkbenchCascadedRect({
5534
+ cascadeOffset: input.result.cascadeOffset,
5424
5535
  currentNodeStack: input.currentState.nodeStack,
5425
5536
  existingNodes: input.currentState.nodes,
5426
5537
  preferredFrame: responsivePreferredFrame,
@@ -6712,7 +6823,7 @@ var WorkbenchHostSessionController = class {
6712
6823
  }
6713
6824
  applyClosedDockWindowFrame(result) {
6714
6825
  const dockEntryId = result.dockEntryId?.trim();
6715
- if (!dockEntryId) {
6826
+ if (!dockEntryId || result.reuseDockEntryNode === false) {
6716
6827
  return result;
6717
6828
  }
6718
6829
  const entry = this.closedDockWindowFrameEntries.get(
@@ -7196,6 +7307,7 @@ function resolveDockMagnificationViewportBounds(viewportRect, dockPlacement) {
7196
7307
  function resolveDockMagnificationVisibleHitBounds({
7197
7308
  dockPlacement,
7198
7309
  hitBounds,
7310
+ mainAxisEdgePadding = 0,
7199
7311
  viewportRect
7200
7312
  }) {
7201
7313
  if (!hitBounds || !viewportRect) {
@@ -7208,14 +7320,41 @@ function resolveDockMagnificationVisibleHitBounds({
7208
7320
  const visibleBounds = {
7209
7321
  crossEnd: Math.min(hitBounds.crossEnd, viewportBounds.crossEnd),
7210
7322
  crossStart: Math.max(hitBounds.crossStart, viewportBounds.crossStart),
7211
- mainEnd: Math.min(hitBounds.mainEnd, viewportBounds.mainEnd),
7212
- mainStart: Math.max(hitBounds.mainStart, viewportBounds.mainStart)
7323
+ mainEnd: Math.min(
7324
+ hitBounds.mainEnd + mainAxisEdgePadding,
7325
+ viewportBounds.mainEnd + mainAxisEdgePadding
7326
+ ),
7327
+ mainStart: Math.max(
7328
+ hitBounds.mainStart - mainAxisEdgePadding,
7329
+ viewportBounds.mainStart - mainAxisEdgePadding
7330
+ )
7213
7331
  };
7214
7332
  if (visibleBounds.mainStart > visibleBounds.mainEnd || visibleBounds.crossStart > visibleBounds.crossEnd) {
7215
7333
  return null;
7216
7334
  }
7217
7335
  return visibleBounds;
7218
7336
  }
7337
+ function resolveDockMagnificationVisibleSlotRects({
7338
+ slotRects,
7339
+ viewportRect
7340
+ }) {
7341
+ if (!viewportRect) {
7342
+ return [...slotRects];
7343
+ }
7344
+ const visibleSlotRects = [];
7345
+ for (const rect of slotRects) {
7346
+ const visibleRect = {
7347
+ bottom: Math.min(rect.bottom, viewportRect.bottom),
7348
+ left: Math.max(rect.left, viewportRect.left),
7349
+ right: Math.min(rect.right, viewportRect.right),
7350
+ top: Math.max(rect.top, viewportRect.top)
7351
+ };
7352
+ if (visibleRect.left <= visibleRect.right && visibleRect.top <= visibleRect.bottom) {
7353
+ visibleSlotRects.push(visibleRect);
7354
+ }
7355
+ }
7356
+ return visibleSlotRects;
7357
+ }
7219
7358
  function isDockMagnificationPointInsideHitBounds({
7220
7359
  clientX,
7221
7360
  clientY,
@@ -7229,6 +7368,13 @@ function isDockMagnificationPointInsideHitBounds({
7229
7368
  const crossAxis = dockPlacement === "left" ? clientX : clientY;
7230
7369
  return mainAxis >= hitBounds.mainStart && mainAxis <= hitBounds.mainEnd && crossAxis >= hitBounds.crossStart && crossAxis <= hitBounds.crossEnd;
7231
7370
  }
7371
+ function isDockMagnificationPointInsideSlotRect({
7372
+ clientX,
7373
+ clientY,
7374
+ rect
7375
+ }) {
7376
+ return clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom;
7377
+ }
7232
7378
 
7233
7379
  // src/host/dockMagnification.ts
7234
7380
  var DOCK_ICON_BASE_SIZE = 43.2;
@@ -7243,6 +7389,9 @@ var MAX_MAGNIFICATION_STEP_SECONDS = 1 / 30;
7243
7389
  var MAGNIFICATION_INFLUENCE_PADDING = 8;
7244
7390
  var DOCK_MAGNIFICATION_ENTRY_RAMP_MS = 90;
7245
7391
  var DOCK_MAGNIFICATION_CROSS_AXIS_PADDING = 8;
7392
+ var DOCK_MAGNIFICATION_MAIN_AXIS_EDGE_PADDING = DOCK_ICON_BASE_SIZE / 2;
7393
+ var DOCK_MAGNIFICATION_AMBIENT_EDGE_RANGE = 180;
7394
+ var DOCK_MAGNIFICATION_AMBIENT_VIEWPORT_PADDING = 8;
7246
7395
  var dockMagnificationShellBySlot = /* @__PURE__ */ new WeakMap();
7247
7396
  var globalPointerListenerOptions = {
7248
7397
  capture: true,
@@ -7432,6 +7581,26 @@ function clearDockSlotMagnification(slotElement, appliedStylesRef) {
7432
7581
  const shell = resolveDockMagnificationShell(slotElement);
7433
7582
  shell?.style.removeProperty("transform");
7434
7583
  }
7584
+ function isPointNearDockScreenEdge({
7585
+ clientX,
7586
+ clientY,
7587
+ dockPlacement
7588
+ }) {
7589
+ if (typeof window === "undefined") {
7590
+ return true;
7591
+ }
7592
+ return dockPlacement === "left" ? clientX <= DOCK_MAGNIFICATION_AMBIENT_EDGE_RANGE : window.innerHeight - clientY <= DOCK_MAGNIFICATION_AMBIENT_EDGE_RANGE;
7593
+ }
7594
+ function isPointNearDockViewport({
7595
+ clientX,
7596
+ clientY,
7597
+ dockPlacement,
7598
+ viewportRect
7599
+ }) {
7600
+ const horizontalPadding = dockPlacement === "bottom" ? DOCK_MAGNIFICATION_MAIN_AXIS_EDGE_PADDING : DOCK_MAGNIFICATION_AMBIENT_VIEWPORT_PADDING;
7601
+ const verticalPadding = dockPlacement === "left" ? DOCK_MAGNIFICATION_MAIN_AXIS_EDGE_PADDING : DOCK_MAGNIFICATION_AMBIENT_VIEWPORT_PADDING;
7602
+ return clientX >= viewportRect.left - horizontalPadding && clientX <= viewportRect.right + horizontalPadding && clientY >= viewportRect.top - verticalPadding && clientY <= viewportRect.bottom + verticalPadding;
7603
+ }
7435
7604
  function useDockMagnification({
7436
7605
  dockPlacement,
7437
7606
  dockRootRef,
@@ -7449,6 +7618,7 @@ function useDockMagnification({
7449
7618
  const entryRampStartedAtRef = useRef5(null);
7450
7619
  const restCentersRef = useRef5(null);
7451
7620
  const hitBoundsRef = useRef5(null);
7621
+ const visibleSlotRectsRef = useRef5(null);
7452
7622
  const slotOrderRef = useRef5([]);
7453
7623
  const magnifyActiveRef = useRef5(false);
7454
7624
  const globalPointerTrackerRef = useRef5(null);
@@ -7498,18 +7668,53 @@ function useDockMagnification({
7498
7668
  centers.set(anchorKey, center);
7499
7669
  }
7500
7670
  slotOrderRef.current = order;
7671
+ const visibleViewportRect = viewportRect ? {
7672
+ bottom: viewportRect.bottom,
7673
+ left: viewportRect.left,
7674
+ right: viewportRect.right,
7675
+ top: viewportRect.top
7676
+ } : null;
7501
7677
  hitBoundsRef.current = resolveDockMagnificationVisibleHitBounds({
7502
7678
  dockPlacement,
7503
7679
  hitBounds: resolveDockMagnificationHitBounds(slotRects, dockPlacement),
7504
- viewportRect: viewportRect ? {
7505
- bottom: viewportRect.bottom,
7506
- left: viewportRect.left,
7507
- right: viewportRect.right,
7508
- top: viewportRect.top
7509
- } : null
7680
+ mainAxisEdgePadding: DOCK_MAGNIFICATION_MAIN_AXIS_EDGE_PADDING,
7681
+ viewportRect: visibleViewportRect
7682
+ });
7683
+ visibleSlotRectsRef.current = resolveDockMagnificationVisibleSlotRects({
7684
+ slotRects,
7685
+ viewportRect: visibleViewportRect
7510
7686
  });
7511
7687
  restCentersRef.current = centers;
7512
7688
  }, [dockPlacement, dockViewportRef, slotRefs]);
7689
+ const isPointerInsideAnyVisibleDockSlot = useCallback8(
7690
+ (clientX, clientY) => {
7691
+ for (const rect of visibleSlotRectsRef.current ?? []) {
7692
+ if (isDockMagnificationPointInsideSlotRect({
7693
+ clientX,
7694
+ clientY,
7695
+ rect
7696
+ })) {
7697
+ return true;
7698
+ }
7699
+ }
7700
+ return false;
7701
+ },
7702
+ []
7703
+ );
7704
+ const ensureDockMagnificationGeometry = useCallback8(() => {
7705
+ if (restCentersRef.current === null || hitBoundsRef.current === null || visibleSlotRectsRef.current === null) {
7706
+ captureRestCenters();
7707
+ }
7708
+ }, [captureRestCenters]);
7709
+ const isPointerInsideDockMagnificationTarget = useCallback8(
7710
+ (clientX, clientY) => isDockMagnificationPointInsideHitBounds({
7711
+ clientX,
7712
+ clientY,
7713
+ dockPlacement,
7714
+ hitBounds: hitBoundsRef.current
7715
+ }) || isPointerInsideAnyVisibleDockSlot(clientX, clientY),
7716
+ [dockPlacement, isPointerInsideAnyVisibleDockSlot]
7717
+ );
7513
7718
  const runAnimationFrame = useCallback8(
7514
7719
  (frameTime) => {
7515
7720
  if (pendingPointerAxisRef.current !== null) {
@@ -7607,6 +7812,7 @@ function useDockMagnification({
7607
7812
  entryRampStartedAtRef.current = null;
7608
7813
  restCentersRef.current = null;
7609
7814
  hitBoundsRef.current = null;
7815
+ visibleSlotRectsRef.current = null;
7610
7816
  slotOrderRef.current = [];
7611
7817
  setMagnifyActive(false);
7612
7818
  }
@@ -7656,15 +7862,8 @@ function useDockMagnification({
7656
7862
  }, [clearTrackedPointer, stopGlobalPointerTracking]);
7657
7863
  const handlePointerMove = useCallback8(
7658
7864
  (clientX, clientY) => {
7659
- if (restCentersRef.current === null) {
7660
- captureRestCenters();
7661
- }
7662
- if (!isDockMagnificationPointInsideHitBounds({
7663
- clientX,
7664
- clientY,
7665
- dockPlacement,
7666
- hitBounds: hitBoundsRef.current
7667
- })) {
7865
+ ensureDockMagnificationGeometry();
7866
+ if (!isPointerInsideDockMagnificationTarget(clientX, clientY)) {
7668
7867
  stopGlobalPointerTracking();
7669
7868
  clearTrackedPointer();
7670
7869
  return;
@@ -7677,9 +7876,10 @@ function useDockMagnification({
7677
7876
  scheduleAnimation();
7678
7877
  },
7679
7878
  [
7680
- captureRestCenters,
7681
7879
  clearTrackedPointer,
7682
7880
  dockPlacement,
7881
+ ensureDockMagnificationGeometry,
7882
+ isPointerInsideDockMagnificationTarget,
7683
7883
  scheduleAnimation,
7684
7884
  setMagnifyActive,
7685
7885
  startGlobalPointerTracking,
@@ -7688,6 +7888,87 @@ function useDockMagnification({
7688
7888
  );
7689
7889
  handleGlobalPointerMoveRef.current = handlePointerMove;
7690
7890
  handleGlobalPointerCancelRef.current = handleGlobalPointerCancel;
7891
+ useEffect6(() => {
7892
+ if (typeof document === "undefined") {
7893
+ return;
7894
+ }
7895
+ let animationFrame = null;
7896
+ let latestPoint = null;
7897
+ const clearAmbientPointerSample = () => {
7898
+ latestPoint = null;
7899
+ if (animationFrame !== null) {
7900
+ cancelAnimationFrame(animationFrame);
7901
+ animationFrame = null;
7902
+ }
7903
+ };
7904
+ const runAmbientPointerMove = () => {
7905
+ animationFrame = null;
7906
+ const point = latestPoint;
7907
+ latestPoint = null;
7908
+ if (!point || magnifyActiveRef.current) {
7909
+ return;
7910
+ }
7911
+ const viewportRect = dockViewportRef.current?.getBoundingClientRect();
7912
+ if (!viewportRect) {
7913
+ return;
7914
+ }
7915
+ const visibleViewportRect = {
7916
+ bottom: viewportRect.bottom,
7917
+ left: viewportRect.left,
7918
+ right: viewportRect.right,
7919
+ top: viewportRect.top
7920
+ };
7921
+ if (!isPointNearDockViewport({
7922
+ clientX: point.clientX,
7923
+ clientY: point.clientY,
7924
+ dockPlacement,
7925
+ viewportRect: visibleViewportRect
7926
+ })) {
7927
+ return;
7928
+ }
7929
+ ensureDockMagnificationGeometry();
7930
+ if (isPointerInsideDockMagnificationTarget(point.clientX, point.clientY)) {
7931
+ handlePointerMove(point.clientX, point.clientY);
7932
+ }
7933
+ };
7934
+ const handleAmbientPointerMove = (event) => {
7935
+ if (magnifyActiveRef.current) {
7936
+ clearAmbientPointerSample();
7937
+ return;
7938
+ }
7939
+ if (!isPointNearDockScreenEdge({
7940
+ clientX: event.clientX,
7941
+ clientY: event.clientY,
7942
+ dockPlacement
7943
+ })) {
7944
+ clearAmbientPointerSample();
7945
+ return;
7946
+ }
7947
+ latestPoint = { clientX: event.clientX, clientY: event.clientY };
7948
+ if (animationFrame === null) {
7949
+ animationFrame = requestAnimationFrame(runAmbientPointerMove);
7950
+ }
7951
+ };
7952
+ document.addEventListener(
7953
+ "pointermove",
7954
+ handleAmbientPointerMove,
7955
+ globalPointerListenerOptions
7956
+ );
7957
+ return () => {
7958
+ document.removeEventListener(
7959
+ "pointermove",
7960
+ handleAmbientPointerMove,
7961
+ globalPointerListenerOptions
7962
+ );
7963
+ clearAmbientPointerSample();
7964
+ };
7965
+ }, [
7966
+ dockPlacement,
7967
+ dockViewportRef,
7968
+ ensureDockMagnificationGeometry,
7969
+ handlePointerMove,
7970
+ isPointerInsideDockMagnificationTarget
7971
+ ]);
7691
7972
  const handlePointerLeave = useCallback8(() => {
7692
7973
  pendingPointerAxisRef.current = null;
7693
7974
  pointerAxisRef.current = null;
@@ -7699,6 +7980,7 @@ function useDockMagnification({
7699
7980
  restCentersRef.current?.delete(anchorKey);
7700
7981
  appliedStylesRef.current.delete(anchorKey);
7701
7982
  hitBoundsRef.current = null;
7983
+ visibleSlotRectsRef.current = null;
7702
7984
  slotOrderRef.current = slotOrderRef.current.filter(
7703
7985
  (key) => key !== anchorKey
7704
7986
  );
@@ -7727,6 +8009,7 @@ function useDockMagnification({
7727
8009
  appliedStylesRef.current.clear();
7728
8010
  restCentersRef.current = null;
7729
8011
  hitBoundsRef.current = null;
8012
+ visibleSlotRectsRef.current = null;
7730
8013
  slotOrderRef.current = [];
7731
8014
  pointerAxisRef.current = null;
7732
8015
  pendingPointerAxisRef.current = null;