@vizejs/fresco 0.716.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-OEhyYUxd.d.mts → accessibility-DwzT925M.d.mts} +46 -5
  2. package/dist/accessibility-DwzT925M.d.mts.map +1 -0
  3. package/dist/{app-CIX6NAW-.mjs → app-C_wFlWSV.mjs} +443 -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-DbJGVNSG.mjs → composables-BJeirPSH.mjs} +116 -55
  18. package/dist/composables-BJeirPSH.mjs.map +1 -0
  19. package/dist/{index-CWRginDz.d.mts → index-B3zQ6E-5.d.mts} +1099 -337
  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-pPZWmwUC.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-OEhyYUxd.d.mts.map +0 -1
  41. package/dist/app-CIX6NAW-.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-D9SeB0Be.mjs +0 -3331
  45. package/dist/components-D9SeB0Be.mjs.map +0 -1
  46. package/dist/composables-DbJGVNSG.mjs.map +0 -1
  47. package/dist/index-CWRginDz.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-pPZWmwUC.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);
422
+ };
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;
395
434
  };
396
- const focusPrevious = () => {
397
- const active = activeFocusables(focusables.value);
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
  /**
@@ -780,6 +997,30 @@ function detachNode(tree, node) {
780
997
  notifyUpdate(tree, parent, "children");
781
998
  return true;
782
999
  }
1000
+ function routedInputEventName(event) {
1001
+ switch (event.type) {
1002
+ case "compositionstart": return "CompositionStart";
1003
+ case "compositionupdate": return "CompositionUpdate";
1004
+ case "compositionend": return "CompositionEnd";
1005
+ default: return `${event.type[0]?.toUpperCase() ?? ""}${event.type.slice(1)}`;
1006
+ }
1007
+ }
1008
+ function callInputHandlers(value, event) {
1009
+ if (typeof value === "function") {
1010
+ value(event);
1011
+ return 1;
1012
+ }
1013
+ if (!Array.isArray(value)) return 0;
1014
+ let handled = 0;
1015
+ for (const handler of value) handled += callInputHandlers(handler, event);
1016
+ return handled;
1017
+ }
1018
+ function dispatchAtNode(node, event, capture, eventName) {
1019
+ const suffix = capture ? "Capture" : "";
1020
+ let handled = callInputHandlers(node.props[`onFrescoInput${suffix}`], event);
1021
+ handled += callInputHandlers(node.props[`on${eventName}${suffix}`], event);
1022
+ return handled;
1023
+ }
783
1024
  /**
784
1025
  * Renderer options for Fresco
785
1026
  */
@@ -867,6 +1108,9 @@ function mapElementType(type) {
867
1108
  case "span": return "text";
868
1109
  case "input":
869
1110
  case "textinput": return "input";
1111
+ case "raw":
1112
+ case "framebuffer":
1113
+ case "frame-buffer": return "raw";
870
1114
  default: return "box";
871
1115
  }
872
1116
  }
