@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 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={[createLogTapeDevtoolsPlugin({ store: logStore })]} />;
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, so the cost of an unmounted panel is close to zero.
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 config object (`{ id, name, defaultOpen, render }`) to pass to `<TanStackDevtools plugins={[...]} />`.
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` | `>=2.0.0` | Required |
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` | `>=0.9.0` | **Optional** — only needed to host the panel |
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 `>=18`. `@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.
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 = Math.max(0, options?.maxRecords ?? DEFAULT_MAX_RECORDS);
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 = Math.max(0, nextMax);
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 circularSafeReplacer() {
101
- const seen = /* @__PURE__ */ new WeakSet();
102
- return (_key, value) => {
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 (seen.has(value)) {
141
+ if (ancestors.some((ancestor) => ancestor.original === value)) {
105
142
  return "[Circular]";
106
143
  }
107
- seen.add(value);
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, null, indent);
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().stack;
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
- category: [...record.category],
198
- message: [...record.message],
199
- messageText: renderMessage(record.message),
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
- return (record) => {
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}-${++counter}-${record.timestamp}`
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-CYXOBB6A.mjs.map
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 * as _logtape_logtape from '@logtape/logtape';
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
- defaultOpen: boolean;
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-CYXOBB6A.mjs";
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
- textPrimary: "#e0e0e0",
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
- trace: { bg: "#1a1a2e", color: "#8888bb", badge: "#5555aa" },
35
- debug: { bg: "#1a2a1a", color: "#88bb88", badge: "#55aa55" },
36
- info: { bg: "#1a2a3a", color: "#60a5fa", badge: "#3b82f6" },
37
- warning: { bg: "#2a2a1a", color: "#facc15", badge: "#eab308" },
38
- error: { bg: "#2a1a1a", color: "#ef4444", badge: "#dc2626" },
39
- fatal: { bg: "#3a0a0a", color: "#ff6b6b", badge: "#b91c1c" }
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
- pill: "10px"
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
- const d = new Date(ts);
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 = { size: 14, color: "currentColor" };
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 = theme.colors.levels[level];
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: LEVEL_LABELS[level]
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 = theme.colors.levels[record.level];
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 && /* @__PURE__ */ jsx3(DetailRow, { label: "Caller", children: /* @__PURE__ */ jsx3("span", { style: callerStyle, children: 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 LogRow = ({ record, expanded, onToggle }) => {
290
- const levelColors = theme.colors.levels[record.level];
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 && /* @__PURE__ */ jsx4("span", { style: callerStyle2, children: 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 && /* @__PURE__ */ jsx4(LogDetail, { record })
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
- getItemKey: (index) => records[index].id,
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 (recordCount === 0 || !(autoScroll && isAtBottomRef.current)) {
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": active,
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.value;
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
- return records.filter((r) => {
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
- }, [records, levelFilter, categoryFilter, searchText]);
984
+ }),
985
+ [records, levelFilter, categoryFilter, searchText]
986
+ );
962
987
  return /* @__PURE__ */ jsxs5("div", { style: rootStyle, children: [
963
988
  /* @__PURE__ */ jsx7(
964
989
  "div",
@@ -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":";;;;;;;;;AAAA,SAAS,eAAAA,cAAa,WAAAC,UAAS,YAAAC,WAAU,4BAA4B;;;ACArE,SAAS,sBAAsB;AAC/B,SAAS,eAAAC,cAAa,WAAW,cAAc;;;ACDxC,IAAM,QAAQ;AAAA,EACnB,QAAQ;AAAA,IACN,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,QAAQ;AAAA,IACR,aAAa;AAAA,IACb,aAAa;AAAA,IAEb,QAAQ;AAAA;AAAA,MAEN,OAAO,EAAE,OAAO,WAAW,IAAI,WAAW,OAAO,UAAU;AAAA,MAC3D,OAAO,EAAE,OAAO,WAAW,IAAI,WAAW,OAAO,UAAU;AAAA,MAC3D,OAAO,EAAE,OAAO,WAAW,IAAI,WAAW,OAAO,UAAU;AAAA,MAC3D,MAAM,EAAE,OAAO,WAAW,IAAI,WAAW,OAAO,UAAU;AAAA,MAC1D,OAAO,EAAE,OAAO,WAAW,IAAI,WAAW,OAAO,UAAU;AAAA,MAC3D,SAAS,EAAE,OAAO,WAAW,IAAI,WAAW,OAAO,UAAU;AAAA,IAC/D;AAAA,IAEA,WAAW;AAAA,IACX,gBAAgB;AAAA,IAChB,SAAS;AAAA,IACT,cAAc;AAAA,IACd,iBAAiB;AAAA,IACjB,YAAY;AAAA,IACZ,WAAW;AAAA,IAEX,aAAa;AAAA,IACb,eAAe;AAAA,IACf,OAAO;AAAA,EACT;AAAA,EAEA,YAAY;AAAA,IACV,MAAM;AAAA,IACN,MAAM;AAAA,EACR;AAAA,EAEA,UAAU;AAAA,IACR,MAAM;AAAA,IACN,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,EACN;AAAA,EAEA,QAAQ;AAAA,IACN,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,IAAI;AAAA,EACN;AAAA,EAEA,SAAS;AAAA,IACP,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,IAAI;AAAA,IACJ,KAAK;AAAA,EACP;AACF;AAQO,IAAM,iBAAiB,CAAC,UAC5B,MAAM,OAAO,OAAsD,KAAK,KACzE,MAAM,OAAO,OAAO;;;ACtEtB,SAAS,MAAM,mBAAmB;;;ACClC,IAAI;AAGG,SAAS,WAAW,IAAoB;AAC7C,oBAAkB,IAAI,KAAK,eAAe,QAAW;AAAA,IACnD,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,QAAQ;AAAA,EACV,CAAC;AACD,SAAO,cAAc,OAAO,IAAI,KAAK,EAAE,CAAC;AAC1C;;;ACOE,cAwBA,YAxBA;AARF,IAAM,WAAW,EAAE,OAAO,gBAAgB,MAAM,GAAG;AAEnD,IAAM,MAAM,CAAC;AAAA,EACX,OAAO,SAAS;AAAA,EAChB,QAAQ,SAAS;AAAA,EACjB;AAAA,EACA;AACF,MACE;AAAA,EAAC;AAAA;AAAA,IACC,eAAY;AAAA,IACZ,MAAK;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,eAAc;AAAA,IACd,gBAAe;AAAA,IACf,aAAa;AAAA,IACb;AAAA,IACA,SAAQ;AAAA,IACR,OAAO;AAAA,IACP,OAAM;AAAA,IAEL;AAAA;AACH;AAGK,IAAM,WAAW,CAAC,UACvB,oBAAC,OAAK,GAAG,OACP,8BAAC,UAAK,GAAE,sFAAqF,GAC/F;AAGK,IAAM,YAAY,CAAC,UACxB,qBAAC,OAAK,GAAG,OACP;AAAA,sBAAC,UAAK,QAAQ,IAAI,IAAI,GAAG,OAAO,GAAG,GAAG,IAAI,GAAG,GAAG;AAAA,EAChD,oBAAC,UAAK,QAAQ,IAAI,IAAI,GAAG,OAAO,GAAG,GAAG,GAAG,GAAG,GAAG;AAAA,GACjD;AAGK,IAAM,QAAQ,CAAC,UACpB,qBAAC,OAAK,GAAG,OACP;AAAA,sBAAC,UAAK,GAAE,cAAa;AAAA,EACrB,oBAAC,UAAK,GAAE,cAAa;AAAA,GACvB;AAGK,IAAM,YAAY,CAAC,UACxB,oBAAC,OAAK,GAAG,OACP,8BAAC,UAAK,GAAE,mBAAkB,GAC5B;AAGK,IAAM,mBAAmB,CAAC,UAC/B,oBAAC,OAAK,GAAG,OACP,8BAAC,UAAK,GAAE,iBAAgB,GAC1B;AAGK,IAAM,qBAAqB,CAAC,UACjC,qBAAC,OAAK,GAAG,OACP;AAAA,sBAAC,UAAK,QAAQ,IAAI,IAAI,GAAG,OAAO,IAAI,GAAG,GAAG,GAAG,GAAG;AAAA,EAChD,oBAAC,UAAK,GAAE,WAAU;AAAA,EAClB,oBAAC,UAAK,GAAE,kBAAiB;AAAA,GAC3B;AAGK,IAAM,oBAAoB,CAAC,UAChC,qBAAC,OAAK,GAAG,OACP;AAAA,sBAAC,UAAK,QAAQ,IAAI,IAAI,GAAG,OAAO,IAAI,GAAG,GAAG,GAAG,GAAG;AAAA,EAChD,oBAAC,UAAK,GAAE,WAAU;AAAA,EAClB,oBAAC,UAAK,GAAE,iBAAgB;AAAA,GAC1B;AAGK,IAAM,aAAa,CAAC,UACzB,qBAAC,OAAK,GAAG,OACP;AAAA,sBAAC,UAAK,GAAE,YAAW;AAAA,EACnB,oBAAC,UAAK,GAAE,YAAW;AAAA,EACnB,oBAAC,UAAK,GAAE,4CAA2C;AAAA,EACnD,oBAAC,UAAK,GAAE,WAAU;AAAA,EAClB,oBAAC,UAAK,GAAE,0CAAyC;AAAA,GACnD;;;AC1EE,gBAAAC,YAAA;AAdJ,IAAM,eAAyC;AAAA,EAC7C,OAAO;AAAA,EACP,OAAO;AAAA,EACP,OAAO;AAAA,EACP,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AACX;AAEO,IAAM,aAAa,CAAC,EAAE,MAAM,MAA2B;AAC5D,QAAM,SAAS,eAAe,KAAK;AACnC,QAAM,QAAS,aAAoD,KAAK,KAAK,MAAM,YAAY;AAE/F,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,YAAY,OAAO;AAAA,QACnB,cAAc,MAAM,OAAO;AAAA,QAC3B,OAAO,MAAM,OAAO;AAAA,QACpB,SAAS;AAAA,QACT,YAAY,MAAM,WAAW;AAAA,QAC7B,UAAU,MAAM,SAAS;AAAA,QACzB,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,QAChD,WAAW;AAAA,QACX,eAAe;AAAA,MACjB;AAAA,MAEC;AAAA;AAAA,EACH;AAEJ;;;ACoBE,SACE,OAAAC,MADF,QAAAC,aAAA;AAnDF,IAAM,iBAAiB;AAAA,EACrB,SAAS;AAAA,EACT,KAAK,MAAM,QAAQ;AAAA,EACnB,cAAc,MAAM,QAAQ;AAC9B;AAEA,IAAM,mBAAmB;AAAA,EACvB,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY;AAAA,EACZ,UAAU,MAAM,SAAS;AAAA,EACzB,UAAU;AAAA,EACV,WAAW;AACb;AAEA,IAAM,mBAAmB;AAAA,EACvB,UAAU,MAAM,SAAS;AAAA,EACzB,WAAW;AACb;AAEA,IAAM,iBAAiB;AAAA,EACrB,WAAW,aAAa,MAAM,OAAO,MAAM;AAAA,EAC3C,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAClD;AAEA,IAAM,gBAAgB;AAAA,EACpB,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY,MAAM,WAAW;AAC/B;AAEA,IAAM,cAAc;AAAA,EAClB,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY,MAAM,WAAW;AAC/B;AAEA,IAAM,eAAe,EAAE,OAAO,MAAM,OAAO,YAAY;AAEvD,IAAM,kBAAkB;AAAA,EACtB,YAAY;AAAA,EACZ,cAAc,MAAM,OAAO;AAAA,EAC3B,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAAA,EACzB,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,UAAU;AAAA,EACV,SAAS,MAAM,QAAQ;AAAA,EACvB,YAAY;AAAA,EACZ,WAAW;AACb;AAEA,IAAM,YAAY,CAAC,EAAE,OAAO,SAAS,MACnC,gBAAAA,MAAC,SAAI,OAAO,gBACV;AAAA,kBAAAD,KAAC,UAAK,OAAO,kBAAmB,iBAAM;AAAA,EACtC,gBAAAA,KAAC,UAAK,OAAO,kBAAmB,UAAS;AAAA,GAC3C;AAGK,IAAM,YAAY,CAAC,EAAE,OAAO,MAAqC;AACtE,QAAM,cAAc,eAAe,OAAO,KAAK;AAC/C,QAAM,gBAAgB,OAAO,KAAK,OAAO,UAAU,EAAE,SAAS;AAE9D,SACE,gBAAAC,MAAC,SAAI,eAAY,cAAa,OAAO,EAAE,GAAG,gBAAgB,YAAY,YAAY,GAAG,GACnF;AAAA,oBAAAD,KAAC,aAAU,OAAM,QAAQ,qBAAW,OAAO,SAAS,GAAE;AAAA,IACtD,gBAAAA,KAAC,aAAU,OAAM,SACf,0BAAAA,KAAC,UAAK,OAAO,EAAE,OAAO,YAAY,OAAO,YAAY,IAAI,GAAI,iBAAO,OAAM,GAC5E;AAAA,IACA,gBAAAA,KAAC,aAAU,OAAM,YACf,0BAAAA,KAAC,UAAK,OAAO,eAAgB,iBAAO,SAAS,KAAK,GAAG,GAAE,GACzD;AAAA,IACC,OAAO,SACN,gBAAAA,KAAC,aAAU,OAAM,UACf,0BAAAA,KAAC,UAAK,OAAO,aAAc,iBAAO,QAAO,GAC3C,IACE;AAAA,IACJ,gBAAAA,KAAC,aAAU,OAAM,WACf,0BAAAA,KAAC,UAAK,OAAO,cAAe,iBAAO,aAAY,GACjD;AAAA,IAEC,iBACC,gBAAAA,KAAC,aAAU,OAAM,QACf,0BAAAA,KAAC,SAAI,OAAO,iBAAkB,wBAAc,OAAO,YAAY,CAAC,GAAE,GACpE;AAAA,KAEJ;AAEJ;;;AJTM,SASE,OAAAE,MATF,QAAAC,aAAA;AApEN,IAAM,WAAW;AAAA,EACf,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,cAAc,aAAa,MAAM,OAAO,MAAM;AAAA,EAC9C,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,KAAK,MAAM,QAAQ;AAAA,EACnB,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,EAChD,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,OAAO;AACT;AAEA,IAAM,iBAAiB;AAAA,EACrB,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY;AAAA,EACZ,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAC3B;AAEA,IAAMC,iBAAgB;AAAA,EACpB,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY;AAAA,EACZ,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAAA,EACzB,UAAU;AAAA,EACV,UAAU;AAAA,EACV,cAAc;AAAA,EACd,YAAY;AACd;AAEA,IAAMC,gBAAe;AAAA,EACnB,OAAO,MAAM,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,UAAU,MAAM,SAAS;AAAA,EACzB,UAAU;AAAA,EACV,cAAc;AAAA,EACd,YAAY;AACd;AAEA,IAAMC,eAAc;AAAA,EAClB,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY;AAAA,EACZ,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAAA,EACzB,UAAU;AAAA,EACV,SAAS;AAAA,EACT,UAAU;AAAA,EACV,cAAc;AAAA,EACd,YAAY;AACd;AAEA,IAAM,eAAe;AAAA,EACnB,OAAO,MAAM,OAAO;AAAA,EACpB,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,YAAY;AACd;AAEA,IAAM,kBAAkB,CAAC,EAAE,QAAQ,UAAU,SAAS,MAAmB;AACvE,QAAM,cAAc,eAAe,OAAO,KAAK;AAE/C,QAAM,cAAc,YAAY,MAAM;AACpC,aAAS,WAAW,OAAO,OAAO,EAAE;AAAA,EACtC,GAAG,CAAC,UAAU,UAAU,OAAO,EAAE,CAAC;AAElC,SACE,gBAAAH,MAAC,SACC;AAAA,oBAAAA;AAAA,MAAC;AAAA;AAAA,QACC,iBAAe;AAAA,QACf,uBAAoB;AAAA,QACpB,eAAY;AAAA,QACZ,SAAS;AAAA,QACT,OAAO,EAAE,GAAG,UAAU,YAAY,WAAW,YAAY,KAAK,cAAc;AAAA,QAC5E,MAAK;AAAA,QAGL;AAAA,0BAAAD,KAAC,UAAK,OAAO,gBAAiB,qBAAW,OAAO,SAAS,GAAE;AAAA,UAG3D,gBAAAA,KAAC,cAAW,OAAO,OAAO,OAAO;AAAA,UAGjC,gBAAAA,KAAC,UAAK,OAAOE,gBAAgB,iBAAO,SAAS,KAAK,GAAG,GAAE;AAAA,UAGvD,gBAAAF,KAAC,UAAK,OAAOG,eAAe,iBAAO,aAAY;AAAA,UAG9C,OAAO,SAAS,gBAAAH,KAAC,UAAK,OAAOI,cAAc,iBAAO,QAAO,IAAU;AAAA,UAGpE,gBAAAJ,KAAC,UAAK,OAAO,EAAE,GAAG,cAAc,WAAW,WAAW,kBAAkB,eAAe,GACrF,0BAAAA,KAAC,oBAAiB,MAAM,IAAI,GAC9B;AAAA;AAAA;AAAA,IACF;AAAA,IAEC,WAAW,gBAAAA,KAAC,aAAU,QAAgB,IAAK;AAAA,KAC9C;AAEJ;AAGO,IAAM,SAAS,KAAK,eAAe;;;AFvCpC,gBAAAK,YAAA;AAhEN,IAAM,uBAAuB;AAC7B,IAAM,WAAW;AAEjB,IAAM,kBAAkB;AAExB,IAAM,aAAa;AAAA,EACjB,YAAY;AAAA,EACZ,OAAO,MAAM,OAAO;AAAA,EACpB,SAAS;AAAA,EACT,MAAM;AAAA,EACN,UAAU,MAAM,SAAS;AAAA,EACzB,gBAAgB;AAClB;AAEA,IAAMC,kBAAiB;AAAA,EACrB,MAAM;AAAA,EACN,UAAU;AAAA,EACV,gBAAgB,GAAG,MAAM,OAAO,SAAS;AAAA,EACzC,gBAAgB;AAClB;AAEO,IAAM,UAAU,CAAC,EAAE,SAAS,YAAY,UAAU,WAAW,MAAoB;AACtF,QAAM,eAAe,OAAuB,IAAI;AAChD,QAAM,gBAAgB,OAAO,IAAI;AAEjC,QAAM,cAAc,eAAe;AAAA,IACjC,OAAO,QAAQ;AAAA,IACf,cAAc,MAAM;AAAA;AAAA;AAAA,IAGpB,YAAY,CAAC,UAAU,QAAQ,KAAK,GAAG,MAAM;AAAA,IAC7C,kBAAkB,MAAM,aAAa;AAAA,IACrC,UAAU;AAAA,EACZ,CAAC;AAED,QAAM,cAAc,QAAQ;AAG5B,QAAM,eAAe,QAAQ,cAAc,CAAC,GAAG;AAC/C,YAAU,MAAM;AACd,QAAI,iBAAiB,UAAa,EAAE,cAAc,cAAc,UAAU;AACxE;AAAA,IACF;AACA,gBAAY,cAAc,cAAc,GAAG,EAAE,OAAO,MAAM,CAAC;AAAA,EAC7D,GAAG,CAAC,YAAY,cAAc,aAAa,WAAW,CAAC;AAIvD,YAAU,MAAM;AACd,QAAI,gBAAgB,GAAG;AACrB,oBAAc,UAAU;AAAA,IAC1B;AAAA,EACF,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,eAAeC,aAAY,MAAM;AACrC,UAAM,KAAK,aAAa;AACxB,QAAI,CAAC,IAAI;AACP;AAAA,IACF;AACA,kBAAc,UAAU,GAAG,eAAe,GAAG,YAAY,GAAG,eAAe;AAAA,EAC7E,GAAG,CAAC,CAAC;AAEL,MAAI,QAAQ,WAAW,GAAG;AACxB,WACE,gBAAAF,KAAC,SAAI,eAAY,kBAAiB,OAAO,YAAY,yBAErD;AAAA,EAEJ;AAEA,QAAM,eAAe,YAAY,gBAAgB;AAEjD,SACE,gBAAAA,KAAC,SAAI,eAAY,YAAW,UAAU,cAAc,KAAK,cAAc,OAAOC,iBAE5E,0BAAAD;AAAA,IAAC;AAAA;AAAA,MACC,OAAO;AAAA,QACL,QAAQ,GAAG,YAAY,aAAa,CAAC;AAAA,QACrC,UAAU;AAAA,QACV,OAAO;AAAA,MACT;AAAA,MAEA,0BAAAA;AAAA,QAAC;AAAA;AAAA,UACC,OAAO;AAAA,YACL,MAAM;AAAA,YACN,UAAU;AAAA,YACV,KAAK;AAAA,YACL,WAAW,cAAc,aAAa,CAAC,GAAG,SAAS,CAAC;AAAA,YACpD,OAAO;AAAA,UACT;AAAA,UAEC,uBAAa,IAAI,CAAC,gBAAgB;AACjC,kBAAM,SAAS,QAAQ,YAAY,KAAK;AACxC,mBACE,gBAAAA;AAAA,cAAC;AAAA;AAAA,gBACC,cAAY,YAAY;AAAA,gBAExB,KAAK,YAAY;AAAA,gBAEjB,0BAAAA,KAAC,UAAO,UAAU,eAAe,OAAO,IAAI,UAAoB,QAAgB;AAAA;AAAA,cAH3E,YAAY;AAAA,YAInB;AAAA,UAEJ,CAAC;AAAA;AAAA,MACH;AAAA;AAAA,EACF,GACF;AAEJ;;;AOzHA,SAAS,gBAAgB;AACzB,SAAS,eAAe;AACxB,SAAS,iBAAiB;AAC1B,SAAS,eAAAG,cAAa,aAAAC,YAAW,SAAS,gBAAgB;AAyGtD,gBAAAC,MA4FE,QAAAC,aA5FF;AAnFJ,IAAM,iBAAiB;AACvB,SAAS,iBAAiB;AACxB,EAAAC,WAAU,MAAM;AACd,QAAI,SAAS,eAAe,cAAc,GAAG;AAC3C;AAAA,IACF;AACA,UAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,UAAM,KAAK;AACX,UAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BASK,MAAM,OAAO,MAAM;AAAA;AAAA;AAAA;AAAA,iBAI/B,MAAM,OAAO,WAAW;AAAA;AAAA;AAAA;AAAA,iBAIxB,MAAM,OAAO,KAAK;AAAA;AAAA;AAG/B,aAAS,KAAK,YAAY,KAAK;AAAA,EACjC,GAAG,CAAC,CAAC;AACP;AAEA,IAAM,cAAc;AAAA,EAClB,YAAY,MAAM,OAAO;AAAA,EACzB,QAAQ,aAAa,MAAM,OAAO,MAAM;AAAA,EACxC,cAAc,MAAM,OAAO;AAAA,EAC3B,OAAO,MAAM,OAAO;AAAA,EACpB,QAAQ;AAAA,EACR,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAAA,EACzB,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,EAChD,YAAY;AAAA,EACZ,OAAO;AACT;AAEA,IAAM,aAAa;AAAA,EACjB,YAAY,MAAM,OAAO;AAAA,EACzB,QAAQ,aAAa,MAAM,OAAO,WAAW;AAAA,EAC7C,cAAc,MAAM,OAAO;AAAA,EAC3B,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAAA,EACzB,SAAS;AAAA,EACT,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,EAChD,YAAY;AAAA,EACZ,OAAO;AACT;AAEA,IAAM,oBAAoB;AAAA,EACxB,OAAO,MAAM,OAAO;AAAA,EACpB,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,eAAe;AACjB;AAEA,IAAM,cAAc,CAAC;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAOM;AACJ,QAAM,SAAS,MAAM,OAAO,OAAO,KAAK;AACxC,QAAM,cAAcC,aAAY,MAAM,SAAS,KAAK,GAAG,CAAC,OAAO,QAAQ,CAAC;AAExE,SACE,gBAAAH;AAAA,IAAC;AAAA;AAAA,MACC,gBAAc;AAAA,MACd,uBAAoB;AAAA,MACpB,eAAa,gBAAgB,KAAK;AAAA,MAClC,SAAS;AAAA,MACT,OAAO;AAAA,QACL,YAAY,SAAS,OAAO,QAAQ;AAAA,QACpC,QAAQ,aAAa,SAAS,OAAO,QAAQ,MAAM,OAAO,MAAM;AAAA,QAChE,cAAc,MAAM,OAAO;AAAA,QAC3B,OAAO,SAAS,MAAM,OAAO,QAAQ,OAAO;AAAA,QAC5C,QAAQ;AAAA,QACR,YAAY,MAAM,WAAW;AAAA,QAC7B,UAAU,MAAM,SAAS;AAAA,QACzB,YAAY;AAAA,QACZ,YAAY;AAAA,QACZ,UAAU;AAAA,QACV,SAAS,SAAS,IAAI;AAAA,QACtB,SAAS;AAAA,QACT,WAAW;AAAA,QACX,YAAY;AAAA,MACd;AAAA,MACA,OAAO,UAAU,KAAK;AAAA,MACtB,MAAK;AAAA,MAEJ,gBAAM,MAAM,GAAG,CAAC,EAAE,YAAY;AAAA;AAAA,EACjC;AAEJ;AAEO,IAAM,UAAU,CAAC;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,MAAoB;AAClB,iBAAe;AAIf,QAAM,CAAC,QAAQ,SAAS,IAAI,SAAS,EAAE;AACvC,QAAM,kBAAkB;AAAA,IACtB,MAAM,IAAI,UAAU,oBAAoB,EAAE,MAAM,IAAI,CAAC;AAAA,IACrD,CAAC,kBAAkB;AAAA,EACrB;AAEA,EAAAE,WAAU,MAAM,MAAM,gBAAgB,OAAO,GAAG,CAAC,eAAe,CAAC;AAEjE,QAAM,qBAAqBC;AAAA,IACzB,CAAC,MAA2C;AAC1C,YAAM,EAAE,MAAM,IAAI,EAAE;AACpB,gBAAU,KAAK;AACf,sBAAgB,aAAa,KAAK;AAAA,IACpC;AAAA,IACA,CAAC,eAAe;AAAA,EAClB;AAEA,QAAM,oBAAoBA;AAAA,IACxB,CAAC,UAAoB;AACnB,YAAM,OAAO,IAAI,IAAI,WAAW;AAChC,UAAI,KAAK,IAAI,KAAK,GAAG;AACnB,aAAK,OAAO,KAAK;AAAA,MACnB,OAAO;AACL,aAAK,IAAI,KAAK;AAAA,MAChB;AACA,0BAAoB,IAAI;AAAA,IAC1B;AAAA,IACA,CAAC,aAAa,mBAAmB;AAAA,EACnC;AAEA,SACE,gBAAAF;AAAA,IAAC;AAAA;AAAA,MACC,eAAY;AAAA,MACZ,OAAO;AAAA,QACL,SAAS;AAAA,QACT,eAAe;AAAA,QACf,YAAY;AAAA,QACZ,KAAK,MAAM,QAAQ;AAAA,QACnB,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,UAAU;AAAA,QACV,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,MAClD;AAAA,MAGA;AAAA,wBAAAA,MAAC,SAAI,OAAO,EAAE,WAAW,SAAS,GAChC;AAAA,0BAAAD;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,gBACL,OAAO,MAAM,OAAO;AAAA,gBACpB,YAAY,MAAM,WAAW;AAAA,gBAC7B,UAAU,MAAM,SAAS;AAAA,gBACzB,YAAY;AAAA,cACd;AAAA,cAEC,4BAAkB,aAAa,GAAG,UAAU,KAAK,GAAG,aAAa,MAAM,UAAU;AAAA;AAAA,UACpF;AAAA,UACA,gBAAAA,KAAC,SAAI,OAAO,EAAE,OAAO,MAAM,OAAO,WAAW,UAAU,MAAM,SAAS,GAAG,GAAG,kBAAI;AAAA,WAClF;AAAA,QAGA,gBAAAC,MAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,eAAe,UAAU,KAAK,MAAM,QAAQ,GAAG,GAC5E;AAAA,0BAAAD,KAAC,SAAI,OAAO,mBAAmB,qBAAO;AAAA,UACtC,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,uBAAoB;AAAA,cACpB,SAAS,SAAS,WAAW;AAAA,cAC7B,OAAO;AAAA,gBACL,GAAG;AAAA,gBACH,YAAY,SAAS,MAAM,OAAO,OAAO,QAAQ,QAAQ,YAAY;AAAA,gBACrE,OAAO,SAAS,MAAM,OAAO,QAAQ,YAAY;AAAA,cACnD;AAAA,cACA,OAAO,SAAS,wBAAwB;AAAA,cACxC,MAAK;AAAA,cAEL,0BAAAC,MAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,eAAe,KAAK,MAAM,GACrE;AAAA,yBAAS,gBAAAD,KAAC,YAAS,MAAM,IAAI,IAAK,gBAAAA,KAAC,aAAU,MAAM,IAAI;AAAA,gBACvD,SAAS,WAAW;AAAA,iBACvB;AAAA;AAAA,UACF;AAAA,UAEA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,uBAAoB;AAAA,cACpB,SAAS;AAAA,cACT,OAAO;AAAA,cACP,OAAM;AAAA,cACN,MAAK;AAAA,cAEL,0BAAAC,MAAC,UAAK,OAAO,EAAE,YAAY,UAAU,SAAS,eAAe,KAAK,MAAM,GACtE;AAAA,gCAAAD,KAAC,cAAW,MAAM,IAAI;AAAA,gBAAE;AAAA,iBAE1B;AAAA;AAAA,UACF;AAAA,WACF;AAAA,QAGA,gBAAAC,MAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,eAAe,UAAU,KAAK,MAAM,QAAQ,GAAG,GAC5E;AAAA,0BAAAA,MAAC,SAAI,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,gBAAgB,gBAAgB,GACnF;AAAA,4BAAAD,KAAC,SAAI,OAAO,mBAAmB,oBAAM;AAAA,YACpC,YAAY,OAAO,KAClB,gBAAAA;AAAA,cAAC;AAAA;AAAA,gBACC,qBAAkB;AAAA,gBAClB,SAAS,MAAM,oBAAoB,oBAAI,IAAI,CAAC;AAAA,gBAC5C,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,OAAO,MAAM,OAAO;AAAA,kBACpB,QAAQ;AAAA,kBACR,UAAU;AAAA,kBACV,SAAS;AAAA,kBACT,YAAY;AAAA,gBACd;AAAA,gBACA,OAAM;AAAA,gBACN,MAAK;AAAA,gBACN;AAAA;AAAA,YAED;AAAA,aAEJ;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,OAAO;AAAA,gBACL,SAAS;AAAA,gBACT,KAAK,MAAM,QAAQ;AAAA,gBACnB,qBAAqB;AAAA,cACvB;AAAA,cAEC,qBAAW,IAAI,CAAC,UACf,gBAAAA;AAAA,gBAAC;AAAA;AAAA,kBACC,QAAQ,YAAY,SAAS,KAAK,YAAY,IAAI,KAAK;AAAA,kBAEvD;AAAA,kBACA,UAAU;AAAA,kBACV,SAAS,YAAY,IAAI,KAAK;AAAA;AAAA,gBAHzB;AAAA,cAIP,CACD;AAAA;AAAA,UACH;AAAA,WACF;AAAA,QAGA,gBAAAC,MAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,eAAe,UAAU,KAAK,MAAM,QAAQ,GAAG,GAC5E;AAAA,0BAAAA,MAAC,SAAI,OAAO,EAAE,YAAY,UAAU,SAAS,QAAQ,gBAAgB,gBAAgB,GACnF;AAAA,4BAAAD,KAAC,SAAI,OAAO,mBAAmB,sBAAQ;AAAA,YACtC,eAAe,SAAS,KACvB,gBAAAA;AAAA,cAAC;AAAA;AAAA,gBACC,qBAAkB;AAAA,gBAClB,SAAS,MAAM,uBAAuB,CAAC,CAAC;AAAA,gBACxC,OAAO;AAAA,kBACL,YAAY;AAAA,kBACZ,QAAQ;AAAA,kBACR,OAAO,MAAM,OAAO;AAAA,kBACpB,QAAQ;AAAA,kBACR,UAAU;AAAA,kBACV,SAAS;AAAA,kBACT,YAAY;AAAA,gBACd;AAAA,gBACA,OAAM;AAAA,gBACN,MAAK;AAAA,gBACN;AAAA;AAAA,YAED;AAAA,aAEJ;AAAA,UACC,WAAW,WAAW,IACrB,gBAAAC,MAAC,QAAQ,MAAR,EACC;AAAA,4BAAAD;AAAA,cAAC,QAAQ;AAAA,cAAR;AAAA,gBACC,OAAO;AAAA,gBACP,cAAc;AAAA,gBACd,aAAW;AAAA,gBACX,QACE,gBAAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,eAAY;AAAA,oBACZ,OAAO;AAAA,sBACL,GAAG;AAAA,sBACH,OAAO,MAAM,OAAO;AAAA,sBACpB,QAAQ;AAAA,sBACR,SAAS;AAAA,oBACX;AAAA,oBACD;AAAA;AAAA,gBAED;AAAA;AAAA,YAEJ;AAAA,YACA,gBAAAA,KAAC,QAAQ,QAAR,EACC,0BAAAA,KAAC,QAAQ,YAAR,EAAmB,YAAY,GAAG,OAAO,EAAE,QAAQ,OAAQ,GAC1D,0BAAAA;AAAA,cAAC,QAAQ;AAAA,cAAR;AAAA,gBACC,OAAO;AAAA,kBACL,YAAY,MAAM,OAAO;AAAA,kBACzB,QAAQ,aAAa,MAAM,OAAO,MAAM;AAAA,kBACxC,cAAc,MAAM,OAAO;AAAA,kBAC3B,WAAW;AAAA,kBACX,UAAU,MAAM,SAAS;AAAA,kBACzB,UAAU;AAAA,kBACV,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,gBAClD;AAAA,gBAEA,0BAAAA,KAAC,OAAE,OAAO,EAAE,OAAO,MAAM,OAAO,WAAW,YAAY,KAAK,QAAQ,EAAE,GAAG,gEAEzE;AAAA;AAAA,YACF,GACF,GACF;AAAA,aACF,IAEA,gBAAAC;AAAA,YAAC,SAAS;AAAA,YAAT;AAAA,cACC,OAAO;AAAA,cACP,UAAQ;AAAA,cACR,eAAe;AAAA,cACf,OAAO;AAAA,cAEP;AAAA,gCAAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,OAAO;AAAA,sBACL,YAAY,MAAM,OAAO;AAAA,sBACzB,QAAQ,aAAa,MAAM,OAAO,WAAW;AAAA,sBAC7C,cAAc,MAAM,OAAO;AAAA,sBAC3B,SAAS;AAAA,sBACT,UAAU;AAAA,sBACV,KAAK;AAAA,sBACL,WAAW;AAAA,sBACX,SAAS;AAAA,sBACT,YAAY;AAAA,oBACd;AAAA,oBAEC;AAAA,qCAAe,IAAI,CAAC,QACnB,gBAAAA;AAAA,wBAAC;AAAA;AAAA,0BAEC,OAAO;AAAA,4BACL,YAAY;AAAA,4BACZ,YAAY;AAAA,4BACZ,QAAQ;AAAA,4BACR,cAAc,MAAM,OAAO;AAAA,4BAC3B,OAAO,MAAM,OAAO;AAAA,4BACpB,SAAS;AAAA,4BACT,YAAY,MAAM,WAAW;AAAA,4BAC7B,UAAU;AAAA,4BACV,KAAK;AAAA,4BACL,YAAY;AAAA,4BACZ,SAAS;AAAA,0BACX;AAAA,0BAEC;AAAA;AAAA,4BACD,gBAAAD;AAAA,8BAAC;AAAA;AAAA,gCACC,uBAAoB;AAAA,gCACpB,SAAS,MAAM,uBAAuB,eAAe,OAAO,CAAC,MAAM,MAAM,GAAG,CAAC;AAAA,gCAC7E,OAAO;AAAA,kCACL,YAAY;AAAA,kCACZ,QAAQ;AAAA,kCACR,OAAO,MAAM,OAAO;AAAA,kCACpB,QAAQ;AAAA,kCACR,UAAU;AAAA,kCACV,YAAY;AAAA,kCACZ,SAAS;AAAA,kCACT,SAAS;AAAA,kCACT,YAAY;AAAA,gCACd;AAAA,gCACA,OAAO,UAAU,GAAG;AAAA,gCACpB,MAAK;AAAA,gCAEL,0BAAAA,KAAC,SAAM,MAAM,GAAG;AAAA;AAAA,4BAClB;AAAA;AAAA;AAAA,wBAlCK;AAAA,sBAmCP,CACD;AAAA,sBACD,gBAAAA;AAAA,wBAAC,SAAS;AAAA,wBAAT;AAAA,0BACC,cAAW;AAAA,0BACX,iBAAc;AAAA,0BACd,eAAY;AAAA,0BACZ,aAAa,eAAe,WAAW,IAAI,mBAAc;AAAA,0BACzD,OAAO;AAAA,4BACL,YAAY;AAAA,4BACZ,QAAQ;AAAA,4BACR,OAAO,MAAM,OAAO;AAAA,4BACpB,MAAM;AAAA,4BACN,YAAY,MAAM,WAAW;AAAA,4BAC7B,UAAU,MAAM,SAAS;AAAA,4BACzB,UAAU;AAAA,4BACV,SAAS;AAAA,4BACT,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,0BAClD;AAAA;AAAA,sBACF;AAAA;AAAA;AAAA,gBACF;AAAA,gBACA,gBAAAA,KAAC,SAAS,QAAT,EACC,0BAAAA,KAAC,SAAS,YAAT,EAAoB,OAAO,EAAE,QAAQ,OAAQ,GAC5C,0BAAAA;AAAA,kBAAC,SAAS;AAAA,kBAAT;AAAA,oBACC,OAAO;AAAA,sBACL,YAAY,MAAM,OAAO;AAAA,sBACzB,QAAQ,aAAa,MAAM,OAAO,MAAM;AAAA,sBACxC,cAAc,MAAM,OAAO;AAAA,sBAC3B,WAAW;AAAA,sBACX,WAAW;AAAA,sBACX,UAAU;AAAA,sBACV,SAAS,MAAM,QAAQ;AAAA,oBACzB;AAAA,oBAEA,0BAAAA,KAAC,SAAS,MAAT,EACE,WAAC,MAAM,UACN,gBAAAC;AAAA,sBAAC,SAAS;AAAA,sBAAT;AAAA,wBACC;AAAA,wBAEA,OAAO;AAAA,0BACL,YAAY;AAAA,0BACZ,cAAc,MAAM,OAAO;AAAA,0BAC3B,OAAO,MAAM,OAAO;AAAA,0BACpB,QAAQ;AAAA,0BACR,SAAS;AAAA,0BACT,YAAY,MAAM,WAAW;AAAA,0BAC7B,UAAU,MAAM,SAAS;AAAA,0BACzB,KAAK,MAAM,QAAQ;AAAA,0BACnB,gBAAgB;AAAA,0BAChB,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,0BAChD,YAAY;AAAA,wBACd;AAAA,wBACA,OAAO;AAAA,wBAEP;AAAA,0CAAAD,KAAC,UAAK,OAAO,EAAE,MAAM,GAAG,UAAU,UAAU,cAAc,WAAW,GAClE,iBAAO,IAAI,GACd;AAAA,0BACA,gBAAAA,KAAC,SAAS,eAAT,EACC,0BAAAA,KAAC,aAAU,OAAO,MAAM,OAAO,QAAQ,MAAM,IAAI,GACnD;AAAA;AAAA;AAAA,sBArBK,OAAO,IAAI;AAAA,oBAsBlB,GAEJ;AAAA;AAAA,gBACF,GACF,GACF;AAAA;AAAA;AAAA,UACF;AAAA,WAEJ;AAAA,QAGA,gBAAAC,MAAC,SAAI,OAAO,EAAE,SAAS,QAAQ,eAAe,UAAU,KAAK,MAAM,QAAQ,GAAG,GAC5E;AAAA,0BAAAD,KAAC,SAAI,OAAO,mBAAmB,oBAAM;AAAA,UACrC,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,cAAW;AAAA,cACX,iBAAc;AAAA,cACd,eAAY;AAAA,cACZ,UAAU;AAAA,cACV,aAAY;AAAA,cACZ,OAAO;AAAA,cACP,MAAK;AAAA,cACL,OAAO;AAAA;AAAA,UACT;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;;;AR9VQ,SAkCI,UAlCJ,OAAAI,MA2BI,QAAAC,aA3BJ;AAvIR,IAAM,YAAY;AAAA,EAChB,YAAY,MAAM,OAAO;AAAA,EACzB,OAAO,MAAM,OAAO;AAAA,EACpB,SAAS;AAAA,EACT,eAAe;AAAA,EACf,YAAY,MAAM,WAAW;AAAA,EAC7B,QAAQ;AAAA,EACR,UAAU;AACZ;AAEA,IAAM,eAAe;AAAA,EACnB,YAAY;AAAA,EACZ,UAAU;AAAA,EACV,YAAY;AACd;AAEA,IAAM,YAAY;AAAA,EAChB,SAAS;AAAA,EACT,MAAM;AAAA,EACN,eAAe;AAAA,EACf,UAAU;AACZ;AAEA,IAAM,cAAc;AAAA,EAClB,YAAY;AAAA,EACZ,cAAc,aAAa,MAAM,OAAO,MAAM;AAAA,EAC9C,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,KAAK,MAAM,QAAQ;AAAA,EACnB,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAClD;AAEA,IAAM,qBAAqB;AAAA,EACzB,YAAY;AAAA,EACZ,QAAQ;AAAA,EACR,OAAO,MAAM,OAAO;AAAA,EACpB,QAAQ;AAAA,EACR,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAAA,EACzB,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,EAChD,YAAY;AACd;AAEA,IAAM,iBAAiB,EAAE,YAAY,UAAU,SAAS,eAAe,KAAK,MAAM;AAElF,IAAM,aAAa;AAAA,EACjB,OAAO,MAAM,OAAO;AAAA,EACpB,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAC3B;AAEA,IAAM,qBAAqB;AAAA,EACzB,YAAY,MAAM,OAAO;AAAA,EACzB,QAAQ,aAAa,MAAM,OAAO,MAAM;AAAA,EACxC,cAAc,MAAM,OAAO;AAAA,EAC3B,OAAO,MAAM,OAAO;AAAA,EACpB,QAAQ;AAAA,EACR,YAAY,MAAM,WAAW;AAAA,EAC7B,UAAU,MAAM,SAAS;AAAA,EACzB,SAAS,GAAG,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE;AAAA,EAChD,YAAY;AACd;AAEO,IAAM,wBAAwB,CAAC,EAAE,MAAM,MAAa;AACzD,QAAM,aAAa,qBAAqB,MAAM,WAAW,MAAM,aAAa,MAAM,WAAW;AAE7F,QAAM,CAAC,QAAQ,SAAS,IAAIC,UAAS,KAAK;AAC1C,QAAM,CAAC,eAAe,gBAAgB,IAAIA,UAA8B,CAAC,CAAC;AAC1E,QAAM,CAAC,aAAa,cAAc,IAAIA,UAAwB,oBAAI,IAAI,CAAC;AACvE,QAAM,CAAC,gBAAgB,iBAAiB,IAAIA,UAAmB,CAAC,CAAC;AACjE,QAAM,CAAC,YAAY,aAAa,IAAIA,UAAS,EAAE;AAC/C,QAAM,CAAC,YAAY,aAAa,IAAIA,UAAwB,IAAI;AAChE,QAAM,CAAC,aAAa,cAAc,IAAIA,UAAS,IAAI;AAEnD,QAAM,cAAcC,aAAY,MAAM;AACpC,cAAU,IAAI;AACd,qBAAiB,MAAM,YAAY,CAAC;AAAA,EACtC,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,eAAeA,aAAY,MAAM;AACrC,cAAU,KAAK;AACf,qBAAiB,CAAC,CAAC;AAAA,EACrB,GAAG,CAAC,CAAC;AAEL,QAAM,cAAcA,aAAY,MAAM;AACpC,UAAM,MAAM;AACZ,qBAAiB,CAAC,CAAC;AACnB,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,KAAK,CAAC;AAEV,QAAM,UAAU,SAAS,gBAAgB;AAEzC,QAAM,mBAAmBC,SAAQ,MAAM;AACrC,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,KAAK,SAAS;AACvB,WAAK,IAAI,EAAE,SAAS,KAAK,GAAG,CAAC;AAAA,IAC/B;AACA,WAAO,MAAM,KAAK,IAAI,EAAE,KAAK;AAAA,EAC/B,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,kBAAkBA;AAAA,IACtB,MACE,QAAQ,OAAO,CAAC,MAAM;AACpB,UAAI,YAAY,OAAO,KAAK,CAAC,YAAY,IAAI,EAAE,KAAK,GAAG;AACrD,eAAO;AAAA,MACT;AACA,UAAI,eAAe,SAAS,KAAK,CAAC,eAAe,SAAS,EAAE,SAAS,KAAK,GAAG,CAAC,GAAG;AAC/E,eAAO;AAAA,MACT;AACA,UAAI,YAAY;AACd,cAAM,OAAO,WAAW,YAAY;AACpC,YACE,EACE,EAAE,YAAY,YAAY,EAAE,SAAS,IAAI,KACzC,EAAE,SAAS,KAAK,GAAG,EAAE,YAAY,EAAE,SAAS,IAAI,IAElD;AACA,iBAAO;AAAA,QACT;AAAA,MACF;AACA,aAAO;AAAA,IACT,CAAC;AAAA,IACH,CAAC,SAAS,aAAa,gBAAgB,UAAU;AAAA,EACnD;AAEA,SACE,gBAAAH,MAAC,SAAI,OAAO,WACV;AAAA,oBAAAD;AAAA,MAAC;AAAA;AAAA,QACC,OAAO;AAAA,UACL,GAAG;AAAA,UACH,aAAa,cAAc,aAAa,MAAM,OAAO,MAAM,KAAK;AAAA,UAChE,SAAS,cAAc,IAAI;AAAA,UAC3B,OAAO,cAAc,UAAU;AAAA,QACjC;AAAA,QAEA,0BAAAA;AAAA,UAAC;AAAA;AAAA,YACC,YAAY;AAAA,YACZ;AAAA,YACA,eAAe,gBAAgB;AAAA,YAC/B;AAAA,YACA,wBAAwB;AAAA,YACxB,SAAS;AAAA,YACT,qBAAqB;AAAA,YACrB,SAAS;AAAA,YACT,UAAU;AAAA,YACV,oBAAoB;AAAA,YACpB;AAAA,YACA,YAAY,QAAQ;AAAA;AAAA,QACtB;AAAA;AAAA,IACF;AAAA,IAEA,gBAAAC,MAAC,SAAI,OAAO,WAEV;AAAA,sBAAAA,MAAC,SAAI,OAAO,aACV;AAAA,wBAAAD;AAAA,UAAC;AAAA;AAAA,YACC,iBAAe;AAAA,YACf,qBAAkB;AAAA,YAClB,SAAS,MAAM,eAAe,CAAC,MAAM,CAAC,CAAC;AAAA,YACvC,OAAO;AAAA,YACP,OAAO,cAAc,qBAAqB;AAAA,YAC1C,MAAK;AAAA,YAEL,0BAAAC,MAAC,UAAK,OAAO,gBACV;AAAA,4BAAc,gBAAAD,KAAC,sBAAmB,MAAM,IAAI,IAAK,gBAAAA,KAAC,qBAAkB,MAAM,IAAI;AAAA,cAC9E,cAAc,kBAAkB;AAAA,eACnC;AAAA;AAAA,QACF;AAAA,QAEC,CAAC,eACA,gBAAAC,MAAA,YACE;AAAA,0BAAAD,KAAC,UAAK,OAAO,YACV,0BAAgB,WAAW,QAAQ,SAChC,GAAG,QAAQ,MAAM,UACjB,GAAG,gBAAgB,MAAM,MAAM,QAAQ,MAAM,SACnD;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,uBAAoB;AAAA,cACpB,SAAS,SAAS,eAAe;AAAA,cACjC,OAAO;AAAA,gBACL,GAAG;AAAA,gBACH,YAAY,SACR,MAAM,OAAO,OAAO,QAAQ,QAC5B,mBAAmB;AAAA,gBACvB,OAAO,SAAS,MAAM,OAAO,QAAQ,mBAAmB;AAAA,cAC1D;AAAA,cACA,OAAO,SAAS,WAAW;AAAA,cAC3B,MAAK;AAAA,cAEJ,mBAAS,gBAAAA,KAAC,YAAS,MAAM,IAAI,IAAK,gBAAAA,KAAC,aAAU,MAAM,IAAI;AAAA;AAAA,UAC1D;AAAA,UACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,uBAAoB;AAAA,cACpB,SAAS;AAAA,cACT,OAAO;AAAA,cACP,OAAM;AAAA,cACN,MAAK;AAAA,cAEL,0BAAAA,KAAC,cAAW,MAAM,IAAI;AAAA;AAAA,UACxB;AAAA,WACF;AAAA,SAEJ;AAAA,MACA,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,YAAY,CAAC;AAAA,UACb;AAAA,UACA,UAAU;AAAA,UACV,SAAS;AAAA;AAAA,MACX;AAAA,OACF;AAAA,KACF;AAEJ;;;ASjMU,gBAAAK,YAAA;AANH,IAAM,8BAA8B,CACzC,aACiC;AAAA,EACjC,aAAa,SAAS,eAAe;AAAA,EACrC,IAAI;AAAA,EACJ,MAAM,SAAS,QAAQ;AAAA,EACvB,QAAQ,gBAAAA,KAAC,yBAAsB,OAAO,SAAS,SAAS,iBAAiB;AAC3E;;;ACWO,SAAS,sBAAsB,SAAwC;AAC5E,QAAM,QAAQ,SAAS,SAAS;AAEhC,SAAO;AAAA,IACL,QAAQ,4BAA4B,EAAE,GAAG,SAAS,QAAQ,MAAM,CAAC;AAAA,IACjE,MAAM,mBAAmB,EAAE,GAAG,SAAS,MAAM,MAAM,CAAC;AAAA,EACtD;AACF;","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): void;
39
+ addRecord: (record: DevtoolsLogRecord) => void;
40
40
  /** Removes every record and notifies subscribers. */
41
- clear(): void;
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(): DevtoolsLogRecord[];
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(): boolean;
60
+ hasListeners: () => boolean;
56
61
  /** Updates the maximum number of retained records, trimming if needed. */
57
- setMaxSize(size: number): void;
62
+ setMaxSize: (size: number) => void;
58
63
  /** Registers a change listener. Returns an unsubscribe function. */
59
- subscribe(listener: Listener): () => void;
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): Sink;
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
@@ -3,7 +3,7 @@ import {
3
3
  createDevtoolsSink,
4
4
  createLogStore,
5
5
  defaultLogStore
6
- } from "./chunk-CYXOBB6A.mjs";
6
+ } from "./chunk-FDWUISCJ.mjs";
7
7
  export {
8
8
  LOG_LEVELS,
9
9
  createDevtoolsSink,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mugenlabs/logtape-devtools",
3
- "version": "0.3.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
- "files": [
29
- "dist",
30
- "assets"
31
- ],
32
- "type": "module",
26
+ "license": "MIT",
27
+ "author": "Yago Gonzalez",
33
28
  "sideEffects": false,
34
- "main": "./dist/index.mjs",
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
- "publishConfig": {
48
- "access": "public",
49
- "provenance": true
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.0.4",
53
- "@storybook/react": "^10.2.19",
54
- "@tanstack/react-devtools": "^0.9.6",
55
- "@storybook/react-vite": "^10.2.19",
56
- "react": "^19.0.0",
57
- "react-dom": "^19.0.0",
58
- "storybook": "^10.2.19"
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": ">=2.0.0",
62
- "@tanstack/react-devtools": ">=0.9.0",
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": ">=18.0.0"
76
+ "node": ">=24.0.0"
73
77
  },
74
- "dependencies": {
75
- "@base-ui-components/react": "^1.0.0-rc.0",
76
- "@tanstack/pacer": "^0.19.0",
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
- "dev": "tsup --watch",
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
- "build-storybook": "storybook build"
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":";AAgDA,IAAM,sBAAsB;AAE5B,SAAS,SAAS,UAA4B;AAC5C,MAAI,OAAO,mBAAmB,YAAY;AACxC,mBAAe,QAAQ;AACvB;AAAA,EACF;AACA,UAAQ,QAAQ,EAAE,KAAK,QAAQ;AACjC;AAYO,SAAS,eAAe,SAAqC;AAClE,MAAI,MAAM,KAAK,IAAI,GAAG,SAAS,cAAc,mBAAmB;AAIhE,MAAI,SAA8B,CAAC;AACnC,MAAI,QAAQ;AACZ,MAAI,OAAO;AAEX,MAAI,WAAgC,CAAC;AACrC,MAAI,gBAAgB;AAEpB,QAAM,YAAY,oBAAI,IAAc;AACpC,MAAI,kBAAkB;AAEtB,WAAS,OAAO;AACd,eAAW,YAAY,WAAW;AAChC,eAAS;AAAA,IACX;AAAA,EACF;AAEA,WAAS,SAAS;AAChB,QAAI,iBAAiB;AACnB;AAAA,IACF;AACA,sBAAkB;AAClB,aAAS,MAAM;AACb,wBAAkB;AAClB,WAAK;AAAA,IACP,CAAC;AAAA,EACH;AAGA,WAAS,UAA+B;AACtC,UAAM,SAA8B,IAAI,MAAM,IAAI;AAClD,aAAS,IAAI,GAAG,IAAI,MAAM,KAAK;AAC7B,aAAO,CAAC,IAAI,QAAQ,QAAQ,KAAK,OAAO,MAAM;AAAA,IAChD;AACA,WAAO;AAAA,EACT;AAMA,WAAS,YAAqB;AAC5B,UAAM,UAAU,OAAO;AACvB,UAAM,OAAO,UAAU,QAAQ,EAAE,MAAM,OAAO,GAAG,IAAI,QAAQ;AAC7D,aAAS;AACT,YAAQ;AACR,WAAO,KAAK;AACZ,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,WAAW,CAAC,WAAW;AACrB,UAAI,QAAQ,GAAG;AACb;AAAA,MACF;AACA,UAAI,OAAO,KAAK;AAEd,eAAO,KAAK,MAAM;AAClB;AAAA,MACF,OAAO;AAEL,eAAO,KAAK,IAAI;AAChB,iBAAS,QAAQ,KAAK,OAAO;AAAA,MAC/B;AACA,sBAAgB;AAChB,aAAO;AAAA,IACT;AAAA,IACA,OAAO,MAAM;AACX,eAAS,CAAC;AACV,cAAQ;AACR,aAAO;AACP,sBAAgB;AAChB,aAAO;AAAA,IACT;AAAA,IACA,aAAa,MAAM;AACjB,UAAI,eAAe;AACjB,mBAAW,QAAQ;AACnB,wBAAgB;AAAA,MAClB;AACA,aAAO;AAAA,IACT;AAAA,IACA,cAAc,MAAM,UAAU,OAAO;AAAA,IACrC,YAAY,CAAC,YAAY;AACvB,YAAM,KAAK,IAAI,GAAG,OAAO;AACzB,UAAI,UAAU,GAAG;AACf,wBAAgB;AAChB,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,WAAW,CAAC,aAAa;AACvB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACX,kBAAU,OAAO,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACF;AAGO,IAAM,kBAAkB,eAAe;;;ACvKvC,IAAM,aAAyB,CAAC,SAAS,SAAS,QAAQ,WAAW,SAAS,OAAO;;;ACI5F,SAAS,uBAAiE;AACxE,QAAM,OAAO,oBAAI,QAAgB;AACjC,SAAO,CAAC,MAAM,UAAU;AACtB,QAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,UAAI,KAAK,IAAI,KAAK,GAAG;AACnB,eAAO;AAAA,MACT;AACA,WAAK,IAAI,KAAK;AAAA,IAChB;AACA,WAAO;AAAA,EACT;AACF;AAQO,SAAS,cAAc,OAAgB,QAAyB;AACrE,MAAI;AACF,UAAM,OAAO,KAAK,UAAU,OAAO,MAAM,MAAM;AAC/C,QAAI,SAAS,QAAW;AACtB,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI;AACF,UAAM,OAAO,KAAK,UAAU,OAAO,qBAAqB,GAAG,MAAM;AACjE,QAAI,SAAS,QAAW;AACtB,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,OAAO,KAAK;AACrB;AAEA,SAAS,UAAU,OAAyB;AAC1C,SAAO,KAAK,MAAM,cAAc,KAAK,CAAC;AACxC;AASO,SAAS,oBAAoB,YAA8D;AAChG,MAAI,OAAO,oBAAoB,YAAY;AACzC,QAAI;AACF,aAAO,gBAAgB,UAAU;AAAA,IACnC,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI;AACF,WAAO,UAAU,UAAU;AAAA,EAC7B,QAAQ;AAAA,EAER;AAEA,QAAM,SAAkC,CAAC;AACzC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,UAAU,GAAG;AACrD,QAAI;AACF,aAAO,GAAG,IAAI,UAAU,KAAK;AAAA,IAC/B,QAAQ;AACN,aAAO,GAAG,IAAI,OAAO,KAAK;AAAA,IAC5B;AAAA,EACF;AACA,SAAO;AACT;;;AC7EA,SAAS,cAAc,SAAqC;AAC1D,SAAO,QACJ,IAAI,CAAC,SAAS;AACb,QAAI,OAAO,SAAS,UAAU;AAC5B,aAAO;AAAA,IACT;AACA,QAAI,SAAS,QAAQ,SAAS,QAAW;AACvC,aAAO,OAAO,IAAI;AAAA,IACpB;AACA,WAAO,cAAc,IAAI;AAAA,EAC3B,CAAC,EACA,KAAK,EAAE;AACZ;AAOA,IAAM,YAAY;AAElB,IAAM,YAAY;AAElB,SAAS,oBAAwC;AAE/C,QAAM,QAAQ,IAAI,MAAM,EAAE;AAC1B,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,MAAM,MAAM,IAAI;AAE9B,aAAW,QAAQ,OAAO;AACxB,UAAM,UAAU,KAAK,KAAK;AAG1B,QAAI,CAAC,WAAW,YAAY,SAAS;AACnC;AAAA,IACF;AAGA,QAAI,WAAW,KAAK,OAAO,GAAG;AAC5B;AAAA,IACF;AAGA,UAAM,QAAQ,UAAU,KAAK,OAAO,KAAK,UAAU,KAAK,OAAO;AAC/D,QAAI,CAAC,OAAO;AACV;AAAA,IACF;AAEA,UAAM,CAAC,EAAE,UAAU,QAAQ,KAAK,IAAI;AAGpC,UAAM,WACJ,SACG,QAAQ,WAAW,EAAE,EACrB,MAAM,GAAG,EACT,IAAI,KAAK;AAEd,WAAO,GAAG,QAAQ,IAAI,MAAM,IAAI,KAAK;AAAA,EACvC;AAEA,SAAO;AACT;AAEA,SAAS,gBAAgB,QAAmB,cAAuB,IAA+B;AAChG,QAAM,aAAgC;AAAA,IACpC;AAAA,IACA,WAAW,OAAO;AAAA,IAClB,OAAO,OAAO;AAAA,IACd,UAAU,CAAC,GAAG,OAAO,QAAQ;AAAA,IAC7B,SAAS,CAAC,GAAG,OAAO,OAAO;AAAA,IAC3B,aAAa,cAAc,OAAO,OAAO;AAAA,IACzC,YAAY,oBAAoB,OAAO,UAAU;AAAA,EACnD;AAEA,MAAI,cAAc;AAChB,UAAM,SAAS,kBAAkB;AACjC,QAAI,QAAQ;AACV,iBAAW,SAAS;AAAA,IACtB;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,kBAA2B;AAClC,MAAI;AAEF,QAAI,OAAO,YAAY,eAAe,QAAQ,KAAK,aAAa,cAAc;AAC5E,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AA+CO,SAAS,mBAAmB,SAAqC;AACtE,QAAM,QAAQ,SAAS,SAAS;AAChC,QAAM,aAAa,SAAS,qBAAqB;AACjD,QAAM,aAAa,SAAS,mBAAmB;AAC/C,QAAM,eAAe,eAAe,cAAc,CAAC,gBAAgB;AAInE,QAAM,SAAS,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC;AACpD,MAAI,UAAU;AAEd,SAAO,CAAC,WAAsB;AAC5B,QAAI;AAGF,YAAM,qBAAqB,iBAAiB,cAAc,MAAM,aAAa;AAC7E,YAAM,aAAa;AAAA,QACjB;AAAA,QACA;AAAA,QACA,OAAO,MAAM,IAAI,EAAE,OAAO,IAAI,OAAO,SAAS;AAAA,MAChD;AACA,YAAM,UAAU,UAAU;AAAA,IAC5B,QAAQ;AAAA,IAER;AAAA,EACF;AACF;","names":[]}