use-everywhere 1.0.0 → 1.0.2

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.
@@ -1,301 +1,396 @@
1
1
  'use client';
2
- import {
3
- getSharedStore,
4
- usePeers
5
- } from "../chunk-YPQ6PLD5.js";
6
-
7
- // src/devtools/inspector.tsx
8
- import { useState as useState2 } from "react";
9
- import { createPortal } from "react-dom";
10
- import { DEFAULT_NAME } from "@use-everywhere/core";
11
-
12
- // src/devtools/panel.tsx
2
+ import { n as usePeers, u as getSharedStore } from "../use-peers-DtpHGzXE.js";
13
3
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
14
- import { observeBus } from "@use-everywhere/core";
15
- import { jsx, jsxs } from "react/jsx-runtime";
16
- var short = (id) => id.slice(0, 6);
4
+ import { DEFAULT_NAME, observeBus } from "@use-everywhere/core";
5
+ import { createPortal } from "react-dom";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+ //#region src/devtools/panel.tsx
8
+ const short = (id) => id.slice(0, 6);
17
9
  function wireLabel(wire) {
18
- return `${wire.scope}/${wire.type}`;
10
+ return `${wire.scope}/${wire.type}`;
19
11
  }
20
- var SCOPES = ["all", "state", "leader", "presence", "channel"];
21
- function Panel({
22
- name,
23
- limit,
24
- leaseMs,
25
- position,
26
- defaultOpen
27
- }) {
28
- const [open, setOpen] = useState(defaultOpen);
29
- const [wires, setWires] = useState([]);
30
- const [crown, setCrown] = useState(null);
31
- const [paused, setPaused] = useState(false);
32
- const [filter, setFilter] = useState("");
33
- const [scopeView, setScopeView] = useState("all");
34
- const [editing, setEditing] = useState(null);
35
- const [frames, setFrames] = useState([]);
36
- const nextId = useRef(0);
37
- const crownAt = useRef(0);
38
- const pausedRef = useRef(false);
39
- useEffect(() => {
40
- pausedRef.current = paused;
41
- }, [paused]);
42
- const peers = usePeers({ name });
43
- const store = getSharedStore(name);
44
- const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
45
- const snapshot = useSyncExternalStore(
46
- subscribe,
47
- () => store.getSnapshot(),
48
- () => store.getSnapshot()
49
- );
50
- const versions = useSyncExternalStore(
51
- subscribe,
52
- () => store.getVersions(),
53
- () => store.getVersions()
54
- );
55
- useEffect(() => {
56
- return observeBus(name, (event) => {
57
- const { wire, direction } = event;
58
- if (wire.scope === "leader") {
59
- if (wire.type === "resign") {
60
- setCrown(null);
61
- crownAt.current = 0;
62
- } else if (wire.type === "claim" || wire.type === "heartbeat") {
63
- setCrown(wire.clientId);
64
- crownAt.current = Date.now();
65
- }
66
- }
67
- if (pausedRef.current) return;
68
- const id = nextId.current++;
69
- const label = wireLabel(wire);
70
- setWires(
71
- (prev) => [
72
- ...prev.slice(-(limit - 1)),
73
- { id, direction, scope: wire.scope, label, from: short(wire.clientId) }
74
- ].slice(-limit)
75
- );
76
- if (wire.scope === "state") {
77
- setFrames(
78
- (prev) => [...prev, { wireId: id, label, snapshot: { ...store.getSnapshot() } }].slice(-limit)
79
- );
80
- }
81
- });
82
- }, [name, limit, store]);
83
- useEffect(() => {
84
- const timer = setInterval(
85
- () => {
86
- if (crownAt.current && Date.now() - crownAt.current > leaseMs) {
87
- setCrown(null);
88
- crownAt.current = 0;
89
- }
90
- },
91
- Math.max(50, Math.floor(leaseMs / 4))
92
- );
93
- return () => clearInterval(timer);
94
- }, [leaseMs]);
95
- const selfId = store.clientId;
96
- const entries = Object.entries(versions);
97
- const needle = filter.trim().toLowerCase();
98
- const shown = wires.filter((wire) => {
99
- if (scopeView !== "all" && wire.scope !== scopeView) return false;
100
- if (!needle) return true;
101
- return wire.label.toLowerCase().includes(needle) || wire.from.toLowerCase().includes(needle);
102
- });
103
- const commit = (key, draft) => {
104
- let value;
105
- try {
106
- value = JSON.parse(draft);
107
- } catch {
108
- return;
109
- }
110
- store.set(key, value);
111
- setEditing(null);
112
- };
113
- const draftIsValid = (draft) => {
114
- try {
115
- JSON.parse(draft);
116
- return true;
117
- } catch {
118
- return false;
119
- }
120
- };
121
- const restore = (frame) => {
122
- for (const [key, value] of Object.entries(frame.snapshot)) {
123
- if (Object.is(store.getSnapshot()[key], value)) continue;
124
- store.set(key, value);
125
- }
126
- };
127
- return /* @__PURE__ */ jsxs("div", { className: `ue-ins ue-ins--${position}`, "data-testid": "ue-inspector", children: [
128
- /* @__PURE__ */ jsxs(
129
- "button",
130
- {
131
- type: "button",
132
- className: "ue-ins__bar",
133
- onClick: () => setOpen((v) => !v),
134
- "aria-expanded": open,
135
- children: [
136
- /* @__PURE__ */ jsx("span", { className: "ue-ins__dot" }),
137
- /* @__PURE__ */ jsx("span", { className: "ue-ins__title", children: "use-everywhere" }),
138
- /* @__PURE__ */ jsx("span", { className: "ue-ins__muted", children: name }),
139
- crown ? /* @__PURE__ */ jsxs("span", { className: "ue-ins__crown", "data-testid": "ue-crown", children: [
140
- "\u2654 ",
141
- crown === selfId ? "this tab" : short(crown)
142
- ] }) : null
143
- ]
144
- }
145
- ),
146
- open ? /* @__PURE__ */ jsxs("div", { className: "ue-ins__body", children: [
147
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
148
- /* @__PURE__ */ jsx("div", { className: "ue-ins__h", children: "This tab" }),
149
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
150
- /* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: short(selfId) }),
151
- /* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: crown === selfId ? "leader" : crown ? "follower" : "no leader" })
152
- ] })
153
- ] }),
154
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
155
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
156
- "Peers (",
157
- peers.length,
158
- ")"
159
- ] }),
160
- peers.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "nobody else here" }) : peers.map((peer) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
161
- /* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: short(peer.id) }),
162
- /* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: peer.kind })
163
- ] }, peer.id))
164
- ] }),
165
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
166
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
167
- "State (",
168
- entries.length,
169
- ")"
170
- ] }),
171
- entries.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "no keys yet" }) : entries.map(([key, version]) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
172
- /* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: key }),
173
- editing?.key === key ? /* @__PURE__ */ jsx(
174
- "input",
175
- {
176
- className: `ue-ins__edit${draftIsValid(editing.draft) ? "" : " ue-ins__v--invalid"}`,
177
- value: editing.draft,
178
- autoFocus: true,
179
- "aria-label": `Value of ${key}`,
180
- onChange: (event) => setEditing({ key, draft: event.target.value }),
181
- onKeyDown: (event) => {
182
- if (event.key === "Enter") commit(key, editing.draft);
183
- if (event.key === "Escape") setEditing(null);
184
- },
185
- onBlur: () => setEditing(null),
186
- "data-testid": `ue-edit-${key}`
187
- }
188
- ) : /* @__PURE__ */ jsx(
189
- "button",
190
- {
191
- type: "button",
192
- className: "ue-ins__v ue-ins__v--editable",
193
- onClick: () => setEditing({ key, draft: JSON.stringify(snapshot[key]) ?? "" }),
194
- "data-testid": `ue-value-${key}`,
195
- children: JSON.stringify(snapshot[key])
196
- }
197
- ),
198
- /* @__PURE__ */ jsxs("span", { className: "ue-ins__ver", children: [
199
- version[0],
200
- "\xB7",
201
- short(version[1])
202
- ] })
203
- ] }, key))
204
- ] }),
205
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
206
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
207
- "Wires (",
208
- shown.length,
209
- shown.length === wires.length ? "" : ` of ${wires.length}`,
210
- ")",
211
- paused ? /* @__PURE__ */ jsx("span", { className: "ue-ins__paused", children: " \xB7 paused" }) : null
212
- ] }),
213
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__tools", children: [
214
- /* @__PURE__ */ jsx(
215
- "button",
216
- {
217
- type: "button",
218
- className: "ue-ins__btn",
219
- "aria-pressed": paused,
220
- onClick: () => setPaused((value) => !value),
221
- "data-testid": "ue-pause",
222
- children: paused ? "resume" : "pause"
223
- }
224
- ),
225
- /* @__PURE__ */ jsx(
226
- "button",
227
- {
228
- type: "button",
229
- className: "ue-ins__btn",
230
- onClick: () => {
231
- setWires([]);
232
- setFrames([]);
233
- },
234
- "data-testid": "ue-clear",
235
- children: "clear"
236
- }
237
- ),
238
- /* @__PURE__ */ jsx(
239
- "input",
240
- {
241
- className: "ue-ins__filter",
242
- value: filter,
243
- placeholder: "filter",
244
- "aria-label": "Filter wires",
245
- onChange: (event) => setFilter(event.target.value),
246
- "data-testid": "ue-filter"
247
- }
248
- )
249
- ] }),
250
- /* @__PURE__ */ jsx("div", { className: "ue-ins__tools", role: "group", "aria-label": "Scope", children: SCOPES.map((scope) => /* @__PURE__ */ jsx(
251
- "button",
252
- {
253
- type: "button",
254
- className: "ue-ins__btn",
255
- "aria-pressed": scopeView === scope,
256
- onClick: () => setScopeView(scope),
257
- "data-testid": `ue-scope-${scope}`,
258
- children: scope
259
- },
260
- scope
261
- )) }),
262
- shown.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", "data-testid": "ue-wires", children: wires.length === 0 ? "nothing yet" : "no matches" }) : /* @__PURE__ */ jsx("div", { className: "ue-ins__log", "data-testid": "ue-wires", children: shown.map((wire) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__wire", children: [
263
- /* @__PURE__ */ jsx("span", { className: `ue-ins__dir ue-ins__dir--${wire.direction}`, children: wire.direction === "out" ? "\u2192" : "\u2190" }),
264
- /* @__PURE__ */ jsx("span", { className: "ue-ins__scope", children: wire.label }),
265
- /* @__PURE__ */ jsx("span", { className: "ue-ins__from", children: wire.from })
266
- ] }, wire.id)) })
267
- ] }),
268
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
269
- /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
270
- "Timeline (",
271
- frames.length,
272
- ")"
273
- ] }),
274
- frames.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "no state on the wire yet" }) : /* @__PURE__ */ jsx("div", { className: "ue-ins__log", children: frames.map((frame) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__wire", children: [
275
- /* @__PURE__ */ jsx(
276
- "button",
277
- {
278
- type: "button",
279
- className: "ue-ins__btn",
280
- onClick: () => restore(frame),
281
- "data-testid": `ue-restore-${String(frame.wireId)}`,
282
- children: "restore"
283
- }
284
- ),
285
- /* @__PURE__ */ jsx("span", { className: "ue-ins__scope", children: frame.label }),
286
- /* @__PURE__ */ jsxs("span", { className: "ue-ins__from", children: [
287
- Object.keys(frame.snapshot).length,
288
- " key",
289
- Object.keys(frame.snapshot).length === 1 ? "" : "s"
290
- ] })
291
- ] }, frame.wireId)) })
292
- ] })
293
- ] }) : null
294
- ] });
12
+ /** The scopes worth separating. `all` is not a scope; it is the absence of one. */
13
+ const SCOPES = [
14
+ "all",
15
+ "state",
16
+ "leader",
17
+ "presence",
18
+ "channel"
19
+ ];
20
+ /**
21
+ * The panel's contents. Split out of `Inspector` because the Inspector is now
22
+ * the shadow host and this is what gets portalled into it — keeping them in one
23
+ * component would mean the hooks re-run whenever the host re-renders for
24
+ * reasons that have nothing to do with what is on the wire.
25
+ */
26
+ function Panel({ name, limit, leaseMs, position, defaultOpen }) {
27
+ const [open, setOpen] = useState(defaultOpen);
28
+ const [wires, setWires] = useState([]);
29
+ const [crown, setCrown] = useState(null);
30
+ const [paused, setPaused] = useState(false);
31
+ const [filter, setFilter] = useState("");
32
+ const [scopeView, setScopeView] = useState("all");
33
+ const [editing, setEditing] = useState(null);
34
+ const [frames, setFrames] = useState([]);
35
+ const nextId = useRef(0);
36
+ const crownAt = useRef(0);
37
+ const pausedRef = useRef(false);
38
+ useEffect(() => {
39
+ pausedRef.current = paused;
40
+ }, [paused]);
41
+ const peers = usePeers({ name });
42
+ const store = getSharedStore(name);
43
+ const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
44
+ const snapshot = useSyncExternalStore(subscribe, () => store.getSnapshot(), () => store.getSnapshot());
45
+ const versions = useSyncExternalStore(subscribe, () => store.getVersions(), () => store.getVersions());
46
+ useEffect(() => {
47
+ return observeBus(name, (event) => {
48
+ const { wire, direction } = event;
49
+ if (wire.scope === "leader") {
50
+ if (wire.type === "resign") {
51
+ setCrown(null);
52
+ crownAt.current = 0;
53
+ } else if (wire.type === "claim" || wire.type === "heartbeat") {
54
+ setCrown(wire.clientId);
55
+ crownAt.current = Date.now();
56
+ }
57
+ }
58
+ if (pausedRef.current) return;
59
+ const id = nextId.current++;
60
+ const label = wireLabel(wire);
61
+ setWires((prev) => [...prev.slice(-(limit - 1)), {
62
+ id,
63
+ direction,
64
+ scope: wire.scope,
65
+ label,
66
+ from: short(wire.clientId)
67
+ }].slice(-limit));
68
+ if (wire.scope === "state") setFrames((prev) => [...prev, {
69
+ wireId: id,
70
+ label,
71
+ snapshot: { ...store.getSnapshot() }
72
+ }].slice(-limit));
73
+ });
74
+ }, [
75
+ name,
76
+ limit,
77
+ store
78
+ ]);
79
+ useEffect(() => {
80
+ const timer = setInterval(() => {
81
+ if (crownAt.current && Date.now() - crownAt.current > leaseMs) {
82
+ setCrown(null);
83
+ crownAt.current = 0;
84
+ }
85
+ }, Math.max(50, Math.floor(leaseMs / 4)));
86
+ return () => clearInterval(timer);
87
+ }, [leaseMs]);
88
+ const selfId = store.clientId;
89
+ const entries = Object.entries(versions);
90
+ const needle = filter.trim().toLowerCase();
91
+ const shown = wires.filter((wire) => {
92
+ if (scopeView !== "all" && wire.scope !== scopeView) return false;
93
+ if (!needle) return true;
94
+ return wire.label.toLowerCase().includes(needle) || wire.from.toLowerCase().includes(needle);
95
+ });
96
+ const commit = (key, draft) => {
97
+ let value;
98
+ try {
99
+ value = JSON.parse(draft);
100
+ } catch {
101
+ return;
102
+ }
103
+ store.set(key, value);
104
+ setEditing(null);
105
+ };
106
+ const draftIsValid = (draft) => {
107
+ try {
108
+ JSON.parse(draft);
109
+ return true;
110
+ } catch {
111
+ return false;
112
+ }
113
+ };
114
+ /**
115
+ * Put a recorded snapshot back.
116
+ *
117
+ * Restoring is a **write**, not a rewind: every key that differs is set
118
+ * through the store, takes a fresh version, and goes on the wire, so peers
119
+ * converge on the restored value instead of quietly disagreeing with the tab
120
+ * that time-travelled. Nothing about history is undone — this is the honest
121
+ * version of the feature, and the label says "restore" rather than "undo" for
122
+ * that reason.
123
+ *
124
+ * Keys added after the frame was taken are left alone rather than deleted:
125
+ * removing a key another tab is using is a bigger claim than a devtool should
126
+ * make from a picture of the past.
127
+ */
128
+ const restore = (frame) => {
129
+ for (const [key, value] of Object.entries(frame.snapshot)) {
130
+ if (Object.is(store.getSnapshot()[key], value)) continue;
131
+ store.set(key, value);
132
+ }
133
+ };
134
+ return /* @__PURE__ */ jsxs("div", {
135
+ className: `ue-ins ue-ins--${position}`,
136
+ "data-testid": "ue-inspector",
137
+ children: [/* @__PURE__ */ jsxs("button", {
138
+ type: "button",
139
+ className: "ue-ins__bar",
140
+ onClick: () => setOpen((v) => !v),
141
+ "aria-expanded": open,
142
+ children: [
143
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__dot" }),
144
+ /* @__PURE__ */ jsx("span", {
145
+ className: "ue-ins__title",
146
+ children: "use-everywhere"
147
+ }),
148
+ /* @__PURE__ */ jsx("span", {
149
+ className: "ue-ins__muted",
150
+ children: name
151
+ }),
152
+ crown ? /* @__PURE__ */ jsxs("span", {
153
+ className: "ue-ins__crown",
154
+ "data-testid": "ue-crown",
155
+ children: ["♔ ", crown === selfId ? "this tab" : short(crown)]
156
+ }) : null
157
+ ]
158
+ }), open ? /* @__PURE__ */ jsxs("div", {
159
+ className: "ue-ins__body",
160
+ children: [
161
+ /* @__PURE__ */ jsxs("div", {
162
+ className: "ue-ins__section",
163
+ children: [/* @__PURE__ */ jsx("div", {
164
+ className: "ue-ins__h",
165
+ children: "This tab"
166
+ }), /* @__PURE__ */ jsxs("div", {
167
+ className: "ue-ins__row",
168
+ children: [/* @__PURE__ */ jsx("span", {
169
+ className: "ue-ins__k",
170
+ children: short(selfId)
171
+ }), /* @__PURE__ */ jsx("span", {
172
+ className: "ue-ins__v",
173
+ children: crown === selfId ? "leader" : crown ? "follower" : "no leader"
174
+ })]
175
+ })]
176
+ }),
177
+ /* @__PURE__ */ jsxs("div", {
178
+ className: "ue-ins__section",
179
+ children: [/* @__PURE__ */ jsxs("div", {
180
+ className: "ue-ins__h",
181
+ children: [
182
+ "Peers (",
183
+ peers.length,
184
+ ")"
185
+ ]
186
+ }), peers.length === 0 ? /* @__PURE__ */ jsx("div", {
187
+ className: "ue-ins__empty",
188
+ children: "nobody else here"
189
+ }) : peers.map((peer) => /* @__PURE__ */ jsxs("div", {
190
+ className: "ue-ins__row",
191
+ children: [/* @__PURE__ */ jsx("span", {
192
+ className: "ue-ins__k",
193
+ children: short(peer.id)
194
+ }), /* @__PURE__ */ jsx("span", {
195
+ className: "ue-ins__v",
196
+ children: peer.kind
197
+ })]
198
+ }, peer.id))]
199
+ }),
200
+ /* @__PURE__ */ jsxs("div", {
201
+ className: "ue-ins__section",
202
+ children: [/* @__PURE__ */ jsxs("div", {
203
+ className: "ue-ins__h",
204
+ children: [
205
+ "State (",
206
+ entries.length,
207
+ ")"
208
+ ]
209
+ }), entries.length === 0 ? /* @__PURE__ */ jsx("div", {
210
+ className: "ue-ins__empty",
211
+ children: "no keys yet"
212
+ }) : entries.map(([key, version]) => /* @__PURE__ */ jsxs("div", {
213
+ className: "ue-ins__row",
214
+ children: [
215
+ /* @__PURE__ */ jsx("span", {
216
+ className: "ue-ins__k",
217
+ children: key
218
+ }),
219
+ editing?.key === key ? /* @__PURE__ */ jsx("input", {
220
+ className: `ue-ins__edit${draftIsValid(editing.draft) ? "" : " ue-ins__v--invalid"}`,
221
+ value: editing.draft,
222
+ autoFocus: true,
223
+ "aria-label": `Value of ${key}`,
224
+ onChange: (event) => setEditing({
225
+ key,
226
+ draft: event.target.value
227
+ }),
228
+ onKeyDown: (event) => {
229
+ if (event.key === "Enter") commit(key, editing.draft);
230
+ if (event.key === "Escape") setEditing(null);
231
+ },
232
+ onBlur: () => setEditing(null),
233
+ "data-testid": `ue-edit-${key}`
234
+ }) : /* @__PURE__ */ jsx("button", {
235
+ type: "button",
236
+ className: "ue-ins__v ue-ins__v--editable",
237
+ onClick: () => setEditing({
238
+ key,
239
+ draft: JSON.stringify(snapshot[key]) ?? ""
240
+ }),
241
+ "data-testid": `ue-value-${key}`,
242
+ children: JSON.stringify(snapshot[key])
243
+ }),
244
+ /* @__PURE__ */ jsxs("span", {
245
+ className: "ue-ins__ver",
246
+ children: [
247
+ version[0],
248
+ "·",
249
+ short(version[1])
250
+ ]
251
+ })
252
+ ]
253
+ }, key))]
254
+ }),
255
+ /* @__PURE__ */ jsxs("div", {
256
+ className: "ue-ins__section",
257
+ children: [
258
+ /* @__PURE__ */ jsxs("div", {
259
+ className: "ue-ins__h",
260
+ children: [
261
+ "Wires (",
262
+ shown.length,
263
+ shown.length === wires.length ? "" : ` of ${wires.length}`,
264
+ ")",
265
+ paused ? /* @__PURE__ */ jsx("span", {
266
+ className: "ue-ins__paused",
267
+ children: " · paused"
268
+ }) : null
269
+ ]
270
+ }),
271
+ /* @__PURE__ */ jsxs("div", {
272
+ className: "ue-ins__tools",
273
+ children: [
274
+ /* @__PURE__ */ jsx("button", {
275
+ type: "button",
276
+ className: "ue-ins__btn",
277
+ "aria-pressed": paused,
278
+ onClick: () => setPaused((value) => !value),
279
+ "data-testid": "ue-pause",
280
+ children: paused ? "resume" : "pause"
281
+ }),
282
+ /* @__PURE__ */ jsx("button", {
283
+ type: "button",
284
+ className: "ue-ins__btn",
285
+ onClick: () => {
286
+ setWires([]);
287
+ setFrames([]);
288
+ },
289
+ "data-testid": "ue-clear",
290
+ children: "clear"
291
+ }),
292
+ /* @__PURE__ */ jsx("input", {
293
+ className: "ue-ins__filter",
294
+ value: filter,
295
+ placeholder: "filter",
296
+ "aria-label": "Filter wires",
297
+ onChange: (event) => setFilter(event.target.value),
298
+ "data-testid": "ue-filter"
299
+ })
300
+ ]
301
+ }),
302
+ /* @__PURE__ */ jsx("div", {
303
+ className: "ue-ins__tools",
304
+ role: "group",
305
+ "aria-label": "Scope",
306
+ children: SCOPES.map((scope) => /* @__PURE__ */ jsx("button", {
307
+ type: "button",
308
+ className: "ue-ins__btn",
309
+ "aria-pressed": scopeView === scope,
310
+ onClick: () => setScopeView(scope),
311
+ "data-testid": `ue-scope-${scope}`,
312
+ children: scope
313
+ }, scope))
314
+ }),
315
+ shown.length === 0 ? /* @__PURE__ */ jsx("div", {
316
+ className: "ue-ins__empty",
317
+ "data-testid": "ue-wires",
318
+ children: wires.length === 0 ? "nothing yet" : "no matches"
319
+ }) : /* @__PURE__ */ jsx("div", {
320
+ className: "ue-ins__log",
321
+ "data-testid": "ue-wires",
322
+ children: shown.map((wire) => /* @__PURE__ */ jsxs("div", {
323
+ className: "ue-ins__wire",
324
+ children: [
325
+ /* @__PURE__ */ jsx("span", {
326
+ className: `ue-ins__dir ue-ins__dir--${wire.direction}`,
327
+ children: wire.direction === "out" ? "→" : "←"
328
+ }),
329
+ /* @__PURE__ */ jsx("span", {
330
+ className: "ue-ins__scope",
331
+ children: wire.label
332
+ }),
333
+ /* @__PURE__ */ jsx("span", {
334
+ className: "ue-ins__from",
335
+ children: wire.from
336
+ })
337
+ ]
338
+ }, wire.id))
339
+ })
340
+ ]
341
+ }),
342
+ /* @__PURE__ */ jsxs("div", {
343
+ className: "ue-ins__section",
344
+ children: [/* @__PURE__ */ jsxs("div", {
345
+ className: "ue-ins__h",
346
+ children: [
347
+ "Timeline (",
348
+ frames.length,
349
+ ")"
350
+ ]
351
+ }), frames.length === 0 ? /* @__PURE__ */ jsx("div", {
352
+ className: "ue-ins__empty",
353
+ children: "no state on the wire yet"
354
+ }) : /* @__PURE__ */ jsx("div", {
355
+ className: "ue-ins__log",
356
+ children: frames.map((frame) => /* @__PURE__ */ jsxs("div", {
357
+ className: "ue-ins__wire",
358
+ children: [
359
+ /* @__PURE__ */ jsx("button", {
360
+ type: "button",
361
+ className: "ue-ins__btn",
362
+ onClick: () => restore(frame),
363
+ "data-testid": `ue-restore-${String(frame.wireId)}`,
364
+ children: "restore"
365
+ }),
366
+ /* @__PURE__ */ jsx("span", {
367
+ className: "ue-ins__scope",
368
+ children: frame.label
369
+ }),
370
+ /* @__PURE__ */ jsxs("span", {
371
+ className: "ue-ins__from",
372
+ children: [
373
+ Object.keys(frame.snapshot).length,
374
+ " key",
375
+ Object.keys(frame.snapshot).length === 1 ? "" : "s"
376
+ ]
377
+ })
378
+ ]
379
+ }, frame.wireId))
380
+ })]
381
+ })
382
+ ]
383
+ }) : null]
384
+ });
295
385
  }
