@versini/ui-debug-overlay 2.0.1 → 2.1.0

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.d.ts CHANGED
@@ -1,133 +1,133 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
-
3
- type DebugOverlayProps = {
4
- /**
5
- * Optional application state to display. If omitted, only manual snapshots
6
- * appear.
7
- */
8
- appState?: unknown;
9
- /**
10
- * Short label shown in the header.
11
- * @default "AppState"
12
- */
13
- title?: string;
14
- /**
15
- * Start collapsed (header only).
16
- * @default false
17
- */
18
- initialCollapsed?: boolean;
19
- /**
20
- * Identifier for manual snapshot targeting..
21
- * @default "default"
22
- */
23
- overlayId?: string;
24
- /**
25
- * Panel positioning.
26
- *
27
- * You can either provide a partial style object with any of the four edges
28
- * (top/right/bottom/left) OR use a shorthand keyword. Shorthand avoids having
29
- * to pass an object and also prevents the default (top/right) edges from being
30
- * applied when the intent is a left anchor.
31
- *
32
- * Supported shorthands:
33
- * - "left" (alias of "top-left")
34
- * - "right" (alias of "top-right")
35
- * - "top", "bottom"
36
- * - "top-left", "top-right", "bottom-left", "bottom-right"
37
- *
38
- */
39
- position?:
40
- | Partial<Pick<CSSStyleDeclaration, "top" | "right" | "bottom" | "left">>
41
- | "left"
42
- | "right"
43
- | "top"
44
- | "bottom"
45
- | "top-left"
46
- | "top-right"
47
- | "bottom-left"
48
- | "bottom-right";
49
- /**
50
- * Maximum body height before scrolling (px number or CSS length e.g. "100vh").
51
- * @default "50svh"
52
- */
53
- maxBodyHeight?: number | string;
54
- /**
55
- * Pretty print spacing for JSON output.
56
- * @default 2
57
- */
58
- indent?: number;
59
- /**
60
- * Maximum snapshots retained in memory (FIFO)..
61
- * @default 50
62
- */
63
- maxSnapshots?: number;
64
- /**
65
- * Maximum snapshots shown without scrolling.
66
- * @default 10
67
- */
68
- maxVisibleSnapshots?: number;
69
- /**
70
- * Visual ordering of snapshots in the panel.
71
- * - "desc": newest snapshot at the top.
72
- * - "asc": oldest snapshot at the top.
73
- * Numbering ALWAYS reflects chronological order with #1 being the oldest
74
- * snapshot regardless of display order.
75
- * @default "desc"
76
- */
77
- snapshotOrder?: "asc" | "desc";
78
- /**
79
- * When true, appends the current snapshot count to the title (e.g. "AppState
80
- * (5)").
81
- * @default false
82
- */
83
- appendSnapshotCountInTitle?: boolean;
84
- };
85
-
86
- /**
87
- * Manual snapshot input now requires an explicit label for clarity; we remove
88
- * the previous flexible single-argument signature to keep debug calls
89
- * unambiguous (always: label, data).
90
- */
91
- declare const LOG_YELLOW: "LOG_YELLOW";
92
- declare const LOG_GREEN: "LOG_GREEN";
93
- declare const LOG_BLUE: "LOG_BLUE";
94
- declare const LOG_MAGENTA: "LOG_MAGENTA";
95
- declare const LOG_RED: "LOG_RED";
96
- type LogColor = typeof LOG_YELLOW | typeof LOG_GREEN | typeof LOG_BLUE | typeof LOG_MAGENTA | typeof LOG_RED;
97
- /**
98
- * Test-only helper (not part of public API). Enables isolation across tests.
99
- * Exported unconditionally; consumer code should not rely on it.
100
- */
101
- declare function __resetDebugOverlayTestState(): void;
102
- interface DebugFnOptions {
103
- color?: LogColor;
104
- targetOverlays?: string[];
105
- }
106
- /**
107
- * React hook returning a function to push a manual snapshot into all (or
108
- * targeted) mounted `DebugOverlay` instances.
109
- *
110
- * Usage:
111
- * ```js
112
- * const debug = useDebugOverlay();
113
- * debug("loaded", { user, meta }, LOG_GREEN);
114
- * debug("error", errorObj, { color: LOG_RED, targetOverlays: ["errors"] });
115
- * ```
116
- *
117
- * Parameters when invoking the returned function:
118
- * - label: short string identifying the snapshot category.
119
- * - data: any serializable (or even cyclic) value; cyclic structures are
120
- * replaced with "[Circular]" markers.
121
- * - options: either a LogColor constant OR an object with `color` and
122
- * optional `targetOverlays` (array of overlayId strings). When
123
- * `targetOverlays` is omitted or empty, the snapshot broadcasts to
124
- * every mounted overlay.
125
- *
126
- * The hook itself has no dependencies and returns a stable callback identity
127
- * for the component lifetime, keeping re-renders minimal.
128
- *
129
- */
130
- declare function useDebugOverlay(): (label: string, data: unknown, options?: LogColor | DebugFnOptions) => void;
131
- declare const DebugOverlay: ({ appState, title, initialCollapsed, overlayId, position, maxBodyHeight, indent, maxSnapshots, maxVisibleSnapshots, snapshotOrder, appendSnapshotCountInTitle, }: DebugOverlayProps) => react_jsx_runtime.JSX.Element;
132
-
133
- export { type DebugFnOptions, DebugOverlay, type DebugOverlayProps, LOG_BLUE, LOG_GREEN, LOG_MAGENTA, LOG_RED, LOG_YELLOW, type LogColor, __resetDebugOverlayTestState, useDebugOverlay };
1
+ import { JSX } from 'react/jsx-runtime';
2
+
3
+ /**
4
+ * Test-only helper (not part of public API). Enables isolation across tests.
5
+ * Exported unconditionally; consumer code should not rely on it.
6
+ */
7
+ export declare function __resetDebugOverlayTestState(): void;
8
+
9
+ export declare interface DebugFnOptions {
10
+ color?: LogColor;
11
+ targetOverlays?: string[];
12
+ }
13
+
14
+ export declare const DebugOverlay: ({ appState, title, initialCollapsed, overlayId, position, maxBodyHeight, indent, maxSnapshots, maxVisibleSnapshots, snapshotOrder, appendSnapshotCountInTitle, }: DebugOverlayProps) => JSX.Element;
15
+
16
+ export declare type DebugOverlayProps = {
17
+ /**
18
+ * Optional application state to display. If omitted, only manual snapshots
19
+ * appear.
20
+ */
21
+ appState?: unknown;
22
+ /**
23
+ * Short label shown in the header.
24
+ * @default "AppState"
25
+ */
26
+ title?: string;
27
+ /**
28
+ * Start collapsed (header only).
29
+ * @default false
30
+ */
31
+ initialCollapsed?: boolean;
32
+ /**
33
+ * Identifier for manual snapshot targeting..
34
+ * @default "default"
35
+ */
36
+ overlayId?: string;
37
+ /**
38
+ * Panel positioning.
39
+ *
40
+ * You can either provide a partial style object with any of the four edges
41
+ * (top/right/bottom/left) OR use a shorthand keyword. Shorthand avoids having
42
+ * to pass an object and also prevents the default (top/right) edges from being
43
+ * applied when the intent is a left anchor.
44
+ *
45
+ * Supported shorthands:
46
+ * - "left" (alias of "top-left")
47
+ * - "right" (alias of "top-right")
48
+ * - "top", "bottom"
49
+ * - "top-left", "top-right", "bottom-left", "bottom-right"
50
+ *
51
+ */
52
+ position?: Partial<Pick<CSSStyleDeclaration, "top" | "right" | "bottom" | "left">> | "left" | "right" | "top" | "bottom" | "top-left" | "top-right" | "bottom-left" | "bottom-right";
53
+ /**
54
+ * Maximum body height before scrolling (px number or CSS length e.g. "100vh").
55
+ * @default "50svh"
56
+ */
57
+ maxBodyHeight?: number | string;
58
+ /**
59
+ * Pretty print spacing for JSON output.
60
+ * @default 2
61
+ */
62
+ indent?: number;
63
+ /**
64
+ * Maximum snapshots retained in memory (FIFO)..
65
+ * @default 50
66
+ */
67
+ maxSnapshots?: number;
68
+ /**
69
+ * Maximum snapshots shown without scrolling.
70
+ * @default 10
71
+ */
72
+ maxVisibleSnapshots?: number;
73
+ /**
74
+ * Visual ordering of snapshots in the panel.
75
+ * - "desc": newest snapshot at the top.
76
+ * - "asc": oldest snapshot at the top.
77
+ * Numbering ALWAYS reflects chronological order with #1 being the oldest
78
+ * snapshot regardless of display order.
79
+ * @default "desc"
80
+ */
81
+ snapshotOrder?: "asc" | "desc";
82
+ /**
83
+ * When true, appends the current snapshot count to the title (e.g. "AppState
84
+ * (5)").
85
+ * @default false
86
+ */
87
+ appendSnapshotCountInTitle?: boolean;
88
+ };
89
+
90
+ export declare const LOG_BLUE: "LOG_BLUE";
91
+
92
+ export declare const LOG_GREEN: "LOG_GREEN";
93
+
94
+ export declare const LOG_MAGENTA: "LOG_MAGENTA";
95
+
96
+ export declare const LOG_RED: "LOG_RED";
97
+
98
+ /**
99
+ * Manual snapshot input now requires an explicit label for clarity; we remove
100
+ * the previous flexible single-argument signature to keep debug calls
101
+ * unambiguous (always: label, data).
102
+ */
103
+ export declare const LOG_YELLOW: "LOG_YELLOW";
104
+
105
+ export declare type LogColor = typeof LOG_YELLOW | typeof LOG_GREEN | typeof LOG_BLUE | typeof LOG_MAGENTA | typeof LOG_RED;
106
+
107
+ /**
108
+ * React hook returning a function to push a manual snapshot into all (or
109
+ * targeted) mounted `DebugOverlay` instances.
110
+ *
111
+ * Usage:
112
+ * ```js
113
+ * const debug = useDebugOverlay();
114
+ * debug("loaded", { user, meta }, LOG_GREEN);
115
+ * debug("error", errorObj, { color: LOG_RED, targetOverlays: ["errors"] });
116
+ * ```
117
+ *
118
+ * Parameters when invoking the returned function:
119
+ * - label: short string identifying the snapshot category.
120
+ * - data: any serializable (or even cyclic) value; cyclic structures are
121
+ * replaced with "[Circular]" markers.
122
+ * - options: either a LogColor constant OR an object with `color` and
123
+ * optional `targetOverlays` (array of overlayId strings). When
124
+ * `targetOverlays` is omitted or empty, the snapshot broadcasts to
125
+ * every mounted overlay.
126
+ *
127
+ * The hook itself has no dependencies and returns a stable callback identity
128
+ * for the component lifetime, keeping re-renders minimal.
129
+ *
130
+ */
131
+ export declare function useDebugOverlay(): (label: string, data: unknown, options?: LogColor | DebugFnOptions) => void;
132
+
133
+ export { }
package/dist/index.js CHANGED
@@ -1,24 +1,550 @@
1
- import { DebugOverlay as r, LOG_BLUE as i, LOG_GREEN as t, LOG_MAGENTA as O, LOG_RED as o, LOG_YELLOW as L, __resetDebugOverlayTestState as G, useDebugOverlay as s } from "./components/DebugOverlay/DebugOverlay.js";
2
1
  /*!
3
- @versini/ui-debug-overlay v2.0.1
2
+ @versini/ui-debug-overlay v2.1.0
4
3
  © 2025 gizmette.com
5
4
  */
