@buoy-gg/shared-ui 3.0.1 → 4.0.1

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 (133) hide show
  1. package/lib/commonjs/JsModal.js +2 -1
  2. package/lib/commonjs/clipboard/clipboard-impl.js +28 -2
  3. package/lib/commonjs/dataViewer/VirtualizedDataExplorer.js +3 -5
  4. package/lib/commonjs/hooks/safe-area-impl.js +1 -1
  5. package/lib/commonjs/icons/lucide-icons.js +130 -22
  6. package/lib/commonjs/index.js +14 -0
  7. package/lib/commonjs/license/DeviceLimitModal.js +2 -1
  8. package/lib/commonjs/license/FeatureGate.js +60 -11
  9. package/lib/commonjs/license/LicenseEntryModal.js +14 -3
  10. package/lib/commonjs/license/ManageDevicesModal.js +2 -1
  11. package/lib/commonjs/license/openPricing.js +36 -0
  12. package/lib/commonjs/storage/devToolsStorageKeys.js +1 -0
  13. package/lib/commonjs/stores/BaseEventStore.js +72 -2
  14. package/lib/commonjs/stores/ignoredPatternsStore.js +229 -0
  15. package/lib/commonjs/stores/index.js +26 -1
  16. package/lib/commonjs/ui/components/CompactRow.js +73 -66
  17. package/lib/commonjs/ui/components/EventHistoryViewer/EventPickerModal.js +3 -2
  18. package/lib/commonjs/ui/components/ExpandableSectionWithModal.js +2 -1
  19. package/lib/commonjs/ui/components/ExpandedInfoRow.js +13 -3
  20. package/lib/commonjs/ui/components/WindowControls.js +9 -3
  21. package/lib/commonjs/ui/console/CyberpunkConsoleSection.js +6 -5
  22. package/lib/commonjs/ui/console/GalaxyButton.js +2 -1
  23. package/lib/commonjs/ui/gameUI/components/GameUIStatusHeader.js +2 -1
  24. package/lib/commonjs/utils/absoluteFill.js +28 -0
  25. package/lib/commonjs/utils/index.js +13 -0
  26. package/lib/commonjs/utils/safeExpoRouter.js +59 -4
  27. package/lib/module/JsModal.js +2 -1
  28. package/lib/module/clipboard/clipboard-impl.js +28 -2
  29. package/lib/module/dataViewer/VirtualizedDataExplorer.js +3 -5
  30. package/lib/module/hooks/safe-area-impl.js +1 -1
  31. package/lib/module/icons/lucide-icons.js +125 -19
  32. package/lib/module/index.js +3 -1
  33. package/lib/module/license/DeviceLimitModal.js +2 -1
  34. package/lib/module/license/FeatureGate.js +61 -12
  35. package/lib/module/license/LicenseEntryModal.js +15 -4
  36. package/lib/module/license/ManageDevicesModal.js +2 -1
  37. package/lib/module/license/openPricing.js +31 -0
  38. package/lib/module/storage/devToolsStorageKeys.js +1 -0
  39. package/lib/module/stores/BaseEventStore.js +72 -2
  40. package/lib/module/stores/ignoredPatternsStore.js +223 -0
  41. package/lib/module/stores/index.js +2 -1
  42. package/lib/module/ui/components/CompactRow.js +73 -66
  43. package/lib/module/ui/components/EventHistoryViewer/EventPickerModal.js +3 -2
  44. package/lib/module/ui/components/ExpandableSectionWithModal.js +2 -1
  45. package/lib/module/ui/components/ExpandedInfoRow.js +13 -3
  46. package/lib/module/ui/components/WindowControls.js +10 -4
  47. package/lib/module/ui/console/CyberpunkConsoleSection.js +6 -5
  48. package/lib/module/ui/console/GalaxyButton.js +2 -1
  49. package/lib/module/ui/gameUI/components/GameUIStatusHeader.js +2 -1
  50. package/lib/module/utils/absoluteFill.js +24 -0
  51. package/lib/module/utils/index.js +2 -1
  52. package/lib/module/utils/safeExpoRouter.js +58 -4
  53. package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/clipboard/clipboard-impl.d.ts +3 -2
  55. package/lib/typescript/commonjs/clipboard/clipboard-impl.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
  58. package/lib/typescript/commonjs/icons/lucide-icons.d.ts +5 -2
  59. package/lib/typescript/commonjs/icons/lucide-icons.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/index.d.ts +1 -1
  61. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  62. package/lib/typescript/commonjs/license/DeviceLimitModal.d.ts.map +1 -1
  63. package/lib/typescript/commonjs/license/FeatureGate.d.ts +14 -1
  64. package/lib/typescript/commonjs/license/FeatureGate.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/license/LicenseEntryModal.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/license/ManageDevicesModal.d.ts.map +1 -1
  67. package/lib/typescript/commonjs/license/openPricing.d.ts +14 -0
  68. package/lib/typescript/commonjs/license/openPricing.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +1 -0
  70. package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/stores/BaseEventStore.d.ts +28 -0
  72. package/lib/typescript/commonjs/stores/BaseEventStore.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/stores/ignoredPatternsStore.d.ts +84 -0
  74. package/lib/typescript/commonjs/stores/ignoredPatternsStore.d.ts.map +1 -0
  75. package/lib/typescript/commonjs/stores/index.d.ts +1 -0
  76. package/lib/typescript/commonjs/stores/index.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +3 -1
  78. package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/ui/components/EventHistoryViewer/EventPickerModal.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/ui/components/ExpandableSectionWithModal.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts +3 -1
  82. package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/ui/components/WindowControls.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/ui/console/CyberpunkConsoleSection.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/ui/console/GalaxyButton.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/ui/gameUI/components/GameUIStatusHeader.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/utils/absoluteFill.d.ts +18 -0
  88. package/lib/typescript/commonjs/utils/absoluteFill.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/utils/index.d.ts +2 -1
  90. package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts +9 -0
  92. package/lib/typescript/commonjs/utils/safeExpoRouter.d.ts.map +1 -1
  93. package/lib/typescript/module/JsModal.d.ts.map +1 -1
  94. package/lib/typescript/module/clipboard/clipboard-impl.d.ts +3 -2
  95. package/lib/typescript/module/clipboard/clipboard-impl.d.ts.map +1 -1
  96. package/lib/typescript/module/dataViewer/VirtualizedDataExplorer.d.ts.map +1 -1
  97. package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
  98. package/lib/typescript/module/icons/lucide-icons.d.ts +5 -2
  99. package/lib/typescript/module/icons/lucide-icons.d.ts.map +1 -1
  100. package/lib/typescript/module/index.d.ts +1 -1
  101. package/lib/typescript/module/index.d.ts.map +1 -1
  102. package/lib/typescript/module/license/DeviceLimitModal.d.ts.map +1 -1
  103. package/lib/typescript/module/license/FeatureGate.d.ts +14 -1
  104. package/lib/typescript/module/license/FeatureGate.d.ts.map +1 -1
  105. package/lib/typescript/module/license/LicenseEntryModal.d.ts.map +1 -1
  106. package/lib/typescript/module/license/ManageDevicesModal.d.ts.map +1 -1
  107. package/lib/typescript/module/license/openPricing.d.ts +14 -0
  108. package/lib/typescript/module/license/openPricing.d.ts.map +1 -0
  109. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +1 -0
  110. package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
  111. package/lib/typescript/module/stores/BaseEventStore.d.ts +28 -0
  112. package/lib/typescript/module/stores/BaseEventStore.d.ts.map +1 -1
  113. package/lib/typescript/module/stores/ignoredPatternsStore.d.ts +84 -0
  114. package/lib/typescript/module/stores/ignoredPatternsStore.d.ts.map +1 -0
  115. package/lib/typescript/module/stores/index.d.ts +1 -0
  116. package/lib/typescript/module/stores/index.d.ts.map +1 -1
  117. package/lib/typescript/module/ui/components/CompactRow.d.ts +3 -1
  118. package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
  119. package/lib/typescript/module/ui/components/EventHistoryViewer/EventPickerModal.d.ts.map +1 -1
  120. package/lib/typescript/module/ui/components/ExpandableSectionWithModal.d.ts.map +1 -1
  121. package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts +3 -1
  122. package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts.map +1 -1
  123. package/lib/typescript/module/ui/components/WindowControls.d.ts.map +1 -1
  124. package/lib/typescript/module/ui/console/CyberpunkConsoleSection.d.ts.map +1 -1
  125. package/lib/typescript/module/ui/console/GalaxyButton.d.ts.map +1 -1
  126. package/lib/typescript/module/ui/gameUI/components/GameUIStatusHeader.d.ts.map +1 -1
  127. package/lib/typescript/module/utils/absoluteFill.d.ts +18 -0
  128. package/lib/typescript/module/utils/absoluteFill.d.ts.map +1 -0
  129. package/lib/typescript/module/utils/index.d.ts +2 -1
  130. package/lib/typescript/module/utils/index.d.ts.map +1 -1
  131. package/lib/typescript/module/utils/safeExpoRouter.d.ts +9 -0
  132. package/lib/typescript/module/utils/safeExpoRouter.d.ts.map +1 -1
  133. package/package.json +4 -4
