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