@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
@@ -96,6 +96,48 @@ export class WebGPURaster {
96
96
  overlayPipeline;
97
97
  presentPipeline;
98
98
  format = 'rgba8unorm';
99
+ presentationRoot;
100
+ presentationOrigin;
101
+ presentationMembers = new Set();
102
+ presentationSequence;
103
+ collectingPresentation = false;
104
+ /** Member samples encode their materials first. The root then presents the whole frame once. */
105
+ withSharedPresentation(draw) {
106
+ this.collectingPresentation = true;
107
+ try {
108
+ draw();
109
+ }
110
+ finally {
111
+ this.collectingPresentation = false;
112
+ }
113
+ }
114
+ /** Retain independent material sampling, but store overlapping fixed chrome once. */
115
+ joinPresentation(root, origin) {
116
+ if (this.presentationRoot === root) {
117
+ this.presentationOrigin = origin;
118
+ if (root.setup)
119
+ root.resizeCanvas();
120
+ return;
121
+ }
122
+ if (this.setup)
123
+ this.clear();
124
+ this.presentationRoot?.presentationMembers.delete(this);
125
+ this.presentationRoot = root;
126
+ this.presentationOrigin = origin;
127
+ root.presentationMembers.add(this);
128
+ if (root.setup)
129
+ root.resizeCanvas();
130
+ root.forgetPresent();
131
+ }
132
+ leavePresentation() {
133
+ const root = this.presentationRoot;
134
+ root?.presentationMembers.delete(this);
135
+ if (root?.setup)
136
+ root.resizeCanvas();
137
+ root?.forgetPresent();
138
+ this.presentationRoot = undefined;
139
+ this.presentationOrigin = undefined;
140
+ }
99
141
  lost = false;
100
142
  suspended = false;
101
143
  covered = false;
@@ -152,6 +194,8 @@ export class WebGPURaster {
152
194
  this.setup = { ...this.setup, appearance, spec: retainedAppearanceSpec(spec, appearance) };
153
195
  }
154
196
  dispose() {
197
+ this.presentationRoot?.presentationMembers.delete(this);
198
+ this.presentationRoot?.forgetPresent();
155
199
  this.unsubscribe?.();
156
200
  this.release();
157
201
  this.passes.destroy();
@@ -571,7 +615,13 @@ export class WebGPURaster {
571
615
  const encoder = this.device?.createCommandEncoder();
572
616
  if (!encoder || !this.encodeResult(encoder, x))
573
617
  return;
574
- this.custom(encoder, this.presentPipeline, drawable(this.context, 'single', encoder).createView(), this.presentUniform(), [this.targets.result], !clear, this.presentScissor(), [this.canvas.width, this.canvas.height]);
618
+ if (this.presentationRoot) {
619
+ const root = this.presentationRoot;
620
+ if (!root.collectingPresentation && root.targets.result)
621
+ WebGPURaster.presentLayer(encoder, root.presentationSequence ?? [root]);
622
+ }
623
+ else
624
+ this.custom(encoder, this.presentPipeline, drawable(this.context, 'single', encoder).createView(), this.presentUniform(), [this.targets.result], !clear, this.presentScissor(), [this.canvas.width, this.canvas.height]);
575
625
  if (this.timing) {
576
626
  encoder.resolveQuerySet(this.timing.queries, 0, 2, this.timing.resolve, 0);
577
627
  encoder.copyBufferToBuffer(this.timing.resolve, 0, this.timing.read, 0, 16);
@@ -601,18 +651,34 @@ export class WebGPURaster {
601
651
  * region presenter's layer already is its sample rectangle and keeps layer space. */
602
652
  localResult = false;
603
653
  /** Device-pixel offset of this layer's canvas within its layer. */
604
- canvasOffset() {
654
+ presentationViewport() {
605
655
  const viewport = canvasViewports.get(this.canvas);
656
+ if (!viewport || !this.setup || !this.presentationMembers.size)
657
+ return viewport;
658
+ // The placeholder keeps its original band. object-fit:contain, aligned to the
659
+ // bottom, lets the worker omit transparent rows above all retained morph bounds.
660
+ const top = Math.max(viewport[1], Math.floor(Math.min(this.setup.spec.frameY - 64, ...[...this.presentationMembers].map(member => member.presentationOrigin.y))));
661
+ return [viewport[0], top, viewport[2], viewport[1] + viewport[3] - top];
662
+ }
663
+ canvasOffset() {
664
+ const viewport = this.presentationViewport();
606
665
  const resolution = this.setup.resolution;
607
666
  return viewport ? [viewport[0] * resolution, viewport[1] * resolution] : [0, 0];
608
667
  }
609
668
  canvasSize() {
610
- const viewport = canvasViewports.get(this.canvas);
669
+ const viewport = this.presentationViewport();
611
670
  const { resolution, width, height } = this.setup;
612
671
  return viewport
613
672
  ? [Math.round(viewport[2] * resolution), Math.round(viewport[3] * resolution)]
614
673
  : [Math.round(width * resolution), Math.round(height * resolution)];
615
674
  }
675
+ resizeCanvas() {
676
+ const [width, height] = this.canvasSize();
677
+ if (this.canvas.width !== width)
678
+ this.canvas.width = width;
679
+ if (this.canvas.height !== height)
680
+ this.canvas.height = height;
681
+ }
616
682
  /** Size the shared layer canvas to a band of its layer (the element is placed there by
617
683
  * the page): a lift overlay presents a strip, not a second full-screen canvas. */
618
684
  setCanvasViewport(viewport) {
@@ -694,20 +760,21 @@ export class WebGPURaster {
694
760
  * `presentedLayers`): the same results at the same places, or nothing at all. */
695
761
  static presentLayer(encoder, rasters) {
696
762
  const root = rasters[0];
697
- const visible = rasters.filter(raster => !raster.resultEmpty);
763
+ root.presentationSequence = rasters;
764
+ const visible = [...new Set([...rasters, ...root.presentationMembers])].filter(raster => !raster.resultEmpty && !!raster.targets.result && !raster.covered && !raster.suspended);
698
765
  const signature = JSON.stringify([
699
766
  root.canvas.width,
700
767
  root.canvas.height,
701
768
  visible.map(raster => [
702
769
  raster.resultOwner,
703
770
  raster.resultRevision,
704
- raster.presentUniform(),
705
- raster.presentScissor(),
771
+ raster.groupPresentUniform(root),
772
+ raster.groupPresentScissor(root),
706
773
  ]),
707
774
  ]);
708
775
  if (presentedLayers.get(root.canvas) === signature)
709
776
  return false;
710
- const draws = visible.map(raster => raster.presentDraw());
777
+ const draws = visible.map(raster => raster.presentDraw(root));
711
778
  const size = [root.canvas.width, root.canvas.height];
712
779
  encodePass(encoder, {
713
780
  colorAttachments: [
@@ -734,8 +801,35 @@ export class WebGPURaster {
734
801
  presentedLayers.set(root.canvas, signature);
735
802
  return true;
736
803
  }
737
- presentDraw() {
738
- const buffer = this.passes.uniform(new Float32Array(this.presentUniform()));
804
+ presentationOffset(root) {
805
+ if (this.presentationRoot !== root || !this.presentationOrigin)
806
+ return [0, 0];
807
+ const resolution = this.setup.resolution;
808
+ const [x, y] = this.canvasOffset();
809
+ const [rootX, rootY] = root.canvasOffset();
810
+ return [
811
+ this.presentationOrigin.x * resolution + x - rootX,
812
+ this.presentationOrigin.y * resolution + y - rootY,
813
+ ];
814
+ }
815
+ groupPresentUniform(root) {
816
+ const values = this.presentUniform();
817
+ const [x, y] = this.presentationOffset(root);
818
+ values[0] += x;
819
+ values[1] += y;
820
+ values[8] += x;
821
+ values[9] += y;
822
+ return values;
823
+ }
824
+ groupPresentScissor(root) {
825
+ const rect = this.presentScissor();
826
+ if (!rect)
827
+ return rect;
828
+ const [x, y] = this.presentationOffset(root);
829
+ return [rect[0] + x, rect[1] + y, rect[2], rect[3]];
830
+ }
831
+ presentDraw(root) {
832
+ const buffer = this.passes.uniform(new Float32Array(this.groupPresentUniform(root)));
739
833
  const group = this.device.createBindGroup({
740
834
  layout: this.presentPipeline.getBindGroupLayout(0),
741
835
  entries: [
@@ -744,7 +838,7 @@ export class WebGPURaster {
744
838
  { binding: 2, resource: viewOf(this.targets.result.texture) },
745
839
  ],
746
840
  });
747
- return { raster: this, group, rect: this.presentScissor() };
841
+ return { raster: this, group, rect: this.groupPresentScissor(root) };
748
842
  }
749
843
  /** Scroll-attached glass is part of the scrolling content, and the navigation bar's scroll-edge
750
844
  * effect is painted over that content (CSS, a layer above the scroll container). Its own
@@ -946,11 +1040,7 @@ export class WebGPURaster {
946
1040
  const setup = this.setup;
947
1041
  const { spec, appearance, resolution } = setup;
948
1042
  const targets = this.targets;
949
- const [canvasWidth, canvasHeight] = this.canvasSize();
950
- if (this.canvas.width !== canvasWidth)
951
- this.canvas.width = canvasWidth;
952
- if (this.canvas.height !== canvasHeight)
953
- this.canvas.height = canvasHeight;
1043
+ this.resizeCanvas();
954
1044
  const sampleRect = this.sampleRectangle();
955
1045
  this.presentRect = sampleRect;
956
1046
  // Result space = layer space shifted by an integer pixel origin, so every kernel
@@ -13,6 +13,10 @@ export declare function usesBarKernel(spec: ResultLayout['spec']): boolean;
13
13
  * its accessory, toolbar and glass buttons. Menus and presentation treatments keep the chained
14
14
  * pyramid their materials were fitted on. */
15
15
  export declare function usesAreaMips(spec: ResultLayout['spec']): boolean;
16
+ /** The tab material does not paint its 128 pt backdrop overscan. Keep a generous
17
+ * 64 pt output margin around every capsule; the fitted bar shadow has radius 4 pt
18
+ * and offset 8 pt. The source and blur pyramid still retain their full sample area. */
19
+ export declare function tabMaterialBounds(spec: ResultLayout['spec']): number[] | undefined;
16
20
  export declare function encodeMaterial(encoder: GPUCommandEncoder, passes: Passes, targets: Record<string, Target>, layout: ResultLayout, material: Values): void;
17
21
  /** The tab bar's selection pill (native selection recipe) and, while the pill lifts, its lens,
18
22
  * over a composed scratch of the backdrop and the bar material. */
@@ -25,9 +25,25 @@ export function usesBarKernel(spec) {
25
25
  export function usesAreaMips(spec) {
26
26
  return usesBarKernel(spec);
27
27
  }
28
+ /** The tab material does not paint its 128 pt backdrop overscan. Keep a generous
29
+ * 64 pt output margin around every capsule; the fitted bar shadow has radius 4 pt
30
+ * and offset 8 pt. The source and blur pyramid still retain their full sample area. */
31
+ export function tabMaterialBounds(spec) {
32
+ if (!spec.tabSelection || spec.materialProfile !== 'chrome' || spec.treatment)
33
+ return undefined;
34
+ const members = spec.shape.members?.length
35
+ ? spec.shape.members
36
+ : [spec.shape.first, ...(spec.shape.second ? [spec.shape.second] : [])];
37
+ const margin = 64 + (Math.max(0, spec.shape.blendRadius) * (members.length - 1)) / 4;
38
+ const left = Math.min(...members.map(s => s.centerX - s.halfWidth)) + spec.sampleInsetX - margin;
39
+ const right = Math.max(...members.map(s => s.centerX + s.halfWidth)) + spec.sampleInsetX + margin;
40
+ const top = Math.min(...members.map(s => s.centerY - s.halfHeight)) + spec.sampleInsetY - margin;
41
+ const bottom = Math.max(...members.map(s => s.centerY + s.halfHeight)) + spec.sampleInsetY + margin;
42
+ return [left, top, right - left, bottom - top];
43
+ }
28
44
  export function encodeMaterial(encoder, passes, targets, layout, material) {
29
45
  const { spec, appearance, resolution, sampleWidth, sampleHeight } = layout;
30
- const fieldBounds = morphFieldSampleBounds(spec);
46
+ const fieldBounds = tabMaterialBounds(spec) ?? morphFieldSampleBounds(spec);
31
47
  const fieldScissor = fieldBounds?.map(value => value * resolution);
32
48
  const first = spec.shape.first;
33
49
  if (spec.treatment === 'contextBackdrop' || spec.treatment === 'contextPreview') {
@@ -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(),
@@ -19,13 +19,12 @@ export declare class SourcePixels {
19
19
  private worker?;
20
20
  private next;
21
21
  private failure?;
22
+ private retrying?;
22
23
  private pending;
23
24
  private create;
24
25
  constructor(create?: () => Transport);
25
- /** The upload worker failed (its device was lost, or a reply never came). The instance stays
26
- * failed for images that need the worker: the bounded recovery of a source preparation replaces
27
- * it (`ProductionGlassRuntime.replaceFailedSourcePixels`), so a failure is reported once per
28
- * attempt and never retried in a loop. Canvas readbacks do not use the worker and still work. */
26
+ /** Exhausted upload failures let the production owner replace this source stager.
27
+ * Canvas readbacks remain available because they do not use the worker. */
29
28
  get failed(): boolean;
30
29
  private fail;
31
30
  /** Stage `image`'s premultiplied bytes for its upload (`takeSourcePixels`). When `capture` is a
@@ -39,4 +38,5 @@ export declare class SourcePixels {
39
38
  * in WebKit (157 ms for a 1320 x 5091 capture in the Catalyst host), more than half of the
40
39
  * stage. Measured for captures up to 1320 x 6495; the cause inside WebKit was not sampled. */
41
40
  prepare(image: ImageBitmap, capture?: ImageBitmapSource): Promise<void>;
41
+ private prepareOnce;
42
42
  }
@@ -32,6 +32,7 @@ export class SourcePixels {
32
32
  worker;
33
33
  next = 0;
34
34
  failure;
35
+ retrying;
35
36
  pending = new Map();
36
37
  create;
37
38
  constructor(create = () => new Worker(new URL('./sourcePixels.worker.js', import.meta.url), {
@@ -40,10 +41,8 @@ export class SourcePixels {
40
41
  })) {
41
42
  this.create = create;
42
43
  }
43
- /** The upload worker failed (its device was lost, or a reply never came). The instance stays
44
- * failed for images that need the worker: the bounded recovery of a source preparation replaces
45
- * it (`ProductionGlassRuntime.replaceFailedSourcePixels`), so a failure is reported once per
46
- * attempt and never retried in a loop. Canvas readbacks do not use the worker and still work. */
44
+ /** Exhausted upload failures let the production owner replace this source stager.
45
+ * Canvas readbacks remain available because they do not use the worker. */
47
46
  get failed() {
48
47
  return !!this.failure;
49
48
  }
@@ -51,7 +50,9 @@ export class SourcePixels {
51
50
  this.failure = error;
52
51
  this.worker?.terminate();
53
52
  this.worker = undefined;
54
- reportGlassError('worker-startup', error, 'source upload worker');
53
+ reportGlassError('worker-startup', error, 'source upload worker', undefined, {
54
+ recovering: true,
55
+ });
55
56
  for (const job of this.pending.values()) {
56
57
  clearTimeout(job.timer);
57
58
  job.reject(error);
@@ -69,6 +70,30 @@ export class SourcePixels {
69
70
  * in WebKit (157 ms for a 1320 x 5091 capture in the Catalyst host), more than half of the
70
71
  * stage. Measured for captures up to 1320 x 6495; the cause inside WebKit was not sampled. */
71
72
  async prepare(image, capture = image) {
73
+ for (let attempt = 0;; attempt++) {
74
+ try {
75
+ if (this.retrying)
76
+ await this.retrying;
77
+ return await this.prepareOnce(image, capture);
78
+ }
79
+ catch (error) {
80
+ if (!this.failure)
81
+ throw error;
82
+ if (attempt === 2) {
83
+ reportGlassError('worker-startup', error, 'source upload worker');
84
+ throw new Error(`Source upload worker startup failed: ${String(error)}`, { cause: error });
85
+ }
86
+ // Concurrent uploads share one restart. Each retry clones from the retained
87
+ // source, never from the bitmap already transferred to the failed worker.
88
+ this.retrying ??= new Promise(resolve => setTimeout(resolve, 250 * 2 ** attempt)).then(() => {
89
+ this.failure = undefined;
90
+ this.retrying = undefined;
91
+ });
92
+ await this.retrying;
93
+ }
94
+ }
95
+ }
96
+ async prepareOnce(image, capture) {
72
97
  const readbackStarted = glassNow();
73
98
  const pixels = canvasPixels(capture);
74
99
  if (pixels) {
@@ -83,8 +108,17 @@ export class SourcePixels {
83
108
  if (this.failure)
84
109
  throw this.failure;
85
110
  if (!this.worker) {
86
- this.worker = this.create();
111
+ try {
112
+ this.worker = this.create();
113
+ }
114
+ catch (error) {
115
+ this.fail(error instanceof Error ? error : Error(String(error)));
116
+ throw error;
117
+ }
118
+ const worker = this.worker;
87
119
  this.worker.onmessage = event => {
120
+ if (this.worker !== worker)
121
+ return;
88
122
  const data = event.data;
89
123
  if (data.id === 0 && data.error) {
90
124
  this.fail(Error(data.error));
@@ -109,8 +143,14 @@ export class SourcePixels {
109
143
  prepared.set(job.image, new Uint8Array(data.buffer));
110
144
  job.resolve();
111
145
  };
112
- this.worker.onerror = event => this.fail(Error(event.message || 'Source upload worker failed'));
113
- this.worker.onmessageerror = () => this.fail(Error('Source upload worker reply could not be decoded'));
146
+ this.worker.onerror = event => {
147
+ if (this.worker === worker)
148
+ this.fail(Error(event.message || 'Source upload worker failed'));
149
+ };
150
+ this.worker.onmessageerror = () => {
151
+ if (this.worker === worker)
152
+ this.fail(Error('Source upload worker reply could not be decoded'));
153
+ };
114
154
  }
115
155
  const started = glassNow(), clone = await createImageBitmap(capture);
116
156
  if (this.failure) {
@@ -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
  *