@bimetal/devtools-react 0.16.5

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 (36) hide show
  1. package/LICENSE +129 -0
  2. package/README.md +61 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/BridgesBody.d.ts +27 -0
  5. package/dist/BridgesBody.d.ts.map +1 -0
  6. package/dist/BridgesBody.js +154 -0
  7. package/dist/BridgesBody.js.map +1 -0
  8. package/dist/CausationBody.d.ts +30 -0
  9. package/dist/CausationBody.d.ts.map +1 -0
  10. package/dist/CausationBody.js +149 -0
  11. package/dist/CausationBody.js.map +1 -0
  12. package/dist/DevtoolsRegistryContext.d.ts +14 -0
  13. package/dist/DevtoolsRegistryContext.d.ts.map +1 -0
  14. package/dist/DevtoolsRegistryContext.js +14 -0
  15. package/dist/DevtoolsRegistryContext.js.map +1 -0
  16. package/dist/DevtoolsRegistryProvider.d.ts +16 -0
  17. package/dist/DevtoolsRegistryProvider.d.ts.map +1 -0
  18. package/dist/DevtoolsRegistryProvider.js +14 -0
  19. package/dist/DevtoolsRegistryProvider.js.map +1 -0
  20. package/dist/EventInspectorPanel.d.ts +27 -0
  21. package/dist/EventInspectorPanel.d.ts.map +1 -0
  22. package/dist/EventInspectorPanel.js +669 -0
  23. package/dist/EventInspectorPanel.js.map +1 -0
  24. package/dist/index.d.ts +29 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +27 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/inspector-shared.d.ts +52 -0
  29. package/dist/inspector-shared.d.ts.map +1 -0
  30. package/dist/inspector-shared.js +168 -0
  31. package/dist/inspector-shared.js.map +1 -0
  32. package/dist/useDevtoolsRegistry.d.ts +20 -0
  33. package/dist/useDevtoolsRegistry.d.ts.map +1 -0
  34. package/dist/useDevtoolsRegistry.js +37 -0
  35. package/dist/useDevtoolsRegistry.js.map +1 -0
  36. package/package.json +56 -0
