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

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 (240) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +1 -2
  3. package/dist/acknowledgements/inventory.json +1 -1
  4. package/dist/actions/RowActions.d.ts +2 -2
  5. package/dist/actions/RowActions.js +91 -25
  6. package/dist/actions/fullSwipeContact.d.ts +73 -0
  7. package/dist/actions/fullSwipeContact.js +186 -0
  8. package/dist/actions/rowScrub.d.ts +7 -4
  9. package/dist/actions/rowScrub.js +39 -37
  10. package/dist/actions/types.d.ts +13 -7
  11. package/dist/app/InstallComponents.d.ts +4 -7
  12. package/dist/app/InstallComponents.js +7 -7
  13. package/dist/app/InstallationDiagnostics.d.ts +2 -1
  14. package/dist/app/InstallationDiagnostics.js +2 -1
  15. package/dist/app/appearance.d.ts +5 -3
  16. package/dist/app/appearance.js +5 -3
  17. package/dist/app/appearanceStore.d.ts +3 -2
  18. package/dist/app/buildInfo.d.ts +4 -4
  19. package/dist/app/buildInfo.js +2 -2
  20. package/dist/app/definition.d.ts +11 -3
  21. package/dist/app/definition.js +2 -0
  22. package/dist/app/head.d.ts +2 -1
  23. package/dist/app/head.js +13 -16
  24. package/dist/app/inlineCSP.d.ts +6 -0
  25. package/dist/app/inlineCSP.js +25 -0
  26. package/dist/app/install.d.ts +1 -0
  27. package/dist/app/install.js +5 -2
  28. package/dist/app/installCheck.js +32 -86
  29. package/dist/app/installContext.d.ts +10 -5
  30. package/dist/app/installContext.js +2 -0
  31. package/dist/app/installCustom.js +1 -0
  32. package/dist/app/installExperience.js +0 -60
  33. package/dist/app/installationFailure.d.ts +3 -2
  34. package/dist/app/installationFailure.js +1 -1
  35. package/dist/app/launchScreen.d.ts +6 -2
  36. package/dist/app/launchScreen.js +104 -1
  37. package/dist/app/launchScreenRuntime.d.ts +31 -4
  38. package/dist/app/launchScreenRuntime.js +150 -22
  39. package/dist/app/launchWatchdog.js +25 -65
  40. package/dist/app/platformScripts.js +11 -0
  41. package/dist/app/styles.d.ts +3 -0
  42. package/dist/app/styles.js +42 -0
  43. package/dist/app/viewportValue.d.ts +2 -1
  44. package/dist/app/vite.d.ts +4 -0
  45. package/dist/app/vite.js +22 -24
  46. package/dist/content/content.d.ts +35 -30
  47. package/dist/content/content.js +19 -16
  48. package/dist/content/layout.d.ts +10 -2
  49. package/dist/content/layout.js +4 -2
  50. package/dist/content/scaledMetric.d.ts +2 -1
  51. package/dist/content/scaledMetric.js +2 -1
  52. package/dist/controls/ColorPicker.d.ts +2 -4
  53. package/dist/controls/ColorPicker.js +2 -4
  54. package/dist/controls/DatePicker.d.ts +4 -4
  55. package/dist/controls/DatePicker.js +2 -2
  56. package/dist/controls/ProgressView.d.ts +4 -4
  57. package/dist/controls/ProgressView.js +1 -2
  58. package/dist/data/database.d.ts +9 -8
  59. package/dist/data/database.js +4 -4
  60. package/dist/docs/reference.md +982 -964
  61. package/dist/docs/tailwind.md +76 -0
  62. package/dist/docs/tokens.md +1 -1
  63. package/dist/gestures/GestureView.d.ts +3 -1
  64. package/dist/gestures/GestureView.js +2 -1
  65. package/dist/gestures/gesture.d.ts +22 -2
  66. package/dist/gestures/gesture.js +7 -2
  67. package/dist/glass/GlassEffect.d.ts +2 -1
  68. package/dist/glass/GlassEffect.js +2 -1
  69. package/dist/glass/GlassPresentation.d.ts +1 -0
  70. package/dist/glass/GlassPresentation.js +21 -4
  71. package/dist/glass/TabBar.js +23 -1
  72. package/dist/glass/errorAlert.js +2 -1
  73. package/dist/glass/errors.d.ts +3 -1
  74. package/dist/glass/fallback/runtime.d.ts +1 -0
  75. package/dist/glass/fallback/runtime.js +8 -1
  76. package/dist/glass/liquidContext.d.ts +2 -0
  77. package/dist/glass/menuPresenter.d.ts +2 -0
  78. package/dist/glass/regions.d.ts +2 -1
  79. package/dist/glass/renderSpecs.d.ts +1 -0
  80. package/dist/glass/renderSpecs.js +1 -0
  81. package/dist/glass/webgpu/controlRim.d.ts +29 -0
  82. package/dist/glass/webgpu/controlRim.js +68 -0
  83. package/dist/glass/webgpu/passes.d.ts +2 -2
  84. package/dist/glass/webgpu/production.d.ts +1 -0
  85. package/dist/glass/webgpu/production.js +27 -1
  86. package/dist/glass/webgpu/rasterUniforms.js +9 -0
  87. package/dist/glass/webgpu/shaders.d.ts +2 -2
  88. package/dist/glass/webgpu/shaders.js +5 -1
  89. package/dist/haptics/SensoryButton.d.ts +4 -4
  90. package/dist/haptics/SensoryButton.js +3 -3
  91. package/dist/image/Image.d.ts +6 -3
  92. package/dist/image/Image.js +4 -3
  93. package/dist/image/asset.d.ts +2 -1
  94. package/dist/keyboard/adapter.d.ts +3 -4
  95. package/dist/keyboard/geometry.d.ts +1 -1
  96. package/dist/list-form/ListForm.d.ts +8 -7
  97. package/dist/list-form/ListForm.js +2 -2
  98. package/dist/list-form/editing.d.ts +5 -6
  99. package/dist/list-form/editing.js +4 -5
  100. package/dist/list-form/offsets.d.ts +2 -2
  101. package/dist/list-form/offsets.js +2 -2
  102. package/dist/media/react.d.ts +2 -1
  103. package/dist/media/react.js +2 -1
  104. package/dist/media/types.d.ts +6 -3
  105. package/dist/motion/TransitionView.d.ts +4 -2
  106. package/dist/motion/TransitionView.js +3 -2
  107. package/dist/motion/animationValue.d.ts +1 -1
  108. package/dist/motion/matchedGeometry.d.ts +6 -5
  109. package/dist/motion/matchedGeometry.js +2 -1
  110. package/dist/motion/transition.d.ts +4 -1
  111. package/dist/motion/transition.js +2 -0
  112. package/dist/navigation/DetailList.d.ts +1 -1
  113. package/dist/navigation/DetailList.js +1 -1
  114. package/dist/navigation/Sidebar.d.ts +3 -1
  115. package/dist/navigation/Sidebar.js +1 -1
  116. package/dist/navigation/SidebarSymbol.d.ts +2 -1
  117. package/dist/navigation/SidebarSymbol.js +2 -1
  118. package/dist/navigation/navigationContext.d.ts +2 -1
  119. package/dist/navigation/navigationContext.js +2 -1
  120. package/dist/navigation/splitContext.d.ts +2 -1
  121. package/dist/navigation/splitContext.js +2 -1
  122. package/dist/navigation/toolbarMinimization.d.ts +4 -3
  123. package/dist/navigation/types.d.ts +7 -5
  124. package/dist/presentation/Presentation.d.ts +8 -7
  125. package/dist/presentation/Presentation.js +3 -2
  126. package/dist/presentation/model.d.ts +2 -1
  127. package/dist/press/Pressable.d.ts +5 -1
  128. package/dist/press/Pressable.js +2 -0
  129. package/dist/press/stateMachine.d.ts +1 -1
  130. package/dist/public/Label.d.ts +2 -1
  131. package/dist/public/Label.js +2 -1
  132. package/dist/public/Screen.d.ts +4 -6
  133. package/dist/public/actions.d.ts +2 -1
  134. package/dist/public/actions.js +2 -1
  135. package/dist/public/buttonBorderShape.d.ts +2 -1
  136. package/dist/public/colors.d.ts +7 -4
  137. package/dist/public/colors.js +3 -2
  138. package/dist/public/controlStyle.d.ts +17 -14
  139. package/dist/public/controls.d.ts +27 -26
  140. package/dist/public/controls.js +20 -19
  141. package/dist/public/deviceActions.d.ts +6 -4
  142. package/dist/public/deviceActions.js +4 -2
  143. package/dist/public/environment.d.ts +16 -13
  144. package/dist/public/environment.js +11 -9
  145. package/dist/public/filePickers.d.ts +6 -4
  146. package/dist/public/filePickers.js +3 -2
  147. package/dist/public/glass.d.ts +6 -3
  148. package/dist/public/glass.js +2 -1
  149. package/dist/public/glassValue.d.ts +3 -4
  150. package/dist/public/glassValue.js +3 -4
  151. package/dist/public/keyboard.d.ts +4 -1
  152. package/dist/public/keyboard.js +2 -1
  153. package/dist/public/navigationTransition.d.ts +5 -5
  154. package/dist/public/navigationTransition.js +2 -2
  155. package/dist/public/presentation.d.ts +8 -7
  156. package/dist/public/presentation.js +5 -5
  157. package/dist/public/runtime.d.ts +9 -8
  158. package/dist/public/runtime.js +7 -7
  159. package/dist/public/scroll.d.ts +6 -3
  160. package/dist/public/scroll.js +3 -1
  161. package/dist/public/structure.d.ts +2 -2
  162. package/dist/public/toolbar.d.ts +15 -11
  163. package/dist/public/toolbar.js +12 -7
  164. package/dist/reachability/model.d.ts +4 -3
  165. package/dist/reachability/react.d.ts +4 -1
  166. package/dist/reachability/react.js +2 -1
  167. package/dist/root/SatinRoot.d.ts +3 -3
  168. package/dist/root/SatinRoot.js +12 -36
  169. package/dist/root/mountSatin.d.ts +1 -1
  170. package/dist/root/mountSatin.js +26 -35
  171. package/dist/root/runtimeContext.d.ts +2 -1
  172. package/dist/root/runtimeContext.js +2 -1
  173. package/dist/safe-area/Overlay.d.ts +3 -2
  174. package/dist/safe-area/Overlay.js +2 -1
  175. package/dist/safe-area/SafeAreaInset.d.ts +11 -7
  176. package/dist/safe-area/SafeAreaInset.js +7 -6
  177. package/dist/safe-area/context.d.ts +3 -3
  178. package/dist/safe-area/context.js +1 -1
  179. package/dist/safe-area/displayCorners.d.ts +4 -2
  180. package/dist/safe-area/displayCorners.js +4 -2
  181. package/dist/safe-area/modifiers.d.ts +14 -10
  182. package/dist/safe-area/modifiers.js +8 -4
  183. package/dist/scroll/ScrollSurface.js +5 -1
  184. package/dist/scroll/ScrollTarget.d.ts +1 -1
  185. package/dist/scroll/ScrollTarget.js +1 -1
  186. package/dist/scroll/edgePolicy.d.ts +2 -2
  187. package/dist/scroll/position.d.ts +5 -3
  188. package/dist/scroll/types.d.ts +1 -1
  189. package/dist/search/PadSearchDrawer.d.ts +7 -0
  190. package/dist/search/PadSearchDrawer.js +8 -0
  191. package/dist/search/PadSearchPresentationContext.d.ts +6 -0
  192. package/dist/search/PadSearchPresentationContext.js +4 -0
  193. package/dist/search/SearchDrawer.d.ts +1 -1
  194. package/dist/search/SearchDrawer.js +6 -1
  195. package/dist/search/SearchSuggestionsPopover.d.ts +6 -0
  196. package/dist/search/SearchSuggestionsPopover.js +42 -0
  197. package/dist/search/SearchToolbarField.d.ts +2 -1
  198. package/dist/search/SearchToolbarField.js +28 -7
  199. package/dist/search/padSearchBarMotion.d.ts +69 -0
  200. package/dist/search/padSearchBarMotion.js +73 -0
  201. package/dist/search/padSearchFieldSamples.json +491 -0
  202. package/dist/search/searchPlacement.d.ts +3 -0
  203. package/dist/search/searchPlacement.js +7 -0
  204. package/dist/search/searchable.d.ts +14 -12
  205. package/dist/search/usePadSearchFieldMotion.d.ts +4 -0
  206. package/dist/search/usePadSearchFieldMotion.js +100 -0
  207. package/dist/search/useSearchPresentation.d.ts +2 -1
  208. package/dist/search/useSearchPresentation.js +23 -5
  209. package/dist/server.d.ts +4 -4
  210. package/dist/server.js +4 -4
  211. package/dist/shapes/corners.d.ts +3 -3
  212. package/dist/shapes/corners.js +1 -2
  213. package/dist/styles.css +8289 -8244
  214. package/dist/sw/vite.d.ts +4 -0
  215. package/dist/sw/vite.js +2 -0
  216. package/dist/symbols/Symbol.d.ts +7 -5
  217. package/dist/symbols/Symbol.js +5 -3
  218. package/dist/symbols/catalogue.d.ts +1 -1
  219. package/dist/symbols/types.d.ts +6 -4
  220. package/dist/tabs/TabSection.d.ts +3 -2
  221. package/dist/tabs/TabSection.js +2 -1
  222. package/dist/tabs/TabView.js +54 -51
  223. package/dist/tasks/useTask.d.ts +5 -4
  224. package/dist/tasks/useTask.js +4 -3
  225. package/dist/testing.d.ts +3 -2
  226. package/dist/testing.js +3 -2
  227. package/dist/text-input/focusChain.d.ts +4 -3
  228. package/dist/text-input/focusChain.js +3 -2
  229. package/dist/text-input/kinds.d.ts +1 -1
  230. package/dist/web/WebView.d.ts +10 -3
  231. package/dist/web/WebView.js +3 -2
  232. package/dist/web/authentication.d.ts +12 -1
  233. package/dist/web/authentication.js +2 -1
  234. package/dist/web/openURL.d.ts +10 -4
  235. package/dist/web/openURL.js +4 -2
  236. package/dist/web/useWebAuthentication.d.ts +2 -1
  237. package/dist/web/useWebAuthentication.js +2 -1
  238. package/dist/zoom/Zoomable.d.ts +3 -3
  239. package/dist/zoom/Zoomable.js +2 -2
  240. package/package.json +1 -1