@@ -7,24 +7,40 @@
7
7
  */
8
8
 
9
9
  import React, { useState, useCallback } from "react";
10
- import { View, Text, StyleSheet, TouchableOpacity, Linking, Modal } from "react-native";
10
+ import { View, Text, StyleSheet, TouchableOpacity, Modal } from "react-native";
11
11
  import { gameUIColors, buoyColors } from "../ui/gameUI/constants/gameUIColors.js";
12
12
  import { LockIcon, X, Zap, Shield, Clock, Check } from "../icons/lucide-icons.js";
13
13
  import { LicenseEntryModal } from "./LicenseEntryModal.js";
14
- import { useIsPro, useLicense } from "@buoy-gg/license";
14
+ import { openPricing, isWeb } from "./openPricing.js";
15
+ import { useIsPro, useLicense, useProAccess } from "@buoy-gg/license";
15
16
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
17
+ // Weekend Pass branding — a distinct violet so it never reads as the gold PRO.
18
+ export const WEEKEND_PASS_LABEL = "WEEKEND PASS";
19
+ const WEEKEND_VIOLET = "#BF5AF2";
20
+
16
21
  /**
17
- * Simple Pro badge for marking premium features
22
+ * Simple Pro badge for marking premium features.
23
+ *
24
+ * Reason-aware: when the free Weekend Pass is the active unlock (a free user on
25
+ * a weekend), it shows a violet "WEEKEND PASS" badge instead of the gold "PRO"
26
+ * so people know Pro is free this weekend. A real license — or a free user on a
27
+ * weekday seeing the badge as a Pro-feature marker — shows "PRO".
18
28
  */
