@okyrychenko-dev/react-action-guard-devtools 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { uiBlockingStoreApi } from '@okyrychenko-dev/react-action-guard';
2
- import { memo, useMemo, useEffect, useCallback } from 'react';
2
+ import { memo, useMemo, useEffect, useRef, Component, useCallback } from 'react';
3
3
  import { createShallowStore } from '@okyrychenko-dev/react-zustand-toolkit';
4
- import clsx2, { clsx } from 'clsx';
4
+ import { clsx } from 'clsx';
5
5
  import { useStore } from 'zustand';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
  import { useShallow } from 'zustand/react/shallow';
@@ -10,122 +10,137 @@ import { useShallow } from 'zustand/react/shallow';
10
10
 
11
11
  // src/store/devtoolsStore.constants.ts
12
12
  var DEFAULT_FILTER = {
13
- actions: ["add", "remove", "update", "cancel", "timeout"],
13
+ actions: ["add", "remove", "update", "timeout", "clear", "clear_scope"],
14
14
  scopes: [],
15
15
  search: ""
16
16
  };
17
+ var createDefaultFilter = () => ({
18
+ actions: [...DEFAULT_FILTER.actions],
19
+ scopes: [...DEFAULT_FILTER.scopes],
20
+ search: DEFAULT_FILTER.search
21
+ });
17
22
  var DEFAULT_MAX_EVENTS = 200;
18
23
  var DEFAULT_TAB = "timeline";
19
24
 
20
25
  // src/store/devtoolsStore.actions.ts
