gesso-framework 0.7.0 → 0.8.1

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.
@@ -1,9 +1,9 @@
1
- import { a as InternalState, f as outputTargetOf, m as withBodyOf, n as computed, o as internalState, s as InputCell, u as isOutputTarget } from "./computed-_cYcdczA.js";
1
+ import { a as InternalState, c as forceInput, d as isOutputTarget, h as withBodyOf, l as input, n as computed, o as internalState, p as outputTargetOf, s as InputCell } from "./computed-DyW9SPpq.js";
2
2
  import { f as formatNodePath, g as printPropValue, r as isPortErrorMessage, u as describeStream, v as applyPatches, w as viewKeys, x as isChannelHostMessage } from "./WorkerPorts-m_hLRoPj.js";
3
3
  import { n as getComponentMetadata, t as createComponent } from "./createComponent-DJads88q.js";
4
4
  import { m as designedTheme } from "./app-De3z58x7.js";
5
- import { EMPTY, Subject, Subscription, map } from "rxjs";
6
- import { AnimationDriver, Box, Canvas2DRenderer, CanvasTextMeasurer, Column, Constraints, DefaultImageResolver, DefaultSvgResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, Stack, TEXT_RUN_ID_SEPARATOR, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiFrame, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiManualFrameClock, UiNodeType, UiPointerController, UiPointerEvent, UiScheduler, UiSelectionController, UiSharedElements, UiSpring, UiTimerFrameClock, UiTouchScroller, UiVirtualSheet, UiVirtualWindow, UiWheelController, VIRTUAL_INDEX_PROP, VIRTUAL_LEAD_PROP, VIRTUAL_SHEET_PROP, VIRTUAL_WINDOW_PROP, WebGPURenderer, buildSemanticsSubtree, buildSemanticsTree, bumpFontStack, clearOverrideProperty, createCanvasSurface, createTween, createWebGPUSurface, defaultMotion, defineModifier, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, easings, fade, formatExplanation, insetsEqual, isComponentLikeElement, isObservable, isUiElement, isWebGPUAvailable, lightTheme, linear, linkOf, measure, mediaLoadsInFlight, motion, noInsets, propertyEffects, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollBounds, scrollRange, scrollbarThumb, scrollsFromRight, semanticsInertAbove, semanticsMemory, setPerformanceMarks, textRunOfRecordId, writeOverrideProperty } from "gesso-core";
5
+ import { EMPTY, Subject, Subscription, concat, map, of, skip } from "rxjs";
6
+ import { AnimationDriver, Box, Canvas2DRenderer, CanvasTextMeasurer, Column, Constraints, DefaultImageResolver, DefaultSvgResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, Stack, TEXT_RUN_ID_SEPARATOR, Text, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiFrame, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiManualFrameClock, UiNodeType, UiPointerController, UiPointerEvent, UiScheduler, UiSelectionController, UiSharedElements, UiSpring, UiTimerFrameClock, UiTouchScroller, UiVirtualSheet, UiVirtualWindow, UiWheelController, VIRTUAL_INDEX_PROP, VIRTUAL_LEAD_PROP, VIRTUAL_SHEET_PROP, VIRTUAL_WINDOW_PROP, WebGPURenderer, buildSemanticsSubtree, buildSemanticsTree, bumpFontStack, clearOverrideProperty, clockOf, createCanvasSurface, createEnvironmentKey, createTween, createWebGPUSurface, defaultMotion, defineModifier, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, durationIn, easingIn, fade, findEnvironmentKey, formatExplanation, insetsEqual, isComponentLikeElement, isObservable, isUiElement, isWebGPUAvailable, lightTheme, linear, linkOf, measure, mediaLoadsInFlight, motion, noInsets, propertyEffects, provided, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollBounds, scrollRange, scrollbarThumb, scrollsFromRight, semanticsInertAbove, semanticsMemory, setPerformanceMarks, springIn, textRunOfRecordId, writeOverrideProperty } from "gesso-core";
7
7
  //#region src/Component.ts
8
8
  /**
9
9
  * Base class for all Gesso framework components.
@@ -161,6 +161,33 @@ function bounds(label) {
161
161
  return cell;
162
162
  }
163
163
  //#endregion
164
+ //#region src/liveInput.ts
165
+ /** Where the component whose body is running keeps its live inputs, while it runs. */
166
+ let target = null;
167
+ /** Runs a component's body with its live inputs going to `into`; see `liveInput`. */
168
+ function withLiveInputs(into, run) {
169
+ const previous = target;
170
+ target = into;
171
+ try {
172
+ return run();
173
+ } finally {
174
+ target = previous;
175
+ }
176
+ }
177
+ /**
178
+ * Says what an input of the component whose body is running holds now,
179
+ * as the component keeps it, and how to read it again: a switch turned
180
+ * on, a field typed in, a list scrolled to 600. The inspector reports it
181
+ * on the component's owner report (`live`), and a design tool saves a
182
+ * moment of a played screen from it, to start the screen there again
183
+ * (`StartingInputs`). `controlled` calls it for its value, so a control
184
+ * written by its contract reports itself; outside a component's body it
185
+ * does nothing.
186
+ */
187
+ function liveInput(name, read) {
188
+ target?.set(name, read);
189
+ }
190
+ //#endregion
164
191
  //#region src/decorators.ts
165
192
  /**
166
193
  * Declares a class as a Gesso component.
@@ -455,6 +482,7 @@ function isClassComponent(component) {
455
482
  * declared the entry.
456
483
  */
