@buoy-gg/highlight-updates 3.0.1 → 3.0.2

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 (83) hide show
  1. package/lib/commonjs/highlight-updates/HighlightUpdatesOverlay.js +285 -1
  2. package/lib/commonjs/highlight-updates/components/HighlightFilterView.js +1371 -1
  3. package/lib/commonjs/highlight-updates/components/HighlightUpdatesModal.js +564 -1
  4. package/lib/commonjs/highlight-updates/components/IdentifierBadge.js +267 -1
  5. package/lib/commonjs/highlight-updates/components/IsolatedRenderList.js +178 -1
  6. package/lib/commonjs/highlight-updates/components/ModalHeaderContent.js +309 -1
  7. package/lib/commonjs/highlight-updates/components/RenderCauseBadge.js +500 -1
  8. package/lib/commonjs/highlight-updates/components/RenderDetailView.js +803 -1
  9. package/lib/commonjs/highlight-updates/components/RenderHistoryViewer.js +894 -1
  10. package/lib/commonjs/highlight-updates/components/RenderListItem.js +220 -1
  11. package/lib/commonjs/highlight-updates/components/RendersCopySettingsView.js +562 -1
  12. package/lib/commonjs/highlight-updates/components/StatsDisplay.js +70 -1
  13. package/lib/commonjs/highlight-updates/components/index.js +97 -1
  14. package/lib/commonjs/highlight-updates/types/copySettings.js +107 -1
  15. package/lib/commonjs/highlight-updates/utils/HighlightUpdatesController.js +1819 -1
  16. package/lib/commonjs/highlight-updates/utils/PerformanceLogger.js +359 -1
  17. package/lib/commonjs/highlight-updates/utils/ProfilerInterceptor.js +371 -1
  18. package/lib/commonjs/highlight-updates/utils/RenderCauseDetector.js +1828 -1
  19. package/lib/commonjs/highlight-updates/utils/RenderTracker.js +919 -1
  20. package/lib/commonjs/highlight-updates/utils/ViewTypeMapper.js +264 -1
  21. package/lib/commonjs/highlight-updates/utils/copySettingsStorage.js +49 -1
  22. package/lib/commonjs/highlight-updates/utils/renderExportFormatter.js +58 -1
  23. package/lib/commonjs/highlight-updates/utils/rendersExportFormatter.js +485 -1
  24. package/lib/commonjs/index.js +320 -1
  25. package/lib/commonjs/preset.js +278 -1
  26. package/lib/commonjs/sync/highlightUpdatesSyncAdapter.js +83 -0
  27. package/lib/module/highlight-updates/HighlightUpdatesOverlay.js +278 -1
  28. package/lib/module/highlight-updates/components/HighlightFilterView.js +1365 -1
  29. package/lib/module/highlight-updates/components/HighlightUpdatesModal.js +558 -1
  30. package/lib/module/highlight-updates/components/IdentifierBadge.js +259 -1
  31. package/lib/module/highlight-updates/components/IsolatedRenderList.js +174 -1
  32. package/lib/module/highlight-updates/components/ModalHeaderContent.js +304 -1
  33. package/lib/module/highlight-updates/components/RenderCauseBadge.js +491 -1
  34. package/lib/module/highlight-updates/components/RenderDetailView.js +797 -1
  35. package/lib/module/highlight-updates/components/RenderHistoryViewer.js +888 -1
  36. package/lib/module/highlight-updates/components/RenderListItem.js +215 -1
  37. package/lib/module/highlight-updates/components/RendersCopySettingsView.js +558 -1
  38. package/lib/module/highlight-updates/components/StatsDisplay.js +67 -1
  39. package/lib/module/highlight-updates/components/index.js +16 -1
  40. package/lib/module/highlight-updates/types/copySettings.js +102 -1
  41. package/lib/module/highlight-updates/utils/HighlightUpdatesController.js +1815 -1
  42. package/lib/module/highlight-updates/utils/PerformanceLogger.js +353 -1
  43. package/lib/module/highlight-updates/utils/ProfilerInterceptor.js +358 -1
  44. package/lib/module/highlight-updates/utils/RenderCauseDetector.js +1818 -1
  45. package/lib/module/highlight-updates/utils/RenderTracker.js +916 -1
  46. package/lib/module/highlight-updates/utils/ViewTypeMapper.js +255 -1
  47. package/lib/module/highlight-updates/utils/copySettingsStorage.js +43 -1
  48. package/lib/module/highlight-updates/utils/renderExportFormatter.js +54 -1
  49. package/lib/module/highlight-updates/utils/rendersExportFormatter.js +478 -1
  50. package/lib/module/index.js +74 -1
  51. package/lib/module/preset.js +272 -1
  52. package/lib/module/sync/highlightUpdatesSyncAdapter.js +78 -0
  53. package/lib/typescript/highlight-updates/HighlightUpdatesOverlay.d.ts.map +1 -0
  54. package/lib/typescript/highlight-updates/components/HighlightFilterView.d.ts.map +1 -0
  55. package/lib/typescript/highlight-updates/components/HighlightUpdatesModal.d.ts.map +1 -0
  56. package/lib/typescript/highlight-updates/components/IdentifierBadge.d.ts.map +1 -0
  57. package/lib/typescript/highlight-updates/components/IsolatedRenderList.d.ts.map +1 -0
  58. package/lib/typescript/highlight-updates/components/ModalHeaderContent.d.ts.map +1 -0
  59. package/lib/typescript/highlight-updates/components/RenderCauseBadge.d.ts.map +1 -0
  60. package/lib/typescript/highlight-updates/components/RenderDetailView.d.ts.map +1 -0
  61. package/lib/typescript/highlight-updates/components/RenderHistoryViewer.d.ts.map +1 -0
  62. package/lib/typescript/highlight-updates/components/RenderListItem.d.ts.map +1 -0
  63. package/lib/typescript/highlight-updates/components/RendersCopySettingsView.d.ts.map +1 -0
  64. package/lib/typescript/highlight-updates/components/StatsDisplay.d.ts.map +1 -0
  65. package/lib/typescript/highlight-updates/components/index.d.ts.map +1 -0
  66. package/lib/typescript/highlight-updates/types/copySettings.d.ts.map +1 -0
  67. package/lib/typescript/highlight-updates/utils/HighlightUpdatesController.d.ts +90 -0
  68. package/lib/typescript/highlight-updates/utils/HighlightUpdatesController.d.ts.map +1 -0
  69. package/lib/typescript/highlight-updates/utils/PerformanceLogger.d.ts.map +1 -0
  70. package/lib/typescript/highlight-updates/utils/ProfilerInterceptor.d.ts.map +1 -0
  71. package/lib/typescript/highlight-updates/utils/RenderCauseDetector.d.ts.map +1 -0
  72. package/lib/typescript/highlight-updates/utils/RenderTracker.d.ts +10 -0
  73. package/lib/typescript/highlight-updates/utils/RenderTracker.d.ts.map +1 -0
  74. package/lib/typescript/highlight-updates/utils/ViewTypeMapper.d.ts.map +1 -0
  75. package/lib/typescript/highlight-updates/utils/copySettingsStorage.d.ts.map +1 -0
  76. package/lib/typescript/highlight-updates/utils/renderExportFormatter.d.ts.map +1 -0
  77. package/lib/typescript/highlight-updates/utils/rendersExportFormatter.d.ts.map +1 -0
  78. package/lib/typescript/index.d.ts +1 -0
  79. package/lib/typescript/index.d.ts.map +1 -0
  80. package/lib/typescript/preset.d.ts.map +1 -0
  81. package/lib/typescript/sync/highlightUpdatesSyncAdapter.d.ts +36 -0
  82. package/lib/typescript/sync/highlightUpdatesSyncAdapter.d.ts.map +1 -0
  83. package/package.json +7 -7
