@swiftbrowser/react 0.23.0 → 0.26.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
@@ -1,13 +1,13 @@
1
- import { createContext, useContext, createElement, useSyncExternalStore, useRef, useLayoutEffect, useState, Children, useEffect, isValidElement, Fragment, useId, cloneElement, useCallback } from "react";
1
+ import { createContext, useContext, createElement, useSyncExternalStore, useLayoutEffect, useRef, Fragment, useState, Children, useEffect, isValidElement, useId, cloneElement, useCallback } from "react";
2
2
  import { DefaultEventPriority, ConcurrentRoot } from "react-reconciler/constants";
3
3
  import Reconciler from "react-reconciler";
4
+ import { jsx, jsxs, Fragment as Fragment$1 } from "react/jsx-runtime";
4
5
  import { horizontalSizeClassFor } from "@swiftbrowser/web/src/protocol";
5
6
  import { BrowserHistory } from "@swiftbrowser/web/src/browserHistory";
6
7
  import { watchKeyboard } from "@swiftbrowser/web/src/deviceMode";
7
8
  import { browserEnvironment, exportFiles, loadableURL, performHostRequest } from "@swiftbrowser/web/src/hostRequests";
8
9
  import { Renderer } from "@swiftbrowser/web/src/renderer";
9
10
  import { currentScenePhase, watchScenePhase } from "@swiftbrowser/web/src/scenePhase";
10
- import { jsx, jsxs, Fragment as Fragment$1 } from "react/jsx-runtime";
11
11
  import { applyTypeSizeVariables } from "@swiftbrowser/web/src/typography";
12
12
  import { configureSignInWithApple } from "@swiftbrowser/web/src/appleSignIn";
13
13
  import { fileURL } from "@swiftbrowser/web/src/files";
@@ -207,10 +207,12 @@ function inheritTitleModes(screens, effective) {
207
207
  }
208
208
  }
