@satinhq/kit 0.1.0-beta.2 → 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 (309) hide show
  1. package/NOTICE +2 -2
  2. package/README.md +2 -3
  3. package/dist/acknowledgements/inventory.json +5 -5
  4. package/dist/actions/RowActions.d.ts +4 -4
  5. package/dist/actions/RowActions.js +93 -27
  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/controls/civilDate.d.ts +1 -1
  59. package/dist/controls/civilDate.js +1 -1
  60. package/dist/data/database.d.ts +9 -8
  61. package/dist/data/database.js +4 -4
  62. package/dist/docs/components/file-importer.md +1 -1
  63. package/dist/docs/components/paste-button.md +1 -1
  64. package/dist/docs/components/photos-picker.md +1 -1
  65. package/dist/docs/components/share-link.md +1 -1
  66. package/dist/docs/limitations.md +1 -1
  67. package/dist/docs/push.md +3 -3
  68. package/dist/docs/reference.md +1888 -0
  69. package/dist/docs/tailwind.md +76 -0
  70. package/dist/docs/tokens.md +1 -1
  71. package/dist/gestures/GestureView.d.ts +3 -1
  72. package/dist/gestures/GestureView.js +2 -1
  73. package/dist/gestures/gesture.d.ts +22 -2
  74. package/dist/gestures/gesture.js +7 -2
  75. package/dist/glass/GlassEffect.d.ts +2 -1
  76. package/dist/glass/GlassEffect.js +2 -1
  77. package/dist/glass/GlassPresentation.d.ts +1 -0
  78. package/dist/glass/GlassPresentation.js +21 -4
  79. package/dist/glass/TabBar.js +23 -1
  80. package/dist/glass/errorAlert.js +2 -1
  81. package/dist/glass/errors.d.ts +3 -1
  82. package/dist/glass/fallback/runtime.d.ts +1 -0
  83. package/dist/glass/fallback/runtime.js +8 -1
  84. package/dist/glass/lens/controlLift.d.ts +1 -1
  85. package/dist/glass/lens/controlLift.js +1 -1
  86. package/dist/glass/lens/interactivePress.d.ts +2 -2
  87. package/dist/glass/lens/interactivePress.js +2 -2
  88. package/dist/glass/lens/menu-opening-native-fits.json +1 -1
  89. package/dist/glass/lens/menuContextFit.json +1 -1
  90. package/dist/glass/lens/menuDropFit.json +1 -1
  91. package/dist/glass/lens/menuField.d.ts +2 -2
  92. package/dist/glass/lens/menuField.js +3 -3
  93. package/dist/glass/lens/menuGlyphDropFit.json +1 -1
  94. package/dist/glass/lens/menuMorphFit.json +1 -1
  95. package/dist/glass/lens/menuMorphPhoneFit.json +1 -1
  96. package/dist/glass/lens/tabAccessoryMotion.d.ts +1 -1
  97. package/dist/glass/lens/tabSearchMember.d.ts +1 -1
  98. package/dist/glass/lens/tabSearchMember.js +1 -1
  99. package/dist/glass/liquidContext.d.ts +2 -0
  100. package/dist/glass/menuPresenter.d.ts +2 -0
  101. package/dist/glass/menuPresenter.js +1 -1
  102. package/dist/glass/regions.d.ts +3 -2
  103. package/dist/glass/regions.js +1 -1
  104. package/dist/glass/renderSpecs.d.ts +1 -0
  105. package/dist/glass/renderSpecs.js +1 -0
  106. package/dist/glass/shapeTrack.d.ts +1 -1
  107. package/dist/glass/submenuMotion.d.ts +1 -1
  108. package/dist/glass/submenuMotion.js +1 -1
  109. package/dist/glass/webgpu/controlRim.d.ts +29 -0
  110. package/dist/glass/webgpu/controlRim.js +68 -0
  111. package/dist/glass/webgpu/entry.worker.js +3 -6
  112. package/dist/glass/webgpu/frameClock.d.ts +6 -2
  113. package/dist/glass/webgpu/frameClock.js +9 -2
  114. package/dist/glass/webgpu/frameProbe.d.ts +1 -1
  115. package/dist/glass/webgpu/frameProbe.js +3 -3
  116. package/dist/glass/webgpu/passes.d.ts +2 -2
  117. package/dist/glass/webgpu/production.d.ts +1 -0
  118. package/dist/glass/webgpu/production.js +31 -1
  119. package/dist/glass/webgpu/raster.d.ts +18 -0
  120. package/dist/glass/webgpu/raster.js +105 -15
  121. package/dist/glass/webgpu/rasterMaterial.d.ts +4 -0
  122. package/dist/glass/webgpu/rasterMaterial.js +17 -1
  123. package/dist/glass/webgpu/rasterUniforms.js +9 -0
  124. package/dist/glass/webgpu/regionScene.d.ts +4 -1
  125. package/dist/glass/webgpu/regionScene.js +30 -1
  126. package/dist/glass/webgpu/renderer.js +23 -7
  127. package/dist/glass/webgpu/shaders.d.ts +2 -2
  128. package/dist/glass/webgpu/shaders.js +5 -1
  129. package/dist/haptics/SensoryButton.d.ts +4 -4
  130. package/dist/haptics/SensoryButton.js +3 -3
  131. package/dist/image/Image.d.ts +6 -3
  132. package/dist/image/Image.js +4 -3
  133. package/dist/image/asset.d.ts +2 -1
  134. package/dist/keyboard/adapter.d.ts +3 -4
  135. package/dist/keyboard/adapter.js +2 -2
  136. package/dist/keyboard/avoidance.d.ts +1 -1
  137. package/dist/keyboard/avoidance.js +1 -1
  138. package/dist/keyboard/geometry.d.ts +1 -1
  139. package/dist/keyboard/motion.d.ts +1 -1
  140. package/dist/keyboard/motion.js +1 -1
  141. package/dist/list-form/ListForm.d.ts +10 -9
  142. package/dist/list-form/ListForm.js +3 -3
  143. package/dist/list-form/ListItem.js +1 -1
  144. package/dist/list-form/RowLabel.d.ts +1 -1
  145. package/dist/list-form/RowLabel.js +1 -1
  146. package/dist/list-form/Slider.js +1 -1
  147. package/dist/list-form/editMotion.js +1 -1
  148. package/dist/list-form/editing.d.ts +10 -11
  149. package/dist/list-form/editing.js +5 -6
  150. package/dist/list-form/offsets.d.ts +2 -2
  151. package/dist/list-form/offsets.js +2 -2
  152. package/dist/media/react.d.ts +2 -1
  153. package/dist/media/react.js +2 -1
  154. package/dist/media/types.d.ts +6 -3
  155. package/dist/motion/TransitionView.d.ts +4 -2
  156. package/dist/motion/TransitionView.js +3 -2
  157. package/dist/motion/animationValue.d.ts +2 -2
  158. package/dist/motion/animationValue.js +9 -10
  159. package/dist/motion/matchedGeometry.d.ts +10 -9
  160. package/dist/motion/matchedGeometry.js +9 -9
  161. package/dist/motion/springTrack.d.ts +1 -2
  162. package/dist/motion/springTrack.js +10 -5
  163. package/dist/motion/transition.d.ts +4 -1
  164. package/dist/motion/transition.js +2 -0
  165. package/dist/navigation/DetailList.d.ts +1 -1
  166. package/dist/navigation/DetailList.js +1 -1
  167. package/dist/navigation/ScreenContainer.js +1 -1
  168. package/dist/navigation/Sidebar.d.ts +3 -1
  169. package/dist/navigation/Sidebar.js +1 -1
  170. package/dist/navigation/SidebarSymbol.d.ts +2 -1
  171. package/dist/navigation/SidebarSymbol.js +2 -1
  172. package/dist/navigation/chromeCurves.js +1 -1
  173. package/dist/navigation/navigationContext.d.ts +2 -1
  174. package/dist/navigation/navigationContext.js +2 -1
  175. package/dist/navigation/splitContext.d.ts +2 -1
  176. package/dist/navigation/splitContext.js +2 -1
  177. package/dist/navigation/toolbarMinimization.d.ts +4 -3
  178. package/dist/navigation/types.d.ts +8 -6
  179. package/dist/navigation/zoomGeometry.js +1 -1
  180. package/dist/navigation/zoomMotion.d.ts +1 -1
  181. package/dist/navigation/zoomMotion.js +2 -2
  182. package/dist/navigation/zoomTransition.js +3 -4
  183. package/dist/presentation/Presentation.d.ts +10 -9
  184. package/dist/presentation/Presentation.js +4 -3
  185. package/dist/presentation/model.d.ts +2 -1
  186. package/dist/presentation/presentationSource.js +1 -1
  187. package/dist/press/Pressable.d.ts +5 -1
  188. package/dist/press/Pressable.js +2 -0
  189. package/dist/press/stateMachine.d.ts +1 -1
  190. package/dist/public/Label.d.ts +2 -1
  191. package/dist/public/Label.js +2 -1
  192. package/dist/public/Screen.d.ts +4 -6
  193. package/dist/public/actions.d.ts +2 -1
  194. package/dist/public/actions.js +2 -1
  195. package/dist/public/buttonBorderShape.d.ts +2 -1
  196. package/dist/public/colors.d.ts +7 -4
  197. package/dist/public/colors.js +3 -2
  198. package/dist/public/controlStyle.d.ts +17 -14
  199. package/dist/public/controls.d.ts +29 -28
  200. package/dist/public/controls.js +25 -24
  201. package/dist/public/deviceActions.d.ts +6 -4
  202. package/dist/public/deviceActions.js +4 -2
  203. package/dist/public/environment.d.ts +16 -13
  204. package/dist/public/environment.js +11 -9
  205. package/dist/public/filePickers.d.ts +6 -4
  206. package/dist/public/filePickers.js +3 -2
  207. package/dist/public/glass.d.ts +8 -6
  208. package/dist/public/glass.js +2 -2
  209. package/dist/public/glassValue.d.ts +3 -4
  210. package/dist/public/glassValue.js +3 -4
  211. package/dist/public/keyboard.d.ts +4 -1
  212. package/dist/public/keyboard.js +2 -1
  213. package/dist/public/navigationTransition.d.ts +6 -6
  214. package/dist/public/navigationTransition.js +3 -3
  215. package/dist/public/presentation.d.ts +8 -7
  216. package/dist/public/presentation.js +5 -5
  217. package/dist/public/runtime.d.ts +9 -8
  218. package/dist/public/runtime.js +7 -7
  219. package/dist/public/scroll.d.ts +6 -3
  220. package/dist/public/scroll.js +3 -1
  221. package/dist/public/structure.d.ts +2 -2
  222. package/dist/public/toolbar.d.ts +15 -11
  223. package/dist/public/toolbar.js +13 -8
  224. package/dist/reachability/model.d.ts +4 -3
  225. package/dist/reachability/react.d.ts +4 -1
  226. package/dist/reachability/react.js +2 -1
  227. package/dist/root/SatinRoot.d.ts +3 -3
  228. package/dist/root/SatinRoot.js +12 -36
  229. package/dist/root/mountSatin.d.ts +1 -1
  230. package/dist/root/mountSatin.js +26 -35
  231. package/dist/root/runtimeContext.d.ts +2 -1
  232. package/dist/root/runtimeContext.js +2 -1
  233. package/dist/safe-area/Overlay.d.ts +3 -2
  234. package/dist/safe-area/Overlay.js +2 -1
  235. package/dist/safe-area/SafeAreaInset.d.ts +11 -7
  236. package/dist/safe-area/SafeAreaInset.js +10 -9
  237. package/dist/safe-area/context.d.ts +3 -3
  238. package/dist/safe-area/context.js +1 -1
  239. package/dist/safe-area/displayCorners.d.ts +4 -2
  240. package/dist/safe-area/displayCorners.js +4 -2
  241. package/dist/safe-area/modifiers.d.ts +14 -10
  242. package/dist/safe-area/modifiers.js +10 -6
  243. package/dist/safe-area/useSafeAreaMetrics.js +2 -2
  244. package/dist/scroll/ScrollSurface.d.ts +5 -5
  245. package/dist/scroll/ScrollSurface.js +6 -2
  246. package/dist/scroll/ScrollTarget.d.ts +1 -1
  247. package/dist/scroll/ScrollTarget.js +1 -1
  248. package/dist/scroll/edgePolicy.d.ts +2 -2
  249. package/dist/scroll/position.d.ts +5 -3
  250. package/dist/scroll/position.js +1 -1
  251. package/dist/scroll/screenGeometry.d.ts +1 -1
  252. package/dist/scroll/screenGeometry.js +1 -1
  253. package/dist/scroll/types.d.ts +1 -1
  254. package/dist/search/PadSearchDrawer.d.ts +7 -0
  255. package/dist/search/PadSearchDrawer.js +8 -0
  256. package/dist/search/PadSearchPresentationContext.d.ts +6 -0
  257. package/dist/search/PadSearchPresentationContext.js +4 -0
  258. package/dist/search/SearchDrawer.d.ts +1 -1
  259. package/dist/search/SearchDrawer.js +7 -2
  260. package/dist/search/SearchSuggestionsPopover.d.ts +6 -0
  261. package/dist/search/SearchSuggestionsPopover.js +42 -0
  262. package/dist/search/SearchToolbarField.d.ts +2 -1
  263. package/dist/search/SearchToolbarField.js +28 -7
  264. package/dist/search/padSearchBarMotion.d.ts +69 -0
  265. package/dist/search/padSearchBarMotion.js +73 -0
  266. package/dist/search/padSearchFieldSamples.json +491 -0
  267. package/dist/search/searchMotion.d.ts +1 -1
  268. package/dist/search/searchMotion.js +1 -1
  269. package/dist/search/searchPlacement.d.ts +3 -0
  270. package/dist/search/searchPlacement.js +7 -0
  271. package/dist/search/searchable.d.ts +14 -12
  272. package/dist/search/usePadSearchFieldMotion.d.ts +4 -0
  273. package/dist/search/usePadSearchFieldMotion.js +100 -0
  274. package/dist/search/useSearchPresentation.d.ts +2 -1
  275. package/dist/search/useSearchPresentation.js +24 -6
  276. package/dist/server.d.ts +4 -4
  277. package/dist/server.js +4 -4
  278. package/dist/shapes/corners.d.ts +3 -3
  279. package/dist/shapes/corners.js +2 -3
  280. package/dist/styles.css +8302 -8236
  281. package/dist/sw/vite.d.ts +4 -0
  282. package/dist/sw/vite.js +2 -0
  283. package/dist/symbols/Symbol.d.ts +7 -5
  284. package/dist/symbols/Symbol.js +5 -3
  285. package/dist/symbols/catalogue.d.ts +1 -1
  286. package/dist/symbols/types.d.ts +6 -4
  287. package/dist/tabs/TabSection.d.ts +3 -2
  288. package/dist/tabs/TabSection.js +2 -1
  289. package/dist/tabs/TabView.js +54 -51
  290. package/dist/tasks/useTask.d.ts +5 -4
  291. package/dist/tasks/useTask.js +4 -3
  292. package/dist/testing.d.ts +3 -2
  293. package/dist/testing.js +3 -2
  294. package/dist/text-input/TextEditor.js +7 -3
  295. package/dist/text-input/focusChain.d.ts +4 -3
  296. package/dist/text-input/focusChain.js +3 -2
  297. package/dist/text-input/kinds.d.ts +1 -1
  298. package/dist/web/WebView.d.ts +10 -3
  299. package/dist/web/WebView.js +3 -2
  300. package/dist/web/authentication.d.ts +12 -1
  301. package/dist/web/authentication.js +2 -1
  302. package/dist/web/openURL.d.ts +10 -4
  303. package/dist/web/openURL.js +4 -2
  304. package/dist/web/useWebAuthentication.d.ts +2 -1
  305. package/dist/web/useWebAuthentication.js +2 -1
  306. package/dist/zoom/Zoomable.d.ts +3 -3
  307. package/dist/zoom/Zoomable.js +2 -2
  308. package/package.json +4 -4
  309. package/dist/docs/swiftui-reference.md +0 -1895