6
5
  try {
7
- window.__VERSINI_UI_DEBUG_OVERLAY__ || (window.__VERSINI_UI_DEBUG_OVERLAY__ = {
8
- version: "2.0.1",
9
- buildTime: "10/17/2025 12:55 PM EDT",
10
- homepage: "https://github.com/aversini/ui-components",
11
- license: "MIT"
12
- });
13
- } catch {
6
+ if (!window.__VERSINI_UI_DEBUG_OVERLAY__) {
7
+ window.__VERSINI_UI_DEBUG_OVERLAY__ = {
8
+ version: "2.1.0",
9
+ buildTime: "11/04/2025 03:45 PM EST",
10
+ homepage: "https://github.com/aversini/ui-components",
11
+ license: "MIT",
12
+ };
13
+ }
14
+ } catch (error) {
15
+ // nothing to declare officer
14
16
  }
15
- export {
16
- r as DebugOverlay,
17
- i as LOG_BLUE,
18
- t as LOG_GREEN,
19
- O as LOG_MAGENTA,
20
- o as LOG_RED,
21
- L as LOG_YELLOW,
22
- G as __resetDebugOverlayTestState,
23
- s as useDebugOverlay
17
+
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
20
+
21
+ ;// CONCATENATED MODULE: external "react/jsx-runtime"
22
+
23
+ ;// CONCATENATED MODULE: external "react"
24
+
25
+ ;// CONCATENATED MODULE: ./src/components/DebugOverlay/DebugOverlay.tsx
26
+
27
+
28
+ /**
29
+ * Manual snapshot input now requires an explicit label for clarity; we remove
30
+ * the previous flexible single-argument signature to keep debug calls
31
+ * unambiguous (always: label, data).
32
+ */ const LOG_YELLOW = "LOG_YELLOW";
33
+ const LOG_GREEN = "LOG_GREEN";
34
+ const LOG_BLUE = "LOG_BLUE";
35
+ const LOG_MAGENTA = "LOG_MAGENTA";
36
+ const LOG_RED = "LOG_RED";
37
+ const MANUAL_BUFFER_LIMIT = 200;
38
+ let manualSeqCounter = 0; // shared seq for manual + state via helper
39
+ const manualBuffer = [];
40
+ const manualSubscribers = new Set();
41
+ /**
42
+ * Test-only helper (not part of public API). Enables isolation across tests.
43
+ * Exported unconditionally; consumer code should not rely on it.
44
+ */ function __resetDebugOverlayTestState() {
45
+ manualBuffer.length = 0;
46
+ manualSeqCounter = 0;
47
+ }
48
+ function nextSeq() {
49
+ return manualSeqCounter++;
50
+ }
51
+ function pushDebugSnapshot(input) {
52
+ const snap = {
53
+ kind: "manual",
54
+ ts: Date.now(),
55
+ seq: nextSeq(),
56
+ label: input.label,
57
+ data: input.data,
58
+ color: input.color,
59
+ targetOverlays: input.targetOverlays && input.targetOverlays.length ? [
60
+ ...input.targetOverlays
61
+ ] : undefined
62
+ };
63
+ manualBuffer.push(snap);
64
+ if (manualBuffer.length > MANUAL_BUFFER_LIMIT) {
65
+ manualBuffer.shift();
66
+ }
67
+ manualSubscribers.forEach((cb)=>cb(snap));
68
+ }
69
+ /**
70
+ * React hook returning a function to push a manual snapshot into all (or
71
+ * targeted) mounted `DebugOverlay` instances.
72
+ *
73
+ * Usage:
74
+ * ```js
75
+ * const debug = useDebugOverlay();
76
+ * debug("loaded", { user, meta }, LOG_GREEN);
77
+ * debug("error", errorObj, { color: LOG_RED, targetOverlays: ["errors"] });
78
+ * ```
79
+ *
80
+ * Parameters when invoking the returned function:
81
+ * - label: short string identifying the snapshot category.
82
+ * - data: any serializable (or even cyclic) value; cyclic structures are
83
+ * replaced with "[Circular]" markers.
84
+ * - options: either a LogColor constant OR an object with `color` and
85
+ * optional `targetOverlays` (array of overlayId strings). When
86
+ * `targetOverlays` is omitted or empty, the snapshot broadcasts to
87
+ * every mounted overlay.
88
+ *
89
+ * The hook itself has no dependencies and returns a stable callback identity
90
+ * for the component lifetime, keeping re-renders minimal.
91
+ *
92
+ */ function useDebugOverlay() {
93
+ return useCallback((label, data, options)=>{
94
+ if (typeof options === "string") {
95
+ pushDebugSnapshot({
96
+ label,
97
+ data,
98
+ color: options
99
+ });
100
+ } else {
101
+ pushDebugSnapshot({
102
+ label,
103
+ data,
104
+ color: options?.color,
105
+ targetOverlays: options?.targetOverlays
106
+ });
107
+ }
108
+ }, []);
109
+ }
110
+ /**
111
+ * Cycle‑safe, deterministic JSON stringify (orders object keys).
112
+ */ function safeStringify(value, space = 2) {
113
+ const seen = new WeakSet();
114
+ const sortKeys = (val)=>{
115
+ if (!val || typeof val !== "object" || val instanceof Date || val instanceof RegExp) {
116
+ return val;
117
+ }
118
+ if (seen.has(val)) {
119
+ return "[Circular]";
120
+ }
121
+ seen.add(val);
122
+ if (Array.isArray(val)) {
123
+ return val.map(sortKeys);
124
+ }
125
+ const out = {};
126
+ for (const k of Object.keys(val).sort()){
127
+ out[k] = sortKeys(val[k]);
128
+ }
129
+ return out;
130
+ };
131
+ try {
132
+ return JSON.stringify(sortKeys(value), null, space);
133
+ } catch (err) {
134
+ return `<<unserializable: ${err.message}>>`;
135
+ }
136
+ }
137
+ const DebugOverlay = ({ appState, title = "AppState", initialCollapsed = false, overlayId = "default", position, maxBodyHeight = "50svh", indent = 2, maxSnapshots = 50, maxVisibleSnapshots = 10, snapshotOrder = "desc", appendSnapshotCountInTitle = false })=>{
138
+ const [collapsed, setCollapsed] = useState(initialCollapsed);
139
+ const [copied, setCopied] = useState(false);
140
+ const containerRef = useRef(null);
141
+ const [snapshots, setSnapshots] = useState([]);
142
+ const [manualSnaps, setManualSnaps] = useState(()=>// Apply same targeting filter to initial buffered snapshots as we do for
143
+ // live subscription events so targeted snapshots don't leak to unrelated
144
+ // overlays.
145
+ manualBuffer.filter((m)=>!m.targetOverlays || m.targetOverlays.length === 0 || m.targetOverlays.includes(overlayId)));
146
+ // Serialize state only if provided; overlay can operate in manual-only mode.
147
+ const json = useMemo(()=>appState !== undefined ? safeStringify(appState, indent) : null, [
148
+ appState,
149
+ indent
150
+ ]);
151
+ // Capture snapshot when appState changes (referential).
152
+ useEffect(()=>{
153
+ if (json == null) {
154
+ return; // no appState provided: skip automatic state snapshotting
155
+ }
156
+ setSnapshots((prev)=>[
157
+ {
158
+ ts: Date.now(),
159
+ json
160
+ },
161
+ ...prev
162
+ ].slice(0, maxSnapshots));
163
+ }, [
164
+ json,
165
+ maxSnapshots
166
+ ]);
167
+ // Subscribe to manual snapshots (external pushes).
168
+ useEffect(()=>{
169
+ const handler = (s)=>{
170
+ /**
171
+ * Filter by overlay target: include if broadcast (no targetOverlays) or if
172
+ * overlayId present in targetOverlays.
173
+ */ if (!s.targetOverlays || s.targetOverlays.length === 0 || s.targetOverlays.includes(overlayId)) {
174
+ setManualSnaps((prev)=>[
175
+ ...prev,
176
+ s
177
+ ]);
178
+ }
179
+ };
180
+ manualSubscribers.add(handler);
181
+ return ()=>{
182
+ manualSubscribers.delete(handler);
183
+ };
184
+ }, [
185
+ overlayId
186
+ ]);
187
+ // Build unified snapshot list.
188
+ const unifiedAsc = useMemo(()=>{
189
+ /**
190
+ * State snapshots: `snapshots` is newest-first; convert to ascending & add
191
+ * seq.
192
+ */ const stateAsc = [
193
+ ...snapshots
194
+ ].reverse().map((s)=>({
195
+ kind: "state",
196
+ ts: s.ts,
197
+ json: s.json,
198
+ seq: nextSeq()
199
+ }));
200
+ /**
201
+ * Manual snapshots already oldest-first in manualBuffer/ manualSnaps; but we
202
+ * rely on local state manualSnaps (which reflects buffer at mount + pushes).
203
+ */ const manualAsc = manualSnaps.map((m)=>({
204
+ kind: "manual",
205
+ ts: m.ts,
206
+ seq: m.seq,
207
+ label: m.label,
208
+ color: m.color,
209
+ json: safeStringify(m.data, indent)
210
+ }));
211
+ /**
212
+ * Merge two ascending arrays by time then seq. Since sizes are small, a
213
+ * simple concat + sort is fine (keeps code simpler than manual merge);
214
+ * optimize if needed.
215
+ */ const merged = [
216
+ ...stateAsc,
217
+ ...manualAsc
218
+ ];
219
+ merged.sort((a, b)=>a.ts === b.ts ? a.seq - b.seq : a.ts - b.ts);
220
+ return merged;
221
+ }, [
222
+ snapshots,
223
+ manualSnaps,
224
+ indent
225
+ ]);
226
+ const displayList = useMemo(()=>snapshotOrder === "asc" ? unifiedAsc : [
227
+ ...unifiedAsc
228
+ ].reverse(), [
229
+ snapshotOrder,
230
+ unifiedAsc
231
+ ]);
232
+ const copy = useCallback(()=>{
233
+ const lines = [];
234
+ lines.push(`${title} (total snapshots: ${unifiedAsc.length})`);
235
+ unifiedAsc.forEach((s, idx)=>{
236
+ const d = new Date(s.ts);
237
+ const ts = d.toLocaleTimeString(undefined, {
238
+ hour: "2-digit",
239
+ minute: "2-digit",
240
+ second: "2-digit",
241
+ hour12: false
242
+ });
243
+ const labelPart = s.kind === "manual" && s.label ? ` [${s.label}]` : "";
244
+ lines.push(`--- ${ts} (#${idx + 1})${labelPart} ---`);
245
+ lines.push(s.json);
246
+ });
247
+ const text = lines.join("\n");
248
+ if (navigator?.clipboard?.writeText) {
249
+ navigator.clipboard.writeText(text).then(()=>{
250
+ setCopied(true);
251
+ setTimeout(()=>setCopied(false), 1300);
252
+ });
253
+ }
254
+ }, [
255
+ title,
256
+ unifiedAsc
257
+ ]);
258
+ const clear = useCallback(()=>{
259
+ // Clear all snapshots (state + manual) and purge global manual buffer.
260
+ setSnapshots([]);
261
+ setManualSnaps([]);
262
+ manualBuffer.length = 0;
263
+ }, []);
264
+ // Normalize position (object or shorthand) into edge declarations.
265
+ const normalizedEdges = useMemo(()=>{
266
+ if (!position) {
267
+ return {};
268
+ }
269
+ if (typeof position === "string") {
270
+ switch(position){
271
+ case "left":
272
+ case "top-left":
273
+ return {
274
+ top: "0",
275
+ left: "0"
276
+ };
277
+ case "right":
278
+ case "top-right":
279
+ return {
280
+ top: "0",
281
+ right: "0"
282
+ };
283
+ case "bottom-left":
284
+ return {
285
+ bottom: "0",
286
+ left: "0"
287
+ };
288
+ case "bottom-right":
289
+ return {
290
+ bottom: "0",
291
+ right: "0"
292
+ };
293
+ case "bottom":
294
+ return {
295
+ bottom: "0",
296
+ right: "0"
297
+ };
298
+ case "top":
299
+ default:
300
+ return {
301
+ top: "0",
302
+ right: "0"
303
+ };
304
+ }
305
+ }
306
+ // Shallow copy to avoid mutating caller object.
307
+ const obj = {};
308
+ for (const k of [
309
+ "top",
310
+ "right",
311
+ "bottom",
312
+ "left"
313
+ ]){
314
+ const v = position[k];
315
+ if (v !== undefined) {
316
+ obj[k] = String(v);
317
+ }
318
+ }
319
+ return obj;
320
+ }, [
321
+ position
322
+ ]);
323
+ /**
324
+ * Final edges: if neither vertical edge provided, default to top:0. If neither
325
+ * horizontal edge provided, default to right:0. This preserves previous
326
+ * behavior while respecting left-anchored shorthands (they supply left so we
327
+ * don't inject right).
328
+ */ const finalEdges = useMemo(()=>{
329
+ const out = {
330
+ ...normalizedEdges
331
+ };
332
+ if (!("top" in out) && !("bottom" in out)) {
333
+ out.top = "0";
334
+ }
335
+ if (!("left" in out) && !("right" in out)) {
336
+ out.right = "0";
337
+ }
338
+ return out;
339
+ }, [
340
+ normalizedEdges
341
+ ]);
342
+ // Keep overlay inside visual viewport when software keyboard shifts layout.
343
+ useEffect(()=>{
344
+ const vv = typeof window !== "undefined" ? window.visualViewport : null;
345
+ if (!vv) {
346
+ return;
347
+ }
348
+ const update = ()=>{
349
+ if (!containerRef.current) {
350
+ return;
351
+ }
352
+ // Adjust dynamic top only if caller didn't set top/bottom explicitly.
353
+ if (!("top" in finalEdges) && !("bottom" in finalEdges)) {
354
+ containerRef.current.style.top = `${vv.offsetTop}px`;
355
+ }
356
+ // Adjust dynamic right only if neither left nor right explicitly set.
357
+ if (!("right" in finalEdges) && !("left" in finalEdges)) {
358
+ const rightGap = Math.max(0, Math.round(window.innerWidth - (vv.offsetLeft + vv.width)));
359
+ containerRef.current.style.right = `${rightGap}px`;
360
+ }
361
+ };
362
+ vv.addEventListener("resize", update);
363
+ vv.addEventListener("scroll", update);
364
+ update();
365
+ return ()=>{
366
+ vv.removeEventListener("resize", update);
367
+ vv.removeEventListener("scroll", update);
368
+ };
369
+ }, [
370
+ finalEdges
371
+ ]);
372
+ const snapshotCount = unifiedAsc.length;
373
+ const headerTitle = appendSnapshotCountInTitle ? `${title} (${snapshotCount})` : title;
374
+ // Base style for all snapshots; manual ones get color variants.
375
+ const statePreStyle = {
376
+ margin: 0,
377
+ padding: 0
378
+ };
379
+ const manualBase = {
380
+ margin: 0,
381
+ padding: 0,
382
+ paddingLeft: 4
383
+ };
384
+ const manualColorStyles = {
385
+ LOG_YELLOW: {
386
+ color: "#ffcf7f",
387
+ background: "rgba(255,200,0,0.08)",
388
+ border: "2px solid #cc9a00"
389
+ },
390
+ LOG_GREEN: {
391
+ color: "#b6ffb6",
392
+ background: "rgba(0,255,0,0.08)",
393
+ border: "2px solid #00aa00"
394
+ },
395
+ LOG_BLUE: {
396
+ color: "#b6d9ff",
397
+ background: "rgba(0,136,255,0.08)",
398
+ border: "2px solid #0066cc"
399
+ },
400
+ LOG_MAGENTA: {
401
+ color: "#ffb6ef",
402
+ background: "rgba(255,0,200,0.08)",
403
+ border: "2px solid #cc0088"
404
+ },
405
+ LOG_RED: {
406
+ color: "#ffb6b6",
407
+ background: "rgba(255,0,0,0.08)",
408
+ border: "2px solid #cc0000"
409
+ }
410
+ };
411
+ function styleForManual(color) {
412
+ const variant = color && manualColorStyles[color] ? manualColorStyles[color] : manualColorStyles.LOG_YELLOW;
413
+ return {
414
+ ...manualBase,
415
+ color: variant.color,
416
+ background: variant.background,
417
+ borderLeft: variant.border
418
+ };
419
+ }
420
+ return /*#__PURE__*/ jsxs("div", {
421
+ ref: containerRef,
422
+ "aria-label": "Debug overlay",
423
+ style: {
424
+ position: "fixed",
425
+ fontFamily: "monospace",
426
+ zIndex: 9999,
427
+ fontSize: 12,
428
+ color: "#d1faff",
429
+ background: "rgba(0,0,0,0.78)",
430
+ border: "1px solid #055",
431
+ borderTop: "none",
432
+ borderRight: "none",
433
+ borderBottomLeftRadius: 6,
434
+ maxWidth: 360,
435
+ boxShadow: "0 2px 6px rgba(0,0,0,0.4)",
436
+ ...finalEdges
437
+ },
438
+ children: [
439
+ /*#__PURE__*/ jsxs("div", {
440
+ style: {
441
+ display: "flex",
442
+ alignItems: "center",
443
+ gap: 6,
444
+ padding: "4px 6px 4px 8px",
445
+ borderBottom: collapsed ? "none" : "1px solid #044",
446
+ background: "#022",
447
+ borderBottomLeftRadius: collapsed ? 6 : 0,
448
+ userSelect: "none"
449
+ },
450
+ children: [
451
+ /*#__PURE__*/ jsx("strong", {
452
+ style: {
453
+ fontWeight: 600,
454
+ fontSize: 11
455
+ },
456
+ children: headerTitle
457
+ }),
458
+ /*#__PURE__*/ jsxs("div", {
459
+ style: {
460
+ marginLeft: "auto",
461
+ display: "flex",
462
+ gap: 4
463
+ },
464
+ children: [
465
+ /*#__PURE__*/ jsx("button", {
466
+ type: "button",
467
+ onClick: ()=>setCollapsed((c)=>!c),
468
+ "aria-label": collapsed ? "Expand debug overlay" : "Collapse debug overlay",
469
+ style: buttonStyle,
470
+ children: collapsed ? "+" : "−"
471
+ }),
472
+ /*#__PURE__*/ jsx("button", {
473
+ type: "button",
474
+ onClick: copy,
475
+ "aria-label": "Copy debug JSON",
476
+ style: {
477
+ ...buttonStyle,
478
+ color: copied ? "#0f0" : buttonStyle.color
479
+ },
480
+ children: copied ? "Copied" : "Copy"
481
+ }),
482
+ unifiedAsc.length > 1 && /*#__PURE__*/ jsx("button", {
483
+ type: "button",
484
+ onClick: clear,
485
+ "aria-label": "Clear stored snapshots",
486
+ style: buttonStyle,
487
+ children: "Clear"
488
+ })
489
+ ]
490
+ })
491
+ ]
492
+ }),
493
+ !collapsed && /*#__PURE__*/ jsxs("div", {
494
+ style: {
495
+ margin: 0,
496
+ padding: "6px 8px 8px 8px",
497
+ maxHeight: maxBodyHeight,
498
+ overflow: "auto",
499
+ lineHeight: 1.25,
500
+ fontSize: 11,
501
+ fontFamily: "monospace",
502
+ whiteSpace: "pre-wrap",
503
+ wordBreak: "break-word"
504
+ },
505
+ children: [
506
+ displayList.slice(0, maxVisibleSnapshots).map((s)=>{
507
+ /**
508
+ * Need global chronological index irrespective of current order. Find in
509
+ * unifiedAsc (ascending) by seq (unique) for numbering.
510
+ */ const ascIndex = unifiedAsc.findIndex((u)=>u.seq === s.seq);
511
+ const d = new Date(s.ts);
512
+ const ts = d.toLocaleTimeString(undefined, {
513
+ hour: "2-digit",
514
+ minute: "2-digit",
515
+ second: "2-digit",
516
+ hour12: false
517
+ });
518
+ const labelPart = s.kind === "manual" && s.label ? ` [${s.label}]` : "";
519
+ return /*#__PURE__*/ jsx("pre", {
520
+ style: s.kind === "manual" ? styleForManual(s.color) : statePreStyle,
521
+ children: `--- ${ts} (#${ascIndex + 1})${labelPart} ---\n${s.json}`
522
+ }, `${s.kind}-${s.seq}`);
523
+ }),
524
+ displayList.length > maxVisibleSnapshots && /*#__PURE__*/ jsx("div", {
525
+ style: {
526
+ opacity: 0.7,
527
+ marginTop: 4
528
+ },
529
+ children: `(+${displayList.length - maxVisibleSnapshots} snapshots not shown)`
530
+ })
531
+ ]
532
+ })
533
+ ]
534
+ });
535
+ };
536
+ const buttonStyle = {
537
+ background: "#033",
538
+ color: "#0ff",
539
+ border: "1px solid #055",
540
+ borderRadius: 4,
541
+ fontSize: 10,
542
+ padding: "2px 6px",
543
+ cursor: "pointer",
544
+ fontFamily: "inherit"
24
545
  };
546
+
547
+ ;// CONCATENATED MODULE: ./src/components/index.ts
548
+
549
+
550
+ export { DebugOverlay, LOG_BLUE, LOG_GREEN, LOG_MAGENTA, LOG_RED, LOG_YELLOW, __resetDebugOverlayTestState, useDebugOverlay };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versini/ui-debug-overlay",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "publishConfig": {
@@ -20,13 +20,13 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "build:check": "tsc",
23
- "build:js": "vite build",
24
- "build:types": "tsup",
25
- "build": "npm-run-all --serial clean build:check build:js build:types",
23
+ "build:js": "rslib build",
24
+ "build:types": "echo 'Types now built with rslib'",
25
+ "build": "npm-run-all --serial clean build:check build:js",
26
26
  "clean": "rimraf dist tmp",
27
- "dev:js": "vite build --watch --mode development",
28
- "dev:types": "tsup --watch src",
29
- "dev": "npm-run-all clean --parallel dev:js dev:types",
27
+ "dev:js": "rslib build --watch",
28
+ "dev:types": "echo 'Types now watched with rslib'",
29
+ "dev": "rslib build --watch",
30
30
  "lint": "biome lint src",
31
31
  "lint:fix": "biome check src --write --no-errors-on-unmatched",
32
32
  "prettier": "biome check --write --no-errors-on-unmatched",
@@ -39,5 +39,5 @@
39
39
  "devDependencies": {
40
40
  "@testing-library/jest-dom": "6.9.1"
41
41
  },
42
- "gitHead": "67e049c9b2e46e8f52509bf6198ca0cdaf5d970a"
42
+ "gitHead": "7484ad443b77ef31e52ae3a7d88b8129bc6cdf1d"
43
43
  }
