@satinhq/kit 0.1.0-beta.2 → 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 (104) 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/controls/civilDate.d.ts +1 -1
  7. package/dist/controls/civilDate.js +1 -1
  8. package/dist/docs/components/file-importer.md +1 -1
  9. package/dist/docs/components/paste-button.md +1 -1
  10. package/dist/docs/components/photos-picker.md +1 -1
  11. package/dist/docs/components/share-link.md +1 -1
  12. package/dist/docs/limitations.md +1 -1
  13. package/dist/docs/push.md +3 -3
  14. package/dist/docs/reference.md +1870 -0
  15. package/dist/glass/lens/controlLift.d.ts +1 -1
  16. package/dist/glass/lens/controlLift.js +1 -1
  17. package/dist/glass/lens/interactivePress.d.ts +2 -2
  18. package/dist/glass/lens/interactivePress.js +2 -2
  19. package/dist/glass/lens/menu-opening-native-fits.json +1 -1
  20. package/dist/glass/lens/menuContextFit.json +1 -1
  21. package/dist/glass/lens/menuDropFit.json +1 -1
  22. package/dist/glass/lens/menuField.d.ts +2 -2
  23. package/dist/glass/lens/menuField.js +3 -3
  24. package/dist/glass/lens/menuGlyphDropFit.json +1 -1
  25. package/dist/glass/lens/menuMorphFit.json +1 -1
  26. package/dist/glass/lens/menuMorphPhoneFit.json +1 -1
  27. package/dist/glass/lens/tabAccessoryMotion.d.ts +1 -1
  28. package/dist/glass/lens/tabSearchMember.d.ts +1 -1
  29. package/dist/glass/lens/tabSearchMember.js +1 -1
  30. package/dist/glass/menuPresenter.js +1 -1
  31. package/dist/glass/regions.d.ts +1 -1
  32. package/dist/glass/regions.js +1 -1
  33. package/dist/glass/shapeTrack.d.ts +1 -1
  34. package/dist/glass/submenuMotion.d.ts +1 -1
  35. package/dist/glass/submenuMotion.js +1 -1
  36. package/dist/glass/webgpu/entry.worker.js +3 -6
  37. package/dist/glass/webgpu/frameClock.d.ts +6 -2
  38. package/dist/glass/webgpu/frameClock.js +9 -2
  39. package/dist/glass/webgpu/frameProbe.d.ts +1 -1
  40. package/dist/glass/webgpu/frameProbe.js +3 -3
  41. package/dist/glass/webgpu/production.js +4 -0
  42. package/dist/glass/webgpu/raster.d.ts +18 -0
  43. package/dist/glass/webgpu/raster.js +105 -15
  44. package/dist/glass/webgpu/rasterMaterial.d.ts +4 -0
  45. package/dist/glass/webgpu/rasterMaterial.js +17 -1
  46. package/dist/glass/webgpu/regionScene.d.ts +4 -1
  47. package/dist/glass/webgpu/regionScene.js +30 -1
  48. package/dist/glass/webgpu/renderer.js +23 -7
  49. package/dist/keyboard/adapter.js +2 -2
  50. package/dist/keyboard/avoidance.d.ts +1 -1
  51. package/dist/keyboard/avoidance.js +1 -1
  52. package/dist/keyboard/motion.d.ts +1 -1
  53. package/dist/keyboard/motion.js +1 -1
  54. package/dist/list-form/ListForm.d.ts +2 -2
  55. package/dist/list-form/ListForm.js +1 -1
  56. package/dist/list-form/ListItem.js +1 -1
  57. package/dist/list-form/RowLabel.d.ts +1 -1
  58. package/dist/list-form/RowLabel.js +1 -1
  59. package/dist/list-form/Slider.js +1 -1
  60. package/dist/list-form/editMotion.js +1 -1
  61. package/dist/list-form/editing.d.ts +5 -5
  62. package/dist/list-form/editing.js +1 -1
  63. package/dist/motion/animationValue.d.ts +2 -2
  64. package/dist/motion/animationValue.js +9 -10
  65. package/dist/motion/matchedGeometry.d.ts +4 -4
  66. package/dist/motion/matchedGeometry.js +7 -8
  67. package/dist/motion/springTrack.d.ts +1 -2
  68. package/dist/motion/springTrack.js +10 -5
  69. package/dist/navigation/ScreenContainer.js +1 -1
  70. package/dist/navigation/chromeCurves.js +1 -1
  71. package/dist/navigation/types.d.ts +1 -1
  72. package/dist/navigation/zoomGeometry.js +1 -1
  73. package/dist/navigation/zoomMotion.d.ts +1 -1
  74. package/dist/navigation/zoomMotion.js +2 -2
  75. package/dist/navigation/zoomTransition.js +3 -4
  76. package/dist/presentation/Presentation.d.ts +2 -2
  77. package/dist/presentation/Presentation.js +1 -1
  78. package/dist/presentation/presentationSource.js +1 -1
  79. package/dist/public/controls.d.ts +4 -4
  80. package/dist/public/controls.js +7 -7
  81. package/dist/public/glass.d.ts +3 -4
  82. package/dist/public/glass.js +1 -2
  83. package/dist/public/navigationTransition.d.ts +1 -1
  84. package/dist/public/navigationTransition.js +1 -1
  85. package/dist/public/scroll.d.ts +1 -1
  86. package/dist/public/scroll.js +1 -1
  87. package/dist/public/toolbar.js +1 -1
  88. package/dist/safe-area/SafeAreaInset.js +3 -3
  89. package/dist/safe-area/modifiers.js +2 -2
  90. package/dist/safe-area/useSafeAreaMetrics.js +2 -2
  91. package/dist/scroll/ScrollSurface.d.ts +5 -5
  92. package/dist/scroll/ScrollSurface.js +1 -1
  93. package/dist/scroll/position.js +1 -1
  94. package/dist/scroll/screenGeometry.d.ts +1 -1
  95. package/dist/scroll/screenGeometry.js +1 -1
  96. package/dist/search/SearchDrawer.js +1 -1
  97. package/dist/search/searchMotion.d.ts +1 -1
  98. package/dist/search/searchMotion.js +1 -1
  99. package/dist/search/useSearchPresentation.js +1 -1
  100. package/dist/shapes/corners.js +1 -1
  101. package/dist/styles.css +55 -34
  102. package/dist/text-input/TextEditor.js +7 -3
  103. package/package.json +4 -4
  104. package/dist/docs/swiftui-reference.md +0 -1895