package/NOTICE CHANGED
@@ -1,6 +1,6 @@
1
1
  # Satin distribution notices
2
2
 
3
- Generated from packages/pwakit/src/acknowledgements/inventory.json.
3
+ Generated from packages/kit/src/acknowledgements/inventory.json.
4
4
 
5
5
  Satin-authored framework packages use MIT; third-party terms are preserved.
6
6
  Retain the copyright and permission notices required by MIT in copies or
@@ -12,7 +12,7 @@ is a separately installed build dependency. Consumer Vite builds emit NOTICE.txt
12
12
  and licenses.json from their actual emitted module graph, including runtime peers.
13
13
  Development dependencies are not assumed to be bundled.
14
14
 
15
- ## Satin 0.1.0-beta.2
15
+ ## Satin 0.1.0-beta.4
16
16
 
17
17
  License: MIT
18
18
  Source: https://opensource.org/license/mit
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Satin builds personal apps for the installed iOS Home Screen runtime. The framework owns navigation, glass, editing, restoration and release updates.
4
4
 
5
- Import app APIs from `@satinhq/kit`, build integration from `@satinhq/kit/vite`, and load `@satinhq/kit/styles.css` once. `@satinhq/kit/internal` is for framework development; app lint rejects it. `@satinhq/kit/testing` belongs in tests. `NavigationSplitView` and `SymbolImage` are public app APIs. Legacy `Sidebar`, `DetailList`, `useSplitSelection` and `SidebarSymbol` composition remains in `@satinhq/kit/unstable`; use its generated export inventory for the exact list.
5
+ Import app APIs from `@satinhq/kit`, build integration from `@satinhq/kit/vite`, and let `satinApp()` include framework styles before app CSS. For utility classes, follow [Using Tailwind with Satin](dist/docs/tailwind.md). `@satinhq/kit/internal` is for framework development; app lint rejects it. `@satinhq/kit/testing` belongs in tests. `NavigationSplitView` and `SymbolImage` are public app APIs. Legacy `Sidebar`, `DetailList`, `useSplitSelection` and `SidebarSymbol` composition remains in `@satinhq/kit/unstable`; use its generated export inventory for the exact list.
6
6
 