19
29
  export const ProBadge = ({
20
30
  style
21
- }) => /*#__PURE__*/_jsx(View, {
22
- style: [styles.proBadge, style],
23
- children: /*#__PURE__*/_jsx(Text, {
24
- style: styles.proBadgeText,
25
- children: "PRO"
26
- })
27
- });
31
+ }) => {
32
+ const {
33
+ reason
34
+ } = useProAccess();
35
+ const weekend = reason === "weekend";
36
+ return /*#__PURE__*/_jsx(View, {
37
+ style: [styles.proBadge, weekend && styles.weekendBadge, style],
38
+ children: /*#__PURE__*/_jsx(Text, {
39
+ style: [styles.proBadgeText, weekend && styles.weekendBadgeText],
40
+ children: weekend ? WEEKEND_PASS_LABEL : "PRO"
41
+ })
42
+ });
43
+ };
28
44
 
29
45
  /**
30
46
  * Simple Pro upgrade modal - minimal modal with just upgrade button
@@ -56,6 +72,10 @@ export const UpgradePrompt = ({
56
72
  const handlePress = useCallback(() => {
57
73
  if (onUpgradePress) {
58
74
  onUpgradePress();
75
+ } else if (isWeb) {
76
+ // Desktop dashboard: buy on the website, not via the mobile Buoy.init()
77
+ // code path. (Already have a key? Enter it from the toolbar Upgrade button.)
78
+ openPricing();
59
79
  } else {
60
80
  setShowLicenseModal(true);
61
81
  }
@@ -181,13 +201,21 @@ export const FeatureGate = ({
181
201
  * Returns { hasAccess, isPro, showUpgrade }
182
202
  */
