@buoy-gg/redux 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 (68) hide show
  1. package/lib/commonjs/index.js +256 -1
  2. package/lib/commonjs/preset.js +90 -1
  3. package/lib/commonjs/redux/components/ReduxActionButton.js +131 -1
  4. package/lib/commonjs/redux/components/ReduxActionDetailContent.js +437 -1
  5. package/lib/commonjs/redux/components/ReduxActionDetailView.js +401 -1
  6. package/lib/commonjs/redux/components/ReduxActionInfoView.js +836 -1
  7. package/lib/commonjs/redux/components/ReduxActionItem.js +334 -1
  8. package/lib/commonjs/redux/components/ReduxAutoInstrument.js +20 -1
  9. package/lib/commonjs/redux/components/ReduxDetailViewToggle.js +50 -1
  10. package/lib/commonjs/redux/components/ReduxIcon.js +18 -1
  11. package/lib/commonjs/redux/components/ReduxModal.js +703 -1
  12. package/lib/commonjs/redux/hooks/useAutoInstrumentRedux.js +187 -1
  13. package/lib/commonjs/redux/hooks/useReduxActions.js +58 -1
  14. package/lib/commonjs/redux/sync/reduxSyncAdapter.js +191 -1
  15. package/lib/commonjs/redux/utils/activeStoreRegistry.js +104 -1
  16. package/lib/commonjs/redux/utils/autoInstrument.js +231 -1
  17. package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +202 -1
  18. package/lib/commonjs/redux/utils/createReduxHistoryAdapter.js +146 -1
  19. package/lib/commonjs/redux/utils/devtoolsHook.js +152 -1
  20. package/lib/commonjs/redux/utils/reduxActionStore.js +473 -1
  21. package/lib/commonjs/redux/utils/snapshotProvider.js +158 -1
  22. package/lib/commonjs/redux/utils/testIDs.js +75 -1
  23. package/lib/module/index.js +125 -1
  24. package/lib/module/preset.js +87 -1
  25. package/lib/module/redux/components/ReduxActionButton.js +128 -1
  26. package/lib/module/redux/components/ReduxActionDetailContent.js +433 -1
  27. package/lib/module/redux/components/ReduxActionDetailView.js +397 -1
  28. package/lib/module/redux/components/ReduxActionInfoView.js +831 -1
  29. package/lib/module/redux/components/ReduxActionItem.js +330 -1
  30. package/lib/module/redux/components/ReduxAutoInstrument.js +17 -1
  31. package/lib/module/redux/components/ReduxDetailViewToggle.js +45 -1
  32. package/lib/module/redux/components/ReduxIcon.js +8 -1
  33. package/lib/module/redux/components/ReduxModal.js +698 -1
  34. package/lib/module/redux/hooks/useAutoInstrumentRedux.js +183 -1
  35. package/lib/module/redux/hooks/useReduxActions.js +54 -1
  36. package/lib/module/redux/sync/reduxSyncAdapter.js +188 -1
  37. package/lib/module/redux/utils/activeStoreRegistry.js +92 -1
  38. package/lib/module/redux/utils/autoInstrument.js +201 -1
  39. package/lib/module/redux/utils/buoyReduxMiddleware.js +188 -1
  40. package/lib/module/redux/utils/createReduxHistoryAdapter.js +142 -1
  41. package/lib/module/redux/utils/devtoolsHook.js +145 -1
  42. package/lib/module/redux/utils/reduxActionStore.js +465 -1
  43. package/lib/module/redux/utils/snapshotProvider.js +157 -1
  44. package/lib/module/redux/utils/testIDs.js +68 -1
  45. package/lib/typescript/index.d.ts.map +1 -0
  46. package/lib/typescript/preset.d.ts.map +1 -0
  47. package/lib/typescript/redux/components/ReduxActionButton.d.ts.map +1 -0
  48. package/lib/typescript/redux/components/ReduxActionDetailContent.d.ts.map +1 -0
  49. package/lib/typescript/redux/components/ReduxActionDetailView.d.ts.map +1 -0
  50. package/lib/typescript/redux/components/ReduxActionInfoView.d.ts.map +1 -0
  51. package/lib/typescript/redux/components/ReduxActionItem.d.ts.map +1 -0
  52. package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts.map +1 -0
  53. package/lib/typescript/redux/components/ReduxDetailViewToggle.d.ts.map +1 -0
  54. package/lib/typescript/redux/components/ReduxIcon.d.ts.map +1 -0
  55. package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -0
  56. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts.map +1 -0
  57. package/lib/typescript/redux/hooks/useReduxActions.d.ts.map +1 -0
  58. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +1 -0
  59. package/lib/typescript/redux/types/index.d.ts.map +1 -0
  60. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +1 -0
  61. package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -0
  62. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -0
  63. package/lib/typescript/redux/utils/createReduxHistoryAdapter.d.ts.map +1 -0
  64. package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +1 -0
  65. package/lib/typescript/redux/utils/reduxActionStore.d.ts.map +1 -0
  66. package/lib/typescript/redux/utils/snapshotProvider.d.ts.map +1 -0
  67. package/lib/typescript/redux/utils/testIDs.d.ts.map +1 -0
  68. package/package.json +4 -4