7
7
  ## Declare routes, then primary navigation
8
8
 
@@ -26,7 +26,6 @@ import {
26
26
  useNavigation,
27
27
  type RouteParameters,
28
28
  } from '@satinhq/kit'
29
- import '@satinhq/kit/styles.css'
30
29
 
31
30
  const app = defineApp({ id: 'reading-list', name: 'Reading List', icons: { source: '/icon.png' } })
32
31
  const bookParams = { id: param.string(), page: param.int().optional() }
@@ -219,7 +218,7 @@ falls back to the base glyph when no fill exists. See the shipped [available sym
219
218
 
220
219
  ## Public API inventory
221
220
 
222
- See the packaged [Satin API reference](dist/docs/swiftui-reference.md) for behavior, limitations, unavailable concepts and the exact export coverage inventory. The index lists supported APIs and known gaps.
221
+ See the packaged [Satin API reference](dist/docs/reference.md) for behavior, limits, unsupported capabilities and the exact export inventory. The index lists supported APIs and known gaps.
223
222
 
224
223
  For a Safari-tab development build, set `advanced: { updateProfile: 'preview' }` in `defineApp`. No environment switch is needed. It retains the iOS WebKit platform gate and uses the existing live-update service-worker policy, which may reload on update. Use the default `release` profile for installed production apps and cold-launch update activation. A query parameter cannot opt a release build into preview. The profile does not change status style, viewport geometry, glass or lifecycle ownership.
225
224
 
@@ -1,13 +1,13 @@
1
1
  [
2
2
  {
3
3
  "name": "Satin",
4
- "version": "0.1.0-beta.2",
4
+ "version": "0.1.0-beta.4",
5
5
  "license": "MIT",
6
6
  "source": "https://opensource.org/license/mit",
7
7
  "text": "MIT License\n\nCopyright (c) 2026 Satin contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
8
8
  "sha256": "a9ca5be2844d1d12fbd9b77cd7446d1895c44b4c5253aed536c2c60ad8d10caa",
9
9
  "bundledPaths": [
10
- "packages/pwakit/src"
10
+ "packages/kit/src"
11
11
  ],
12
12
  "role": "framework"
13
13
  },
@@ -19,8 +19,8 @@
19
19
  "text": "The MIT License (MIT)\n\nCopyright (c) 2016 Vladimir Kharlampidi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
20
20
  "sha256": "61afeee2f1d59f21412901a0015ccbec0315ae4806e3f4fbd167201ebb17b2f8",
21
21
  "bundledPaths": [
22
- "packages/pwakit/src/symbols/glyphs",
23
- "packages/pwakit/src/glass/lens/icons.ts"
22
+ "packages/kit/src/symbols/glyphs",
23
+ "packages/kit/src/glass/lens/icons.ts"
24
24
  ],
25
25
  "role": "adapted or generated source"
26
26
  },
@@ -32,7 +32,7 @@
32
32
  "text": "MIT License\n\nCopyright (c) 2026 Pallav Agarwal\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
33
33
  "sha256": "949e33ed2f105f112b392ce9832799a1c6d2c1fe89784a87c919dfc6b115eacc",
34
34
  "bundledPaths": [
35
- "packages/pwakit/src/glass/lens"
35
+ "packages/kit/src/glass/lens"
36
36
  ],
37
37
  "role": "adapted or generated source"
38
38
  }
