@vizejs/fresco 0.717.0 → 0.718.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.
Files changed (50) hide show
  1. package/dist/{accessibility-DY_alnTr.d.mts → accessibility-DwzT925M.d.mts} +11 -5
  2. package/dist/accessibility-DwzT925M.d.mts.map +1 -0
  3. package/dist/{app-B9yJzz5S.mjs → app-C_wFlWSV.mjs} +368 -123
  4. package/dist/app-C_wFlWSV.mjs.map +1 -0
  5. package/dist/app-_QmZ-PoH.d.mts +270 -0
  6. package/dist/app-_QmZ-PoH.d.mts.map +1 -0
  7. package/dist/ascii-font/index.d.mts +60 -0
  8. package/dist/ascii-font/index.d.mts.map +1 -0
  9. package/dist/ascii-font/index.mjs +68 -0
  10. package/dist/ascii-font/index.mjs.map +1 -0
  11. package/dist/components/index.d.mts +2 -2
  12. package/dist/components/index.mjs +2 -2
  13. package/dist/components-DzkL-yHj.mjs +6165 -0
  14. package/dist/components-DzkL-yHj.mjs.map +1 -0
  15. package/dist/composables/index.d.mts +3 -3
  16. package/dist/composables/index.mjs +4 -4
  17. package/dist/{composables-uj7hkBfP.mjs → composables-BJeirPSH.mjs} +116 -55
  18. package/dist/composables-BJeirPSH.mjs.map +1 -0
  19. package/dist/{index-BjK3JSNo.d.mts → index-B3zQ6E-5.d.mts} +1076 -236
  20. package/dist/index-B3zQ6E-5.d.mts.map +1 -0
  21. package/dist/{index-Dikou9bu.d.mts → index-Ddp2Txtm.d.mts} +52 -4
  22. package/dist/index-Ddp2Txtm.d.mts.map +1 -0
  23. package/dist/index.d.mts +98 -7
  24. package/dist/index.d.mts.map +1 -0
  25. package/dist/index.mjs +262 -5
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/{protocol-D8xt3DG7.d.mts → protocol-DavoeNLn.d.mts} +14 -4
  28. package/dist/{protocol-D8xt3DG7.d.mts.map → protocol-DavoeNLn.d.mts.map} +1 -1
  29. package/dist/qr-code/index.d.mts +101 -0
  30. package/dist/qr-code/index.d.mts.map +1 -0
  31. package/dist/qr-code/index.mjs +88 -0
  32. package/dist/qr-code/index.mjs.map +1 -0
  33. package/dist/testing/index.d.mts +3 -3
  34. package/dist/testing/index.mjs +1 -1
  35. package/dist/{useApp-CVB2jNCO.d.mts → useApp-vIRJ5Th3.d.mts} +34 -8
  36. package/dist/useApp-vIRJ5Th3.d.mts.map +1 -0
  37. package/dist/{useVirtualViewport-DYuUlTxU.mjs → useBoxMetrics-lF5Myjc7.mjs} +66 -6
  38. package/dist/useBoxMetrics-lF5Myjc7.mjs.map +1 -0
  39. package/package.json +14 -6
  40. package/dist/accessibility-DY_alnTr.d.mts.map +0 -1
  41. package/dist/app-B9yJzz5S.mjs.map +0 -1
  42. package/dist/app-CKsTWQuP.d.mts +0 -154
  43. package/dist/app-CKsTWQuP.d.mts.map +0 -1
  44. package/dist/components-C57eT1BL.mjs +0 -3406
  45. package/dist/components-C57eT1BL.mjs.map +0 -1
  46. package/dist/composables-uj7hkBfP.mjs.map +0 -1
  47. package/dist/index-BjK3JSNo.d.mts.map +0 -1
  48. package/dist/index-Dikou9bu.d.mts.map +0 -1
  49. package/dist/useApp-CVB2jNCO.d.mts.map +0 -1
  50. package/dist/useVirtualViewport-DYuUlTxU.mjs.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { format } from "node:util";
2
- import { computed, createRenderer, defineComponent, h, inject, isRef, onUnmounted, ref, shallowRef, watch } from "vue";
2
+ import { computed, createRenderer, defineComponent, h, inject, isRef, nextTick, onUnmounted, provide, ref, shallowRef, watch } from "vue";
3
3
  //#region src/renderPayload.ts
4
4
  const displays = ["flex", "none"];
