@djangocfg/debuger 2.1.219

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 (46) hide show
  1. package/README.md +345 -0
  2. package/dist/chunk-ESIQODSI.mjs +115 -0
  3. package/dist/chunk-ESIQODSI.mjs.map +1 -0
  4. package/dist/chunk-PAWJFY3S.mjs +6 -0
  5. package/dist/chunk-PAWJFY3S.mjs.map +1 -0
  6. package/dist/chunk-YQE3KTBG.mjs +64 -0
  7. package/dist/chunk-YQE3KTBG.mjs.map +1 -0
  8. package/dist/customEmitter-BO-1IWxm.d.ts +57 -0
  9. package/dist/emitters/index.cjs +74 -0
  10. package/dist/emitters/index.cjs.map +1 -0
  11. package/dist/emitters/index.d.ts +33 -0
  12. package/dist/emitters/index.mjs +4 -0
  13. package/dist/emitters/index.mjs.map +1 -0
  14. package/dist/index.cjs +887 -0
  15. package/dist/index.cjs.map +1 -0
  16. package/dist/index.d.ts +162 -0
  17. package/dist/index.mjs +694 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/logger/index.cjs +123 -0
  20. package/dist/logger/index.cjs.map +1 -0
  21. package/dist/logger/index.d.ts +50 -0
  22. package/dist/logger/index.mjs +4 -0
  23. package/dist/logger/index.mjs.map +1 -0
  24. package/package.json +67 -0
  25. package/src/DebugButton.tsx +143 -0
  26. package/src/DebugPanel.tsx +171 -0
  27. package/src/bridges/index.ts +1 -0
  28. package/src/bridges/monitorBridge.ts +63 -0
  29. package/src/emitters/Emitter.ts +51 -0
  30. package/src/emitters/audioEmitter.ts +74 -0
  31. package/src/emitters/customEmitter.ts +42 -0
  32. package/src/emitters/index.ts +10 -0
  33. package/src/hooks/useAudioEventLog.ts +147 -0
  34. package/src/hooks/useCustomEventLog.ts +39 -0
  35. package/src/hooks/useDebugShortcut.ts +27 -0
  36. package/src/hooks/useStoreSnapshot.ts +70 -0
  37. package/src/index.ts +62 -0
  38. package/src/logger/index.ts +3 -0
  39. package/src/logger/logStore.ts +89 -0
  40. package/src/logger/logger.ts +95 -0
  41. package/src/logger/types.ts +39 -0
  42. package/src/panels/AudioDebugPanel.tsx +157 -0
  43. package/src/panels/LogsPanel.tsx +267 -0
  44. package/src/panels/StorePanel.tsx +71 -0
  45. package/src/store/debugStore.ts +42 -0
  46. package/src/styles/index.css +5 -0
