@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,9 +1,32 @@
1
- // Serialized into the head so failures before module loading use the same
2
- // controller as ordinary readiness. Keep this function self-contained.
3
- export function installLaunchScreenRuntime() {
1
+ export function installLaunchScreenRuntime(options) {
4
2
  const launchWindow = window;
5
- if (launchWindow.__SATIN_LAUNCH_SCREEN__)
6
- return launchWindow.__SATIN_LAUNCH_SCREEN__;
3
+ if (launchWindow.__SATIN_PRELAUNCH__)
4
+ return launchWindow.__SATIN_PRELAUNCH__;
5
+ const host = document.createElement('satin-prelaunch');
6
+ const root = host.attachShadow({ mode: 'closed' });
7
+ const style = document.createElement('style');
8
+ style.nonce = options.nonce ?? document.currentScript?.nonce ?? '';
9
+ style.textContent = options.css;
10
+ const content = document.createElement('div');
11
+ content.innerHTML = options.markup;
12
+ root.append(style, content);
13
+ document.documentElement.append(host);
14
+ const element = (id) => root.getElementById(id);
15
+ const cover = element('satin-launch-screen');
16
+ const failure = document.createElement('section');
17
+ failure.id = 'satin-launch-error';
18
+ failure.setAttribute('role', 'alert');
19
+ failure.hidden = true;
20
+ let state = options.cover === false ? 'idle' : 'preparing';
21
+ cover.hidden = options.cover === false;
22
+ const dark = matchMedia('(prefers-color-scheme: dark)');
23
+ const appearance = (background, ink) => {
24
+ host.style.setProperty('--satin-launch-background', background, 'important');
25
+ host.style.setProperty('--satin-launch-ink', ink, 'important');
26
+ };
27
+ const systemAppearance = () => appearance(dark.matches ? options.darkBackground : options.background, dark.matches ? options.darkInk : '#1c1c1e');
28
+ systemAppearance();
29
+ dark.addEventListener?.('change', systemAppearance);
7
30
  let value = 0;
8
31
  let velocity = 0;
9
32
  let target = 0;
@@ -21,7 +44,7 @@ export function installLaunchScreenRuntime() {
21
44
  // Extend the cover into that excluded space so its center stays in the
22
45
  // native startup image's screen frame. Windowed apps keep their own frame.
23
46
  const excludedTop = standalone && fullWidth ? Math.max(0, screen.height - innerHeight) : 0;
24
- document.documentElement.style.setProperty('--satin-launch-excluded-top', `${excludedTop}px`);
47
+ host.style.setProperty('--satin-launch-excluded-top', `${excludedTop}px`, 'important');
25
48
  };
26
49
  alignCover();
27
50
  window.addEventListener('resize', alignCover);
@@ -33,7 +56,7 @@ export function installLaunchScreenRuntime() {
33
56
  let retiring = null;
34
57
  let appliedUpdate = null;
35
58
  let updateAnimation = null;
36
- const ringElement = () => document.getElementById('satin-launch-progress');
59
+ const ringElement = () => element('satin-launch-progress');
37
60
  const paint = () => {
38
61
  ringElement()
39
62
  ?.querySelector('circle')
@@ -101,7 +124,7 @@ export function installLaunchScreenRuntime() {
101
124
  .finished.catch(() => { });
102
125
  };
103
126
  const retire = (onRetired) => {
104
- const screen = document.getElementById('satin-launch-screen');
127
+ const screen = element('satin-launch-screen');
105
128
  if (!screen) {
106
129
  onRetired();
107
130
  return;
@@ -110,7 +133,7 @@ export function installLaunchScreenRuntime() {
110
133
  retiring ??= (async () => {
111
134
  const ring = ringElement();
112
135
  if (ring && !ring.hasAttribute('hidden')) {
113
- // React and head failure panels must stay behind the completing arc.
136
+ // The failure panel stays behind the completing arc.
114
137
  screen.style.zIndex = '20003';
115
138
  ring.setAttribute('aria-label', 'Finishing launch');
116
139
  await complete();
@@ -135,6 +158,100 @@ export function installLaunchScreenRuntime() {
135
158
  });
136
159
  };
137
160
  const controller = {
161
+ version: 1,
162
+ stylesReady() {
163
+ return Promise.all([...document.querySelectorAll('link[data-satin-style]')].map(link => {
164
+ if (link.dataset.satinStyleReady === 'true')
165
+ return Promise.resolve();
166
+ return new Promise((resolve, reject) => {
167
+ const cleanup = () => {
168
+ link.removeEventListener('load', loaded);
169
+ link.removeEventListener('error', failed);
170
+ };
171
+ const loaded = () => {
172
+ cleanup();
173
+ resolve();
174
+ };
175
+ const failed = () => {
176
+ cleanup();
177
+ reject(new Error('The app stylesheet failed to load. Please retry.'));
178
+ };
179
+ if (link.dataset.satinStyleError === 'true') {
180
+ failed();
181
+ return;
182
+ }
183
+ link.addEventListener('load', loaded, { once: true });
184
+ link.addEventListener('error', failed, { once: true });
185
+ });
186
+ })).then(() => { });
187
+ },
188
+ appearance,
189
+ snapshot() {
190
+ const ring = ringElement();
191
+ const box = ring.getBoundingClientRect();
192
+ const style = getComputedStyle(ring);
193
+ return {
194
+ version: 1,
195
+ state,
196
+ error: failure.isConnected ? (failure.textContent ?? '') : '',
197
+ coverVisible: !cover.hidden,
198
+ ringVisible: !cover.hidden &&
199
+ !ring.hasAttribute('hidden') &&
200
+ style.display !== 'none' &&
201
+ Number(style.opacity) > 0,
202
+ label: ring.getAttribute('aria-label'),
203
+ offset: getComputedStyle(ring.querySelector('circle')).strokeDashoffset,
204
+ box: [box.x, box.y, box.width, box.height],
205
+ background: getComputedStyle(cover).backgroundColor,
206
+ };
207
+ },
208
+ error(reason, title = 'Unable to Open App', retry, detail) {
209
+ state = 'failed';
210
+ failure.replaceChildren();
211
+ const heading = document.createElement('h1');
212
+ heading.textContent = title;
213
+ const message = document.createElement('p');
214
+ message.textContent = reason;
215
+ const diagnostics = document.createElement('p');
216
+ diagnostics.className = 'detail';
217
+ diagnostics.textContent = detail ?? '';
218
+ diagnostics.hidden = !detail;
219
+ const button = document.createElement('button');
220
+ button.type = 'button';
221
+ button.textContent = 'Retry';
222
+ button.onclick =
223
+ retry ??
224
+ (() => {
225
+ try {
226
+ sessionStorage.setItem('satin-install-reset:' + options.scope, '1');
227
+ }
228
+ catch {
229
+ /* Retry still reloads without storage. */
230
+ }
231
+ location.reload();
232
+ });
233
+ failure.append(heading, message, diagnostics, button);
234
+ root.append(failure);
235
+ failure.hidden = false;
236
+ // The same controller finishes a visible arc before showing its failure.
237
+ if (ringElement().hasAttribute('hidden'))
238
+ cover.hidden = true;
239
+ else
240
+ controller.fail(() => {
241
+ if (state === 'failed')
242
+ cover.hidden = true;
243
+ });
244
+ },
245
+ recover() {
246
+ failure.remove();
247
+ if (state === 'failed' && !revealed) {
248
+ state = options.cover === false ? 'idle' : 'preparing';
249
+ if (options.cover === false)
250
+ cover.hidden = true;
251
+ else
252
+ controller.restore();
253
+ }
254
+ },
138
255
  updated(start = 0) {
139
256
  if (appliedUpdate)
140
257
  return appliedUpdate;
@@ -179,7 +296,7 @@ export function installLaunchScreenRuntime() {
179
296
  const ring = ringElement();
180
297
  // A phase ending does not mean the cover is ready to retire. Keep the
181
298
  // real last sample until readiness or an explicit error owns the handoff.
182
- if (!ring || revealed || retiring || !progress || progress.total <= 0)
299
+ if (revealed || retiring || appliedUpdate || !progress || progress.total <= 0)
183
300
  return;
184
301
  if (!Number.isFinite(progress.loaded) || !Number.isFinite(progress.total))
185
302
  return;
@@ -194,9 +311,11 @@ export function installLaunchScreenRuntime() {
194
311
  },
195
312
  complete,
196
313
  reveal() {
197
- if (revealed)
314
+ if (revealed || state === 'failed')
198
315
  return;
199
316
  revealed = true;
317
+ state = 'ready';
318
+ failure.remove();
200
319
  window.dispatchEvent(new Event('satin-launch-revealed'));
201
320
  retire(() => { });
202
321
  },
@@ -218,7 +337,7 @@ export function installLaunchScreenRuntime() {
218
337
  // spring, rather than leaving a cancelled completion targeted at 100.
219
338
  target = downloadTarget;
220
339
  const ring = ringElement();
221
- const cover = document.getElementById('satin-launch-screen');
340
+ const cover = element('satin-launch-screen');
222
341
  if (ring && !ring.hasAttribute('hidden'))
223
342
  start();
224
343
  for (const [element, duration] of [
@@ -227,7 +346,7 @@ export function installLaunchScreenRuntime() {
227
346
  ]) {
228
347
  if (!element)
229
348
  continue;
230
- const opacity = Number(getComputedStyle(element).opacity);
349
+ const opacity = Number.parseFloat(getComputedStyle(element).opacity);
231
350
  element.getAnimations().forEach(animation => animation.cancel());
232
351
  if (Number.isFinite(opacity) && opacity < 1 && !element.hidden) {
233
352
  void element
@@ -239,38 +358,47 @@ export function installLaunchScreenRuntime() {
239
358
  .finished.catch(() => { });
240
359
  }
241
360
  }
242
- const screen = document.getElementById('satin-launch-screen');
361
+ const screen = element('satin-launch-screen');
243
362
  if (screen) {
244
363
  screen.hidden = false;
245
364
  screen.style.removeProperty('z-index');
246
365
  }
247
366
  },
248
367
  };
249
- launchWindow.__SATIN_LAUNCH_SCREEN__ = controller;
368
+ launchWindow.__SATIN_PRELAUNCH__ = controller;
250
369
  return controller;
251
370
  }
371
+ export function prelaunchController() {
372
+ const controller = window.__SATIN_PRELAUNCH__;
373
+ if (!controller || controller.version !== 1)
374
+ throw new Error('Satin pre-launch v1 bootstrap is missing.');
375
+ return controller;
376
+ }
377
+ export function showLaunchFailure(reason, title) {
378
+ prelaunchController().error(reason, title);
379
+ }
252
380
  export function setLaunchScreen(visible) {
253
381
  if (visible) {
254
- installLaunchScreenRuntime().restore();
382
+ prelaunchController().restore();
255
383
  return;
256
384
  }
257
- installLaunchScreenRuntime().reveal();
385
+ prelaunchController().reveal();
258
386
  }
259
387
  export function setLaunchProgress(progress) {
260
388
  window.dispatchEvent(new CustomEvent('satin-launch-progress', { detail: progress }));
261
- installLaunchScreenRuntime().progress(progress);
389
+ prelaunchController().progress(progress);
262
390
  }
263
391
  /** Complete a visible arc before a cold-update document replacement. */
264
392
  export function completeLaunchProgress() {
265
393
  if (typeof document === 'undefined' || typeof document.getElementById !== 'function')
266
394
  return Promise.resolve(true);
267
- return installLaunchScreenRuntime().complete();
395
+ return prelaunchController().complete();
268
396
  }
269
397
  /** Confirm an applied update on its successor document before revealing the app. */
270
398
  export function showAppliedUpdate() {
271
- return installLaunchScreenRuntime().updated();
399
+ return prelaunchController().updated();
272
400
  }
273
- /** Serialized after the declarative cover, before modules and their first paint. */
401
+ /** Runs immediately after the inline environment, before application modules. */
274
402
  export function resumeAppliedUpdate(scope, buildId) {
275
403
  let ringStart = null;
276
404
  try {
@@ -309,5 +437,5 @@ export function resumeAppliedUpdate(scope, buildId) {
309
437
  }, { once: true });
310
438
  }
311
439
  if (ringStart !== null)
312
- void window.__SATIN_LAUNCH_SCREEN__?.updated(ringStart);
440
+ void window.__SATIN_PRELAUNCH__?.updated(ringStart);
313
441
  }
@@ -16,63 +16,34 @@ export function installLaunchWatchdog(budgetMs) {
16
16
  .filter(mark => mark.name.startsWith('satin-'));
17
17
  const stage = marks.at(-1)?.name ?? 'satin-document-head';
18
18
  console.error('[Satin launch]', reason, 'Last stage:', stage, recordedError);
19
- const screen = document.createElement('section');
20
- screen.id = 'satin-launch-error';
21
- screen.setAttribute('role', 'alert');
22
- screen.style.cssText =
23
- 'position:fixed;inset:0;z-index:20002;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px;box-sizing:border-box;background:var(--satin-launch-background,#f2f2f7);color:var(--satin-launch-ink,#1c1c1e);font:17px/1.45 system-ui;text-align:center;pointer-events:auto';
24
- const title = document.createElement('h1');
25
- title.textContent = 'Unable to Open App';
26
- title.style.cssText = 'font-size:24px;margin:0 0 12px';
27
- const message = document.createElement('p');
28
- message.textContent = reason;
29
- message.style.maxWidth = '420px';
30
- const reached = document.createElement('p');
31
- reached.textContent = 'Last stage: ' + stage;
32
- reached.style.cssText = 'font-size:13px;opacity:.65;overflow-wrap:anywhere';
33
- const retry = document.createElement('button');
34
- retry.textContent = 'Retry';
35
- retry.style.cssText =
36
- 'font:600 17px system-ui;color:#007aff;background:transparent;border:0;padding:16px 32px;min-height:44px';
37
- retry.onclick = () => {
38
- const definition = window
39
- .__SATIN_DEFINITION__;
40
- try {
41
- sessionStorage.setItem('satin-install-reset:' + (definition?.scope ?? '/'), '1');
42
- }
43
- catch {
44
- // A reload can still join the worker after its own deadline.
45
- }
46
- location.reload();
47
- };
48
- screen.append(title, message, reached, retry);
49
- (document.body ?? document.documentElement).append(screen);
50
- if (!document.body) {
51
- document.addEventListener('DOMContentLoaded', () => {
52
- if (screen.isConnected)
53
- document.body?.append(screen);
54
- }, { once: true });
55
- }
56
- const cover = document.getElementById('satin-launch-screen');
57
- const controller = window.__SATIN_LAUNCH_SCREEN__;
58
- const ring = document.getElementById('satin-launch-progress');
59
- if (cover && ring && !ring.hasAttribute('hidden') && controller) {
60
- // Put the failure underneath the cover until its visible arc completes.
61
- screen.style.zIndex = '19999';
62
- controller.fail(() => {
63
- if (failed)
64
- screen.style.zIndex = '20002';
65
- });
66
- }
67
- else if (cover)
68
- cover.hidden = true;
19
+ const controller = window.__SATIN_PRELAUNCH__;
20
+ controller?.error(reason, 'Unable to Open App', undefined, 'Last stage: ' + stage);
69
21
  performance.mark('satin-launch-failed');
70
22
  };
71
23
  window.addEventListener('error', event => {
72
- recordedError = event.message || 'Application script failed to load.';
73
- });
24
+ const target = event.target;
25
+ if (target instanceof HTMLLinkElement && target.rel === 'stylesheet') {
26
+ target.dataset.satinStyleError = 'true';
27
+ showFailure('The app stylesheet failed to load. Please retry.');
28
+ }
29
+ else if (target instanceof HTMLScriptElement) {
30
+ showFailure('The application script failed to load. Please retry.');
31
+ }
32
+ else if (event instanceof ErrorEvent) {
33
+ recordedError = event.message || 'Application script failed to load.';
34
+ showFailure('Launch failed. ' + recordedError.split('\n')[0].slice(0, 160));
35
+ }
36
+ }, true);
37
+ document.addEventListener('load', event => {
38
+ const link = event.target;
39
+ if (link instanceof HTMLLinkElement && link.hasAttribute('data-satin-style')) {
40
+ link.media = link.getAttribute('data-satin-style') || 'all';
41
+ link.dataset.satinStyleReady = 'true';
42
+ }
43
+ }, true);
74
44
  window.addEventListener('unhandledrejection', event => {
75
45
  recordedError = event.reason instanceof Error ? event.reason.message : String(event.reason);
46
+ showFailure('Launch failed. ' + recordedError.split('\n')[0].slice(0, 160));
76
47
  });
77
48
  window.addEventListener('satin-glass-error', event => {
78
49
  const error = event.detail;
@@ -80,19 +51,8 @@ export function installLaunchWatchdog(budgetMs) {
80
51
  showFailure('Glass could not start: ' + error.code + '.');
81
52
  });
82
53
  const recover = () => {
83
- const restoreCover = failed;
84
54
  failed = false;
85
- document.getElementById('satin-launch-error')?.remove();
86
- const cover = document.getElementById('satin-launch-screen');
87
- // The runtime owns normal ring settlement and cover retirement. Only undo
88
- // the cover hiding performed by this watchdog's own failure screen.
89
- if (cover && restoreCover) {
90
- const controller = window.__SATIN_LAUNCH_SCREEN__;
91
- if (controller)
92
- controller.restore();
93
- else
94
- cover.hidden = false;
95
- }
55
+ window.__SATIN_PRELAUNCH__?.recover();
96
56
  };
97
57
  const receiveProgress = (progress) => {
98
58
  if (progress && (progress.loaded > lastBytes || (progress.activity ?? 0) > lastActivity)) {
@@ -178,7 +138,7 @@ export function installLaunchWatchdog(budgetMs) {
178
138
  // A first install can download before the application module is available.
179
139
  // Controlled documents are cached launches/updates and keep their existing policy.
180
140
  if (!navigator.serviceWorker?.controller && !revealed)
181
- window.__SATIN_LAUNCH_SCREEN__?.progress(progress);
141
+ window.__SATIN_PRELAUNCH__?.progress(progress);
182
142
  }
183
143
  });
184
144
  // A client's worker messages are queued until DOMContentLoaded, and a bootstrap module that
@@ -32,6 +32,17 @@ export function gateAppScripts(html) {
32
32
  html.slice(firstScript.index, gate.index) +
33
33
  html.slice(gate.index + gate[0].length);
34
34
  }
35
+ // Fetch app styles without blocking the independent head environment. The
36
+ // head's capture listener activates each stylesheet only after it loads.
37
+ html = html.replace(/<link\b[^>]*>/gi, tag => {
38
+ const attributes = scriptAttributes(tag.slice(5, -1));
39
+ if (attributes.get('rel')?.value !== 'stylesheet')
40
+ return tag;
41
+ const media = attributes.get('media')?.value ?? 'all';
42
+ return tag
43
+ .replace(/\smedia\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/i, '')
44
+ .replace(/>$/, ` media="not all" data-satin-style="${media.replaceAll('"', '&quot;')}">`);
45
+ });
35
46
  return html.replace(scriptPattern, (original, attributes) => {
36
47
  if (attributes === undefined)
37
48
  return original;
@@ -0,0 +1,3 @@
1
+ import { type Plugin } from 'vite';
2
+ export declare function frameworkStylesheet(base: string): string;
3
+ export declare function includedStyles(): Plugin;
@@ -0,0 +1,42 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import { normalizePath } from 'vite';
3
+ const stylesheet = normalizePath(fileURLToPath(new URL('../styles.css', import.meta.url)));
4
+ const legacyImport = '\0satin:included-styles';
5
+ const stylesheetUrl = '/@satin/framework.css';
6
+ export function frameworkStylesheet(base) {
7
+ // Keep this a real stylesheet link: WebKit must load it before first paint.
8
+ // Vite processes the link during builds and precaches the resulting CSS asset.
9
+ return `<link rel="stylesheet" href="${base}${stylesheetUrl.slice(1)}">`;
10
+ }
11
+ export function includedStyles() {
12
+ let serving = false;
13
+ let base = '/';
14
+ return {
15
+ name: 'satin:included-styles',
16
+ enforce: 'pre',
17
+ configResolved(config) {
18
+ serving = config.command === 'serve';
19
+ base = config.base;
20
+ },
21
+ resolveId(id, importer) {
22
+ // Resolve a URL rather than embedding a filesystem path in HTML. Vite's
23
+ // build resolver does not URL-decode package paths such as @scope or spaces.
24
+ const queryAt = id.indexOf('?');
25
+ const path = queryAt < 0 ? id : id.slice(0, queryAt);
26
+ if (path === stylesheetUrl || path === base + stylesheetUrl.slice(1))
27
+ return stylesheet + (queryAt < 0 ? '' : id.slice(queryAt));
28
+ // In production Vite deduplicates the link and import in its CSS graph.
29
+ // In dev a JS CSS import would add a second <style> after the app CSS.
30
+ // The link already owns loading and CSS HMR, so old side-effect imports
31
+ // are empty modules. Leave CSS @imports and explicit ?inline/?url uses alone.
32
+ if (!serving || !importer || /\.(?:css|scss|sass|less|styl)(?:\?|$)/.test(importer))
33
+ return;
34
+ if (id === '@satinhq/kit/styles.css' || normalizePath(id) === stylesheet)
35
+ return legacyImport;
36
+ },
37
+ load(id) {
38
+ if (id === legacyImport)
39
+ return 'export {}';
40
+ },
41
+ };
42
+ }
@@ -1,4 +1,5 @@
1
- /** The viewport fitting policy selected before rendering an app. */
1
+ /** How the app fits the screen around the notch and home indicator. `adaptive` picks a fit per
2
+ * device before the app renders. */
2
3
  export type ViewportFitPolicy = 'adaptive' | 'auto' | 'cover' | 'contain';
3
4
  export declare const SATIN_VIEWPORT = "width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover";
4
5
  /** Self-contained so the generated head and runtime use exactly the same rule. */
@@ -1,6 +1,8 @@
1
1
  import type { Plugin } from 'vite';
2
2
  import { type AppDefinition } from './definition.js';
3
3
  import { type AppFixture } from './head.js';
4
+ /** Build options for `satinApp`: a custom install page, strict glass, the development menu and a
5
+ * media probe. */
4
6
  export interface SatinAppOptions {
5
7
  /** Output-relative bundled probe for remote-only media. Default: first emitted media
6
8
  * file. Proves this deployment server only, never the remote source or playback. */
@@ -22,6 +24,8 @@ export interface SatinAppOptions {
22
24
  readonly installPage?: string;
23
25
  }
24
26
  export declare function platformBuildWarning(app: AppDefinition, production: boolean): string | undefined;
27
+ /** Generates the app shell and loads framework CSS once, before app styles.
28
+ * The stylesheet participates in Vite CSS HMR and builds; pre-launch paints independently. */
25
29
  export declare function satinApp(app: AppDefinition, options?: SatinAppOptions): Plugin[];
26
30
  /** Multi-install lab fixture only. Production apps use satinApp(app). */
27
31
  export declare function satinAppFixtures(input: readonly AppFixture[], options?: SatinAppOptions): Plugin[];
package/dist/app/vite.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { inlinePolicySources } from './inlineCSP.js';
1
2
  import { bundledLicenseNotices, renderNotices } from '../acknowledgements/bundledNotices.js';
2
3
  import { serveStaticFile } from '../server.js';
3
4
  import { mediaTypes } from './mediaTypes.js';
@@ -5,8 +6,7 @@ import { extname } from 'node:path';
5
6
  import { imageAssets } from '../image/vite.js';
6
7
  import { generateAppIcons, generateStartupImage } from './icons.js';
7
8
  import { generateWebClipProfile } from './mdm.js';
8
- import { launchScreenMarkup } from './launchScreen.js';
9
- import { resumeAppliedUpdate } from './launchScreenRuntime.js';
9
+ import { launchScreenMarkup, prelaunchCSS } from './launchScreen.js';
10
10
  import { startupImages } from './startup.js';
11
11
  import { access, readFile, writeFile, mkdir } from 'node:fs/promises';
12
12
  import { installPage, installRouteForBase } from './install.js';
@@ -19,6 +19,7 @@ import { deploymentExpectation } from './deployment.js';
19
19
  import { gateAppScripts } from './platformScripts.js';
20
20
  import { devMenuBuild } from '../dev-menu/build.js';
21
21
  import { createBuildInfo } from './buildInfoBuild.js';
22
+ import { frameworkStylesheet, includedStyles } from './styles.js';
22
23
  export function platformBuildWarning(app, production) {
23
24
  if (!production || app.platforms !== 'all')
24
25
  return undefined;
@@ -26,6 +27,8 @@ export function platformBuildWarning(app, production) {
26
27
  'desktop, Chromium and Gecko browsers to execute your app. ' +
27
28
  "Satin only supports iPhone/iPad WebKit. Restore platforms: 'ios' before shipping.");
28
29
  }
30
+ /** Generates the app shell and loads framework CSS once, before app styles.
31
+ * The stylesheet participates in Vite CSS HMR and builds; pre-launch paints independently. */
29
32
  export function satinApp(app, options = {}) {
30
33
  return satinAppFixtures([{ app, query: {} }], options);
31
34
  }
@@ -52,7 +55,6 @@ export function satinAppFixtures(input, options = {}) {
52
55
  let output = '';
53
56
  let publicDirectory = '';
54
57
  const covers = new Map();
55
- const launchIconPreloads = new Set();
56
58
  const generatedIcons = new Map();
57
59
  const startupIcons = new Map();
58
60
  const files = Object.assign({}, ...fixtures.map(({ app, display }) => Object.fromEntries(Object.entries(appManifestFiles(app, appManifestStem(app))).map(([name, source]) => [
@@ -96,7 +98,7 @@ export function satinAppFixtures(input, options = {}) {
96
98
  "import { createElement } from 'react'",
97
99
  "import { createRoot } from 'react-dom/client'",
98
100
  `import Page from ${page}`,
99
- "createRoot(document.getElementById('satin-install-guide')).render(createElement(Page))",
101
+ "createRoot(document.getElementById('satin-install-guide'), { onUncaughtError: error => window.__SATIN_PRELAUNCH__.error(String(error), 'Install Page Failed') }).render(createElement(Page))",
100
102
  ].join('\n');
101
103
  },
102
104
  writeBundle() {
@@ -198,7 +200,6 @@ export function satinAppFixtures(input, options = {}) {
198
200
  const inlineIcon = `data:${mime};base64,${iconBytes.toString('base64')}`;
199
201
  // Even inline images enter WebKit's image loader asynchronously. Start
200
202
  // it in the head so the body cover's first paint already has its icon.
201
- launchIconPreloads.add(`<link rel="preload" as="image" href="${inlineIcon}">`);
202
203
  covers.set(app.id, launchScreenMarkup({
203
204
  ...app,
204
205
  launchScreen: {
@@ -230,23 +231,7 @@ export function satinAppFixtures(input, options = {}) {
230
231
  const head = /<head\b[^>]*>(?:\s*<meta\s+charset\s*=\s*[^>]+>)?/i;
231
232
  if (!head.test(html))
232
233
  throw new Error('Satin app definition requires an index.html head element.');
233
- const coverData = JSON.stringify(fixtures.map(f => ({ query: f.query, html: covers.get(f.app.id) }))).replaceAll('<', '\\u003c');
234
- // Single-target apps get real parser markup, including the inline icon.
235
- // Only the lab's query-selected fixtures need document.write selection.
236
- const cover = fixtures.length === 1 ? covers.get(fixtures[0].app.id) : '';
237
- const selectCover = fixtures.length === 1
238
- ? ''
239
- : `
240
- const q = new URLSearchParams(location.search);
241
- const f = ${coverData}.find(f => Object.entries(f.query).every(([k,v]) => q.get(k) === v));
242
- document.write(f.html);`;
243
- html = html.replace(/<body\b[^>]*>/i, opening => `${opening}${cover}<script id="satin-launch-cover">(() => {
244
- if (window.__SATIN_PLATFORM_BLOCKED__ !== false) return;
245
- ${selectCover}
246
- (${resumeAppliedUpdate.toString()})(window.__SATIN_DEFINITION__.scope,
247
- window.__SATIN_DEFINITION__.updateProfile === 'release' ? ${JSON.stringify(building ? buildInfo?.appBuildId : undefined)} : undefined);
248
- })();</script>`);
249
- return html.replace(head, opening => `${opening}\n${[...launchIconPreloads].join('')}\n${generateAppHead(fixtures, base, production, !building)}`);
234
+ return html.replace(head, opening => `${opening}\n${generateAppHead(fixtures, base, production, !building, covers, building ? buildInfo?.appBuildId : undefined)}\n${frameworkStylesheet(base)}`);
250
235
  },
251
236
  },
252
237
  async closeBundle() {
@@ -265,7 +250,10 @@ export function satinAppFixtures(input, options = {}) {
265
250
  await mkdir(resolve(output, installRouteForBase(app, base).slice(base.length)), {
266
251
  recursive: true,
267
252
  });
268
- await writeFile(resolve(output, installRouteForBase(app, base).slice(base.length), 'index.html'), installPage(app, 'install', base, { customPage: customInstallUrl }));
253
+ await writeFile(resolve(output, installRouteForBase(app, base).slice(base.length), 'index.html'), installPage(app, 'install', base, {
254
+ customPage: customInstallUrl,
255
+ cover: covers.get(app.id),
256
+ }));
269
257
  if (app.scope !== base) {
270
258
  const html = await readFile(resolve(output, 'index.html'), 'utf8');
271
259
  await writeFile(resolve(output, app.scope.slice(base.length), 'index.html'), html);
@@ -342,7 +330,7 @@ export function satinAppFixtures(input, options = {}) {
342
330
  const customPage = kind === 'install' && options.installPage
343
331
  ? base + '@id/__x00__' + CUSTOM_INSTALL_ID
344
332
  : undefined;
345
- let html = installPage(app, kind, base, { customPage });
333
+ let html = installPage(app, kind, base, { customPage, cover: covers.get(app.id) });
346
334
  // In dev the custom page is served through Vite (its client and React's refresh).
347
335
  if (customPage)
348
336
  html = await server.transformIndexHtml(path, html);
@@ -382,6 +370,7 @@ export function satinAppFixtures(input, options = {}) {
382
370
  });
383
371
  },
384
372
  },
373
+ includedStyles(),
385
374
  {
386
375
  name: 'satin:platform-script-gate',
387
376
  transformIndexHtml: {
@@ -432,6 +421,15 @@ export function satinAppFixtures(input, options = {}) {
432
421
  for (const { app } of fixtures) {
433
422
  await validateBuiltDeployment(output, deploymentExpectation(app, base));
434
423
  }
424
+ const policies = {};
425
+ for (const path of [
426
+ fixtures[0].app.startRoute,
427
+ installRouteForBase(fixtures[0].app, base) + '/',
428
+ ]) {
429
+ const file = resolve(output, path.slice(base.length), 'index.html');
430
+ policies[path] = inlinePolicySources(await readFile(file, 'utf8'), prelaunchCSS);
431
+ }
432
+ await writeFile(resolve(output, 'satin-csp.json'), JSON.stringify({ version: 1, policies }, null, 2));
435
433
  console.info(`Deploy dist/ at ${base}, then run satin check <url>`);
436
434
  },
437
435
  },