@wireai/activation 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/AGENTS.md +73 -0
  2. package/CHANGELOG.md +572 -0
  3. package/INTEGRATION_PROMPT.md +55 -0
  4. package/LICENSE +21 -0
  5. package/README.md +923 -0
  6. package/dist/CenteredModal-C3qQBHsA.d.mts +38 -0
  7. package/dist/CenteredModal-Cdgns6--.d.ts +38 -0
  8. package/dist/coachmarks/index.d.mts +352 -0
  9. package/dist/coachmarks/index.d.ts +352 -0
  10. package/dist/coachmarks/index.js +944 -0
  11. package/dist/coachmarks/index.js.map +1 -0
  12. package/dist/coachmarks/index.mjs +918 -0
  13. package/dist/coachmarks/index.mjs.map +1 -0
  14. package/dist/decision-CFvGY6nP.d.ts +228 -0
  15. package/dist/decision-Cl8OFYzu.d.mts +228 -0
  16. package/dist/index.d.mts +1390 -0
  17. package/dist/index.d.ts +1390 -0
  18. package/dist/index.js +3260 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/index.mjs +3186 -0
  21. package/dist/index.mjs.map +1 -0
  22. package/dist/questionnaire/index.d.mts +249 -0
  23. package/dist/questionnaire/index.d.ts +249 -0
  24. package/dist/questionnaire/index.js +922 -0
  25. package/dist/questionnaire/index.js.map +1 -0
  26. package/dist/questionnaire/index.mjs +906 -0
  27. package/dist/questionnaire/index.mjs.map +1 -0
  28. package/dist/reviews/index.d.mts +213 -0
  29. package/dist/reviews/index.d.ts +213 -0
  30. package/dist/reviews/index.js +1001 -0
  31. package/dist/reviews/index.js.map +1 -0
  32. package/dist/reviews/index.mjs +971 -0
  33. package/dist/reviews/index.mjs.map +1 -0
  34. package/dist/showcase/index.d.mts +119 -0
  35. package/dist/showcase/index.d.ts +119 -0
  36. package/dist/showcase/index.js +660 -0
  37. package/dist/showcase/index.js.map +1 -0
  38. package/dist/showcase/index.mjs +651 -0
  39. package/dist/showcase/index.mjs.map +1 -0
  40. package/dist/types-BKfpdZzX.d.mts +84 -0
  41. package/dist/types-BKfpdZzX.d.ts +84 -0
  42. package/dist/types-CMuOexw0.d.mts +116 -0
  43. package/dist/types-CMuOexw0.d.ts +116 -0
  44. package/dist/types-GL_hQ0TN.d.mts +46 -0
  45. package/dist/types-GL_hQ0TN.d.ts +46 -0
  46. package/llms.txt +36 -0
  47. package/metro/index.d.ts +32 -0
  48. package/metro/index.js +83 -0
  49. package/package.json +171 -0
  50. package/src/DemoOnboarding.tsx +133 -0
  51. package/src/OnboardingFlow.tsx +504 -0
  52. package/src/WireOnboarding.tsx +248 -0
  53. package/src/analytics/analyticsEvent.ts +69 -0
  54. package/src/analytics/reportClientEvent.ts +119 -0
  55. package/src/analytics/sendPreview.ts +73 -0
  56. package/src/attribution/attribution.ts +46 -0
  57. package/src/cards/ChipSelectCard.tsx +265 -0
  58. package/src/cards/InterstitialCard.tsx +299 -0
  59. package/src/cards/NumberStepperCard.tsx +121 -0
  60. package/src/cards/SelectionCard.tsx +270 -0
  61. package/src/cards/StatusCard.tsx +170 -0
  62. package/src/cards/TextInputCard.tsx +147 -0
  63. package/src/cards/index.ts +23 -0
  64. package/src/coachmarks/CoachmarkOverlayHost.tsx +44 -0
  65. package/src/coachmarks/CoachmarkProvider.tsx +108 -0
  66. package/src/coachmarks/GestureHint.tsx +331 -0
  67. package/src/coachmarks/SpotlightOverlay.tsx +254 -0
  68. package/src/coachmarks/coachmarkAnchorRegistry.ts +70 -0
  69. package/src/coachmarks/coachmarkOverlayStore.ts +63 -0
  70. package/src/coachmarks/index.ts +62 -0
  71. package/src/coachmarks/runtime.ts +103 -0
  72. package/src/coachmarks/selectTourSteps.ts +45 -0
  73. package/src/coachmarks/types.ts +58 -0
  74. package/src/coachmarks/useCoachmarkAnchor.ts +49 -0
  75. package/src/coachmarks/useCoachmarkTour.ts +200 -0
  76. package/src/components/AnimatedSparkle.tsx +101 -0
  77. package/src/components/Button.tsx +129 -0
  78. package/src/components/CardHandoff.tsx +212 -0
  79. package/src/components/CardLayout.tsx +103 -0
  80. package/src/components/CenteredModal.tsx +184 -0
  81. package/src/components/CompletionView.tsx +186 -0
  82. package/src/components/DoneBlock.tsx +37 -0
  83. package/src/components/ErrorBlock.tsx +42 -0
  84. package/src/components/Illustration.tsx +36 -0
  85. package/src/components/LoadingBlock.tsx +182 -0
  86. package/src/components/LoadingScreen.tsx +35 -0
  87. package/src/components/OnboardingScaffold.tsx +105 -0
  88. package/src/components/StepProgress.tsx +85 -0
  89. package/src/components/loaderChrome.ts +28 -0
  90. package/src/config/onboardingFlag.ts +39 -0
  91. package/src/config/wireConfigFromEnv.ts +52 -0
  92. package/src/device/deviceContext.ts +158 -0
  93. package/src/features/WireFeaturesProvider.tsx +81 -0
  94. package/src/features/cache.ts +92 -0
  95. package/src/features/defaults.ts +67 -0
  96. package/src/features/fetchWireFeatures.ts +86 -0
  97. package/src/features/index.ts +39 -0
  98. package/src/features/types.ts +61 -0
  99. package/src/features/useWireFeatures.ts +57 -0
  100. package/src/identity/userIdentity.ts +100 -0
  101. package/src/illustrations/defaultIllustrations.tsx +99 -0
  102. package/src/index.ts +159 -0
  103. package/src/motion/centeredModalMotion.ts +45 -0
  104. package/src/motion/motionSpec.ts +133 -0
  105. package/src/motion/useReducedMotion.ts +43 -0
  106. package/src/questionnaire/QuestionnaireGate.tsx +235 -0
  107. package/src/questionnaire/decision.ts +63 -0
  108. package/src/questionnaire/index.ts +54 -0
  109. package/src/questionnaire/runtime.ts +20 -0
  110. package/src/questionnaire/transport.ts +83 -0
  111. package/src/questionnaire/types.ts +182 -0
  112. package/src/questionnaire/useQuestionnaireGate.ts +159 -0
  113. package/src/reviews/ReviewGate.tsx +316 -0
  114. package/src/reviews/ReviewModal.tsx +13 -0
  115. package/src/reviews/decision.ts +120 -0
  116. package/src/reviews/equality.ts +54 -0
  117. package/src/reviews/expo-store-review.d.ts +16 -0
  118. package/src/reviews/feedbackForm.ts +41 -0
  119. package/src/reviews/index.ts +78 -0
  120. package/src/reviews/presentation.ts +29 -0
  121. package/src/reviews/runtime.ts +45 -0
  122. package/src/reviews/storeReview.ts +62 -0
  123. package/src/reviews/transport.ts +89 -0
  124. package/src/reviews/types.ts +172 -0
  125. package/src/reviews/useReviewGate.ts +137 -0
  126. package/src/session/persistedSession.ts +143 -0
  127. package/src/session-analytics/index.ts +18 -0
  128. package/src/session-analytics/reportSessionStart.ts +144 -0
  129. package/src/session-analytics/useSessionStart.ts +114 -0
  130. package/src/showcase/FeatureShowcase.tsx +233 -0
  131. package/src/showcase/index.ts +19 -0
  132. package/src/showcase/selectShowcaseSlides.ts +45 -0
  133. package/src/showcase/showcaseColors.ts +91 -0
  134. package/src/showcase/types.ts +74 -0
  135. package/src/theme/ThemeContext.tsx +38 -0
  136. package/src/theme/defaultTheme.ts +70 -0
  137. package/src/theme/themeFromBrand.ts +93 -0
  138. package/src/theme/types.ts +88 -0
  139. package/src/theme/typography.ts +46 -0
  140. package/src/types.ts +215 -0
  141. package/src/utils/deriveAnswers.ts +43 -0
  142. package/src/utils/readProgress.ts +33 -0
