@okyrychenko-dev/react-action-guard-devtools 0.1.1 β†’ 0.1.3

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/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  - πŸ“Š **Real-time Timeline** - Visual timeline of all blocking events with duration tracking
12
12
  - 🎯 **Active Blockers View** - See all currently active blockers at a glance
13
- - πŸ” **Filtering** - Search by blocker ID or reason (advanced filters via store API)
13
+ - πŸ” **Filtering** - Search by blocker ID, reason, or scope (advanced filters via store API)
14
14
  - ⏸️ **Pause/Resume** - Pause event recording to inspect specific moments
15
15
  - πŸ“ **Detailed Event Info** - View full configuration, duration, and state changes
16
16
  - 🎨 **Customizable Position** - Place devtools panel on the left or right
@@ -31,7 +31,7 @@ pnpm add @okyrychenko-dev/react-action-guard-devtools
31
31
 
32
32
  This package requires the following peer dependencies:
33
33
 
34
- - [@okyrychenko-dev/react-action-guard](https://github.com/okyrychenko-dev/react-action-guard) ^0.5.0
34
+ - [@okyrychenko-dev/react-action-guard](https://github.com/okyrychenko-dev/react-action-guard) ^0.6.0
35
35
  - [React](https://react.dev/) ^17.0.0 || ^18.0.0 || ^19.0.0
36
36
 
37
37
  ## Quick Start
@@ -216,7 +216,7 @@ See all currently active blockers with:
216
216
 
217
217
  Filter events by:
218
218
 
219
- - **Search**: Search by blocker ID or reason
219
+ - **Search**: Search by blocker ID, reason, or scope
220
220
 
221
221
  For action/scope filtering, use `useDevtoolsStore` and `setFilter` in your own UI.
222
222
 
@@ -241,9 +241,6 @@ The package is written in TypeScript and includes full type definitions:
241
241
 
242
242
  ```typescript
243
243
  import type {
244
- // Component types
245
- ActionGuardDevtoolsProps,
246
-
247
244
  // Event types
248
245
  DevtoolsEvent,
249
246
  DevtoolsFilter,
package/dist/index.cjs CHANGED
@@ -3,135 +3,146 @@
3
3
  var reactActionGuard = require('@okyrychenko-dev/react-action-guard');
4
4
  var react = require('react');
5
5
  var reactZustandToolkit = require('@okyrychenko-dev/react-zustand-toolkit');
6
- var clsx2 = require('clsx');
6
+ var clsx = require('clsx');
7
7
  var zustand = require('zustand');
8
8
  var jsxRuntime = require('react/jsx-runtime');
9
9
  var shallow = require('zustand/react/shallow');
10
10
 
11
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
-
13
- var clsx2__default = /*#__PURE__*/_interopDefault(clsx2);
14
-
15
11
  // src/components/actionGuardDevtools/ActionGuardDevtools.tsx
16
12
 
17
13
  // src/store/devtoolsStore.constants.ts
18
14
  var DEFAULT_FILTER = {
19
- actions: ["add", "remove", "update", "cancel", "timeout"],
15
+ actions: ["add", "remove", "update", "timeout", "clear", "clear_scope"],
20
16
  scopes: [],
21
17
  search: ""
22
18
  };
19
+ var createDefaultFilter = () => ({
20
+ actions: [...DEFAULT_FILTER.actions],
21
+ scopes: [...DEFAULT_FILTER.scopes],
22
+ search: DEFAULT_FILTER.search
23
+ });
23
24
  var DEFAULT_MAX_EVENTS = 200;
24
25
  var DEFAULT_TAB = "timeline";
25
26
 
26
27
  // src/store/devtoolsStore.actions.ts
27
- var createDevtoolsActions = (set, get) => ({
28
- // Initial State
29
- events: [],
30
- maxEvents: DEFAULT_MAX_EVENTS,
31
- isOpen: false,
32
- isMinimized: false,
33
- activeTab: DEFAULT_TAB,
34
- filter: DEFAULT_FILTER,
35
- selectedEventId: null,
36
- isPaused: false,
37
- // Actions
38
- /**
39
- * Add a new event to history
40
- *
41
- * @param eventData - Event data without ID (auto-generated)
42
- */
43
- addEvent: (eventData) => {
44
- if (get().isPaused) {
45
- return;
28
+ var createDevtoolsActions = (set, get) => {
29
+ let eventCounter = 0;
30
+ const createEventId = (eventData) => {
31
+ eventCounter += 1;
32
+ return `${String(eventData.timestamp)}-${eventData.blockerId}-${eventCounter.toString(36)}`;
33
+ };
34
+ const trimEvents = (events, maxEvents) => {
35
+ if (events.length <= maxEvents) {
36
+ return events;
46
37
  }
47
- const event = {
48
- ...eventData,
49
- id: `${String(eventData.timestamp)}-${eventData.blockerId}-${Math.random().toString(36).slice(2, 8)}`
50
- };
51
- set((state) => {
52
- const newEvents = [event, ...state.events];
53
- if (newEvents.length > state.maxEvents) {
54
- newEvents.pop();
38
+ return events.slice(0, maxEvents);
39
+ };
40
+ return {
41
+ // Initial State
42
+ events: [],
43
+ maxEvents: DEFAULT_MAX_EVENTS,
44
+ isOpen: false,
45
+ isMinimized: false,
46
+ activeTab: DEFAULT_TAB,
47
+ filter: createDefaultFilter(),
48
+ selectedEventId: null,
49
+ isPaused: false,
50
+ // Actions
51
+ /**
52
+ * Add a new event to history
53
+ *
54
+ * @param eventData - Event data without ID (auto-generated)
55
+ */
56
+ addEvent: (eventData) => {
57
+ if (get().isPaused) {
58
+ return;
55
59
  }
56
- return { events: newEvents };
57
- });
58
- },
59
- /**
60
- * Clear all events from history
61
- */
62
- clearEvents: () => {
63
- set({ events: [], selectedEventId: null });
64
- },
65
- /**
66
- * Toggle panel open/closed state
67
- */
68
- toggleOpen: () => {
69
- set((state) => ({ isOpen: !state.isOpen }));
70
- },
71
- /**
72
- * Set panel open state
73
- *
74
- * @param open - Whether panel should be open
75
- */
76
- setOpen: (open) => {
77
- set({ isOpen: open });
78
- },
79
- /**
80
- * Toggle minimized state
81
- */
82
- toggleMinimized: () => {
83
- set((state) => ({ isMinimized: !state.isMinimized }));
84
- },
85
- /**
86
- * Set active tab
87
- *
88
- * @param tab - Tab to activate
89
- */
90
- setActiveTab: (tab) => {
91
- set({ activeTab: tab, selectedEventId: null });
92
- },
93
- /**
94
- * Update filter settings (partial update)
95
- *
96
- * @param filterUpdate - Partial filter update
97
- */
98
- setFilter: (filterUpdate) => {
99
- set((state) => ({
100
- filter: { ...state.filter, ...filterUpdate }
101
- }));
102
- },
103
- /**
104
- * Reset filters to default
105
- */
106
- resetFilter: () => {
107
- set({ filter: DEFAULT_FILTER });
108
- },
109
- /**
110
- * Select an event for detail view
111
- *
112
- * @param eventId - Event ID to select (null to deselect)
113
- */
114
- selectEvent: (eventId) => {
115
- set({ selectedEventId: eventId });
116
- },
117
- /**
118
- * Toggle pause state (stops/resumes recording)
119
- */
120
- togglePause: () => {
121
- set((state) => ({ isPaused: !state.isPaused }));
122
- },
123
- /**
124
- * Set maximum events limit
125
- *
126
- * @param max - Maximum number of events to keep
127
- */
128
- setMaxEvents: (max) => {
129
- set((state) => {
130
- const events = state.events.length > max ? state.events.slice(0, max) : state.events;
131
- return { maxEvents: max, events };
132
- });
133
- }
134
- });
60
+ const event = {
61
+ ...eventData,
62
+ id: createEventId(eventData)
63
+ };
64
+ set((state) => {
65
+ const newEvents = [event, ...state.events];
66
+ return { events: trimEvents(newEvents, state.maxEvents) };
67
+ });
68
+ },
69
+ /**
70
+ * Clear all events from history
71
+ */
72
+ clearEvents: () => {
73
+ set({ events: [], selectedEventId: null });
74
+ },
75
+ /**
76
+ * Toggle panel open/closed state
77
+ */
78
+ toggleOpen: () => {
79
+ set((state) => ({ isOpen: !state.isOpen }));
80
+ },
81
+ /**
82
+ * Set panel open state
83
+ *
84
+ * @param open - Whether panel should be open
85
+ */
86
+ setOpen: (open) => {
87
+ set({ isOpen: open });
88
+ },
89
+ /**
90
+ * Toggle minimized state
91
+ */
92
+ toggleMinimized: () => {
93
+ set((state) => ({ isMinimized: !state.isMinimized }));
94
+ },
95
+ /**
96
+ * Set active tab
97
+ *
98
+ * @param tab - Tab to activate
99
+ */
100
+ setActiveTab: (tab) => {
101
+ set({ activeTab: tab, selectedEventId: null });
102
+ },
103
+ /**
104
+ * Update filter settings (partial update)
105
+ *
106
+ * @param filterUpdate - Partial filter update
107
+ */
108
+ setFilter: (filterUpdate) => {
109
+ set((state) => ({
110
+ filter: { ...state.filter, ...filterUpdate }
111
+ }));
112
+ },
113
+ /**
114
+ * Reset filters to default
115
+ */
116
+ resetFilter: () => {
117
+ set({ filter: createDefaultFilter() });
118
+ },
119
+ /**
120
+ * Select an event for detail view
121
+ *
122
+ * @param eventId - Event ID to select (null to deselect)
123
+ */
124
+ selectEvent: (eventId) => {
125
+ set({ selectedEventId: eventId });
126
+ },
127
+ /**
128
+ * Toggle pause state (stops/resumes recording)
129
+ */
130
+ togglePause: () => {
131
+ set((state) => ({ isPaused: !state.isPaused }));
132
+ },
133
+ /**
134
+ * Set maximum events limit
135
+ *
136
+ * @param max - Maximum number of events to keep
137
+ */
138
+ setMaxEvents: (max) => {
139
+ set((state) => ({
140
+ maxEvents: max,
141
+ events: trimEvents(state.events, max)
142
+ }));
143
+ }
144
+ };
145
+ };
135
146
 
136
147
  // src/store/devtoolsStore.store.ts
137
148
  var {
@@ -140,40 +151,50 @@ var {
140
151
  } = reactZustandToolkit.createShallowStore(createDevtoolsActions);
141
152
 
142
153
  // src/store/devtoolsStore.selectors.ts
154
+ function normalizeScopes(scope) {
155
+ if (!scope) {
156
+ return [];
157
+ }
158
+ if (typeof scope === "string") {
159
+ return [scope];
160
+ }
161
+ return scope;
162
+ }
163
+ function matchesActionFilter(event, actions) {
164
+ return actions.length === 0 || actions.includes(event.action);
165
+ }
166
+ function matchesScopeFilter(event, scopes) {
167
+ if (scopes.length === 0) {
168
+ return true;
169
+ }
170
+ const eventScopes = normalizeScopes(event.config?.scope);
171
+ if (eventScopes.length === 0) {
172
+ return false;
173
+ }
174
+ return eventScopes.some((scope) => scopes.includes(scope));
175
+ }
176
+ function matchesSearchQuery(event, search) {
177
+ if (!search) {
178
+ return true;
179
+ }
180
+ const searchLower = search.toLowerCase();
181
+ const matchesId = event.blockerId.toLowerCase().includes(searchLower);
182
+ const matchesReason = (event.config?.reason ?? "").toLowerCase().includes(searchLower);
183
+ const matchesScope = normalizeScopes(event.config?.scope).some(
184
+ (scope) => scope.toLowerCase().includes(searchLower)
185
+ );
186
+ return matchesId || matchesReason || matchesScope;
187
+ }
143
188
  function selectFilteredEvents(state) {
144
189
  const { events, filter } = state;
145
190
  return events.filter((event) => {
146
- if (filter.actions.length > 0 && !filter.actions.includes(event.action)) {
147
- return false;
148
- }
149
- if (filter.scopes.length > 0) {
150
- if (!event.config?.scope) {
151
- return false;
152
- }
153
- const eventScopes = Array.isArray(event.config.scope) ? event.config.scope : [event.config.scope];
154
- const hasMatchingScope = eventScopes.some((s) => filter.scopes.includes(s));
155
- if (!hasMatchingScope) {
156
- return false;
157
- }
158
- }
159
- if (filter.search) {
160
- const searchLower = filter.search.toLowerCase();
161
- const matchesId = event.blockerId.toLowerCase().includes(searchLower);
162
- const matchesReason = event.config?.reason?.toLowerCase().includes(searchLower);
163
- if (!matchesId && !matchesReason) {
164
- return false;
165
- }
166
- }
167
- return true;
191
+ return matchesActionFilter(event, filter.actions) && matchesScopeFilter(event, filter.scopes) && matchesSearchQuery(event, filter.search);
168
192
  });
169
193
  }
170
194
  function selectUniqueScopes(state) {
171
195
  const scopes = /* @__PURE__ */ new Set();
172
196
  state.events.forEach((event) => {
173
- if (event.config?.scope) {
174
- const eventScopes = Array.isArray(event.config.scope) ? event.config.scope : [event.config.scope];
175
- eventScopes.forEach((s) => scopes.add(s));
176
- }
197
+ normalizeScopes(event.config?.scope).forEach((scope) => scopes.add(scope));
177
198
  });
178
199
  return Array.from(scopes).sort();
179
200
  }
@@ -185,19 +206,29 @@ function selectAllEvents(state) {
185
206
  var DEVTOOLS_MIDDLEWARE_NAME = "action-guard-devtools";
186
207
  function createDevtoolsMiddleware() {
187
208
  const addTimestamps = /* @__PURE__ */ new Map();
209
+ const terminalActions = /* @__PURE__ */ new Set([
210
+ "remove",
211
+ "timeout",
212
+ "clear",
213
+ "clear_scope"
214
+ ]);
215
+ const getDuration = (action, blockerId, timestamp) => {
216
+ if (!terminalActions.has(action)) {
217
+ return void 0;
218
+ }
219
+ const addTime = addTimestamps.get(blockerId);
220
+ if (addTime === void 0) {
221
+ return void 0;
222
+ }
223
+ addTimestamps.delete(blockerId);
224
+ return timestamp - addTime;
225
+ };
188
226
  return (context) => {
189
227
  const { addEvent } = devtoolsStoreApi.getState();
190
228
  if (context.action === "add") {
191
229
  addTimestamps.set(context.blockerId, context.timestamp);
192
230
  }
193
- let duration;
194
- if (context.action === "remove" || context.action === "timeout" || context.action === "cancel") {
195
- const addTime = addTimestamps.get(context.blockerId);
196
- if (addTime !== void 0) {
197
- duration = context.timestamp - addTime;
198
- addTimestamps.delete(context.blockerId);
199
- }
200
- }
231
+ const duration = getDuration(context.action, context.blockerId, context.timestamp);
201
232
  addEvent({
202
233
  action: context.action,
203
234
  blockerId: context.blockerId,
@@ -209,6 +240,33 @@ function createDevtoolsMiddleware() {
209
240
  };
210
241
  }
211
242
 
243
+ // src/components/actionGuardDevtools/ActionGuardDevtools.utils.ts
244
+ function isTypingTarget(target) {
245
+ return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
246
+ }
247
+ function getDevtoolsKeyboardAction(event, isOpen) {
248
+ if (!isOpen) {
249
+ return null;
250
+ }
251
+ if (isTypingTarget(event.target)) {
252
+ return null;
253
+ }
254
+ switch (event.key) {
255
+ case "Escape":
256
+ return { action: "close", preventDefault: false };
257
+ case " ":
258
+ return { action: "togglePause", preventDefault: true };
259
+ case "c":
260
+ case "C":
261
+ if (!event.metaKey && !event.ctrlKey) {
262
+ return { action: "clearEvents", preventDefault: false };
263
+ }
264
+ return null;
265
+ default:
266
+ return null;
267
+ }
268
+ }
269
+
212
270
  // src/styles/position.module.css
213
271
  var position_default = {};
214
272
 
@@ -220,7 +278,7 @@ function getPositionClass(position) {
220
278
  return position === "left" ? position_default.positionLeft : position_default.positionRight;
221
279
  }
222
280
  function getPanelClassName(position, isMinimized) {
223
- return clsx2.clsx(
281
+ return clsx.clsx(
224
282
  DevtoolsPanel_default.panel,
225
283
  position_default.positionBase,
226
284
  getPositionClass(position),
@@ -280,28 +338,52 @@ var COLORS = {
280
338
  var shared_default = {};
281
339
  function Badge(props) {
282
340
  const { children, className, style } = props;
283
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx2__default.default(shared_default.badge, className), style, children });
341
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: clsx.clsx(shared_default.badge, className), style, children });
284
342
  }
285
343
  var Badge_default = Badge;
286
344
  function Content(props) {
287
345
  const { children, className } = props;
288
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx2__default.default(shared_default.content, className), children });
346
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx.clsx(shared_default.content, className), children });
289
347
  }
290
348
  var Content_default = Content;
291
349
  function EmptyState(props) {
292
350
  const { children, className } = props;
293
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx2__default.default(shared_default.emptyState, className), children });
351
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: clsx.clsx(shared_default.emptyState, className), children });
294
352
  }
295
353
  var EmptyState_default = EmptyState;
354
+ var ErrorBoundary = class extends react.Component {
355
+ constructor(props) {
356
+ super(props);
357
+ this.state = { hasError: false, error: null };
358
+ }
359
+ static getDerivedStateFromError(error) {
360
+ return { hasError: true, error };
361
+ }
362
+ render() {
363
+ const { hasError, error } = this.state;
364
+ const { children, fallback } = this.props;
365
+ if (hasError) {
366
+ if (fallback) {
367
+ return fallback;
368
+ }
369
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: shared_default.errorBoundary, children: [
370
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: shared_default.errorTitle, children: "Devtools Error" }),
371
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: shared_default.errorMessage, children: error?.message ?? "Unknown error" })
372
+ ] });
373
+ }
374
+ return children;
375
+ }
376
+ };
377
+ var ErrorBoundary_default = ErrorBoundary;
296
378
  function EventBadge(props) {
297
379
  const { children, className, style, action } = props;
298
- const classes = clsx2.clsx(shared_default.eventBadge, action && shared_default.eventBadgeAction, className);
380
+ const classes = clsx.clsx(shared_default.eventBadge, action && shared_default.eventBadgeAction, className);
299
381
  return /* @__PURE__ */ jsxRuntime.jsx("span", { className: classes, style, "data-action": action, children });
300
382
  }
301
383
  var EventBadge_default = EventBadge;
302
384
  function IconButton(props) {
303
385
  const { children, className, type = "button", ...others } = props;
304
- return /* @__PURE__ */ jsxRuntime.jsx("button", { type, className: clsx2__default.default(shared_default.iconButton, className), ...others, children });
386
+ return /* @__PURE__ */ jsxRuntime.jsx("button", { type, className: clsx.clsx(shared_default.iconButton, className), ...others, children });
305
387
  }
306
388
  var IconButton_default = IconButton;
307
389
 
@@ -428,7 +510,7 @@ function EventItem(props) {
428
510
  const handleSelect = () => {
429
511
  onSelect(selected ? null : event.id);
430
512
  };
431
- return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: clsx2.clsx(EventItem_default.eventItem, selected && EventItem_default.selected), onClick: handleSelect, children: [
513
+ return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: clsx.clsx(EventItem_default.eventItem, selected && EventItem_default.selected), onClick: handleSelect, children: [
432
514
  /* @__PURE__ */ jsxRuntime.jsx(EventItemHeader_default, { event }),
433
515
  /* @__PURE__ */ jsxRuntime.jsx(EventItemDetails_default, { event })
434
516
  ] });
@@ -601,7 +683,7 @@ function Timeline() {
601
683
  );
602
684
  const selectedEvent = selectedEventId ? events.find((e) => e.id === selectedEventId) : null;
603
685
  react.useEffect(() => {
604
- if (selectedEventId && !allEvents.some((e) => e.id === selectedEventId)) {
686
+ if (selectedEventId && !allEvents.some((event) => event.id === selectedEventId)) {
605
687
  selectEvent(null);
606
688
  }
607
689
  }, [selectedEventId, allEvents, selectEvent]);
@@ -633,7 +715,7 @@ function Timeline() {
633
715
  var Timeline_default2 = Timeline;
634
716
  function DevtoolsPanelContent(props) {
635
717
  const { activeTab, store } = props;
636
- return activeTab === "timeline" ? /* @__PURE__ */ jsxRuntime.jsx(Timeline_default2, {}) : /* @__PURE__ */ jsxRuntime.jsx(ActiveBlockers_default2, { store });
718
+ return /* @__PURE__ */ jsxRuntime.jsx(ErrorBoundary_default, { children: activeTab === "timeline" ? /* @__PURE__ */ jsxRuntime.jsx(Timeline_default2, {}) : /* @__PURE__ */ jsxRuntime.jsx(ActiveBlockers_default2, { store }) });
637
719
  }
638
720
  var DevtoolsPanelContent_default = DevtoolsPanelContent;
639
721
  var MaximizeIcon = createSvgIcon(/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3h18v2H3z" }));
@@ -644,7 +726,7 @@ function getMinimizeButtonTitle(isMinimized) {
644
726
  return isMinimized ? "Maximize" : "Minimize";
645
727
  }
646
728
  function getPauseButtonClassName(isPaused) {
647
- return clsx2.clsx(isPaused && DevtoolsPanel_default.pauseButtonActive);
729
+ return clsx.clsx(isPaused && DevtoolsPanel_default.pauseButtonActive);
648
730
  }
649
731
  function DevtoolsPanelHeader(props) {
650
732
  const {
@@ -693,7 +775,7 @@ function DevtoolsPanelTabs(props) {
693
775
  onClick: () => {
694
776
  onSelectTab(tab.id);
695
777
  },
696
- className: clsx2.clsx(DevtoolsPanel_default.tab, activeTab === tab.id && DevtoolsPanel_default.tabActive),
778
+ className: clsx.clsx(DevtoolsPanel_default.tab, activeTab === tab.id && DevtoolsPanel_default.tabActive),
697
779
  children: tab.label
698
780
  },
699
781
  tab.id
@@ -746,7 +828,7 @@ function getPositionClass2(position) {
746
828
  return position === "left" ? position_default.positionLeft : position_default.positionRight;
747
829
  }
748
830
  function getToggleButtonClassName(position) {
749
- return clsx2.clsx(
831
+ return clsx.clsx(
750
832
  ToggleButton_default.toggleButton,
751
833
  position_default.positionBase,
752
834
  getPositionClass2(position),
@@ -757,7 +839,7 @@ function shouldShowBadge(count) {
757
839
  return count > 0;
758
840
  }
759
841
  function getBadgeClassName(isPaused) {
760
- return clsx2.clsx(ToggleButton_default.badgePosition, isPaused ? ToggleButton_default.badgePaused : ToggleButton_default.badgeActive);
842
+ return clsx.clsx(ToggleButton_default.badgePosition, isPaused ? ToggleButton_default.badgePaused : ToggleButton_default.badgeActive);
761
843
  }
762
844
  function ToggleButtonBadge(props) {
763
845
  const { count, isPaused } = props;
@@ -798,41 +880,8 @@ function ActionGuardDevtoolsContent(props) {
798
880
  ] });
799
881
  }
800
882
  var ActionGuardDevtoolsContent_default = ActionGuardDevtoolsContent;
801
-
802
- // src/components/actionGuardDevtools/ActionGuardDevtools.utils.ts
803
- function isTypingTarget(target) {
804
- return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
805
- }
806
- function getDevtoolsKeyboardAction(event, isOpen) {
807
- if (!isOpen) {
808
- return null;
809
- }
810
- if (isTypingTarget(event.target)) {
811
- return null;
812
- }
813
- switch (event.key) {
814
- case "Escape":
815
- return { action: "close", preventDefault: false };
816
- case " ":
817
- return { action: "togglePause", preventDefault: true };
818
- case "c":
819
- case "C":
820
- if (!event.metaKey && !event.ctrlKey) {
821
- return { action: "clearEvents", preventDefault: false };
822
- }
823
- return null;
824
- default:
825
- return null;
826
- }
827
- }
828
- function ActionGuardDevtools(props) {
829
- const {
830
- position = "right",
831
- defaultOpen = false,
832
- maxEvents = 200,
833
- showInProduction = false,
834
- store: customStore
835
- } = props;
883
+ function ActionGuardDevtoolsInternal(props) {
884
+ const { position = "right", defaultOpen = false, maxEvents = 200, store: customStore } = props;
836
885
  const { setOpen, setMaxEvents, isOpen, togglePause, clearEvents } = useDevtoolsStore();
837
886
  const targetStore = react.useMemo(() => customStore ?? reactActionGuard.uiBlockingStoreApi, [customStore]);
838
887
  react.useEffect(() => {
@@ -847,9 +896,14 @@ function ActionGuardDevtools(props) {
847
896
  setOpen(defaultOpen);
848
897
  setMaxEvents(maxEvents);
849
898
  }, [defaultOpen, maxEvents, setOpen, setMaxEvents]);
850
- const handleKeyDown = react.useCallback(
851
- (event) => {
852
- const action = getDevtoolsKeyboardAction(event, isOpen);
899
+ const stateRef = react.useRef({ isOpen, setOpen, togglePause, clearEvents });
900
+ react.useEffect(() => {
901
+ stateRef.current = { isOpen, setOpen, togglePause, clearEvents };
902
+ }, [isOpen, setOpen, togglePause, clearEvents]);
903
+ react.useEffect(() => {
904
+ const handleKeyDown = (event) => {
905
+ const { isOpen: isOpen2, setOpen: setOpen2, togglePause: togglePause2, clearEvents: clearEvents2 } = stateRef.current;
906
+ const action = getDevtoolsKeyboardAction(event, isOpen2);
853
907
  if (!action) {
854
908
  return;
855
909
  }
@@ -858,28 +912,29 @@ function ActionGuardDevtools(props) {
858
912
  }
859
913
  switch (action.action) {
860
914
  case "close":
861
- setOpen(false);
915
+ setOpen2(false);
862
916
  break;
863
917
  case "togglePause":
864
- togglePause();
918
+ togglePause2();
865
919
  break;
866
920
  case "clearEvents":
867
- clearEvents();
921
+ clearEvents2();
868
922
  break;
869
923
  }
870
- },
871
- [isOpen, setOpen, togglePause, clearEvents]
872
- );
873
- react.useEffect(() => {
924
+ };
874
925
  document.addEventListener("keydown", handleKeyDown);
875
926
  return () => {
876
927
  document.removeEventListener("keydown", handleKeyDown);
877
928
  };
878
- }, [handleKeyDown]);
929
+ }, []);
930
+ return /* @__PURE__ */ jsxRuntime.jsx(ActionGuardDevtoolsContent_default, { position, store: customStore });
931
+ }
932
+ function ActionGuardDevtools(props) {
933
+ const { showInProduction = false, ...others } = props;
879
934
  if (process.env.NODE_ENV === "production" && !showInProduction) {
880
935
  return null;
881
936
  }
882
- return /* @__PURE__ */ jsxRuntime.jsx(ActionGuardDevtoolsContent_default, { position, store: customStore });
937
+ return /* @__PURE__ */ jsxRuntime.jsx(ActionGuardDevtoolsInternal, { ...others });
883
938
  }
884
939
  var ActionGuardDevtools_default = ActionGuardDevtools;
885
940