183
203
  export function useFeatureGate() {
184
- const isPro = useIsPro();
204
+ const {
205
+ isPro,
206
+ isLicensed,
207
+ isWeekendFree,
208
+ reason
209
+ } = useProAccess();
185
210
  const showUpgrade = () => {
186
- Linking.openURL("https://buoy.gg/pro");
211
+ openPricing("https://buoy.gg/pro");
187
212
  };
188
213
  return {
189
214
  hasAccess: isPro,
190
215
  isPro,
216
+ isLicensed,
217
+ isWeekendFree,
218
+ reason,
191
219
  showUpgrade
192
220
  };
193
221
  }
@@ -205,6 +233,12 @@ export const ProFeatureBanner = ({
205
233
  const [showLicenseModal, setShowLicenseModal] = useState(false);
206
234
  const license = useLicense();
207
235
  const handleUpgrade = useCallback(() => {
236
+ // Desktop dashboard: open pricing in the browser instead of the mobile
237
+ // license-entry modal.
238
+ if (isWeb) {
239
+ openPricing();
240
+ return;
241
+ }
208
242
  setShowLicenseModal(true);
209
243
  }, []);
210
244
  const handleCloseModal = useCallback(() => {
@@ -280,6 +314,12 @@ export const UpgradeModal = ({
280
314
  const [showLicenseModal, setShowLicenseModal] = useState(false);
281
315
  const license = useLicense();
282
316
  const handleUpgrade = useCallback(() => {
317
+ // Desktop dashboard: send users straight to the pricing page in their
318
+ // browser instead of the mobile Buoy.init() license-entry modal.
319
+ if (isWeb) {
320
+ openPricing();
321
+ return;
322
+ }
283
323
  setShowLicenseModal(true);
284
324
  }, []);
285
325
  const handleCloseLicenseModal = useCallback(() => {
@@ -414,6 +454,15 @@ const styles = StyleSheet.create({
414
454
  color: buoyColors.primary,
415
455
  letterSpacing: 0.5
416
456
  },
457
+ // Weekend Pass variant — violet instead of the gold/primary PRO.
458
+ weekendBadge: {
459
+ backgroundColor: WEEKEND_VIOLET + "1A",
460
+ borderColor: WEEKEND_VIOLET + "55"
461
+ },
462
+ weekendBadgeText: {
463
+ color: WEEKEND_VIOLET,
464
+ letterSpacing: 0.4
465
+ },
417
466
  container: {
418
467
  padding: 20,
419
468
  alignItems: "center",
@@ -9,8 +9,10 @@
9
9
  */
10
10
 
11
11
  import React, { useCallback } from "react";
12
- import { View, Text, TouchableOpacity, StyleSheet, Linking, Modal, Platform } from "react-native";
12
+ import { View, Text, TouchableOpacity, StyleSheet, Modal, Platform } from "react-native";
13
+ import { absoluteFill } from "../utils/absoluteFill.js";
13
14
  import { macOSColors } from "../ui/gameUI/constants/macOSDesignSystemColors.js";
15
+ import { openPricing, isWeb } from "./openPricing.js";
14
16
  import { X, Key, Link, FileCode, Copy } from "../icons/lucide-icons.js";
15
17
 
16
18
  // For clipboard functionality
@@ -36,7 +38,7 @@ export const LicenseEntryModal = ({
36
38
  }) => {
37
39
  const [copied, setCopied] = React.useState(false);
38
40
  const handleGetLicense = useCallback(() => {
39
- Linking.openURL(purchaseUrl);
41
+ openPricing(purchaseUrl);
40
42
  }, [purchaseUrl]);
41
43
  const handleCopyCode = useCallback(() => {
42
44
  if (Clipboard?.setString) {
@@ -102,7 +104,16 @@ export const LicenseEntryModal = ({
102
104
  }), /*#__PURE__*/_jsx(View, {
103
105
  style: styles.dividerLine
104
106
  })]
105
- }), /*#__PURE__*/_jsxs(View, {
107
+ }), isWeb ? /*#__PURE__*/_jsx(View, {
108
+ style: styles.instructionsContainer,
109
+ children: /*#__PURE__*/_jsx(Text, {
110
+ style: styles.instructionsNote,
111
+ children: "Click the Upgrade button in the top toolbar and paste your license key. It also unlocks automatically when a connected device is running Buoy Pro."
112
+ })
113
+ }) :
114
+ /*#__PURE__*/
115
+ /* Option 2: Instructions */
116
+ _jsxs(View, {
106
117
  style: styles.instructionsContainer,
107
118
  children: [/*#__PURE__*/_jsxs(View, {
108
119
  style: styles.instructionsHeader,
@@ -194,7 +205,7 @@ const styles = StyleSheet.create({
194
205
  alignItems: "center"
195
206
  },
196
207
  backdrop: {
197
- ...StyleSheet.absoluteFillObject,
208
+ ...absoluteFill,
198
209
  backgroundColor: "rgba(0, 0, 0, 0.8)"
199
210
  },
200
211
  modal: {
@@ -9,6 +9,7 @@
9
9
 
10
10
  import React, { useState, useCallback, useEffect } from "react";
11
11
  import { View, Text, TouchableOpacity, StyleSheet, ActivityIndicator, Modal, ScrollView, Alert, Platform } from "react-native";
12
+ import { absoluteFill } from "../utils/absoluteFill.js";
12
13
  import { gameUIColors } from "../ui/gameUI/constants/gameUIColors.js";
13
14
  import { X, Smartphone, Trash2, RefreshCw, CheckCircle, AlertTriangle } from "../icons/lucide-icons.js";
14
15
 
@@ -337,7 +338,7 @@ const styles = StyleSheet.create({
337
338
  alignItems: "center"
338
339
  },
339
340
  backdrop: {
340
- ...StyleSheet.absoluteFillObject,
341
+ ...absoluteFill,
341
342
  backgroundColor: "rgba(0, 0, 0, 0.7)"
342
343
  },
343
344
  modal: {
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+
3
+ import { Linking, Platform } from "react-native";
4
+ export const PRICING_URL = "https://buoy.gg/pricing";
5
+
6
+ /** True when running on the desktop dashboard / web (react-native-web). */
7
+ export const isWeb = Platform.OS === "web";
8
+
9
+ /**
10
+ * Open an upgrade/pricing URL.
11
+ *
12
+ * On the desktop dashboard (web) the upgrade flow is NOT the mobile
13
+ * `Buoy.init()` code path — the user buys on the website (or enters a key in
14
+ * the toolbar). Route the link through the Electron shell bridge so it lands in
15
+ * the user's DEFAULT browser instead of a bare in-app window; fall back to
16
+ * window.open for a plain browser tab. On native, use React Native Linking.
17
+ */
18
+ export function openPricing(url = PRICING_URL) {
19
+ // Access the browser globals via globalThis so this file doesn't depend on
20
+ // the DOM lib (the shared package targets React Native).
21
+ const win = globalThis.window;
22
+ if (isWeb && win) {
23
+ if (win.buoyShell?.openExternal) {
24
+ win.buoyShell.openExternal(url);
25
+ return;
26
+ }
27
+ win.open?.(url, "_blank", "noopener");
28
+ return;
29
+ }
30
+ Linking.openURL(url);
31
+ }
@@ -91,6 +91,7 @@ export const devToolsStorageKeys = {
91
91
  filters: () => `${devToolsStorageKeys.storage.root()}_filters`,
92
92
  eventFilters: () => `${devToolsStorageKeys.storage.root()}_event_filters`,
93
93
  keyFilters: () => `${devToolsStorageKeys.storage.root()}_key_filters`,
94
+ pinnedKeys: () => `${devToolsStorageKeys.storage.root()}_pinned_keys`,
94
95
  preferences: () => `${devToolsStorageKeys.storage.root()}_preferences`,
95
96
  activeTab: () => `${devToolsStorageKeys.storage.root()}_active_tab`,
96
97
  isMonitoring: () => `${devToolsStorageKeys.storage.root()}_is_monitoring`,
@@ -59,6 +59,8 @@ import { notifySubscriberCountChange } from "../utils/subscriberCountNotifier.js
59
59
  export class BaseEventStore extends Subscribable {
60
60
  events = [];
61
61
  arrayListeners = new Set();
62
+ captureSuppressed = false;
63
+ clearListeners = new Set();
62
64
  constructor(options) {
63
65
  super();
64
66
  this.maxEvents = options.maxEvents ?? 500;
@@ -92,7 +94,7 @@ export class BaseEventStore extends Subscribable {
92
94
  * Starts capturing if no one was subscribed.
93
95
  */
94
96
  onSubscribe() {
95
- if (this.getTotalSubscriberCount() === 1) {
97
+ if (this.getTotalSubscriberCount() === 1 && !this.captureSuppressed) {
96
98
  this.startCapturing();
97
99
  }
98
100
  notifySubscriberCountChange(this.storeName);
@@ -132,7 +134,7 @@ export class BaseEventStore extends Subscribable {
132
134
  this.arrayListeners.add(listener);
133
135
 
134
136
  // Start capturing if this is the first subscriber
135
- if (wasEmpty) {
137
+ if (wasEmpty && !this.captureSuppressed) {
136
138
  this.startCapturing();
137
139
  }
138
140
 
@@ -223,6 +225,74 @@ export class BaseEventStore extends Subscribable {
223
225
  clearEvents() {
224
226
  this.events = [];
225
227
  this.notifyArrayListeners();
228
+ this.clearListeners.forEach(listener => {
229
+ try {
230
+ listener();
231
+ } catch {
232
+ // Ignore listener errors
233
+ }
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Listen for clearEvents() calls. Used in remote mirror mode to forward a
239
+ * clear performed in the dashboard UI to the synced device.
240
+ */
241
+ onClear(listener) {
242
+ this.clearListeners.add(listener);
243
+ return () => {
244
+ this.clearListeners.delete(listener);
245
+ };
246
+ }
247
+
248
+ // ===========================================================================
249
+ // REMOTE MIRROR MODE
250
+ // ===========================================================================
251
+
252
+ /**
253
+ * Suppress the auto start/stop capture lifecycle. Use when this store acts
254
+ * as a mirror of a remote device's events (e.g. the desktop dashboard):
255
+ * events arrive via replaceEvents() and the local interceptors must never
256
+ * start — on the dashboard they would capture the dashboard's own traffic.
257
+ */
258
+ disableCapture() {
259
+ this.captureSuppressed = true;
260
+ if (this.isCapturing()) {
261
+ this.stopCapturing();
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Replace the entire event list and notify array listeners. Used in remote
267
+ * mirror mode where full snapshots arrive from a synced device.
268
+ */
269
+ replaceEvents(events) {
270
+ this.events = this.dedupeById(events).slice(0, this.maxEvents);
271
+ this.notifyArrayListeners();
272
+ }
273
+
274
+ /**
275
+ * Drop events that share an `id` with an earlier event, keeping the first
276
+ * (newest, since events are newest-first) occurrence. Remote snapshots can
277
+ * momentarily carry duplicate ids — e.g. a device's request counter resets
278
+ * on reload while older events with the same id are still in the buffer —
279
+ * which makes React list keys (keyed on `id`) collide. Events without an
280
+ * `id` are passed through untouched.
281
+ */
282
+ dedupeById(events) {
283
+ const seen = new Set();
284
+ const result = [];
285
+ for (const event of events) {
286
+ const id = event?.id;
287
+ if (id == null) {
288
+ result.push(event);
289
+ continue;
290
+ }
291
+ if (seen.has(id)) continue;
292
+ seen.add(id);
293
+ result.push(event);
294
+ }
295
+ return result;
226
296
  }
227
297
 
228
298
  /**
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Shared "ignored patterns" store for network-style URL filtering.
5
+ *
6
+ * This is the SINGLE source of truth for the domains/URL patterns that the
7
+ * Network tool (and now the Events tool) hide from their lists. Both tools read
8
+ * and mutate this one singleton, so an ignore toggled in either place is shared
9
+ * everywhere and persisted to the same storage key.
10
+ *
11
+ * Lives in @buoy-gg/shared-ui (a hard dependency of both @buoy-gg/network and
12
+ * @buoy-gg/events) so the filter logic can be shared without @buoy-gg/events
13
+ * having to take a hard dependency on @buoy-gg/network (which is optional).
14
+ */
15
+
16
+ import { useEffect, useMemo, useState } from "react";
17
+ import { persistentStorage } from "../utils/persistentStorage.js";
18
+ import { devToolsStorageKeys } from "../storage/devToolsStorageKeys.js";
19
+
20
+ /**
21
+ * How an ignored-pattern entry should be compared against captured URLs.
22
+ *
23
+ * - `contains`: case-insensitive substring match on the full URL (legacy behavior).
24
+ * - `exact`: smart equality — if pattern starts with `/`, matches URL.pathname;
25
+ * if it starts with `http://`/`https://`, matches origin+pathname; otherwise
26
+ * matches URL.host.
27
+ */
28
+
29
+ /** A single exclude pattern with its match mode. */
30
+
31
+ /** Patterns hidden by default — Buoy's own license API traffic. */
32
+ const DEFAULT_PATTERNS = [{
33
+ value: "api.keygen.sh",
34
+ mode: "contains"
35
+ }];
36
+
37
+ /**
38
+ * Returns true when `url` matches the ignored `pattern` according to its mode.
39
+ *
40
+ * `contains` → case-insensitive substring on the full URL (legacy behavior).
41
+ * `exact` → smart equality: pattern starting with `/` compares URL.pathname,
42
+ * full URLs compare origin+pathname (ignoring query/hash), bare
43
+ * values compare URL.host. Falls back to literal equality if URL
44
+ * parsing fails (e.g. relative URLs).
45
+ */
46
+ export function urlMatchesIgnoredPattern(url, pattern) {
47
+ const lowerUrl = url.toLowerCase();
48
+ const lowerValue = pattern.value.toLowerCase();
49
+ if (pattern.mode === "contains") {
50
+ return lowerUrl.includes(lowerValue);
51
+ }
52
+ try {
53
+ const parsed = new URL(url);
54
+ if (lowerValue.startsWith("/")) {
55
+ return parsed.pathname.toLowerCase() === lowerValue;
56
+ }
57
+ if (lowerValue.startsWith("http://") || lowerValue.startsWith("https://")) {
58
+ return `${parsed.origin}${parsed.pathname}`.toLowerCase() === lowerValue;
59
+ }
60
+ return parsed.host.toLowerCase() === lowerValue;
61
+ } catch {
62
+ return lowerUrl === lowerValue;
63
+ }
64
+ }
65
+
66
+ /** Convenience: does `url` match ANY of the ignored `patterns`? */
67
+ export function isUrlIgnored(url, patterns) {
68
+ if (!url || patterns.length === 0) return false;
69
+ return patterns.some(pattern => urlMatchesIgnoredPattern(url, pattern));
70
+ }
71
+
72
+ /** Tolerate the legacy `string[]` persisted format and stray junk. */
73
+ function migratePatterns(raw) {
74
+ if (!Array.isArray(raw)) return [];
75
+ const migrated = [];
76
+ for (const entry of raw) {
77
+ if (typeof entry === "string" && entry.trim()) {
78
+ migrated.push({
79
+ value: entry,
80
+ mode: "contains"
81
+ });
82
+ } else if (entry && typeof entry === "object" && typeof entry.value === "string") {
83
+ const e = entry;
84
+ migrated.push({
85
+ value: e.value,
86
+ mode: e.mode === "exact" ? "exact" : "contains"
87
+ });
88
+ }
89
+ }
90
+ return migrated;
91
+ }
92
+
93
+ /** Always keep the default patterns present (e.g. Buoy's license API). */
94
+ function ensureDefaults(patterns) {
95
+ const result = [...patterns];
96
+ for (const def of DEFAULT_PATTERNS) {
97
+ if (!result.some(p => p.value === def.value)) {
98
+ result.push({
99
+ ...def
100
+ });
101
+ }
102
+ }
103
+ return result;
104
+ }
105
+ /**
106
+ * Singleton store. One instance per JS runtime, so every consumer (Network tool,
107
+ * Events tool, on mobile or on the desktop dashboard) shares the same patterns.
108
+ */
109
+ class IgnoredPatternsStore {
110
+ patterns = ensureDefaults(DEFAULT_PATTERNS.map(p => ({
111
+ ...p
112
+ })));
113
+ listeners = new Set();
114
+ loaded = false;
115
+ loadPromise = null;
116
+ /** Set once a mutation happens so a slow initial load can't clobber it. */
117
+ dirty = false;
118
+
119
+ /** Current patterns (stable reference until a mutation occurs). */
120
+ getPatterns() {
121
+ return this.patterns;
122
+ }
123
+ subscribe(listener) {
124
+ this.listeners.add(listener);
125
+ // Lazily hydrate from storage the first time anyone cares.
126
+ void this.ensureLoaded();
127
+ return () => {
128
+ this.listeners.delete(listener);
129
+ };
130
+ }
131
+ emit() {
132
+ this.listeners.forEach(l => l());
133
+ }
134
+ ensureLoaded() {
135
+ if (this.loaded) return Promise.resolve();
136
+ if (this.loadPromise) return this.loadPromise;
137
+ this.loadPromise = (async () => {
138
+ try {
139
+ const stored = await persistentStorage.getItem(devToolsStorageKeys.network.ignoredDomains());
140
+ // A mutation may have raced this read — never overwrite user intent.
141
+ if (stored && !this.dirty) {
142
+ const migrated = ensureDefaults(migratePatterns(JSON.parse(stored)));
143
+ this.patterns = migrated;
144
+ }
145
+ } catch {
146
+ // Silently fall back to defaults.
147
+ } finally {
148
+ this.loaded = true;
149
+ this.emit();
150
+ }
151
+ })();
152
+ return this.loadPromise;
153
+ }
154
+ persist() {
155
+ persistentStorage.setItem(devToolsStorageKeys.network.ignoredDomains(), JSON.stringify(this.patterns)).catch(() => {
156
+ // Silently fail — patterns remain in memory.
157
+ });
158
+ }
159
+ commit(next) {
160
+ this.dirty = true;
161
+ this.patterns = next;
162
+ this.emit();
163
+ this.persist();
164
+ }
165
+
166
+ /** Add a pattern (no-op if its value already exists). */
167
+ add(pattern) {
168
+ const value = pattern.value.trim();
169
+ if (!value) return;
170
+ if (this.patterns.some(p => p.value === value)) return;
171
+ this.commit([...this.patterns, {
172
+ value,
173
+ mode: pattern.mode
174
+ }]);
175
+ }
176
+
177
+ /** Remove a pattern by value. */
178
+ remove(value) {
179
+ if (!this.patterns.some(p => p.value === value)) return;
180
+ this.commit(this.patterns.filter(p => p.value !== value));
181
+ }
182
+
183
+ /** Add as `contains` if absent, otherwise remove (used by detail-view chips). */
184
+ toggle(value) {
185
+ const trimmed = value.trim();
186
+ if (!trimmed) return;
187
+ this.commit(this.patterns.some(p => p.value === trimmed) ? this.patterns.filter(p => p.value !== trimmed) : [...this.patterns, {
188
+ value: trimmed,
189
+ mode: "contains"
190
+ }]);
191
+ }
192
+
193
+ /** Flip an existing pattern between `contains` and `exact`. */
194
+ toggleMode(value) {
195
+ if (!this.patterns.some(p => p.value === value)) return;
196
+ this.commit(this.patterns.map(p => p.value === value ? {
197
+ ...p,
198
+ mode: p.mode === "contains" ? "exact" : "contains"
199
+ } : p));
200
+ }
201
+ }
202
+ export const ignoredPatternsStore = new IgnoredPatternsStore();
203
+ /**
204
+ * Subscribe to the shared ignored-patterns store. All consumers in a runtime
205
+ * see the same patterns and the same mutations.
206
+ */
207
+ export function useIgnoredPatterns() {
208
+ const [patterns, setPatterns] = useState(() => ignoredPatternsStore.getPatterns());
209
+ useEffect(() => {
210
+ return ignoredPatternsStore.subscribe(() => {
211
+ setPatterns(ignoredPatternsStore.getPatterns());
212
+ });
213
+ }, []);
214
+ const values = useMemo(() => new Set(patterns.map(p => p.value)), [patterns]);
215
+ return {
216
+ patterns,
217
+ values,
218
+ add: pattern => ignoredPatternsStore.add(pattern),
219
+ remove: value => ignoredPatternsStore.remove(value),
220
+ toggle: value => ignoredPatternsStore.toggle(value),
221
+ toggleMode: value => ignoredPatternsStore.toggleMode(value)
222
+ };
223
+ }
@@ -4,4 +4,5 @@
4
4
  * Shared store utilities and base classes
5
5
  */
6
6
 
7
- export { BaseEventStore } from "./BaseEventStore.js";
7
+ export { BaseEventStore } from "./BaseEventStore.js";
8
+ export { ignoredPatternsStore, useIgnoredPatterns, urlMatchesIgnoredPattern, isUrlIgnored } from "./ignoredPatternsStore.js";