@@ -0,0 +1,906 @@
1
+ import React4, { createContext, forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useContext, useMemo } from 'react';
2
+ import { StyleSheet, Animated, Easing, BackHandler, Modal, Pressable, Text, AccessibilityInfo, TextInput } from 'react-native';
3
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
4
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
+
6
+ // src/questionnaire/QuestionnaireGate.tsx
7
+
8
+ // src/theme/defaultTheme.ts
9
+ var defaultOnboardingTheme = {
10
+ colors: {
11
+ background: "#FFFFFF",
12
+ surface: "#F7F7F8",
13
+ text: "#101114",
14
+ textMuted: "#6B7280",
15
+ primary: "#111111",
16
+ onPrimary: "#FFFFFF",
17
+ primarySoft: "rgba(17,17,17,0.06)",
18
+ border: "#E4E4E7",
19
+ borderStrong: "#C7C7CC",
20
+ disabled: "#EBEBEB",
21
+ success: "#16A34A",
22
+ warning: "#D97706",
23
+ error: "#DC2626",
24
+ info: "#2563EB"
25
+ },
26
+ fonts: {
27
+ regular: void 0,
28
+ medium: void 0,
29
+ bold: void 0,
30
+ baseSize: 16,
31
+ scale: 1
32
+ },
33
+ radius: {
34
+ sm: 8,
35
+ md: 12,
36
+ lg: 16,
37
+ full: 9999
38
+ },
39
+ spacing: {
40
+ xs: 4,
41
+ sm: 8,
42
+ md: 16,
43
+ lg: 24,
44
+ xl: 32
45
+ },
46
+ // Circled (pill) CTA by default; an app can pass `button: { radius: 12 }` for
47
+ // a rounded rectangle, or override height/paddingX to match its design system.
48
+ button: {
49
+ radius: 9999,
50
+ height: 52,
51
+ paddingX: 24
52
+ }
53
+ };
54
+ var mergeTheme = (override) => {
55
+ return defaultOnboardingTheme;
56
+ };
57
+ var OnboardingThemeContext = createContext(null);
58
+ var useOnboardingTheme = () => {
59
+ const ctx = useContext(OnboardingThemeContext);
60
+ return ctx != null ? ctx : mergeTheme();
61
+ };
62
+
63
+ // src/theme/typography.ts
64
+ var round = (n) => Math.round(n);
65
+ var bodyStyle = (f) => ({
66
+ fontFamily: f.regular,
67
+ fontSize: round(f.baseSize * f.scale),
68
+ lineHeight: round(f.baseSize * f.scale * 1.5)
69
+ });
70
+ var captionStyle = (f) => ({
71
+ fontFamily: f.regular,
72
+ fontSize: round(f.baseSize * 0.8125 * f.scale),
73
+ lineHeight: round(f.baseSize * 0.8125 * f.scale * 1.5)
74
+ });
75
+ var headingStyle = (f) => ({
76
+ fontFamily: f.bold,
77
+ fontSize: round(f.baseSize * 1.25 * f.scale),
78
+ lineHeight: round(f.baseSize * 1.25 * f.scale * 1.3),
79
+ fontWeight: f.bold ? void 0 : "700"
80
+ });
81
+ var buttonLabelStyle = (f) => {
82
+ var _a;
83
+ return {
84
+ fontFamily: (_a = f.bold) != null ? _a : f.medium,
85
+ fontSize: round(f.baseSize * f.scale),
86
+ fontWeight: f.bold ? void 0 : "600"
87
+ };
88
+ };
89
+
90
+ // src/motion/motionSpec.ts
91
+ var REDUCED_FADE_MS = 120;
92
+ var BUTTON_PRESS_SCALE = 0.975;
93
+ var REDUCED_PRESS_OPACITY = 0.85;
94
+ var BUTTON_PRESS_SPRING = { friction: 8, tension: 240 };
95
+ var durationScale = 1;
96
+ var scaledMs = (ms) => Math.round(ms * durationScale);
97
+ var lastKnown = false;
98
+ var useReducedMotion = () => {
99
+ const [reduced, setReduced] = useState(lastKnown);
100
+ useEffect(() => {
101
+ let mounted = true;
102
+ AccessibilityInfo.isReduceMotionEnabled().then((value) => {
103
+ lastKnown = value;
104
+ if (mounted) setReduced(value);
105
+ }).catch(() => {
106
+ });
107
+ const sub = AccessibilityInfo.addEventListener("reduceMotionChanged", (value) => {
108
+ lastKnown = value;
109
+ setReduced(value);
110
+ });
111
+ return () => {
112
+ mounted = false;
113
+ sub == null ? void 0 : sub.remove();
114
+ };
115
+ }, []);
116
+ return reduced;
117
+ };
118
+ var _Button = ({
119
+ title,
120
+ onPress,
121
+ variant = "primary",
122
+ disabled = false,
123
+ fullWidth = false
124
+ }) => {
125
+ const t = useOnboardingTheme();
126
+ const reduced = useReducedMotion();
127
+ const isPrimary = variant === "primary";
128
+ const pressT = useRef(new Animated.Value(0)).current;
129
+ const pressTo = useCallback(
130
+ (to) => {
131
+ if (reduced) {
132
+ Animated.timing(pressT, {
133
+ toValue: to,
134
+ duration: scaledMs(REDUCED_FADE_MS),
135
+ useNativeDriver: true
136
+ }).start();
137
+ return;
138
+ }
139
+ Animated.spring(pressT, {
140
+ toValue: to,
141
+ friction: BUTTON_PRESS_SPRING.friction,
142
+ tension: BUTTON_PRESS_SPRING.tension,
143
+ useNativeDriver: true
144
+ }).start();
145
+ },
146
+ [pressT, reduced]
147
+ );
148
+ const handlePressIn = useCallback(() => pressTo(1), [pressTo]);
149
+ const handlePressOut = useCallback(() => pressTo(0), [pressTo]);
150
+ const scale = pressT.interpolate({
151
+ inputRange: [0, 1],
152
+ outputRange: [1, reduced ? 1 : BUTTON_PRESS_SCALE]
153
+ });
154
+ const opacity = pressT.interpolate({
155
+ inputRange: [0, 1],
156
+ outputRange: [1, reduced ? REDUCED_PRESS_OPACITY : 1]
157
+ });
158
+ return /* @__PURE__ */ jsx(
159
+ Pressable,
160
+ {
161
+ onPress: disabled ? void 0 : onPress,
162
+ onPressIn: disabled ? void 0 : handlePressIn,
163
+ onPressOut: disabled ? void 0 : handlePressOut,
164
+ disabled,
165
+ accessibilityRole: "button",
166
+ accessibilityState: { disabled },
167
+ style: fullWidth ? styles.fill : void 0,
168
+ children: /* @__PURE__ */ jsx(
169
+ Animated.View,
170
+ {
171
+ style: [
172
+ styles.base,
173
+ fullWidth && styles.fill,
174
+ {
175
+ borderRadius: t.button.radius,
176
+ minHeight: t.button.height,
177
+ paddingHorizontal: t.button.paddingX,
178
+ opacity,
179
+ transform: [{ scale }]
180
+ },
181
+ isPrimary ? { backgroundColor: disabled ? t.colors.disabled : t.colors.primary } : { borderWidth: 1.5, borderColor: disabled ? t.colors.border : t.colors.primary }
182
+ ],
183
+ children: /* @__PURE__ */ jsx(
184
+ Text,
185
+ {
186
+ style: [
187
+ buttonLabelStyle(t.fonts),
188
+ {
189
+ color: disabled ? t.colors.textMuted : isPrimary ? t.colors.onPrimary : t.colors.primary
190
+ }
191
+ ],
192
+ children: title
193
+ }
194
+ )
195
+ }
196
+ )
197
+ }
198
+ );
199
+ };
200
+ var Button = React4.memo(_Button);
201
+ var styles = StyleSheet.create({
202
+ base: { alignItems: "center", justifyContent: "center" },
203
+ fill: { width: "100%" }
204
+ });
205
+
206
+ // src/motion/centeredModalMotion.ts
207
+ var CENTERED_MODAL_ANIM = {
208
+ /** Spring config for the card scale/opacity entrance (mirrors CompletionView's pop). */
209
+ enterSpring: { friction: 7, tension: 80 },
210
+ /** Exit timing in ms (fade + shrink back out before unmount). */
211
+ exitMs: 160,
212
+ /** Card scale at rest (progress 0); grows to 1 at progress 1. */
213
+ scaleFrom: 0.9,
214
+ /** Peak dim of the backdrop at progress 1. */
215
+ backdropOpacity: 0.55
216
+ };
217
+ var _CenteredModal = forwardRef(
218
+ ({ theme, dismissible = true, onDismiss, children }, ref) => {
219
+ const progress = useRef(new Animated.Value(0)).current;
220
+ const [open, setOpen] = useState(true);
221
+ const closingRef = useRef(false);
222
+ const reduced = useReducedMotion();
223
+ useEffect(() => {
224
+ const anim = reduced ? Animated.timing(progress, {
225
+ toValue: 1,
226
+ duration: REDUCED_FADE_MS,
227
+ useNativeDriver: true
228
+ }) : Animated.spring(progress, {
229
+ toValue: 1,
230
+ useNativeDriver: true,
231
+ friction: CENTERED_MODAL_ANIM.enterSpring.friction,
232
+ tension: CENTERED_MODAL_ANIM.enterSpring.tension
233
+ });
234
+ anim.start();
235
+ return () => anim.stop();
236
+ }, [progress, reduced]);
237
+ const close = useCallback(() => {
238
+ if (closingRef.current) return;
239
+ closingRef.current = true;
240
+ Animated.timing(progress, {
241
+ toValue: 0,
242
+ duration: reduced ? REDUCED_FADE_MS : CENTERED_MODAL_ANIM.exitMs,
243
+ easing: Easing.in(Easing.cubic),
244
+ useNativeDriver: true
245
+ }).start(() => {
246
+ setOpen(false);
247
+ onDismiss();
248
+ });
249
+ }, [progress, onDismiss, reduced]);
250
+ useImperativeHandle(ref, () => ({ close }), [close]);
251
+ useEffect(() => {
252
+ const sub = BackHandler.addEventListener("hardwareBackPress", () => {
253
+ if (dismissible) close();
254
+ return true;
255
+ });
256
+ return () => sub.remove();
257
+ }, [dismissible, close]);
258
+ const onBackdrop = useCallback(() => {
259
+ if (dismissible) close();
260
+ }, [dismissible, close]);
261
+ const backdropOpacity = progress.interpolate({
262
+ inputRange: [0, 1],
263
+ outputRange: [0, CENTERED_MODAL_ANIM.backdropOpacity]
264
+ });
265
+ const cardScale = reduced ? 1 : progress.interpolate({
266
+ inputRange: [0, 1],
267
+ outputRange: [CENTERED_MODAL_ANIM.scaleFrom, 1]
268
+ });
269
+ return /* @__PURE__ */ jsxs(
270
+ Modal,
271
+ {
272
+ visible: open,
273
+ transparent: true,
274
+ animationType: "none",
275
+ statusBarTranslucent: true,
276
+ onRequestClose: dismissible ? close : void 0,
277
+ children: [
278
+ /* @__PURE__ */ jsx(
279
+ Animated.View,
280
+ {
281
+ style: [styles2.backdrop, { opacity: backdropOpacity }],
282
+ pointerEvents: "none"
283
+ }
284
+ ),
285
+ /* @__PURE__ */ jsx(
286
+ Pressable,
287
+ {
288
+ style: styles2.fill,
289
+ onPress: onBackdrop,
290
+ accessibilityRole: "button",
291
+ accessibilityLabel: "Dismiss",
292
+ children: /* @__PURE__ */ jsx(SafeAreaView, { style: styles2.center, pointerEvents: "box-none", children: /* @__PURE__ */ jsx(Pressable, { onPress: () => {
293
+ }, style: styles2.cardWrap, children: /* @__PURE__ */ jsx(
294
+ Animated.View,
295
+ {
296
+ accessibilityViewIsModal: true,
297
+ accessibilityRole: "alert",
298
+ style: [
299
+ styles2.card,
300
+ {
301
+ backgroundColor: theme.colors.surface,
302
+ borderRadius: theme.radius.lg,
303
+ padding: theme.spacing.lg,
304
+ gap: theme.spacing.md,
305
+ opacity: progress,
306
+ transform: [{ scale: cardScale }]
307
+ }
308
+ ],
309
+ children
310
+ }
311
+ ) }) })
312
+ }
313
+ )
314
+ ]
315
+ }
316
+ );
317
+ }
318
+ );
319
+ _CenteredModal.displayName = "CenteredModal";
320
+ var CenteredModal = React4.memo(_CenteredModal);
321
+ var styles2 = StyleSheet.create({
322
+ fill: { flex: 1 },
323
+ backdrop: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, backgroundColor: "#000" },
324
+ center: { flex: 1, alignItems: "center", justifyContent: "center", padding: 24 },
325
+ cardWrap: { width: "100%", maxWidth: 420 },
326
+ card: {
327
+ width: "100%",
328
+ // Soft elevation so the popup reads as lifted above the app in both themes.
329
+ shadowColor: "#000",
330
+ shadowOffset: { width: 0, height: 8 },
331
+ shadowOpacity: 0.28,
332
+ shadowRadius: 24,
333
+ elevation: 12
334
+ }
335
+ });
336
+
337
+ // src/reviews/feedbackForm.ts
338
+ var FEEDBACK_MIN_LINES = 4;
339
+ var FEEDBACK_MAX_LINES = 5;
340
+ var feedbackInputHeights = (fonts, verticalPadding = 0) => {
341
+ var _a;
342
+ const lineHeight = (_a = bodyStyle(fonts).lineHeight) != null ? _a : Math.round(fonts.baseSize * fonts.scale * 1.5);
343
+ const pad = verticalPadding * 2;
344
+ return {
345
+ minHeight: lineHeight * FEEDBACK_MIN_LINES + pad,
346
+ maxHeight: lineHeight * FEEDBACK_MAX_LINES + pad
347
+ };
348
+ };
349
+
350
+ // src/reviews/decision.ts
351
+ var decideReview = (local, decision) => decision != null ? decision : local;
352
+ var resolveRules = (config) => {
353
+ var _a, _b, _c, _d, _e;
354
+ return {
355
+ enabled: (_a = config.enabled) != null ? _a : true,
356
+ minSessions: (_b = config.minSessions) != null ? _b : 0,
357
+ minEvents: (_c = config.minEvents) != null ? _c : 0,
358
+ cooldownDays: (_d = config.cooldownDays) != null ? _d : 0,
359
+ oncePerVersion: (_e = config.oncePerVersion) != null ? _e : true
360
+ };
361
+ };
362
+ var MS_PER_DAY = 864e5;
363
+ var evaluateGate = (rules, s) => {
364
+ if (!rules.enabled) return { fire: false, reason: "disabled" };
365
+ if (rules.oncePerVersion && s.seen) return { fire: false, reason: "already_shown" };
366
+ if (s.sessions < rules.minSessions) return { fire: false, reason: "min_sessions" };
367
+ if (s.events < rules.minEvents) return { fire: false, reason: "min_events" };
368
+ if (rules.cooldownDays > 0 && s.lastShownAt != null) {
369
+ const elapsedDays = (s.now - s.lastShownAt) / MS_PER_DAY;
370
+ if (elapsedDays < rules.cooldownDays) return { fire: false, reason: "cooldown" };
371
+ }
372
+ return { fire: true, reason: "local_rules_met" };
373
+ };
374
+
375
+ // src/questionnaire/decision.ts
376
+ var decideQuestionnaire = (local, decision) => decideReview(local, decision);
377
+ var buildQuestionnaireSubmission = (input) => {
378
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
379
+ const opinion = input.answers.opinion.trim();
380
+ const improve = (_a = input.answers.improve) == null ? void 0 : _a.trim();
381
+ const suggestions = (_b = input.answers.suggestions) == null ? void 0 : _b.trim();
382
+ return {
383
+ session_id: (_c = input.sessionId) != null ? _c : null,
384
+ answers: {
385
+ opinion,
386
+ improve: improve ? improve : null,
387
+ suggestions: suggestions ? suggestions : null
388
+ },
389
+ meta: {
390
+ user_id: (_e = (_d = input.meta) == null ? void 0 : _d.user_id) != null ? _e : null,
391
+ device_key: (_g = (_f = input.meta) == null ? void 0 : _f.device_key) != null ? _g : null,
392
+ session_count: (_i = (_h = input.meta) == null ? void 0 : _h.session_count) != null ? _i : null
393
+ }
394
+ };
395
+ };
396
+
397
+ // src/questionnaire/transport.ts
398
+ var submitQuestionnaireResponse = (target, id, submission) => {
399
+ if (!(target == null ? void 0 : target.serverUrl) || !id) return;
400
+ try {
401
+ const base = target.serverUrl.replace(/\/$/, "");
402
+ const url = `${base}/v1/questionnaires/${encodeURIComponent(id)}/responses`;
403
+ const headers = { "Content-Type": "application/json" };
404
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
405
+ void fetch(url, {
406
+ method: "POST",
407
+ headers,
408
+ body: JSON.stringify(submission)
409
+ }).catch(() => {
410
+ });
411
+ } catch {
412
+ }
413
+ };
414
+ var fetchQuestionnaireDecision = async (target, options = {}) => {
415
+ if (!(target == null ? void 0 : target.serverUrl)) return null;
416
+ try {
417
+ const base = target.serverUrl.replace(/\/$/, "");
418
+ const params = new URLSearchParams();
419
+ if (options.sessionId) params.set("session_id", options.sessionId);
420
+ if (options.deviceKey) params.set("device_key", options.deviceKey);
421
+ const qs = params.toString();
422
+ const url = `${base}/v1/questionnaires/decision${qs ? `?${qs}` : ""}`;
423
+ const headers = {};
424
+ if (target.apiKey) headers.Authorization = `Bearer ${target.apiKey}`;
425
+ const res = await fetch(url, { headers });
426
+ if (!res || !res.ok) return null;
427
+ const json = await res.json();
428
+ return json != null ? json : null;
429
+ } catch {
430
+ return null;
431
+ }
432
+ };
433
+ var mergeThemeOver = (base, override) => {
434
+ if (!override) return base;
435
+ return {
436
+ colors: { ...base.colors, ...override.colors },
437
+ fonts: { ...base.fonts, ...override.fonts },
438
+ radius: { ...base.radius, ...override.radius },
439
+ spacing: { ...base.spacing, ...override.spacing },
440
+ button: { ...base.button, ...override.button }
441
+ };
442
+ };
443
+ var QuestionnaireGate = ({
444
+ questionnaire,
445
+ target,
446
+ sessionId,
447
+ meta,
448
+ onEvent,
449
+ onResolved,
450
+ onShown,
451
+ theme: themeOverride,
452
+ dismissible = true
453
+ }) => {
454
+ var _a, _b, _c, _d;
455
+ const active = useOnboardingTheme();
456
+ const t = useMemo(() => mergeThemeOver(active, themeOverride), [active, themeOverride]);
457
+ const modalRef = React4.useRef(null);
458
+ const id = questionnaire.id;
459
+ const [phase, setPhase] = useState("form");
460
+ const [opinion, setOpinion] = useState("");
461
+ const [improve, setImprove] = useState("");
462
+ const [suggestions, setSuggestions] = useState("");
463
+ const shownRef = React4.useRef(false);
464
+ const onShownRef = React4.useRef(onShown);
465
+ const onEventRef = React4.useRef(onEvent);
466
+ onShownRef.current = onShown;
467
+ onEventRef.current = onEvent;
468
+ React4.useEffect(() => {
469
+ var _a2, _b2;
470
+ if (shownRef.current) return;
471
+ shownRef.current = true;
472
+ (_a2 = onShownRef.current) == null ? void 0 : _a2.call(onShownRef);
473
+ (_b2 = onEventRef.current) == null ? void 0 : _b2.call(onEventRef, { name: "questionnaire_prompt_shown", id });
474
+ }, []);
475
+ const resolve = useCallback(() => {
476
+ onResolved == null ? void 0 : onResolved();
477
+ }, [onResolved]);
478
+ const canSend = opinion.trim().length > 0;
479
+ const send = useCallback(() => {
480
+ if (!canSend) return;
481
+ const submission = buildQuestionnaireSubmission({
482
+ answers: { opinion, improve, suggestions },
483
+ sessionId,
484
+ meta
485
+ });
486
+ submitQuestionnaireResponse(target, id, submission);
487
+ onEvent == null ? void 0 : onEvent({ name: "questionnaire_submitted", id });
488
+ setPhase("thanks");
489
+ resolve();
490
+ }, [canSend, opinion, improve, suggestions, sessionId, meta, target, id, onEvent, resolve]);
491
+ const handleDismissTap = useCallback(() => {
492
+ var _a2;
493
+ onEvent == null ? void 0 : onEvent({ name: "questionnaire_dismissed", id });
494
+ (_a2 = modalRef.current) == null ? void 0 : _a2.close();
495
+ }, [onEvent, id]);
496
+ const title = (_a = questionnaire.title) != null ? _a : "How's it going so far?";
497
+ const opinionLabel = (_b = questionnaire.opinion_label) != null ? _b : "What do you think about the app?";
498
+ const improveLabel = (_c = questionnaire.improve_label) != null ? _c : "How can we improve? (optional)";
499
+ const suggestionsLabel = (_d = questionnaire.suggestions_label) != null ? _d : "Any suggestions? (optional)";
500
+ const opinionBox = feedbackInputHeights(t.fonts, t.spacing.md);
501
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
502
+ /* @__PURE__ */ jsx(Text, { style: [headingStyle(t.fonts), { color: t.colors.text }], children: title }),
503
+ phase === "form" && /* @__PURE__ */ jsxs(Fragment, { children: [
504
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: "A few words is plenty. Only the first box is required." }),
505
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: opinionLabel }),
506
+ /* @__PURE__ */ jsx(
507
+ TextInput,
508
+ {
509
+ value: opinion,
510
+ onChangeText: setOpinion,
511
+ placeholder: "e.g. it feels fast and clear",
512
+ placeholderTextColor: t.colors.textMuted,
513
+ multiline: true,
514
+ textAlignVertical: "top",
515
+ style: [
516
+ bodyStyle(t.fonts),
517
+ styles3.input,
518
+ {
519
+ color: t.colors.text,
520
+ backgroundColor: t.colors.background,
521
+ borderRadius: t.radius.md,
522
+ borderColor: t.colors.border,
523
+ padding: t.spacing.md,
524
+ minHeight: opinionBox.minHeight,
525
+ maxHeight: opinionBox.maxHeight
526
+ }
527
+ ]
528
+ }
529
+ ),
530
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: improveLabel }),
531
+ /* @__PURE__ */ jsx(
532
+ TextInput,
533
+ {
534
+ value: improve,
535
+ onChangeText: setImprove,
536
+ placeholder: "e.g. fewer steps to start",
537
+ placeholderTextColor: t.colors.textMuted,
538
+ style: [
539
+ bodyStyle(t.fonts),
540
+ styles3.input,
541
+ {
542
+ color: t.colors.text,
543
+ backgroundColor: t.colors.background,
544
+ borderRadius: t.radius.md,
545
+ borderColor: t.colors.border,
546
+ paddingHorizontal: t.spacing.md,
547
+ paddingVertical: t.spacing.sm,
548
+ minHeight: t.button.height
549
+ }
550
+ ]
551
+ }
552
+ ),
553
+ /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), { color: t.colors.textMuted }], children: suggestionsLabel }),
554
+ /* @__PURE__ */ jsx(
555
+ TextInput,
556
+ {
557
+ value: suggestions,
558
+ onChangeText: setSuggestions,
559
+ placeholder: "e.g. a dark theme",
560
+ placeholderTextColor: t.colors.textMuted,
561
+ style: [
562
+ bodyStyle(t.fonts),
563
+ styles3.input,
564
+ {
565
+ color: t.colors.text,
566
+ backgroundColor: t.colors.background,
567
+ borderRadius: t.radius.md,
568
+ borderColor: t.colors.border,
569
+ paddingHorizontal: t.spacing.md,
570
+ paddingVertical: t.spacing.sm,
571
+ minHeight: t.button.height
572
+ }
573
+ ]
574
+ }
575
+ ),
576
+ /* @__PURE__ */ jsx(Button, { title: "Send", onPress: send, variant: "primary", fullWidth: true, disabled: !canSend }),
577
+ dismissible && /* @__PURE__ */ jsx(Pressable, { onPress: handleDismissTap, hitSlop: 8, children: /* @__PURE__ */ jsx(Text, { style: [captionStyle(t.fonts), styles3.link, { color: t.colors.textMuted }], children: "Not now" }) })
578
+ ] }),
579
+ phase === "thanks" && /* @__PURE__ */ jsx(Text, { style: [bodyStyle(t.fonts), { color: t.colors.textMuted }], children: "Thank you. This helps us make it better." })
580
+ ] });
581
+ return /* @__PURE__ */ jsx(CenteredModal, { ref: modalRef, theme: t, dismissible, onDismiss: resolve, children: content });
582
+ };
583
+ QuestionnaireGate.displayName = "QuestionnaireGate";
584
+ var styles3 = StyleSheet.create({
585
+ input: { borderWidth: 1 },
586
+ link: { textAlign: "center", textDecorationLine: "underline" }
587
+ });
588
+
589
+ // src/coachmarks/runtime.ts
590
+ var storageSingleton = null;
591
+ var testing = false;
592
+ var getCoachmarkStorage = () => storageSingleton;
593
+ var isCoachmarkTesting = () => testing;
594
+ var SEEN_VALUE = "1";
595
+ var hasSeenGate = (key, storageOverride, testingOverride) => {
596
+ if (testingOverride != null ? testingOverride : testing) return false;
597
+ const storage = storageOverride != null ? storageOverride : storageSingleton;
598
+ if (!storage) return false;
599
+ try {
600
+ return storage.getItem(key) === SEEN_VALUE;
601
+ } catch {
602
+ return false;
603
+ }
604
+ };
605
+ var markSeenGate = (key, storageOverride, testingOverride) => {
606
+ if (testingOverride != null ? testingOverride : testing) return;
607
+ const storage = storageOverride != null ? storageOverride : storageSingleton;
608
+ if (!storage) return;
609
+ try {
610
+ storage.setItem(key, SEEN_VALUE);
611
+ } catch {
612
+ }
613
+ };
614
+
615
+ // src/features/defaults.ts
616
+ var defaultWireFeatures = Object.freeze({
617
+ coachmarks: Object.freeze({ enabled: true }),
618
+ showcase: Object.freeze({ enabled: true }),
619
+ onboarding: Object.freeze({ mode: "ai" }),
620
+ review: Object.freeze({ enabled: true }),
621
+ questionnaire: Object.freeze({ enabled: true }),
622
+ announcements: Object.freeze({ enabled: true })
623
+ });
624
+ var VALID_MODES = ["ai", "static", "off"];
625
+ var readEnabled = (raw) => {
626
+ if (raw && typeof raw === "object" && typeof raw.enabled === "boolean") {
627
+ return { enabled: raw.enabled };
628
+ }
629
+ return { enabled: true };
630
+ };
631
+ var parseWireFeatures = (raw) => {
632
+ var _a;
633
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return defaultWireFeatures;
634
+ const src = raw;
635
+ const modeRaw = (_a = src.onboarding) == null ? void 0 : _a.mode;
636
+ const mode = typeof modeRaw === "string" && VALID_MODES.includes(modeRaw) ? modeRaw : "ai";
637
+ return {
638
+ coachmarks: readEnabled(src.coachmarks),
639
+ showcase: readEnabled(src.showcase),
640
+ onboarding: { mode },
641
+ review: readEnabled(src.review),
642
+ questionnaire: readEnabled(src.questionnaire),
643
+ announcements: readEnabled(src.announcements)
644
+ };
645
+ };
646
+ var featuresEqual = (a, b) => a === b || a.coachmarks.enabled === b.coachmarks.enabled && a.showcase.enabled === b.showcase.enabled && a.onboarding.mode === b.onboarding.mode && a.review.enabled === b.review.enabled && a.questionnaire.enabled === b.questionnaire.enabled && a.announcements.enabled === b.announcements.enabled;
647
+
648
+ // src/features/cache.ts
649
+ var READ_TIMEOUT_MS = 1500;
650
+ var featuresCacheKey = (appId) => `wireai:features:${appId && appId.length > 0 ? appId : "default"}`;
651
+ var withTimeout = (p, ms) => {
652
+ let timer;
653
+ const timeout = new Promise((resolve) => {
654
+ timer = setTimeout(() => resolve(void 0), ms);
655
+ });
656
+ return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
657
+ };
658
+ var readCachedFeatures = async (storage, key) => {
659
+ let raw;
660
+ try {
661
+ raw = await withTimeout(storage.getItem(key), READ_TIMEOUT_MS);
662
+ } catch {
663
+ return void 0;
664
+ }
665
+ if (!raw) return void 0;
666
+ try {
667
+ const parsed = JSON.parse(raw);
668
+ if (parsed && typeof parsed === "object" && typeof parsed.ts === "number") {
669
+ const ts = parsed.ts;
670
+ return {
671
+ features: parseWireFeatures(parsed.features),
672
+ ageMs: Date.now() - ts
673
+ };
674
+ }
675
+ } catch {
676
+ }
677
+ return void 0;
678
+ };
679
+ var writeCachedFeatures = (storage, key, features) => {
680
+ try {
681
+ void storage.setItem(key, JSON.stringify({ features, ts: Date.now() })).catch(() => {
682
+ });
683
+ } catch {
684
+ }
685
+ };
686
+
687
+ // src/features/fetchWireFeatures.ts
688
+ var DEFAULT_TIMEOUT_MS = 4e3;
689
+ var failOpen = async (storage, key) => {
690
+ var _a;
691
+ if (!storage) return defaultWireFeatures;
692
+ const cached = await readCachedFeatures(storage, key);
693
+ return (_a = cached == null ? void 0 : cached.features) != null ? _a : defaultWireFeatures;
694
+ };
695
+ var fetchWithTimeout = async (url, apiKey, timeoutMs) => {
696
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
697
+ const timer = setTimeout(() => controller == null ? void 0 : controller.abort(), timeoutMs);
698
+ try {
699
+ return await fetch(url, {
700
+ method: "GET",
701
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
702
+ signal: controller == null ? void 0 : controller.signal
703
+ });
704
+ } finally {
705
+ clearTimeout(timer);
706
+ }
707
+ };
708
+ var fetchWireFeatures = async (config) => {
709
+ const key = featuresCacheKey(config == null ? void 0 : config.appId);
710
+ const storage = config == null ? void 0 : config.storage;
711
+ if (!(config == null ? void 0 : config.serverUrl) || !(config == null ? void 0 : config.apiKey)) return failOpen(storage, key);
712
+ const url = `${config.serverUrl.replace(/\/$/, "")}/v1/features`;
713
+ const timeoutMs = config.timeoutMs && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS;
714
+ try {
715
+ const res = await fetchWithTimeout(url, config.apiKey, timeoutMs);
716
+ if (!res || !res.ok) return failOpen(storage, key);
717
+ const json = await res.json();
718
+ const features = parseWireFeatures(json);
719
+ if (storage) writeCachedFeatures(storage, key, features);
720
+ return features;
721
+ } catch {
722
+ return failOpen(storage, key);
723
+ }
724
+ };
725
+
726
+ // src/features/useWireFeatures.ts
727
+ var useStableValue = (value, isEqual) => {
728
+ const ref = useRef(value);
729
+ if (!isEqual(ref.current, value)) ref.current = value;
730
+ return ref.current;
731
+ };
732
+ var sameFetchKey = (a, b) => (a == null ? void 0 : a.serverUrl) === (b == null ? void 0 : b.serverUrl) && (a == null ? void 0 : a.apiKey) === (b == null ? void 0 : b.apiKey) && (a == null ? void 0 : a.appId) === (b == null ? void 0 : b.appId);
733
+ var useWireFeatures = (config) => {
734
+ const stableConfig = useStableValue(config, sameFetchKey);
735
+ const [flags, setFlags] = useState(defaultWireFeatures);
736
+ const canFetch = !!(stableConfig == null ? void 0 : stableConfig.serverUrl) && !!(stableConfig == null ? void 0 : stableConfig.apiKey);
737
+ useEffect(() => {
738
+ if (!canFetch) {
739
+ setFlags((prev) => featuresEqual(prev, defaultWireFeatures) ? prev : defaultWireFeatures);
740
+ return;
741
+ }
742
+ let alive = true;
743
+ void fetchWireFeatures(stableConfig).then((next) => {
744
+ if (!alive) return;
745
+ setFlags((prev) => featuresEqual(prev, next) ? prev : next);
746
+ });
747
+ return () => {
748
+ alive = false;
749
+ };
750
+ }, [canFetch, stableConfig]);
751
+ return flags;
752
+ };
753
+ var WireFeaturesContext = createContext(null);
754
+ var useWireFeaturesContext = () => useContext(WireFeaturesContext);
755
+ var useResolvedFeatures = (options) => {
756
+ var _a, _b, _c;
757
+ const ctx = useWireFeaturesContext();
758
+ const shouldFetch = !(options == null ? void 0 : options.flags) && ctx == null;
759
+ const fetched = useWireFeatures(shouldFetch ? options == null ? void 0 : options.config : void 0);
760
+ return (_c = (_b = (_a = options == null ? void 0 : options.flags) != null ? _a : ctx) != null ? _b : fetched) != null ? _c : defaultWireFeatures;
761
+ };
762
+
763
+ // src/reviews/equality.ts
764
+ var shallowEqual = (a, b) => {
765
+ if (Object.is(a, b)) return true;
766
+ if (a == null || b == null || typeof a !== "object" || typeof b !== "object") {
767
+ return false;
768
+ }
769
+ const ak = Object.keys(a);
770
+ const bk = Object.keys(b);
771
+ if (ak.length !== bk.length) return false;
772
+ for (const k of ak) {
773
+ if (!Object.is(a[k], b[k])) {
774
+ return false;
775
+ }
776
+ }
777
+ return true;
778
+ };
779
+ var sameDecision = (a, b) => {
780
+ if (a == null || b == null) return a == b;
781
+ return a.fire === b.fire && a.reason === b.reason;
782
+ };
783
+
784
+ // src/reviews/runtime.ts
785
+ var resolveStorage = (override) => override != null ? override : getCoachmarkStorage();
786
+ var readInt = (storage, key) => {
787
+ if (!storage) return 0;
788
+ try {
789
+ const raw = storage.getItem(key);
790
+ const n = raw == null ? 0 : parseInt(raw, 10);
791
+ return Number.isFinite(n) ? n : 0;
792
+ } catch {
793
+ return 0;
794
+ }
795
+ };
796
+ var writeInt = (storage, key, value) => {
797
+ if (!storage) return;
798
+ try {
799
+ storage.setItem(key, String(value));
800
+ } catch {
801
+ }
802
+ };
803
+
804
+ // src/questionnaire/runtime.ts
805
+ var questionnaireSeenKey = (id, version) => version ? `wire_questionnaire_${id}_${version}_seen` : `wire_questionnaire_${id}_seen`;
806
+ var questionnaireLastShownKey = (id) => `wire_questionnaire_${id}_last`;
807
+ var questionnaireSessionsKey = (id) => `wire_questionnaire_${id}_sessions`;
808
+
809
+ // src/questionnaire/useQuestionnaireGate.ts
810
+ var useStableValue2 = (value, isEqual) => {
811
+ const ref = useRef(value);
812
+ if (!isEqual(ref.current, value)) ref.current = value;
813
+ return ref.current;
814
+ };
815
+ var useQuestionnaireGate = ({
816
+ config: configProp,
817
+ decision: decisionProp,
818
+ events = 0,
819
+ storage,
820
+ isTesting,
821
+ features,
822
+ featuresConfig
823
+ }) => {
824
+ const config = useStableValue2(configProp, shallowEqual);
825
+ const decision = useStableValue2(decisionProp, (a, b) => sameDecision(a, b));
826
+ const featureEnabled = useResolvedFeatures({ flags: features, config: featuresConfig }).questionnaire.enabled;
827
+ const testing2 = isTesting != null ? isTesting : isCoachmarkTesting();
828
+ const seenKey = questionnaireSeenKey(
829
+ config.id,
830
+ config.oncePerVersion === false ? void 0 : config.appVersion
831
+ );
832
+ const lastKey = questionnaireLastShownKey(config.id);
833
+ const sessionsKey = questionnaireSessionsKey(config.id);
834
+ const sessions = useState(() => {
835
+ const store = resolveStorage(storage);
836
+ const next = readInt(store, sessionsKey) + 1;
837
+ writeInt(store, sessionsKey, next);
838
+ return next;
839
+ })[0];
840
+ const hasServerDecision = decision != null;
841
+ const timeoutMs = config.timeoutFallbackMs;
842
+ const waits = !hasServerDecision && timeoutMs != null && timeoutMs > 0;
843
+ const [elapsed, setElapsed] = useState(!waits);
844
+ useEffect(() => {
845
+ if (!waits) {
846
+ setElapsed(true);
847
+ return;
848
+ }
849
+ const timer = setTimeout(() => setElapsed(true), timeoutMs);
850
+ return () => clearTimeout(timer);
851
+ }, [waits, timeoutMs]);
852
+ const { visible, verdict } = useMemo(() => {
853
+ if (!featureEnabled)
854
+ return {
855
+ visible: false,
856
+ verdict: { fire: false, reason: "feature_disabled" }
857
+ };
858
+ if (testing2)
859
+ return {
860
+ visible: true,
861
+ verdict: { fire: true, reason: "testing" }
862
+ };
863
+ const store = resolveStorage(storage);
864
+ const local = evaluateGate(resolveRules(config), {
865
+ sessions,
866
+ events,
867
+ lastShownAt: readInt(store, lastKey) || null,
868
+ seen: hasSeenGate(seenKey, storage, isTesting),
869
+ now: Date.now()
870
+ });
871
+ const resolved = decideQuestionnaire(local, decision);
872
+ const alreadySeen = hasSeenGate(seenKey, storage, isTesting);
873
+ const ready = hasServerDecision || elapsed;
874
+ return { visible: ready && resolved.fire && !alreadySeen, verdict: resolved };
875
+ }, [
876
+ featureEnabled,
877
+ testing2,
878
+ config,
879
+ decision,
880
+ events,
881
+ sessions,
882
+ elapsed,
883
+ hasServerDecision,
884
+ seenKey,
885
+ lastKey,
886
+ storage,
887
+ isTesting
888
+ ]);
889
+ const shownRef = useRef(false);
890
+ const markShown = useCallback(() => {
891
+ if (shownRef.current) return;
892
+ shownRef.current = true;
893
+ writeInt(resolveStorage(storage), lastKey, Date.now());
894
+ }, [storage, lastKey]);
895
+ const markResolved = useCallback(() => {
896
+ markSeenGate(seenKey, storage, isTesting);
897
+ }, [seenKey, storage, isTesting]);
898
+ return useMemo(
899
+ () => ({ visible, decision: verdict, markShown, markResolved }),
900
+ [visible, verdict, markShown, markResolved]
901
+ );
902
+ };
903
+
904
+ export { QuestionnaireGate, buildQuestionnaireSubmission, decideQuestionnaire, evaluateGate, fetchQuestionnaireDecision, questionnaireLastShownKey, questionnaireSeenKey, questionnaireSessionsKey, resolveRules, submitQuestionnaireResponse, useQuestionnaireGate };
905
+ //# sourceMappingURL=index.mjs.map
906
+ //# sourceMappingURL=index.mjs.map