296
-
297
- // src/devtools/styles.ts
298
- var STYLES = `
386
+ //#endregion
387
+ //#region src/devtools/styles.ts
388
+ /**
389
+ * Shipped as a <style> element inside the panel rather than a CSS file: no
390
+ * import for consumers to wire up, no document.head mutation, and it renders
391
+ * fine on the server. Every rule is scoped under .ue-ins.
392
+ */
393
+ const STYLES = `
299
394
  .ue-ins {
300
395
  position: fixed;
301
396
  z-index: 2147483000;
@@ -398,38 +493,50 @@ button.ue-ins__v {
398
493
  .ue-ins__v--invalid { color: #f85149; }
399
494
  .ue-ins__paused { color: #d29922; }
400
495
  `;
401
-
402
- // src/devtools/inspector.tsx
403
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
404
- function Inspector({
405
- name = DEFAULT_NAME,
406
- position = "bottom-right",
407
- limit = 50,
408
- defaultOpen = false,
409
- leaseMs = 3e3
410
- } = {}) {
411
- const [root, setRoot] = useState2(null);
412
- const attach = (host) => {
413
- if (!host || root) return;
414
- setRoot(host.shadowRoot ?? host.attachShadow({ mode: "open" }));
415
- };
416
- return /* @__PURE__ */ jsx2("div", { ref: attach, "data-testid": "ue-inspector-host", children: root ? createPortal(
417
- /* @__PURE__ */ jsxs2(Fragment, { children: [
418
- /* @__PURE__ */ jsx2("style", { children: STYLES }),
419
- /* @__PURE__ */ jsx2(
420
- Panel,
421
- {
422
- name,
423
- position,
424
- limit,
425
- defaultOpen,
426
- leaseMs
427
- }
428
- )
429
- ] }),
430
- root
431
- ) : null });
496
+ //#endregion
497
+ //#region src/devtools/inspector.tsx
498
+ /**
499
+ * A floating panel showing what this tab is saying and hearing on the bus:
500
+ * peers, the leader, store keys with their version clocks, a live wire log in
501
+ * both directions, and a timeline of the state each wire produced.
502
+ *
503
+ * It deliberately does **not** create a Leader. Under dynamic eligibility,
504
+ * mounting one with `eligible: false` would disable candidacy for the whole
505
+ * tab, and mounting a plain one would enrol a tab that never asked to be a
506
+ * candidate — a devtool must not change what it measures. Instead it reads the
507
+ * crown out of the wire log, which it already sees in both directions.
508
+ *
509
+ * Presence is fine to use: the bus heartbeats regardless of whether anything
510
+ * created a Presence, so usePeers observes rather than perturbs.
511
+ *
512
+ * **The panel lives in a shadow root.** Its styles used to be a `<style>` tag
513
+ * in the page, which is only ever half true: `.ue-ins` could not leak out, but
514
+ * everything in the host document leaked *in* — a `* { box-sizing }` reset, an
515
+ * app-wide `button { text-transform: uppercase }`, a Tailwind preflight, any
516
+ * `!important`. A devtool that renders differently depending on whose app it is
517
+ * mounted in is a devtool you cannot trust when it looks wrong. Inside a shadow
518
+ * root, page CSS does not apply and the panel's own CSS cannot escape.
519
+ *
520
+ * A consequence worth knowing when testing an app that mounts it: the panel is
521
+ * **not** in `document`. Reach it through `host.shadowRoot`.
522
+ */
523
+ function Inspector({ name = DEFAULT_NAME, position = "bottom-right", limit = 50, defaultOpen = false, leaseMs = 3e3 } = {}) {
524
+ const [root, setRoot] = useState(null);
525
+ const attach = (host) => {
526
+ if (!host || root) return;
527
+ setRoot(host.shadowRoot ?? host.attachShadow({ mode: "open" }));
528
+ };
529
+ return /* @__PURE__ */ jsx("div", {
530
+ ref: attach,
531
+ "data-testid": "ue-inspector-host",
532
+ children: root ? createPortal(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("style", { children: STYLES }), /* @__PURE__ */ jsx(Panel, {
533
+ name,
534
+ position,
535
+ limit,
536
+ defaultOpen,
537
+ leaseMs
538
+ })] }), root) : null
539
+ });
432
540
  }
433
- export {
434
- Inspector
435
- };
541
+ //#endregion
542
+ export { Inspector };