@@ -1,350 +0,0 @@
1
- import { jsxs as L, jsx as g } from "react/jsx-runtime";
2
- import { useCallback as _, useState as x, useRef as V, useMemo as h, useEffect as S } from "react";
3
- const re = "LOG_YELLOW", ne = "LOG_GREEN", se = "LOG_BLUE", ae = "LOG_MAGENTA", le = "LOG_RED", ee = 200;
4
- let W = 0;
5
- const p = [], $ = /* @__PURE__ */ new Set();
6
- function ie() {
7
- p.length = 0, W = 0;
8
- }
9
- function F() {
10
- return W++;
11
- }
12
- function z(s) {
13
- const a = {
14
- kind: "manual",
15
- ts: Date.now(),
16
- seq: F(),
17
- label: s.label,
18
- data: s.data,
19
- color: s.color,
20
- targetOverlays: s.targetOverlays && s.targetOverlays.length ? [...s.targetOverlays] : void 0
21
- };
22
- p.push(a), p.length > ee && p.shift(), $.forEach((l) => l(a));
23
- }
24
- function ce() {
25
- return _(
26
- (s, a, l) => {
27
- z(typeof l == "string" ? { label: s, data: a, color: l } : {
28
- label: s,
29
- data: a,
30
- color: l?.color,
31
- targetOverlays: l?.targetOverlays
32
- });
33
- },
34
- []
35
- );
36
- }
37
- function N(s, a = 2) {
38
- const l = /* @__PURE__ */ new WeakSet(), c = (o) => {
39
- if (!o || typeof o != "object" || o instanceof Date || o instanceof RegExp)
40
- return o;
41
- if (l.has(o))
42
- return "[Circular]";
43
- if (l.add(o), Array.isArray(o))
44
- return o.map(c);
45
- const y = {};
46
- for (const d of Object.keys(o).sort())
47
- y[d] = c(o[d]);
48
- return y;
49
- };
50
- try {
51
- return JSON.stringify(c(s), null, a);
52
- } catch (o) {
53
- return `<<unserializable: ${o.message}>>`;
54
- }
55
- }
56
- const de = ({
57
- appState: s,
58
- title: a = "AppState",
59
- initialCollapsed: l = !1,
60
- overlayId: c = "default",
61
- position: o,
62
- maxBodyHeight: y = "50svh",
63
- indent: d = 2,
64
- maxSnapshots: C = 50,
65
- maxVisibleSnapshots: v = 10,
66
- snapshotOrder: A = "desc",
67
- appendSnapshotCountInTitle: U = !1
68
- }) => {
69
- const [b, I] = x(l), [R, T] = x(!1), m = V(null), [D, j] = x([]), [q, B] = x(
70
- () => (
71
- // Apply same targeting filter to initial buffered snapshots as we do for
72
- // live subscription events so targeted snapshots don't leak to unrelated
73
- // overlays.
74
- p.filter(
75
- (e) => !e.targetOverlays || e.targetOverlays.length === 0 || e.targetOverlays.includes(c)
76
- )
77
- )
78
- ), E = h(
79
- () => s !== void 0 ? N(s, d) : null,
80
- [s, d]
81
- );
82
- S(() => {
83
- E != null && j(
84
- (e) => [{ ts: Date.now(), json: E }, ...e].slice(0, C)
85
- );
86
- }, [E, C]), S(() => {
87
- const e = (t) => {
88
- (!t.targetOverlays || t.targetOverlays.length === 0 || t.targetOverlays.includes(c)) && B((r) => [...r, t]);
89
- };
90
- return $.add(e), () => {
91
- $.delete(e);
92
- };
93
- }, [c]);
94
- const i = h(() => {
95
- const e = [...D].reverse().map((n) => ({
96
- kind: "state",
97
- ts: n.ts,
98
- json: n.json,
99
- seq: F()
100
- })), t = q.map((n) => ({
101
- kind: "manual",
102
- ts: n.ts,
103
- seq: n.seq,
104
- label: n.label,
105
- color: n.color,
106
- json: N(n.data, d)
107
- })), r = [...e, ...t];
108
- return r.sort((n, f) => n.ts === f.ts ? n.seq - f.seq : n.ts - f.ts), r;
109
- }, [D, q, d]), k = h(
110
- () => A === "asc" ? i : [...i].reverse(),
111
- [A, i]
112
- ), Y = _(() => {
113
- const e = [];
114
- e.push(`${a} (total snapshots: ${i.length})`), i.forEach((r, n) => {
115
- const G = new Date(r.ts).toLocaleTimeString(void 0, {
116
- hour: "2-digit",
117
- minute: "2-digit",
118
- second: "2-digit",
119
- hour12: !1
120
- }), Z = r.kind === "manual" && r.label ? ` [${r.label}]` : "";
121
- e.push(`--- ${G} (#${n + 1})${Z} ---`), e.push(r.json);
122
- });
123
- const t = e.join(`
124
- `);
125
- navigator?.clipboard?.writeText && navigator.clipboard.writeText(t).then(() => {
126
- T(!0), setTimeout(() => T(!1), 1300);
127
- });
128
- }, [a, i]), P = _(() => {
129
- j([]), B([]), p.length = 0;
130
- }, []), M = h(() => {
131
- if (!o)
132
- return {};
133
- if (typeof o == "string")
134
- switch (o) {
135
- case "left":
136
- case "top-left":
137
- return { top: "0", left: "0" };
138
- case "right":
139
- case "top-right":
140
- return { top: "0", right: "0" };
141
- case "bottom-left":
142
- return { bottom: "0", left: "0" };
143
- case "bottom-right":
144
- return { bottom: "0", right: "0" };
145
- case "bottom":
146
- return { bottom: "0", right: "0" };
147
- case "top":
148
- default:
149
- return { top: "0", right: "0" };
150
- }
151
- const e = {};
152
- for (const t of ["top", "right", "bottom", "left"]) {
153
- const r = o[t];
154
- r !== void 0 && (e[t] = String(r));
155
- }
156
- return e;
157
- }, [o]), u = h(() => {
158
- const e = { ...M };
159
- return !("top" in e) && !("bottom" in e) && (e.top = "0"), !("left" in e) && !("right" in e) && (e.right = "0"), e;
160
- }, [M]);
161
- S(() => {
162
- const e = typeof window < "u" ? window.visualViewport : null;
163
- if (!e)
164
- return;
165
- const t = () => {
166
- if (m.current && (!("top" in u) && !("bottom" in u) && (m.current.style.top = `${e.offsetTop}px`), !("right" in u) && !("left" in u))) {
167
- const r = Math.max(
168
- 0,
169
- Math.round(window.innerWidth - (e.offsetLeft + e.width))
170
- );
171
- m.current.style.right = `${r}px`;
172
- }
173
- };
174
- return e.addEventListener("resize", t), e.addEventListener("scroll", t), t(), () => {
175
- e.removeEventListener("resize", t), e.removeEventListener("scroll", t);
176
- };
177
- }, [u]);
178
- const H = i.length, J = U ? `${a} (${H})` : a, K = { margin: 0, padding: 0 }, Q = {
179
- margin: 0,
180
- padding: 0,
181
- paddingLeft: 4
182
- }, w = {
183
- LOG_YELLOW: {
184
- color: "#ffcf7f",
185
- background: "rgba(255,200,0,0.08)",
186
- border: "2px solid #cc9a00"
187
- },
188
- LOG_GREEN: {
189
- color: "#b6ffb6",
190
- background: "rgba(0,255,0,0.08)",
191
- border: "2px solid #00aa00"
192
- },
193
- LOG_BLUE: {
194
- color: "#b6d9ff",
195
- background: "rgba(0,136,255,0.08)",
196
- border: "2px solid #0066cc"
197
- },
198
- LOG_MAGENTA: {
199
- color: "#ffb6ef",
200
- background: "rgba(255,0,200,0.08)",
201
- border: "2px solid #cc0088"
202
- },
203
- LOG_RED: {
204
- color: "#ffb6b6",
205
- background: "rgba(255,0,0,0.08)",
206
- border: "2px solid #cc0000"
207
- }
208
- };
209
- function X(e) {
210
- const t = e && w[e] ? w[e] : w.LOG_YELLOW;
211
- return {
212
- ...Q,
213
- color: t.color,
214
- background: t.background,
215
- borderLeft: t.border
216
- };
217
- }
218
- return /* @__PURE__ */ L(
219
- "div",
220
- {
221
- ref: m,
222
- "aria-label": "Debug overlay",
223
- style: {
224
- position: "fixed",
225
- fontFamily: "monospace",
226
- zIndex: 9999,
227
- fontSize: 12,
228
- color: "#d1faff",
229
- background: "rgba(0,0,0,0.78)",
230
- border: "1px solid #055",
231
- borderTop: "none",
232
- borderRight: "none",
233
- borderBottomLeftRadius: 6,
234
- maxWidth: 360,
235
- boxShadow: "0 2px 6px rgba(0,0,0,0.4)",
236
- ...u
237
- },
238
- children: [
239
- /* @__PURE__ */ L(
240
- "div",
241
- {
242
- style: {
243
- display: "flex",
244
- alignItems: "center",
245
- gap: 6,
246
- padding: "4px 6px 4px 8px",
247
- borderBottom: b ? "none" : "1px solid #044",
248
- background: "#022",
249
- borderBottomLeftRadius: b ? 6 : 0,
250
- userSelect: "none"
251
- },
252
- children: [
253
- /* @__PURE__ */ g("strong", { style: { fontWeight: 600, fontSize: 11 }, children: J }),
254
- /* @__PURE__ */ L("div", { style: { marginLeft: "auto", display: "flex", gap: 4 }, children: [
255
- /* @__PURE__ */ g(
256
- "button",
257
- {
258
- type: "button",
259
- onClick: () => I((e) => !e),
260
- "aria-label": b ? "Expand debug overlay" : "Collapse debug overlay",
261
- style: O,
262
- children: b ? "+" : "−"
263
- }
264
- ),
265
- /* @__PURE__ */ g(
266
- "button",
267
- {
268
- type: "button",
269
- onClick: Y,
270
- "aria-label": "Copy debug JSON",
271
- style: {
272
- ...O,
273
- color: R ? "#0f0" : O.color
274
- },
275
- children: R ? "Copied" : "Copy"
276
- }
277
- ),
278
- i.length > 1 && /* @__PURE__ */ g(
279
- "button",
280
- {
281
- type: "button",
282
- onClick: P,
283
- "aria-label": "Clear stored snapshots",
284
- style: O,
285
- children: "Clear"
286
- }
287
- )
288
- ] })
289
- ]
290
- }
291
- ),
292
- !b && /* @__PURE__ */ L(
293
- "div",
294
- {
295
- style: {
296
- margin: 0,
297
- padding: "6px 8px 8px 8px",
298
- maxHeight: y,
299
- overflow: "auto",
300
- lineHeight: 1.25,
301
- fontSize: 11,
302
- fontFamily: "monospace",
303
- whiteSpace: "pre-wrap",
304
- wordBreak: "break-word"
305
- },
306
- children: [
307
- k.slice(0, v).map((e) => {
308
- const t = i.findIndex((G) => G.seq === e.seq), n = new Date(e.ts).toLocaleTimeString(void 0, {
309
- hour: "2-digit",
310
- minute: "2-digit",
311
- second: "2-digit",
312
- hour12: !1
313
- }), f = e.kind === "manual" && e.label ? ` [${e.label}]` : "";
314
- return /* @__PURE__ */ g(
315
- "pre",
316
- {
317
- style: e.kind === "manual" ? X(e.color) : K,
318
- children: `--- ${n} (#${t + 1})${f} ---
319
- ${e.json}`
320
- },
321
- `${e.kind}-${e.seq}`
322
- );
323
- }),
324
- k.length > v && /* @__PURE__ */ g("div", { style: { opacity: 0.7, marginTop: 4 }, children: `(+${k.length - v} snapshots not shown)` })
325
- ]
326
- }
327
- )
328
- ]
329
- }
330
- );
331
- }, O = {
332
- background: "#033",
333
- color: "#0ff",
334
- border: "1px solid #055",
335
- borderRadius: 4,
336
- fontSize: 10,
337
- padding: "2px 6px",
338
- cursor: "pointer",
339
- fontFamily: "inherit"
340
- };
341
- export {
342
- de as DebugOverlay,
343
- se as LOG_BLUE,
344
- ne as LOG_GREEN,
345
- ae as LOG_MAGENTA,
346
- le as LOG_RED,
347
- re as LOG_YELLOW,
348
- ie as __resetDebugOverlayTestState,
349
- ce as useDebugOverlay
350
- };