@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.
- package/lib/commonjs/index.js +105 -1
- package/lib/commonjs/preset.js +102 -1
- package/lib/commonjs/zustand/components/ZustandActionButton.js +118 -1
- package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +48 -1
- package/lib/commonjs/zustand/components/ZustandEventFilterView.js +43 -1
- package/lib/commonjs/zustand/components/ZustandIcon.js +18 -1
- package/lib/commonjs/zustand/components/ZustandModal.js +772 -1
- package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +117 -1
- package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +372 -1
- package/lib/commonjs/zustand/components/ZustandStateInfoView.js +488 -1
- package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +204 -1
- package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +471 -1
- package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +393 -1
- package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +74 -1
- package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +382 -1
- package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +284 -1
- package/lib/commonjs/zustand/utils/editableState.js +146 -1
- package/lib/commonjs/zustand/utils/snapshotProvider.js +209 -1
- package/lib/commonjs/zustand/utils/testIDs.js +82 -1
- package/lib/commonjs/zustand/utils/zustandStateStore.js +379 -1
- package/lib/module/index.js +90 -1
- package/lib/module/preset.js +98 -1
- package/lib/module/zustand/components/ZustandActionButton.js +114 -1
- package/lib/module/zustand/components/ZustandDetailViewToggle.js +43 -1
- package/lib/module/zustand/components/ZustandEventFilterView.js +39 -1
- package/lib/module/zustand/components/ZustandIcon.js +11 -1
- package/lib/module/zustand/components/ZustandModal.js +767 -1
- package/lib/module/zustand/components/ZustandStateChangeItem.js +113 -1
- package/lib/module/zustand/components/ZustandStateDetailContent.js +368 -1
- package/lib/module/zustand/components/ZustandStateInfoView.js +483 -1
- package/lib/module/zustand/components/ZustandStoreBrowser.js +200 -1
- package/lib/module/zustand/components/ZustandStoreDetailContent.js +469 -1
- package/lib/module/zustand/components/ZustandStoreEditorScreen.js +389 -1
- package/lib/module/zustand/hooks/useZustandStateChanges.js +70 -1
- package/lib/module/zustand/sync/zustandSyncAdapter.js +380 -1
- package/lib/module/zustand/utils/buoyZustandMiddleware.js +278 -1
- package/lib/module/zustand/utils/editableState.js +138 -1
- package/lib/module/zustand/utils/snapshotProvider.js +209 -1
- package/lib/module/zustand/utils/testIDs.js +74 -1
- package/lib/module/zustand/utils/zustandStateStore.js +375 -1
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/preset.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +1 -0
- package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +1 -0
- package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -0
- package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -0
- package/lib/typescript/zustand/types/index.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/editableState.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/testIDs.d.ts.map +1 -0
- package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -0
- 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
|
+
});
|