@vectojs/core 1.18.0 → 1.20.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.mjs CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  MSDFTextEntity,
14
14
  SVGEntity,
15
15
  VectoJSEvent
16
- } from "./chunk-XWBVBXFZ.mjs";
16
+ } from "./chunk-TJCXB2F6.mjs";
17
17
 
18
18
  // src/tree/Scene.ts
19
19
  import { SpringDriver, TweenDriver } from "@vectojs/animation";
@@ -1538,6 +1538,70 @@ var Scene = class _Scene {
1538
1538
  renderMode = "always";
1539
1539
  /** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
1540
1540
  static MAX_DIRTY_REASONS = 200;
1541
+ _phaseTiming = false;
1542
+ _phaseTotals = /* @__PURE__ */ new Map();
1543
+ /**
1544
+ * Start or stop per-phase render timing.
1545
+ *
1546
+ * Off by default, and the probes compile to a single boolean test when off:
1547
+ * these sit on the frame path, so the disabled cost has to be nothing. Enable,
1548
+ * run the scene, then read {@link renderPhases}.
1549
+ *
1550
+ * Exists because a frame total cannot tell you where the time went. The
1551
+ * markdown streaming benchmark put render at 85-99% of an append's cost, and
1552
+ * there was no way to decompose that number further — which is exactly the
1553
+ * position that led to two wrong optimisation guesses earlier
1554
+ * (`CodeBlock` reuse, hit-grid fusion), both of which measured as no change.
1555
+ */
1556
+ setPhaseTiming(enabled) {
1557
+ this._phaseTiming = enabled;
1558
+ if (!enabled) this.clearRenderPhases();
1559
+ }
1560
+ /** Whether per-phase render timing is being recorded. */
1561
+ get phaseTiming() {
1562
+ return this._phaseTiming;
1563
+ }
1564
+ /**
1565
+ * Accumulate one phase sample.
1566
+ *
1567
+ * Totals rather than a per-frame log: the question is always "which phase owns
1568
+ * the frame", and a log of thousands of samples answers it less directly while
1569
+ * costing far more memory. `maxMs` is kept because a phase that is cheap on
1570
+ * average but spikes is a different problem from one that is uniformly slow.
1571
+ */
1572
+ _recordPhase(phase, ms) {
1573
+ const existing = this._phaseTotals.get(phase);
1574
+ if (existing) {
1575
+ existing.totalMs += ms;
1576
+ existing.calls++;
1577
+ if (ms > existing.maxMs) existing.maxMs = ms;
1578
+ return;
1579
+ }
1580
+ this._phaseTotals.set(phase, { totalMs: ms, calls: 1, maxMs: ms });
1581
+ }
1582
+ /**
1583
+ * Recorded phase timings, most expensive first, with each phase's share of the
1584
+ * measured total.
1585
+ *
1586
+ * `share` is the number that matters: a phase at 4% cannot be worth optimising
1587
+ * however inefficient it looks in isolation.
1588
+ */
1589
+ get renderPhases() {
1590
+ const entries = [...this._phaseTotals.entries()];
1591
+ const denominator = entries.filter(([phase]) => phase !== "render").reduce((sum, [, v]) => sum + v.totalMs, 0);
1592
+ return entries.map(([phase, v]) => ({
1593
+ phase,
1594
+ totalMs: +v.totalMs.toFixed(3),
1595
+ calls: v.calls,
1596
+ avgMs: +(v.totalMs / Math.max(1, v.calls)).toFixed(4),
1597
+ maxMs: +v.maxMs.toFixed(3),
1598
+ share: phase === "render" ? null : +(100 * v.totalMs / Math.max(1e-9, denominator)).toFixed(1)
1599
+ })).sort((a, b) => b.totalMs - a.totalMs);
1600
+ }
1601
+ /** Drop recorded phase timings, keeping timing enabled. */
1602
+ clearRenderPhases() {
1603
+ this._phaseTotals.clear();
1604
+ }
1541
1605
  _dirtyTracking = false;
1542
1606
  _dirtyReasons = /* @__PURE__ */ new Map();
1543
1607
  dirty = true;
@@ -2834,6 +2898,20 @@ var Scene = class _Scene {
2834
2898
  delete el.dataset.vectoGridCalibrationMs;
2835
2899
  this.releaseContentSelectionForRebuild(el);
2836
2900
  }
2901
+ /**
2902
+ * Drop any projected elements under `node` without touching the entity tree.
2903
+ *
2904
+ * Used when the walk reaches an invisible subtree: the entities stay put (a
2905
+ * later `show()` re-projects them), but nothing under here may remain
2906
+ * focusable or announced while hidden.
2907
+ */
2908
+ pruneA11ySubtree(node) {
2909
+ if (this.a11yElements.has(node.id) || this.contentElements.has(node.id)) {
2910
+ this.removeA11yRecursively(node);
2911
+ return;
2912
+ }
2913
+ for (const child of node.children) this.pruneA11ySubtree(child);
2914
+ }
2837
2915
  removeA11yRecursively(node) {
2838
2916
  if (node.isDOMPortal) {
2839
2917
  node.releaseDOMBindings();
@@ -3151,7 +3229,9 @@ var Scene = class _Scene {
3151
3229
  step(dt) {
3152
3230
  const time = this.lastTime + dt;
3153
3231
  this.lastTime = time;
3232
+ const t0 = this._phaseTiming ? performance.now() : 0;
3154
3233
  this.render(this.renderer, dt, time);
3234
+ if (this._phaseTiming) this._recordPhase("render", performance.now() - t0);
3155
3235
  this.dirty = false;
3156
3236
  }
3157
3237
  /**
@@ -3286,6 +3366,10 @@ var Scene = class _Scene {
3286
3366
  if (node.isDOMPortal) {
3287
3367
  return;
3288
3368
  }
3369
+ if (node.a11yHidden) {
3370
+ this.pruneA11ySubtree(node);
3371
+ return;
3372
+ }
3289
3373
  if (this.shouldProjectA11y(node)) {
3290
3374
  let el = this.a11yElements.get(node.id);
3291
3375
  const attrs = node.getA11yAttributes();
@@ -3559,6 +3643,28 @@ var Scene = class _Scene {
3559
3643
  "aria-level",
3560
3644
  attrs.level === void 0 ? void 0 : String(attrs.level)
3561
3645
  );
3646
+ this.syncOptionalAttribute(
3647
+ el,
3648
+ "aria-posinset",
3649
+ attrs.posInSet === void 0 ? void 0 : String(attrs.posInSet)
3650
+ );
3651
+ this.syncOptionalAttribute(
3652
+ el,
3653
+ "aria-setsize",
3654
+ attrs.setSize === void 0 ? void 0 : String(attrs.setSize)
3655
+ );
3656
+ this.syncOptionalAttribute(
3657
+ el,
3658
+ "aria-rowcount",
3659
+ attrs.rowCount === void 0 ? void 0 : String(attrs.rowCount)
3660
+ );
3661
+ this.syncOptionalAttribute(
3662
+ el,
3663
+ "aria-rowindex",
3664
+ attrs.rowIndex === void 0 ? void 0 : String(attrs.rowIndex)
3665
+ );
3666
+ this.syncOptionalAttribute(el, "aria-valuetext", attrs.valueText);
3667
+ this.syncOptionalAttribute(el, "aria-orientation", attrs.orientation);
3562
3668
  if (attrs.value !== void 0) {
3563
3669
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
3564
3670
  if (el.value !== attrs.value) {
@@ -4350,7 +4456,9 @@ var Scene = class _Scene {
4350
4456
  }
4351
4457
  this._lastRenderTick = time;
4352
4458
  this._lastDt = dt;
4459
+ const phaseClock = this._phaseTiming ? performance.now() : 0;
4353
4460
  this.render(this.renderer, dt, time);
4461
+ if (this._phaseTiming) this._recordPhase("render", performance.now() - phaseClock);
4354
4462
  this._lastFrameMs = (typeof performance !== "undefined" ? performance.now() : time) - now;
4355
4463
  this._renderedFrames++;
4356
4464
  const hasActiveAnimation = this.frameHadAnimation;
@@ -4360,9 +4468,13 @@ var Scene = class _Scene {
4360
4468
  if ((hasInteractive || this.a11yElements.size > 0 || wantsContentSync) && (shouldSyncInterval || this.a11yPendingSyncAfterAnimation)) {
4361
4469
  this.lastA11ySync = time;
4362
4470
  if (hasInteractive || wantsContentSync) {
4471
+ const t0 = this._phaseTiming ? performance.now() : 0;
4363
4472
  this.syncA11y(this.root);
4473
+ if (this._phaseTiming) this._recordPhase("a11ySync", performance.now() - t0);
4364
4474
  }
4475
+ const t1 = this._phaseTiming ? performance.now() : 0;
4365
4476
  this.enforceA11yDomOrder();
4477
+ if (this._phaseTiming) this._recordPhase("a11yOrder", performance.now() - t1);
4366
4478
  this.a11yPendingSyncAfterAnimation = hasActiveAnimation;
4367
4479
  } else if (hasActiveAnimation) {
4368
4480
  this.a11yPendingSyncAfterAnimation = true;
@@ -4536,7 +4648,9 @@ var Scene = class _Scene {
4536
4648
  updateWalk(this.root);
4537
4649
  for (const overlay of this.overlayRoot.children) updateWalk(overlay);
4538
4650
  }
4651
+ const wasmT0 = this._phaseTiming ? performance.now() : 0;
4539
4652
  const wasmWorld = wasmMain ? this._syncWasmStore() : null;
4653
+ if (this._phaseTiming) this._recordPhase("transform", performance.now() - wasmT0);
4540
4654
  const wasmSlotEntity = this._slotEntity;
4541
4655
  const renderNode = (node, pa, pb, pc, pd, pe, pf, parentOpacity) => {
4542
4656
  if (isMainRenderer && !wasmMain) {
@@ -4666,7 +4780,13 @@ var Scene = class _Scene {
4666
4780
  );
4667
4781
  }
4668
4782
  } else {
4669
- node.render(renderer);
4783
+ if (this._phaseTiming) {
4784
+ const t0 = performance.now();
4785
+ node.render(renderer);
4786
+ this._recordPhase("entityPaint", performance.now() - t0);
4787
+ } else {
4788
+ node.render(renderer);
4789
+ }
4670
4790
  }
4671
4791
  }
4672
4792
  if (node.clipChildren) {
@@ -4678,20 +4798,24 @@ var Scene = class _Scene {
4678
4798
  renderer.flush();
4679
4799
  renderer.restore();
4680
4800
  };
4801
+ const drawT0 = this._phaseTiming ? performance.now() : 0;
4681
4802
  renderNode(this.root, 1, 0, 0, 1, 0, 0, 1);
4682
4803
  for (const overlay of this.overlayRoot.children) {
4683
4804
  renderNode(overlay, 1, 0, 0, 1, 0, 0, 1);
4684
4805
  }
4806
+ if (this._phaseTiming) this._recordPhase("drawWalk", performance.now() - drawT0);
4685
4807
  if (isMainRenderer) {
4686
4808
  this.frameHadAnimation = walkHadAnimation;
4687
4809
  this.frameHadInteractive = walkHadInteractive;
4688
4810
  this.reconcilePortals();
4689
4811
  }
4812
+ const flushT0 = this._phaseTiming ? performance.now() : 0;
4690
4813
  renderer.flush();
4691
4814
  if (isMainRenderer) {
4692
4815
  this.pointRenderer?.flush();
4693
4816
  }
4694
4817
  renderer.present?.();
4818
+ if (this._phaseTiming) this._recordPhase("flush", performance.now() - flushT0);
4695
4819
  if (this._devActive) {
4696
4820
  this._devFrameCount++;
4697
4821
  this._devRunChecks();
package/dist/text.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _chunkIA3KW4CGjs = require('./chunk-IA3KW4CG.js');
5
+ var _chunkYLH7F4ZVjs = require('./chunk-YLH7F4ZV.js');
6
6
 
7
7
  // src/text/index.ts
8
8
  var _text = require('@vectojs/text'); _createStarExport(_text);
9
9
 
10
10
 
11
11
 
12
- exports.MSDFTextEntity = _chunkIA3KW4CGjs.MSDFTextEntity; exports.SVGEntity = _chunkIA3KW4CGjs.SVGEntity;
12
+ exports.MSDFTextEntity = _chunkYLH7F4ZVjs.MSDFTextEntity; exports.SVGEntity = _chunkYLH7F4ZVjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MSDFTextEntity,
3
3
  SVGEntity
4
- } from "./chunk-XWBVBXFZ.mjs";
4
+ } from "./chunk-TJCXB2F6.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -221,6 +221,48 @@ export interface A11yAttributes {
221
221
  /** `aria-invalid`: the field's current value fails validation. */
222
222
  invalid?: boolean;
223
223
  /** `aria-level`: hierarchical level (headings, tree items, etc.). */
224
+ /**
225
+ * Position within a set, 1-based — projected as `aria-posinset`.
226
+ *
227
+ * Required whenever the DOM contains only part of the set, which is exactly
228
+ * what virtualization produces: a list rendering rows 40-52 of 10,000 otherwise
229
+ * announces "item 3 of 12", because that is all the accessibility tree can see.
230
+ * Pair with {@link setSize}.
231
+ */
232
+ posInSet?: number;
233
+ /**
234
+ * Total size of the set the element belongs to — projected as `aria-setsize`.
235
+ *
236
+ * `-1` is the ARIA-defined value for "unknown but non-empty", appropriate for a
237
+ * lazily loaded set whose total is not yet known.
238
+ */
239
+ setSize?: number;
240
+ /**
241
+ * Total row count of a grid whose DOM holds only the visible rows — projected
242
+ * as `aria-rowcount`. Same rationale as {@link posInSet}: a virtualized table
243
+ * needs to state the real total, not the rendered one.
244
+ */
245
+ rowCount?: number;
246
+ /** 1-based row index within the full grid — projected as `aria-rowindex`. */
247
+ rowIndex?: number;
248
+ /**
249
+ * Human-readable form of a range widget's current value — projected as
250
+ * `aria-valuetext`.
251
+ *
252
+ * A bare `aria-valuenow` is announced as a number out of context: "40" rather
253
+ * than "40 percent" or "Medium". Only set this when the number alone is
254
+ * genuinely ambiguous; a redundant valuetext makes announcements longer for no
255
+ * gain.
256
+ */
257
+ valueText?: string;
258
+ /**
259
+ * Orientation of a composite widget — projected as `aria-orientation`.
260
+ *
261
+ * Worth setting when it differs from the role's default (`slider` and
262
+ * `separator` default horizontal; `listbox`, `menu`, `tree` default vertical),
263
+ * because it tells assistive technology which arrow keys to expect.
264
+ */
265
+ orientation?: 'horizontal' | 'vertical';
224
266
  level?: number;
225
267
  /** `aria-modal`: marks a `role="dialog"` as modal so assistive tech confines
226
268
  * reading to it. Set on a modal dialog's shell. */
@@ -374,6 +416,24 @@ export declare abstract class Entity {
374
416
  * nodes, so on-top components stay clickable.
375
417
  */
376
418
  a11yFullViewport: boolean;
419
+ /**
420
+ * Hide this entity AND its whole subtree from the accessibility/automation
421
+ * projection, regardless of each node's own `interactive` flag.
422
+ *
423
+ * For a container that is logically closed while still mounted — an `Overlay`
424
+ * after `hide()`, a collapsed panel kept in the tree for its transition. Setting
425
+ * `interactive = false` on the container alone is not enough: the projection walk
426
+ * still descends, and any still-interactive child is re-created on the next
427
+ * frame. Measured before this existed: after `Popover.hide()` the popover's own
428
+ * element was gone while its button stayed projected with `tabIndex: 0` and a
429
+ * live box, so a keyboard user could Tab into a hidden popover.
430
+ *
431
+ * Deliberately NOT inferred from `opacity`: `Overlay.hide()` springs opacity
432
+ * toward 0, so mid-transition it reads nonzero (~0.26 when measured) and an
433
+ * `=== 0` test never fires; a threshold would instead silently un-project a
434
+ * faint-but-live control.
435
+ */
436
+ a11yHidden: boolean;
377
437
  /**
378
438
  * Clip this node's children to its local box (`[0,0]–[width,height]`) while
379
439
  * rendering. Combined with translating a content child, this is how
@@ -17,6 +17,27 @@ import { type CoreWasmRuntime } from '../wasm/runtime';
17
17
  import { type HitModuleSource, type HitTestBackend } from '../wasm/hit-backend';
18
18
  import { type AnimModuleSource, type AnimBackend } from '../wasm/anim-backend';
19
19
  import { type ParticleModuleSource, type ParticleBackend } from '../wasm/particle-backend';
20
+ /**
21
+ * A timed phase of a frame.
22
+ *
23
+ * `render` is the ENCLOSING phase — it contains `transform`, `drawWalk` and
24
+ * `flush` — so it is reported without a share to avoid double-counting.
25
+ * `a11ySync` and `a11yOrder` run after `render` in the frame loop, so they are
26
+ * siblings of it, not children.
27
+ */
28
+ export type RenderPhase = 'render' | 'transform' | 'drawWalk' | 'flush' | 'a11ySync' | 'a11yOrder'
29
+ /** Sum of every entity's own render(), nested inside drawWalk. */
30
+ | 'entityPaint';
31
+ export interface RenderPhaseEntry {
32
+ phase: RenderPhase;
33
+ totalMs: number;
34
+ calls: number;
35
+ avgMs: number;
36
+ /** Worst single sample — a spiky phase is a different problem from a slow one. */
37
+ maxMs: number;
38
+ /** Percent of the measured total, or `null` for the enclosing `render` phase. */
39
+ share: number | null;
40
+ }
20
41
  /**
21
42
  * Who marked the scene dirty, and why.
22
43
  *
@@ -230,6 +251,43 @@ export declare class Scene {
230
251
  renderMode: 'always' | 'onDemand';
231
252
  /** Cap on distinct recorded dirty reasons (see `recordDirtyReason`). */
232
253
  private static readonly MAX_DIRTY_REASONS;
254
+ private _phaseTiming;
255
+ private _phaseTotals;
256
+ /**
257
+ * Start or stop per-phase render timing.
258
+ *
259
+ * Off by default, and the probes compile to a single boolean test when off:
260
+ * these sit on the frame path, so the disabled cost has to be nothing. Enable,
261
+ * run the scene, then read {@link renderPhases}.
262
+ *
263
+ * Exists because a frame total cannot tell you where the time went. The
264
+ * markdown streaming benchmark put render at 85-99% of an append's cost, and
265
+ * there was no way to decompose that number further — which is exactly the
266
+ * position that led to two wrong optimisation guesses earlier
267
+ * (`CodeBlock` reuse, hit-grid fusion), both of which measured as no change.
268
+ */
269
+ setPhaseTiming(enabled: boolean): void;
270
+ /** Whether per-phase render timing is being recorded. */
271
+ get phaseTiming(): boolean;
272
+ /**
273
+ * Accumulate one phase sample.
274
+ *
275
+ * Totals rather than a per-frame log: the question is always "which phase owns
276
+ * the frame", and a log of thousands of samples answers it less directly while
277
+ * costing far more memory. `maxMs` is kept because a phase that is cheap on
278
+ * average but spikes is a different problem from one that is uniformly slow.
279
+ */
280
+ private _recordPhase;
281
+ /**
282
+ * Recorded phase timings, most expensive first, with each phase's share of the
283
+ * measured total.
284
+ *
285
+ * `share` is the number that matters: a phase at 4% cannot be worth optimising
286
+ * however inefficient it looks in isolation.
287
+ */
288
+ get renderPhases(): RenderPhaseEntry[];
289
+ /** Drop recorded phase timings, keeping timing enabled. */
290
+ clearRenderPhases(): void;
233
291
  private _dirtyTracking;
234
292
  private _dirtyReasons;
235
293
  private dirty;
@@ -779,6 +837,14 @@ export declare class Scene {
779
837
  */
780
838
  add(entity: Entity): this;
781
839
  private clearContentGridState;
840
+ /**
841
+ * Drop any projected elements under `node` without touching the entity tree.
842
+ *
843
+ * Used when the walk reaches an invisible subtree: the entities stay put (a
844
+ * later `show()` re-projects them), but nothing under here may remain
845
+ * focusable or announced while hidden.
846
+ */
847
+ private pruneA11ySubtree;
782
848
  private removeA11yRecursively;
783
849
  /**
784
850
  * If `el` is about to be removed from the DOM while it holds browser focus,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },