@variantlab/react-native 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.
package/dist/debug.js ADDED
@@ -0,0 +1,926 @@
1
+ import { StyleSheet, View, Text, Pressable, Animated, Modal, TextInput, ScrollView } from 'react-native';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { useVariantLabEngine } from '@variantlab/react';
4
+ import { useMemo, useState, useEffect, useCallback, useRef, useSyncExternalStore } from 'react';
5
+
6
+ // src/overlay/experiment-card.tsx
7
+ function ExperimentCard({
8
+ theme,
9
+ experiment,
10
+ activeVariantId,
11
+ expanded,
12
+ onToggleExpand,
13
+ onSelect,
14
+ onReset
15
+ }) {
16
+ return /* @__PURE__ */ jsxs(
17
+ View,
18
+ {
19
+ style: [styles.card, { backgroundColor: theme.surface, borderColor: theme.border }],
20
+ testID: `variantlab-experiment-card-${experiment.id}`,
21
+ children: [
22
+ /* @__PURE__ */ jsxs(
23
+ Pressable,
24
+ {
25
+ onPress: onToggleExpand,
26
+ accessibilityRole: "button",
27
+ accessibilityLabel: `Experiment ${experiment.name}, current variant ${activeVariantId}`,
28
+ style: styles.header,
29
+ children: [
30
+ /* @__PURE__ */ jsxs(View, { style: styles.headerText, children: [
31
+ /* @__PURE__ */ jsx(Text, { style: [styles.name, { color: theme.text }], numberOfLines: 1, children: experiment.name }),
32
+ /* @__PURE__ */ jsx(Text, { style: [styles.id, { color: theme.textMuted }], numberOfLines: 1, children: experiment.id })
33
+ ] }),
34
+ /* @__PURE__ */ jsx(View, { style: [styles.activePill, { backgroundColor: theme.accent }], children: /* @__PURE__ */ jsx(Text, { style: [styles.activePillText, { color: theme.accentText }], children: activeVariantId }) })
35
+ ]
36
+ }
37
+ ),
38
+ expanded ? /* @__PURE__ */ jsxs(View, { style: styles.body, children: [
39
+ experiment.variants.map((v) => {
40
+ const isActive = v.id === activeVariantId;
41
+ return /* @__PURE__ */ jsxs(
42
+ Pressable,
43
+ {
44
+ onPress: () => onSelect(v.id),
45
+ accessibilityRole: "button",
46
+ accessibilityState: { selected: isActive },
47
+ accessibilityLabel: `Set variant ${v.id}`,
48
+ style: [
49
+ styles.variantRow,
50
+ {
51
+ borderColor: isActive ? theme.accent : theme.border,
52
+ backgroundColor: isActive ? `${theme.accent}22` : "transparent"
53
+ }
54
+ ],
55
+ testID: `variantlab-variant-row-${experiment.id}-${v.id}`,
56
+ children: [
57
+ /* @__PURE__ */ jsx(Text, { style: [styles.variantId, { color: theme.text }], children: v.label ?? v.id }),
58
+ v.description !== void 0 ? /* @__PURE__ */ jsx(Text, { style: [styles.variantDesc, { color: theme.textMuted }], numberOfLines: 2, children: v.description }) : null
59
+ ]
60
+ },
61
+ v.id
62
+ );
63
+ }),
64
+ /* @__PURE__ */ jsx(
65
+ Pressable,
66
+ {
67
+ onPress: onReset,
68
+ accessibilityRole: "button",
69
+ accessibilityLabel: "Reset to default",
70
+ style: styles.resetRow,
71
+ testID: `variantlab-reset-${experiment.id}`,
72
+ children: /* @__PURE__ */ jsx(Text, { style: [styles.resetText, { color: theme.textMuted }], children: "Reset to default" })
73
+ }
74
+ )
75
+ ] }) : null
76
+ ]
77
+ }
78
+ );
79
+ }
80
+ function describeAssignmentSource(hasOverride, matchedTargeting) {
81
+ if (hasOverride) return "manual override";
82
+ if (matchedTargeting) return "by targeting";
83
+ return "by default";
84
+ }
85
+ var styles = StyleSheet.create({
86
+ card: {
87
+ borderRadius: 10,
88
+ borderWidth: 1,
89
+ marginBottom: 8,
90
+ overflow: "hidden"
91
+ },
92
+ header: {
93
+ flexDirection: "row",
94
+ alignItems: "center",
95
+ padding: 12,
96
+ gap: 8
97
+ },
98
+ headerText: {
99
+ flex: 1,
100
+ flexDirection: "column"
101
+ },
102
+ name: {
103
+ fontSize: 14,
104
+ fontWeight: "600"
105
+ },
106
+ id: {
107
+ fontSize: 11,
108
+ fontFamily: "Menlo, monospace",
109
+ marginTop: 2
110
+ },
111
+ activePill: {
112
+ borderRadius: 999,
113
+ paddingHorizontal: 10,
114
+ paddingVertical: 4
115
+ },
116
+ activePillText: {
117
+ fontSize: 11,
118
+ fontWeight: "700"
119
+ },
120
+ body: {
121
+ paddingHorizontal: 12,
122
+ paddingBottom: 12,
123
+ gap: 6
124
+ },
125
+ variantRow: {
126
+ borderRadius: 8,
127
+ borderWidth: 1,
128
+ paddingHorizontal: 10,
129
+ paddingVertical: 8
130
+ },
131
+ variantId: {
132
+ fontSize: 13,
133
+ fontWeight: "600"
134
+ },
135
+ variantDesc: {
136
+ fontSize: 11,
137
+ marginTop: 2
138
+ },
139
+ resetRow: {
140
+ paddingVertical: 8,
141
+ alignItems: "flex-end"
142
+ },
143
+ resetText: {
144
+ fontSize: 11,
145
+ textDecorationLine: "underline"
146
+ }
147
+ });
148
+
149
+ // src/overlay/filter.ts
150
+ function isVisibleExperiment(experiment) {
151
+ return experiment.status !== "archived";
152
+ }
153
+ function matchesSearch(experiment, query) {
154
+ if (query === "") return true;
155
+ const needle = query.toLowerCase();
156
+ if (experiment.id.toLowerCase().includes(needle)) return true;
157
+ if (experiment.name.toLowerCase().includes(needle)) return true;
158
+ for (const v of experiment.variants) {
159
+ if (v.id.toLowerCase().includes(needle)) return true;
160
+ if (v.label !== void 0 && v.label.toLowerCase().includes(needle)) return true;
161
+ }
162
+ return false;
163
+ }
164
+ function filterExperiments(list, query) {
165
+ const out = [];
166
+ for (const exp of list) {
167
+ if (!isVisibleExperiment(exp)) continue;
168
+ if (!matchesSearch(exp, query)) continue;
169
+ out.push(exp);
170
+ }
171
+ return out;
172
+ }
173
+
174
+ // src/overlay/imperative.ts
175
+ var subscribers = /* @__PURE__ */ new Set();
176
+ function registerOverlay(setVisible) {
177
+ subscribers.add(setVisible);
178
+ return () => {
179
+ subscribers.delete(setVisible);
180
+ };
181
+ }
182
+ function openDebugOverlay() {
183
+ for (const fn of subscribers) fn(true);
184
+ }
185
+ function closeDebugOverlay() {
186
+ for (const fn of subscribers) fn(false);
187
+ }
188
+ function BottomSheet({
189
+ visible,
190
+ onRequestClose,
191
+ theme,
192
+ insetBottom = 0,
193
+ children
194
+ }) {
195
+ const progress = useRef(new Animated.Value(0)).current;
196
+ useEffect(() => {
197
+ Animated.timing(progress, {
198
+ toValue: visible ? 1 : 0,
199
+ duration: 200,
200
+ useNativeDriver: true
201
+ }).start();
202
+ }, [visible, progress]);
203
+ const translateY = progress.interpolate({
204
+ inputRange: [0, 1],
205
+ outputRange: [600, 0]
206
+ });
207
+ const opacity = progress.interpolate({
208
+ inputRange: [0, 1],
209
+ outputRange: [0, 1]
210
+ });
211
+ return /* @__PURE__ */ jsx(
212
+ Modal,
213
+ {
214
+ visible,
215
+ transparent: true,
216
+ animationType: "none",
217
+ onRequestClose,
218
+ statusBarTranslucent: true,
219
+ children: /* @__PURE__ */ jsxs(View, { style: styles2.root, pointerEvents: "box-none", children: [
220
+ /* @__PURE__ */ jsx(Animated.View, { style: [styles2.scrim, { opacity }], children: /* @__PURE__ */ jsx(
221
+ Pressable,
222
+ {
223
+ accessibilityRole: "button",
224
+ accessibilityLabel: "Dismiss debug overlay",
225
+ onPress: onRequestClose,
226
+ style: StyleSheet.absoluteFill,
227
+ testID: "variantlab-scrim"
228
+ }
229
+ ) }),
230
+ /* @__PURE__ */ jsx(
231
+ Animated.View,
232
+ {
233
+ style: [
234
+ styles2.sheet,
235
+ {
236
+ backgroundColor: theme.background,
237
+ borderColor: theme.border,
238
+ paddingBottom: insetBottom + 12,
239
+ transform: [{ translateY }]
240
+ }
241
+ ],
242
+ testID: "variantlab-bottom-sheet",
243
+ children
244
+ }
245
+ )
246
+ ] })
247
+ }
248
+ );
249
+ }
250
+ var styles2 = StyleSheet.create({
251
+ root: {
252
+ flex: 1,
253
+ justifyContent: "flex-end"
254
+ },
255
+ scrim: {
256
+ position: "absolute",
257
+ top: 0,
258
+ left: 0,
259
+ right: 0,
260
+ bottom: 0,
261
+ backgroundColor: "rgba(0,0,0,0.5)"
262
+ },
263
+ sheet: {
264
+ borderTopLeftRadius: 16,
265
+ borderTopRightRadius: 16,
266
+ borderTopWidth: 1,
267
+ borderLeftWidth: 1,
268
+ borderRightWidth: 1,
269
+ paddingHorizontal: 16,
270
+ paddingTop: 16,
271
+ maxHeight: "85%"
272
+ }
273
+ });
274
+ function FloatingButton({
275
+ theme,
276
+ corner,
277
+ offset,
278
+ count,
279
+ onPress
280
+ }) {
281
+ const positionStyle = cornerStyle(corner, offset);
282
+ return /* @__PURE__ */ jsxs(
283
+ Pressable,
284
+ {
285
+ accessibilityRole: "button",
286
+ accessibilityLabel: "Open variantlab debug overlay",
287
+ onPress,
288
+ style: [styles3.button, positionStyle, { backgroundColor: theme.accent }],
289
+ hitSlop: 8,
290
+ testID: "variantlab-floating-button",
291
+ children: [
292
+ /* @__PURE__ */ jsx(BeakerIcon, { color: theme.accentText }),
293
+ count > 0 ? /* @__PURE__ */ jsx(View, { style: [styles3.badge, { backgroundColor: theme.surface, borderColor: theme.accent }], children: /* @__PURE__ */ jsx(Text, { style: [styles3.badgeText, { color: theme.text }], children: String(count) }) }) : null
294
+ ]
295
+ }
296
+ );
297
+ }
298
+ function BeakerIcon({ color }) {
299
+ return /* @__PURE__ */ jsxs(View, { style: styles3.iconWrap, children: [
300
+ /* @__PURE__ */ jsx(View, { style: [styles3.iconNeck, { backgroundColor: color }] }),
301
+ /* @__PURE__ */ jsx(View, { style: [styles3.iconBody, { borderColor: color }] })
302
+ ] });
303
+ }
304
+ function cornerStyle(corner, offset) {
305
+ switch (corner) {
306
+ case "top-left":
307
+ return { top: offset.y, left: offset.x };
308
+ case "top-right":
309
+ return { top: offset.y, right: offset.x };
310
+ case "bottom-left":
311
+ return { bottom: offset.y, left: offset.x };
312
+ case "bottom-right":
313
+ return { bottom: offset.y, right: offset.x };
314
+ }
315
+ }
316
+ var styles3 = StyleSheet.create({
317
+ button: {
318
+ position: "absolute",
319
+ width: 48,
320
+ height: 48,
321
+ borderRadius: 24,
322
+ alignItems: "center",
323
+ justifyContent: "center",
324
+ elevation: 4,
325
+ shadowColor: "#000",
326
+ shadowOpacity: 0.3,
327
+ shadowRadius: 6,
328
+ shadowOffset: { width: 0, height: 2 }
329
+ },
330
+ badge: {
331
+ position: "absolute",
332
+ top: -4,
333
+ right: -4,
334
+ minWidth: 18,
335
+ height: 18,
336
+ borderRadius: 9,
337
+ paddingHorizontal: 4,
338
+ borderWidth: 1,
339
+ alignItems: "center",
340
+ justifyContent: "center"
341
+ },
342
+ badgeText: {
343
+ fontSize: 10,
344
+ fontWeight: "700"
345
+ },
346
+ iconWrap: {
347
+ width: 24,
348
+ height: 24,
349
+ alignItems: "center",
350
+ justifyContent: "flex-end"
351
+ },
352
+ iconNeck: {
353
+ width: 4,
354
+ height: 6,
355
+ marginBottom: 0
356
+ },
357
+ iconBody: {
358
+ width: 18,
359
+ height: 14,
360
+ borderWidth: 2,
361
+ borderRadius: 3,
362
+ borderTopWidth: 0
363
+ }
364
+ });
365
+ function SearchInput({
366
+ theme,
367
+ value,
368
+ onChange,
369
+ placeholder
370
+ }) {
371
+ return /* @__PURE__ */ jsx(View, { style: [styles4.wrapper, { backgroundColor: theme.surface, borderColor: theme.border }], children: /* @__PURE__ */ jsx(
372
+ TextInput,
373
+ {
374
+ accessibilityLabel: "Filter experiments",
375
+ value,
376
+ onChangeText: onChange,
377
+ placeholder: placeholder ?? "Search experiments\u2026",
378
+ placeholderTextColor: theme.textMuted,
379
+ autoCorrect: false,
380
+ autoCapitalize: "none",
381
+ style: [styles4.input, { color: theme.text }],
382
+ testID: "variantlab-search-input"
383
+ }
384
+ ) });
385
+ }
386
+ var styles4 = StyleSheet.create({
387
+ wrapper: {
388
+ borderRadius: 8,
389
+ borderWidth: 1,
390
+ paddingHorizontal: 10,
391
+ paddingVertical: 6
392
+ },
393
+ input: {
394
+ fontSize: 14,
395
+ paddingVertical: 4
396
+ }
397
+ });
398
+ function ConfigTab({ theme, config }) {
399
+ const enabled = config.enabled !== false;
400
+ return /* @__PURE__ */ jsxs(
401
+ ScrollView,
402
+ {
403
+ style: styles5.scroll,
404
+ contentContainerStyle: styles5.content,
405
+ testID: "variantlab-config-view",
406
+ children: [
407
+ /* @__PURE__ */ jsx(SummaryRow, { theme, label: "Version", value: String(config.version) }),
408
+ /* @__PURE__ */ jsx(SummaryRow, { theme, label: "Status", value: enabled ? "enabled" : "kill-switched" }),
409
+ /* @__PURE__ */ jsx(SummaryRow, { theme, label: "Experiments", value: String(config.experiments.length) }),
410
+ /* @__PURE__ */ jsx(View, { style: [styles5.section, { borderColor: theme.border, backgroundColor: theme.surface }], children: config.experiments.map((exp) => /* @__PURE__ */ jsxs(View, { style: [styles5.row, { borderBottomColor: theme.border }], children: [
411
+ /* @__PURE__ */ jsx(Text, { style: [styles5.rowId, { color: theme.text }], numberOfLines: 1, children: exp.id }),
412
+ /* @__PURE__ */ jsxs(Text, { style: [styles5.rowMeta, { color: theme.textMuted }], numberOfLines: 1, children: [
413
+ exp.status ?? "active",
414
+ " \xB7 default = ",
415
+ exp.default
416
+ ] })
417
+ ] }, exp.id)) })
418
+ ]
419
+ }
420
+ );
421
+ }
422
+ function SummaryRow({
423
+ theme,
424
+ label,
425
+ value
426
+ }) {
427
+ return /* @__PURE__ */ jsxs(View, { style: styles5.summaryRow, children: [
428
+ /* @__PURE__ */ jsx(Text, { style: [styles5.summaryLabel, { color: theme.textMuted }], children: label }),
429
+ /* @__PURE__ */ jsx(Text, { style: [styles5.summaryValue, { color: theme.text }], children: value })
430
+ ] });
431
+ }
432
+ var styles5 = StyleSheet.create({
433
+ scroll: { flexGrow: 0 },
434
+ content: { paddingVertical: 8, gap: 8 },
435
+ summaryRow: {
436
+ flexDirection: "row",
437
+ justifyContent: "space-between",
438
+ paddingHorizontal: 4
439
+ },
440
+ summaryLabel: { fontSize: 12 },
441
+ summaryValue: { fontSize: 12, fontWeight: "600" },
442
+ section: {
443
+ borderRadius: 8,
444
+ borderWidth: 1,
445
+ paddingVertical: 4
446
+ },
447
+ row: {
448
+ paddingHorizontal: 12,
449
+ paddingVertical: 8,
450
+ borderBottomWidth: 1
451
+ },
452
+ rowId: { fontSize: 12, fontWeight: "600" },
453
+ rowMeta: { fontSize: 11, marginTop: 2 }
454
+ });
455
+ function ContextTab({ theme, context }) {
456
+ const json = stringifyContext(context);
457
+ return /* @__PURE__ */ jsx(
458
+ ScrollView,
459
+ {
460
+ style: styles6.scroll,
461
+ contentContainerStyle: styles6.content,
462
+ testID: "variantlab-context-view",
463
+ children: /* @__PURE__ */ jsx(Text, { style: [styles6.json, { color: theme.text }], children: json })
464
+ }
465
+ );
466
+ }
467
+ function stringifyContext(context) {
468
+ const masked = { ...context };
469
+ if (typeof masked.userId === "string" && masked.userId.length > 0) {
470
+ masked.userId = maskId(masked.userId);
471
+ }
472
+ return JSON.stringify(masked, null, 2);
473
+ }
474
+ function maskId(id) {
475
+ if (id.length <= 4) return "***";
476
+ return `${id.slice(0, 2)}\u2026${id.slice(-2)}`;
477
+ }
478
+ var styles6 = StyleSheet.create({
479
+ scroll: { flexGrow: 0 },
480
+ content: { paddingVertical: 8 },
481
+ json: {
482
+ fontSize: 11,
483
+ fontFamily: "Menlo, monospace",
484
+ lineHeight: 16
485
+ }
486
+ });
487
+ function HistoryTab({ theme, events }) {
488
+ const ordered = useMemo(() => {
489
+ const list = [];
490
+ for (let i = events.length - 1; i >= 0; i--) {
491
+ const event = events[i];
492
+ if (event !== void 0) list.push({ seq: i, event });
493
+ }
494
+ return list;
495
+ }, [events]);
496
+ if (ordered.length === 0) {
497
+ return /* @__PURE__ */ jsx(View, { style: styles7.empty, testID: "variantlab-history-empty", children: /* @__PURE__ */ jsx(Text, { style: [styles7.emptyText, { color: theme.textMuted }], children: "No events yet." }) });
498
+ }
499
+ return /* @__PURE__ */ jsx(
500
+ ScrollView,
501
+ {
502
+ style: styles7.scroll,
503
+ contentContainerStyle: styles7.content,
504
+ testID: "variantlab-history-list",
505
+ children: ordered.map(({ seq, event }) => /* @__PURE__ */ jsxs(
506
+ View,
507
+ {
508
+ style: [styles7.row, { borderColor: theme.border, backgroundColor: theme.surface }],
509
+ children: [
510
+ /* @__PURE__ */ jsx(Text, { style: [styles7.type, { color: theme.accent }], children: event.type }),
511
+ /* @__PURE__ */ jsx(Text, { style: [styles7.detail, { color: theme.text }], numberOfLines: 2, children: summarize(event) })
512
+ ]
513
+ },
514
+ `${seq}:${event.type}`
515
+ ))
516
+ }
517
+ );
518
+ }
519
+ function summarize(event) {
520
+ switch (event.type) {
521
+ case "ready":
522
+ return `engine ready \xB7 ${event.config.experiments.length} experiments`;
523
+ case "assignment":
524
+ return `${event.experimentId} \u2192 ${event.variantId}`;
525
+ case "exposure":
526
+ return `${event.experimentId} \u2192 ${event.variantId} (exposure)`;
527
+ case "variantChanged":
528
+ return `${event.experimentId} \u2192 ${event.variantId} (${event.source})`;
529
+ case "rollback":
530
+ return `${event.experimentId} rolled back: ${event.reason}`;
531
+ case "configLoaded":
532
+ return `config reloaded \xB7 ${event.config.experiments.length} experiments`;
533
+ case "contextUpdated":
534
+ return "context updated";
535
+ case "error":
536
+ return `error: ${event.error.message}`;
537
+ }
538
+ }
539
+ var styles7 = StyleSheet.create({
540
+ scroll: { flexGrow: 0 },
541
+ content: { paddingVertical: 8, gap: 6 },
542
+ row: {
543
+ borderRadius: 8,
544
+ borderWidth: 1,
545
+ paddingHorizontal: 10,
546
+ paddingVertical: 6
547
+ },
548
+ type: {
549
+ fontSize: 10,
550
+ fontWeight: "700",
551
+ textTransform: "uppercase",
552
+ letterSpacing: 0.5
553
+ },
554
+ detail: {
555
+ fontSize: 12,
556
+ marginTop: 2
557
+ },
558
+ empty: { paddingVertical: 32, alignItems: "center" },
559
+ emptyText: { fontSize: 12 }
560
+ });
561
+ function OverviewTab({
562
+ theme,
563
+ engine,
564
+ experiments,
565
+ variantsById
566
+ }) {
567
+ const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
568
+ const toggle = useCallback((id) => {
569
+ setExpanded((prev) => {
570
+ const next = new Set(prev);
571
+ if (next.has(id)) {
572
+ next.delete(id);
573
+ } else {
574
+ next.add(id);
575
+ }
576
+ return next;
577
+ });
578
+ }, []);
579
+ if (experiments.length === 0) {
580
+ return /* @__PURE__ */ jsxs(View, { style: styles8.empty, testID: "variantlab-overview-empty", children: [
581
+ /* @__PURE__ */ jsx(Text, { style: [styles8.emptyTitle, { color: theme.text }], children: "No experiments match this view." }),
582
+ /* @__PURE__ */ jsx(Text, { style: [styles8.emptyHint, { color: theme.textMuted }], children: 'Clear the search or switch to "All experiments".' })
583
+ ] });
584
+ }
585
+ return /* @__PURE__ */ jsx(
586
+ ScrollView,
587
+ {
588
+ style: styles8.scroll,
589
+ contentContainerStyle: styles8.content,
590
+ keyboardShouldPersistTaps: "handled",
591
+ testID: "variantlab-overview-list",
592
+ children: experiments.map((exp) => /* @__PURE__ */ jsx(
593
+ ExperimentCard,
594
+ {
595
+ theme,
596
+ experiment: exp,
597
+ activeVariantId: variantsById[exp.id] ?? exp.default,
598
+ expanded: expanded.has(exp.id),
599
+ onToggleExpand: () => toggle(exp.id),
600
+ onSelect: (variantId) => engine.setVariant(exp.id, variantId, "user"),
601
+ onReset: () => engine.clearVariant(exp.id)
602
+ },
603
+ exp.id
604
+ ))
605
+ }
606
+ );
607
+ }
608
+ var styles8 = StyleSheet.create({
609
+ scroll: {
610
+ flexGrow: 0
611
+ },
612
+ content: {
613
+ paddingVertical: 8
614
+ },
615
+ empty: {
616
+ paddingVertical: 32,
617
+ alignItems: "center",
618
+ gap: 4
619
+ },
620
+ emptyTitle: {
621
+ fontSize: 14,
622
+ fontWeight: "600"
623
+ },
624
+ emptyHint: {
625
+ fontSize: 12
626
+ }
627
+ });
628
+
629
+ // src/overlay/theme.ts
630
+ var DEFAULT_THEME = {
631
+ background: "#0b0b10",
632
+ surface: "#161620",
633
+ border: "#272735",
634
+ text: "#f4f4f5",
635
+ textMuted: "#9ca3af",
636
+ accent: "#8b5cf6",
637
+ accentText: "#ffffff",
638
+ danger: "#f43f5e"
639
+ };
640
+ function mergeTheme(base, patch) {
641
+ if (patch === void 0) return base;
642
+ return { ...base, ...patch };
643
+ }
644
+ function useEngineSnapshot(engine, select, isEqual = Object.is) {
645
+ const cache = useRef(null);
646
+ const subscribe = useCallback(
647
+ (notify) => {
648
+ const unsub = engine.subscribe((_event) => notify());
649
+ return unsub;
650
+ },
651
+ [engine]
652
+ );
653
+ const getSnapshot = useCallback(() => {
654
+ const next = select(engine);
655
+ const prev = cache.current;
656
+ if (prev !== null && isEqual(prev.value, next)) {
657
+ return prev.value;
658
+ }
659
+ cache.current = { value: next };
660
+ return next;
661
+ }, [engine, select, isEqual]);
662
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
663
+ }
664
+ function VariantDebugOverlay(props) {
665
+ if (!shouldRender(props.forceEnable)) {
666
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "production") {
667
+ console.warn(
668
+ "[variantlab] VariantDebugOverlay rendered in production. Pass forceEnable={true} if this is intentional."
669
+ );
670
+ }
671
+ return null;
672
+ }
673
+ return /* @__PURE__ */ jsx(OverlayImpl, { ...props });
674
+ }
675
+ function shouldRender(forceEnable) {
676
+ if (forceEnable === true) return true;
677
+ if (typeof __DEV__ !== "undefined" && __DEV__ === true) return true;
678
+ if (typeof process !== "undefined" && process.env?.NODE_ENV === "development") return true;
679
+ return false;
680
+ }
681
+ function OverlayImpl(props) {
682
+ const engine = useVariantLabEngine();
683
+ const theme = useMemo(() => mergeTheme(DEFAULT_THEME, props.theme), [props.theme]);
684
+ const corner = props.position ?? "bottom-right";
685
+ const offset = props.offset ?? { x: 16, y: 80 };
686
+ const [visible, setVisible] = useState(false);
687
+ const [tab, setTab] = useState("overview");
688
+ const [query, setQuery] = useState("");
689
+ const [routeFilterActive, setRouteFilterActive] = useState(props.routeFilter !== false);
690
+ useEffect(() => registerOverlay(setVisible), []);
691
+ const snapshot = useEngineSnapshot(
692
+ engine,
693
+ useCallback((e) => {
694
+ const ctx = e.getContext();
695
+ const route = typeof ctx.route === "string" ? ctx.route : void 0;
696
+ const all = e.getExperiments();
697
+ const scoped = e.getExperiments(route);
698
+ const variantsById = {};
699
+ for (const exp of all) {
700
+ variantsById[exp.id] = e.getVariant(exp.id);
701
+ }
702
+ return {
703
+ all,
704
+ scoped,
705
+ variantsById,
706
+ context: ctx,
707
+ config: e.getConfig(),
708
+ history: e.getHistory()
709
+ };
710
+ }, []),
711
+ snapshotEqual
712
+ );
713
+ const visibleExperiments = useMemo(() => {
714
+ const source = routeFilterActive ? snapshot.scoped : snapshot.all;
715
+ return filterExperiments(source, query);
716
+ }, [routeFilterActive, snapshot.scoped, snapshot.all, query]);
717
+ const handleResetAll = useCallback(() => engine.resetAll(), [engine]);
718
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
719
+ props.hideButton === true ? null : /* @__PURE__ */ jsx(
720
+ FloatingButton,
721
+ {
722
+ theme,
723
+ corner,
724
+ offset,
725
+ count: snapshot.scoped.length,
726
+ onPress: () => setVisible(true)
727
+ }
728
+ ),
729
+ /* @__PURE__ */ jsxs(
730
+ BottomSheet,
731
+ {
732
+ visible,
733
+ onRequestClose: () => setVisible(false),
734
+ theme,
735
+ insetBottom: props.safeAreaBottom ?? 0,
736
+ children: [
737
+ /* @__PURE__ */ jsx(Header, { theme, onClose: () => setVisible(false), onResetAll: handleResetAll }),
738
+ /* @__PURE__ */ jsx(SearchInput, { theme, value: query, onChange: setQuery }),
739
+ /* @__PURE__ */ jsx(
740
+ RouteToggle,
741
+ {
742
+ theme,
743
+ active: routeFilterActive,
744
+ onToggle: () => setRouteFilterActive((v) => !v)
745
+ }
746
+ ),
747
+ /* @__PURE__ */ jsx(TabBar, { theme, value: tab, onChange: setTab }),
748
+ /* @__PURE__ */ jsxs(View, { style: styles9.body, children: [
749
+ tab === "overview" ? /* @__PURE__ */ jsx(
750
+ OverviewTab,
751
+ {
752
+ theme,
753
+ engine,
754
+ experiments: visibleExperiments,
755
+ variantsById: snapshot.variantsById
756
+ }
757
+ ) : null,
758
+ tab === "context" ? /* @__PURE__ */ jsx(ContextTab, { theme, context: snapshot.context }) : null,
759
+ tab === "config" ? /* @__PURE__ */ jsx(ConfigTab, { theme, config: snapshot.config }) : null,
760
+ tab === "history" ? /* @__PURE__ */ jsx(HistoryTab, { theme, events: snapshot.history }) : null
761
+ ] })
762
+ ]
763
+ }
764
+ )
765
+ ] });
766
+ }
767
+ function snapshotEqual(a, b) {
768
+ if (a.config !== b.config) return false;
769
+ if (!sameRefArray(a.all, b.all)) return false;
770
+ if (!sameRefArray(a.scoped, b.scoped)) return false;
771
+ if (!sameRefArray(a.history, b.history)) return false;
772
+ if (!sameRefRecord(a.variantsById, b.variantsById)) return false;
773
+ if (!sameRefRecord(a.context, b.context)) {
774
+ return false;
775
+ }
776
+ return true;
777
+ }
778
+ function sameRefArray(a, b) {
779
+ if (a === b) return true;
780
+ if (a.length !== b.length) return false;
781
+ for (let i = 0; i < a.length; i++) {
782
+ if (a[i] !== b[i]) return false;
783
+ }
784
+ return true;
785
+ }
786
+ function sameRefRecord(a, b) {
787
+ if (a === b) return true;
788
+ const aKeys = Object.keys(a);
789
+ const bKeys = Object.keys(b);
790
+ if (aKeys.length !== bKeys.length) return false;
791
+ for (const k of aKeys) {
792
+ if (a[k] !== b[k]) return false;
793
+ }
794
+ return true;
795
+ }
796
+ function Header({
797
+ theme,
798
+ onClose,
799
+ onResetAll
800
+ }) {
801
+ return /* @__PURE__ */ jsxs(View, { style: styles9.header, children: [
802
+ /* @__PURE__ */ jsx(Text, { style: [styles9.title, { color: theme.text }], children: "variantlab" }),
803
+ /* @__PURE__ */ jsxs(View, { style: styles9.headerActions, children: [
804
+ /* @__PURE__ */ jsx(
805
+ Pressable,
806
+ {
807
+ onPress: onResetAll,
808
+ accessibilityRole: "button",
809
+ accessibilityLabel: "Reset all overrides",
810
+ style: [styles9.headerButton, { borderColor: theme.border }],
811
+ testID: "variantlab-reset-all",
812
+ children: /* @__PURE__ */ jsx(Text, { style: [styles9.headerButtonText, { color: theme.textMuted }], children: "Reset" })
813
+ }
814
+ ),
815
+ /* @__PURE__ */ jsx(
816
+ Pressable,
817
+ {
818
+ onPress: onClose,
819
+ accessibilityRole: "button",
820
+ accessibilityLabel: "Close debug overlay",
821
+ style: [styles9.headerButton, { borderColor: theme.border }],
822
+ testID: "variantlab-close",
823
+ children: /* @__PURE__ */ jsx(Text, { style: [styles9.headerButtonText, { color: theme.textMuted }], children: "Close" })
824
+ }
825
+ )
826
+ ] })
827
+ ] });
828
+ }
829
+ function RouteToggle({
830
+ theme,
831
+ active,
832
+ onToggle
833
+ }) {
834
+ return /* @__PURE__ */ jsx(
835
+ Pressable,
836
+ {
837
+ onPress: onToggle,
838
+ accessibilityRole: "button",
839
+ accessibilityLabel: active ? "Show all experiments" : "Show only current route",
840
+ style: styles9.routeToggle,
841
+ testID: "variantlab-route-toggle",
842
+ children: /* @__PURE__ */ jsx(Text, { style: [styles9.routeToggleText, { color: theme.textMuted }], children: active ? "Showing: current route" : "Showing: all experiments" })
843
+ }
844
+ );
845
+ }
846
+ function TabBar({
847
+ theme,
848
+ value,
849
+ onChange
850
+ }) {
851
+ const tabs = ["overview", "context", "config", "history"];
852
+ return /* @__PURE__ */ jsx(View, { style: [styles9.tabBar, { borderColor: theme.border }], children: tabs.map((t) => {
853
+ const isActive = t === value;
854
+ return /* @__PURE__ */ jsx(
855
+ Pressable,
856
+ {
857
+ onPress: () => onChange(t),
858
+ accessibilityRole: "button",
859
+ accessibilityState: { selected: isActive },
860
+ accessibilityLabel: `${t} tab`,
861
+ style: [
862
+ styles9.tab,
863
+ isActive ? { borderBottomColor: theme.accent } : { borderBottomColor: "transparent" }
864
+ ],
865
+ testID: `variantlab-tab-${t}`,
866
+ children: /* @__PURE__ */ jsx(Text, { style: [styles9.tabText, { color: isActive ? theme.text : theme.textMuted }], children: t })
867
+ },
868
+ t
869
+ );
870
+ }) });
871
+ }
872
+ var styles9 = StyleSheet.create({
873
+ header: {
874
+ flexDirection: "row",
875
+ alignItems: "center",
876
+ justifyContent: "space-between",
877
+ paddingBottom: 12
878
+ },
879
+ title: {
880
+ fontSize: 16,
881
+ fontWeight: "700"
882
+ },
883
+ headerActions: {
884
+ flexDirection: "row",
885
+ gap: 8
886
+ },
887
+ headerButton: {
888
+ paddingHorizontal: 10,
889
+ paddingVertical: 4,
890
+ borderRadius: 6,
891
+ borderWidth: 1
892
+ },
893
+ headerButtonText: {
894
+ fontSize: 11,
895
+ fontWeight: "600"
896
+ },
897
+ routeToggle: {
898
+ paddingVertical: 8
899
+ },
900
+ routeToggleText: {
901
+ fontSize: 11
902
+ },
903
+ tabBar: {
904
+ flexDirection: "row",
905
+ borderTopWidth: 1,
906
+ marginTop: 4
907
+ },
908
+ tab: {
909
+ flex: 1,
910
+ alignItems: "center",
911
+ paddingVertical: 10,
912
+ borderBottomWidth: 2
913
+ },
914
+ tabText: {
915
+ fontSize: 12,
916
+ fontWeight: "600",
917
+ textTransform: "capitalize"
918
+ },
919
+ body: {
920
+ flexShrink: 1
921
+ }
922
+ });
923
+
924
+ export { DEFAULT_THEME, VariantDebugOverlay, closeDebugOverlay, describeAssignmentSource, filterExperiments, isVisibleExperiment, matchesSearch, mergeTheme, openDebugOverlay, registerOverlay, shouldRender, stringifyContext, summarize as summarizeEvent };
925
+ //# sourceMappingURL=debug.js.map
926
+ //# sourceMappingURL=debug.js.map