@satinhq/kit 0.1.0-beta.1 → 0.1.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/NOTICE +2 -2
  2. package/README.md +1 -1
  3. package/dist/acknowledgements/inventory.json +5 -5
  4. package/dist/actions/RowActions.d.ts +2 -2
  5. package/dist/actions/RowActions.js +2 -2
  6. package/dist/app/deployment.js +8 -2
  7. package/dist/app/installCheck.js +4 -3
  8. package/dist/app/launchWatchdog.js +6 -1
  9. package/dist/controls/civilDate.d.ts +1 -1
  10. package/dist/controls/civilDate.js +1 -1
  11. package/dist/docs/components/file-importer.md +1 -1
  12. package/dist/docs/components/paste-button.md +1 -1
  13. package/dist/docs/components/photos-picker.md +1 -1
  14. package/dist/docs/components/share-link.md +1 -1
  15. package/dist/docs/limitations.md +1 -1
  16. package/dist/docs/push.md +3 -3
  17. package/dist/docs/reference.md +1870 -0
  18. package/dist/glass/GlassMaterialRoot.d.ts +3 -1
  19. package/dist/glass/GlassMaterialRoot.js +2 -2
  20. package/dist/glass/MaterialEffectRoot.js +5 -1
  21. package/dist/glass/errors.d.ts +3 -1
  22. package/dist/glass/errors.js +49 -3
  23. package/dist/glass/fallback/canvasOwnership.d.ts +7 -0
  24. package/dist/glass/fallback/canvasOwnership.js +23 -0
  25. package/dist/glass/fallback/material.js +3 -2
  26. package/dist/glass/fallback/regions.d.ts +1 -0
  27. package/dist/glass/fallback/regions.js +9 -12
  28. package/dist/glass/fallback/runtime.d.ts +2 -0
  29. package/dist/glass/fallback/runtime.js +15 -4
  30. package/dist/glass/lens/controlLift.d.ts +1 -1
  31. package/dist/glass/lens/controlLift.js +1 -1
  32. package/dist/glass/lens/interactivePress.d.ts +2 -2
  33. package/dist/glass/lens/interactivePress.js +2 -2
  34. package/dist/glass/lens/menu-opening-native-fits.json +1 -1
  35. package/dist/glass/lens/menuContextFit.json +1 -1
  36. package/dist/glass/lens/menuDropFit.json +1 -1
  37. package/dist/glass/lens/menuField.d.ts +2 -2
  38. package/dist/glass/lens/menuField.js +3 -3
  39. package/dist/glass/lens/menuGlyphDropFit.json +1 -1
  40. package/dist/glass/lens/menuMorphFit.json +1 -1
  41. package/dist/glass/lens/menuMorphPhoneFit.json +1 -1
  42. package/dist/glass/lens/tabAccessoryMotion.d.ts +1 -1
  43. package/dist/glass/lens/tabSearchMember.d.ts +1 -1
  44. package/dist/glass/lens/tabSearchMember.js +1 -1
  45. package/dist/glass/menuPresenter.js +1 -1
  46. package/dist/glass/regions.d.ts +1 -1
  47. package/dist/glass/regions.js +1 -1
  48. package/dist/glass/shapeTrack.d.ts +1 -1
  49. package/dist/glass/submenuMotion.d.ts +1 -1
  50. package/dist/glass/submenuMotion.js +1 -1
  51. package/dist/glass/webgpu/client.d.ts +3 -0
  52. package/dist/glass/webgpu/client.js +57 -9
  53. package/dist/glass/webgpu/entry.worker.js +3 -6
  54. package/dist/glass/webgpu/errorCode.js +3 -2
  55. package/dist/glass/webgpu/frameClock.d.ts +6 -2
  56. package/dist/glass/webgpu/frameClock.js +9 -2
  57. package/dist/glass/webgpu/frameProbe.d.ts +1 -1
  58. package/dist/glass/webgpu/frameProbe.js +3 -3
  59. package/dist/glass/webgpu/layers.d.ts +5 -1
  60. package/dist/glass/webgpu/layers.js +61 -6
  61. package/dist/glass/webgpu/minimize.js +20 -2
  62. package/dist/glass/webgpu/production.js +13 -1
  63. package/dist/glass/webgpu/raster.d.ts +18 -0
  64. package/dist/glass/webgpu/raster.js +105 -15
  65. package/dist/glass/webgpu/rasterMaterial.d.ts +4 -0
  66. package/dist/glass/webgpu/rasterMaterial.js +17 -1
  67. package/dist/glass/webgpu/regionScene.d.ts +4 -1
  68. package/dist/glass/webgpu/regionScene.js +30 -1
  69. package/dist/glass/webgpu/renderer.js +23 -7
  70. package/dist/glass/webgpu/sourcePixels.d.ts +4 -4
  71. package/dist/glass/webgpu/sourcePixels.js +48 -8
  72. package/dist/keyboard/adapter.js +2 -2
  73. package/dist/keyboard/avoidance.d.ts +1 -1
  74. package/dist/keyboard/avoidance.js +1 -1
  75. package/dist/keyboard/motion.d.ts +1 -1
  76. package/dist/keyboard/motion.js +1 -1
  77. package/dist/list-form/ListForm.d.ts +2 -2
  78. package/dist/list-form/ListForm.js +1 -1
  79. package/dist/list-form/ListItem.js +1 -1
  80. package/dist/list-form/RowLabel.d.ts +1 -1
  81. package/dist/list-form/RowLabel.js +1 -1
  82. package/dist/list-form/Slider.js +1 -1
  83. package/dist/list-form/editMotion.js +1 -1
  84. package/dist/list-form/editing.d.ts +5 -5
  85. package/dist/list-form/editing.js +1 -1
  86. package/dist/motion/animationValue.d.ts +2 -2
  87. package/dist/motion/animationValue.js +9 -10
  88. package/dist/motion/matchedGeometry.d.ts +4 -4
  89. package/dist/motion/matchedGeometry.js +7 -8
  90. package/dist/motion/springTrack.d.ts +1 -2
  91. package/dist/motion/springTrack.js +10 -5
  92. package/dist/navigation/ScreenContainer.js +1 -1
  93. package/dist/navigation/chromeCurves.js +1 -1
  94. package/dist/navigation/types.d.ts +1 -1
  95. package/dist/navigation/zoomGeometry.js +1 -1
  96. package/dist/navigation/zoomMotion.d.ts +1 -1
  97. package/dist/navigation/zoomMotion.js +2 -2
  98. package/dist/navigation/zoomTransition.js +3 -4
  99. package/dist/presentation/Presentation.d.ts +2 -2
  100. package/dist/presentation/Presentation.js +1 -1
  101. package/dist/presentation/presentationSource.js +1 -1
  102. package/dist/public/controls.d.ts +4 -4
  103. package/dist/public/controls.js +7 -7
  104. package/dist/public/glass.d.ts +3 -4
  105. package/dist/public/glass.js +1 -2
  106. package/dist/public/navigationTransition.d.ts +1 -1
  107. package/dist/public/navigationTransition.js +1 -1
  108. package/dist/public/scroll.d.ts +1 -1
  109. package/dist/public/scroll.js +1 -1
  110. package/dist/public/toolbar.js +1 -1
  111. package/dist/root/SatinRoot.js +9 -5
  112. package/dist/safe-area/SafeAreaInset.js +3 -3
  113. package/dist/safe-area/modifiers.js +2 -2
  114. package/dist/safe-area/useSafeAreaMetrics.js +2 -2
  115. package/dist/scroll/ScrollSurface.d.ts +5 -5
  116. package/dist/scroll/ScrollSurface.js +1 -1
  117. package/dist/scroll/position.js +1 -1
  118. package/dist/scroll/screenGeometry.d.ts +1 -1
  119. package/dist/scroll/screenGeometry.js +1 -1
  120. package/dist/search/SearchDrawer.js +1 -1
  121. package/dist/search/searchMotion.d.ts +1 -1
  122. package/dist/search/searchMotion.js +1 -1
  123. package/dist/search/useSearchPresentation.js +1 -1
  124. package/dist/shapes/corners.js +1 -1
  125. package/dist/styles.css +55 -34
  126. package/dist/sw/renderServiceWorker.js +12 -3
  127. package/dist/text-input/TextEditor.js +7 -3
  128. package/package.json +4 -4
  129. package/dist/docs/swiftui-reference.md +0 -1895
