@mugenlabs/logtape-devtools 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -10
- package/dist/{chunk-CYXOBB6A.mjs → chunk-FDWUISCJ.mjs} +80 -27
- package/dist/chunk-FDWUISCJ.mjs.map +1 -0
- package/dist/index.d.ts +8 -17
- package/dist/index.mjs +81 -56
- package/dist/index.mjs.map +1 -1
- package/dist/sink.d.ts +18 -8
- package/dist/sink.mjs +1 -1
- package/package.json +33 -30
- package/dist/chunk-CYXOBB6A.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -90,7 +90,7 @@ The package root pulls in React (the panel is a React component). If your LogTap
|
|
|
90
90
|
import { createDevtoolsSink, createLogStore } from "@mugenlabs/logtape-devtools/sink";
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
-
Exported from `/sink`: `createDevtoolsSink`, `createLogStore`, `defaultLogStore`, `LOG_LEVELS`, and the types `DevtoolsSinkOptions`, `LogStore`, `LogStoreOptions`, `DevtoolsLogRecord`, `LogLevel`.
|
|
93
|
+
Exported from `/sink`: `createDevtoolsSink`, `createLogStore`, `defaultLogStore`, `LOG_LEVELS`, and the types `DevtoolsSink`, `DevtoolsSinkOptions`, `LogStore`, `LogStoreOptions`, `DevtoolsLogRecord`, `LogLevel`.
|
|
94
94
|
|
|
95
95
|
## Production
|
|
96
96
|
|
|
@@ -121,8 +121,11 @@ import { createLogTapeDevtoolsPlugin } from "@mugenlabs/logtape-devtools";
|
|
|
121
121
|
import { TanStackDevtools } from "@tanstack/react-devtools";
|
|
122
122
|
import { logStore } from "./logging";
|
|
123
123
|
|
|
124
|
+
// Create the plugin once, outside the component, so it is not re-created on every render.
|
|
125
|
+
const plugin = createLogTapeDevtoolsPlugin({ store: logStore });
|
|
126
|
+
|
|
124
127
|
export default function Devtools() {
|
|
125
|
-
return <TanStackDevtools plugins={[
|
|
128
|
+
return <TanStackDevtools plugins={[plugin]} />;
|
|
126
129
|
}
|
|
127
130
|
```
|
|
128
131
|
|
|
@@ -131,9 +134,9 @@ With webpack or other bundlers, `process.env.NODE_ENV !== "production"` works th
|
|
|
131
134
|
Two related behaviours make the sink itself cheap to leave in place:
|
|
132
135
|
|
|
133
136
|
- **Stack capture auto-disables in production.** When `process.env.NODE_ENV === "production"`, `captureStackTrace` is ignored (minified bundles produce meaningless file and line references, and browsers do not apply source maps to `Error.stack`). Set `forceStackTrace: true` to override.
|
|
134
|
-
- **Stack capture is skipped while no panel is open.** The sink checks `store.hasListeners()` on every record and does no stack work while nothing is subscribed
|
|
137
|
+
- **Stack capture is skipped while no panel is open.** The sink checks `store.hasListeners()` on every record and does no stack work while nothing is subscribed. Message text is rendered lazily, so an unmounted panel costs one property clone per record.
|
|
135
138
|
|
|
136
|
-
Records are still buffered in memory when no panel is mounted. If you do not want that in production either, keep the sink out of your production LogTape configuration, or pass `createLogStore({ maxRecords: 0 })
|
|
139
|
+
Records are still buffered in memory when no panel is mounted. If you do not want that in production either, keep the sink out of your production LogTape configuration, or pass `createLogStore({ maxRecords: 0 })` — the sink then returns before doing any work at all.
|
|
137
140
|
|
|
138
141
|
## API
|
|
139
142
|
|
|
@@ -156,7 +159,7 @@ const { sink, plugin } = createLogTapeDevtools({
|
|
|
156
159
|
|
|
157
160
|
### `createDevtoolsSink(options?)`
|
|
158
161
|
|
|
159
|
-
Creates a LogTape sink that forwards log records to the devtools store. Returns a LogTape `Sink
|
|
162
|
+
Creates a LogTape sink that forwards log records to the devtools store. Returns a `DevtoolsSink` — a LogTape `Sink` that is also `Disposable`, so LogTape can dispose it when its configuration is reset (a disposed sink stops writing).
|
|
160
163
|
|
|
161
164
|
| Option | Type | Default | Description |
|
|
162
165
|
| --- | --- | --- | --- |
|
|
@@ -166,7 +169,7 @@ Creates a LogTape sink that forwards log records to the devtools store. Returns
|
|
|
166
169
|
|
|
167
170
|
### `createLogTapeDevtoolsPlugin(options?)`
|
|
168
171
|
|
|
169
|
-
Creates a TanStack DevTools plugin
|
|
172
|
+
Creates a TanStack DevTools plugin (`TanStackDevtoolsReactPlugin` from `@tanstack/react-devtools`) to pass to `<TanStackDevtools plugins={[...]} />`. Create it once at module scope rather than inside a component.
|
|
170
173
|
|
|
171
174
|
| Option | Type | Default | Description |
|
|
172
175
|
| --- | --- | --- | --- |
|
|
@@ -189,6 +192,7 @@ The returned `LogStore` exposes:
|
|
|
189
192
|
| `addRecord` | `(record: DevtoolsLogRecord) => void` | Appends a record, evicting the oldest when full |
|
|
190
193
|
| `clear` | `() => void` | Removes every record and notifies subscribers |
|
|
191
194
|
| `getSnapshot` | `() => DevtoolsLogRecord[]` | Current records, oldest first. Reference is stable between mutations (safe for `useSyncExternalStore`) |
|
|
195
|
+
| `getMaxRecords` | `() => number` | Current retention limit. Optional for custom stores; when it returns `0` the sink skips all work |
|
|
192
196
|
| `hasListeners` | `() => boolean` | Whether anything is currently subscribed |
|
|
193
197
|
| `setMaxSize` | `(size: number) => void` | Updates the retention limit, trimming if needed |
|
|
194
198
|
| `subscribe` | `(listener: () => void) => () => void` | Registers a change listener; returns an unsubscribe function |
|
|
@@ -210,7 +214,7 @@ The shared `LogStore` instance (created with the default `maxRecords: 1000`) use
|
|
|
210
214
|
| `level` | `LogLevel` | Severity of the record |
|
|
211
215
|
| `category` | `string[]` | Logger category path, e.g. `["app", "auth"]` |
|
|
212
216
|
| `message` | `unknown[]` | Raw message parts, alternating literals and interpolated values |
|
|
213
|
-
| `messageText` | `string` | Message parts rendered into one searchable string |
|
|
217
|
+
| `messageText` | `string` | Message parts rendered into one searchable string (computed lazily on first access) |
|
|
214
218
|
| `properties` | `Record<string, unknown>` | Structured properties, deep-cloned for safety |
|
|
215
219
|
| `caller` | `string \| undefined` | Source location, present only when stack capture ran |
|
|
216
220
|
|
|
@@ -240,12 +244,12 @@ The shared `LogStore` instance (created with the default `maxRecords: 1000`) use
|
|
|
240
244
|
|
|
241
245
|
| Peer dependency | Range | Notes |
|
|
242
246
|
| --- | --- | --- |
|
|
243
|
-
| `@logtape/logtape` |
|
|
247
|
+
| `@logtape/logtape` | `^2.0.0` | **Optional** — only types are imported; you need it to configure LogTape anyway |
|
|
244
248
|
| `react` | `^18.0.0 \|\| ^19.0.0` | Required for the panel |
|
|
245
249
|
| `react-dom` | `^18.0.0 \|\| ^19.0.0` | Required for the panel |
|
|
246
|
-
| `@tanstack/react-devtools` |
|
|
250
|
+
| `@tanstack/react-devtools` | `^0.9.0` | **Optional** — only needed to host the panel. Bound to 0.9.x because 0.10 changed how props are passed into plugins |
|
|
247
251
|
|
|
248
|
-
Node `>=
|
|
252
|
+
Node `>=24.0.0` (Active LTS). The package is published as ESM only (`import` condition, no `require()`). `@tanstack/react-devtools` being optional means you can depend on this package purely for the sink (via `@mugenlabs/logtape-devtools/sink`) without installing the DevTools shell.
|
|
249
253
|
|
|
250
254
|
## Demo
|
|
251
255
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// src/store.ts
|
|
2
2
|
var DEFAULT_MAX_RECORDS = 1e3;
|
|
3
|
+
function normalizeMaxRecords(value) {
|
|
4
|
+
const numeric = Math.floor(Number(value ?? DEFAULT_MAX_RECORDS));
|
|
5
|
+
return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
|
|
6
|
+
}
|
|
3
7
|
function schedule(callback) {
|
|
4
8
|
if (typeof queueMicrotask === "function") {
|
|
5
9
|
queueMicrotask(callback);
|
|
@@ -8,7 +12,7 @@ function schedule(callback) {
|
|
|
8
12
|
Promise.resolve().then(callback);
|
|
9
13
|
}
|
|
10
14
|
function createLogStore(options) {
|
|
11
|
-
let max =
|
|
15
|
+
let max = normalizeMaxRecords(options?.maxRecords);
|
|
12
16
|
let buffer = [];
|
|
13
17
|
let start = 0;
|
|
14
18
|
let size = 0;
|
|
@@ -33,7 +37,7 @@ function createLogStore(options) {
|
|
|
33
37
|
}
|
|
34
38
|
function toArray() {
|
|
35
39
|
const result = new Array(size);
|
|
36
|
-
for (let i = 0; i < size; i
|
|
40
|
+
for (let i = 0; i < size; i += 1) {
|
|
37
41
|
result[i] = buffer[(start + i) % buffer.length];
|
|
38
42
|
}
|
|
39
43
|
return result;
|
|
@@ -53,7 +57,7 @@ function createLogStore(options) {
|
|
|
53
57
|
}
|
|
54
58
|
if (size < max) {
|
|
55
59
|
buffer.push(record);
|
|
56
|
-
size
|
|
60
|
+
size += 1;
|
|
57
61
|
} else {
|
|
58
62
|
buffer[start] = record;
|
|
59
63
|
start = (start + 1) % buffer.length;
|
|
@@ -68,6 +72,7 @@ function createLogStore(options) {
|
|
|
68
72
|
snapshotDirty = true;
|
|
69
73
|
notify();
|
|
70
74
|
},
|
|
75
|
+
getMaxRecords: () => max,
|
|
71
76
|
getSnapshot: () => {
|
|
72
77
|
if (snapshotDirty) {
|
|
73
78
|
snapshot = toArray();
|
|
@@ -77,7 +82,7 @@ function createLogStore(options) {
|
|
|
77
82
|
},
|
|
78
83
|
hasListeners: () => listeners.size > 0,
|
|
79
84
|
setMaxSize: (nextMax) => {
|
|
80
|
-
max =
|
|
85
|
+
max = normalizeMaxRecords(nextMax);
|
|
81
86
|
if (normalize()) {
|
|
82
87
|
snapshotDirty = true;
|
|
83
88
|
notify();
|
|
@@ -97,28 +102,57 @@ var defaultLogStore = createLogStore();
|
|
|
97
102
|
var LOG_LEVELS = ["trace", "debug", "info", "warning", "error", "fatal"];
|
|
98
103
|
|
|
99
104
|
// src/safe-json.ts
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
function toSerializable(value) {
|
|
106
|
+
switch (typeof value) {
|
|
107
|
+
case "bigint":
|
|
108
|
+
return `${value}n`;
|
|
109
|
+
case "symbol":
|
|
110
|
+
return value.toString();
|
|
111
|
+
case "function":
|
|
112
|
+
return `[Function ${value.name || "anonymous"}]`;
|
|
113
|
+
default:
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (value instanceof Error) {
|
|
117
|
+
const error = { message: value.message, name: value.name };
|
|
118
|
+
if (value.stack) {
|
|
119
|
+
error.stack = value.stack;
|
|
120
|
+
}
|
|
121
|
+
if ("cause" in value && value.cause !== void 0) {
|
|
122
|
+
error.cause = value.cause;
|
|
123
|
+
}
|
|
124
|
+
return error;
|
|
125
|
+
}
|
|
126
|
+
if (value instanceof Map) {
|
|
127
|
+
return [...value.entries()];
|
|
128
|
+
}
|
|
129
|
+
if (value instanceof Set) {
|
|
130
|
+
return [...value];
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
function createReplacer() {
|
|
135
|
+
const ancestors = [];
|
|
136
|
+
return function replace(_key, value) {
|
|
137
|
+
while (ancestors.length > 0 && ancestors.at(-1)?.serialized !== this) {
|
|
138
|
+
ancestors.pop();
|
|
139
|
+
}
|
|
103
140
|
if (typeof value === "object" && value !== null) {
|
|
104
|
-
if (
|
|
141
|
+
if (ancestors.some((ancestor) => ancestor.original === value)) {
|
|
105
142
|
return "[Circular]";
|
|
106
143
|
}
|
|
107
|
-
|
|
144
|
+
const serialized = toSerializable(value);
|
|
145
|
+
if (typeof serialized === "object" && serialized !== null) {
|
|
146
|
+
ancestors.push({ original: value, serialized });
|
|
147
|
+
}
|
|
148
|
+
return serialized;
|
|
108
149
|
}
|
|
109
|
-
return value;
|
|
150
|
+
return toSerializable(value);
|
|
110
151
|
};
|
|
111
152
|
}
|
|
112
153
|
function safeStringify(value, indent) {
|
|
113
154
|
try {
|
|
114
|
-
const json = JSON.stringify(value,
|
|
115
|
-
if (json !== void 0) {
|
|
116
|
-
return json;
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const json = JSON.stringify(value, circularSafeReplacer(), indent);
|
|
155
|
+
const json = JSON.stringify(value, createReplacer(), indent);
|
|
122
156
|
if (json !== void 0) {
|
|
123
157
|
return json;
|
|
124
158
|
}
|
|
@@ -166,7 +200,7 @@ function renderMessage(message) {
|
|
|
166
200
|
var v8FrameRe = /at\s+(?:.*?\s+\()?(.+?):(\d+):(\d+)\)?$/;
|
|
167
201
|
var smFrameRe = /(?:^|@)(.+?):(\d+):(\d+)$/;
|
|
168
202
|
function captureCallerInfo() {
|
|
169
|
-
const stack = new Error()
|
|
203
|
+
const { stack } = new Error();
|
|
170
204
|
if (!stack) {
|
|
171
205
|
return void 0;
|
|
172
206
|
}
|
|
@@ -190,15 +224,24 @@ function captureCallerInfo() {
|
|
|
190
224
|
return void 0;
|
|
191
225
|
}
|
|
192
226
|
function normalizeRecord(record, captureStack, id) {
|
|
227
|
+
const message = [...record.message];
|
|
193
228
|
const normalized = {
|
|
229
|
+
category: [...record.category],
|
|
194
230
|
id,
|
|
195
|
-
timestamp: record.timestamp,
|
|
196
231
|
level: record.level,
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
properties: safeCloneProperties(record.properties)
|
|
232
|
+
message,
|
|
233
|
+
properties: safeCloneProperties(record.properties),
|
|
234
|
+
timestamp: record.timestamp
|
|
201
235
|
};
|
|
236
|
+
let messageText;
|
|
237
|
+
Object.defineProperty(normalized, "messageText", {
|
|
238
|
+
configurable: true,
|
|
239
|
+
enumerable: true,
|
|
240
|
+
get: () => {
|
|
241
|
+
messageText ??= renderMessage(message);
|
|
242
|
+
return messageText;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
202
245
|
if (captureStack) {
|
|
203
246
|
const caller = captureCallerInfo();
|
|
204
247
|
if (caller) {
|
|
@@ -223,18 +266,28 @@ function createDevtoolsSink(options) {
|
|
|
223
266
|
const captureStack = wantsStack && (forceStack || !isProductionEnv());
|
|
224
267
|
const sinkId = Math.random().toString(36).slice(2, 8);
|
|
225
268
|
let counter = 0;
|
|
226
|
-
|
|
269
|
+
let disposed = false;
|
|
270
|
+
const sink = (record) => {
|
|
271
|
+
if (disposed || store.getMaxRecords?.() === 0) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
227
274
|
try {
|
|
228
275
|
const shouldCaptureStack = captureStack && (forceStack || store.hasListeners());
|
|
276
|
+
counter += 1;
|
|
229
277
|
const normalized = normalizeRecord(
|
|
230
278
|
record,
|
|
231
279
|
shouldCaptureStack,
|
|
232
|
-
`log-${sinkId}-${
|
|
280
|
+
`log-${sinkId}-${counter}-${record.timestamp}`
|
|
233
281
|
);
|
|
234
282
|
store.addRecord(normalized);
|
|
235
283
|
} catch {
|
|
236
284
|
}
|
|
237
285
|
};
|
|
286
|
+
return Object.assign(sink, {
|
|
287
|
+
[Symbol.dispose]: () => {
|
|
288
|
+
disposed = true;
|
|
289
|
+
}
|
|
290
|
+
});
|
|
238
291
|
}
|
|
239
292
|
|
|
240
293
|
export {
|
|
@@ -244,4 +297,4 @@ export {
|
|
|
244
297
|
LOG_LEVELS,
|
|
245
298
|
createDevtoolsSink
|
|
246
299
|
};
|
|
247
|
-
//# sourceMappingURL=chunk-
|
|
300
|
+
//# sourceMappingURL=chunk-FDWUISCJ.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store.ts","../src/types.ts","../src/safe-json.ts","../src/sink.ts"],"sourcesContent":["import type { DevtoolsLogRecord } from \"./types\";\n\ntype Listener = () => void;\n\n/**\n * Reactive log record container backing the devtools panel.\n *\n * Records are kept in a fixed-capacity ring buffer so appending is O(1); the\n * array returned by {@link LogStore.getSnapshot} is rebuilt lazily at most once\n * per mutation batch. Listener notifications are coalesced into a microtask, so\n * a synchronous burst of {@link LogStore.addRecord} calls results in a single\n * notification.\n */\nexport interface LogStore {\n /** Appends a record, evicting the oldest one when the max size is reached. */\n addRecord: (record: DevtoolsLogRecord) => void;\n /** Removes every record and notifies subscribers. */\n clear: () => void;\n /**\n * The current retention limit. `0` means the store discards everything, which\n * lets the sink skip normalisation entirely. Optional for custom stores.\n */\n getMaxRecords?: () => number;\n /**\n * Returns the current records, oldest first.\n *\n * The reference is stable until the store changes, which makes it safe to use\n * with `useSyncExternalStore`.\n */\n getSnapshot: () => DevtoolsLogRecord[];\n /**\n * Whether anything is currently subscribed to the store.\n *\n * Used by the sink to skip expensive work (such as stack trace capture) while\n * no devtools panel is mounted.\n */\n hasListeners: () => boolean;\n /** Updates the maximum number of retained records, trimming if needed. */\n setMaxSize: (size: number) => void;\n /** Registers a change listener. Returns an unsubscribe function. */\n subscribe: (listener: Listener) => () => void;\n}\n\n/** Options for {@link createLogStore}. */\nexport interface LogStoreOptions {\n /**\n * Maximum number of log records to retain. Older records are evicted first.\n *\n * @default 1000\n */\n maxRecords?: number;\n}\n\nconst DEFAULT_MAX_RECORDS = 1000;\n\n/**\n * Coerce a user-supplied capacity into a non-negative integer. Fractions would\n * defeat the `size < max` capacity check and NaN would wedge the ring buffer.\n */\nfunction normalizeMaxRecords(value: number | undefined): number {\n const numeric = Math.floor(Number(value ?? DEFAULT_MAX_RECORDS));\n return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;\n}\n\nfunction schedule(callback: () => void): void {\n if (typeof queueMicrotask === \"function\") {\n queueMicrotask(callback);\n return;\n }\n Promise.resolve().then(callback);\n}\n\n/**\n * Creates an isolated log store.\n *\n * Pass the same instance to `createDevtoolsSink` and\n * `createLogTapeDevtoolsPlugin` to keep the sink and the panel in sync.\n *\n * ```ts\n * const store = createLogStore({ maxRecords: 5000 });\n * ```\n */\nexport function createLogStore(options?: LogStoreOptions): LogStore {\n let max = normalizeMaxRecords(options?.maxRecords);\n\n // Ring buffer: `start` is the index of the oldest record, `size` the number\n // of live entries. Appending is O(1) — no per-record array copy.\n let buffer: DevtoolsLogRecord[] = [];\n let start = 0;\n let size = 0;\n\n let snapshot: DevtoolsLogRecord[] = [];\n let snapshotDirty = false;\n\n const listeners = new Set<Listener>();\n let notifyScheduled = false;\n\n function emit() {\n for (const listener of listeners) {\n listener();\n }\n }\n\n function notify() {\n if (notifyScheduled) {\n return;\n }\n notifyScheduled = true;\n schedule(() => {\n notifyScheduled = false;\n emit();\n });\n }\n\n /** Materializes the ring buffer into a plain, oldest-first array. */\n function toArray(): DevtoolsLogRecord[] {\n const result: DevtoolsLogRecord[] = new Array(size);\n for (let i = 0; i < size; i += 1) {\n result[i] = buffer[(start + i) % buffer.length];\n }\n return result;\n }\n\n /**\n * Rewrites the buffer in logical order, dropping records beyond `max`.\n * Keeps the invariant that a below-capacity buffer is linear (`start === 0`).\n */\n function normalize(): boolean {\n const trimmed = size > max;\n const kept = trimmed ? toArray().slice(size - max) : toArray();\n buffer = kept;\n start = 0;\n size = kept.length;\n return trimmed;\n }\n\n return {\n addRecord: (record) => {\n if (max === 0) {\n return;\n }\n if (size < max) {\n // Below capacity — the buffer is linear, so a plain append is enough.\n buffer.push(record);\n size += 1;\n } else {\n // At capacity — overwrite the oldest slot.\n buffer[start] = record;\n start = (start + 1) % buffer.length;\n }\n snapshotDirty = true;\n notify();\n },\n clear: () => {\n buffer = [];\n start = 0;\n size = 0;\n snapshotDirty = true;\n notify();\n },\n getMaxRecords: () => max,\n getSnapshot: () => {\n if (snapshotDirty) {\n snapshot = toArray();\n snapshotDirty = false;\n }\n return snapshot;\n },\n hasListeners: () => listeners.size > 0,\n setMaxSize: (nextMax) => {\n max = normalizeMaxRecords(nextMax);\n if (normalize()) {\n snapshotDirty = true;\n notify();\n }\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** Default shared store instance used when no custom store is provided */\nexport const defaultLogStore = createLogStore();\n","/** Severity levels supported by LogTape, ordered from least to most severe. */\nexport type LogLevel = \"trace\" | \"debug\" | \"info\" | \"warning\" | \"error\" | \"fatal\";\n\n/** Every {@link LogLevel}, ordered from least to most severe. */\nexport const LOG_LEVELS: LogLevel[] = [\"trace\", \"debug\", \"info\", \"warning\", \"error\", \"fatal\"];\n\n/** A LogTape record normalized for display in the devtools panel. */\nexport interface DevtoolsLogRecord {\n /** Source location of the log call (only present when `captureStackTrace` is enabled). */\n caller?: string;\n /** Logger category path, e.g. `[\"app\", \"auth\"]`. */\n category: string[];\n /** Unique identifier, stable for the lifetime of the record. */\n id: string;\n /** Severity of the record. */\n level: LogLevel;\n /** Raw message parts, alternating literals and interpolated values. */\n message: unknown[];\n /** Message parts rendered into a single searchable string. */\n messageText: string;\n /** Structured properties attached to the log call, deep-cloned for safety. */\n properties: Record<string, unknown>;\n /** Creation time in milliseconds since the Unix epoch. */\n timestamp: number;\n}\n","/**\n * JSON helpers shared by the sink and the panel.\n *\n * This module is intentionally React-free so that it can be pulled into the\n * `@mugenlabs/logtape-devtools/sink` entry point.\n */\n\n/**\n * Map values JSON cannot represent (or would silently flatten to `{}`) onto a\n * readable equivalent. Anything else is returned untouched.\n */\nfunction toSerializable(value: unknown): unknown {\n switch (typeof value) {\n case \"bigint\":\n return `${value}n`;\n case \"symbol\":\n return value.toString();\n case \"function\":\n return `[Function ${value.name || \"anonymous\"}]`;\n default:\n break;\n }\n if (value instanceof Error) {\n const error: Record<string, unknown> = { message: value.message, name: value.name };\n if (value.stack) {\n error.stack = value.stack;\n }\n if (\"cause\" in value && value.cause !== undefined) {\n error.cause = value.cause;\n }\n return error;\n }\n if (value instanceof Map) {\n return [...value.entries()];\n }\n if (value instanceof Set) {\n return [...value];\n }\n return value;\n}\n\ninterface Ancestor {\n /** The value as it appeared in the source graph — used for cycle detection. */\n original: object;\n /** What JSON.stringify actually recursed into — used to unwind the stack. */\n serialized: object;\n}\n\n/**\n * Replacer that marks true cycles as `\"[Circular]\"` while leaving shared\n * (non-circular) references intact, and converts unrepresentable values via\n * {@link toSerializable}.\n */\nfunction createReplacer(): (this: unknown, key: string, value: unknown) => unknown {\n const ancestors: Ancestor[] = [];\n return function replace(this: unknown, _key, value) {\n // `this` is the object holding `key`; unwind to the current branch.\n while (ancestors.length > 0 && ancestors.at(-1)?.serialized !== this) {\n ancestors.pop();\n }\n if (typeof value === \"object\" && value !== null) {\n if (ancestors.some((ancestor) => ancestor.original === value)) {\n return \"[Circular]\";\n }\n const serialized = toSerializable(value);\n if (typeof serialized === \"object\" && serialized !== null) {\n ancestors.push({ original: value, serialized });\n }\n return serialized;\n }\n return toSerializable(value);\n };\n}\n\n/**\n * Serializes any value to JSON, never throwing.\n *\n * Errors, Maps, Sets, BigInts, symbols and functions are rendered in a readable\n * form instead of being dropped, circular references become `\"[Circular]\"`,\n * and anything still unserializable falls back to `String(value)`.\n */\nexport function safeStringify(value: unknown, indent?: number): string {\n try {\n const json = JSON.stringify(value, createReplacer(), indent);\n if (json !== undefined) {\n return json;\n }\n } catch {\n // A throwing `toJSON` or getter — fall through.\n }\n return String(value);\n}\n\nfunction jsonClone(value: unknown): unknown {\n return JSON.parse(safeStringify(value));\n}\n\n/**\n * Deep-clones a properties bag, never throwing.\n *\n * Uses `structuredClone` when available and degrades per entry (via a JSON\n * round trip, then `String(value)`) so that a single unclonable value cannot\n * drop the whole object.\n */\nexport function safeCloneProperties(properties: Record<string, unknown>): Record<string, unknown> {\n if (typeof structuredClone === \"function\") {\n try {\n return structuredClone(properties);\n } catch {\n // Functions, DOM nodes, proxies… fall through to the JSON path.\n }\n }\n\n try {\n return jsonClone(properties) as Record<string, unknown>;\n } catch {\n // Fall through to the per-entry path.\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(properties)) {\n try {\n result[key] = jsonClone(entry);\n } catch {\n result[key] = String(entry);\n }\n }\n return result;\n}\n","import type { LogRecord, Sink } from \"@logtape/logtape\";\nimport { safeCloneProperties, safeStringify } from \"./safe-json\";\nimport { defaultLogStore, type LogStore } from \"./store\";\nimport type { DevtoolsLogRecord, LogLevel } from \"./types\";\n\nfunction renderMessage(message: readonly unknown[]): string {\n return message\n .map((part) => {\n if (typeof part === \"string\") {\n return part;\n }\n if (part === null || part === undefined) {\n return String(part);\n }\n return safeStringify(part);\n })\n .join(\"\");\n}\n\n// ---------------------------------------------------------------------------\n// Stack trace parser — extracts the caller frame from `new Error().stack`\n// ---------------------------------------------------------------------------\n\n// V8 (Chrome/Node/Edge): \" at functionName (file:line:col)\"\nconst v8FrameRe = /at\\s+(?:.*?\\s+\\()?(.+?):(\\d+):(\\d+)\\)?$/;\n// SpiderMonkey (Firefox) / JSC (Safari): \"functionName@file:line:col\"\nconst smFrameRe = /(?:^|@)(.+?):(\\d+):(\\d+)$/;\n\nfunction captureCallerInfo(): string | undefined {\n // biome-ignore lint/suspicious/useErrorMessage: we only need the stack trace, not the message\n const { stack } = new Error();\n if (!stack) {\n return undefined;\n }\n\n const lines = stack.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and the \"Error\" header\n if (!trimmed || trimmed === \"Error\") {\n continue;\n }\n\n // Skip frames from logtape internals (both @logtape/logtape and @mugenlabs/logtape-devtools)\n if (/logtape/i.test(trimmed)) {\n continue;\n }\n\n // Try V8 format first, then SpiderMonkey/JSC\n const match = v8FrameRe.exec(trimmed) ?? smFrameRe.exec(trimmed);\n if (!match) {\n continue;\n }\n\n const [, filePath, lineNo, colNo] = match;\n\n // Extract just the filename from the full path/URL\n const fileName =\n filePath\n .replace(/[?#].*$/, \"\")\n .split(\"/\")\n .pop() ?? filePath;\n\n return `${fileName}:${lineNo}:${colNo}`;\n }\n\n return undefined;\n}\n\nfunction normalizeRecord(record: LogRecord, captureStack: boolean, id: string): DevtoolsLogRecord {\n const message = [...record.message];\n const normalized = {\n category: [...record.category],\n id,\n level: record.level as LogLevel,\n message,\n properties: safeCloneProperties(record.properties),\n timestamp: record.timestamp,\n } as DevtoolsLogRecord;\n\n // Rendering the message serialises every interpolated value, so defer it to\n // the first read (search or display) instead of paying for it on every log.\n let messageText: string | undefined;\n Object.defineProperty(normalized, \"messageText\", {\n configurable: true,\n enumerable: true,\n get: () => {\n messageText ??= renderMessage(message);\n return messageText;\n },\n });\n\n if (captureStack) {\n const caller = captureCallerInfo();\n if (caller) {\n normalized.caller = caller;\n }\n }\n\n return normalized;\n}\n\nfunction isProductionEnv(): boolean {\n try {\n // Bundlers (Vite, webpack, etc.) statically replace this at build time\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") {\n return true;\n }\n } catch {\n // process may not exist in browser environments\n }\n return false;\n}\n\n/** Options for {@link createDevtoolsSink}. */\nexport interface DevtoolsSinkOptions {\n /**\n * Capture the source location of each log call via stack trace parsing.\n *\n * Relies on `new Error().stack`, which is engine-dependent. Automatically\n * disabled in production builds (when `process.env.NODE_ENV === \"production\"`)\n * because minified bundles produce meaningless file/line references. Note that\n * source maps do not help here — browsers do not apply source maps to\n * `Error.stack`.\n *\n * Also skipped while no devtools panel is mounted (the store has no\n * subscribers), unless `forceStackTrace` is `true`.\n *\n * @experimental\n * @default true\n */\n captureStackTrace?: boolean;\n /**\n * Force stack trace capture even in production builds and while the panel is\n * closed. Only has an effect when `captureStackTrace` is `true`.\n *\n * @default false\n */\n forceStackTrace?: boolean;\n /** Custom log store instance. Defaults to the shared store. */\n store?: LogStore;\n}\n\n/**\n * A LogTape sink that is also disposable. LogTape disposes sinks when the\n * configuration is reset; a disposed sink stops writing to its store.\n */\nexport type DevtoolsSink = Sink & Disposable;\n\n/**\n * Creates a LogTape sink that forwards log records to the devtools panel.\n *\n * Usage:\n * ```ts\n * import { configure } from \"@logtape/logtape\";\n * import { createDevtoolsSink } from \"@mugenlabs/logtape-devtools\";\n *\n * await configure({\n * sinks: { devtools: createDevtoolsSink() },\n * loggers: [\n * { category: [\"app\"], lowestLevel: \"debug\", sinks: [\"devtools\"] },\n * ],\n * });\n * ```\n */\nexport function createDevtoolsSink(options?: DevtoolsSinkOptions): DevtoolsSink {\n const store = options?.store ?? defaultLogStore;\n const wantsStack = options?.captureStackTrace ?? true;\n const forceStack = options?.forceStackTrace ?? false;\n const captureStack = wantsStack && (forceStack || !isProductionEnv());\n\n // Per-sink counter, plus a random discriminator so that two sinks sharing a\n // single store cannot emit the same id for records with equal timestamps.\n const sinkId = Math.random().toString(36).slice(2, 8);\n let counter = 0;\n let disposed = false;\n\n const sink = (record: LogRecord): void => {\n // A store with no capacity drops every record, so skip normalisation too.\n if (disposed || store.getMaxRecords?.() === 0) {\n return;\n }\n try {\n // Stack capture is the most expensive part of normalization — skip it\n // while nothing is watching the store (i.e. no panel is mounted).\n const shouldCaptureStack = captureStack && (forceStack || store.hasListeners());\n counter += 1;\n const normalized = normalizeRecord(\n record,\n shouldCaptureStack,\n `log-${sinkId}-${counter}-${record.timestamp}`\n );\n store.addRecord(normalized);\n } catch {\n // Fail silently — devtools should never break the application\n }\n };\n\n return Object.assign(sink, {\n [Symbol.dispose]: () => {\n disposed = true;\n },\n });\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
3
|
-
import { LogStore, DevtoolsSinkOptions } from './sink.js';
|
|
1
|
+
import { LogStore, DevtoolsSinkOptions, DevtoolsSink } from './sink.js';
|
|
4
2
|
export { DevtoolsLogRecord, LOG_LEVELS, LogLevel, LogStoreOptions, createDevtoolsSink, createLogStore, defaultLogStore } from './sink.js';
|
|
3
|
+
import * as _tanstack_react_devtools from '@tanstack/react-devtools';
|
|
4
|
+
import { TanStackDevtoolsReactPlugin } from '@tanstack/react-devtools';
|
|
5
|
+
import '@logtape/logtape';
|
|
5
6
|
|
|
6
7
|
interface LogTapeDevtoolsPluginOptions {
|
|
7
8
|
/** Whether to open the devtools panel by default. Default: true */
|
|
@@ -22,12 +23,7 @@ interface LogTapeDevtoolsPluginOptions {
|
|
|
22
23
|
* <TanStackDevtools plugins={[createLogTapeDevtoolsPlugin()]} />
|
|
23
24
|
* ```
|
|
24
25
|
*/
|
|
25
|
-
declare const createLogTapeDevtoolsPlugin: (options?: LogTapeDevtoolsPluginOptions) =>
|
|
26
|
-
defaultOpen: boolean;
|
|
27
|
-
id: string;
|
|
28
|
-
name: string;
|
|
29
|
-
render: react_jsx_runtime.JSX.Element;
|
|
30
|
-
};
|
|
26
|
+
declare const createLogTapeDevtoolsPlugin: (options?: LogTapeDevtoolsPluginOptions) => TanStackDevtoolsReactPlugin;
|
|
31
27
|
|
|
32
28
|
/** Options for {@link createLogTapeDevtools}. */
|
|
33
29
|
interface CreateLogTapeDevtoolsOptions {
|
|
@@ -64,13 +60,8 @@ interface CreateLogTapeDevtoolsOptions {
|
|
|
64
60
|
* ```
|
|
65
61
|
*/
|
|
66
62
|
declare function createLogTapeDevtools(options?: CreateLogTapeDevtoolsOptions): {
|
|
67
|
-
plugin:
|
|
68
|
-
|
|
69
|
-
id: string;
|
|
70
|
-
name: string;
|
|
71
|
-
render: react_jsx_runtime.JSX.Element;
|
|
72
|
-
};
|
|
73
|
-
sink: _logtape_logtape.Sink;
|
|
63
|
+
plugin: _tanstack_react_devtools.TanStackDevtoolsReactPlugin;
|
|
64
|
+
sink: DevtoolsSink;
|
|
74
65
|
};
|
|
75
66
|
|
|
76
|
-
export { type CreateLogTapeDevtoolsOptions, DevtoolsSinkOptions, LogStore, type LogTapeDevtoolsPluginOptions, createLogTapeDevtools, createLogTapeDevtoolsPlugin };
|
|
67
|
+
export { type CreateLogTapeDevtoolsOptions, DevtoolsSink, DevtoolsSinkOptions, LogStore, type LogTapeDevtoolsPluginOptions, createLogTapeDevtools, createLogTapeDevtoolsPlugin };
|
package/dist/index.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
createLogStore,
|
|
5
5
|
defaultLogStore,
|
|
6
6
|
safeStringify
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FDWUISCJ.mjs";
|
|
8
8
|
|
|
9
9
|
// src/plugin/logtape-devtools-plugin.tsx
|
|
10
10
|
import { useCallback as useCallback4, useMemo as useMemo2, useState as useState2, useSyncExternalStore } from "react";
|
|
@@ -16,75 +16,78 @@ import { useCallback as useCallback2, useEffect, useRef } from "react";
|
|
|
16
16
|
// src/plugin/theme.ts
|
|
17
17
|
var theme = {
|
|
18
18
|
colors: {
|
|
19
|
+
accent: "#6366f1",
|
|
20
|
+
accentHover: "#818cf8",
|
|
19
21
|
background: "#1e1e1e",
|
|
20
|
-
surface: "#1a1a1a",
|
|
21
|
-
surfaceHover: "#2a2a2a",
|
|
22
|
-
surfaceSelected: "#2a2a3a",
|
|
23
22
|
border: "#333",
|
|
24
|
-
borderInput: "#444",
|
|
25
23
|
borderFocus: "#6366f1",
|
|
26
|
-
|
|
27
|
-
textSecondary: "#888",
|
|
28
|
-
textMuted: "#666",
|
|
29
|
-
textDimmed: "#555",
|
|
30
|
-
white: "#fff",
|
|
31
|
-
accent: "#6366f1",
|
|
32
|
-
accentHover: "#818cf8",
|
|
24
|
+
borderInput: "#444",
|
|
33
25
|
levels: {
|
|
34
|
-
|
|
35
|
-
debug: {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
// Badge colours keep at least 4.5:1 contrast against white badge text.
|
|
27
|
+
debug: { badge: "#2f7a2f", bg: "#1a2a1a", color: "#88bb88" },
|
|
28
|
+
error: { badge: "#dc2626", bg: "#2a1a1a", color: "#ef4444" },
|
|
29
|
+
fatal: { badge: "#b91c1c", bg: "#3a0a0a", color: "#ff6b6b" },
|
|
30
|
+
info: { badge: "#2563eb", bg: "#1a2a3a", color: "#60a5fa" },
|
|
31
|
+
trace: { badge: "#5555aa", bg: "#1a1a2e", color: "#8888bb" },
|
|
32
|
+
warning: { badge: "#eab308", bg: "#2a2a1a", color: "#facc15" }
|
|
40
33
|
},
|
|
41
34
|
scrollbar: "#444",
|
|
42
|
-
scrollbarHover: "#555"
|
|
35
|
+
scrollbarHover: "#555",
|
|
36
|
+
surface: "#1a1a1a",
|
|
37
|
+
surfaceHover: "#2a2a2a",
|
|
38
|
+
surfaceSelected: "#2a2a3a",
|
|
39
|
+
textDimmed: "#555",
|
|
40
|
+
textMuted: "#666",
|
|
41
|
+
textPrimary: "#e0e0e0",
|
|
42
|
+
textSecondary: "#888",
|
|
43
|
+
white: "#fff"
|
|
43
44
|
},
|
|
44
45
|
fontFamily: {
|
|
45
46
|
mono: 'ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, monospace',
|
|
46
47
|
sans: 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'
|
|
47
48
|
},
|
|
48
49
|
fontSize: {
|
|
49
|
-
xs: "9px",
|
|
50
|
-
sm: "10px",
|
|
51
|
-
md: "11px",
|
|
52
50
|
base: "12px",
|
|
53
|
-
lg: "13px"
|
|
51
|
+
lg: "13px",
|
|
52
|
+
md: "11px",
|
|
53
|
+
sm: "10px",
|
|
54
|
+
xs: "9px"
|
|
54
55
|
},
|
|
55
56
|
radius: {
|
|
56
|
-
sm: "3px",
|
|
57
|
-
md: "4px",
|
|
58
57
|
lg: "6px",
|
|
59
|
-
|
|
58
|
+
md: "4px",
|
|
59
|
+
pill: "10px",
|
|
60
|
+
sm: "3px"
|
|
60
61
|
},
|
|
61
62
|
spacing: {
|
|
62
|
-
xs: "2px",
|
|
63
|
-
sm: "4px",
|
|
64
|
-
md: "6px",
|
|
65
63
|
lg: "8px",
|
|
64
|
+
md: "6px",
|
|
65
|
+
sm: "4px",
|
|
66
66
|
xl: "12px",
|
|
67
|
+
xs: "2px",
|
|
67
68
|
xxl: "16px"
|
|
68
69
|
}
|
|
69
70
|
};
|
|
71
|
+
var getLevelColors = (level) => theme.colors.levels[level] ?? theme.colors.levels.info;
|
|
70
72
|
|
|
71
73
|
// src/plugin/components/log-row.tsx
|
|
72
|
-
import { useCallback } from "react";
|
|
74
|
+
import { memo, useCallback } from "react";
|
|
73
75
|
|
|
74
76
|
// src/plugin/format.ts
|
|
77
|
+
var timeFormatter;
|
|
75
78
|
function formatTime(ts) {
|
|
76
|
-
|
|
77
|
-
return d.toLocaleTimeString(void 0, {
|
|
78
|
-
hour12: false,
|
|
79
|
+
timeFormatter ??= new Intl.DateTimeFormat(void 0, {
|
|
79
80
|
hour: "2-digit",
|
|
81
|
+
hour12: false,
|
|
80
82
|
minute: "2-digit",
|
|
81
83
|
second: "2-digit"
|
|
82
84
|
});
|
|
85
|
+
return timeFormatter.format(new Date(ts));
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
// src/plugin/icons.tsx
|
|
86
89
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
87
|
-
var defaults = {
|
|
90
|
+
var defaults = { color: "currentColor", size: 14 };
|
|
88
91
|
var Svg = ({
|
|
89
92
|
size = defaults.size,
|
|
90
93
|
color = defaults.color,
|
|
@@ -139,15 +142,16 @@ var Trash2Icon = (props) => /* @__PURE__ */ jsxs(Svg, { ...props, children: [
|
|
|
139
142
|
// src/plugin/components/level-badge.tsx
|
|
140
143
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
141
144
|
var LEVEL_LABELS = {
|
|
142
|
-
trace: "TRC",
|
|
143
145
|
debug: "DBG",
|
|
144
|
-
info: "INF",
|
|
145
|
-
warning: "WRN",
|
|
146
146
|
error: "ERR",
|
|
147
|
-
fatal: "FTL"
|
|
147
|
+
fatal: "FTL",
|
|
148
|
+
info: "INF",
|
|
149
|
+
trace: "TRC",
|
|
150
|
+
warning: "WRN"
|
|
148
151
|
};
|
|
149
152
|
var LevelBadge = ({ level }) => {
|
|
150
|
-
const colors =
|
|
153
|
+
const colors = getLevelColors(level);
|
|
154
|
+
const label = LEVEL_LABELS[level] ?? level.toUpperCase();
|
|
151
155
|
return /* @__PURE__ */ jsx2(
|
|
152
156
|
"span",
|
|
153
157
|
{
|
|
@@ -165,7 +169,7 @@ var LevelBadge = ({ level }) => {
|
|
|
165
169
|
textAlign: "center",
|
|
166
170
|
textTransform: "uppercase"
|
|
167
171
|
},
|
|
168
|
-
children:
|
|
172
|
+
children: label
|
|
169
173
|
}
|
|
170
174
|
);
|
|
171
175
|
};
|
|
@@ -219,13 +223,13 @@ var DetailRow = ({ label, children }) => /* @__PURE__ */ jsxs2("div", { style: d
|
|
|
219
223
|
/* @__PURE__ */ jsx3("span", { style: detailValueStyle, children })
|
|
220
224
|
] });
|
|
221
225
|
var LogDetail = ({ record }) => {
|
|
222
|
-
const levelColors =
|
|
226
|
+
const levelColors = getLevelColors(record.level);
|
|
223
227
|
const hasProperties = Object.keys(record.properties).length > 0;
|
|
224
228
|
return /* @__PURE__ */ jsxs2("div", { "data-testid": "log-detail", style: { ...containerStyle, background: levelColors.bg }, children: [
|
|
225
229
|
/* @__PURE__ */ jsx3(DetailRow, { label: "Time", children: formatTime(record.timestamp) }),
|
|
226
230
|
/* @__PURE__ */ jsx3(DetailRow, { label: "Level", children: /* @__PURE__ */ jsx3("span", { style: { color: levelColors.color, fontWeight: 600 }, children: record.level }) }),
|
|
227
231
|
/* @__PURE__ */ jsx3(DetailRow, { label: "Category", children: /* @__PURE__ */ jsx3("span", { style: categoryStyle, children: record.category.join(".") }) }),
|
|
228
|
-
record.caller
|
|
232
|
+
record.caller ? /* @__PURE__ */ jsx3(DetailRow, { label: "Caller", children: /* @__PURE__ */ jsx3("span", { style: callerStyle, children: record.caller }) }) : null,
|
|
229
233
|
/* @__PURE__ */ jsx3(DetailRow, { label: "Message", children: /* @__PURE__ */ jsx3("span", { style: messageStyle, children: record.messageText }) }),
|
|
230
234
|
hasProperties && /* @__PURE__ */ jsx3(DetailRow, { label: "Data", children: /* @__PURE__ */ jsx3("pre", { style: propertiesStyle, children: safeStringify(record.properties, 2) }) })
|
|
231
235
|
] });
|
|
@@ -286,8 +290,8 @@ var chevronStyle = {
|
|
|
286
290
|
flexShrink: 0,
|
|
287
291
|
transition: "transform 0.15s"
|
|
288
292
|
};
|
|
289
|
-
var
|
|
290
|
-
const levelColors =
|
|
293
|
+
var LogRowComponent = ({ record, expanded, onToggle }) => {
|
|
294
|
+
const levelColors = getLevelColors(record.level);
|
|
291
295
|
const handleClick = useCallback(() => {
|
|
292
296
|
onToggle(expanded ? null : record.id);
|
|
293
297
|
}, [expanded, onToggle, record.id]);
|
|
@@ -306,14 +310,15 @@ var LogRow = ({ record, expanded, onToggle }) => {
|
|
|
306
310
|
/* @__PURE__ */ jsx4(LevelBadge, { level: record.level }),
|
|
307
311
|
/* @__PURE__ */ jsx4("span", { style: categoryStyle2, children: record.category.join(".") }),
|
|
308
312
|
/* @__PURE__ */ jsx4("span", { style: messageStyle2, children: record.messageText }),
|
|
309
|
-
record.caller
|
|
313
|
+
record.caller ? /* @__PURE__ */ jsx4("span", { style: callerStyle2, children: record.caller }) : null,
|
|
310
314
|
/* @__PURE__ */ jsx4("span", { style: { ...chevronStyle, transform: expanded ? "rotate(90deg)" : "rotate(0deg)" }, children: /* @__PURE__ */ jsx4(ChevronRightIcon, { size: 12 }) })
|
|
311
315
|
]
|
|
312
316
|
}
|
|
313
317
|
),
|
|
314
|
-
expanded
|
|
318
|
+
expanded ? /* @__PURE__ */ jsx4(LogDetail, { record }) : null
|
|
315
319
|
] });
|
|
316
320
|
};
|
|
321
|
+
var LogRow = memo(LogRowComponent);
|
|
317
322
|
|
|
318
323
|
// src/plugin/components/log-list.tsx
|
|
319
324
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
@@ -340,17 +345,25 @@ var LogList = ({ records, expandedId, onToggle, autoScroll }) => {
|
|
|
340
345
|
const virtualizer = useVirtualizer({
|
|
341
346
|
count: records.length,
|
|
342
347
|
estimateSize: () => ESTIMATED_ROW_HEIGHT,
|
|
343
|
-
|
|
348
|
+
// The virtualizer can ask for keys of indices from a previous, longer list
|
|
349
|
+
// (after clear or a narrowing filter), so never assume the index exists.
|
|
350
|
+
getItemKey: (index) => records[index]?.id ?? index,
|
|
344
351
|
getScrollElement: () => containerRef.current,
|
|
345
352
|
overscan: OVERSCAN
|
|
346
353
|
});
|
|
347
354
|
const recordCount = records.length;
|
|
355
|
+
const lastRecordId = records[recordCount - 1]?.id;
|
|
348
356
|
useEffect(() => {
|
|
349
|
-
if (
|
|
357
|
+
if (lastRecordId === void 0 || !(autoScroll && isAtBottomRef.current)) {
|
|
350
358
|
return;
|
|
351
359
|
}
|
|
352
360
|
virtualizer.scrollToIndex(recordCount - 1, { align: "end" });
|
|
353
|
-
}, [autoScroll, recordCount, virtualizer]);
|
|
361
|
+
}, [autoScroll, lastRecordId, recordCount, virtualizer]);
|
|
362
|
+
useEffect(() => {
|
|
363
|
+
if (recordCount === 0) {
|
|
364
|
+
isAtBottomRef.current = true;
|
|
365
|
+
}
|
|
366
|
+
}, [recordCount]);
|
|
354
367
|
const handleScroll = useCallback2(() => {
|
|
355
368
|
const el = containerRef.current;
|
|
356
369
|
if (!el) {
|
|
@@ -419,6 +432,11 @@ function useHoverStyles() {
|
|
|
419
432
|
[data-lt-interactive]:active {
|
|
420
433
|
filter: brightness(0.9);
|
|
421
434
|
}
|
|
435
|
+
[data-lt-interactive]:focus-visible,
|
|
436
|
+
[data-lt-input]:focus-visible {
|
|
437
|
+
outline: 2px solid ${theme.colors.accent};
|
|
438
|
+
outline-offset: 1px;
|
|
439
|
+
}
|
|
422
440
|
[data-lt-clear-btn]:hover {
|
|
423
441
|
color: ${theme.colors.textPrimary} !important;
|
|
424
442
|
}
|
|
@@ -464,6 +482,7 @@ var sectionLabelStyle = {
|
|
|
464
482
|
var LevelToggle = ({
|
|
465
483
|
level,
|
|
466
484
|
active,
|
|
485
|
+
pressed,
|
|
467
486
|
onToggle
|
|
468
487
|
}) => {
|
|
469
488
|
const colors = theme.colors.levels[level];
|
|
@@ -471,7 +490,7 @@ var LevelToggle = ({
|
|
|
471
490
|
return /* @__PURE__ */ jsx6(
|
|
472
491
|
"button",
|
|
473
492
|
{
|
|
474
|
-
"aria-pressed":
|
|
493
|
+
"aria-pressed": pressed,
|
|
475
494
|
"data-lt-interactive": "",
|
|
476
495
|
"data-testid": `level-toggle-${level}`,
|
|
477
496
|
onClick: handleClick,
|
|
@@ -520,7 +539,7 @@ var Toolbar = ({
|
|
|
520
539
|
useEffect2(() => () => debouncedSearch.cancel(), [debouncedSearch]);
|
|
521
540
|
const handleSearchChange = useCallback3(
|
|
522
541
|
(e) => {
|
|
523
|
-
const value = e.target
|
|
542
|
+
const { value } = e.target;
|
|
524
543
|
setSearch(value);
|
|
525
544
|
debouncedSearch.maybeExecute(value);
|
|
526
545
|
},
|
|
@@ -639,7 +658,8 @@ var Toolbar = ({
|
|
|
639
658
|
{
|
|
640
659
|
active: levelFilter.size === 0 || levelFilter.has(level),
|
|
641
660
|
level,
|
|
642
|
-
onToggle: handleLevelToggle
|
|
661
|
+
onToggle: handleLevelToggle,
|
|
662
|
+
pressed: levelFilter.has(level)
|
|
643
663
|
},
|
|
644
664
|
level
|
|
645
665
|
))
|
|
@@ -775,6 +795,8 @@ var Toolbar = ({
|
|
|
775
795
|
/* @__PURE__ */ jsx6(
|
|
776
796
|
Combobox.Input,
|
|
777
797
|
{
|
|
798
|
+
"aria-label": "Filter by category",
|
|
799
|
+
"data-lt-input": "",
|
|
778
800
|
"data-testid": "category-input",
|
|
779
801
|
placeholder: categoryFilter.length === 0 ? "Category\u2026" : "",
|
|
780
802
|
style: {
|
|
@@ -841,6 +863,8 @@ var Toolbar = ({
|
|
|
841
863
|
/* @__PURE__ */ jsx6(
|
|
842
864
|
"input",
|
|
843
865
|
{
|
|
866
|
+
"aria-label": "Search logs",
|
|
867
|
+
"data-lt-input": "",
|
|
844
868
|
"data-testid": "search-input",
|
|
845
869
|
onChange: handleSearchChange,
|
|
846
870
|
placeholder: "Search\u2026",
|
|
@@ -942,8 +966,8 @@ var LogTapeDevtoolsPlugin = ({ store }) => {
|
|
|
942
966
|
}
|
|
943
967
|
return Array.from(seen).sort();
|
|
944
968
|
}, [records]);
|
|
945
|
-
const filteredRecords = useMemo2(
|
|
946
|
-
|
|
969
|
+
const filteredRecords = useMemo2(
|
|
970
|
+
() => records.filter((r) => {
|
|
947
971
|
if (levelFilter.size > 0 && !levelFilter.has(r.level)) {
|
|
948
972
|
return false;
|
|
949
973
|
}
|
|
@@ -957,8 +981,9 @@ var LogTapeDevtoolsPlugin = ({ store }) => {
|
|
|
957
981
|
}
|
|
958
982
|
}
|
|
959
983
|
return true;
|
|
960
|
-
})
|
|
961
|
-
|
|
984
|
+
}),
|
|
985
|
+
[records, levelFilter, categoryFilter, searchText]
|
|
986
|
+
);
|
|
962
987
|
return /* @__PURE__ */ jsxs5("div", { style: rootStyle, children: [
|
|
963
988
|
/* @__PURE__ */ jsx7(
|
|
964
989
|
"div",
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin/logtape-devtools-plugin.tsx","../src/plugin/components/log-list.tsx","../src/plugin/theme.ts","../src/plugin/components/log-row.tsx","../src/plugin/format.ts","../src/plugin/icons.tsx","../src/plugin/components/level-badge.tsx","../src/plugin/components/log-detail.tsx","../src/plugin/components/toolbar.tsx","../src/plugin/create-logtape-devtools-plugin.tsx","../src/create-logtape-devtools.ts"],"sourcesContent":["import { useCallback, useMemo, useState, useSyncExternalStore } from \"react\";\nimport type { LogStore } from \"../store\";\nimport type { DevtoolsLogRecord, LogLevel } from \"../types\";\nimport { LogList } from \"./components/log-list\";\nimport { Toolbar } from \"./components/toolbar\";\nimport { PanelLeftCloseIcon, PanelLeftOpenIcon, PauseIcon, PlayIcon, Trash2Icon } from \"./icons\";\nimport { theme } from \"./theme\";\n\ninterface Props {\n store: LogStore;\n}\n\nconst rootStyle = {\n background: theme.colors.background,\n color: theme.colors.textPrimary,\n display: \"flex\",\n flexDirection: \"row\",\n fontFamily: theme.fontFamily.sans,\n height: \"100%\",\n overflow: \"hidden\",\n} as const;\n\nconst sidebarStyle = {\n flexShrink: 0,\n overflow: \"hidden\",\n transition: \"width 0.2s ease, opacity 0.15s ease\",\n} as const;\n\nconst mainStyle = {\n display: \"flex\",\n flex: 1,\n flexDirection: \"column\",\n overflow: \"hidden\",\n} as const;\n\nconst topBarStyle = {\n alignItems: \"center\",\n borderBottom: `1px solid ${theme.colors.border}`,\n display: \"flex\",\n flexShrink: 0,\n gap: theme.spacing.lg,\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n} as const;\n\nconst sidebarToggleStyle = {\n background: \"none\",\n border: \"none\",\n color: theme.colors.textMuted,\n cursor: \"pointer\",\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.md,\n padding: `${theme.spacing.xs} ${theme.spacing.sm}`,\n transition: \"color 0.1s\",\n} as const;\n\nconst iconLabelStyle = { alignItems: \"center\", display: \"inline-flex\", gap: \"6px\" } as const;\n\nconst countStyle = {\n color: theme.colors.textMuted,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.sm,\n} as const;\n\nconst compactButtonStyle = {\n background: theme.colors.surfaceHover,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: theme.radius.sm,\n color: theme.colors.textPrimary,\n cursor: \"pointer\",\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.sm,\n padding: `${theme.spacing.xs} ${theme.spacing.sm}`,\n transition: \"filter 0.1s\",\n} as const;\n\nexport const LogTapeDevtoolsPlugin = ({ store }: Props) => {\n const allRecords = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n\n const [paused, setPaused] = useState(false);\n const [pausedRecords, setPausedRecords] = useState<DevtoolsLogRecord[]>([]);\n const [levelFilter, setLevelFilter] = useState<Set<LogLevel>>(new Set());\n const [categoryFilter, setCategoryFilter] = useState<string[]>([]);\n const [searchText, setSearchText] = useState(\"\");\n const [expandedId, setExpandedId] = useState<string | null>(null);\n const [sidebarOpen, setSidebarOpen] = useState(true);\n\n const handlePause = useCallback(() => {\n setPaused(true);\n setPausedRecords(store.getSnapshot());\n }, [store]);\n\n const handleResume = useCallback(() => {\n setPaused(false);\n setPausedRecords([]);\n }, []);\n\n const handleClear = useCallback(() => {\n store.clear();\n setPausedRecords([]);\n setExpandedId(null);\n }, [store]);\n\n const records = paused ? pausedRecords : allRecords;\n\n const uniqueCategories = useMemo(() => {\n const seen = new Set<string>();\n for (const r of records) {\n seen.add(r.category.join(\".\"));\n }\n return Array.from(seen).sort();\n }, [records]);\n\n const filteredRecords = useMemo(() => {\n return records.filter((r) => {\n if (levelFilter.size > 0 && !levelFilter.has(r.level)) {\n return false;\n }\n if (categoryFilter.length > 0 && !categoryFilter.includes(r.category.join(\".\"))) {\n return false;\n }\n if (searchText) {\n const term = searchText.toLowerCase();\n if (\n !(\n r.messageText.toLowerCase().includes(term) ||\n r.category.join(\".\").toLowerCase().includes(term)\n )\n ) {\n return false;\n }\n }\n return true;\n });\n }, [records, levelFilter, categoryFilter, searchText]);\n\n return (\n <div style={rootStyle}>\n <div\n style={{\n ...sidebarStyle,\n borderRight: sidebarOpen ? `1px solid ${theme.colors.border}` : \"none\",\n opacity: sidebarOpen ? 1 : 0,\n width: sidebarOpen ? \"200px\" : \"0px\",\n }}\n >\n <Toolbar\n categories={uniqueCategories}\n categoryFilter={categoryFilter}\n filteredCount={filteredRecords.length}\n levelFilter={levelFilter}\n onCategoryFilterChange={setCategoryFilter}\n onClear={handleClear}\n onLevelFilterChange={setLevelFilter}\n onPause={handlePause}\n onResume={handleResume}\n onSearchTextChange={setSearchText}\n paused={paused}\n totalCount={records.length}\n />\n </div>\n\n <div style={mainStyle}>\n {/* Sidebar toggle bar */}\n <div style={topBarStyle}>\n <button\n aria-expanded={sidebarOpen}\n data-lt-clear-btn=\"\"\n onClick={() => setSidebarOpen((v) => !v)}\n style={sidebarToggleStyle}\n title={sidebarOpen ? \"Collapse sidebar\" : \"Expand sidebar\"}\n type=\"button\"\n >\n <span style={iconLabelStyle}>\n {sidebarOpen ? <PanelLeftCloseIcon size={14} /> : <PanelLeftOpenIcon size={14} />}\n {sidebarOpen ? \"Hide controls\" : \"Show controls\"}\n </span>\n </button>\n\n {!sidebarOpen && (\n <>\n <span style={countStyle}>\n {filteredRecords.length === records.length\n ? `${records.length} logs`\n : `${filteredRecords.length} / ${records.length} logs`}\n </span>\n <button\n data-lt-interactive=\"\"\n onClick={paused ? handleResume : handlePause}\n style={{\n ...compactButtonStyle,\n background: paused\n ? theme.colors.levels.warning.badge\n : compactButtonStyle.background,\n color: paused ? theme.colors.white : compactButtonStyle.color,\n }}\n title={paused ? \"Resume\" : \"Pause\"}\n type=\"button\"\n >\n {paused ? <PlayIcon size={12} /> : <PauseIcon size={12} />}\n </button>\n <button\n data-lt-interactive=\"\"\n onClick={handleClear}\n style={compactButtonStyle}\n title=\"Clear\"\n type=\"button\"\n >\n <Trash2Icon size={12} />\n </button>\n </>\n )}\n </div>\n <LogList\n autoScroll={!paused}\n expandedId={expandedId}\n onToggle={setExpandedId}\n records={filteredRecords}\n />\n </div>\n </div>\n );\n};\n","import { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { DevtoolsLogRecord } from \"../../types\";\nimport { theme } from \"../theme\";\nimport { LogRow } from \"./log-row\";\n\ninterface LogListProps {\n autoScroll: boolean;\n expandedId: string | null;\n onToggle: (id: string | null) => void;\n records: DevtoolsLogRecord[];\n}\n\n/** Collapsed row height — rows are measured after mount, this is only the seed. */\nconst ESTIMATED_ROW_HEIGHT = 28;\nconst OVERSCAN = 10;\n/** Distance from the bottom (px) still considered \"stuck to the bottom\". */\nconst STICK_THRESHOLD = 40;\n\nconst emptyStyle = {\n alignItems: \"center\",\n color: theme.colors.textMuted,\n display: \"flex\",\n flex: 1,\n fontSize: theme.fontSize.lg,\n justifyContent: \"center\",\n} as const;\n\nconst containerStyle = {\n flex: 1,\n overflow: \"auto\",\n scrollbarColor: `${theme.colors.scrollbar} transparent`,\n scrollbarWidth: \"thin\",\n} as const;\n\nexport const LogList = ({ records, expandedId, onToggle, autoScroll }: LogListProps) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const isAtBottomRef = useRef(true);\n\n const virtualizer = useVirtualizer({\n count: records.length,\n estimateSize: () => ESTIMATED_ROW_HEIGHT,\n getItemKey: (index) => records[index].id,\n getScrollElement: () => containerRef.current,\n overscan: OVERSCAN,\n });\n\n const recordCount = records.length;\n useEffect(() => {\n if (recordCount === 0 || !(autoScroll && isAtBottomRef.current)) {\n return;\n }\n virtualizer.scrollToIndex(recordCount - 1, { align: \"end\" });\n }, [autoScroll, recordCount, virtualizer]);\n\n const handleScroll = useCallback(() => {\n const el = containerRef.current;\n if (!el) {\n return;\n }\n isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_THRESHOLD;\n }, []);\n\n if (records.length === 0) {\n return (\n <div data-testid=\"log-list-empty\" style={emptyStyle}>\n No logs yet\n </div>\n );\n }\n\n const virtualItems = virtualizer.getVirtualItems();\n\n return (\n <div data-testid=\"log-list\" onScroll={handleScroll} ref={containerRef} style={containerStyle}>\n {/* Spacer sized to the full list, with only the visible window rendered */}\n <div\n style={{\n height: `${virtualizer.getTotalSize()}px`,\n position: \"relative\",\n width: \"100%\",\n }}\n >\n <div\n style={{\n left: 0,\n position: \"absolute\",\n top: 0,\n transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,\n width: \"100%\",\n }}\n >\n {virtualItems.map((virtualItem) => {\n const record = records[virtualItem.index];\n return (\n <div\n data-index={virtualItem.index}\n key={virtualItem.key}\n ref={virtualizer.measureElement}\n >\n <LogRow expanded={expandedId === record.id} onToggle={onToggle} record={record} />\n </div>\n );\n })}\n </div>\n </div>\n </div>\n );\n};\n","export const theme = {\n colors: {\n background: \"#1e1e1e\",\n surface: \"#1a1a1a\",\n surfaceHover: \"#2a2a2a\",\n surfaceSelected: \"#2a2a3a\",\n border: \"#333\",\n borderInput: \"#444\",\n borderFocus: \"#6366f1\",\n\n textPrimary: \"#e0e0e0\",\n textSecondary: \"#888\",\n textMuted: \"#666\",\n textDimmed: \"#555\",\n white: \"#fff\",\n\n accent: \"#6366f1\",\n accentHover: \"#818cf8\",\n\n levels: {\n trace: { bg: \"#1a1a2e\", color: \"#8888bb\", badge: \"#5555aa\" },\n debug: { bg: \"#1a2a1a\", color: \"#88bb88\", badge: \"#55aa55\" },\n info: { bg: \"#1a2a3a\", color: \"#60a5fa\", badge: \"#3b82f6\" },\n warning: { bg: \"#2a2a1a\", color: \"#facc15\", badge: \"#eab308\" },\n error: { bg: \"#2a1a1a\", color: \"#ef4444\", badge: \"#dc2626\" },\n fatal: { bg: \"#3a0a0a\", color: \"#ff6b6b\", badge: \"#b91c1c\" },\n },\n\n scrollbar: \"#444\",\n scrollbarHover: \"#555\",\n },\n\n fontFamily: {\n mono: 'ui-monospace, \"Cascadia Code\", \"Source Code Pro\", Menlo, Consolas, monospace',\n sans: 'system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif',\n },\n\n fontSize: {\n xs: \"9px\",\n sm: \"10px\",\n md: \"11px\",\n base: \"12px\",\n lg: \"13px\",\n },\n\n radius: {\n sm: \"3px\",\n md: \"4px\",\n lg: \"6px\",\n pill: \"10px\",\n },\n\n spacing: {\n xs: \"2px\",\n sm: \"4px\",\n md: \"6px\",\n lg: \"8px\",\n xl: \"12px\",\n xxl: \"16px\",\n },\n} as const;\n\nexport type LogLevelColors = (typeof theme.colors.levels)[keyof typeof theme.colors.levels];\n","import { useCallback } from \"react\";\nimport type { DevtoolsLogRecord } from \"../../types\";\nimport { formatTime } from \"../format\";\nimport { ChevronRightIcon } from \"../icons\";\nimport { theme } from \"../theme\";\nimport { LevelBadge } from \"./level-badge\";\nimport { LogDetail } from \"./log-detail\";\n\ninterface LogRowProps {\n expanded: boolean;\n onToggle: (id: string | null) => void;\n record: DevtoolsLogRecord;\n}\n\nconst rowStyle = {\n alignItems: \"center\",\n border: \"none\",\n borderBottom: `1px solid ${theme.colors.border}`,\n cursor: \"pointer\",\n display: \"flex\",\n gap: theme.spacing.lg,\n padding: `${theme.spacing.md} ${theme.spacing.xl}`,\n textAlign: \"left\",\n transition: \"background 0.1s, filter 0.1s\",\n width: \"100%\",\n} as const;\n\nconst timestampStyle = {\n color: theme.colors.textDimmed,\n flexShrink: 0,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.md,\n} as const;\n\nconst categoryStyle = {\n color: theme.colors.accent,\n flexShrink: 0,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.md,\n maxWidth: \"180px\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n} as const;\n\nconst messageStyle = {\n color: theme.colors.textPrimary,\n flex: 1,\n fontSize: theme.fontSize.base,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n} as const;\n\nconst callerStyle = {\n color: theme.colors.textDimmed,\n flexShrink: 0,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.sm,\n maxWidth: \"200px\",\n opacity: 0.7,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n} as const;\n\nconst chevronStyle = {\n color: theme.colors.textDimmed,\n display: \"flex\",\n flexShrink: 0,\n transition: \"transform 0.15s\",\n} as const;\n\nexport const LogRow = ({ record, expanded, onToggle }: LogRowProps) => {\n const levelColors = theme.colors.levels[record.level];\n\n const handleClick = useCallback(() => {\n onToggle(expanded ? null : record.id);\n }, [expanded, onToggle, record.id]);\n\n return (\n <div>\n <button\n aria-expanded={expanded}\n data-lt-interactive=\"\"\n data-testid=\"log-row\"\n onClick={handleClick}\n style={{ ...rowStyle, background: expanded ? levelColors.bg : \"transparent\" }}\n type=\"button\"\n >\n {/* Timestamp */}\n <span style={timestampStyle}>{formatTime(record.timestamp)}</span>\n\n {/* Level badge */}\n <LevelBadge level={record.level} />\n\n {/* Category */}\n <span style={categoryStyle}>{record.category.join(\".\")}</span>\n\n {/* Message */}\n <span style={messageStyle}>{record.messageText}</span>\n\n {/* Caller (right-aligned) */}\n {record.caller && <span style={callerStyle}>{record.caller}</span>}\n\n {/* Expand indicator */}\n <span style={{ ...chevronStyle, transform: expanded ? \"rotate(90deg)\" : \"rotate(0deg)\" }}>\n <ChevronRightIcon size={12} />\n </span>\n </button>\n\n {expanded && <LogDetail record={record} />}\n </div>\n );\n};\n","/** Formats a timestamp as a 24-hour `HH:MM:SS` local time string. */\nexport function formatTime(ts: number): string {\n const d = new Date(ts);\n return d.toLocaleTimeString(undefined, {\n hour12: false,\n hour: \"2-digit\",\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n}\n","/**\n * Inline SVG icons based on Lucide (https://lucide.dev) — ISC License.\n * Using inline SVGs avoids adding lucide-react as a runtime dependency.\n */\n\ninterface IconProps {\n color?: string;\n size?: number;\n style?: React.CSSProperties;\n}\n\nconst defaults = { size: 14, color: \"currentColor\" } as const;\n\nconst Svg = ({\n size = defaults.size,\n color = defaults.color,\n style,\n children,\n}: IconProps & { children: React.ReactNode }) => (\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n height={size}\n stroke={color}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n style={style}\n viewBox=\"0 0 24 24\"\n width={size}\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n {children}\n </svg>\n);\n\nexport const PlayIcon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\" />\n </Svg>\n);\n\nexport const PauseIcon = (props: IconProps) => (\n <Svg {...props}>\n <rect height={18} rx={1} width={5} x={14} y={3} />\n <rect height={18} rx={1} width={5} x={5} y={3} />\n </Svg>\n);\n\nexport const XIcon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </Svg>\n);\n\nexport const CheckIcon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"M20 6 9 17l-5-5\" />\n </Svg>\n);\n\nexport const ChevronRightIcon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"m9 18 6-6-6-6\" />\n </Svg>\n);\n\nexport const PanelLeftCloseIcon = (props: IconProps) => (\n <Svg {...props}>\n <rect height={18} rx={2} width={18} x={3} y={3} />\n <path d=\"M9 3v18\" />\n <path d=\"m16 15-3-3 3-3\" />\n </Svg>\n);\n\nexport const PanelLeftOpenIcon = (props: IconProps) => (\n <Svg {...props}>\n <rect height={18} rx={2} width={18} x={3} y={3} />\n <path d=\"M9 3v18\" />\n <path d=\"m14 9 3 3-3 3\" />\n </Svg>\n);\n\nexport const Trash2Icon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"M10 11v6\" />\n <path d=\"M14 11v6\" />\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\" />\n <path d=\"M3 6h18\" />\n <path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n </Svg>\n);\n","import type { LogLevel } from \"../../types\";\nimport { theme } from \"../theme\";\n\nconst LEVEL_LABELS: Record<LogLevel, string> = {\n trace: \"TRC\",\n debug: \"DBG\",\n info: \"INF\",\n warning: \"WRN\",\n error: \"ERR\",\n fatal: \"FTL\",\n};\n\nexport const LevelBadge = ({ level }: { level: LogLevel }) => {\n const colors = theme.colors.levels[level];\n\n return (\n <span\n style={{\n background: colors.badge,\n borderRadius: theme.radius.sm,\n color: theme.colors.white,\n display: \"inline-block\",\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.sm,\n fontWeight: 600,\n lineHeight: 1,\n minWidth: \"28px\",\n padding: `${theme.spacing.xs} ${theme.spacing.sm}`,\n textAlign: \"center\",\n textTransform: \"uppercase\",\n }}\n >\n {LEVEL_LABELS[level]}\n </span>\n );\n};\n","import { safeStringify } from \"../../safe-json\";\nimport type { DevtoolsLogRecord } from \"../../types\";\nimport { formatTime } from \"../format\";\nimport { theme } from \"../theme\";\n\nconst detailRowStyle = {\n display: \"flex\",\n gap: theme.spacing.lg,\n marginBottom: theme.spacing.md,\n} as const;\n\nconst detailLabelStyle = {\n color: theme.colors.textMuted,\n flexShrink: 0,\n fontSize: theme.fontSize.md,\n minWidth: \"72px\",\n textAlign: \"right\",\n} as const;\n\nconst detailValueStyle = {\n fontSize: theme.fontSize.base,\n wordBreak: \"break-all\",\n} as const;\n\nconst containerStyle = {\n borderTop: `1px solid ${theme.colors.border}`,\n padding: `${theme.spacing.lg} ${theme.spacing.xl}`,\n} as const;\n\nconst categoryStyle = {\n color: theme.colors.accent,\n fontFamily: theme.fontFamily.mono,\n} as const;\n\nconst callerStyle = {\n color: theme.colors.textSecondary,\n fontFamily: theme.fontFamily.mono,\n} as const;\n\nconst messageStyle = { color: theme.colors.textPrimary } as const;\n\nconst propertiesStyle = {\n background: \"rgba(0,0,0,0.3)\",\n borderRadius: theme.radius.md,\n color: theme.colors.textPrimary,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.md,\n margin: 0,\n maxHeight: \"200px\",\n overflow: \"auto\",\n padding: theme.spacing.lg,\n whiteSpace: \"pre-wrap\",\n wordBreak: \"break-all\",\n} as const;\n\nconst DetailRow = ({ label, children }: { label: string; children: React.ReactNode }) => (\n <div style={detailRowStyle}>\n <span style={detailLabelStyle}>{label}</span>\n <span style={detailValueStyle}>{children}</span>\n </div>\n);\n\nexport const LogDetail = ({ record }: { record: DevtoolsLogRecord }) => {\n const levelColors = theme.colors.levels[record.level];\n const hasProperties = Object.keys(record.properties).length > 0;\n\n return (\n <div data-testid=\"log-detail\" style={{ ...containerStyle, background: levelColors.bg }}>\n <DetailRow label=\"Time\">{formatTime(record.timestamp)}</DetailRow>\n <DetailRow label=\"Level\">\n <span style={{ color: levelColors.color, fontWeight: 600 }}>{record.level}</span>\n </DetailRow>\n <DetailRow label=\"Category\">\n <span style={categoryStyle}>{record.category.join(\".\")}</span>\n </DetailRow>\n {record.caller && (\n <DetailRow label=\"Caller\">\n <span style={callerStyle}>{record.caller}</span>\n </DetailRow>\n )}\n <DetailRow label=\"Message\">\n <span style={messageStyle}>{record.messageText}</span>\n </DetailRow>\n\n {hasProperties && (\n <DetailRow label=\"Data\">\n <pre style={propertiesStyle}>{safeStringify(record.properties, 2)}</pre>\n </DetailRow>\n )}\n </div>\n );\n};\n","import { Combobox } from \"@base-ui-components/react/combobox\";\nimport { Popover } from \"@base-ui-components/react/popover\";\nimport { Debouncer } from \"@tanstack/pacer\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { LOG_LEVELS, type LogLevel } from \"../../types\";\nimport { CheckIcon, PauseIcon, PlayIcon, Trash2Icon, XIcon } from \"../icons\";\nimport { theme } from \"../theme\";\n\ninterface ToolbarProps {\n categories: string[];\n categoryFilter: string[];\n filteredCount: number;\n levelFilter: Set<LogLevel>;\n onCategoryFilterChange: (value: string[]) => void;\n onClear: () => void;\n onLevelFilterChange: (levels: Set<LogLevel>) => void;\n onPause: () => void;\n onResume: () => void;\n /** Called with the debounced search term. The toolbar owns the input value. */\n onSearchTextChange: (value: string) => void;\n paused: boolean;\n totalCount: number;\n}\n\n// Inject hover styles once (scoped via data attribute to avoid leaking)\nconst HOVER_STYLE_ID = \"logtape-devtools-hover\";\nfunction useHoverStyles() {\n useEffect(() => {\n if (document.getElementById(HOVER_STYLE_ID)) {\n return;\n }\n const style = document.createElement(\"style\");\n style.id = HOVER_STYLE_ID;\n style.textContent = `\n [data-lt-interactive]:hover {\n filter: brightness(1.25);\n }\n [data-lt-interactive]:active {\n filter: brightness(0.9);\n }\n [data-lt-clear-btn]:hover {\n color: ${theme.colors.textPrimary} !important;\n }\n [data-lt-chip-remove]:hover {\n opacity: 1 !important;\n color: ${theme.colors.white} !important;\n }\n `;\n document.head.appendChild(style);\n }, []);\n}\n\nconst buttonStyle = {\n background: theme.colors.surfaceHover,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: theme.radius.md,\n color: theme.colors.textPrimary,\n cursor: \"pointer\",\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.md,\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n transition: \"filter 0.1s, background 0.1s, border-color 0.1s\",\n width: \"100%\",\n} as const;\n\nconst inputStyle = {\n background: theme.colors.surface,\n border: `1px solid ${theme.colors.borderInput}`,\n borderRadius: theme.radius.md,\n color: theme.colors.textPrimary,\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.md,\n outline: \"none\",\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n transition: \"border-color 0.15s\",\n width: \"100%\",\n} as const;\n\nconst sectionLabelStyle = {\n color: theme.colors.textMuted,\n fontSize: \"9px\",\n fontWeight: 600,\n letterSpacing: \"0.05em\",\n textTransform: \"uppercase\",\n} as const;\n\nconst LevelToggle = ({\n level,\n active,\n onToggle,\n}: {\n level: LogLevel;\n active: boolean;\n onToggle: (level: LogLevel) => void;\n}) => {\n const colors = theme.colors.levels[level];\n const handleClick = useCallback(() => onToggle(level), [level, onToggle]);\n\n return (\n <button\n aria-pressed={active}\n data-lt-interactive=\"\"\n data-testid={`level-toggle-${level}`}\n onClick={handleClick}\n style={{\n background: active ? colors.badge : \"transparent\",\n border: `1px solid ${active ? colors.badge : theme.colors.border}`,\n borderRadius: theme.radius.sm,\n color: active ? theme.colors.white : colors.color,\n cursor: \"pointer\",\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.sm,\n fontWeight: 600,\n lineHeight: 1,\n minWidth: \"30px\",\n opacity: active ? 1 : 0.5,\n padding: \"3px 0\",\n textAlign: \"center\" as const,\n transition: \"all 0.15s\",\n }}\n title={`Filter ${level} logs`}\n type=\"button\"\n >\n {level.slice(0, 3).toUpperCase()}\n </button>\n );\n};\n\nexport const Toolbar = ({\n paused,\n onPause,\n onResume,\n onClear,\n levelFilter,\n onLevelFilterChange,\n categories,\n categoryFilter,\n onCategoryFilterChange,\n onSearchTextChange,\n totalCount,\n filteredCount,\n}: ToolbarProps) => {\n useHoverStyles();\n\n // The toolbar is the single source of truth for the search input; the parent\n // only ever receives the debounced value.\n const [search, setSearch] = useState(\"\");\n const debouncedSearch = useMemo(\n () => new Debouncer(onSearchTextChange, { wait: 200 }),\n [onSearchTextChange]\n );\n\n useEffect(() => () => debouncedSearch.cancel(), [debouncedSearch]);\n\n const handleSearchChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const value = e.target.value;\n setSearch(value);\n debouncedSearch.maybeExecute(value);\n },\n [debouncedSearch]\n );\n\n const handleLevelToggle = useCallback(\n (level: LogLevel) => {\n const next = new Set(levelFilter);\n if (next.has(level)) {\n next.delete(level);\n } else {\n next.add(level);\n }\n onLevelFilterChange(next);\n },\n [levelFilter, onLevelFilterChange]\n );\n\n return (\n <div\n data-testid=\"toolbar\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n flexShrink: 0,\n gap: theme.spacing.xl,\n height: \"100%\",\n minWidth: \"200px\",\n overflow: \"auto\",\n padding: `${theme.spacing.lg} ${theme.spacing.xl}`,\n }}\n >\n {/* Log count */}\n <div style={{ textAlign: \"center\" }}>\n <span\n style={{\n color: theme.colors.textPrimary,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.lg,\n fontWeight: 600,\n }}\n >\n {filteredCount === totalCount ? `${totalCount}` : `${filteredCount} / ${totalCount}`}\n </span>\n <div style={{ color: theme.colors.textMuted, fontSize: theme.fontSize.sm }}>logs</div>\n </div>\n\n {/* Actions */}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: theme.spacing.sm }}>\n <div style={sectionLabelStyle}>Actions</div>\n <button\n data-lt-interactive=\"\"\n onClick={paused ? onResume : onPause}\n style={{\n ...buttonStyle,\n background: paused ? theme.colors.levels.warning.badge : buttonStyle.background,\n color: paused ? theme.colors.white : buttonStyle.color,\n }}\n title={paused ? \"Resume live updates\" : \"Pause live updates\"}\n type=\"button\"\n >\n <span style={{ alignItems: \"center\", display: \"inline-flex\", gap: \"6px\" }}>\n {paused ? <PlayIcon size={12} /> : <PauseIcon size={12} />}\n {paused ? \"Resume\" : \"Pause\"}\n </span>\n </button>\n\n <button\n data-lt-interactive=\"\"\n onClick={onClear}\n style={buttonStyle}\n title=\"Clear all logs\"\n type=\"button\"\n >\n <span style={{ alignItems: \"center\", display: \"inline-flex\", gap: \"6px\" }}>\n <Trash2Icon size={12} />\n Clear\n </span>\n </button>\n </div>\n\n {/* Level filters */}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: theme.spacing.sm }}>\n <div style={{ alignItems: \"center\", display: \"flex\", justifyContent: \"space-between\" }}>\n <div style={sectionLabelStyle}>Levels</div>\n {levelFilter.size > 0 && (\n <button\n data-lt-clear-btn=\"\"\n onClick={() => onLevelFilterChange(new Set())}\n style={{\n background: \"none\",\n border: \"none\",\n color: theme.colors.textMuted,\n cursor: \"pointer\",\n fontSize: \"9px\",\n padding: 0,\n transition: \"color 0.1s\",\n }}\n title=\"Show all levels\"\n type=\"button\"\n >\n clear\n </button>\n )}\n </div>\n <div\n style={{\n display: \"grid\",\n gap: theme.spacing.xs,\n gridTemplateColumns: \"repeat(3, 1fr)\",\n }}\n >\n {LOG_LEVELS.map((level) => (\n <LevelToggle\n active={levelFilter.size === 0 || levelFilter.has(level)}\n key={level}\n level={level}\n onToggle={handleLevelToggle}\n />\n ))}\n </div>\n </div>\n\n {/* Category filter (multi-select combobox) */}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: theme.spacing.sm }}>\n <div style={{ alignItems: \"center\", display: \"flex\", justifyContent: \"space-between\" }}>\n <div style={sectionLabelStyle}>Category</div>\n {categoryFilter.length > 0 && (\n <button\n data-lt-clear-btn=\"\"\n onClick={() => onCategoryFilterChange([])}\n style={{\n background: \"none\",\n border: \"none\",\n color: theme.colors.textMuted,\n cursor: \"pointer\",\n fontSize: \"9px\",\n padding: 0,\n transition: \"color 0.1s\",\n }}\n title=\"Clear all categories\"\n type=\"button\"\n >\n clear\n </button>\n )}\n </div>\n {categories.length === 0 ? (\n <Popover.Root>\n <Popover.Trigger\n delay={300}\n nativeButton={false}\n openOnHover\n render={\n <div\n data-testid=\"category-input\"\n style={{\n ...inputStyle,\n color: theme.colors.textMuted,\n cursor: \"not-allowed\",\n opacity: 0.5,\n }}\n >\n Category…\n </div>\n }\n />\n <Popover.Portal>\n <Popover.Positioner sideOffset={6} style={{ zIndex: 999_999 }}>\n <Popover.Popup\n style={{\n background: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: theme.radius.md,\n boxShadow: \"0 8px 24px rgba(0,0,0,0.5)\",\n fontSize: theme.fontSize.sm,\n maxWidth: \"200px\",\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n }}\n >\n <p style={{ color: theme.colors.textMuted, lineHeight: 1.4, margin: 0 }}>\n Waiting for logs to populate available categories.\n </p>\n </Popover.Popup>\n </Popover.Positioner>\n </Popover.Portal>\n </Popover.Root>\n ) : (\n <Combobox.Root\n items={categories}\n multiple\n onValueChange={onCategoryFilterChange}\n value={categoryFilter}\n >\n <div\n style={{\n background: theme.colors.surface,\n border: `1px solid ${theme.colors.borderInput}`,\n borderRadius: theme.radius.md,\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"3px\",\n minHeight: \"28px\",\n padding: \"3px\",\n transition: \"border-color 0.15s\",\n }}\n >\n {categoryFilter.map((cat) => (\n <span\n key={cat}\n style={{\n alignItems: \"center\",\n background: \"rgba(99, 102, 241, 0.15)\",\n border: \"1px solid rgba(99, 102, 241, 0.3)\",\n borderRadius: theme.radius.sm,\n color: theme.colors.accentHover,\n display: \"inline-flex\",\n fontFamily: theme.fontFamily.mono,\n fontSize: \"9px\",\n gap: \"3px\",\n lineHeight: 1,\n padding: \"1px 4px\",\n }}\n >\n {cat}\n <button\n data-lt-chip-remove=\"\"\n onClick={() => onCategoryFilterChange(categoryFilter.filter((c) => c !== cat))}\n style={{\n background: \"none\",\n border: \"none\",\n color: theme.colors.textMuted,\n cursor: \"pointer\",\n fontSize: \"7px\",\n lineHeight: 1,\n opacity: 0.7,\n padding: 0,\n transition: \"color 0.1s, opacity 0.1s\",\n }}\n title={`Remove ${cat}`}\n type=\"button\"\n >\n <XIcon size={7} />\n </button>\n </span>\n ))}\n <Combobox.Input\n data-testid=\"category-input\"\n placeholder={categoryFilter.length === 0 ? \"Category…\" : \"\"}\n style={{\n background: \"transparent\",\n border: \"none\",\n color: theme.colors.textPrimary,\n flex: 1,\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.md,\n minWidth: \"40px\",\n outline: \"none\",\n padding: `${theme.spacing.xs} ${theme.spacing.sm}`,\n }}\n />\n </div>\n <Combobox.Portal>\n <Combobox.Positioner style={{ zIndex: 999_999 }}>\n <Combobox.Popup\n style={{\n background: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: theme.radius.md,\n boxShadow: \"0 8px 24px rgba(0,0,0,0.5)\",\n maxHeight: \"200px\",\n overflow: \"auto\",\n padding: theme.spacing.xs,\n }}\n >\n <Combobox.List>\n {(item, index) => (\n <Combobox.Item\n index={index}\n key={String(item)}\n style={{\n alignItems: \"center\",\n borderRadius: theme.radius.sm,\n color: theme.colors.textPrimary,\n cursor: \"pointer\",\n display: \"flex\",\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.md,\n gap: theme.spacing.sm,\n justifyContent: \"space-between\",\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n transition: \"background 0.1s\",\n }}\n value={item}\n >\n <span style={{ flex: 1, overflow: \"hidden\", textOverflow: \"ellipsis\" }}>\n {String(item)}\n </span>\n <Combobox.ItemIndicator>\n <CheckIcon color={theme.colors.accent} size={14} />\n </Combobox.ItemIndicator>\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Popup>\n </Combobox.Positioner>\n </Combobox.Portal>\n </Combobox.Root>\n )}\n </div>\n\n {/* Text search */}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: theme.spacing.sm }}>\n <div style={sectionLabelStyle}>Search</div>\n <input\n data-testid=\"search-input\"\n onChange={handleSearchChange}\n placeholder=\"Search…\"\n style={inputStyle}\n type=\"text\"\n value={search}\n />\n </div>\n </div>\n );\n};\n","import { defaultLogStore, type LogStore } from \"../store\";\nimport { LogTapeDevtoolsPlugin } from \"./logtape-devtools-plugin\";\n\nexport interface LogTapeDevtoolsPluginOptions {\n /** Whether to open the devtools panel by default. Default: true */\n defaultOpen?: boolean;\n /** Plugin display name. Default: \"LogTape\" */\n name?: string;\n /** Custom log store instance. Must match the store passed to createDevtoolsSink. */\n store?: LogStore;\n}\n\n/**\n * Creates a TanStack DevTools plugin config object for LogTape.\n *\n * Usage:\n * ```tsx\n * import { TanStackDevtools } from \"@tanstack/react-devtools\";\n * import { createLogTapeDevtoolsPlugin } from \"@mugenlabs/logtape-devtools\";\n *\n * <TanStackDevtools plugins={[createLogTapeDevtoolsPlugin()]} />\n * ```\n */\nexport const createLogTapeDevtoolsPlugin = (options?: LogTapeDevtoolsPluginOptions) => ({\n defaultOpen: options?.defaultOpen ?? true,\n id: \"logtape-devtools-plugin\",\n name: options?.name ?? \"LogTape\",\n render: <LogTapeDevtoolsPlugin store={options?.store ?? defaultLogStore} />,\n});\n","import {\n createLogTapeDevtoolsPlugin,\n type LogTapeDevtoolsPluginOptions,\n} from \"./plugin/create-logtape-devtools-plugin\";\nimport { createDevtoolsSink, type DevtoolsSinkOptions } from \"./sink\";\nimport { defaultLogStore, type LogStore } from \"./store\";\n\n/** Options for {@link createLogTapeDevtools}. */\nexport interface CreateLogTapeDevtoolsOptions {\n /** Plugin options, minus `store` — the shared store is wired automatically. */\n plugin?: Omit<LogTapeDevtoolsPluginOptions, \"store\">;\n /** Sink options, minus `store` — the shared store is wired automatically. */\n sink?: Omit<DevtoolsSinkOptions, \"store\">;\n /** Store shared by the sink and the plugin. Defaults to the shared store. */\n store?: LogStore;\n}\n\n/**\n * Creates a LogTape sink and its matching TanStack DevTools plugin, both wired\n * to the same store.\n *\n * Use this instead of calling `createDevtoolsSink` and\n * `createLogTapeDevtoolsPlugin` separately when you do not need to hold on to\n * the store yourself.\n *\n * ```tsx\n * import { configure, getLogger } from \"@logtape/logtape\";\n * import { TanStackDevtools } from \"@tanstack/react-devtools\";\n * import { createLogTapeDevtools } from \"@mugenlabs/logtape-devtools\";\n *\n * const { sink, plugin } = createLogTapeDevtools();\n *\n * await configure({\n * sinks: { devtools: sink },\n * loggers: [{ category: [], lowestLevel: \"trace\", sinks: [\"devtools\"] }],\n * });\n *\n * function App() {\n * return <TanStackDevtools plugins={[plugin]} />;\n * }\n * ```\n */\nexport function createLogTapeDevtools(options?: CreateLogTapeDevtoolsOptions) {\n const store = options?.store ?? defaultLogStore;\n\n return {\n plugin: createLogTapeDevtoolsPlugin({ ...options?.plugin, store }),\n sink: createDevtoolsSink({ ...options?.sink, store }),\n };\n}\n"],"mappings":"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","names":["useCallback","useMemo","useState","useCallback","jsx","jsx","jsxs","jsx","jsxs","categoryStyle","messageStyle","callerStyle","jsx","containerStyle","useCallback","useCallback","useEffect","jsx","jsxs","useEffect","useCallback","jsx","jsxs","useState","useCallback","useMemo","jsx"]}
|
|
1
|
+
{"version":3,"sources":["../src/plugin/logtape-devtools-plugin.tsx","../src/plugin/components/log-list.tsx","../src/plugin/theme.ts","../src/plugin/components/log-row.tsx","../src/plugin/format.ts","../src/plugin/icons.tsx","../src/plugin/components/level-badge.tsx","../src/plugin/components/log-detail.tsx","../src/plugin/components/toolbar.tsx","../src/plugin/create-logtape-devtools-plugin.tsx","../src/create-logtape-devtools.ts"],"sourcesContent":["import { useCallback, useMemo, useState, useSyncExternalStore } from \"react\";\nimport type { LogStore } from \"../store\";\nimport type { DevtoolsLogRecord, LogLevel } from \"../types\";\nimport { LogList } from \"./components/log-list\";\nimport { Toolbar } from \"./components/toolbar\";\nimport { PanelLeftCloseIcon, PanelLeftOpenIcon, PauseIcon, PlayIcon, Trash2Icon } from \"./icons\";\nimport { theme } from \"./theme\";\n\ninterface Props {\n store: LogStore;\n}\n\nconst rootStyle = {\n background: theme.colors.background,\n color: theme.colors.textPrimary,\n display: \"flex\",\n flexDirection: \"row\",\n fontFamily: theme.fontFamily.sans,\n height: \"100%\",\n overflow: \"hidden\",\n} as const;\n\nconst sidebarStyle = {\n flexShrink: 0,\n overflow: \"hidden\",\n transition: \"width 0.2s ease, opacity 0.15s ease\",\n} as const;\n\nconst mainStyle = {\n display: \"flex\",\n flex: 1,\n flexDirection: \"column\",\n overflow: \"hidden\",\n} as const;\n\nconst topBarStyle = {\n alignItems: \"center\",\n borderBottom: `1px solid ${theme.colors.border}`,\n display: \"flex\",\n flexShrink: 0,\n gap: theme.spacing.lg,\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n} as const;\n\nconst sidebarToggleStyle = {\n background: \"none\",\n border: \"none\",\n color: theme.colors.textMuted,\n cursor: \"pointer\",\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.md,\n padding: `${theme.spacing.xs} ${theme.spacing.sm}`,\n transition: \"color 0.1s\",\n} as const;\n\nconst iconLabelStyle = { alignItems: \"center\", display: \"inline-flex\", gap: \"6px\" } as const;\n\nconst countStyle = {\n color: theme.colors.textMuted,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.sm,\n} as const;\n\nconst compactButtonStyle = {\n background: theme.colors.surfaceHover,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: theme.radius.sm,\n color: theme.colors.textPrimary,\n cursor: \"pointer\",\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.sm,\n padding: `${theme.spacing.xs} ${theme.spacing.sm}`,\n transition: \"filter 0.1s\",\n} as const;\n\nexport const LogTapeDevtoolsPlugin = ({ store }: Props) => {\n const allRecords = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);\n\n const [paused, setPaused] = useState(false);\n const [pausedRecords, setPausedRecords] = useState<DevtoolsLogRecord[]>([]);\n const [levelFilter, setLevelFilter] = useState<Set<LogLevel>>(new Set());\n const [categoryFilter, setCategoryFilter] = useState<string[]>([]);\n const [searchText, setSearchText] = useState(\"\");\n const [expandedId, setExpandedId] = useState<string | null>(null);\n const [sidebarOpen, setSidebarOpen] = useState(true);\n\n const handlePause = useCallback(() => {\n setPaused(true);\n setPausedRecords(store.getSnapshot());\n }, [store]);\n\n const handleResume = useCallback(() => {\n setPaused(false);\n setPausedRecords([]);\n }, []);\n\n const handleClear = useCallback(() => {\n store.clear();\n setPausedRecords([]);\n setExpandedId(null);\n }, [store]);\n\n const records = paused ? pausedRecords : allRecords;\n\n const uniqueCategories = useMemo(() => {\n const seen = new Set<string>();\n for (const r of records) {\n seen.add(r.category.join(\".\"));\n }\n return Array.from(seen).sort();\n }, [records]);\n\n const filteredRecords = useMemo(\n () =>\n records.filter((r) => {\n if (levelFilter.size > 0 && !levelFilter.has(r.level)) {\n return false;\n }\n if (categoryFilter.length > 0 && !categoryFilter.includes(r.category.join(\".\"))) {\n return false;\n }\n if (searchText) {\n const term = searchText.toLowerCase();\n if (\n !(\n r.messageText.toLowerCase().includes(term) ||\n r.category.join(\".\").toLowerCase().includes(term)\n )\n ) {\n return false;\n }\n }\n return true;\n }),\n [records, levelFilter, categoryFilter, searchText]\n );\n\n return (\n <div style={rootStyle}>\n <div\n style={{\n ...sidebarStyle,\n borderRight: sidebarOpen ? `1px solid ${theme.colors.border}` : \"none\",\n opacity: sidebarOpen ? 1 : 0,\n width: sidebarOpen ? \"200px\" : \"0px\",\n }}\n >\n <Toolbar\n categories={uniqueCategories}\n categoryFilter={categoryFilter}\n filteredCount={filteredRecords.length}\n levelFilter={levelFilter}\n onCategoryFilterChange={setCategoryFilter}\n onClear={handleClear}\n onLevelFilterChange={setLevelFilter}\n onPause={handlePause}\n onResume={handleResume}\n onSearchTextChange={setSearchText}\n paused={paused}\n totalCount={records.length}\n />\n </div>\n\n <div style={mainStyle}>\n {/* Sidebar toggle bar */}\n <div style={topBarStyle}>\n <button\n aria-expanded={sidebarOpen}\n data-lt-clear-btn=\"\"\n onClick={() => setSidebarOpen((v) => !v)}\n style={sidebarToggleStyle}\n title={sidebarOpen ? \"Collapse sidebar\" : \"Expand sidebar\"}\n type=\"button\"\n >\n <span style={iconLabelStyle}>\n {sidebarOpen ? <PanelLeftCloseIcon size={14} /> : <PanelLeftOpenIcon size={14} />}\n {sidebarOpen ? \"Hide controls\" : \"Show controls\"}\n </span>\n </button>\n\n {!sidebarOpen && (\n <>\n <span style={countStyle}>\n {filteredRecords.length === records.length\n ? `${records.length} logs`\n : `${filteredRecords.length} / ${records.length} logs`}\n </span>\n <button\n data-lt-interactive=\"\"\n onClick={paused ? handleResume : handlePause}\n style={{\n ...compactButtonStyle,\n background: paused\n ? theme.colors.levels.warning.badge\n : compactButtonStyle.background,\n color: paused ? theme.colors.white : compactButtonStyle.color,\n }}\n title={paused ? \"Resume\" : \"Pause\"}\n type=\"button\"\n >\n {paused ? <PlayIcon size={12} /> : <PauseIcon size={12} />}\n </button>\n <button\n data-lt-interactive=\"\"\n onClick={handleClear}\n style={compactButtonStyle}\n title=\"Clear\"\n type=\"button\"\n >\n <Trash2Icon size={12} />\n </button>\n </>\n )}\n </div>\n <LogList\n autoScroll={!paused}\n expandedId={expandedId}\n onToggle={setExpandedId}\n records={filteredRecords}\n />\n </div>\n </div>\n );\n};\n","import { useVirtualizer } from \"@tanstack/react-virtual\";\nimport { useCallback, useEffect, useRef } from \"react\";\nimport type { DevtoolsLogRecord } from \"../../types\";\nimport { theme } from \"../theme\";\nimport { LogRow } from \"./log-row\";\n\ninterface LogListProps {\n autoScroll: boolean;\n expandedId: string | null;\n onToggle: (id: string | null) => void;\n records: DevtoolsLogRecord[];\n}\n\n/** Collapsed row height — rows are measured after mount, this is only the seed. */\nconst ESTIMATED_ROW_HEIGHT = 28;\nconst OVERSCAN = 10;\n/** Distance from the bottom (px) still considered \"stuck to the bottom\". */\nconst STICK_THRESHOLD = 40;\n\nconst emptyStyle = {\n alignItems: \"center\",\n color: theme.colors.textMuted,\n display: \"flex\",\n flex: 1,\n fontSize: theme.fontSize.lg,\n justifyContent: \"center\",\n} as const;\n\nconst containerStyle = {\n flex: 1,\n overflow: \"auto\",\n scrollbarColor: `${theme.colors.scrollbar} transparent`,\n scrollbarWidth: \"thin\",\n} as const;\n\nexport const LogList = ({ records, expandedId, onToggle, autoScroll }: LogListProps) => {\n const containerRef = useRef<HTMLDivElement>(null);\n const isAtBottomRef = useRef(true);\n\n const virtualizer = useVirtualizer({\n count: records.length,\n estimateSize: () => ESTIMATED_ROW_HEIGHT,\n // The virtualizer can ask for keys of indices from a previous, longer list\n // (after clear or a narrowing filter), so never assume the index exists.\n getItemKey: (index) => records[index]?.id ?? index,\n getScrollElement: () => containerRef.current,\n overscan: OVERSCAN,\n });\n\n const recordCount = records.length;\n // Key on the newest record rather than the count: once the ring buffer is\n // full the count stays constant while the contents keep changing.\n const lastRecordId = records[recordCount - 1]?.id;\n useEffect(() => {\n if (lastRecordId === undefined || !(autoScroll && isAtBottomRef.current)) {\n return;\n }\n virtualizer.scrollToIndex(recordCount - 1, { align: \"end\" });\n }, [autoScroll, lastRecordId, recordCount, virtualizer]);\n\n // The scroll container unmounts while the list is empty; treat the next\n // batch of logs as a fresh start instead of keeping a stale \"scrolled up\".\n useEffect(() => {\n if (recordCount === 0) {\n isAtBottomRef.current = true;\n }\n }, [recordCount]);\n\n const handleScroll = useCallback(() => {\n const el = containerRef.current;\n if (!el) {\n return;\n }\n isAtBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_THRESHOLD;\n }, []);\n\n if (records.length === 0) {\n return (\n <div data-testid=\"log-list-empty\" style={emptyStyle}>\n No logs yet\n </div>\n );\n }\n\n const virtualItems = virtualizer.getVirtualItems();\n\n return (\n <div data-testid=\"log-list\" onScroll={handleScroll} ref={containerRef} style={containerStyle}>\n {/* Spacer sized to the full list, with only the visible window rendered */}\n <div\n style={{\n height: `${virtualizer.getTotalSize()}px`,\n position: \"relative\",\n width: \"100%\",\n }}\n >\n <div\n style={{\n left: 0,\n position: \"absolute\",\n top: 0,\n transform: `translateY(${virtualItems[0]?.start ?? 0}px)`,\n width: \"100%\",\n }}\n >\n {virtualItems.map((virtualItem) => {\n const record = records[virtualItem.index];\n return (\n <div\n data-index={virtualItem.index}\n key={virtualItem.key}\n ref={virtualizer.measureElement}\n >\n <LogRow expanded={expandedId === record.id} onToggle={onToggle} record={record} />\n </div>\n );\n })}\n </div>\n </div>\n </div>\n );\n};\n","export const theme = {\n colors: {\n accent: \"#6366f1\",\n accentHover: \"#818cf8\",\n background: \"#1e1e1e\",\n border: \"#333\",\n borderFocus: \"#6366f1\",\n borderInput: \"#444\",\n\n levels: {\n // Badge colours keep at least 4.5:1 contrast against white badge text.\n debug: { badge: \"#2f7a2f\", bg: \"#1a2a1a\", color: \"#88bb88\" },\n error: { badge: \"#dc2626\", bg: \"#2a1a1a\", color: \"#ef4444\" },\n fatal: { badge: \"#b91c1c\", bg: \"#3a0a0a\", color: \"#ff6b6b\" },\n info: { badge: \"#2563eb\", bg: \"#1a2a3a\", color: \"#60a5fa\" },\n trace: { badge: \"#5555aa\", bg: \"#1a1a2e\", color: \"#8888bb\" },\n warning: { badge: \"#eab308\", bg: \"#2a2a1a\", color: \"#facc15\" },\n },\n\n scrollbar: \"#444\",\n scrollbarHover: \"#555\",\n surface: \"#1a1a1a\",\n surfaceHover: \"#2a2a2a\",\n surfaceSelected: \"#2a2a3a\",\n textDimmed: \"#555\",\n textMuted: \"#666\",\n\n textPrimary: \"#e0e0e0\",\n textSecondary: \"#888\",\n white: \"#fff\",\n },\n\n fontFamily: {\n mono: 'ui-monospace, \"Cascadia Code\", \"Source Code Pro\", Menlo, Consolas, monospace',\n sans: 'system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif',\n },\n\n fontSize: {\n base: \"12px\",\n lg: \"13px\",\n md: \"11px\",\n sm: \"10px\",\n xs: \"9px\",\n },\n\n radius: {\n lg: \"6px\",\n md: \"4px\",\n pill: \"10px\",\n sm: \"3px\",\n },\n\n spacing: {\n lg: \"8px\",\n md: \"6px\",\n sm: \"4px\",\n xl: \"12px\",\n xs: \"2px\",\n xxl: \"16px\",\n },\n} as const;\n\nexport type LogLevelColors = (typeof theme.colors.levels)[keyof typeof theme.colors.levels];\n\n/**\n * Colors for a level, falling back to \"info\" for levels this version does not\n * know about (the LogTape peer range allows newer levels to appear).\n */\nexport const getLevelColors = (level: string): LogLevelColors =>\n (theme.colors.levels as Record<string, LogLevelColors | undefined>)[level] ??\n theme.colors.levels.info;\n","import { memo, useCallback } from \"react\";\nimport type { DevtoolsLogRecord } from \"../../types\";\nimport { formatTime } from \"../format\";\nimport { ChevronRightIcon } from \"../icons\";\nimport { getLevelColors, theme } from \"../theme\";\nimport { LevelBadge } from \"./level-badge\";\nimport { LogDetail } from \"./log-detail\";\n\ninterface LogRowProps {\n expanded: boolean;\n onToggle: (id: string | null) => void;\n record: DevtoolsLogRecord;\n}\n\nconst rowStyle = {\n alignItems: \"center\",\n border: \"none\",\n borderBottom: `1px solid ${theme.colors.border}`,\n cursor: \"pointer\",\n display: \"flex\",\n gap: theme.spacing.lg,\n padding: `${theme.spacing.md} ${theme.spacing.xl}`,\n textAlign: \"left\",\n transition: \"background 0.1s, filter 0.1s\",\n width: \"100%\",\n} as const;\n\nconst timestampStyle = {\n color: theme.colors.textDimmed,\n flexShrink: 0,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.md,\n} as const;\n\nconst categoryStyle = {\n color: theme.colors.accent,\n flexShrink: 0,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.md,\n maxWidth: \"180px\",\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n} as const;\n\nconst messageStyle = {\n color: theme.colors.textPrimary,\n flex: 1,\n fontSize: theme.fontSize.base,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n} as const;\n\nconst callerStyle = {\n color: theme.colors.textDimmed,\n flexShrink: 0,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.sm,\n maxWidth: \"200px\",\n opacity: 0.7,\n overflow: \"hidden\",\n textOverflow: \"ellipsis\",\n whiteSpace: \"nowrap\",\n} as const;\n\nconst chevronStyle = {\n color: theme.colors.textDimmed,\n display: \"flex\",\n flexShrink: 0,\n transition: \"transform 0.15s\",\n} as const;\n\nconst LogRowComponent = ({ record, expanded, onToggle }: LogRowProps) => {\n const levelColors = getLevelColors(record.level);\n\n const handleClick = useCallback(() => {\n onToggle(expanded ? null : record.id);\n }, [expanded, onToggle, record.id]);\n\n return (\n <div>\n <button\n aria-expanded={expanded}\n data-lt-interactive=\"\"\n data-testid=\"log-row\"\n onClick={handleClick}\n style={{ ...rowStyle, background: expanded ? levelColors.bg : \"transparent\" }}\n type=\"button\"\n >\n {/* Timestamp */}\n <span style={timestampStyle}>{formatTime(record.timestamp)}</span>\n\n {/* Level badge */}\n <LevelBadge level={record.level} />\n\n {/* Category */}\n <span style={categoryStyle}>{record.category.join(\".\")}</span>\n\n {/* Message */}\n <span style={messageStyle}>{record.messageText}</span>\n\n {/* Caller (right-aligned) */}\n {record.caller ? <span style={callerStyle}>{record.caller}</span> : null}\n\n {/* Expand indicator */}\n <span style={{ ...chevronStyle, transform: expanded ? \"rotate(90deg)\" : \"rotate(0deg)\" }}>\n <ChevronRightIcon size={12} />\n </span>\n </button>\n\n {expanded ? <LogDetail record={record} /> : null}\n </div>\n );\n};\n\n/** Memoised so a new batch of records only re-renders the rows that changed. */\nexport const LogRow = memo(LogRowComponent);\n","// Built once: constructing an Intl formatter per row per render is measurably slow.\nlet timeFormatter: Intl.DateTimeFormat | undefined;\n\n/** Formats a timestamp as a 24-hour `HH:MM:SS` local time string. */\nexport function formatTime(ts: number): string {\n timeFormatter ??= new Intl.DateTimeFormat(undefined, {\n hour: \"2-digit\",\n hour12: false,\n minute: \"2-digit\",\n second: \"2-digit\",\n });\n return timeFormatter.format(new Date(ts));\n}\n","/**\n * Inline SVG icons based on Lucide (https://lucide.dev) — ISC License.\n * Using inline SVGs avoids adding lucide-react as a runtime dependency.\n */\n\ninterface IconProps {\n color?: string;\n size?: number;\n style?: React.CSSProperties;\n}\n\nconst defaults = { color: \"currentColor\", size: 14 } as const;\n\nconst Svg = ({\n size = defaults.size,\n color = defaults.color,\n style,\n children,\n}: IconProps & { children: React.ReactNode }) => (\n <svg\n aria-hidden=\"true\"\n fill=\"none\"\n height={size}\n stroke={color}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n style={style}\n viewBox=\"0 0 24 24\"\n width={size}\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n {children}\n </svg>\n);\n\nexport const PlayIcon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\" />\n </Svg>\n);\n\nexport const PauseIcon = (props: IconProps) => (\n <Svg {...props}>\n <rect height={18} rx={1} width={5} x={14} y={3} />\n <rect height={18} rx={1} width={5} x={5} y={3} />\n </Svg>\n);\n\nexport const XIcon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"M18 6 6 18\" />\n <path d=\"m6 6 12 12\" />\n </Svg>\n);\n\nexport const CheckIcon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"M20 6 9 17l-5-5\" />\n </Svg>\n);\n\nexport const ChevronRightIcon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"m9 18 6-6-6-6\" />\n </Svg>\n);\n\nexport const PanelLeftCloseIcon = (props: IconProps) => (\n <Svg {...props}>\n <rect height={18} rx={2} width={18} x={3} y={3} />\n <path d=\"M9 3v18\" />\n <path d=\"m16 15-3-3 3-3\" />\n </Svg>\n);\n\nexport const PanelLeftOpenIcon = (props: IconProps) => (\n <Svg {...props}>\n <rect height={18} rx={2} width={18} x={3} y={3} />\n <path d=\"M9 3v18\" />\n <path d=\"m14 9 3 3-3 3\" />\n </Svg>\n);\n\nexport const Trash2Icon = (props: IconProps) => (\n <Svg {...props}>\n <path d=\"M10 11v6\" />\n <path d=\"M14 11v6\" />\n <path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6\" />\n <path d=\"M3 6h18\" />\n <path d=\"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" />\n </Svg>\n);\n","import type { LogLevel } from \"../../types\";\nimport { getLevelColors, theme } from \"../theme\";\n\nconst LEVEL_LABELS: Record<LogLevel, string> = {\n debug: \"DBG\",\n error: \"ERR\",\n fatal: \"FTL\",\n info: \"INF\",\n trace: \"TRC\",\n warning: \"WRN\",\n};\n\nexport const LevelBadge = ({ level }: { level: LogLevel }) => {\n const colors = getLevelColors(level);\n const label = (LEVEL_LABELS as Record<string, string | undefined>)[level] ?? level.toUpperCase();\n\n return (\n <span\n style={{\n background: colors.badge,\n borderRadius: theme.radius.sm,\n color: theme.colors.white,\n display: \"inline-block\",\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.sm,\n fontWeight: 600,\n lineHeight: 1,\n minWidth: \"28px\",\n padding: `${theme.spacing.xs} ${theme.spacing.sm}`,\n textAlign: \"center\",\n textTransform: \"uppercase\",\n }}\n >\n {label}\n </span>\n );\n};\n","import { safeStringify } from \"../../safe-json\";\nimport type { DevtoolsLogRecord } from \"../../types\";\nimport { formatTime } from \"../format\";\nimport { getLevelColors, theme } from \"../theme\";\n\nconst detailRowStyle = {\n display: \"flex\",\n gap: theme.spacing.lg,\n marginBottom: theme.spacing.md,\n} as const;\n\nconst detailLabelStyle = {\n color: theme.colors.textMuted,\n flexShrink: 0,\n fontSize: theme.fontSize.md,\n minWidth: \"72px\",\n textAlign: \"right\",\n} as const;\n\nconst detailValueStyle = {\n fontSize: theme.fontSize.base,\n wordBreak: \"break-all\",\n} as const;\n\nconst containerStyle = {\n borderTop: `1px solid ${theme.colors.border}`,\n padding: `${theme.spacing.lg} ${theme.spacing.xl}`,\n} as const;\n\nconst categoryStyle = {\n color: theme.colors.accent,\n fontFamily: theme.fontFamily.mono,\n} as const;\n\nconst callerStyle = {\n color: theme.colors.textSecondary,\n fontFamily: theme.fontFamily.mono,\n} as const;\n\nconst messageStyle = { color: theme.colors.textPrimary } as const;\n\nconst propertiesStyle = {\n background: \"rgba(0,0,0,0.3)\",\n borderRadius: theme.radius.md,\n color: theme.colors.textPrimary,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.md,\n margin: 0,\n maxHeight: \"200px\",\n overflow: \"auto\",\n padding: theme.spacing.lg,\n whiteSpace: \"pre-wrap\",\n wordBreak: \"break-all\",\n} as const;\n\nconst DetailRow = ({ label, children }: { label: string; children: React.ReactNode }) => (\n <div style={detailRowStyle}>\n <span style={detailLabelStyle}>{label}</span>\n <span style={detailValueStyle}>{children}</span>\n </div>\n);\n\nexport const LogDetail = ({ record }: { record: DevtoolsLogRecord }) => {\n const levelColors = getLevelColors(record.level);\n const hasProperties = Object.keys(record.properties).length > 0;\n\n return (\n <div data-testid=\"log-detail\" style={{ ...containerStyle, background: levelColors.bg }}>\n <DetailRow label=\"Time\">{formatTime(record.timestamp)}</DetailRow>\n <DetailRow label=\"Level\">\n <span style={{ color: levelColors.color, fontWeight: 600 }}>{record.level}</span>\n </DetailRow>\n <DetailRow label=\"Category\">\n <span style={categoryStyle}>{record.category.join(\".\")}</span>\n </DetailRow>\n {record.caller ? (\n <DetailRow label=\"Caller\">\n <span style={callerStyle}>{record.caller}</span>\n </DetailRow>\n ) : null}\n <DetailRow label=\"Message\">\n <span style={messageStyle}>{record.messageText}</span>\n </DetailRow>\n\n {hasProperties && (\n <DetailRow label=\"Data\">\n <pre style={propertiesStyle}>{safeStringify(record.properties, 2)}</pre>\n </DetailRow>\n )}\n </div>\n );\n};\n","import { Combobox } from \"@base-ui-components/react/combobox\";\nimport { Popover } from \"@base-ui-components/react/popover\";\nimport { Debouncer } from \"@tanstack/pacer\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { LOG_LEVELS, type LogLevel } from \"../../types\";\nimport { CheckIcon, PauseIcon, PlayIcon, Trash2Icon, XIcon } from \"../icons\";\nimport { theme } from \"../theme\";\n\ninterface ToolbarProps {\n categories: string[];\n categoryFilter: string[];\n filteredCount: number;\n levelFilter: Set<LogLevel>;\n onCategoryFilterChange: (value: string[]) => void;\n onClear: () => void;\n onLevelFilterChange: (levels: Set<LogLevel>) => void;\n onPause: () => void;\n onResume: () => void;\n /** Called with the debounced search term. The toolbar owns the input value. */\n onSearchTextChange: (value: string) => void;\n paused: boolean;\n totalCount: number;\n}\n\n// Inject hover styles once (scoped via data attribute to avoid leaking)\nconst HOVER_STYLE_ID = \"logtape-devtools-hover\";\nfunction useHoverStyles() {\n useEffect(() => {\n if (document.getElementById(HOVER_STYLE_ID)) {\n return;\n }\n const style = document.createElement(\"style\");\n style.id = HOVER_STYLE_ID;\n style.textContent = `\n [data-lt-interactive]:hover {\n filter: brightness(1.25);\n }\n [data-lt-interactive]:active {\n filter: brightness(0.9);\n }\n [data-lt-interactive]:focus-visible,\n [data-lt-input]:focus-visible {\n outline: 2px solid ${theme.colors.accent};\n outline-offset: 1px;\n }\n [data-lt-clear-btn]:hover {\n color: ${theme.colors.textPrimary} !important;\n }\n [data-lt-chip-remove]:hover {\n opacity: 1 !important;\n color: ${theme.colors.white} !important;\n }\n `;\n document.head.appendChild(style);\n }, []);\n}\n\nconst buttonStyle = {\n background: theme.colors.surfaceHover,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: theme.radius.md,\n color: theme.colors.textPrimary,\n cursor: \"pointer\",\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.md,\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n transition: \"filter 0.1s, background 0.1s, border-color 0.1s\",\n width: \"100%\",\n} as const;\n\nconst inputStyle = {\n background: theme.colors.surface,\n border: `1px solid ${theme.colors.borderInput}`,\n borderRadius: theme.radius.md,\n color: theme.colors.textPrimary,\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.md,\n outline: \"none\",\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n transition: \"border-color 0.15s\",\n width: \"100%\",\n} as const;\n\nconst sectionLabelStyle = {\n color: theme.colors.textMuted,\n fontSize: \"9px\",\n fontWeight: 600,\n letterSpacing: \"0.05em\",\n textTransform: \"uppercase\",\n} as const;\n\nconst LevelToggle = ({\n level,\n active,\n pressed,\n onToggle,\n}: {\n level: LogLevel;\n /** Visual state: lit when the level is included (all levels when no filter is set). */\n active: boolean;\n /** Semantic state: whether the user has explicitly selected this level. */\n pressed: boolean;\n onToggle: (level: LogLevel) => void;\n}) => {\n const colors = theme.colors.levels[level];\n const handleClick = useCallback(() => onToggle(level), [level, onToggle]);\n\n return (\n <button\n aria-pressed={pressed}\n data-lt-interactive=\"\"\n data-testid={`level-toggle-${level}`}\n onClick={handleClick}\n style={{\n background: active ? colors.badge : \"transparent\",\n border: `1px solid ${active ? colors.badge : theme.colors.border}`,\n borderRadius: theme.radius.sm,\n color: active ? theme.colors.white : colors.color,\n cursor: \"pointer\",\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.sm,\n fontWeight: 600,\n lineHeight: 1,\n minWidth: \"30px\",\n opacity: active ? 1 : 0.5,\n padding: \"3px 0\",\n textAlign: \"center\" as const,\n transition: \"all 0.15s\",\n }}\n title={`Filter ${level} logs`}\n type=\"button\"\n >\n {level.slice(0, 3).toUpperCase()}\n </button>\n );\n};\n\nexport const Toolbar = ({\n paused,\n onPause,\n onResume,\n onClear,\n levelFilter,\n onLevelFilterChange,\n categories,\n categoryFilter,\n onCategoryFilterChange,\n onSearchTextChange,\n totalCount,\n filteredCount,\n}: ToolbarProps) => {\n useHoverStyles();\n\n // The toolbar is the single source of truth for the search input; the parent\n // only ever receives the debounced value.\n const [search, setSearch] = useState(\"\");\n const debouncedSearch = useMemo(\n () => new Debouncer(onSearchTextChange, { wait: 200 }),\n [onSearchTextChange]\n );\n\n useEffect(() => () => debouncedSearch.cancel(), [debouncedSearch]);\n\n const handleSearchChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const { value } = e.target;\n setSearch(value);\n debouncedSearch.maybeExecute(value);\n },\n [debouncedSearch]\n );\n\n const handleLevelToggle = useCallback(\n (level: LogLevel) => {\n const next = new Set(levelFilter);\n if (next.has(level)) {\n next.delete(level);\n } else {\n next.add(level);\n }\n onLevelFilterChange(next);\n },\n [levelFilter, onLevelFilterChange]\n );\n\n return (\n <div\n data-testid=\"toolbar\"\n style={{\n display: \"flex\",\n flexDirection: \"column\",\n flexShrink: 0,\n gap: theme.spacing.xl,\n height: \"100%\",\n minWidth: \"200px\",\n overflow: \"auto\",\n padding: `${theme.spacing.lg} ${theme.spacing.xl}`,\n }}\n >\n {/* Log count */}\n <div style={{ textAlign: \"center\" }}>\n <span\n style={{\n color: theme.colors.textPrimary,\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.lg,\n fontWeight: 600,\n }}\n >\n {filteredCount === totalCount ? `${totalCount}` : `${filteredCount} / ${totalCount}`}\n </span>\n <div style={{ color: theme.colors.textMuted, fontSize: theme.fontSize.sm }}>logs</div>\n </div>\n\n {/* Actions */}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: theme.spacing.sm }}>\n <div style={sectionLabelStyle}>Actions</div>\n <button\n data-lt-interactive=\"\"\n onClick={paused ? onResume : onPause}\n style={{\n ...buttonStyle,\n background: paused ? theme.colors.levels.warning.badge : buttonStyle.background,\n color: paused ? theme.colors.white : buttonStyle.color,\n }}\n title={paused ? \"Resume live updates\" : \"Pause live updates\"}\n type=\"button\"\n >\n <span style={{ alignItems: \"center\", display: \"inline-flex\", gap: \"6px\" }}>\n {paused ? <PlayIcon size={12} /> : <PauseIcon size={12} />}\n {paused ? \"Resume\" : \"Pause\"}\n </span>\n </button>\n\n <button\n data-lt-interactive=\"\"\n onClick={onClear}\n style={buttonStyle}\n title=\"Clear all logs\"\n type=\"button\"\n >\n <span style={{ alignItems: \"center\", display: \"inline-flex\", gap: \"6px\" }}>\n <Trash2Icon size={12} />\n Clear\n </span>\n </button>\n </div>\n\n {/* Level filters */}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: theme.spacing.sm }}>\n <div style={{ alignItems: \"center\", display: \"flex\", justifyContent: \"space-between\" }}>\n <div style={sectionLabelStyle}>Levels</div>\n {levelFilter.size > 0 && (\n <button\n data-lt-clear-btn=\"\"\n onClick={() => onLevelFilterChange(new Set())}\n style={{\n background: \"none\",\n border: \"none\",\n color: theme.colors.textMuted,\n cursor: \"pointer\",\n fontSize: \"9px\",\n padding: 0,\n transition: \"color 0.1s\",\n }}\n title=\"Show all levels\"\n type=\"button\"\n >\n clear\n </button>\n )}\n </div>\n <div\n style={{\n display: \"grid\",\n gap: theme.spacing.xs,\n gridTemplateColumns: \"repeat(3, 1fr)\",\n }}\n >\n {LOG_LEVELS.map((level) => (\n <LevelToggle\n active={levelFilter.size === 0 || levelFilter.has(level)}\n key={level}\n level={level}\n onToggle={handleLevelToggle}\n pressed={levelFilter.has(level)}\n />\n ))}\n </div>\n </div>\n\n {/* Category filter (multi-select combobox) */}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: theme.spacing.sm }}>\n <div style={{ alignItems: \"center\", display: \"flex\", justifyContent: \"space-between\" }}>\n <div style={sectionLabelStyle}>Category</div>\n {categoryFilter.length > 0 && (\n <button\n data-lt-clear-btn=\"\"\n onClick={() => onCategoryFilterChange([])}\n style={{\n background: \"none\",\n border: \"none\",\n color: theme.colors.textMuted,\n cursor: \"pointer\",\n fontSize: \"9px\",\n padding: 0,\n transition: \"color 0.1s\",\n }}\n title=\"Clear all categories\"\n type=\"button\"\n >\n clear\n </button>\n )}\n </div>\n {categories.length === 0 ? (\n <Popover.Root>\n <Popover.Trigger\n delay={300}\n nativeButton={false}\n openOnHover\n render={\n <div\n data-testid=\"category-input\"\n style={{\n ...inputStyle,\n color: theme.colors.textMuted,\n cursor: \"not-allowed\",\n opacity: 0.5,\n }}\n >\n Category…\n </div>\n }\n />\n <Popover.Portal>\n <Popover.Positioner sideOffset={6} style={{ zIndex: 999_999 }}>\n <Popover.Popup\n style={{\n background: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: theme.radius.md,\n boxShadow: \"0 8px 24px rgba(0,0,0,0.5)\",\n fontSize: theme.fontSize.sm,\n maxWidth: \"200px\",\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n }}\n >\n <p style={{ color: theme.colors.textMuted, lineHeight: 1.4, margin: 0 }}>\n Waiting for logs to populate available categories.\n </p>\n </Popover.Popup>\n </Popover.Positioner>\n </Popover.Portal>\n </Popover.Root>\n ) : (\n <Combobox.Root\n items={categories}\n multiple\n onValueChange={onCategoryFilterChange}\n value={categoryFilter}\n >\n <div\n style={{\n background: theme.colors.surface,\n border: `1px solid ${theme.colors.borderInput}`,\n borderRadius: theme.radius.md,\n display: \"flex\",\n flexWrap: \"wrap\",\n gap: \"3px\",\n minHeight: \"28px\",\n padding: \"3px\",\n transition: \"border-color 0.15s\",\n }}\n >\n {categoryFilter.map((cat) => (\n <span\n key={cat}\n style={{\n alignItems: \"center\",\n background: \"rgba(99, 102, 241, 0.15)\",\n border: \"1px solid rgba(99, 102, 241, 0.3)\",\n borderRadius: theme.radius.sm,\n color: theme.colors.accentHover,\n display: \"inline-flex\",\n fontFamily: theme.fontFamily.mono,\n fontSize: \"9px\",\n gap: \"3px\",\n lineHeight: 1,\n padding: \"1px 4px\",\n }}\n >\n {cat}\n <button\n data-lt-chip-remove=\"\"\n onClick={() => onCategoryFilterChange(categoryFilter.filter((c) => c !== cat))}\n style={{\n background: \"none\",\n border: \"none\",\n color: theme.colors.textMuted,\n cursor: \"pointer\",\n fontSize: \"7px\",\n lineHeight: 1,\n opacity: 0.7,\n padding: 0,\n transition: \"color 0.1s, opacity 0.1s\",\n }}\n title={`Remove ${cat}`}\n type=\"button\"\n >\n <XIcon size={7} />\n </button>\n </span>\n ))}\n <Combobox.Input\n aria-label=\"Filter by category\"\n data-lt-input=\"\"\n data-testid=\"category-input\"\n placeholder={categoryFilter.length === 0 ? \"Category…\" : \"\"}\n style={{\n background: \"transparent\",\n border: \"none\",\n color: theme.colors.textPrimary,\n flex: 1,\n fontFamily: theme.fontFamily.sans,\n fontSize: theme.fontSize.md,\n minWidth: \"40px\",\n outline: \"none\",\n padding: `${theme.spacing.xs} ${theme.spacing.sm}`,\n }}\n />\n </div>\n <Combobox.Portal>\n <Combobox.Positioner style={{ zIndex: 999_999 }}>\n <Combobox.Popup\n style={{\n background: theme.colors.surface,\n border: `1px solid ${theme.colors.border}`,\n borderRadius: theme.radius.md,\n boxShadow: \"0 8px 24px rgba(0,0,0,0.5)\",\n maxHeight: \"200px\",\n overflow: \"auto\",\n padding: theme.spacing.xs,\n }}\n >\n <Combobox.List>\n {(item, index) => (\n <Combobox.Item\n index={index}\n key={String(item)}\n style={{\n alignItems: \"center\",\n borderRadius: theme.radius.sm,\n color: theme.colors.textPrimary,\n cursor: \"pointer\",\n display: \"flex\",\n fontFamily: theme.fontFamily.mono,\n fontSize: theme.fontSize.md,\n gap: theme.spacing.sm,\n justifyContent: \"space-between\",\n padding: `${theme.spacing.sm} ${theme.spacing.lg}`,\n transition: \"background 0.1s\",\n }}\n value={item}\n >\n <span style={{ flex: 1, overflow: \"hidden\", textOverflow: \"ellipsis\" }}>\n {String(item)}\n </span>\n <Combobox.ItemIndicator>\n <CheckIcon color={theme.colors.accent} size={14} />\n </Combobox.ItemIndicator>\n </Combobox.Item>\n )}\n </Combobox.List>\n </Combobox.Popup>\n </Combobox.Positioner>\n </Combobox.Portal>\n </Combobox.Root>\n )}\n </div>\n\n {/* Text search */}\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: theme.spacing.sm }}>\n <div style={sectionLabelStyle}>Search</div>\n <input\n aria-label=\"Search logs\"\n data-lt-input=\"\"\n data-testid=\"search-input\"\n onChange={handleSearchChange}\n placeholder=\"Search…\"\n style={inputStyle}\n type=\"text\"\n value={search}\n />\n </div>\n </div>\n );\n};\n","import type { TanStackDevtoolsReactPlugin } from \"@tanstack/react-devtools\";\nimport { defaultLogStore, type LogStore } from \"../store\";\nimport { LogTapeDevtoolsPlugin } from \"./logtape-devtools-plugin\";\n\nexport interface LogTapeDevtoolsPluginOptions {\n /** Whether to open the devtools panel by default. Default: true */\n defaultOpen?: boolean;\n /** Plugin display name. Default: \"LogTape\" */\n name?: string;\n /** Custom log store instance. Must match the store passed to createDevtoolsSink. */\n store?: LogStore;\n}\n\n/**\n * Creates a TanStack DevTools plugin config object for LogTape.\n *\n * Usage:\n * ```tsx\n * import { TanStackDevtools } from \"@tanstack/react-devtools\";\n * import { createLogTapeDevtoolsPlugin } from \"@mugenlabs/logtape-devtools\";\n *\n * <TanStackDevtools plugins={[createLogTapeDevtoolsPlugin()]} />\n * ```\n */\nexport const createLogTapeDevtoolsPlugin = (\n options?: LogTapeDevtoolsPluginOptions\n): TanStackDevtoolsReactPlugin => ({\n defaultOpen: options?.defaultOpen ?? true,\n id: \"logtape-devtools-plugin\",\n name: options?.name ?? \"LogTape\",\n render: <LogTapeDevtoolsPlugin store={options?.store ?? defaultLogStore} />,\n});\n","import {\n createLogTapeDevtoolsPlugin,\n type LogTapeDevtoolsPluginOptions,\n} from \"./plugin/create-logtape-devtools-plugin\";\nimport { createDevtoolsSink, type DevtoolsSinkOptions } from \"./sink\";\nimport { defaultLogStore, type LogStore } from \"./store\";\n\n/** Options for {@link createLogTapeDevtools}. */\nexport interface CreateLogTapeDevtoolsOptions {\n /** Plugin options, minus `store` — the shared store is wired automatically. */\n plugin?: Omit<LogTapeDevtoolsPluginOptions, \"store\">;\n /** Sink options, minus `store` — the shared store is wired automatically. */\n sink?: Omit<DevtoolsSinkOptions, \"store\">;\n /** Store shared by the sink and the plugin. Defaults to the shared store. */\n store?: LogStore;\n}\n\n/**\n * Creates a LogTape sink and its matching TanStack DevTools plugin, both wired\n * to the same store.\n *\n * Use this instead of calling `createDevtoolsSink` and\n * `createLogTapeDevtoolsPlugin` separately when you do not need to hold on to\n * the store yourself.\n *\n * ```tsx\n * import { configure, getLogger } from \"@logtape/logtape\";\n * import { TanStackDevtools } from \"@tanstack/react-devtools\";\n * import { createLogTapeDevtools } from \"@mugenlabs/logtape-devtools\";\n *\n * const { sink, plugin } = createLogTapeDevtools();\n *\n * await configure({\n * sinks: { devtools: sink },\n * loggers: [{ category: [], lowestLevel: \"trace\", sinks: [\"devtools\"] }],\n * });\n *\n * function App() {\n * return <TanStackDevtools plugins={[plugin]} />;\n * }\n * ```\n */\nexport function createLogTapeDevtools(options?: CreateLogTapeDevtoolsOptions) {\n const store = options?.store ?? defaultLogStore;\n\n return {\n plugin: createLogTapeDevtoolsPlugin({ ...options?.plugin, store }),\n sink: createDevtoolsSink({ ...options?.sink, store }),\n };\n}\n"],"mappings":"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","names":["useCallback","useMemo","useState","useCallback","jsx","jsx","jsxs","jsx","jsxs","categoryStyle","messageStyle","callerStyle","jsx","containerStyle","useCallback","useCallback","useEffect","jsx","jsxs","useEffect","useCallback","jsx","jsxs","useState","useCallback","useMemo","jsx"]}
|
package/dist/sink.d.ts
CHANGED
|
@@ -36,27 +36,32 @@ type Listener = () => void;
|
|
|
36
36
|
*/
|
|
37
37
|
interface LogStore {
|
|
38
38
|
/** Appends a record, evicting the oldest one when the max size is reached. */
|
|
39
|
-
addRecord(record: DevtoolsLogRecord)
|
|
39
|
+
addRecord: (record: DevtoolsLogRecord) => void;
|
|
40
40
|
/** Removes every record and notifies subscribers. */
|
|
41
|
-
clear()
|
|
41
|
+
clear: () => void;
|
|
42
|
+
/**
|
|
43
|
+
* The current retention limit. `0` means the store discards everything, which
|
|
44
|
+
* lets the sink skip normalisation entirely. Optional for custom stores.
|
|
45
|
+
*/
|
|
46
|
+
getMaxRecords?: () => number;
|
|
42
47
|
/**
|
|
43
48
|
* Returns the current records, oldest first.
|
|
44
49
|
*
|
|
45
50
|
* The reference is stable until the store changes, which makes it safe to use
|
|
46
51
|
* with `useSyncExternalStore`.
|
|
47
52
|
*/
|
|
48
|
-
getSnapshot()
|
|
53
|
+
getSnapshot: () => DevtoolsLogRecord[];
|
|
49
54
|
/**
|
|
50
55
|
* Whether anything is currently subscribed to the store.
|
|
51
56
|
*
|
|
52
57
|
* Used by the sink to skip expensive work (such as stack trace capture) while
|
|
53
58
|
* no devtools panel is mounted.
|
|
54
59
|
*/
|
|
55
|
-
hasListeners()
|
|
60
|
+
hasListeners: () => boolean;
|
|
56
61
|
/** Updates the maximum number of retained records, trimming if needed. */
|
|
57
|
-
setMaxSize(size: number)
|
|
62
|
+
setMaxSize: (size: number) => void;
|
|
58
63
|
/** Registers a change listener. Returns an unsubscribe function. */
|
|
59
|
-
subscribe(listener: Listener)
|
|
64
|
+
subscribe: (listener: Listener) => () => void;
|
|
60
65
|
}
|
|
61
66
|
/** Options for {@link createLogStore}. */
|
|
62
67
|
interface LogStoreOptions {
|
|
@@ -109,6 +114,11 @@ interface DevtoolsSinkOptions {
|
|
|
109
114
|
/** Custom log store instance. Defaults to the shared store. */
|
|
110
115
|
store?: LogStore;
|
|
111
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* A LogTape sink that is also disposable. LogTape disposes sinks when the
|
|
119
|
+
* configuration is reset; a disposed sink stops writing to its store.
|
|
120
|
+
*/
|
|
121
|
+
type DevtoolsSink = Sink & Disposable;
|
|
112
122
|
/**
|
|
113
123
|
* Creates a LogTape sink that forwards log records to the devtools panel.
|
|
114
124
|
*
|
|
@@ -125,6 +135,6 @@ interface DevtoolsSinkOptions {
|
|
|
125
135
|
* });
|
|
126
136
|
* ```
|
|
127
137
|
*/
|
|
128
|
-
declare function createDevtoolsSink(options?: DevtoolsSinkOptions):
|
|
138
|
+
declare function createDevtoolsSink(options?: DevtoolsSinkOptions): DevtoolsSink;
|
|
129
139
|
|
|
130
|
-
export { type DevtoolsLogRecord, type DevtoolsSinkOptions, LOG_LEVELS, type LogLevel, type LogStore, type LogStoreOptions, createDevtoolsSink, createLogStore, defaultLogStore };
|
|
140
|
+
export { type DevtoolsLogRecord, type DevtoolsSink, type DevtoolsSinkOptions, LOG_LEVELS, type LogLevel, type LogStore, type LogStoreOptions, createDevtoolsSink, createLogStore, defaultLogStore };
|
package/dist/sink.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mugenlabs/logtape-devtools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "A TanStack DevTools plugin for inspecting LogTape logs in the browser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"logtape",
|
|
@@ -18,21 +18,15 @@
|
|
|
18
18
|
"bugs": {
|
|
19
19
|
"url": "https://github.com/mugenlabs-dev/logtape-devtools/issues"
|
|
20
20
|
},
|
|
21
|
-
"license": "MIT",
|
|
22
|
-
"author": "Yago Gonzalez",
|
|
23
21
|
"repository": {
|
|
24
22
|
"type": "git",
|
|
25
23
|
"url": "https://github.com/mugenlabs-dev/logtape-devtools.git",
|
|
26
24
|
"directory": "packages/core"
|
|
27
25
|
},
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
"assets"
|
|
31
|
-
],
|
|
32
|
-
"type": "module",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Yago Gonzalez",
|
|
33
28
|
"sideEffects": false,
|
|
34
|
-
"
|
|
35
|
-
"types": "./dist/index.d.ts",
|
|
29
|
+
"type": "module",
|
|
36
30
|
"exports": {
|
|
37
31
|
".": {
|
|
38
32
|
"types": "./dist/index.d.ts",
|
|
@@ -44,44 +38,53 @@
|
|
|
44
38
|
},
|
|
45
39
|
"./package.json": "./package.json"
|
|
46
40
|
},
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
"main": "./dist/index.mjs",
|
|
42
|
+
"types": "./dist/index.d.ts",
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"assets"
|
|
46
|
+
],
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@base-ui-components/react": "1.0.0-rc.0",
|
|
49
|
+
"@tanstack/pacer": "^0.19.0",
|
|
50
|
+
"@tanstack/react-virtual": "^3.14.10"
|
|
50
51
|
},
|
|
51
52
|
"devDependencies": {
|
|
52
|
-
"@logtape/logtape": "^2.
|
|
53
|
-
"@storybook/react": "^10.
|
|
54
|
-
"@
|
|
55
|
-
"@
|
|
56
|
-
"react": "^19.
|
|
57
|
-
"react-dom": "^19.
|
|
58
|
-
"storybook": "^10.
|
|
53
|
+
"@logtape/logtape": "^2.3.2",
|
|
54
|
+
"@storybook/react": "^10.6.0",
|
|
55
|
+
"@storybook/react-vite": "^10.6.0",
|
|
56
|
+
"@tanstack/react-devtools": "^0.9.13",
|
|
57
|
+
"react": "^19.2.8",
|
|
58
|
+
"react-dom": "^19.2.8",
|
|
59
|
+
"storybook": "^10.6.0"
|
|
59
60
|
},
|
|
60
61
|
"peerDependencies": {
|
|
61
|
-
"@logtape/logtape": "
|
|
62
|
-
"@tanstack/react-devtools": "
|
|
62
|
+
"@logtape/logtape": "^2.0.0",
|
|
63
|
+
"@tanstack/react-devtools": "^0.9.0",
|
|
63
64
|
"react": "^18.0.0 || ^19.0.0",
|
|
64
65
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
65
66
|
},
|
|
66
67
|
"peerDependenciesMeta": {
|
|
68
|
+
"@logtape/logtape": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
67
71
|
"@tanstack/react-devtools": {
|
|
68
72
|
"optional": true
|
|
69
73
|
}
|
|
70
74
|
},
|
|
71
75
|
"engines": {
|
|
72
|
-
"node": ">=
|
|
76
|
+
"node": ">=24.0.0"
|
|
73
77
|
},
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"@tanstack/react-virtual": "^3.14.8"
|
|
78
|
+
"publishConfig": {
|
|
79
|
+
"access": "public",
|
|
80
|
+
"provenance": true
|
|
78
81
|
},
|
|
79
82
|
"scripts": {
|
|
80
83
|
"build": "tsup",
|
|
81
|
-
"
|
|
82
|
-
"typecheck": "tsc --noEmit",
|
|
84
|
+
"build-storybook": "storybook build",
|
|
83
85
|
"clean": "rm -rf dist",
|
|
86
|
+
"dev": "tsup --watch",
|
|
84
87
|
"storybook": "storybook dev -p 6006",
|
|
85
|
-
"
|
|
88
|
+
"typecheck": "tsc --noEmit"
|
|
86
89
|
}
|
|
87
90
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/store.ts","../src/types.ts","../src/safe-json.ts","../src/sink.ts"],"sourcesContent":["import type { DevtoolsLogRecord } from \"./types\";\n\ntype Listener = () => void;\n\n/**\n * Reactive log record container backing the devtools panel.\n *\n * Records are kept in a fixed-capacity ring buffer so appending is O(1); the\n * array returned by {@link LogStore.getSnapshot} is rebuilt lazily at most once\n * per mutation batch. Listener notifications are coalesced into a microtask, so\n * a synchronous burst of {@link LogStore.addRecord} calls results in a single\n * notification.\n */\nexport interface LogStore {\n /** Appends a record, evicting the oldest one when the max size is reached. */\n addRecord(record: DevtoolsLogRecord): void;\n /** Removes every record and notifies subscribers. */\n clear(): void;\n /**\n * Returns the current records, oldest first.\n *\n * The reference is stable until the store changes, which makes it safe to use\n * with `useSyncExternalStore`.\n */\n getSnapshot(): DevtoolsLogRecord[];\n /**\n * Whether anything is currently subscribed to the store.\n *\n * Used by the sink to skip expensive work (such as stack trace capture) while\n * no devtools panel is mounted.\n */\n hasListeners(): boolean;\n /** Updates the maximum number of retained records, trimming if needed. */\n setMaxSize(size: number): void;\n /** Registers a change listener. Returns an unsubscribe function. */\n subscribe(listener: Listener): () => void;\n}\n\n/** Options for {@link createLogStore}. */\nexport interface LogStoreOptions {\n /**\n * Maximum number of log records to retain. Older records are evicted first.\n *\n * @default 1000\n */\n maxRecords?: number;\n}\n\nconst DEFAULT_MAX_RECORDS = 1000;\n\nfunction schedule(callback: () => void): void {\n if (typeof queueMicrotask === \"function\") {\n queueMicrotask(callback);\n return;\n }\n Promise.resolve().then(callback);\n}\n\n/**\n * Creates an isolated log store.\n *\n * Pass the same instance to `createDevtoolsSink` and\n * `createLogTapeDevtoolsPlugin` to keep the sink and the panel in sync.\n *\n * ```ts\n * const store = createLogStore({ maxRecords: 5000 });\n * ```\n */\nexport function createLogStore(options?: LogStoreOptions): LogStore {\n let max = Math.max(0, options?.maxRecords ?? DEFAULT_MAX_RECORDS);\n\n // Ring buffer: `start` is the index of the oldest record, `size` the number\n // of live entries. Appending is O(1) — no per-record array copy.\n let buffer: DevtoolsLogRecord[] = [];\n let start = 0;\n let size = 0;\n\n let snapshot: DevtoolsLogRecord[] = [];\n let snapshotDirty = false;\n\n const listeners = new Set<Listener>();\n let notifyScheduled = false;\n\n function emit() {\n for (const listener of listeners) {\n listener();\n }\n }\n\n function notify() {\n if (notifyScheduled) {\n return;\n }\n notifyScheduled = true;\n schedule(() => {\n notifyScheduled = false;\n emit();\n });\n }\n\n /** Materializes the ring buffer into a plain, oldest-first array. */\n function toArray(): DevtoolsLogRecord[] {\n const result: DevtoolsLogRecord[] = new Array(size);\n for (let i = 0; i < size; i++) {\n result[i] = buffer[(start + i) % buffer.length];\n }\n return result;\n }\n\n /**\n * Rewrites the buffer in logical order, dropping records beyond `max`.\n * Keeps the invariant that a below-capacity buffer is linear (`start === 0`).\n */\n function normalize(): boolean {\n const trimmed = size > max;\n const kept = trimmed ? toArray().slice(size - max) : toArray();\n buffer = kept;\n start = 0;\n size = kept.length;\n return trimmed;\n }\n\n return {\n addRecord: (record) => {\n if (max === 0) {\n return;\n }\n if (size < max) {\n // Below capacity — the buffer is linear, so a plain append is enough.\n buffer.push(record);\n size++;\n } else {\n // At capacity — overwrite the oldest slot.\n buffer[start] = record;\n start = (start + 1) % buffer.length;\n }\n snapshotDirty = true;\n notify();\n },\n clear: () => {\n buffer = [];\n start = 0;\n size = 0;\n snapshotDirty = true;\n notify();\n },\n getSnapshot: () => {\n if (snapshotDirty) {\n snapshot = toArray();\n snapshotDirty = false;\n }\n return snapshot;\n },\n hasListeners: () => listeners.size > 0,\n setMaxSize: (nextMax) => {\n max = Math.max(0, nextMax);\n if (normalize()) {\n snapshotDirty = true;\n notify();\n }\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** Default shared store instance used when no custom store is provided */\nexport const defaultLogStore = createLogStore();\n","/** Severity levels supported by LogTape, ordered from least to most severe. */\nexport type LogLevel = \"trace\" | \"debug\" | \"info\" | \"warning\" | \"error\" | \"fatal\";\n\n/** Every {@link LogLevel}, ordered from least to most severe. */\nexport const LOG_LEVELS: LogLevel[] = [\"trace\", \"debug\", \"info\", \"warning\", \"error\", \"fatal\"];\n\n/** A LogTape record normalized for display in the devtools panel. */\nexport interface DevtoolsLogRecord {\n /** Source location of the log call (only present when `captureStackTrace` is enabled). */\n caller?: string;\n /** Logger category path, e.g. `[\"app\", \"auth\"]`. */\n category: string[];\n /** Unique identifier, stable for the lifetime of the record. */\n id: string;\n /** Severity of the record. */\n level: LogLevel;\n /** Raw message parts, alternating literals and interpolated values. */\n message: unknown[];\n /** Message parts rendered into a single searchable string. */\n messageText: string;\n /** Structured properties attached to the log call, deep-cloned for safety. */\n properties: Record<string, unknown>;\n /** Creation time in milliseconds since the Unix epoch. */\n timestamp: number;\n}\n","/**\n * JSON helpers shared by the sink and the panel.\n *\n * This module is intentionally React-free so that it can be pulled into the\n * `@mugenlabs/logtape-devtools/sink` entry point.\n */\n\n/** Replacer that turns circular references into a `\"[Circular]\"` marker. */\nfunction circularSafeReplacer(): (key: string, value: unknown) => unknown {\n const seen = new WeakSet<object>();\n return (_key, value) => {\n if (typeof value === \"object\" && value !== null) {\n if (seen.has(value)) {\n return \"[Circular]\";\n }\n seen.add(value);\n }\n return value;\n };\n}\n\n/**\n * Serializes any value to JSON, never throwing.\n *\n * Falls back to a circular-reference-safe pass, then to `String(value)` for\n * values JSON cannot represent at all (`undefined`, symbols, BigInt…).\n */\nexport function safeStringify(value: unknown, indent?: number): string {\n try {\n const json = JSON.stringify(value, null, indent);\n if (json !== undefined) {\n return json;\n }\n } catch {\n // Circular references, or a throwing `toJSON`.\n }\n try {\n const json = JSON.stringify(value, circularSafeReplacer(), indent);\n if (json !== undefined) {\n return json;\n }\n } catch {\n // Still unserializable — fall through.\n }\n return String(value);\n}\n\nfunction jsonClone(value: unknown): unknown {\n return JSON.parse(safeStringify(value));\n}\n\n/**\n * Deep-clones a properties bag, never throwing.\n *\n * Uses `structuredClone` when available and degrades per entry (via a JSON\n * round trip, then `String(value)`) so that a single unclonable value cannot\n * drop the whole object.\n */\nexport function safeCloneProperties(properties: Record<string, unknown>): Record<string, unknown> {\n if (typeof structuredClone === \"function\") {\n try {\n return structuredClone(properties);\n } catch {\n // Functions, DOM nodes, proxies… fall through to the JSON path.\n }\n }\n\n try {\n return jsonClone(properties) as Record<string, unknown>;\n } catch {\n // Fall through to the per-entry path.\n }\n\n const result: Record<string, unknown> = {};\n for (const [key, entry] of Object.entries(properties)) {\n try {\n result[key] = jsonClone(entry);\n } catch {\n result[key] = String(entry);\n }\n }\n return result;\n}\n","import type { LogRecord, Sink } from \"@logtape/logtape\";\nimport { safeCloneProperties, safeStringify } from \"./safe-json\";\nimport { defaultLogStore, type LogStore } from \"./store\";\nimport type { DevtoolsLogRecord, LogLevel } from \"./types\";\n\nfunction renderMessage(message: readonly unknown[]): string {\n return message\n .map((part) => {\n if (typeof part === \"string\") {\n return part;\n }\n if (part === null || part === undefined) {\n return String(part);\n }\n return safeStringify(part);\n })\n .join(\"\");\n}\n\n// ---------------------------------------------------------------------------\n// Stack trace parser — extracts the caller frame from `new Error().stack`\n// ---------------------------------------------------------------------------\n\n// V8 (Chrome/Node/Edge): \" at functionName (file:line:col)\"\nconst v8FrameRe = /at\\s+(?:.*?\\s+\\()?(.+?):(\\d+):(\\d+)\\)?$/;\n// SpiderMonkey (Firefox) / JSC (Safari): \"functionName@file:line:col\"\nconst smFrameRe = /(?:^|@)(.+?):(\\d+):(\\d+)$/;\n\nfunction captureCallerInfo(): string | undefined {\n // biome-ignore lint/suspicious/useErrorMessage: we only need the stack trace, not the message\n const stack = new Error().stack;\n if (!stack) {\n return undefined;\n }\n\n const lines = stack.split(\"\\n\");\n\n for (const line of lines) {\n const trimmed = line.trim();\n\n // Skip empty lines and the \"Error\" header\n if (!trimmed || trimmed === \"Error\") {\n continue;\n }\n\n // Skip frames from logtape internals (both @logtape/logtape and @mugenlabs/logtape-devtools)\n if (/logtape/i.test(trimmed)) {\n continue;\n }\n\n // Try V8 format first, then SpiderMonkey/JSC\n const match = v8FrameRe.exec(trimmed) ?? smFrameRe.exec(trimmed);\n if (!match) {\n continue;\n }\n\n const [, filePath, lineNo, colNo] = match;\n\n // Extract just the filename from the full path/URL\n const fileName =\n filePath\n .replace(/[?#].*$/, \"\")\n .split(\"/\")\n .pop() ?? filePath;\n\n return `${fileName}:${lineNo}:${colNo}`;\n }\n\n return undefined;\n}\n\nfunction normalizeRecord(record: LogRecord, captureStack: boolean, id: string): DevtoolsLogRecord {\n const normalized: DevtoolsLogRecord = {\n id,\n timestamp: record.timestamp,\n level: record.level as LogLevel,\n category: [...record.category],\n message: [...record.message],\n messageText: renderMessage(record.message),\n properties: safeCloneProperties(record.properties),\n };\n\n if (captureStack) {\n const caller = captureCallerInfo();\n if (caller) {\n normalized.caller = caller;\n }\n }\n\n return normalized;\n}\n\nfunction isProductionEnv(): boolean {\n try {\n // Bundlers (Vite, webpack, etc.) statically replace this at build time\n if (typeof process !== \"undefined\" && process.env?.NODE_ENV === \"production\") {\n return true;\n }\n } catch {\n // process may not exist in browser environments\n }\n return false;\n}\n\n/** Options for {@link createDevtoolsSink}. */\nexport interface DevtoolsSinkOptions {\n /**\n * Capture the source location of each log call via stack trace parsing.\n *\n * Relies on `new Error().stack`, which is engine-dependent. Automatically\n * disabled in production builds (when `process.env.NODE_ENV === \"production\"`)\n * because minified bundles produce meaningless file/line references. Note that\n * source maps do not help here — browsers do not apply source maps to\n * `Error.stack`.\n *\n * Also skipped while no devtools panel is mounted (the store has no\n * subscribers), unless `forceStackTrace` is `true`.\n *\n * @experimental\n * @default true\n */\n captureStackTrace?: boolean;\n /**\n * Force stack trace capture even in production builds and while the panel is\n * closed. Only has an effect when `captureStackTrace` is `true`.\n *\n * @default false\n */\n forceStackTrace?: boolean;\n /** Custom log store instance. Defaults to the shared store. */\n store?: LogStore;\n}\n\n/**\n * Creates a LogTape sink that forwards log records to the devtools panel.\n *\n * Usage:\n * ```ts\n * import { configure } from \"@logtape/logtape\";\n * import { createDevtoolsSink } from \"@mugenlabs/logtape-devtools\";\n *\n * await configure({\n * sinks: { devtools: createDevtoolsSink() },\n * loggers: [\n * { category: [\"app\"], lowestLevel: \"debug\", sinks: [\"devtools\"] },\n * ],\n * });\n * ```\n */\nexport function createDevtoolsSink(options?: DevtoolsSinkOptions): Sink {\n const store = options?.store ?? defaultLogStore;\n const wantsStack = options?.captureStackTrace ?? true;\n const forceStack = options?.forceStackTrace ?? false;\n const captureStack = wantsStack && (forceStack || !isProductionEnv());\n\n // Per-sink counter, plus a random discriminator so that two sinks sharing a\n // single store cannot emit the same id for records with equal timestamps.\n const sinkId = Math.random().toString(36).slice(2, 8);\n let counter = 0;\n\n return (record: LogRecord) => {\n try {\n // Stack capture is the most expensive part of normalization — skip it\n // while nothing is watching the store (i.e. no panel is mounted).\n const shouldCaptureStack = captureStack && (forceStack || store.hasListeners());\n const normalized = normalizeRecord(\n record,\n shouldCaptureStack,\n `log-${sinkId}-${++counter}-${record.timestamp}`\n );\n store.addRecord(normalized);\n } catch {\n // Fail silently — devtools should never break the application\n }\n };\n}\n"],"mappings":"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","names":[]}
|