@stacksjs/mobile 0.75.97 → 0.75.99

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/README.md CHANGED
@@ -14,6 +14,30 @@ if (isNativeMobile())
14
14
  const position = await location.getCurrentPosition()
15
15
  ```
16
16
 
17
+ ## Waiting for the bridge
18
+
19
+ Craft's newer iOS shell injects `window.craft` at document start; an older
20
+ one installs it once the page has loaded. A call made inside a native host
21
+ before the bridge exists now waits for it (up to `BRIDGE_WAIT_MS`, 2.5s)
22
+ instead of quietly taking the web path, and a browser takes the fallback at
23
+ once. `callNative(path, args, fallback)` and `afterBridge(run)` are the
24
+ building blocks, for a bridge API this package does not wrap yet.
25
+
26
+ ## Beyond the core services
27
+
28
+ Each with a web fallback, and none throwing for being unsupported:
29
+ `dialog` (alert, confirm, actionSheet; HTML stand-ins in a browser),
30
+ `confirmAction`, `useActionSheet`, `contextMenu`, `browser.open` (in-app
31
+ Safari or an auth session), `symbols.image` (SF Symbols, cached), `statusBar`,
32
+ `chrome`, `refresh` and `observeNativeRefresh` (the native refresh control),
33
+ `background`, `clipboard`, `db`, `files`, `shortcuts`, `widgets`,
34
+ `orientation`, `auth.signInWithApple` and `storeKit`; the phone's events
35
+ `onAppearance`, `onResume`, `onMemoryWarning`, `onSilentPush` and
36
+ `onBackgroundRefresh`; and `haptics.prepare()`. The gesture logic behind the
37
+ Native components (sheet detents, the large title, long press, segment
38
+ scrubbing, pressables, the chrome sync) is exported too. See the
39
+ [Mobile Apps guide](https://stacksjs.org/guide/mobile).
40
+
17
41
  ## Native bridge availability
18
42
 
19
43
  `mobile` is a typed service collection (`MobileApi`), available in both browsers
@@ -0,0 +1,90 @@
1
+ /** The page's window, when there is one. */
2
+ export declare function craftHost(): CraftHost | undefined;
3
+ /** `window.craft`, the bridge object, once installed. */
4
+ export declare function craftRoot(): Record<string, any> | undefined;
5
+ /**
6
+ * Whether this page runs inside Craft's phone shell, before the bridge exists.
7
+ *
8
+ * Craft installs `window.craft` once the page has finished loading on an older
9
+ * shell — images and all — so a bridge check answers `false` during setup on
10
+ * a phone, and a layout that decides then renders the website's chrome in the
11
+ * app. The transports are there from the start: Android's `CraftAndroid`
12
+ * interface, and on iOS the `craft` message handler. Craft's macOS windows
13
+ * carry that handler too, so on iOS an iPhone or iPad user agent is what makes
14
+ * it the phone app.
15
+ */
16
+ export declare function hasNativeMobileHost(): boolean;
17
+ /** A native host whose bridge object is not installed yet: worth waiting for. */
18
+ export declare function bridgePending(): boolean;
19
+ /**
20
+ * Resolves once the bridge object (`window.craft`) is installed: `true` then,
21
+ * `false` in a browser at once, or after `timeoutMs` in a host that never
22
+ * installs it.
23
+ *
24
+ * Not whenNativeMobile(), which answers `true` as soon as the phone shell is
25
+ * recognisable, before the bridge exists and before anything can be asked of it.
26
+ */
27
+ export declare function whenBridgeReady(timeoutMs?: unknown): Promise<boolean>;
28
+ /**
29
+ * The bridge function at `path` (`'dialog.confirm'`), or null when this host
30
+ * has none. Synchronous: no waiting for a bridge still on its way.
31
+ */
32
+ export declare function nativeFunctionNow(path: string): ((...args: any[]) => unknown) | null;
33
+ /**
34
+ * The bridge function at `path`, waiting for a native host's bridge to arrive
35
+ * first. Null in a browser, on a shell that predates the API, or when the
36
+ * bridge did not come within `timeoutMs`.
37
+ */
38
+ export declare function nativeFunction(path: string, timeoutMs?: unknown): Promise<((...args: any[]) => unknown) | null>;
39
+ /**
40
+ * Calls the bridge function at `path`, or `fallback` where there is none.
41
+ *
42
+ * Inside a native host whose bridge is still loading, the call is held until
43
+ * it arrives, so an early call reaches the native implementation rather than
44
+ * the web one.
45
+ */
46
+ export declare function callNative<T>(path: string, args: unknown[], fallback: () => T | Promise<T>, options?: CallNativeOptions): Promise<T>;
47
+ /**
48
+ * Runs `run` once a native host's bridge is installed: at once when it is
49
+ * there or in a browser, after it arrives (or `BRIDGE_WAIT_MS` passes) in a
50
+ * host still loading it.
51
+ *
52
+ * craft-native's services read `window.craft` at call time and take their web
53
+ * path when it is missing, which during an older shell's launch it is. The
54
+ * services this package exports call through here, as plain object literals
55
+ * rather than wrapped at import: a bundler drops an object literal nobody
56
+ * imports, and could not drop a call made at module scope once minified.
57
+ */
58
+ export declare function afterBridge<T>(run: () => T | Promise<T>, timeoutMs?: unknown): Promise<T>;
59
+ /**
60
+ * Subscribes once the bridge can take the subscription: at once when it is
61
+ * there or in a browser, otherwise when it arrives. The returned function
62
+ * unsubscribes either way, including before the subscription was made.
63
+ */
64
+ export declare function subscribeWhenReady(subscribe: () => (() => void) | void, timeoutMs?: unknown): () => void;
65
+ /** Listens for a Craft DOM event on the window; the detail is passed through. */
66
+ export declare function onCraftEvent<T = Record<string, unknown>>(name: string, callback: (detail: T) => void): () => void;
67
+ /**
68
+ * Reaching Craft's bridge at the right moment.
69
+ *
70
+ * Craft's iOS shell injects `window.craft` at document start, but an older
71
+ * shell installs it once the page has finished loading. A call made before
72
+ * then used to find no bridge and quietly take the web path: a secure-storage
73
+ * read answered from localStorage, a confirm drew the browser's own dialog.
74
+ * Inside a native host, a call now waits for the bridge (briefly: a host that
75
+ * never installs one gets the web fallback after `BRIDGE_WAIT_MS`), and only a
76
+ * browser takes the fallback at once.
77
+ */
78
+ /** How long a call waits for a native host's bridge before using the web fallback. */
79
+ export declare const BRIDGE_WAIT_MS: 2500;
80
+ export declare interface CraftHost extends EventTarget {
81
+ craft?: unknown
82
+ CraftAndroid?: unknown
83
+ webkit?: { messageHandlers?: Record<string, unknown> }
84
+ document?: Document
85
+ navigator?: Navigator
86
+ }
87
+ export declare interface CallNativeOptions {
88
+ fallbackOnError?: boolean
89
+ timeoutMs?: number
90
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Calls `onLongPress` when a finger (or mouse) stays on `element` for
3
+ * `delay`. The click that follows a long press is swallowed, so a long press
4
+ * on a link opens its menu without also following it, and the browser's own
5
+ * callout menu is kept out of the way.
6
+ */
7
+ export declare function observeLongPress(element: HTMLElement, options: LongPressOptions): () => void;
8
+ /** Which of `count` equal segments across `rect` the x coordinate is over, clamped to the ends. */
9
+ export declare function segmentAt(x: number, rect: { left: number, width: number }, count: number): number;
10
+ /**
11
+ * Scrubbing a segmented control: a finger that lands on the chosen segment and
12
+ * slides drags the thumb across, and lifting chooses where it stopped. A tap
13
+ * elsewhere stays the button's own click.
14
+ */
15
+ export declare function observeSegmentScrub(track: HTMLElement, options: SegmentScrubOptions): () => void;
16
+ /** The segment an arrow, Home or End key moves to from `index`, or null for any other key. */
17
+ export declare function segmentForKey(key: string, index: number, count: number): number | null;
18
+ /** Moves focus to the `index`th tab of a segmented control, as arrow keys do in a tab list. */
19
+ export declare function focusSegment(track: HTMLElement, index: number): void;
20
+ /**
21
+ * Wakes the Taptic Engine as a finger lands on anything marked
22
+ * `data-native-pressable`, so the tap that follows its press plays at once.
23
+ *
24
+ * Listening for touchstart at all is also what makes iOS apply `:active`, the
25
+ * pressed state the shell's stylesheet draws.
26
+ */
27
+ export declare function installPressables(root: EventTarget): () => void;
28
+ export declare interface LongPressOptions {
29
+ delay?: number
30
+ tolerance?: number
31
+ onPressStart?: () => void
32
+ onPressEnd?: () => void
33
+ onLongPress: (event: { x: number, y: number }) => void
34
+ }
35
+ export declare interface SegmentScrubOptions {
36
+ count: () => number
37
+ selected: () => number
38
+ onPress?: (pressed: boolean) => void
39
+ onScrub: (index: number) => void
40
+ onCommit: (index: number) => void
41
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The appearance as it stands. The shell writes `--craft-font-scale` and
3
+ * `data-craft-reduce-*` on the root element before the page runs, so this is
4
+ * right at setup too; elsewhere it reads the media queries.
5
+ */
6
+ export declare function currentAppearance(): NativeAppearance;
7
+ /** A `craftAppearance` detail, made whole. */
8
+ export declare function normalizeAppearance(detail: Partial<NativeAppearance> | null | undefined): NativeAppearance;
9
+ /**
10
+ * Calls `callback` with the appearance now and whenever the phone's settings
11
+ * change (Dynamic Type, Reduce Motion, Reduce Transparency, Light/Dark).
12
+ */
13
+ export declare function onAppearance(callback: (appearance: NativeAppearance) => void, options?: { immediate?: boolean }): () => void;
14
+ /** The app came back to the front after `backgroundedMs` away. */
15
+ export declare function onResume(callback: (detail: { backgroundedMs: number }) => void): () => void;
16
+ /** iOS is short of memory: drop caches that can be rebuilt. */
17
+ export declare function onMemoryWarning(callback: () => void): () => void;
18
+ /** A content-available push arrived. The handler's settling completes it. */
19
+ export declare function onSilentPush(handler: (payload: Record<string, unknown>) => unknown): () => void;
20
+ /** iOS woke the app to refresh (BGAppRefreshTask). The handler's settling completes it. */
21
+ export declare function onBackgroundRefresh(handler: () => unknown): () => void;
22
+ export declare interface NativeAppearance {
23
+ contentSizeCategory?: string
24
+ fontScale: number
25
+ reduceMotion: boolean
26
+ reduceTransparency: boolean
27
+ colorScheme: 'light' | 'dark'
28
+ }
@@ -25,14 +25,50 @@ export declare function pullDistance(scrollTop: number, fingerTravel: number): n
25
25
  * it disarms. Letting go while armed runs `onRefresh`.
26
26
  */
27
27
  export declare function observePullToRefresh(options: PullToRefreshOptions): () => void;
28
- /** Where a page scrolls: the window, or a stand-in with the same shape. */
28
+ /**
29
+ * Where a large-title navigation bar is for a scroll offset.
30
+ *
31
+ * The title collapses over its own height. Pulled past the top (iOS's rubber
32
+ * band, a negative offset) it grows a little, at most 10%, anchored at its
33
+ * leading edge.
34
+ */
35
+ export declare function largeTitleMetrics(offset: number, height?: number): LargeTitleMetrics;
36
+ /**
37
+ * Links a navigation bar to the page's scroll, a frame at a time.
38
+ *
39
+ * Writes `--native-nav-progress` (0 to 1) and `--native-nav-stretch` on
40
+ * `element`, so its CSS can fade the material in, move the small title into
41
+ * the bar and stretch the large one, all on the compositor. Nothing re-renders.
42
+ */
43
+ export declare function observeLargeTitle(element: HTMLElement, options?: LargeTitleOptions): () => void;
44
+ /**
45
+ * The element a selector names, as a scroll host: for a screen whose content
46
+ * scrolls inside a container rather than the page. Undefined (the window)
47
+ * when nothing matches.
48
+ */
49
+ export declare function resolveScrollHost(selector: string | null | undefined): ScrollHost | undefined;
50
+ /**
51
+ * Where a page scrolls: the window, a scrolling element (a screen that scrolls
52
+ * inside its own container), or a stand-in with the same shape.
53
+ */
29
54
  export declare interface ScrollHost extends EventTarget {
30
55
  scrollY?: number
56
+ scrollTop?: number
31
57
  document?: { scrollingElement?: { scrollTop: number } | null }
32
58
  }
33
59
  export declare interface PullToRefreshOptions {
34
60
  threshold?: number
35
- onPull?: (distance: number, armed: boolean) => void
61
+ onPull?: (distance: number, armed: boolean, pull: { bounce: number }) => void
36
62
  onRefresh: () => unknown | Promise<unknown>
37
63
  host?: ScrollHost
38
64
  }
65
+ export declare interface LargeTitleMetrics {
66
+ progress: number
67
+ stretch: number
68
+ }
69
+ export declare interface LargeTitleOptions {
70
+ height?: number
71
+ title?: HTMLElement | null
72
+ host?: ScrollHost
73
+ onChange?: (metrics: LargeTitleMetrics) => void
74
+ }