@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.
- package/NOTICE +1 -1
- package/README.md +1 -2
- package/dist/acknowledgements/inventory.json +1 -1
- package/dist/actions/RowActions.d.ts +2 -2
- package/dist/actions/RowActions.js +91 -25
- package/dist/actions/fullSwipeContact.d.ts +73 -0
- package/dist/actions/fullSwipeContact.js +186 -0
- package/dist/actions/rowScrub.d.ts +7 -4
- package/dist/actions/rowScrub.js +39 -37
- package/dist/actions/types.d.ts +13 -7
- package/dist/app/InstallComponents.d.ts +4 -7
- package/dist/app/InstallComponents.js +7 -7
- package/dist/app/InstallationDiagnostics.d.ts +2 -1
- package/dist/app/InstallationDiagnostics.js +2 -1
- package/dist/app/appearance.d.ts +5 -3
- package/dist/app/appearance.js +5 -3
- package/dist/app/appearanceStore.d.ts +3 -2
- package/dist/app/buildInfo.d.ts +4 -4
- package/dist/app/buildInfo.js +2 -2
- package/dist/app/definition.d.ts +11 -3
- package/dist/app/definition.js +2 -0
- package/dist/app/head.d.ts +2 -1
- package/dist/app/head.js +13 -16
- package/dist/app/inlineCSP.d.ts +6 -0
- package/dist/app/inlineCSP.js +25 -0
- package/dist/app/install.d.ts +1 -0
- package/dist/app/install.js +5 -2
- package/dist/app/installCheck.js +32 -86
- package/dist/app/installContext.d.ts +10 -5
- package/dist/app/installContext.js +2 -0
- package/dist/app/installCustom.js +1 -0
- package/dist/app/installExperience.js +0 -60
- package/dist/app/installationFailure.d.ts +3 -2
- package/dist/app/installationFailure.js +1 -1
- package/dist/app/launchScreen.d.ts +6 -2
- package/dist/app/launchScreen.js +104 -1
- package/dist/app/launchScreenRuntime.d.ts +31 -4
- package/dist/app/launchScreenRuntime.js +150 -22
- package/dist/app/launchWatchdog.js +25 -65
- package/dist/app/platformScripts.js +11 -0
- package/dist/app/styles.d.ts +3 -0
- package/dist/app/styles.js +42 -0
- package/dist/app/viewportValue.d.ts +2 -1
- package/dist/app/vite.d.ts +4 -0
- package/dist/app/vite.js +22 -24
- package/dist/content/content.d.ts +35 -30
- package/dist/content/content.js +19 -16
- package/dist/content/layout.d.ts +10 -2
- package/dist/content/layout.js +4 -2
- package/dist/content/scaledMetric.d.ts +2 -1
- package/dist/content/scaledMetric.js +2 -1
- package/dist/controls/ColorPicker.d.ts +2 -4
- package/dist/controls/ColorPicker.js +2 -4
- package/dist/controls/DatePicker.d.ts +4 -4
- package/dist/controls/DatePicker.js +2 -2
- package/dist/controls/ProgressView.d.ts +4 -4
- package/dist/controls/ProgressView.js +1 -2
- package/dist/data/database.d.ts +9 -8
- package/dist/data/database.js +4 -4
- package/dist/docs/reference.md +982 -964
- package/dist/docs/tailwind.md +76 -0
- package/dist/docs/tokens.md +1 -1
- package/dist/gestures/GestureView.d.ts +3 -1
- package/dist/gestures/GestureView.js +2 -1
- package/dist/gestures/gesture.d.ts +22 -2
- package/dist/gestures/gesture.js +7 -2
- package/dist/glass/GlassEffect.d.ts +2 -1
- package/dist/glass/GlassEffect.js +2 -1
- package/dist/glass/GlassPresentation.d.ts +1 -0
- package/dist/glass/GlassPresentation.js +21 -4
- package/dist/glass/TabBar.js +23 -1
- package/dist/glass/errorAlert.js +2 -1
- package/dist/glass/errors.d.ts +3 -1
- package/dist/glass/fallback/runtime.d.ts +1 -0
- package/dist/glass/fallback/runtime.js +8 -1
- package/dist/glass/liquidContext.d.ts +2 -0
- package/dist/glass/menuPresenter.d.ts +2 -0
- package/dist/glass/regions.d.ts +2 -1
- package/dist/glass/renderSpecs.d.ts +1 -0
- package/dist/glass/renderSpecs.js +1 -0
- package/dist/glass/webgpu/controlRim.d.ts +29 -0
- package/dist/glass/webgpu/controlRim.js +68 -0
- package/dist/glass/webgpu/passes.d.ts +2 -2
- package/dist/glass/webgpu/production.d.ts +1 -0
- package/dist/glass/webgpu/production.js +27 -1
- package/dist/glass/webgpu/rasterUniforms.js +9 -0
- package/dist/glass/webgpu/shaders.d.ts +2 -2
- package/dist/glass/webgpu/shaders.js +5 -1
- package/dist/haptics/SensoryButton.d.ts +4 -4
- package/dist/haptics/SensoryButton.js +3 -3
- package/dist/image/Image.d.ts +6 -3
- package/dist/image/Image.js +4 -3
- package/dist/image/asset.d.ts +2 -1
- package/dist/keyboard/adapter.d.ts +3 -4
- package/dist/keyboard/geometry.d.ts +1 -1
- package/dist/list-form/ListForm.d.ts +8 -7
- package/dist/list-form/ListForm.js +2 -2
- package/dist/list-form/editing.d.ts +5 -6
- package/dist/list-form/editing.js +4 -5
- package/dist/list-form/offsets.d.ts +2 -2
- package/dist/list-form/offsets.js +2 -2
- package/dist/media/react.d.ts +2 -1
- package/dist/media/react.js +2 -1
- package/dist/media/types.d.ts +6 -3
- package/dist/motion/TransitionView.d.ts +4 -2
- package/dist/motion/TransitionView.js +3 -2
- package/dist/motion/animationValue.d.ts +1 -1
- package/dist/motion/matchedGeometry.d.ts +6 -5
- package/dist/motion/matchedGeometry.js +2 -1
- package/dist/motion/transition.d.ts +4 -1
- package/dist/motion/transition.js +2 -0
- package/dist/navigation/DetailList.d.ts +1 -1
- package/dist/navigation/DetailList.js +1 -1
- package/dist/navigation/Sidebar.d.ts +3 -1
- package/dist/navigation/Sidebar.js +1 -1
- package/dist/navigation/SidebarSymbol.d.ts +2 -1
- package/dist/navigation/SidebarSymbol.js +2 -1
- package/dist/navigation/navigationContext.d.ts +2 -1
- package/dist/navigation/navigationContext.js +2 -1
- package/dist/navigation/splitContext.d.ts +2 -1
- package/dist/navigation/splitContext.js +2 -1
- package/dist/navigation/toolbarMinimization.d.ts +4 -3
- package/dist/navigation/types.d.ts +7 -5
- package/dist/presentation/Presentation.d.ts +8 -7
- package/dist/presentation/Presentation.js +3 -2
- package/dist/presentation/model.d.ts +2 -1
- package/dist/press/Pressable.d.ts +5 -1
- package/dist/press/Pressable.js +2 -0
- package/dist/press/stateMachine.d.ts +1 -1
- package/dist/public/Label.d.ts +2 -1
- package/dist/public/Label.js +2 -1
- package/dist/public/Screen.d.ts +4 -6
- package/dist/public/actions.d.ts +2 -1
- package/dist/public/actions.js +2 -1
- package/dist/public/buttonBorderShape.d.ts +2 -1
- package/dist/public/colors.d.ts +7 -4
- package/dist/public/colors.js +3 -2
- package/dist/public/controlStyle.d.ts +17 -14
- package/dist/public/controls.d.ts +27 -26
- package/dist/public/controls.js +20 -19
- package/dist/public/deviceActions.d.ts +6 -4
- package/dist/public/deviceActions.js +4 -2
- package/dist/public/environment.d.ts +16 -13
- package/dist/public/environment.js +11 -9
- package/dist/public/filePickers.d.ts +6 -4
- package/dist/public/filePickers.js +3 -2
- package/dist/public/glass.d.ts +6 -3
- package/dist/public/glass.js +2 -1
- package/dist/public/glassValue.d.ts +3 -4
- package/dist/public/glassValue.js +3 -4
- package/dist/public/keyboard.d.ts +4 -1
- package/dist/public/keyboard.js +2 -1
- package/dist/public/navigationTransition.d.ts +5 -5
- package/dist/public/navigationTransition.js +2 -2
- package/dist/public/presentation.d.ts +8 -7
- package/dist/public/presentation.js +5 -5
- package/dist/public/runtime.d.ts +9 -8
- package/dist/public/runtime.js +7 -7
- package/dist/public/scroll.d.ts +6 -3
- package/dist/public/scroll.js +3 -1
- package/dist/public/structure.d.ts +2 -2
- package/dist/public/toolbar.d.ts +15 -11
- package/dist/public/toolbar.js +12 -7
- package/dist/reachability/model.d.ts +4 -3
- package/dist/reachability/react.d.ts +4 -1
- package/dist/reachability/react.js +2 -1
- package/dist/root/SatinRoot.d.ts +3 -3
- package/dist/root/SatinRoot.js +12 -36
- package/dist/root/mountSatin.d.ts +1 -1
- package/dist/root/mountSatin.js +26 -35
- package/dist/root/runtimeContext.d.ts +2 -1
- package/dist/root/runtimeContext.js +2 -1
- package/dist/safe-area/Overlay.d.ts +3 -2
- package/dist/safe-area/Overlay.js +2 -1
- package/dist/safe-area/SafeAreaInset.d.ts +11 -7
- package/dist/safe-area/SafeAreaInset.js +7 -6
- package/dist/safe-area/context.d.ts +3 -3
- package/dist/safe-area/context.js +1 -1
- package/dist/safe-area/displayCorners.d.ts +4 -2
- package/dist/safe-area/displayCorners.js +4 -2
- package/dist/safe-area/modifiers.d.ts +14 -10
- package/dist/safe-area/modifiers.js +8 -4
- package/dist/scroll/ScrollSurface.js +5 -1
- package/dist/scroll/ScrollTarget.d.ts +1 -1
- package/dist/scroll/ScrollTarget.js +1 -1
- package/dist/scroll/edgePolicy.d.ts +2 -2
- package/dist/scroll/position.d.ts +5 -3
- package/dist/scroll/types.d.ts +1 -1
- package/dist/search/PadSearchDrawer.d.ts +7 -0
- package/dist/search/PadSearchDrawer.js +8 -0
- package/dist/search/PadSearchPresentationContext.d.ts +6 -0
- package/dist/search/PadSearchPresentationContext.js +4 -0
- package/dist/search/SearchDrawer.d.ts +1 -1
- package/dist/search/SearchDrawer.js +6 -1
- package/dist/search/SearchSuggestionsPopover.d.ts +6 -0
- package/dist/search/SearchSuggestionsPopover.js +42 -0
- package/dist/search/SearchToolbarField.d.ts +2 -1
- package/dist/search/SearchToolbarField.js +28 -7
- package/dist/search/padSearchBarMotion.d.ts +69 -0
- package/dist/search/padSearchBarMotion.js +73 -0
- package/dist/search/padSearchFieldSamples.json +491 -0
- package/dist/search/searchPlacement.d.ts +3 -0
- package/dist/search/searchPlacement.js +7 -0
- package/dist/search/searchable.d.ts +14 -12
- package/dist/search/usePadSearchFieldMotion.d.ts +4 -0
- package/dist/search/usePadSearchFieldMotion.js +100 -0
- package/dist/search/useSearchPresentation.d.ts +2 -1
- package/dist/search/useSearchPresentation.js +23 -5
- package/dist/server.d.ts +4 -4
- package/dist/server.js +4 -4
- package/dist/shapes/corners.d.ts +3 -3
- package/dist/shapes/corners.js +1 -2
- package/dist/styles.css +8289 -8244
- package/dist/sw/vite.d.ts +4 -0
- package/dist/sw/vite.js +2 -0
- package/dist/symbols/Symbol.d.ts +7 -5
- package/dist/symbols/Symbol.js +5 -3
- package/dist/symbols/catalogue.d.ts +1 -1
- package/dist/symbols/types.d.ts +6 -4
- package/dist/tabs/TabSection.d.ts +3 -2
- package/dist/tabs/TabSection.js +2 -1
- package/dist/tabs/TabView.js +54 -51
- package/dist/tasks/useTask.d.ts +5 -4
- package/dist/tasks/useTask.js +4 -3
- package/dist/testing.d.ts +3 -2
- package/dist/testing.js +3 -2
- package/dist/text-input/focusChain.d.ts +4 -3
- package/dist/text-input/focusChain.js +3 -2
- package/dist/text-input/kinds.d.ts +1 -1
- package/dist/web/WebView.d.ts +10 -3
- package/dist/web/WebView.js +3 -2
- package/dist/web/authentication.d.ts +12 -1
- package/dist/web/authentication.js +2 -1
- package/dist/web/openURL.d.ts +10 -4
- package/dist/web/openURL.js +4 -2
- package/dist/web/useWebAuthentication.d.ts +2 -1
- package/dist/web/useWebAuthentication.js +2 -1
- package/dist/zoom/Zoomable.d.ts +3 -3
- package/dist/zoom/Zoomable.js +2 -2
- package/package.json +1 -1
package/NOTICE
CHANGED
|
@@ -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.
|
|
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
|
|
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() }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
3
|
"name": "Satin",
|
|
4
|
-
"version": "0.1.0-beta.
|
|
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",
|
|
@@ -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
|
-
*
|
|
35
|
-
*
|
|
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,
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
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]
|
|
456
|
-
// The same
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
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
|
-
|
|
513
|
+
if (!anchoredClose)
|
|
514
|
+
element.dataset.satinSwipeActive = String(current !== 0);
|
|
469
515
|
live.current.offset = current;
|
|
470
516
|
setOffset(current);
|
|
471
|
-
|
|
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)'
|
|
@@ -812,8 +878,8 @@ function withTrailingAction(swipeActions, action) {
|
|
|
812
878
|
return configurations;
|
|
813
879
|
}
|
|
814
880
|
/**
|
|
815
|
-
*
|
|
816
|
-
*
|
|
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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FullSwipeContact } from './fullSwipeContact.js';
|
|
1
2
|
import { type SwipeActions } from './types.js';
|
|
2
3
|
export declare const swipeGeometry: {
|
|
3
4
|
readonly buttonWidth: 60;
|
|
@@ -14,8 +15,10 @@ export declare function swipeWidths(host: HTMLElement): SwipeWidths;
|
|
|
14
15
|
export declare function fullSwipeThreshold(width: number, count: number, side?: 'leading' | 'trailing', buttonWidth?: number, inline?: boolean): number;
|
|
15
16
|
export declare const swipeDragLimit: (width: number, count: number, buttonWidth?: number) => number;
|
|
16
17
|
/** Spatial fit of native's sequential capsule reveal. Rest interpolates the
|
|
17
|
-
* contact pose into the fully revealed spring target; it is never a DOM writer.
|
|
18
|
-
|
|
18
|
+
* contact pose into the fully revealed spring target; it is never a DOM writer.
|
|
19
|
+
* A destructive takeover packs the other actions against the row and the primary
|
|
20
|
+
* fills the rest (native: primary = revealed - 80 on a tall two-action row). */
|
|
21
|
+
export declare function swipeActionPose(position: number, index: number, count: number, takeover?: number, rest?: number, buttonWidth?: number, inline?: boolean, allowsFullSwipe?: boolean, destructive?: boolean): {
|
|
19
22
|
scale: number;
|
|
20
23
|
spread: number;
|
|
21
24
|
opacity: number;
|
|
@@ -36,8 +39,8 @@ export declare function primaryParts(primary: HTMLElement): HTMLElement[];
|
|
|
36
39
|
export declare function primaryTransform(part: HTMLElement, growth: number, middleWidth?: number): string;
|
|
37
40
|
/** Recover takeover from the presented capsule, including a caught settle. */
|
|
38
41
|
export declare function presentedTakeover(primary: HTMLElement, position: number): number;
|
|
39
|
-
export declare function createRowScrub(host: HTMLElement, source: HTMLElement, content: HTMLElement, initial: number, width: number, configuration
|
|
40
|
-
flush: (now?: number) => {
|
|
42
|
+
export declare function createRowScrub(host: HTMLElement, source: HTMLElement, content: HTMLElement, initial: number, width: number, configuration: SwipeActions | undefined, contact: FullSwipeContact): {
|
|
43
|
+
flush: (now?: number, evaluate?: boolean) => {
|
|
41
44
|
raw: number;
|
|
42
45
|
position: number;
|
|
43
46
|
};
|