@buoy-gg/zustand 7.0.38 → 7.0.39

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 (62) hide show
  1. package/lib/commonjs/index.js +105 -1
  2. package/lib/commonjs/preset.js +102 -1
  3. package/lib/commonjs/zustand/components/ZustandActionButton.js +118 -1
  4. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +48 -1
  5. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +43 -1
  6. package/lib/commonjs/zustand/components/ZustandIcon.js +18 -1
  7. package/lib/commonjs/zustand/components/ZustandModal.js +772 -1
  8. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +117 -1
  9. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +372 -1
  10. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +488 -1
  11. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +204 -1
  12. package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +471 -1
  13. package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +393 -1
  14. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +74 -1
  15. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +382 -1
  16. package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +284 -1
  17. package/lib/commonjs/zustand/utils/editableState.js +146 -1
  18. package/lib/commonjs/zustand/utils/snapshotProvider.js +209 -1
  19. package/lib/commonjs/zustand/utils/testIDs.js +82 -1
  20. package/lib/commonjs/zustand/utils/zustandStateStore.js +379 -1
  21. package/lib/module/index.js +90 -1
  22. package/lib/module/preset.js +98 -1
  23. package/lib/module/zustand/components/ZustandActionButton.js +114 -1
  24. package/lib/module/zustand/components/ZustandDetailViewToggle.js +43 -1
  25. package/lib/module/zustand/components/ZustandEventFilterView.js +39 -1
  26. package/lib/module/zustand/components/ZustandIcon.js +11 -1
  27. package/lib/module/zustand/components/ZustandModal.js +767 -1
  28. package/lib/module/zustand/components/ZustandStateChangeItem.js +113 -1
  29. package/lib/module/zustand/components/ZustandStateDetailContent.js +368 -1
  30. package/lib/module/zustand/components/ZustandStateInfoView.js +483 -1
  31. package/lib/module/zustand/components/ZustandStoreBrowser.js +200 -1
  32. package/lib/module/zustand/components/ZustandStoreDetailContent.js +469 -1
  33. package/lib/module/zustand/components/ZustandStoreEditorScreen.js +389 -1
  34. package/lib/module/zustand/hooks/useZustandStateChanges.js +70 -1
  35. package/lib/module/zustand/sync/zustandSyncAdapter.js +380 -1
  36. package/lib/module/zustand/utils/buoyZustandMiddleware.js +278 -1
  37. package/lib/module/zustand/utils/editableState.js +138 -1
  38. package/lib/module/zustand/utils/snapshotProvider.js +209 -1
  39. package/lib/module/zustand/utils/testIDs.js +74 -1
  40. package/lib/module/zustand/utils/zustandStateStore.js +375 -1
  41. package/lib/typescript/index.d.ts.map +1 -0
  42. package/lib/typescript/preset.d.ts.map +1 -0
  43. package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +1 -0
  44. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -0
  45. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -0
  46. package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +1 -0
  47. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -0
  48. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -0
  49. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -0
  50. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -0
  51. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -0
  52. package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +1 -0
  53. package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +1 -0
  54. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -0
  55. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -0
  56. package/lib/typescript/zustand/types/index.d.ts.map +1 -0
  57. package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +1 -0
  58. package/lib/typescript/zustand/utils/editableState.d.ts.map +1 -0
  59. package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +1 -0
  60. package/lib/typescript/zustand/utils/testIDs.d.ts.map +1 -0
  61. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -0
  62. package/package.json +3 -3
