@buoy-gg/network 7.0.4 → 7.0.6

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 (69) hide show
  1. package/README.md +2 -1
  2. package/lib/commonjs/index.js +85 -0
  3. package/lib/commonjs/network/components/NetworkEventDetailView.js +201 -40
  4. package/lib/commonjs/network/components/NetworkEventItemCompact.js +52 -5
  5. package/lib/commonjs/network/components/NetworkHeaderLive.js +132 -0
  6. package/lib/commonjs/network/components/NetworkListScreen.js +134 -56
  7. package/lib/commonjs/network/components/NetworkModal.js +154 -36
  8. package/lib/commonjs/network/components/NetworkSavedScreen.js +245 -0
  9. package/lib/commonjs/network/components/NetworkUpgradeBanner.js +76 -0
  10. package/lib/commonjs/network/hooks/useLiveNetworkEvent.js +62 -0
  11. package/lib/commonjs/network/hooks/useNetworkSaved.js +38 -0
  12. package/lib/commonjs/network/index.js +84 -0
  13. package/lib/commonjs/network/sync/networkSyncAdapter.js +85 -7
  14. package/lib/commonjs/network/utils/generateCurl.js +71 -0
  15. package/lib/commonjs/network/utils/networkEventStore.js +10 -1
  16. package/lib/commonjs/network/utils/networkListener.js +54 -1
  17. package/lib/commonjs/network/utils/networkSavedStore.js +575 -0
  18. package/lib/commonjs/network/utils/withPinnedFirst.js +32 -0
  19. package/lib/module/index.js +17 -1
  20. package/lib/module/network/components/NetworkEventDetailView.js +204 -41
  21. package/lib/module/network/components/NetworkEventItemCompact.js +53 -6
  22. package/lib/module/network/components/NetworkHeaderLive.js +132 -2
  23. package/lib/module/network/components/NetworkListScreen.js +136 -58
  24. package/lib/module/network/components/NetworkModal.js +156 -39
  25. package/lib/module/network/components/NetworkSavedScreen.js +241 -0
  26. package/lib/module/network/components/NetworkUpgradeBanner.js +72 -0
  27. package/lib/module/network/hooks/useLiveNetworkEvent.js +58 -0
  28. package/lib/module/network/hooks/useNetworkSaved.js +34 -0
  29. package/lib/module/network/index.js +7 -1
  30. package/lib/module/network/sync/networkSyncAdapter.js +85 -7
  31. package/lib/module/network/utils/generateCurl.js +67 -0
  32. package/lib/module/network/utils/networkEventStore.js +10 -1
  33. package/lib/module/network/utils/networkListener.js +54 -1
  34. package/lib/module/network/utils/networkSavedStore.js +569 -0
  35. package/lib/module/network/utils/withPinnedFirst.js +28 -0
  36. package/lib/typescript/index.d.ts +13 -1
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/network/components/NetworkEventDetailView.d.ts.map +1 -1
  39. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts +12 -0
  40. package/lib/typescript/network/components/NetworkEventItemCompact.d.ts.map +1 -1
  41. package/lib/typescript/network/components/NetworkHeaderLive.d.ts +28 -0
  42. package/lib/typescript/network/components/NetworkHeaderLive.d.ts.map +1 -1
  43. package/lib/typescript/network/components/NetworkListScreen.d.ts.map +1 -1
  44. package/lib/typescript/network/components/NetworkModal.d.ts +34 -0
  45. package/lib/typescript/network/components/NetworkModal.d.ts.map +1 -1
  46. package/lib/typescript/network/components/NetworkSavedScreen.d.ts +29 -0
  47. package/lib/typescript/network/components/NetworkSavedScreen.d.ts.map +1 -0
  48. package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts +17 -0
  49. package/lib/typescript/network/components/NetworkUpgradeBanner.d.ts.map +1 -0
  50. package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts +19 -0
  51. package/lib/typescript/network/hooks/useLiveNetworkEvent.d.ts.map +1 -0
  52. package/lib/typescript/network/hooks/useNetworkSaved.d.ts +21 -0
  53. package/lib/typescript/network/hooks/useNetworkSaved.d.ts.map +1 -0
  54. package/lib/typescript/network/index.d.ts +8 -1
  55. package/lib/typescript/network/index.d.ts.map +1 -1
  56. package/lib/typescript/network/sync/networkSyncAdapter.d.ts +52 -3
  57. package/lib/typescript/network/sync/networkSyncAdapter.d.ts.map +1 -1
  58. package/lib/typescript/network/types/index.d.ts +43 -0
  59. package/lib/typescript/network/types/index.d.ts.map +1 -1
  60. package/lib/typescript/network/utils/generateCurl.d.ts +3 -0
  61. package/lib/typescript/network/utils/generateCurl.d.ts.map +1 -0
  62. package/lib/typescript/network/utils/networkEventStore.d.ts.map +1 -1
  63. package/lib/typescript/network/utils/networkListener.d.ts +4 -0
  64. package/lib/typescript/network/utils/networkListener.d.ts.map +1 -1
  65. package/lib/typescript/network/utils/networkSavedStore.d.ts +186 -0
  66. package/lib/typescript/network/utils/networkSavedStore.d.ts.map +1 -0
  67. package/lib/typescript/network/utils/withPinnedFirst.d.ts +24 -0
  68. package/lib/typescript/network/utils/withPinnedFirst.d.ts.map +1 -0
  69. package/package.json +2 -2
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.NetworkModal = NetworkModal;
7
+ exports.NetworkToolView = NetworkToolView;
7
8
  var _react = require("react");