@@ -901,6 +1145,64 @@ function createRenderer$1(options = {}) {
901
1145
  disposeNode(tree, node);
902
1146
  tree.onMutation();
903
1147
  return true;
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
+ },
1156
+ /** Route normalized input through capture, target, and bubble handlers. */
1157
+ dispatchInput(targetId, input) {
1158
+ const target = tree.nodes.get(targetId);
1159
+ if (!target) return void 0;
1160
+ const path = [];
1161
+ for (let node = target; node; node = node.parent) path.push(node);
1162
+ const routed = {
1163
+ input,
1164
+ target,
1165
+ currentTarget: target,
1166
+ phase: "target",
1167
+ defaultPrevented: false,
1168
+ propagationStopped: false,
1169
+ preventDefault() {
1170
+ this.defaultPrevented = true;
1171
+ },
1172
+ stopPropagation() {
1173
+ this.propagationStopped = true;
1174
+ }
1175
+ };
1176
+ const eventName = routedInputEventName(input);
1177
+ let handled = 0;
1178
+ for (let index = path.length - 1; index > 0; index -= 1) {
1179
+ const node = path[index];
1180
+ if (!node) continue;
1181
+ routed.currentTarget = node;
1182
+ routed.phase = "capture";
1183
+ handled += dispatchAtNode(node, routed, true, eventName);
1184
+ if (routed.propagationStopped) break;
1185
+ }
1186
+ if (!routed.propagationStopped) {
1187
+ routed.currentTarget = target;
1188
+ routed.phase = "target";
1189
+ handled += dispatchAtNode(target, routed, true, eventName);
1190
+ handled += dispatchAtNode(target, routed, false, eventName);
1191
+ }
1192
+ if (!routed.propagationStopped) for (let index = 1; index < path.length; index += 1) {
1193
+ const node = path[index];
1194
+ if (!node) continue;
1195
+ routed.currentTarget = node;
1196
+ routed.phase = "bubble";
1197
+ handled += dispatchAtNode(node, routed, false, eventName);
1198
+ if (routed.propagationStopped) break;
1199
+ }
1200
+ return {
1201
+ target,
1202
+ handled,
1203
+ defaultPrevented: routed.defaultPrevented,
1204
+ propagationStopped: routed.propagationStopped
1205
+ };
904
1206
  }
905
1207
  });
906
1208
  }
@@ -962,6 +1264,43 @@ function isStaticItemNode(node) {
962
1264
  return node.props.internal_static_item === true || node.props.internalStaticItem === true;
963
1265
  }
964
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
965
1304
  //#region src/app.ts
966
1305
  /**
967
1306
  * Fresco App - Application instance management
@@ -1121,33 +1460,12 @@ function updateAppSize(context, width, height) {
1121
1460
  context.width.value = width;
1122
1461
  context.height.value = height;
1123
1462
  }
1124
- const CI_ENVIRONMENT_KEYS = [
1125
- "CI",
1126
- "CONTINUOUS_INTEGRATION",
1127
- "BUILD_NUMBER",
1128
- "RUN_ID"
1129
- ];
1130
- function isTruthyEnvironmentValue(value) {
1131
- if (!value) return false;
1132
- return ![
1133
- "0",
1134
- "false",
1135
- "no"
1136
- ].includes(value.toLowerCase());
1137
- }
1138
- function isCiEnvironment(env = process.env) {
1139
- return CI_ENVIRONMENT_KEYS.some((key) => isTruthyEnvironmentValue(env[key]));
1140
- }
1141
- function detectInteractiveMode(options) {
1142
- const stdout = options.stdout ?? process.stdout;
1143
- return options.interactive ?? (stdout.isTTY === true && !isCiEnvironment());
1144
- }
1145
1463
  /**
1146
1464
  * Create a Fresco TUI app
1147
1465
  */