209
209
  class HostTree {
210
- constructor(sink) {
210
+ constructor(sink, options = {}) {
211
211
  this.sink = sink;
212
+ this.options = options;
212
213
  }
213
214
  sink;
215
+ options;
214
216
  root = { id: 0, kind: "root", children: [], committedChildren: [] };
215
217
  nextId = 1;
216
218
  /** Emitted elements by id, for event dispatch. */
@@ -225,6 +227,8 @@ class HostTree {
225
227
  completions = /* @__PURE__ */ new Map();
226
228
  /** The root's `colorScheme` last sent (`.preferredColorScheme`); undefined while none was. */
227
229
  committedColorScheme;
230
+ /** Remote: the root's props last sent. */
231
+ committedRootProps = {};
228
232
  createInstance(props) {
229
233
  return {
230
234
  id: this.nextId++,
@@ -273,7 +277,7 @@ class HostTree {
273
277
  const visit = (child, sink, ctx) => {
274
278
  if (child.hidden) return;
275
279
  if (child.kind === PREF_KIND) {
276
- const target = child.target === "root" || child.target === void 0 ? this.root : ctx[child.target];
280
+ const target = child.target === "root" || child.target === void 0 ? this.root : ctx[child.target] ?? (this.options.remote ? this.root : void 0);
277
281
  const entry2 = target ? effective.get(target) : void 0;
278
282
  if (entry2) Object.assign(entry2.props, child.props);
279
283
  return;
@@ -287,8 +291,8 @@ class HostTree {
287
291
  const entry = { props: { ...child.props }, children: [], hoisted: [] };
288
292
  effective.set(child, entry);
289
293
  sink.push(child);
290
- const next2 = child.kind === "navscreen" ? { ...ctx, navscreen: child } : child.kind === "sheet" ? { ...ctx, sheet: child } : ctx;
291
- for (const grandchild of child.children) visit(grandchild, entry.children, next2);
294
+ const next = child.kind === "navscreen" ? { ...ctx, navscreen: child } : child.kind === "sheet" ? { ...ctx, sheet: child } : ctx;
295
+ for (const grandchild of child.children) visit(grandchild, entry.children, next);
292
296
  };
293
297
  for (const child of this.root.children) visit(child, rootEffective.children, {});
294
298
  for (const entry of effective.values()) {
@@ -347,13 +351,23 @@ class HostTree {
347
351
  for (const id of this.retired) ops.push({ op: "remove", id });
348
352
  this.retired = [];
349
353
  this.committedLive = live;
350
- const scheme = rootEffective.props["colorScheme"];
351
- const next = scheme === void 0 ? this.committedColorScheme === void 0 ? void 0 : null : scheme;
352
- if (next !== void 0 && next !== this.committedColorScheme) {
353
- ops.push({ op: "update", id: 0, props: { colorScheme: next } });
354
- this.committedColorScheme = next === null ? void 0 : next;
354
+ if (this.options.remote) {
355
+ if (!deepEqual(rootEffective.props, this.committedRootProps)) {
356
+ ops.push({ op: "update", id: 0, props: rootEffective.props });
357
+ this.committedRootProps = rootEffective.props;
358
+ }
359
+ } else {
360
+ const scheme = rootEffective.props["colorScheme"];
361
+ const next = scheme === void 0 ? this.committedColorScheme === void 0 ? void 0 : null : scheme;
362
+ if (next !== void 0 && next !== this.committedColorScheme) {
363
+ ops.push({ op: "update", id: 0, props: { colorScheme: next } });
364
+ this.committedColorScheme = next === null ? void 0 : next;
365
+ }
366
+ }
367
+ if (ops.length === 0) {
368
+ if (completion?.whenNothingDrawn) completion.callback();
369
+ return ops;
355
370
  }
356
- if (ops.length === 0) return ops;
357
371
  const commit = { op: "commit" };
358
372
  if (animation) commit.animation = animation;
359
373
  if (completion) {
@@ -382,7 +396,7 @@ class HostTree {
382
396
  }
383
397
  if (!("id" in event)) return;
384
398
  const instance = this.live.get(event.id);
385
- const handler = instance?.on?.[event.type];
399
+ const handler = instance?.on?.[event.type] ?? instance?.on?.["*"];
386
400
  handler?.(event);
387
401
  }
388
402
  }
@@ -402,8 +416,9 @@ function deepEqual(a, b) {
402
416
  }
403
417
  const DEFAULT_ENVIRONMENT = { colorScheme: "light", dynamicTypeSize: "large", horizontalSizeClass: "compact", scenePhase: "active" };
404
418
  const roots = /* @__PURE__ */ new Set();
405
- function createSwiftUIRoot(sink) {
406
- const tree = new HostTree(sink);
419
+ const HostTreeContext = createContext(null);
420
+ function createSwiftUIRoot(sink, options = {}) {
421
+ const tree = new HostTree(sink, options);
407
422
  roots.add(tree);
408
423
  const container = reconciler.createContainer(
409
424
  tree,
@@ -424,7 +439,7 @@ function createSwiftUIRoot(sink) {
424
439
  let environment = DEFAULT_ENVIRONMENT;
425
440
  let current = null;
426
441
  const update = () => {
427
- reconciler.updateContainerSync(createElement(EnvironmentProvider, { value: environment }, current), container, null, null);
442
+ reconciler.updateContainerSync(createElement(HostTreeContext.Provider, { value: tree }, createElement(EnvironmentProvider, { value: environment }, current)), container, null, null);
428
443
  reconciler.flushSyncWork();
429
444
  };
430
445
  return {
@@ -450,6 +465,13 @@ function injectIntoDevTools() {
450
465
  return reconciler.injectIntoDevTools() === true;
451
466
  }
452
467
  let nextCompletionSerial = 1;
468
+ function stampNextCommit(tree, animation, completion) {
469
+ tree.pendingAnimation = animation;
470
+ if (!completion) return 0;
471
+ const serial = nextCompletionSerial++;
472
+ tree.pendingCompletion = { serial, criteria: completion.criteria, callback: completion.callback, whenNothingDrawn: true };
473
+ return serial;
474
+ }
453
475
  function withAnimation(first, second, third, fourth) {
454
476
  const animation = typeof first === "function" ? { type: "default" } : first ?? { type: "default" };
455
477
  let body;
@@ -482,6 +504,258 @@ function withAnimation(first, second, third, fourth) {
482
504
  if (completion && !committed) completion();
483
505
  }
484
506
  }
507
+ function createRemoteRoot() {
508
+ const root = { id: 0, kind: "root", props: {}, children: [], parent: null };
509
+ const nodes = /* @__PURE__ */ new Map([[0, root]]);
510
+ const handlers = /* @__PURE__ */ new Map();
511
+ const listeners = /* @__PURE__ */ new Set();
512
+ let version = 0;
513
+ let rootProps = {};
514
+ let eventHandler;
515
+ let environmentHandler;
516
+ let lastEnvironment;
517
+ let hostTree = null;
518
+ const detach2 = (node) => {
519
+ if (!node.parent) return;
520
+ const siblings = node.parent.children;
521
+ const at = siblings.indexOf(node);
522
+ if (at >= 0) siblings.splice(at, 1);
523
+ node.parent = null;
524
+ };
525
+ const forget = (node) => {
526
+ nodes.delete(node.id);
527
+ handlers.delete(node.id);
528
+ for (const child of node.children) forget(child);
529
+ };
530
+ const notify2 = () => {
531
+ version += 1;
532
+ for (const listener of listeners) listener();
533
+ };
534
+ const remote = {
535
+ applyOps(ops) {
536
+ let commit = null;
537
+ for (const op of ops) {
538
+ switch (op.op) {
539
+ case "create": {
540
+ if (nodes.has(op.id)) throw new Error(`remote tree: element ${op.id} created twice`);
541
+ nodes.set(op.id, { id: op.id, kind: op.kind, props: op.props, children: [], parent: null });
542
+ break;
543
+ }
544
+ case "update": {
545
+ if (op.id === 0) {
546
+ rootProps = op.props;
547
+ break;
548
+ }
549
+ const node = nodes.get(op.id);
550
+ if (!node) throw new Error(`remote tree: update of unknown element ${op.id}`);
551
+ node.props = op.props;
552
+ break;
553
+ }
554
+ case "insert": {
555
+ const node = nodes.get(op.id);
556
+ const parent = nodes.get(op.parent);
557
+ if (!node || !parent) throw new Error(`remote tree: insert of ${op.id} into ${op.parent}: unknown element`);
558
+ detach2(node);
559
+ parent.children.splice(op.index, 0, node);
560
+ node.parent = parent;
561
+ break;
562
+ }
563
+ case "remove": {
564
+ const node = nodes.get(op.id);
565
+ if (!node) break;
566
+ detach2(node);
567
+ forget(node);
568
+ break;
569
+ }
570
+ case "commit":
571
+ commit = op;
572
+ break;
573
+ }
574
+ }
575
+ if (!commit) return;
576
+ const { animation, completion, completionCriteria } = commit;
577
+ const answer = completion === void 0 ? void 0 : () => remote.dispatch({ type: "animationcomplete", serial: completion });
578
+ if (animation && hostTree) {
579
+ stampNextCommit(hostTree, animation, answer && { criteria: completionCriteria ?? "logicallyComplete", callback: answer });
580
+ notify2();
581
+ return;
582
+ }
583
+ notify2();
584
+ answer?.();
585
+ },
586
+ node(id) {
587
+ return nodes.get(id);
588
+ },
589
+ onEvent(handler) {
590
+ eventHandler = handler;
591
+ },
592
+ onEnvironment(handler) {
593
+ environmentHandler = handler;
594
+ if (lastEnvironment) handler(lastEnvironment);
595
+ },
596
+ dispatch(event) {
597
+ eventHandler?.(event);
598
+ },
599
+ setEnvironment(environment) {
600
+ if (lastEnvironment && lastEnvironment.colorScheme === environment.colorScheme && lastEnvironment.dynamicTypeSize === environment.dynamicTypeSize && lastEnvironment.horizontalSizeClass === environment.horizontalSizeClass && lastEnvironment.scenePhase === environment.scenePhase)
601
+ return;
602
+ lastEnvironment = environment;
603
+ environmentHandler?.(environment);
604
+ },
605
+ subscribe(listener) {
606
+ listeners.add(listener);
607
+ return () => {
608
+ listeners.delete(listener);
609
+ };
610
+ },
611
+ getVersion: () => version,
612
+ get root() {
613
+ return root;
614
+ },
615
+ get rootProps() {
616
+ return rootProps;
617
+ },
618
+ attach(tree) {
619
+ hostTree = tree;
620
+ },
621
+ handlersFor(id) {
622
+ let on = handlers.get(id);
623
+ if (!on) {
624
+ on = { "*": (event) => remote.dispatch({ ...event, id }) };
625
+ handlers.set(id, on);
626
+ }
627
+ return on;
628
+ },
629
+ dispose() {
630
+ root.children = [];
631
+ nodes.clear();
632
+ nodes.set(0, root);
633
+ handlers.clear();
634
+ listeners.clear();
635
+ eventHandler = void 0;
636
+ environmentHandler = void 0;
637
+ hostTree = null;
638
+ }
639
+ };
640
+ return remote;
641
+ }
642
+ function sb(kind, props, on, children, target) {
643
+ const hostProps = { kind };
644
+ if (props) hostProps.props = props;
645
+ if (on) hostProps.on = on;
646
+ if (target) hostProps.target = target;
647
+ return children === void 0 ? createElement("sb", hostProps) : createElement("sb", hostProps, children);
648
+ }
649
+ const NavigationContext = createContext(null);
650
+ const ScreenDepthContext = createContext(0);
651
+ function useScreenDepth() {
652
+ return useContext(ScreenDepthContext);
653
+ }
654
+ function useNavigation() {
655
+ return useContext(NavigationContext);
656
+ }
657
+ function RegisterDestination({ destination }) {
658
+ const navigation = useContext(NavigationContext);
659
+ navigation?.registerDestination(destination);
660
+ return null;
661
+ }
662
+ const SCREEN_PROPS = ["title", "displayMode", "toolbarRole", "navigationBarHidden", "bottomBarHidden", "statusBarHidden"];
663
+ function screenPrefs(props, key) {
664
+ const picked = {};
665
+ for (const name of SCREEN_PROPS) if (name in props) picked[name] = props[name];
666
+ if (picked["title"] === "") delete picked["title"];
667
+ if (picked["displayMode"] === "automatic") delete picked["displayMode"];
668
+ return Object.keys(picked).length ? createElement(Fragment, { key }, sb(PREF_KIND, picked, void 0, void 0, "navscreen")) : null;
669
+ }
670
+ function findStack(node) {
671
+ for (const child of node.children) {
672
+ if (child.kind === "sheet" || child.kind === "alert") continue;
673
+ if (child.kind === "navstack") return child;
674
+ const found = findStack(child);
675
+ if (found) return found;
676
+ }
677
+ return null;
678
+ }
679
+ const screensOf = (stack) => stack ? stack.children.filter((child) => child.kind === "navscreen") : [];
680
+ function renderNode(root, node, stack) {
681
+ if (node.kind === "host") return null;
682
+ if (node === stack) {
683
+ const first = screensOf(stack)[0];
684
+ return createElement(Fragment, { key: node.id }, first ? renderScreen(root, first, stack) : null);
685
+ }
686
+ const children = node.children.length ? node.children.map((child) => renderNode(root, child, stack)) : void 0;
687
+ return createElement(Fragment, { key: node.id }, sb(node.kind, node.props, root.handlersFor(node.id), children));
688
+ }
689
+ function renderScreen(root, screen, stack) {
690
+ const out = [screenPrefs(screen.props, "$screen")];
691
+ for (const child of screen.children) {
692
+ const drawn = renderNode(root, child, stack);
693
+ if (child.kind === "toolbar" || child.kind === "searchfield") out.push(createElement(Fragment, { key: child.id }, sb(HOIST_KIND, void 0, void 0, drawn, "navscreen")));
694
+ else out.push(drawn);
695
+ }
696
+ return createElement(Fragment, null, ...out);
697
+ }
698
+ function RemoteScreen({ root, id }) {
699
+ useSyncExternalStore(root.subscribe, root.getVersion, root.getVersion);
700
+ const screen = root.node(id);
701
+ if (!screen?.parent) return null;
702
+ return renderScreen(root, screen, findStack(root.root));
703
+ }
704
+ function RemoteTree({ root }) {
705
+ useSyncExternalStore(root.subscribe, root.getVersion, root.getVersion);
706
+ const environment = useContext(EnvironmentContext);
707
+ const tree = useContext(HostTreeContext);
708
+ const navigation = useNavigation();
709
+ const depth = useScreenDepth();
710
+ useLayoutEffect(() => {
711
+ root.attach(tree);
712
+ return () => root.attach(null);
713
+ }, [root, tree]);
714
+ useLayoutEffect(() => {
715
+ root.setEnvironment(environment);
716
+ }, [root, environment]);
717
+ const stack = navigation ? findStack(root.root) : null;
718
+ const pushed = screensOf(stack).slice(1);
719
+ const values = useRef(/* @__PURE__ */ new Map());
720
+ const ours = useRef(/* @__PURE__ */ new WeakSet());
721
+ const lastPushed = useRef([]);
722
+ const valueFor = (id) => {
723
+ let value = values.current.get(id);
724
+ if (!value) {
725
+ value = { view: /* @__PURE__ */ jsx(RemoteScreen, { root, id }) };
726
+ values.current.set(id, value);
727
+ ours.current.add(value);
728
+ }
729
+ return value;
730
+ };
731
+ useLayoutEffect(() => {
732
+ if (!navigation) return;
733
+ const ids = pushed.map((screen) => screen.id);
734
+ const wanted = ids.map(valueFor);
735
+ for (const id of values.current.keys()) if (!ids.includes(id)) values.current.delete(id);
736
+ const tail = navigation.path.slice(depth);
737
+ let shown = 0;
738
+ while (shown < tail.length && typeof tail[shown] === "object" && tail[shown] !== null && ours.current.has(tail[shown])) shown += 1;
739
+ const childMoved = ids.length !== lastPushed.current.length || ids.some((id, i) => id !== lastPushed.current[i]);
740
+ lastPushed.current = ids;
741
+ if (!childMoved && shown < wanted.length && tail.slice(0, shown).every((value, i) => value === wanted[i])) {
742
+ root.dispatch({ type: "tap", id: pushed[shown].id });
743
+ return;
744
+ }
745
+ if (shown === wanted.length && wanted.every((value, i) => tail[i] === value)) return;
746
+ navigation.setPath([...navigation.path.slice(0, depth), ...wanted]);
747
+ });
748
+ const out = [screenPrefs(root.rootProps, "$root")];
749
+ if ("colorScheme" in root.rootProps)
750
+ out.push(createElement(Fragment, { key: "$scheme" }, sb(PREF_KIND, { colorScheme: root.rootProps["colorScheme"] }, void 0, void 0, "root")));
751
+ for (const child of root.root.children) {
752
+ const drawn = renderNode(root, child, stack);
753
+ if (child.kind === "sheet" || child.kind === "alert") out.push(createElement(Fragment, { key: child.id }, sb(HOIST_KIND, void 0, void 0, drawn, "root")));
754
+ else if (child.kind === "toolbar" || child.kind === "searchfield") out.push(createElement(Fragment, { key: child.id }, sb(HOIST_KIND, void 0, void 0, drawn, "navscreen")));
755
+ else out.push(drawn);
756
+ }
757
+ return createElement(Fragment, null, ...out);
758
+ }
485
759
  const HIERARCHY = { primary: 1, secondary: 2, tertiary: 3, quaternary: 4 };
486
760
  function rgb(r2, g, b, a = 1) {
487
761
  return { r: r2, g, b, a };
@@ -713,15 +987,6 @@ function mount(screen, element, options = {}) {
713
987
  };
714
988
  return mounted;
715
989
  }
716
- const NavigationContext = createContext(null);
717
- function useNavigation() {
718
- return useContext(NavigationContext);
719
- }
720
- function RegisterDestination({ destination }) {
721
- const navigation = useContext(NavigationContext);
722
- navigation?.registerDestination(destination);
723
- return null;
724
- }
725
990
  const EMPTY = { canUndo: false, canRedo: false, undoActionName: "", redoActionName: "" };
726
991
  const defaultSchedule = (close) => {
727
992
  if (typeof queueMicrotask === "function") queueMicrotask(close);
@@ -1021,13 +1286,6 @@ function readBinding(value, onChange) {
1021
1286
  return [value, onChange ?? (() => {
1022
1287
  })];
1023
1288
  }
1024
- function sb(kind, props, on, children, target) {
1025
- const hostProps = { kind };
1026
- if (props) hostProps.props = props;
1027
- if (on) hostProps.on = on;
1028
- if (target) hostProps.target = target;
1029
- return children === void 0 ? createElement("sb", hostProps) : createElement("sb", hostProps, children);
1030
- }
1031
1289
  const CoordinateSpacesContext = createContext({});
1032
1290
  function useCoordinateSpaces() {
1033
1291
  return useContext(CoordinateSpacesContext);
@@ -1782,12 +2040,14 @@ function OwnStack(props) {
1782
2040
  const context = {
1783
2041
  push: (value) => setPath([...path, value]),
1784
2042
  pop: () => setPath(path.slice(0, -1)),
2043
+ path,
2044
+ setPath: (next) => setPath(next),
1785
2045
  registerDestination: (builder) => {
1786
2046
  destination.current = builder;
1787
2047
  }
1788
2048
  };
1789
2049
  const screens = [
1790
- /* @__PURE__ */ jsx(Fragment, { children: sb("navscreen", { title: "", displayMode: "automatic", depth: 0 }, { tap: context.pop }, own.children) }, "root")
2050
+ /* @__PURE__ */ jsx(Fragment, { children: sb("navscreen", { title: "", displayMode: "automatic", depth: 0 }, { tap: context.pop }, /* @__PURE__ */ jsx(ScreenDepthContext.Provider, { value: 0, children: own.children })) }, "root")
1791
2051
  ];
1792
2052
  path.forEach((value, index) => {
1793
2053
  const pop = () => setPath(path.slice(0, index));
@@ -1796,7 +2056,7 @@ function OwnStack(props) {
1796
2056
  "navscreen",
1797
2057
  { title: "", displayMode: "automatic", depth: index + 1 },
1798
2058
  { tap: pop },
1799
- /* @__PURE__ */ jsx(DismissContext.Provider, { value: pop, children: /* @__PURE__ */ jsx(PathScreen, { value, destination }) })
2059
+ /* @__PURE__ */ jsx(DismissContext.Provider, { value: pop, children: /* @__PURE__ */ jsx(ScreenDepthContext.Provider, { value: index + 1, children: /* @__PURE__ */ jsx(PathScreen, { value, destination }) }) })
1800
2060
  ) }, index + 1)
1801
2061
  );
1802
2062
  });
@@ -6948,6 +7208,7 @@ export {
6948
7208
  RadialGradient,
6949
7209
  Rectangle,
6950
7210
  RegisteredView,
7211
+ RemoteTree,
6951
7212
  ResponseStream,
6952
7213
  RoundedRectangle,
6953
7214
  Row,
@@ -6993,6 +7254,7 @@ export {
6993
7254
  color,
6994
7255
  configureSignInWithApple,
6995
7256
  createFileManager,
7257
+ createRemoteRoot,
6996
7258
  createSwiftUIRoot,
6997
7259
  exportFile,
6998
7260
  fileManager,