8
9
  var _reactNative = require("react-native");
9
10
  var _sharedUi = require("@buoy-gg/shared-ui");
@@ -13,7 +14,10 @@ var _NetworkEventDetailView = require("./NetworkEventDetailView");
13
14
  var _NetworkCopySettingsView = require("./NetworkCopySettingsView");
14
15
  var _NetworkHeaderLive = require("./NetworkHeaderLive");
15
16
  var _NetworkListScreen = require("./NetworkListScreen");
17
+ var _NetworkSavedScreen = require("./NetworkSavedScreen");
16
18
  var _useNetworkEventList = require("../hooks/useNetworkEventList");
19
+ var _useLiveNetworkEvent = require("../hooks/useLiveNetworkEvent");
20
+ var _networkSavedStore = require("../utils/networkSavedStore");
17
21
  var _jsxRuntime = require("react/jsx-runtime");
18
22
  // ============================================================================
19
23
  // Screens (rule4 stable router: each gates itself with `return null`; the
@@ -24,10 +28,17 @@ var _jsxRuntime = require("react/jsx-runtime");
24
28
 
25
29
  function NetworkDetailScreen({
26
30
  visible,
27
- event,
31
+ eventId,
32
+ paused,
28
33
  ignoredPatternValues,
29
34
  onTogglePattern
30
35
  }) {
36
+ // Resolve the ID to the live event so a request opened while pending picks
37
+ // up its response/error in place instead of freezing on the snapshot taken
38
+ // when the row was tapped.
39
+ const event = (0, _useLiveNetworkEvent.useLiveNetworkEvent)(eventId, {
40
+ paused: paused || !visible
41
+ });
31
42
  if (!visible || !event) return null;
32
43
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventDetailView.NetworkEventDetailView, {
33
44
  event: event,
@@ -105,18 +116,31 @@ function NetworkModalInner({
105
116
  onClose,
106
117
  onBack,
107
118
  onMinimize,
108
- enableSharedModalDimensions = false
119
+ enableSharedModalDimensions = false,
120
+ inline = false,
121
+ paused: pausedProp,
122
+ onPausedChange
109
123
  }) {
110
124
  const {
111
125
  isPro
112
126
  } = (0, _sharedUi.useFeatureGate)();
113
127
 
114
128
  // Capture pause (the power toggle's freeze-stream semantic) — gates
115
- // every subscriber in this modal's tree via props.
116
- const [paused, setPaused] = (0, _react.useState)(false);
129
+ // every subscriber in this modal's tree via props. Controlled when the host
130
+ // supplies both halves; otherwise this local state is the source of truth.
131
+ const [pausedState, setPausedState] = (0, _react.useState)(false);
132
+ const controlledPause = pausedProp != null && onPausedChange != null;
133
+ const paused = controlledPause ? pausedProp : pausedState;
134
+ const setPaused = next => {
135
+ const value = typeof next === "function" ? next(paused) : next;
136
+ if (controlledPause) onPausedChange(value);else setPausedState(value);
137
+ };
117
138
  const [filter, setFilter] = (0, _react.useState)({});
118
- const [selectedEvent, setSelectedEvent] = (0, _react.useState)(null);
139
+ // The SELECTION is UI state (an ID); the event object it points at is read
140
+ // live from the store by the detail screen — see useLiveNetworkEvent.
141
+ const [selectedEventId, setSelectedEventId] = (0, _react.useState)(null);
119
142
  const [showFilterView, setShowFilterView] = (0, _react.useState)(false);
143
+ const [showSavedView, setShowSavedView] = (0, _react.useState)(false);
120
144
  const [activeTab, setActiveTab] = (0, _react.useState)("filters");
121
145
  const [searchText, setSearchText] = (0, _react.useState)("");
122
146
  const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
@@ -134,6 +158,13 @@ function NetworkModalInner({
134
158
  const [copySettings, setCopySettings] = (0, _react.useState)(_NetworkCopySettingsView.DEFAULT_COPY_SETTINGS);
135
159
  const hasLoadedCopySettings = (0, _react.useRef)(false);
136
160
  const [showUpgradeModal, setShowUpgradeModal] = (0, _react.useState)(false);
161
+ // Which ceiling the user hit — the upgrade sheet has to argue for the thing
162
+ // they were actually trying to do, not for exporting.
163
+ const [upgradeReason, setUpgradeReason] = (0, _react.useState)("export");
164
+ const showUpgrade = reason => {
165
+ setUpgradeReason(reason);
166
+ setShowUpgradeModal(true);
167
+ };
137
168
 
138
169
  // Auto-close upgrade modal when user becomes Pro
139
170
  (0, _react.useEffect)(() => {
@@ -177,8 +208,13 @@ function NetworkModalInner({
177
208
 
178
209
  // Simple handlers - no useCallback needed per rule2
179
210
  const handleBack = () => {
180
- setSelectedEvent(null);
211
+ setSelectedEventId(null);
181
212
  };
213
+
214
+ // Stable identity: this is passed down to the memoized list rows.
215
+ const handleEventPress = (0, _react.useCallback)(event => {
216
+ setSelectedEventId(event.id);
217
+ }, []);
182
218
  const handleSearch = text => {
183
219
  setSearchText(text);
184
220
  setFilter(prev => ({
@@ -230,13 +266,32 @@ function NetworkModalInner({
230
266
  }
231
267
 
232
268
  // Event detail view header
233
- if (selectedEvent) {
269
+ if (selectedEventId) {
234
270
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
235
271
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
236
272
  onBack: handleBack
237
273
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
238
274
  title: "Request Details",
239
275
  centered: true
276
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
277
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkDetailPinActions, {
278
+ eventId: selectedEventId,
279
+ isPro: isPro,
280
+ onUpgradePress: () => showUpgrade("saved")
281
+ })
282
+ })]
283
+ });
284
+ }
285
+
286
+ // Saved (favorites) header. Checked AFTER the detail branch so opening a
287
+ // saved request still gets the detail header — and its pin/save toggles.
288
+ if (showSavedView) {
289
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
290
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
291
+ onBack: () => setShowSavedView(false)
292
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
293
+ title: "Saved Requests",
294
+ centered: true
240
295
  })]
241
296
  });
242
297
  }
@@ -305,13 +360,15 @@ function NetworkModalInner({
305
360
  size: 14,
306
361
  color: filter.status || filter.method || filter.contentType ? _sharedUi.macOSColors.semantic.info : _sharedUi.macOSColors.text.muted
307
362
  })
363
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkSavedButton, {
364
+ onPress: () => setShowSavedView(true)
308
365
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkHeaderLive.NetworkHeaderCopyButton, {
309
366
  isPro: isPro,
310
367
  filter: filter,
311
368
  ignoredPatterns: ignoredPatterns,
312
369
  copySettings: copySettings,
313
370
  paused: paused,
314
- onUpgradePress: () => setShowUpgradeModal(true)
371
+ onUpgradePress: () => showUpgrade("export")
315
372
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
316
373
  isEnabled: !paused,
317
374
  onToggle: () => setPaused(prev => !prev),
@@ -328,32 +385,20 @@ function NetworkModalInner({
328
385
  // The LIST screen owns its scrolling (FlatList virtualization requires
329
386
  // it — inside JsModal's ScrollView wrapper every row mounts, which at
330
387
  // the 500-event cap measured 17 JS FPS / 133% CPU / 1.4GB; see
331
- // NetworkListScreen). Detail/filter/copy still use the wrapper.
332
- const listOwnsScroll = selectedEvent == null && !showFilterView;
333
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.JsModal, {
334
- visible: visible,
335
- onClose: onClose,
336
- onMinimize: onMinimize,
337
- persistenceKey: persistenceKey,
338
- header: {
339
- showToggleButton: true,
340
- customContent: renderHeaderContent()
341
- },
342
- onModeChange: handleModeChange,
343
- enablePersistence: true,
344
- initialMode: "bottomSheet",
345
- enableGlitchEffects: true,
346
- disableScrollWrapper: listOwnsScroll,
347
- styles: {},
388
+ // NetworkListScreen). The Saved screen is a FlatList too, so it takes the
389
+ // same deal. Detail/filter/copy still use the wrapper.
390
+ const listOwnsScroll = selectedEventId == null && !showFilterView;
391
+ const body = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
348
392
  children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
349
393
  style: styles.container,
350
394
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkDetailScreen, {
351
- visible: selectedEvent != null,
352
- event: selectedEvent,
395
+ visible: selectedEventId != null,
396
+ eventId: selectedEventId,
397
+ paused: paused,
353
398
  ignoredPatternValues: ignoredPatternValues,
354
399
  onTogglePattern: toggleIgnoredPattern
355
400
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkFilterScreen, {
356
- visible: showFilterView && activeTab === "filters" && !selectedEvent,
401
+ visible: showFilterView && activeTab === "filters" && !selectedEventId,
357
402
  paused: paused,
358
403
  isPro: isPro,
359
404
  filter: filter,
@@ -363,37 +408,86 @@ function NetworkModalInner({
363
408
  onAddPattern: addIgnoredPattern,
364
409
  onTogglePatternMode: toggleIgnoredPatternMode
365
410
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkCopyScreen, {
366
- visible: showFilterView && activeTab === "copy" && !selectedEvent,
411
+ visible: showFilterView && activeTab === "copy" && !selectedEventId,
367
412
  paused: paused,
368
413
  isPro: isPro,
369
414
  filter: filter,
370
415
  ignoredPatterns: ignoredPatterns,
371
416
  settings: copySettings,
372
417
  onSettingsChange: setCopySettings
418
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkSavedScreen.NetworkSavedScreen, {
419
+ visible: showSavedView && selectedEventId == null && !showFilterView,
420
+ isPro: isPro,
421
+ copySettings: copySettings,
422
+ onEventPress: handleEventPress,
423
+ onUpgradePress: () => showUpgrade("saved")
373
424
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkListScreen.NetworkListScreen, {
374
- visible: selectedEvent == null && !showFilterView,
425
+ visible: selectedEventId == null && !showFilterView && !showSavedView,
375
426
  paused: paused,
376
427
  filter: filter,
377
428
  ignoredPatterns: ignoredPatterns,
378
429
  isPro: isPro,
379
430
  searchText: searchText,
380
- onEventPress: setSelectedEvent,
381
- onUpgradePress: () => setShowUpgradeModal(true)
431
+ onEventPress: handleEventPress,
432
+ onUpgradePress: () => showUpgrade("export")
382
433
  })]
383
434
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.UpgradeModal, {
384
435
  visible: showUpgradeModal,
385
436
  onClose: () => setShowUpgradeModal(false),
386
- featureName: "Network Export",
387
- description: "Export network requests to share with your team, debug APIs, or feed into AI tools.",
388
- benefits: ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
437
+ featureName: upgradeReason === "saved" ? "Saved Requests" : "Network Export",
438
+ description: upgradeReason === "saved" ? "Keep the requests that matter. Saved requests survive Clear, app restarts, and the 500-request history cap." : "Export network requests to share with your team, debug APIs, or feed into AI tools.",
439
+ benefits: upgradeReason === "saved" ? [`Save up to ${_networkSavedStore.MAX_SAVED} requests (free keeps ${_networkSavedStore.FREE_TIER_SAVED_LIMIT})`, "Failed requests stay put through a Clear", "Snapshots survive an app restart", "Export saved requests to teammates or AI assistants"] : ["Export to JSON, Markdown, or plain text", "Customizable copy presets (URLs only, LLM-ready, full data)", "Share request details with teammates", "Feed network data directly to AI assistants"]
389
440
  })]
390
441
  });
442
+
443
+ // The ONLY thing that differs between the phone and the desktop dashboard is
444
+ // which shell the header and body go in. Everything above this line is shared.
445
+ if (inline) {
446
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
447
+ style: styles.inlineRoot,
448
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
449
+ style: styles.inlineHeader,
450
+ children: renderHeaderContent()
451
+ }), body]
452
+ });
453
+ }
454
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.JsModal, {
455
+ visible: visible,
456
+ onClose: onClose,
457
+ onMinimize: onMinimize,
458
+ persistenceKey: persistenceKey,
459
+ header: {
460
+ showToggleButton: true,
461
+ customContent: renderHeaderContent()
462
+ },
463
+ onModeChange: handleModeChange,
464
+ enablePersistence: true,
465
+ initialMode: "bottomSheet",
466
+ enableGlitchEffects: true,
467
+ disableScrollWrapper: listOwnsScroll,
468
+ styles: {},
469
+ children: body
470
+ });
391
471
  }
392
472
  const styles = _reactNative.StyleSheet.create({
393
473
  container: {
394
474
  flex: 1,
395
475
  backgroundColor: _sharedUi.macOSColors.background.base
396
476
  },
477
+ /** Inline shell — fills the host panel; `minHeight: 0` so the list's own
478
+ * FlatList can scroll instead of stretching the column. */
479
+ inlineRoot: {
480
+ flex: 1,
481
+ minHeight: 0,
482
+ backgroundColor: _sharedUi.macOSColors.background.base
483
+ },
484
+ inlineHeader: {
485
+ paddingHorizontal: 12,
486
+ paddingVertical: 8,
487
+ borderBottomWidth: 1,
488
+ borderBottomColor: _sharedUi.macOSColors.border.default,
489
+ backgroundColor: _sharedUi.macOSColors.background.card
490
+ },
397
491
  headerSearchContainer: {
398
492
  flexDirection: "row",
399
493
  alignItems: "center",
@@ -441,4 +535,28 @@ function NetworkModal(props) {
441
535
  ...props
442
536
  })
443
537
  });
444
- }
538
+ }
539
+ /**
540
+ * The same network tool as `NetworkModal`, rendered INLINE — header bar, then
541
+ * body — for hosts that place tools in a panel rather than a modal. Buoy Desktop
542
+ * is the only one today.
543
+ *
544
+ * This exists so there is exactly ONE network tool. The desktop dashboard used
545
+ * to ship its own 1,258-line copy of the modal's internals, which is how the two
546
+ * surfaces drifted apart in the first place; anything added here now shows up on
547
+ * both without a second edit.
548
+ */
549
+ function NetworkToolView(props = {}) {
550
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_useTickEveryMinute.TickProvider, {
551
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(NetworkModalInner, {
552
+ ...props,
553
+ inline: true,
554
+ visible: true
555
+ // Inline has no chrome to drive: there is no close button, no minimize
556
+ // and no outer back — the host panel owns all of that.
557
+ ,
558
+ onClose: noop
559
+ })
560
+ });
561
+ }
562
+ const noop = () => {};
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NetworkSavedScreen = NetworkSavedScreen;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _sharedUi = require("@buoy-gg/shared-ui");
10
+ var _NetworkEventItemCompact = require("./NetworkEventItemCompact");
11
+ var _NetworkUpgradeBanner = require("./NetworkUpgradeBanner");
12
+ var _generateNetworkCopyText = require("../utils/generateNetworkCopyText");
13
+ var _filterNetworkEvents = require("../utils/filterNetworkEvents");
14
+ var _useNetworkSaved = require("../hooks/useNetworkSaved");
15
+ var _networkSavedStore = require("../utils/networkSavedStore");
16
+ var _jsxRuntime = require("react/jsx-runtime");
17
+ /**
18
+ * The SAVED screen — the network tool's favorites list.
19
+ *
20
+ * Everything here is read from the persisted saved store, never from the live
21
+ * event store. That is the whole point of the screen: these requests survive a
22
+ * Clear, the 500-event cap, and an app restart, so a failure you kept is still
23
+ * here tomorrow.
24
+ *
25
+ * Rows are the same two row components the live list uses, so a saved request
26
+ * looks exactly like the request it was. Long-press here UNSAVES rather than
27
+ * pins — in this list "get rid of it" is the action the row affords, and
28
+ * pinning is still available from the detail header.
29
+ *
30
+ * Stable-tree contract (rule4): hooks run unconditionally, then the component
31
+ * returns null when not visible.
32
+ */
33
+
34
+ const keyExtractor = item => item.id;
35
+ function EmptyState() {
36
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
37
+ style: styles.emptyState,
38
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Bookmark, {
39
+ size: 32,
40
+ color: _sharedUi.macOSColors.text.muted
41
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
42
+ style: styles.emptyTitle,
43
+ children: "No saved requests"
44
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
45
+ style: styles.emptyText,
46
+ children: "Open a request and tap the bookmark to keep it here \u2014 saved requests survive Clear and app restarts."
47
+ })]
48
+ });
49
+ }
50
+ function NetworkSavedScreen({
51
+ visible,
52
+ isPro,
53
+ copySettings,
54
+ onEventPress,
55
+ onUpgradePress
56
+ }) {
57
+ const {
58
+ savedRecords,
59
+ pinnedLiveIds,
60
+ toggleSave,
61
+ clearSaved
62
+ } = (0, _useNetworkSaved.useNetworkSaved)();
63
+ const [search, setSearch] = (0, _react.useState)("");
64
+ if (!visible) return null;
65
+
66
+ /**
67
+ * A downgrade (or a limit change) can leave a free user holding more saves
68
+ * than the free tier allows. Rather than deleting their data, the overflow
69
+ * is LOCKED — oldest first, so the newest saves stay reachable — and the
70
+ * banner offers the upgrade that unlocks it.
71
+ */
72
+ const limit = isPro ? _networkSavedStore.MAX_SAVED : _networkSavedStore.FREE_TIER_SAVED_LIMIT;
73
+ const unlocked = savedRecords.slice(0, limit);
74
+ const lockedCount = savedRecords.length - unlocked.length;
75
+ const events = unlocked.map(record => record.event);
76
+ const filtered = search ? (0, _filterNetworkEvents.filterNetworkEvents)(events, {
77
+ searchText: search
78
+ }) : events;
79
+ const handleLongPress = event => {
80
+ toggleSave(event);
81
+ };
82
+ const renderItem = ({
83
+ item
84
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkEventItemCompact.NetworkEventItemCompact, {
85
+ event: item,
86
+ onPress: onEventPress,
87
+ onLongPress: handleLongPress,
88
+ saved: true,
89
+ pinned: pinnedLiveIds.has(item.id)
90
+ });
91
+ const renderListFooter = () => {
92
+ if (lockedCount === 0) return null;
93
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_NetworkUpgradeBanner.NetworkUpgradeBanner, {
94
+ title: `${lockedCount} saved ${lockedCount === 1 ? "request" : "requests"} locked`,
95
+ subtitle: `Free keeps ${_networkSavedStore.FREE_TIER_SAVED_LIMIT}. Upgrade to Pro to unlock the rest.`,
96
+ onPress: onUpgradePress
97
+ });
98
+ };
99
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
100
+ style: styles.root,
101
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
102
+ style: styles.toolbar,
103
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Bookmark, {
104
+ size: 11,
105
+ color: _sharedUi.macOSColors.semantic.debug
106
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
107
+ style: styles.toolbarTitle,
108
+ children: ["SAVED \xB7 ", savedRecords.length, isPro ? "" : `/${_networkSavedStore.FREE_TIER_SAVED_LIMIT}`]
109
+ }), isPro ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CopyButton
110
+ // Lazy value, resolved at press time — no string is built per
111
+ // render, and it always reflects the current list.
112
+ , {
113
+ value: () => (0, _generateNetworkCopyText.generateNetworkCopyText)(filtered, copySettings),
114
+ size: 13,
115
+ buttonStyle: styles.toolbarButton,
116
+ colors: {
117
+ idle: _sharedUi.macOSColors.text.secondary,
118
+ success: _sharedUi.macOSColors.semantic.success,
119
+ error: _sharedUi.macOSColors.semantic.error
120
+ },
121
+ disabled: filtered.length === 0
122
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
123
+ onPress: clearSaved,
124
+ "sentry-label": "ignore clear saved",
125
+ style: styles.toolbarClear,
126
+ disabled: savedRecords.length === 0,
127
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
128
+ style: [styles.toolbarClearText, savedRecords.length === 0 && styles.toolbarClearTextDisabled],
129
+ children: "Clear all"
130
+ })
131
+ })]
132
+ }), savedRecords.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
133
+ style: styles.searchRow,
134
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
135
+ size: 13,
136
+ color: _sharedUi.macOSColors.text.secondary
137
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TextInput, {
138
+ style: styles.searchInput,
139
+ placeholder: "Search saved requests\u2026",
140
+ placeholderTextColor: _sharedUi.macOSColors.text.muted,
141
+ value: search,
142
+ onChangeText: setSearch,
143
+ "sentry-label": "ignore saved search",
144
+ accessibilityLabel: "Search saved requests",
145
+ autoCapitalize: "none",
146
+ autoCorrect: false,
147
+ returnKeyType: "search"
148
+ })]
149
+ }) : null, savedRecords.length === 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyState, {}) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
150
+ data: filtered,
151
+ renderItem: renderItem,
152
+ keyExtractor: keyExtractor,
153
+ contentContainerStyle: styles.listContent,
154
+ showsVerticalScrollIndicator: true,
155
+ removeClippedSubviews: true,
156
+ scrollEnabled: true,
157
+ initialNumToRender: 12,
158
+ maxToRenderPerBatch: 8,
159
+ windowSize: 5,
160
+ extraData: pinnedLiveIds,
161
+ "sentry-label": "ignore saved requests list",
162
+ ListFooterComponent: renderListFooter
163
+ })]
164
+ });
165
+ }
166
+ const styles = _reactNative.StyleSheet.create({
167
+ root: {
168
+ flex: 1,
169
+ minHeight: 0
170
+ },
171
+ toolbar: {
172
+ flexDirection: "row",
173
+ alignItems: "center",
174
+ gap: 6,
175
+ paddingHorizontal: 20,
176
+ paddingTop: 10,
177
+ paddingBottom: 6
178
+ },
179
+ toolbarTitle: {
180
+ flex: 1,
181
+ color: _sharedUi.macOSColors.semantic.debug,
182
+ fontSize: 10,
183
+ fontWeight: "700",
184
+ letterSpacing: 0.6,
185
+ fontFamily: "monospace"
186
+ },
187
+ toolbarButton: {
188
+ width: 26,
189
+ height: 26,
190
+ borderRadius: 6,
191
+ alignItems: "center",
192
+ justifyContent: "center"
193
+ },
194
+ toolbarClear: {
195
+ paddingHorizontal: 6,
196
+ paddingVertical: 2
197
+ },
198
+ toolbarClearText: {
199
+ color: _sharedUi.macOSColors.text.muted,
200
+ fontSize: 10
201
+ },
202
+ toolbarClearTextDisabled: {
203
+ color: _sharedUi.macOSColors.text.disabled
204
+ },
205
+ searchRow: {
206
+ flexDirection: "row",
207
+ alignItems: "center",
208
+ gap: 6,
209
+ marginHorizontal: 12,
210
+ marginBottom: 8,
211
+ paddingHorizontal: 10,
212
+ paddingVertical: 5,
213
+ backgroundColor: _sharedUi.macOSColors.background.input,
214
+ borderRadius: 10,
215
+ borderWidth: 1,
216
+ borderColor: _sharedUi.macOSColors.border.default
217
+ },
218
+ searchInput: {
219
+ flex: 1,
220
+ color: _sharedUi.macOSColors.text.primary,
221
+ fontSize: 13,
222
+ paddingVertical: 2
223
+ },
224
+ listContent: {
225
+ paddingTop: 4
226
+ },
227
+ emptyState: {
228
+ alignItems: "center",
229
+ paddingVertical: 40,
230
+ paddingHorizontal: 24
231
+ },
232
+ emptyTitle: {
233
+ color: _sharedUi.macOSColors.text.primary,
234
+ fontSize: 14,
235
+ fontWeight: "600",
236
+ marginTop: 12,
237
+ marginBottom: 6
238
+ },
239
+ emptyText: {
240
+ color: _sharedUi.macOSColors.text.muted,
241
+ fontSize: 12,
242
+ textAlign: "center",
243
+ lineHeight: 17
244
+ }
245
+ });
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NetworkUpgradeBanner = NetworkUpgradeBanner;
7
+ var _reactNative = require("react-native");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
9
+ var _jsxRuntime = require("react/jsx-runtime");
10
+ /**
11
+ * The "N things are locked, upgrade to see them" banner.
12
+ *
13
+ * Extracted because there are now two of them — locked request history on the
14
+ * live list, and locked saves on the Saved screen — and they are the same
15
+ * offer. Two copies would drift the moment one of them gets a copy tweak.
16
+ */
17
+
18
+ function NetworkUpgradeBanner({
19
+ title,
20
+ subtitle,
21
+ onPress
22
+ }) {
23
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
24
+ style: styles.banner,
25
+ onPress: onPress,
26
+ activeOpacity: 0.8,
27
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
28
+ style: styles.content,
29
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
30
+ style: styles.left,
31
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Zap, {
32
+ size: 16,
33
+ color: _sharedUi.buoyColors.primary
34
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
35
+ style: styles.title,
36
+ children: title
37
+ })]
38
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ProBadge, {})]
39
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
40
+ style: styles.subtitle,
41
+ children: subtitle
42
+ })]
43
+ });
44
+ }
45
+ const styles = _reactNative.StyleSheet.create({
46
+ banner: {
47
+ backgroundColor: _sharedUi.buoyColors.card,
48
+ borderRadius: 10,
49
+ borderWidth: 1,
50
+ borderColor: _sharedUi.buoyColors.primary + "40",
51
+ padding: 12,
52
+ marginHorizontal: 12,
53
+ marginTop: 12,
54
+ marginBottom: 34
55
+ },
56
+ content: {
57
+ flexDirection: "row",
58
+ alignItems: "center",
59
+ justifyContent: "space-between",
60
+ marginBottom: 4
61
+ },
62
+ left: {
63
+ flexDirection: "row",
64
+ alignItems: "center",
65
+ gap: 8
66
+ },
67
+ title: {
68
+ color: _sharedUi.buoyColors.text,
69
+ fontSize: 13,
70
+ fontWeight: "600"
71
+ },
72
+ subtitle: {
73
+ color: _sharedUi.buoyColors.textMuted,
74
+ fontSize: 11
75
+ }
76
+ });