gesso-framework 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { a as Each, i as show, n as getComponentMetadata, o as each, r as Show,
3
3
  import { c as channelSchema, l as describeChannel, t as AGENT_PORT } from "./remote-xxij8zXe.js";
4
4
  import { n as serveApplicationAgent } from "./app-BvlIO1G9.js";
5
5
  import { BehaviorSubject, EMPTY, Observable, Subject, Subscription, distinctUntilChanged, map } from "rxjs";
6
- import { AnimationDriver, Box, Canvas2DRenderer, CanvasPlatformSurface, CanvasTextMeasurer, Constraints, DefaultImageResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, MARK_PREFIX, Stack, TEXT_RUN_ID_SEPARATOR, UI_ROLES, UI_SEMANTIC_STATES, UiAnimationFrameClock, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiHostFrameClock, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiNodeType, UiPlatformAdapter, 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, capturePointer, createCanvasSurface, createTween, createWebGPUSurface, defaultMotion, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, easings, formatExplanation, insetsEqual, isComponentLikeElement, isObservable, isWebGPUAvailable, linear, linkOf, markInstant, markNow, measure, measureSpan, motion, noInsets, observeViewportInsets, percent, performanceMarksEnabled, pointerDeviceOf, prepareInputSurface, propertyEffects, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollRange, scrollbarThumb, semanticsInertAbove, semanticsMemory, setPerformanceMarks, setPerformanceMarks as setPerformanceMarks$1, textRunOfRecordId, themeTokens, touchActionFor, wheelDeltaYOf } from "gesso-core";
6
+ import { AnimationDriver, Box, Canvas2DRenderer, CanvasPlatformSurface, CanvasTextMeasurer, Constraints, DefaultImageResolver, DefaultVideoResolver, DirtyFlags, EnvironmentNotifier, FocusNotifier, IconRasterizer, LayoutEngine, LayoutInspector, LayoutNotifier, MARK_PREFIX, Stack, TEXT_RUN_ID_SEPARATOR, UI_ROLES, UI_SEMANTIC_STATES, UiAnimationFrameClock, UiEditingController, UiEnvironmentKeys, UiEventType, UiFindController, UiFocusManager, UiGestureRecognizer, UiGraph, UiGraphBuilder, UiHitTester, UiHostFrameClock, UiInputDispatcher, UiInsetRegistry, UiKeyboardController, UiNodeType, UiPlatformAdapter, 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, capturePointer, createCanvasSurface, createTween, createWebGPUSurface, defaultMotion, defineModifier, describeOverrides, diffSemantics, dragSessionFor, dropIndexShifts, easings, fade, formatExplanation, insetsEqual, isComponentLikeElement, isObservable, isWebGPUAvailable, linear, linkOf, markInstant, markNow, measure, measureSpan, motion, noInsets, observeViewportInsets, percent, performanceMarksEnabled, pointerDeviceOf, prepareInputSurface, propertyEffects, recordsEqual, registerFontStack, resolveCursor, rewalkSemantics, scrollRange, scrollbarThumb, semanticsInertAbove, semanticsMemory, setPerformanceMarks, setPerformanceMarks as setPerformanceMarks$1, textRunOfRecordId, themeTokens, touchActionFor, wheelDeltaYOf } from "gesso-core";
7
7
  //#region src/Component.ts
8
8
  /**
9
9
  * Base class for all Gesso framework components.
@@ -587,15 +587,28 @@ var ChannelReplica = class {
587
587
  get send() {
588
588
  return this.commandProxy;
589
589
  }
590
- viewProxy = new Proxy({}, { get: (_target, property) => {
591
- if (typeof property !== "string") return;
592
- const cell = this.cells.get(property);
593
- if (cell === void 0) {
594
- const names = [...this.cells.keys()].sort().join(", ");
595
- throw new Error(`'${property}' is not a view key on channel '${this.token.name}'. Declared keys: ${names.length > 0 ? names : "(none)"}.`);
590
+ viewProxy = new Proxy({}, {
591
+ get: (_target, property) => {
592
+ if (typeof property !== "string") return;
593
+ const cell = this.cells.get(property);
594
+ if (cell === void 0) {
595
+ const names = [...this.cells.keys()].sort().join(", ");
596
+ throw new Error(`'${property}' is not a view key on channel '${this.token.name}'. Declared keys: ${names.length > 0 ? names : "(none)"}.`);
597
+ }
598
+ return cell;
599
+ },
600
+ has: (_target, property) => typeof property === "string" && this.cells.has(property),
601
+ ownKeys: () => [...this.cells.keys()],
602
+ getOwnPropertyDescriptor: (_target, property) => {
603
+ const cell = typeof property === "string" ? this.cells.get(property) : void 0;
604
+ return cell === void 0 ? void 0 : {
605
+ value: cell,
606
+ enumerable: true,
607
+ configurable: true,
608
+ writable: false
609
+ };
596
610
  }
597
- return cell;
598
- } });
611
+ });
599
612
  createCommandProxy() {
600
613
  return new Proxy({}, { get: (_target, property) => {
601
614
  if (typeof property !== "string") return;
@@ -841,19 +854,28 @@ let OverlayLayer = class OverlayLayer extends Component {
841
854
  renderEntries(entries) {
842
855
  const elements = [];
843
856
  for (const entry of entries) {
844
- if (entry.dismissOnOutsidePress) elements.push(Box({
845
- key: `${entry.id}\0backdrop`,
846
- position: "absolute",
847
- inset: 0,
848
- zIndex: entry.zIndex,
849
- onPointerDown: () => this.overlays.close(entry.id),
850
- onWheel: () => this.overlays.close(entry.id)
851
- }));
857
+ if (entry.dismissOnOutsidePress === true || entry.modal === true) {
858
+ const dismiss = entry.dismissOnOutsidePress === true;
859
+ const environment = entry.environment ?? entry.anchor ?? null;
860
+ elements.push(Box({
861
+ key: `${entry.id}\0backdrop`,
862
+ position: "absolute",
863
+ inset: 0,
864
+ zIndex: entry.zIndex,
865
+ ...inheritedFrom(environment),
866
+ modifiers: followEnvironment(environment),
867
+ ...dismiss ? {
868
+ onPointerDown: () => this.overlays.close(entry.id),
869
+ onWheel: () => this.overlays.close(entry.id)
870
+ } : {}
871
+ }, ...entry.scrim ?? entry.modal === true ? [scrim()] : []));
872
+ }
852
873
  elements.push(Box({
853
874
  key: entry.id,
854
875
  position: "absolute",
855
876
  anchor: entry.anchor ?? void 0,
856
877
  anchorPoint: entry.anchor == null ? entry.point : void 0,
878
+ anchorRect: entry.anchor == null ? void 0 : entry.anchorRect,
857
879
  placement: entry.placement,
858
880
  anchorOffset: entry.offset,
859
881
  top: entry.top,
@@ -862,7 +884,8 @@ let OverlayLayer = class OverlayLayer extends Component {
862
884
  left: entry.left,
863
885
  zIndex: entry.zIndex,
864
886
  ...centering(entry),
865
- ...inheritedFrom(entry.environment ?? entry.anchor ?? null)
887
+ ...inheritedFrom(entry.environment ?? entry.anchor ?? null),
888
+ modifiers: followEnvironment(entry.environment ?? entry.anchor ?? null)
866
889
  }, entry.content));
867
890
  }
868
891
  return elements;
@@ -871,6 +894,28 @@ let OverlayLayer = class OverlayLayer extends Component {
871
894
  __decorate([Inject(OverlayService)], OverlayLayer.prototype, "overlays", void 0);
872
895
  OverlayLayer = __decorate([Define("gesso-overlay-layer")], OverlayLayer);
873
896
  /**
897
+ * The dim over the page, inside the backdrop rather than on it.
898
+ *
899
+ * A node at opacity 0 takes no presses, so a backdrop fading in would
900
+ * let a press through to the page on the frame it opens. The fade is
901
+ * this child's; the backdrop under it is solid to input from the start,
902
+ * and the child takes none of its own. It comes in at the pace a
903
+ * `Dialog` does, and under reduced motion it is simply there.
904
+ */
905
+ function scrim() {
906
+ return Box({
907
+ position: "absolute",
908
+ inset: 0,
909
+ hitTestable: false,
910
+ backgroundColor: "scrim",
911
+ modifiers: [motion({
912
+ initial: fade,
913
+ duration: "slow",
914
+ easing: "decelerate"
915
+ })]
916
+ });
917
+ }
918
+ /**
874
919
  * `center` as the layout engine takes it.
875
920
  *
876
921
  * Both edges of the axis are pinned, which stretches the entry's box
@@ -905,11 +950,7 @@ function centering(entry) {
905
950
  }
906
951
  /**
907
952
  * The scoped values an entry's content should keep, re-provided on the
908
- * box that holds it.
909
- *
910
- * Read once, when the entry opens. An overlay that outlives a theme
911
- * change re-opens; nothing here watches, because an entry's content is
912
- * built once too.
953
+ * box that holds it, as they are when the entry opens.
913
954
  */
914
955
  function inheritedFrom(node) {
915
956
  const environment = node?.environment;
@@ -917,9 +958,33 @@ function inheritedFrom(node) {
917
958
  return {
918
959
  theme: environment.get(UiEnvironmentKeys.theme),
919
960
  textStyle: environment.get(UiEnvironmentKeys.textStyle),
961
+ color: environment.get(UiEnvironmentKeys.color),
920
962
  contentColor: environment.get(UiEnvironmentKeys.contentColor)
921
963
  };
922
964
  }
965
+ /**
966
+ * The same values again whenever they change where the entry was
967
+ * declared, for as long as it is open.
968
+ *
969
+ * Reading them once let a dialog open while the page went dark (the
970
+ * system's setting changed, or the theme a person chose arrived from
971
+ * another worker a moment after they opened it) stay light over a dark
972
+ * page until it was closed.
973
+ */
974
+ const followEnvironmentKind = defineModifier({
975
+ name: "overlayEnvironment",
976
+ attach(host, from) {
977
+ host.onEnvironment(() => {
978
+ host.set("theme", host.environment(UiEnvironmentKeys.theme, from));
979
+ host.set("textStyle", host.environment(UiEnvironmentKeys.textStyle, from));
980
+ host.set("color", host.environment(UiEnvironmentKeys.color, from));
981
+ host.set("contentColor", host.environment(UiEnvironmentKeys.contentColor, from));
982
+ }, from);
983
+ }
984
+ });
985
+ function followEnvironment(node) {
986
+ return node === null ? [] : [followEnvironmentKind(node)];
987
+ }
923
988
  //#endregion
924
989
  //#region src/ComponentElement.ts
925
990
  function isComponentElement(value) {
@@ -1884,6 +1949,38 @@ var ShellService = class {
1884
1949
  });
1885
1950
  }
1886
1951
  /**
1952
+ * Replaces the page with `url`, leaving the application.
1953
+ *
1954
+ * Not the router's `navigate`, which moves between screens *inside*
1955
+ * the app and keeps it running: this unloads the page, and whatever
1956
+ * the app held in memory goes with it. It is for the places a page
1957
+ * has to be handed to somebody else's server and wait to be sent
1958
+ * back — an OAuth sign-in that goes to `/api/auth/login`, is
1959
+ * redirected to an identity provider and returns with a session, or
1960
+ * a sign-out at `/api/auth/logout` that clears a cookie only the
1961
+ * server can clear. It is also where a sign-in falls back to when
1962
+ * `openPopup` reports the popup blocked.
1963
+ *
1964
+ * A request rather than a call because `window.location` belongs to
1965
+ * the main thread and a render worker cannot reach it. A relative
1966
+ * url resolves against the page, so `/api/auth/login` means the
1967
+ * app's own server. The shell refuses anything that is not http,
1968
+ * https or the page's own scheme — a `javascript:` url, above all,
1969
+ * which would run in the page rather than go anywhere — so a url
1970
+ * built from data cannot be turned into script.
1971
+ *
1972
+ * Nothing comes back, and there is nothing to wait for: if the
1973
+ * redirect happens, the code that would have read the answer is
1974
+ * gone. Leaving the page is not cancellable from here either, so a
1975
+ * component with unsaved work should settle it before asking.
1976
+ */
1977
+ redirect(url) {
1978
+ this.handler?.({
1979
+ type: "redirect",
1980
+ url
1981
+ });
1982
+ }
1983
+ /**
1887
1984
  * Whether the application's surface is filling the screen.
1888
1985
  *
1889
1986
  * Read rather than assumed: the person can leave fullscreen with
@@ -1925,8 +2022,8 @@ var ShellService = class {
1925
2022
  * The answer is a promise, the one place in the framework where a
1926
2023
  * shell request has a reply, because there is nothing useful an
1927
2024
  * application can do with a popup it cannot see the fate of. A sign-in
1928
- * flow that is blocked falls back to a full-page redirect, and it can
1929
- * only choose that if it is told.
2025
+ * flow that is blocked falls back to a full-page redirect — `redirect`,
2026
+ * below — and it can only choose that if it is told.
1930
2027
  *
1931
2028
  * Two browser rules shape the contract and both were measured in
1932
2029
  * Chrome before this existed:
@@ -2463,6 +2560,89 @@ function browserFilesHost(view) {
2463
2560
  };
2464
2561
  }
2465
2562
  //#endregion
2563
+ //#region src/app/shellRedirect.ts
2564
+ /**
2565
+ * The shell's half of `ShellService.redirect`: the page is replaced
2566
+ * with a url a component asked for.
2567
+ *
2568
+ * Both hosts answer the request — `WorkerApp` when it arrives as a
2569
+ * message, `GessoApp` when the runtime hands it over on the same
2570
+ * thread — and both go through here, so the one rule that matters is
2571
+ * written once.
2572
+ *
2573
+ * That rule is the scheme. `location.assign('javascript:…')` does not
2574
+ * go anywhere; it runs the script in the page, with everything the
2575
+ * page can reach. A component builds the url it redirects to, often
2576
+ * from data — a `returnTo` from the address bar, a link from the
2577
+ * server — and a shell that assigned whatever it was given would make
2578
+ * every such url a way to run script. So a url is resolved against
2579
+ * the page first, the way the browser would resolve it, and only an
2580
+ * http or https result, or one in the page's own scheme, is followed.
2581
+ * The page's own scheme is there for a page that is not served over
2582
+ * http at all — an Electrobun window loads from `views://` — where a
2583
+ * relative url resolves to that scheme and is no less the app's own.
2584
+ */
2585
+ /** Schemes a redirect may always go to, whatever the page was served from. */
2586
+ const WEB_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:"]);
2587
+ /**
2588
+ * Schemes refused even when the page itself has one of them.
2589
+ *
2590
+ * A page served from `data:` or `blob:` is unusual but real — a
2591
+ * preview, a sandboxed frame — and "the page's own scheme" must not
2592
+ * then admit a `data:` url built from data, which is script by
2593
+ * another name.
2594
+ */
2595
+ const REFUSED_SCHEMES = /* @__PURE__ */ new Set([
2596
+ "javascript:",
2597
+ "data:",
2598
+ "blob:",
2599
+ "about:"
2600
+ ]);
2601
+ /**
2602
+ * The absolute url `url` names, resolved against `base` (the page's
2603
+ * address), or null when it is not one a redirect may follow.
2604
+ *
2605
+ * Null as well for a url that does not parse, or a base that does
2606
+ * not: a redirect nobody can resolve is refused rather than guessed.
2607
+ */
2608
+ function redirectTarget(url, base) {
2609
+ let page;
2610
+ let target;
2611
+ try {
2612
+ page = new URL(base);
2613
+ target = new URL(url, page);
2614
+ } catch {
2615
+ return null;
2616
+ }
2617
+ if (WEB_SCHEMES.has(target.protocol)) return target.href;
2618
+ if (target.protocol === page.protocol && !REFUSED_SCHEMES.has(target.protocol)) return target.href;
2619
+ return null;
2620
+ }
2621
+ /**
2622
+ * Follows a redirect, or refuses it, and says which.
2623
+ *
2624
+ * The url is checked before the handler is called as well as before
2625
+ * the default: a desktop host's `onRedirect` navigates a webview, and
2626
+ * a `javascript:` url is no safer there. The handler is given the
2627
+ * resolved, absolute url, so it never has to know what page the
2628
+ * request came from.
2629
+ *
2630
+ * A refusal is a warning on the console and nothing else. There is no
2631
+ * one to report it to — the request carries no answer — and throwing
2632
+ * on the main thread would take down a message handler over a url
2633
+ * that was simply ignored.
2634
+ */
2635
+ function redirectWith(handler, url, location) {
2636
+ const target = redirectTarget(url, location.href);
2637
+ if (target === null) {
2638
+ console.warn(`[gesso] ShellService.redirect refused ${JSON.stringify(url)}: only an http, https or relative url can replace the page.`);
2639
+ return false;
2640
+ }
2641
+ if (handler !== void 0) handler(target);
2642
+ else location.assign(target);
2643
+ return true;
2644
+ }
2645
+ //#endregion
2466
2646
  //#region src/app/EditingProxy.ts
2467
2647
  /** Composition-owned input types: the composition events carry these. */
2468
2648
  const COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
@@ -2667,7 +2847,7 @@ var EditingProxy = class {
2667
2847
  * the reason the field's semantics live here rather than on a second
2668
2848
  * element beside it.
2669
2849
  */
2670
- describe(record, activeDescendant) {
2850
+ describe(record, activeDescendant, controls) {
2671
2851
  if (this.disposed) return;
2672
2852
  const textarea = this.textarea;
2673
2853
  if (record === null) {
@@ -2681,7 +2861,9 @@ var EditingProxy = class {
2681
2861
  "aria-readonly",
2682
2862
  "aria-disabled",
2683
2863
  "aria-expanded",
2684
- "aria-activedescendant"
2864
+ "aria-activedescendant",
2865
+ "aria-controls",
2866
+ "aria-autocomplete"
2685
2867
  ]) textarea.removeAttribute(attribute);
2686
2868
  return;
2687
2869
  }
@@ -2696,6 +2878,8 @@ var EditingProxy = class {
2696
2878
  setOrClear(textarea, "aria-disabled", record.disabled === true ? "true" : void 0);
2697
2879
  setOrClear(textarea, "aria-expanded", states.has("expanded") ? "true" : states.has("collapsed") ? "false" : void 0);
2698
2880
  setOrClear(textarea, "aria-activedescendant", activeDescendant);
2881
+ setOrClear(textarea, "aria-controls", controls);
2882
+ setOrClear(textarea, "aria-autocomplete", record.autocomplete);
2699
2883
  }
2700
2884
  dispose() {
2701
2885
  if (this.disposed) return;
@@ -2935,7 +3119,9 @@ const RECORD_ATTRIBUTES = [
2935
3119
  "aria-modal",
2936
3120
  "aria-multiselectable",
2937
3121
  "aria-current",
2938
- "aria-activedescendant"
3122
+ "aria-activedescendant",
3123
+ "aria-controls",
3124
+ "aria-autocomplete"
2939
3125
  ];
2940
3126
  let mirrors = 0;
2941
3127
  /**
@@ -3098,10 +3284,11 @@ var SemanticsMirror = class {
3098
3284
  if (existing?.box !== void 0) this.position(existing);
3099
3285
  if (record.id === this.focusedId) this.describeEditing(this.editing?.active === true ? record : null);
3100
3286
  }
3101
- /** Hands the proxy the focused editable's record, with its active descendant's DOM id. */
3287
+ /** Hands the proxy the focused editable's record, with the DOM ids of its active descendant and what it controls. */
3102
3288
  describeEditing(record) {
3103
3289
  const active = record?.activeDescendant;
3104
- this.editing?.describe(record, active === void 0 ? void 0 : this.idPrefix + active);
3290
+ const controls = record?.controls;
3291
+ this.editing?.describe(record, active === void 0 ? void 0 : this.idPrefix + active, controls === void 0 ? void 0 : this.idPrefix + controls);
3105
3292
  }
3106
3293
  createElement() {
3107
3294
  const element = this.doc.createElement("div");
@@ -3148,6 +3335,8 @@ var SemanticsMirror = class {
3148
3335
  setNumber(element, "aria-level", record.level);
3149
3336
  if (record.valueText !== void 0) element.setAttribute("aria-valuetext", record.valueText);
3150
3337
  if (record.activeDescendant !== void 0) element.setAttribute("aria-activedescendant", this.idPrefix + record.activeDescendant);
3338
+ if (record.controls !== void 0) element.setAttribute("aria-controls", this.idPrefix + record.controls);
3339
+ if (record.autocomplete !== void 0) element.setAttribute("aria-autocomplete", record.autocomplete);
3151
3340
  }
3152
3341
  /**
3153
3342
  * Puts an element under its parent, at the index the record gives.
@@ -4359,6 +4548,10 @@ var WorkerApp = class {
4359
4548
  openUrlWith(this.options.onOpenUrl, message.url);
4360
4549
  return;
4361
4550
  }
4551
+ if (message.type === "redirect") {
4552
+ redirectWith(this.options.onRedirect, message.url, window.location);
4553
+ return;
4554
+ }
4362
4555
  if (message.type === "fullscreen") {
4363
4556
  if (this.canvas !== void 0) setElementFullscreen(this.canvas, message.enter);
4364
4557
  return;
@@ -5551,8 +5744,8 @@ var RouterService = class {
5551
5744
  const settled = match?.url ?? normalize$1(url);
5552
5745
  if (options.replace === true) this.history?.replace(settled);
5553
5746
  else if (options.push) this.history?.push(settled);
5554
- this.url.value = settled;
5555
5747
  this.match.value = match;
5748
+ this.url.value = settled;
5556
5749
  }
5557
5750
  /** First declared route whose pattern describes this url wins. */
5558
5751
  resolve(url) {
@@ -5770,14 +5963,19 @@ var EditingService = class {
5770
5963
  * scroll already applied: a popup placed under the caret adds the
5771
5964
  * node's position on screen and nothing else.
5772
5965
  *
5966
+ * `offset` asks about a caret at another character than the one the
5967
+ * caret is at: the start of the word being completed, so its list
5968
+ * stays put as the word grows. With an overlay's `anchorRect` and the
5969
+ * field as its anchor, the list follows the field from then on.
5970
+ *
5773
5971
  * Null when the node is not an editable, when it has not been laid
5774
5972
  * out yet, or before the runtime has wired the controller — all
5775
5973
  * three of which are "ask again next frame" rather than errors, and
5776
5974
  * all three of which happen during the frame a field first appears.
5777
5975
  */
5778
- caretRectOf(node) {
5976
+ caretRectOf(node, offset) {
5779
5977
  if (node === null || this.controller === null) return null;
5780
- return this.controller.caretRectOf(node);
5978
+ return this.controller.caretRectOf(node, offset);
5781
5979
  }
5782
5980
  /**
5783
5981
  * Selects from `anchor` to `focus` and focuses the field the focus is
@@ -6989,7 +7187,7 @@ var GessoRuntime = class {
6989
7187
  onChange: (node, listener) => this.environmentNotifier.add(node, listener)
6990
7188
  },
6991
7189
  layout: {
6992
- box: (node) => this.engine.recordFor(node) === void 0 ? null : this.engine.visibleBox(node),
7190
+ box: (node) => this.engine.recordFor(node) === void 0 ? null : this.engine.screenBox(node),
6993
7191
  flowBox: (node) => this.engine.recordFor(node) === void 0 ? null : this.engine.worldBox(node),
6994
7192
  scroll: (node) => {
6995
7193
  const record = this.engine.recordFor(node);
@@ -8067,6 +8265,7 @@ var GessoRuntime = class {
8067
8265
  const editing = new UiEditingController({
8068
8266
  recordFor: (node) => this.engine.recordFor(node),
8069
8267
  visibleBox: (node) => this.engine.visibleBox(node),
8268
+ screenBox: (node, part) => this.engine.screenBox(node, part),
8070
8269
  toLocal: (node, x, y) => hitTester.toLocal(node, x, y),
8071
8270
  measurer: this.textMeasurer,
8072
8271
  markDirty: (node, flags) => this.graph.markDirty(node, flags),
@@ -8076,6 +8275,7 @@ var GessoRuntime = class {
8076
8275
  const selection = new UiSelectionController({
8077
8276
  recordFor: (node) => this.engine.recordFor(node),
8078
8277
  visibleBox: (node) => this.engine.visibleBox(node),
8278
+ screenBox: (node) => this.engine.screenBox(node),
8079
8279
  measurer: this.textMeasurer,
8080
8280
  markDirty: (node, flags) => this.graph.markDirty(node, flags),
8081
8281
  root: () => this.layoutRoot(),
@@ -8124,7 +8324,7 @@ var GessoRuntime = class {
8124
8324
  selection,
8125
8325
  find,
8126
8326
  activation: { clickAt: (node) => {
8127
- const box = this.engine.visibleBox(node);
8327
+ const box = this.engine.screenBox(node);
8128
8328
  return {
8129
8329
  x: box.x + box.width / 2,
8130
8330
  y: box.y + box.height / 2
@@ -8266,7 +8466,7 @@ var GessoRuntime = class {
8266
8466
  return;
8267
8467
  }
8268
8468
  this.focusManager.focus(node);
8269
- const box = this.engine.visibleBox(node);
8469
+ const box = this.engine.screenBox(node);
8270
8470
  this.dispatcher.dispatch(new UiPointerEvent(UiEventType.Click, box.x + box.width / 2, box.y + box.height / 2, 1), node);
8271
8471
  }
8272
8472
  /**
@@ -8639,7 +8839,9 @@ var GessoRuntime = class {
8639
8839
  * goes down from the root carrying the scroll and sticky offsets that
8640
8840
  * `visibleBox` adds up per node, and passes over any subtree whose
8641
8841
  * bounds (the hit tester's, see `subtreeBoundsFor`) are off screen.
8642
- * A node it passes over is one this skipped before anyway.
8842
+ * A node it passes over is one this skipped before anyway. Below a
8843
+ * `transform` the walk asks `screenBox` instead, which carries the
8844
+ * box through it.
8643
8845
  */
8644
8846
  collectSemanticsBoxes() {
8645
8847
  const changed = [];
@@ -8660,14 +8862,25 @@ var GessoRuntime = class {
8660
8862
  const stack = [[
8661
8863
  root,
8662
8864
  0,
8663
- 0
8865
+ 0,
8866
+ false
8664
8867
  ]];
8665
8868
  while (stack.length > 0) {
8666
- const [node, dx, dy] = stack.pop();
8869
+ const [node, dx, dy, inherited] = stack.pop();
8667
8870
  const rec = this.engine.recordFor(node);
8668
8871
  let childX = dx;
8669
8872
  let childY = dy;
8670
- if (rec !== void 0) {
8873
+ const own = node.properties.get("transform");
8874
+ const transformed = inherited || typeof own === "object" && own !== null;
8875
+ if (rec !== void 0 && inherited) {
8876
+ if (this.semantics.has(node.id)) {
8877
+ const box = this.engine.screenBox(node);
8878
+ if (node === focused) {
8879
+ focusedSeen = true;
8880
+ offer(node.id, box);
8881
+ } else if (this.onScreen(box)) offer(node.id, box);
8882
+ }
8883
+ } else if (rec !== void 0) {
8671
8884
  const x = dx + rec.stickyOffsetX;
8672
8885
  const y = dy + rec.stickyOffsetY;
8673
8886
  if (!rec.boundsUnbounded && node !== focused && !this.onScreen({
@@ -8694,12 +8907,13 @@ var GessoRuntime = class {
8694
8907
  for (let child = node.lastChild; child !== null; child = child.previousSibling) stack.push([
8695
8908
  child,
8696
8909
  childX,
8697
- childY
8910
+ childY,
8911
+ transformed
8698
8912
  ]);
8699
8913
  }
8700
8914
  }
8701
8915
  if (focused !== null && focused !== void 0 && !focusedSeen && this.semantics.has(focused.id)) {
8702
- if (this.engine.recordFor(focused) !== void 0) offer(focused.id, this.engine.visibleBox(focused));
8916
+ if (this.engine.recordFor(focused) !== void 0) offer(focused.id, this.engine.screenBox(focused));
8703
8917
  }
8704
8918
  if (this.semanticsBoxes.size > this.semantics.size) {
8705
8919
  for (const id of this.semanticsBoxes.keys()) if (!this.semantics.has(id)) this.semanticsBoxes.delete(id);
@@ -8965,7 +9179,7 @@ var GessoRuntime = class {
8965
9179
  const read = (node) => {
8966
9180
  const record = this.engine.recordFor(node);
8967
9181
  return {
8968
- box: this.engine.visibleBox(node),
9182
+ box: this.engine.screenBox(node),
8969
9183
  scrollX: record?.scrollX ?? 0,
8970
9184
  scrollY: record?.scrollY ?? 0
8971
9185
  };
@@ -9811,6 +10025,10 @@ var GessoApp = class {
9811
10025
  this.runtime.settlePopup(request.id, opened);
9812
10026
  return;
9813
10027
  }
10028
+ if (request.type === "redirect") {
10029
+ if (view !== null) redirectWith(void 0, request.url, view.location);
10030
+ return;
10031
+ }
9814
10032
  view?.open(request.url, "_blank", "noopener,noreferrer");
9815
10033
  }
9816
10034
  observeResize() {
@@ -11119,6 +11337,10 @@ var RenderWorkerApp = class {
11119
11337
  type: "openUrl",
11120
11338
  url: request.url
11121
11339
  });
11340
+ else if (request.type === "redirect") this.host.postMessage({
11341
+ type: "redirect",
11342
+ url: request.url
11343
+ });
11122
11344
  else if (request.type === "fullscreen") this.host.postMessage({
11123
11345
  type: "fullscreen",
11124
11346
  enter: request.enter