@@ -1 +1,558 @@
1
- "use strict";import React,{useState,useEffect,useRef,useCallback}from"react";import{View,Text,StyleSheet}from"react-native";import{Power,JsModal,devToolsStorageKeys,persistentStorage,buoyColors,TickProvider}from"@buoy-gg/shared-ui";import{useIsPro}from"@buoy-gg/license";import HighlightUpdatesController from"../utils/HighlightUpdatesController";import{RenderTracker}from"../utils/RenderTracker";import{RenderDetailView}from"./RenderDetailView";import{EventStepperFooter}from"@buoy-gg/shared-ui";import{HighlightFilterView}from"./HighlightFilterView";import{IsolatedRenderList}from"./IsolatedRenderList";import{MainListHeader,FilterViewHeader,DetailViewHeader,CopySettingsViewHeader}from"./ModalHeaderContent";import{RenderHistoryViewer,RenderHistoryFooter}from"./RenderHistoryViewer";import{RendersCopySettingsView}from"./RendersCopySettingsView";import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";const DisabledBanner=React.memo(function(){return _jsxs(View,{style:styles.disabledBanner,children:[_jsx(Power,{size:14,color:buoyColors.warning}),_jsx(Text,{style:styles.disabledText,children:"Render tracking is disabled"})]})});export function HighlightUpdatesModal({visible:e,onClose:t,onBack:r,onMinimize:n,enableSharedModalDimensions:s=!1,initialNativeTag:i,onInitialNativeTagHandled:l}){const a=useIsPro(),[o,c]=useState(!1),[d,u]=useState(()=>HighlightUpdatesController.getFrozen()),[g,h]=useState(()=>RenderTracker.getStats().totalComponents>0),[p,C]=useState(null),[f,m]=useState(0),[S,T]=useState(!1),[y,x]=useState(!1),[b,k]=useState([]),[R,w]=useState("filters"),[v,H]=useState("details"),[I,F]=useState(0),[P,V]=useState(""),[D,_]=useState(!1),j=useRef(null),U=useRef([]),E=useRef(RenderTracker.getFilters()),[N,z]=useState(()=>{const e=RenderTracker.getFilters();return e.includePatterns.length+e.excludePatterns.length}),[A,M]=useState(()=>RenderTracker.getFilters()),[B,K]=useState(()=>RenderTracker.getSettings()),L=useRef(!1),J=useRef(!1),O=useRef(!1);useEffect(()=>{e&&!L.current&&(L.current=!0)},[e]),useEffect(()=>{e&&!J.current&&(async()=>{try{const e=await persistentStorage.getItem(devToolsStorageKeys.highlightUpdates.filters());if(e){const t=JSON.parse(e),r={includeTestID:new Set(t.includeTestID||[]),includeNativeID:new Set(t.includeNativeID||[]),includeViewType:new Set(t.includeViewType||[]),includeComponent:new Set(t.includeComponent||[]),excludeTestID:new Set(t.excludeTestID||[]),excludeNativeID:new Set(t.excludeNativeID||[]),excludeViewType:new Set(t.excludeViewType||[]),excludeComponent:new Set(t.excludeComponent||[]),includePatterns:t.includePatterns||[],excludePatterns:t.excludePatterns||[]};RenderTracker.setFilters(r);const n=RenderTracker.getFilters();E.current=n,M(n),z(n.includePatterns.length+n.excludePatterns.length)}J.current=!0}catch(e){}})()},[e]),useEffect(()=>{J.current&&(async()=>{try{const e={includeTestID:Array.from(A.includeTestID),includeNativeID:Array.from(A.includeNativeID),includeViewType:Array.from(A.includeViewType),includeComponent:Array.from(A.includeComponent),excludeTestID:Array.from(A.excludeTestID),excludeNativeID:Array.from(A.excludeNativeID),excludeViewType:Array.from(A.excludeViewType),excludeComponent:Array.from(A.excludeComponent),includePatterns:A.includePatterns,excludePatterns:A.excludePatterns};await persistentStorage.setItem(devToolsStorageKeys.highlightUpdates.filters(),JSON.stringify(e))}catch(e){}})()},[A]),useEffect(()=>{e&&!O.current&&(async()=>{try{const e=await persistentStorage.getItem(devToolsStorageKeys.highlightUpdates.settings());if(e){const t=JSON.parse(e),{performanceLogging:r,excludeDevTools:n,...s}=t;RenderTracker.setSettings(s),K(RenderTracker.getSettings())}O.current=!0}catch(e){O.current=!0}})()},[e]),useEffect(()=>{O.current&&(async()=>{try{const{performanceLogging:e,excludeDevTools:t,...r}=B;await persistentStorage.setItem(devToolsStorageKeys.highlightUpdates.settings(),JSON.stringify(r))}catch(e){}})()},[B]),useEffect(()=>{const e=RenderTracker.subscribeToState(e=>{c(e.isTracking)});return()=>{e()}},[]),useEffect(()=>{const e=HighlightUpdatesController.subscribeToFreeze(e=>{u(e)});return()=>{e()}},[]),useEffect(()=>{D&&requestAnimationFrame(()=>{j.current?.focus()})},[D]),useEffect(()=>{e||HighlightUpdatesController.setSpotlight(null)},[e]),useEffect(()=>{if(e&&null!=i){const e=RenderTracker.getRender(String(i));e&&(C(e),m(0),T(!1),HighlightUpdatesController.setSpotlight(e.nativeTag)),l?.()}},[e,i,l]);const q=useCallback(()=>{HighlightUpdatesController.isInitialized()||HighlightUpdatesController.initialize(),HighlightUpdatesController.toggle()},[]),G=useCallback(()=>{HighlightUpdatesController.toggleFreeze()},[]),W=useCallback(()=>{HighlightUpdatesController.clearRenderCounts(),h(!1)},[]),Q=useCallback(()=>{k(RenderTracker.getFilteredRenders(P)),x(!0)},[P]),X=useCallback(()=>{x(!1)},[]),Y=useCallback(e=>{V(e)},[]),Z=useCallback((e,t,r)=>{C(e),m(t),H("details"),F(0),U.current=r,HighlightUpdatesController.setSpotlight(e.nativeTag)},[]),$=useCallback(e=>{U.current=e},[]),ee=useCallback(()=>{C(null),m(0),HighlightUpdatesController.setSpotlight(null)},[]),te=useCallback(()=>{const e=U.current;if(f>0){const t=f-1,r=e[t];r&&(C(r),m(t),HighlightUpdatesController.setSpotlight(r.nativeTag))}},[f]),re=useCallback(()=>{const e=U.current;if(f<e.length-1){const t=f+1,r=e[t];r&&(C(r),m(t),HighlightUpdatesController.setSpotlight(r.nativeTag))}},[f]),ne=useCallback(()=>{T(!1)},[]),se=useCallback(()=>{T(!0)},[]),ie=useCallback(()=>{_(!0)},[]),le=useCallback(()=>{_(!1)},[]),ae=useCallback(e=>{RenderTracker.setFilters(e);const t=RenderTracker.getFilters();E.current=t,M(t),z(t.includePatterns.length+t.excludePatterns.length)},[]),oe=useCallback((e,t)=>{const r=RenderTracker.getFilters(),n={};"include"===t?r.includePatterns.some(t=>t.type===e.type&&t.value===e.value)||(n.includePatterns=[...r.includePatterns,e]):r.excludePatterns.some(t=>t.type===e.type&&t.value===e.value)||(n.excludePatterns=[...r.excludePatterns,e]),Object.keys(n).length>0&&(ae(n),C(null),m(0),HighlightUpdatesController.setSpotlight(null))},[ae]),ce=useCallback(e=>{RenderTracker.setSettings(e),K(RenderTracker.getSettings())},[]),de=useCallback(e=>{h(e.totalComponents>0)},[]),ue=useCallback(()=>y?_jsx(CopySettingsViewHeader,{onBack:X}):S?_jsx(FilterViewHeader,{onBack:ne,activeTab:R,onTabChange:w,activeFilterCount:N}):p?_jsx(DetailViewHeader,{onBack:ee,activeTab:v,onTabChange:H,hasHistory:(p.renderHistory?.length??0)>0}):_jsx(MainListHeader,{onBack:r,isSearchActive:D,searchText:P,onSearchChange:Y,onSearchToggle:ie,onSearchClose:le,onFilterToggle:se,onToggleTracking:q,onToggleFreeze:G,onClear:W,onOpenCopySettings:Q,isTracking:o,isFrozen:d,activeFilterCount:N,hasRenders:g,searchInputRef:j}),[y,S,p,r,D,P,o,d,N,g,R,v,ne,ee,X,Y,ie,le,se,q,G,W,Q]),ge=s?devToolsStorageKeys.modal.root():devToolsStorageKeys.highlightUpdates.modal();if(!e)return null;const he=U.current.length,pe=p?"details"===v?_jsx(EventStepperFooter,{currentIndex:f,totalItems:he,onPrevious:te,onNext:re,itemLabel:"Component",subtitle:p.componentName||p.displayName||p.viewType}):_jsx(RenderHistoryFooter,{render:p,selectedEventIndex:I,onEventIndexChange:F,isPro:a}):null;return _jsx(TickProvider,{children:_jsx(JsModal,{visible:e,onClose:t,onMinimize:n,persistenceKey:ge,header:{showToggleButton:!0,customContent:ue()},enablePersistence:!0,initialMode:"bottomSheet",enableGlitchEffects:!0,styles:{},footer:pe,footerHeight:p&&("details"===v||(p.renderHistory?.length??0)>1)?68:0,children:_jsx(View,{nativeID:"__rn_buoy__highlight-modal",style:styles.container,children:y?_jsx(RendersCopySettingsView,{renders:b}):p?"details"===v?_jsx(RenderDetailView,{render:p,disableInternalFooter:!0,onAddFilter:oe,isPro:a}):_jsx(RenderHistoryViewer,{render:p,disableInternalFooter:!0,selectedEventIndex:I,onEventIndexChange:F,isPro:a}):S?_jsx(HighlightFilterView,{filters:A,onFilterChange:ae,settings:B,onSettingsChange:ce,availableProps:RenderTracker.getAvailableProps(),activeTab:R}):_jsxs(_Fragment,{children:[!o&&_jsx(DisabledBanner,{}),_jsx(IsolatedRenderList,{searchText:P,filters:A,onSelectRender:Z,onStatsChange:de,onRendersChange:$,isTracking:o,isPro:a})]})})})})}const styles=StyleSheet.create({container:{flex:1,backgroundColor:buoyColors.base},disabledBanner:{flexDirection:"row",alignItems:"center",gap:8,padding:10,marginHorizontal:12,marginTop:8,backgroundColor:buoyColors.warning+"15",borderRadius:8,borderWidth:1,borderColor:buoyColors.warning+"20"},disabledText:{color:buoyColors.warning,fontSize:11,flex:1}});export default HighlightUpdatesModal;
1
+ "use strict";
2
+
3
+ /**
4
+ * HighlightUpdatesModal
5
+ *
6
+ * Main modal interface for the Highlight Updates dev tool.
7
+ * Shows a list of tracked component renders with controls for
8
+ * start/stop, clear, and filtering.
9
+ *
10
+ * PERFORMANCE OPTIMIZED:
11
+ * - Uses isolated components to prevent parent re-renders
12
+ * - Stats display is isolated (StatsDisplay)
13
+ * - Render list is isolated (IsolatedRenderList)
14
+ * - Header components are memoized
15
+ * - Uses refs for values not displayed in UI
16
+ */
17
+
18
+ import React, { useState, useEffect, useRef, useCallback } from "react";
19
+ import { View, Text, StyleSheet } from "react-native";
20
+ import { Power, JsModal, devToolsStorageKeys, persistentStorage, buoyColors, TickProvider } from "@buoy-gg/shared-ui";
21
+ import { useIsPro } from "@buoy-gg/license";
22
+ import HighlightUpdatesController from "../utils/HighlightUpdatesController";
23
+ import { RenderTracker } from "../utils/RenderTracker";
24
+ import { RenderDetailView } from "./RenderDetailView";
25
+ import { EventStepperFooter } from "@buoy-gg/shared-ui";
26
+ import { HighlightFilterView } from "./HighlightFilterView";
27
+ import { IsolatedRenderList } from "./IsolatedRenderList";
28
+ import { MainListHeader, FilterViewHeader, DetailViewHeader, CopySettingsViewHeader } from "./ModalHeaderContent";
29
+ import { RenderHistoryViewer, RenderHistoryFooter } from "./RenderHistoryViewer";
30
+ import { RendersCopySettingsView } from "./RendersCopySettingsView";
31
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
32
+ // Disabled banner - memoized since props rarely change
33
+ const DisabledBanner = /*#__PURE__*/React.memo(function DisabledBanner() {
34
+ return /*#__PURE__*/_jsxs(View, {
35
+ style: styles.disabledBanner,
36
+ children: [/*#__PURE__*/_jsx(Power, {
37
+ size: 14,
38
+ color: buoyColors.warning
39
+ }), /*#__PURE__*/_jsx(Text, {
40
+ style: styles.disabledText,
41
+ children: "Render tracking is disabled"
42
+ })]
43
+ });
44
+ });
45
+ export function HighlightUpdatesModal({
46
+ visible,
47
+ onClose,
48
+ onBack,
49
+ onMinimize,
50
+ enableSharedModalDimensions = false,
51
+ initialNativeTag,
52
+ onInitialNativeTagHandled
53
+ }) {
54
+ const isPro = useIsPro();
55
+
56
+ // ============================================================================
57
+ // TRACKING STATE - subscribed via isolated component
58
+ // ============================================================================
59
+ const [isTracking, setIsTracking] = useState(false);
60
+ const [isFrozen, setIsFrozen] = useState(() => HighlightUpdatesController.getFrozen());
61
+
62
+ // Track if there are renders for header clear button state
63
+ const [hasRenders, setHasRenders] = useState(() => RenderTracker.getStats().totalComponents > 0);
64
+
65
+ // ============================================================================
66
+ // UI STATE - kept in parent for view switching
67
+ // ============================================================================
68
+ const [selectedRender, setSelectedRender] = useState(null);
69
+ const [selectedRenderIndex, setSelectedRenderIndex] = useState(0);
70
+ const [showFilterView, setShowFilterView] = useState(false);
71
+ const [showCopySettingsView, setShowCopySettingsView] = useState(false);
72
+ const [copyRendersSnapshot, setCopyRendersSnapshot] = useState([]);
73
+ const [filterViewActiveTab, setFilterViewActiveTab] = useState("filters");
74
+ const [detailViewActiveTab, setDetailViewActiveTab] = useState("details");
75
+ const [historyEventIndex, setHistoryEventIndex] = useState(0);
76
+ const [searchText, setSearchText] = useState("");
77
+ const [isSearchActive, setIsSearchActive] = useState(false);
78
+ const searchInputRef = useRef(null);
79
+
80
+ // Track the renders list for navigation
81
+ const rendersListRef = useRef([]);
82
+
83
+ // ============================================================================
84
+ // FILTER STATE - use ref for filter config, state for display count only
85
+ // ============================================================================
86
+ const filtersRef = useRef(RenderTracker.getFilters());
87
+ const [activeFilterCount, setActiveFilterCount] = useState(() => {
88
+ const filters = RenderTracker.getFilters();
89
+ return filters.includePatterns.length + filters.excludePatterns.length;
90
+ });
91
+
92
+ // Expose filters via state for HighlightFilterView (it needs to display them)
93
+ const [filters, setFilters] = useState(() => RenderTracker.getFilters());
94
+
95
+ // ============================================================================
96
+ // SETTINGS STATE
97
+ // ============================================================================
98
+ const [settings, setSettings] = useState(() => RenderTracker.getSettings());
99
+
100
+ // ============================================================================
101
+ // PERSISTENCE REFS - prevent saving on initial load
102
+ // ============================================================================
103
+ const hasLoadedTrackingState = useRef(false);
104
+ const hasLoadedFilters = useRef(false);
105
+ const hasLoadedSettings = useRef(false);
106
+
107
+ // ============================================================================
108
+ // LOAD PERSISTED STATE
109
+ // ============================================================================
110
+
111
+ // Tracking state is intentionally NOT persisted.
112
+ // Always starts OFF to prevent infinite re-render loops on load where
113
+ // the overlay highlights its own components before exclusion filters can take effect.
114
+ // The user must explicitly enable tracking each session.
115
+ useEffect(() => {
116
+ if (!visible || hasLoadedTrackingState.current) return;
117
+ hasLoadedTrackingState.current = true;
118
+ }, [visible]);
119
+
120
+ // Load persisted filters on mount
121
+ useEffect(() => {
122
+ if (!visible || hasLoadedFilters.current) return;
123
+ const loadFilters = async () => {
124
+ try {
125
+ const storedFilters = await persistentStorage.getItem(devToolsStorageKeys.highlightUpdates.filters());
126
+ if (storedFilters) {
127
+ const parsedFilters = JSON.parse(storedFilters);
128
+ const restoredFilters = {
129
+ includeTestID: new Set(parsedFilters.includeTestID || []),
130
+ includeNativeID: new Set(parsedFilters.includeNativeID || []),
131
+ includeViewType: new Set(parsedFilters.includeViewType || []),
132
+ includeComponent: new Set(parsedFilters.includeComponent || []),
133
+ excludeTestID: new Set(parsedFilters.excludeTestID || []),
134
+ excludeNativeID: new Set(parsedFilters.excludeNativeID || []),
135
+ excludeViewType: new Set(parsedFilters.excludeViewType || []),
136
+ excludeComponent: new Set(parsedFilters.excludeComponent || []),
137
+ includePatterns: parsedFilters.includePatterns || [],
138
+ excludePatterns: parsedFilters.excludePatterns || []
139
+ };
140
+ RenderTracker.setFilters(restoredFilters);
141
+ const newFilters = RenderTracker.getFilters();
142
+ filtersRef.current = newFilters;
143
+ setFilters(newFilters);
144
+ setActiveFilterCount(newFilters.includePatterns.length + newFilters.excludePatterns.length);
145
+ }
146
+ hasLoadedFilters.current = true;
147
+ } catch (error) {
148
+ // Failed to load filters
149
+ }
150
+ };
151
+ loadFilters();
152
+ }, [visible]);
153
+
154
+ // Save filters when they change
155
+ useEffect(() => {
156
+ if (!hasLoadedFilters.current) return;
157
+ const saveFilters = async () => {
158
+ try {
159
+ const filtersToSave = {
160
+ includeTestID: Array.from(filters.includeTestID),
161
+ includeNativeID: Array.from(filters.includeNativeID),
162
+ includeViewType: Array.from(filters.includeViewType),
163
+ includeComponent: Array.from(filters.includeComponent),
164
+ excludeTestID: Array.from(filters.excludeTestID),
165
+ excludeNativeID: Array.from(filters.excludeNativeID),
166
+ excludeViewType: Array.from(filters.excludeViewType),
167
+ excludeComponent: Array.from(filters.excludeComponent),
168
+ includePatterns: filters.includePatterns,
169
+ excludePatterns: filters.excludePatterns
170
+ };
171
+ await persistentStorage.setItem(devToolsStorageKeys.highlightUpdates.filters(), JSON.stringify(filtersToSave));
172
+ } catch (error) {
173
+ // Failed to save filters
174
+ }
175
+ };
176
+ saveFilters();
177
+ }, [filters]);
178
+
179
+ // Load persisted settings on mount
180
+ useEffect(() => {
181
+ if (!visible || hasLoadedSettings.current) return;
182
+ const loadSettings = async () => {
183
+ try {
184
+ const storedSettings = await persistentStorage.getItem(devToolsStorageKeys.highlightUpdates.settings());
185
+ if (storedSettings) {
186
+ const parsedSettings = JSON.parse(storedSettings);
187
+ // Never restore excludeDevTools or performanceLogging from persistence -
188
+ // excludeDevTools must always default to true to prevent infinite re-render loops
189
+ const {
190
+ performanceLogging: _ignored,
191
+ excludeDevTools: _alwaysTrue,
192
+ ...settingsToRestore
193
+ } = parsedSettings;
194
+ RenderTracker.setSettings(settingsToRestore);
195
+ setSettings(RenderTracker.getSettings());
196
+ }
197
+ hasLoadedSettings.current = true;
198
+ } catch (error) {
199
+ hasLoadedSettings.current = true;
200
+ }
201
+ };
202
+ loadSettings();
203
+ }, [visible]);
204
+
205
+ // Save settings when they change
206
+ useEffect(() => {
207
+ if (!hasLoadedSettings.current) return;
208
+ const saveSettings = async () => {
209
+ try {
210
+ // Never persist excludeDevTools - it must always default to true on startup
211
+ const {
212
+ performanceLogging: _ignored,
213
+ excludeDevTools: _neverPersist,
214
+ ...settingsToSave
215
+ } = settings;
216
+ await persistentStorage.setItem(devToolsStorageKeys.highlightUpdates.settings(), JSON.stringify(settingsToSave));
217
+ } catch (error) {
218
+ // Failed to save settings
219
+ }
220
+ };
221
+ saveSettings();
222
+ }, [settings]);
223
+
224
+ // ============================================================================
225
+ // SUBSCRIPTIONS - only for tracking state, not renders
226
+ // ============================================================================
227
+
228
+ // Subscribe to tracking state changes only
229
+ useEffect(() => {
230
+ const unsubscribeState = RenderTracker.subscribeToState(state => {
231
+ setIsTracking(state.isTracking);
232
+ });
233
+ return () => {
234
+ unsubscribeState();
235
+ };
236
+ }, []);
237
+
238
+ // Subscribe to freeze state changes
239
+ useEffect(() => {
240
+ const unsubscribeFreeze = HighlightUpdatesController.subscribeToFreeze(frozen => {
241
+ setIsFrozen(frozen);
242
+ });
243
+ return () => {
244
+ unsubscribeFreeze();
245
+ };
246
+ }, []);
247
+
248
+ // Focus search input when activated
249
+ useEffect(() => {
250
+ if (isSearchActive) {
251
+ requestAnimationFrame(() => {
252
+ searchInputRef.current?.focus();
253
+ });
254
+ }
255
+ }, [isSearchActive]);
256
+
257
+ // Clear spotlight when modal is closed
258
+ useEffect(() => {
259
+ if (!visible) {
260
+ HighlightUpdatesController.setSpotlight(null);
261
+ }
262
+ }, [visible]);
263
+
264
+ // ============================================================================
265
+ // DEEP LINK NAVIGATION - "Click Overlay Badge → Jump to Detail"
266
+ // ============================================================================
267
+
268
+ // Handle initial navigation when a badge is tapped
269
+ useEffect(() => {
270
+ if (visible && initialNativeTag != null) {
271
+ // Look up the render by nativeTag
272
+ const render = RenderTracker.getRender(String(initialNativeTag));
273
+ if (render) {
274
+ // Navigate to detail view for this component
275
+ setSelectedRender(render);
276
+ setSelectedRenderIndex(0);
277
+ setShowFilterView(false);
278
+ // Set spotlight to show which component is being viewed
279
+ HighlightUpdatesController.setSpotlight(render.nativeTag);
280
+ }
281
+ // Notify parent that we've handled the navigation
282
+ onInitialNativeTagHandled?.();
283
+ }
284
+ }, [visible, initialNativeTag, onInitialNativeTagHandled]);
285
+
286
+ // ============================================================================
287
+ // CALLBACKS - stable references for child components
288
+ // ============================================================================
289
+
290
+ const handleToggleTracking = useCallback(() => {
291
+ if (!HighlightUpdatesController.isInitialized()) {
292
+ HighlightUpdatesController.initialize();
293
+ }
294
+ HighlightUpdatesController.toggle();
295
+ }, []);
296
+ const handleToggleFreeze = useCallback(() => {
297
+ HighlightUpdatesController.toggleFreeze();
298
+ }, []);
299
+ const handleClear = useCallback(() => {
300
+ HighlightUpdatesController.clearRenderCounts();
301
+ setHasRenders(false);
302
+ }, []);
303
+ const handleOpenCopySettings = useCallback(() => {
304
+ // Snapshot the same filtered list the user sees in the main list view
305
+ // (respects include/exclude patterns and the search box).
306
+ setCopyRendersSnapshot(RenderTracker.getFilteredRenders(searchText));
307
+ setShowCopySettingsView(true);
308
+ }, [searchText]);
309
+ const handleCloseCopySettings = useCallback(() => {
310
+ setShowCopySettingsView(false);
311
+ }, []);
312
+ const handleSearch = useCallback(text => {
313
+ setSearchText(text);
314
+ }, []);
315
+ const handleRenderPress = useCallback((render, index, allRenders) => {
316
+ setSelectedRender(render);
317
+ setSelectedRenderIndex(index);
318
+ setDetailViewActiveTab("details"); // Reset to details tab when selecting a new render
319
+ setHistoryEventIndex(0); // Reset history event index when selecting a new render
320
+ rendersListRef.current = allRenders;
321
+ // Set spotlight to show which component is being viewed
322
+ HighlightUpdatesController.setSpotlight(render.nativeTag);
323
+ }, []);
324
+ const handleRendersChange = useCallback(renders => {
325
+ rendersListRef.current = renders;
326
+ }, []);
327
+ const handleBackFromDetail = useCallback(() => {
328
+ setSelectedRender(null);
329
+ setSelectedRenderIndex(0);
330
+ // Clear the spotlight
331
+ HighlightUpdatesController.setSpotlight(null);
332
+ }, []);
333
+ const handlePreviousRender = useCallback(() => {
334
+ const renders = rendersListRef.current;
335
+ if (selectedRenderIndex > 0) {
336
+ const newIndex = selectedRenderIndex - 1;
337
+ const newRender = renders[newIndex];
338
+ if (newRender) {
339
+ setSelectedRender(newRender);
340
+ setSelectedRenderIndex(newIndex);
341
+ HighlightUpdatesController.setSpotlight(newRender.nativeTag);
342
+ }
343
+ }
344
+ }, [selectedRenderIndex]);
345
+ const handleNextRender = useCallback(() => {
346
+ const renders = rendersListRef.current;
347
+ if (selectedRenderIndex < renders.length - 1) {
348
+ const newIndex = selectedRenderIndex + 1;
349
+ const newRender = renders[newIndex];
350
+ if (newRender) {
351
+ setSelectedRender(newRender);
352
+ setSelectedRenderIndex(newIndex);
353
+ HighlightUpdatesController.setSpotlight(newRender.nativeTag);
354
+ }
355
+ }
356
+ }, [selectedRenderIndex]);
357
+ const handleBackFromFilter = useCallback(() => {
358
+ setShowFilterView(false);
359
+ }, []);
360
+ const handleFilterToggle = useCallback(() => {
361
+ setShowFilterView(true);
362
+ }, []);
363
+ const handleSearchToggle = useCallback(() => {
364
+ setIsSearchActive(true);
365
+ }, []);
366
+ const handleSearchClose = useCallback(() => {
367
+ setIsSearchActive(false);
368
+ }, []);
369
+ const handleFilterChange = useCallback(newFilters => {
370
+ RenderTracker.setFilters(newFilters);
371
+ const updatedFilters = RenderTracker.getFilters();
372
+ filtersRef.current = updatedFilters;
373
+ setFilters(updatedFilters);
374
+ setActiveFilterCount(updatedFilters.includePatterns.length + updatedFilters.excludePatterns.length);
375
+ }, []);
376
+
377
+ // Handler for adding a filter from the detail view (quick actions)
378
+ const handleAddFilter = useCallback((pattern, mode) => {
379
+ const currentFilters = RenderTracker.getFilters();
380
+ const newFilters = {};
381
+ if (mode === "include") {
382
+ // Check if pattern already exists
383
+ const exists = currentFilters.includePatterns.some(p => p.type === pattern.type && p.value === pattern.value);
384
+ if (!exists) {
385
+ newFilters.includePatterns = [...currentFilters.includePatterns, pattern];
386
+ }
387
+ } else {
388
+ // Check if pattern already exists
389
+ const exists = currentFilters.excludePatterns.some(p => p.type === pattern.type && p.value === pattern.value);
390
+ if (!exists) {
391
+ newFilters.excludePatterns = [...currentFilters.excludePatterns, pattern];
392
+ }
393
+ }
394
+ if (Object.keys(newFilters).length > 0) {
395
+ handleFilterChange(newFilters);
396
+ // Go back to the list view after adding filter
397
+ setSelectedRender(null);
398
+ setSelectedRenderIndex(0);
399
+ // Clear the spotlight
400
+ HighlightUpdatesController.setSpotlight(null);
401
+ }
402
+ }, [handleFilterChange]);
403
+ const handleSettingsChange = useCallback(newSettings => {
404
+ RenderTracker.setSettings(newSettings);
405
+ setSettings(RenderTracker.getSettings());
406
+ }, []);
407
+
408
+ // Callback for IsolatedRenderList to update hasRenders (for header clear button)
409
+ const handleStatsChange = useCallback(stats => {
410
+ setHasRenders(stats.totalComponents > 0);
411
+ }, []);
412
+
413
+ // ============================================================================
414
+ // HEADER RENDERING - using memoized components
415
+ // ============================================================================
416
+
417
+ const renderHeaderContent = useCallback(() => {
418
+ if (showCopySettingsView) {
419
+ return /*#__PURE__*/_jsx(CopySettingsViewHeader, {
420
+ onBack: handleCloseCopySettings
421
+ });
422
+ }
423
+ if (showFilterView) {
424
+ return /*#__PURE__*/_jsx(FilterViewHeader, {
425
+ onBack: handleBackFromFilter,
426
+ activeTab: filterViewActiveTab,
427
+ onTabChange: setFilterViewActiveTab,
428
+ activeFilterCount: activeFilterCount
429
+ });
430
+ }
431
+ if (selectedRender) {
432
+ return /*#__PURE__*/_jsx(DetailViewHeader, {
433
+ onBack: handleBackFromDetail,
434
+ activeTab: detailViewActiveTab,
435
+ onTabChange: setDetailViewActiveTab,
436
+ hasHistory: (selectedRender.renderHistory?.length ?? 0) > 0
437
+ });
438
+ }
439
+ return /*#__PURE__*/_jsx(MainListHeader, {
440
+ onBack: onBack,
441
+ isSearchActive: isSearchActive,
442
+ searchText: searchText,
443
+ onSearchChange: handleSearch,
444
+ onSearchToggle: handleSearchToggle,
445
+ onSearchClose: handleSearchClose,
446
+ onFilterToggle: handleFilterToggle,
447
+ onToggleTracking: handleToggleTracking,
448
+ onToggleFreeze: handleToggleFreeze,
449
+ onClear: handleClear,
450
+ onOpenCopySettings: handleOpenCopySettings,
451
+ isTracking: isTracking,
452
+ isFrozen: isFrozen,
453
+ activeFilterCount: activeFilterCount,
454
+ hasRenders: hasRenders,
455
+ searchInputRef: searchInputRef
456
+ });
457
+ }, [showCopySettingsView, showFilterView, selectedRender, onBack, isSearchActive, searchText, isTracking, isFrozen, activeFilterCount, hasRenders, filterViewActiveTab, detailViewActiveTab, handleBackFromFilter, handleBackFromDetail, handleCloseCopySettings, handleSearch, handleSearchToggle, handleSearchClose, handleFilterToggle, handleToggleTracking, handleToggleFreeze, handleClear, handleOpenCopySettings]);
458
+
459
+ // ============================================================================
460
+ // RENDER
461
+ // ============================================================================
462
+
463
+ const persistenceKey = enableSharedModalDimensions ? devToolsStorageKeys.modal.root() : devToolsStorageKeys.highlightUpdates.modal();
464
+ if (!visible) return null;
465
+
466
+ // Footer for navigating through the renders list or history events
467
+ const totalRenders = rendersListRef.current.length;
468
+ const footerNode = selectedRender ? detailViewActiveTab === "details" ? /*#__PURE__*/_jsx(EventStepperFooter, {
469
+ currentIndex: selectedRenderIndex,
470
+ totalItems: totalRenders,
471
+ onPrevious: handlePreviousRender,
472
+ onNext: handleNextRender,
473
+ itemLabel: "Component",
474
+ subtitle: selectedRender.componentName || selectedRender.displayName || selectedRender.viewType
475
+ }) : /*#__PURE__*/_jsx(RenderHistoryFooter, {
476
+ render: selectedRender,
477
+ selectedEventIndex: historyEventIndex,
478
+ onEventIndexChange: setHistoryEventIndex,
479
+ isPro: isPro
480
+ }) : null;
481
+ return /*#__PURE__*/_jsx(TickProvider, {
482
+ children: /*#__PURE__*/_jsx(JsModal, {
483
+ visible: visible,
484
+ onClose: onClose,
485
+ onMinimize: onMinimize,
486
+ persistenceKey: persistenceKey,
487
+ header: {
488
+ showToggleButton: true,
489
+ customContent: renderHeaderContent()
490
+ },
491
+ enablePersistence: true,
492
+ initialMode: "bottomSheet",
493
+ enableGlitchEffects: true,
494
+ styles: {},
495
+ footer: footerNode,
496
+ footerHeight: selectedRender ? detailViewActiveTab === "details" ? 68 : (selectedRender.renderHistory?.length ?? 0) > 1 ? 68 : 0 : 0,
497
+ children: /*#__PURE__*/_jsx(View, {
498
+ nativeID: "__rn_buoy__highlight-modal",
499
+ style: styles.container,
500
+ children: showCopySettingsView ? /*#__PURE__*/_jsx(RendersCopySettingsView, {
501
+ renders: copyRendersSnapshot
502
+ }) : selectedRender ? detailViewActiveTab === "details" ? /*#__PURE__*/_jsx(RenderDetailView, {
503
+ render: selectedRender,
504
+ disableInternalFooter: true,
505
+ onAddFilter: handleAddFilter,
506
+ isPro: isPro
507
+ }) : /*#__PURE__*/_jsx(RenderHistoryViewer, {
508
+ render: selectedRender,
509
+ disableInternalFooter: true,
510
+ selectedEventIndex: historyEventIndex,
511
+ onEventIndexChange: setHistoryEventIndex,
512
+ isPro: isPro
513
+ }) : showFilterView ? /*#__PURE__*/_jsx(HighlightFilterView, {
514
+ filters: filters,
515
+ onFilterChange: handleFilterChange,
516
+ settings: settings,
517
+ onSettingsChange: handleSettingsChange,
518
+ availableProps: RenderTracker.getAvailableProps(),
519
+ activeTab: filterViewActiveTab
520
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
521
+ children: [!isTracking && /*#__PURE__*/_jsx(DisabledBanner, {}), /*#__PURE__*/_jsx(IsolatedRenderList, {
522
+ searchText: searchText,
523
+ filters: filters,
524
+ onSelectRender: handleRenderPress,
525
+ onStatsChange: handleStatsChange,
526
+ onRendersChange: handleRendersChange,
527
+ isTracking: isTracking,
528
+ isPro: isPro
529
+ })]
530
+ })
531
+ })
532
+ })
533
+ });
534
+ }
535
+ const styles = StyleSheet.create({
536
+ container: {
537
+ flex: 1,
538
+ backgroundColor: buoyColors.base
539
+ },
540
+ disabledBanner: {
541
+ flexDirection: "row",
542
+ alignItems: "center",
543
+ gap: 8,
544
+ padding: 10,
545
+ marginHorizontal: 12,
546
+ marginTop: 8,
547
+ backgroundColor: buoyColors.warning + "15",
548
+ borderRadius: 8,
549
+ borderWidth: 1,
550
+ borderColor: buoyColors.warning + "20"
551
+ },
552
+ disabledText: {
553
+ color: buoyColors.warning,
554
+ fontSize: 11,
555
+ flex: 1
556
+ }
557
+ });
558
+ export default HighlightUpdatesModal;