@@ -1,5 +1,4 @@
1
1
  import { cornerClipPath, roundedCornerPath } from '../shapes/corners.js';
2
- import { takeoverSample } from './swipeSpring.js';
3
2
  import { swipeEdge } from './types.js';
4
3
  // iOS 27 matched phone reference, light/dark, 1/2/3 actions, both edges:
5
4
  // color-mask width 60pt, gap 10pt, outer padding 10pt. Capsule 44pt layout / 42-43pt solid mask.
@@ -63,9 +62,16 @@ function inlineEntrance(distance, buttonWidth) {
63
62
  }
64
63
  return { scale, opacity };
65
64
  }
65
+ /** A destructive takeover keeps the other actions visible and dimmed. Native tall
66
+ * Delete row (r3 receipts): More fades from 0.73 at 142pt revealed to 0.30 at 381pt. */
67
+ function destructiveSecondaryOpacity(distance) {
68
+ return clamp(0.73 - ((0.73 - 0.3) / (381 - 142)) * (distance - 142));
69
+ }
66
70
  /** Spatial fit of native's sequential capsule reveal. Rest interpolates the
67
- * contact pose into the fully revealed spring target; it is never a DOM writer. */
68
- export function swipeActionPose(position, index, count, takeover = 0, rest = 0, buttonWidth = 60, inline = false, allowsFullSwipe = false) {
71
+ * contact pose into the fully revealed spring target; it is never a DOM writer.
72
+ * A destructive takeover packs the other actions against the row and the primary
73
+ * fills the rest (native: primary = revealed - 80 on a tall two-action row). */
74
+ export function swipeActionPose(position, index, count, takeover = 0, rest = 0, buttonWidth = 60, inline = false, allowsFullSwipe = false, destructive = false) {
69
75
  const distance = Math.abs(position);
70
76
  const open = swipeOpenWidth(count, buttonWidth);
71
77
  const excess = Math.max(0, distance - open);
@@ -77,15 +83,17 @@ export function swipeActionPose(position, index, count, takeover = 0, rest = 0,
77
83
  const entrance = inline ? inlineEntrance(localDistance + revealLead, buttonWidth) : undefined;
78
84
  const contactScale = entrance?.scale ?? revealScale(localDistance);
79
85
  const reveal = contactScale + (clamp(distance / open) - contactScale) * rest;
80
- const spread = (((index + 0.5) * excess) / (count + 0.5)) * (1 - takeover);
86
+ const packed = destructive && index > 0 ? takeover * excess : 0;
87
+ const spread = (((index + 0.5) * excess) / (count + 0.5)) * (1 - takeover) + packed;
88
+ const opacity = (entrance?.opacity ?? contactScale) + (1 - (entrance?.opacity ?? contactScale)) * rest;
89
+ const takenOver = destructive ? destructiveSecondaryOpacity(distance) : 0;
81
90
  return {
82
91
  scale: reveal,
83
92
  spread,
84
- opacity: ((entrance?.opacity ?? contactScale) + (1 - (entrance?.opacity ?? contactScale)) * rest) *
85
- (index === 0 ? 1 : 1 - takeover),
93
+ opacity: index === 0 ? opacity : opacity * (1 - takeover) + takenOver * takeover,
86
94
  growth: index === 0
87
95
  ? (inline && allowsFullSwipe ? excess * (1 - takeover) : 0) +
88
- takeover * Math.max(0, distance - buttonWidth - 20)
96
+ takeover * (destructive ? excess : Math.max(0, distance - buttonWidth - 20))
89
97
  : 0,
90
98
  };
91
99
  }
@@ -102,6 +110,7 @@ export function swipeActionTracks(primary, count, width, allowsFullSwipe) {
102
110
  const trailing = primary.parentElement?.dataset.satinSwipeTray === 'trailing';
103
111
  const inline = primary.closest('[data-satin-row-actions]')?.dataset.satinSwipeLabelLayout ===
104
112
  'inline';
113
+ const destructive = primary.dataset.satinSwipeRole === 'destructive';
105
114
  return buttons.flatMap((button, index) => {
106
115
  const buttonWidth = Number.parseFloat(getComputedStyle(button).width) || 60;
107
116
  const middleWidth = Math.max(1, buttonWidth - capsuleHeight(primary));
@@ -118,7 +127,7 @@ export function swipeActionTracks(primary, count, width, allowsFullSwipe) {
118
127
  : count === 1 || raw === Infinity
119
128
  ? 1
120
129
  : progress * progress * (3 - 2 * progress));
121
- return swipeActionPose(position, index, count, raw === Infinity ? takeover : takeover * (1 - rest), rest, buttonWidth, inline, allowsFullSwipe);
130
+ return swipeActionPose(position, index, count, raw === Infinity ? takeover : takeover * (1 - rest), rest, buttonWidth, inline, allowsFullSwipe, destructive);
122
131
  };
123
132
  return [
124
133
  {
@@ -208,15 +217,20 @@ export function presentedTakeover(primary, position) {
208
217
  const count = primary.parentElement?.children.length ?? 1;
209
218
  const inline = primary.closest('[data-satin-row-actions]')?.dataset.satinSwipeLabelLayout ===
210
219
  'inline';
211
- const ordinary = inline ? Math.max(0, Math.abs(position) - swipeOpenWidth(count, buttonWidth)) : 0;
212
- const expanded = Math.max(0, Math.abs(position) - buttonWidth - 20);
220
+ const excess = Math.max(0, Math.abs(position) - swipeOpenWidth(count, buttonWidth));
221
+ const ordinary = inline ? excess : 0;
222
+ // A destructive takeover grows only into the excess; inline rows grow that much
223
+ // before takeover too, so their caught takeover reads as none.
224
+ const expanded = primary.dataset.satinSwipeRole === 'destructive'
225
+ ? excess
226
+ : Math.max(0, Math.abs(position) - buttonWidth - 20);
213
227
  if (expanded <= ordinary)
214
228
  return 0;
215
229
  const matrix = new DOMMatrixReadOnly(getComputedStyle(middle).transform);
216
230
  const growth = (matrix.a - 1) * Math.max(1, buttonWidth - capsuleHeight(primary));
217
231
  return clamp((growth - ordinary) / (expanded - ordinary));
218
232
  }
219
- export function createRowScrub(host, source, content, initial, width, configuration) {
233
+ export function createRowScrub(host, source, content, initial, width, configuration, contact) {
220
234
  if (!width || source.scrollWidth <= source.clientWidth)
221
235
  return undefined;
222
236
  const widths = swipeWidths(host);
@@ -234,34 +248,18 @@ export function createRowScrub(host, source, content, initial, width, configurat
234
248
  if (primary) {
235
249
  const edge = swipeEdge(configuration, side);
236
250
  const actions = swipeActionTracks(primary, edge.actions.length, width, edge.allowsFullSwipe);
237
- const reducedMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
238
- const caught = initial ? presentedTakeover(primary, initial) : 0;
239
- const state = { from: caught, target: caught, velocity: 0, started: performance.now() };
240
- const threshold = fullSwipeThreshold(width, edge.actions.length, side, widths[side], host.dataset.satinSwipeLabelLayout === 'inline');
241
251
  const openWidth = swipeOpenWidth(edge.actions.length, widths[side]);
242
- let previousRaw = Number.NaN;
252
+ let previousPosition = Number.NaN;
243
253
  let previousTakeover = Number.NaN;
244
254
  const update = (now, raw, position) => {
245
- const sample = takeoverSample(state.from, state.target, state.velocity, (now - state.started) / 1000);
246
- const target = edge.allowsFullSwipe && raw * sign >= threshold ? 1 : 0;
247
- if (target !== state.target) {
248
- state.from = sample.position;
249
- state.velocity = sample.velocity;
250
- state.target = target;
251
- state.started = now;
252
- }
253
- // Both spring states advance across reversals, but the hidden tray has
255
+ // Both takeover springs advance across reversals, but the hidden tray has
254
256
  // no pixels to update. Read the scroll source before either tray writes.
257
+ const takeover = contact.takeover(side, raw, now);
255
258
  if (position * sign <= 0)
256
259
  return;
257
- const takeover = edge.actions.length === 1 && edge.allowsFullSwipe
258
- ? 1
259
- : reducedMotion
260
- ? target
261
- : Math.max(0, Math.min(1, sample.position));
262
- if (raw === previousRaw && takeover === previousTakeover)
260
+ if (position === previousPosition && takeover === previousTakeover)
263
261
  return;
264
- previousRaw = raw;
262
+ previousPosition = position;
265
263
  previousTakeover = takeover;
266
264
  const rest = initial ? clamp(1 - Math.abs(raw - initialRaw) / openWidth) : 0;
267
265
  for (const action of actions) {
@@ -279,10 +277,14 @@ export function createRowScrub(host, source, content, initial, width, configurat
279
277
  // WebKit owns touch arbitration and the scroll offset. Sample it once per
280
278
  // frame and present the row, trays, and fitted takeover together. Mixing a
281
279
  // compositor row with main-thread pills exposes every delayed main-thread frame.
280
+ // The contact maps the raw offset, so a takeover's anchor moves row and pills alike.
282
281
  let actionFrame = 0;
283
- const flush = (now = performance.now()) => {
284
- const raw = initialRaw + origin - source.scrollLeft;
285
- const position = rowSwipePosition(raw, width, configuration, widths);
282
+ const rawPosition = () => initialRaw + origin - source.scrollLeft;
283
+ /** Present the current scroll sample. A release passes `evaluate: false`: lifting
284
+ * the finger cannot start a takeover that no frame has shown. */
285
+ const flush = (now = performance.now(), evaluate = true) => {
286
+ const raw = rawPosition();
287
+ const position = contact.present(raw, now, evaluate);
286
288
  Object.assign(content.style, contentFrame(position, initial ? 1 : 0));
287
289
  for (const update of actionUpdates)
288
290
  update(now, raw, position);
@@ -295,8 +297,8 @@ export function createRowScrub(host, source, content, initial, width, configurat
295
297
  paintActions(performance.now());
296
298
  return {
297
299
  flush,
298
- rawPosition: () => initialRaw + origin - source.scrollLeft,
299
- position: () => rowSwipePosition(initialRaw + origin - source.scrollLeft, width, configuration, widths),
300
+ rawPosition,
301
+ position: () => contact.present(rawPosition(), performance.now(), false),
300
302
  cancel: () => {
301
303
  cancelAnimationFrame(actionFrame);
302
304
  // Outside contact, the settled side's visibility belongs to the row CSS.
@@ -1,7 +1,8 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { SymbolName } from '../symbols/Symbol.js';
3
3
  import type { CornerShape } from '../shapes/corners.js';
4
- /** Shared by menus, context menus and row swipe actions. */
4
+ /** An entry in a menu, context menu or swipe action list: a command, a titled section of
5
+ * entries, or a submenu. */
5
6
  export type Action = ActionItem | Readonly<{
6
7
  section: readonly Action[];
7
8
  label?: string;
@@ -13,7 +14,8 @@ export type Action = ActionItem | Readonly<{
13
14
  disabled?: boolean;
14
15
  submenu: readonly Action[];
15
16
  }>;
16
- /** A leaf command shared by menus, swipe actions, alerts and confirmation dialogs. */
17
+ /** A single command with an id, a label, an optional symbol and role, and an `onPress` handler.
18
+ * Menus, swipe actions, alerts and confirmation dialogs all use it. */
17
19
  export type ActionItem = Readonly<{
18
20
  id: string;
19
21
  label: string;
@@ -27,7 +29,8 @@ export type ActionItem = Readonly<{
27
29
  onPress?: (() => void) | (() => Promise<void>);
28
30
  tint?: string;
29
31
  }>;
30
- /** A swipe command may omit its visible title while retaining an accessible name. */
32
+ /** A swipe action command. It may drop its visible label if it has a symbol or icon and an
33
+ * `accessibilityLabel`. */
31
34
  export type SwipeActionItem = ActionItem | Readonly<Omit<ActionItem, 'label' | 'accessibilityLabel'> & {
32
35
  label?: never;
33
36
  accessibilityLabel: string;
@@ -36,18 +39,21 @@ export type SwipeActionItem = ActionItem | Readonly<Omit<ActionItem, 'label' | '
36
39
  } | {
37
40
  icon: ReactNode;
38
41
  })>;
42
+ /** An entry in a row's swipe actions: any menu entry, or a swipe command that shows only a
43
+ * symbol. */
39
44
  export type SwipeAction = Action | SwipeActionItem;
40
- /** `swipeActions`: one or more edge configurations for a row. */
45
+ /** The `swipeActions` prop of a row: actions for one edge, or a list with one entry per edge. */
41
46
  export type SwipeActions = SwipeActionsConfiguration | readonly SwipeActionsConfiguration[];
42
- /** Swipe actions on one row edge. Full swipe is enabled unless explicitly disabled. */
47
+ /** The swipe actions on one row edge, `trailing` by default. A full swipe runs the first action
48
+ * unless `allowsFullSwipe` is false. */
43
49
  export type SwipeActionsConfiguration = Readonly<{
44
50
  edge?: 'leading' | 'trailing';
45
- /** Revealed row face. Omit for the native default; custom shapes are unrestricted. */
51
+ /** The shape of the revealed row face. Omit it for the system default. */
46
52
  shape?: CornerShape;
47
53
  allowsFullSwipe?: boolean;
48
54
  actions: readonly SwipeAction[];
49
55
  }>;
50
- /** `contextMenu`: row commands and an optional preview. */
56
+ /** The `contextMenu` prop of a row: a list of actions, or actions with a custom `preview`. */
51
57
  export type ContextMenuConfiguration = readonly Action[] | Readonly<{
52
58
  actions: readonly Action[];
53
59
  preview?: ReactNode;
@@ -1,12 +1,9 @@
1
1
  import { type InstallContext } from './installContext.js';
2
- /**
3
- * Optional building blocks for a custom install page (public through `@satinhq/kit/install`): the
4
- * context hook, the steps list and the tip, with the default page's words and look. Styles
5
- * are scoped to `satin-` classes; `--satin-install-tint` overrides the app's tint.
6
- */
7
- /** The install context, read once on the page (it does not change while the page is open). */
2
+ /** Returns the install context, read once when the page opens. It throws outside the install
3
+ * page. */
8
4
  export declare function useInstallContext(): InstallContext;
9
- /** The three steps, in the words of Satin's page, for this device, browser and Safari layout. */
5
+ /** The three install steps for this device, browser and Safari layout, worded as on Satin's own
6
+ * install page. */
10
7
  export declare function InstallSteps(): import("react").JSX.Element;
11
8
  /**
12
9
  * The tip at Safari's real control ("Page Menu, then Share" or "Share"), in Satin's glass.
@@ -6,12 +6,11 @@ import { deviceTokens } from './installExperienceDevice.js';
6
6
  import { TIP_GLASS } from './installExperienceHome.js';
7
7
  import { readInstallContext } from './installContext.js';
8
8
  import { classifySafariBar, tipPath } from './installTipPath.js';
9
- /**
10
- * Optional building blocks for a custom install page (public through `@satinhq/kit/install`): the
11
- * context hook, the steps list and the tip, with the default page's words and look. Styles
12
- * are scoped to `satin-` classes; `--satin-install-tint` overrides the app's tint.
13
- */
14
- /** The install context, read once on the page (it does not change while the page is open). */
9
+ // Optional building blocks for a custom install page (public through `@satinhq/kit/install`): the
10
+ // context hook, the steps list and the tip, with the default page's words and look. Styles
11
+ // are scoped to `satin-` classes; `--satin-install-tint` overrides the app's tint.
12
+ /** Returns the install context, read once when the page opens. It throws outside the install
13
+ * page. */
15
14
  export function useInstallContext() {
16
15
  const [context] = useState(readInstallContext);
17
16
  return context;
@@ -52,7 +51,8 @@ function useAppTint(context) {
52
51
  document.documentElement.style.setProperty('--satin-app-tint', context.app.tint);
53
52
  }, [context.app.tint]);
54
53
  }
55
- /** The three steps, in the words of Satin's page, for this device, browser and Safari layout. */
54
+ /** The three install steps for this device, browser and Safari layout, worded as on Satin's own
55
+ * install page. */
56
56
  export function InstallSteps() {
57
57
  const context = useInstallContext();
58
58
  const bar = useSafariBar(context);
@@ -3,5 +3,6 @@ export { readInstallationFailure, type InstallationFailure } from './installatio
3
3
  export type InstallationDiagnosticsProps = {
4
4
  scope: string;
5
5
  };
6
- /** Installation receipts for an optional Settings diagnostics screen. Use the app's scope. */
6
+ /** Shows the app's installation receipts, for an optional diagnostics screen in Settings. Pass the
7
+ * app's scope. */
7
8
  export declare function InstallationDiagnostics({ scope }: InstallationDiagnosticsProps): import("react").JSX.Element;
@@ -3,7 +3,8 @@ import { useEffect, useState } from 'react';
3
3
  import { NavigationRow, Section, ValueRow } from '../list-form/ListForm.js';
4
4
  import { readInstallationFailure, readInstallationSuccess } from './installationFailure.js';
5
5
  export { readInstallationFailure } from './installationFailure.js';
6
- /** Installation receipts for an optional Settings diagnostics screen. Use the app's scope. */
6
+ /** Shows the app's installation receipts, for an optional diagnostics screen in Settings. Pass the
7
+ * app's scope. */
7
8
  export function InstallationDiagnostics({ scope }) {
8
9
  const [failure, setFailure] = useState(() => readInstallationFailure(scope));
9
10
  const [success, setSuccess] = useState(() => readInstallationSuccess(scope));
@@ -2,7 +2,8 @@ import { type PreferredColorScheme } from './appearanceStore.js';
2
2
  export declare const readDarkAppearance: () => boolean;
3
3
  export declare const subscribeAppearance: (listener: () => void) => () => void;
4
4
  export declare function useDarkAppearance(): boolean;
5
- /** Bounded trait and lifecycle log for installed-runtime resume diagnostics. */
5
+ /** Returns a short log of appearance changes and lifecycle events, for diagnosing appearance
6
+ * after a resume. */
6
7
  export declare const readAppearanceDiagnostics: () => readonly Readonly<{
7
8
  at: number;
8
9
  event: string;
@@ -12,7 +13,8 @@ export declare const readAppearanceDiagnostics: () => readonly Readonly<{
12
13
  eventDark?: boolean;
13
14
  heldDark: boolean;
14
15
  }>[];
15
- /** Set this installed scene's appearance. System or null resumes the retained system trait.
16
- * Store a user's choice with useAppStorage; the override itself lasts for this document. */
16
+ /** Forces the whole app into `light` or `dark`. `system` or null follows the system again. The
17
+ * override lasts until the page reloads, so store the user's choice with `useAppStorage` and
18
+ * apply it at launch. */
17
19
  export declare function preferredColorScheme(scheme: PreferredColorScheme): void;
18
20
  export type { PreferredColorScheme };
@@ -6,10 +6,12 @@ export const subscribeAppearance = (listener) => store().subscribe(listener);
6
6
  export function useDarkAppearance() {
7
7
  return useSyncExternalStore(subscribeAppearance, readDarkAppearance, () => false);
8
8
  }
9
- /** Bounded trait and lifecycle log for installed-runtime resume diagnostics. */
9
+ /** Returns a short log of appearance changes and lifecycle events, for diagnosing appearance
10
+ * after a resume. */
10
11
  export const readAppearanceDiagnostics = () => store().getDiagnostics();
11
- /** Set this installed scene's appearance. System or null resumes the retained system trait.
12
- * Store a user's choice with useAppStorage; the override itself lasts for this document. */
12
+ /** Forces the whole app into `light` or `dark`. `system` or null follows the system again. The
13
+ * override lasts until the page reloads, so store the user's choice with `useAppStorage` and
14
+ * apply it at launch. */
13
15
  export function preferredColorScheme(scheme) {
14
16
  store().setPreferredColorScheme(scheme);
15
17
  }
@@ -1,10 +1,11 @@
1
- /** `preferredColorScheme`: a whole-scene appearance override, or system appearance. */
1
+ /** An appearance override for the whole app: `light`, `dark`, or `system` (or null) to follow the
2
+ * system. */
2
3
  export type PreferredColorScheme = 'light' | 'dark' | 'system' | null;
3
4
  export type AppearanceSnapshot = Readonly<{
4
5
  dark: boolean;
5
6
  pending: boolean;
6
7
  }>;
7
- /** A diagnostic appearance event and its observed state. */
8
+ /** One entry in the appearance diagnostics log: what happened and the appearance seen then. */
8
9
  export type AppearanceEvent = Readonly<{
9
10
  at: number;
10
11
  event: string;
@@ -1,11 +1,11 @@
1
- /** Identity embedded in the running app's JavaScript bundle. The app build ID is
2
- * a unique build identifier, not a git revision or the service worker's cache hash. */
1
+ /** The framework version, build id, build time and build kind baked into the running bundle.
2
+ * The build id is unique per build. */
3
3
  export type BuildInfo = Readonly<{
4
4
  frameworkVersion: string;
5
5
  appBuildId: string;
6
6
  builtAt: string | null;
7
7
  buildKind: 'development' | 'production' | 'unavailable';
8
8
  }>;
9
- /** The build loaded by this app, including offline launches. Metadata comes from
10
- * satinApp at build time; it never reads the server's latest deployment. */
9
+ /** Returns the build that is running, even offline. The values are written by `satinApp` at
10
+ * build time; it never asks the server for the latest deployment. */
11
11
  export declare function useBuildInfo(): BuildInfo;
@@ -5,8 +5,8 @@ const buildInfo = Object.freeze(import.meta.env
5
5
  builtAt: null,
6
6
  buildKind: 'unavailable',
7
7
  });
8
- /** The build loaded by this app, including offline launches. Metadata comes from
9
- * satinApp at build time; it never reads the server's latest deployment. */
8
+ /** Returns the build that is running, even offline. The values are written by `satinApp` at
9
+ * build time; it never asks the server for the latest deployment. */
10
10
  export function useBuildInfo() {
11
11
  return buildInfo;
12
12
  }
@@ -2,14 +2,18 @@ import type { ViewportFitPolicy } from './viewportValue.js';
2
2
  import type { LaunchScreenSlot } from './launchScreen.js';
3
3
  import type { ServerReachabilityConfig } from '../reachability/model.js';
4
4
  declare const DEFINITION: unique symbol;
5
- /** A declared app capability that requires framework-owned runtime support. */
5
+ /** A capability the app declares so Satin sets up its runtime support: `push`, `badging` or
6
+ * `background-work`. */
6
7
  export type AppCapability = 'badging' | 'background-work' | 'push';
7
8
  /** An orientation that the app supports. */
8
9
  export type AppOrientation = 'landscape' | 'portrait';
9
- /** The presentation policy when the window uses an unsupported orientation. */
10
+ /** What the app shows when the device is in an orientation it does not support: a rotation
11
+ * prompt, a letterboxed layout, a responsive layout, or nothing. */
10
12
  export type OrientationFallback = 'letterbox' | 'none' | 'rotation-overlay' | 'responsive';
11
- /** When a prepared app update may activate. */
13
+ /** When a downloaded update may apply: `cold-launch`, the default, or `cold-launch-or-explicit`,
14
+ * which also lets it apply on request, as the development menu does. */
12
15
  export type UpdateActivation = 'cold-launch' | 'cold-launch-or-explicit';
16
+ /** One generated app icon file: its URL, pixel sizes, image type and purpose. */
13
17
  export interface AppIcon {
14
18
  readonly purpose?: 'any' | 'maskable' | 'monochrome';
15
19
  readonly sizes: string;
@@ -72,12 +76,16 @@ export type AppDeploymentInput = AppDeploymentOptions & ({
72
76
  readonly connectivity: 'server';
73
77
  readonly reachability: ServerReachabilityConfig;
74
78
  });
79
+ /** A validated app definition, returned by `defineApp`. Pass it to `runApp` and to the Vite
80
+ * plugin. */
75
81
  export type AppDefinition = AppDeploymentInput & {
76
82
  readonly [DEFINITION]: true;
77
83
  };
78
84
  export interface SatinAppShellPluginOptions {
79
85
  readonly additionalPrecache: readonly string[];
80
86
  }
87
+ /** Validates a full app definition and fills in its defaults. `defineApp` uses it; lab fixtures
88
+ * call it directly. */
81
89
  export declare function defineSatinApp(input: AppDeploymentInput): AppDefinition;
82
90
  export declare function generateSatinManifestJson(definition: AppDefinition, idiom?: 'phone' | 'pad'): string;
83
91
  export declare function generatePreReactAppearanceScript(definition: AppDefinition): string;
@@ -185,6 +185,8 @@ function cloneReachability(value) {
185
185
  surfaces: Object.freeze({ ...value.surfaces }),
186
186
  });
187
187
  }
188
+ /** Validates a full app definition and fills in its defaults. `defineApp` uses it; lab fixtures
189
+ * call it directly. */
188
190
  export function defineSatinApp(input) {
189
191
  assertRecord(input, 'app');
190
192
  assertString(input.id, 'id');
@@ -1,5 +1,6 @@
1
1
  import type { AppDefinition } from './definition.js';
2
2
  export { appManifestStem, appManifestFiles } from './manifests.js';
3
+ /** One app definition in a multi-install lab build, chosen by exact query parameters. */
3
4
  export interface AppFixture {
4
5
  /** Legacy display-mode capability rig only, never a production option. */
5
6
  readonly display?: 'standalone' | 'fullscreen' | 'minimal-ui';
@@ -7,4 +8,4 @@ export interface AppFixture {
7
8
  /** Exact query parameters for this lab-only installed app. First match wins. */
8
9
  readonly query: Readonly<Record<string, string>>;
9
10
  }
10
- export declare function generateAppHead(fixtures: readonly AppFixture[], base: string, production?: boolean, devServer?: boolean): string;
11
+ export declare function generateAppHead(fixtures: readonly AppFixture[], base: string, production?: boolean, devServer?: boolean, covers?: ReadonlyMap<string, string>, buildId?: string): string;
package/dist/app/head.js CHANGED
@@ -1,18 +1,18 @@
1
1
  import { registerEarlyWorker } from './earlyWorker.js';
2
2
  import { deploymentExpectation } from './deployment.js';
3
3
  import { installLaunchWatchdog } from './launchWatchdog.js';
4
- import { installLaunchScreenRuntime } from './launchScreenRuntime.js';
4
+ import { installLaunchScreenRuntime, resumeAppliedUpdate } from './launchScreenRuntime.js';
5
5
  import { GLASS_LAUNCH_BUDGET_MS } from './launchReadiness.js';
6
6
  import { appManifestHrefExpression, isIOSWebKit, installRouteForBase } from './install.js';
7
7
  import { resolvePlatformGate } from './platformPolicy.js';
8
- import { launchScreenCSS } from './launchScreen.js';
8
+ import { launchScreenMarkup, prelaunchCSS } from './launchScreen.js';
9
9
  import { installAppearanceStore } from './appearanceStore.js';
10
10
  import { ADAPTIVE_LAYOUT_POLICY } from '../layout/layoutClass.js';
11
11
  import { SATIN_VIEWPORT, resolveViewportFit } from './viewportValue.js';
12
12
  import { STARTUP_SCREENS, startupImages } from './startup.js';
13
13
  import { appManifestStem } from './manifests.js';
14
14
  export { appManifestStem, appManifestFiles } from './manifests.js';
15
- export function generateAppHead(fixtures, base, production = true, devServer = false) {
15
+ export function generateAppHead(fixtures, base, production = true, devServer = false, covers = new Map(), buildId) {
16
16
  const appearance = fixtures[0]?.app.appearance;
17
17
  const lightBackground = appearance?.background ?? '#ffffff';
18
18
  const darkBackground = appearance?.darkLaunch === 'match-light'
@@ -20,6 +20,7 @@ export function generateAppHead(fixtures, base, production = true, devServer = f
20
20
  : (appearance?.darkLaunch.background ?? '#000000');
21
21
  const data = JSON.stringify(fixtures.map(({ app, query }) => ({
22
22
  app,
23
+ cover: covers.get(app.id) ?? launchScreenMarkup(app),
23
24
  query,
24
25
  installRoute: installRouteForBase(app, base),
25
26
  startupStem: appManifestStem(app),
@@ -27,7 +28,7 @@ export function generateAppHead(fixtures, base, production = true, devServer = f
27
28
  }))).replaceAll('<', '\\u003c');
28
29
  // A cached navigation must paint its launch surface even before head scripts
29
30
  // run. Match the manifest's two backgrounds, including match-light apps.
30
- return `<style id="satin-launch-paint">:root{--satin-launch-background:${lightBackground};--satin-launch-ink:#1c1c1e;--satin-background:${lightBackground}}@media(prefers-color-scheme:dark){:root{--satin-launch-background:${darkBackground};--satin-launch-ink:${appearance?.darkLaunch === 'match-light' ? '#1c1c1e' : '#ffffff'};--satin-background:${darkBackground}}}${launchScreenCSS}html,body{background:var(--satin-background);margin:0} [data-satin-root][data-satin-launch]:not([data-satin-launch="ready"]){opacity:0;pointer-events:none}</style>
31
+ return `<style id="satin-launch-paint">:root{--satin-launch-background:${lightBackground};--satin-launch-ink:#1c1c1e;--satin-background:${lightBackground}}@media(prefers-color-scheme:dark){:root{--satin-launch-background:${darkBackground};--satin-launch-ink:${appearance?.darkLaunch === 'match-light' ? '#1c1c1e' : '#ffffff'};--satin-background:${darkBackground}}}html,body{background:var(--satin-background);margin:0} [data-satin-root][data-satin-launch]:not([data-satin-launch="ready"]){opacity:0;pointer-events:none}</style>
31
32
  <script id="satin-platform-gate">(() => {
32
33
  performance.mark('satin-document-head');
33
34
  const fixtures = ${data};
@@ -68,7 +69,13 @@ export function generateAppHead(fixtures, base, production = true, devServer = f
68
69
  const fit = (${resolveViewportFit.toString()})(app.viewportFit || 'adaptive', navigator.userAgent, navigator.platform, navigator.maxTouchPoints, innerWidth, innerHeight, ${ADAPTIVE_LAYOUT_POLICY.horizontal.regularMinimum}, ${ADAPTIVE_LAYOUT_POLICY.vertical.regularMinimum});
69
70
  document.querySelector('meta[name="viewport"]').content = ${JSON.stringify(SATIN_VIEWPORT)}.replace('viewport-fit=cover', 'viewport-fit=' + fit);
70
71
  document.documentElement.dataset.satinViewportFit = fit;
71
- (${installLaunchScreenRuntime.toString()})();
72
+ (${installLaunchScreenRuntime.toString()})({
73
+ markup: selected.cover, css: ${JSON.stringify(prelaunchCSS)}, scope: app.scope,
74
+ background: app.appearance.background,
75
+ darkBackground: app.appearance.darkLaunch === 'match-light' ? app.appearance.background : app.appearance.darkLaunch.background,
76
+ darkInk: app.appearance.darkLaunch === 'match-light' ? '#1c1c1e' : '#ffffff'
77
+ });
78
+ (${resumeAppliedUpdate.toString()})(app.scope, app.updateProfile === 'release' ? ${JSON.stringify(buildId)} : undefined);
72
79
  (${installLaunchWatchdog.toString()})(${Math.max(10_000, GLASS_LAUNCH_BUDGET_MS)});
73
80
  // First installation can overlap module loading. Existing registrations,
74
81
  // with active or waiting versions, remain under the normal launch/update policy.
@@ -89,18 +96,7 @@ export function generateAppHead(fixtures, base, production = true, devServer = f
89
96
  const darkBackground = app.appearance.darkLaunch === 'match-light' ? app.appearance.background : app.appearance.darkLaunch.background;
90
97
  // CSS can select the cold background before the controller runs. Once
91
98
  // installed, the shared controller owns every root color, in every session.
92
- const field = document.createElement('style');
93
99
  const darkInk = app.appearance.darkLaunch === 'match-light' ? '#1c1c1e' : '#ffffff';
94
- field.textContent = [
95
- ':root{--satin-launch-background:' + app.appearance.background + ';--satin-launch-ink:#1c1c1e}',
96
- '@media(prefers-color-scheme:dark){:root:not([data-satin-appearance]){',
97
- '--satin-launch-background:' + darkBackground + ';--satin-launch-ink:' + darkInk + '}}',
98
- ':root[data-satin-appearance="light"]{--satin-launch-background:' + app.appearance.background,
99
- ';--satin-launch-ink:#1c1c1e}',
100
- ':root[data-satin-appearance="dark"]{--satin-launch-background:' + darkBackground,
101
- ';--satin-launch-ink:' + darkInk + '}',
102
- ].join('');
103
- document.head.appendChild(field);
104
100
  const store = (${installAppearanceStore.toString()})();
105
101
  const apply = () => {
106
102
  const dark = store.getSnapshot().dark;
@@ -109,6 +105,7 @@ export function generateAppHead(fixtures, base, production = true, devServer = f
109
105
  document.documentElement.style.setProperty('--satin-launch-background', appearance.background);
110
106
  document.documentElement.style.setProperty('--satin-launch-ink', dark ? darkInk : '#1c1c1e');
111
107
  document.documentElement.style.setProperty('--satin-background', appearance.background);
108
+ window.__SATIN_PRELAUNCH__.appearance(appearance.background, dark ? darkInk : '#1c1c1e');
112
109
  document.getElementById('satin-theme-color').content = appearance.background;
113
110
  document.getElementById('satin-manifest').href = ${appManifestHrefExpression('selected.manifest')};
114
111
  };
@@ -0,0 +1,6 @@
1
+ /** Add these sources to a deployment's existing policy. Satin does not impose
2
+ * a site-wide CSP. Hashes cover exact final bytes, including the shadow style. */
3
+ export declare function inlinePolicySources(html: string, shadowStyle: string): {
4
+ scriptSrc: string[];
5
+ styleSrc: string[];
6
+ };
@@ -0,0 +1,25 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** Add these sources to a deployment's existing policy. Satin does not impose
3
+ * a site-wide CSP. Hashes cover exact final bytes, including the shadow style. */
4
+ export function inlinePolicySources(html, shadowStyle) {
5
+ const digest = (value) => `'sha256-${createHash('sha256').update(value).digest('base64')}'`;
6
+ const scripts = new Set();
7
+ const styles = new Set([digest(shadowStyle)]);
8
+ for (const match of html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)) {
9
+ if (/\b(?:src|type\s*=\s*["']application\/json)/i.test(match[1]) || !match[2].trim())
10
+ continue;
11
+ scripts.add(digest(match[2]));
12
+ // Platform admission reinserts the original parser script. A hash policy
13
+ // must admit that script as well as the small admission wrapper.
14
+ const original = match[2].match(/const script = ("(?:[^"\\]|\\.)*");/);
15
+ if (original) {
16
+ const tag = JSON.parse(original[1]);
17
+ const body = tag.match(/<script\b([^>]*)>([\s\S]*?)<\/script>/i);
18
+ if (body && !/\bsrc\s*=/.test(body[1]) && body[2].trim())
19
+ scripts.add(digest(body[2]));
20
+ }
21
+ }
22
+ for (const match of html.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi))
23
+ styles.add(digest(match[1]));
24
+ return { scriptSrc: [...scripts], styleSrc: [...styles] };
25
+ }
@@ -11,6 +11,7 @@ export declare function appManifestHrefExpression(stemExpression: string): strin
11
11
  export declare function installRouteForBase(app: AppDefinition, base: string): string;
12
12
  /** Preview builds accept device/layout overrides; production never does. */
13
13
  export interface InstallPageOptions {
14
+ readonly cover?: string;
14
15
  readonly preview?: boolean;
15
16
  /** The author's bundled install page, set by satinApp(app, { installPage }). */
16
17
  readonly customPage?: string;
@@ -1,3 +1,5 @@
1
+ import { installLaunchScreenRuntime } from './launchScreenRuntime.js';
2
+ import { launchScreenMarkup, prelaunchCSS } from './launchScreen.js';
1
3
  import { resolveDeviceIdiom } from '../layout/deviceIdiom.js';
2
4
  import { checkDeployment, deploymentExpectation } from './deployment.js';
3
5
  import { installPageCheck } from './installCheck.js';
@@ -48,12 +50,13 @@ export function installPage(app, kind, base = '/', options = {}) {
48
50
  .join('') +
49
51
  '</noscript>'
50
52
  : '';
51
- const startup = `<script src="${app.scope}${stem}-startup.js"></script>${fallback}`;
53
+ const startup = `<script defer src="${app.scope}${stem}-startup.js"></script>${fallback}`;
52
54
  const status = `<meta name="apple-mobile-web-app-capable" content="yes"><meta id="satin-install-status" name="apple-mobile-web-app-status-bar-style" content="default"><meta name="theme-color" content="${app.appearance.background}" media="(prefers-color-scheme: light)"><meta name="theme-color" content="${dark}" media="(prefers-color-scheme: dark)">`;
53
55
  const title = kind === 'gate' ? 'This app is designed for iPhone and iPad' : app.name;
54
56
  const body = `<p>${escape(app.description)}</p><a href="${escape(installRouteForBase(app, base))}">Add to Home Screen</a>`;
55
57
  const head = `<meta charset="utf-8"><meta name="viewport" content="${SATIN_VIEWPORT}"><script>(()=>{const fit=(${resolveViewportFit.toString()})(${JSON.stringify(app.viewportFit ?? 'adaptive')},navigator.userAgent,navigator.platform,navigator.maxTouchPoints,innerWidth,innerHeight,${ADAPTIVE_LAYOUT_POLICY.horizontal.regularMinimum},${ADAPTIVE_LAYOUT_POLICY.vertical.regularMinimum});document.querySelector('meta[name="viewport"]').content=${JSON.stringify(SATIN_VIEWPORT)}.replace('viewport-fit=cover','viewport-fit='+fit)})();</script><title>${escape(title)}</title>${status}${startup}<link rel="manifest" href="${base}${stem}.webmanifest"><script>(()=>{document.querySelector('link[rel="manifest"]').href=${appManifestHrefExpression(JSON.stringify(base + stem))}})();</script><link rel="apple-touch-icon" href="${escape(icon.src)}">`;
56
- const scripts = `<script id="satin-deployment" type="application/json">${JSON.stringify([deploymentExpectation(app, base)]).replaceAll('<', '\\u003c')}</script>${kind === 'install' ? `<script data-satin-install-check${options.customPage ? ` data-satin-install-entry="${escape(options.customPage)}"` : ''}>(${installPageCheck.toString()})(${JSON.stringify(deploymentExpectation(app, base))},${checkDeployment.toString()},4000${options.customPage ? `,(${installCustomMount.toString()}).bind(null,${JSON.stringify(options.customPage)})` : ''}).catch(error=>{console.error("[Satin deployment]",error);const status=document.getElementById("satin-install-check");status.textContent="This app is misconfigured: configuration check failed. Reload and check the console.";status.setAttribute("role","alert");status.dataset.error="true";status.hidden=false;document.getElementById("satin-install-guide").hidden=true})</script>` : ''}`;
58
+ const environment = `<script id="satin-prelaunch-install">(${installLaunchScreenRuntime.toString()})(${JSON.stringify({ markup: options.cover ?? launchScreenMarkup({ ...app, launchScreen: { icon: 'data:,' } }), css: prelaunchCSS, scope: app.scope, background: app.appearance.background, darkBackground: dark, darkInk: app.appearance.darkLaunch === 'match-light' ? '#1c1c1e' : '#fff', cover: false }).replaceAll('<', '\\u003c')});</script>`;
59
+ const scripts = `${kind === 'install' ? environment : ''}<script id="satin-deployment" type="application/json">${JSON.stringify([deploymentExpectation(app, base)]).replaceAll('<', '\\u003c')}</script>${kind === 'install' ? `<script data-satin-install-check${options.customPage ? ` data-satin-install-entry="${escape(options.customPage)}"` : ''}>(${installPageCheck.toString()})(${JSON.stringify(deploymentExpectation(app, base))},${checkDeployment.toString()},4000${options.customPage ? `,(${installCustomMount.toString()}).bind(null,${JSON.stringify(options.customPage)})` : ''}).catch(error=>{console.error("[Satin deployment]",error);window.__SATIN_PRELAUNCH__.error("Configuration check failed. Reload and check the console.","This app is misconfigured");document.getElementById("satin-install-guide").hidden=true})</script>` : ''}`;
57
60
  if (options.customPage && kind === 'install') {
58
61
  return customInstallPage({
59
62
  head,