@@ -186,7 +186,8 @@ export declare class RegionScene {
186
186
  private owner;
187
187
  /** Worker-to-page events (a close morph that ended, so its panel can be torn down). */
188
188
  private notify;
189
- constructor(owner: LayerDevice, notify?: (event: GlassRegionEvent) => void, submissionAllowed?: () => boolean);
189
+ private readonly chrome?;
190
+ constructor(owner: LayerDevice, notify?: (event: GlassRegionEvent) => void, submissionAllowed?: () => boolean, chrome?: WebGPURaster);
190
191
  request(request: GlassRendererRequest): Promise<unknown>;
191
192
  /** New source images (uploads) since the last install: only those need a completion fence. */
192
193
  private presentationReceipt;
@@ -200,6 +201,8 @@ export declare class RegionScene {
200
201
  height: number;
201
202
  pixels: Uint8Array<ArrayBuffer>;
202
203
  }>;
204
+ /** Geometry sampled for the frame trace, in viewport points. No DOM measurements. */
205
+ accessoryGeometry(): string;
203
206
  diagnostics(): {
204
207
  navigation: {
205
208
  key: string;
@@ -1132,7 +1132,9 @@ export class RegionScene {
1132
1132
  owner;
1133
1133
  /** Worker-to-page events (a close morph that ended, so its panel can be torn down). */
1134
1134
  notify;
1135
- constructor(owner, notify = () => { }, submissionAllowed = () => true) {
1135
+ chrome;
1136
+ constructor(owner, notify = () => { }, submissionAllowed = () => true, chrome) {
1137
+ this.chrome = chrome;
1136
1138
  this.submissionAllowed = () => !this.sourcePending && submissionAllowed();
1137
1139
  this.owner = owner;
1138
1140
  this.notify = notify;
@@ -1683,6 +1685,12 @@ export class RegionScene {
1683
1685
  width: bounds.width,
1684
1686
  height: bounds.height,
1685
1687
  };
1688
+ if (this.chrome) {
1689
+ if (region.accessoryTrack?.held && p.descriptor.role === 'material')
1690
+ p.raster?.joinPresentation(this.chrome, p.descriptor.origin);
1691
+ else
1692
+ p.raster?.leavePresentation();
1693
+ }
1686
1694
  const viewportTrack = region.presentationViewportTrack ?? region.accessoryViewportTrack ?? region.accessoryTrack;
1687
1695
  const accessoryTranslation = viewportTrack && !held ? accessoryOffset(viewportTrack, this.trackClock()) : undefined;
1688
1696
  const viewport = held
@@ -1902,6 +1910,8 @@ export class RegionScene {
1902
1910
  p.raster.setViewport(viewport);
1903
1911
  p.raster.setSubmissionGate(this.submissionAllowed);
1904
1912
  await p.raster.init(p.canvas, this.owner, region.layer, p.descriptor);
1913
+ if (this.chrome && region.accessoryTrack?.held && p.descriptor.role === 'material')
1914
+ p.raster.joinPresentation(this.chrome, p.descriptor.origin);
1905
1915
  p.raster.setVisible(this.visible);
1906
1916
  fence = true;
1907
1917
  }
@@ -2070,6 +2080,25 @@ export class RegionScene {
2070
2080
  throw Error('Unknown glass presenter snapshot ' + id);
2071
2081
  return raster.snapshot();
2072
2082
  }
2083
+ /** Geometry sampled for the frame trace, in viewport points. No DOM measurements. */
2084
+ accessoryGeometry() {
2085
+ return JSON.stringify([...this.presenters.values()].flatMap(p => {
2086
+ const region = this.scene.regions.find(r => r.id === p.descriptor.regionIds[0]);
2087
+ const spec = p.raster?.spec;
2088
+ if (!region?.accessoryTrack || !spec || p.descriptor.role !== 'material')
2089
+ return [];
2090
+ const shape = spec.shape.first;
2091
+ return [
2092
+ {
2093
+ region: region.id,
2094
+ left: p.descriptor.origin.x + spec.frameX + shape.centerX - shape.halfWidth,
2095
+ right: p.descriptor.origin.x + spec.frameX + shape.centerX + shape.halfWidth,
2096
+ top: p.descriptor.origin.y + spec.frameY + shape.centerY - shape.halfHeight,
2097
+ inkLayers: spec.ink?.layers.length ?? 0,
2098
+ },
2099
+ ];
2100
+ }));
2101
+ }
2073
2102
  diagnostics() {
2074
2103
  return {
2075
2104
  navigation: this.navigation
@@ -65,7 +65,7 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
65
65
  const raster = new WebGPURaster();
66
66
  raster.setSubmissionGate(submissionAllowed);
67
67
  const layers = new Map([['chrome', raster]]);
68
- const appRegions = new RegionScene(owner, event => emit({ glassEvent: event }), submissionAllowed);
68
+ const appRegions = new RegionScene(owner, event => emit({ glassEvent: event }), submissionAllowed, clock.kind === 'worker-raf' ? raster : undefined);
69
69
  let tab = new TabMotion(), sharedTabLift = false, reducedMotion = false;
70
70
  const menuLift = new TabMotion(TAB_INTERACTIVE_MOTION);
71
71
  let tabTiming;
@@ -435,8 +435,15 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
435
435
  restart: restarted,
436
436
  };
437
437
  loopSample = undefined;
438
- appRegions.sample(at, sampleAt);
439
- render(x, [], sampleAt);
438
+ if (clock.kind === 'worker-raf')
439
+ raster.withSharedPresentation(() => {
440
+ appRegions.sample(at, sampleAt);
441
+ render(x, [], sampleAt);
442
+ });
443
+ else {
444
+ appRegions.sample(at, sampleAt);
445
+ render(x, [], sampleAt);
446
+ }
440
447
  }
441
448
  catch (error) {
442
449
  frameContext = undefined;
@@ -481,7 +488,16 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
481
488
  late: 0,
482
489
  interval: previous === undefined ? null : at - previous,
483
490
  restart: restarted,
484
- clock: 'page-raf',
491
+ clock: clock.kind ?? 'page-raf',
492
+ homeLeft: raster.spec &&
493
+ raster.spec.frameX +
494
+ raster.spec.shape.first.centerX -
495
+ raster.spec.shape.first.halfWidth,
496
+ homeRight: raster.spec &&
497
+ raster.spec.frameX +
498
+ raster.spec.shape.first.centerX +
499
+ raster.spec.shape.first.halfWidth,
500
+ accessoryGeometry: clock.kind === 'worker-raf' ? appRegions.accessoryGeometry() : undefined,
485
501
  callbackDelayMs,
486
502
  presented: takePresentedCanvases(),
487
503
  tabCentre: tab.frame.centre,
@@ -509,7 +525,7 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
509
525
  }
510
526
  };
511
527
  owner.subscribe(device => {
512
- frameProbe.useDevice(device);
528
+ frameProbe.useDevice(device, clock.kind !== 'worker-raf');
513
529
  lost = !device;
514
530
  if (!device) {
515
531
  cancel();
@@ -934,7 +950,7 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
934
950
  }
935
951
  break;
936
952
  case 'capability':
937
- frameProbe.useDevice(await owner.ready());
953
+ frameProbe.useDevice(await owner.ready(), clock.kind !== 'worker-raf');
938
954
  value = { backend: 'webgpu', timestampQuery: frameProbe.timestampQuery };
939
955
  break;
940
956
  case 'init':
@@ -1291,7 +1307,7 @@ export function createWebGPURenderer(emit, clock = documentFrameClock) {
1291
1307
  callbacks,
1292
1308
  framePending: !!handle,
1293
1309
  rate: 60,
1294
- clock: 'page-raf',
1310
+ clock: clock.kind ?? 'page-raf',
1295
1311
  visible,
1296
1312
  presentationGate: {
1297
1313
  allowed: submissionAllowed(),
@@ -150,7 +150,7 @@ export function dismissKeyboard() {
150
150
  }
151
151
  /** One adapter per SatinRoot. Owns viewport listeners, never cancels editing gestures. */
152
152
  /**
153
- * Tapping a control keeps the editor's keyboard, as a UIKit or SwiftUI button tap keeps the first
153
+ * Tapping a control keeps the editor's keyboard, so button presses preserve the first
154
154
  * responder: a hand-rolled composer's Send must not close the keyboard (iPad, October 12, 0/5 kept).
155
155
  * Controls that leave the editor end editing themselves: navigation and tab switches dismiss
156
156
  * explicitly, and a presented modal takes focus through showModal().
@@ -517,7 +517,7 @@ export function installKeyboardAdapter(root) {
517
517
  releaseSurface();
518
518
  revealedFor = '';
519
519
  revealedEditor = null;
520
- // Navigation ends editing, as a pushed or popped SwiftUI screen dismisses its keyboard.
520
+ // Navigation ends editing, when a screen is pushed or popped.
521
521
  if (editable(document.activeElement) && root.contains(document.activeElement))
522
522
  document.activeElement.blur();
523
523
  };
@@ -7,7 +7,7 @@ export type KeyboardClearance = Readonly<{
7
7
  bottom: number;
8
8
  }>;
9
9
  /**
10
- * Keyboard avoidance for the focused editor, as SwiftUI's keyboard safe area does it: every
10
+ * Keyboard avoidance for the focused editor, through the keyboard safe area: every
11
11
  * scroll surface around the editor gets room below its content for the keyboard and scrolls the
12
12
  * editor (a tall editor: its caret line) above the keyboard, innermost first. An editor on a page
13
13
  * whose surfaces cannot bring it up (a screen without a scroll surface, or a nested scroller pinned
@@ -29,7 +29,7 @@ function keepEditorInBox(delta, editor, box, offsetTop, room) {
29
29
  return Math.min(high, Math.max(low, delta));
30
30
  }
31
31
  /**
32
- * Keyboard avoidance for the focused editor, as SwiftUI's keyboard safe area does it: every
32
+ * Keyboard avoidance for the focused editor, through the keyboard safe area: every
33
33
  * scroll surface around the editor gets room below its content for the keyboard and scrolls the
34
34
  * editor (a tall editor: its caret line) above the keyboard, innermost first. An editor on a page
35
35
  * whose surfaces cannot bring it up (a screen without a scroll surface, or a nested scroller pinned
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The software keyboard's show and hide motion. Measured per frame from the native composer's
3
- * presentation layer (SwiftUI `ScrollView` + `.safeAreaInset(edge: .bottom)` chat, iOS 27.0
3
+ * presentation layer (`ScrollView` + `.safeAreaInset(edge: .bottom)` chat, iOS 27.0
4
4
  * Simulator, 4 shows and 4 hides; UIKit reports duration 0.3833 s, curve 7): a spring with mass 1,
5
5
  * stiffness 535 and damping 45.5, released at rest, fits every frame of both directions within
6
6
  * 0.84 pt of a 313 pt travel. UIKit ends the animation at 383 ms (0.4 pt short of rest).
@@ -1,7 +1,7 @@
1
1
  import { menuSpring } from '../glass/lens/menuSpring.js';
2
2
  /**
3
3
  * The software keyboard's show and hide motion. Measured per frame from the native composer's
4
- * presentation layer (SwiftUI `ScrollView` + `.safeAreaInset(edge: .bottom)` chat, iOS 27.0
4
+ * presentation layer (`ScrollView` + `.safeAreaInset(edge: .bottom)` chat, iOS 27.0
5
5
  * Simulator, 4 shows and 4 hides; UIKit reports duration 0.3833 s, curve 7): a spring with mass 1,
6
6
  * stiffness 535 and damping 45.5, released at rest, fits every frame of both directions within
7
7
  * 0.84 pt of a 313 pt travel. UIKit ends the animation at 383 ms (0.4 pt short of rest).
@@ -11,13 +11,13 @@ export type ListEngineProps = Omit<ScrollSurfaceProps, 'axis'> & {
11
11
  variant?: 'insetGrouped' | 'plain' | 'grouped' | 'sidebar';
12
12
  navigationTitle?: string;
13
13
  navigationTitleDisplay?: 'large' | 'inline';
14
- /** SwiftUI `searchable`: a search field in the navigation bar drawer above this List. */
14
+ /** A search field in the navigation bar drawer above this List. */
15
15
  searchable?: Searchable;
16
16
  /** Default keeps measured header/footer gaps. Numbers are section gaps in points. */
17
17
  listSectionSpacing?: 'default' | 'compact' | number;
18
18
  /** Controls the scroll surface background, leaving row backgrounds intact. */
19
19
  scrollContentBackground?: 'hidden' | 'visible';
20
- /** SwiftUI `List(selection:)`, resolved by the public List. */
20
+ /** Selection state resolved by the public List. */
21
21
  selectionBinding?: ListSelectionBinding;
22
22
  };
23
23
  /** A controlled List selection: one optional row, or a set of rows. */
@@ -410,7 +410,7 @@ export function StepperControl({ value, onChange, min = -Infinity, max = Infinit
410
410
  return (_jsxs("span", { "data-satin-row-intrinsic": "true", "data-satin-stepper": "true", "data-satin-stepper-disabled": disabled ? 'true' : undefined, role: "group", "aria-label": `${label}, ${value}`, children: [_jsx(StepperButton, { onEditingChange: onEditingChange, label: `Decrease ${label}, ${value}`, disabled: disabled || value <= min, onStep: () => onChange(clampStepper(value, min, max, -step)), children: _jsx(SymbolImage, { name: "minus", weight: "medium" }) }), _jsx(StepperButton, { onEditingChange: onEditingChange, label: `Increase ${label}, ${value}`, disabled: disabled || value >= max, onStep: () => onChange(clampStepper(value, min, max, step)), children: _jsx(SymbolImage, { name: "plus", weight: "semibold" }) })] }));
411
411
  }
412
412
  export function StepperRow(props) {
413
- return _jsx(Row, { label: props.label, trailing: _jsx(StepperControl, { ...props }) });
413
+ return (_jsx(Row, { className: "satin-stepper-row", label: props.label, trailing: _jsx(StepperControl, { ...props }) }));
414
414
  }
415
415
  export function SegmentedControl({ value, onChange, options, disabled = false, label = 'Options', style: cssStyle, }) {
416
416
  const choose = useChooseOption({ options, disabled, onChange });
@@ -208,7 +208,7 @@ function playEditShift(item, before, after) {
208
208
  }
209
209
  /**
210
210
  * The reorder handle's pointer-down: lifts this row and hands it to the drag. The move is
211
- * reported through `onMove` with SwiftUI's offsets once the row settles; the new order must then
211
+ * reported through `onMove` with source-sequence offsets once the row settles; the new order must then
212
212
  * come back through `data`, and until it does the rows stay where they were dropped.
213
213
  */
214
214
  function useReorder(element, item, editing) {
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  /**
3
- * A row title with a secondary line under it: SwiftUI's two-Text label (a title, then a
3
+ * A row title with a secondary line under it: a two-line label (a title, then a
4
4
  * `.subheadline` `.secondary` line). `RowLabel`, a Toggle subtitle and any other two-line row
5
5
  * label share this one body, so their type and spacing cannot drift apart.
6
6
  *
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * A row title with a secondary line under it: SwiftUI's two-Text label (a title, then a
3
+ * A row title with a secondary line under it: a two-line label (a title, then a
4
4
  * `.subheadline` `.secondary` line). `RowLabel`, a Toggle subtitle and any other two-line row
5
5
  * label share this one body, so their type and spacing cannot drift apart.
6
6
  *
@@ -5,7 +5,7 @@ import { requestGlassSourceDamage } from '../glass/sourceDamage.js';
5
5
  import { CONTROL_LIFT, controlLensSize } from '../glass/lens/controlLift.js';
6
6
  import { SliderContact } from './sliderContact.js';
7
7
  /**
8
- * Native geometry (stock SwiftUI Slider in a Form, iPhone 17 Pro Max Simulator, iOS 27.0, 3×
8
+ * Native geometry (a slider in a form, iPhone 17 Pro Max Simulator, iOS 27.0, 3×
9
9
  * stills, native control measurements `continuous.mov`): a 6 pt capsule track spans the row's content
10
10
  * width, the 37 × 24 pt white thumb stays inside it (its centre travels from 18.5 pt to width −
11
11
  * 18.5 pt), and the accent fill runs from the track start to the thumb centre with a round end.
@@ -1,5 +1,5 @@
1
1
  /*
2
- * Edit mode's row motion, measured from a stock SwiftUI `List` with `onDelete` and `onMove`
2
+ * Edit mode's row motion, measured from a native list with `onDelete` and `onMove`
3
3
  * (iOS 27.0 Simulator, native reference `native list editing fixture native editing fixture`, five Edit/Done toggles, frame counter
4
4
  * retimed: 0 skipped refreshes of 922; fits in native measurements).
5
5
  *
@@ -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({