457
484
  function physicalPlacement(placement, rtl) {
485
+ if (placement === "over") return placement;
458
486
  const [side, align] = placement.split("-");
459
487
  if (side === "top" || side === "bottom") {
460
488
  if (align === void 0 || !rtl) return placement;
@@ -498,6 +526,149 @@ var OverlayService = class {
498
526
  return this.entries.value.some((entry) => entry.id === id);
499
527
  }
500
528
  };
529
+ /**
530
+ * The overlays of the `OverlayScope` a node is in: where an entry
531
+ * opened there goes instead of the runtime's layer. Null outside every
532
+ * scope.
533
+ */
534
+ const OverlayScopeKey = createEnvironmentKey({
535
+ name: "gesso.overlayScope",
536
+ defaultValue: null
537
+ });
538
+ //#endregion
539
+ //#region src/app/FocusService.ts
540
+ /**
541
+ * Keyboard focus, as a store components can inject.
542
+ *
543
+ * `UiFocusManager` lives in the runtime and a component cannot reach
544
+ * it — components reach the world through stores, as they do for the
545
+ * clipboard (`ShellService`), overlays (`OverlayService`) and find
546
+ * (`FindService`). Without this a component cannot autofocus a field,
547
+ * trap the keyboard in a dialog, or put the caret in the input that
548
+ * failed validation.
549
+ *
550
+ * Nodes come from a `ref` prop. A tree's refs fire before the runtime
551
+ * installs the manager, so an action taken during the first build is
552
+ * queued and replayed once there is one; that is the same problem
553
+ * `FindService` solves for its query field, and the reason `autoFocus`
554
+ * in a dialog works on the frame it mounts.
555
+ *
556
+ * It must stay on the render thread: its actions take `UiNode`s, which
557
+ * never cross a worker boundary.
558
+ */
559
+ /** The nodes a trap taken inside is modal only within: each `OverlayScope`'s. */
560
+ const focusBounds = /* @__PURE__ */ new WeakSet();
561
+ /** Makes a node one, for `OverlayScope`. */
562
+ function boundFocus(node) {
563
+ focusBounds.add(node);
564
+ }
565
+ var FocusService = class {
566
+ /** The node holding focus, or null. A control binds its focus ring to this. */
567
+ focused = internalState(null);
568
+ /** Whether focus is confined to a subtree by an open trap. */
569
+ trapped = internalState(false);
570
+ /**
571
+ * Whether the focus held is focus the keyboard can see: reached by Tab
572
+ * or an arrow key, or by code with no pointer since, rather than by a
573
+ * press. CSS's `:focus-visible`, for something other than a ring that
574
+ * should answer the keyboard and not the click that happened to focus
575
+ * the same control: a tooltip, say.
576
+ */
577
+ focusVisible = internalState(false);
578
+ manager = null;
579
+ detach = null;
580
+ /** Actions taken before the runtime installed a manager, in order. */
581
+ queued = [];
582
+ /** Installed by the runtime; without one every action is queued. */
583
+ setManager(manager) {
584
+ this.detach?.();
585
+ this.detach = null;
586
+ this.manager = manager;
587
+ if (manager === null) {
588
+ this.queued = [];
589
+ return;
590
+ }
591
+ const focusChange = manager.onFocusChange((node) => {
592
+ this.focused.value = node;
593
+ this.focusVisible.value = manager.focusVisible;
594
+ this.trapped.value = manager.trapped;
595
+ });
596
+ manager.setFocusBounds((node) => focusBounds.has(node));
597
+ const scopeChange = manager.onScopeChange(() => {
598
+ this.trapped.value = manager.trapped;
599
+ });
600
+ this.detach = () => {
601
+ focusChange();
602
+ scopeChange();
603
+ };
604
+ const pending = this.queued;
605
+ this.queued = [];
606
+ for (const action of pending) action(manager);
607
+ this.sync(manager);
608
+ }
609
+ /**
610
+ * Gives the node keyboard focus. Non-focusable nodes are ignored.
611
+ *
612
+ * The node is scrolled into view, unless `options.preventScroll` asks
613
+ * for the page to stay where it is, as `element.focus({ preventScroll:
614
+ * true })` does in a browser.
615
+ */
616
+ focus(node, options) {
617
+ this.run((manager) => manager.focus(node, "program", options));
618
+ }
619
+ /** Drops focus without moving it anywhere. */
620
+ blur() {
621
+ this.run((manager) => manager.blur());
622
+ }
623
+ /** Moves focus to the next focusable node, wrapping around. */
624
+ focusNext() {
625
+ this.run((manager) => manager.focusNext());
626
+ }
627
+ /** Moves focus to the previous focusable node, wrapping around. */
628
+ focusPrevious() {
629
+ this.run((manager) => manager.focusPrevious());
630
+ }
631
+ /**
632
+ * The tab stops under `within` (the whole tree when not given), in
633
+ * the order Tab visits them: what a design tool numbers to show a
634
+ * screen's keyboard order. Empty before the runtime has a manager.
635
+ */
636
+ tabStops(within) {
637
+ return this.manager?.tabStops(within) ?? [];
638
+ }
639
+ /**
640
+ * Confines focus to `scope` until `releaseTrap`, moving it inside if
641
+ * it was elsewhere. Traps nest: a dialog opened over a dialog traps
642
+ * again, and each release restores its own opener.
643
+ */
644
+ trap(scope) {
645
+ this.run((manager) => manager.pushScope(scope));
646
+ }
647
+ /**
648
+ * Ends the innermost trap and returns focus to whatever held it when
649
+ * the trap was taken — the button that opened the dialog. Given the
650
+ * node trapped in, only that trap ends, and nothing when it ended
651
+ * already: a trap's subtree removed releases it, and a release after
652
+ * that would otherwise end the trap under it.
653
+ */
654
+ releaseTrap(scope) {
655
+ this.run((manager) => manager.popScope(scope));
656
+ }
657
+ run(action) {
658
+ const manager = this.manager;
659
+ if (manager === null) {
660
+ this.queued.push(action);
661
+ return;
662
+ }
663
+ action(manager);
664
+ this.sync(manager);
665
+ }
666
+ sync(manager) {
667
+ this.focused.value = manager.focusedNode;
668
+ this.focusVisible.value = manager.focusVisible;
669
+ this.trapped.value = manager.trapped;
670
+ }
671
+ };
501
672
  //#endregion
502
673
  //#region \0@oxc-project+runtime@0.143.0/helpers/esm/decorate.js
503
674
  function __decorate(decorators, target, key, desc) {
@@ -509,7 +680,12 @@ function __decorate(decorators, target, key, desc) {
509
680
  //#endregion
510
681
  //#region src/overlay/OverlayLayer.ts
511
682
  let OverlayLayer = class OverlayLayer extends Component {
512
- overlays;
683
+ runtimeOverlays;
684
+ /** The entries to draw, when not the runtime's: an `OverlayScope`'s. */
685
+ service = input(null);
686
+ get overlays() {
687
+ return this.service.value ?? this.runtimeOverlays;
688
+ }
513
689
  render() {
514
690
  return Box({
515
691
  position: "absolute",
@@ -547,6 +723,7 @@ let OverlayLayer = class OverlayLayer extends Component {
547
723
  top: entry.top,
548
724
  bottom: entry.bottom,
549
725
  zIndex: entry.zIndex,
726
+ ...entry.placement === "over" && entry.anchor != null ? { hitTestable: false } : {},
550
727
  ...centering(entry),
551
728
  ...physicalEdges(entry, readsRightToLeft(environment)),
552
729
  ...inheritedFrom(environment),
@@ -556,7 +733,8 @@ let OverlayLayer = class OverlayLayer extends Component {
556
733
  return elements;
557
734
  }
558
735
  };
559
- __decorate([Inject(OverlayService)], OverlayLayer.prototype, "overlays", void 0);
736
+ __decorate([Inject(OverlayService)], OverlayLayer.prototype, "runtimeOverlays", void 0);
737
+ __decorate([Input()], OverlayLayer.prototype, "service", void 0);
560
738
  OverlayLayer = __decorate([Define("gesso-overlay-layer")], OverlayLayer);
561
739
  /**
562
740
  * The dim over the page, inside the backdrop rather than on it.
@@ -646,9 +824,31 @@ function inheritedFrom(node) {
646
824
  theme: environment.get(UiEnvironmentKeys.theme),
647
825
  textStyle: environment.get(UiEnvironmentKeys.textStyle),
648
826
  color: environment.get(UiEnvironmentKeys.color),
649
- contentColor: environment.get(UiEnvironmentKeys.contentColor)
827
+ contentColor: environment.get(UiEnvironmentKeys.contentColor),
828
+ provide: providedFrom(environment)
650
829
  };
651
830
  }
831
+ /** The built-in keys an entry's box carries by props of its own, or leaves to where it is drawn. */
832
+ const CARRIED_BY_PROPS = new Set(Object.values(UiEnvironmentKeys).map((key) => key.name).filter((name) => name !== UiEnvironmentKeys.clock.name && name !== UiEnvironmentKeys.motion.name));
833
+ /**
834
+ * The rest of what the entry was opened under, re-provided: the clock
835
+ * its animations run on, so a dialog opened on a settled frame is at
836
+ * rest there and one opened in a slowed player slows, the motion its
837
+ * names mean, and every key the
838
+ * application provides itself, so a component in a dialog reads with
839
+ * `ctx.environment` what the one that opened it reads.
840
+ */
841
+ function providedFrom(environment) {
842
+ const out = [];
843
+ const seen = /* @__PURE__ */ new Set();
844
+ for (let at = environment; at !== null; at = at.parent) for (const name of at.providedKeys()) {
845
+ if (seen.has(name) || CARRIED_BY_PROPS.has(name)) continue;
846
+ seen.add(name);
847
+ const key = findEnvironmentKey(name);
848
+ if (key !== void 0) out.push(provided(key, environment.get(key)));
849
+ }
850
+ return out;
851
+ }
652
852
  /**
653
853
  * The same values again whenever they change where the entry was
654
854
  * declared, for as long as it is open.
@@ -668,6 +868,7 @@ const followEnvironmentKind = defineModifier({
668
868
  host.set("textStyle", textStyle);
669
869
  host.set("color", host.environment(UiEnvironmentKeys.color, from));
670
870
  host.set("contentColor", host.environment(UiEnvironmentKeys.contentColor, from));
871
+ if (from.environment !== null) host.set("provide", providedFrom(from.environment));
671
872
  if (entry !== null) {
672
873
  const edges = physicalEdges(entry, textStyle.textDirection === "rtl");
673
874
  host.set("placement", edges.placement);
@@ -684,6 +885,36 @@ function followEnvironment(node, entry) {
684
885
  })];
685
886
  }
686
887
  //#endregion
888
+ //#region src/startingInputsKey.ts
889
+ /**
890
+ * The inputs components under a node start with, by component name,
891
+ * over what their parents pass; see `StartingInputs`. Empty outside one.
892
+ */
893
+ const StartingInputsKey = createEnvironmentKey({
894
+ name: "gesso.startingInputs",
895
+ defaultValue: {}
896
+ });
897
+ /** How many of each name have been made under each set of starting inputs, for a list's next. */
898
+ const made = /* @__PURE__ */ new WeakMap();
899
+ /** What a component named `name`, made at `anchor`, starts with: null for nothing. */
900
+ function startingInputsFor(anchor, name) {
901
+ for (let at = anchor; at !== null; at = at.parent) if (at.environment !== null) {
902
+ const all = at.environment.get(StartingInputsKey);
903
+ const given = all[name];
904
+ if (given === void 0) return null;
905
+ if (!Array.isArray(given)) return given;
906
+ let counts = made.get(all);
907
+ if (counts === void 0) {
908
+ counts = /* @__PURE__ */ new Map();
909
+ made.set(all, counts);
910
+ }
911
+ const index = counts.get(name) ?? 0;
912
+ counts.set(name, index + 1);
913
+ return given[index] ?? null;
914
+ }
915
+ return null;
916
+ }
917
+ //#endregion
687
918
  //#region src/ComponentHost.ts
688
919
  /**
689
920
  * Owns a single component instance and its lifecycle.
@@ -710,6 +941,7 @@ function followEnvironment(node, entry) {
710
941
  var ComponentHost = class {
711
942
  services;
712
943
  channels;
944
+ anchor;
713
945
  /** The class instance; undefined for a functional component. */
714
946
  instance;
715
947
  element;
@@ -731,14 +963,34 @@ var ComponentHost = class {
731
963
  inputSubscriptions = /* @__PURE__ */ new Map();
732
964
  /** A functional component's cells, by prop name, as they were handed out. */
733
965
  functionalCells = /* @__PURE__ */ new Map();
966
+ /**
967
+ * Inputs it is drawn with over its parent's, from a `StartingInputs`
968
+ * above it when it was made: a moment of a screen, a tour's second
969
+ * step. A preview goes over these, and ending one comes back to them.
970
+ */
971
+ started;
972
+ /** What the parent passed for each started input at birth: a new value from it takes over. */
973
+ startedOver = /* @__PURE__ */ new Map();
974
+ /** A started input the parent passes as a stream: the start, then the stream's changes; one per input, so it is not resubscribed. */
975
+ startedStreams = /* @__PURE__ */ new Map();
976
+ /** What the component says it holds now, by input; see `liveInput`. */
977
+ live = /* @__PURE__ */ new Map();
978
+ /** Inputs a tool draws the component with over its parent's, until it ends them; see `previewInputs`. */
979
+ previewed = null;
980
+ /** A class's input values from before they were previewed, for the ones its parent does not pass. */
981
+ beforePreview = /* @__PURE__ */ new Map();
734
982
  mountHooks = [];
735
983
  unmountHooks = [];
736
984
  /** True only while the component function runs; hooks may register then. */
737
985
  rendering = false;
738
- constructor(element, services = new ServiceRegistry(), channels = new ChannelRegistry()) {
986
+ constructor(element, services = new ServiceRegistry(), channels = new ChannelRegistry(), anchor = null) {
739
987
  this.services = services;
740
988
  this.channels = channels;
989
+ this.anchor = anchor;
741
990
  this.element = element;
991
+ const started = startingInputsFor(anchor, element.tag);
992
+ this.started = started === null ? null : { ...started };
993
+ for (const name of Object.keys(this.started ?? {})) this.startedOver.set(name, element.props[name]);
742
994
  if (isClassComponent(element.component)) {
743
995
  this.instance = new element.component();
744
996
  this.validateInputs();
@@ -793,14 +1045,68 @@ var ComponentHost = class {
793
1045
  this.wireInputs();
794
1046
  return;
795
1047
  }
796
- for (const [name, cell] of this.functionalCells) this.applyInput(name, cell, props[name], true);
1048
+ for (const [name, cell] of this.functionalCells) this.applyInput(name, cell, this.providedFor(name), true);
1049
+ }
1050
+ /**
1051
+ * Feeds the inputs `values` names from them instead of from the
1052
+ * parent, until a call with null hands them back to the parent: a
1053
+ * design tool showing one of a component's states on the live tree.
1054
+ * A later call replaces the earlier one; a new prop from the parent
1055
+ * for a previewed input waits until the preview ends.
1056
+ *
1057
+ * Each input a preview starts or ends is handed its value even when
1058
+ * it holds that value already, as a new value from the parent is: a
1059
+ * control that owns what it was started at (`controlled`) starts
1060
+ * again from it, rather than keeping what a person turned it to.
1061
+ */
1062
+ previewInputs(values) {
1063
+ const names = /* @__PURE__ */ new Set([...Object.keys(this.previewed ?? {}), ...Object.keys(values ?? {})]);
1064
+ if (this.instance !== void 0) {
1065
+ const cells = this.instance;
1066
+ const inputs = new Set(getComponentMetadata(this.element.component).inputs);
1067
+ for (const name of Object.keys(values ?? {})) if (inputs.has(name) && !this.beforePreview.has(name)) this.beforePreview.set(name, cells[name].getValue());
1068
+ this.previewed = values;
1069
+ this.wireInputs(names);
1070
+ for (const [name, value] of this.beforePreview) if (values === null || !Object.hasOwn(values, name)) {
1071
+ this.beforePreview.delete(name);
1072
+ if (this.providedFor(name) === void 0) cells[name].next(value);
1073
+ }
1074
+ return;
1075
+ }
1076
+ this.previewed = values;
1077
+ for (const name of names) {
1078
+ const cell = this.functionalCells.get(name);
1079
+ if (cell !== void 0) this.applyInput(name, cell, this.providedFor(name), true, true);
1080
+ }
1081
+ }
1082
+ /** What an input is fed from: a preview's value for it, else what it was started with, else the parent's prop. */
1083
+ providedFor(name) {
1084
+ if (this.previewed !== null && Object.hasOwn(this.previewed, name)) return this.previewed[name];
1085
+ const parent = this.element.props[name];
1086
+ if (this.started !== null && Object.hasOwn(this.started, name)) {
1087
+ if (parent !== this.startedOver.get(name)) {
1088
+ delete this.started[name];
1089
+ this.startedStreams.delete(name);
1090
+ return parent;
1091
+ }
1092
+ if (isObservable(parent)) {
1093
+ let stream = this.startedStreams.get(name);
1094
+ if (stream === void 0) {
1095
+ stream = concat(of(this.started[name]), parent.pipe(skip(1)));
1096
+ this.startedStreams.set(name, stream);
1097
+ }
1098
+ return stream;
1099
+ }
1100
+ return this.started[name];
1101
+ }
1102
+ return this.element.props[name];
797
1103
  }
798
1104
  renderFunction() {
799
1105
  const component = this.element.component;
800
1106
  const context = this.createContext();
801
1107
  this.rendering = true;
802
1108
  try {
803
- return withBodyOf(this.element.tag, () => component(this.createInputRecord(), context));
1109
+ return withLiveInputs(this.live, () => withBodyOf(this.element.tag, () => component(this.createInputRecord(), context)));
804
1110
  } finally {
805
1111
  this.rendering = false;
806
1112
  }
@@ -820,7 +1126,7 @@ var ComponentHost = class {
820
1126
  cell = new InputCell(void 0);
821
1127
  cell.label = `${this.element.tag}.${name}`;
822
1128
  this.functionalCells.set(name, cell);
823
- this.applyInput(name, cell, this.element.props[name], true);
1129
+ this.applyInput(name, cell, this.providedFor(name), true);
824
1130
  }
825
1131
  return cell;
826
1132
  };
@@ -863,18 +1169,25 @@ var ComponentHost = class {
863
1169
  const cell = bounds(label ?? `${this.element.tag}.bounds`);
864
1170
  this.subscriptions.add(() => cell.complete());
865
1171
  return cell;
1172
+ },
1173
+ environment: (key) => {
1174
+ for (let at = this.anchor; at !== null; at = at.parent) if (at.environment !== null) return at.environment.get(key);
1175
+ return key.defaultValue;
866
1176
  }
867
1177
  };
868
1178
  }
869
- wireInputs() {
1179
+ wireInputs(forced = /* @__PURE__ */ new Set()) {
870
1180
  const metadata = getComponentMetadata(this.element.component);
871
- const props = this.element.props;
872
1181
  for (const inputName of metadata.inputs) {
873
1182
  const cell = this.instance[inputName];
874
1183
  cell.label ??= `${metadata.tag}.${inputName}`;
875
- this.applyInput(inputName, cell, props[inputName], false);
1184
+ this.applyInput(inputName, cell, this.providedFor(inputName), false, forced.has(inputName));
876
1185
  }
877
1186
  }
1187
+ /** What the component says each of its inputs holds now; see `liveInput`. */
1188
+ liveInputs() {
1189
+ return new Map([...this.live].map(([name, read]) => [name, read()]));
1190
+ }
878
1191
  /**
879
1192
  * What the parent most recently supplied for each input, as given:
880
1193
  * a value, an element or a stream, whether or not the component has
@@ -894,8 +1207,8 @@ var ComponentHost = class {
894
1207
  * fallback)` supplies one — so a prop the parent stops passing is
895
1208
  * pushed through as `undefined`, which re-applies the fallback.
896
1209
  */
897
- applyInput(inputName, cell, provided, resetWhenAbsent) {
898
- if (this.inputSources.has(inputName) && this.inputSources.get(inputName) === provided) return;
1210
+ applyInput(inputName, cell, provided, resetWhenAbsent, force = false) {
1211
+ if (!force && this.inputSources.has(inputName) && this.inputSources.get(inputName) === provided) return;
899
1212
  const previous = this.inputSubscriptions.get(inputName);
900
1213
  if (previous !== void 0) {
901
1214
  previous.unsubscribe();
@@ -914,12 +1227,19 @@ var ComponentHost = class {
914
1227
  return;
915
1228
  }
916
1229
  if (isObservable(provided)) {
917
- const subscription = provided.subscribe((value) => pushInput(cell, value));
1230
+ let first = force;
1231
+ const subscription = provided.subscribe((value) => {
1232
+ if (first) {
1233
+ first = false;
1234
+ forceInput(cell, value);
1235
+ } else pushInput(cell, value);
1236
+ });
918
1237
  this.inputSubscriptions.set(inputName, subscription);
919
1238
  this.subscriptions.add(subscription);
920
1239
  return;
921
1240
  }
922
- pushInput(cell, provided);
1241
+ if (force) forceInput(cell, provided);
1242
+ else pushInput(cell, provided);
923
1243
  }
924
1244
  validateInputs() {
925
1245
  const metadata = getComponentMetadata(this.element.component);
@@ -988,7 +1308,7 @@ var ComponentHostResolver = class {
988
1308
  this.services = services;
989
1309
  this.channels = channels;
990
1310
  }
991
- resolve(element, anchorId) {
1311
+ resolve(element, anchorId, anchor) {
992
1312
  let host = this.hosts.get(anchorId);
993
1313
  if (host !== void 0 && host.component !== element.component) {
994
1314
  host.dispose();
@@ -996,7 +1316,7 @@ var ComponentHostResolver = class {
996
1316
  host = void 0;
997
1317
  }
998
1318
  if (host === void 0) {
999
- host = new ComponentHost(element, this.services, this.channels);
1319
+ host = new ComponentHost(element, this.services, this.channels, anchor ?? null);
1000
1320
  this.hosts.set(anchorId, host);
1001
1321
  this.pendingMounts.push(host);
1002
1322
  } else host.updateProps(element.props);
@@ -1145,6 +1465,8 @@ var ShellService = class {
1145
1465
  handler = null;
1146
1466
  /** What `warnOnLeave` last said. */
1147
1467
  leaving = false;
1468
+ /** What `preferColorScheme` last asked for; null until it is asked. */
1469
+ preferred = null;
1148
1470
  scheme = internalState("light");
1149
1471
  contrastState = internalState("standard");
1150
1472
  pointerState = internalState("fine");
@@ -1262,6 +1584,10 @@ var ShellService = class {
1262
1584
  type: "leaveWarning",
1263
1585
  on: true
1264
1586
  });
1587
+ if (handler !== null && this.preferred !== null) handler({
1588
+ type: "colorScheme",
1589
+ preference: this.preferred
1590
+ });
1265
1591
  }
1266
1592
  /**
1267
1593
  * Called by the runtime when the shell reports the appearance.
@@ -1478,6 +1804,33 @@ var ShellService = class {
1478
1804
  on
1479
1805
  });
1480
1806
  }
1807
+ /**
1808
+ * Asks the shell to report this appearance from now on: `light` or
1809
+ * `dark` whatever the platform says, or `auto` to follow the platform
1810
+ * again. `colorScheme` then reports the answer, as it does when the
1811
+ * host calls `setColorScheme`, which is the same switch.
1812
+ *
1813
+ * For an application whose appearance setting is its own and stored:
1814
+ * the host reads the stored choice before the first frame and starts
1815
+ * with it (`createApp({ colorScheme })`), and the application, where
1816
+ * the person changes it, says so here. Without this, an application in
1817
+ * a render worker could not take back an override the host started it
1818
+ * with, and `auto` would keep reporting the stored choice instead of
1819
+ * the platform's. An application that stores nothing can combine its
1820
+ * choice with `colorScheme` instead and never call this.
1821
+ *
1822
+ * A request, because the media query is the window's; and a state,
1823
+ * like `warnOnLeave`, so a shell that arrives after it was asked is
1824
+ * told. Asking for the same thing twice sends nothing.
1825
+ */
1826
+ preferColorScheme(preference) {
1827
+ if (preference === this.preferred) return;
1828
+ this.preferred = preference;
1829
+ this.handler?.({
1830
+ type: "colorScheme",
1831
+ preference
1832
+ });
1833
+ }
1481
1834
  /** Called by the runtime when the shell reports the state. Not for applications. */
1482
1835
  applyFullscreen(active) {
1483
1836
  if (this.isFullscreen.value !== active) this.isFullscreen.value = active;
@@ -1728,6 +2081,44 @@ function shellFilesUnsupported(error = "There is no shell to reach files through
1728
2081
  error
1729
2082
  };
1730
2083
  }
2084
+ //#endregion
2085
+ //#region src/app/FrameService.ts
2086
+ /**
2087
+ * Every frame the runtime draws, as a component can hear it.
2088
+ *
2089
+ * The shell already receives these as `onFrame`, for an FPS readout or
2090
+ * a profiler on the host thread. A screen that wants to show its own
2091
+ * frame gap or input latency, as a demo of the thread model does,
2092
+ * needs them on this thread, and this is where the runtime puts them.
2093
+ * Injected like any service: `ctx.inject(FrameService).frames`.
2094
+ */
2095
+ var FrameService = class {
2096
+ subject = new Subject();
2097
+ flusher = null;
2098
+ /** Emits after each frame finishes, with what it cost and what it answered. */
2099
+ frames = this.subject.asObservable();
2100
+ /** Called by the runtime; not for components. */
2101
+ publish(metrics) {
2102
+ this.subject.next(metrics);
2103
+ }
2104
+ /** Called by the shell that drives the frames; not for components. */
2105
+ setFlush(flush) {
2106
+ this.flusher = flush;
2107
+ }
2108
+ /**
2109
+ * Draws a pending frame now, rather than when the display next asks
2110
+ * for one; nothing when none is pending.
2111
+ *
2112
+ * A tab in the background is sent no frames. A design editor's tool,
2113
+ * called by an agent while the editor's window is behind another,
2114
+ * changes the screen and waits for it to be drawn before reading it:
2115
+ * without this, it waits for a frame that never comes. The agent's
2116
+ * own screen tools (`uiSurface`) do the same after each action.
2117
+ */
2118
+ flush() {
2119
+ this.flusher?.();
2120
+ }
2121
+ };
1731
2122
  /** A text node's text as the tree shows it. */
1732
2123
  function treeText(text) {
1733
2124
  if (typeof text !== "string" || text === "") return;
@@ -2570,6 +2961,20 @@ var EditingService = class {
2570
2961
  return this.controller.caretRectOf(node, offset);
2571
2962
  }
2572
2963
  /**
2964
+ * The caret position nearest a point in a field, the point given as a
2965
+ * pointer event gives it: where a press there would put the caret.
2966
+ *
2967
+ * What a field opened over text the person pointed at uses to start
2968
+ * the caret where they pointed: ask once the field is laid out, then
2969
+ * `select` that offset. Null when the node is not an editable, when it
2970
+ * has not been laid out yet, or before the runtime has wired the
2971
+ * controller: "ask again next frame", as for `caretRectOf`.
2972
+ */
2973
+ offsetNear(node, x, y) {
2974
+ if (node === null || this.controller === null) return null;
2975
+ return this.controller.offsetNear(node, x, y);
2976
+ }
2977
+ /**
2573
2978
  * Selects from `anchor` to `focus` and focuses the field the focus is
2574
2979
  * in: within one field, or across the fields of an editing group (see
2575
2980
  * `UiEditingGroup`). What an editor does after a command over a
@@ -2613,124 +3018,6 @@ var ScrollService = class {
2613
3018
  }
2614
3019
  };
2615
3020
  //#endregion
2616
- //#region src/app/FocusService.ts
2617
- /**
2618
- * Keyboard focus, as a store components can inject.
2619
- *
2620
- * `UiFocusManager` lives in the runtime and a component cannot reach
2621
- * it — components reach the world through stores, as they do for the
2622
- * clipboard (`ShellService`), overlays (`OverlayService`) and find
2623
- * (`FindService`). Without this a component cannot autofocus a field,
2624
- * trap the keyboard in a dialog, or put the caret in the input that
2625
- * failed validation.
2626
- *
2627
- * Nodes come from a `ref` prop. A tree's refs fire before the runtime
2628
- * installs the manager, so an action taken during the first build is
2629
- * queued and replayed once there is one; that is the same problem
2630
- * `FindService` solves for its query field, and the reason `autoFocus`
2631
- * in a dialog works on the frame it mounts.
2632
- *
2633
- * It must stay on the render thread: its actions take `UiNode`s, which
2634
- * never cross a worker boundary.
2635
- */
2636
- var FocusService = class {
2637
- /** The node holding focus, or null. A control binds its focus ring to this. */
2638
- focused = internalState(null);
2639
- /** Whether focus is confined to a subtree by an open trap. */
2640
- trapped = internalState(false);
2641
- /**
2642
- * Whether the focus held is focus the keyboard can see: reached by Tab
2643
- * or an arrow key, or by code with no pointer since, rather than by a
2644
- * press. CSS's `:focus-visible`, for something other than a ring that
2645
- * should answer the keyboard and not the click that happened to focus
2646
- * the same control: a tooltip, say.
2647
- */
2648
- focusVisible = internalState(false);
2649
- manager = null;
2650
- detach = null;
2651
- /** Actions taken before the runtime installed a manager, in order. */
2652
- queued = [];
2653
- /** Installed by the runtime; without one every action is queued. */
2654
- setManager(manager) {
2655
- this.detach?.();
2656
- this.detach = null;
2657
- this.manager = manager;
2658
- if (manager === null) {
2659
- this.queued = [];
2660
- return;
2661
- }
2662
- const focusChange = manager.onFocusChange((node) => {
2663
- this.focused.value = node;
2664
- this.focusVisible.value = manager.focusVisible;
2665
- });
2666
- const scopeChange = manager.onScopeChange(() => {
2667
- this.trapped.value = manager.trapped;
2668
- });
2669
- this.detach = () => {
2670
- focusChange();
2671
- scopeChange();
2672
- };
2673
- const pending = this.queued;
2674
- this.queued = [];
2675
- for (const action of pending) action(manager);
2676
- this.sync(manager);
2677
- }
2678
- /**
2679
- * Gives the node keyboard focus. Non-focusable nodes are ignored.
2680
- *
2681
- * The node is scrolled into view, unless `options.preventScroll` asks
2682
- * for the page to stay where it is, as `element.focus({ preventScroll:
2683
- * true })` does in a browser.
2684
- */
2685
- focus(node, options) {
2686
- this.run((manager) => manager.focus(node, "program", options));
2687
- }
2688
- /** Drops focus without moving it anywhere. */
2689
- blur() {
2690
- this.run((manager) => manager.blur());
2691
- }
2692
- /** Moves focus to the next focusable node, wrapping around. */
2693
- focusNext() {
2694
- this.run((manager) => manager.focusNext());
2695
- }
2696
- /** Moves focus to the previous focusable node, wrapping around. */
2697
- focusPrevious() {
2698
- this.run((manager) => manager.focusPrevious());
2699
- }
2700
- /**
2701
- * Confines focus to `scope` until `releaseTrap`, moving it inside if
2702
- * it was elsewhere. Traps nest: a dialog opened over a dialog traps
2703
- * again, and each release restores its own opener.
2704
- */
2705
- trap(scope) {
2706
- this.run((manager) => manager.pushScope(scope));
2707
- }
2708
- /**
2709
- * Ends the innermost trap and returns focus to whatever held it when
2710
- * the trap was taken — the button that opened the dialog. Given the
2711
- * node trapped in, only that trap ends, and nothing when it ended
2712
- * already: a trap's subtree removed releases it, and a release after
2713
- * that would otherwise end the trap under it.
2714
- */
2715
- releaseTrap(scope) {
2716
- this.run((manager) => manager.popScope(scope));
2717
- }
2718
- run(action) {
2719
- const manager = this.manager;
2720
- if (manager === null) {
2721
- this.queued.push(action);
2722
- return;
2723
- }
2724
- action(manager);
2725
- this.sync(manager);
2726
- }
2727
- sync(manager) {
2728
- this.focused.value = manager.focusedNode;
2729
- this.focusVisible.value = manager.focusVisible;
2730
- this.trapped.value = manager.trapped;
2731
- }
2732
- };
2733
- //#endregion
2734
3021
  //#region src/app/MediaService.ts
2735
3022
  /**
2736
3023
  * Where an `Image` finds its bitmap, an `Icon` its raster and a
@@ -3065,6 +3352,16 @@ var AnimationService = class {
3065
3352
  */
3066
3353
  setMotion(motion) {
3067
3354
  this.motionVocabulary = motion;
3355
+ this.motionListener?.(motion);
3356
+ }
3357
+ motionListener = null;
3358
+ /**
3359
+ * Called by the runtime, which provides the vocabulary at its root so
3360
+ * a name in a `transition` or a `motion` state resolves to it too; a
3361
+ * part of the tree may provide its own over it (`UiEnvironmentKeys.motion`).
3362
+ */
3363
+ setMotionListener(listener) {
3364
+ this.motionListener = listener;
3068
3365
  }
3069
3366
  get motion() {
3070
3367
  return this.motionVocabulary;
@@ -3115,7 +3412,7 @@ var AnimationService = class {
3115
3412
  tween.snap();
3116
3413
  return tween.values;
3117
3414
  }
3118
- return driver.start(tween);
3415
+ return driver.start(tween, options.clock ?? null);
3119
3416
  }
3120
3417
  /**
3121
3418
  * Pulls a number towards a value on a spring, with no duration.
@@ -3124,7 +3421,7 @@ var AnimationService = class {
3124
3421
  * so a flick that changes direction bends rather than restarting.
3125
3422
  */
3126
3423
  spring(cell, to, options = {}) {
3127
- const base = typeof options.spring === "string" || options.spring === void 0 ? this.motionVocabulary.springs[options.spring ?? "snappy"] : options.spring;
3424
+ const base = springIn(this.motionVocabulary, options.spring);
3128
3425
  const driver = this.driver;
3129
3426
  const previous = driver?.animationFor(cell);
3130
3427
  const animation = new UiSpring(cell, to, {
@@ -3143,7 +3440,7 @@ var AnimationService = class {
3143
3440
  animation.snap();
3144
3441
  return animation.values;
3145
3442
  }
3146
- return driver.start(animation);
3443
+ return driver.start(animation, options.clock ?? null);
3147
3444
  }
3148
3445
  /** Stops whatever is driving a cell, leaving it where it stands. */
3149
3446
  stop(cell) {
@@ -3162,31 +3459,10 @@ var AnimationService = class {
3162
3459
  return this.driver?.animationFor(cell);
3163
3460
  }
3164
3461
  resolveDuration(duration) {
3165
- return typeof duration === "number" ? duration : this.motionVocabulary.durations[duration];
3462
+ return durationIn(this.motionVocabulary, duration);
3166
3463
  }
3167
3464
  resolveEasing(easing) {
3168
- if (typeof easing === "function") return easing;
3169
- return this.motionVocabulary.easings[easing] ?? easings.standard;
3170
- }
3171
- };
3172
- //#endregion
3173
- //#region src/app/FrameService.ts
3174
- /**
3175
- * Every frame the runtime draws, as a component can hear it.
3176
- *
3177
- * The shell already receives these as `onFrame`, for an FPS readout or
3178
- * a profiler on the host thread. A screen that wants to show its own
3179
- * frame gap or input latency, as a demo of the thread model does,
3180
- * needs them on this thread, and this is where the runtime puts them.
3181
- * Injected like any service: `ctx.inject(FrameService).frames`.
3182
- */
3183
- var FrameService = class {
3184
- subject = new Subject();
3185
- /** Emits after each frame finishes, with what it cost and what it answered. */
3186
- frames = this.subject.asObservable();
3187
- /** Called by the runtime; not for components. */
3188
- publish(metrics) {
3189
- this.subject.next(metrics);
3465
+ return easingIn(this.motionVocabulary, easing);
3190
3466
  }
3191
3467
  };
3192
3468
  //#endregion
@@ -3228,6 +3504,17 @@ var InspectionService = class {
3228
3504
  return this.host.inspect(node);
3229
3505
  }
3230
3506
  /**
3507
+ * Why the node has the size it has, as the layout decided it: for
3508
+ * each axis, the rule that fixed it (`decidedBy`: its own length, a
3509
+ * flex share, a stretch across its parent, its content, a clamp) and
3510
+ * the sentences that say so. What a design tool needs to tell an
3511
+ * element that fills its parent from one that only happens to be as
3512
+ * wide, which the box alone cannot. Null before the node is laid out.
3513
+ */
3514
+ explain(node) {
3515
+ return this.host?.explain?.(node) ?? null;
3516
+ }
3517
+ /**
3231
3518
  * Where the node is drawn, as `inspect(node).box` reports it, without
3232
3519
  * the rest of the report: for an editor that reads the boxes of many
3233
3520
  * nodes as its view moves (a minimap of a board of frames), where
@@ -3238,6 +3525,19 @@ var InspectionService = class {
3238
3525
  return this.host.box?.(node) ?? this.host.inspect(node).box;
3239
3526
  }
3240
3527
  /**
3528
+ * Where the node is seen on the screen: `box` with every transform
3529
+ * above it applied, so a node in a frame drawn at half its size is
3530
+ * half its size, where it is drawn. What an overlay laid over the
3531
+ * live tree is placed by (an outline on the component under the
3532
+ * pointer). `box` keeps the untransformed box, which is where the
3533
+ * node is in the layout: what an editor reading a zoomed board in
3534
+ * the board's own coordinates (a minimap) needs.
3535
+ */
3536
+ screenBox(node) {
3537
+ if (this.host === null) throw new Error("InspectionService has nothing to measure before the runtime has built its tree.");
3538
+ return this.host.screenBox?.(node) ?? this.box(node);
3539
+ }
3540
+ /**
3241
3541
  * Draws a node as though it declared `value` for `property`, until
3242
3542
  * the preview is ended or the node leaves the tree.
3243
3543
  *
@@ -3255,6 +3555,43 @@ var InspectionService = class {
3255
3555
  endPreview(node, property) {
3256
3556
  this.host?.endPreview?.(node, property);
3257
3557
  }
3558
+ /**
3559
+ * Draws a component as though its parent passed `inputs`, over what
3560
+ * the parent did pass, until the preview is ended or the component
3561
+ * leaves the tree. The component is named by its anchor, as
3562
+ * `inspect(node).owners[].anchorId` gives it.
3563
+ *
3564
+ * What a design editor shows when a component on a played screen is
3565
+ * jumped to one of its states (Filled, Error, Disabled): the same
3566
+ * instance, still live, given that state's inputs, and the inputs it
3567
+ * was written with back when the preview ends. A later preview of the
3568
+ * same component replaces this one. The names and values are props',
3569
+ * as JSX would write them. An anchor no component holds is ignored.
3570
+ */
3571
+ previewInputs(anchorId, inputs) {
3572
+ this.host?.previewInputs?.(anchorId, inputs);
3573
+ }
3574
+ /** Ends a preview of a component's inputs: its parent's are its own again. */
3575
+ endInputPreview(anchorId) {
3576
+ this.host?.previewInputs?.(anchorId, null);
3577
+ }
3578
+ /**
3579
+ * Presses a node, or replaces the text of the field it is, as a
3580
+ * person would: a click at the middle of where it is drawn, which
3581
+ * its handlers and every handler above it get, or the edit a
3582
+ * keyboard makes once it has focus. The path the accessibility
3583
+ * mirror's actions take, for any node in the tree, record or not.
3584
+ *
3585
+ * What a design editor's agent does to play a prototype in its live
3586
+ * frames: a row pressed, an email typed, by a node it found with
3587
+ * `nodesAt` or by walking the tree. False when it did not take: a
3588
+ * node no longer in the tree, or a field that could not have focus
3589
+ * because an open dialog's trap holds it, or takes no text.
3590
+ */
3591
+ act(node, action, value) {
3592
+ if (this.host?.act === void 0) throw new Error("InspectionService cannot act before the runtime has built its tree.");
3593
+ return this.host.act(node, action, value);
3594
+ }
3258
3595
  };
3259
3596
  //#endregion
3260
3597
  //#region src/app/InputLatency.ts
@@ -3421,7 +3758,10 @@ var SmoothScroller = class {
3421
3758
  const { min, max } = this.boundsOf(node, axis);
3422
3759
  const target = clamp((pending ?? from) + delta, min, max);
3423
3760
  if (pending !== void 0 && target === pending) return;
3424
- this.animations.spring(cell, target, { spring: SCROLL_SPRING });
3761
+ this.animations.spring(cell, target, {
3762
+ spring: SCROLL_SPRING,
3763
+ clock: clockOf(node)
3764
+ });
3425
3765
  }
3426
3766
  /**
3427
3767
  * Shifts a running scroll by a coordinate correction.
@@ -3446,7 +3786,10 @@ var SmoothScroller = class {
3446
3786
  const pending = this.destinationOf(state.cell);
3447
3787
  if (pending === void 0) return;
3448
3788
  state.lastWritten = state.cell.value;
3449
- this.animations.spring(state.cell, pending + delta, { spring: SCROLL_SPRING });
3789
+ this.animations.spring(state.cell, pending + delta, {
3790
+ spring: SCROLL_SPRING,
3791
+ clock: clockOf(node)
3792
+ });
3450
3793
  }
3451
3794
  /** Whether this container is being animated right now. */
3452
3795
  isScrolling(node) {
@@ -3654,6 +3997,8 @@ var GessoRuntime = class {
3654
3997
  lastEarlyFrame = -Infinity;
3655
3998
  /** When the last discrete input was routed; see `answerInput`. */
3656
3999
  lastDiscreteInput = -Infinity;
4000
+ /** The wheel's steps since the last frame, added together; see `wheel`. */
4001
+ heldWheel = null;
3657
4002
  /** The display time the frame being drawn is for; see `FrameMetrics.time`. */
3658
4003
  frameTime = null;
3659
4004
  /** When the frame's first phase began, before its changes were collected; see `FrameMetrics.durationMs`. */
@@ -3763,6 +4108,11 @@ var GessoRuntime = class {
3763
4108
  * them. Only populated while a listener is attached.
3764
4109
  */
3765
4110
  semanticsBoxes = /* @__PURE__ */ new Map();
4111
+ /** When a frame last moved something, and when the boxes were last swept; see `updateSemantics`. */
4112
+ semanticsMovedAt = -Infinity;
4113
+ semanticsSweptAt = -Infinity;
4114
+ /** The sweep owed once a run of moving frames stops. */
4115
+ semanticsSweepTimer = null;
3766
4116
  /** What each semantics walk leaves for the next; see `SemanticsMemory`. */
3767
4117
  semanticsMemory = semanticsMemory();
3768
4118
  lastFocusedId = null;
@@ -3809,6 +4159,10 @@ var GessoRuntime = class {
3809
4159
  * lays out, whichever comes first; see `resize`.
3810
4160
  */
3811
4161
  laidOutOnce = false;
4162
+ /** Frames in a row that threw; see `reportFrameError`. */
4163
+ framesFailed = 0;
4164
+ /** Nodes that declare `holdUnseen`, as frames have found them; see `updateHolds`. */
4165
+ holdable = /* @__PURE__ */ new Set();
3812
4166
  constructor(options) {
3813
4167
  this.services = options.services ?? new ServiceRegistry();
3814
4168
  this.channels = options.channels ?? new ChannelRegistry();
@@ -3902,7 +4256,8 @@ var GessoRuntime = class {
3902
4256
  width: this.width,
3903
4257
  height: this.height
3904
4258
  }),
3905
- onLayout: (node, listener) => this.layoutNotifier.add(node, listener)
4259
+ onLayout: (node, listener) => this.layoutNotifier.add(node, listener),
4260
+ onFlowLayout: (node, listener) => this.layoutNotifier.addFlow(node, listener)
3906
4261
  },
3907
4262
  animations: this.animations,
3908
4263
  sharedElements: this.sharedElements
@@ -3926,6 +4281,9 @@ var GessoRuntime = class {
3926
4281
  if (options.fonts !== void 0) this.services.get(FontService).declare(options.fonts);
3927
4282
  if (!this.services.has(AnimationService)) this.services.register(AnimationService);
3928
4283
  this.services.get(AnimationService).setDriver(this.animations);
4284
+ this.services.get(AnimationService).setMotionListener((motion) => {
4285
+ if (this.graph.updateNodePropertyNow(this.graph.root, "provide", [provided(UiEnvironmentKeys.motion, motion)], DirtyFlags.Environment)) this.requestRepaint();
4286
+ });
3929
4287
  this.services.get(AudioService).setAnimations(this.services.get(AnimationService));
3930
4288
  this.smoothScroller = new SmoothScroller(this.graph, this.services.get(AnimationService), (node, axis) => {
3931
4289
  const record = this.engine.recordFor(node);
@@ -3997,6 +4355,7 @@ var GessoRuntime = class {
3997
4355
  this.services.get(InspectionService).attach({
3998
4356
  nodesAt: (x, y) => this.hitTester.hitStack(x, y),
3999
4357
  inspect: (node) => this.inspectNode(node),
4358
+ explain: (node) => this.engine.explain(node),
4000
4359
  box: (node) => {
4001
4360
  const box = this.engine.visibleBox(node);
4002
4361
  return {
@@ -4006,8 +4365,22 @@ var GessoRuntime = class {
4006
4365
  height: box.height
4007
4366
  };
4008
4367
  },
4368
+ screenBox: (node) => {
4369
+ const box = this.engine.screenBox(node);
4370
+ return {
4371
+ x: box.x,
4372
+ y: box.y,
4373
+ width: box.width,
4374
+ height: box.height
4375
+ };
4376
+ },
4009
4377
  preview: (node, property, value) => this.previewProperty(node, property, value),
4010
- endPreview: (node, property) => this.endPreviews(node, property)
4378
+ endPreview: (node, property) => this.endPreviews(node, property),
4379
+ previewInputs: (anchorId, inputs) => this.resolver.hostFor(anchorId)?.previewInputs(inputs),
4380
+ act: (node, action, value) => this.graph.getNode(node.id) === node && this.actOn(node, value === void 0 ? { action } : {
4381
+ action,
4382
+ value
4383
+ })
4011
4384
  });
4012
4385
  this.services.get(FocusService).setManager(this.input.focus);
4013
4386
  this.services.get(EditingService).setController(this.input.editing);
@@ -4070,6 +4443,7 @@ var GessoRuntime = class {
4070
4443
  this.frameErrorListener = listener;
4071
4444
  }
4072
4445
  reportFrameError(error) {
4446
+ if (this.framesFailed++ === 0) this.requestRepaint();
4073
4447
  const message = error instanceof Error ? error.message : String(error);
4074
4448
  const stack = error instanceof Error ? error.stack : void 0;
4075
4449
  if (this.frameErrorListener !== null) {
@@ -4236,6 +4610,51 @@ var GessoRuntime = class {
4236
4610
  this.lastDiscreteInput = now();
4237
4611
  this.drawEarly();
4238
4612
  }
4613
+ /**
4614
+ * A step of the wheel, or of a trackpad's two fingers or its pinch,
4615
+ * held until the frame and added to the others that arrive before it.
4616
+ *
4617
+ * A trackpad sends a step on every refresh, and a handler that takes
4618
+ * longer than a refresh over one (a design tool's board zoomed over
4619
+ * hundreds of frames, each of whose names it scales) fell behind for
4620
+ * good: each step waited for the one before it, the frame waited for
4621
+ * them all, and a pinch drew nothing until the fingers stopped, then
4622
+ * jumped. Held, the steps that queue while the handler runs are one
4623
+ * step at the next frame, so the handler runs once a frame however
4624
+ * many arrive, and what it draws follows the fingers a frame at a
4625
+ * time. A step with other modifiers or in another unit is not added
4626
+ * to the held one but follows it, and any other input lets the held
4627
+ * step go first (`releaseWheel`), so nothing is answered out of order.
4628
+ */
4629
+ wheel(...step) {
4630
+ const held = this.heldWheel;
4631
+ if (held !== null && !sameWheel(held, step)) this.releaseWheel();
4632
+ if (this.heldWheel === null) {
4633
+ this.heldWheel = step;
4634
+ this.scheduler.wake();
4635
+ return;
4636
+ }
4637
+ const [, , deltaX, deltaY, modifiers, deltaMode, wheelDeltaY] = this.heldWheel;
4638
+ const [x, y, moreX, moreY, , , moreWheelY, at] = step;
4639
+ const legacy = wheelDeltaY === void 0 || moreWheelY === void 0 ? moreWheelY : wheelDeltaY + moreWheelY;
4640
+ this.heldWheel = [
4641
+ x,
4642
+ y,
4643
+ deltaX + moreX,
4644
+ deltaY + moreY,
4645
+ modifiers,
4646
+ deltaMode,
4647
+ legacy,
4648
+ at
4649
+ ];
4650
+ }
4651
+ /** Hands the held wheel step to the input now, before what follows it; see `wheel`. */
4652
+ releaseWheel() {
4653
+ const held = this.heldWheel;
4654
+ if (held === null) return;
4655
+ this.heldWheel = null;
4656
+ this.input.wheel.wheel(...held);
4657
+ }
4239
4658
  /** Draws the pending frame in a task of its own, if the guards in `answerInput` allow. */
4240
4659
  drawEarly() {
4241
4660
  if (!this.started || !this.visible || this.earlyFrame !== null || !this.scheduler.framePending) return;
@@ -4436,7 +4855,8 @@ var GessoRuntime = class {
4436
4855
  source: PREVIEW,
4437
4856
  name: "preview",
4438
4857
  order: Number.MAX_SAFE_INTEGER,
4439
- value
4858
+ value,
4859
+ snap: true
4440
4860
  }, propertyEffects(property));
4441
4861
  }
4442
4862
  endPreviews(node, property) {
@@ -4780,6 +5200,7 @@ var GessoRuntime = class {
4780
5200
  listens: this.dispatcher.listenerTypes(node),
4781
5201
  beneath: this.beneathAtPointer(node),
4782
5202
  semantics: this.semanticsOf(node),
5203
+ ...this.interactionOf(node),
4783
5204
  explanation: formatExplanation(this.engine.explain(node)),
4784
5205
  ...this.sourceReport(node)
4785
5206
  };
@@ -4827,6 +5248,27 @@ var GessoRuntime = class {
4827
5248
  const source = this.builder.sourceOf(node);
4828
5249
  return source === void 0 ? {} : { source };
4829
5250
  }
5251
+ /** The props the node's `interactive` modifiers write while hovered and pressed, later ones over earlier. */
5252
+ interactionOf(node) {
5253
+ let hovered;
5254
+ let pressed;
5255
+ for (const modifier of this.builder.modifiersFor(node)?.modifiers ?? []) {
5256
+ if (modifier.kind.name !== "interactive") continue;
5257
+ const options = modifier.args;
5258
+ if (options.hovered !== void 0) hovered = {
5259
+ ...hovered,
5260
+ ...options.hovered
5261
+ };
5262
+ if (options.pressed !== void 0) pressed = {
5263
+ ...pressed,
5264
+ ...options.pressed
5265
+ };
5266
+ }
5267
+ return hovered === void 0 && pressed === void 0 ? {} : { interaction: {
5268
+ ...hovered === void 0 ? {} : { hovered },
5269
+ ...pressed === void 0 ? {} : { pressed }
5270
+ } };
5271
+ }
4830
5272
  ownersOf(node) {
4831
5273
  const owners = [];
4832
5274
  for (let current = node; current !== null; current = current.parent) {
@@ -4838,11 +5280,14 @@ var GessoRuntime = class {
4838
5280
  const value = ownerInput(supplied);
4839
5281
  if (value !== void 0) inputs[name] = value;
4840
5282
  }
5283
+ const live = {};
5284
+ for (const [name, value] of host.liveInputs()) if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") live[name] = value;
4841
5285
  owners.push({
4842
5286
  name: getComponentMetadata(host.component).tag,
4843
5287
  anchorId: current.id,
4844
5288
  ...usedAt === void 0 ? {} : { usedAt },
4845
- ...Object.keys(inputs).length === 0 ? {} : { inputs }
5289
+ ...Object.keys(inputs).length === 0 ? {} : { inputs },
5290
+ ...Object.keys(live).length === 0 ? {} : { live }
4846
5291
  });
4847
5292
  }
4848
5293
  }
@@ -4999,7 +5444,9 @@ var GessoRuntime = class {
4999
5444
  * reconciles rather than recreating, so a component whose class is
5000
5445
  * the same object as before keeps its host and its `internalState`;
5001
5446
  * one whose module was replaced is a different class, and its host
5002
- * is disposed and mounted again. Scroll offsets on containers that
5447
+ * is disposed and mounted again with everything it rendered: its body
5448
+ * made new objects for its children, and a child kept from before
5449
+ * would go on holding the old ones. Scroll offsets on containers that
5003
5450
  * survive are kept, because the node is kept. Component state in a
5004
5451
  * replaced module is lost, which is the honest cost and the reason
5005
5452
  * this is a development tool.
@@ -5051,6 +5498,10 @@ var GessoRuntime = class {
5051
5498
  clearTimeout(this.rendererTimer);
5052
5499
  this.rendererTimer = null;
5053
5500
  }
5501
+ if (this.semanticsSweepTimer !== null) {
5502
+ clearTimeout(this.semanticsSweepTimer);
5503
+ this.semanticsSweepTimer = null;
5504
+ }
5054
5505
  this.detachViewportInsetEnvironment?.();
5055
5506
  this.detachViewportInsetEnvironment = null;
5056
5507
  this.viewportInsetWrite?.();
@@ -5129,7 +5580,10 @@ var GessoRuntime = class {
5129
5580
  if (isObservable(definition)) throw new Error("Root definition cannot be an Observable. Wrap it in a component or static element.");
5130
5581
  if (isComponentLikeElement(definition)) {
5131
5582
  if (depth > MAX_ROOT_COMPONENT_DEPTH) throw new Error(`Root component chain exceeded ${MAX_ROOT_COMPONENT_DEPTH} levels without producing an element.`);
5132
- const output = this.resolver.resolve(definition, `app:component:${depth}`);
5583
+ const anchorId = `app:component:${depth}`;
5584
+ const before = this.resolver.hostFor(anchorId);
5585
+ if (before !== void 0 && before.component !== definition.component && this.root !== void 0) this.builder.reconcileChildren(this.root, []);
5586
+ const output = this.resolver.resolve(definition, anchorId);
5133
5587
  return this.resolveRootElement(output, depth + 1);
5134
5588
  }
5135
5589
  return definition;
@@ -5241,7 +5695,7 @@ var GessoRuntime = class {
5241
5695
  op: "add",
5242
5696
  node
5243
5697
  })),
5244
- boxes: this.collectSemanticsBoxes(),
5698
+ boxes: this.sweepSemanticsBoxes(),
5245
5699
  focused: this.focusManager.focusedNode?.id ?? null
5246
5700
  });
5247
5701
  }
@@ -5343,18 +5797,21 @@ var GessoRuntime = class {
5343
5797
  if (this.applyTextRunAction(action)) return;
5344
5798
  const node = this.graph.getNode(action.id);
5345
5799
  if (node === void 0 || !this.semantics.has(action.id)) return;
5346
- if (action.action === "focus") {
5347
- this.focusManager.focus(node);
5348
- return;
5349
- }
5350
- if (action.action === "setValue") {
5351
- this.focusManager.focus(node);
5352
- this.input.editing.replaceText(action.value ?? "");
5353
- return;
5354
- }
5800
+ this.actOn(node, action);
5801
+ }
5802
+ /**
5803
+ * A semantics action on a node: the mirror's, or `InspectionService.act`'s
5804
+ * on a node with no record of its own. False when it did not take: focus
5805
+ * refused (an open dialog's trap holds it elsewhere), or a field that
5806
+ * takes no text.
5807
+ */
5808
+ actOn(node, action) {
5809
+ if (action.action === "focus") return this.focusManager.focus(node);
5810
+ if (action.action === "setValue") return this.focusManager.focus(node) && this.input.editing.replaceText(action.value ?? "");
5355
5811
  this.focusManager.focus(node);
5356
5812
  const box = this.engine.screenBox(node);
5357
5813
  this.dispatcher.dispatch(new UiPointerEvent(UiEventType.Click, box.x + box.width / 2, box.y + box.height / 2, 1), node);
5814
+ return true;
5358
5815
  }
5359
5816
  /**
5360
5817
  * An action on an inline link, which is a run and not a node.
@@ -5398,7 +5855,7 @@ var GessoRuntime = class {
5398
5855
  else patches = this.rescopeSemantics(frame) ?? this.restructureSemantics(frame) ?? this.rebuildSemantics();
5399
5856
  }
5400
5857
  if (listener === null) return;
5401
- const boxes = moved || patches.length > 0 ? this.collectSemanticsBoxes() : EMPTY_BOXES;
5858
+ const boxes = patches.length > 0 ? this.sweepSemanticsBoxes() : moved ? this.movedSemanticsBoxes() : EMPTY_BOXES;
5402
5859
  const focused = this.focusManager.focusedNode?.id ?? null;
5403
5860
  const focusMoved = focused !== this.lastFocusedId;
5404
5861
  this.lastFocusedId = focused;
@@ -5695,6 +6152,70 @@ var GessoRuntime = class {
5695
6152
  return ids;
5696
6153
  }
5697
6154
  /**
6155
+ * The boxes for a frame that moved something but changed no meaning:
6156
+ * all of them when it is the first move after a rest, and otherwise
6157
+ * only the focused node's, with the sweep owed for when the moving
6158
+ * stops (`SEMANTICS_SETTLE_MS`), or for when the boxes have been stale
6159
+ * too long (`SEMANTICS_MOST_STALE_MS`).
6160
+ *
6161
+ * A pan or a zoom moves every box under the camera on every frame. In
6162
+ * Atelier, a board of three dozen screens panned at 15%, the sweep was
6163
+ * 3 to 4 ms of a 4 to 6 ms frame, with frames to 13 ms, past a 120 Hz
6164
+ * display's 8.3; and the main thread wrote four styles on each of 545
6165
+ * elements per frame, which in Chromium was 45% of that thread while
6166
+ * it also had the wheel events to deliver. Nobody reads the rectangle
6167
+ * of an element that is moving under a pan: a screen reader's cursor
6168
+ * is on what it rests on. The focused node is the exception, since its
6169
+ * focus ring is drawn from its element, so it moves every frame.
6170
+ */
6171
+ movedSemanticsBoxes() {
6172
+ const at = now();
6173
+ const resting = at - this.semanticsMovedAt > SEMANTICS_SETTLE_MS;
6174
+ this.semanticsMovedAt = at;
6175
+ if (resting || at - this.semanticsSweptAt > SEMANTICS_MOST_STALE_MS) return this.sweepSemanticsBoxes();
6176
+ if (this.semanticsSweepTimer === null) this.scheduleSemanticsSweep(SEMANTICS_SETTLE_MS);
6177
+ return this.focusedSemanticsBox();
6178
+ }
6179
+ /** Sweeps the boxes once the moving has stopped, and sends what changed. */
6180
+ scheduleSemanticsSweep(wait) {
6181
+ this.semanticsSweepTimer = setTimeout(() => {
6182
+ this.semanticsSweepTimer = null;
6183
+ const listener = this.semanticsListener;
6184
+ if (listener === null) return;
6185
+ const left = this.semanticsMovedAt + SEMANTICS_SETTLE_MS - now();
6186
+ if (left > 0) {
6187
+ this.scheduleSemanticsSweep(left);
6188
+ return;
6189
+ }
6190
+ const boxes = this.sweepSemanticsBoxes();
6191
+ if (boxes.length > 0) listener({
6192
+ patches: EMPTY_PATCHES,
6193
+ boxes
6194
+ });
6195
+ }, Math.max(1, wait));
6196
+ }
6197
+ sweepSemanticsBoxes() {
6198
+ this.semanticsSweptAt = now();
6199
+ if (this.semanticsSweepTimer !== null) {
6200
+ clearTimeout(this.semanticsSweepTimer);
6201
+ this.semanticsSweepTimer = null;
6202
+ }
6203
+ return this.collectSemanticsBoxes();
6204
+ }
6205
+ /** The focused node's box when it differs from the one last sent, as `collectSemanticsBoxes` would give it. */
6206
+ focusedSemanticsBox() {
6207
+ const focused = this.focusManager.focusedNode;
6208
+ if (focused === null || focused === void 0 || !this.semantics.has(focused.id) || this.engine.recordFor(focused) === void 0) return EMPTY_BOXES;
6209
+ const box = this.engine.screenBox(focused);
6210
+ const last = this.semanticsBoxes.get(focused.id);
6211
+ if (last !== void 0 && boxesEqual(last, box)) return EMPTY_BOXES;
6212
+ this.semanticsBoxes.set(focused.id, box);
6213
+ return [{
6214
+ id: focused.id,
6215
+ box
6216
+ }];
6217
+ }
6218
+ /**
5698
6219
  * The mirrored nodes whose box differs from the one last sent, and
5699
6220
  * which are on screen.
5700
6221
  *
@@ -5793,6 +6314,7 @@ var GessoRuntime = class {
5793
6314
  childX = x - (rec.scrollable ? rec.scrollX : 0);
5794
6315
  childY = y - (rec.scrollable ? rec.scrollY : 0);
5795
6316
  }
6317
+ if (node.hold !== null && !(focused != null && focused.heldBy === node)) continue;
5796
6318
  for (let child = node.lastChild; child !== null; child = child.previousSibling) stack.push([
5797
6319
  child,
5798
6320
  childX,
@@ -5921,6 +6443,7 @@ var GessoRuntime = class {
5921
6443
  this.frameBegan = now();
5922
6444
  this.phaseTimings = emptyPhaseTimings();
5923
6445
  this.frameTime = time;
6446
+ this.releaseWheel();
5924
6447
  this.phaseTimings.ticks = this.timePhase(() => this.visible && this.animations.isRunning, () => this.animations.advance(time));
5925
6448
  this.scheduleAnimationTick(time);
5926
6449
  this.phaseTimings.patches = this.timePhase(() => this.replicas.some((replica) => replica.hasPendingPatches), () => {
@@ -6092,10 +6615,18 @@ var GessoRuntime = class {
6092
6615
  scrollY: record?.scrollY ?? 0
6093
6616
  };
6094
6617
  };
6618
+ const readFlow = (node) => {
6619
+ const record = this.engine.recordFor(node);
6620
+ return {
6621
+ box: record === void 0 ? NO_BOX : this.engine.worldBox(node),
6622
+ scrollX: record?.scrollX ?? 0,
6623
+ scrollY: record?.scrollY ?? 0
6624
+ };
6625
+ };
6095
6626
  this.deferredReveals = [];
6096
6627
  try {
6097
6628
  for (;;) {
6098
- if (!this.layoutNotifier.isEmpty()) this.layoutNotifier.notify(read);
6629
+ if (!this.layoutNotifier.isEmpty()) this.layoutNotifier.notify(read, readFlow);
6099
6630
  if (!this.layoutPending()) {
6100
6631
  this.finishSettling(settled.passes > 0);
6101
6632
  if (!this.layoutPending()) break;
@@ -6144,6 +6675,39 @@ var GessoRuntime = class {
6144
6675
  this.warnedLayoutPasses = true;
6145
6676
  console.warn(`Gesso: a frame laid out ${MAX_LAYOUT_PASSES} times and its layout listeners were still changing it, so it was painted unsettled and the rest left for the next frame. Something on \`host.onLayout\` (a breakpoint, a sizeContainer, a Responsive, a modifier of your own) is writing a property that changes the box it is listening to, back and forth. Shown once.`);
6146
6677
  }
6678
+ /**
6679
+ * Holds each subtree that declares `holdUnseen` while it cannot be
6680
+ * read, and lets it go on the frame it can (see `UiGraph.hold`).
6681
+ *
6682
+ * Decided after the frame is drawn, from where it was drawn: a subtree
6683
+ * is only ever held once it has been laid out and painted, so what is
6684
+ * kept of it is what it showed, and one let go catches up on the next
6685
+ * frame, which its own marks arm. Only frames that moved or laid
6686
+ * something out can change the answer, so the rest pay nothing.
6687
+ */
6688
+ updateHolds(frame, laidOut) {
6689
+ let found = false;
6690
+ for (const [node, flags] of frame.entries()) if ((flags & DirtyFlags.Properties) !== 0 && node.properties.get("holdUnseen") !== void 0 && !this.holdable.has(node)) {
6691
+ this.holdable.add(node);
6692
+ found = true;
6693
+ }
6694
+ if (this.holdable.size === 0 || !laidOut && !found) return;
6695
+ for (const node of this.holdable) {
6696
+ const how = node.properties.get("holdUnseen");
6697
+ if (how === void 0 || this.graph.getNode(node.id) !== node) {
6698
+ this.graph.release(node);
6699
+ this.holdable.delete(node);
6700
+ continue;
6701
+ }
6702
+ const rec = this.engine.recordFor(node);
6703
+ if (rec === void 0 || !(rec.width > 0) || !(rec.height > 0)) continue;
6704
+ const box = this.engine.screenBox(node);
6705
+ const margin = how === true ? 0 : how.margin ?? 0;
6706
+ const below = how === true ? 0 : how.belowScale ?? 0;
6707
+ if (box.x + box.width > -margin && box.y + box.height > -margin && box.x < this.width + margin && box.y < this.height + margin && box.width / rec.width >= below) this.graph.release(node);
6708
+ else this.graph.hold(node);
6709
+ }
6710
+ }
6147
6711
  handleFrame(collected) {
6148
6712
  const root = this.root;
6149
6713
  if (root === void 0) return;
@@ -6175,11 +6739,13 @@ var GessoRuntime = class {
6175
6739
  overlay: overlay?.shapes,
6176
6740
  changes
6177
6741
  }));
6742
+ this.updateHolds(frame, laidOut);
6178
6743
  if (overlay !== null) {
6179
6744
  if (laidOut) this.sendInspection();
6180
6745
  this.scheduleInspectorRepaint(overlay.nextChange);
6181
6746
  }
6182
6747
  this.sendDevtoolsUpdates(frame);
6748
+ this.framesFailed = 0;
6183
6749
  const finished = now();
6184
6750
  const elapsed = finished - (this.frameBegan ?? started);
6185
6751
  this.frameBegan = null;
@@ -6522,6 +7088,14 @@ function semanticIndices(list) {
6522
7088
  * one frame still hands the mirror the rows it is about to show.
6523
7089
  */
6524
7090
  const SEMANTICS_VIEWPORT_MARGIN = 400;
7091
+ /**
7092
+ * How long a run of moving frames has to pause before the mirrored
7093
+ * boxes are swept again; see `movedSemanticsBoxes`. Longer than the gap
7094
+ * between a trackpad's wheel events, so a pan is one run.
7095
+ */
7096
+ const SEMANTICS_SETTLE_MS = 120;
7097
+ /** The longest the mirrored boxes go unswept while something keeps moving: an endless animation still gets them. */
7098
+ const SEMANTICS_MOST_STALE_MS = 1e3;
6525
7099
  const EMPTY_PATCHES = [];
6526
7100
  const EMPTY_BOXES = [];
6527
7101
  function boxesEqual(a, b) {
@@ -6536,6 +7110,19 @@ function boxesEqual(a, b) {
6536
7110
  function frameChangedTree(frame) {
6537
7111
  return frameNeedsSemantics(frame);
6538
7112
  }
7113
+ /** The box a node with no record reports. */
7114
+ const NO_BOX = {
7115
+ x: 0,
7116
+ y: 0,
7117
+ width: 0,
7118
+ height: 0
7119
+ };
7120
+ /** Whether two wheel steps can be added together: the same keys held, in the same unit. */
7121
+ function sameWheel(a, b) {
7122
+ const [, , , , keysA, unitA] = a;
7123
+ const [, , , , keysB, unitB] = b;
7124
+ return unitA === unitB && (keysA?.ctrl ?? false) === (keysB?.ctrl ?? false) && (keysA?.shift ?? false) === (keysB?.shift ?? false) && (keysA?.alt ?? false) === (keysB?.alt ?? false) && (keysA?.meta ?? false) === (keysB?.meta ?? false);
7125
+ }
6539
7126
  function frameNeedsSemantics(frame) {
6540
7127
  return frame.anyFlags(DirtyFlags.Semantics | DirtyFlags.Children);
6541
7128
  }
@@ -6752,25 +7339,58 @@ async function loadModule(module, load) {
6752
7339
  }
6753
7340
  /** The export to draw, or why none can be chosen. */
6754
7341
  function chooseExport(exports, wanted, module) {
6755
- const drawable = (value) => typeof value === "function" || isUiElement(value);
7342
+ const drawable = (value) => typeof value === "function" || isUiElement(value) || examplesOf(value) !== null;
6756
7343
  if (wanted !== void 0) {
6757
- if (!drawable(exports[wanted])) return `${module} has no component or element exported as ${wanted}. It exports ${list(exports, drawable)}.`;
7344
+ const [head, ...rest] = wanted.split(".");
7345
+ let value = exports[head];
7346
+ for (const key of rest) value = typeof value === "object" && value !== null ? value[key] : void 0;
7347
+ if (!drawable(value)) return `${module} has no component or element exported as ${wanted}. It exports ${list(exports, drawable)}.`;
7348
+ const examples = examplesOf(value);
6758
7349
  return {
6759
7350
  name: wanted,
6760
- value: exports[wanted]
7351
+ value: examples === null ? value : examplesSheet(examples)
6761
7352
  };
6762
7353
  }
7354
+ const asDrawn = (value) => {
7355
+ const examples = examplesOf(value);
7356
+ return examples === null ? value : examplesSheet(examples);
7357
+ };
6763
7358
  if (drawable(exports.default)) return {
6764
7359
  name: "default",
6765
- value: exports.default
7360
+ value: asDrawn(exports.default)
6766
7361
  };
6767
7362
  const candidates = Object.keys(exports).filter((name) => /^[A-Z]/.test(name) && drawable(exports[name]));
6768
7363
  if (candidates.length === 1) return {
6769
7364
  name: candidates[0],
6770
- value: exports[candidates[0]]
7365
+ value: asDrawn(exports[candidates[0]])
6771
7366
  };
6772
7367
  return candidates.length === 0 ? `${module} exports nothing to draw: no component, and no element.` : `${module} exports ${candidates.join(", ")}; say which with export.`;
6773
7368
  }
7369
+ /**
7370
+ * An object of examples, as a component's states are kept for a design
7371
+ * tool (`export const Button = { Primary: <Button … />, … }`): each
7372
+ * element by its name, or null for anything else.
7373
+ */
7374
+ function examplesOf(value) {
7375
+ if (typeof value !== "object" || value === null || isUiElement(value) || Array.isArray(value)) return null;
7376
+ const entries = Object.entries(value).filter((entry) => isUiElement(entry[1]));
7377
+ return entries.length === 0 ? null : entries;
7378
+ }
7379
+ /** Every example, each under its name, down the frame. */
7380
+ function examplesSheet(examples) {
7381
+ return Column({
7382
+ gap: 24,
7383
+ padding: 16
7384
+ }, ...examples.map(([name, element]) => Column({
7385
+ key: name,
7386
+ gap: 8
7387
+ }, Text({
7388
+ text: name,
7389
+ fontSize: 12,
7390
+ fontWeight: "600",
7391
+ color: "textMuted"
7392
+ }), element)));
7393
+ }
6774
7394
  function list(exports, keep) {
6775
7395
  const names = Object.keys(exports).filter((name) => keep(exports[name]));
6776
7396
  return names.length === 0 ? "nothing that can be drawn" : names.join(", ");
@@ -6790,6 +7410,6 @@ function failure(text) {
6790
7410
  };
6791
7411
  }
6792
7412
  //#endregion
6793
- export { modifiersFrom as A, Define as B, treeText as C, epochNow as D, epochFromEvent as E, OverlayService as F, bounds as G, Input as H, isClassComponent as I, ServiceRegistry as K, ChannelRegistry as L, ComponentHost as M, OverlayLayer as N, isDiscreteInputMessage as O, __decorate as P, ChannelReplica as R, AudioService as S, shellFilesUnsupported as T, Output as U, Inject as V, BoundsCell as W, buildPath as _, UI_FRAME_PHASES as a, parseUrl as b, AnimationService as c, MediaService as d, FocusService as f, RouterService as g, FindService as h, GessoRuntime as i, ComponentHostResolver as j, isInputMessage as k, TextService as l, EditingService as m, renderFrame as n, InspectionService as o, ScrollService as p, Component as q, isClaimedError as r, FrameService as s, renderModule as t, FontService as u, formatUrl as v, ShellService as w, RouteState as x, parsePattern as y, Channel as z };
7413
+ export { ComponentHostResolver as A, ChannelRegistry as B, ShellService as C, isDiscreteInputMessage as D, epochNow as E, FocusService as F, Input as G, Channel as H, boundFocus as I, BoundsCell as J, Output as K, OverlayScopeKey as L, StartingInputsKey as M, OverlayLayer as N, isInputMessage as O, __decorate as P, OverlayService as R, FrameService as S, epochFromEvent as T, Define as U, ChannelReplica as V, Inject as W, ServiceRegistry as X, bounds as Y, Component as Z, parsePattern as _, UI_FRAME_PHASES as a, AudioService as b, TextService as c, ScrollService as d, EditingService as f, formatUrl as g, buildPath as h, GessoRuntime as i, ComponentHost as j, modifiersFrom as k, FontService as l, RouterService as m, renderFrame as n, InspectionService as o, FindService as p, liveInput as q, isClaimedError as r, AnimationService as s, renderModule as t, MediaService as u, parseUrl as v, shellFilesUnsupported as w, treeText as x, RouteState as y, isClassComponent as z };
6794
7414
 
6795
- //# sourceMappingURL=render-cydQ10Nd.js.map
7415
+ //# sourceMappingURL=render-DlhL5y0d.js.map