@vectojs/core 1.27.0 → 1.28.0

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.js CHANGED
@@ -3640,10 +3640,24 @@ var Scene = (_class7 = class _Scene {
3640
3640
  * resume when it returns — instead of running the full update/render every
3641
3641
  * frame for a scene nobody can see. No-op (stays "on screen") where
3642
3642
  * `IntersectionObserver` is unavailable, so SSR/jsdom behavior is unchanged.
3643
+ *
3644
+ * Also a no-op for a canvas that is not in the document. An offscreen canvas
3645
+ * used purely as a texture source — `@vectojs/three`'s `ThreeAdapter` wraps
3646
+ * one in a `CanvasTexture`, and the same pattern shows up in any
3647
+ * render-to-texture setup — is never appended anywhere, and an
3648
+ * `IntersectionObserver` reports a detached element as not intersecting.
3649
+ * Observing it would therefore set `_canvasOnScreen = false` on the first
3650
+ * callback, and since {@link loop} returns without rescheduling in that
3651
+ * state, the loop would stop permanently: the only resume path is an
3652
+ * `isIntersecting` transition, which a detached element can never produce.
3653
+ * Such a canvas is always "visible" as far as this scene is concerned,
3654
+ * because whether its output is seen depends on the consumer sampling the
3655
+ * texture, which this scene cannot observe.
3643
3656
  */
3644
3657
  watchCanvasVisibility() {
3645
3658
  if (this._canvasObserver || typeof IntersectionObserver === "undefined") return;
3646
3659
  if (!this.canvas || typeof this.canvas.getBoundingClientRect !== "function") return;
3660
+ if (this.canvas.isConnected === false) return;
3647
3661
  this._canvasObserver = new IntersectionObserver((entries) => {
3648
3662
  const entry = entries[entries.length - 1];
3649
3663
  if (!entry) return;
@@ -3879,6 +3893,7 @@ var Scene = (_class7 = class _Scene {
3879
3893
  let el = this.a11yElements.get(node.id);
3880
3894
  const attrs = node.getA11yAttributes();
3881
3895
  const expectedTag = attrs.tag || "div";
3896
+ let emitInitialScroll = null;
3882
3897
  if (el && el.tagName.toLowerCase() !== expectedTag.toLowerCase()) {
3883
3898
  if (el === this.focusedA11yElement) {
3884
3899
  this.focusedA11yElement = null;
@@ -3966,6 +3981,26 @@ var Scene = (_class7 = class _Scene {
3966
3981
  },
3967
3982
  { passive: false }
3968
3983
  );
3984
+ const scrollTarget = el;
3985
+ const emitScroll = () => {
3986
+ node.emit("scroll", {
3987
+ scrollTop: scrollTarget.scrollTop,
3988
+ scrollLeft: scrollTarget.scrollLeft,
3989
+ scrollHeight: scrollTarget.scrollHeight,
3990
+ scrollWidth: scrollTarget.scrollWidth,
3991
+ clientHeight: scrollTarget.clientHeight,
3992
+ clientWidth: scrollTarget.clientWidth
3993
+ });
3994
+ };
3995
+ el.addEventListener(
3996
+ "scroll",
3997
+ () => {
3998
+ emitScroll();
3999
+ this.markDirty();
4000
+ },
4001
+ { passive: true }
4002
+ );
4003
+ emitInitialScroll = emitScroll;
3969
4004
  el.addEventListener("keydown", (e) => {
3970
4005
  node.dispatchEvent(new (0, _chunkZ3HFY75Rjs.VectoJSEvent)("keydown", node, e));
3971
4006
  });
@@ -4196,6 +4231,7 @@ var Scene = (_class7 = class _Scene {
4196
4231
  if (el.style.padding !== padding) el.style.padding = padding;
4197
4232
  if (el.style.boxSizing !== "border-box") el.style.boxSizing = "border-box";
4198
4233
  if (el instanceof HTMLTextAreaElement) el.style.resize = "none";
4234
+ if (el.style.scrollbarWidth !== "none") el.style.scrollbarWidth = "none";
4199
4235
  }
4200
4236
  const nestedIn = container && attrs.role && container.owned.has(attrs.role) ? container : null;
4201
4237
  if (node.a11yFullViewport) {
@@ -4242,6 +4278,7 @@ var Scene = (_class7 = class _Scene {
4242
4278
  const display = visible ? "" : "none";
4243
4279
  if (el.style.display !== display) el.style.display = display;
4244
4280
  }
4281
+ _optionalChain([emitInitialScroll, 'optionalCall', _122 => _122()]);
4245
4282
  }
4246
4283
  if (this._phaseTiming) {
4247
4284
  const projectionStart = performance.now();
@@ -4480,9 +4517,9 @@ var Scene = (_class7 = class _Scene {
4480
4517
  for (let lineIndex = 0; lineIndex < grid.lines.length; lineIndex++) {
4481
4518
  const gridLine = grid.lines[lineIndex];
4482
4519
  const projectedLine = projectionLines[lineIndex];
4483
- const lineHeight = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _122 => _122.lineHeight]), () => ( grid.lineHeight));
4484
- const baseline = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _123 => _123.baseline]), () => ( grid.baseline));
4485
- const lineFont = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _124 => _124.font]), () => ( grid.font));
4520
+ const lineHeight = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _123 => _123.lineHeight]), () => ( grid.lineHeight));
4521
+ const baseline = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _124 => _124.baseline]), () => ( grid.baseline));
4522
+ const lineFont = _nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _125 => _125.font]), () => ( grid.font));
4486
4523
  const lineSignature = contentGridLineSignature(
4487
4524
  grid,
4488
4525
  gridLine,
@@ -4502,8 +4539,8 @@ var Scene = (_class7 = class _Scene {
4502
4539
  lineElement.dir = "ltr";
4503
4540
  lineElement.dataset.vectoGridLine = `${lineIndex}`;
4504
4541
  lineElement.style.position = "absolute";
4505
- lineElement.style.left = `${_nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _125 => _125.x]), () => ( 0))}px`;
4506
- lineElement.style.top = `${(_nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _126 => _126.y]), () => ( lineIndex * grid.lineHeight))) + baseline - _text.cssLineBoxBaseline.call(void 0, lineFont, lineHeight)}px`;
4542
+ lineElement.style.left = `${_nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _126 => _126.x]), () => ( 0))}px`;
4543
+ lineElement.style.top = `${(_nullishCoalesce(_optionalChain([projectedLine, 'optionalAccess', _127 => _127.y]), () => ( lineIndex * grid.lineHeight))) + baseline - _text.cssLineBoxBaseline.call(void 0, lineFont, lineHeight)}px`;
4507
4544
  lineElement.style.width = `${gridLine.width}px`;
4508
4545
  lineElement.style.height = `${lineHeight}px`;
4509
4546
  lineElement.style.whiteSpace = "pre";
@@ -4573,7 +4610,7 @@ var Scene = (_class7 = class _Scene {
4573
4610
  if (selectionLine !== null && selectionLine >= grid.lines.length) {
4574
4611
  rebuiltSelectionLine = true;
4575
4612
  }
4576
- _optionalChain([el, 'access', _127 => _127.lastElementChild, 'optionalAccess', _128 => _128.remove, 'call', _129 => _129()]);
4613
+ _optionalChain([el, 'access', _128 => _128.lastElementChild, 'optionalAccess', _129 => _129.remove, 'call', _130 => _130()]);
4577
4614
  }
4578
4615
  if (rebuiltSelectionLine) this.releaseContentSelectionForRebuild(el);
4579
4616
  el.dataset.vectoProjectionLines = signature;
@@ -4637,7 +4674,7 @@ var Scene = (_class7 = class _Scene {
4637
4674
  if (previous !== void 0 && typeof cancelAnimationFrame === "function") {
4638
4675
  cancelAnimationFrame(previous);
4639
4676
  }
4640
- _optionalChain([this, 'access', _130 => _130.contentGridCalibrationProbes, 'access', _131 => _131.get, 'call', _132 => _132(entityId), 'optionalAccess', _133 => _133.remove, 'call', _134 => _134()]);
4677
+ _optionalChain([this, 'access', _131 => _131.contentGridCalibrationProbes, 'access', _132 => _132.get, 'call', _133 => _133(entityId), 'optionalAccess', _134 => _134.remove, 'call', _135 => _135()]);
4641
4678
  this.contentGridCalibrationProbes.delete(entityId);
4642
4679
  const calibrationStart = typeof performance !== "undefined" ? performance.now() : 0;
4643
4680
  const probe = document.createElement("div");
@@ -4671,7 +4708,7 @@ var Scene = (_class7 = class _Scene {
4671
4708
  target.dataset.vectoGridCalib = generation;
4672
4709
  continue;
4673
4710
  }
4674
- const sourceText = _nullishCoalesce(_optionalChain([target, 'access', _135 => _135.textContent, 'optionalAccess', _136 => _136.slice, 'call', _137 => _137(0, sourceLength)]), () => ( ""));
4711
+ const sourceText = _nullishCoalesce(_optionalChain([target, 'access', _136 => _136.textContent, 'optionalAccess', _137 => _137.slice, 'call', _138 => _138(0, sourceLength)]), () => ( ""));
4675
4712
  if (!sourceText) {
4676
4713
  target.dataset.vectoGridCalib = generation;
4677
4714
  continue;
@@ -4898,12 +4935,12 @@ var Scene = (_class7 = class _Scene {
4898
4935
  syncOverlayGeometry() {
4899
4936
  const parent = this.canvas.parentElement;
4900
4937
  if (!parent) return;
4901
- const canvasRect = _optionalChain([this, 'access', _138 => _138.canvas, 'access', _139 => _139.getBoundingClientRect, 'optionalCall', _140 => _140()]);
4902
- const parentRect = _optionalChain([parent, 'access', _141 => _141.getBoundingClientRect, 'optionalCall', _142 => _142()]);
4903
- const cssWidth = _optionalChain([canvasRect, 'optionalAccess', _143 => _143.width]) || this.canvas.clientWidth || this.width;
4904
- const cssHeight = _optionalChain([canvasRect, 'optionalAccess', _144 => _144.height]) || this.canvas.clientHeight || this.height;
4905
- const left = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _145 => _145.left]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _146 => _146.left]), () => ( 0))) - (parent.clientLeft || 0) + parent.scrollLeft;
4906
- const top = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _147 => _147.top]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _148 => _148.top]), () => ( 0))) - (parent.clientTop || 0) + parent.scrollTop;
4938
+ const canvasRect = _optionalChain([this, 'access', _139 => _139.canvas, 'access', _140 => _140.getBoundingClientRect, 'optionalCall', _141 => _141()]);
4939
+ const parentRect = _optionalChain([parent, 'access', _142 => _142.getBoundingClientRect, 'optionalCall', _143 => _143()]);
4940
+ const cssWidth = _optionalChain([canvasRect, 'optionalAccess', _144 => _144.width]) || this.canvas.clientWidth || this.width;
4941
+ const cssHeight = _optionalChain([canvasRect, 'optionalAccess', _145 => _145.height]) || this.canvas.clientHeight || this.height;
4942
+ const left = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _146 => _146.left]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _147 => _147.left]), () => ( 0))) - (parent.clientLeft || 0) + parent.scrollLeft;
4943
+ const top = (_nullishCoalesce(_optionalChain([canvasRect, 'optionalAccess', _148 => _148.top]), () => ( 0))) - (_nullishCoalesce(_optionalChain([parentRect, 'optionalAccess', _149 => _149.top]), () => ( 0))) - (parent.clientTop || 0) + parent.scrollTop;
4907
4944
  const scaleX = this.width > 0 ? cssWidth / this.width : 1;