@@ -1 +1,698 @@
1
- "use strict";import{useState,useMemo,useRef,useCallback,useEffect}from"react";import{View,Text,StyleSheet,FlatList,TouchableOpacity}from"react-native";import{JsModal,ModalHeader,macOSColors,night,Search,Trash2,Power,Zap,CheckCircle,Copy,Lock,AlertTriangle,devToolsStorageKeys,copyToClipboard,ProUpgradeModal,PowerToggleButton,TickProvider,useLimit,TIER_LIMITS,windowDragProps,ToolEmptyState,resolveListEmptyState,EVENT_LIST_PRESET,monoFont}from"@buoy-gg/shared-ui";import{useIsPro}from"@buoy-gg/license";import{useReduxActions}from"../hooks/useReduxActions";import{reduxActionStore}from"../utils/reduxActionStore";import{useAutoInstrumentRedux}from"../hooks/useAutoInstrumentRedux";import{ReduxActionItem}from"./ReduxActionItem";import{ReduxActionDetailContent,ReduxActionDetailFooter}from"./ReduxActionDetailContent";import{REDUX_TEST_ID,buildActionRowTestIDs}from"../utils/testIDs";import{jsx as _jsx,jsxs as _jsxs,Fragment as _Fragment}from"react/jsx-runtime";export const FREE_TIER_ACTION_LIMIT=TIER_LIMITS["redux.actions"].free;function EmptyState({isEnabled:e,query:t,totalActions:o}){const r=resolveListEmptyState({subject:"actions",search:t,total:o,capturing:e,waiting:{otto:"waiting",title:"No Redux actions",message:"Actions will appear here as they are dispatched"},paused:{title:"No Redux actions",message:"Enable capture to start recording actions"}});return _jsx(ToolEmptyState,{...r,testID:`${REDUX_TEST_ID}-empty`})}export function ReduxModal({visible:e,onClose:t,onBack:o,onMinimize:r,enableSharedModalDimensions:i=!1,inline:n=!1,onActionSelect:s,initialSelectedActionId:l,initialDetailView:a}){const c=useIsPro(),d=useLimit("redux.actions"),[u,h]=useState(!1),{isInstrumented:g,isLoading:p,error:y,isReactReduxAvailable:x,captureMechanism:b}=useAutoInstrumentRedux(),{filteredActions:T,filter:m,setFilter:_,stats:S,clearActions:C,isEnabled:D,toggleCapture:I}=useReduxActions(),R=useMemo(()=>T.slice(0,d),[T,d]),f=useMemo(()=>Math.max(0,T.length-d),[T.length,d]),E=f>0,[j,A]=useState(null),w=useMemo(()=>{if(null===j)return null;const e=R.findIndex(e=>e.id===j);return e>=0?e:null},[j,R]),k=null!==w?R[w]:null,B=useRef(null);useEffect(()=>{if(!s)return;const e=k?.id??null;null!==e&&e!==B.current&&(B.current=e,s(k))},[k,s]);const v=useRef(null);useEffect(()=>{l&&v.current!==l&&(R.findIndex(e=>e.id===l)<0||(v.current=l,A(l)))},[l,R]);const[M,L]=useState(""),[P,U]=useState(!1),$=(useRef(null),useRef(null)),z=useCallback(e=>{},[]),V=e=>{L(e),_(t=>({...t,searchText:e}))},X=useCallback(()=>{L(""),_(e=>({...e,searchText:""})),U(!1)},[_]),W=useCallback(e=>{A(e.id)},[]),H=useCallback(()=>{h(!0)},[]),O=useCallback(()=>{A(null)},[]),F=useCallback(e=>{const t=R[e];t&&A(t.id)},[R]),N=async()=>{if(!c)return void h(!0);const e=T.map(e=>({id:e.id,type:e.type,...void 0!==e.payload&&{payload:e.payload},timestamp:e.timestamp,prevState:e.prevState,nextState:e.nextState,hasStateChange:e.hasStateChange}));await copyToClipboard(e)},q=useCallback(e=>e.id,[]),K=useMemo(()=>buildActionRowTestIDs(R),[R]),G=useMemo(()=>{const e=new Map;for(const t of reduxActionStore.getActions()){const o=t.meta?.requestId;o&&e.set(o,(e.get(o)??0)+1)}return e},[T]),J=useCallback(({item:e})=>{const t=e.meta?.requestId;return _jsx(ReduxActionItem,{action:e,onPress:W,linkedCount:t?G.get(t)??0:0,testID:K.get(e.id)})},[W,K,G]),Z=()=>_jsxs(ModalHeader,k?{children:[_jsx(ModalHeader.Navigation,{onBack:O}),_jsx(ModalHeader.Content,{title:k.type,centered:!0,testID:`${REDUX_TEST_ID}-detail-title`})]}:{children:[o&&_jsx(ModalHeader.Navigation,{onBack:o}),_jsx(ModalHeader.Content,{title:"",children:P?_jsx(ModalHeader.Search,{value:M,onChangeText:V,onCancel:X,placeholder:"Search action types...",accessibilityLabel:"Search action types",iconSize:14,testID:`${REDUX_TEST_ID}-search-field`,containerTestID:`${REDUX_TEST_ID}-search`,clearTestID:`${REDUX_TEST_ID}-search-clear`,onBlur:()=>{0===M.length&&U(!1)}}):_jsxs(View,{style:styles.headerChipRow,children:[_jsxs(TouchableOpacity,{style:[styles.headerChip,!m.onlyWithChanges&&styles.headerChipActive],onPress:()=>_(e=>({...e,onlyWithChanges:!1})),testID:`${REDUX_TEST_ID}-chip-all`,accessibilityRole:"button",accessibilityLabel:`Show all actions, ${S.totalActions} captured`,accessibilityState:{selected:!m.onlyWithChanges},children:[_jsx(Text,{style:styles.headerChipLabel,children:"ALL"}),_jsx(Text,{style:[styles.headerChipValue,{color:night.color.text}],testID:`${REDUX_TEST_ID}-chip-all-value`,children:S.totalActions})]}),_jsxs(TouchableOpacity,{style:[styles.headerChip,m.onlyWithChanges&&styles.headerChipActive],onPress:()=>_(e=>({...e,onlyWithChanges:!0})),testID:`${REDUX_TEST_ID}-chip-changed`,accessibilityRole:"button",accessibilityLabel:`Show only actions that changed state, ${S.actionsWithChanges} of them`,accessibilityState:{selected:!!m.onlyWithChanges},children:[_jsx(Zap,{size:12,color:macOSColors.semantic.warning}),_jsx(Text,{style:[styles.headerChipValue,{color:macOSColors.semantic.warning}],testID:`${REDUX_TEST_ID}-chip-changed-value`,children:S.actionsWithChanges})]}),_jsxs(View,{style:styles.headerChip,testID:`${REDUX_TEST_ID}-chip-types`,accessibilityLabel:`${S.uniqueActionTypes} distinct action types`,children:[_jsx(CheckCircle,{size:12,color:macOSColors.semantic.success}),_jsx(Text,{style:[styles.headerChipValue,{color:macOSColors.semantic.success}],testID:`${REDUX_TEST_ID}-chip-types-value`,children:S.uniqueActionTypes})]})]})}),_jsxs(ModalHeader.Actions,{children:[!P&&_jsx(TouchableOpacity,{onPress:()=>U(!0),style:[styles.headerActionButton,M.length>0&&styles.headerActionButtonActive],testID:`${REDUX_TEST_ID}-search-open`,accessibilityRole:"button",accessibilityLabel:"Search action types",children:_jsx(Search,{size:14,color:M.length>0?night.color.accent:night.color.textSecondary})}),_jsx(TouchableOpacity,{onPress:N,style:[styles.headerActionButton,0===T.length&&styles.headerActionButtonDisabled],disabled:0===T.length,testID:`${REDUX_TEST_ID}-copy`,accessibilityRole:"button",accessibilityLabel:"Copy all actions",accessibilityState:{disabled:0===T.length},children:_jsx(Copy,{size:14,color:T.length>0?night.color.textSecondary:night.color.textTertiary})}),_jsx(PowerToggleButton,{isEnabled:D,onToggle:I,accessibilityLabel:"Toggle Redux action capture",testID:`${REDUX_TEST_ID}-power`}),_jsx(TouchableOpacity,{onPress:C,style:[styles.headerActionButton,0===T.length&&styles.headerActionButtonDisabled],disabled:0===T.length,testID:`${REDUX_TEST_ID}-clear`,accessibilityRole:"button",accessibilityLabel:"Clear actions",accessibilityState:{disabled:0===T.length},children:_jsx(Trash2,{size:14,color:(T.length,night.color.textTertiary)})})]})]}),Q=i?devToolsStorageKeys.modal.root():"buoy-redux-modal";if(!e)return null;const Y=k&&null!==w?_jsx(ReduxActionDetailFooter,{action:k,actions:R,selectedIndex:w,onIndexChange:F}):null,ee=k&&R.length>1?68:0,te=null==k,oe=_jsx(View,{style:styles.container,children:k&&null!==w?_jsx(ReduxActionDetailContent,{action:k,actions:R,selectedIndex:w,onIndexChange:F,disableInternalFooter:!0,initialView:a}):_jsxs(_Fragment,{children:[y&&!g&&!reduxActionStore.isCaptureSuppressed()&&_jsxs(View,{style:styles.errorBanner,testID:`${REDUX_TEST_ID}-banner-error`,children:[_jsx(AlertTriangle,{size:14,color:night.color.danger}),_jsx(Text,{style:styles.errorText,children:x?"Not inside a Redux Provider. Wrap your app with <Provider store={store}>.":"react-redux not installed. Run: npm install react-redux"})]}),!D&&g&&_jsxs(View,{style:styles.disabledBanner,testID:`${REDUX_TEST_ID}-banner-disabled`,children:[_jsx(Power,{size:14,color:night.color.warning}),_jsx(Text,{style:styles.disabledText,children:"Action capture is disabled"})]}),"patch"===b&&!reduxActionStore.isCaptureSuppressed()&&_jsxs(View,{style:styles.disabledBanner,testID:`${REDUX_TEST_ID}-banner-patch`,children:[_jsx(AlertTriangle,{size:14,color:night.color.warning}),_jsx(Text,{style:styles.disabledText,children:"Top-level capture only — thunk/RTK Query internals aren't visible. Put import '@buoy-gg/redux' first in your app entry (or add buoyReduxMiddleware) for full capture."})]}),E&&_jsxs(TouchableOpacity,{style:styles.lockedBanner,onPress:H,activeOpacity:.8,testID:`${REDUX_TEST_ID}-locked`,accessibilityRole:"button",accessibilityLabel:`${f} older ${1===f?"action":"actions"} locked. Upgrade to see them.`,children:[_jsx(Lock,{size:14,color:night.color.accent}),_jsxs(Text,{style:styles.lockedText,testID:`${REDUX_TEST_ID}-locked-text`,children:[f," older ",1===f?"action":"actions"," locked"]}),_jsx(View,{style:styles.upgradeBadge,children:_jsx(Text,{style:styles.upgradeBadgeText,children:"UPGRADE"})})]}),R.length>0?_jsx(FlatList,{ref:$,testID:`${REDUX_TEST_ID}-list`,data:R,renderItem:J,keyExtractor:q,contentContainerStyle:styles.listContent,showsVerticalScrollIndicator:!0,...EVENT_LIST_PRESET}):_jsx(EmptyState,{isEnabled:D,query:M,totalActions:S.totalActions})]})}),re=_jsx(ProUpgradeModal,{visible:u,onClose:()=>h(!1),featureName:"Full Action History"});return _jsxs(TickProvider,n?{children:[_jsxs(View,{style:styles.inlineRoot,children:[_jsx(View,{style:styles.inlineHeader,...windowDragProps,children:Z()}),oe,Y]}),re]}:{children:[_jsx(JsModal,{visible:e,onClose:t,onMinimize:r,persistenceKey:Q,header:{showToggleButton:!0,customContent:Z()},onModeChange:z,enablePersistence:!0,initialMode:"bottomSheet",enableGlitchEffects:!0,disableScrollWrapper:te,styles:{},footer:Y,footerHeight:ee,children:oe}),re]})}export function ReduxToolView(e={}){return _jsx(ReduxModal,{...e,inline:!0,visible:!0,onClose:()=>{}})}const styles=StyleSheet.create({inlineRoot:{flex:1,minHeight:0,backgroundColor:"transparent"},inlineHeader:{paddingHorizontal:12,paddingVertical:8,borderBottomWidth:1,borderBottomColor:macOSColors.border.default,backgroundColor:"transparent"},container:{flex:1,backgroundColor:"transparent"},headerChipRow:{flexDirection:"row",alignItems:"center",gap:8},headerChip:{flexDirection:"row",alignItems:"center",gap:4,backgroundColor:night.color.surfaceElevated,paddingHorizontal:10,paddingVertical:5,borderRadius:12,borderWidth:1,borderColor:night.color.border},headerChipActive:{backgroundColor:night.color.accentSoft,borderColor:night.color.accentBorderStrong},headerChipLabel:{fontSize:10,fontWeight:"700",color:night.color.textTertiary},headerChipValue:{fontSize:12,fontWeight:"600",fontFamily:monoFont},headerActionButton:{width:32,height:32,borderRadius:8,backgroundColor:night.color.surfaceElevated,borderWidth:1,borderColor:night.color.border,alignItems:"center",justifyContent:"center"},headerActionButtonDisabled:{opacity:.55},headerActionButtonActive:{backgroundColor:night.color.accentSoft,borderColor:night.color.accentBorderStrong},startButton:{backgroundColor:macOSColors.semantic.successBackground,borderColor:macOSColors.semantic.success+"40"},stopButton:{backgroundColor:macOSColors.semantic.errorBackground,borderColor:macOSColors.semantic.error+"40"},errorBanner:{flexDirection:"row",alignItems:"center",gap:8,padding:10,marginHorizontal:12,marginTop:8,backgroundColor:night.color.dangerSoft,borderRadius:8,borderWidth:1,borderColor:night.color.danger+"20"},errorText:{color:night.color.danger,fontSize:11,flex:1},disabledBanner:{flexDirection:"row",alignItems:"center",gap:8,padding:10,marginHorizontal:12,marginTop:8,backgroundColor:night.color.warningSoft,borderRadius:8,borderWidth:1,borderColor:night.color.warning+"20"},disabledText:{color: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:night.color.accentSoft,borderRadius:8,borderWidth:1,borderColor:night.color.accentBorder},lockedText:{color:night.color.accent,fontSize:11,fontWeight:"500",flex:1},upgradeBadge:{backgroundColor:night.color.accent,paddingHorizontal:8,paddingVertical:3,borderRadius:4},upgradeBadgeText:{color:night.color.onAccent,fontSize:9,fontWeight:"700",letterSpacing:.5}});
1
+ "use strict";
2
+
3
+ /**
4
+ * Main Redux DevTools modal
5
+ *
6
+ * This component auto-instruments the Redux store when it mounts,
7
+ * providing zero-config action capture for the DevTools.
8
+ */
9
+
10
+ import { useState, useMemo, useRef, useCallback, useEffect } from "react";
11
+ import { View, Text, StyleSheet, FlatList, TouchableOpacity } from "react-native";
12
+ import { JsModal, ModalHeader, macOSColors, night, Search, Trash2, Power, Zap, CheckCircle, Copy, Lock, AlertTriangle, devToolsStorageKeys, copyToClipboard, ProUpgradeModal, PowerToggleButton, TickProvider, useLimit, TIER_LIMITS, windowDragProps, ToolEmptyState, resolveListEmptyState, EVENT_LIST_PRESET, monoFont } from "@buoy-gg/shared-ui";
13
+ import { useIsPro } from "@buoy-gg/license";
14
+ import { useReduxActions } from "../hooks/useReduxActions";
15
+ import { reduxActionStore } from "../utils/reduxActionStore";
16
+ import { useAutoInstrumentRedux } from "../hooks/useAutoInstrumentRedux";
17
+ import { ReduxActionItem } from "./ReduxActionItem";
18
+ import { ReduxActionDetailContent, ReduxActionDetailFooter } from "./ReduxActionDetailContent";
19
+ import { REDUX_TEST_ID, buildActionRowTestIDs } from "../utils/testIDs";
20
+
21
+ /**
22
+ * @deprecated Read the live limit with `useLimit("redux.actions")` — this
23
+ * constant only ever reports the FREE tier's value and is blind to the
24
+ * anonymous tier. Kept because it is part of the package's public exports.
25
+ */
26
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
27
+ export const FREE_TIER_ACTION_LIMIT = TIER_LIMITS["redux.actions"].free;
28
+ /**
29
+ * Why this takes the query as well as the enabled flag: "nothing was captured"
30
+ * and "nothing matched what you typed" are different problems, and an empty
31
+ * list under a live search reads as a dead tool. Naming the query back is what
32
+ * makes the difference legible without clearing the field to find out.
33
+ */
34
+ function EmptyState({
35
+ isEnabled,
36
+ query,
37
+ totalActions
38
+ }) {
39
+ const content = resolveListEmptyState({
40
+ subject: "actions",
41
+ search: query,
42
+ total: totalActions,
43
+ capturing: isEnabled,
44
+ waiting: {
45
+ otto: "waiting",
46
+ title: "No Redux actions",
47
+ message: "Actions will appear here as they are dispatched"
48
+ },
49
+ paused: {
50
+ title: "No Redux actions",
51
+ message: "Enable capture to start recording actions"
52
+ }
53
+ });
54
+ return /*#__PURE__*/_jsx(ToolEmptyState, {
55
+ ...content,
56
+ testID: `${REDUX_TEST_ID}-empty`
57
+ });
58
+ }
59
+ export function ReduxModal({
60
+ visible,
61
+ onClose,
62
+ onBack,
63
+ onMinimize,
64
+ enableSharedModalDimensions = false,
65
+ inline = false,
66
+ onActionSelect,
67
+ initialSelectedActionId,
68
+ initialDetailView
69
+ }) {
70
+ const isPro = useIsPro();
71
+ const actionLimit = useLimit("redux.actions");
72
+ const [showProModal, setShowProModal] = useState(false);
73
+
74
+ // Auto-instrument the Redux store (zero-config)
75
+ const {
76
+ isInstrumented,
77
+ isLoading: isInstrumentLoading,
78
+ error: instrumentError,
79
+ isReactReduxAvailable,
80
+ captureMechanism
81
+ } = useAutoInstrumentRedux();
82
+ const {
83
+ filteredActions,
84
+ filter,
85
+ setFilter,
86
+ stats,
87
+ clearActions,
88
+ isEnabled,
89
+ toggleCapture
90
+ } = useReduxActions();
91
+
92
+ // Pro is Infinity here, so slice() returns everything and the locked count
93
+ // floors at 0 — no tier branching needed at the call site.
94
+ const visibleActions = useMemo(() => filteredActions.slice(0, actionLimit), [filteredActions, actionLimit]);
95
+ const lockedActionCount = useMemo(() => Math.max(0, filteredActions.length - actionLimit), [filteredActions.length, actionLimit]);
96
+ const hasLockedActions = lockedActionCount > 0;
97
+
98
+ // Selection state, held as an action ID rather than as a list position.
99
+ //
100
+ // This list grows at the HEAD: every dispatch prepends a row and pushes
101
+ // everything below it down one. An index therefore names a different action
102
+ // a moment after it is taken — so a detail page opened while the app was
103
+ // still dispatching silently became a detail page for some OTHER action,
104
+ // with no transition and nothing to say it had happened. A devtool that
105
+ // quietly swaps what it is showing you is worse than one that shows nothing.
106
+ //
107
+ // The index is still what the stepper and the diff's "previous action" are
108
+ // expressed in — both are genuinely about position — so it is derived here
109
+ // and re-derived whenever the list moves.
110
+ const [selectedActionId, setSelectedActionId] = useState(null);
111
+ const selectedActionIndex = useMemo(() => {
112
+ if (selectedActionId === null) return null;
113
+ const index = visibleActions.findIndex(a => a.id === selectedActionId);
114
+ // Below zero means the action is gone — cleared, or aged out past the tier
115
+ // cap. Falling back to the list is the honest outcome; the alternative is
116
+ // showing whatever moved into its place.
117
+ return index >= 0 ? index : null;
118
+ }, [selectedActionId, visibleActions]);
119
+ const selectedAction = selectedActionIndex !== null ? visibleActions[selectedActionIndex] : null;
120
+
121
+ // Notify the host on selection (keyed by action id so re-renders with the
122
+ // same selection don't re-fire; remote mirrors fetch state trees here).
123
+ const lastNotifiedSelectionRef = useRef(null);
124
+ useEffect(() => {
125
+ if (!onActionSelect) return;
126
+ const id = selectedAction?.id ?? null;
127
+ if (id === null || id === lastNotifiedSelectionRef.current) return;
128
+ lastNotifiedSelectionRef.current = id;
129
+ onActionSelect(selectedAction);
130
+ }, [selectedAction, onActionSelect]);
131
+
132
+ // Deep link into an action's detail (see initialSelectedActionId). Applied
133
+ // at most once per id, so the list re-rendering — or the user pressing Back
134
+ // — never re-opens it.
135
+ const appliedInitialSelectionRef = useRef(null);
136
+ useEffect(() => {
137
+ if (!initialSelectedActionId) return;
138
+ if (appliedInitialSelectionRef.current === initialSelectedActionId) return;
139
+ const index = visibleActions.findIndex(a => a.id === initialSelectedActionId);
140
+ if (index < 0) return;
141
+ appliedInitialSelectionRef.current = initialSelectedActionId;
142
+ setSelectedActionId(initialSelectedActionId);
143
+ }, [initialSelectedActionId, visibleActions]);
144
+ const [searchText, setSearchText] = useState("");
145
+ const [isSearchActive, setIsSearchActive] = useState(false);
146
+ const searchInputRef = useRef(null);
147
+ const flatListRef = useRef(null);
148
+ const handleModeChange = useCallback(_mode => {
149
+ // Mode changes handled by JsModal
150
+ }, []);
151
+ const handleSearch = text => {
152
+ setSearchText(text);
153
+ setFilter(prev => ({
154
+ ...prev,
155
+ searchText: text
156
+ }));
157
+ };
158
+
159
+ /** Drop the query AND put the header back — the ✕ means "stop filtering". */
160
+ const closeSearch = useCallback(() => {
161
+ setSearchText("");
162
+ setFilter(prev => ({
163
+ ...prev,
164
+ searchText: ""
165
+ }));
166
+ setIsSearchActive(false);
167
+ }, [setFilter]);
168
+ const handleActionPress = useCallback(action => {
169
+ setSelectedActionId(action.id);
170
+ }, []);
171
+ const handleUpgradePress = useCallback(() => {
172
+ setShowProModal(true);
173
+ }, []);
174
+ const handleBack = useCallback(() => {
175
+ setSelectedActionId(null);
176
+ }, []);
177
+
178
+ // The stepper and the async timeline both navigate by POSITION; the
179
+ // selection is kept by identity, so the position is resolved to an id here,
180
+ // once, at the moment of the tap.
181
+ const handleIndexChange = useCallback(newIndex => {
182
+ const next = visibleActions[newIndex];
183
+ if (next) setSelectedActionId(next.id);
184
+ }, [visibleActions]);
185
+ const handleCopyAll = async () => {
186
+ if (!isPro) {
187
+ setShowProModal(true);
188
+ return;
189
+ }
190
+ const actionsData = filteredActions.map(action => ({
191
+ id: action.id,
192
+ type: action.type,
193
+ ...(action.payload !== undefined && {
194
+ payload: action.payload
195
+ }),
196
+ timestamp: action.timestamp,
197
+ prevState: action.prevState,
198
+ nextState: action.nextState,
199
+ hasStateChange: action.hasStateChange
200
+ }));
201
+ await copyToClipboard(actionsData);
202
+ };
203
+
204
+ // FlatList optimization
205
+ const keyExtractor = useCallback(item => item.id, []);
206
+
207
+ // An action row's handle carries an ordinal among rows of the SAME type, and
208
+ // an ordinal is a fact about the list rather than about the row — see
209
+ // ../utils/testIDs.ts for why a type alone cannot address a redux row.
210
+ const rowTestIDs = useMemo(() => buildActionRowTestIDs(visibleActions), [visibleActions]);
211
+
212
+ // How many captured actions share each requestId, i.e. how far along each
213
+ // async operation is.
214
+ //
215
+ // Computed HERE rather than in the row, and passed down, because a row's
216
+ // async badge is a fact about OTHER rows. `pending` is captured and drawn
217
+ // while it is still alone in the world; `fulfilled` arrives a moment later
218
+ // and makes the earlier row's badge wrong. `ReduxActionItem` is memoized on
219
+ // its props and the action object it holds never changes identity, so
220
+ // nothing about that second arrival reached the first row — the pending half
221
+ // of every settled thunk permanently showed no flow badge, which is exactly
222
+ // backwards from what the badge is for.
223
+ //
224
+ // One pass over the store instead of a per-row scan: the previous shape was
225
+ // O(rows x actions) at 200 actions, on a list that re-renders on every
226
+ // dispatch.
227
+ const linkedCounts = useMemo(() => {
228
+ const counts = new Map();
229
+ for (const action of reduxActionStore.getActions()) {
230
+ const requestId = action.meta?.requestId;
231
+ if (!requestId) continue;
232
+ counts.set(requestId, (counts.get(requestId) ?? 0) + 1);
233
+ }
234
+ return counts;
235
+ // `filteredActions` takes a new identity whenever the store notifies, which
236
+ // is the only moment these counts can change.
237
+ }, [filteredActions]);
238
+ const renderItem = useCallback(({
239
+ item
240
+ }) => {
241
+ const requestId = item.meta?.requestId;
242
+ return /*#__PURE__*/_jsx(ReduxActionItem, {
243
+ action: item,
244
+ onPress: handleActionPress,
245
+ linkedCount: requestId ? linkedCounts.get(requestId) ?? 0 : 0,
246
+ testID: rowTestIDs.get(item.id)
247
+ });
248
+ }, [handleActionPress, rowTestIDs, linkedCounts]);
249
+ const renderHeaderContent = () => {
250
+ // Detail view header - show action type
251
+ if (selectedAction) {
252
+ return /*#__PURE__*/_jsxs(ModalHeader, {
253
+ children: [/*#__PURE__*/_jsx(ModalHeader.Navigation, {
254
+ onBack: handleBack
255
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
256
+ title: selectedAction.type,
257
+ centered: true,
258
+ testID: `${REDUX_TEST_ID}-detail-title`
259
+ })]
260
+ });
261
+ }
262
+
263
+ // Main list view header
264
+ return /*#__PURE__*/_jsxs(ModalHeader, {
265
+ children: [onBack && /*#__PURE__*/_jsx(ModalHeader.Navigation, {
266
+ onBack: onBack
267
+ }), /*#__PURE__*/_jsx(ModalHeader.Content, {
268
+ title: "",
269
+ children: isSearchActive ? /*#__PURE__*/_jsx(ModalHeader.Search, {
270
+ value: searchText,
271
+ onChangeText: handleSearch,
272
+ onCancel: closeSearch,
273
+ placeholder: "Search action types...",
274
+ accessibilityLabel: "Search action types",
275
+ iconSize: 14,
276
+ testID: `${REDUX_TEST_ID}-search-field`,
277
+ containerTestID: `${REDUX_TEST_ID}-search`,
278
+ clearTestID: `${REDUX_TEST_ID}-search-clear`
279
+ // Stay open while the query still filters the list.
280
+ //
281
+ // Collapsing it on blur hid the reason rows were missing: the
282
+ // header went back to its chips — which count the WHOLE
283
+ // capture, not the filtered list — while the list stayed
284
+ // narrowed, so the tool showed "4 actions" above a single row
285
+ // and offered nothing to explain the gap. The only way back
286
+ // was to reopen a field the user had no reason to think was
287
+ // still armed.
288
+ ,
289
+ onBlur: () => {
290
+ if (searchText.length === 0) setIsSearchActive(false);
291
+ }
292
+ }) : /*#__PURE__*/_jsxs(View, {
293
+ style: styles.headerChipRow,
294
+ children: [/*#__PURE__*/_jsxs(TouchableOpacity, {
295
+ style: [styles.headerChip, !filter.onlyWithChanges && styles.headerChipActive],
296
+ onPress: () => setFilter(prev => ({
297
+ ...prev,
298
+ onlyWithChanges: false
299
+ })),
300
+ testID: `${REDUX_TEST_ID}-chip-all`,
301
+ accessibilityRole: "button",
302
+ accessibilityLabel: `Show all actions, ${stats.totalActions} captured`,
303
+ accessibilityState: {
304
+ selected: !filter.onlyWithChanges
305
+ },
306
+ children: [/*#__PURE__*/_jsx(Text, {
307
+ style: styles.headerChipLabel,
308
+ children: "ALL"
309
+ }), /*#__PURE__*/_jsx(Text, {
310
+ style: [styles.headerChipValue, {
311
+ color: night.color.text
312
+ }],
313
+ testID: `${REDUX_TEST_ID}-chip-all-value`,
314
+ children: stats.totalActions
315
+ })]
316
+ }), /*#__PURE__*/_jsxs(TouchableOpacity, {
317
+ style: [styles.headerChip, filter.onlyWithChanges && styles.headerChipActive],
318
+ onPress: () => setFilter(prev => ({
319
+ ...prev,
320
+ onlyWithChanges: true
321
+ })),
322
+ testID: `${REDUX_TEST_ID}-chip-changed`,
323
+ accessibilityRole: "button",
324
+ accessibilityLabel: `Show only actions that changed state, ${stats.actionsWithChanges} of them`,
325
+ accessibilityState: {
326
+ selected: !!filter.onlyWithChanges
327
+ },
328
+ children: [/*#__PURE__*/_jsx(Zap, {
329
+ size: 12,
330
+ color: macOSColors.semantic.warning
331
+ }), /*#__PURE__*/_jsx(Text, {
332
+ style: [styles.headerChipValue, {
333
+ color: macOSColors.semantic.warning
334
+ }],
335
+ testID: `${REDUX_TEST_ID}-chip-changed-value`,
336
+ children: stats.actionsWithChanges
337
+ })]
338
+ }), /*#__PURE__*/_jsxs(View, {
339
+ style: styles.headerChip,
340
+ testID: `${REDUX_TEST_ID}-chip-types`,
341
+ accessibilityLabel: `${stats.uniqueActionTypes} distinct action types`,
342
+ children: [/*#__PURE__*/_jsx(CheckCircle, {
343
+ size: 12,
344
+ color: macOSColors.semantic.success
345
+ }), /*#__PURE__*/_jsx(Text, {
346
+ style: [styles.headerChipValue, {
347
+ color: macOSColors.semantic.success
348
+ }],
349
+ testID: `${REDUX_TEST_ID}-chip-types-value`,
350
+ children: stats.uniqueActionTypes
351
+ })]
352
+ })]
353
+ })
354
+ }), /*#__PURE__*/_jsxs(ModalHeader.Actions, {
355
+ children: [!isSearchActive && /*#__PURE__*/_jsx(TouchableOpacity, {
356
+ onPress: () => setIsSearchActive(true),
357
+ style: [styles.headerActionButton, searchText.length > 0 && styles.headerActionButtonActive],
358
+ testID: `${REDUX_TEST_ID}-search-open`,
359
+ accessibilityRole: "button",
360
+ accessibilityLabel: "Search action types",
361
+ children: /*#__PURE__*/_jsx(Search, {
362
+ size: 14,
363
+ color: searchText.length > 0 ? night.color.accent : night.color.textSecondary
364
+ })
365
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
366
+ onPress: handleCopyAll,
367
+ style: [styles.headerActionButton, filteredActions.length === 0 && styles.headerActionButtonDisabled],
368
+ disabled: filteredActions.length === 0,
369
+ testID: `${REDUX_TEST_ID}-copy`,
370
+ accessibilityRole: "button",
371
+ accessibilityLabel: "Copy all actions",
372
+ accessibilityState: {
373
+ disabled: filteredActions.length === 0
374
+ },
375
+ children: /*#__PURE__*/_jsx(Copy, {
376
+ size: 14,
377
+ color: filteredActions.length > 0 ? night.color.textSecondary : night.color.textTertiary
378
+ })
379
+ }), /*#__PURE__*/_jsx(PowerToggleButton, {
380
+ isEnabled: isEnabled,
381
+ onToggle: toggleCapture,
382
+ accessibilityLabel: "Toggle Redux action capture",
383
+ testID: `${REDUX_TEST_ID}-power`
384
+ }), /*#__PURE__*/_jsx(TouchableOpacity, {
385
+ onPress: clearActions,
386
+ style: [styles.headerActionButton, filteredActions.length === 0 && styles.headerActionButtonDisabled],
387
+ disabled: filteredActions.length === 0,
388
+ testID: `${REDUX_TEST_ID}-clear`,
389
+ accessibilityRole: "button",
390
+ accessibilityLabel: "Clear actions",
391
+ accessibilityState: {
392
+ disabled: filteredActions.length === 0
393
+ },
394
+ children: /*#__PURE__*/_jsx(Trash2, {
395
+ size: 14,
396
+ color: filteredActions.length > 0 ? night.color.textTertiary : night.color.textTertiary
397
+ })
398
+ })]
399
+ })]
400
+ });
401
+ };
402
+ const persistenceKey = enableSharedModalDimensions ? devToolsStorageKeys.modal.root() : "buoy-redux-modal";
403
+ if (!visible) return null;
404
+
405
+ // Footer for detail view navigation
406
+ const footerNode = selectedAction && selectedActionIndex !== null ? /*#__PURE__*/_jsx(ReduxActionDetailFooter, {
407
+ action: selectedAction,
408
+ actions: visibleActions,
409
+ selectedIndex: selectedActionIndex,
410
+ onIndexChange: handleIndexChange
411
+ }) : null;
412
+ const footerHeight = selectedAction && visibleActions.length > 1 ? 68 : 0;
413
+
414
+ // The action list owns its scrolling so FlatList virtualization works
415
+ // (see ZustandModal — same pattern, same tool-bench evidence).
416
+ const listOwnsScroll = selectedAction == null;
417
+ const body = /*#__PURE__*/_jsx(View, {
418
+ style: styles.container,
419
+ children: selectedAction && selectedActionIndex !== null ? /*#__PURE__*/_jsx(ReduxActionDetailContent, {
420
+ action: selectedAction,
421
+ actions: visibleActions,
422
+ selectedIndex: selectedActionIndex,
423
+ onIndexChange: handleIndexChange,
424
+ disableInternalFooter: true,
425
+ initialView: initialDetailView
426
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
427
+ children: [instrumentError && !isInstrumented && !reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/_jsxs(View, {
428
+ style: styles.errorBanner,
429
+ testID: `${REDUX_TEST_ID}-banner-error`,
430
+ children: [/*#__PURE__*/_jsx(AlertTriangle, {
431
+ size: 14,
432
+ color: night.color.danger
433
+ }), /*#__PURE__*/_jsx(Text, {
434
+ style: styles.errorText,
435
+ children: isReactReduxAvailable ? "Not inside a Redux Provider. Wrap your app with <Provider store={store}>." : "react-redux not installed. Run: npm install react-redux"
436
+ })]
437
+ }), !isEnabled && isInstrumented && /*#__PURE__*/_jsxs(View, {
438
+ style: styles.disabledBanner,
439
+ testID: `${REDUX_TEST_ID}-banner-disabled`,
440
+ children: [/*#__PURE__*/_jsx(Power, {
441
+ size: 14,
442
+ color: night.color.warning
443
+ }), /*#__PURE__*/_jsx(Text, {
444
+ style: styles.disabledText,
445
+ children: "Action capture is disabled"
446
+ })]
447
+ }), captureMechanism === "patch" && !reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/_jsxs(View, {
448
+ style: styles.disabledBanner,
449
+ testID: `${REDUX_TEST_ID}-banner-patch`,
450
+ children: [/*#__PURE__*/_jsx(AlertTriangle, {
451
+ size: 14,
452
+ color: night.color.warning
453
+ }), /*#__PURE__*/_jsx(Text, {
454
+ style: styles.disabledText,
455
+ children: "Top-level capture only \u2014 thunk/RTK Query internals aren't visible. Put import '@buoy-gg/redux' first in your app entry (or add buoyReduxMiddleware) for full capture."
456
+ })]
457
+ }), hasLockedActions && /*#__PURE__*/_jsxs(TouchableOpacity, {
458
+ style: styles.lockedBanner,
459
+ onPress: handleUpgradePress,
460
+ activeOpacity: 0.8,
461
+ testID: `${REDUX_TEST_ID}-locked`,
462
+ accessibilityRole: "button",
463
+ accessibilityLabel: `${lockedActionCount} older ${lockedActionCount === 1 ? "action" : "actions"} locked. Upgrade to see them.`,
464
+ children: [/*#__PURE__*/_jsx(Lock, {
465
+ size: 14,
466
+ color: night.color.accent
467
+ }), /*#__PURE__*/_jsxs(Text, {
468
+ style: styles.lockedText,
469
+ testID: `${REDUX_TEST_ID}-locked-text`,
470
+ children: [lockedActionCount, " older ", lockedActionCount === 1 ? "action" : "actions", " locked"]
471
+ }), /*#__PURE__*/_jsx(View, {
472
+ style: styles.upgradeBadge,
473
+ children: /*#__PURE__*/_jsx(Text, {
474
+ style: styles.upgradeBadgeText,
475
+ children: "UPGRADE"
476
+ })
477
+ })]
478
+ }), visibleActions.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
479
+ ref: flatListRef,
480
+ testID: `${REDUX_TEST_ID}-list`,
481
+ data: visibleActions,
482
+ renderItem: renderItem,
483
+ keyExtractor: keyExtractor,
484
+ contentContainerStyle: styles.listContent,
485
+ showsVerticalScrollIndicator: true,
486
+ ...EVENT_LIST_PRESET
487
+ }) : /*#__PURE__*/_jsx(EmptyState, {
488
+ isEnabled: isEnabled,
489
+ query: searchText,
490
+ totalActions: stats.totalActions
491
+ })]
492
+ })
493
+ });
494
+ const upgradeModal = /*#__PURE__*/_jsx(ProUpgradeModal, {
495
+ visible: showProModal,
496
+ onClose: () => setShowProModal(false),
497
+ featureName: "Full Action History"
498
+ });
499
+
500
+ // The ONLY difference between the phone and the desktop dashboard is which
501
+ // shell the header, body and footer go in. Everything above this is shared.
502
+ if (inline) {
503
+ return /*#__PURE__*/_jsxs(TickProvider, {
504
+ children: [/*#__PURE__*/_jsxs(View, {
505
+ style: styles.inlineRoot,
506
+ children: [/*#__PURE__*/_jsx(View, {
507
+ style: styles.inlineHeader,
508
+ ...windowDragProps,
509
+ children: renderHeaderContent()
510
+ }), body, footerNode]
511
+ }), upgradeModal]
512
+ });
513
+ }
514
+ return /*#__PURE__*/_jsxs(TickProvider, {
515
+ children: [/*#__PURE__*/_jsx(JsModal, {
516
+ visible: visible,
517
+ onClose: onClose,
518
+ onMinimize: onMinimize,
519
+ persistenceKey: persistenceKey,
520
+ header: {
521
+ showToggleButton: true,
522
+ customContent: renderHeaderContent()
523
+ },
524
+ onModeChange: handleModeChange,
525
+ enablePersistence: true,
526
+ initialMode: "bottomSheet",
527
+ enableGlitchEffects: true,
528
+ disableScrollWrapper: listOwnsScroll,
529
+ styles: {},
530
+ footer: footerNode,
531
+ footerHeight: footerHeight,
532
+ children: body
533
+ }), upgradeModal]
534
+ });
535
+ }
536
+
537
+ /**
538
+ * The same Redux tool as `ReduxModal`, rendered INLINE for hosts that place
539
+ * tools in a panel rather than a modal. Exists so there is exactly one
540
+ * implementation — the desktop dashboard used to ship its own copy of this
541
+ * component's header, action list and detail router.
542
+ */
543
+ export function ReduxToolView(props = {}) {
544
+ return /*#__PURE__*/_jsx(ReduxModal, {
545
+ ...props,
546
+ inline: true,
547
+ visible: true,
548
+ onClose: () => {}
549
+ });
550
+ }
551
+ const styles = StyleSheet.create({
552
+ inlineRoot: {
553
+ flex: 1,
554
+ minHeight: 0,
555
+ // Transparent so the desktop workbench's hoisted sky (one field spanning
556
+ // sidebar + panel) shows through — the shell paints its base beneath it.
557
+ backgroundColor: "transparent"
558
+ },
559
+ inlineHeader: {
560
+ paddingHorizontal: 12,
561
+ paddingVertical: 8,
562
+ borderBottomWidth: 1,
563
+ borderBottomColor: macOSColors.border.default,
564
+ // Clear, like Bench — the host's hoisted sky shows through the header.
565
+ backgroundColor: "transparent"
566
+ },
567
+ container: {
568
+ flex: 1,
569
+ // Transparent so JsModal's ToolBackground shows through (the modal paints opaque base beneath it).
570
+ backgroundColor: "transparent"
571
+ },
572
+ headerChipRow: {
573
+ flexDirection: "row",
574
+ alignItems: "center",
575
+ gap: 8
576
+ },
577
+ headerChip: {
578
+ flexDirection: "row",
579
+ alignItems: "center",
580
+ gap: 4,
581
+ backgroundColor: night.color.surfaceElevated,
582
+ paddingHorizontal: 10,
583
+ paddingVertical: 5,
584
+ borderRadius: 12,
585
+ borderWidth: 1,
586
+ borderColor: night.color.border
587
+ },
588
+ headerChipActive: {
589
+ // Active chip chrome takes the night accent, not info-cyan.
590
+ backgroundColor: night.color.accentSoft,
591
+ borderColor: night.color.accentBorderStrong
592
+ },
593
+ headerChipLabel: {
594
+ fontSize: 10,
595
+ fontWeight: "700",
596
+ color: night.color.textTertiary
597
+ },
598
+ headerChipValue: {
599
+ fontSize: 12,
600
+ fontWeight: "600",
601
+ fontFamily: monoFont
602
+ },
603
+ headerActionButton: {
604
+ width: 32,
605
+ height: 32,
606
+ borderRadius: 8,
607
+ backgroundColor: night.color.surfaceElevated,
608
+ borderWidth: 1,
609
+ borderColor: night.color.border,
610
+ alignItems: "center",
611
+ justifyContent: "center"
612
+ },
613
+ headerActionButtonDisabled: {
614
+ opacity: 0.55
615
+ },
616
+ headerActionButtonActive: {
617
+ backgroundColor: night.color.accentSoft,
618
+ borderColor: night.color.accentBorderStrong
619
+ },
620
+ startButton: {
621
+ backgroundColor: macOSColors.semantic.successBackground,
622
+ borderColor: macOSColors.semantic.success + "40"
623
+ },
624
+ stopButton: {
625
+ backgroundColor: macOSColors.semantic.errorBackground,
626
+ borderColor: macOSColors.semantic.error + "40"
627
+ },
628
+ errorBanner: {
629
+ flexDirection: "row",
630
+ alignItems: "center",
631
+ gap: 8,
632
+ padding: 10,
633
+ marginHorizontal: 12,
634
+ marginTop: 8,
635
+ backgroundColor: night.color.dangerSoft,
636
+ borderRadius: 8,
637
+ borderWidth: 1,
638
+ borderColor: night.color.danger + "20"
639
+ },
640
+ errorText: {
641
+ color: night.color.danger,
642
+ fontSize: 11,
643
+ flex: 1
644
+ },
645
+ disabledBanner: {
646
+ flexDirection: "row",
647
+ alignItems: "center",
648
+ gap: 8,
649
+ padding: 10,
650
+ marginHorizontal: 12,
651
+ marginTop: 8,
652
+ backgroundColor: night.color.warningSoft,
653
+ borderRadius: 8,
654
+ borderWidth: 1,
655
+ borderColor: night.color.warning + "20"
656
+ },
657
+ disabledText: {
658
+ color: night.color.warning,
659
+ fontSize: 11,
660
+ flex: 1
661
+ },
662
+ listContent: {
663
+ paddingTop: 8,
664
+ paddingBottom: 20
665
+ },
666
+ // Locked actions banner styles
667
+ lockedBanner: {
668
+ flexDirection: "row",
669
+ alignItems: "center",
670
+ gap: 8,
671
+ padding: 10,
672
+ marginHorizontal: 12,
673
+ marginTop: 8,
674
+ backgroundColor: night.color.accentSoft,
675
+ borderRadius: 8,
676
+ borderWidth: 1,
677
+ borderColor: night.color.accentBorder
678
+ },
679
+ lockedText: {
680
+ color: night.color.accent,
681
+ fontSize: 11,
682
+ fontWeight: "500",
683
+ flex: 1
684
+ },
685
+ upgradeBadge: {
686
+ backgroundColor: night.color.accent,
687
+ paddingHorizontal: 8,
688
+ paddingVertical: 3,
689
+ borderRadius: 4
690
+ },
691
+ upgradeBadgeText: {
692
+ // The night accent takes dark ink.
693
+ color: night.color.onAccent,
694
+ fontSize: 9,
695
+ fontWeight: "700",
696
+ letterSpacing: 0.5
697
+ }
698
+ });