1148
1466
  function createApp(rootComponent, options = {}) {
1149
1467
  const { mouse = false, exitOnCtrlC = true, onError } = options;
1150
- const interactive = detectInteractiveMode(options);
1468
+ const interactive = detectInteractiveMode(options.interactive, options.stdout ?? process.stdout);
1151
1469
  const screenReaderEnabled = ref(options.isScreenReaderEnabled ?? isScreenReaderEnabledByDefault());
1152
1470
  const frameDelay = Math.max(1, Math.floor(1e3 / Math.max(1, options.maxFps ?? 30)));
1153
1471
  let streamsContext;
@@ -1169,6 +1487,8 @@ function createApp(rootComponent, options = {}) {
1169
1487
  let vueApp = null;
1170
1488
  let rootElement = null;
1171
1489
  let mounted = false;
1490
+ let pendingMount = null;
1491
+ let hasMountedSuccessfully = false;
1172
1492
  let running = false;
1173
1493
  let needsRender = true;
1174
1494
  let appContext = null;
@@ -1180,6 +1500,8 @@ function createApp(rootComponent, options = {}) {
1180
1500
  const renderedStaticItems = /* @__PURE__ */ new Map();
1181
1501
  let cursorPosition;
1182
1502
  let hasCursorOverride = false;
1503
+ let instance;
1504
+ const pluginInstallers = [];
1183
1505
  let exitSettled = false;
1184
1506
  let resolveExit = null;
1185
1507
  let rejectExit = null;
@@ -1187,26 +1509,38 @@ function createApp(rootComponent, options = {}) {
1187
1509
  resolveExit = resolve;
1188
1510
  rejectExit = reject;
1189
1511
  });
1190
- const { createApp: createVueApp } = createRenderer$1(() => {
1191
- needsRender = true;
1512
+ const renderer = createRenderer$1({
1513
+ lifecycle: options.devtools?.lifecycle,
1514
+ onMutation() {
1515
+ needsRender = true;
1516
+ }
1192
1517
  });
1193
- async function mount() {
1194
- 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);
1195
1539
  const n = interactive ? await loadNative() : null;
1196
- if (n) {
1197
- if (typeof n.initTerminalWithOptions === "function") n.initTerminalWithOptions({
1198
- alternateScreen: options.alternateScreen === true,
1199
- bracketedPaste: true,
1200
- hideCursor: true,
1201
- mouse,
1202
- rawMode: true
1203
- });
1204
- else if (mouse) n.initTerminalWithMouse();
1205
- else n.initTerminal();
1206
- n.enableIme?.();
1207
- n.initLayout();
1208
- }
1209
- const app = createVueApp(componentFromRoot(rootComponent));
1540
+ if (n) initializeTerminal(n, {
1541
+ alternateScreen: options.alternateScreen === true,
1542
+ mouse
1543
+ });
1210
1544
  const info = n?.getTerminalInfo() ?? {
1211
1545
  width: streamsContext.stdout.columns ?? 80,
1212
1546
  height: streamsContext.stdout.rows ?? 24
@@ -1214,9 +1548,7 @@ function createApp(rootComponent, options = {}) {
1214
1548
  appContext = createAppContext({
1215
1549
  width: info.width,
1216
1550
  height: info.height,
1217
- exit: (value) => {
1218
- unmount(value);
1219
- },
1551
+ exit: (value) => void unmount(value),
1220
1552
  render,
1221
1553
  clear,
1222
1554
  waitUntilRenderFlush,
@@ -1242,10 +1574,18 @@ function createApp(rootComponent, options = {}) {
1242
1574
  }
1243
1575
  vueApp = app;
1244
1576
  mounted = true;
1577
+ hasMountedSuccessfully = true;
1245
1578
  running = true;
1246
1579
  needsRender = true;
1247
1580
  eventLoop();
1248
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
+ }
1249
1589
  async function unmount(errorOrResult) {
1250
1590
  if (!mounted) return;
1251
1591
  running = false;
@@ -1282,7 +1622,11 @@ function createApp(rootComponent, options = {}) {
1282
1622
  return exitPromise;
1283
1623
  }
1284
1624
  async function waitUntilRenderFlush() {
1285
- return Promise.resolve();
1625
+ if (pendingMount) await pendingMount;
1626
+ await nextTick();
1627
+ if (!needsRender) return;
1628
+ needsRender = false;
1629
+ render();
1286
1630
  }
1287
1631
  function clear() {
1288
1632
  if (!mounted) return;
@@ -1351,7 +1695,7 @@ function createApp(rootComponent, options = {}) {
1351
1695
  if (!interactive) {
1352
1696
  staticOutput = appendOutput(staticOutput, captureStaticOutput(rootElement, renderedStaticItems));
1353
1697
  nonInteractiveOutput = renderPlainOutput(rootElement, staticOutput);
1354
- options.onRender?.({ renderTime: performance.now() - start });
1698
+ publishRenderMetrics(rootElement, [], start);
1355
1699
  return;
1356
1700
  }
1357
1701
  if (!native) return;
@@ -1361,10 +1705,11 @@ function createApp(rootComponent, options = {}) {
1361
1705
  if (renderNodes.length > 0) {
1362
1706
  if (options.debug) streamsContext.stderr.write(`${JSON.stringify(renderNodes, null, 2)}\n`);
1363
1707
  native.renderTree(renderNodes);
1364
- if ("getLastRenderLayouts" in native) updateLastRenderLayouts(native.getLastRenderLayouts());
1708
+ const layouts = "getLastRenderLayouts" in native ? native.getLastRenderLayouts() : [];
1709
+ updateLastRenderLayouts(layouts);
1365
1710
  applyCursorOverride();
1366
1711
  native.flushTerminal();
1367
- options.onRender?.({ renderTime: performance.now() - start });
1712
+ publishRenderMetrics(rootElement, layouts, start);
1368
1713
  }
1369
1714
  } catch (error) {
1370
1715
  if (onError) onError(error);
@@ -1375,58 +1720,31 @@ function createApp(rootComponent, options = {}) {
1375
1720
  return (await loadNative()).getTerminalInfo();
1376
1721
  }
1377
1722
  function dispatchEvent(event) {
1378
- if (event.eventType === "resize") {
1379
- const resizeEvent = {
1380
- type: "resize",
1381
- width: event.width ?? 0,
1382
- height: event.height ?? 0
1383
- };
1384
- lastResizeEvent.value = resizeEvent;
1385
- updateAppSize(appContext, resizeEvent.width, resizeEvent.height);
1386
- return;
1387
- }
1388
- if (event.eventType === "paste") {
1389
- lastPasteEvent.value = {
1390
- type: "paste",
1391
- text: event.text ?? ""
1392
- };
1393
- return;
1394
- }
1395
- if (event.eventType === "mouse") {
1396
- lastMouseEvent.value = normalizeMouseEvent(event);
1397
- return;
1398
- }
1399
- if (event.eventType === "focus") {
1400
- lastFocusEvent.value = {
1401
- type: "focus",
1402
- focused: event.key === "gained"
1403
- };
1404
- return;
1405
- }
1406
- if (event.eventType === "compositionstart" || event.eventType === "compositionupdate" || event.eventType === "compositionend") {
1407
- lastCompositionEvent.value = {
1408
- type: event.eventType,
1409
- text: event.text ?? "",
1410
- cursor: event.cursor ?? 0
1411
- };
1412
- return;
1413
- }
1414
- if (event.eventType === "key") {
1415
- const modifiers = event.modifiers ?? {};
1416
- lastKeyEvent.value = {
1417
- type: "key",
1418
- key: event.key ?? void 0,
1419
- char: event.char ?? void 0,
1420
- ctrl: modifiers.ctrl ?? false,
1421
- alt: modifiers.alt ?? false,
1422
- shift: modifiers.shift ?? false,
1423
- meta: modifiers.meta ?? false,
1424
- super: modifiers.super ?? false,
1425
- hyper: modifiers.hyper ?? false,
1426
- capsLock: modifiers.capsLock ?? false,
1427
- numLock: modifiers.numLock ?? false,
1428
- eventType: normalizeKeyEventType(event.keyEventType)
1429
- };
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;
1430
1748
  }
1431
1749
  }
1432
1750
  async function eventLoop() {
@@ -1463,7 +1781,8 @@ function createApp(rootComponent, options = {}) {
1463
1781
  await new Promise((resolve) => setTimeout(resolve, frameDelay));
1464
1782
  }
1465
1783
  }
1466
- return {
1784
+ instance = {
1785
+ use,
1467
1786
  mount,
1468
1787
  unmount,
1469
1788
  waitUntilExit,
@@ -1472,6 +1791,7 @@ function createApp(rootComponent, options = {}) {
1472
1791
  clear,
1473
1792
  getTerminalInfo
1474
1793
  };
1794
+ return instance;
1475
1795
  }
1476
1796
  /**
1477
1797
  * Ink-compatible helper that mounts immediately.
@@ -1534,6 +1854,6 @@ function renderToString(root, options = {}) {
1534
1854
  return output;
1535
1855
  }
1536
1856
  //#endregion
1537
- 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 };
1538
1858
 
1539
- //# sourceMappingURL=app-CIX6NAW-.mjs.map
1859
+ //# sourceMappingURL=app-C_wFlWSV.mjs.map