4908
4945
  const scaleY = this.height > 0 ? cssHeight / this.height : 1;
4909
4946
  const prev = this._overlayGeometry;
@@ -5042,7 +5079,7 @@ var Scene = (_class7 = class _Scene {
5042
5079
  * (and {@link respectReducedMotion} is on). `0` means uncapped.
5043
5080
  */
5044
5081
  effectiveMaxFPS() {
5045
- const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _149 => _149.reducedMotionQuery, 'optionalAccess', _150 => _150.matches]);
5082
+ const reduced = this.respectReducedMotion && !!_optionalChain([this, 'access', _150 => _150.reducedMotionQuery, 'optionalAccess', _151 => _151.matches]);
5046
5083
  if (reduced)
5047
5084
  return this.maxFPS > 0 ? Math.min(this.maxFPS, REDUCED_MOTION_FPS) : REDUCED_MOTION_FPS;
5048
5085
  return this.maxFPS;
@@ -5137,7 +5174,7 @@ var Scene = (_class7 = class _Scene {
5137
5174
  * @param time - Current absolute time in milliseconds (default 0).
5138
5175
  */
5139
5176
  render(renderer, dt = 0, time = 0) {
5140
- if (_optionalChain([renderer, 'access', _151 => _151.isContextLost, 'optionalCall', _152 => _152()])) return;
5177
+ if (_optionalChain([renderer, 'access', _152 => _152.isContextLost, 'optionalCall', _153 => _153()])) return;
5141
5178
  const isMainRenderer = renderer === this.renderer;
5142
5179
  if (isMainRenderer && this.a11yRoot && this.canvas.parentElement) {
5143
5180
  const parentStyle = this.canvas.parentElement.style;
@@ -5281,7 +5318,7 @@ var Scene = (_class7 = class _Scene {
5281
5318
  }
5282
5319
  renderer.clear();
5283
5320
  if (isMainRenderer) {
5284
- _optionalChain([this, 'access', _153 => _153.pointRenderer, 'optionalAccess', _154 => _154.begin, 'call', _155 => _155()]);
5321
+ _optionalChain([this, 'access', _154 => _154.pointRenderer, 'optionalAccess', _155 => _155.begin, 'call', _156 => _156()]);
5285
5322
  }
5286
5323
  const vw = this.width;
5287
5324
  const vh = this.height;
@@ -5495,9 +5532,9 @@ var Scene = (_class7 = class _Scene {
5495
5532
  const flushT0 = this._phaseTiming ? performance.now() : 0;
5496
5533
  renderer.flush();
5497
5534
  if (isMainRenderer) {
5498
- _optionalChain([this, 'access', _156 => _156.pointRenderer, 'optionalAccess', _157 => _157.flush, 'call', _158 => _158()]);
5535
+ _optionalChain([this, 'access', _157 => _157.pointRenderer, 'optionalAccess', _158 => _158.flush, 'call', _159 => _159()]);
5499
5536
  }
5500
- _optionalChain([renderer, 'access', _159 => _159.present, 'optionalCall', _160 => _160()]);
5537
+ _optionalChain([renderer, 'access', _160 => _160.present, 'optionalCall', _161 => _161()]);
5501
5538
  if (this._phaseTiming) this._recordPhase("flush", performance.now() - flushT0);
5502
5539
  if (flushTiming) endVectoUserTiming(flushTiming);
5503
5540
  if (this._devActive) {
@@ -5778,8 +5815,8 @@ function pointInBounds(b, x, y) {
5778
5815
  function contentGridLineSignature(grid, line, projected, lineHeight, baseline, font, isFirstLine) {
5779
5816
  const parts = [
5780
5817
  // Line box: position, size, and the font that resolves its baseline.
5781
- `${_nullishCoalesce(_optionalChain([projected, 'optionalAccess', _161 => _161.x]), () => ( 0))}`,
5782
- `${_nullishCoalesce(_optionalChain([projected, 'optionalAccess', _162 => _162.y]), () => ( ""))}`,
5818
+ `${_nullishCoalesce(_optionalChain([projected, 'optionalAccess', _162 => _162.x]), () => ( 0))}`,
5819
+ `${_nullishCoalesce(_optionalChain([projected, 'optionalAccess', _163 => _163.y]), () => ( ""))}`,
5783
5820
  `${lineHeight}`,
5784
5821
  `${baseline}`,
5785
5822
  font,
@@ -5966,7 +6003,7 @@ var GridTextEntity = (_class9 = class extends _chunkZ3HFY75Rjs.Entity {
5966
6003
  updateGrid(ascii) {
5967
6004
  this.grid = ascii;
5968
6005
  this.rows = ascii.length;
5969
- this.cols = _optionalChain([ascii, 'access', _163 => _163[0], 'optionalAccess', _164 => _164.length]) || 0;
6006
+ this.cols = _optionalChain([ascii, 'access', _164 => _164[0], 'optionalAccess', _165 => _165.length]) || 0;
5970
6007
  }
5971
6008
  isPointInside(_globalX, _globalY) {
5972
6009
  return false;
@@ -6076,7 +6113,7 @@ var SplineEntity = (_class10 = class extends _chunkZ3HFY75Rjs.Entity {
6076
6113
  this.width = this.bounds.width;
6077
6114
  this.height = this.bounds.height;
6078
6115
  const isGradient = (c) => c !== null && !Array.isArray(c);
6079
- this.containsGradient = (_nullishCoalesce(_optionalChain([this, 'access', _165 => _165.doc, 'access', _166 => _166.equations, 'optionalAccess', _167 => _167.some, 'call', _168 => _168((eq) => isGradient(eq.color_rgb))]), () => ( false))) || (_nullishCoalesce(_optionalChain([this, 'access', _169 => _169.doc, 'access', _170 => _170.paths, 'optionalAccess', _171 => _171.some, 'call', _172 => _172((p) => isGradient(p.color_rgb))]), () => ( false)));
6116
+ this.containsGradient = (_nullishCoalesce(_optionalChain([this, 'access', _166 => _166.doc, 'access', _167 => _167.equations, 'optionalAccess', _168 => _168.some, 'call', _169 => _169((eq) => isGradient(eq.color_rgb))]), () => ( false))) || (_nullishCoalesce(_optionalChain([this, 'access', _170 => _170.doc, 'access', _171 => _171.paths, 'optionalAccess', _172 => _172.some, 'call', _173 => _173((p) => isGradient(p.color_rgb))]), () => ( false)));
6080
6117
  this.interactive = true;
6081
6118
  }
6082
6119
  computeBounds() {
package/dist/index.mjs CHANGED
@@ -3639,10 +3639,24 @@ var Scene = class _Scene {
3639
3639
  * resume when it returns — instead of running the full update/render every
3640
3640
  * frame for a scene nobody can see. No-op (stays "on screen") where
3641
3641
  * `IntersectionObserver` is unavailable, so SSR/jsdom behavior is unchanged.
3642
+ *
3643
+ * Also a no-op for a canvas that is not in the document. An offscreen canvas
3644
+ * used purely as a texture source — `@vectojs/three`'s `ThreeAdapter` wraps
3645
+ * one in a `CanvasTexture`, and the same pattern shows up in any
3646
+ * render-to-texture setup — is never appended anywhere, and an
3647
+ * `IntersectionObserver` reports a detached element as not intersecting.
3648
+ * Observing it would therefore set `_canvasOnScreen = false` on the first
3649
+ * callback, and since {@link loop} returns without rescheduling in that
3650
+ * state, the loop would stop permanently: the only resume path is an
3651
+ * `isIntersecting` transition, which a detached element can never produce.
3652
+ * Such a canvas is always "visible" as far as this scene is concerned,
3653
+ * because whether its output is seen depends on the consumer sampling the
3654
+ * texture, which this scene cannot observe.
3642
3655
  */
3643
3656
  watchCanvasVisibility() {
3644
3657
  if (this._canvasObserver || typeof IntersectionObserver === "undefined") return;
3645
3658
  if (!this.canvas || typeof this.canvas.getBoundingClientRect !== "function") return;
3659
+ if (this.canvas.isConnected === false) return;
3646
3660
  this._canvasObserver = new IntersectionObserver((entries) => {
3647
3661
  const entry = entries[entries.length - 1];
3648
3662
  if (!entry) return;
@@ -3878,6 +3892,7 @@ var Scene = class _Scene {
3878
3892
  let el = this.a11yElements.get(node.id);
3879
3893
  const attrs = node.getA11yAttributes();
3880
3894
  const expectedTag = attrs.tag || "div";
3895
+ let emitInitialScroll = null;
3881
3896
  if (el && el.tagName.toLowerCase() !== expectedTag.toLowerCase()) {
3882
3897
  if (el === this.focusedA11yElement) {
3883
3898
  this.focusedA11yElement = null;
@@ -3965,6 +3980,26 @@ var Scene = class _Scene {
3965
3980
  },
3966
3981
  { passive: false }
3967
3982
  );
3983
+ const scrollTarget = el;
3984
+ const emitScroll = () => {
3985
+ node.emit("scroll", {
3986
+ scrollTop: scrollTarget.scrollTop,
3987
+ scrollLeft: scrollTarget.scrollLeft,
3988
+ scrollHeight: scrollTarget.scrollHeight,
3989
+ scrollWidth: scrollTarget.scrollWidth,
3990
+ clientHeight: scrollTarget.clientHeight,
3991
+ clientWidth: scrollTarget.clientWidth
3992
+ });
3993
+ };
3994
+ el.addEventListener(
3995
+ "scroll",
3996
+ () => {
3997
+ emitScroll();
3998
+ this.markDirty();
3999
+ },
4000
+ { passive: true }
4001
+ );
4002
+ emitInitialScroll = emitScroll;
3968
4003
  el.addEventListener("keydown", (e) => {
3969
4004
  node.dispatchEvent(new VectoJSEvent("keydown", node, e));
3970
4005
  });
@@ -4195,6 +4230,7 @@ var Scene = class _Scene {
4195
4230
  if (el.style.padding !== padding) el.style.padding = padding;
4196
4231
  if (el.style.boxSizing !== "border-box") el.style.boxSizing = "border-box";
4197
4232
  if (el instanceof HTMLTextAreaElement) el.style.resize = "none";
4233
+ if (el.style.scrollbarWidth !== "none") el.style.scrollbarWidth = "none";
4198
4234
  }
4199
4235
  const nestedIn = container && attrs.role && container.owned.has(attrs.role) ? container : null;
4200
4236
  if (node.a11yFullViewport) {
@@ -4241,6 +4277,7 @@ var Scene = class _Scene {
4241
4277
  const display = visible ? "" : "none";
4242
4278
  if (el.style.display !== display) el.style.display = display;
4243
4279
  }
4280
+ emitInitialScroll?.();
4244
4281
  }
4245
4282
  if (this._phaseTiming) {
4246
4283
  const projectionStart = performance.now();
@@ -374,7 +374,30 @@ export interface A11yAttributes {
374
374
  /**
375
375
  * Union of all pointer/interaction events that can be emitted by an {@link Entity}.
376
376
  */
377
- export type VectoEvent = 'click' | 'dblclick' | 'hover' | 'pointerdown' | 'pointerup' | 'pointercancel' | 'pointermove' | 'pointerleave' | 'change' | 'focus' | 'blur' | 'wheel' | 'keydown' | 'keyup';
377
+ export type VectoEvent = 'click' | 'dblclick' | 'hover' | 'pointerdown' | 'pointerup' | 'pointercancel' | 'pointermove' | 'pointerleave' | 'change' | 'focus' | 'blur' | 'wheel' | 'scroll' | 'keydown' | 'keyup';
378
+ /**
379
+ * Payload of the {@link VectoEvent} `'scroll'`: the scroll geometry of an
380
+ * entity's accessibility/automation shadow node, in CSS pixels.
381
+ *
382
+ * An entity that paints its own view of scrollable content (a text editor, a
383
+ * list) should treat these as authoritative while the mirror exists — the
384
+ * browser resolved them against the same content, and any offset the element
385
+ * reports (`selectionStart` from a click) is relative to them.
386
+ */
387
+ export interface ScrollEventPayload {
388
+ /** The mirror's vertical scroll offset. */
389
+ scrollTop: number;
390
+ /** The mirror's horizontal scroll offset. */
391
+ scrollLeft: number;
392
+ /** Full scrollable content height, including the part out of view. */
393
+ scrollHeight: number;
394
+ /** Full scrollable content width, including the part out of view. */
395
+ scrollWidth: number;
396
+ /** Visible height inside the mirror's padding box (excludes any scrollbar). */
397
+ clientHeight: number;
398
+ /** Visible width inside the mirror's padding box (excludes any scrollbar). */
399
+ clientWidth: number;
400
+ }
378
401
  /** Options for {@link Entity.on} / {@link Entity.off}. */
379
402
  export interface ListenerOptions {
380
403
  /** Register the listener for the capture phase (root→target) instead of bubble. */
@@ -1168,6 +1168,19 @@ export declare class Scene {
1168
1168
  * resume when it returns — instead of running the full update/render every
1169
1169
  * frame for a scene nobody can see. No-op (stays "on screen") where
1170
1170
  * `IntersectionObserver` is unavailable, so SSR/jsdom behavior is unchanged.
1171
+ *
1172
+ * Also a no-op for a canvas that is not in the document. An offscreen canvas
1173
+ * used purely as a texture source — `@vectojs/three`'s `ThreeAdapter` wraps
1174
+ * one in a `CanvasTexture`, and the same pattern shows up in any
1175
+ * render-to-texture setup — is never appended anywhere, and an
1176
+ * `IntersectionObserver` reports a detached element as not intersecting.
1177
+ * Observing it would therefore set `_canvasOnScreen = false` on the first
1178
+ * callback, and since {@link loop} returns without rescheduling in that
1179
+ * state, the loop would stop permanently: the only resume path is an
1180
+ * `isIntersecting` transition, which a detached element can never produce.
1181
+ * Such a canvas is always "visible" as far as this scene is concerned,
1182
+ * because whether its output is seen depends on the consumer sampling the
1183
+ * texture, which this scene cannot observe.
1171
1184
  */
1172
1185
  private watchCanvasVisibility;
1173
1186
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "dependencies": {
67
67
  "@vectojs/animation": "^0.1.1",
68
- "@vectojs/layout": "^0.5.0",
68
+ "@vectojs/layout": "^0.6.0",
69
69
  "@vectojs/math": "^0.1.1",
70
70
  "@vectojs/text": "^0.3.0"
71
71
  },