21
- var createDevtoolsActions = (set, get) => ({
22
- // Initial State
23
- events: [],
24
- maxEvents: DEFAULT_MAX_EVENTS,
25
- isOpen: false,
26
- isMinimized: false,
27
- activeTab: DEFAULT_TAB,
28
- filter: DEFAULT_FILTER,
29
- selectedEventId: null,
30
- isPaused: false,
31
- // Actions
32
- /**
33
- * Add a new event to history
34
- *
35
- * @param eventData - Event data without ID (auto-generated)
36
- */
37
- addEvent: (eventData) => {
38
- if (get().isPaused) {
39
- return;
26
+ var createDevtoolsActions = (set, get) => {
27
+ let eventCounter = 0;
28
+ const createEventId = (eventData) => {
29
+ eventCounter += 1;
30
+ return `${String(eventData.timestamp)}-${eventData.blockerId}-${eventCounter.toString(36)}`;
31
+ };
32
+ const trimEvents = (events, maxEvents) => {
33
+ if (events.length <= maxEvents) {
34
+ return events;
40
35
  }
41
- const event = {
42
- ...eventData,
43
- id: `${String(eventData.timestamp)}-${eventData.blockerId}-${Math.random().toString(36).slice(2, 8)}`
44
- };
45
- set((state) => {
46
- const newEvents = [event, ...state.events];
47
- if (newEvents.length > state.maxEvents) {
48
- newEvents.pop();
36
+ return events.slice(0, maxEvents);
37
+ };
38
+ return {
39
+ // Initial State
40
+ events: [],
41
+ maxEvents: DEFAULT_MAX_EVENTS,
42
+ isOpen: false,
43
+ isMinimized: false,
44
+ activeTab: DEFAULT_TAB,
45
+ filter: createDefaultFilter(),
46
+ selectedEventId: null,
47
+ isPaused: false,
48
+ // Actions
49
+ /**
50
+ * Add a new event to history
51
+ *
52
+ * @param eventData - Event data without ID (auto-generated)
53
+ */
54
+ addEvent: (eventData) => {
55
+ if (get().isPaused) {
56
+ return;
49
57
  }
50
- return { events: newEvents };
51
- });
52
- },
53
- /**
54
- * Clear all events from history
55
- */
56
- clearEvents: () => {
57
- set({ events: [], selectedEventId: null });
58
- },
59
- /**
60
- * Toggle panel open/closed state
61
- */
62
- toggleOpen: () => {
63
- set((state) => ({ isOpen: !state.isOpen }));
64
- },
65
- /**
66
- * Set panel open state
67
- *
68
- * @param open - Whether panel should be open
69
- */
70
- setOpen: (open) => {
71
- set({ isOpen: open });
72
- },
73
- /**
74
- * Toggle minimized state
75
- */
76
- toggleMinimized: () => {
77
- set((state) => ({ isMinimized: !state.isMinimized }));
78
- },
79
- /**
80
- * Set active tab
81
- *
82
- * @param tab - Tab to activate
83
- */
84
- setActiveTab: (tab) => {
85
- set({ activeTab: tab, selectedEventId: null });
86
- },
87
- /**
88
- * Update filter settings (partial update)
89
- *
90
- * @param filterUpdate - Partial filter update
91
- */
92
- setFilter: (filterUpdate) => {
93
- set((state) => ({
94
- filter: { ...state.filter, ...filterUpdate }
95
- }));
96
- },
97
- /**
98
- * Reset filters to default
99
- */
100
- resetFilter: () => {
101
- set({ filter: DEFAULT_FILTER });
102
- },
103
- /**
104
- * Select an event for detail view
105
- *
106
- * @param eventId - Event ID to select (null to deselect)
107
- */
108
- selectEvent: (eventId) => {
109
- set({ selectedEventId: eventId });
110
- },
111
- /**
112
- * Toggle pause state (stops/resumes recording)
113
- */
114
- togglePause: () => {
115
- set((state) => ({ isPaused: !state.isPaused }));
116
- },
117
- /**
118
- * Set maximum events limit
119
- *
120
- * @param max - Maximum number of events to keep
121
- */
122
- setMaxEvents: (max) => {
123
- set((state) => {
124
- const events = state.events.length > max ? state.events.slice(0, max) : state.events;
125
- return { maxEvents: max, events };
126
- });
127
- }
128
- });
58
+ const event = {
59
+ ...eventData,
60
+ id: createEventId(eventData)
61
+ };
62
+ set((state) => {
63
+ const newEvents = [event, ...state.events];
64
+ return { events: trimEvents(newEvents, state.maxEvents) };
65
+ });
66
+ },
67
+ /**
68
+ * Clear all events from history
69
+ */
70
+ clearEvents: () => {
71
+ set({ events: [], selectedEventId: null });
72
+ },
73
+ /**
74
+ * Toggle panel open/closed state
75
+ */
76
+ toggleOpen: () => {
77
+ set((state) => ({ isOpen: !state.isOpen }));
78
+ },
79
+ /**
80
+ * Set panel open state
81
+ *
82
+ * @param open - Whether panel should be open
83
+ */
84
+ setOpen: (open) => {
85
+ set({ isOpen: open });
86
+ },
87
+ /**
88
+ * Toggle minimized state
89
+ */
90
+ toggleMinimized: () => {
91
+ set((state) => ({ isMinimized: !state.isMinimized }));
92
+ },
93
+ /**
94
+ * Set active tab
95
+ *
96
+ * @param tab - Tab to activate
97
+ */
98
+ setActiveTab: (tab) => {
99
+ set({ activeTab: tab, selectedEventId: null });
100
+ },
101
+ /**
102
+ * Update filter settings (partial update)
103
+ *
104
+ * @param filterUpdate - Partial filter update
105
+ */
106
+ setFilter: (filterUpdate) => {
107
+ set((state) => ({
108
+ filter: { ...state.filter, ...filterUpdate }
109
+ }));
110
+ },
111
+ /**
112
+ * Reset filters to default
113
+ */
114
+ resetFilter: () => {
115
+ set({ filter: createDefaultFilter() });
116
+ },
117
+ /**
118
+ * Select an event for detail view
119
+ *
120
+ * @param eventId - Event ID to select (null to deselect)
121
+ */
122
+ selectEvent: (eventId) => {
123
+ set({ selectedEventId: eventId });
124
+ },
125
+ /**
126
+ * Toggle pause state (stops/resumes recording)
127
+ */
128
+ togglePause: () => {
129
+ set((state) => ({ isPaused: !state.isPaused }));
130
+ },
131
+ /**
132
+ * Set maximum events limit
133
+ *
134
+ * @param max - Maximum number of events to keep
135
+ */
136
+ setMaxEvents: (max) => {
137
+ set((state) => ({
138
+ maxEvents: max,
139
+ events: trimEvents(state.events, max)
140
+ }));
141
+ }
142
+ };
143
+ };
129
144
 
130
145
  // src/store/devtoolsStore.store.ts
131
146
  var {
@@ -134,40 +149,50 @@ var {
134
149
  } = createShallowStore(createDevtoolsActions);
135
150
 
136
151
  // src/store/devtoolsStore.selectors.ts
152
+ function normalizeScopes(scope) {
153
+ if (!scope) {
154
+ return [];
155
+ }
156
+ if (typeof scope === "string") {
157
+ return [scope];
158
+ }
159
+ return scope;
160
+ }
161
+ function matchesActionFilter(event, actions) {
162
+ return actions.length === 0 || actions.includes(event.action);
163
+ }
164
+ function matchesScopeFilter(event, scopes) {
165
+ if (scopes.length === 0) {
166
+ return true;
167
+ }
168
+ const eventScopes = normalizeScopes(event.config?.scope);
169
+ if (eventScopes.length === 0) {
170
+ return false;
171
+ }
172
+ return eventScopes.some((scope) => scopes.includes(scope));
173
+ }
174
+ function matchesSearchQuery(event, search) {
175
+ if (!search) {
176
+ return true;
177
+ }
178
+ const searchLower = search.toLowerCase();
179
+ const matchesId = event.blockerId.toLowerCase().includes(searchLower);
180
+ const matchesReason = (event.config?.reason ?? "").toLowerCase().includes(searchLower);
181
+ const matchesScope = normalizeScopes(event.config?.scope).some(
182
+ (scope) => scope.toLowerCase().includes(searchLower)
183
+ );
184
+ return matchesId || matchesReason || matchesScope;
185
+ }
137
186
  function selectFilteredEvents(state) {
138
187
  const { events, filter } = state;
139
188
  return events.filter((event) => {
140
- if (filter.actions.length > 0 && !filter.actions.includes(event.action)) {
141
- return false;
142
- }
143
- if (filter.scopes.length > 0) {
144
- if (!event.config?.scope) {
145
- return false;
146
- }
147
- const eventScopes = Array.isArray(event.config.scope) ? event.config.scope : [event.config.scope];
148
- const hasMatchingScope = eventScopes.some((s) => filter.scopes.includes(s));
149
- if (!hasMatchingScope) {
150
- return false;
151
- }
152
- }
153
- if (filter.search) {
154
- const searchLower = filter.search.toLowerCase();
155
- const matchesId = event.blockerId.toLowerCase().includes(searchLower);
156
- const matchesReason = event.config?.reason?.toLowerCase().includes(searchLower);
157
- if (!matchesId && !matchesReason) {
158
- return false;
159
- }
160
- }
161
- return true;
189
+ return matchesActionFilter(event, filter.actions) && matchesScopeFilter(event, filter.scopes) && matchesSearchQuery(event, filter.search);
162
190
  });
163
191
  }
164
192
  function selectUniqueScopes(state) {
165
193
  const scopes = /* @__PURE__ */ new Set();
166
194
  state.events.forEach((event) => {
167
- if (event.config?.scope) {
168
- const eventScopes = Array.isArray(event.config.scope) ? event.config.scope : [event.config.scope];
169
- eventScopes.forEach((s) => scopes.add(s));
170
- }
195
+ normalizeScopes(event.config?.scope).forEach((scope) => scopes.add(scope));
171
196
  });
172
197
  return Array.from(scopes).sort();
173
198
  }
@@ -179,19 +204,29 @@ function selectAllEvents(state) {
179
204
  var DEVTOOLS_MIDDLEWARE_NAME = "action-guard-devtools";
180
205
  function createDevtoolsMiddleware() {
181
206
  const addTimestamps = /* @__PURE__ */ new Map();
207
+ const terminalActions = /* @__PURE__ */ new Set([
208
+ "remove",
209
+ "timeout",
210
+ "clear",
211
+ "clear_scope"
212
+ ]);
213
+ const getDuration = (action, blockerId, timestamp) => {
214
+ if (!terminalActions.has(action)) {
215
+ return void 0;
216
+ }
217
+ const addTime = addTimestamps.get(blockerId);
218
+ if (addTime === void 0) {
219
+ return void 0;
220
+ }
221
+ addTimestamps.delete(blockerId);
222
+ return timestamp - addTime;
223
+ };
182
224
  return (context) => {
183
225
  const { addEvent } = devtoolsStoreApi.getState();
184
226
  if (context.action === "add") {
185
227
  addTimestamps.set(context.blockerId, context.timestamp);
186
228
  }
187
- let duration;
188
- if (context.action === "remove" || context.action === "timeout" || context.action === "cancel") {
189
- const addTime = addTimestamps.get(context.blockerId);
190
- if (addTime !== void 0) {
191
- duration = context.timestamp - addTime;
192
- addTimestamps.delete(context.blockerId);
193
- }
194
- }
229
+ const duration = getDuration(context.action, context.blockerId, context.timestamp);
195
230
  addEvent({
196
231
  action: context.action,
197
232
  blockerId: context.blockerId,
@@ -203,6 +238,33 @@ function createDevtoolsMiddleware() {
203
238
  };
204
239
  }
205
240
 
241
+ // src/components/actionGuardDevtools/ActionGuardDevtools.utils.ts
242
+ function isTypingTarget(target) {
243
+ return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
244
+ }
245
+ function getDevtoolsKeyboardAction(event, isOpen) {
246
+ if (!isOpen) {
247
+ return null;
248
+ }
249
+ if (isTypingTarget(event.target)) {
250
+ return null;
251
+ }
252
+ switch (event.key) {
253
+ case "Escape":
254
+ return { action: "close", preventDefault: false };
255
+ case " ":
256
+ return { action: "togglePause", preventDefault: true };
257
+ case "c":
258
+ case "C":
259
+ if (!event.metaKey && !event.ctrlKey) {
260
+ return { action: "clearEvents", preventDefault: false };
261
+ }
262
+ return null;
263
+ default:
264
+ return null;
265
+ }
266
+ }
267
+
206
268
  // src/styles/position.module.css
207
269
  var position_default = {};
208
270
 
@@ -274,19 +336,43 @@ var COLORS = {
274
336
  var shared_default = {};
275
337
  function Badge(props) {
276
338
  const { children, className, style } = props;
277
- return /* @__PURE__ */ jsx("span", { className: clsx2(shared_default.badge, className), style, children });
339
+ return /* @__PURE__ */ jsx("span", { className: clsx(shared_default.badge, className), style, children });
278
340
  }
279
341
  var Badge_default = Badge;
280
342
  function Content(props) {
281
343
  const { children, className } = props;
282
- return /* @__PURE__ */ jsx("div", { className: clsx2(shared_default.content, className), children });
344
+ return /* @__PURE__ */ jsx("div", { className: clsx(shared_default.content, className), children });
283
345
  }
284
346
  var Content_default = Content;
285
347
  function EmptyState(props) {
286
348
  const { children, className } = props;
287
- return /* @__PURE__ */ jsx("div", { className: clsx2(shared_default.emptyState, className), children });
349
+ return /* @__PURE__ */ jsx("div", { className: clsx(shared_default.emptyState, className), children });
288
350
  }
289
351
  var EmptyState_default = EmptyState;
352
+ var ErrorBoundary = class extends Component {
353
+ constructor(props) {
354
+ super(props);
355
+ this.state = { hasError: false, error: null };
356
+ }
357
+ static getDerivedStateFromError(error) {
358
+ return { hasError: true, error };
359
+ }
360
+ render() {
361
+ const { hasError, error } = this.state;
362
+ const { children, fallback } = this.props;
363
+ if (hasError) {
364
+ if (fallback) {
365
+ return fallback;
366
+ }
367
+ return /* @__PURE__ */ jsxs("div", { className: shared_default.errorBoundary, children: [
368
+ /* @__PURE__ */ jsx("span", { className: shared_default.errorTitle, children: "Devtools Error" }),
369
+ /* @__PURE__ */ jsx("span", { className: shared_default.errorMessage, children: error?.message ?? "Unknown error" })
370
+ ] });
371
+ }
372
+ return children;
373
+ }
374
+ };
375
+ var ErrorBoundary_default = ErrorBoundary;
290
376
  function EventBadge(props) {
291
377
  const { children, className, style, action } = props;
292
378
  const classes = clsx(shared_default.eventBadge, action && shared_default.eventBadgeAction, className);
@@ -295,7 +381,7 @@ function EventBadge(props) {
295
381
  var EventBadge_default = EventBadge;
296
382
  function IconButton(props) {
297
383
  const { children, className, type = "button", ...others } = props;
298
- return /* @__PURE__ */ jsx("button", { type, className: clsx2(shared_default.iconButton, className), ...others, children });
384
+ return /* @__PURE__ */ jsx("button", { type, className: clsx(shared_default.iconButton, className), ...others, children });
299
385
  }
300
386
  var IconButton_default = IconButton;
301
387
 
@@ -595,7 +681,7 @@ function Timeline() {
595
681
  );
596
682
  const selectedEvent = selectedEventId ? events.find((e) => e.id === selectedEventId) : null;
597
683
  useEffect(() => {
598
- if (selectedEventId && !allEvents.some((e) => e.id === selectedEventId)) {
684
+ if (selectedEventId && !allEvents.some((event) => event.id === selectedEventId)) {
599
685
  selectEvent(null);
600
686
  }
601
687
  }, [selectedEventId, allEvents, selectEvent]);
@@ -627,7 +713,7 @@ function Timeline() {
627
713
  var Timeline_default2 = Timeline;
628
714
  function DevtoolsPanelContent(props) {
629
715
  const { activeTab, store } = props;
630
- return activeTab === "timeline" ? /* @__PURE__ */ jsx(Timeline_default2, {}) : /* @__PURE__ */ jsx(ActiveBlockers_default2, { store });
716
+ return /* @__PURE__ */ jsx(ErrorBoundary_default, { children: activeTab === "timeline" ? /* @__PURE__ */ jsx(Timeline_default2, {}) : /* @__PURE__ */ jsx(ActiveBlockers_default2, { store }) });
631
717
  }
632
718
  var DevtoolsPanelContent_default = DevtoolsPanelContent;
633
719
  var MaximizeIcon = createSvgIcon(/* @__PURE__ */ jsx("path", { d: "M3 3h18v2H3z" }));
@@ -792,41 +878,8 @@ function ActionGuardDevtoolsContent(props) {
792
878
  ] });
793
879
  }
794
880
  var ActionGuardDevtoolsContent_default = ActionGuardDevtoolsContent;
795
-
796
- // src/components/actionGuardDevtools/ActionGuardDevtools.utils.ts
797
- function isTypingTarget(target) {
798
- return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement;
799
- }
800
- function getDevtoolsKeyboardAction(event, isOpen) {
801
- if (!isOpen) {
802
- return null;
803
- }
804
- if (isTypingTarget(event.target)) {
805
- return null;
806
- }
807
- switch (event.key) {
808
- case "Escape":
809
- return { action: "close", preventDefault: false };
810
- case " ":
811
- return { action: "togglePause", preventDefault: true };
812
- case "c":
813
- case "C":
814
- if (!event.metaKey && !event.ctrlKey) {
815
- return { action: "clearEvents", preventDefault: false };
816
- }
817
- return null;
818
- default:
819
- return null;
820
- }
821
- }
822
- function ActionGuardDevtools(props) {
823
- const {
824
- position = "right",
825
- defaultOpen = false,
826
- maxEvents = 200,
827
- showInProduction = false,
828
- store: customStore
829
- } = props;
881
+ function ActionGuardDevtoolsInternal(props) {
882
+ const { position = "right", defaultOpen = false, maxEvents = 200, store: customStore } = props;
830
883
  const { setOpen, setMaxEvents, isOpen, togglePause, clearEvents } = useDevtoolsStore();
831
884
  const targetStore = useMemo(() => customStore ?? uiBlockingStoreApi, [customStore]);
832
885
  useEffect(() => {
@@ -841,9 +894,14 @@ function ActionGuardDevtools(props) {
841
894
  setOpen(defaultOpen);
842
895
  setMaxEvents(maxEvents);
843
896
  }, [defaultOpen, maxEvents, setOpen, setMaxEvents]);
844
- const handleKeyDown = useCallback(
845
- (event) => {
846
- const action = getDevtoolsKeyboardAction(event, isOpen);
897
+ const stateRef = useRef({ isOpen, setOpen, togglePause, clearEvents });
898
+ useEffect(() => {
899
+ stateRef.current = { isOpen, setOpen, togglePause, clearEvents };
900
+ }, [isOpen, setOpen, togglePause, clearEvents]);
901
+ useEffect(() => {
902
+ const handleKeyDown = (event) => {
903
+ const { isOpen: isOpen2, setOpen: setOpen2, togglePause: togglePause2, clearEvents: clearEvents2 } = stateRef.current;
904
+ const action = getDevtoolsKeyboardAction(event, isOpen2);
847
905
  if (!action) {
848
906
  return;
849
907
  }
@@ -852,28 +910,29 @@ function ActionGuardDevtools(props) {
852
910
  }
853
911
  switch (action.action) {
854
912
  case "close":
855
- setOpen(false);
913
+ setOpen2(false);
856
914
  break;
857
915
  case "togglePause":
858
- togglePause();
916
+ togglePause2();
859
917
  break;
860
918
  case "clearEvents":
861
- clearEvents();
919
+ clearEvents2();
862
920
  break;
863
921
  }
864
- },
865
- [isOpen, setOpen, togglePause, clearEvents]
866
- );
867
- useEffect(() => {
922
+ };
868
923
  document.addEventListener("keydown", handleKeyDown);
869
924
  return () => {
870
925
  document.removeEventListener("keydown", handleKeyDown);
871
926
  };
872
- }, [handleKeyDown]);
927
+ }, []);
928
+ return /* @__PURE__ */ jsx(ActionGuardDevtoolsContent_default, { position, store: customStore });
929
+ }
930
+ function ActionGuardDevtools(props) {
931
+ const { showInProduction = false, ...others } = props;
873
932
  if (process.env.NODE_ENV === "production" && !showInProduction) {
874
933
  return null;
875
934
  }
876
- return /* @__PURE__ */ jsx(ActionGuardDevtoolsContent_default, { position, store: customStore });
935
+ return /* @__PURE__ */ jsx(ActionGuardDevtoolsInternal, { ...others });
877
936
  }
878
937
  var ActionGuardDevtools_default = ActionGuardDevtools;
879
938