@@ -1 +1,772 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ZustandModal=ZustandModal,exports.ZustandToolView=ZustandToolView;var _react=require("react"),_reactNative=require("react-native"),_sharedUi=require("@buoy-gg/shared-ui"),_license=require("@buoy-gg/license"),_useZustandStateChanges=require("../hooks/useZustandStateChanges"),_ZustandStateChangeItem=require("./ZustandStateChangeItem"),_ZustandStateDetailContent=require("./ZustandStateDetailContent"),_ZustandStoreBrowser=require("./ZustandStoreBrowser"),_ZustandStoreDetailContent=require("./ZustandStoreDetailContent"),_ZustandEventFilterView=require("./ZustandEventFilterView"),_testIDs=require("../utils/testIDs"),_jsxRuntime=require("react/jsx-runtime");function EventsEmptyState({isEnabled:e,searchText:t,totalChanges:s}){const a=(0,_sharedUi.resolveListEmptyState)({subject:"changes",search:t,total:s,capturing:e,waiting:{otto:"waiting",title:"No state changes",message:"State changes will appear here as stores update."},paused:{title:"No state changes",message:"Enable capture to start recording state changes"}});return(0,_jsxRuntime.jsx)(_sharedUi.ToolEmptyState,{...a,testID:`${_testIDs.ZUSTAND_TEST_ID}-empty`})}function ZustandModal({visible:e,onClose:t,onBack:s,onMinimize:a,enableSharedModalDimensions:n=!1,inline:r=!1,storesLoading:i=!1,initialTab:o="stores",initialSelectedChangeId:l,initialDetailView:d,onChangeSelect:c,onStoreExpand:u}){(0,_license.useIsPro)();const _=(0,_sharedUi.useLimit)("zustand.changes"),[h,g]=(0,_react.useState)(!1),[x,S]=(0,_react.useState)(o),[m,j]=(0,_react.useState)(null),[T,D]=(0,_react.useState)(null),[b,y]=(0,_react.useState)(!1),[I,C]=(0,_react.useState)(new Set),{stateChanges:U,filteredChanges:p,filter:R,setFilter:f,stats:N,stores:Z,clearChanges:v,isEnabled:E,toggleCapture:w}=(0,_useZustandStateChanges.useZustandStateChanges)(),k=(0,_react.useMemo)(()=>0===I.size?p:p.filter(e=>!Array.from(I).some(t=>e.storeName.toLowerCase().includes(t.toLowerCase()))),[p,I]),A=(0,_react.useMemo)(()=>0===I.size?Z:Z.filter(e=>!Array.from(I).some(t=>e.name.toLowerCase().includes(t.toLowerCase()))),[Z,I]),B=(0,_react.useMemo)(()=>m?U.filter(e=>e.storeName===m).length:0,[U,m]),M=(0,_react.useMemo)(()=>m?k.filter(e=>e.storeName===m):k,[k,m]),$=(0,_react.useMemo)(()=>M.slice(0,_),[M,_]),H=(0,_react.useMemo)(()=>Math.max(0,M.length-_),[M.length,_]),P=H>0,V=$,[z,L]=(0,_react.useState)(null),F=(0,_react.useMemo)(()=>{if(null===z)return null;const e=V.findIndex(e=>e.id===z);return e>=0?e:null},[z,V]),q=null!==F?V[F]:null,O=(0,_react.useRef)(null);(0,_react.useEffect)(()=>{if(!c)return;const e=q?.id??null;null!==e&&e!==O.current&&(O.current=e,c(q))},[q,c]);const W=(0,_react.useRef)(null);(0,_react.useEffect)(()=>{l&&W.current!==l&&V.some(e=>e.id===l)&&(W.current=l,L(l))},[l,V]);const[K,G]=(0,_react.useState)(""),[J,Q]=(0,_react.useState)(!1),X=((0,_react.useRef)(null),(0,_react.useRef)(null)),Y=(0,_react.useCallback)(e=>{},[]),ee=e=>{G(e),"events"===x&&f(t=>({...t,searchText:e}))},te=(0,_react.useCallback)(()=>{G(""),f(e=>({...e,searchText:""})),Q(!1)},[f]),se=(0,_react.useCallback)(e=>{L(e.id)},[]),ae=(0,_react.useCallback)(()=>{g(!0)},[]),ne=(0,_react.useCallback)(()=>{L(null)},[]),re=(0,_react.useCallback)(e=>{const t=V[e];t&&L(t.id)},[V]),ie=(0,_react.useCallback)(e=>{j(e),L(null)},[]),oe=(0,_react.useCallback)(()=>{j(null),L(null)},[]),le=(0,_react.useCallback)(e=>{D(e),u?.(e)},[u]),de=(0,_react.useCallback)(()=>{D(null)},[]),ce=(0,_react.useCallback)(()=>A.reduce((e,t)=>{try{const s=t.api.getState();if(s&&"object"==typeof s){const a={};for(const[e,t]of Object.entries(s))"function"!=typeof t&&(a[e]=(0,_sharedUi.truncatePayload)(t));e[t.name]=a}else e[t.name]=(0,_sharedUi.truncatePayload)(s)}catch{e[t.name]=null}return e},{}),[A]),ue=(0,_react.useCallback)(()=>k.map(e=>({id:e.id,storeName:e.storeName,timestamp:e.timestamp,prevState:(0,_sharedUi.truncatePayload)(e.prevState),nextState:(0,_sharedUi.truncatePayload)(e.nextState),hasStateChange:e.hasStateChange,changedKeys:e.changedKeys})),[k]),_e=(0,_react.useCallback)(()=>V.map(e=>({id:e.id,storeName:e.storeName,timestamp:e.timestamp,prevState:(0,_sharedUi.truncatePayload)(e.prevState),nextState:(0,_sharedUi.truncatePayload)(e.nextState),hasStateChange:e.hasStateChange,changedKeys:e.changedKeys})),[V]),he=(0,_react.useCallback)(e=>{C(t=>{const s=new Set(t);return s.has(e)?s.delete(e):s.add(e),s})},[]),ge=(0,_react.useCallback)(e=>{C(t=>new Set([...t,e]))},[]),xe=(0,_react.useCallback)(e=>{S(e),j(null),D(null),L(null),y(!1),K&&(G(""),f(e=>({...e,searchText:""}))),Q(!1)},[K,f]),Se=(0,_react.useCallback)(e=>e.id,[]),me=(0,_react.useMemo)(()=>(0,_testIDs.buildChangeRowTestIDs)(V),[V]),je=(0,_react.useCallback)(({item:e})=>(0,_jsxRuntime.jsx)(_ZustandStateChangeItem.ZustandStateChangeItem,{change:e,onPress:se,testID:me.get(e.id)}),[se,me]),Te=I.size>0,De=()=>b?(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:()=>y(!1)}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:"Filters",centered:!0,testID:`${_testIDs.ZUSTAND_TEST_ID}-filters-title`})]}):q?(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:ne}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:`${q.storeName}/setState`,centered:!0,testID:`${_testIDs.ZUSTAND_TEST_ID}-detail-title`})]}):m?(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:oe}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:`${m} History`,centered:!0,testID:`${_testIDs.ZUSTAND_TEST_ID}-history-title`}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions,{children:(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:_e,disabled:0===V.length,buttonStyle:styles.headerActionButton,testID:`${_testIDs.ZUSTAND_TEST_ID}-copy`})})]}):T?(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:de}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:T,centered:!0,testID:`${_testIDs.ZUSTAND_TEST_ID}-store-detail-title`})]}):(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader,{children:[s&&(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation,{onBack:s}),(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Content,{title:"",children:J?(0,_jsxRuntime.jsx)(_sharedUi.ModalHeader.Search,{value:K,onChangeText:ee,onCancel:te,placeholder:"stores"===x?"Search stores...":"Search events...",accessibilityLabel:"Search Zustand stores and events",iconSize:14,testID:`${_testIDs.ZUSTAND_TEST_ID}-search-field`,containerTestID:`${_testIDs.ZUSTAND_TEST_ID}-search`,clearTestID:`${_testIDs.ZUSTAND_TEST_ID}-search-clear`,onBlur:()=>{0===K.length&&Q(!1)}}):(0,_jsxRuntime.jsx)(_sharedUi.TabSelector,{tabs:[{key:"stores",label:"Stores"+(A.length>0?` (${A.length})`:"")},{key:"events",label:"Events"+(k.length>0?` (${k.length})`:"")}],activeTab:x,onTabChange:xe,testIDPrefix:`${_testIDs.ZUSTAND_TEST_ID}-tab`})}),(0,_jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions,{children:[(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{onPress:()=>Q(!0),style:styles.headerActionButton,testID:`${_testIDs.ZUSTAND_TEST_ID}-search-open`,accessibilityRole:"button",accessibilityLabel:"Search",children:(0,_jsxRuntime.jsx)(_sharedUi.Search,{size:14,color:_sharedUi.night.color.textSecondary})}),(0,_jsxRuntime.jsx)(_reactNative.TouchableOpacity,{onPress:()=>y(!0),style:[styles.headerActionButton,Te&&styles.headerActionButtonActive],testID:`${_testIDs.ZUSTAND_TEST_ID}-filters-open`,accessibilityRole:"button",accessibilityLabel:"Filters",children:(0,_jsxRuntime.jsx)(_sharedUi.Filter,{size:14,color:Te?_sharedUi.night.color.accent:_sharedUi.night.color.textSecondary})}),"stores"===x&&(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:ce,disabled:0===A.length,buttonStyle:styles.headerActionButton,testID:`${_testIDs.ZUSTAND_TEST_ID}-copy`}),"events"===x&&(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[(0,_jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton,{value:ue,disabled:0===k.length,buttonStyle:styles.headerActionButton,testID:`${_testIDs.ZUSTAND_TEST_ID}-copy`}),(0,_jsxRuntime.jsx)(_sharedUi.PowerToggleButton,{isEnabled:E,onToggle:w,accessibilityLabel:"Toggle Zustand state capture",testID:`${_testIDs.ZUSTAND_TEST_ID}-power`}),(0,_jsxRuntime.jsx)(_sharedUi.ToolbarClearButton,{onPress:v,disabled:0===k.length,buttonStyle:styles.headerActionButton,testID:`${_testIDs.ZUSTAND_TEST_ID}-clear`})]})]})]}),be=n?_sharedUi.devToolsStorageKeys.modal.root():"buoy-zustand-modal";if(!e)return null;const ye=q&&null!==F?(0,_jsxRuntime.jsx)(_ZustandStateDetailContent.ZustandStateDetailFooter,{change:q,changes:V,selectedIndex:F,onIndexChange:re}):null,Ie=q&&V.length>1?68:0,Ce=!(b||q&&null!==F||null==m&&"events"!==x&&"stores"!==x),Ue=(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.container,children:(()=>{if(b)return(0,_jsxRuntime.jsx)(_ZustandEventFilterView.ZustandEventFilterView,{ignoredPatterns:I,onTogglePattern:he,onAddPattern:ge,stores:Z});if(q&&null!==F)return(0,_jsxRuntime.jsx)(_ZustandStateDetailContent.ZustandStateDetailContent,{change:q,changes:V,selectedIndex:F,onIndexChange:re,disableInternalFooter:!0,initialView:d});if(m)return V.length>0?(0,_jsxRuntime.jsx)(_reactNative.FlatList,{testID:`${_testIDs.ZUSTAND_TEST_ID}-history-list`,data:V,renderItem:je,keyExtractor:Se,contentContainerStyle:styles.listContent,showsVerticalScrollIndicator:!0,..._sharedUi.EVENT_LIST_PRESET}):(0,_jsxRuntime.jsx)(_sharedUi.ToolEmptyState,{testID:`${_testIDs.ZUSTAND_TEST_ID}-history-empty`,...(0,_sharedUi.resolveListEmptyState)({subject:"changes",search:K,total:K.trim()?B:0,capturing:E,waiting:{otto:"waiting",title:"No history yet",message:`State changes for ${m} will appear here.`},paused:{title:"No history yet",message:`Capture is off, so nothing is being recorded for ${m}.`}})});if(T){const e=Z.find(e=>e.name===T)??null;return e?(0,_jsxRuntime.jsx)(_ZustandStoreDetailContent.ZustandStoreDetailContent,{store:e,onViewHistory:ie}):(0,_jsxRuntime.jsx)(_sharedUi.ToolEmptyState,{testID:`${_testIDs.ZUSTAND_TEST_ID}-store-detail-gone`,otto:"ghost",title:"Store no longer registered",message:`${T} is not in the registry any more.`})}return"stores"===x?(0,_jsxRuntime.jsx)(_ZustandStoreBrowser.ZustandStoreBrowser,{stores:A,searchQuery:K,isLoading:i,onOpenStore:le}):(0,_jsxRuntime.jsxs)(_jsxRuntime.Fragment,{children:[!E&&(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.disabledBanner,testID:`${_testIDs.ZUSTAND_TEST_ID}-banner-disabled`,children:[(0,_jsxRuntime.jsx)(_sharedUi.Power,{size:14,color:_sharedUi.night.color.warning}),(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.disabledText,children:"State capture is disabled"})]}),P&&(0,_jsxRuntime.jsxs)(_reactNative.TouchableOpacity,{style:styles.lockedBanner,onPress:ae,activeOpacity:.8,testID:`${_testIDs.ZUSTAND_TEST_ID}-locked`,accessibilityRole:"button",children:[(0,_jsxRuntime.jsx)(_sharedUi.Lock,{size:14,color:_sharedUi.night.color.accent}),(0,_jsxRuntime.jsxs)(_reactNative.Text,{style:styles.lockedText,testID:`${_testIDs.ZUSTAND_TEST_ID}-locked-text`,children:[H," older"," ",1===H?"change":"changes"," locked"]}),(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.upgradeBadge,children:(0,_jsxRuntime.jsx)(_reactNative.Text,{style:styles.upgradeBadgeText,children:"UPGRADE"})})]}),$.length>0?(0,_jsxRuntime.jsx)(_reactNative.FlatList,{ref:X,testID:`${_testIDs.ZUSTAND_TEST_ID}-list`,data:$,renderItem:je,keyExtractor:Se,contentContainerStyle:styles.listContent,showsVerticalScrollIndicator:!0,..._sharedUi.EVENT_LIST_PRESET}):(0,_jsxRuntime.jsx)(EventsEmptyState,{isEnabled:E,searchText:K,totalChanges:N.totalChanges})]})})()}),pe=(0,_jsxRuntime.jsx)(_sharedUi.ProUpgradeModal,{visible:h,onClose:()=>g(!1),featureName:"Full State History"});return r?(0,_jsxRuntime.jsxs)(_sharedUi.TickProvider,{children:[(0,_jsxRuntime.jsxs)(_reactNative.View,{style:styles.inlineRoot,children:[(0,_jsxRuntime.jsx)(_reactNative.View,{style:styles.inlineHeader,..._sharedUi.windowDragProps,children:De()}),Ue,ye]}),pe]}):(0,_jsxRuntime.jsxs)(_sharedUi.TickProvider,{children:[(0,_jsxRuntime.jsx)(_sharedUi.JsModal,{visible:e,onClose:t,onMinimize:a,persistenceKey:be,header:{showToggleButton:!0,customContent:De()},onModeChange:Y,enablePersistence:!0,initialMode:"bottomSheet",enableGlitchEffects:!0,disableScrollWrapper:Ce,styles:{},footer:ye,footerHeight:Ie,children:Ue}),pe]})}function ZustandToolView(e={}){return(0,_jsxRuntime.jsx)(ZustandModal,{...e,inline:!0,visible:!0,onClose:()=>{}})}const styles=_reactNative.StyleSheet.create({inlineRoot:{flex:1,minHeight:0,backgroundColor:"transparent"},inlineHeader:{paddingHorizontal:12,paddingVertical:8,borderBottomWidth:1,borderBottomColor:_sharedUi.macOSColors.border.default,backgroundColor:"transparent"},container:{flex:1,backgroundColor:"transparent"},headerActionButton:{width:32,height:32,borderRadius:8,backgroundColor:_sharedUi.night.color.buttonSurface,borderWidth:1,borderColor:_sharedUi.night.color.border,alignItems:"center",justifyContent:"center"},headerActionButtonDisabled:{opacity:.55},headerActionButtonActive:{backgroundColor:_sharedUi.night.color.accentSoft,borderColor:_sharedUi.night.color.accentBorderStrong},disabledBanner:{flexDirection:"row",alignItems:"center",gap:8,padding:10,marginHorizontal:12,marginTop:8,backgroundColor:_sharedUi.night.color.warningSoft,borderRadius:8,borderWidth:1,borderColor:_sharedUi.night.color.warning+"20"},disabledText:{color:_sharedUi.night.color.warning,fontSize:11,flex:1},listContent:{paddingTop:8,paddingBottom:20},lockedBanner:{flexDirection:"row",alignItems:"center",gap:8,padding:10,marginHorizontal:12,marginTop:8,backgroundColor:_sharedUi.night.color.accentSoft,borderRadius:8,borderWidth:1,borderColor:_sharedUi.night.color.accentBorder},lockedText:{color:_sharedUi.night.color.accent,fontSize:11,fontWeight:"500",flex:1},upgradeBadge:{backgroundColor:_sharedUi.night.color.accent,paddingHorizontal:8,paddingVertical:3,borderRadius:4},upgradeBadgeText:{color:_sharedUi.night.color.onAccent,fontSize:9,fontWeight:"700",letterSpacing:.5}});
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ZustandModal = ZustandModal;
7
+ exports.ZustandToolView = ZustandToolView;
8
+ var _react = require("react");
9
+ var _reactNative = require("react-native");
10
+ var _sharedUi = require("@buoy-gg/shared-ui");
11
+ var _license = require("@buoy-gg/license");
12
+ var _useZustandStateChanges = require("../hooks/useZustandStateChanges");
13
+ var _ZustandStateChangeItem = require("./ZustandStateChangeItem");
14
+ var _ZustandStateDetailContent = require("./ZustandStateDetailContent");
15
+ var _ZustandStoreBrowser = require("./ZustandStoreBrowser");
16
+ var _ZustandStoreDetailContent = require("./ZustandStoreDetailContent");
17
+ var _ZustandEventFilterView = require("./ZustandEventFilterView");
18
+ var _testIDs = require("../utils/testIDs");
19
+ var _jsxRuntime = require("react/jsx-runtime");
20
+ /**
21
+ * Main Zustand DevTools modal
22
+ *
23
+ * Two tabs mirroring the Storage DevTools pattern:
24
+ * - Stores tab: Browse all registered stores and their current state
25
+ * - Events tab: Live state change monitoring with diffs
26
+ */
27
+
28
+ /**
29
+ * The change log's "nothing here". Everything but the nouns is the shared
30
+ * precedence — see `ToolEmptyState` in shared-ui for why it lives there.
31
+ */
32
+ function EventsEmptyState({
33
+ isEnabled,
34
+ searchText,
35
+ totalChanges
36
+ }) {
37
+ const content = (0, _sharedUi.resolveListEmptyState)({
38
+ subject: "changes",
39
+ search: searchText,
40
+ total: totalChanges,
41
+ capturing: isEnabled,
42
+ waiting: {
43
+ otto: "waiting",
44
+ title: "No state changes",
45
+ message: "State changes will appear here as stores update."
46
+ },
47
+ paused: {
48
+ title: "No state changes",
49
+ message: "Enable capture to start recording state changes"
50
+ }
51
+ });
52
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
53
+ ...content,
54
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-empty`
55
+ });
56
+ }
57
+ function ZustandModal({
58
+ visible,
59
+ onClose,
60
+ onBack,
61
+ onMinimize,
62
+ enableSharedModalDimensions = false,
63
+ inline = false,
64
+ storesLoading = false,
65
+ initialTab = "stores",
66
+ initialSelectedChangeId,
67
+ initialDetailView,
68
+ onChangeSelect,
69
+ onStoreExpand
70
+ }) {
71
+ const isPro = (0, _license.useIsPro)();
72
+ const changeLimit = (0, _sharedUi.useLimit)("zustand.changes");
73
+ const [showProModal, setShowProModal] = (0, _react.useState)(false);
74
+ const [activeTab, setActiveTab] = (0, _react.useState)(initialTab);
75
+ const [selectedHistoryStore, setSelectedHistoryStore] = (0, _react.useState)(null);
76
+ /**
77
+ * Which store's DETAIL screen is open (live editor + details + tree —
78
+ * the React Query interaction). Null means the browser list.
79
+ */
80
+ const [selectedStoreName, setSelectedStoreName] = (0, _react.useState)(null);
81
+ const [showFilters, setShowFilters] = (0, _react.useState)(false);
82
+ const [ignoredPatterns, setIgnoredPatterns] = (0, _react.useState)(new Set());
83
+ const {
84
+ stateChanges,
85
+ filteredChanges,
86
+ filter,
87
+ setFilter,
88
+ stats,
89
+ stores,
90
+ clearChanges,
91
+ isEnabled,
92
+ toggleCapture
93
+ } = (0, _useZustandStateChanges.useZustandStateChanges)();
94
+
95
+ // Apply ignored patterns on top of hook's filteredChanges
96
+ const displayedChanges = (0, _react.useMemo)(() => {
97
+ if (ignoredPatterns.size === 0) return filteredChanges;
98
+ return filteredChanges.filter(c => !Array.from(ignoredPatterns).some(p => c.storeName.toLowerCase().includes(p.toLowerCase())));
99
+ }, [filteredChanges, ignoredPatterns]);
100
+
101
+ // Apply ignored patterns to stores list too
102
+ const displayedStores = (0, _react.useMemo)(() => {
103
+ if (ignoredPatterns.size === 0) return stores;
104
+ return stores.filter(s => !Array.from(ignoredPatterns).some(p => s.name.toLowerCase().includes(p.toLowerCase())));
105
+ }, [stores, ignoredPatterns]);
106
+
107
+ /**
108
+ * How many changes this store has captured, before the search — the
109
+ * denominator the scoped empty state needs so "clear the search" is only
110
+ * offered when clearing it would actually show something.
111
+ */
112
+ const scopedTotalForStore = (0, _react.useMemo)(() => {
113
+ if (!selectedHistoryStore) return 0;
114
+ return stateChanges.filter(c => c.storeName === selectedHistoryStore).length;
115
+ }, [stateChanges, selectedHistoryStore]);
116
+
117
+ // Scope to a store BEFORE the tier cap, not after.
118
+ //
119
+ // The other way round, the cap was applied to the whole log and a store's
120
+ // history was whatever survived it — so a store with one change, made a
121
+ // while ago, opened an EMPTY history from a card that said "Changes: 1" and
122
+ // offered a "view history" link. The cap is a limit on how much of a list
123
+ // you can see; which list you are looking at is decided first.
124
+ const scopedChanges = (0, _react.useMemo)(() => {
125
+ if (!selectedHistoryStore) return displayedChanges;
126
+ return displayedChanges.filter(c => c.storeName === selectedHistoryStore);
127
+ }, [displayedChanges, selectedHistoryStore]);
128
+
129
+ // Pro is Infinity here, so slice() returns everything and the locked count
130
+ // floors at 0 — no tier branching needed at the call site.
131
+ const visibleChanges = (0, _react.useMemo)(() => scopedChanges.slice(0, changeLimit), [scopedChanges, changeLimit]);
132
+ const lockedChangeCount = (0, _react.useMemo)(() => Math.max(0, scopedChanges.length - changeLimit), [scopedChanges.length, changeLimit]);
133
+ const hasLockedChanges = lockedChangeCount > 0;
134
+
135
+ /** The list the detail stepper walks: already scoped, already capped. */
136
+ const activeChanges = visibleChanges;
137
+
138
+ // Selection state — track by ID so new events don't shift position
139
+ const [selectedChangeId, setSelectedChangeId] = (0, _react.useState)(null);
140
+ const selectedChangeIndex = (0, _react.useMemo)(() => {
141
+ if (selectedChangeId === null) return null;
142
+ const idx = activeChanges.findIndex(c => c.id === selectedChangeId);
143
+ return idx >= 0 ? idx : null;
144
+ }, [selectedChangeId, activeChanges]);
145
+ const selectedChange = selectedChangeIndex !== null ? activeChanges[selectedChangeIndex] : null;
146
+
147
+ // Notify the host on selection (keyed by change id so re-renders with the
148
+ // same selection don't re-fire; remote mirrors fetch state trees here).
149
+ const lastNotifiedSelectionRef = (0, _react.useRef)(null);
150
+ (0, _react.useEffect)(() => {
151
+ if (!onChangeSelect) return;
152
+ const id = selectedChange?.id ?? null;
153
+ if (id === null || id === lastNotifiedSelectionRef.current) return;
154
+ lastNotifiedSelectionRef.current = id;
155
+ onChangeSelect(selectedChange);
156
+ }, [selectedChange, onChangeSelect]);
157
+
158
+ // Deep link into a change's detail (see initialSelectedChangeId). Applied
159
+ // at most once per id, so the list re-rendering — or the user pressing Back
160
+ // — never re-opens it.
161
+ const appliedInitialSelectionRef = (0, _react.useRef)(null);
162
+ (0, _react.useEffect)(() => {
163
+ if (!initialSelectedChangeId) return;
164
+ if (appliedInitialSelectionRef.current === initialSelectedChangeId) return;
165
+ const exists = activeChanges.some(c => c.id === initialSelectedChangeId);
166
+ if (!exists) return;
167
+ appliedInitialSelectionRef.current = initialSelectedChangeId;
168
+ setSelectedChangeId(initialSelectedChangeId);
169
+ }, [initialSelectedChangeId, activeChanges]);
170
+
171
+ // Search state — shared across tabs
172
+ const [searchText, setSearchText] = (0, _react.useState)("");
173
+ const [isSearchActive, setIsSearchActive] = (0, _react.useState)(false);
174
+ const searchInputRef = (0, _react.useRef)(null);
175
+ const flatListRef = (0, _react.useRef)(null);
176
+ const handleModeChange = (0, _react.useCallback)(_mode => {}, []);
177
+ const handleSearch = text => {
178
+ setSearchText(text);
179
+ if (activeTab === "events") {
180
+ setFilter(prev => ({
181
+ ...prev,
182
+ searchText: text
183
+ }));
184
+ }
185
+ };
186
+
187
+ /** Drop the query AND put the header back. The X, and only the X. */
188
+ const closeSearch = (0, _react.useCallback)(() => {
189
+ setSearchText("");
190
+ setFilter(prev => ({
191
+ ...prev,
192
+ searchText: ""
193
+ }));
194
+ setIsSearchActive(false);
195
+ }, [setFilter]);
196
+ const handleChangePress = (0, _react.useCallback)(change => {
197
+ setSelectedChangeId(change.id);
198
+ }, []);
199
+ const handleUpgradePress = (0, _react.useCallback)(() => {
200
+ setShowProModal(true);
201
+ }, []);
202
+ const handleBack = (0, _react.useCallback)(() => {
203
+ setSelectedChangeId(null);
204
+ }, []);
205
+ const handleIndexChange = (0, _react.useCallback)(newIndex => {
206
+ const change = activeChanges[newIndex];
207
+ if (change) {
208
+ setSelectedChangeId(change.id);
209
+ }
210
+ }, [activeChanges]);
211
+ const handleViewHistory = (0, _react.useCallback)(storeName => {
212
+ setSelectedHistoryStore(storeName);
213
+ setSelectedChangeId(null);
214
+ }, []);
215
+ const handleBackFromHistory = (0, _react.useCallback)(() => {
216
+ setSelectedHistoryStore(null);
217
+ setSelectedChangeId(null);
218
+ }, []);
219
+ const handleOpenStore = (0, _react.useCallback)(storeName => {
220
+ setSelectedStoreName(storeName);
221
+ // The desktop mirror hydrates a store's full state on demand — this is
222
+ // the moment it becomes needed (was fired on card-expand before the
223
+ // detail screen replaced expansion).
224
+ onStoreExpand?.(storeName);
225
+ }, [onStoreExpand]);
226
+ const handleBackFromStore = (0, _react.useCallback)(() => {
227
+ setSelectedStoreName(null);
228
+ }, []);
229
+ const getStoresSnapshot = (0, _react.useCallback)(() => {
230
+ return displayedStores.reduce((acc, store) => {
231
+ try {
232
+ const state = store.api.getState();
233
+ if (state && typeof state === "object") {
234
+ const filtered = {};
235
+ for (const [key, value] of Object.entries(state)) {
236
+ if (typeof value !== "function") filtered[key] = (0, _sharedUi.truncatePayload)(value);
237
+ }
238
+ acc[store.name] = filtered;
239
+ } else {
240
+ acc[store.name] = (0, _sharedUi.truncatePayload)(state);
241
+ }
242
+ } catch {
243
+ acc[store.name] = null;
244
+ }
245
+ return acc;
246
+ }, {});
247
+ }, [displayedStores]);
248
+ const getEventsSnapshot = (0, _react.useCallback)(() => {
249
+ return displayedChanges.map(change => ({
250
+ id: change.id,
251
+ storeName: change.storeName,
252
+ timestamp: change.timestamp,
253
+ prevState: (0, _sharedUi.truncatePayload)(change.prevState),
254
+ nextState: (0, _sharedUi.truncatePayload)(change.nextState),
255
+ hasStateChange: change.hasStateChange,
256
+ changedKeys: change.changedKeys
257
+ }));
258
+ }, [displayedChanges]);
259
+ const getHistorySnapshot = (0, _react.useCallback)(() => {
260
+ return activeChanges.map(change => ({
261
+ id: change.id,
262
+ storeName: change.storeName,
263
+ timestamp: change.timestamp,
264
+ prevState: (0, _sharedUi.truncatePayload)(change.prevState),
265
+ nextState: (0, _sharedUi.truncatePayload)(change.nextState),
266
+ hasStateChange: change.hasStateChange,
267
+ changedKeys: change.changedKeys
268
+ }));
269
+ }, [activeChanges]);
270
+ const handleTogglePattern = (0, _react.useCallback)(pattern => {
271
+ setIgnoredPatterns(prev => {
272
+ const next = new Set(prev);
273
+ if (next.has(pattern)) next.delete(pattern);else next.add(pattern);
274
+ return next;
275
+ });
276
+ }, []);
277
+ const handleAddPattern = (0, _react.useCallback)(pattern => {
278
+ setIgnoredPatterns(prev => new Set([...prev, pattern]));
279
+ }, []);
280
+ const handleTabChange = (0, _react.useCallback)(tab => {
281
+ setActiveTab(tab);
282
+ setSelectedHistoryStore(null);
283
+ setSelectedStoreName(null);
284
+ setSelectedChangeId(null);
285
+ setShowFilters(false);
286
+ if (searchText) {
287
+ setSearchText("");
288
+ setFilter(prev => ({
289
+ ...prev,
290
+ searchText: ""
291
+ }));
292
+ }
293
+ setIsSearchActive(false);
294
+ }, [searchText, setFilter]);
295
+ const keyExtractor = (0, _react.useCallback)(item => item.id, []);
296
+
297
+ // Built for the whole list rather than per row: the ordinal a row carries is
298
+ // a fact about the list around it. See ../utils/testIDs.ts for why a store
299
+ // name alone cannot address a change row.
300
+ const rowTestIDs = (0, _react.useMemo)(() => (0, _testIDs.buildChangeRowTestIDs)(activeChanges), [activeChanges]);
301
+ const renderItem = (0, _react.useCallback)(({
302
+ item
303
+ }) => {
304
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ZustandStateChangeItem.ZustandStateChangeItem, {
305
+ change: item,
306
+ onPress: handleChangePress,
307
+ testID: rowTestIDs.get(item.id)
308
+ });
309
+ }, [handleChangePress, rowTestIDs]);
310
+
311
+ // ---- Header rendering ----
312
+
313
+ const hasActiveFilters = ignoredPatterns.size > 0;
314
+ const renderHeaderContent = () => {
315
+ // Filter view header
316
+ if (showFilters) {
317
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
318
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
319
+ onBack: () => setShowFilters(false)
320
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
321
+ title: "Filters",
322
+ centered: true,
323
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-filters-title`
324
+ })]
325
+ });
326
+ }
327
+
328
+ // Change detail header
329
+ if (selectedChange) {
330
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
331
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
332
+ onBack: handleBack
333
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
334
+ title: `${selectedChange.storeName}/setState`,
335
+ centered: true,
336
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-detail-title`
337
+ })]
338
+ });
339
+ }
340
+
341
+ // Store history header
342
+ if (selectedHistoryStore) {
343
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
344
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
345
+ onBack: handleBackFromHistory
346
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
347
+ title: `${selectedHistoryStore} History`,
348
+ centered: true,
349
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-history-title`
350
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Actions, {
351
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
352
+ value: getHistorySnapshot,
353
+ disabled: activeChanges.length === 0,
354
+ buttonStyle: styles.headerActionButton,
355
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-copy`
356
+ })
357
+ })]
358
+ });
359
+ }
360
+
361
+ // Store detail header — the live editor screen. AFTER history: history is
362
+ // reached FROM the detail screen and stacks over it, so when both are set
363
+ // history owns the header, and its back lands you back on the detail.
364
+ if (selectedStoreName) {
365
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
366
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
367
+ onBack: handleBackFromStore
368
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
369
+ title: selectedStoreName,
370
+ centered: true,
371
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-store-detail-title`
372
+ })]
373
+ });
374
+ }
375
+
376
+ // Main view with tabs
377
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader, {
378
+ children: [onBack && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Navigation, {
379
+ onBack: onBack
380
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Content, {
381
+ title: "",
382
+ children: isSearchActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ModalHeader.Search, {
383
+ value: searchText,
384
+ onChangeText: handleSearch,
385
+ onCancel: closeSearch,
386
+ placeholder: activeTab === "stores" ? "Search stores..." : "Search events...",
387
+ accessibilityLabel: "Search Zustand stores and events",
388
+ iconSize: 14,
389
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-search-field`,
390
+ containerTestID: `${_testIDs.ZUSTAND_TEST_ID}-search`,
391
+ clearTestID: `${_testIDs.ZUSTAND_TEST_ID}-search-clear`
392
+ // Stay open while the query is still narrowing the list: the
393
+ // header would otherwise go back to its TABS — whose counts are
394
+ // of the WHOLE capture — above a filtered list, with nothing to
395
+ // explain the gap.
396
+ ,
397
+ onBlur: () => {
398
+ if (searchText.length === 0) setIsSearchActive(false);
399
+ }
400
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.TabSelector, {
401
+ tabs: [{
402
+ key: "stores",
403
+ label: `Stores${displayedStores.length > 0 ? ` (${displayedStores.length})` : ""}`
404
+ }, {
405
+ key: "events",
406
+ label: `Events${displayedChanges.length > 0 ? ` (${displayedChanges.length})` : ""}`
407
+ }],
408
+ activeTab: activeTab,
409
+ onTabChange: handleTabChange,
410
+ testIDPrefix: `${_testIDs.ZUSTAND_TEST_ID}-tab`
411
+ })
412
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.ModalHeader.Actions, {
413
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
414
+ onPress: () => setIsSearchActive(true),
415
+ style: styles.headerActionButton,
416
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-search-open`,
417
+ accessibilityRole: "button",
418
+ accessibilityLabel: "Search",
419
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Search, {
420
+ size: 14,
421
+ color: _sharedUi.night.color.textSecondary
422
+ })
423
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
424
+ onPress: () => setShowFilters(true),
425
+ style: [styles.headerActionButton, hasActiveFilters && styles.headerActionButtonActive],
426
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-filters-open`,
427
+ accessibilityRole: "button",
428
+ accessibilityLabel: "Filters",
429
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Filter, {
430
+ size: 14,
431
+ color: hasActiveFilters ? _sharedUi.night.color.accent : _sharedUi.night.color.textSecondary
432
+ })
433
+ }), activeTab === "stores" && /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
434
+ value: getStoresSnapshot,
435
+ disabled: displayedStores.length === 0,
436
+ buttonStyle: styles.headerActionButton,
437
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-copy`
438
+ }), activeTab === "events" && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
439
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarCopyButton, {
440
+ value: getEventsSnapshot,
441
+ disabled: displayedChanges.length === 0,
442
+ buttonStyle: styles.headerActionButton,
443
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-copy`
444
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.PowerToggleButton, {
445
+ isEnabled: isEnabled,
446
+ onToggle: toggleCapture,
447
+ accessibilityLabel: "Toggle Zustand state capture",
448
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-power`
449
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolbarClearButton, {
450
+ onPress: clearChanges,
451
+ disabled: displayedChanges.length === 0,
452
+ buttonStyle: styles.headerActionButton,
453
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-clear`
454
+ })]
455
+ })]
456
+ })]
457
+ });
458
+ };
459
+
460
+ // ---- Content rendering ----
461
+
462
+ const renderContent = () => {
463
+ // Filter view (applies to both tabs)
464
+ if (showFilters) {
465
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ZustandEventFilterView.ZustandEventFilterView, {
466
+ ignoredPatterns: ignoredPatterns,
467
+ onTogglePattern: handleTogglePattern,
468
+ onAddPattern: handleAddPattern,
469
+ stores: stores
470
+ });
471
+ }
472
+
473
+ // Change detail view (works for both events tab and store history)
474
+ if (selectedChange && selectedChangeIndex !== null) {
475
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ZustandStateDetailContent.ZustandStateDetailContent, {
476
+ change: selectedChange,
477
+ changes: activeChanges,
478
+ selectedIndex: selectedChangeIndex,
479
+ onIndexChange: handleIndexChange,
480
+ disableInternalFooter: true,
481
+ initialView: initialDetailView
482
+ });
483
+ }
484
+
485
+ // Store history view — filtered to one store
486
+ if (selectedHistoryStore) {
487
+ return activeChanges.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
488
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-history-list`,
489
+ data: activeChanges,
490
+ renderItem: renderItem,
491
+ keyExtractor: keyExtractor,
492
+ contentContainerStyle: styles.listContent,
493
+ showsVerticalScrollIndicator: true,
494
+ ..._sharedUi.EVENT_LIST_PRESET
495
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
496
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-history-empty`,
497
+ ...(0, _sharedUi.resolveListEmptyState)({
498
+ subject: "changes",
499
+ search: searchText,
500
+ // Scoped to one store, so the count is this store's, not the log's.
501
+ total: searchText.trim() ? scopedTotalForStore : 0,
502
+ capturing: isEnabled,
503
+ waiting: {
504
+ otto: "waiting",
505
+ title: "No history yet",
506
+ message: `State changes for ${selectedHistoryStore} will appear here.`
507
+ },
508
+ paused: {
509
+ title: "No history yet",
510
+ message: `Capture is off, so nothing is being recorded for ${selectedHistoryStore}.`
511
+ }
512
+ })
513
+ });
514
+ }
515
+
516
+ // Store detail — live editor + details + read-only tree. After history
517
+ // in this chain for the same reason as the header: history stacks OVER
518
+ // the detail screen it was opened from.
519
+ if (selectedStoreName) {
520
+ const selectedStore = stores.find(s => s.name === selectedStoreName) ?? null;
521
+ return selectedStore ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ZustandStoreDetailContent.ZustandStoreDetailContent, {
522
+ store: selectedStore,
523
+ onViewHistory: handleViewHistory
524
+ }) :
525
+ /*#__PURE__*/
526
+ // The store fell out of the registry mid-look (a remote mirror
527
+ // rebuilding, a dev reload). Say so rather than rendering nothing.
528
+ (0, _jsxRuntime.jsx)(_sharedUi.ToolEmptyState, {
529
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-store-detail-gone`
530
+ // ghost = "stale or detached: an unmounted component, an old
531
+ // snapshot" — exactly what the store you are looking at has become.
532
+ // No other tool has this cause, so it skips the resolver.
533
+ ,
534
+ otto: "ghost",
535
+ title: "Store no longer registered",
536
+ message: `${selectedStoreName} is not in the registry any more.`
537
+ });
538
+ }
539
+
540
+ // Stores browse tab
541
+ if (activeTab === "stores") {
542
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ZustandStoreBrowser.ZustandStoreBrowser, {
543
+ stores: displayedStores,
544
+ searchQuery: searchText,
545
+ isLoading: storesLoading,
546
+ onOpenStore: handleOpenStore
547
+ });
548
+ }
549
+
550
+ // Events tab
551
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
552
+ children: [!isEnabled && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
553
+ style: styles.disabledBanner,
554
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-banner-disabled`,
555
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Power, {
556
+ size: 14,
557
+ color: _sharedUi.night.color.warning
558
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
559
+ style: styles.disabledText,
560
+ children: "State capture is disabled"
561
+ })]
562
+ }), hasLockedChanges && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
563
+ style: styles.lockedBanner,
564
+ onPress: handleUpgradePress,
565
+ activeOpacity: 0.8,
566
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-locked`,
567
+ accessibilityRole: "button",
568
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Lock, {
569
+ size: 14,
570
+ color: _sharedUi.night.color.accent
571
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
572
+ style: styles.lockedText,
573
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-locked-text`,
574
+ children: [lockedChangeCount, " older", " ", lockedChangeCount === 1 ? "change" : "changes", " locked"]
575
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
576
+ style: styles.upgradeBadge,
577
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
578
+ style: styles.upgradeBadgeText,
579
+ children: "UPGRADE"
580
+ })
581
+ })]
582
+ }), visibleChanges.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
583
+ ref: flatListRef,
584
+ testID: `${_testIDs.ZUSTAND_TEST_ID}-list`,
585
+ data: visibleChanges,
586
+ renderItem: renderItem,
587
+ keyExtractor: keyExtractor,
588
+ contentContainerStyle: styles.listContent,
589
+ showsVerticalScrollIndicator: true,
590
+ ..._sharedUi.EVENT_LIST_PRESET
591
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EventsEmptyState, {
592
+ isEnabled: isEnabled,
593
+ searchText: searchText,
594
+ totalChanges: stats.totalChanges
595
+ })]
596
+ });
597
+ };
598
+ const persistenceKey = enableSharedModalDimensions ? _sharedUi.devToolsStorageKeys.modal.root() : "buoy-zustand-modal";
599
+ if (!visible) return null;
600
+ const footerNode = selectedChange && selectedChangeIndex !== null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ZustandStateDetailContent.ZustandStateDetailFooter, {
601
+ change: selectedChange,
602
+ changes: activeChanges,
603
+ selectedIndex: selectedChangeIndex,
604
+ onIndexChange: handleIndexChange
605
+ }) : null;
606
+ const footerHeight = selectedChange && activeChanges.length > 1 ? 68 : 0;
607
+
608
+ // The change lists own their scrolling so FlatList virtualization works
609
+ // (inside JsModal's ScrollView wrapper every row mounts — the Network
610
+ // tool measured 17 JS FPS / 133% CPU at max load from this exact
611
+ // pattern; see tool-bench batches 2026-07-06).
612
+ //
613
+ // The STORES tab owns its scrolling too, for a different reason: the
614
+ // browser hosts the store editor as an absolute-fill overlay over a
615
+ // flex:1 root, and inside JsModal's ScrollView wrapper that root
616
+ // collapses to zero height — the list still paints (iOS overflow), but
617
+ // the editor opens into a 0pt box and never appears. The nested-flex:1-
618
+ // in-JsModal trap; storage's browser disables the wrapper for the same
619
+ // reason.
620
+ const listOwnsScroll = !showFilters && !(selectedChange && selectedChangeIndex !== null) && (selectedHistoryStore != null || activeTab === "events" || activeTab === "stores");
621
+ const body = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
622
+ style: styles.container,
623
+ children: renderContent()
624
+ });
625
+ const upgradeModal = /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ProUpgradeModal, {
626
+ visible: showProModal,
627
+ onClose: () => setShowProModal(false),
628
+ featureName: "Full State History"
629
+ });
630
+
631
+ // The ONLY difference between the phone and the desktop dashboard is which
632
+ // shell the header, body and footer go in. Everything above this is shared.
633
+ if (inline) {
634
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.TickProvider, {
635
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
636
+ style: styles.inlineRoot,
637
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
638
+ style: styles.inlineHeader,
639
+ ..._sharedUi.windowDragProps,
640
+ children: renderHeaderContent()
641
+ }), body, footerNode]
642
+ }), upgradeModal]
643
+ });
644
+ }
645
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.TickProvider, {
646
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.JsModal, {
647
+ visible: visible,
648
+ onClose: onClose,
649
+ onMinimize: onMinimize,
650
+ persistenceKey: persistenceKey,
651
+ header: {
652
+ showToggleButton: true,
653
+ customContent: renderHeaderContent()
654
+ },
655
+ onModeChange: handleModeChange,
656
+ enablePersistence: true,
657
+ initialMode: "bottomSheet",
658
+ enableGlitchEffects: true,
659
+ disableScrollWrapper: listOwnsScroll,
660
+ styles: {},
661
+ footer: footerNode,
662
+ footerHeight: footerHeight,
663
+ children: body
664
+ }), upgradeModal]
665
+ });
666
+ }
667
+
668
+ /**
669
+ * The same Zustand tool as `ZustandModal`, rendered INLINE for hosts that
670
+ * place tools in a panel rather than a modal. Exists so there is exactly one
671
+ * implementation — the desktop dashboard used to ship its own copy of this
672
+ * component's header, list and detail router.
673
+ */
674
+ function ZustandToolView(props = {}) {
675
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(ZustandModal, {
676
+ ...props,
677
+ inline: true,
678
+ visible: true,
679
+ onClose: () => {}
680
+ });
681
+ }
682
+ const styles = _reactNative.StyleSheet.create({
683
+ inlineRoot: {
684
+ flex: 1,
685
+ minHeight: 0,
686
+ // Transparent so the desktop workbench's hoisted sky (one field spanning
687
+ // sidebar + panel) shows through — the shell paints its base beneath it.
688
+ backgroundColor: "transparent"
689
+ },
690
+ inlineHeader: {
691
+ paddingHorizontal: 12,
692
+ paddingVertical: 8,
693
+ borderBottomWidth: 1,
694
+ borderBottomColor: _sharedUi.macOSColors.border.default,
695
+ // Clear, like Bench — the host's hoisted sky shows through the header.
696
+ backgroundColor: "transparent"
697
+ },
698
+ container: {
699
+ flex: 1,
700
+ // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
701
+ backgroundColor: "transparent"
702
+ },
703
+ headerActionButton: {
704
+ width: 32,
705
+ height: 32,
706
+ borderRadius: 8,
707
+ backgroundColor: _sharedUi.night.color.buttonSurface,
708
+ borderWidth: 1,
709
+ borderColor: _sharedUi.night.color.border,
710
+ alignItems: "center",
711
+ justifyContent: "center"
712
+ },
713
+ headerActionButtonDisabled: {
714
+ opacity: 0.55
715
+ },
716
+ headerActionButtonActive: {
717
+ backgroundColor: _sharedUi.night.color.accentSoft,
718
+ borderColor: _sharedUi.night.color.accentBorderStrong
719
+ },
720
+ disabledBanner: {
721
+ flexDirection: "row",
722
+ alignItems: "center",
723
+ gap: 8,
724
+ padding: 10,
725
+ marginHorizontal: 12,
726
+ marginTop: 8,
727
+ backgroundColor: _sharedUi.night.color.warningSoft,
728
+ borderRadius: 8,
729
+ borderWidth: 1,
730
+ borderColor: _sharedUi.night.color.warning + "20"
731
+ },
732
+ disabledText: {
733
+ color: _sharedUi.night.color.warning,
734
+ fontSize: 11,
735
+ flex: 1
736
+ },
737
+ listContent: {
738
+ paddingTop: 8,
739
+ paddingBottom: 20
740
+ },
741
+ lockedBanner: {
742
+ flexDirection: "row",
743
+ alignItems: "center",
744
+ gap: 8,
745
+ padding: 10,
746
+ marginHorizontal: 12,
747
+ marginTop: 8,
748
+ backgroundColor: _sharedUi.night.color.accentSoft,
749
+ borderRadius: 8,
750
+ borderWidth: 1,
751
+ borderColor: _sharedUi.night.color.accentBorder
752
+ },
753
+ lockedText: {
754
+ color: _sharedUi.night.color.accent,
755
+ fontSize: 11,
756
+ fontWeight: "500",
757
+ flex: 1
758
+ },
759
+ upgradeBadge: {
760
+ backgroundColor: _sharedUi.night.color.accent,
761
+ paddingHorizontal: 8,
762
+ paddingVertical: 3,
763
+ borderRadius: 4
764
+ },
765
+ upgradeBadgeText: {
766
+ // Ink on a filled accent surface — the night accent takes dark ink.
767
+ color: _sharedUi.night.color.onAccent,
768
+ fontSize: 9,
769
+ fontWeight: "700",
770
+ letterSpacing: 0.5
771
+ }
772
+ });