@@ -16,13 +16,13 @@ export type RowActionsController = {
16
16
  * A row's swipe actions and context menu. Inside an editable `List` item, the item owns the row's
17
17
  * one gesture host (it adds swipe-to-delete), so the row hands its actions to the item instead of
18
18
  * hosting a second recognizer. Swipe actions are unavailable while the list is editing, as in
19
- * SwiftUI; the context menu stays.
19
+ * selection; the context menu stays.
20
20
  */
21
21
  export declare function RowActions(props: RowActionsEngineProps): import("react").JSX.Element;
22
22
  /**
23
23
  * The one gesture host of an editable `List` item. It hosts the row's own swipe actions and context
24
24
  * menu (handed over by the row, or declared on its `ListRow`) and adds the `onDelete` action as the
25
- * trailing swipe, unless the row declares trailing actions of its own: SwiftUI's `swipeActions`
25
+ * trailing swipe, unless the row declares its own trailing `swipeActions`
26
26
  * replace swipe-to-delete.
27
27
  */
28
28
  export declare function ListItemActions({ children, swipeActions, contextMenu, deleteAction, controller, }: RowActionsProps & {
@@ -31,8 +31,8 @@ export declare function ListItemActions({ children, swipeActions, contextMenu, d
31
31
  controller?: RowActionsController;
32
32
  }): import("react").JSX.Element;
33
33
  /**
34
- * `contextMenu`: long-pressing the content opens the glass menu of its actions, with an
35
- * optional custom `preview`. Rows in a `List` take a `contextMenu` prop instead.
34
+ * Opens a menu of actions when the user long-presses its content, with an optional custom
35
+ * `preview`. Rows in a `List` take a `contextMenu` prop instead.
36
36
  */
37
37
  export declare function ContextMenu({ children, actions, preview, }: {
38
38
  children: ReactNode;
@@ -2,7 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { markRowSwipeRemoval } from '../list-form/rowMotion.js';
3
3
  import { cancelOnNativeScroll } from '../scroll/nativeScroll.js';
4
4
  import { SymbolImage } from '../symbols/Symbol.js';
5
- import { createRowScrub, rowSwipeShouldClose, rowSwipePosition, rowSwipeRaw, primaryParts, fullSwipeThreshold, swipeActionTracks, swipeContentTrack, swipeOpenWidth, swipeWidths, updateRowCorners, } from './rowScrub.js';
5
+ import { createRowScrub, rowSwipeShouldClose, rowSwipeRaw, primaryParts, presentedTakeover, swipeActionTracks, swipeContentTrack, swipeOpenWidth, swipeWidths, updateRowCorners, } from './rowScrub.js';
6
+ import { createFullSwipeContact } from './fullSwipeContact.js';
6
7
  import { CONTEXT_HOLD_MS, CONTEXT_STILL_MS } from './contextMotion.js';
7
8
  import { contextPreviewSource } from './preview.js';
8
9
  import { useMenuPresenter } from '../glass/menuPresenter.js';
@@ -29,6 +30,14 @@ function retireRowSource(source) {
29
30
  if (source)
30
31
  retireNativeScrubSource(source, source.clientWidth * 2);
31
32
  }
33
+ /** Pill takeover each tray presents when a new contact catches the row. */
34
+ function caughtTakeovers(element, initial) {
35
+ const caught = (side) => {
36
+ const primary = element.querySelector(`[data-satin-swipe-tray="${side}"] > :first-child`);
37
+ return primary ? presentedTakeover(primary, initial) : 0;
38
+ };
39
+ return { leading: caught('leading'), trailing: caught('trailing') };
40
+ }
32
41
  function RowActionsEngine({ children, swipeActions, contextMenu, disabled = false, content = false, controller, }) {
33
42
  const host = useRef(null), body = useRef(null);
34
43
  const scrollSource = useRef(null);
@@ -315,8 +324,17 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
315
324
  let navigationOwns = false;
316
325
  // Native drags an open row from its action buttons too; their taps stay presses.
317
326
  let startedOnAction = false;
318
- let rowWidth = 0;
327
+ let rowWidth = 0, rowLeft = 0;
328
+ // One contact maps raw travel to the presented row for the scroll source and
329
+ // for this touch fallback alike, including a destructive takeover's anchor.
330
+ let contact;
331
+ let cachedSide = 'trailing';
319
332
  let cachedActions = [];
333
+ let fallbackRaw = 0, fallbackFrame = 0;
334
+ const cancelFallbackFrame = () => {
335
+ cancelAnimationFrame(fallbackFrame);
336
+ fallbackFrame = 0;
337
+ };
320
338
  let contentFrame;
321
339
  let initial = 0, current = 0, owned = false, last;
322
340
  const clearTimer = () => {
@@ -326,6 +344,28 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
326
344
  timer = undefined;
327
345
  delete element.dataset.satinContextPending;
328
346
  };
347
+ /** Present the touch-fallback row and pills from one raw sample. A takeover's
348
+ * jump keeps moving without input, so it presents further frames itself. */
349
+ const paintFallback = (raw, now, evaluate) => {
350
+ if (!contact)
351
+ return current;
352
+ fallbackRaw = raw;
353
+ const position = contact.present(raw, now, evaluate);
354
+ const takeover = contact.takeover(cachedSide, raw, now);
355
+ for (const action of cachedActions)
356
+ Object.assign(action.element.style, action.frame(position, raw, initial ? 1 : 0, takeover));
357
+ if (body.current && contentFrame)
358
+ Object.assign(body.current.style, contentFrame(position, initial ? 1 : 0));
359
+ current = position;
360
+ live.current.offset = position;
361
+ if (evaluate && !fallbackFrame && contact.settling(now))
362
+ fallbackFrame = requestAnimationFrame(time => {
363
+ fallbackFrame = 0;
364
+ if (session && !scrub.current)
365
+ paintFallback(fallbackRaw, time, true);
366
+ });
367
+ return position;
368
+ };
329
369
  const cancelPress = () => {
330
370
  if (pointerId !== undefined)
331
371
  cancelPointerPressForOwnedGesture(pointerId);
@@ -344,6 +384,7 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
344
384
  }
345
385
  const previous = last;
346
386
  last = sample;
387
+ contact?.track(sample.x - rowLeft);
347
388
  session = reduceGestureArena(arena, session, sample).session;
348
389
  if (!session)
349
390
  return;
@@ -397,6 +438,7 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
397
438
  const primary = element.querySelector(`[data-satin-swipe-tray="${element.dataset.satinSwipeSide}"] > :first-child`);
398
439
  const side = element.dataset.satinSwipeSide === 'leading' ? 'leading' : 'trailing';
399
440
  const edge = swipeEdge(props.current.swipeActions, side);
441
+ cachedSide = side;
400
442
  cachedActions =
401
443
  primary && !scrub.current
402
444
  ? swipeActionTracks(primary, edge.actions.length, rowWidth, edge.allowsFullSwipe)
@@ -414,16 +456,14 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
414
456
  const raw = rowSwipeRaw(initial, rowWidth, props.current.swipeActions, swipeWidths(element)) +
415
457
  sample.x -
416
458
  session.start.x;
417
- current = scrub.current
418
- ? scrub.current.position()
419
- : rowSwipePosition(raw, rowWidth, props.current.swipeActions, swipeWidths(element));
420
- if (!scrub.current) {
421
- for (const action of cachedActions)
422
- Object.assign(action.element.style, action.frame(current, raw, initial ? 1 : 0));
423
- if (body.current && contentFrame)
424
- Object.assign(body.current.style, contentFrame(current, initial ? 1 : 0));
459
+ // A lift-off sample is presented but never evaluated: release reads the
460
+ // takeover that the last presented frame showed.
461
+ if (scrub.current) {
462
+ current = scrub.current.position();
463
+ live.current.offset = current;
425
464
  }
426
- live.current.offset = current;
465
+ else
466
+ paintFallback(raw, sample.time, sample.phase === 'move');
427
467
  }
428
468
  }
429
469
  if (sample.phase === 'end' && initial !== 0 && !owned && !startedOnAction) {
@@ -432,10 +472,11 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
432
472
  }
433
473
  if (sample.phase === 'end' || sample.phase === 'cancel') {
434
474
  clearTimer();
475
+ cancelFallbackFrame();
435
476
  if (owned)
436
477
  cancelPress();
437
478
  if (swipe && sample.phase === 'end') {
438
- const released = scrub.current?.flush();
479
+ const released = scrub.current?.flush(performance.now(), false);
439
480
  const releaseRaw = released?.raw ??
440
481
  rowSwipeRaw(initial, rowWidth, props.current.swipeActions, swipeWidths(element)) +
441
482
  sample.x -
@@ -452,26 +493,37 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
452
493
  element.dataset.satinSwipeSide = current >= 0 ? 'leading' : 'trailing';
453
494
  const edge = swipeEdge(props.current.swipeActions, current >= 0 ? 'leading' : 'trailing');
454
495
  const actions = edge.actions;
455
- const first = actions[0], width = rowWidth;
456
- // The same per-edge threshold that drives the takeover while held. Native
457
- // closes a leading three-action row dragged back from 295pt.
458
- const releaseSide = current >= 0 ? 'leading' : 'trailing';
459
- const threshold = fullSwipeThreshold(width, actions.length, releaseSide, swipeWidths(element)[releaseSide], element.dataset.satinSwipeLabelLayout === 'inline');
460
- if (edge.allowsFullSwipe && Math.abs(releaseRaw) >= threshold && first && !first.disabled)
496
+ const first = actions[0];
497
+ // The same rule that drives the takeover while held: a distance threshold,
498
+ // or a destructive takeover still shown by the last frame. Native closes a
499
+ // leading three-action row dragged back from 295pt.
500
+ if (first && contact?.armed(releaseRaw))
461
501
  perform(first, current, true);
462
502
  else {
463
503
  const from = current;
464
- current = !rowSwipeShouldClose(initial, current, sample.x - session.start.x)
504
+ // Native closes a destructive takeover that was reversed out of its zone.
505
+ // That close starts far across the row, so its pills stay and shrink
506
+ // with it, as when an open row is dismissed, until it reaches rest.
507
+ const anchoredClose = !!contact?.anchored();
508
+ const closes = anchoredClose || rowSwipeShouldClose(initial, current, sample.x - session.start.x);
509
+ current = !closes
465
510
  ? Math.sign(current) *
466
511
  swipeOpenWidth(actions.length, swipeWidths(element)[current >= 0 ? 'leading' : 'trailing'])
467
512
  : 0;
468
- element.dataset.satinSwipeActive = String(current !== 0);
513
+ if (!anchoredClose)
514
+ element.dataset.satinSwipeActive = String(current !== 0);
469
515
  live.current.offset = current;
470
516
  setOffset(current);
471
- if (body.current)
517
+ const settling = body.current &&
472
518
  settleSwipe(body.current, from, current, previous
473
519
  ? (sample.x - previous.x) / Math.max(0.001, (sample.time - previous.time) / 1000)
474
520
  : 0, element.querySelector(`[data-satin-swipe-tray="${from >= 0 ? 'leading' : 'trailing'}"] > :first-child`) ?? undefined, swipeOpenWidth(actions.length, swipeWidths(element)[current >= 0 ? 'leading' : 'trailing']), initial !== 0, rowWidth, edge.allowsFullSwipe);
521
+ if (anchoredClose) {
522
+ if (settling)
523
+ void settling.finished.then(close, () => { });
524
+ else
525
+ close();
526
+ }
475
527
  }
476
528
  }
477
529
  else if (sample.phase === 'cancel' && !hold && !navigationOwns)
@@ -494,6 +546,8 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
494
546
  return;
495
547
  const rect = element.getBoundingClientRect();
496
548
  rowWidth = rect.width;
549
+ rowLeft = rect.left;
550
+ cancelFallbackFrame();
497
551
  contentFrame = body.current ? swipeContentTrack(body.current) : undefined;
498
552
  navigationOwns = false;
499
553
  startedOnAction = !!target.closest('[data-satin-swipe-action]');
@@ -523,6 +577,17 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
523
577
  if (body.current && animations.length)
524
578
  body.current.style.transform = `translate3d(${initial}px,0,0)`;
525
579
  scrub.current?.cancel();
580
+ // A new contact starts a new mapping: catching a settling row never inherits
581
+ // the previous contact's anchor, only its presented pose and pill takeover.
582
+ contact = createFullSwipeContact({
583
+ width: rowWidth,
584
+ configuration: props.current.swipeActions,
585
+ widths: swipeWidths(element),
586
+ inline: element.dataset.satinSwipeLabelLayout === 'inline',
587
+ caught: initial ? caughtTakeovers(element, initial) : undefined,
588
+ reducedMotion: window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false,
589
+ now: performance.now(),
590
+ });
526
591
  // A contact that touched down before the retired source re-armed cannot scroll
527
592
  // it; the touch path drives that contact while the clamp holds the source.
528
593
  const sourceLive = nativeTouch &&
@@ -533,7 +598,7 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
533
598
  releaseNativeScrubClamp(scrollSource.current);
534
599
  scrub.current =
535
600
  sourceLive && scrollSource.current && body.current
536
- ? createRowScrub(element, scrollSource.current, body.current, initial, rowWidth, props.current.swipeActions)
601
+ ? createRowScrub(element, scrollSource.current, body.current, initial, rowWidth, props.current.swipeActions, contact)
537
602
  : undefined;
538
603
  live.current.offset = initial;
539
604
  current = initial;
@@ -710,6 +775,7 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
710
775
  if (pointerId !== undefined && captureOwner?.hasPointerCapture?.(pointerId))
711
776
  captureOwner.releasePointerCapture(pointerId);
712
777
  scrub.current?.cancel();
778
+ cancelFallbackFrame();
713
779
  delete element.dataset.satinRowCompositor;
714
780
  finishLift();
715
781
  clearTimer();
@@ -748,7 +814,7 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
748
814
  }
749
815
  if (event.key === 'Escape' && !open)
750
816
  close();
751
- }, children: [_jsx("div", { ref: scrollSource, "data-satin-row-scrub": "true", children: _jsx("div", { "data-satin-row-scrub-range": "true", children: _jsxs("div", { "data-satin-row-scrub-sticky": "true", children: [['leading', 'trailing'].map(side => (_jsx("div", { "data-satin-swipe-tray": side, inert: disabled || !offset || (offset > 0 ? side !== 'leading' : side !== 'trailing'), children: swipeEdge(swipeActions, side).actions.map(action => (_jsxs(Pressable, { disabled: disabled || action.disabled, "aria-label": action.accessibilityLabel ?? action.label, onPress: () => select(action), "data-satin-swipe-action": "true", "data-satin-swipe-icon": Boolean(action.icon || action.symbol), "data-satin-swipe-title": Boolean(action.label), style: {
817
+ }, children: [_jsx("div", { ref: scrollSource, "data-satin-row-scrub": "true", children: _jsx("div", { "data-satin-row-scrub-range": "true", children: _jsxs("div", { "data-satin-row-scrub-sticky": "true", children: [['leading', 'trailing'].map(side => (_jsx("div", { "data-satin-swipe-tray": side, inert: disabled || !offset || (offset > 0 ? side !== 'leading' : side !== 'trailing'), children: swipeEdge(swipeActions, side).actions.map(action => (_jsxs(Pressable, { disabled: disabled || action.disabled, "aria-label": action.accessibilityLabel ?? action.label, onPress: () => select(action), "data-satin-swipe-action": "true", "data-satin-swipe-role": action.role, "data-satin-swipe-icon": Boolean(action.icon || action.symbol), "data-satin-swipe-title": Boolean(action.label), style: {
752
818
  '--satin-swipe-tint': action.tint ??
753
819
  (action.role === 'destructive'
754
820
  ? 'var(--satin-action-destructive)'
@@ -762,7 +828,7 @@ function RowActionsEngine({ children, swipeActions, contextMenu, disabled = fals
762
828
  * A row's swipe actions and context menu. Inside an editable `List` item, the item owns the row's
763
829
  * one gesture host (it adds swipe-to-delete), so the row hands its actions to the item instead of
764
830
  * hosting a second recognizer. Swipe actions are unavailable while the list is editing, as in
765
- * SwiftUI; the context menu stays.
831
+ * selection; the context menu stays.
766
832
  */
767
833
  export function RowActions(props) {
768
834
  const item = useContext(ListItemContext);
@@ -781,7 +847,7 @@ function DelegatedRowActions({ children, swipeActions, contextMenu, disabled, de
781
847
  /**
782
848
  * The one gesture host of an editable `List` item. It hosts the row's own swipe actions and context
783
849
  * menu (handed over by the row, or declared on its `ListRow`) and adds the `onDelete` action as the
784
- * trailing swipe, unless the row declares trailing actions of its own: SwiftUI's `swipeActions`
850
+ * trailing swipe, unless the row declares its own trailing `swipeActions`
785
851
  * replace swipe-to-delete.
786
852
  */
787
853
  export function ListItemActions({ children, swipeActions, contextMenu, deleteAction, controller, }) {
@@ -812,8 +878,8 @@ function withTrailingAction(swipeActions, action) {
812
878
  return configurations;
813
879
  }
814
880
  /**
815
- * `contextMenu`: long-pressing the content opens the glass menu of its actions, with an
816
- * optional custom `preview`. Rows in a `List` take a `contextMenu` prop instead.
881
+ * Opens a menu of actions when the user long-presses its content, with an optional custom
882
+ * `preview`. Rows in a `List` take a `contextMenu` prop instead.
817
883
  */
818
884
  export function ContextMenu({ children, actions, preview, }) {
819
885
  return (_jsx(RowActions, { contextMenu: { actions, preview }, content: true, children: children }));
@@ -0,0 +1,73 @@
1
+ import { type SwipeWidths } from './rowScrub.js';
2
+ import { type SwipeActions } from './types.js';
3
+ /**
4
+ * Destructive full swipe, from the iOS 27 stock List reference (400pt rows, 80pt
5
+ * tall, UIKit pan translation plus per-frame model and presentation geometry;
6
+ * fullswipe r3 receipts, re-read in r4).
7
+ *
8
+ * - The trigger is where the finger is, not how far it went. Strokes starting at
9
+ * x300, x350 and x400 all take over when the finger reaches screen x88 (row-local
10
+ * 68pt on a 20pt inset row), after 187, 236 and 287pt of travel; x90 never does.
11
+ * It also needs recognized travel: 165pt that ends at x60 never takes over.
12
+ * - At takeover the row jumps once, to 29pt short of the far edge (-371 of 400),
13
+ * then follows the finger 1:1 from that anchor for the rest of the contact,
14
+ * including on the way back. Past 372.5pt it resists (5 contacts, RMS 0.02pt).
15
+ * - The jump settles as a critically damped 48/s spring after one 18ms frame
16
+ * (4 takeovers, normalized RMS 0.020).
17
+ * - Leaving the zone disarms the release but keeps the anchor and the expanded pill.
18
+ * All 5 reversals (to x140, 220, 280, 340 and 380) cancel and close to rest.
19
+ * - A release commits only if the last presented frame showed the takeover. A
20
+ * 2100pt/s flick whose in-zone sample lands after lift-off cancels; a
21
+ * 1300pt/s flick whose in-zone sample lands a frame earlier commits.
22
+ *
23
+ * Non-destructive first actions never re-anchor. They keep the distance threshold,
24
+ * read at release (leading Read: 205pt cancels, 210pt commits, row 1:1).
25
+ * No velocity projection appears in any of these trials, so none is modelled.
26
+ */
27
+ export declare const destructiveFullSwipe: {
28
+ /** The finger must be within this distance of the row's far edge (bracket 68..70pt). */
29
+ readonly edgeZone: 69;
30
+ /** Recognized travel needed before the zone counts (bracket 165..187pt). */
31
+ readonly minimumTravel: 176;
32
+ /** The jump leaves this much of the row showing. */
33
+ readonly anchorInset: 29;
34
+ /** Past this inset from the far edge, the anchored row resists. */
35
+ readonly limitInset: 27.5;
36
+ readonly resistance: 0.46;
37
+ readonly resistanceLength: 31;
38
+ /** Critically damped approach of the jump, after one held frame. */
39
+ readonly jumpRate: 48;
40
+ readonly jumpDelay: 0.018;
41
+ };
42
+ type Side = 'leading' | 'trailing';
43
+ /** Remaining fraction of the jump distance `seconds` after it began. */
44
+ export declare function destructiveJumpRemaining(seconds: number): number;
45
+ export type FullSwipeContactOptions = {
46
+ width: number;
47
+ configuration?: SwipeActions;
48
+ widths: SwipeWidths;
49
+ /** Single-line rows; selects the inline full-swipe threshold. */
50
+ inline: boolean;
51
+ /** Pill takeover already presented when this contact caught the row. */
52
+ caught?: Partial<Record<Side, number>>;
53
+ reducedMotion?: boolean;
54
+ now: number;
55
+ };
56
+ /**
57
+ * One finger's mapping from the row's raw offset to its presented position, plus
58
+ * whether releasing now performs the first action. Created per contact, so a new
59
+ * contact (including one that catches a settling row) starts without an anchor.
60
+ * The scroll-source path and the touch fallback both present through it.
61
+ */
62
+ export declare function createFullSwipeContact(options: FullSwipeContactOptions): {
63
+ /** Record the finger's row-local x; the next presented frame evaluates it. */
64
+ track(x: number): void;
65
+ present: (raw: number, now: number, evaluate?: boolean) => number;
66
+ takeover: (side: Side, raw: number, now: number) => number;
67
+ armed: (raw: number) => boolean;
68
+ anchored: () => boolean;
69
+ /** The jump is still visibly moving, so a frame must be presented without input. */
70
+ settling: (now: number) => boolean;
71
+ };
72
+ export type FullSwipeContact = ReturnType<typeof createFullSwipeContact>;
73
+ export {};
@@ -0,0 +1,186 @@
1
+ import { fullSwipeThreshold, rowSwipePosition } from './rowScrub.js';
2
+ import { takeoverSample } from './swipeSpring.js';
3
+ import { swipeEdge } from './types.js';
4
+ /**
5
+ * Destructive full swipe, from the iOS 27 stock List reference (400pt rows, 80pt
6
+ * tall, UIKit pan translation plus per-frame model and presentation geometry;
7
+ * fullswipe r3 receipts, re-read in r4).
8
+ *
9
+ * - The trigger is where the finger is, not how far it went. Strokes starting at
10
+ * x300, x350 and x400 all take over when the finger reaches screen x88 (row-local
11
+ * 68pt on a 20pt inset row), after 187, 236 and 287pt of travel; x90 never does.
12
+ * It also needs recognized travel: 165pt that ends at x60 never takes over.
13
+ * - At takeover the row jumps once, to 29pt short of the far edge (-371 of 400),
14
+ * then follows the finger 1:1 from that anchor for the rest of the contact,
15
+ * including on the way back. Past 372.5pt it resists (5 contacts, RMS 0.02pt).
16
+ * - The jump settles as a critically damped 48/s spring after one 18ms frame
17
+ * (4 takeovers, normalized RMS 0.020).
18
+ * - Leaving the zone disarms the release but keeps the anchor and the expanded pill.
19
+ * All 5 reversals (to x140, 220, 280, 340 and 380) cancel and close to rest.
20
+ * - A release commits only if the last presented frame showed the takeover. A
21
+ * 2100pt/s flick whose in-zone sample lands after lift-off cancels; a
22
+ * 1300pt/s flick whose in-zone sample lands a frame earlier commits.
23
+ *
24
+ * Non-destructive first actions never re-anchor. They keep the distance threshold,
25
+ * read at release (leading Read: 205pt cancels, 210pt commits, row 1:1).
26
+ * No velocity projection appears in any of these trials, so none is modelled.
27
+ */
28
+ export const destructiveFullSwipe = {
29
+ /** The finger must be within this distance of the row's far edge (bracket 68..70pt). */
30
+ edgeZone: 69,
31
+ /** Recognized travel needed before the zone counts (bracket 165..187pt). */
32
+ minimumTravel: 176,
33
+ /** The jump leaves this much of the row showing. */
34
+ anchorInset: 29,
35
+ /** Past this inset from the far edge, the anchored row resists. */
36
+ limitInset: 27.5,
37
+ resistance: 0.46,
38
+ resistanceLength: 31,
39
+ /** Critically damped approach of the jump, after one held frame. */
40
+ jumpRate: 48,
41
+ jumpDelay: 0.018,
42
+ };
43
+ const sides = { leading: 1, trailing: -1 };
44
+ const sideOf = (sign) => (sign >= 0 ? 'leading' : 'trailing');
45
+ /** Remaining fraction of the jump distance `seconds` after it began. */
46
+ export function destructiveJumpRemaining(seconds) {
47
+ const elapsed = seconds - destructiveFullSwipe.jumpDelay;
48
+ if (elapsed <= 0)
49
+ return 1;
50
+ const rate = destructiveFullSwipe.jumpRate;
51
+ return (1 + rate * elapsed) * Math.exp(-rate * elapsed);
52
+ }
53
+ /** The anchored row tracks 1:1 up to the soft limit, then resists. */
54
+ function anchoredResistance(distance, width) {
55
+ const { limitInset, resistance, resistanceLength } = destructiveFullSwipe;
56
+ const limit = width - limitInset;
57
+ const excess = distance - limit;
58
+ if (excess <= 0)
59
+ return distance;
60
+ return limit + (resistance * excess) / (1 + (resistance * excess) / resistanceLength);
61
+ }
62
+ const firstActionEnabled = (edge) => !!edge.actions[0] && !edge.actions[0].disabled;
63
+ const isDestructive = (edge) => edge.allowsFullSwipe && firstActionEnabled(edge) && edge.actions[0].role === 'destructive';
64
+ /**
65
+ * One finger's mapping from the row's raw offset to its presented position, plus
66
+ * whether releasing now performs the first action. Created per contact, so a new
67
+ * contact (including one that catches a settling row) starts without an anchor.
68
+ * The scroll-source path and the touch fallback both present through it.
69
+ */
70
+ export function createFullSwipeContact(options) {
71
+ const { width, configuration, widths, inline, reducedMotion = false } = options;
72
+ const edges = {
73
+ leading: swipeEdge(configuration, 'leading'),
74
+ trailing: swipeEdge(configuration, 'trailing'),
75
+ };
76
+ const thresholds = {
77
+ leading: fullSwipeThreshold(width, edges.leading.actions.length, 'leading', widths.leading, inline),
78
+ trailing: fullSwipeThreshold(width, edges.trailing.actions.length, 'trailing', widths.trailing, inline),
79
+ };
80
+ /** Row-local x of the latest finger sample. */
81
+ let finger;
82
+ /** Set once, at the first takeover of this contact; never moved afterward. */
83
+ let anchor;
84
+ /** Zone state of the last evaluated frame; this is what a release reads. */
85
+ let engaged = false;
86
+ const takeovers = {
87
+ leading: springState(options.caught?.leading ?? 0, options.now),
88
+ trailing: springState(options.caught?.trailing ?? 0, options.now),
89
+ };
90
+ function inZone(raw) {
91
+ const side = sideOf(raw);
92
+ if (!raw || finger === undefined || !isDestructive(edges[side]))
93
+ return false;
94
+ const fromFarEdge = side === 'trailing' ? finger : width - finger;
95
+ return (fromFarEdge <= destructiveFullSwipe.edgeZone &&
96
+ Math.abs(raw) >= destructiveFullSwipe.minimumTravel);
97
+ }
98
+ function anchoredPosition(raw, side, offset) {
99
+ const sign = sides[side];
100
+ // The anchor keeps the row on its side even if the finger crosses back.
101
+ const distance = Math.max(0, (raw + offset) * sign);
102
+ return sign * anchoredResistance(distance, width);
103
+ }
104
+ /** Presented row position. `evaluate` is true for presented frames; a release
105
+ * sample is presented with `false`, so it cannot arm or anchor after lift-off. */
106
+ function present(raw, now, evaluate = true) {
107
+ if (evaluate) {
108
+ engaged = inZone(raw);
109
+ if (engaged && !anchor) {
110
+ const side = sideOf(raw);
111
+ const sign = sides[side];
112
+ const target = sign * (width - destructiveFullSwipe.anchorInset);
113
+ const before = rowSwipePosition(raw, width, configuration, widths);
114
+ anchor = { side, offset: target - raw, gap: before - target, time: now };
115
+ }
116
+ }
117
+ if (!anchor)
118
+ return rowSwipePosition(raw, width, configuration, widths);
119
+ // Once the jump is within a quarter point, finish it so the row stops writing.
120
+ if (!jumpVisible(now))
121
+ anchor.gap = 0;
122
+ const remaining = destructiveJumpRemaining((now - anchor.time) / 1000);
123
+ return anchoredPosition(raw, anchor.side, anchor.offset) + anchor.gap * remaining;
124
+ }
125
+ function jumpVisible(now) {
126
+ if (!anchor)
127
+ return false;
128
+ return Math.abs(anchor.gap) * destructiveJumpRemaining((now - anchor.time) / 1000) > 0.25;
129
+ }
130
+ /** Pill takeover for one tray. Destructive edges take over with the anchor and keep
131
+ * it through reversals; other edges follow the distance threshold. */
132
+ function takeover(side, raw, now) {
133
+ const edge = edges[side];
134
+ if (!edge.allowsFullSwipe)
135
+ return 0;
136
+ if (edge.actions.length === 1)
137
+ return 1;
138
+ const target = isDestructive(edge)
139
+ ? anchor?.side === side
140
+ ? 1
141
+ : 0
142
+ : raw * sides[side] >= thresholds[side]
143
+ ? 1
144
+ : 0;
145
+ const sample = takeovers[side].sample(target, now);
146
+ return reducedMotion ? target : Math.max(0, Math.min(1, sample));
147
+ }
148
+ /** Whether releasing with `raw` performs the first action of the presented side. */
149
+ function armed(raw) {
150
+ const side = anchor?.side ?? sideOf(raw);
151
+ const edge = edges[side];
152
+ if (!edge.allowsFullSwipe || !firstActionEnabled(edge))
153
+ return false;
154
+ if (isDestructive(edge))
155
+ return engaged && anchor?.side === side;
156
+ return raw * sides[side] >= thresholds[side];
157
+ }
158
+ return {
159
+ /** Record the finger's row-local x; the next presented frame evaluates it. */
160
+ track(x) {
161
+ finger = x;
162
+ },
163
+ present,
164
+ takeover,
165
+ armed,
166
+ anchored: () => anchor !== undefined,
167
+ /** The jump is still visibly moving, so a frame must be presented without input. */
168
+ settling: jumpVisible,
169
+ };
170
+ }
171
+ /** A takeover spring that retargets from its presented position and velocity. */
172
+ function springState(initial, now) {
173
+ const state = { from: initial, target: initial, velocity: 0, started: now };
174
+ return {
175
+ sample(target, time) {
176
+ const sample = takeoverSample(state.from, state.target, state.velocity, (time - state.started) / 1000);
177
+ if (target !== state.target) {
178
+ state.from = sample.position;
179
+ state.velocity = sample.velocity;
180
+ state.target = target;
181
+ state.started = time;
182
+ }
183
+ return sample.position;
184
+ },
185
+ };
186
+ }