@kontsedal/olas-devtools 0.0.1-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1914 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { use } from "@kontsedal/olas-react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { signal } from "@kontsedal/olas-core";
5
+ //#region src/format.ts
6
+ /**
7
+ * Render a payload (props, vars, result, etc.) as a single-line string for
8
+ * the panel. Cuts at `maxLen` so a giant blob doesn't blow up the layout.
9
+ */
10
+ function formatPayload(value, maxLen = 200) {
11
+ if (value === void 0) return "undefined";
12
+ if (value === null) return "null";
13
+ if (typeof value === "function") return "[fn]";
14
+ let s;
15
+ try {
16
+ s = JSON.stringify(value, replaceUnserializable);
17
+ } catch {
18
+ s = String(value);
19
+ }
20
+ if (s === void 0) s = String(value);
21
+ return s.length > maxLen ? `${s.slice(0, maxLen)}…` : s;
22
+ }
23
+ function replaceUnserializable(_key, value) {
24
+ if (typeof value === "function") return "[fn]";
25
+ if (typeof value === "bigint") return value.toString();
26
+ if (value instanceof Error) return {
27
+ name: value.name,
28
+ message: value.message
29
+ };
30
+ return value;
31
+ }
32
+ /** Render an HH:MM:SS.mmm timestamp from epoch ms. */
33
+ function formatTime(t) {
34
+ const d = new Date(t);
35
+ const pad = (n, w = 2) => n.toString().padStart(w, "0");
36
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
37
+ }
38
+ /** Render a controller path / query key as a compact string. */
39
+ function formatPath(path) {
40
+ if (path.length === 0) return "∅";
41
+ return path.map((p) => String(p)).join(" › ");
42
+ }
43
+ //#endregion
44
+ //#region src/JsonView.tsx
45
+ function JsonView({ value, depth = 0 }) {
46
+ return /* @__PURE__ */ jsx(Render, {
47
+ value,
48
+ depth,
49
+ initiallyOpen: depth === 0
50
+ });
51
+ }
52
+ function Render({ value, depth, initiallyOpen }) {
53
+ if (value === null) return /* @__PURE__ */ jsx("span", {
54
+ className: "olas-devtools-json-null",
55
+ children: "null"
56
+ });
57
+ if (value === void 0) return /* @__PURE__ */ jsx("span", {
58
+ className: "olas-devtools-json-null",
59
+ children: "undefined"
60
+ });
61
+ const t = typeof value;
62
+ if (t === "string") return /* @__PURE__ */ jsxs("span", {
63
+ className: "olas-devtools-json-string",
64
+ children: [
65
+ "\"",
66
+ value,
67
+ "\""
68
+ ]
69
+ });
70
+ if (t === "number") return /* @__PURE__ */ jsx("span", {
71
+ className: "olas-devtools-json-number",
72
+ children: String(value)
73
+ });
74
+ if (t === "boolean") return /* @__PURE__ */ jsx("span", {
75
+ className: "olas-devtools-json-boolean",
76
+ children: String(value)
77
+ });
78
+ if (t === "bigint") return /* @__PURE__ */ jsxs("span", {
79
+ className: "olas-devtools-json-number",
80
+ children: [String(value), "n"]
81
+ });
82
+ if (value instanceof Error) return /* @__PURE__ */ jsxs("span", {
83
+ className: "olas-devtools-json-error",
84
+ children: [
85
+ value.name,
86
+ "(",
87
+ JSON.stringify(value.message),
88
+ ")"
89
+ ]
90
+ });
91
+ if (Array.isArray(value)) return /* @__PURE__ */ jsx(CollapsibleArray, {
92
+ value,
93
+ depth,
94
+ initiallyOpen
95
+ });
96
+ if (t === "object") return /* @__PURE__ */ jsx(CollapsibleObject, {
97
+ value,
98
+ depth,
99
+ initiallyOpen
100
+ });
101
+ return /* @__PURE__ */ jsx("span", { children: String(value) });
102
+ }
103
+ function CollapsibleArray({ value, depth, initiallyOpen }) {
104
+ const [open, setOpen] = useState(initiallyOpen && value.length <= 12);
105
+ if (value.length === 0) return /* @__PURE__ */ jsx("span", {
106
+ className: "olas-devtools-json-bracket",
107
+ children: "[]"
108
+ });
109
+ if (!open) return /* @__PURE__ */ jsxs("button", {
110
+ type: "button",
111
+ className: "olas-devtools-json-toggle",
112
+ onClick: () => setOpen(true),
113
+ children: [
114
+ /* @__PURE__ */ jsx("span", {
115
+ className: "olas-devtools-json-bracket",
116
+ children: "["
117
+ }),
118
+ /* @__PURE__ */ jsxs("span", {
119
+ className: "olas-devtools-json-summary",
120
+ children: [
121
+ value.length,
122
+ " item",
123
+ value.length === 1 ? "" : "s"
124
+ ]
125
+ }),
126
+ /* @__PURE__ */ jsx("span", {
127
+ className: "olas-devtools-json-bracket",
128
+ children: "]"
129
+ })
130
+ ]
131
+ });
132
+ return /* @__PURE__ */ jsxs("span", {
133
+ className: "olas-devtools-json-block",
134
+ children: [
135
+ /* @__PURE__ */ jsx("button", {
136
+ type: "button",
137
+ className: "olas-devtools-json-toggle olas-devtools-json-toggle-open",
138
+ onClick: () => setOpen(false),
139
+ children: /* @__PURE__ */ jsx("span", {
140
+ className: "olas-devtools-json-bracket",
141
+ children: "["
142
+ })
143
+ }),
144
+ /* @__PURE__ */ jsx("span", {
145
+ className: "olas-devtools-json-children",
146
+ children: value.map((item, idx) => /* @__PURE__ */ jsxs("span", {
147
+ className: "olas-devtools-json-row",
148
+ children: [/* @__PURE__ */ jsxs("span", {
149
+ className: "olas-devtools-json-index",
150
+ children: [idx, ":"]
151
+ }), /* @__PURE__ */ jsx(Render, {
152
+ value: item,
153
+ depth: depth + 1,
154
+ initiallyOpen: false
155
+ })]
156
+ }, idx))
157
+ }),
158
+ /* @__PURE__ */ jsx("span", {
159
+ className: "olas-devtools-json-bracket",
160
+ children: "]"
161
+ })
162
+ ]
163
+ });
164
+ }
165
+ function CollapsibleObject({ value, depth, initiallyOpen }) {
166
+ const keys = Object.keys(value);
167
+ const [open, setOpen] = useState(initiallyOpen && keys.length <= 8);
168
+ if (keys.length === 0) return /* @__PURE__ */ jsx("span", {
169
+ className: "olas-devtools-json-bracket",
170
+ children: "{}"
171
+ });
172
+ if (!open) return /* @__PURE__ */ jsxs("button", {
173
+ type: "button",
174
+ className: "olas-devtools-json-toggle",
175
+ onClick: () => setOpen(true),
176
+ children: [
177
+ /* @__PURE__ */ jsx("span", {
178
+ className: "olas-devtools-json-bracket",
179
+ children: "{"
180
+ }),
181
+ /* @__PURE__ */ jsxs("span", {
182
+ className: "olas-devtools-json-summary",
183
+ children: [keys.slice(0, 3).join(", "), keys.length > 3 ? ` +${keys.length - 3}` : ""]
184
+ }),
185
+ /* @__PURE__ */ jsx("span", {
186
+ className: "olas-devtools-json-bracket",
187
+ children: "}"
188
+ })
189
+ ]
190
+ });
191
+ return /* @__PURE__ */ jsxs("span", {
192
+ className: "olas-devtools-json-block",
193
+ children: [
194
+ /* @__PURE__ */ jsx("button", {
195
+ type: "button",
196
+ className: "olas-devtools-json-toggle olas-devtools-json-toggle-open",
197
+ onClick: () => setOpen(false),
198
+ children: /* @__PURE__ */ jsx("span", {
199
+ className: "olas-devtools-json-bracket",
200
+ children: "{"
201
+ })
202
+ }),
203
+ /* @__PURE__ */ jsx("span", {
204
+ className: "olas-devtools-json-children",
205
+ children: keys.map((k) => /* @__PURE__ */ jsxs("span", {
206
+ className: "olas-devtools-json-row",
207
+ children: [/* @__PURE__ */ jsxs("span", {
208
+ className: "olas-devtools-json-key",
209
+ children: [k, ":"]
210
+ }), /* @__PURE__ */ jsx(Render, {
211
+ value: value[k],
212
+ depth: depth + 1,
213
+ initiallyOpen: false
214
+ })]
215
+ }, k))
216
+ }),
217
+ /* @__PURE__ */ jsx("span", {
218
+ className: "olas-devtools-json-bracket",
219
+ children: "}"
220
+ })
221
+ ]
222
+ });
223
+ }
224
+ /**
225
+ * Subscribes to a root's `__debug` bus and maintains live state for the
226
+ * devtools panel. Exposes signals so the React layer can consume via
227
+ * `@kontsedal/olas-react`'s `use()`.
228
+ *
229
+ * Pure logic — no DOM, no React. Construct one per root.
230
+ */
231
+ var DevtoolsStore = class {
232
+ tree$ = signal(makeRoot());
233
+ cache$ = signal([]);
234
+ mutations$ = signal([]);
235
+ fields$ = signal([]);
236
+ maxEntries;
237
+ now;
238
+ nextId = 1;
239
+ /** Keyed by `path|name` so a mutation:run can be paired with its
240
+ * success/error to compute duration. Cleared after pairing. */
241
+ mutationStarts = /* @__PURE__ */ new Map();
242
+ constructor(options) {
243
+ this.maxEntries = options?.maxEntries ?? 100;
244
+ this.now = options?.now ?? (() => Date.now());
245
+ }
246
+ /**
247
+ * Subscribe to the given root's debug bus. Returns the unsubscribe. The
248
+ * caller (typically the React component) is responsible for invoking it
249
+ * on unmount.
250
+ */
251
+ attach(root) {
252
+ return root.__debug.subscribe((event) => this.handle(event));
253
+ }
254
+ /** Apply one event. Exposed for tests. */
255
+ handle(event) {
256
+ switch (event.type) {
257
+ case "controller:constructed":
258
+ this.tree$.set(insertNode(this.tree$.peek(), event.path, event.props));
259
+ return;
260
+ case "controller:suspended":
261
+ this.tree$.set(setNodeState(this.tree$.peek(), event.path, "suspended"));
262
+ return;
263
+ case "controller:resumed":
264
+ this.tree$.set(setNodeState(this.tree$.peek(), event.path, "active"));
265
+ return;
266
+ case "controller:disposed":
267
+ this.tree$.set(setNodeState(this.tree$.peek(), event.path, "disposed"));
268
+ return;
269
+ case "cache:subscribed":
270
+ this.pushCache({
271
+ kind: "subscribed",
272
+ queryKey: event.queryKey,
273
+ subscriberPath: event.subscriberPath
274
+ });
275
+ return;
276
+ case "cache:fetch-start":
277
+ this.pushCache({
278
+ kind: "fetch-start",
279
+ queryKey: event.queryKey
280
+ });
281
+ return;
282
+ case "cache:fetch-success":
283
+ this.pushCache({
284
+ kind: "fetch-success",
285
+ queryKey: event.queryKey,
286
+ durationMs: event.durationMs
287
+ });
288
+ return;
289
+ case "cache:fetch-error":
290
+ this.pushCache({
291
+ kind: "fetch-error",
292
+ queryKey: event.queryKey,
293
+ durationMs: event.durationMs,
294
+ error: event.error
295
+ });
296
+ return;
297
+ case "cache:invalidated":
298
+ this.pushCache({
299
+ kind: "invalidated",
300
+ queryKey: event.queryKey
301
+ });
302
+ return;
303
+ case "cache:gc":
304
+ this.pushCache({
305
+ kind: "gc",
306
+ queryKey: event.queryKey
307
+ });
308
+ return;
309
+ case "mutation:run":
310
+ this.mutationStarts.set(mutationKey(event.path, event.name), this.now());
311
+ this.pushMutation({
312
+ kind: "run",
313
+ path: event.path,
314
+ name: event.name,
315
+ vars: event.vars
316
+ });
317
+ return;
318
+ case "mutation:success": {
319
+ const durationMs = this.consumeStart(event.path, event.name);
320
+ this.pushMutation({
321
+ kind: "success",
322
+ path: event.path,
323
+ name: event.name,
324
+ result: event.result,
325
+ ...durationMs !== void 0 ? { durationMs } : {}
326
+ });
327
+ return;
328
+ }
329
+ case "mutation:error": {
330
+ const durationMs = this.consumeStart(event.path, event.name);
331
+ this.pushMutation({
332
+ kind: "error",
333
+ path: event.path,
334
+ name: event.name,
335
+ error: event.error,
336
+ ...durationMs !== void 0 ? { durationMs } : {}
337
+ });
338
+ return;
339
+ }
340
+ case "mutation:rollback":
341
+ this.pushMutation({
342
+ kind: "rollback",
343
+ path: event.path,
344
+ name: event.name
345
+ });
346
+ return;
347
+ case "field:validated":
348
+ this.pushField({
349
+ path: event.path,
350
+ field: event.field,
351
+ valid: event.valid,
352
+ errors: event.errors
353
+ });
354
+ return;
355
+ }
356
+ }
357
+ /** Clear every log. Tree state is preserved — the live tree is not a log. */
358
+ clearLogs() {
359
+ this.cache$.set([]);
360
+ this.mutations$.set([]);
361
+ this.fields$.set([]);
362
+ }
363
+ pushCache(entry) {
364
+ const full = {
365
+ id: this.nextId++,
366
+ t: this.now(),
367
+ ...entry
368
+ };
369
+ this.cache$.set(appendBounded(this.cache$.peek(), full, this.maxEntries));
370
+ }
371
+ pushMutation(entry) {
372
+ const full = {
373
+ id: this.nextId++,
374
+ t: this.now(),
375
+ ...entry
376
+ };
377
+ this.mutations$.set(appendBounded(this.mutations$.peek(), full, this.maxEntries));
378
+ }
379
+ pushField(entry) {
380
+ const full = {
381
+ id: this.nextId++,
382
+ t: this.now(),
383
+ ...entry
384
+ };
385
+ this.fields$.set(appendBounded(this.fields$.peek(), full, this.maxEntries));
386
+ }
387
+ consumeStart(path, name) {
388
+ const key = mutationKey(path, name);
389
+ const startedAt = this.mutationStarts.get(key);
390
+ if (startedAt === void 0) return void 0;
391
+ this.mutationStarts.delete(key);
392
+ return this.now() - startedAt;
393
+ }
394
+ };
395
+ function mutationKey(path, name) {
396
+ return `${path.join(">")}#${name ?? ""}`;
397
+ }
398
+ function makeRoot() {
399
+ return {
400
+ path: [],
401
+ state: "active",
402
+ props: void 0,
403
+ children: []
404
+ };
405
+ }
406
+ function appendBounded(arr, item, max) {
407
+ const next = arr.length >= max ? arr.slice(arr.length - max + 1) : arr.slice();
408
+ next.push(item);
409
+ return next;
410
+ }
411
+ /**
412
+ * Insert (or update) a node at `path` inside the tree. Auto-creates any
413
+ * missing intermediate ancestors as 'active' placeholders — needed if the
414
+ * subscriber attached after the root was constructed.
415
+ *
416
+ * Returns a NEW tree object (immutable update).
417
+ */
418
+ function insertNode(root, path, props) {
419
+ if (path.length === 0) return {
420
+ ...root,
421
+ state: "active",
422
+ props
423
+ };
424
+ return cloneWithUpsert(root, path, 0, props);
425
+ }
426
+ function cloneWithUpsert(node, path, depth, props) {
427
+ if (depth === path.length) return {
428
+ ...node,
429
+ state: "active",
430
+ props
431
+ };
432
+ const segment = path[depth];
433
+ const idx = node.children.findIndex((c) => c.path[c.path.length - 1] === segment);
434
+ const childPath = path.slice(0, depth + 1);
435
+ if (idx === -1) {
436
+ const newChild = cloneWithUpsert({
437
+ path: childPath,
438
+ state: "active",
439
+ props: void 0,
440
+ children: []
441
+ }, path, depth + 1, props);
442
+ return {
443
+ ...node,
444
+ children: [...node.children, newChild]
445
+ };
446
+ }
447
+ const existing = node.children[idx];
448
+ const updatedChild = cloneWithUpsert(existing, path, depth + 1, props);
449
+ const nextChildren = node.children.slice();
450
+ nextChildren[idx] = updatedChild;
451
+ return {
452
+ ...node,
453
+ children: nextChildren
454
+ };
455
+ }
456
+ /**
457
+ * Set `state` on the node at `path`. If the node doesn't exist (out-of-order
458
+ * event delivery), the tree is returned unchanged.
459
+ */
460
+ function setNodeState(root, path, state) {
461
+ if (path.length === 0) return {
462
+ ...root,
463
+ state
464
+ };
465
+ return setStateAt(root, path, 0, state) ?? root;
466
+ }
467
+ function setStateAt(node, path, depth, state) {
468
+ if (depth === path.length) return {
469
+ ...node,
470
+ state
471
+ };
472
+ const segment = path[depth];
473
+ const idx = node.children.findIndex((c) => c.path[c.path.length - 1] === segment);
474
+ if (idx === -1) return null;
475
+ const existing = node.children[idx];
476
+ const updatedChild = setStateAt(existing, path, depth + 1, state);
477
+ if (updatedChild === null) return null;
478
+ const nextChildren = node.children.slice();
479
+ nextChildren[idx] = updatedChild;
480
+ return {
481
+ ...node,
482
+ children: nextChildren
483
+ };
484
+ }
485
+ //#endregion
486
+ //#region src/styles.ts
487
+ /**
488
+ * Inline CSS for the devtools panel. Scoped to the `.olas-devtools-*` class
489
+ * prefix so it doesn't bleed into the host app. Honors `prefers-color-scheme`
490
+ * and accepts host palette overrides via `--olas-*` custom properties.
491
+ */
492
+ const DEVTOOLS_CSS = `
493
+ .olas-devtools {
494
+ container-type: inline-size;
495
+ --olas-bg: #ffffff;
496
+ --olas-fg: #1f2330;
497
+ --olas-muted: #6b7280;
498
+ --olas-soft: #f5f6f9;
499
+ --olas-soft-2: #eef0f4;
500
+ --olas-accent: #4f46e5;
501
+ --olas-accent-soft: rgba(79,70,229,0.10);
502
+ --olas-success: #1e8a3d;
503
+ --olas-success-soft: rgba(30,138,61,0.12);
504
+ --olas-warn: #ad6800;
505
+ --olas-warn-soft: rgba(173,104,0,0.12);
506
+ --olas-error: #b8361f;
507
+ --olas-error-soft: rgba(184,54,31,0.12);
508
+ --olas-border: #e6e6ea;
509
+ --olas-border-soft: #eeeef2;
510
+ --olas-row-alt: #fafafc;
511
+
512
+ /* JSON viewer colors */
513
+ --olas-json-key: #7a4ab8;
514
+ --olas-json-string: #1e8a3d;
515
+ --olas-json-number: #b25400;
516
+ --olas-json-boolean: #4f46e5;
517
+ --olas-json-null: #9aa0aa;
518
+ --olas-json-bracket: #6b7280;
519
+ --olas-json-summary: #6b7280;
520
+
521
+ font-family: -apple-system, BlinkMacSystemFont, "Inter var", "Inter", "Segoe UI", system-ui, sans-serif;
522
+ font-size: 12.5px;
523
+ line-height: 1.55;
524
+ color: var(--olas-fg);
525
+ background: var(--olas-bg);
526
+ border: 1px solid var(--olas-border);
527
+ border-radius: 10px;
528
+ display: flex;
529
+ flex-direction: column;
530
+ height: 100%;
531
+ min-height: 320px;
532
+ overflow: hidden;
533
+ box-sizing: border-box;
534
+ }
535
+ .olas-devtools *,
536
+ .olas-devtools *::before,
537
+ .olas-devtools *::after { box-sizing: border-box; }
538
+
539
+ @media (prefers-color-scheme: dark) {
540
+ .olas-devtools {
541
+ --olas-bg: #15171e;
542
+ --olas-fg: #e8ebf2;
543
+ --olas-muted: #97a0b3;
544
+ --olas-soft: #1d2029;
545
+ --olas-soft-2: #232733;
546
+ --olas-accent: #8b86f0;
547
+ --olas-accent-soft: rgba(139,134,240,0.18);
548
+ --olas-success: #4ade80;
549
+ --olas-success-soft: rgba(74,222,128,0.16);
550
+ --olas-warn: #f5b740;
551
+ --olas-warn-soft: rgba(245,183,64,0.16);
552
+ --olas-error: #ef6b53;
553
+ --olas-error-soft: rgba(239,107,83,0.16);
554
+ --olas-border: #2a2e3a;
555
+ --olas-border-soft: #20232c;
556
+ --olas-row-alt: #1a1d25;
557
+
558
+ --olas-json-key: #c39bff;
559
+ --olas-json-string: #7ee79d;
560
+ --olas-json-number: #f5b740;
561
+ --olas-json-boolean: #8b86f0;
562
+ --olas-json-null: #7c8090;
563
+ --olas-json-bracket: #97a0b3;
564
+ --olas-json-summary: #97a0b3;
565
+ }
566
+ }
567
+
568
+ /* ---- tabs ------------------------------------------------------------- */
569
+ .olas-devtools-tabs {
570
+ display: flex;
571
+ align-items: center;
572
+ gap: 1px;
573
+ border-bottom: 1px solid var(--olas-border);
574
+ background: var(--olas-soft);
575
+ padding: 0 8px;
576
+ flex-shrink: 0;
577
+ overflow-x: auto;
578
+ scrollbar-width: none;
579
+ }
580
+ .olas-devtools-tabs::-webkit-scrollbar { display: none; }
581
+ .olas-devtools-tab {
582
+ display: inline-flex;
583
+ align-items: center;
584
+ gap: 6px;
585
+ padding: 9px 10px 8px;
586
+ background: transparent;
587
+ color: var(--olas-muted);
588
+ border: 0;
589
+ border-bottom: 2px solid transparent;
590
+ margin-bottom: -1px;
591
+ cursor: pointer;
592
+ font: inherit;
593
+ font-weight: 500;
594
+ font-size: 12px;
595
+ white-space: nowrap;
596
+ flex-shrink: 0;
597
+ transition: color 80ms, border-color 80ms;
598
+ }
599
+ .olas-devtools-tab-label-full { display: inline; }
600
+ .olas-devtools-tab-label-short { display: none; }
601
+ @container (max-width: 480px) {
602
+ .olas-devtools-tab { padding: 9px 8px 8px; font-size: 11.5px; gap: 4px; }
603
+ .olas-devtools-tab-label-full { display: none; }
604
+ .olas-devtools-tab-label-short { display: inline; }
605
+ .olas-devtools-pause-text,
606
+ .olas-devtools-clear-text { display: none; }
607
+ }
608
+ .olas-devtools-tab:hover { color: var(--olas-fg); }
609
+ .olas-devtools-tab[aria-selected="true"] {
610
+ color: var(--olas-fg);
611
+ border-bottom-color: var(--olas-accent);
612
+ }
613
+ .olas-devtools-tab[aria-selected="true"] .olas-devtools-tab-count {
614
+ background: var(--olas-accent-soft);
615
+ color: var(--olas-accent);
616
+ }
617
+ .olas-devtools-tab-count {
618
+ min-width: 18px;
619
+ padding: 0 6px;
620
+ height: 16px;
621
+ border-radius: 999px;
622
+ background: color-mix(in oklch, var(--olas-fg) 8%, transparent);
623
+ color: var(--olas-muted);
624
+ font-size: 10.5px;
625
+ font-weight: 600;
626
+ display: inline-flex;
627
+ align-items: center;
628
+ justify-content: center;
629
+ font-variant-numeric: tabular-nums;
630
+ line-height: 1;
631
+ }
632
+ .olas-devtools-pause,
633
+ .olas-devtools-clear {
634
+ padding: 4px 10px;
635
+ background: transparent;
636
+ color: var(--olas-muted);
637
+ border: 0;
638
+ border-radius: 6px;
639
+ cursor: pointer;
640
+ font: inherit;
641
+ font-size: 11.5px;
642
+ align-self: center;
643
+ }
644
+ .olas-devtools-pause { margin-left: auto; }
645
+ .olas-devtools-pause:hover,
646
+ .olas-devtools-clear:hover {
647
+ color: var(--olas-fg);
648
+ background: color-mix(in oklch, var(--olas-bg) 60%, transparent);
649
+ }
650
+ .olas-devtools-pause-on { color: var(--olas-warn); background: var(--olas-warn-soft); }
651
+ .olas-devtools-pause-on:hover { color: var(--olas-warn); }
652
+ .olas-devtools-clear-icon { display: none; }
653
+ @container (max-width: 480px) {
654
+ .olas-devtools-clear-text { display: none; }
655
+ .olas-devtools-clear-icon { display: inline; }
656
+ .olas-devtools-pause, .olas-devtools-clear { padding: 4px 8px; }
657
+ }
658
+
659
+ /* ---- filter ---------------------------------------------------------- */
660
+ .olas-devtools-filter {
661
+ position: sticky;
662
+ top: 0;
663
+ z-index: 1;
664
+ display: flex;
665
+ align-items: center;
666
+ gap: 6px;
667
+ padding: 8px 10px;
668
+ border-bottom: 1px solid var(--olas-border-soft);
669
+ background: var(--olas-bg);
670
+ }
671
+ .olas-devtools-filter input {
672
+ flex: 1;
673
+ padding: 5px 9px;
674
+ border: 1px solid var(--olas-border);
675
+ border-radius: 6px;
676
+ background: var(--olas-soft);
677
+ color: var(--olas-fg);
678
+ font: inherit;
679
+ font-size: 12px;
680
+ outline: none;
681
+ transition: border-color 80ms, box-shadow 80ms;
682
+ }
683
+ .olas-devtools-filter input:focus {
684
+ border-color: var(--olas-accent);
685
+ box-shadow: 0 0 0 2px var(--olas-accent-soft);
686
+ }
687
+ .olas-devtools-filter input::placeholder { color: var(--olas-muted); }
688
+ .olas-devtools-filter button {
689
+ background: transparent;
690
+ border: 0;
691
+ color: var(--olas-muted);
692
+ cursor: pointer;
693
+ font: inherit;
694
+ font-size: 13px;
695
+ padding: 2px 6px;
696
+ }
697
+ .olas-devtools-filter button:hover { color: var(--olas-fg); }
698
+
699
+ /* ---- body ------------------------------------------------------------ */
700
+ .olas-devtools-body {
701
+ flex: 1;
702
+ overflow: auto;
703
+ }
704
+
705
+ /* ---- list rows ------------------------------------------------------- */
706
+ .olas-devtools-list {
707
+ margin: 0;
708
+ padding: 0;
709
+ list-style: none;
710
+ }
711
+ .olas-devtools-list li {
712
+ border-bottom: 1px solid var(--olas-border-soft);
713
+ display: flex;
714
+ flex-direction: column;
715
+ }
716
+ .olas-devtools-list li:last-child { border-bottom: none; }
717
+ .olas-devtools-list li:hover { background: var(--olas-row-alt); }
718
+
719
+ .olas-devtools-row-top {
720
+ display: flex;
721
+ align-items: center;
722
+ gap: 10px;
723
+ padding: 8px 12px;
724
+ min-height: 32px;
725
+ }
726
+ .olas-devtools-row-clickable .olas-devtools-row-top { cursor: pointer; user-select: none; }
727
+
728
+ .olas-devtools-target {
729
+ flex: 1;
730
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
731
+ font-size: 11.5px;
732
+ color: var(--olas-fg);
733
+ word-break: break-word;
734
+ min-width: 0;
735
+ }
736
+ .olas-devtools-target strong { font-weight: 600; color: var(--olas-accent); }
737
+ .olas-devtools-time {
738
+ color: var(--olas-muted);
739
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
740
+ font-size: 10.5px;
741
+ font-variant-numeric: tabular-nums;
742
+ white-space: nowrap;
743
+ }
744
+ .olas-devtools-chevron {
745
+ display: inline-flex;
746
+ align-items: center;
747
+ justify-content: center;
748
+ color: var(--olas-muted);
749
+ font-size: 12px;
750
+ width: 16px;
751
+ height: 16px;
752
+ transition: transform 100ms;
753
+ user-select: none;
754
+ }
755
+ .olas-devtools-chevron-open { transform: rotate(90deg); color: var(--olas-fg); }
756
+
757
+ .olas-devtools-kind {
758
+ color: var(--olas-accent);
759
+ background: var(--olas-accent-soft);
760
+ border-radius: 4px;
761
+ padding: 1px 7px;
762
+ font-size: 10.5px;
763
+ font-weight: 600;
764
+ letter-spacing: 0.02em;
765
+ white-space: nowrap;
766
+ line-height: 1.4;
767
+ font-variant-numeric: tabular-nums;
768
+ }
769
+ .olas-devtools-kind-success { color: var(--olas-success); background: var(--olas-success-soft); }
770
+ .olas-devtools-kind-error { color: var(--olas-error); background: var(--olas-error-soft); }
771
+ .olas-devtools-kind-warn,
772
+ .olas-devtools-kind-rollback { color: var(--olas-warn); background: var(--olas-warn-soft); }
773
+ .olas-devtools-duration {
774
+ color: var(--olas-muted);
775
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
776
+ font-size: 10.5px;
777
+ font-variant-numeric: tabular-nums;
778
+ background: var(--olas-soft);
779
+ border-radius: 4px;
780
+ padding: 1px 6px;
781
+ white-space: nowrap;
782
+ }
783
+
784
+ .olas-devtools-payload {
785
+ margin: 0 12px 10px;
786
+ padding: 8px 10px;
787
+ background: var(--olas-soft);
788
+ border: 1px solid var(--olas-border-soft);
789
+ border-radius: 6px;
790
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
791
+ font-size: 11.5px;
792
+ color: var(--olas-fg);
793
+ overflow-x: auto;
794
+ }
795
+ .olas-devtools-payload-inline {
796
+ margin-top: -4px;
797
+ padding: 4px 10px;
798
+ background: transparent;
799
+ border: 0;
800
+ color: var(--olas-muted);
801
+ font-size: 11px;
802
+ }
803
+ .olas-devtools-payload-json { line-height: 1.5; }
804
+
805
+ /* ---- JSON viewer ----------------------------------------------------- */
806
+ .olas-devtools-json-row {
807
+ display: block;
808
+ padding-left: 14px;
809
+ white-space: pre-wrap;
810
+ word-break: break-word;
811
+ }
812
+ .olas-devtools-json-children {
813
+ display: block;
814
+ border-left: 1px dashed var(--olas-border);
815
+ margin-left: 4px;
816
+ }
817
+ .olas-devtools-json-block {
818
+ display: inline-flex;
819
+ flex-direction: column;
820
+ vertical-align: top;
821
+ }
822
+ .olas-devtools-json-key {
823
+ color: var(--olas-json-key);
824
+ margin-right: 6px;
825
+ font-weight: 500;
826
+ }
827
+ .olas-devtools-json-index {
828
+ color: var(--olas-json-bracket);
829
+ margin-right: 6px;
830
+ }
831
+ .olas-devtools-json-string { color: var(--olas-json-string); }
832
+ .olas-devtools-json-number { color: var(--olas-json-number); }
833
+ .olas-devtools-json-boolean { color: var(--olas-json-boolean); }
834
+ .olas-devtools-json-null { color: var(--olas-json-null); font-style: italic; }
835
+ .olas-devtools-json-bracket { color: var(--olas-json-bracket); }
836
+ .olas-devtools-json-error { color: var(--olas-error); }
837
+ .olas-devtools-json-summary {
838
+ color: var(--olas-json-summary);
839
+ font-style: italic;
840
+ margin: 0 4px;
841
+ }
842
+ .olas-devtools-json-toggle {
843
+ display: inline-flex;
844
+ align-items: center;
845
+ gap: 0;
846
+ background: transparent;
847
+ border: 0;
848
+ padding: 0;
849
+ margin: 0;
850
+ cursor: pointer;
851
+ color: inherit;
852
+ font: inherit;
853
+ }
854
+ .olas-devtools-json-toggle:hover { background: var(--olas-accent-soft); border-radius: 3px; }
855
+
856
+ /* ---- empty state ---------------------------------------------------- */
857
+ .olas-devtools-empty {
858
+ padding: 36px 24px;
859
+ text-align: center;
860
+ color: var(--olas-muted);
861
+ }
862
+ .olas-devtools-empty-title {
863
+ color: var(--olas-fg);
864
+ font-weight: 600;
865
+ font-size: 13px;
866
+ margin-bottom: 4px;
867
+ }
868
+ .olas-devtools-empty-hint {
869
+ font-size: 12px;
870
+ max-width: 320px;
871
+ margin: 0 auto;
872
+ line-height: 1.55;
873
+ }
874
+
875
+ /* ---- tree ------------------------------------------------------------ */
876
+ .olas-devtools-tree { padding: 10px 12px; }
877
+ .olas-devtools-tree-node {
878
+ padding: 1px 0;
879
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
880
+ font-size: 12px;
881
+ }
882
+ .olas-devtools-tree-row {
883
+ display: inline-flex;
884
+ align-items: center;
885
+ gap: 8px;
886
+ padding: 1px 0;
887
+ }
888
+ .olas-devtools-tree-name { color: var(--olas-fg); font-weight: 500; }
889
+ .olas-devtools-tree-state-active,
890
+ .olas-devtools-tree-state-suspended,
891
+ .olas-devtools-tree-state-disposed {
892
+ border-radius: 4px;
893
+ padding: 0 6px;
894
+ font-size: 9.5px;
895
+ font-weight: 700;
896
+ text-transform: uppercase;
897
+ letter-spacing: 0.05em;
898
+ }
899
+ .olas-devtools-tree-state-active {
900
+ color: var(--olas-success);
901
+ background: var(--olas-success-soft);
902
+ }
903
+ .olas-devtools-tree-state-suspended {
904
+ color: var(--olas-warn);
905
+ background: var(--olas-warn-soft);
906
+ }
907
+ .olas-devtools-tree-state-disposed {
908
+ color: var(--olas-muted);
909
+ background: color-mix(in oklch, var(--olas-fg) 8%, transparent);
910
+ }
911
+ .olas-devtools-tree-pending {
912
+ color: var(--olas-warn);
913
+ background: var(--olas-warn-soft);
914
+ border-radius: 4px;
915
+ padding: 0 6px;
916
+ font-size: 10px;
917
+ font-weight: 600;
918
+ letter-spacing: 0.02em;
919
+ }
920
+ .olas-devtools-tree-props-toggle {
921
+ background: transparent;
922
+ border: 0;
923
+ cursor: pointer;
924
+ font: inherit;
925
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
926
+ font-size: 11px;
927
+ color: var(--olas-muted);
928
+ padding: 0 4px;
929
+ border-radius: 4px;
930
+ max-width: 280px;
931
+ overflow: hidden;
932
+ text-overflow: ellipsis;
933
+ white-space: nowrap;
934
+ text-align: left;
935
+ }
936
+ .olas-devtools-tree-props-toggle:hover {
937
+ color: var(--olas-fg);
938
+ background: var(--olas-soft);
939
+ }
940
+ .olas-devtools-tree-props {
941
+ margin: 4px 0 6px 8px;
942
+ padding: 6px 10px;
943
+ background: var(--olas-soft);
944
+ border: 1px solid var(--olas-border-soft);
945
+ border-radius: 6px;
946
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
947
+ font-size: 11px;
948
+ overflow-x: auto;
949
+ }
950
+ .olas-devtools-tree-children {
951
+ margin-left: 8px;
952
+ border-left: 1px dashed var(--olas-border);
953
+ padding-left: 10px;
954
+ margin-top: 2px;
955
+ }
956
+
957
+ /* ---- floating window + launcher ------------------------------------- */
958
+ .olas-devtools-launcher {
959
+ position: fixed;
960
+ right: 16px;
961
+ bottom: 16px;
962
+ z-index: 2147483645;
963
+ display: inline-flex;
964
+ align-items: center;
965
+ gap: 8px;
966
+ padding: 7px 12px 7px 11px;
967
+ background: #1f2330;
968
+ color: #e8ebf2;
969
+ border: 1px solid #2a2e3a;
970
+ border-radius: 999px;
971
+ box-shadow: 0 2px 6px rgba(0,0,0,0.15), 0 8px 24px rgba(0,0,0,0.18);
972
+ cursor: pointer;
973
+ font: inherit;
974
+ font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", system-ui, sans-serif;
975
+ font-size: 12px;
976
+ font-weight: 500;
977
+ }
978
+ .olas-devtools-launcher:hover { filter: brightness(1.1); }
979
+ .olas-devtools-launcher-active { box-shadow: 0 0 0 2px rgba(139,134,240,0.5), 0 8px 24px rgba(0,0,0,0.18); }
980
+ .olas-devtools-launcher-dot {
981
+ width: 8px;
982
+ height: 8px;
983
+ border-radius: 50%;
984
+ background: #4ade80;
985
+ box-shadow: 0 0 6px rgba(74,222,128,0.7);
986
+ }
987
+ .olas-devtools-launcher-label { letter-spacing: 0.01em; }
988
+
989
+ .olas-devtools-floating {
990
+ position: fixed;
991
+ z-index: 2147483646;
992
+ display: flex;
993
+ flex-direction: column;
994
+ background: var(--olas-bg, #ffffff);
995
+ color: var(--olas-fg, #1f2330);
996
+ border: 1px solid var(--olas-border, #e6e6ea);
997
+ border-radius: 10px;
998
+ box-shadow: 0 8px 24px rgba(0,0,0,0.18), 0 24px 64px rgba(0,0,0,0.18);
999
+ overflow: hidden;
1000
+ /* Inherit the panel's own CSS vars when DevtoolsPanel is mounted inside. */
1001
+ }
1002
+ @media (prefers-color-scheme: dark) {
1003
+ .olas-devtools-floating {
1004
+ background: #15171e;
1005
+ color: #e8ebf2;
1006
+ border-color: #2a2e3a;
1007
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5), 0 24px 64px rgba(0,0,0,0.5);
1008
+ }
1009
+ }
1010
+ .olas-devtools-floating-header {
1011
+ display: flex;
1012
+ align-items: center;
1013
+ gap: 8px;
1014
+ height: 30px;
1015
+ padding: 0 8px 0 10px;
1016
+ background: var(--olas-soft, #f5f6f9);
1017
+ border-bottom: 1px solid var(--olas-border, #e6e6ea);
1018
+ cursor: grab;
1019
+ user-select: none;
1020
+ flex-shrink: 0;
1021
+ }
1022
+ .olas-devtools-floating-header:active { cursor: grabbing; }
1023
+ .olas-devtools-floating-grip {
1024
+ color: var(--olas-muted, #6b7280);
1025
+ font-size: 14px;
1026
+ line-height: 1;
1027
+ }
1028
+ .olas-devtools-floating-title {
1029
+ flex: 1;
1030
+ font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", system-ui, sans-serif;
1031
+ font-size: 11.5px;
1032
+ font-weight: 600;
1033
+ color: var(--olas-fg, #1f2330);
1034
+ letter-spacing: 0.01em;
1035
+ }
1036
+ .olas-devtools-floating-actions { display: inline-flex; gap: 2px; }
1037
+ .olas-devtools-floating-action {
1038
+ width: 22px;
1039
+ height: 22px;
1040
+ display: inline-flex;
1041
+ align-items: center;
1042
+ justify-content: center;
1043
+ background: transparent;
1044
+ border: 0;
1045
+ border-radius: 4px;
1046
+ color: var(--olas-muted, #6b7280);
1047
+ cursor: pointer;
1048
+ font: inherit;
1049
+ font-size: 14px;
1050
+ line-height: 1;
1051
+ }
1052
+ .olas-devtools-floating-action:hover {
1053
+ color: var(--olas-fg, #1f2330);
1054
+ background: color-mix(in oklch, currentColor 14%, transparent);
1055
+ }
1056
+ .olas-devtools-floating-body {
1057
+ flex: 1;
1058
+ min-height: 0;
1059
+ display: flex;
1060
+ }
1061
+ .olas-devtools-floating-body > .olas-devtools {
1062
+ flex: 1;
1063
+ border: 0;
1064
+ border-radius: 0;
1065
+ min-height: 0;
1066
+ }
1067
+ .olas-devtools-floating-resize {
1068
+ position: absolute;
1069
+ right: 0;
1070
+ bottom: 0;
1071
+ width: 14px;
1072
+ height: 14px;
1073
+ cursor: nwse-resize;
1074
+ background:
1075
+ linear-gradient(135deg, transparent 0 7px, var(--olas-muted, #6b7280) 7px 8px, transparent 8px 10px,
1076
+ var(--olas-muted, #6b7280) 10px 11px, transparent 11px 100%);
1077
+ opacity: 0.6;
1078
+ }
1079
+ .olas-devtools-floating-resize:hover { opacity: 1; }
1080
+ `;
1081
+ //#endregion
1082
+ //#region src/DevtoolsPanel.tsx
1083
+ /**
1084
+ * Drop-in devtools panel for an Olas root.
1085
+ *
1086
+ * Features:
1087
+ * - **Tree** populated from the snapshot replay on mount (no lost events).
1088
+ * - **Cache / Mutations / Fields** event logs in reverse chronological order.
1089
+ * - **Filter** field per tab — text-matches kind, path, name, payload.
1090
+ * - **Pause** toggle freezes the log without stopping ingestion.
1091
+ * - **Click a row** to expand its payload from a truncated preview to the full
1092
+ * JSON.
1093
+ * - **Mutation durations** — `run → success/error` pairing surfaces elapsed ms.
1094
+ *
1095
+ * Styled inline (no CSS import needed) and scoped to the `.olas-devtools-*`
1096
+ * class prefix. Hosts override the palette via `--olas-*` custom properties.
1097
+ * Spec §13.
1098
+ */
1099
+ function DevtoolsPanel(props) {
1100
+ const { root, defaultTab = "tree", maxEntries, urlHashKey, inspectorPollMs = 800 } = props;
1101
+ const store = useMemo(() => new DevtoolsStore(maxEntries !== void 0 ? { maxEntries } : void 0), [maxEntries]);
1102
+ useEffect(() => store.attach(root), [root, store]);
1103
+ const initial = useMemo(() => readUrlHash(urlHashKey, defaultTab), [urlHashKey, defaultTab]);
1104
+ const [tab, setTab] = useState(initial.tab);
1105
+ const [paused, setPaused] = useState(false);
1106
+ const [filters, setFilters] = useState(initial.filters);
1107
+ const filter = filters[tab];
1108
+ const setFilter = (q) => setFilters((prev) => ({
1109
+ ...prev,
1110
+ [tab]: q
1111
+ }));
1112
+ useEffect(() => {
1113
+ if (urlHashKey === void 0) return;
1114
+ writeUrlHash(urlHashKey, {
1115
+ tab,
1116
+ filters
1117
+ });
1118
+ }, [
1119
+ urlHashKey,
1120
+ tab,
1121
+ filters
1122
+ ]);
1123
+ const [cacheEntries, setCacheEntries] = useState([]);
1124
+ const rootRef = useRef(root);
1125
+ rootRef.current = root;
1126
+ useEffect(() => {
1127
+ if (tab !== "inspector") return;
1128
+ const tick = () => setCacheEntries(rootRef.current.__debug.queryEntries());
1129
+ tick();
1130
+ const id = window.setInterval(tick, inspectorPollMs);
1131
+ return () => window.clearInterval(id);
1132
+ }, [tab, inspectorPollMs]);
1133
+ const liveTree = use(store.tree$);
1134
+ const liveCache = use(store.cache$);
1135
+ const liveMutations = use(store.mutations$);
1136
+ const liveFields = use(store.fields$);
1137
+ const [frozen, setFrozen] = useState(null);
1138
+ useEffect(() => {
1139
+ if (paused) setFrozen({
1140
+ tree: liveTree,
1141
+ cache: liveCache,
1142
+ mutations: liveMutations,
1143
+ fields: liveFields
1144
+ });
1145
+ else setFrozen(null);
1146
+ }, [paused]);
1147
+ const tree = frozen?.tree ?? liveTree;
1148
+ const cache = frozen?.cache ?? liveCache;
1149
+ const mutations = frozen?.mutations ?? liveMutations;
1150
+ const fields = frozen?.fields ?? liveFields;
1151
+ return /* @__PURE__ */ jsxs("div", {
1152
+ className: "olas-devtools",
1153
+ "data-testid": "olas-devtools",
1154
+ children: [
1155
+ /* @__PURE__ */ jsx("style", { children: DEVTOOLS_CSS }),
1156
+ /* @__PURE__ */ jsxs("div", {
1157
+ className: "olas-devtools-tabs",
1158
+ role: "tablist",
1159
+ children: [
1160
+ /* @__PURE__ */ jsx(Tab, {
1161
+ name: "tree",
1162
+ current: tab,
1163
+ setTab,
1164
+ label: "Tree",
1165
+ short: "Tree",
1166
+ count: countLiveControllers(liveTree)
1167
+ }),
1168
+ /* @__PURE__ */ jsx(Tab, {
1169
+ name: "cache",
1170
+ current: tab,
1171
+ setTab,
1172
+ label: "Cache",
1173
+ short: "Cache",
1174
+ count: liveCache.length
1175
+ }),
1176
+ /* @__PURE__ */ jsx(Tab, {
1177
+ name: "inspector",
1178
+ current: tab,
1179
+ setTab,
1180
+ label: "Inspector",
1181
+ short: "Insp",
1182
+ count: cacheEntries.length
1183
+ }),
1184
+ /* @__PURE__ */ jsx(Tab, {
1185
+ name: "mutations",
1186
+ current: tab,
1187
+ setTab,
1188
+ label: "Mutations",
1189
+ short: "Mut",
1190
+ count: liveMutations.length
1191
+ }),
1192
+ /* @__PURE__ */ jsx(Tab, {
1193
+ name: "fields",
1194
+ current: tab,
1195
+ setTab,
1196
+ label: "Fields",
1197
+ short: "Fld",
1198
+ count: liveFields.length
1199
+ }),
1200
+ /* @__PURE__ */ jsxs("button", {
1201
+ type: "button",
1202
+ "aria-pressed": paused,
1203
+ className: paused ? "olas-devtools-pause olas-devtools-pause-on" : "olas-devtools-pause",
1204
+ onClick: () => setPaused(!paused),
1205
+ title: paused ? "Resume live updates" : "Pause live updates",
1206
+ children: [/* @__PURE__ */ jsx("span", {
1207
+ "aria-hidden": "true",
1208
+ children: paused ? "▶" : "⏸"
1209
+ }), /* @__PURE__ */ jsx("span", {
1210
+ className: "olas-devtools-pause-text",
1211
+ children: paused ? " Resume" : " Pause"
1212
+ })]
1213
+ }),
1214
+ /* @__PURE__ */ jsxs("button", {
1215
+ className: "olas-devtools-clear",
1216
+ type: "button",
1217
+ onClick: () => store.clearLogs(),
1218
+ title: "Clear logs",
1219
+ children: [/* @__PURE__ */ jsx("span", {
1220
+ className: "olas-devtools-clear-text",
1221
+ children: "Clear"
1222
+ }), /* @__PURE__ */ jsx("span", {
1223
+ className: "olas-devtools-clear-icon",
1224
+ "aria-hidden": "true",
1225
+ children: "✕"
1226
+ })]
1227
+ })
1228
+ ]
1229
+ }),
1230
+ (tab === "cache" || tab === "inspector" || tab === "mutations" || tab === "fields") && /* @__PURE__ */ jsxs("div", {
1231
+ className: "olas-devtools-filter",
1232
+ children: [/* @__PURE__ */ jsx("input", {
1233
+ type: "search",
1234
+ value: filter,
1235
+ placeholder: `Filter ${tab}…`,
1236
+ onChange: (e) => setFilter(e.target.value)
1237
+ }), filter !== "" && /* @__PURE__ */ jsx("button", {
1238
+ type: "button",
1239
+ onClick: () => setFilter(""),
1240
+ "aria-label": "Clear filter",
1241
+ children: "✕"
1242
+ })]
1243
+ }),
1244
+ /* @__PURE__ */ jsxs("div", {
1245
+ className: "olas-devtools-body",
1246
+ role: "tabpanel",
1247
+ children: [
1248
+ tab === "tree" && /* @__PURE__ */ jsx(TreeView, {
1249
+ tree,
1250
+ mutations: liveMutations
1251
+ }),
1252
+ tab === "cache" && /* @__PURE__ */ jsx(CacheView, {
1253
+ entries: cache,
1254
+ filter
1255
+ }),
1256
+ tab === "inspector" && /* @__PURE__ */ jsx(InspectorView, {
1257
+ entries: cacheEntries,
1258
+ filter
1259
+ }),
1260
+ tab === "mutations" && /* @__PURE__ */ jsx(MutationsView, {
1261
+ entries: mutations,
1262
+ filter
1263
+ }),
1264
+ tab === "fields" && /* @__PURE__ */ jsx(FieldsView, {
1265
+ entries: fields,
1266
+ filter
1267
+ })
1268
+ ]
1269
+ })
1270
+ ]
1271
+ });
1272
+ }
1273
+ function Tab(props) {
1274
+ return /* @__PURE__ */ jsxs("button", {
1275
+ role: "tab",
1276
+ type: "button",
1277
+ "aria-selected": props.current === props.name,
1278
+ title: props.label,
1279
+ className: "olas-devtools-tab",
1280
+ onClick: () => props.setTab(props.name),
1281
+ children: [
1282
+ /* @__PURE__ */ jsx("span", {
1283
+ className: "olas-devtools-tab-label-full",
1284
+ children: props.label
1285
+ }),
1286
+ /* @__PURE__ */ jsx("span", {
1287
+ className: "olas-devtools-tab-label-short",
1288
+ "aria-hidden": "true",
1289
+ children: props.short
1290
+ }),
1291
+ props.count > 0 && /* @__PURE__ */ jsx("span", {
1292
+ className: "olas-devtools-tab-count",
1293
+ "aria-hidden": "true",
1294
+ children: props.count
1295
+ })
1296
+ ]
1297
+ });
1298
+ }
1299
+ function countLiveControllers(node) {
1300
+ let total = node.state !== "disposed" ? 1 : 0;
1301
+ for (const c of node.children) total += countLiveControllers(c);
1302
+ return Math.max(total - 1, 0);
1303
+ }
1304
+ function TreeView({ tree, mutations }) {
1305
+ if (tree.children.length === 0) return /* @__PURE__ */ jsx(Empty, {
1306
+ title: "No controllers yet",
1307
+ hint: "The root hasn't constructed any controllers."
1308
+ });
1309
+ const pending = useMemo(() => rollupPending(mutations), [mutations]);
1310
+ return /* @__PURE__ */ jsx("div", {
1311
+ className: "olas-devtools-tree",
1312
+ children: tree.children.map((child) => /* @__PURE__ */ jsx(TreeNode, {
1313
+ node: child,
1314
+ pending
1315
+ }, child.path.join("/")))
1316
+ });
1317
+ }
1318
+ function rollupPending(entries) {
1319
+ const inFlight = /* @__PURE__ */ new Map();
1320
+ const out = /* @__PURE__ */ new Map();
1321
+ for (const e of entries) {
1322
+ const key = `${e.path.join(">")}#${e.name ?? ""}`;
1323
+ const pathKey = e.path.join(">");
1324
+ if (e.kind === "run") {
1325
+ inFlight.set(key, (inFlight.get(key) ?? 0) + 1);
1326
+ out.set(pathKey, (out.get(pathKey) ?? 0) + 1);
1327
+ } else if (e.kind === "success" || e.kind === "error") {
1328
+ const n = inFlight.get(key) ?? 0;
1329
+ if (n > 0) inFlight.set(key, n - 1);
1330
+ const p = out.get(pathKey) ?? 0;
1331
+ if (p > 0) out.set(pathKey, p - 1);
1332
+ }
1333
+ }
1334
+ return out;
1335
+ }
1336
+ function TreeNode({ node, pending }) {
1337
+ const name = node.path[node.path.length - 1] ?? "?";
1338
+ const stateClass = node.state === "suspended" ? "olas-devtools-tree-state-suspended" : node.state === "disposed" ? "olas-devtools-tree-state-disposed" : "olas-devtools-tree-state-active";
1339
+ const pendingCount = pending.get(node.path.join(">")) ?? 0;
1340
+ const propsPreview = useMemo(() => summarizeProps(node.props), [node.props]);
1341
+ const [propsOpen, setPropsOpen] = useState(false);
1342
+ const canExpandProps = node.props !== void 0 && node.props !== null;
1343
+ return /* @__PURE__ */ jsxs("div", {
1344
+ className: "olas-devtools-tree-node",
1345
+ children: [
1346
+ /* @__PURE__ */ jsxs("span", {
1347
+ className: "olas-devtools-tree-row",
1348
+ children: [
1349
+ /* @__PURE__ */ jsx("span", {
1350
+ className: "olas-devtools-tree-name",
1351
+ children: name
1352
+ }),
1353
+ /* @__PURE__ */ jsx("span", {
1354
+ className: stateClass,
1355
+ children: node.state
1356
+ }),
1357
+ pendingCount > 0 && /* @__PURE__ */ jsxs("span", {
1358
+ className: "olas-devtools-tree-pending",
1359
+ title: "pending mutations on this controller",
1360
+ children: [pendingCount, " pending"]
1361
+ }),
1362
+ canExpandProps && /* @__PURE__ */ jsx("button", {
1363
+ type: "button",
1364
+ className: "olas-devtools-tree-props-toggle",
1365
+ "aria-expanded": propsOpen,
1366
+ onClick: () => setPropsOpen((v) => !v),
1367
+ title: propsOpen ? "Hide props" : "Show full props",
1368
+ children: propsPreview
1369
+ })
1370
+ ]
1371
+ }),
1372
+ propsOpen && canExpandProps && /* @__PURE__ */ jsx("div", {
1373
+ className: "olas-devtools-tree-props",
1374
+ children: /* @__PURE__ */ jsx(JsonView, { value: node.props })
1375
+ }),
1376
+ node.children.length > 0 && /* @__PURE__ */ jsx("div", {
1377
+ className: "olas-devtools-tree-children",
1378
+ children: node.children.map((child) => /* @__PURE__ */ jsx(TreeNode, {
1379
+ node: child,
1380
+ pending
1381
+ }, child.path.join("/")))
1382
+ })
1383
+ ]
1384
+ });
1385
+ }
1386
+ /** Build a one-line props summary for the tree row. */
1387
+ function summarizeProps(props) {
1388
+ if (props === null || props === void 0) return "";
1389
+ if (typeof props === "string") return `"${truncate(props, 24)}"`;
1390
+ if (typeof props === "number" || typeof props === "boolean") return String(props);
1391
+ if (Array.isArray(props)) return `[${props.length}]`;
1392
+ if (typeof props === "object") {
1393
+ const keys = Object.keys(props);
1394
+ if (keys.length === 0) return "{}";
1395
+ return `{ ${keys.slice(0, 2).map((k) => {
1396
+ const v = props[k];
1397
+ return `${k}: ${shortValue(v)}`;
1398
+ }).join(", ")}${keys.length > 2 ? `, +${keys.length - 2}` : ""} }`;
1399
+ }
1400
+ return String(props);
1401
+ }
1402
+ function shortValue(v) {
1403
+ if (v === null) return "null";
1404
+ if (v === void 0) return "undefined";
1405
+ if (typeof v === "string") return `"${truncate(v, 16)}"`;
1406
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
1407
+ if (Array.isArray(v)) return `[${v.length}]`;
1408
+ if (typeof v === "object") return `{${Object.keys(v).length}}`;
1409
+ return String(v);
1410
+ }
1411
+ function truncate(s, max) {
1412
+ return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
1413
+ }
1414
+ function InspectorView({ entries, filter }) {
1415
+ const filtered = useFiltered(entries, filter, inspectorHaystack);
1416
+ if (entries.length === 0) return /* @__PURE__ */ jsx(Empty, {
1417
+ title: "No cache entries",
1418
+ hint: "Subscribe to a query somewhere in the tree to see its data."
1419
+ });
1420
+ if (filtered.length === 0) return /* @__PURE__ */ jsx(Empty, {
1421
+ title: "No matches",
1422
+ hint: `Nothing matches “${filter}”.`
1423
+ });
1424
+ return /* @__PURE__ */ jsx("ul", {
1425
+ className: "olas-devtools-list",
1426
+ children: filtered.map((entry) => /* @__PURE__ */ jsx(InspectorRow, { entry }, entry.key.join("|")))
1427
+ });
1428
+ }
1429
+ function inspectorHaystack(e) {
1430
+ return [
1431
+ ...e.key.map(String),
1432
+ e.status,
1433
+ safeStringify(e.data)
1434
+ ].join(" ");
1435
+ }
1436
+ function InspectorRow({ entry }) {
1437
+ const kindClass = entry.status === "error" ? "olas-devtools-kind-error" : entry.status === "success" ? "olas-devtools-kind-success" : entry.status === "pending" ? "olas-devtools-kind-warn" : "";
1438
+ const ageMs = entry.lastUpdatedAt != null ? Date.now() - entry.lastUpdatedAt : null;
1439
+ const tags = [];
1440
+ if (entry.isStale) tags.push("stale");
1441
+ if (entry.isFetching) tags.push("fetching");
1442
+ if (entry.hasPendingMutations) tags.push("optimistic");
1443
+ return /* @__PURE__ */ jsx(Row, {
1444
+ kind: entry.status,
1445
+ kindClass,
1446
+ target: formatPath(entry.key),
1447
+ t: entry.lastUpdatedAt ?? Date.now(),
1448
+ payload: entry.error ?? entry.data,
1449
+ suffix: [ageMs != null ? `${formatAge(ageMs)} ago` : "—", ...tags].join(" · ")
1450
+ });
1451
+ }
1452
+ function safeStringify(v) {
1453
+ try {
1454
+ return JSON.stringify(v) ?? "";
1455
+ } catch {
1456
+ return String(v);
1457
+ }
1458
+ }
1459
+ function formatAge(ms) {
1460
+ if (ms < 1e3) return `${ms}ms`;
1461
+ if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
1462
+ if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
1463
+ return `${Math.round(ms / 36e5)}h`;
1464
+ }
1465
+ function readUrlHash(key, defaultTab) {
1466
+ const empty = {
1467
+ tree: "",
1468
+ cache: "",
1469
+ inspector: "",
1470
+ mutations: "",
1471
+ fields: ""
1472
+ };
1473
+ if (key === void 0) return {
1474
+ tab: defaultTab,
1475
+ filters: empty
1476
+ };
1477
+ if (typeof window === "undefined") return {
1478
+ tab: defaultTab,
1479
+ filters: empty
1480
+ };
1481
+ try {
1482
+ const raw = new URLSearchParams(window.location.hash.replace(/^#/, "")).get(key);
1483
+ if (raw === null) return {
1484
+ tab: defaultTab,
1485
+ filters: empty
1486
+ };
1487
+ const parsed = JSON.parse(decodeURIComponent(raw));
1488
+ return {
1489
+ tab: parsed.tab ?? defaultTab,
1490
+ filters: {
1491
+ ...empty,
1492
+ ...parsed.filters ?? {}
1493
+ }
1494
+ };
1495
+ } catch {
1496
+ return {
1497
+ tab: defaultTab,
1498
+ filters: empty
1499
+ };
1500
+ }
1501
+ }
1502
+ function writeUrlHash(key, state) {
1503
+ if (typeof window === "undefined") return;
1504
+ const params = new URLSearchParams(window.location.hash.replace(/^#/, ""));
1505
+ params.set(key, encodeURIComponent(JSON.stringify(state)));
1506
+ const next = `#${params.toString()}`;
1507
+ if (next !== window.location.hash) window.history.replaceState(null, "", next);
1508
+ }
1509
+ function CacheView({ entries, filter }) {
1510
+ const filtered = useFiltered(entries, filter, cacheHaystack);
1511
+ if (entries.length === 0) return /* @__PURE__ */ jsx(Empty, {
1512
+ title: "No cache events yet",
1513
+ hint: "Trigger a query subscription to see fetches here."
1514
+ });
1515
+ if (filtered.length === 0) return /* @__PURE__ */ jsx(Empty, {
1516
+ title: "No matches",
1517
+ hint: `Nothing matches “${filter}”.`
1518
+ });
1519
+ return /* @__PURE__ */ jsx("ul", {
1520
+ className: "olas-devtools-list",
1521
+ children: [...filtered].reverse().map((entry) => /* @__PURE__ */ jsx(CacheRow, { entry }, entry.id))
1522
+ });
1523
+ }
1524
+ function cacheHaystack(e) {
1525
+ const parts = [e.kind, ...e.queryKey.map((p) => String(p))];
1526
+ if (e.kind === "fetch-error") parts.push(safeStringify(e.error));
1527
+ if (e.kind === "subscribed") parts.push(...e.subscriberPath);
1528
+ return parts.join(" ");
1529
+ }
1530
+ function CacheRow({ entry }) {
1531
+ const kindClass = entry.kind === "fetch-error" ? "olas-devtools-kind-error" : entry.kind === "fetch-success" ? "olas-devtools-kind-success" : entry.kind === "invalidated" || entry.kind === "gc" ? "olas-devtools-kind-warn" : "";
1532
+ let inline = null;
1533
+ let payload;
1534
+ let suffix = null;
1535
+ if (entry.kind === "fetch-success") suffix = `${entry.durationMs}ms`;
1536
+ else if (entry.kind === "fetch-error") {
1537
+ suffix = `${entry.durationMs}ms`;
1538
+ payload = entry.error;
1539
+ } else if (entry.kind === "subscribed") inline = `from ${formatPath(entry.subscriberPath)}`;
1540
+ return /* @__PURE__ */ jsx(Row, {
1541
+ kind: entry.kind,
1542
+ kindClass,
1543
+ target: formatPath(entry.queryKey),
1544
+ t: entry.t,
1545
+ inline,
1546
+ payload,
1547
+ suffix
1548
+ });
1549
+ }
1550
+ function MutationsView({ entries, filter }) {
1551
+ const filtered = useFiltered(entries, filter, mutationHaystack);
1552
+ if (entries.length === 0) return /* @__PURE__ */ jsx(Empty, {
1553
+ title: "No mutations yet",
1554
+ hint: "Trigger a mutation to see the lifecycle here."
1555
+ });
1556
+ if (filtered.length === 0) return /* @__PURE__ */ jsx(Empty, {
1557
+ title: "No matches",
1558
+ hint: `Nothing matches “${filter}”.`
1559
+ });
1560
+ return /* @__PURE__ */ jsx("ul", {
1561
+ className: "olas-devtools-list",
1562
+ children: [...filtered].reverse().map((entry) => /* @__PURE__ */ jsx(MutationRow, { entry }, entry.id))
1563
+ });
1564
+ }
1565
+ function mutationHaystack(e) {
1566
+ const parts = [
1567
+ e.kind,
1568
+ ...e.path,
1569
+ e.name ?? ""
1570
+ ];
1571
+ if (e.kind === "run") parts.push(safeStringify(e.vars));
1572
+ if (e.kind === "success") parts.push(safeStringify(e.result));
1573
+ if (e.kind === "error") parts.push(safeStringify(e.error));
1574
+ return parts.join(" ");
1575
+ }
1576
+ function MutationRow({ entry }) {
1577
+ const kindClass = entry.kind === "error" ? "olas-devtools-kind-error" : entry.kind === "rollback" ? "olas-devtools-kind-rollback" : entry.kind === "success" ? "olas-devtools-kind-success" : "";
1578
+ const target = entry.name ? `${entry.name} · ${formatPath(entry.path)}` : formatPath(entry.path);
1579
+ let payload;
1580
+ let suffix = null;
1581
+ if (entry.kind === "run") payload = entry.vars;
1582
+ else if (entry.kind === "success") {
1583
+ payload = entry.result;
1584
+ if (entry.durationMs !== void 0) suffix = `${entry.durationMs}ms`;
1585
+ } else if (entry.kind === "error") {
1586
+ payload = entry.error;
1587
+ if (entry.durationMs !== void 0) suffix = `${entry.durationMs}ms`;
1588
+ }
1589
+ return /* @__PURE__ */ jsx(Row, {
1590
+ kind: entry.kind,
1591
+ kindClass,
1592
+ target,
1593
+ t: entry.t,
1594
+ payload,
1595
+ suffix
1596
+ });
1597
+ }
1598
+ function FieldsView({ entries, filter }) {
1599
+ const filtered = useFiltered(entries, filter, fieldHaystack);
1600
+ if (entries.length === 0) return /* @__PURE__ */ jsx(Empty, {
1601
+ title: "No field validations yet",
1602
+ hint: "Type into a form bound via ctx.form(...) or ctx.field(...) — each pass lands here."
1603
+ });
1604
+ if (filtered.length === 0) return /* @__PURE__ */ jsx(Empty, {
1605
+ title: "No matches",
1606
+ hint: `Nothing matches “${filter}”.`
1607
+ });
1608
+ return /* @__PURE__ */ jsx("ul", {
1609
+ className: "olas-devtools-list",
1610
+ children: [...filtered].reverse().map((entry) => /* @__PURE__ */ jsx(FieldRow, { entry }, entry.id))
1611
+ });
1612
+ }
1613
+ function fieldHaystack(e) {
1614
+ return [
1615
+ e.field,
1616
+ ...e.path,
1617
+ e.valid ? "valid" : "invalid",
1618
+ ...e.errors
1619
+ ].join(" ");
1620
+ }
1621
+ function FieldRow({ entry }) {
1622
+ const kindClass = entry.valid ? "olas-devtools-kind-success" : "olas-devtools-kind-error";
1623
+ return /* @__PURE__ */ jsx(Row, {
1624
+ kind: entry.valid ? "valid" : "invalid",
1625
+ kindClass,
1626
+ target: `${formatPath(entry.path)} · ${entry.field}`,
1627
+ t: entry.t,
1628
+ inline: entry.errors.length > 0 ? entry.errors.join(" · ") : null
1629
+ });
1630
+ }
1631
+ function Row(props) {
1632
+ const { kind, kindClass, target, t, inline, payload, suffix } = props;
1633
+ const hasPayload = payload !== void 0;
1634
+ const [expanded, setExpanded] = useState(false);
1635
+ const togglable = hasPayload;
1636
+ return /* @__PURE__ */ jsxs("li", {
1637
+ className: togglable ? "olas-devtools-row-clickable" : "",
1638
+ children: [
1639
+ /* @__PURE__ */ jsxs("div", {
1640
+ className: "olas-devtools-row-top",
1641
+ onClick: togglable ? () => setExpanded((v) => !v) : void 0,
1642
+ children: [
1643
+ /* @__PURE__ */ jsx("span", {
1644
+ className: `olas-devtools-kind ${kindClass}`,
1645
+ children: kind
1646
+ }),
1647
+ /* @__PURE__ */ jsx("span", {
1648
+ className: "olas-devtools-target",
1649
+ children: target
1650
+ }),
1651
+ suffix !== void 0 && suffix !== null && /* @__PURE__ */ jsx("span", {
1652
+ className: "olas-devtools-duration",
1653
+ children: suffix
1654
+ }),
1655
+ /* @__PURE__ */ jsx("span", {
1656
+ className: "olas-devtools-time",
1657
+ children: formatTime(t)
1658
+ }),
1659
+ togglable && /* @__PURE__ */ jsx("span", {
1660
+ "aria-hidden": "true",
1661
+ className: `olas-devtools-chevron ${expanded ? "olas-devtools-chevron-open" : ""}`,
1662
+ children: "›"
1663
+ })
1664
+ ]
1665
+ }),
1666
+ inline != null && /* @__PURE__ */ jsx("div", {
1667
+ className: "olas-devtools-payload olas-devtools-payload-inline",
1668
+ children: inline
1669
+ }),
1670
+ hasPayload && expanded && /* @__PURE__ */ jsx("div", {
1671
+ className: "olas-devtools-payload olas-devtools-payload-json",
1672
+ children: /* @__PURE__ */ jsx(JsonView, { value: payload })
1673
+ })
1674
+ ]
1675
+ });
1676
+ }
1677
+ function useFiltered(items, filter, haystack) {
1678
+ return useMemo(() => {
1679
+ if (filter.trim() === "") return [...items];
1680
+ const q = filter.toLowerCase();
1681
+ return items.filter((item) => haystack(item).toLowerCase().includes(q));
1682
+ }, [
1683
+ items,
1684
+ filter,
1685
+ haystack
1686
+ ]);
1687
+ }
1688
+ function Empty({ title, hint }) {
1689
+ return /* @__PURE__ */ jsxs("div", {
1690
+ className: "olas-devtools-empty",
1691
+ children: [/* @__PURE__ */ jsx("div", {
1692
+ className: "olas-devtools-empty-title",
1693
+ children: title
1694
+ }), /* @__PURE__ */ jsx("div", {
1695
+ className: "olas-devtools-empty-hint",
1696
+ children: hint
1697
+ })]
1698
+ });
1699
+ }
1700
+ //#endregion
1701
+ //#region src/DevtoolsLauncher.tsx
1702
+ const MIN_W = 360;
1703
+ const MIN_H = 280;
1704
+ const HEADER_H = 30;
1705
+ const DEFAULT_W = 520;
1706
+ const DEFAULT_H = 520;
1707
+ const MARGIN = 16;
1708
+ function DevtoolsLauncher(props) {
1709
+ const storageKey = props.storageKey ?? "olas-devtools-window";
1710
+ const [state, setState] = useState(() => loadState(storageKey, props.initial));
1711
+ useEffect(() => {
1712
+ if (typeof localStorage === "undefined") return;
1713
+ try {
1714
+ localStorage.setItem(storageKey, JSON.stringify(state));
1715
+ } catch {}
1716
+ }, [state, storageKey]);
1717
+ useEffect(() => {
1718
+ const onResize = () => setState((s) => clampToViewport(s));
1719
+ if (typeof window === "undefined") return;
1720
+ window.addEventListener("resize", onResize);
1721
+ return () => window.removeEventListener("resize", onResize);
1722
+ }, []);
1723
+ const setOpen = useCallback((open) => setState((s) => ({
1724
+ ...s,
1725
+ open
1726
+ })), []);
1727
+ const setMin = useCallback((minimized) => setState((s) => ({
1728
+ ...s,
1729
+ minimized
1730
+ })), []);
1731
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1732
+ /* @__PURE__ */ jsx("style", { children: DEVTOOLS_CSS }),
1733
+ /* @__PURE__ */ jsx(LauncherButton, {
1734
+ open: state.open,
1735
+ onClick: () => setOpen(!state.open)
1736
+ }),
1737
+ state.open && /* @__PURE__ */ jsx(FloatingWindow, {
1738
+ state,
1739
+ setState,
1740
+ onClose: () => setOpen(false),
1741
+ onMinimize: () => setMin(!state.minimized),
1742
+ children: !state.minimized && /* @__PURE__ */ jsx("div", {
1743
+ className: "olas-devtools-floating-body",
1744
+ children: /* @__PURE__ */ jsx(DevtoolsPanel, {
1745
+ root: props.root,
1746
+ defaultTab: props.defaultTab,
1747
+ maxEntries: props.maxEntries,
1748
+ urlHashKey: props.urlHashKey
1749
+ })
1750
+ })
1751
+ })
1752
+ ] });
1753
+ }
1754
+ function LauncherButton({ open, onClick }) {
1755
+ return /* @__PURE__ */ jsxs("button", {
1756
+ type: "button",
1757
+ "aria-label": open ? "Hide Olas devtools" : "Show Olas devtools",
1758
+ onClick,
1759
+ className: `olas-devtools-launcher ${open ? "olas-devtools-launcher-active" : ""}`,
1760
+ children: [/* @__PURE__ */ jsx("span", {
1761
+ "aria-hidden": "true",
1762
+ className: "olas-devtools-launcher-dot"
1763
+ }), /* @__PURE__ */ jsx("span", {
1764
+ className: "olas-devtools-launcher-label",
1765
+ children: "Olas devtools"
1766
+ })]
1767
+ });
1768
+ }
1769
+ function FloatingWindow(props) {
1770
+ const { state, setState, onClose, onMinimize } = props;
1771
+ const dragState = useRef(null);
1772
+ const onPointerDown = (kind) => (e) => {
1773
+ e.preventDefault();
1774
+ e.target.setPointerCapture(e.pointerId);
1775
+ dragState.current = {
1776
+ kind,
1777
+ ox: e.clientX,
1778
+ oy: e.clientY,
1779
+ sx: state.x,
1780
+ sy: state.y,
1781
+ sw: state.w,
1782
+ sh: state.h
1783
+ };
1784
+ };
1785
+ const onPointerMove = (e) => {
1786
+ const d = dragState.current;
1787
+ if (d === null) return;
1788
+ const dx = e.clientX - d.ox;
1789
+ const dy = e.clientY - d.oy;
1790
+ if (d.kind === "move") setState((s) => clampToViewport({
1791
+ ...s,
1792
+ x: d.sx + dx,
1793
+ y: d.sy + dy
1794
+ }));
1795
+ else {
1796
+ const w = Math.max(MIN_W, d.sw + dx);
1797
+ const h = Math.max(MIN_H, d.sh + dy);
1798
+ setState((s) => clampToViewport({
1799
+ ...s,
1800
+ w,
1801
+ h
1802
+ }));
1803
+ }
1804
+ };
1805
+ const onPointerUp = () => {
1806
+ dragState.current = null;
1807
+ };
1808
+ const minimized = state.minimized;
1809
+ const height = minimized ? HEADER_H : state.h;
1810
+ return /* @__PURE__ */ jsxs("div", {
1811
+ className: "olas-devtools-floating",
1812
+ role: "dialog",
1813
+ "aria-label": "Olas devtools",
1814
+ style: {
1815
+ left: state.x,
1816
+ top: state.y,
1817
+ width: state.w,
1818
+ height
1819
+ },
1820
+ onPointerMove,
1821
+ onPointerUp,
1822
+ onPointerCancel: onPointerUp,
1823
+ children: [
1824
+ /* @__PURE__ */ jsxs("div", {
1825
+ className: "olas-devtools-floating-header",
1826
+ onPointerDown: onPointerDown("move"),
1827
+ children: [
1828
+ /* @__PURE__ */ jsx("span", {
1829
+ className: "olas-devtools-floating-grip",
1830
+ "aria-hidden": "true",
1831
+ children: "⠿"
1832
+ }),
1833
+ /* @__PURE__ */ jsx("span", {
1834
+ className: "olas-devtools-floating-title",
1835
+ children: "Olas devtools"
1836
+ }),
1837
+ /* @__PURE__ */ jsxs("div", {
1838
+ className: "olas-devtools-floating-actions",
1839
+ children: [/* @__PURE__ */ jsx("button", {
1840
+ type: "button",
1841
+ "aria-label": minimized ? "Expand" : "Minimize",
1842
+ onClick: onMinimize,
1843
+ className: "olas-devtools-floating-action",
1844
+ children: minimized ? "▢" : "–"
1845
+ }), /* @__PURE__ */ jsx("button", {
1846
+ type: "button",
1847
+ "aria-label": "Close",
1848
+ onClick: onClose,
1849
+ className: "olas-devtools-floating-action",
1850
+ children: "×"
1851
+ })]
1852
+ })
1853
+ ]
1854
+ }),
1855
+ props.children,
1856
+ !minimized && /* @__PURE__ */ jsx("span", {
1857
+ className: "olas-devtools-floating-resize",
1858
+ onPointerDown: onPointerDown("resize"),
1859
+ "aria-label": "Resize",
1860
+ role: "separator"
1861
+ })
1862
+ ]
1863
+ });
1864
+ }
1865
+ function clampToViewport(s) {
1866
+ if (typeof window === "undefined") return s;
1867
+ const vw = window.innerWidth;
1868
+ const vh = window.innerHeight;
1869
+ const w = Math.min(s.w, vw - MARGIN * 2);
1870
+ const h = Math.min(s.h, vh - MARGIN * 2);
1871
+ const x = Math.max(MARGIN, Math.min(s.x, vw - w - MARGIN));
1872
+ const y = Math.max(MARGIN, Math.min(s.y, vh - HEADER_H - MARGIN));
1873
+ return {
1874
+ ...s,
1875
+ x,
1876
+ y,
1877
+ w,
1878
+ h
1879
+ };
1880
+ }
1881
+ function loadState(storageKey, initial) {
1882
+ const vw = typeof window !== "undefined" ? window.innerWidth : 1024;
1883
+ const vh = typeof window !== "undefined" ? window.innerHeight : 768;
1884
+ const w = initial?.w ?? DEFAULT_W;
1885
+ const h = initial?.h ?? DEFAULT_H;
1886
+ const defaults = {
1887
+ x: initial?.x ?? Math.max(MARGIN, vw - w - MARGIN),
1888
+ y: initial?.y ?? Math.max(MARGIN, vh - h - MARGIN - 56),
1889
+ w,
1890
+ h,
1891
+ open: false,
1892
+ minimized: false
1893
+ };
1894
+ if (typeof localStorage === "undefined") return defaults;
1895
+ try {
1896
+ const raw = localStorage.getItem(storageKey);
1897
+ if (!raw) return defaults;
1898
+ const parsed = JSON.parse(raw);
1899
+ return clampToViewport({
1900
+ x: parsed.x ?? defaults.x,
1901
+ y: parsed.y ?? defaults.y,
1902
+ w: parsed.w ?? defaults.w,
1903
+ h: parsed.h ?? defaults.h,
1904
+ open: parsed.open ?? false,
1905
+ minimized: parsed.minimized ?? false
1906
+ });
1907
+ } catch {
1908
+ return defaults;
1909
+ }
1910
+ }
1911
+ //#endregion
1912
+ export { DevtoolsLauncher, DevtoolsPanel, DevtoolsStore, formatPath, formatPayload, formatTime, insertNode, setNodeState };
1913
+
1914
+ //# sourceMappingURL=index.mjs.map