@swiftbrowser/react 0.27.0 → 0.28.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.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useContext, createElement, useSyncExternalStore, useLayoutEffect, useRef, Fragment, useState, Children, useEffect, isValidElement, useId, cloneElement, useCallback } from "react";
1
+ import { createContext, useContext, createElement, isValidElement, useMemo, useSyncExternalStore, useLayoutEffect, useRef, Fragment, useState, Children, useEffect, useId, cloneElement, useCallback } from "react";
2
2
  import { DefaultEventPriority, ConcurrentRoot } from "react-reconciler/constants";
3
3
  import Reconciler from "react-reconciler";
4
4
  import { jsx, jsxs, Fragment as Fragment$1 } from "react/jsx-runtime";
@@ -221,6 +221,7 @@ class HostTree {
221
221
  };
222
222
  const visit = (child, sink, owner, ctx, hoistedTo) => {
223
223
  if (child.hidden) return;
224
+ if (child.kind === PREF_KIND || child.kind === HOIST_KIND) child.stale = false;
224
225
  if (child.kind === PREF_KIND) {
225
226
  contribute({ kind: "pref", target: child.target, props: child.props }, ctx);
226
227
  return;
@@ -746,6 +747,37 @@ function sb(kind, props, on, children, target) {
746
747
  if (target) hostProps.target = target;
747
748
  return children === void 0 ? createElement("sb", hostProps) : createElement("sb", hostProps, children);
748
749
  }
750
+ const INHERITED_STYLE_KEYS = /* @__PURE__ */ new Set([
751
+ "font",
752
+ "fontWeight",
753
+ "bold",
754
+ "monospacedDigit",
755
+ "tint",
756
+ "lineLimit",
757
+ "multilineTextAlignment",
758
+ "imageScale",
759
+ "defaultMinListRowHeight"
760
+ ]);
761
+ const PRESENTATION_KEYS = /* @__PURE__ */ new Set(["sheet", "fullScreenCover", "popover"]);
762
+ const InheritedStylesContext = createContext([]);
763
+ function useInheritedStyles() {
764
+ return useContext(InheritedStylesContext);
765
+ }
766
+ function styleOf(node) {
767
+ if (!isValidElement(node) || node.props.kind !== "styled") return {};
768
+ return node.props.props?.style ?? {};
769
+ }
770
+ function InheritStyle({ style, children }) {
771
+ const parent = useContext(InheritedStylesContext);
772
+ const key = JSON.stringify(style);
773
+ const value = useMemo(() => key === "{}" ? parent : [...parent, style], [parent, key]);
774
+ return createElement(InheritedStylesContext.Provider, { value }, children);
775
+ }
776
+ function replayStyles(styles, content) {
777
+ if (styles.length === 0) return content;
778
+ const provided = createElement(InheritedStylesContext.Provider, { value: styles }, content);
779
+ return styles.reduceRight((child, style) => sb("styled", { style }, void 0, child), provided);
780
+ }
749
781
  const NavigationContext = createContext(null);
750
782
  const ScreenDepthContext = createContext(0);
751
783
  function useScreenDepth() {
@@ -786,6 +818,10 @@ function renderNode(root, node, stack) {
786
818
  const children = node.children.length ? node.children.map((child) => renderNode(root, child, stack)) : void 0;
787
819
  return createElement(Fragment, { key: node.id }, sb(node.kind, node.props, root.handlersFor(node.id), children));
788
820
  }
821
+ function renderSheet(root, node, stack, inherited) {
822
+ const children = node.children.map((child) => renderNode(root, child, stack));
823
+ return createElement(Fragment, { key: node.id }, sb(node.kind, node.props, root.handlersFor(node.id), replayStyles(inherited, children)));
824
+ }
789
825
  function renderScreen(root, screen, stack) {
790
826
  const out = [screenPrefs(screen.props, "$screen")];
791
827
  for (const child of screen.children) {
@@ -804,6 +840,7 @@ function RemoteScreen({ root, id }) {
804
840
  function RemoteTree({ root, initialPath }) {
805
841
  useSyncExternalStore(root.subscribe, root.getVersion, root.getVersion);
806
842
  const environment = useContext(EnvironmentContext);
843
+ const inherited = useInheritedStyles();
807
844
  const tree = useContext(HostTreeContext);
808
845
  const navigation = useNavigation();
809
846
  const depth = useScreenDepth();
@@ -859,7 +896,7 @@ function RemoteTree({ root, initialPath }) {
859
896
  if ("colorScheme" in root.rootProps)
860
897
  out.push(createElement(Fragment, { key: "$scheme" }, sb(PREF_KIND, { colorScheme: root.rootProps["colorScheme"] }, void 0, void 0, "root")));
861
898
  for (const child of root.root.children) {
862
- const drawn = renderNode(root, child, stack);
899
+ const drawn = child.kind === "sheet" && inherited.length > 0 ? renderSheet(root, child, stack, inherited) : renderNode(root, child, stack);
863
900
  if (child.kind === "sheet" || child.kind === "alert") out.push(createElement(Fragment, { key: child.id }, sb(HOIST_KIND, void 0, void 0, drawn, "root")));
864
901
  else if (child.kind === "toolbar" || child.kind === "searchfield") out.push(createElement(Fragment, { key: child.id }, sb(HOIST_KIND, void 0, void 0, drawn, "navscreen")));
865
902
  else out.push(drawn);
@@ -1052,6 +1089,7 @@ function mount(screen, element, options = {}) {
1052
1089
  platform: options.platform === void 0 || options.platform === "auto" ? decidePlatform(width) : options.platform
1053
1090
  };
1054
1091
  renderer.contentHeight = options.contentHeight ?? false;
1092
+ if (options.sidebarTitleBar === false) renderer.sidebarTitleBar = false;
1055
1093
  if (options.assets) renderer.setAssetCatalog(options.assets);
1056
1094
  if (options.app) browserEnvironment.files?.setApp?.(options.app);
1057
1095
  renderer.setEnvironment({ colorScheme: environment.colorScheme, dynamicTypeSize: environment.dynamicTypeSize, platform: environment.platform });
@@ -1360,6 +1398,7 @@ function SwiftUIView({
1360
1398
  safeArea,
1361
1399
  history = true,
1362
1400
  keyboard = fit === "fill",
1401
+ sidebarTitleBar = true,
1363
1402
  className,
1364
1403
  style
1365
1404
  }) {
@@ -1387,6 +1426,9 @@ function SwiftUIView({
1387
1426
  instance.unmount();
1388
1427
  };
1389
1428
  }, [fit, history, keyboard]);
1429
+ useLayoutEffect(() => {
1430
+ if (mounted.current) mounted.current.renderer.sidebarTitleBar = sidebarTitleBar;
1431
+ }, [sidebarTitleBar, fit, history, keyboard]);
1390
1432
  useLayoutEffect(() => {
1391
1433
  mounted.current?.root.render(tree);
1392
1434
  });
@@ -2334,7 +2376,8 @@ function useReportSplitSelection(split, selected, clear, active2 = true) {
2334
2376
  function OutsideSplit({ children }) {
2335
2377
  return /* @__PURE__ */ jsx(SplitSelectionContext.Provider, { value: null, children });
2336
2378
  }
2337
- function SheetPresenter({ isPresented, onDismiss, content, fullScreen }) {
2379
+ function SheetPresenter({ isPresented, onDismiss, content, fullScreen, inherited }) {
2380
+ const around = useInheritedStyles();
2338
2381
  const [presented, setPresented] = readBinding(isPresented);
2339
2382
  if (!presented) return null;
2340
2383
  const dismiss = () => {
@@ -2350,7 +2393,10 @@ function SheetPresenter({ isPresented, onDismiss, content, fullScreen }) {
2350
2393
  "sheet",
2351
2394
  { detents },
2352
2395
  { tap: dismiss },
2353
- /* @__PURE__ */ jsx(NavigationContext.Provider, { value: null, children: /* @__PURE__ */ jsx(OutsideSplit, { children: /* @__PURE__ */ jsx(DismissContext.Provider, { value: dismiss, children: content }) }) })
2396
+ replayStyles(
2397
+ inherited && inherited.length > 0 ? [...around, ...inherited] : around,
2398
+ /* @__PURE__ */ jsx(NavigationContext.Provider, { value: null, children: /* @__PURE__ */ jsx(OutsideSplit, { children: /* @__PURE__ */ jsx(DismissContext.Provider, { value: dismiss, children: content }) }) })
2399
+ )
2354
2400
  ),
2355
2401
  "root"
2356
2402
  );
@@ -2857,13 +2903,30 @@ function applyModifiers(element, modifiers) {
2857
2903
  const isPreference = (side) => isValidElement(side) && side.props.kind === PREF_KIND;
2858
2904
  function wrap(element, modifiers, sides) {
2859
2905
  let node = element;
2906
+ const holds = holdsViews(element);
2907
+ const presentations = [];
2860
2908
  for (const [key, value] of modifiers) {
2861
2909
  const inactive = value === void 0 || value === null && key !== "lineLimit" && key !== "badge";
2910
+ const before = sides.length;
2862
2911
  const next = inactive ? node : apply(key, value, node, sides);
2863
2912
  node = next === node ? neutral(key, node) : next;
2913
+ if (INHERITED_STYLE_KEYS.has(key)) {
2914
+ const style = styleOf(node);
2915
+ if (Object.keys(style).length > 0) for (const presentation of presentations) presentation.after.push(style);
2916
+ if (holds) node = createElement(InheritStyle, { style }, node);
2917
+ }
2918
+ if (PRESENTATION_KEYS.has(key) && sides.length > before) presentations.push({ at: before, after: [] });
2919
+ }
2920
+ for (const { at, after } of presentations) {
2921
+ const side = sides[at];
2922
+ if (after.length > 0 && isValidElement(side)) sides[at] = cloneElement(side, { inherited: after.reverse() });
2864
2923
  }
2865
2924
  return node;
2866
2925
  }
2926
+ function holdsViews(element) {
2927
+ if (!isValidElement(element)) return false;
2928
+ return element.type !== "sb" || element.props.children !== void 0;
2929
+ }
2867
2930
  const STYLED_KEYS = /* @__PURE__ */ new Set([
2868
2931
  "padding",
2869
2932
  "font",
@@ -3974,7 +4037,7 @@ function protocolRuns(text) {
3974
4037
  }
3975
4038
  function useAudioRecorder(options = {}) {
3976
4039
  const interval = options.meterInterval ?? 50;
3977
- const host = browserEnvironment.audioRecorder;
4040
+ const host2 = browserEnvironment.audioRecorder;
3978
4041
  const [status, setStatus] = useState("idle");
3979
4042
  const [time, setTime] = useState(0);
3980
4043
  const [levels, setLevels] = useState([]);
@@ -3983,13 +4046,13 @@ function useAudioRecorder(options = {}) {
3983
4046
  statusRef.current = status;
3984
4047
  const fail = (reason) => setError(reason instanceof Error ? reason.message : String(reason));
3985
4048
  useEffect(() => {
3986
- if (status !== "recording" || !host) return;
4049
+ if (status !== "recording" || !host2) return;
3987
4050
  let reading = false;
3988
4051
  let live = true;
3989
4052
  const timer = setInterval(() => {
3990
4053
  if (reading) return;
3991
4054
  reading = true;
3992
- host.meter().then((meter) => {
4055
+ host2.meter().then((meter) => {
3993
4056
  if (!live) return;
3994
4057
  setTime(meter.time);
3995
4058
  setLevels((previous) => [...previous, meter.level]);
@@ -4001,15 +4064,15 @@ function useAudioRecorder(options = {}) {
4001
4064
  live = false;
4002
4065
  clearInterval(timer);
4003
4066
  };
4004
- }, [status, host, interval]);
4067
+ }, [status, host2, interval]);
4005
4068
  useEffect(
4006
4069
  () => () => {
4007
- if (statusRef.current !== "idle") void host?.cancel().catch(() => void 0);
4070
+ if (statusRef.current !== "idle") void host2?.cancel().catch(() => void 0);
4008
4071
  },
4009
- [host]
4072
+ [host2]
4010
4073
  );
4011
4074
  const record = useCallback(async () => {
4012
- if (!host) {
4075
+ if (!host2) {
4013
4076
  setError("unsupported: this page cannot record");
4014
4077
  return;
4015
4078
  }
@@ -4019,28 +4082,28 @@ function useAudioRecorder(options = {}) {
4019
4082
  setLevels([]);
4020
4083
  setStatus("starting");
4021
4084
  try {
4022
- await host.start();
4085
+ await host2.start();
4023
4086
  setStatus("recording");
4024
4087
  } catch (reason) {
4025
4088
  fail(reason);
4026
4089
  setStatus("idle");
4027
4090
  }
4028
- }, [host]);
4091
+ }, [host2]);
4029
4092
  const pause = useCallback(async () => {
4030
- if (!host || statusRef.current !== "recording") return;
4031
- await host.pause().catch(fail);
4093
+ if (!host2 || statusRef.current !== "recording") return;
4094
+ await host2.pause().catch(fail);
4032
4095
  setStatus("paused");
4033
- }, [host]);
4096
+ }, [host2]);
4034
4097
  const resume = useCallback(async () => {
4035
- if (!host || statusRef.current !== "paused") return;
4036
- await host.resume().catch(fail);
4098
+ if (!host2 || statusRef.current !== "paused") return;
4099
+ await host2.resume().catch(fail);
4037
4100
  setStatus("recording");
4038
- }, [host]);
4101
+ }, [host2]);
4039
4102
  const stop = useCallback(async () => {
4040
- if (!host || statusRef.current !== "recording" && statusRef.current !== "paused") return null;
4103
+ if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return null;
4041
4104
  setStatus("stopping");
4042
4105
  try {
4043
- const recording = await host.stop();
4106
+ const recording = await host2.stop();
4044
4107
  setTime(recording.duration);
4045
4108
  return recording;
4046
4109
  } catch (reason) {
@@ -4049,30 +4112,538 @@ function useAudioRecorder(options = {}) {
4049
4112
  } finally {
4050
4113
  setStatus("idle");
4051
4114
  }
4052
- }, [host]);
4115
+ }, [host2]);
4053
4116
  const cancel = useCallback(async () => {
4054
- if (!host || statusRef.current === "idle") return;
4055
- await host.cancel().catch(fail);
4117
+ if (!host2 || statusRef.current === "idle") return;
4118
+ await host2.cancel().catch(fail);
4056
4119
  setStatus("idle");
4057
4120
  setTime(0);
4058
4121
  setLevels([]);
4059
- }, [host]);
4122
+ }, [host2]);
4060
4123
  const discard = useCallback(async (url) => {
4061
- await host?.discard(url).catch(fail);
4062
- }, [host]);
4124
+ await host2?.discard(url).catch(fail);
4125
+ }, [host2]);
4063
4126
  const trim = useCallback(
4064
4127
  async (url, keep) => {
4065
- if (!host) throw new Error("unsupported: this page cannot cut audio");
4128
+ if (!host2) throw new Error("unsupported: this page cannot cut audio");
4066
4129
  try {
4067
- return await host.trim(url, keep);
4130
+ return await host2.trim(url, keep);
4068
4131
  } catch (reason) {
4069
4132
  fail(reason);
4070
4133
  throw reason;
4071
4134
  }
4072
4135
  },
4073
- [host]
4136
+ [host2]
4137
+ );
4138
+ return { available: host2 !== void 0, status, time, levels, error, record, pause, resume, stop, cancel, discard, trim };
4139
+ }
4140
+ function host() {
4141
+ const found = browserEnvironment.avAsset;
4142
+ if (!found) throw new Error("unsupported: this page cannot read or write video");
4143
+ return found;
4144
+ }
4145
+ class CMTime {
4146
+ constructor(value, timescale) {
4147
+ this.value = value;
4148
+ this.timescale = timescale;
4149
+ }
4150
+ value;
4151
+ timescale;
4152
+ static zero = new CMTime(0, 1);
4153
+ static invalid = new CMTime(0, 0);
4154
+ static positiveInfinity = new CMTime(Infinity, 1);
4155
+ /** `CMTime(value:timescale:)`. */
4156
+ static make(value, timescale) {
4157
+ return new CMTime(value, timescale);
4158
+ }
4159
+ /** `CMTime(seconds:preferredTimescale:)`: rounded to the timescale. */
4160
+ static seconds(seconds, preferredTimescale = 600) {
4161
+ if (!Number.isFinite(seconds)) return seconds > 0 ? CMTime.positiveInfinity : CMTime.invalid;
4162
+ return new CMTime(Math.round(seconds * preferredTimescale), preferredTimescale);
4163
+ }
4164
+ get isValid() {
4165
+ return this.timescale > 0;
4166
+ }
4167
+ /** `CMTimeGetSeconds`. */
4168
+ get seconds() {
4169
+ return this.timescale > 0 ? this.value / this.timescale : Number.NaN;
4170
+ }
4171
+ add(other) {
4172
+ return CMTime.seconds(this.seconds + other.seconds, Math.max(this.timescale, other.timescale, 1));
4173
+ }
4174
+ subtract(other) {
4175
+ return CMTime.seconds(this.seconds - other.seconds, Math.max(this.timescale, other.timescale, 1));
4176
+ }
4177
+ /** `CMTimeCompare`: -1, 0 or 1. */
4178
+ compare(other) {
4179
+ const a = this.seconds;
4180
+ const b = other.seconds;
4181
+ return a < b ? -1 : a > b ? 1 : 0;
4182
+ }
4183
+ static minimum(a, b) {
4184
+ return a.compare(b) <= 0 ? a : b;
4185
+ }
4186
+ static maximum(a, b) {
4187
+ return a.compare(b) >= 0 ? a : b;
4188
+ }
4189
+ }
4190
+ class CMTimeRange {
4191
+ constructor(start, duration) {
4192
+ this.start = start;
4193
+ this.duration = duration;
4194
+ }
4195
+ start;
4196
+ duration;
4197
+ static zero = new CMTimeRange(CMTime.zero, CMTime.zero);
4198
+ /** `CMTimeRange(start:end:)`. */
4199
+ static fromTo(start, end) {
4200
+ return new CMTimeRange(start, CMTime.maximum(end.subtract(start), CMTime.zero));
4201
+ }
4202
+ /** `CMTimeRange(start:duration:)` in seconds. */
4203
+ static seconds(start, duration, preferredTimescale = 600) {
4204
+ return new CMTimeRange(CMTime.seconds(start, preferredTimescale), CMTime.seconds(duration, preferredTimescale));
4205
+ }
4206
+ get end() {
4207
+ return this.start.add(this.duration);
4208
+ }
4209
+ get isEmpty() {
4210
+ return this.duration.seconds <= 0;
4211
+ }
4212
+ containsTime(time) {
4213
+ return time.compare(this.start) >= 0 && time.compare(this.end) < 0;
4214
+ }
4215
+ }
4216
+ const AVMediaType = { video: "vide", audio: "soun" };
4217
+ class AVAssetTrack {
4218
+ constructor(trackID, mediaType, naturalSize, nominalFrameRate, timeRange, codec) {
4219
+ this.trackID = trackID;
4220
+ this.mediaType = mediaType;
4221
+ this.naturalSize = naturalSize;
4222
+ this.nominalFrameRate = nominalFrameRate;
4223
+ this.timeRange = timeRange;
4224
+ this.codec = codec;
4225
+ }
4226
+ trackID;
4227
+ mediaType;
4228
+ naturalSize;
4229
+ nominalFrameRate;
4230
+ timeRange;
4231
+ codec;
4232
+ }
4233
+ class AVAsset {
4234
+ /** @internal */
4235
+ segmentsForExport() {
4236
+ return this.segments();
4237
+ }
4238
+ /** `loadTracks(withMediaType:)`. */
4239
+ async loadTracks(mediaType) {
4240
+ return (await this.load("tracks")).filter((track) => track.mediaType === mediaType);
4241
+ }
4242
+ }
4243
+ const tracksOf = (info) => info.tracks.map(
4244
+ (t) => new AVAssetTrack(
4245
+ t.trackID,
4246
+ t.mediaType === "video" ? AVMediaType.video : AVMediaType.audio,
4247
+ t.naturalSize,
4248
+ t.nominalFrameRate,
4249
+ CMTimeRange.seconds(t.timeRange.start, t.timeRange.duration),
4250
+ t.codec
4251
+ )
4252
+ );
4253
+ class AVURLAsset extends AVAsset {
4254
+ constructor(url) {
4255
+ super();
4256
+ this.url = url;
4257
+ }
4258
+ url;
4259
+ info = null;
4260
+ loaded() {
4261
+ if (!this.info) {
4262
+ this.info = host().load(this.url);
4263
+ this.info.catch(() => {
4264
+ this.info = null;
4265
+ });
4266
+ }
4267
+ return this.info;
4268
+ }
4269
+ async segments() {
4270
+ return [{ url: this.url, start: 0, end: (await this.loaded()).duration }];
4271
+ }
4272
+ async load(key) {
4273
+ const info = await this.loaded();
4274
+ const values = {
4275
+ duration: CMTime.seconds(info.duration),
4276
+ tracks: tracksOf(info),
4277
+ naturalSize: info.naturalSize,
4278
+ isPlayable: info.tracks.length > 0,
4279
+ isExportable: info.tracks.length > 0
4280
+ };
4281
+ return values[key];
4282
+ }
4283
+ }
4284
+ class AVMutableComposition extends AVAsset {
4285
+ parts = [];
4286
+ /** The composition's length. */
4287
+ get duration() {
4288
+ return CMTime.seconds(this.parts.reduce((sum, part) => sum + part.range.duration.seconds, 0));
4289
+ }
4290
+ /** `insertTimeRange(_:of:at:)`: `range` of `asset` (an `AVURLAsset`, or another composition's parts) placed at `at`. */
4291
+ async insertTimeRange(range, asset, at) {
4292
+ const inserted = [];
4293
+ if (asset instanceof AVURLAsset) {
4294
+ const length = (await asset.load("duration")).seconds;
4295
+ const start = Math.max(0, range.start.seconds);
4296
+ const end = Math.min(length, range.end.seconds);
4297
+ if (end - start > 1e-3) inserted.push({ asset, range: CMTimeRange.seconds(start, end - start) });
4298
+ } else if (asset instanceof AVMutableComposition) {
4299
+ inserted.push(...asset.partsIn(range.start.seconds, range.end.seconds));
4300
+ } else {
4301
+ throw new Error("unsupported: only an AVURLAsset or an AVMutableComposition can be inserted");
4302
+ }
4303
+ const time = Math.max(0, Math.min(at.seconds, this.duration.seconds));
4304
+ const [before, after] = this.splitAt(time);
4305
+ this.parts = [...before, ...inserted, ...after];
4306
+ }
4307
+ /** `removeTimeRange(_:)`: the media in `range` cut out, the rest closing up. */
4308
+ removeTimeRange(range) {
4309
+ const [before] = this.splitAt(range.start.seconds);
4310
+ const [, after] = this.splitAt(range.end.seconds);
4311
+ this.parts = [...before, ...after];
4312
+ }
4313
+ /** The parts' media from `start` to `end` of the composition's timeline. */
4314
+ partsIn(start, end) {
4315
+ const [, from] = this.splitAt(start);
4316
+ const out = [];
4317
+ let cursor = start;
4318
+ for (const part of from) {
4319
+ if (cursor >= end) break;
4320
+ const take = Math.min(part.range.duration.seconds, end - cursor);
4321
+ if (take > 1e-3) out.push({ asset: part.asset, range: CMTimeRange.seconds(part.range.start.seconds, take) });
4322
+ cursor += take;
4323
+ }
4324
+ return out;
4325
+ }
4326
+ /** The parts before `time` and from it, a part across it split in two. */
4327
+ splitAt(time) {
4328
+ const before = [];
4329
+ const after = [];
4330
+ let cursor = 0;
4331
+ for (const part of this.parts) {
4332
+ const length = part.range.duration.seconds;
4333
+ const start = part.range.start.seconds;
4334
+ if (cursor + length <= time + 1e-9) before.push(part);
4335
+ else if (cursor >= time - 1e-9) after.push(part);
4336
+ else {
4337
+ const head = time - cursor;
4338
+ before.push({ asset: part.asset, range: CMTimeRange.seconds(start, head) });
4339
+ after.push({ asset: part.asset, range: CMTimeRange.seconds(start + head, length - head) });
4340
+ }
4341
+ cursor += length;
4342
+ }
4343
+ return [before, after];
4344
+ }
4345
+ async segments() {
4346
+ return this.parts.map((part) => ({ url: part.asset.url, start: part.range.start.seconds, end: part.range.end.seconds }));
4347
+ }
4348
+ async load(key) {
4349
+ const first = this.parts[0]?.asset;
4350
+ const tracks = first ? await first.load("tracks") : [];
4351
+ const values = {
4352
+ duration: this.duration,
4353
+ tracks: tracks.map((t) => new AVAssetTrack(t.trackID, t.mediaType, t.naturalSize, t.nominalFrameRate, new CMTimeRange(CMTime.zero, this.duration), t.codec)),
4354
+ naturalSize: first ? await first.load("naturalSize") : { width: 0, height: 0 },
4355
+ isPlayable: this.parts.length > 0,
4356
+ isExportable: this.parts.length > 0
4357
+ };
4358
+ return values[key];
4359
+ }
4360
+ }
4361
+ class AVAssetImageGenerator {
4362
+ constructor(asset) {
4363
+ this.asset = asset;
4364
+ }
4365
+ asset;
4366
+ maximumSize = { width: 0, height: 0 };
4367
+ /** Stills come upright (the track's rotation applied), as with `appliesPreferredTrackTransform = true`. */
4368
+ appliesPreferredTrackTransform = true;
4369
+ made = [];
4370
+ /** The file and the time in it that `time` on the asset's timeline falls on (a composition's part). */
4371
+ async source(time) {
4372
+ let cursor = 0;
4373
+ const segments = await this.asset.segmentsForExport();
4374
+ for (const segment of segments) {
4375
+ const length = segment.end - segment.start;
4376
+ if (time < cursor + length || segment === segments[segments.length - 1]) return { url: segment.url, time: segment.start + Math.max(0, Math.min(length, time - cursor)) };
4377
+ cursor += length;
4378
+ }
4379
+ throw new Error("empty: the asset has no media");
4380
+ }
4381
+ /** `image(at:)`: the still at `time`. */
4382
+ async image(at) {
4383
+ const { url, time } = await this.source(Math.max(0, at.seconds));
4384
+ const options = {
4385
+ ...this.maximumSize.width > 0 ? { maxWidth: this.maximumSize.width } : {},
4386
+ ...this.maximumSize.height > 0 ? { maxHeight: this.maximumSize.height } : {}
4387
+ };
4388
+ const still = await host().image(url, time, options);
4389
+ this.made.push(still.url);
4390
+ return { image: still.url, actualTime: CMTime.seconds(at.seconds + (still.actualTime - time)), width: still.width, height: still.height };
4391
+ }
4392
+ /** `images(for:)`: a still for each time, in turn. */
4393
+ async *images(times) {
4394
+ for (const time of times) yield { ...await this.image(time), requestedTime: time };
4395
+ }
4396
+ /** Lets go of the stills this generator made (their `blob:` URLs on a page). */
4397
+ async release() {
4398
+ const urls = this.made.splice(0);
4399
+ await Promise.all(urls.map((url) => host().release(url)));
4400
+ }
4401
+ }
4402
+ const AVFileType = { mp4: "public.mpeg-4", mov: "com.apple.quicktime-movie", m4v: "com.apple.m4v-video", webm: "org.webmproject.webm" };
4403
+ const FILE_TYPES = {
4404
+ "public.mpeg-4": "mp4",
4405
+ "com.apple.m4v-video": "mp4",
4406
+ "com.apple.quicktime-movie": "mov",
4407
+ "org.webmproject.webm": "webm"
4408
+ };
4409
+ const AVAssetExportPresetPassthrough = "AVAssetExportPresetPassthrough";
4410
+ const AVAssetExportPresetHighestQuality = "AVAssetExportPresetHighestQuality";
4411
+ const AVAssetExportPresetMediumQuality = "AVAssetExportPresetMediumQuality";
4412
+ const AVAssetExportPresetLowQuality = "AVAssetExportPresetLowQuality";
4413
+ const AVAssetExportPreset640x480 = "AVAssetExportPreset640x480";
4414
+ const AVAssetExportPreset960x540 = "AVAssetExportPreset960x540";
4415
+ const AVAssetExportPreset1280x720 = "AVAssetExportPreset1280x720";
4416
+ const AVAssetExportPreset1920x1080 = "AVAssetExportPreset1920x1080";
4417
+ const AVAssetExportPreset3840x2160 = "AVAssetExportPreset3840x2160";
4418
+ let exports$1 = 0;
4419
+ class AVAssetExportSession {
4420
+ constructor(asset, presetName) {
4421
+ this.asset = asset;
4422
+ this.presetName = presetName;
4423
+ }
4424
+ asset;
4425
+ presetName;
4426
+ outputURL = null;
4427
+ outputFileType = AVFileType.mp4;
4428
+ timeRange = null;
4429
+ status = "unknown";
4430
+ progress = 0;
4431
+ error = null;
4432
+ /** Heard as `progress` changes (a few times a second). */
4433
+ onProgress = null;
4434
+ /** The written file's length and MIME type, once completed. */
4435
+ result = null;
4436
+ id = `export-${++exports$1}`;
4437
+ /** `AVAssetExportSession.allExportPresets()`. */
4438
+ static allExportPresets() {
4439
+ return [
4440
+ AVAssetExportPresetPassthrough,
4441
+ AVAssetExportPresetHighestQuality,
4442
+ AVAssetExportPresetMediumQuality,
4443
+ AVAssetExportPresetLowQuality,
4444
+ AVAssetExportPreset640x480,
4445
+ AVAssetExportPreset960x540,
4446
+ AVAssetExportPreset1280x720,
4447
+ AVAssetExportPreset1920x1080,
4448
+ AVAssetExportPreset3840x2160
4449
+ ];
4450
+ }
4451
+ /** The asset's parts, cut to `timeRange` when it is set. */
4452
+ async parts() {
4453
+ const all = await this.asset.segmentsForExport();
4454
+ if (!this.timeRange) return all;
4455
+ const from = this.timeRange.start.seconds;
4456
+ const to = this.timeRange.end.seconds;
4457
+ const out = [];
4458
+ let cursor = 0;
4459
+ for (const segment of all) {
4460
+ const length = segment.end - segment.start;
4461
+ const start = Math.max(from, cursor);
4462
+ const end = Math.min(to, cursor + length);
4463
+ if (end - start > 1e-3) out.push({ url: segment.url, start: segment.start + (start - cursor), end: segment.start + (end - cursor) });
4464
+ cursor += length;
4465
+ }
4466
+ return out;
4467
+ }
4468
+ /** `export()`: writes the file and answers its URL (also `outputURL`); rejects with why it could not. */
4469
+ async export() {
4470
+ if (this.status === "exporting") throw new Error("busy: already exporting");
4471
+ this.status = "exporting";
4472
+ this.error = null;
4473
+ this.setProgress(0);
4474
+ const assets = host();
4475
+ const ticker = setInterval(() => {
4476
+ void assets.progress(this.id).then((p) => this.status === "exporting" && this.setProgress(p));
4477
+ }, 200);
4478
+ try {
4479
+ const result = await assets.export(this.id, {
4480
+ segments: await this.parts(),
4481
+ preset: this.presetName,
4482
+ fileType: FILE_TYPES[this.outputFileType] ?? "mp4",
4483
+ ...this.outputURL ? { outputURL: this.outputURL } : {}
4484
+ });
4485
+ this.outputURL = result.url;
4486
+ this.result = { duration: result.duration, type: result.type };
4487
+ this.status = "completed";
4488
+ this.setProgress(1);
4489
+ return result.url;
4490
+ } catch (reason) {
4491
+ const error = reason instanceof Error ? reason : new Error(String(reason));
4492
+ this.error = error;
4493
+ this.status = error.message.startsWith("cancelled") ? "cancelled" : "failed";
4494
+ throw error;
4495
+ } finally {
4496
+ clearInterval(ticker);
4497
+ }
4498
+ }
4499
+ /** `cancelExport()`. */
4500
+ cancelExport() {
4501
+ if (this.status !== "exporting") return;
4502
+ void host().cancel(this.id);
4503
+ }
4504
+ setProgress(progress) {
4505
+ if (progress === this.progress) return;
4506
+ this.progress = progress;
4507
+ this.onProgress?.(progress);
4508
+ }
4509
+ }
4510
+ function useVideoRecorder(options = {}) {
4511
+ const interval = options.timeInterval ?? 100;
4512
+ const host2 = browserEnvironment.videoRecorder;
4513
+ const [status, setStatus] = useState("closed");
4514
+ const [stream, setStream] = useState(null);
4515
+ const [facing, setFacing] = useState("user");
4516
+ const [time, setTime] = useState(0);
4517
+ const [error, setError] = useState(null);
4518
+ const statusRef = useRef(status);
4519
+ statusRef.current = status;
4520
+ const facingRef = useRef(facing);
4521
+ facingRef.current = facing;
4522
+ const fail = (reason) => setError(reason instanceof Error ? reason.message : String(reason));
4523
+ useEffect(() => {
4524
+ if (status !== "recording" || !host2) return;
4525
+ let reading = false;
4526
+ let live = true;
4527
+ const timer = setInterval(() => {
4528
+ if (reading) return;
4529
+ reading = true;
4530
+ host2.time().then((seconds) => live && setTime(seconds)).catch(() => void 0).finally(() => {
4531
+ reading = false;
4532
+ });
4533
+ }, interval);
4534
+ return () => {
4535
+ live = false;
4536
+ clearInterval(timer);
4537
+ };
4538
+ }, [status, host2, interval]);
4539
+ useEffect(
4540
+ () => () => {
4541
+ if (statusRef.current !== "closed") void host2?.close().catch(() => void 0);
4542
+ },
4543
+ [host2]
4544
+ );
4545
+ const open = useCallback(
4546
+ async (wanted) => {
4547
+ if (!host2) {
4548
+ setError("unsupported: this page cannot record video");
4549
+ return;
4550
+ }
4551
+ if (statusRef.current === "recording" || statusRef.current === "paused" || statusRef.current === "stopping") return;
4552
+ setError(null);
4553
+ setStatus("opening");
4554
+ try {
4555
+ const opened = await host2.open({ facing: wanted ?? facingRef.current });
4556
+ setStream(opened.stream);
4557
+ setFacing(opened.facing);
4558
+ setStatus("ready");
4559
+ } catch (reason) {
4560
+ fail(reason);
4561
+ setStream(null);
4562
+ setStatus("closed");
4563
+ }
4564
+ },
4565
+ [host2]
4566
+ );
4567
+ const close = useCallback(async () => {
4568
+ if (!host2) return;
4569
+ await host2.close().catch(fail);
4570
+ setStream(null);
4571
+ setStatus("closed");
4572
+ setTime(0);
4573
+ }, [host2]);
4574
+ const flip = useCallback(async () => {
4575
+ if (statusRef.current !== "ready") return;
4576
+ await open(facingRef.current === "user" ? "environment" : "user");
4577
+ }, [open]);
4578
+ const record = useCallback(async () => {
4579
+ if (!host2 || statusRef.current !== "ready") return;
4580
+ setError(null);
4581
+ setTime(0);
4582
+ try {
4583
+ await host2.start();
4584
+ setStatus("recording");
4585
+ } catch (reason) {
4586
+ fail(reason);
4587
+ }
4588
+ }, [host2]);
4589
+ const pause = useCallback(async () => {
4590
+ if (!host2 || statusRef.current !== "recording") return;
4591
+ await host2.pause().catch(fail);
4592
+ setStatus("paused");
4593
+ }, [host2]);
4594
+ const resume = useCallback(async () => {
4595
+ if (!host2 || statusRef.current !== "paused") return;
4596
+ await host2.resume().catch(fail);
4597
+ setStatus("recording");
4598
+ }, [host2]);
4599
+ const stop = useCallback(async () => {
4600
+ if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return null;
4601
+ setStatus("stopping");
4602
+ try {
4603
+ const recording = await host2.stop();
4604
+ setTime(recording.duration);
4605
+ return recording;
4606
+ } catch (reason) {
4607
+ fail(reason);
4608
+ return null;
4609
+ } finally {
4610
+ setStatus("ready");
4611
+ }
4612
+ }, [host2]);
4613
+ const cancel = useCallback(async () => {
4614
+ if (!host2 || statusRef.current !== "recording" && statusRef.current !== "paused") return;
4615
+ await host2.cancel().catch(fail);
4616
+ setStatus("ready");
4617
+ setTime(0);
4618
+ }, [host2]);
4619
+ const discard = useCallback(
4620
+ async (url) => {
4621
+ await host2?.discard(url).catch(fail);
4622
+ },
4623
+ [host2]
4624
+ );
4625
+ return { available: host2 !== void 0, status, stream, facing, time, error, open, close, flip, record, pause, resume, stop, cancel, discard };
4626
+ }
4627
+ function CameraPreview(props) {
4628
+ const { own, modifiers } = splitProps(props);
4629
+ const stream = own.recorder.stream;
4630
+ if (!stream) return applyModifiers(sb("color", { color: { r: 0, g: 0, b: 0, a: 1 } }), modifiers);
4631
+ return applyModifiers(
4632
+ sb("video", {
4633
+ url: null,
4634
+ playing: false,
4635
+ muted: true,
4636
+ volume: 0,
4637
+ rate: 1,
4638
+ loop: false,
4639
+ gravity: own.gravity ?? "resizeAspectFill",
4640
+ controls: false,
4641
+ seek: { time: 0, serial: 0 },
4642
+ stream,
4643
+ mirrored: own.mirrored ?? own.recorder.facing === "user"
4644
+ }),
4645
+ modifiers
4074
4646
  );
4075
- return { available: host !== void 0, status, time, levels, error, record, pause, resume, stop, cancel, discard, trim };
4076
4647
  }
4077
4648
  let nextId$1 = 1;
4078
4649
  const newId = () => nextId$1++;
@@ -4081,9 +4652,9 @@ function unsupported() {
4081
4652
  return new Error("unsupported: this page or app has no audio engine");
4082
4653
  }
4083
4654
  function perform$1(command2) {
4084
- const host = hostNow$1();
4085
- if (!host) return Promise.reject(unsupported());
4086
- return host.perform(command2);
4655
+ const host2 = hostNow$1();
4656
+ if (!host2) return Promise.reject(unsupported());
4657
+ return host2.perform(command2);
4087
4658
  }
4088
4659
  class AVAudioTime {
4089
4660
  spec;
@@ -4689,9 +5260,9 @@ function useAudioEngine(build) {
4689
5260
  let nextId = 1;
4690
5261
  const hostNow = () => browserEnvironment.languageModel;
4691
5262
  function perform(command2) {
4692
- const host = hostNow();
4693
- if (!host) return Promise.reject(new GenerationError("assetsUnavailable", "This page or app has no language model."));
4694
- return host.perform(command2).catch((error) => {
5263
+ const host2 = hostNow();
5264
+ if (!host2) return Promise.reject(new GenerationError("assetsUnavailable", "This page or app has no language model."));
5265
+ return host2.perform(command2).catch((error) => {
4695
5266
  throw GenerationError.from(error);
4696
5267
  });
4697
5268
  }
@@ -5174,8 +5745,9 @@ function ProgressView(props) {
5174
5745
  }
5175
5746
  function ToolbarItem(props) {
5176
5747
  const hidden = props.sharedBackgroundVisibility === "hidden";
5177
- const items = withEnvironment({ labelStyle: "iconOnly" }, props.children);
5178
- return sb("toolbar", hidden ? { placement: toolbarPlacement(props.placement), sharedBackground: "hidden" } : { placement: toolbarPlacement(props.placement) }, void 0, items);
5748
+ const placement = toolbarPlacement(props.placement);
5749
+ const items = placement === "secondary" ? props.children : withEnvironment({ labelStyle: "iconOnly" }, props.children);
5750
+ return sb("toolbar", hidden ? { placement, sharedBackground: "hidden" } : { placement }, void 0, items);
5179
5751
  }
5180
5752
  const ToolbarItemGroup = ToolbarItem;
5181
5753
  function ToolbarSpacer(props) {
@@ -5377,14 +5949,14 @@ function canSignInWithApple() {
5377
5949
  return browserEnvironment.appleSignIn !== void 0;
5378
5950
  }
5379
5951
  async function signInWithApple(options = {}) {
5380
- const host = browserEnvironment.appleSignIn;
5381
- if (!host) throw new Error("unsupported: Sign in with Apple is not available here (configureSignInWithApple on a page)");
5382
- return host.signIn({ scopes: options.scopes ?? ["fullName", "email"], ...options.nonce ? { nonce: options.nonce } : {}, ...options.state ? { state: options.state } : {} });
5952
+ const host2 = browserEnvironment.appleSignIn;
5953
+ if (!host2) throw new Error("unsupported: Sign in with Apple is not available here (configureSignInWithApple on a page)");
5954
+ return host2.signIn({ scopes: options.scopes ?? ["fullName", "email"], ...options.nonce ? { nonce: options.nonce } : {}, ...options.state ? { state: options.state } : {} });
5383
5955
  }
5384
5956
  async function appleCredentialState(user) {
5385
- const host = browserEnvironment.appleSignIn;
5386
- if (!host) return "unknown";
5387
- return host.credentialState(user);
5957
+ const host2 = browserEnvironment.appleSignIn;
5958
+ if (!host2) return "unknown";
5959
+ return host2.credentialState(user);
5388
5960
  }
5389
5961
  const LABELS = { signIn: "Sign in with Apple", signUp: "Sign up with Apple", continue: "Continue with Apple" };
5390
5962
  function SignInWithAppleButton(props) {
@@ -5715,16 +6287,16 @@ function hostData(contents) {
5715
6287
  if (contents instanceof ArrayBuffer) return new Uint8Array(contents);
5716
6288
  return contents;
5717
6289
  }
5718
- function createFileManager(host) {
6290
+ function createFileManager(host2) {
5719
6291
  const required = () => {
5720
- const files = host();
6292
+ const files = host2();
5721
6293
  if (!files) throw new Error("unsupported: this page keeps no files");
5722
6294
  return files;
5723
6295
  };
5724
- const directories = () => host()?.directories ?? NONE;
6296
+ const directories = () => host2()?.directories ?? NONE;
5725
6297
  return {
5726
6298
  get available() {
5727
- return host() !== void 0;
6299
+ return host2() !== void 0;
5728
6300
  },
5729
6301
  get documentsDirectory() {
5730
6302
  return directories().documents;
@@ -5761,8 +6333,8 @@ function createFileManager(host) {
5761
6333
  copyItem: async (from, to) => required().copy(from, to),
5762
6334
  removeItem: async (url) => required().remove(url),
5763
6335
  loadURL: async (url) => required().loadURL(url),
5764
- iCloudDirectory: async () => await host()?.iCloud?.() ?? null,
5765
- watch: (listener) => host()?.watch?.(listener) ?? (() => {
6336
+ iCloudDirectory: async () => await host2()?.iCloud?.() ?? null,
6337
+ watch: (listener) => host2()?.watch?.(listener) ?? (() => {
5766
6338
  })
5767
6339
  };
5768
6340
  }
@@ -6327,7 +6899,7 @@ function Chart(props) {
6327
6899
  const key = mark.props.foregroundStyleBy;
6328
6900
  if (key !== void 0 && !series.includes(key)) series.push(key);
6329
6901
  }
6330
- const styleOf = (mark) => {
6902
+ const styleOf2 = (mark) => {
6331
6903
  if (mark.props.foregroundStyle !== void 0) return shapeStyle(mark.props.foregroundStyle);
6332
6904
  const key = mark.props.foregroundStyleBy;
6333
6905
  if (key !== void 0) return { name: PALETTE[series.indexOf(key) % PALETTE.length] };
@@ -6385,7 +6957,7 @@ function Chart(props) {
6385
6957
  );
6386
6958
  };
6387
6959
  const common = (mark) => {
6388
- const base = { style: styleOf(mark) };
6960
+ const base = { style: styleOf2(mark) };
6389
6961
  if (mark.props.opacity !== void 0) base.opacity = mark.props.opacity;
6390
6962
  return base;
6391
6963
  };
@@ -7358,6 +7930,19 @@ function KeyframeAnimator({
7358
7930
  return children(value);
7359
7931
  }
7360
7932
  export {
7933
+ AVAsset,
7934
+ AVAssetExportPreset1280x720,
7935
+ AVAssetExportPreset1920x1080,
7936
+ AVAssetExportPreset3840x2160,
7937
+ AVAssetExportPreset640x480,
7938
+ AVAssetExportPreset960x540,
7939
+ AVAssetExportPresetHighestQuality,
7940
+ AVAssetExportPresetLowQuality,
7941
+ AVAssetExportPresetMediumQuality,
7942
+ AVAssetExportPresetPassthrough,
7943
+ AVAssetExportSession,
7944
+ AVAssetImageGenerator,
7945
+ AVAssetTrack,
7361
7946
  AVAudioEngine,
7362
7947
  AVAudioFile,
7363
7948
  AVAudioInputNode,
@@ -7375,8 +7960,12 @@ export {
7375
7960
  AVAudioUnitReverb,
7376
7961
  AVAudioUnitSampler,
7377
7962
  AVAudioUnitTimePitch,
7963
+ AVFileType,
7964
+ AVMediaType,
7965
+ AVMutableComposition,
7378
7966
  AVPlayer,
7379
7967
  AVQueuePlayer,
7968
+ AVURLAsset,
7380
7969
  AngularGradient,
7381
7970
  AreaMark,
7382
7971
  AsyncImage,
@@ -7384,6 +7973,9 @@ export {
7384
7973
  AudioPlayer,
7385
7974
  BarMark,
7386
7975
  Button,
7976
+ CMTime,
7977
+ CMTimeRange,
7978
+ CameraPreview,
7387
7979
  Canvas,
7388
7980
  Capsule,
7389
7981
  Chart,
@@ -7555,6 +8147,7 @@ export {
7555
8147
  useSpeechSynthesis,
7556
8148
  useSystemLanguageModel,
7557
8149
  useUndoManager,
8150
+ useVideoRecorder,
7558
8151
  useViewEnvironment,
7559
8152
  useWakeLock,
7560
8153
  weighted,