@@ -0,0 +1,267 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useCallback, useEffect, useRef } from 'react';
4
+ import {
5
+ useDebugLogStore,
6
+ useDebugFilteredLogs,
7
+ useDebugLogCount,
8
+ useDebugErrorCount,
9
+ type LogLevel,
10
+ type LogEntry,
11
+ } from '../logger';
12
+ import {
13
+ Button,
14
+ CopyButton,
15
+ Badge,
16
+ Input,
17
+ Tooltip,
18
+ TooltipContent,
19
+ TooltipTrigger,
20
+ } from '@djangocfg/ui-core/components';
21
+ import { LazyJsonTree } from '@djangocfg/ui-tools';
22
+ import { useVirtualizer, type VirtualItem } from '@tanstack/react-virtual';
23
+ import {
24
+ Bug, Trash2, Download, ChevronDown, ChevronUp,
25
+ AlertCircle, Info, AlertTriangle, CheckCircle, Search,
26
+ } from 'lucide-react';
27
+ import { cn } from '@djangocfg/ui-core/lib';
28
+
29
+ // ============================================================================
30
+ // Constants
31
+ // ============================================================================
32
+
33
+ const ROW_ESTIMATE_PX = 32;
34
+ const OVERSCAN = 5;
35
+
36
+ const LOG_LEVEL_CONFIG: Record<LogLevel, { icon: React.ElementType; color: string }> = {
37
+ debug: { icon: Bug, color: 'text-muted-foreground' },
38
+ info: { icon: Info, color: 'text-blue-500' },
39
+ warn: { icon: AlertTriangle, color: 'text-yellow-500' },
40
+ error: { icon: AlertCircle, color: 'text-red-500' },
41
+ success: { icon: CheckCircle, color: 'text-green-500' },
42
+ };
43
+
44
+ const LOG_LEVELS = Object.keys(LOG_LEVEL_CONFIG) as LogLevel[];
45
+
46
+ // ============================================================================
47
+ // LogEntryRow
48
+ // ============================================================================
49
+
50
+ interface LogEntryRowProps {
51
+ entry: LogEntry;
52
+ expanded: boolean;
53
+ onToggle: () => void;
54
+ }
55
+
56
+ function LogEntryRow({ entry, expanded, onToggle }: LogEntryRowProps) {
57
+ const config = LOG_LEVEL_CONFIG[entry.level];
58
+ const Icon = config.icon;
59
+ const pad = (n: number, len = 2) => String(n).padStart(len, '0');
60
+ const d = entry.timestamp;
61
+ const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
62
+ const hasData = entry.data && Object.keys(entry.data).length > 0;
63
+ const hasStack = !!entry.stack;
64
+ const isExpandable = hasData || hasStack;
65
+
66
+ return (
67
+ <div className="border-b border-border/50 last:border-0">
68
+ <button
69
+ type="button"
70
+ onClick={isExpandable ? onToggle : undefined}
71
+ disabled={!isExpandable}
72
+ className={cn(
73
+ 'flex w-full items-start gap-2 px-3 py-1.5 text-left text-xs',
74
+ isExpandable ? 'hover:bg-muted/50 cursor-pointer' : 'cursor-default'
75
+ )}
76
+ >
77
+ <Icon className={cn('h-3.5 w-3.5 mt-0.5 shrink-0', config.color)} />
78
+ <span className="text-muted-foreground font-mono shrink-0">{time}</span>
79
+ <Badge variant="outline" className="shrink-0 text-[10px] px-1 py-0">{entry.component}</Badge>
80
+ <span className="flex-1 truncate">{entry.message}</span>
81
+ {isExpandable && (
82
+ <span className="shrink-0 text-muted-foreground">
83
+ {expanded ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
84
+ </span>
85
+ )}
86
+ </button>
87
+ {expanded && isExpandable && (
88
+ <div className="px-3 pb-2 pl-8">
89
+ {hasData && <div className="mt-1"><LazyJsonTree data={entry.data} mode="compact" /></div>}
90
+ {hasStack && (
91
+ <pre className="mt-2 text-[10px] text-red-400 whitespace-pre-wrap font-mono bg-red-950/20 p-2 rounded">
92
+ {entry.stack}
93
+ </pre>
94
+ )}
95
+ </div>
96
+ )}
97
+ </div>
98
+ );
99
+ }
100
+
101
+ // ============================================================================
102
+ // LogsPanel
103
+ // ============================================================================
104
+
105
+ export function LogsPanel({ isActive }: { isActive: boolean }) {
106
+ const [expandedLogs, setExpandedLogs] = useState<Set<string>>(new Set());
107
+ const [searchQuery, setSearchQuery] = useState('');
108
+ const [selectedLevels, setSelectedLevels] = useState<Set<LogLevel>>(
109
+ new Set(['debug', 'info', 'warn', 'error', 'success'])
110
+ );
111
+ const [componentFilter, setComponentFilter] = useState('');
112
+
113
+ const logs = useDebugFilteredLogs();
114
+ const logCount = useDebugLogCount();
115
+ const errorCount = useDebugErrorCount();
116
+ const clearLogs = useDebugLogStore((s) => s.clearLogs);
117
+ const setFilter = useDebugLogStore((s) => s.setFilter);
118
+ const exportLogs = useDebugLogStore((s) => s.exportLogs);
119
+
120
+ // Virtualizer
121
+ const scrollRef = useRef<HTMLDivElement>(null);
122
+ const virtualizer = useVirtualizer({
123
+ count: logs.length,
124
+ getScrollElement: () => scrollRef.current,
125
+ estimateSize: () => ROW_ESTIMATE_PX,
126
+ overscan: OVERSCAN,
127
+ });
128
+
129
+ useEffect(() => {
130
+ if (!isActive) return;
131
+ setFilter({
132
+ levels: Array.from(selectedLevels),
133
+ component: componentFilter || undefined,
134
+ search: searchQuery || undefined,
135
+ });
136
+ }, [isActive, selectedLevels, componentFilter, searchQuery, setFilter]);
137
+
138
+ const handleToggleExpand = useCallback((id: string) => {
139
+ setExpandedLogs((prev) => {
140
+ const next = new Set(prev);
141
+ next.has(id) ? next.delete(id) : next.add(id);
142
+ return next;
143
+ });
144
+ }, []);
145
+
146
+ const handleToggleLevel = useCallback((level: LogLevel) => {
147
+ setSelectedLevels((prev) => {
148
+ const next = new Set(prev);
149
+ next.has(level) ? next.delete(level) : next.add(level);
150
+ return next;
151
+ });
152
+ }, []);
153
+
154
+ const handleExport = useCallback(() => {
155
+ const json = exportLogs();
156
+ const blob = new Blob([json], { type: 'application/json' });
157
+ const url = URL.createObjectURL(blob);
158
+ const a = document.createElement('a');
159
+ a.href = url;
160
+ a.download = `debug-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
161
+ a.click();
162
+ URL.revokeObjectURL(url);
163
+ }, [exportLogs]);
164
+
165
+ const handleClear = useCallback(() => {
166
+ clearLogs();
167
+ setExpandedLogs(new Set());
168
+ }, [clearLogs]);
169
+
170
+ const logsJson = exportLogs();
171
+ const virtualItems = virtualizer.getVirtualItems();
172
+ const totalSize = virtualizer.getTotalSize();
173
+
174
+ return (
175
+ <div className="flex flex-col h-full">
176
+ {/* Stats */}
177
+ <div className="flex items-center gap-2 border-b border-border px-3 py-1.5 shrink-0">
178
+ <Badge variant="secondary" className="text-[10px]">{logCount} logs</Badge>
179
+ {errorCount > 0 && <Badge variant="destructive" className="text-[10px]">{errorCount} errors</Badge>}
180
+ </div>
181
+
182
+ {/* Filters */}
183
+ <div className="flex flex-wrap items-center gap-2 border-b border-border px-3 py-2 shrink-0">
184
+ <div className="relative flex-1 min-w-[140px]">
185
+ <Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
186
+ <Input
187
+ placeholder="Search..."
188
+ value={searchQuery}
189
+ onChange={(e) => setSearchQuery(e.target.value)}
190
+ className="h-7 pl-7 text-xs"
191
+ />
192
+ </div>
193
+ <Input
194
+ placeholder="Component..."
195
+ value={componentFilter}
196
+ onChange={(e) => setComponentFilter(e.target.value)}
197
+ className="h-7 w-24 text-xs"
198
+ />
199
+ <div className="flex items-center gap-0.5">
200
+ {LOG_LEVELS.map((level) => {
201
+ const { icon: Icon, color } = LOG_LEVEL_CONFIG[level];
202
+ const active = selectedLevels.has(level);
203
+ return (
204
+ <Tooltip key={level}>
205
+ <TooltipTrigger asChild>
206
+ <Button variant={active ? 'secondary' : 'ghost'} size="icon" className="h-6 w-6" onClick={() => handleToggleLevel(level)}>
207
+ <Icon className={cn('h-3.5 w-3.5', active && color)} />
208
+ </Button>
209
+ </TooltipTrigger>
210
+ <TooltipContent>{level}</TooltipContent>
211
+ </Tooltip>
212
+ );
213
+ })}
214
+ </div>
215
+ <div className="flex items-center gap-0.5 ml-auto">
216
+ <CopyButton value={logsJson} size="icon" className="h-6 w-6" />
217
+ <Tooltip>
218
+ <TooltipTrigger asChild>
219
+ <Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleExport}>
220
+ <Download className="h-3.5 w-3.5" />
221
+ </Button>
222
+ </TooltipTrigger>
223
+ <TooltipContent>Export JSON</TooltipContent>
224
+ </Tooltip>
225
+ <Tooltip>
226
+ <TooltipTrigger asChild>
227
+ <Button variant="ghost" size="icon" className="h-6 w-6" onClick={handleClear}>
228
+ <Trash2 className="h-3.5 w-3.5" />
229
+ </Button>
230
+ </TooltipTrigger>
231
+ <TooltipContent>Clear</TooltipContent>
232
+ </Tooltip>
233
+ </div>
234
+ </div>
235
+
236
+ {/* Virtualized log list */}
237
+ <div ref={scrollRef} className="flex-1 overflow-auto">
238
+ {logs.length === 0 ? (
239
+ <div className="flex flex-col items-center justify-center py-10 text-muted-foreground">
240
+ <Bug className="h-8 w-8 mb-2 opacity-40" />
241
+ <p className="text-sm">No logs yet</p>
242
+ </div>
243
+ ) : (
244
+ <div style={{ height: `${totalSize}px`, position: 'relative' }}>
245
+ {virtualItems.map((vItem: VirtualItem) => {
246
+ const entry = logs[vItem.index];
247
+ return (
248
+ <div
249
+ key={entry.id}
250
+ data-index={vItem.index}
251
+ ref={virtualizer.measureElement}
252
+ style={{ position: 'absolute', top: 0, left: 0, width: '100%', transform: `translateY(${vItem.start}px)` }}
253
+ >
254
+ <LogEntryRow
255
+ entry={entry}
256
+ expanded={expandedLogs.has(entry.id)}
257
+ onToggle={() => handleToggleExpand(entry.id)}
258
+ />
259
+ </div>
260
+ );
261
+ })}
262
+ </div>
263
+ )}
264
+ </div>
265
+ </div>
266
+ );
267
+ }
@@ -0,0 +1,71 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { ScrollArea } from '@djangocfg/ui-core/components';
5
+ import { LazyJsonTree } from '@djangocfg/ui-tools';
6
+ import { Database } from 'lucide-react';
7
+ import { useStoreSnapshot } from '../hooks/useStoreSnapshot';
8
+
9
+ // ============================================================================
10
+ // Types
11
+ // ============================================================================
12
+
13
+ export interface StorePanelProps {
14
+ /** Display label shown above the JSON tree */
15
+ label: string;
16
+ /**
17
+ * Getter for the store state slice to display.
18
+ * Use `() => useMyStore.getState()` — stable function ref is not required,
19
+ * the hook handles stabilization internally.
20
+ */
21
+ getState: () => Record<string, unknown>;
22
+ /**
23
+ * Poll interval in ms. Default 200ms.
24
+ * Use higher values for stores that update very frequently.
25
+ */
26
+ intervalMs?: number;
27
+ /** Only polls when active (panel tab is visible) */
28
+ isActive: boolean;
29
+ }
30
+
31
+ // ============================================================================
32
+ // Component
33
+ // ============================================================================
34
+
35
+ /**
36
+ * Generic Zustand store viewer for the debug panel.
37
+ *
38
+ * Uses polling (not reactive subscription) to read external store state.
39
+ * This avoids Zustand v5 infinite loop issues and prevents the debug panel
40
+ * from re-rendering on every store change.
41
+ *
42
+ * @example — in consuming app's custom tab:
43
+ * import { StorePanel } from '@org/debuger';
44
+ * import { useTimelineStore } from '@stores/timelineStore';
45
+ *
46
+ * <StorePanel
47
+ * label="Timeline Store"
48
+ * getState={() => useTimelineStore.getState()}
49
+ * isActive={isActive}
50
+ * />
51
+ */
52
+ export function StorePanel({ label, getState, intervalMs = 200, isActive }: StorePanelProps) {
53
+ const snapshot = useStoreSnapshot(getState, intervalMs, isActive);
54
+
55
+ return (
56
+ <div className="flex flex-col h-full">
57
+ <div className="flex items-center gap-2 border-b border-border px-3 py-2 shrink-0">
58
+ <Database className="h-3.5 w-3.5 text-muted-foreground" />
59
+ <span className="text-xs font-medium text-muted-foreground">{label}</span>
60
+ <span className="ml-auto text-[10px] text-muted-foreground/60">
61
+ polling {intervalMs}ms
62
+ </span>
63
+ </div>
64
+ <ScrollArea className="flex-1">
65
+ <div className="p-2">
66
+ <LazyJsonTree data={snapshot} mode="full" />
67
+ </div>
68
+ </ScrollArea>
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import { create } from 'zustand';
4
+
5
+ // ============================================================================
6
+ // Types
7
+ // ============================================================================
8
+
9
+ export type DebugTab = 'logs' | 'audio' | 'pipeline' | 'custom';
10
+
11
+ export interface DebugStore {
12
+ isOpen: boolean;
13
+ tab: DebugTab;
14
+ isUnlocked: boolean;
15
+ open: () => void;
16
+ close: () => void;
17
+ toggle: () => void;
18
+ setTab: (t: DebugTab) => void;
19
+ unlock: (key: string) => boolean;
20
+ }
21
+
22
+ // ============================================================================
23
+ // Store
24
+ // ============================================================================
25
+
26
+ export const useDebugStore = create<DebugStore>((set) => ({
27
+ isOpen: false,
28
+ tab: 'logs',
29
+ isUnlocked: false,
30
+
31
+ open: () => set({ isOpen: true }),
32
+ close: () => set({ isOpen: false }),
33
+ toggle: () => set((s) => ({ isOpen: !s.isOpen })),
34
+ setTab: (tab) => set({ tab }),
35
+
36
+ unlock: (_key) => {
37
+ // Any non-empty value unlocks — no secret key required
38
+ const valid = true;
39
+ if (valid) set({ isUnlocked: true });
40
+ return valid;
41
+ },
42
+ }));
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tailwind v4 Source Detection for Debuger Package
3
+ * Add this import to your app's globals.css to enable Tailwind class scanning.
4
+ */
5
+ @source "../**/*.{ts,tsx}";