@@ -0,0 +1,669 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Event-Inspector — Vollseiten-Variant (v0.14.0, in `@bimetal/devtools-react`
4
+ * extrahiert in v0.16.5).
5
+ *
6
+ * UX-Vorbild laut Architektur-Doc: EDA-Lab / KurrentDB-UI.
7
+ * Tabelle statt Cards. ID-Stubs (8-Zeichen) statt Volltext.
8
+ * Causation/Correlation als first-class Spalten.
9
+ *
10
+ * Ehrlichkeits-Grundsatz: causationId zeigt im Calendar-DomainStore-Flow
11
+ * auf eine Command-ID, nicht auf ein vorausgehendes Event. Die Tabelle
12
+ * zeigt die rohe ID — keine Fake-Causation-Tree-Verlinkung.
13
+ *
14
+ * **Registry-Resolution (v0.16.5 Pilot-Schärfung):** Prop > Context >
15
+ * Throw. Kein impliziter Singleton-Fallback. Siehe
16
+ * `16-devtools-react-architecture.md` Leitplanke 2.
17
+ */
18
+ import { useState, useSyncExternalStore, useMemo, useEffect } from 'react';
19
+ import { exportSnapshot, serializeSnapshot } from '@bimetal/devtools';
20
+ import { CausationBody } from './CausationBody.js';
21
+ import { BridgesBody } from './BridgesBody.js';
22
+ import { useResolvedRegistry } from './useDevtoolsRegistry.js';
23
+ import { DevtoolsRegistryContext } from './DevtoolsRegistryContext.js';
24
+ import { EventDetail, MetaPlainRow, MetaRow, Section, detailColumnStyle, detailHeaderStyle, detailInnerStyle, detailMetaGridStyle, emptyDetailStyle, emptyStyle, formatTimeWithMs, inlineCodeStyle, preStyle, safeStringify, stub, tableScrollStyle, twoColStyle, } from './inspector-shared.js';
25
+ export function EventInspectorPanel({ registry: registryProp } = {}) {
26
+ const registry = useResolvedRegistry(registryProp);
27
+ const [activeTab, setActiveTab] = useState('events');
28
+ const [selectedSeq, setSelectedSeq] = useState(null);
29
+ const [typeFilter, setTypeFilter] = useState('');
30
+ const [streamFilter, setStreamFilter] = useState('');
31
+ const [storeFilter, setStoreFilter] = useState('');
32
+ const [minSeq, setMinSeq] = useState(0);
33
+ const [snapshotWhenPaused, setSnapshotWhenPaused] = useState(null);
34
+ const liveState = useSyncExternalStore(registry.subscribe, () => registry.state, () => registry.state);
35
+ const state = snapshotWhenPaused ?? liveState;
36
+ const paused = snapshotWhenPaused !== null;
37
+ const visible = useMemo(() => {
38
+ const tf = typeFilter.toLowerCase();
39
+ const sf = streamFilter.toLowerCase();
40
+ return state.events.filter(e => {
41
+ if (e.seq < minSeq)
42
+ return false;
43
+ if (storeFilter && e.storeId !== storeFilter)
44
+ return false;
45
+ if (tf && !e.event.type.toLowerCase().includes(tf))
46
+ return false;
47
+ if (sf && !e.streamId.toLowerCase().includes(sf))
48
+ return false;
49
+ return true;
50
+ });
51
+ }, [state.events, minSeq, storeFilter, typeFilter, streamFilter]);
52
+ const streamCount = useMemo(() => {
53
+ const set = new Set();
54
+ for (const e of state.events) {
55
+ if (e.seq >= minSeq)
56
+ set.add(`${e.storeId}::${e.streamId}`);
57
+ }
58
+ return set.size;
59
+ }, [state.events, minSeq]);
60
+ const selected = state.events.find(e => e.seq === selectedSeq) ?? null;
61
+ function togglePause() {
62
+ setSnapshotWhenPaused(prev => (prev === null ? liveState : null));
63
+ }
64
+ function clear() {
65
+ const maxSeq = state.events.length > 0 ? state.events[state.events.length - 1].seq : 0;
66
+ setMinSeq(maxSeq + 1);
67
+ setSelectedSeq(null);
68
+ }
69
+ // Snapshot-Export (v0.16.6 Sub 2). Date.now() läuft am UI-Layer —
70
+ // exportSnapshot bleibt deterministisch (Pilot-Schärfung Sub 1).
71
+ // Exportiert den aktuellen `state` (= paused-Snapshot wenn pausiert,
72
+ // sonst Live-Snapshot). Filename mit saniertem ISO-Datum
73
+ // (Doppelpunkt/Punkt → Bindestrich; Windows-Datei-System-freundlich).
74
+ function exportAndDownload() {
75
+ const exportedAt = Date.now();
76
+ const snap = exportSnapshot(state, { exportedAt });
77
+ const json = serializeSnapshot(snap);
78
+ const iso = new Date(exportedAt).toISOString();
79
+ const filename = `bimetal-devtools-snapshot-${iso.replace(/[:.]/g, '-')}.json`;
80
+ triggerDownload(filename, json);
81
+ }
82
+ const visibleCountInWindow = useMemo(() => state.events.filter(e => e.seq >= minSeq).length, [state.events, minSeq]);
83
+ // Causation-Tab respektiert den Clear-Button-Vertrag wie der Events-Tab:
84
+ // nach Clear (= minSeq nach vorne geschoben) sollen die alten Events
85
+ // auch im Forest verschwinden. Andere Filter (type/stream/store) bleiben
86
+ // bewusst draußen — der Forest soll die Causation-Topologie zeigen, nicht
87
+ // eine gefilterte Teilsicht.
88
+ const causationEvents = useMemo(() => state.events.filter(e => e.seq >= minSeq), [state.events, minSeq]);
89
+ // v0.15.5 + v0.16.0: Tab-Bar wird zur Laufzeit aufgebaut. Tabs werden
90
+ // einzeln sichtbar, sobald ihr Datenfeld nicht leer ist (überlebt
91
+ // Variant-Switch via rolling-window oder Registry-Stand).
92
+ // Bridges-Tab respektiert Clear-Button-Vertrag wie Events- und Causation-
93
+ // Tab: nach Clear (minSeq nach vorne) verschwinden alte Bridge-Records.
94
+ // Type-/Stream-/Store-Filter bleiben bewusst draußen — der Bridge-Tab
95
+ // zeigt Transport-Topologie, keine Event-Filter-Teilsicht.
96
+ const bridgeRecords = useMemo(() => state.bridgeRecords.filter(r => r.seq >= minSeq), [state.bridgeRecords, minSeq]);
97
+ const showSyncTab = state.syncTransports.length > 0 || state.syncRecords.length > 0;
98
+ const showStateTab = state.replayTargets.length > 0;
99
+ const showCausationTab = causationEvents.length > 0;
100
+ const showBridgesTab = state.bridges.length > 0 || bridgeRecords.length > 0;
101
+ const showTabs = showSyncTab || showStateTab || showCausationTab || showBridgesTab;
102
+ // Wenn der gerade aktive Tab nicht mehr verfügbar ist (Variant-Wechsel
103
+ // hat Targets/Transports abgemeldet), fall back auf Events.
104
+ const effectiveTab = (activeTab === 'sync' && !showSyncTab) ||
105
+ (activeTab === 'state' && !showStateTab) ||
106
+ (activeTab === 'causation' && !showCausationTab) ||
107
+ (activeTab === 'bridges' && !showBridgesTab)
108
+ ? 'events'
109
+ : activeTab;
110
+ // Resolved Registry intern als Context bereitstellen, damit interne
111
+ // Tab-Bodies und zukünftige Sub-Komponenten useDevtoolsRegistry() nutzen
112
+ // können, auch wenn der Konsument nur per Prop konfiguriert hat.
113
+ // Pilot-Schärfung Sub 1: Hook liest ausschließlich Context — der Panel
114
+ // ist die Stelle, die Prop und Context vereinheitlicht.
115
+ return (_jsx(DevtoolsRegistryContext.Provider, { value: registry, children: _jsxs("div", { style: pageStyle, children: [_jsx(PanelHeader, { events: visibleCountInWindow, streams: streamCount, stores: state.eventStores.length, syncTransports: state.syncTransports.length, replayTargets: state.replayTargets.length, filtered: visible.length, paused: paused, onTogglePause: togglePause, onClear: clear, onExport: exportAndDownload }), showTabs && (_jsx(TabBar, { activeTab: effectiveTab, onSelect: setActiveTab, showSync: showSyncTab, showState: showStateTab, showCausation: showCausationTab, showBridges: showBridgesTab })), effectiveTab === 'events' && (_jsxs(_Fragment, { children: [_jsx(FilterBar, { stores: state.eventStores, storeFilter: storeFilter, onStoreFilter: setStoreFilter, typeFilter: typeFilter, onTypeFilter: setTypeFilter, streamFilter: streamFilter, onStreamFilter: setStreamFilter }), _jsx(EventsBody, { state: state, visible: visible, selected: selected, selectedSeq: selectedSeq, onSelect: setSelectedSeq, paused: paused })] })), effectiveTab === 'sync' && (_jsx(SyncBody, { records: state.syncRecords, paused: paused })), effectiveTab === 'state' && (_jsx(StateBody, { targets: state.replayTargets, registry: registry })), effectiveTab === 'causation' && (_jsx(CausationBody, { events: causationEvents })), effectiveTab === 'bridges' && (_jsx(BridgesBody, { bridges: state.bridges, records: bridgeRecords }))] }) }));
116
+ }
117
+ function TabBar({ activeTab, onSelect, showSync, showState, showCausation, showBridges, }) {
118
+ return (_jsxs("div", { style: tabBarStyle, children: [_jsx("button", { onClick: () => onSelect('events'), style: activeTab === 'events' ? tabActiveStyle : tabStyle, children: "Events" }), showSync && (_jsx("button", { onClick: () => onSelect('sync'), style: activeTab === 'sync' ? tabActiveStyle : tabStyle, children: "Sync" })), showState && (_jsx("button", { onClick: () => onSelect('state'), style: activeTab === 'state' ? tabActiveStyle : tabStyle, children: "State" })), showCausation && (_jsx("button", { onClick: () => onSelect('causation'), style: activeTab === 'causation' ? tabActiveStyle : tabStyle, children: "Causation" })), showBridges && (_jsx("button", { onClick: () => onSelect('bridges'), style: activeTab === 'bridges' ? tabActiveStyle : tabStyle, children: "Bridges" }))] }));
119
+ }
120
+ // ── Sync-Tab ──────────────────────────────────────────────────────
121
+ function SyncBody({ records, paused }) {
122
+ const [selectedSeq, setSelectedSeq] = useState(null);
123
+ const selected = records.find(r => r.seq === selectedSeq) ?? null;
124
+ if (records.length === 0) {
125
+ return (_jsxs("div", { style: emptyStyle, children: ["Noch keine Sync-Records. Sobald ein registrierter Transport sendet/empf\u00E4ngt, erscheinen hier ", _jsx("code", { children: "sync-connect" }), ", ", _jsx("code", { children: "sync-append-sent" }), ",", _jsx("code", { children: "sync-events-received" }), " etc."] }));
126
+ }
127
+ return (_jsxs("div", { style: twoColStyle, children: [_jsxs("div", { style: tableScrollStyle, children: [paused && _jsx("div", { style: pausedBannerStyle, children: "\u23F8 Live-Stream pausiert \u2014 Ansicht eingefroren." }), _jsxs("table", { style: tableStyle, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: thStyle, children: "Zeit" }), _jsx("th", { style: thStyle, children: "Dir" }), _jsx("th", { style: thStyle, children: "Kind" }), _jsx("th", { style: thStyle, children: "Transport" }), _jsx("th", { style: thStyle, children: "Stream / Sub" }), _jsx("th", { style: thStyle, children: "Req-ID" }), _jsx("th", { style: thNumStyle, children: "Info" })] }) }), _jsx("tbody", { children: records.slice().reverse().map(r => (_jsx(SyncRow, { captured: r, isSelected: r.seq === selectedSeq, onSelect: () => setSelectedSeq(r.seq === selectedSeq ? null : r.seq) }, r.seq))) })] })] }), _jsx("div", { style: detailColumnStyle, children: selected
128
+ ? _jsx(SyncDetail, { captured: selected })
129
+ : _jsx("div", { style: emptyDetailStyle, children: "Klick auf eine Zeile zeigt das vollst\u00E4ndige Wire-Frame." }) })] }));
130
+ }
131
+ function SyncRow({ captured, isSelected, onSelect, }) {
132
+ const r = captured.record;
133
+ const direction = syncDirection(r.kind);
134
+ const dirColor = direction === '↑' ? '#a8d8a8' : direction === '↓' ? '#9ec8ff' : '#888';
135
+ const reqId = 'requestId' in r ? r.requestId : '';
136
+ const subId = 'subscriptionId' in r ? r.subscriptionId : '';
137
+ const streamSubLabel = ('streamId' in r ? r.streamId : '')
138
+ + (subId ? ` · ${stub(subId)}` : '');
139
+ const info = syncRowInfo(r);
140
+ return (_jsxs("tr", { onClick: onSelect, style: { ...trStyle, ...(isSelected ? trSelectedStyle : null), cursor: 'pointer' }, children: [_jsx("td", { style: tdStyle, children: formatTime(captured.capturedAt) }), _jsx("td", { style: { ...tdStyle, color: dirColor, fontFamily: 'ui-monospace, monospace', fontWeight: 600 }, children: direction }), _jsx("td", { style: { ...tdStyle, fontFamily: 'ui-monospace, monospace' }, children: r.kind.replace('sync-', '') }), _jsx("td", { style: tdStoreStyle, children: captured.transportName }), _jsx("td", { style: tdMonoStyle, children: streamSubLabel || '—' }), _jsx("td", { style: tdMonoStyle, children: reqId ? stub(reqId) : '—' }), _jsx("td", { style: tdNumStyle, children: info })] }));
141
+ }
142
+ function SyncDetail({ captured }) {
143
+ return (_jsxs("div", { style: detailInnerStyle, children: [_jsxs("div", { style: detailHeaderStyle, children: [_jsx("div", { style: { fontWeight: 600, fontSize: 14, fontFamily: 'ui-monospace, monospace' }, children: captured.record.kind }), _jsxs("div", { style: { fontSize: 11, color: '#888', marginTop: 4 }, children: ["#", captured.seq, " \u00B7 Transport ", _jsx("code", { style: inlineCodeStyle, children: captured.transportName }), " ", _jsxs("span", { style: { opacity: 0.6 }, children: ["(id: ", captured.transportId, ")"] })] }), _jsx("div", { style: { fontSize: 11, color: '#888', marginTop: 4 }, children: formatTimeWithMs(captured.capturedAt) })] }), _jsx(Section, { title: "Record", children: _jsx("pre", { style: preStyle, children: safeStringify(captured.record) }) })] }));
144
+ }
145
+ function syncDirection(kind) {
146
+ if (kind === 'sync-append-sent')
147
+ return '↑';
148
+ if (kind === 'sync-append-confirmed' || kind === 'sync-append-rejected' || kind === 'sync-events-received')
149
+ return '↓';
150
+ return '·';
151
+ }
152
+ function syncRowInfo(r) {
153
+ switch (r.kind) {
154
+ case 'sync-connect': return r.url ?? 'connected';
155
+ case 'sync-disconnect': return r.code !== undefined ? `code ${r.code}` : 'disconnect';
156
+ case 'sync-reconnect-scheduled': return `attempt ${r.attempt} in ${r.nextAttemptInMs}ms`;
157
+ case 'sync-append-sent': return `${r.eventCount} ev @ v${r.expectedVersion}`;
158
+ case 'sync-append-confirmed': return `→ v${r.newVersion} (${r.tookMs.toFixed(1)}ms)`;
159
+ case 'sync-append-rejected': return `${r.reason} (exp ${r.expectedVersion}, act ${r.actualVersion})`;
160
+ case 'sync-events-received': return `${r.eventCount} ev → v${r.toVersion}`;
161
+ default: return '';
162
+ }
163
+ }
164
+ // ── State-Tab (v0.16.0 Replay-Target) ────────────────────────────
165
+ function StateBody({ targets, registry, }) {
166
+ const [selectedId, setSelectedId] = useState(null);
167
+ const selected = targets.find(t => t.id === selectedId) ?? targets[0] ?? null;
168
+ if (targets.length === 0) {
169
+ return (_jsxs("div", { style: emptyStyle, children: ["Keine ReplayTargets registriert. Variants registrieren ihre Calendar-Stores via ", _jsx("code", { children: "createObservedReplayTarget" }), " \u2014 der Stream-State erscheint hier sobald ein Target l\u00E4uft."] }));
170
+ }
171
+ return (_jsxs("div", { style: twoColStyle, children: [_jsx("div", { style: tableScrollStyle, children: _jsxs("table", { style: tableStyle, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: thStyle, children: "Target" }), _jsx("th", { style: thStyle, children: "Stream" }), _jsx("th", { style: thStyle, children: "Status" }), _jsx("th", { style: thNumStyle, children: "v" }), _jsx("th", { style: thNumStyle, children: "schema" }), _jsx("th", { style: thNumStyle, children: "proj" }), _jsx("th", { style: thNumStyle, children: "blocked" })] }) }), _jsx("tbody", { children: targets.map(t => (_jsx(StateRow, { target: t, isSelected: selected?.id === t.id, onSelect: () => setSelectedId(t.id) }, t.id))) })] }) }), _jsx("div", { style: detailColumnStyle, children: selected
172
+ ? _jsx(StateDetail, { target: selected, registry: registry })
173
+ : _jsx("div", { style: emptyDetailStyle, children: "Klick auf eine Zeile zeigt State + ReadModel." }) })] }));
174
+ }
175
+ /**
176
+ * Drift erkannt wenn `schema` und `projection` auseinanderlaufen. Visuell
177
+ * rot markiert in der Tabelle. Beide Werte werden separat angezeigt,
178
+ * sodass der User sofort sieht, in welche Richtung die Drift geht.
179
+ */
180
+ function isSchemaDrift(schemaV, projV) {
181
+ return schemaV !== undefined && schemaV !== projV;
182
+ }
183
+ function StateRow({ target, isSelected, onSelect, }) {
184
+ const snap = target.snapshot;
185
+ const statusColor = snap.status === 'error' ? '#d97a6b' : snap.status === 'live' ? '#a8d8a8' : '#e0c878';
186
+ const drift = isSchemaDrift(snap.lastAppliedSchemaVersion, target.projectionVersion);
187
+ const driftStyle = drift ? { color: '#d97a6b', fontWeight: 600 } : {};
188
+ return (_jsxs("tr", { onClick: onSelect, style: { ...trStyle, ...(isSelected ? trSelectedStyle : null), cursor: 'pointer' }, children: [_jsx("td", { style: tdStoreStyle, children: target.name }), _jsx("td", { style: tdMonoStyle, children: target.streamId }), _jsx("td", { style: { ...tdStyle, color: statusColor, fontFamily: 'ui-monospace, monospace', fontWeight: 600 }, children: snap.status }), _jsx("td", { style: tdNumStyle, children: snap.lastAppliedVersion }), _jsx("td", { style: { ...tdNumStyle, ...driftStyle }, title: drift ? 'Schema-Drift: Event-schemaVersion ≠ projection.version' : undefined, children: snap.lastAppliedSchemaVersion ?? '—' }), _jsx("td", { style: { ...tdNumStyle, ...driftStyle }, children: target.projectionVersion }), _jsx("td", { style: tdNumStyle, children: snap.blockedEventCount })] }));
189
+ }
190
+ function StateDetail({ target, registry, }) {
191
+ const snap = target.snapshot;
192
+ const drift = isSchemaDrift(snap.lastAppliedSchemaVersion, target.projectionVersion);
193
+ return (_jsxs("div", { style: detailInnerStyle, children: [_jsxs("div", { style: detailHeaderStyle, children: [_jsx("div", { style: { fontWeight: 600, fontSize: 14 }, children: target.name }), _jsxs("div", { style: { fontSize: 11, color: '#888', marginTop: 4 }, children: ["Stream ", _jsx("code", { style: inlineCodeStyle, children: target.streamId }), " \u00B7 ", _jsxs("span", { style: { opacity: 0.6 }, children: ["id: ", target.id] })] }), drift && (_jsxs("div", { style: { marginTop: 8, padding: '6px 10px', background: '#3a2a1a', color: '#ffc88a', fontSize: 11, borderRadius: 4 }, children: ["\u26A0 Schema-Drift: zuletzt applied Event hat ", _jsxs("code", { style: inlineCodeStyle, children: ["schemaVersion=", snap.lastAppliedSchemaVersion] }), ", aber ", _jsxs("code", { style: inlineCodeStyle, children: ["projection.version=", target.projectionVersion] }), ". Upcaster auf Domain-Ebene pr\u00FCfen."] })), _jsxs("div", { style: detailMetaGridStyle, children: [_jsx(MetaPlainRow, { label: "Status", value: snap.status }), _jsx(MetaPlainRow, { label: "Version", value: String(snap.lastAppliedVersion) }), _jsx(MetaPlainRow, { label: "Schema v", value: snap.lastAppliedSchemaVersion !== undefined ? String(snap.lastAppliedSchemaVersion) : '—' }), _jsx(MetaPlainRow, { label: "Projection v", value: String(target.projectionVersion) }), _jsx(MetaPlainRow, { label: "Last event", value: snap.lastAppliedEventId ?? '—', mono: true }), _jsx(MetaPlainRow, { label: "Blocked", value: String(snap.blockedEventCount) })] })] }), snap.lastError && (_jsx(Section, { title: "Last Error", children: _jsxs("pre", { style: preStyle, children: [snap.lastError.error.message, snap.lastError.event ? `\n\nAt event v${snap.lastError.event.envelope.version} (${snap.lastError.event.type})` : '\n\n(from read() — no event)'] }) })), _jsx(TimeTravelSection, { targetId: target.id, liveVersion: snap.lastAppliedVersion, registry: registry }, target.id)] }));
194
+ }
195
+ function TimeTravelSection({ targetId, liveVersion, registry, }) {
196
+ // Pilot-Bug-Fix 2026-06-09: Controller-Lifecycle gehört in `useEffect`,
197
+ // NICHT in `useState`. Vorher: `useState(() => createCtrl)` hielt eine
198
+ // einzige Instanz, die der `useEffect`-Cleanup in StrictMode-Dev sofort
199
+ // nach Mount disposed hat — der Re-Setup-Pass feuerte `goTo` auf der
200
+ // toten Instanz, `view.status='loading'` blieb dauerhaft. In Production
201
+ // (ohne StrictMode) lief das durch, in Dev hing es immer.
202
+ //
203
+ // Lifecycle hier: Setup-1 erzeugt ctrl_A; Cleanup-1 dispose'd + nullt
204
+ // state; Setup-2 erzeugt ctrl_B; Inner re-rendered mit ctrl_B und der
205
+ // useSyncExternalStore-Hook subscribed neu.
206
+ const [controller, setController] = useState(null);
207
+ useEffect(() => {
208
+ const ctrl = registry.createTimeTravelController(targetId);
209
+ setController(ctrl);
210
+ return () => {
211
+ ctrl.dispose();
212
+ setController(curr => (curr === ctrl ? null : curr));
213
+ };
214
+ }, [targetId, registry]);
215
+ if (!controller) {
216
+ return (_jsx(Section, { title: "Time Travel \u00B7 \u2026", children: _jsx("div", { style: { opacity: 0.6, fontSize: 12 }, children: "Initialisiere \u2026" }) }));
217
+ }
218
+ return (_jsx(TimeTravelSectionInner, { controller: controller, liveVersion: liveVersion }));
219
+ }
220
+ function TimeTravelSectionInner({ controller, liveVersion, }) {
221
+ const view = useSyncExternalStore(controller.subscribe, () => controller.view, () => controller.view);
222
+ // Auto-Follow Live: solange `hasUserMoved=false` zieht der Cursor
223
+ // dem Live-Stand nach. Sobald der User scrubt, friert die Position
224
+ // fest. `→ Live`-Button setzt das Flag zurück.
225
+ //
226
+ // Deps bewusst nur `[hasUserMoved, liveVersion, controller]`:
227
+ // - status/cursor SIND NICHT in deps → kein Re-Run pro Status-Tick
228
+ // (kein Spam).
229
+ // - Wenn Live während eines laufenden goTo(oldLive) springt, läuft
230
+ // der Effect mit neuem liveVersion und ruft goTo(newLive). Der
231
+ // Controller invalidiert den alten Read via running-counter
232
+ // (Stale-Cancel). Genau dafür ist der running-counter da.
233
+ const [hasUserMoved, setHasUserMoved] = useState(false);
234
+ useEffect(() => {
235
+ if (hasUserMoved)
236
+ return;
237
+ void controller.goTo(liveVersion);
238
+ }, [hasUserMoved, liveVersion, controller]);
239
+ const [mode, setMode] = useState('live');
240
+ const [diff, setDiff] = useState([]);
241
+ const [diffLoading, setDiffLoading] = useState(false);
242
+ // Diff: pro view.status sauberen UI-Zustand. Nicht-ready löscht den
243
+ // alten Diff, damit kein stale-Inhalt zur falschen Version stehen
244
+ // bleibt. Promise mit catch/finally + stale-Flag.
245
+ useEffect(() => {
246
+ let stale = false;
247
+ if (view.status !== 'ready') {
248
+ setDiff([]);
249
+ setDiffLoading(view.status === 'loading');
250
+ return () => { stale = true; };
251
+ }
252
+ setDiffLoading(true);
253
+ const compareTo = mode === 'previous' ? Math.max(0, view.cursor - 1) : undefined;
254
+ controller.diff(compareTo).then((d) => {
255
+ if (stale)
256
+ return;
257
+ setDiff(d);
258
+ }, () => {
259
+ if (stale)
260
+ return;
261
+ setDiff([]);
262
+ }).finally(() => {
263
+ if (stale)
264
+ return;
265
+ setDiffLoading(false);
266
+ });
267
+ return () => { stale = true; };
268
+ }, [view.cursor, view.status, mode, controller]);
269
+ const max = Math.max(liveVersion, view.maxVersion);
270
+ // atLive = synchron mit Live (nicht: am Slider-Ende). Falls
271
+ // view.maxVersion temporär > liveVersion, kann der User über
272
+ // liveVersion hinaus scrubben — aber "am Live" heißt explizit
273
+ // cursor === liveVersion.
274
+ const atLive = view.cursor === liveVersion;
275
+ const stateLabel = atLive ? 'Live' : `v${view.cursor}`;
276
+ return (_jsxs(Section, { title: `Time Travel · ${stateLabel} / max v${max}`, children: [_jsxs("div", { style: ttRowStyle, children: [_jsx("input", { type: "range", min: 0, max: max, value: view.cursor, onChange: (e) => {
277
+ setHasUserMoved(true);
278
+ void controller.goTo(Number(e.target.value));
279
+ }, style: sliderStyle, "aria-label": "Time-Travel-Cursor" }), _jsx("button", { type: "button", onClick: () => {
280
+ // → Live = „folge wieder Live": Flag zurücksetzen, der
281
+ // Auto-Follow-Effect macht den goTo(liveVersion). Keine
282
+ // doppelte goTo-Quelle.
283
+ setHasUserMoved(false);
284
+ }, disabled: atLive, style: atLive ? ttBtnDisabledStyle : ttBtnStyle, title: "Auto-Follow Live aktivieren", children: "\u2192 Live" })] }), _jsxs("div", { style: ttMetaRowStyle, children: [_jsxs("span", { children: ["Status: ", _jsx("code", { style: inlineCodeStyle, children: view.status }), view.status === 'loading' && ' · liest Stream …', view.status === 'error' && view.lastError && ` · ${view.lastError.message}`, !hasUserMoved && view.status === 'ready' && ' · auto-follow Live'] }), _jsxs("span", { children: ["Vergleich:", _jsx(DiffModeButton, { mode: mode, setMode: setMode, value: "live", label: "vs Live" }), _jsx(DiffModeButton, { mode: mode, setMode: setMode, value: "previous", label: "vs Previous", disabled: view.cursor === 0 })] })] }), _jsx(Section, { title: `Aggregate State @ ${stateLabel}`, children: _jsx("pre", { style: preStyle, children: safeStringify(view.snapshot.state) }) }), _jsx(Section, { title: `ReadModel @ ${stateLabel}`, children: _jsx("pre", { style: preStyle, children: safeStringify(view.snapshot.readModel) }) }), _jsx(DiffPanel, { diff: diff, loading: diffLoading, mode: mode, cursor: view.cursor, atLive: atLive })] }));
285
+ }
286
+ function DiffModeButton({ mode, setMode, value, label, disabled, }) {
287
+ const active = mode === value;
288
+ return (_jsx("button", { type: "button", onClick: () => setMode(value), disabled: disabled, style: active ? ttToggleActiveStyle : disabled ? ttToggleDisabledStyle : ttToggleStyle, children: label }));
289
+ }
290
+ function DiffPanel({ diff, loading, mode, cursor, atLive, }) {
291
+ if (loading && diff.length === 0) {
292
+ return _jsx("div", { style: diffEmptyStyle, children: "diff l\u00E4dt \u2026" });
293
+ }
294
+ if (mode === 'live' && atLive) {
295
+ return _jsx("div", { style: diffEmptyStyle, children: "Cursor ist am Live-Stand \u2014 kein Diff." });
296
+ }
297
+ if (mode === 'previous' && cursor === 0) {
298
+ return _jsx("div", { style: diffEmptyStyle, children: "v0 hat keinen Vorg\u00E4nger." });
299
+ }
300
+ if (diff.length === 0) {
301
+ return _jsx("div", { style: diffEmptyStyle, children: "Kein Unterschied im Aggregate-State." });
302
+ }
303
+ return (_jsx("div", { style: diffListStyle, children: diff.map((entry, i) => (_jsx(DiffRow, { entry: entry }, `${entry.path}-${i}`))) }));
304
+ }
305
+ function DiffRow({ entry }) {
306
+ const kindColor = entry.kind === 'added' ? '#a8d8a8' :
307
+ entry.kind === 'removed' ? '#d97a6b' :
308
+ '#e0c878';
309
+ return (_jsxs("div", { style: diffRowStyle, children: [_jsx("span", { style: { ...diffKindStyle, color: kindColor }, children: entry.kind === 'added' ? '+' : entry.kind === 'removed' ? '−' : '~' }), _jsx("code", { style: diffPathStyle, children: entry.path || '(root)' }), _jsxs("span", { style: diffArrowStyle, children: [_jsx("code", { style: diffValStyle, children: safeStringify(entry.before) }), _jsx("span", { style: { margin: '0 6px', opacity: 0.5 }, children: "\u2192" }), _jsx("code", { style: diffValStyle, children: safeStringify(entry.after) })] })] }));
310
+ }
311
+ // ── Header ────────────────────────────────────────────────────────
312
+ function PanelHeader({ events, streams, stores, syncTransports, replayTargets, filtered, paused, onTogglePause, onClear, onExport, }) {
313
+ return (_jsxs("div", { style: headerStyle, children: [_jsxs("div", { style: headerRowStyle, children: [_jsx("div", { style: titleStyle, children: "Event-Inspector" }), _jsxs("div", { style: statsStyle, children: [_jsx(Stat, { label: "Events", value: filtered === events ? events : `${filtered} / ${events}` }), _jsx(Stat, { label: "Streams", value: streams }), _jsx(Stat, { label: "Stores", value: stores }), syncTransports > 0 && _jsx(Stat, { label: "Sync", value: syncTransports }), replayTargets > 0 && _jsx(Stat, { label: "Replay", value: replayTargets })] }), _jsx("div", { style: { flex: 1 } }), _jsx("button", { onClick: onTogglePause, style: paused ? buttonActiveStyle : buttonStyle, title: paused ? 'Live-Stream wieder aufnehmen' : 'Live-Stream einfrieren', children: paused ? '▶ Resume' : '⏸ Pause' }), _jsx("button", { onClick: onClear, style: buttonStyle, title: "Sichtbare Events ausblenden (folgende bleiben sichtbar)", children: "Clear" }), _jsx("button", { onClick: onExport, style: buttonStyle, title: "Snapshot des aktuellen DevtoolsState als JSON herunterladen (v0.16.6)", "aria-label": "Snapshot als JSON exportieren", children: "\u21E9 Export" })] }), _jsx("div", { style: subtitleStyle, children: "Live-Stream aller Events aus den registrierten EventStores. Klick auf eine Zeile \u00F6ffnet die Drei-Schichten-Sicht." })] }));
314
+ }
315
+ /**
316
+ * Download-Trigger via temporären `<a download>`. Pilot-Schärfungen:
317
+ * - Date.now() läuft beim Button-Klick im UI-Layer, NICHT im Core-
318
+ * Helper — exportSnapshot bleibt deterministisch.
319
+ * - Cleanup im try/finally — wenn `a.click()` synchron wirft (manche
320
+ * Browser-Policies, Test-Mocks), MÜSSEN trotzdem removeChild und
321
+ * revokeObjectURL laufen. Sonst URL-Leak + DOM-Müll.
322
+ */
323
+ function triggerDownload(filename, content) {
324
+ const blob = new Blob([content], { type: 'application/json' });
325
+ const url = URL.createObjectURL(blob);
326
+ const a = document.createElement('a');
327
+ a.href = url;
328
+ a.download = filename;
329
+ document.body.appendChild(a);
330
+ try {
331
+ a.click();
332
+ }
333
+ finally {
334
+ document.body.removeChild(a);
335
+ URL.revokeObjectURL(url);
336
+ }
337
+ }
338
+ function Stat({ label, value }) {
339
+ return (_jsxs("span", { style: statItemStyle, children: [_jsx("span", { style: statValueStyle, children: value }), _jsx("span", { style: statLabelStyle, children: label })] }));
340
+ }
341
+ // ── Filter-Bar ────────────────────────────────────────────────────
342
+ function FilterBar({ stores, storeFilter, onStoreFilter, typeFilter, onTypeFilter, streamFilter, onStreamFilter, }) {
343
+ return (_jsxs("div", { style: filterBarStyle, children: [_jsxs("select", { value: storeFilter, onChange: e => onStoreFilter(e.target.value), style: selectStyle, title: "Auf einen Store filtern", children: [_jsx("option", { value: "", children: "Alle Stores" }), stores.map(s => (_jsx("option", { value: s.id, children: s.name }, s.id)))] }), _jsx("input", { type: "text", placeholder: "Type filter (substring)", value: typeFilter, onChange: e => onTypeFilter(e.target.value), style: inputStyle }), _jsx("input", { type: "text", placeholder: "Stream filter (substring)", value: streamFilter, onChange: e => onStreamFilter(e.target.value), style: inputStyle })] }));
344
+ }
345
+ // ── Events-Body (Tabelle + Detail-Sidebar) ────────────────────────
346
+ function EventsBody({ state, visible, selected, selectedSeq, onSelect, paused, }) {
347
+ if (state.eventStores.length === 0 && state.events.length === 0) {
348
+ return (_jsx("div", { style: emptyStyle, children: "Keine EventStores registriert und kein History-Buffer. Wechsel zur Variant \u201EMinimal\", \u201EMulti-View\", \u2026 \u2014 sobald eine Variant einen observed Store mountet, erscheinen die Events hier." }));
349
+ }
350
+ if (state.events.length === 0) {
351
+ return (_jsxs("div", { style: emptyStyle, children: ["EventStore registriert (", state.eventStores.map(s => s.name).join(', '), "), aber noch keine Events. Wechsel zu einer Calendar-Variant und lege einen Termin an."] }));
352
+ }
353
+ // state.events.length > 0 — auch wenn eventStores leer ist (Variant
354
+ // wurde gewechselt, rolling-window-Buffer überlebt). Tabelle rendern.
355
+ if (visible.length === 0) {
356
+ return (_jsx("div", { style: emptyStyle, children: "Keine Events sichtbar. Filter ggf. zur\u00FCcksetzen oder \u201EClear\" r\u00FCckg\u00E4ngig (Variant neu \u00F6ffnen)." }));
357
+ }
358
+ return (_jsxs("div", { style: twoColStyle, children: [_jsxs("div", { style: tableScrollStyle, children: [paused && _jsx("div", { style: pausedBannerStyle, children: "\u23F8 Live-Stream pausiert \u2014 Ansicht eingefroren." }), _jsxs("table", { style: tableStyle, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: thStyle, children: "Zeit" }), _jsx("th", { style: thStyle, children: "Store" }), _jsx("th", { style: thStyle, children: "Type" }), _jsx("th", { style: thStyle, children: "Label" }), _jsx("th", { style: thStyle, children: "Stream" }), _jsx("th", { style: thStyle, children: "Entity" }), _jsx("th", { style: thNumStyle, children: "v" }), _jsx("th", { style: thNumStyle, children: "sv" }), _jsx("th", { style: thStyle, children: "Event-ID" }), _jsx("th", { style: thStyle, children: "Corr" }), _jsx("th", { style: thStyle, children: "Cau" }), _jsx("th", { style: thNumStyle, children: "tookMs" })] }) }), _jsx("tbody", { children: visible.slice().reverse().map(e => (_jsx(EventRow, { captured: e, isSelected: e.seq === selectedSeq, onSelect: () => onSelect(e.seq === selectedSeq ? null : e.seq) }, e.seq))) })] })] }), _jsx("div", { style: detailColumnStyle, children: selected
359
+ ? _jsx(EventDetail, { captured: selected })
360
+ : _jsx("div", { style: emptyDetailStyle, children: "Klick auf eine Zeile zeigt Envelope, Metadata und Payload." }) })] }));
361
+ }
362
+ function EventRow({ captured, isSelected, onSelect, }) {
363
+ const { event, streamId, storeName, tookMs } = captured;
364
+ const label = typeToLabel(event.type);
365
+ const sev = typeSeverity(event.type);
366
+ const schemaVersion = event.metadata.schemaVersion;
367
+ return (_jsxs("tr", { onClick: onSelect, style: { ...trStyle, ...(isSelected ? trSelectedStyle : null), cursor: 'pointer' }, children: [_jsx("td", { style: tdStyle, children: formatTime(event.envelope.timestamp) }), _jsx("td", { style: tdStoreStyle, children: storeName }), _jsx("td", { style: { ...tdStyle, color: sev === 'destructive' ? '#d97a6b' : '#5da3dc', fontFamily: 'ui-monospace, monospace' }, children: event.type }), _jsx("td", { style: tdStyle, children: label }), _jsx("td", { style: tdMonoStyle, children: streamId }), _jsx("td", { style: tdMonoStyle, children: event.envelope.entityId }), _jsx("td", { style: tdNumStyle, children: event.envelope.version }), _jsx("td", { style: tdNumStyle, children: schemaVersion ?? '—' }), _jsx("td", { style: tdMonoStyle, children: stub(event.envelope.id) }), _jsx("td", { style: tdMonoStyle, children: stub(event.metadata.correlationId) }), _jsx("td", { style: tdMonoStyle, children: stub(event.metadata.causationId) }), _jsx("td", { style: tdNumStyle, children: tookMs.toFixed(2) })] }));
368
+ }
369
+ // ── Helpers ────────────────────────────────────────────────────────
370
+ function formatTime(ms) {
371
+ const d = new Date(ms);
372
+ const hh = String(d.getHours()).padStart(2, '0');
373
+ const mm = String(d.getMinutes()).padStart(2, '0');
374
+ const ss = String(d.getSeconds()).padStart(2, '0');
375
+ return `${hh}:${mm}:${ss}`;
376
+ }
377
+ function typeToLabel(type) {
378
+ const lastSegment = type.split('.').pop() ?? type;
379
+ const spaced = lastSegment.replace(/([a-z])([A-Z])/g, '$1 $2');
380
+ if (spaced.length === 0)
381
+ return type;
382
+ return spaced[0].toUpperCase() + spaced.slice(1).toLowerCase();
383
+ }
384
+ function typeSeverity(type) {
385
+ return /Cancelled|Deleted|Failed|Rejected|Declined/i.test(type) ? 'destructive' : 'normal';
386
+ }
387
+ // ── Styles ─────────────────────────────────────────────────────────
388
+ const pageStyle = {
389
+ display: 'flex',
390
+ flexDirection: 'column',
391
+ height: '100%',
392
+ background: '#1a1a1a',
393
+ color: '#eee',
394
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
395
+ fontSize: 13,
396
+ };
397
+ const headerStyle = {
398
+ padding: '10px 16px 8px',
399
+ background: '#222',
400
+ borderBottom: '1px solid #333',
401
+ };
402
+ const headerRowStyle = {
403
+ display: 'flex',
404
+ alignItems: 'center',
405
+ gap: 12,
406
+ };
407
+ const titleStyle = {
408
+ fontSize: 14,
409
+ fontWeight: 600,
410
+ color: '#fff',
411
+ };
412
+ const subtitleStyle = {
413
+ marginTop: 6,
414
+ fontSize: 11,
415
+ color: '#888',
416
+ };
417
+ const statsStyle = {
418
+ display: 'flex',
419
+ gap: 14,
420
+ marginLeft: 8,
421
+ };
422
+ const statItemStyle = {
423
+ display: 'inline-flex',
424
+ alignItems: 'baseline',
425
+ gap: 4,
426
+ };
427
+ const statValueStyle = {
428
+ fontFamily: 'ui-monospace, "SF Mono", monospace',
429
+ fontSize: 13,
430
+ fontWeight: 600,
431
+ color: '#fff',
432
+ };
433
+ const statLabelStyle = {
434
+ fontSize: 10,
435
+ textTransform: 'uppercase',
436
+ letterSpacing: 0.5,
437
+ color: '#888',
438
+ };
439
+ const tabBarStyle = {
440
+ display: 'flex',
441
+ gap: 0,
442
+ background: '#1e1e1e',
443
+ borderBottom: '1px solid #2a2a2a',
444
+ padding: '0 16px',
445
+ };
446
+ const tabStyle = {
447
+ background: 'transparent',
448
+ border: 'none',
449
+ color: '#888',
450
+ padding: '10px 16px',
451
+ fontSize: 12,
452
+ cursor: 'pointer',
453
+ borderBottom: '2px solid transparent',
454
+ };
455
+ const tabActiveStyle = {
456
+ ...tabStyle,
457
+ color: '#fff',
458
+ borderBottom: '2px solid #5082dc',
459
+ fontWeight: 600,
460
+ };
461
+ const filterBarStyle = {
462
+ display: 'flex',
463
+ gap: 8,
464
+ padding: '8px 16px',
465
+ background: '#1e1e1e',
466
+ borderBottom: '1px solid #2a2a2a',
467
+ };
468
+ const selectStyle = {
469
+ background: '#262626',
470
+ color: '#eee',
471
+ border: '1px solid #3a3a3a',
472
+ borderRadius: 4,
473
+ padding: '4px 8px',
474
+ fontSize: 12,
475
+ minWidth: 140,
476
+ };
477
+ const inputStyle = {
478
+ background: '#262626',
479
+ color: '#eee',
480
+ border: '1px solid #3a3a3a',
481
+ borderRadius: 4,
482
+ padding: '4px 8px',
483
+ fontSize: 12,
484
+ flex: 1,
485
+ minWidth: 0,
486
+ };
487
+ const buttonStyle = {
488
+ background: '#2e2e2e',
489
+ color: '#ddd',
490
+ border: '1px solid #3a3a3a',
491
+ borderRadius: 4,
492
+ padding: '4px 10px',
493
+ fontSize: 12,
494
+ cursor: 'pointer',
495
+ };
496
+ const buttonActiveStyle = {
497
+ ...buttonStyle,
498
+ background: '#3a4a6a',
499
+ borderColor: '#5082dc',
500
+ color: '#fff',
501
+ };
502
+ const pausedBannerStyle = {
503
+ padding: '6px 12px',
504
+ background: '#3a2a1a',
505
+ color: '#ffc88a',
506
+ borderBottom: '1px solid #443322',
507
+ fontSize: 12,
508
+ };
509
+ const tableStyle = {
510
+ width: '100%',
511
+ borderCollapse: 'collapse',
512
+ fontSize: 12,
513
+ };
514
+ const thStyle = {
515
+ textAlign: 'left',
516
+ padding: '8px 10px',
517
+ background: '#1e1e1e',
518
+ borderBottom: '1px solid #333',
519
+ position: 'sticky',
520
+ top: 0,
521
+ fontWeight: 600,
522
+ fontSize: 11,
523
+ textTransform: 'uppercase',
524
+ letterSpacing: 0.4,
525
+ color: '#999',
526
+ };
527
+ const thNumStyle = {
528
+ ...thStyle,
529
+ textAlign: 'right',
530
+ };
531
+ const trStyle = {
532
+ borderBottom: '1px solid #2a2a2a',
533
+ };
534
+ const trSelectedStyle = {
535
+ background: 'rgba(80, 130, 220, 0.22)',
536
+ outline: '1px solid rgba(80, 130, 220, 0.55)',
537
+ outlineOffset: -1,
538
+ };
539
+ const tdStyle = {
540
+ padding: '6px 10px',
541
+ fontSize: 12,
542
+ };
543
+ const tdMonoStyle = {
544
+ ...tdStyle,
545
+ fontFamily: 'ui-monospace, "SF Mono", monospace',
546
+ fontSize: 11,
547
+ color: '#bbb',
548
+ };
549
+ const tdNumStyle = {
550
+ ...tdStyle,
551
+ fontFamily: 'ui-monospace, "SF Mono", monospace',
552
+ fontSize: 11,
553
+ color: '#888',
554
+ textAlign: 'right',
555
+ };
556
+ const tdStoreStyle = {
557
+ ...tdStyle,
558
+ fontSize: 11,
559
+ color: '#9ec8ff',
560
+ };
561
+ // ── Time-Travel ────────────────────────────────────────────────────
562
+ const ttRowStyle = {
563
+ display: 'flex',
564
+ alignItems: 'center',
565
+ gap: 10,
566
+ padding: '8px 0 4px',
567
+ };
568
+ const sliderStyle = {
569
+ flex: 1,
570
+ accentColor: '#7aaef5',
571
+ };
572
+ const ttBtnStyle = {
573
+ background: '#2a2a2a',
574
+ color: '#ddd',
575
+ border: '1px solid #444',
576
+ borderRadius: 4,
577
+ padding: '4px 10px',
578
+ fontSize: 11,
579
+ cursor: 'pointer',
580
+ };
581
+ const ttBtnDisabledStyle = {
582
+ ...ttBtnStyle,
583
+ opacity: 0.4,
584
+ cursor: 'default',
585
+ };
586
+ const ttMetaRowStyle = {
587
+ display: 'flex',
588
+ justifyContent: 'space-between',
589
+ alignItems: 'center',
590
+ fontSize: 11,
591
+ color: '#999',
592
+ padding: '4px 0 8px',
593
+ gap: 10,
594
+ };
595
+ const ttToggleStyle = {
596
+ background: 'transparent',
597
+ color: '#bbb',
598
+ border: '1px solid #333',
599
+ borderRadius: 3,
600
+ padding: '2px 8px',
601
+ marginLeft: 6,
602
+ fontSize: 11,
603
+ cursor: 'pointer',
604
+ };
605
+ const ttToggleActiveStyle = {
606
+ ...ttToggleStyle,
607
+ background: '#3a4f6a',
608
+ borderColor: '#5a7aa6',
609
+ color: '#e8f0ff',
610
+ };
611
+ const ttToggleDisabledStyle = {
612
+ ...ttToggleStyle,
613
+ opacity: 0.4,
614
+ cursor: 'default',
615
+ };
616
+ const diffEmptyStyle = {
617
+ padding: '8px 12px',
618
+ fontSize: 11,
619
+ color: '#888',
620
+ background: '#161616',
621
+ borderRadius: 4,
622
+ fontStyle: 'italic',
623
+ };
624
+ const diffListStyle = {
625
+ display: 'flex',
626
+ flexDirection: 'column',
627
+ gap: 2,
628
+ fontSize: 11,
629
+ background: '#161616',
630
+ borderRadius: 4,
631
+ padding: 6,
632
+ maxHeight: 240,
633
+ overflow: 'auto',
634
+ };
635
+ const diffRowStyle = {
636
+ display: 'grid',
637
+ gridTemplateColumns: '16px minmax(80px, 200px) 1fr',
638
+ alignItems: 'center',
639
+ gap: 6,
640
+ padding: '2px 4px',
641
+ fontFamily: 'ui-monospace, "SF Mono", monospace',
642
+ };
643
+ const diffKindStyle = {
644
+ fontWeight: 700,
645
+ textAlign: 'center',
646
+ };
647
+ const diffPathStyle = {
648
+ fontSize: 11,
649
+ color: '#ddd',
650
+ overflow: 'hidden',
651
+ textOverflow: 'ellipsis',
652
+ whiteSpace: 'nowrap',
653
+ };
654
+ const diffArrowStyle = {
655
+ display: 'flex',
656
+ alignItems: 'center',
657
+ overflow: 'hidden',
658
+ };
659
+ const diffValStyle = {
660
+ fontSize: 11,
661
+ background: '#0f0f0f',
662
+ padding: '1px 5px',
663
+ borderRadius: 3,
664
+ whiteSpace: 'nowrap',
665
+ overflow: 'hidden',
666
+ textOverflow: 'ellipsis',
667
+ maxWidth: '40%',
668
+ };
669
+ //# sourceMappingURL=EventInspectorPanel.js.map