@stacksjs/mobile 0.75.55 → 0.75.57

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
@@ -117,6 +117,36 @@ hasNativeMobileHost() // true inside the phone shell, even before the bridge
117
117
  const native = await whenNativeMobile() // resolves once it knows, false in a browser
118
118
  ```
119
119
 
120
+ ## Device search index
121
+
122
+ What a device may index is configuration (`config/mobile.ts`, `spotlight`): one
123
+ entry per kind of content, with the number of slots it may claim, the route a
124
+ tapped entry opens, and the noun that names a record with no name of its own.
125
+
126
+ ```ts
127
+ import mobileConfig from '../config/mobile'
128
+ import { createSpotlightIndex, onSpotlightTap } from '@stacksjs/mobile'
129
+
130
+ const spotlight = createSpotlightIndex({ kinds: mobileConfig.spotlight.kinds })
131
+
132
+ await spotlight.index('trail', { id: 42, name: 'Eagle Peak Loop' })
133
+ await spotlight.sync('club', myClubs)
134
+ await spotlight.remove('club', 7)
135
+ await spotlight.clear()
136
+ onSpotlightTap(spotlight, route => location.assign(route))
137
+ ```
138
+
139
+ iOS indexes donated `NSUserActivity` objects — Craft's `siri` bridge — and
140
+ hands a tapped one back only for an activity type the build declares in
141
+ `Info.plist`, a list fixed at build time. A type per record id cannot be
142
+ declared, so each kind gets a fixed number of slots, each slot holds whichever
143
+ record is in it, and the oldest donation makes room; `buddy build:ios` writes
144
+ the declarations from the same config.
145
+
146
+ Every call is a no-op that reports as much off a native host, on a build whose
147
+ host predates the bridge, in a WebView with no `localStorage`, and where the
148
+ index is turned off, so a page can donate unconditionally.
149
+
120
150
  ## Page gestures and navigation
121
151
 
122
152
  The phone-native behaviour a screen expects, kept out of STX templates:
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Bundled TypeScript declarations
3
- * Generated from 5 source files
3
+ * Generated from 6 source files
4
4
  */
5
5
 
6
6
  // From: src/index.ts
@@ -205,6 +205,204 @@ declare interface RouteRecorder {
205
205
  readonly stats: RouteStats
206
206
  }
207
207
 
208
+ // From: src/spotlight.ts
209
+ /**
210
+ * Read configured kinds, leaving out any that could not work.
211
+ *
212
+ * Dropping rather than throwing: a typo in one kind's budget should cost that
213
+ * kind its index, not the app's startup. The problems come back so a caller
214
+ * can say what it ignored — silence here looks exactly like a kind nothing has
215
+ * donated to yet.
216
+ */
217
+ declare function readSpotlightKinds(raw: unknown): SpotlightKindsResult;
218
+ /** One kind by name, or null for anything this build does not index. */
219
+ declare function spotlightKind(kinds: readonly SpotlightKind[], name: string): SpotlightKind | null;
220
+ /** The action a kind's slot is donated under, or null for no such slot. */
221
+ declare function spotlightAction(kinds: readonly SpotlightKind[], kind: string, slot: number): string | null;
222
+ /**
223
+ * The kind and slot an action names, or null for anything else.
224
+ *
225
+ * Strict about the form — no padding, no trailing text, and the kind has to be
226
+ * one these kinds carry — because this is what decides whether something the
227
+ * system handed back is one of the app's own.
228
+ */
229
+ declare function spotlightSlot(kinds: readonly SpotlightKind[], action: string): { kind: string, slot: number } | null;
230
+ /** Every action these kinds may be donated under, in configured order. */
231
+ declare function spotlightActions(kinds: readonly SpotlightKind[]): string[];
232
+ /**
233
+ * Every activity type a build has to declare, for an `Info.plist`.
234
+ *
235
+ * The slots of every configured kind, plus whatever the app donates outside
236
+ * the registry (`activityTypes`: a Siri phrase, an App Intent), each prefixed
237
+ * with the bundle id the way Craft types the activity it donates.
238
+ */
239
+ declare function spotlightActivityTypes(config: unknown, bundleId: string): string[];
240
+ /**
241
+ * What the device shows for a record.
242
+ *
243
+ * The record's name alone: Craft hands the same string to Spotlight as the
244
+ * activity's title and to Siri as its invocation phrase, and a name is the one
245
+ * form that reads well in both — it is what somebody types looking for the
246
+ * record, and what they could say.
247
+ */
248
+ declare function spotlightTitle(kinds: readonly SpotlightKind[], kind: string, item: SpotlightItem): string;
249
+ /**
250
+ * Put a record in its kind's slots, taking the oldest when every one is taken.
251
+ *
252
+ * Returns the whole next list rather than mutating, so a caller stores exactly
253
+ * what it donated: a failure between the two cannot leave bookkeeping that
254
+ * claims an entry the device does not have.
255
+ */
256
+ declare function placeSpotlightItem(kinds: readonly SpotlightKind[], entries: readonly SpotlightEntry[], kind: string, item: SpotlightItem, now?: number): SpotlightPlacement | null;
257
+ /** Take a record out — what it held is the slot to un-donate. */
258
+ declare function removeSpotlightItem(entries: readonly SpotlightEntry[], kind: string, itemId: number): { entries: SpotlightEntry[], removed: SpotlightEntry | null };
259
+ /** The route a tapped entry means, or null for a slot holding nothing. */
260
+ declare function spotlightRoute(kinds: readonly SpotlightKind[], entries: readonly SpotlightEntry[], action: string): string | null;
261
+ /**
262
+ * Read back a stored assignment.
263
+ *
264
+ * Stored state outlives the build that wrote it — a kind can be renamed and a
265
+ * budget can shrink — so anything that is not a slot these kinds declare is
266
+ * dropped rather than trusted: a tap on it would resolve to a record the
267
+ * `Info.plist` no longer lets through.
268
+ */
269
+ declare function readSpotlightEntries(kinds: readonly SpotlightKind[], raw: unknown): SpotlightEntry[];
270
+ /** The host's Siri bridge, or undefined anywhere that is not a native build. */
271
+ declare function spotlightBridge(): SpotlightBridge | undefined;
272
+ declare function createSpotlightIndex(options: SpotlightIndexOptions): SpotlightIndex;
273
+ /**
274
+ * Follow a tap on an indexed entry.
275
+ *
276
+ * Craft reports one as a `craftSiriShortcut` event carrying `{action}` (a
277
+ * home-screen quick action arrives as `craftShortcut` with `{type}`, handled
278
+ * here too), and the handler is called with the route the action resolves to —
279
+ * anything this index does not recognise is left alone, so an app with its own
280
+ * shortcut actions can keep handling those itself.
281
+ *
282
+ * These are plain window events, listened for directly rather than through
283
+ * Craft's `shortcuts.onShortcut` and `siri.onInvoke`: both of those are
284
+ * themselves listeners on these same events, so going through them would run
285
+ * every tap twice and leave a subscription their wrappers cannot take back
286
+ * down.
287
+ */
288
+ declare function onSpotlightTap(index: SpotlightIndex, handler: (route: string) => void): () => void;
289
+ /**
290
+ * The most slots one kind may claim.
291
+ *
292
+ * Every slot is a line in the generated `Info.plist` and a possible donation
293
+ * at launch, so a mistyped budget is worth catching before a build log.
294
+ */
295
+ declare const MAX_SPOTLIGHT_SLOTS_PER_KIND: 64;
296
+ /**
297
+ * The app's own content in a device's search index.
298
+ *
299
+ * An app somebody opens is not the same thing as a record somebody finds: a
300
+ * trail, a club, a document that turns up when its name is typed into the home
301
+ * screen is reached without the app being thought of first. iOS builds that
302
+ * index out of donated `NSUserActivity` objects, which Craft's `siri` bridge
303
+ * is: a title and an action, and what the app gets back on a tap is that
304
+ * action and nothing else. Two consequences shape this whole module:
305
+ *
306
+ * 1. iOS only hands a tapped activity back to an app that declares its type
307
+ * in `NSUserActivityTypes`, and that list is fixed at build time. A type
308
+ * per record id would be unbounded, so it cannot be declared — and an
309
+ * entry iOS will not hand back is worse than no entry, because it opens
310
+ * the app on whatever screen it was last on.
311
+ * 2. The activity carries no payload of the app's. Whatever a tap needs to
312
+ * know has to be derivable from the action alone.
313
+ *
314
+ * So each kind of content gets a fixed number of *slots* — `trail-slot-0`,
315
+ * `club-slot-0`, every one of them declared in the generated `Info.plist` by
316
+ * the iOS build — and each slot holds whichever record is currently in it.
317
+ * This module is that assignment: the pure part decides it, and
318
+ * `createSpotlightIndex` stores it and makes the bridge calls. Because a
319
+ * slot's identifier is also the activity's `persistentIdentifier`, re-donating
320
+ * a slot replaces the entry iOS already has rather than adding another.
321
+ *
322
+ * What is indexed, how many of each, and where a tap opens is configuration
323
+ * (`config/mobile.ts`, `spotlight`), so an app adds a kind with an entry and a
324
+ * call rather than with another copy of the bookkeeping below.
325
+ *
326
+ * Nothing here needs a native host: off one, every call is a no-op that says
327
+ * so, which is what lets a page donate unconditionally.
328
+ */
329
+ /** One kind of content, as the index holds it. */
330
+ declare interface SpotlightKind {
331
+ name: string
332
+ slots: number
333
+ route: string
334
+ noun: string
335
+ }
336
+ /** A kind as an app configures it, before it is checked. */
337
+ declare interface SpotlightKindInput {
338
+ slots: number
339
+ route: string
340
+ noun?: string
341
+ }
342
+ /** What this module needs of a record. A list row and a detail row both fit. */
343
+ declare interface SpotlightItem {
344
+ id: number
345
+ name?: string | null
346
+ }
347
+ /** One record's place in the index. */
348
+ declare interface SpotlightEntry {
349
+ kind: string
350
+ slot: number
351
+ itemId: number
352
+ title: string
353
+ donatedAt: number
354
+ }
355
+ /** Configured kinds, and what was wrong with the ones left out. */
356
+ declare interface SpotlightKindsResult {
357
+ kinds: SpotlightKind[]
358
+ problems: string[]
359
+ }
360
+ /** The result of fitting a record into its kind's slots. */
361
+ declare interface SpotlightPlacement {
362
+ entries: SpotlightEntry[]
363
+ entry: SpotlightEntry
364
+ evicted: SpotlightEntry | null
365
+ changed: boolean
366
+ }
367
+ /**
368
+ * Craft's Siri calls, which are also its Spotlight calls.
369
+ *
370
+ * `register` builds an `NSUserActivity` typed `<bundle id>.<action>`, titles it
371
+ * with the phrase, marks it eligible for search and prediction and makes it
372
+ * current — so a registered phrase is both a Siri suggestion and a Spotlight
373
+ * entry — and `remove` deletes the saved activity by that same identifier.
374
+ *
375
+ * Typed here because `craft-native`'s browser SDK exports neither: they exist
376
+ * only on the object Craft injects into the WebView, so without this seam every
377
+ * caller reaches into `globalThis.craft` and guesses at shapes.
378
+ */
379
+ declare interface SpotlightBridge {
380
+ register?: (phrase: string, action: string) => Promise<unknown>
381
+ remove?: (action: string) => Promise<unknown>
382
+ }
383
+ declare interface SpotlightIndexOptions {
384
+ kinds: Record<string, SpotlightKindInput> | readonly SpotlightKind[]
385
+ enabled?: boolean
386
+ storageKey?: string
387
+ legacyStorageKeys?: readonly string[]
388
+ timeoutMs?: number
389
+ bridge?: () => SpotlightBridge | undefined
390
+ now?: () => number
391
+ onProblem?: (message: string) => void
392
+ }
393
+ /** The device's index, as an app drives it. */
394
+ declare interface SpotlightIndex {
395
+ readonly kinds: readonly SpotlightKind[]
396
+ index: (kind: string, item: SpotlightItem) => Promise<boolean>
397
+ sync: (kind: string, items: readonly SpotlightItem[]) => Promise<number>
398
+ remove: (kind: string, itemId: number) => Promise<boolean>
399
+ clear: () => Promise<number>
400
+ routeFor: (action: string) => string | null
401
+ entries: () => readonly SpotlightEntry[]
402
+ activityTypes: (bundleId: string) => string[]
403
+ reset: () => void
404
+ }
405
+
208
406
  // From: src/types.ts
209
407
  /** Enabled native features, not the hardware report from device.getCapabilities(). */
210
408
  declare interface CraftMobileCapabilities {
@@ -568,4 +766,4 @@ type HealthWorkoutActivity = | 'running' | 'cycling' | 'walking' | 'hiking' | 's
568
766
  | 'stairClimbing' | 'yoga' | 'pilates' | 'strength' | 'hiit' | 'crossTraining'
569
767
  | 'crossCountrySkiing' | 'skiing' | 'paddling' | 'climbing' | 'dance' | 'mobility' | 'other';
570
768
 
571
- export { normalizeDeepLinkURL, getNativeMobileBridge, isNativeMobile, hasNativeMobileHost, whenNativeMobile, whenBridgeReady, onMobileReady, withNativeFeedback, biometrics, camera, device, haptics, lifecycle, location, notifications, permissions, secureStorage, share, appReview, deepLinks, keepAwake, network, pushNotifications, health, liveActivities, watchConnectivity, mobile, pageScrollTop, observePageScroll, pullDistance, observePullToRefresh, ScrollHost, PullToRefreshOptions, canGoBack, goBack, deepLinkPath, followDeepLinks, HistoryHost, fixDistance, usableFixes, routeStats, paceLabel, speedLabel, createRouteRecorder, ROUTE_GAP_S, RouteFix, RouteStats, RouteStatsOptions, RouteRecorderOptions, RouteRecorder, CraftMobileCapabilities, CraftMobileBridge, CraftReadyEvent, MobileApi, DeviceInfo, DeviceCapabilities, CameraOptions, PhotoResult, Location, LocationOptions, LocationRecordingState, LocationRecordingResult, ShareOptions, NotificationOptions, DeviceApi, HapticsApi, PermissionsApi, CameraApi, BiometricsApi, SecureStorageApi, LocationApi, ShareApi, LifecycleApi, NotificationsApi, KeepAwakeApi, DeepLinksApi, NetworkStatus, NetworkApi, AppReviewApi, PushNotificationsApi, HealthDataOptions, HealthDataResult, HealthWorkoutLocation, HealthWorkout, HealthWorkoutResult, HealthWorkoutQuery, HealthWorkoutSample, HealthDailyValue, HealthAuthorizationOptions, HealthApi, LiveActivityState, LiveActivityOptions, LiveActivitiesApi, WatchConnectivityApi, HapticStyle, HapticNotificationType, PermissionType, PermissionStatus, BiometricType, AppState, HealthDataType, HealthDailyType, HealthWorkoutType, HealthWorkoutActivity };
769
+ export { normalizeDeepLinkURL, getNativeMobileBridge, isNativeMobile, hasNativeMobileHost, whenNativeMobile, whenBridgeReady, onMobileReady, withNativeFeedback, biometrics, camera, device, haptics, lifecycle, location, notifications, permissions, secureStorage, share, appReview, deepLinks, keepAwake, network, pushNotifications, health, liveActivities, watchConnectivity, mobile, pageScrollTop, observePageScroll, pullDistance, observePullToRefresh, ScrollHost, PullToRefreshOptions, canGoBack, goBack, deepLinkPath, followDeepLinks, HistoryHost, fixDistance, usableFixes, routeStats, paceLabel, speedLabel, createRouteRecorder, ROUTE_GAP_S, RouteFix, RouteStats, RouteStatsOptions, RouteRecorderOptions, RouteRecorder, readSpotlightKinds, spotlightKind, spotlightAction, spotlightSlot, spotlightActions, spotlightActivityTypes, spotlightTitle, placeSpotlightItem, removeSpotlightItem, spotlightRoute, readSpotlightEntries, spotlightBridge, createSpotlightIndex, onSpotlightTap, MAX_SPOTLIGHT_SLOTS_PER_KIND, SpotlightKind, SpotlightKindInput, SpotlightItem, SpotlightEntry, SpotlightKindsResult, SpotlightPlacement, SpotlightBridge, SpotlightIndexOptions, SpotlightIndex, CraftMobileCapabilities, CraftMobileBridge, CraftReadyEvent, MobileApi, DeviceInfo, DeviceCapabilities, CameraOptions, PhotoResult, Location, LocationOptions, LocationRecordingState, LocationRecordingResult, ShareOptions, NotificationOptions, DeviceApi, HapticsApi, PermissionsApi, CameraApi, BiometricsApi, SecureStorageApi, LocationApi, ShareApi, LifecycleApi, NotificationsApi, KeepAwakeApi, DeepLinksApi, NetworkStatus, NetworkApi, AppReviewApi, PushNotificationsApi, HealthDataOptions, HealthDataResult, HealthWorkoutLocation, HealthWorkout, HealthWorkoutResult, HealthWorkoutQuery, HealthWorkoutSample, HealthDailyValue, HealthAuthorizationOptions, HealthApi, LiveActivityState, LiveActivityOptions, LiveActivitiesApi, WatchConnectivityApi, HapticStyle, HapticNotificationType, PermissionType, PermissionStatus, BiometricType, AppState, HealthDataType, HealthDailyType, HealthWorkoutType, HealthWorkoutActivity };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{appReview as _,biometrics as I,camera as q,deepLinks as P,device as Y,haptics as K,health as j,keepAwake as G,lifecycle as W,liveActivities as z,location as J,network as Q,notifications as V,permissions as X,pushNotifications as Z,secureStorage as ee,share as te,watchConnectivity as ne}from"craft-native/mobile";import{haptics as C}from"craft-native/mobile";function A(){if(typeof window>"u")return;return window}function b(e=A()){if(!e)return 0;if(typeof e.scrollY==="number")return e.scrollY;return e.document?.scrollingElement?.scrollTop??0}function Se(e,t=A()){if(!t)return()=>{};let r=!1,n=typeof requestAnimationFrame==="function"?requestAnimationFrame:(i)=>setTimeout(i,16),o=()=>{if(r)return;r=!0,n(()=>{r=!1,e(b(t))})};return e(b(t)),t.addEventListener("scroll",o,{passive:!0}),()=>t.removeEventListener("scroll",o)}function N(e,t){let r=Math.max(0,-e),n=e<=0?Math.max(0,t)*0.5:0;return Math.round(Math.max(r,n))}function Le(e){let t=e.host??A();if(!t)return()=>{};let r=e.threshold??72,n=null,o=!1,i=!1,c=typeof requestAnimationFrame==="function"?requestAnimationFrame:(a)=>setTimeout(a,16),u=null,s=(a)=>{u=null,e.onPull?.(a,o)},f=(a)=>{let m=u!==null;if(u=a,m)return;c(()=>{if(u!==null)s(u)})},v=(a)=>{let m=a.touches?.[0];return m?m.clientY:null},l=(a)=>{if(i||b(t)>0)return;n=v(a),o=!1},p=(a)=>{if(n===null)return;let m=v(a);if(m===null)return;let x=N(b(t),m-n);if(!o&&x>=r)o=!0,Promise.resolve(C.impact("medium")).catch(()=>{});else if(o&&x<r*0.8)o=!1;f(x)},h=async()=>{if(n===null)return;if(n=null,!o){s(0);return}o=!1,i=!0,s(r);try{await e.onRefresh()}finally{i=!1,s(0)}},d=()=>{if(n===null)return;n=null,o=!1,s(0)};return t.addEventListener("touchstart",l,{passive:!0}),t.addEventListener("touchmove",p,{passive:!0}),t.addEventListener("touchend",h),t.addEventListener("touchcancel",d),()=>{u=null,t.removeEventListener("touchstart",l),t.removeEventListener("touchmove",p),t.removeEventListener("touchend",h),t.removeEventListener("touchcancel",d)}}function L(){if(typeof window>"u")return;return window}function F(e=L()){let t=e?.history?.state;return t!==null&&t!==void 0}function Me(e=L()){if(!F(e))return!1;return e.history.back(),!0}function D(e,t=O()){let r;try{r=new URL(e)}catch{return null}let n=`${r.search}${r.hash}`;if(r.protocol==="http:"||r.protocol==="https:"){if(!t||r.origin!==t)return null;return`${r.pathname||"/"}${n}`}return`${`/${[r.host,r.pathname.replace(/^\/+/,"")].filter(Boolean).join("/")}`}${n}`}function O(){if(typeof window>"u")return;return window.location?.origin}var S="stacks-mobile-deep-link";function Pe(e,t){let r=(n)=>{let o=D(n);if(o)e(o)};return t.getInitialURL().then((n)=>{if(!n)return;try{if(sessionStorage.getItem(S)===n)return;sessionStorage.setItem(S,n)}catch{}r(n)}).catch(()=>{}),t.onLink((n,o)=>{if(o?.initial)return;r(n)})}var He=30;function U(e,t){let r=Math.PI/180,n=(t.latitude-e.latitude)*r,o=(t.longitude-e.longitude)*r,i=Math.sin(n/2)**2+Math.cos(e.latitude*r)*Math.cos(t.latitude*r)*Math.sin(o/2)**2;return 12742017.6*Math.asin(Math.min(1,Math.sqrt(i)))}function B(e,t=25){let r=new Set;return e.filter((n)=>Number.isFinite(n.latitude)&&Number.isFinite(n.longitude)&&Number.isFinite(n.timestamp)&&n.accuracy>=0&&n.accuracy<=t).sort((n,o)=>n.timestamp-o.timestamp).filter((n)=>{if(r.has(n.timestamp))return!1;return r.add(n.timestamp),!0})}function k(e,t={}){let r=B(e,t.maxAccuracyM??25),n=t.paceWindowS??30,o=[],i=0,c=0,u=null;for(let l of r){let p=o[o.length-1],h=u?(l.timestamp-u.timestamp)/1000:0;if(u=l,!p){o.push({fix:l,cumM:0});continue}if(h>30){o.push({fix:l,cumM:i});continue}let d=(l.timestamp-p.fix.timestamp)/1000,a=U(p.fix,l),m=Math.min(10,Math.max(3,p.fix.accuracy,l.accuracy));if(a<m)continue;if(i+=a,d>0&&a/d>=0.5)c+=d;o.push({fix:l,cumM:i})}let s=o[o.length-1],f=null,v=null;if(s){let l=s.fix.timestamp-n*1000,p=o.find((a)=>a.fix.timestamp>=l)??s,h=s.cumM-p.cumM,d=(s.fix.timestamp-p.fix.timestamp)/1000;if(h>=20&&d>0)v=h/d,f=d/(h/1000)}return{distanceM:i,movingS:c,paceSPerKm:f,averagePaceSPerKm:i>=50&&c>0?c/(i/1000):null,speedMps:v,accuracyM:r.length?r[r.length-1].accuracy:e.length?e[e.length-1].accuracy??null:null,fixes:r.length}}function Te(e){if(!e||!Number.isFinite(e)||e>3600)return"–";let t=Math.round(e);return`${Math.floor(t/60)}:${String(t%60).padStart(2,"0")}`}function Ce(e){return e&&Number.isFinite(e)?(e*3.6).toFixed(1):"–"}var M={distanceM:0,movingS:0,paceSPerKm:null,averagePaceSPerKm:null,speedMps:null,accuracyM:null,fixes:0};function Ne(e){let t=e.location,r=e.intervalMs??3000,n=M,o=null,i=()=>typeof document<"u"&&document.visibilityState==="hidden";async function c(){let f=await t.readRecording();return n=k(f,e),e.onUpdate?.(n),n}function u(){s(),o=setInterval(()=>{if(!i())c().catch(()=>{})},r)}function s(){if(o)clearInterval(o);o=null}return{get stats(){return n},async start(){return await t.startRecording({enableHighAccuracy:!0,...e.locationOptions}),n=M,e.onUpdate?.(n),u(),n},async attach(){let f=await t.getRecordingState().catch(()=>null);if(!f?.active)return null;if(await c().catch(()=>{}),!f.paused)u();return f},async pause(){s(),await t.pauseRecording(),await c().catch(()=>{})},async resume(){await t.resumeRecording(),u()},async stop(){s();let v=(await t.stopRecording())?.locations??[];return n=k(v,e),e.onUpdate?.(n),v},refresh:c}}var re=I,oe=q,ie=Y,w=K,ae=W,se=J,ce=V,ue=X,le=ee,fe=te,pe=_;function E(e){if(typeof e==="string")return e.trim()||null;if(!e||typeof e!=="object")return null;let t=e.url;return typeof t==="string"&&t.trim()?t:null}var de={async getInitialURL(){if(!await H())return null;return E(await P.getInitialURL?.())},onLink(e){let t=null,r=!1,n=()=>{if(r)return;t=P.onLink?.((o,i)=>{let c=E(o);if(c)e(c,{initial:i?.initial===!0})})??null};if(y()?.craft)n();else H().then(n);return()=>{r=!0,t?.()}}},me=G,ve=Q,he=Z,ye=j,be=z,we=ne;function y(){if(typeof window>"u")return;return window}function g(){let e=y()?.craft;if(!e||typeof e!=="object")return null;if(!("platform"in e)||e.platform!=="ios"&&e.platform!=="android")return null;let t="capabilities"in e?e.capabilities:void 0,r=t&&typeof t==="object"&&!Array.isArray(t)?Object.fromEntries(Object.entries(t).filter(([,n])=>typeof n==="boolean")):{};return{platform:e.platform,capabilities:r}}function R(){return g()!==null}function T(){if(typeof window>"u")return!1;let e=window;if(e.CraftAndroid)return!0;let t=typeof navigator>"u"?"":navigator.userAgent;return Boolean(e.webkit?.messageHandlers?.craft)&&/iPhone|iPad|iPod/.test(t)}function ge(e=2000){if(R()||T())return Promise.resolve(!0);let t=y(),r=t;if(!t||!(r?.craft||r?.webkit?.messageHandlers?.craft))return Promise.resolve(!1);return new Promise((n)=>{let o=()=>{clearTimeout(i),t.removeEventListener("craftReady",o),n(R())},i=setTimeout(o,e);t.addEventListener("craftReady",o,{once:!0})})}function H(e=15000){let t=y();if(t?.craft)return Promise.resolve(!0);let r=t;if(!t||!(T()||r?.webkit?.messageHandlers?.craft))return Promise.resolve(!1);return new Promise((n)=>{let o=()=>{clearTimeout(i),t.removeEventListener("craftReady",o),n(Boolean(t.craft))},i=setTimeout(o,e);t.addEventListener("craftReady",o)})}function xe(e){let t=y();if(!t)return()=>{};if(g()){let n=!1;return queueMicrotask(()=>{if(!n)e(new Event("craftReady"))}),()=>{n=!0}}let r=(n)=>{if(!g())return;t.removeEventListener("craftReady",r),e(n)};return t.addEventListener("craftReady",r),()=>t.removeEventListener("craftReady",r)}async function Ae(e){await w.impact("light");try{let t=await e();return await w.notification("success"),t}catch(t){throw await w.notification("error"),t}}var Oe={get nativeBridge(){return g()},biometrics:re,camera:oe,device:ie,haptics:w,lifecycle:ae,location:se,notifications:ce,permissions:ue,secureStorage:le,share:fe,appReview:pe,deepLinks:de,keepAwake:me,network:ve,pushNotifications:he,health:ye,liveActivities:be,watchConnectivity:we,isNativeMobile:R,whenNative:ge,onReady:xe,withFeedback:Ae};export{He as ROUTE_GAP_S,pe as appReview,re as biometrics,oe as camera,F as canGoBack,Ne as createRouteRecorder,D as deepLinkPath,de as deepLinks,ie as device,U as fixDistance,Pe as followDeepLinks,g as getNativeMobileBridge,Me as goBack,w as haptics,T as hasNativeMobileHost,ye as health,R as isNativeMobile,me as keepAwake,ae as lifecycle,be as liveActivities,se as location,Oe as mobile,ve as network,E as normalizeDeepLinkURL,ce as notifications,Se as observePageScroll,Le as observePullToRefresh,xe as onMobileReady,Te as paceLabel,b as pageScrollTop,ue as permissions,N as pullDistance,he as pushNotifications,k as routeStats,le as secureStorage,fe as share,Ce as speedLabel,B as usableFixes,we as watchConnectivity,H as whenBridgeReady,ge as whenNativeMobile,Ae as withNativeFeedback};
1
+ import{appReview as mt,biometrics as gt,camera as ht,deepLinks as U,device as yt,haptics as St,health as vt,keepAwake as bt,lifecycle as At,liveActivities as xt,location as wt,network as Rt,notifications as Tt,permissions as Et,pushNotifications as kt,secureStorage as Lt,share as Pt,watchConnectivity as Mt}from"craft-native/mobile";import{haptics as z}from"craft-native/mobile";function M(){if(typeof window>"u")return;return window}function k(t=M()){if(!t)return 0;if(typeof t.scrollY==="number")return t.scrollY;return t.document?.scrollingElement?.scrollTop??0}function Qt(t,n=M()){if(!n)return()=>{};let r=!1,e=typeof requestAnimationFrame==="function"?requestAnimationFrame:(i)=>setTimeout(i,16),o=()=>{if(r)return;r=!0,e(()=>{r=!1,t(k(n))})};return t(k(n)),n.addEventListener("scroll",o,{passive:!0}),()=>n.removeEventListener("scroll",o)}function J(t,n){let r=Math.max(0,-t),e=t<=0?Math.max(0,n)*0.5:0;return Math.round(Math.max(r,e))}function Zt(t){let n=t.host??M();if(!n)return()=>{};let r=t.threshold??72,e=null,o=!1,i=!1,s=typeof requestAnimationFrame==="function"?requestAnimationFrame:(g)=>setTimeout(g,16),l=null,a=(g)=>{l=null,t.onPull?.(g,o)},u=(g)=>{let b=l!==null;if(l=g,b)return;s(()=>{if(l!==null)a(l)})},h=(g)=>{let b=g.touches?.[0];return b?b.clientY:null},d=(g)=>{if(i||k(n)>0)return;e=h(g),o=!1},p=(g)=>{if(e===null)return;let b=h(g);if(b===null)return;let x=J(k(n),b-e);if(!o&&x>=r)o=!0,Promise.resolve(z.impact("medium")).catch(()=>{});else if(o&&x<r*0.8)o=!1;u(x)},v=async()=>{if(e===null)return;if(e=null,!o){a(0);return}o=!1,i=!0,a(r);try{await t.onRefresh()}finally{i=!1,a(0)}},m=()=>{if(e===null)return;e=null,o=!1,a(0)};return n.addEventListener("touchstart",d,{passive:!0}),n.addEventListener("touchmove",p,{passive:!0}),n.addEventListener("touchend",v),n.addEventListener("touchcancel",m),()=>{l=null,n.removeEventListener("touchstart",d),n.removeEventListener("touchmove",p),n.removeEventListener("touchend",v),n.removeEventListener("touchcancel",m)}}function O(){if(typeof window>"u")return;return window}function V(t=O()){let n=t?.history?.state;return n!==null&&n!==void 0}function ee(t=O()){if(!V(t))return!1;return t.history.back(),!0}function Q(t,n=Z()){let r;try{r=new URL(t)}catch{return null}let e=`${r.search}${r.hash}`;if(r.protocol==="http:"||r.protocol==="https:"){if(!n||r.origin!==n)return null;return`${r.pathname||"/"}${e}`}return`${`/${[r.host,r.pathname.replace(/^\/+/,"")].filter(Boolean).join("/")}`}${e}`}function Z(){if(typeof window>"u")return;return window.location?.origin}var _="stacks-mobile-deep-link";function ne(t,n){let r=(e)=>{let o=Q(e);if(o)t(o)};return n.getInitialURL().then((e)=>{if(!e)return;try{if(sessionStorage.getItem(_)===e)return;sessionStorage.setItem(_,e)}catch{}r(e)}).catch(()=>{}),n.onLink((e,o)=>{if(o?.initial)return;r(e)})}var oe=30;function tt(t,n){let r=Math.PI/180,e=(n.latitude-t.latitude)*r,o=(n.longitude-t.longitude)*r,i=Math.sin(e/2)**2+Math.cos(t.latitude*r)*Math.cos(n.latitude*r)*Math.sin(o/2)**2;return 12742017.6*Math.asin(Math.min(1,Math.sqrt(i)))}function et(t,n=25){let r=new Set;return t.filter((e)=>Number.isFinite(e.latitude)&&Number.isFinite(e.longitude)&&Number.isFinite(e.timestamp)&&e.accuracy>=0&&e.accuracy<=n).sort((e,o)=>e.timestamp-o.timestamp).filter((e)=>{if(r.has(e.timestamp))return!1;return r.add(e.timestamp),!0})}function H(t,n={}){let r=et(t,n.maxAccuracyM??25),e=n.paceWindowS??30,o=[],i=0,s=0,l=null;for(let d of r){let p=o[o.length-1],v=l?(d.timestamp-l.timestamp)/1000:0;if(l=d,!p){o.push({fix:d,cumM:0});continue}if(v>30){o.push({fix:d,cumM:i});continue}let m=(d.timestamp-p.fix.timestamp)/1000,g=tt(p.fix,d),b=Math.min(10,Math.max(3,p.fix.accuracy,d.accuracy));if(g<b)continue;if(i+=g,m>0&&g/m>=0.5)s+=m;o.push({fix:d,cumM:i})}let a=o[o.length-1],u=null,h=null;if(a){let d=a.fix.timestamp-e*1000,p=o.find((g)=>g.fix.timestamp>=d)??a,v=a.cumM-p.cumM,m=(a.fix.timestamp-p.fix.timestamp)/1000;if(v>=20&&m>0)h=v/m,u=m/(v/1000)}return{distanceM:i,movingS:s,paceSPerKm:u,averagePaceSPerKm:i>=50&&s>0?s/(i/1000):null,speedMps:h,accuracyM:r.length?r[r.length-1].accuracy:t.length?t[t.length-1].accuracy??null:null,fixes:r.length}}function ie(t){if(!t||!Number.isFinite(t)||t>3600)return"–";let n=Math.round(t);return`${Math.floor(n/60)}:${String(n%60).padStart(2,"0")}`}function se(t){return t&&Number.isFinite(t)?(t*3.6).toFixed(1):"–"}var K={distanceM:0,movingS:0,paceSPerKm:null,averagePaceSPerKm:null,speedMps:null,accuracyM:null,fixes:0};function ae(t){let n=t.location,r=t.intervalMs??3000,e=K,o=null,i=()=>typeof document<"u"&&document.visibilityState==="hidden";async function s(){let u=await n.readRecording();return e=H(u,t),t.onUpdate?.(e),e}function l(){a(),o=setInterval(()=>{if(!i())s().catch(()=>{})},r)}function a(){if(o)clearInterval(o);o=null}return{get stats(){return e},async start(){return await n.startRecording({enableHighAccuracy:!0,...t.locationOptions}),e=K,t.onUpdate?.(e),l(),e},async attach(){let u=await n.getRecordingState().catch(()=>null);if(!u?.active)return null;if(await s().catch(()=>{}),!u.paused)l();return u},async pause(){a(),await n.pauseRecording(),await s().catch(()=>{})},async resume(){await n.resumeRecording(),l()},async stop(){a();let h=(await n.stopRecording())?.locations??[];return e=H(h,t),t.onUpdate?.(e),h},refresh:s}}var ce=64;var nt=/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/,rt=/^(?:0|[1-9]\d*)$/;function F(t){let n=[],r=[],e=t&&typeof t==="object"&&!Array.isArray(t)?t:{};for(let[o,i]of Object.entries(e)){let s=i&&typeof i==="object"?i:null;if(!s){r.push(`spotlight kind "${o}" is not an object`);continue}let{slots:l,route:a}=s,u=typeof s.noun==="string"&&s.noun.trim()?s.noun.trim():"Item";if(!nt.test(o)||o.includes("-slot-")){r.push(`spotlight kind "${o}" is not a usable name: lowercase letters, digits and single dashes, and never "-slot-"`);continue}if(typeof l!=="number"||!Number.isInteger(l)||l<=0||l>64){r.push(`spotlight kind "${o}" needs slots between 1 and 64, not ${String(l)}`);continue}if(typeof a!=="string"||!a.startsWith("/")||!a.includes(":id")){r.push(`spotlight kind "${o}" needs a route starting with "/" and carrying ":id", not ${String(a)}`);continue}n.push({name:o,slots:l,route:a,noun:u})}return{kinds:n,problems:r}}function T(t,n){return t.find((r)=>r.name===n)??null}function R(t,n,r){let e=T(t,n);if(!e||!Number.isInteger(r)||r<0||r>=e.slots)return null;return`${n}-slot-${r}`}function ot(t,n){if(typeof n!=="string")return null;let r=n.lastIndexOf("-slot-");if(r<=0)return null;let e=n.slice(0,r),o=n.slice(r+6);if(!rt.test(o))return null;let i=Number(o);return R(t,e,i)===n?{kind:e,slot:i}:null}function C(t){return t.flatMap((n)=>Array.from({length:n.slots},(r,e)=>`${n.name}-slot-${e}`))}function ue(t,n){let r=t&&typeof t==="object"?t:{};if(r.enabled===!1)return[];let o=[...(Array.isArray(r.activityTypes)?r.activityTypes.filter((i)=>typeof i==="string"&&i.trim().length>0):[]).map((i)=>i.trim()),...C(F(r.kinds).kinds)];return[...new Set(o)].map((i)=>`${n}.${i}`)}function D(t,n,r){let e=typeof r.name==="string"?r.name.replace(/\s+/g," ").trim():"";if(!e)return`${T(t,n)?.noun??"Item"} #${r.id}`;return e.length>120?`${e.slice(0,119).trimEnd()}…`:e}function it(t,n,r,e,o=Date.now()){let i=T(t,r);if(!i||!Number.isInteger(e.id)||e.id<=0)return null;let s=n.filter((m)=>m.kind===r),l=D(t,r,e),a=s.find((m)=>m.itemId===e.id)??null,u=a?null:ct(s,i.slots),h=a||u!==null?null:ut(s),d=a?.slot??u??h?.slot;if(d===void 0||d===null)return null;let p={kind:r,slot:d,itemId:e.id,title:l,donatedAt:o},v=n.filter((m)=>m.kind!==r||m.slot!==d&&m.itemId!==e.id);return{entries:B(t,[...v,p]),entry:p,evicted:h,changed:!a||a.title!==l}}function st(t,n,r){let e=t.find((o)=>o.kind===n&&o.itemId===r)??null;return{entries:e?t.filter((o)=>o!==e):[...t],removed:e}}function at(t,n,r){let e=ot(t,r);if(!e)return null;let o=n.find((s)=>s.kind===e.kind&&s.slot===e.slot),i=T(t,e.kind)?.route;return o&&i?i.replace(":id",String(o.itemId)):null}function lt(t,n){if(!Array.isArray(n))return[];let r=new Map,e=new Set;for(let o of n){if(!o||typeof o!=="object")continue;let i=o,{kind:s,slot:l,itemId:a,title:u,donatedAt:h}=i;if(typeof s!=="string"||typeof l!=="number"||R(t,s,l)===null)continue;if(typeof a!=="number"||!Number.isInteger(a)||a<=0)continue;let d=`${s}-slot-${l}`,p=`${s}#${a}`;if(r.has(d)||e.has(p))continue;r.set(d,{kind:s,slot:l,itemId:a,title:typeof u==="string"&&u.trim()?u:D(t,s,{id:a}),donatedAt:typeof h==="number"&&Number.isFinite(h)&&h>0?h:0}),e.add(p)}return B(t,[...r.values()])}function B(t,n){let r=new Map(t.map((e,o)=>[e.name,o]));return[...n].sort((e,o)=>(r.get(e.kind)??0)-(r.get(o.kind)??0)||e.slot-o.slot)}function ct(t,n){let r=new Set(t.map((e)=>e.slot));for(let e=0;e<n;e++)if(!r.has(e))return e;return null}function ut(t){return t.reduce((n,r)=>!n||r.donatedAt<n.donatedAt?r:n,null)}function dt(){let t=globalThis.craft;return t&&typeof t==="object"?t.siri:void 0}var ft="stacks_spotlight_index",pt=3000;function de(t){let n=t.onProblem??((c)=>console.warn(`[spotlight] ${c}`)),r=Array.isArray(t.kinds)?{kinds:[...t.kinds],problems:[]}:F(t.kinds);for(let c of r.problems)n(c);let e=t.enabled===!1?[]:r.kinds,o=t.storageKey??ft,i=t.legacyStorageKeys??[],s=t.timeoutMs??pt,l=t.now??(()=>Date.now()),a=t.bridge??dt,u=null;function h(){try{return typeof localStorage>"u"?null:localStorage}catch{return null}}function d(){if(u)return u;u=[];let c=h();if(!c)return u;try{let f=[o,...i].map((y)=>c.getItem(y)).find((y)=>y);u=f?lt(e,JSON.parse(f)):[]}catch{u=[]}return u}function p(c){u=c;let f=h();if(!f)return;try{f.setItem(o,JSON.stringify(c));for(let y of i)f.removeItem(y)}catch{}}async function v(c){if(!c)return null;let f;try{return await Promise.race([c,new Promise((y)=>{f=setTimeout(()=>y(null),s)})])}catch{return null}finally{if(f!==void 0)clearTimeout(f)}}let m=Promise.resolve();function g(c){let f=m.then(c,c);return m=f.catch(()=>{return}),f}async function b(c,f){let y=a()?.register;if(!y||e.length===0)return!1;return g(async()=>{let S=it(e,d(),c,f,l()),A=S&&R(e,c,S.entry.slot);if(!S||!A)return!1;if(!S.changed)return p(S.entries),!0;let w=await v(y(S.entry.title,A))!==null;if(w)p(S.entries);return w})}async function x(c,f){let y=a()?.remove;if(!y)return!1;return g(async()=>{let{entries:S,removed:A}=st(d(),c,f);if(!A)return!1;let w=R(e,c,A.slot),X=w?await v(y(w))!==null:!1;return p(S),X})}async function Y(c,f){let y=T(e,c)?.slots;if(!y||!a()?.register)return 0;let S=0;for(let A of[...f].slice(0,y).reverse())if(await b(c,A))S++;return S}async function W(){let c=a()?.remove;if(!c)return p([]),0;return g(async()=>{let f=0;for(let y of d()){let S=R(e,y.kind,y.slot);if(S&&await v(c(S))!==null)f++}return p([]),f})}return{kinds:e,index:b,sync:Y,remove:x,clear:W,routeFor:(c)=>at(e,d(),c),entries:()=>d(),activityTypes:(c)=>C(e).map((f)=>`${c}.${f}`),reset:()=>{u=null}}}var N=["craftShortcut","craftSiriShortcut"];function fe(t,n){let r=(e)=>{let o=e.detail,i=typeof o==="string"?o:o&&typeof o==="object"?o.action??o.type:null,s=typeof i==="string"?t.routeFor(i):null;if(s)n(s)};if(typeof globalThis.addEventListener!=="function")return()=>{};for(let e of N)globalThis.addEventListener(e,r);return()=>{for(let e of N)globalThis.removeEventListener(e,r)}}var It=gt,_t=ht,Ot=yt,L=St,Ht=At,Kt=wt,Nt=Tt,$t=Et,Ft=Lt,Ct=Pt,Dt=mt;function j(t){if(typeof t==="string")return t.trim()||null;if(!t||typeof t!=="object")return null;let n=t.url;return typeof n==="string"&&n.trim()?n:null}var Bt={async getInitialURL(){if(!await G())return null;return j(await U.getInitialURL?.())},onLink(t){let n=null,r=!1,e=()=>{if(r)return;n=U.onLink?.((o,i)=>{let s=j(o);if(s)t(s,{initial:i?.initial===!0})})??null};if(E()?.craft)e();else G().then(e);return()=>{r=!0,n?.()}}},Ut=bt,jt=Rt,Gt=kt,qt=vt,Yt=xt,Wt=Mt;function E(){if(typeof window>"u")return;return window}function P(){let t=E()?.craft;if(!t||typeof t!=="object")return null;if(!("platform"in t)||t.platform!=="ios"&&t.platform!=="android")return null;let n="capabilities"in t?t.capabilities:void 0,r=n&&typeof n==="object"&&!Array.isArray(n)?Object.fromEntries(Object.entries(n).filter(([,e])=>typeof e==="boolean")):{};return{platform:t.platform,capabilities:r}}function I(){return P()!==null}function q(){if(typeof window>"u")return!1;let t=window;if(t.CraftAndroid)return!0;let n=typeof navigator>"u"?"":navigator.userAgent;return Boolean(t.webkit?.messageHandlers?.craft)&&/iPhone|iPad|iPod/.test(n)}function Xt(t=2000){if(I()||q())return Promise.resolve(!0);let n=E(),r=n;if(!n||!(r?.craft||r?.webkit?.messageHandlers?.craft))return Promise.resolve(!1);return new Promise((e)=>{let o=()=>{clearTimeout(i),n.removeEventListener("craftReady",o),e(I())},i=setTimeout(o,t);n.addEventListener("craftReady",o,{once:!0})})}function G(t=15000){let n=E();if(n?.craft)return Promise.resolve(!0);let r=n;if(!n||!(q()||r?.webkit?.messageHandlers?.craft))return Promise.resolve(!1);return new Promise((e)=>{let o=()=>{clearTimeout(i),n.removeEventListener("craftReady",o),e(Boolean(n.craft))},i=setTimeout(o,t);n.addEventListener("craftReady",o)})}function zt(t){let n=E();if(!n)return()=>{};if(P()){let e=!1;return queueMicrotask(()=>{if(!e)t(new Event("craftReady"))}),()=>{e=!0}}let r=(e)=>{if(!P())return;n.removeEventListener("craftReady",r),t(e)};return n.addEventListener("craftReady",r),()=>n.removeEventListener("craftReady",r)}async function Jt(t){await L.impact("light");try{let n=await t();return await L.notification("success"),n}catch(n){throw await L.notification("error"),n}}var ge={get nativeBridge(){return P()},biometrics:It,camera:_t,device:Ot,haptics:L,lifecycle:Ht,location:Kt,notifications:Nt,permissions:$t,secureStorage:Ft,share:Ct,appReview:Dt,deepLinks:Bt,keepAwake:Ut,network:jt,pushNotifications:Gt,health:qt,liveActivities:Yt,watchConnectivity:Wt,isNativeMobile:I,whenNative:Xt,onReady:zt,withFeedback:Jt};export{ce as MAX_SPOTLIGHT_SLOTS_PER_KIND,oe as ROUTE_GAP_S,Dt as appReview,It as biometrics,_t as camera,V as canGoBack,ae as createRouteRecorder,de as createSpotlightIndex,Q as deepLinkPath,Bt as deepLinks,Ot as device,tt as fixDistance,ne as followDeepLinks,P as getNativeMobileBridge,ee as goBack,L as haptics,q as hasNativeMobileHost,qt as health,I as isNativeMobile,Ut as keepAwake,Ht as lifecycle,Yt as liveActivities,Kt as location,ge as mobile,jt as network,j as normalizeDeepLinkURL,Nt as notifications,Qt as observePageScroll,Zt as observePullToRefresh,zt as onMobileReady,fe as onSpotlightTap,ie as paceLabel,k as pageScrollTop,$t as permissions,it as placeSpotlightItem,J as pullDistance,Gt as pushNotifications,lt as readSpotlightEntries,F as readSpotlightKinds,st as removeSpotlightItem,H as routeStats,Ft as secureStorage,Ct as share,se as speedLabel,R as spotlightAction,C as spotlightActions,ue as spotlightActivityTypes,dt as spotlightBridge,T as spotlightKind,at as spotlightRoute,ot as spotlightSlot,D as spotlightTitle,et as usableFixes,Wt as watchConnectivity,G as whenBridgeReady,Xt as whenNativeMobile,Jt as withNativeFeedback};
@@ -0,0 +1,196 @@
1
+ /**
2
+ * Read configured kinds, leaving out any that could not work.
3
+ *
4
+ * Dropping rather than throwing: a typo in one kind's budget should cost that
5
+ * kind its index, not the app's startup. The problems come back so a caller
6
+ * can say what it ignored — silence here looks exactly like a kind nothing has
7
+ * donated to yet.
8
+ */
9
+ export declare function readSpotlightKinds(raw: unknown): SpotlightKindsResult;
10
+ /** One kind by name, or null for anything this build does not index. */
11
+ export declare function spotlightKind(kinds: readonly SpotlightKind[], name: string): SpotlightKind | null;
12
+ /** The action a kind's slot is donated under, or null for no such slot. */
13
+ export declare function spotlightAction(kinds: readonly SpotlightKind[], kind: string, slot: number): string | null;
14
+ /**
15
+ * The kind and slot an action names, or null for anything else.
16
+ *
17
+ * Strict about the form — no padding, no trailing text, and the kind has to be
18
+ * one these kinds carry — because this is what decides whether something the
19
+ * system handed back is one of the app's own.
20
+ */
21
+ export declare function spotlightSlot(kinds: readonly SpotlightKind[], action: string): { kind: string, slot: number } | null;
22
+ /** Every action these kinds may be donated under, in configured order. */
23
+ export declare function spotlightActions(kinds: readonly SpotlightKind[]): string[];
24
+ /**
25
+ * Every activity type a build has to declare, for an `Info.plist`.
26
+ *
27
+ * The slots of every configured kind, plus whatever the app donates outside
28
+ * the registry (`activityTypes`: a Siri phrase, an App Intent), each prefixed
29
+ * with the bundle id the way Craft types the activity it donates.
30
+ */
31
+ export declare function spotlightActivityTypes(config: unknown, bundleId: string): string[];
32
+ /**
33
+ * What the device shows for a record.
34
+ *
35
+ * The record's name alone: Craft hands the same string to Spotlight as the
36
+ * activity's title and to Siri as its invocation phrase, and a name is the one
37
+ * form that reads well in both — it is what somebody types looking for the
38
+ * record, and what they could say.
39
+ */
40
+ export declare function spotlightTitle(kinds: readonly SpotlightKind[], kind: string, item: SpotlightItem): string;
41
+ /**
42
+ * Put a record in its kind's slots, taking the oldest when every one is taken.
43
+ *
44
+ * Returns the whole next list rather than mutating, so a caller stores exactly
45
+ * what it donated: a failure between the two cannot leave bookkeeping that
46
+ * claims an entry the device does not have.
47
+ */
48
+ export declare function placeSpotlightItem(kinds: readonly SpotlightKind[], entries: readonly SpotlightEntry[], kind: string, item: SpotlightItem, now?: number): SpotlightPlacement | null;
49
+ /** Take a record out — what it held is the slot to un-donate. */
50
+ export declare function removeSpotlightItem(entries: readonly SpotlightEntry[], kind: string, itemId: number): { entries: SpotlightEntry[], removed: SpotlightEntry | null };
51
+ /** The route a tapped entry means, or null for a slot holding nothing. */
52
+ export declare function spotlightRoute(kinds: readonly SpotlightKind[], entries: readonly SpotlightEntry[], action: string): string | null;
53
+ /**
54
+ * Read back a stored assignment.
55
+ *
56
+ * Stored state outlives the build that wrote it — a kind can be renamed and a
57
+ * budget can shrink — so anything that is not a slot these kinds declare is
58
+ * dropped rather than trusted: a tap on it would resolve to a record the
59
+ * `Info.plist` no longer lets through.
60
+ */
61
+ export declare function readSpotlightEntries(kinds: readonly SpotlightKind[], raw: unknown): SpotlightEntry[];
62
+ /** The host's Siri bridge, or undefined anywhere that is not a native build. */
63
+ export declare function spotlightBridge(): SpotlightBridge | undefined;
64
+ export declare function createSpotlightIndex(options: SpotlightIndexOptions): SpotlightIndex;
65
+ /**
66
+ * Follow a tap on an indexed entry.
67
+ *
68
+ * Craft reports one as a `craftSiriShortcut` event carrying `{action}` (a
69
+ * home-screen quick action arrives as `craftShortcut` with `{type}`, handled
70
+ * here too), and the handler is called with the route the action resolves to —
71
+ * anything this index does not recognise is left alone, so an app with its own
72
+ * shortcut actions can keep handling those itself.
73
+ *
74
+ * These are plain window events, listened for directly rather than through
75
+ * Craft's `shortcuts.onShortcut` and `siri.onInvoke`: both of those are
76
+ * themselves listeners on these same events, so going through them would run
77
+ * every tap twice and leave a subscription their wrappers cannot take back
78
+ * down.
79
+ */
80
+ export declare function onSpotlightTap(index: SpotlightIndex, handler: (route: string) => void): () => void;
81
+ /**
82
+ * The most slots one kind may claim.
83
+ *
84
+ * Every slot is a line in the generated `Info.plist` and a possible donation
85
+ * at launch, so a mistyped budget is worth catching before a build log.
86
+ */
87
+ export declare const MAX_SPOTLIGHT_SLOTS_PER_KIND: 64;
88
+ /**
89
+ * The app's own content in a device's search index.
90
+ *
91
+ * An app somebody opens is not the same thing as a record somebody finds: a
92
+ * trail, a club, a document that turns up when its name is typed into the home
93
+ * screen is reached without the app being thought of first. iOS builds that
94
+ * index out of donated `NSUserActivity` objects, which Craft's `siri` bridge
95
+ * is: a title and an action, and what the app gets back on a tap is that
96
+ * action and nothing else. Two consequences shape this whole module:
97
+ *
98
+ * 1. iOS only hands a tapped activity back to an app that declares its type
99
+ * in `NSUserActivityTypes`, and that list is fixed at build time. A type
100
+ * per record id would be unbounded, so it cannot be declared — and an
101
+ * entry iOS will not hand back is worse than no entry, because it opens
102
+ * the app on whatever screen it was last on.
103
+ * 2. The activity carries no payload of the app's. Whatever a tap needs to
104
+ * know has to be derivable from the action alone.
105
+ *
106
+ * So each kind of content gets a fixed number of *slots* — `trail-slot-0`,
107
+ * `club-slot-0`, every one of them declared in the generated `Info.plist` by
108
+ * the iOS build — and each slot holds whichever record is currently in it.
109
+ * This module is that assignment: the pure part decides it, and
110
+ * `createSpotlightIndex` stores it and makes the bridge calls. Because a
111
+ * slot's identifier is also the activity's `persistentIdentifier`, re-donating
112
+ * a slot replaces the entry iOS already has rather than adding another.
113
+ *
114
+ * What is indexed, how many of each, and where a tap opens is configuration
115
+ * (`config/mobile.ts`, `spotlight`), so an app adds a kind with an entry and a
116
+ * call rather than with another copy of the bookkeeping below.
117
+ *
118
+ * Nothing here needs a native host: off one, every call is a no-op that says
119
+ * so, which is what lets a page donate unconditionally.
120
+ */
121
+ /** One kind of content, as the index holds it. */
122
+ export declare interface SpotlightKind {
123
+ name: string
124
+ slots: number
125
+ route: string
126
+ noun: string
127
+ }
128
+ /** A kind as an app configures it, before it is checked. */
129
+ export declare interface SpotlightKindInput {
130
+ slots: number
131
+ route: string
132
+ noun?: string
133
+ }
134
+ /** What this module needs of a record. A list row and a detail row both fit. */
135
+ export declare interface SpotlightItem {
136
+ id: number
137
+ name?: string | null
138
+ }
139
+ /** One record's place in the index. */
140
+ export declare interface SpotlightEntry {
141
+ kind: string
142
+ slot: number
143
+ itemId: number
144
+ title: string
145
+ donatedAt: number
146
+ }
147
+ /** Configured kinds, and what was wrong with the ones left out. */
148
+ export declare interface SpotlightKindsResult {
149
+ kinds: SpotlightKind[]
150
+ problems: string[]
151
+ }
152
+ /** The result of fitting a record into its kind's slots. */
153
+ export declare interface SpotlightPlacement {
154
+ entries: SpotlightEntry[]
155
+ entry: SpotlightEntry
156
+ evicted: SpotlightEntry | null
157
+ changed: boolean
158
+ }
159
+ /**
160
+ * Craft's Siri calls, which are also its Spotlight calls.
161
+ *
162
+ * `register` builds an `NSUserActivity` typed `<bundle id>.<action>`, titles it
163
+ * with the phrase, marks it eligible for search and prediction and makes it
164
+ * current — so a registered phrase is both a Siri suggestion and a Spotlight
165
+ * entry — and `remove` deletes the saved activity by that same identifier.
166
+ *
167
+ * Typed here because `craft-native`'s browser SDK exports neither: they exist
168
+ * only on the object Craft injects into the WebView, so without this seam every
169
+ * caller reaches into `globalThis.craft` and guesses at shapes.
170
+ */
171
+ export declare interface SpotlightBridge {
172
+ register?: (phrase: string, action: string) => Promise<unknown>
173
+ remove?: (action: string) => Promise<unknown>
174
+ }
175
+ export declare interface SpotlightIndexOptions {
176
+ kinds: Record<string, SpotlightKindInput> | readonly SpotlightKind[]
177
+ enabled?: boolean
178
+ storageKey?: string
179
+ legacyStorageKeys?: readonly string[]
180
+ timeoutMs?: number
181
+ bridge?: () => SpotlightBridge | undefined
182
+ now?: () => number
183
+ onProblem?: (message: string) => void
184
+ }
185
+ /** The device's index, as an app drives it. */
186
+ export declare interface SpotlightIndex {
187
+ readonly kinds: readonly SpotlightKind[]
188
+ index: (kind: string, item: SpotlightItem) => Promise<boolean>
189
+ sync: (kind: string, items: readonly SpotlightItem[]) => Promise<number>
190
+ remove: (kind: string, itemId: number) => Promise<boolean>
191
+ clear: () => Promise<number>
192
+ routeFor: (action: string) => string | null
193
+ entries: () => readonly SpotlightEntry[]
194
+ activityTypes: (bundleId: string) => string[]
195
+ reset: () => void
196
+ }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/mobile",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.55",
5
+ "version": "0.75.57",
6
6
  "description": "Craft-powered native mobile runtime for Stacks and STX applications",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",
@@ -32,10 +32,10 @@
32
32
  "test": "bun test"
33
33
  },
34
34
  "dependencies": {
35
- "craft-native": ">=0.0.107"
35
+ "craft-native": ">=0.0.111"
36
36
  },
37
37
  "devDependencies": {
38
- "@stacksjs/build": "0.75.55",
38
+ "@stacksjs/build": "0.75.57",
39
39
  "better-dx": "^0.2.24"
40
40
  }
41
41
  }