@@ -5,7 +5,7 @@ export type EditMode = 'inactive' | 'active';
5
5
  /** A row's identity in `List` selection: the `ForEach` id. */
6
6
  export type ListItemID = string | number;
7
7
  /**
8
- * Every navigation destination owns one edit mode, as a SwiftUI `NavigationStack` destination does:
8
+ * Every navigation destination owns one edit mode, for its retained entry:
9
9
  * its `EditButton` and its lists read and write the same value, and another screen keeps its own.
10
10
  */
11
11
  export declare function EditModeScope({ children }: {
@@ -25,14 +25,14 @@ export type ListSelectionModel = Readonly<{
25
25
  /** `single` selects one row (an optional binding); `multiple` collects a set. */
26
26
  mode: 'single' | 'multiple';
27
27
  isSelected(id: ListItemID): boolean;
28
- /** A row tap: replaces a single selection; toggles a set only while editing, as SwiftUI does. */
28
+ /** A row tap: replaces a single selection; toggles a set only while editing. */
29
29
  activate(id: ListItemID, editing: boolean): void;
30
30
  }>;
31
31
  export declare const ListSelectionContext: import("react").Context<Readonly<{
32
32
  /** `single` selects one row (an optional binding); `multiple` collects a set. */
33
33
  mode: 'single' | 'multiple';
34
34
  isSelected(id: ListItemID): boolean;
35
- /** A row tap: replaces a single selection; toggles a set only while editing, as SwiftUI does. */
35
+ /** A row tap: replaces a single selection; toggles a set only while editing. */
36
36
  activate(id: ListItemID, editing: boolean): void;
37
37
  }> | null>;
38
38
  /**
@@ -82,7 +82,7 @@ export declare function forEachRows<T>(props: ForEachProps<T>): ForEachRow[];
82
82
  /** What an editable row needs from its list item: edit state, selection and the item's actions. */
83
83
  export type ListItemState = Readonly<{
84
84
  editing: boolean;
85
- /** While editing with a selection, a row's own press selects it, as a SwiftUI list row does. */
85
+ /** While editing with a selection, a row's own press selects it, as a list row does. */
86
86
  pressSelects: boolean;
87
87
  select(): void;
88
88
  /** Rows that also declare actions hand them to the item, which owns the row's one gesture host. */
@@ -94,7 +94,7 @@ export type RowActionsDelegation = RowActionsProps & Readonly<{
94
94
  }>;
95
95
  export declare const ListItemContext: import("react").Context<Readonly<{
96
96
  editing: boolean;
97
- /** While editing with a selection, a row's own press selects it, as a SwiftUI list row does. */
97
+ /** While editing with a selection, a row's own press selects it, as a list row does. */
98
98
  pressSelects: boolean;
99
99
  select(): void;
100
100
  /** Rows that also declare actions hand them to the item, which owns the row's one gesture host. */
@@ -3,7 +3,7 @@ import { createContext, Fragment, useContext, useMemo, useState } from 'react';
3
3
  const inactiveEverywhere = ['inactive', () => { }];
4
4
  const EditModeContext = createContext(inactiveEverywhere);
5
5
  /**
6
- * Every navigation destination owns one edit mode, as a SwiftUI `NavigationStack` destination does:
6
+ * Every navigation destination owns one edit mode, for its retained entry:
7
7
  * its `EditButton` and its lists read and write the same value, and another screen keeps its own.
8
8
  */
9
9
  export function EditModeScope({ children }) {
@@ -4,14 +4,14 @@ declare const brand: unique symbol;
4
4
  export type Animation = Readonly<{
5
5
  [brand]: true;
6
6
  }>;
7
- /** Duration and bounce options for `Animation.spring`. */
7
+ /** Options for a spring defined by duration and bounce. */
8
8
  export type SpringOptions = Readonly<{
9
9
  /** The perceptual duration in milliseconds. Defaults to 500. */
10
10
  durationMs?: number;
11
11
  /** 0 settles without overshoot. Values are clamped to 0 through 0.99. Defaults to 0. */
12
12
  bounce?: number;
13
13
  }>;
14
- /** SwiftUI's named springs. `default` is the curve `withAnimation` uses with no argument. */
14
+ /** Named spring timing values. `default` is the curve `withAnimation` uses with no argument. */
15
15
  export declare const Animation: Readonly<{
16
16
  spring: (options?: SpringOptions) => Animation;
17
17
  smooth: Animation;
@@ -1,8 +1,8 @@
1
1
  const brand = Symbol('satin.animation');
2
2
  /**
3
- * SwiftUI's `spring(duration:bounce:)` mapping, as Apple documents it: the duration is the
3
+ * Duration is the
4
4
  * period of the undamped spring (omega = 2π / duration), and bounce is 1 − damping ratio for
5
- * bounce ≥ 0. SwiftUI's negative (over-damped) bounces are not offered: the shared spring
5
+ * bounce ≥ 0. Negative, over-damped bounces are not offered: the shared spring
6
6
  * solver (glass/lens/menuSpring.ts) models damping ratios up to 1.
7
7
  */
8
8
  function spring({ durationMs = 500, bounce = 0 } = {}) {
@@ -15,24 +15,23 @@ function spring({ durationMs = 500, bounce = 0 } = {}) {
15
15
  const animation = Object.freeze({ [brand]: true, omega, zeta });
16
16
  return animation;
17
17
  }
18
- /** SwiftUI's named springs. `default` is the curve `withAnimation` uses with no argument. */
18
+ /** Named spring timing values. `default` is the curve `withAnimation` uses with no argument. */
19
19
  export const Animation = Object.freeze({
20
20
  spring,
21
- /** SwiftUI `.smooth`: duration 0.5 s, bounce 0. */
21
+ /** Smooth spring: duration 0.5 s, bounce 0. */
22
22
  smooth: spring({ durationMs: 500, bounce: 0 }),
23
- /** SwiftUI `.snappy`: duration 0.5 s, bounce 0.15. */
23
+ /** Snappy spring: duration 0.5 s, bounce 0.15. */
24
24
  snappy: spring({ durationMs: 500, bounce: 0.15 }),
25
- /** SwiftUI `.bouncy`: duration 0.5 s, bounce 0.3. */
25
+ /** Bouncy spring: duration 0.5 s, bounce 0.3. */
26
26
  bouncy: spring({ durationMs: 500, bounce: 0.3 }),
27
- /** SwiftUI `.default`. The value is the measured fit recorded in the measured reference
27
+ /** Default spring. The value is the measured fit recorded in the measured reference
28
28
  * (matchedGeometryEffect, October 11); see DEFAULT_ANIMATION_DURATION_MS. */
29
29
  get default() {
30
30
  return defaultAnimation;
31
31
  },
32
32
  });
33
- /* The perceptual duration of SwiftUI's `.default` animation on iOS 27, which is a critically
34
- * damped spring. Source: the stock SwiftUI matchedGeometryEffect fixture recorded by the
35
- * matched-geometry implementation (the measured reference, October 11). */
33
+ /* Default perceptual duration for the critically damped animation spring. The measured
34
+ * fit and recording provenance live in the measured reference. */
36
35
  export const DEFAULT_ANIMATION_DURATION_MS = 500;
37
36
  const defaultAnimation = spring({ durationMs: DEFAULT_ANIMATION_DURATION_MS, bounce: 0 });
38
37
  /** The spring with a unit displacement and the given initial velocity (units per second). */
@@ -27,22 +27,22 @@ export type MatchedGeometryEffectProps = Readonly<{
27
27
  children: ReactNode;
28
28
  }>;
29
29
  /**
30
- * SwiftUI `matchedGeometryEffect(id:in:isSource:)`: when a `withAnimation` change removes this
30
+ * When a `withAnimation` change removes this
31
31
  * view and inserts another with the same id, the new one moves from the old one's frame to its
32
32
  * own while the two cross-fade. A view that only moves or resizes
33
33
  * animates from where it was. A matched view with no partner fades in or out.
34
34
  *
35
35
  * Wrap one element that has no transform of its own. Changes made outside `withAnimation`
36
- * apply at once, as in SwiftUI.
36
+ * apply at once.
37
37
  * @example
38
38
  * const art = useNamespace()
39
39
  * <MatchedGeometryEffect id="artwork" in={art}><Image src={cover} alt="" /></MatchedGeometryEffect>
40
- * @experimental Frame matching only (SwiftUI's default `properties: .frame`, `anchor: .center`).
40
+ * @experimental Matches the frame around its center only.
41
41
  * A non-source view does not follow its source outside an animation.
42
42
  */
43
43
  export declare function MatchedGeometryEffect({ id, in: namespace, isSource, children, }: MatchedGeometryEffectProps): import("react").JSX.Element;
44
44
  /**
45
- * `withAnimation` applies the state changes in `update`, then animates the
45
+ * Applies the state changes in `update`, then animates the
46
46
  * `MatchedGeometryEffect` views they move, insert or remove. Call it from an event handler.
47
47
  * Keyed List rows also move to their new positions. Other views take their new layout at once.
48
48
  * Returns the callback result and propagates errors. Only synchronous updates participate.
@@ -17,9 +17,8 @@ import { fadeSettleMs, runFrameTimes, sampleChannel, sampleProgress, } from './s
17
17
  *
18
18
  * A view whose id leaves and enters in the same change keeps its identity: the new view starts
19
19
  * from the old one's frame and fades in, and the old view stays on screen as a "ghost" (its own
20
- * detached DOM node, already decoded) that travels with it and fades out. That cross-fade is
21
- * SwiftUI's: a stock iOS 27 fixture shows both views on one frame, with opacities that sum to 1
22
- * on the animation's own spring (the measured reference, October 11).
20
+ * detached DOM node, already decoded) that travels with it and fades out. Their opacities sum to 1
21
+ * throughout the spring-driven cross-fade. Measurement provenance lives in the measured reference.
23
22
  */
24
23
  const namespaceBrand = Symbol('satin.namespace');
25
24
  const entries = new Set();
@@ -40,17 +39,17 @@ export function defineNamespace() {
40
39
  return Object.freeze({ [namespaceBrand]: true });
41
40
  }
42
41
  /**
43
- * SwiftUI `matchedGeometryEffect(id:in:isSource:)`: when a `withAnimation` change removes this
42
+ * When a `withAnimation` change removes this
44
43
  * view and inserts another with the same id, the new one moves from the old one's frame to its
45
44
  * own while the two cross-fade. A view that only moves or resizes
46
45
  * animates from where it was. A matched view with no partner fades in or out.
47
46
  *
48
47
  * Wrap one element that has no transform of its own. Changes made outside `withAnimation`
49
- * apply at once, as in SwiftUI.
48
+ * apply at once.
50
49
  * @example
51
50
  * const art = useNamespace()
52
51
  * <MatchedGeometryEffect id="artwork" in={art}><Image src={cover} alt="" /></MatchedGeometryEffect>
53
- * @experimental Frame matching only (SwiftUI's default `properties: .frame`, `anchor: .center`).
52
+ * @experimental Matches the frame around its center only.
54
53
  * A non-source view does not follow its source outside an animation.
55
54
  */
56
55
  export function MatchedGeometryEffect({ id, in: namespace, isSource = true, children, }) {
@@ -226,13 +225,13 @@ function moveView(element, key, from, box, animation) {
226
225
  target: box,
227
226
  velocity: from.velocity,
228
227
  animation,
229
- // A new partner fades in (SwiftUI's default .opacity insertion); a view that stays carries
228
+ // A new partner fades in through the default opacity transition; a view that stays carries
230
229
  // on from the opacity it is showing.
231
230
  opacity: { from: stayed ? from.opacity : 0, to: 1 },
232
231
  ghost: false,
233
232
  });
234
233
  }
235
- /** SwiftUI's default `.opacity` transition for a matched view with no partner. */
234
+ /** An opacity transition for a matched view with no partner. */
236
235
  function fadeView(element, key, box, animation, opacity) {
237
236
  startRun({
238
237
  element,
@@ -1,6 +1,5 @@
1
1
  import { type MenuSpring } from '../glass/lens/menuSpring.js';
2
- /** A measured spring: SwiftUI's `.interpolatingSpring(stiffness:damping:)` with unit mass, released
3
- * `delay` ms after the animation starts. */
2
+ /** A unit-mass spring defined by stiffness and damping, released after an optional delay in ms. */
4
3
  export type SpringParameters = Readonly<{
5
4
  stiffness: number;
6
5
  damping: number;
@@ -28,19 +28,22 @@ export function sampledKeyframes(track, frame) {
28
28
  return track.progress.map((progress, index) => ({ ...frame(progress), offset: index / last }));
29
29
  }
30
30
  export function sampledMotionTrack(duration, sample, tolerance = 0.2) {
31
- if (!(duration > 0) || !Number.isFinite(duration) || !(tolerance > 0) || !Number.isFinite(tolerance))
31
+ if (!(duration > 0) ||
32
+ !Number.isFinite(duration) ||
33
+ !(tolerance > 0) ||
34
+ !Number.isFinite(tolerance))
32
35
  throw Error('Motion tracks require a finite positive duration and tolerance');
33
36
  let count = Math.max(1, Math.ceil(duration / (1000 / 120)));
34
37
  // GPU and compositor consumers require even offsets. Refine the whole grid,
35
38
  // rather than giving them independent adaptive sample positions.
36
39
  while (count <= 4096) {
37
- const values = Array.from({ length: count + 1 }, (_, index) => sample(duration * index / count));
40
+ const values = Array.from({ length: count + 1 }, (_, index) => sample((duration * index) / count));
38
41
  if (!values.every(Number.isFinite))
39
42
  throw Error('Motion track contains a non-finite value');
40
43
  let accurate = true;
41
44
  for (let index = 0; accurate && index < count; index++) {
42
45
  for (const fraction of [0.25, 0.5, 0.75]) {
43
- const exact = sample(duration * (index + fraction) / count);
46
+ const exact = sample((duration * (index + fraction)) / count);
44
47
  const linear = values[index] + (values[index + 1] - values[index]) * fraction;
45
48
  if (!Number.isFinite(exact))
46
49
  throw Error('Motion track contains a non-finite value');
@@ -59,11 +62,13 @@ export function sampledMotionTrack(duration, sample, tolerance = 0.2) {
59
62
  /** Read the submitted segment, including its velocity, when a run is interrupted. */
60
63
  export function sampledMotionState(track, milliseconds) {
61
64
  const last = track.values.length - 1;
62
- const position = Math.max(0, Math.min(last, milliseconds / track.duration * last));
65
+ const position = Math.max(0, Math.min(last, (milliseconds / track.duration) * last));
63
66
  const index = Math.min(last - 1, Math.floor(position));
64
67
  const from = track.values[index], to = track.values[index + 1];
65
68
  return {
66
69
  value: from + (to - from) * (position - index),
67
- velocity: milliseconds < 0 || milliseconds >= track.duration ? 0 : (to - from) * last / track.duration * 1000,
70
+ velocity: milliseconds < 0 || milliseconds >= track.duration
71
+ ? 0
72
+ : (((to - from) * last) / track.duration) * 1000,
68
73
  };
69
74
  }
@@ -130,7 +130,7 @@ export function ScreenContainer({ children, entry, interactionBlocked = false, c
130
130
  }), [entry.key, entry.params, entry.transition.phase, isCovered, scrollRegistry, tabActive]);
131
131
  useLayoutEffect(() => navigator[navigatorScrollRuntime].bind(entry.key, scrollRegistry), [entry.key, navigator, scrollRegistry]);
132
132
  // Without a primary scroll view, content is laid out in the safe area below the bar, as a
133
- // SwiftUI VStack is (styles.css). The attribute follows the registry in the same frame.
133
+ // VStack is (styles.css). The attribute follows the registry in the same frame.
134
134
  // The primary's vertical scroll indicator is drawn in the screen frame (scroll/scrollIndicator.ts).
135
135
  const motionLayer = useRef(null);
136
136
  const indicatorTrack = useRef(null);
@@ -1,7 +1,7 @@
1
1
  // Measured iOS 27 navigation bar chrome curves (what bar items do while a page moves) and the
2
2
  // keyframe builders that turn them into Web Animations keyframes. Pure data and math: no DOM.
3
3
  /** Incoming bar chrome, measured on the iOS 27.0 Simulator from the presentation layers of stock
4
- * SwiftUI (every display frame,
4
+ * Measured at every display frame,
5
5
  * light and dark agree within 0.04): milliseconds since the page starts moving. A push brings the
6
6
  * destination's glass items in faster than a pop brings the revealed screen's back: a push shows
7
7
  * 0.56 at 100 ms (Reminders-style list → list, back circle and trailing "…" together), a pop 0.30
@@ -16,7 +16,7 @@ export type NavigationBarDefinition<Params = unknown> = Readonly<{
16
16
  display?: TitleDisplayMode;
17
17
  visibility?: Visibility;
18
18
  title: string | ((params: Params) => string);
19
- /** SwiftUI `navigationSubtitle`: a line under the title, inline or large. */
19
+ /** A line under the title, inline or large. */
20
20
  subtitle?: string;
21
21
  }>;
22
22
  export type TransitionControllerContext = Readonly<{
@@ -1,5 +1,5 @@
1
1
  /*
2
- * The geometry of SwiftUI's zoom navigation transition, as pure functions of a rectangle.
2
+ * Zoom navigation geometry, as pure functions of a rectangle.
3
3
  *
4
4
  * At every moment the pushed screen is shown inside a rounded "window" rectangle that moves between
5
5
  * the source view's frame and the full screen. As natively, the screen's content is scaled so its
@@ -9,7 +9,7 @@ export type ZoomSprings = Readonly<{
9
9
  size: ZoomSpring;
10
10
  }>;
11
11
  /**
12
- * Push: the centre and the width move together, critically damped (half way at 88 ms; SwiftUI's
12
+ * Push: the centre and the width move together, critically damped (half way at 88 ms; the measured
13
13
  * duration 0.33 s); the height lags slightly. Native moves the width with the centre, so the
14
14
  * width shares the centre's spring here and the height takes the size spring (rms 0.002, 51 ticks).
15
15
  */
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Measured values of SwiftUI's `.navigationTransition(.zoom(sourceID:in:))` on iOS 27.
2
+ * Measured spring values for the zoom navigation transition on iOS 27.
3
3
  *
4
4
  * native reference: a stock native fixture (a 180 pt tile with a 10 pt corner in a
5
5
  * NavigationStack grid, pushed to a full-screen page), iPhone 17 Pro Max Simulator, iOS 27.0.
@@ -10,7 +10,7 @@
10
10
  * (the measured reference, October 11).
11
11
  */
12
12
  /**
13
- * Push: the centre and the width move together, critically damped (half way at 88 ms; SwiftUI's
13
+ * Push: the centre and the width move together, critically damped (half way at 88 ms; the measured
14
14
  * duration 0.33 s); the height lags slightly. Native moves the width with the centre, so the
15
15
  * width shares the centre's spring here and the height takes the size spring (rms 0.002, 51 ticks).
16
16
  */
@@ -1,6 +1,5 @@
1
1
  /*
2
- * SwiftUI's zoom navigation transition: `matchedTransitionSource(id:in:)` on the view a push starts
3
- * from, and `.navigationTransition(.zoom(sourceID:in:))` on the pushed screen.
2
+ * Zoom navigation connects a MatchedTransitionSource to a screen configured with a zoom transition.
4
3
  *
5
4
  * A push whose screen asks for a zoom, and whose source is on screen in the screen it covers, grows
6
5
  * out of the source instead of sliding in. Its pop (the back button) and a downward drag on the
@@ -116,7 +115,7 @@ export function visibleZoomSource(entryKey, rear, excluded) {
116
115
  }
117
116
  return chosen?.view;
118
117
  }
119
- /** SwiftUI zooms only from a source the user can see: at least part of it in the viewport. */
118
+ /** Zoom transitions require a visible source: at least part of it in the viewport. */
120
119
  function isOnScreen(view) {
121
120
  const bounds = view.getBoundingClientRect();
122
121
  if (!(bounds.width > 0 && bounds.height > 0))
@@ -950,7 +949,7 @@ export function windowKeyframes(from, to, springs, velocity) {
950
949
  frames.push({ ms: frames.at(-1).ms + FRAME_MS, rect: to });
951
950
  return frames;
952
951
  }
953
- /** A measured Core Animation spring as Satin's `Animation` (SwiftUI's duration/bounce mapping is
952
+ /** A measured Core Animation spring as Satin's `Animation` (the duration/bounce representation is
954
953
  * exact: omega = 2π / duration, damping ratio = 1 − bounce). */
955
954
  function animationOf(spring) {
956
955
  return SpringAnimation.spring({
@@ -19,7 +19,7 @@ export type SheetProps = PresentationProps & {
19
19
  * content that fills the card (Now Playing). A modal NavigationStack root's title is drawn by
20
20
  * its navigation bar either way. */
21
21
  titleVisibility?: 'visible' | 'hidden';
22
- /** SwiftUI's `presentationDragIndicator`. `automatic` shows it only when there is more than one
22
+ /** Control the sheet drag indicator. `automatic` shows it only when there is more than one
23
23
  * detent to drag between, as native does. */
24
24
  dragIndicator?: 'automatic' | 'visible' | 'hidden';
25
25
  };
@@ -27,7 +27,7 @@ export declare function Sheet(props: SheetProps): import("react").JSX.Element |
27
27
  export type FullScreenCoverProps = PresentationProps & {
28
28
  children: ReactNode;
29
29
  };
30
- /** SwiftUI's `fullScreenCover`: an opaque presentation over the whole screen that slides up from
30
+ /** An opaque presentation over the whole screen that slides up from
31
31
  * the bottom edge. It has no grabber and no interactive dismissal; close it with its binding or
32
32
  * `useDismiss()`. Its title labels the dialog for assistive technology and is not drawn. */
33
33
  export declare function FullScreenCover({ children, ...props }: FullScreenCoverProps): import("react").JSX.Element | null;
@@ -23,7 +23,7 @@ export function Sheet(props) {
23
23
  props.onDismiss?.();
24
24
  } })) : null;
25
25
  }
26
- /** SwiftUI's `fullScreenCover`: an opaque presentation over the whole screen that slides up from
26
+ /** An opaque presentation over the whole screen that slides up from
27
27
  * the bottom edge. It has no grabber and no interactive dismissal; close it with its binding or
28
28
  * `useDismiss()`. Its title labels the dialog for assistive technology and is not drawn. */
29
29
  export function FullScreenCover({ children, ...props }) {
@@ -1,7 +1,7 @@
1
1
  /** The control a presentation grows from when the app names no anchor. Native iOS 27 presents
2
2
  * `confirmationDialog` as a popover from the view that asked for it: a swipe action's dialog points
3
3
  * at its row, a toolbar button's dialog grows out of the button. Satin's natural structure attaches
4
- * the dialog at screen level, so the pressed control stands in for SwiftUI's attachment point. */
4
+ * the dialog at screen level, so the pressed control supplies the attachment point. */
5
5
  const lastPress = new WeakMap();
6
6
  const tracked = new WeakSet();
7
7
  // A press older than this did not open the dialog (a timer or a data change did).
@@ -112,7 +112,7 @@ export type ButtonProps = AriaAttributes & RowInteractionProps & {
112
112
  iconOnly?: false;
113
113
  });
114
114
  /**
115
- * SwiftUI `Button`. It adapts to where it is: a tinted row in a `List`, a glass button in a
115
+ * A pressable control that adapts to where it is: a tinted row in a `List`, a glass button in a
116
116
  * toolbar, otherwise the `buttonStyle` you choose. `role="destructive"` carries into every context.
117
117
  *
118
118
  * @example
@@ -291,7 +291,7 @@ export type VerticalTextFieldProps = Omit<TextareaHTMLAttributes<HTMLTextAreaEle
291
291
  /** A single-line field, or with `axis="vertical"` a field that grows to its `lineLimit`. */
292
292
  export type TextFieldProps = HorizontalTextFieldProps | VerticalTextFieldProps;
293
293
  /**
294
- * SwiftUI `TextField`: controlled text with a label, an optional `prompt` and keyboard intent from
294
+ * Single-line controlled text with a label, an optional `prompt` and keyboard intent from
295
295
  * `kind`. In a `List` or `Form` it becomes the row; elsewhere it draws a standalone field.
296
296
  * `axis="vertical"` grows with its content.
297
297
  *
@@ -314,7 +314,7 @@ export type SecureFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type
314
314
  prompt?: string;
315
315
  textFieldStyle?: TextFieldStyle;
316
316
  };
317
- /** SwiftUI `SecureField`: password entry with secure system input, adapting to a row in a form. */
317
+ /** Password entry with secure system input, adapting to a row in a form. */
318
318
  export declare const SecureField: import("react").ForwardRefExoticComponent<Omit<InputHTMLAttributes<HTMLInputElement>, "defaultValue" | "onChange" | "onSubmit" | "placeholder" | "size" | "type" | "value"> & {
319
319
  label: string;
320
320
  value: string;
@@ -336,7 +336,7 @@ export type TextEditorProps = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>,
336
336
  prompt?: string;
337
337
  textFieldStyle?: TextFieldStyle;
338
338
  };
339
- /** SwiftUI `TextEditor`: multiline text, adapting to a row in a form. */
339
+ /** Multiline text entry, adapting to a row in a form. */
340
340
  export declare const TextEditor: import("react").ForwardRefExoticComponent<Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "defaultValue" | "onChange" | "placeholder" | "rows" | "value"> & {
341
341
  value: string;
342
342
  onChange(value: string): void;
@@ -52,7 +52,7 @@ export function Form(props) {
52
52
  return (_jsx(List, { ...props, listStyle: "insetGrouped", scrollDismissesKeyboard: props.scrollDismissesKeyboard ?? 'immediately' }));
53
53
  }
54
54
  /**
55
- * SwiftUI `Button`. It adapts to where it is: a tinted row in a `List`, a glass button in a
55
+ * A pressable control that adapts to where it is: a tinted row in a `List`, a glass button in a
56
56
  * toolbar, otherwise the `buttonStyle` you choose. `role="destructive"` carries into every context.
57
57
  *
58
58
  * @example
@@ -72,9 +72,9 @@ export const Button = forwardRef(function Button({ children, symbol, role, butto
72
72
  const resolvedBorderShape = resolveButtonBorderShape(buttonBorderShape);
73
73
  const [isPressed, setPressed] = useState(false);
74
74
  // A toolbar button labelled with an icon-only Label or a SymbolImage is a glass circle, and so is a
75
- // titled button with a symbol: SwiftUI's `Button("New Reminder", systemImage: "plus")` in a toolbar
75
+ // A titled toolbar button with a symbol uses a circular control.
76
76
  // draws only its symbol (iOS 27.0 native reference), the title becoming its accessible name. A bare symbol
77
- // with no title is the same circle (SwiftUI's `Button { } label: { Image(systemName:) }`).
77
+ // An icon-only toolbar button uses the same circle.
78
78
  const symbolTitle = toolbar && symbol && typeof children === 'string' && !rest.iconOnly ? children : undefined;
79
79
  const bareSymbolName = toolbar && symbol && (children == null || typeof children === 'boolean') && !rest.iconOnly
80
80
  ? rest['aria-label']?.trim() || symbol
@@ -236,7 +236,7 @@ function toggleLabel(props, descriptionId) {
236
236
  * options={[{ id: 'home', label: 'Home', symbol: 'house', color: systemColor('red') }]} />
237
237
  */
238
238
  export function Picker({ pickerStyle = 'menu', selection, onSelectionChange, disabled, ...rest }) {
239
- // `ControlStyleProvider disabled` disables a Picker as SwiftUI's `.disabled` does a subtree.
239
+ // `ControlStyleProvider disabled` disables a Picker throughout its subtree.
240
240
  const configuration = useControlStyle({ disabled });
241
241
  const props = {
242
242
  ...rest,
@@ -276,7 +276,7 @@ const Field = forwardRef(function TextField({ prompt, textFieldStyle, style, ...
276
276
  return (_jsx(Fields.TextField, { ...fieldProps, style: style, ref: ref, placeholder: prompt ?? (hidden ? props.label : undefined), variant: variant, labelVisibility: hidden ? 'hidden' : 'visible' }));
277
277
  });
278
278
  /**
279
- * SwiftUI `TextField`: controlled text with a label, an optional `prompt` and keyboard intent from
279
+ * Single-line controlled text with a label, an optional `prompt` and keyboard intent from
280
280
  * `kind`. In a `List` or `Form` it becomes the row; elsewhere it draws a standalone field.
281
281
  * `axis="vertical"` grows with its content.
282
282
  *
@@ -285,12 +285,12 @@ const Field = forwardRef(function TextField({ prompt, textFieldStyle, style, ...
285
285
  */
286
286
  // The cast keeps single-line refs and events precisely typed when axis is omitted.
287
287
  export const TextField = Field;
288
- /** SwiftUI `SecureField`: password entry with secure system input, adapting to a row in a form. */
288
+ /** Password entry with secure system input, adapting to a row in a form. */
289
289
  export const SecureField = forwardRef(function SecureField({ prompt, textFieldStyle, style, ...props }, ref) {
290
290
  const variant = useFieldVariant(textFieldStyle);
291
291
  return (_jsx(Fields.SecureField, { ...props, style: style, ref: ref, placeholder: prompt ?? (variant !== 'standalone' ? props.label : undefined), variant: variant, labelVisibility: prompt !== undefined || variant !== 'standalone' ? 'hidden' : 'visible' }));
292
292
  });
293
- /** SwiftUI `TextEditor`: multiline text, adapting to a row in a form. */
293
+ /** Multiline text entry, adapting to a row in a form. */
294
294
  export const TextEditor = forwardRef(function TextEditor({ prompt, textFieldStyle, style, ...props }, ref) {
295
295
  const variant = useFieldVariant(textFieldStyle);
296
296
  return (_jsx(Editor, { ...props, minLines: props.minLines ?? (variant === 'row' ? 1 : 3), style: style, ref: ref, placeholder: prompt ?? (variant === 'plain' ? props.label : undefined), variant: variant, labelVisibility: prompt !== undefined || variant !== 'standalone' ? 'hidden' : 'visible' }));
@@ -14,14 +14,13 @@ export type GlassEffectProps = HTMLAttributes<HTMLDivElement> & {
14
14
  in?: NamespaceID;
15
15
  /** Morph identity within namespace; independent of the DOM id. */
16
16
  glassEffectID?: string;
17
- /** `glassEffectUnion(id:namespace:)`: effects with one union id draw as one shape. */
17
+ /** Effects with one union id draw as one shape. */
18
18
  glassEffectUnion?: string;
19
- /** `glassEffectTransition`. */
19
+ /** Controls how an identified effect enters or leaves its container. */
20
20
  glassEffectTransition?: 'matchedGeometry' | 'materialize' | 'identity';
21
21
  };
22
22
  /**
23
- * `glassEffect(_:in:)`: draws Liquid Glass behind its content in the given shape (a
24
- * capsule by default). Inside a `GlassEffectContainer`, effects with ids morph into one another.
23
+ * Draws glass behind its content in the given shape, using a capsule by default. Inside a `GlassEffectContainer`, effects with ids morph into one another.
25
24
  *
26
25
  * @example
27
26
  * <GlassEffect glass={Glass.regular.interactive()} shape="circle">
@@ -3,8 +3,7 @@ import { Glass, resolveGlass } from './glassValue.js';
3
3
  import { GlassFieldContext } from './fieldContext.js';
4
4
  import { GlassEffect as Effect } from '../glass/GlassEffect.js';
5
5
  /**
6
- * `glassEffect(_:in:)`: draws Liquid Glass behind its content in the given shape (a
7
- * capsule by default). Inside a `GlassEffectContainer`, effects with ids morph into one another.
6
+ * Draws glass behind its content in the given shape, using a capsule by default. Inside a `GlassEffectContainer`, effects with ids morph into one another.
8
7
  *
9
8
  * @example
10
9
  * <GlassEffect glass={Glass.regular.interactive()} shape="circle">
@@ -17,7 +17,7 @@ export type ZoomTransitionOptions = Readonly<{
17
17
  sourceID: string;
18
18
  in: NamespaceID;
19
19
  }>;
20
- /** SwiftUI's navigation transitions. */
20
+ /** Transitions between retained navigation entries. */
21
21
  export declare const NavigationTransition: Readonly<{
22
22
  automatic: NavigationTransition;
23
23
  zoom: (options: ZoomTransitionOptions) => NavigationTransition;
@@ -4,7 +4,7 @@ import { NavigatorContext } from '../navigation/navigationContext.js';
4
4
  import { installZoomDismissDrag } from '../navigation/zoomDrag.js';
5
5
  import { registerTransitionSource, registerZoomRequest } from '../navigation/zoomTransition.js';
6
6
  const brand = Symbol('satin.navigationTransition');
7
- /** SwiftUI's navigation transitions. */
7
+ /** Transitions between retained navigation entries. */
8
8
  export const NavigationTransition = Object.freeze({
9
9
  /** The platform's push: the screen slides in from the trailing edge. */
10
10
  automatic: Object.freeze({ [brand]: true, kind: 'automatic' }),
@@ -34,5 +34,5 @@ export type ScrollViewProps = Omit<HTMLAttributes<HTMLDivElement>, 'id'> & {
34
34
  scrollPosition?: never;
35
35
  onScrollGeometryChange?: never;
36
36
  });
37
- /** SwiftUI ScrollView with framework-owned restoration and native scrolling. */
37
+ /** A scroll surface with framework-owned restoration and native scrolling. */
38
38
  export declare const ScrollView: import("react").ForwardRefExoticComponent<ScrollViewProps & import("react").RefAttributes<ScrollViewHandle>>;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
3
  import { ScrollSurface } from '../scroll/ScrollSurface.js';
4
- /** SwiftUI ScrollView with framework-owned restoration and native scrolling. */
4
+ /** A scroll surface with framework-owned restoration and native scrolling. */
5
5
  export const ScrollView = forwardRef(function ScrollView(props, ref) {
6
6
  return _jsx(ScrollSurface, { ...props, ref: ref });
7
7
  });
@@ -69,7 +69,7 @@ export function ToolbarSpacer({ sizing = 'flexible', placement = 'automatic', })
69
69
  /** `ToolbarItemGroup`: its items render as consecutive `ToolbarItem`s of the placement,
70
70
  * which share one glass capsule (iOS 26+ draws a group and adjacent items identically). */
71
71
  export function ToolbarItemGroup({ children, placement }) {
72
- // SwiftUI gives confirmation actions separate capsules, even inside a group.
72
+ // gives confirmation actions separate capsules, even inside a group.
73
73
  if (placement === 'confirmationAction')
74
74
  return (_jsx(ToolbarItem, { placement: placement, children: _jsx("div", { "data-satin-toolbar-confirmation-group": "true", children: children }) }));
75
75
  return (_jsx(_Fragment, { children: Children.toArray(children).map((child, index) => (_jsx(ToolbarItem, { placement: placement, children: child }, isValidElement(child) && child.key !== null ? child.key : index))) }));
@@ -159,6 +159,7 @@ export function SatinRoot({ background: legacyBackground, app: suppliedApp, offl
159
159
  const downloadedBytes = useRef(0);
160
160
  const [launchAttempt, setLaunchAttempt] = useState(0);
161
161
  const [glassReady, setGlassReady] = useState(false);
162
+ const [bundlePrepared, setBundlePrepared] = useState(import.meta.env.DEV || !app);
162
163
  const [glassError, setGlassError] = useState(null);
163
164
  const [glassFailed, setGlassFailed] = useState(false);
164
165
  useLayoutEffect(() => {
@@ -235,10 +236,13 @@ export function SatinRoot({ background: legacyBackground, app: suppliedApp, offl
235
236
  downloadActivity.current = performance.now();
236
237
  downloadedBytes.current = progress.loaded;
237
238
  }
238
- // Release launch rings confirm updates; first-install bytes
239
- // still drive readiness/watchdogs without promising an update.
240
- if (app.updateProfile !== 'release')
241
- setLaunchProgress(progress);
239
+ // First-install bytes describe a real download in every profile.
240
+ // Applied-update confirmation remains a separate successor phase.
241
+ setLaunchProgress(progress);
242
+ }).then(ready => {
243
+ if (!cancelled && ready)
244
+ setBundlePrepared(true);
245
+ return ready;
242
246
  }),
243
247
  prepareLaunchContent(rootRef.current, contentPreparation.signal),
244
248
  ]);
@@ -489,7 +493,7 @@ export function SatinRoot({ background: legacyBackground, app: suppliedApp, offl
489
493
  return;
490
494
  return startDevFidelityAssertions(initialHistoryLength.current);
491
495
  }, []);
492
- return (_jsx(RuntimeContext, { value: runtime, children: _jsxs(MediaPlayerRoot, { onActivityChange: onMediaActivityChange, children: [_jsx("div", { ...props, ref: rootRef, className: className, "data-satin-role": "chrome", "data-satin-root": "true", "data-satin-launch": launch, "data-satin-presentation": presentationPending ? 'preparing' : 'ready', inert: launch !== 'ready' || presentationPending, "data-satin-layout-horizontal": nextLayout.layoutClass.horizontal, "data-satin-layout-vertical": nextLayout.layoutClass.vertical, style: geometryStyles, children: _jsx(AdaptiveTabsContext, { value: tabs, children: _jsx("div", { "data-satin-status-band-source": "scene", children: _jsxs(GlassMaterialRoot, { displacement: glassDisplacement, onLaunchReady: setGlassReady, children: [DeveloperMenu && app && launch === 'ready' ? (_jsx(Suspense, { fallback: null, children: _jsx(DeveloperMenu, {}) })) : null, app ? (_jsx(OrientationGuard, { app: app, width: metrics.safeFrame.width, height: metrics.safeFrame.height, children: _jsx(ReachabilityProvider, { profile: app, offlinePresentation: offlinePresentation, children: children }) })) : (children)] }) }) }) }), launch === 'failed'
496
+ return (_jsx(RuntimeContext, { value: runtime, children: _jsxs(MediaPlayerRoot, { onActivityChange: onMediaActivityChange, children: [_jsx("div", { ...props, ref: rootRef, className: className, "data-satin-role": "chrome", "data-satin-root": "true", "data-satin-launch": launch, "data-satin-presentation": presentationPending ? 'preparing' : 'ready', inert: launch !== 'ready' || presentationPending, "data-satin-layout-horizontal": nextLayout.layoutClass.horizontal, "data-satin-layout-vertical": nextLayout.layoutClass.vertical, style: geometryStyles, children: _jsx(AdaptiveTabsContext, { value: tabs, children: _jsx("div", { "data-satin-status-band-source": "scene", children: _jsxs(GlassMaterialRoot, { displacement: glassDisplacement, prepared: bundlePrepared, onLaunchReady: setGlassReady, children: [DeveloperMenu && app && launch === 'ready' ? (_jsx(Suspense, { fallback: null, children: _jsx(DeveloperMenu, {}) })) : null, app ? (_jsx(OrientationGuard, { app: app, width: metrics.safeFrame.width, height: metrics.safeFrame.height, children: _jsx(ReachabilityProvider, { profile: app, offlinePresentation: offlinePresentation, children: children }) })) : (children)] }) }) }) }), launch === 'failed'
493
497
  ? createPortal(_jsxs("div", { "data-satin-role": "chrome", "data-satin-launch-error": "true", "data-satin-glass-error": glassError ? true : undefined, role: "alert", style: { zIndex: launchErrorLayer }, children: [!glassError ? (_jsx("img", { className: "satin-launch-error-icon", src: document.querySelector('#satin-launch-screen .satin-launch-icon img')?.src ??
494
498
  app?.launchScreen?.icon ??
495
499
  app?.icons[0]?.src, alt: "" })) : null, _jsxs("div", { className: "satin-launch-error-message", children: [_jsx("h1", { children: glassError ? 'Glass Renderer Failed' : 'Unable to Open App' }), glassError ? (_jsx("pre", { style: { whiteSpace: 'pre-wrap', overflowWrap: 'anywhere' }, children: `${glassError.code} · ${glassError.stage}\n${glassError.message}${glassError.reason ? `\n${glassError.reason}` : ''}` })) : (_jsx("p", { children: preparationError })), _jsx("button", { type: "button", onClick: async () => {