5
5
  const positions = [
@@ -89,6 +89,11 @@ function numberValue(value) {
89
89
  function booleanValue(value) {
90
90
  return typeof value === "boolean" ? value : void 0;
91
91
  }
92
+ function stringLines(value) {
93
+ if (!Array.isArray(value)) return void 0;
94
+ const lines = value.filter((line) => typeof line === "string");
95
+ return lines.length > 0 ? lines : void 0;
96
+ }
92
97
  function enabledValue(value) {
93
98
  return value === true ? true : void 0;
94
99
  }
@@ -199,6 +204,15 @@ function frescoNodeToRenderNode(node) {
199
204
  }
200
205
  return renderNode;
201
206
  }
207
+ if (node.type === "raw") {
208
+ const renderNode = {
209
+ ...common,
210
+ nodeType: "raw"
211
+ };
212
+ const lines = stringLines(node.props.lines);
213
+ if (lines) renderNode.lines = lines;
214
+ return renderNode;
215
+ }
202
216
  const renderNode = {
203
217
  ...common,
204
218
  nodeType: "input"
@@ -207,12 +221,16 @@ function frescoNodeToRenderNode(node) {
207
221
  const placeholder = stringValue$1(node.props.placeholder);
208
222
  const focused = booleanValue(firstValue(node.props, "focused", "focus"));
209
223
  const cursor = numberValue(node.props.cursor);
224
+ const selectionStart = numberValue(firstValue(node.props, "selectionStart", "selection_start"));
225
+ const selectionEnd = numberValue(firstValue(node.props, "selectionEnd", "selection_end"));
210
226
  const mask = booleanValue(node.props.mask);
211
227
  const maskChar = stringValue$1(firstValue(node.props, "maskChar", "mask_char", "mask-char"));
212
228
  if (value !== void 0) renderNode.value = value;
213
229
  if (placeholder !== void 0) renderNode.placeholder = placeholder;
214
230
  if (focused !== void 0) renderNode.focused = focused;
215
231
  if (cursor !== void 0) renderNode.cursor = cursor;
232
+ if (selectionStart !== void 0) renderNode.selectionStart = selectionStart;
233
+ if (selectionEnd !== void 0) renderNode.selectionEnd = selectionEnd;
216
234
  if (mask !== void 0) renderNode.mask = mask;
217
235
  if (maskChar !== void 0) renderNode.maskChar = maskChar;
218
236
  return renderNode;
@@ -371,6 +389,7 @@ function useApp() {
371
389
  * useFocus - Focus management composable
372
390
  */
373
391
  const FOCUS_KEY = Symbol("fresco-focus");
392
+ const FOCUS_PARENT_KEY = Symbol("fresco-focus-parent");
374
393
  function activeFocusables(focusables) {
375
394
  return focusables.filter((focusable) => focusable.isActive);
376
395
  }
@@ -382,38 +401,76 @@ function createFocusManager() {
382
401
  const activeId = computed(() => focusedId.value ?? void 0);
383
402
  const focusables = ref([]);
384
403
  const focusableIds = computed(() => activeFocusables(focusables.value).map(({ id }) => id));
404
+ const trapStack = ref([]);
405
+ const activeTrapId = computed(() => trapStack.value.at(-1));
385
406
  const isEnabled = ref(true);
386
- const focus = (id) => {
387
- const target = focusables.value.find((focusable) => focusable.id === id);
388
- if (isEnabled.value && target?.isActive) focusedId.value = id;
407
+ const findFocusable = (id) => focusables.value.find((focusable) => focusable.id === id);
408
+ const isWithin = (id, rootId) => {
409
+ let currentId = id;
410
+ const visited = /* @__PURE__ */ new Set();
411
+ while (currentId !== null) {
412
+ if (currentId === rootId) return true;
413
+ if (visited.has(currentId)) return false;
414
+ visited.add(currentId);
415
+ currentId = findFocusable(currentId)?.parentId ?? null;
416
+ }
417
+ return false;
389
418
  };
390
- const focusNext = () => {
391
- const active = activeFocusables(focusables.value);
392
- if (!isEnabled.value || active.length === 0) return;
393
- const currentIndex = focusedId.value !== null ? focusables.value.findIndex((focusable) => focusable.id === focusedId.value) : -1;
394
- focusedId.value = focusables.value.slice(currentIndex + 1).find((focusable) => focusable.isActive)?.id ?? active[0]?.id ?? null;
419
+ const allowedByActiveTrap = (id) => {
420
+ const trapId = activeTrapId.value;
421
+ return trapId === void 0 || isWithin(id, trapId);
395
422
  };
396
- const focusPrevious = () => {
397
- const active = activeFocusables(focusables.value);
423
+ const scopedFocusables = () => activeFocusables(focusables.value).filter(({ id }) => allowedByActiveTrap(id));
424
+ const delegatedTarget = (target) => {
425
+ let current = target;
426
+ const visited = /* @__PURE__ */ new Set();
427
+ while (current.delegateFocusTo !== null && !visited.has(current.id)) {
428
+ visited.add(current.id);
429
+ const delegated = findFocusable(current.delegateFocusTo);
430
+ if (!delegated?.isActive || !isWithin(delegated.id, current.id) || !allowedByActiveTrap(delegated.id)) break;
431
+ current = delegated;
432
+ }
433
+ return current;
434
+ };
435
+ const focus = (id) => {
436
+ const target = findFocusable(id);
437
+ if (isEnabled.value && target?.isActive && allowedByActiveTrap(id)) focusedId.value = delegatedTarget(target).id;
438
+ };
439
+ const moveFocus = (offset) => {
440
+ const active = scopedFocusables();
398
441
  if (!isEnabled.value || active.length === 0) return;
399
- const currentIndex = focusedId.value !== null ? focusables.value.findIndex((focusable) => focusable.id === focusedId.value) : focusables.value.length;
400
- focusedId.value = focusables.value.slice(0, currentIndex < 0 ? 0 : currentIndex).findLast((focusable) => focusable.isActive)?.id ?? active.at(-1)?.id ?? null;
442
+ const currentIndex = active.findIndex((focusable) => focusable.id === focusedId.value);
443
+ for (let distance = 1; distance <= active.length; distance += 1) {
444
+ const candidate = active[currentIndex === -1 ? offset === 1 ? distance - 1 : active.length - distance : (currentIndex + offset * distance + active.length * 2) % active.length];
445
+ if (!candidate) continue;
446
+ const resolved = delegatedTarget(candidate);
447
+ if (resolved.id !== focusedId.value || active.length === 1) {
448
+ focusedId.value = resolved.id;
449
+ return;
450
+ }
451
+ }
401
452
  };
453
+ const focusNext = () => moveFocus(1);
454
+ const focusPrevious = () => moveFocus(-1);
402
455
  const register = (id, options = {}) => {
403
456
  const existing = focusables.value.find((focusable) => focusable.id === id);
404
457
  if (existing) {
405
458
  existing.isActive = options.isActive ?? existing.isActive;
406
459
  if ("parentId" in options) existing.parentId = options.parentId ?? null;
460
+ if ("delegateFocusTo" in options) existing.delegateFocusTo = options.delegateFocusTo ?? null;
407
461
  } else focusables.value.push({
408
462
  id,
409
463
  isActive: options.isActive ?? true,
410
- parentId: options.parentId ?? null
464
+ parentId: options.parentId ?? null,
465
+ delegateFocusTo: options.delegateFocusTo ?? null
411
466
  });
412
467
  if (options.autoFocus && focusedId.value === null && options.isActive !== false) focus(id);
413
468
  };
414
469
  const unregister = (id) => {
415
470
  const index = focusables.value.findIndex((focusable) => focusable.id === id);
416
471
  if (index !== -1) {
472
+ const trapIndex = trapStack.value.indexOf(id);
473
+ if (trapIndex !== -1) trapStack.value.splice(trapIndex);
417
474
  focusables.value.splice(index, 1);
418
475
  if (focusedId.value === id) focusedId.value = null;
419
476
  }
@@ -422,12 +479,33 @@ function createFocusManager() {
422
479
  const focusable = focusables.value.find((item) => item.id === id);
423
480
  if (!focusable) return;
424
481
  focusable.isActive = isActive;
482
+ if (!isActive) {
483
+ const trapIndex = trapStack.value.indexOf(id);
484
+ if (trapIndex !== -1) trapStack.value.splice(trapIndex);
485
+ }
425
486
  if (!isActive && focusedId.value === id) focusedId.value = null;
426
487
  };
427
488
  const setParent = (id, parentId) => {
428
- const focusable = focusables.value.find((item) => item.id === id);
489
+ const focusable = findFocusable(id);
429
490
  if (!focusable) return;
430
491
  focusable.parentId = parentId ?? null;
492
+ if (focusedId.value !== null && !allowedByActiveTrap(focusedId.value)) focusedId.value = null;
493
+ };
494
+ const setDelegate = (id, delegateFocusTo) => {
495
+ const focusable = findFocusable(id);
496
+ if (focusable) focusable.delegateFocusTo = delegateFocusTo ?? null;
497
+ };
498
+ const trapFocus = (id) => {
499
+ if (!findFocusable(id)?.isActive || !allowedByActiveTrap(id)) return;
500
+ if (activeTrapId.value !== id) trapStack.value.push(id);
501
+ if (focusedId.value === null || !isWithin(focusedId.value, id)) focus(id);
502
+ };
503
+ const releaseFocusTrap = (id) => {
504
+ const index = trapStack.value.indexOf(id);
505
+ if (index === -1) return;
506
+ trapStack.value.splice(index);
507
+ const remaining = activeTrapId.value;
508
+ if (remaining !== void 0 && focusedId.value !== null && !isWithin(focusedId.value, remaining)) focus(remaining);
431
509
  };
432
510
  const hasFocusedDescendant = (id) => {
433
511
  let currentId = focusedId.value;
@@ -436,7 +514,7 @@ function createFocusManager() {
436
514
  while (true) {
437
515
  if (visited.has(currentId)) return false;
438
516
  visited.add(currentId);
439
- const parentId = focusables.value.find((focusable) => focusable.id === currentId)?.parentId ?? null;
517
+ const parentId = findFocusable(currentId)?.parentId ?? null;
440
518
  if (parentId === null) return false;
441
519
  if (parentId === id) return true;
442
520
  currentId = parentId;
@@ -453,6 +531,7 @@ function createFocusManager() {
453
531
  focusedId,
454
532
  activeId,
455
533
  focusableIds,
534
+ activeTrapId,
456
535
  isEnabled,
457
536
  enableFocus,
458
537
  disableFocus,
@@ -463,23 +542,35 @@ function createFocusManager() {
463
542
  unregister,
464
543
  setActive,
465
544
  setParent,
545
+ setDelegate,
546
+ trapFocus,
547
+ releaseFocusTrap,
466
548
  hasFocusedDescendant
467
549
  };
468
550
  }
551
+ /** Make an owning focus target the default parent for nested useFocus calls. */
552
+ function provideFocusScope(parentId) {
553
+ provide(FOCUS_PARENT_KEY, parentId);
554
+ }
469
555
  /**
470
556
  * Use focus management
471
557
  */
472
558
  function useFocus(options = {}) {
473
- const { autoFocus = false, id = `focus-${Math.random().toString(36).slice(2)}`, isActive: isActiveOption = true, parentId: parentIdOption = null } = options;
559
+ const inheritedParentId = inject(FOCUS_PARENT_KEY, null);
560
+ const { autoFocus = false, id = `focus-${Math.random().toString(36).slice(2)}`, isActive: isActiveOption = true, delegateFocusTo: delegateFocusToOption = null, trapFocus: trapFocusOption = false } = options;
561
+ const parentIdOption = "parentId" in options ? options.parentId ?? null : inheritedParentId;
474
562
  const manager = inject(FOCUS_KEY, null);
475
563
  const localFocused = ref(autoFocus);
476
564
  const active = isRef(isActiveOption) ? isActiveOption : ref(isActiveOption);
477
565
  const parentId = isRef(parentIdOption) ? parentIdOption : ref(parentIdOption);
566
+ const delegateFocusTo = isRef(delegateFocusToOption) ? delegateFocusToOption : ref(delegateFocusToOption);
567
+ const trapsFocus = isRef(trapFocusOption) ? trapFocusOption : ref(trapFocusOption);
478
568
  const isFocused = computed(() => {
479
569
  if (manager) return manager.isEnabled.value && manager.focusedId.value === id;
480
570
  return active.value && localFocused.value;
481
571
  });
482
572
  const hasFocusedDescendant = computed(() => manager ? manager.hasFocusedDescendant(id) : false);
573
+ const isFocusTrapActive = computed(() => manager?.activeTrapId.value === id);
483
574
  const focus = (targetId = id) => {
484
575
  if (manager) manager.focus(targetId);
485
576
  else if (targetId === id) localFocused.value = true;
@@ -493,16 +584,27 @@ function useFocus(options = {}) {
493
584
  manager.register(id, {
494
585
  isActive: active.value,
495
586
  autoFocus,
496
- parentId: parentId.value
587
+ parentId: parentId.value,
588
+ delegateFocusTo: delegateFocusTo.value
497
589
  });
590
+ if (trapsFocus.value) manager.trapFocus(id);
498
591
  watch(active, (enabled) => {
499
592
  manager.setActive(id, enabled);
593
+ if (enabled && trapsFocus.value) manager.trapFocus(id);
500
594
  if (enabled && autoFocus && !manager.focusedId.value) manager.focus(id);
501
595
  }, { immediate: false });
502
596
  watch(parentId, (nextParentId) => {
503
597
  manager.setParent(id, nextParentId);
504
598
  }, { immediate: false });
599
+ watch(delegateFocusTo, (nextDelegate) => {
600
+ manager.setDelegate(id, nextDelegate);
601
+ }, { immediate: false });
602
+ watch(trapsFocus, (trapped) => {
603
+ if (trapped) manager.trapFocus(id);
604
+ else manager.releaseFocusTrap(id);
605
+ }, { immediate: false });
505
606
  onUnmounted(() => {
607
+ manager.releaseFocusTrap(id);
506
608
  manager.unregister(id);
507
609
  });
508
610
  }
@@ -510,6 +612,7 @@ function useFocus(options = {}) {
510
612
  id,
511
613
  isFocused,
512
614
  hasFocusedDescendant,
615
+ isFocusTrapActive,
513
616
  focus,
514
617
  blur
515
618
  };
@@ -521,6 +624,7 @@ function useFocusManager() {
521
624
  const manager = inject(FOCUS_KEY, null);
522
625
  if (!manager) {
523
626
  const empty = ref([]);
627
+ const activeId = ref(void 0);
524
628
  return {
525
629
  enableFocus: () => {},
526
630
  disableFocus: () => {},
@@ -528,8 +632,12 @@ function useFocusManager() {
528
632
  focusPrevious: () => {},
529
633
  focus: (_id) => {},
530
634
  hasFocusedDescendant: (_id) => false,
531
- activeId: ref(void 0),
532
- focusableIds: empty
635
+ trapFocus: (_id) => {},
636
+ releaseFocusTrap: (_id) => {},
637
+ setDelegate: (_id, _delegateFocusTo) => {},
638
+ activeId,
639
+ focusableIds: empty,
640
+ activeTrapId: activeId
533
641
  };
534
642
  }
535
643
  return {
@@ -539,8 +647,12 @@ function useFocusManager() {
539
647
  focusPrevious: manager.focusPrevious,
540
648
  focus: manager.focus,
541
649
  hasFocusedDescendant: manager.hasFocusedDescendant,
650
+ trapFocus: manager.trapFocus,
651
+ releaseFocusTrap: manager.releaseFocusTrap,
652
+ setDelegate: manager.setDelegate,
542
653
  activeId: manager.activeId,
543
- focusableIds: manager.focusableIds
654
+ focusableIds: manager.focusableIds,
655
+ activeTrapId: manager.activeTrapId
544
656
  };
545
657
  }
546
658
  //#endregion
@@ -674,6 +786,10 @@ let lastRenderLayouts = [];
674
786
  function updateLastRenderLayouts(layouts) {
675
787
  lastRenderLayouts = layouts;
676
788
  }
789
+ /** Read the complete layout snapshot produced by the latest native frame. */
790
+ function getLastRenderLayouts() {
791
+ return lastRenderLayouts;
792
+ }
677
793
  /** Look up the latest native layout for an element or component public instance. */
678
794
  function getLastRenderLayout(node) {
679
795
  const id = node?.id ?? node?.$el?.id;
@@ -689,6 +805,21 @@ function measureElement(node) {
689
805
  };
690
806
  }
691
807
  //#endregion
808
+ //#region src/mouseRouting.ts
809
+ /** Hit-test one normalized mouse packet and route it through the host tree. */
810
+ function routeMouseInput(renderer, root, event, layouts) {
811
+ const target = renderer.hitTest(root, event.x, event.y, layouts);
812
+ if (target) renderer.dispatchInput(target.id, event);
813
+ }
814
+ /** Route first, then publish the packet even when a routed handler throws. */
815
+ function routeAndPublishMouseInput(renderer, root, event, layouts, sink) {
816
+ try {
817
+ if (root) routeMouseInput(renderer, root, event, layouts);
818
+ } finally {
819
+ sink.value = event;
820
+ }
821
+ }
822
+ //#endregion
692
823
  //#region src/nativeInput.ts
693
824
  /** Accept only key event phases supported by the public Fresco protocol. */
694
825
  function normalizeKeyEventType(value) {
@@ -719,6 +850,92 @@ function normalizeMouseEvent(event) {
719
850
  } : void 0
720
851
  };
721
852
  }
853
+ /** Normalize one native input packet into the public Fresco event protocol. */
854
+ function normalizeInputEvent(event) {
855
+ switch (event.eventType) {
856
+ case "resize": return {
857
+ type: "resize",
858
+ width: event.width ?? 0,
859
+ height: event.height ?? 0
860
+ };
861
+ case "paste": return {
862
+ type: "paste",
863
+ text: event.text ?? ""
864
+ };
865
+ case "mouse": return normalizeMouseEvent(event);
866
+ case "focus": return {
867
+ type: "focus",
868
+ focused: event.key === "gained"
869
+ };
870
+ case "compositionstart":
871
+ case "compositionupdate":
872
+ case "compositionend": return {
873
+ type: event.eventType,
874
+ text: event.text ?? "",
875
+ cursor: event.cursor ?? 0
876
+ };
877
+ case "key": {
878
+ const modifiers = event.modifiers ?? {};
879
+ return {
880
+ type: "key",
881
+ key: event.key ?? void 0,
882
+ char: event.char ?? void 0,
883
+ ctrl: modifiers.ctrl ?? false,
884
+ alt: modifiers.alt ?? false,
885
+ shift: modifiers.shift ?? false,
886
+ meta: modifiers.meta ?? false,
887
+ super: modifiers.super ?? false,
888
+ hyper: modifiers.hyper ?? false,
889
+ capsLock: modifiers.capsLock ?? false,
890
+ numLock: modifiers.numLock ?? false,
891
+ eventType: normalizeKeyEventType(event.keyEventType)
892
+ };
893
+ }
894
+ default: return;
895
+ }
896
+ }
897
+ //#endregion
898
+ //#region src/hitTesting.ts
899
+ function clipsOverflow(value) {
900
+ return value === "hidden" || value === "scroll";
901
+ }
902
+ function clipsDescendants(node) {
903
+ const style = normalizeFrescoStyle(node.props.style);
904
+ return clipsOverflow(style?.overflow ?? style?.overflowX ?? style?.overflowY);
905
+ }
906
+ function containsPoint(layout, clip, x, y) {
907
+ return x >= layout.x && x < layout.x + layout.width && y >= layout.y && y < layout.y + layout.height && x >= clip.left && x < clip.right && y >= clip.top && y < clip.bottom;
908
+ }
909
+ /** Return the deepest topmost live host node under one native-layout coordinate. */
910
+ function hitTest(nodes, root, x, y, layouts) {
911
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return void 0;
912
+ const byId = new Map(layouts.map((layout) => [layout.id, layout]));
913
+ const unbounded = {
914
+ left: Number.NEGATIVE_INFINITY,
915
+ top: Number.NEGATIVE_INFINITY,
916
+ right: Number.POSITIVE_INFINITY,
917
+ bottom: Number.POSITIVE_INFINITY
918
+ };
919
+ const visit = (node, ancestorClip) => {
920
+ const layout = byId.get(node.id);
921
+ const clips = clipsDescendants(node);
922
+ const childClip = layout ? {
923
+ left: clips ? Math.max(ancestorClip.left, layout.x) : ancestorClip.left,
924
+ top: clips ? Math.max(ancestorClip.top, layout.y) : ancestorClip.top,
925
+ right: clips ? Math.min(ancestorClip.right, layout.x + layout.width) : ancestorClip.right,
926
+ bottom: clips ? Math.min(ancestorClip.bottom, layout.y + layout.height) : ancestorClip.bottom
927
+ } : ancestorClip;
928
+ for (let index = node.children.length - 1; index >= 0; index -= 1) {
929
+ const child = node.children[index];
930
+ if (!child) continue;
931
+ const target = visit(child, childClip);
932
+ if (target) return target;
933
+ }
934
+ if (!layout || !nodes.has(node.id)) return void 0;
935
+ return containsPoint(layout, ancestorClip, x, y) ? node : void 0;
936
+ };
937
+ return visit(root, unbounded);
938
+ }
722
939
  //#endregion
723
940
  //#region src/renderer.ts
724
941
  /**
@@ -891,6 +1108,9 @@ function mapElementType(type) {
891
1108
  case "span": return "text";
892
1109
  case "input":
893
1110
  case "textinput": return "input";
1111
+ case "raw":
1112
+ case "framebuffer":
1113
+ case "frame-buffer": return "raw";
894
1114
  default: return "box";
895
1115
  }
896
1116
  }
@@ -926,6 +1146,13 @@ function createRenderer$1(options = {}) {
926
1146
  tree.onMutation();
927
1147
  return true;
928
1148
  },
1149
+ /**
1150
+ * Return the deepest topmost node under one native-layout coordinate.
1151
+ * Hidden and scroll overflow constrain descendant targeting on each axis.
1152
+ */
1153
+ hitTest(root, x, y, layouts) {
1154
+ return hitTest(tree.nodes, root, x, y, layouts);
1155
+ },
929
1156
  /** Route normalized input through capture, target, and bubble handlers. */
930
1157
  dispatchInput(targetId, input) {
931
1158
  const target = tree.nodes.get(targetId);
@@ -1037,6 +1264,43 @@ function isStaticItemNode(node) {
1037
1264
  return node.props.internal_static_item === true || node.props.internalStaticItem === true;
1038
1265
  }
1039
1266
  //#endregion
1267
+ //#region src/terminalLifecycle.ts
1268
+ const CI_ENVIRONMENT_KEYS = [
1269
+ "CI",
1270
+ "CONTINUOUS_INTEGRATION",
1271
+ "BUILD_NUMBER",
1272
+ "RUN_ID"
1273
+ ];
1274
+ function isTruthyEnvironmentValue(value) {
1275
+ if (!value) return false;
1276
+ return ![
1277
+ "0",
1278
+ "false",
1279
+ "no"
1280
+ ].includes(value.toLowerCase());
1281
+ }
1282
+ function isCiEnvironment(env = process.env) {
1283
+ return CI_ENVIRONMENT_KEYS.some((key) => isTruthyEnvironmentValue(env[key]));
1284
+ }
1285
+ /** Resolve whether an application can own the interactive terminal lifecycle. */
1286
+ function detectInteractiveMode(interactive, stdout) {
1287
+ return interactive ?? (stdout.isTTY === true && !isCiEnvironment());
1288
+ }
1289
+ /** Initialize the native terminal modes required by an interactive application. */
1290
+ function initializeTerminal(native, options) {
1291
+ if (typeof native.initTerminalWithOptions === "function") native.initTerminalWithOptions({
1292
+ alternateScreen: options.alternateScreen,
1293
+ bracketedPaste: true,
1294
+ hideCursor: true,
1295
+ mouse: options.mouse,
1296
+ rawMode: true
1297
+ });
1298
+ else if (options.mouse) native.initTerminalWithMouse();
1299
+ else native.initTerminal();
1300
+ native.enableIme?.();
1301
+ native.initLayout();
1302
+ }
1303
+ //#endregion
1040
1304
  //#region src/app.ts
1041
1305
  /**
1042
1306
  * Fresco App - Application instance management
@@ -1196,33 +1460,12 @@ function updateAppSize(context, width, height) {
1196
1460
  context.width.value = width;
1197
1461
  context.height.value = height;
1198
1462
  }
1199
- const CI_ENVIRONMENT_KEYS = [
1200
- "CI",
1201
- "CONTINUOUS_INTEGRATION",
1202
- "BUILD_NUMBER",
1203
- "RUN_ID"
1204
- ];
1205
- function isTruthyEnvironmentValue(value) {
1206
- if (!value) return false;
1207
- return ![
1208
- "0",
1209
- "false",
1210
- "no"
1211
- ].includes(value.toLowerCase());
1212
- }
1213
- function isCiEnvironment(env = process.env) {
1214
- return CI_ENVIRONMENT_KEYS.some((key) => isTruthyEnvironmentValue(env[key]));
1215
- }
1216
- function detectInteractiveMode(options) {
1217
- const stdout = options.stdout ?? process.stdout;
1218
- return options.interactive ?? (stdout.isTTY === true && !isCiEnvironment());
1219
- }
1220
1463
  /**
1221
1464
  * Create a Fresco TUI app
1222
1465
  */
1223
1466
  function createApp(rootComponent, options = {}) {
1224
1467
  const { mouse = false, exitOnCtrlC = true, onError } = options;
1225
- const interactive = detectInteractiveMode(options);
1468
+ const interactive = detectInteractiveMode(options.interactive, options.stdout ?? process.stdout);
1226
1469
  const screenReaderEnabled = ref(options.isScreenReaderEnabled ?? isScreenReaderEnabledByDefault());
1227
1470
  const frameDelay = Math.max(1, Math.floor(1e3 / Math.max(1, options.maxFps ?? 30)));
1228
1471
  let streamsContext;
@@ -1244,6 +1487,8 @@ function createApp(rootComponent, options = {}) {
1244
1487
  let vueApp = null;
1245
1488
  let rootElement = null;
1246
1489
  let mounted = false;
1490
+ let pendingMount = null;
1491
+ let hasMountedSuccessfully = false;
1247
1492
  let running = false;
1248
1493
  let needsRender = true;
1249
1494
  let appContext = null;
@@ -1255,6 +1500,8 @@ function createApp(rootComponent, options = {}) {
1255
1500
  const renderedStaticItems = /* @__PURE__ */ new Map();
1256
1501
  let cursorPosition;
1257
1502
  let hasCursorOverride = false;
1503
+ let instance;
1504
+ const pluginInstallers = [];
1258
1505
  let exitSettled = false;
1259
1506
  let resolveExit = null;
1260
1507
  let rejectExit = null;
@@ -1262,26 +1509,38 @@ function createApp(rootComponent, options = {}) {
1262
1509
  resolveExit = resolve;
1263
1510
  rejectExit = reject;
1264
1511
  });
1265
- const { createApp: createVueApp } = createRenderer$1(() => {
1266
- needsRender = true;
1512
+ const renderer = createRenderer$1({
1513
+ lifecycle: options.devtools?.lifecycle,
1514
+ onMutation() {
1515
+ needsRender = true;
1516
+ }
1267
1517
  });
1268
- async function mount() {
1269
- if (mounted) return;
1518
+ function publishRenderMetrics(root, layouts, start) {
1519
+ if (!options.devtools && !options.onRender) return;
1520
+ const metrics = {
1521
+ renderTime: performance.now() - start,
1522
+ nodeCount: treeToRenderNodes(root).length,
1523
+ layoutCount: layouts.length
1524
+ };
1525
+ options.devtools?.recordRender(root, layouts, metrics);
1526
+ options.onRender?.(metrics);
1527
+ }
1528
+ function mount() {
1529
+ if (mounted) return Promise.resolve();
1530
+ if (pendingMount) return pendingMount;
1531
+ pendingMount = Promise.resolve().then(performMount).finally(() => {
1532
+ pendingMount = null;
1533
+ });
1534
+ return pendingMount;
1535
+ }
1536
+ async function performMount() {
1537
+ const app = renderer.createApp(componentFromRoot(rootComponent));
1538
+ for (const install of pluginInstallers) install(app);
1270
1539
  const n = interactive ? await loadNative() : null;
1271
- if (n) {
1272
- if (typeof n.initTerminalWithOptions === "function") n.initTerminalWithOptions({
1273
- alternateScreen: options.alternateScreen === true,
1274
- bracketedPaste: true,
1275
- hideCursor: true,
1276
- mouse,
1277
- rawMode: true
1278
- });
1279
- else if (mouse) n.initTerminalWithMouse();
1280
- else n.initTerminal();
1281
- n.enableIme?.();
1282
- n.initLayout();
1283
- }
1284
- const app = createVueApp(componentFromRoot(rootComponent));
1540
+ if (n) initializeTerminal(n, {
1541
+ alternateScreen: options.alternateScreen === true,
1542
+ mouse
1543
+ });
1285
1544
  const info = n?.getTerminalInfo() ?? {
1286
1545
  width: streamsContext.stdout.columns ?? 80,
1287
1546
  height: streamsContext.stdout.rows ?? 24
@@ -1289,9 +1548,7 @@ function createApp(rootComponent, options = {}) {
1289
1548
  appContext = createAppContext({
1290
1549
  width: info.width,
1291
1550
  height: info.height,
1292
- exit: (value) => {
1293
- unmount(value);
1294
- },
1551
+ exit: (value) => void unmount(value),
1295
1552
  render,
1296
1553
  clear,
1297
1554
  waitUntilRenderFlush,
@@ -1317,10 +1574,18 @@ function createApp(rootComponent, options = {}) {
1317
1574
  }
1318
1575
  vueApp = app;
1319
1576
  mounted = true;
1577
+ hasMountedSuccessfully = true;
1320
1578
  running = true;
1321
1579
  needsRender = true;
1322
1580
  eventLoop();
1323
1581
  }
1582
+ function use(plugin, ...pluginOptions) {
1583
+ if (pendingMount || hasMountedSuccessfully) throw new Error("Fresco plugins must be installed before the first successful mount");
1584
+ pluginInstallers.push((app) => {
1585
+ app.use(plugin, ...pluginOptions);
1586
+ });
1587
+ return instance;
1588
+ }
1324
1589
  async function unmount(errorOrResult) {
1325
1590
  if (!mounted) return;
1326
1591
  running = false;
@@ -1357,7 +1622,11 @@ function createApp(rootComponent, options = {}) {
1357
1622
  return exitPromise;
1358
1623
  }
1359
1624
  async function waitUntilRenderFlush() {
1360
- return Promise.resolve();
1625
+ if (pendingMount) await pendingMount;
1626
+ await nextTick();
1627
+ if (!needsRender) return;
1628
+ needsRender = false;
1629
+ render();
1361
1630
  }
1362
1631
  function clear() {
1363
1632
  if (!mounted) return;
@@ -1426,7 +1695,7 @@ function createApp(rootComponent, options = {}) {
1426
1695
  if (!interactive) {
1427
1696
  staticOutput = appendOutput(staticOutput, captureStaticOutput(rootElement, renderedStaticItems));
1428
1697
  nonInteractiveOutput = renderPlainOutput(rootElement, staticOutput);
1429
- options.onRender?.({ renderTime: performance.now() - start });
1698
+ publishRenderMetrics(rootElement, [], start);
1430
1699
  return;
1431
1700
  }
1432
1701
  if (!native) return;
@@ -1436,10 +1705,11 @@ function createApp(rootComponent, options = {}) {
1436
1705
  if (renderNodes.length > 0) {
1437
1706
  if (options.debug) streamsContext.stderr.write(`${JSON.stringify(renderNodes, null, 2)}\n`);
1438
1707
  native.renderTree(renderNodes);
1439
- if ("getLastRenderLayouts" in native) updateLastRenderLayouts(native.getLastRenderLayouts());
1708
+ const layouts = "getLastRenderLayouts" in native ? native.getLastRenderLayouts() : [];
1709
+ updateLastRenderLayouts(layouts);
1440
1710
  applyCursorOverride();
1441
1711
  native.flushTerminal();
1442
- options.onRender?.({ renderTime: performance.now() - start });
1712
+ publishRenderMetrics(rootElement, layouts, start);
1443
1713
  }
1444
1714
  } catch (error) {
1445
1715
  if (onError) onError(error);
@@ -1450,58 +1720,31 @@ function createApp(rootComponent, options = {}) {
1450
1720
  return (await loadNative()).getTerminalInfo();
1451
1721
  }
1452
1722
  function dispatchEvent(event) {
1453
- if (event.eventType === "resize") {
1454
- const resizeEvent = {
1455
- type: "resize",
1456
- width: event.width ?? 0,
1457
- height: event.height ?? 0
1458
- };
1459
- lastResizeEvent.value = resizeEvent;
1460
- updateAppSize(appContext, resizeEvent.width, resizeEvent.height);
1461
- return;
1462
- }
1463
- if (event.eventType === "paste") {
1464
- lastPasteEvent.value = {
1465
- type: "paste",
1466
- text: event.text ?? ""
1467
- };
1468
- return;
1469
- }
1470
- if (event.eventType === "mouse") {
1471
- lastMouseEvent.value = normalizeMouseEvent(event);
1472
- return;
1473
- }
1474
- if (event.eventType === "focus") {
1475
- lastFocusEvent.value = {
1476
- type: "focus",
1477
- focused: event.key === "gained"
1478
- };
1479
- return;
1480
- }
1481
- if (event.eventType === "compositionstart" || event.eventType === "compositionupdate" || event.eventType === "compositionend") {
1482
- lastCompositionEvent.value = {
1483
- type: event.eventType,
1484
- text: event.text ?? "",
1485
- cursor: event.cursor ?? 0
1486
- };
1487
- return;
1488
- }
1489
- if (event.eventType === "key") {
1490
- const modifiers = event.modifiers ?? {};
1491
- lastKeyEvent.value = {
1492
- type: "key",
1493
- key: event.key ?? void 0,
1494
- char: event.char ?? void 0,
1495
- ctrl: modifiers.ctrl ?? false,
1496
- alt: modifiers.alt ?? false,
1497
- shift: modifiers.shift ?? false,
1498
- meta: modifiers.meta ?? false,
1499
- super: modifiers.super ?? false,
1500
- hyper: modifiers.hyper ?? false,
1501
- capsLock: modifiers.capsLock ?? false,
1502
- numLock: modifiers.numLock ?? false,
1503
- eventType: normalizeKeyEventType(event.keyEventType)
1504
- };
1723
+ const normalized = normalizeInputEvent(event);
1724
+ if (!normalized) return;
1725
+ options.devtools?.recordInput(normalized);
1726
+ switch (normalized.type) {
1727
+ case "resize":
1728
+ lastResizeEvent.value = normalized;
1729
+ updateAppSize(appContext, normalized.width, normalized.height);
1730
+ break;
1731
+ case "paste":
1732
+ lastPasteEvent.value = normalized;
1733
+ break;
1734
+ case "mouse":
1735
+ routeAndPublishMouseInput(renderer, rootElement, normalized, getLastRenderLayouts(), lastMouseEvent);
1736
+ break;
1737
+ case "focus":
1738
+ lastFocusEvent.value = normalized;
1739
+ break;
1740
+ case "compositionstart":
1741
+ case "compositionupdate":
1742
+ case "compositionend":
1743
+ lastCompositionEvent.value = normalized;
1744
+ break;
1745
+ case "key":
1746
+ lastKeyEvent.value = normalized;
1747
+ break;
1505
1748
  }
1506
1749
  }
1507
1750
  async function eventLoop() {
@@ -1538,7 +1781,8 @@ function createApp(rootComponent, options = {}) {
1538
1781
  await new Promise((resolve) => setTimeout(resolve, frameDelay));
1539
1782
  }
1540
1783
  }
1541
- return {
1784
+ instance = {
1785
+ use,
1542
1786
  mount,
1543
1787
  unmount,
1544
1788
  waitUntilExit,
@@ -1547,6 +1791,7 @@ function createApp(rootComponent, options = {}) {
1547
1791
  clear,
1548
1792
  getTerminalInfo
1549
1793
  };
1794
+ return instance;
1550
1795
  }
1551
1796
  /**
1552
1797
  * Ink-compatible helper that mounts immediately.
@@ -1609,6 +1854,6 @@ function renderToString(root, options = {}) {
1609
1854
  return output;
1610
1855
  }
1611
1856
  //#endregion
1612
- export { APP_KEY as A, useStdin as C, createFocusManager as D, FOCUS_KEY as E, useApp as M, SCREEN_READER_KEY as N, useFocus as O, isScreenReaderEnabledByDefault as P, useStderr as S, useStreamsContext as T, CURSOR_KEY as _, lastMouseEvent as a, STREAMS_KEY as b, render as c, isStaticNode as d, createRenderer$1 as f, updateLastRenderLayouts as g, measureElement as h, lastKeyEvent as i, createAppContext as j, useFocusManager as k, renderToString as l, getLastRenderLayout as m, lastCompositionEvent as n, lastPasteEvent as o, treeToRenderNodes as p, lastFocusEvent as r, lastResizeEvent as s, createApp as t, isStaticItemNode as u, createCursorContext as v, useStdout as w, createStreamsContext as x, useCursor as y };
1857
+ export { useFocusManager as A, useStdin as C, createFocusManager as D, FOCUS_KEY as E, isScreenReaderEnabledByDefault as F, createAppContext as M, useApp as N, provideFocusScope as O, SCREEN_READER_KEY as P, useStderr as S, useStreamsContext as T, CURSOR_KEY as _, lastMouseEvent as a, STREAMS_KEY as b, render as c, isStaticNode as d, createRenderer$1 as f, updateLastRenderLayouts as g, measureElement as h, lastKeyEvent as i, APP_KEY as j, useFocus as k, renderToString as l, getLastRenderLayout as m, lastCompositionEvent as n, lastPasteEvent as o, treeToRenderNodes as p, lastFocusEvent as r, lastResizeEvent as s, createApp as t, isStaticItemNode as u, createCursorContext as v, useStdout as w, createStreamsContext as x, useCursor as y };
1613
1858
 
1614
- //# sourceMappingURL=app-B9yJzz5S.mjs.map
1859
+ //# sourceMappingURL=app-C_wFlWSV.mjs.map