@atom.io/spa-devtools 0.0.0-stage → 0.0.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/LICENSE +373 -0
- package/README.md +51 -2
- package/README.mdx +35 -0
- package/dist/css.d.ts +2 -0
- package/dist/devtools.css +289 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +904 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -5
- package/src/AtomIODevtools.tsx +194 -0
- package/src/Indexes.tsx +331 -0
- package/src/JsonEditor.tsx +259 -0
- package/src/css.d.ts +2 -0
- package/src/devtools.css +289 -0
- package/src/index.ts +2 -0
- package/src/store.ts +148 -0
- package/src/value.ts +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import { jsx } from "@atom.io/spa/jsx-runtime";
|
|
2
|
+
import { onCleanup, onMount, render, useId, useSetter, useTimeline, useValue } from "@atom.io/spa";
|
|
3
|
+
import { IMPLICIT, createRegularAtomFamily, disposeFromStore, findInStore, isReservedIntrospectionKey, setIntoStore } from "atom.io/internal";
|
|
4
|
+
import { attachIntrospectionStates } from "atom.io/introspection";
|
|
5
|
+
// src/store.ts
|
|
6
|
+
const locals = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
const introspection = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
function ownedAtom(store, initial) {
|
|
9
|
+
let family = locals.get(store);
|
|
10
|
+
if (!family) {
|
|
11
|
+
family = createRegularAtomFamily(store, {
|
|
12
|
+
key: `🔍 spa devtools`,
|
|
13
|
+
default: void 0
|
|
14
|
+
});
|
|
15
|
+
locals.set(store, family);
|
|
16
|
+
}
|
|
17
|
+
const token = findInStore(store, family, useId());
|
|
18
|
+
setIntoStore(store, token, initial);
|
|
19
|
+
onCleanup(() => {
|
|
20
|
+
disposeFromStore(store, token);
|
|
21
|
+
});
|
|
22
|
+
return token;
|
|
23
|
+
}
|
|
24
|
+
function useLocal(store, initial) {
|
|
25
|
+
const token = ownedAtom(store, initial);
|
|
26
|
+
return [useValue(token), useSetter(token)];
|
|
27
|
+
}
|
|
28
|
+
function useSaved(store, key, fallback) {
|
|
29
|
+
let initial = fallback;
|
|
30
|
+
try {
|
|
31
|
+
const saved = JSON.parse(localStorage.getItem(key) ?? `null`);
|
|
32
|
+
if (saved !== null && typeof saved === typeof fallback) initial = saved;
|
|
33
|
+
} catch {}
|
|
34
|
+
const [value, set] = useLocal(store, initial);
|
|
35
|
+
return [value, (next) => {
|
|
36
|
+
set(next);
|
|
37
|
+
try {
|
|
38
|
+
localStorage.setItem(key, JSON.stringify(value()));
|
|
39
|
+
} catch {}
|
|
40
|
+
}];
|
|
41
|
+
}
|
|
42
|
+
function useTools(store) {
|
|
43
|
+
let states = introspection.get(store);
|
|
44
|
+
if (!states) {
|
|
45
|
+
states = attachIntrospectionStates(store);
|
|
46
|
+
introspection.set(store, states);
|
|
47
|
+
}
|
|
48
|
+
let expanded = {};
|
|
49
|
+
try {
|
|
50
|
+
const saved = JSON.parse(localStorage.getItem(`🔍 spa expanded`) ?? `{}`);
|
|
51
|
+
if (saved && typeof saved === `object` && !Array.isArray(saved) && Object.values(saved).every((value) => typeof value === `boolean`)) expanded = saved;
|
|
52
|
+
} catch {}
|
|
53
|
+
const error = ownedAtom(store, ``);
|
|
54
|
+
const setError = useSetter(error);
|
|
55
|
+
return {
|
|
56
|
+
...states,
|
|
57
|
+
store,
|
|
58
|
+
error,
|
|
59
|
+
expanded: ownedAtom(store, expanded),
|
|
60
|
+
attempt(run) {
|
|
61
|
+
try {
|
|
62
|
+
run();
|
|
63
|
+
setError(``);
|
|
64
|
+
} catch (caught) {
|
|
65
|
+
setError(caught instanceof Error ? caught.message : String(caught));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function useExpanded(tools, path, initial = false) {
|
|
71
|
+
const value = useValue(tools.expanded);
|
|
72
|
+
const set = useSetter(tools.expanded);
|
|
73
|
+
const key = JSON.stringify(path);
|
|
74
|
+
const group = `all:${JSON.stringify(path.slice(0, -1))}`;
|
|
75
|
+
const read = () => value()[key] ?? value()[group] ?? initial;
|
|
76
|
+
return [read, (event) => {
|
|
77
|
+
const next = !read();
|
|
78
|
+
set((previous) => {
|
|
79
|
+
const result = { ...previous };
|
|
80
|
+
if (event.shiftKey) {
|
|
81
|
+
const parent = JSON.stringify(path.slice(0, -1));
|
|
82
|
+
for (const item of Object.keys(result)) if (item.startsWith(`[`)) {
|
|
83
|
+
const candidate = JSON.parse(item);
|
|
84
|
+
if (JSON.stringify(candidate.slice(0, -1)) === parent) Reflect.deleteProperty(result, item);
|
|
85
|
+
}
|
|
86
|
+
result[group] = next;
|
|
87
|
+
} else result[key] = next;
|
|
88
|
+
try {
|
|
89
|
+
localStorage.setItem(`🔍 spa expanded`, JSON.stringify(result));
|
|
90
|
+
} catch {}
|
|
91
|
+
return result;
|
|
92
|
+
});
|
|
93
|
+
}];
|
|
94
|
+
}
|
|
95
|
+
// src/value.ts
|
|
96
|
+
function kind(value) {
|
|
97
|
+
if (value === null) return `null`;
|
|
98
|
+
if (Array.isArray(value)) return `array`;
|
|
99
|
+
if (typeof value === `object` && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) return `object`;
|
|
100
|
+
if (typeof value === `string` || typeof value === `number` || typeof value === `boolean`) return typeof value;
|
|
101
|
+
return `other`;
|
|
102
|
+
}
|
|
103
|
+
function describe(value) {
|
|
104
|
+
try {
|
|
105
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
106
|
+
return JSON.stringify(value, (_, item) => {
|
|
107
|
+
if (typeof item === `bigint`) return `${item}n`;
|
|
108
|
+
if (item instanceof Error) return `${item.name}: ${item.message}`;
|
|
109
|
+
if (item instanceof Map) return { Map: [...item] };
|
|
110
|
+
if (item instanceof Set) return { Set: [...item] };
|
|
111
|
+
if (typeof item === `object` && item !== null) {
|
|
112
|
+
if (seen.has(item)) return `[Circular]`;
|
|
113
|
+
seen.add(item);
|
|
114
|
+
}
|
|
115
|
+
return item;
|
|
116
|
+
}) ?? String(value);
|
|
117
|
+
} catch {
|
|
118
|
+
return Object.prototype.toString.call(value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function convert(value, type) {
|
|
122
|
+
switch (type) {
|
|
123
|
+
case `null`: return null;
|
|
124
|
+
case `string`: return value == null ? `` : typeof value === `string` ? value : describe(value);
|
|
125
|
+
case `number`: {
|
|
126
|
+
const next = Array.isArray(value) ? value.length : Number(value);
|
|
127
|
+
return Number.isFinite(next) ? next : 0;
|
|
128
|
+
}
|
|
129
|
+
case `boolean`: return typeof value === `string` ? value === `true` : Boolean(value);
|
|
130
|
+
case `array`: return Array.isArray(value) ? value : value == null ? [] : typeof value === `object` ? Object.entries(value) : [value];
|
|
131
|
+
case `object`: return kind(value) === `object` ? value : Array.isArray(value) ? Object.fromEntries(value.map((item, index) => [index, item])) : value == null ? {} : { value };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// src/JsonEditor.tsx
|
|
135
|
+
const TYPES = [
|
|
136
|
+
`string`,
|
|
137
|
+
`number`,
|
|
138
|
+
`boolean`,
|
|
139
|
+
`null`,
|
|
140
|
+
`array`,
|
|
141
|
+
`object`
|
|
142
|
+
];
|
|
143
|
+
function JsonEditor(props) {
|
|
144
|
+
const [expanded, toggle] = useExpanded(props.tools, props.path);
|
|
145
|
+
const circular = () => (props.ancestors?.includes(props.value) ?? false) || props.path.length > 50;
|
|
146
|
+
const container = () => !circular() && (kind(props.value) === `object` || kind(props.value) === `array`);
|
|
147
|
+
const entries = () => Object.entries(props.value);
|
|
148
|
+
return jsx("section", {
|
|
149
|
+
"class": "json-editor",
|
|
150
|
+
get "data-testid"() {
|
|
151
|
+
return `${props.path.join(`.`)}-state-editor`;
|
|
152
|
+
},
|
|
153
|
+
children: [jsx("header", { children: [
|
|
154
|
+
() => container() ? jsx("button", {
|
|
155
|
+
"type": "button",
|
|
156
|
+
get "aria-label"() {
|
|
157
|
+
return `Expand ${props.path.at(-1)}`;
|
|
158
|
+
},
|
|
159
|
+
get "aria-expanded"() {
|
|
160
|
+
return expanded();
|
|
161
|
+
},
|
|
162
|
+
get "onClick"() {
|
|
163
|
+
return toggle;
|
|
164
|
+
},
|
|
165
|
+
children: () => expanded() ? `▾` : `▸`
|
|
166
|
+
}) : jsx("span", { "class": "carat-spacer" }),
|
|
167
|
+
jsx("span", {
|
|
168
|
+
"class": "field-name",
|
|
169
|
+
children: () => props.label
|
|
170
|
+
}),
|
|
171
|
+
() => props.change ? jsx("select", {
|
|
172
|
+
get "aria-label"() {
|
|
173
|
+
return `Type of ${props.path.at(-1)}`;
|
|
174
|
+
},
|
|
175
|
+
get "value"() {
|
|
176
|
+
return kind(props.value);
|
|
177
|
+
},
|
|
178
|
+
"onChange": (event) => {
|
|
179
|
+
const type = event.currentTarget.value;
|
|
180
|
+
props.tools.attempt(() => {
|
|
181
|
+
props.change?.((previous) => convert(previous, type));
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
children: [jsx("option", {
|
|
185
|
+
"value": "other",
|
|
186
|
+
"disabled": true,
|
|
187
|
+
children: "other"
|
|
188
|
+
}), () => TYPES.map((type) => jsx("option", {
|
|
189
|
+
get "value"() {
|
|
190
|
+
return type;
|
|
191
|
+
},
|
|
192
|
+
children: () => type
|
|
193
|
+
}))]
|
|
194
|
+
}) : jsx("small", { children: () => kind(props.value) }),
|
|
195
|
+
() => container() ? jsx("code", {
|
|
196
|
+
get "title"() {
|
|
197
|
+
return describe(props.value);
|
|
198
|
+
},
|
|
199
|
+
children: () => describe(props.value)
|
|
200
|
+
}) : circular() ? jsx("code", { children: "[Circular]" }) : jsx(Primitive, {
|
|
201
|
+
get "value"() {
|
|
202
|
+
return props.value;
|
|
203
|
+
},
|
|
204
|
+
get "change"() {
|
|
205
|
+
return props.change;
|
|
206
|
+
},
|
|
207
|
+
get "tools"() {
|
|
208
|
+
return props.tools;
|
|
209
|
+
},
|
|
210
|
+
get "label"() {
|
|
211
|
+
return props.path.at(-1) ?? `value`;
|
|
212
|
+
}
|
|
213
|
+
}),
|
|
214
|
+
() => props.actions
|
|
215
|
+
] }), () => container() && expanded() ? jsx("section", {
|
|
216
|
+
"class": "properties",
|
|
217
|
+
children: [() => entries().map(([key]) => jsx(Property, {
|
|
218
|
+
"key": key,
|
|
219
|
+
get "name"() {
|
|
220
|
+
return key;
|
|
221
|
+
},
|
|
222
|
+
get "editor"() {
|
|
223
|
+
return props;
|
|
224
|
+
}
|
|
225
|
+
})), () => props.change ? jsx("footer", { children: [jsx("button", {
|
|
226
|
+
"type": "button",
|
|
227
|
+
"onClick": () => {
|
|
228
|
+
props.tools.attempt(() => {
|
|
229
|
+
props.change?.((previous) => {
|
|
230
|
+
if (Array.isArray(previous)) return [...previous, null];
|
|
231
|
+
const object = previous;
|
|
232
|
+
let name = `new`;
|
|
233
|
+
while (Object.hasOwn(object, name)) name += `_`;
|
|
234
|
+
return {
|
|
235
|
+
...object,
|
|
236
|
+
[name]: null
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
children: ["add ", () => Array.isArray(props.value) ? `item` : `property`]
|
|
242
|
+
}), () => Array.isArray(props.value) ? jsx("button", {
|
|
243
|
+
"type": "button",
|
|
244
|
+
"onClick": () => {
|
|
245
|
+
props.tools.attempt(() => {
|
|
246
|
+
props.change?.((previous) => [...previous].sort((a, b) => typeof a === `number` && typeof b === `number` ? a - b : describe(a).localeCompare(describe(b))));
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
children: "sort"
|
|
250
|
+
}) : null] }) : null]
|
|
251
|
+
}) : null]
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function Primitive(props) {
|
|
255
|
+
const set = (value) => {
|
|
256
|
+
props.tools.attempt(() => {
|
|
257
|
+
props.change?.(() => value);
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
return jsx("span", {
|
|
261
|
+
"class": "primitive",
|
|
262
|
+
children: () => kind(props.value) === `boolean` ? jsx("input", {
|
|
263
|
+
get "aria-label"() {
|
|
264
|
+
return props.label;
|
|
265
|
+
},
|
|
266
|
+
"type": "checkbox",
|
|
267
|
+
get "checked"() {
|
|
268
|
+
return Boolean(props.value);
|
|
269
|
+
},
|
|
270
|
+
get "disabled"() {
|
|
271
|
+
return !props.change;
|
|
272
|
+
},
|
|
273
|
+
"onChange": (event) => {
|
|
274
|
+
set(event.currentTarget.checked);
|
|
275
|
+
}
|
|
276
|
+
}) : kind(props.value) === `string` || kind(props.value) === `number` ? jsx("input", {
|
|
277
|
+
get "aria-label"() {
|
|
278
|
+
return props.label;
|
|
279
|
+
},
|
|
280
|
+
"type": "text",
|
|
281
|
+
get "inputmode"() {
|
|
282
|
+
return kind(props.value) === `number` ? `decimal` : `text`;
|
|
283
|
+
},
|
|
284
|
+
get "value"() {
|
|
285
|
+
return String(props.value);
|
|
286
|
+
},
|
|
287
|
+
get "readOnly"() {
|
|
288
|
+
return !props.change;
|
|
289
|
+
},
|
|
290
|
+
"onInput": (event) => {
|
|
291
|
+
const text = event.currentTarget.value;
|
|
292
|
+
if (kind(props.value) === `string`) set(text);
|
|
293
|
+
else if (text.trim() && Number.isFinite(Number(text))) set(Number(text));
|
|
294
|
+
},
|
|
295
|
+
"onBlur": (event) => {
|
|
296
|
+
event.currentTarget.value = String(props.value);
|
|
297
|
+
}
|
|
298
|
+
}) : jsx("code", { children: () => describe(props.value) })
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function Property(props) {
|
|
302
|
+
const parent = props.editor;
|
|
303
|
+
const read = () => parent.value[props.name];
|
|
304
|
+
const change = (update) => {
|
|
305
|
+
parent.change?.((previous) => {
|
|
306
|
+
if (Array.isArray(previous)) return previous.map((value, index) => index === Number(props.name) ? update(value) : value);
|
|
307
|
+
const object = previous;
|
|
308
|
+
return {
|
|
309
|
+
...object,
|
|
310
|
+
[props.name]: update(object[props.name])
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
};
|
|
314
|
+
return jsx("section", {
|
|
315
|
+
"class": "property",
|
|
316
|
+
children: jsx(JsonEditor, {
|
|
317
|
+
get "label"() {
|
|
318
|
+
return parent.change && !Array.isArray(parent.value) ? jsx("input", {
|
|
319
|
+
get "aria-label"() {
|
|
320
|
+
return `Rename ${props.name}`;
|
|
321
|
+
},
|
|
322
|
+
get "style"() {
|
|
323
|
+
return `width:calc(${Math.max(1, props.name.length)}ch + 6px)`;
|
|
324
|
+
},
|
|
325
|
+
get "value"() {
|
|
326
|
+
return props.name;
|
|
327
|
+
},
|
|
328
|
+
"onChange": (event) => {
|
|
329
|
+
const input = event.currentTarget;
|
|
330
|
+
const name = input.value;
|
|
331
|
+
parent.tools.attempt(() => {
|
|
332
|
+
parent.change?.((previous) => {
|
|
333
|
+
const object = previous;
|
|
334
|
+
if (name !== props.name && Object.hasOwn(object, name)) throw new Error(`Property "${name}" already exists`);
|
|
335
|
+
return Object.fromEntries(Object.entries(object).map(([key, value]) => [key === props.name ? name : key, value]));
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
input.value = props.name;
|
|
339
|
+
}
|
|
340
|
+
}) : jsx("strong", { children: () => props.name });
|
|
341
|
+
},
|
|
342
|
+
get "actions"() {
|
|
343
|
+
return parent.change ? jsx("button", {
|
|
344
|
+
"type": "button",
|
|
345
|
+
get "aria-label"() {
|
|
346
|
+
return `Delete ${props.name}`;
|
|
347
|
+
},
|
|
348
|
+
"onClick": () => {
|
|
349
|
+
parent.tools.attempt(() => {
|
|
350
|
+
parent.change?.((previous) => Array.isArray(previous) ? previous.filter((_, index) => index !== Number(props.name)) : Object.fromEntries(Object.entries(previous).filter(([key]) => key !== props.name)));
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
children: "×"
|
|
354
|
+
}) : null;
|
|
355
|
+
},
|
|
356
|
+
get "tools"() {
|
|
357
|
+
return parent.tools;
|
|
358
|
+
},
|
|
359
|
+
get "path"() {
|
|
360
|
+
return [...parent.path, props.name];
|
|
361
|
+
},
|
|
362
|
+
get "value"() {
|
|
363
|
+
return read();
|
|
364
|
+
},
|
|
365
|
+
get "change"() {
|
|
366
|
+
return parent.change ? change : void 0;
|
|
367
|
+
},
|
|
368
|
+
get "ancestors"() {
|
|
369
|
+
return [...parent.ancestors ?? [], parent.value];
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
JsonEditor.spa = true;
|
|
375
|
+
Primitive.spa = true;
|
|
376
|
+
Property.spa = true;
|
|
377
|
+
// src/Indexes.tsx
|
|
378
|
+
function StateIndex(props) {
|
|
379
|
+
const index = useValue(props.type === `atoms` ? props.tools.atomIndex : props.tools.selectorIndex);
|
|
380
|
+
return jsx("article", {
|
|
381
|
+
"class": "index",
|
|
382
|
+
"data-testid": "state-index",
|
|
383
|
+
children: () => index().size === 0 ? jsx("p", { children: [
|
|
384
|
+
"(no ",
|
|
385
|
+
() => props.type,
|
|
386
|
+
")"
|
|
387
|
+
] }) : [...index().values()].map((node) => jsx(StateNode, {
|
|
388
|
+
"key": node.key,
|
|
389
|
+
get "tools"() {
|
|
390
|
+
return props.tools;
|
|
391
|
+
},
|
|
392
|
+
get "node"() {
|
|
393
|
+
return node;
|
|
394
|
+
}
|
|
395
|
+
}))
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function StateNode(props) {
|
|
399
|
+
const [open, toggle] = useExpanded(props.tools, [props.node.key]);
|
|
400
|
+
return jsx("section", {
|
|
401
|
+
"class": "node",
|
|
402
|
+
get "data-testid"() {
|
|
403
|
+
return `state-${props.node.key}`;
|
|
404
|
+
},
|
|
405
|
+
children: () => `familyMembers` in props.node ? jsx(null, { children: [jsx("header", { children: [
|
|
406
|
+
jsx("button", {
|
|
407
|
+
"type": "button",
|
|
408
|
+
get "aria-expanded"() {
|
|
409
|
+
return open();
|
|
410
|
+
},
|
|
411
|
+
get "data-testid"() {
|
|
412
|
+
return `open-close-state-family-${props.node.key}`;
|
|
413
|
+
},
|
|
414
|
+
get "onClick"() {
|
|
415
|
+
return toggle;
|
|
416
|
+
},
|
|
417
|
+
children: () => open() ? `▾` : `▸`
|
|
418
|
+
}),
|
|
419
|
+
jsx("h2", { children: () => props.node.key }),
|
|
420
|
+
jsx("small", { children: "family" })
|
|
421
|
+
] }), () => open() ? [...props.node.familyMembers.values()].map((token) => jsx(StateLeaf, {
|
|
422
|
+
"key": token.key,
|
|
423
|
+
get "tools"() {
|
|
424
|
+
return props.tools;
|
|
425
|
+
},
|
|
426
|
+
get "token"() {
|
|
427
|
+
return token;
|
|
428
|
+
}
|
|
429
|
+
})) : null] }) : jsx(StateLeaf, {
|
|
430
|
+
get "tools"() {
|
|
431
|
+
return props.tools;
|
|
432
|
+
},
|
|
433
|
+
get "token"() {
|
|
434
|
+
return props.node;
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
function StateLeaf(props) {
|
|
440
|
+
const read = useValue(props.token);
|
|
441
|
+
const set = !props.token.type.startsWith(`readonly`) ? useSetter(props.token) : void 0;
|
|
442
|
+
const value = () => {
|
|
443
|
+
try {
|
|
444
|
+
return read();
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return error;
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
return jsx("section", {
|
|
450
|
+
"class": "state-leaf",
|
|
451
|
+
get "data-testid"() {
|
|
452
|
+
return `state-value-${props.token.key}`;
|
|
453
|
+
},
|
|
454
|
+
children: jsx(JsonEditor, {
|
|
455
|
+
get "label"() {
|
|
456
|
+
return jsx("h3", { children: () => props.token.family?.subKey ?? props.token.key });
|
|
457
|
+
},
|
|
458
|
+
get "actions"() {
|
|
459
|
+
return props.token.family ? jsx("button", {
|
|
460
|
+
"type": "button",
|
|
461
|
+
get "aria-label"() {
|
|
462
|
+
return `Dispose ${props.token.key}`;
|
|
463
|
+
},
|
|
464
|
+
"onClick": () => {
|
|
465
|
+
props.tools.attempt(() => {
|
|
466
|
+
disposeFromStore(props.tools.store, props.token);
|
|
467
|
+
});
|
|
468
|
+
},
|
|
469
|
+
children: "×"
|
|
470
|
+
}) : null;
|
|
471
|
+
},
|
|
472
|
+
get "tools"() {
|
|
473
|
+
return props.tools;
|
|
474
|
+
},
|
|
475
|
+
get "value"() {
|
|
476
|
+
return value();
|
|
477
|
+
},
|
|
478
|
+
get "change"() {
|
|
479
|
+
return set ? (update) => {
|
|
480
|
+
const next = update(read());
|
|
481
|
+
set(() => next);
|
|
482
|
+
} : void 0;
|
|
483
|
+
},
|
|
484
|
+
get "path"() {
|
|
485
|
+
return props.token.family ? [props.token.family.key, props.token.family.subKey] : [props.token.key];
|
|
486
|
+
}
|
|
487
|
+
})
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
function TransactionIndex(props) {
|
|
491
|
+
const index = useValue(props.tools.transactionIndex);
|
|
492
|
+
const visible = () => index().filter((token) => !isReservedIntrospectionKey(token.key));
|
|
493
|
+
return jsx("article", {
|
|
494
|
+
"class": "index",
|
|
495
|
+
"data-testid": "transaction-index",
|
|
496
|
+
children: () => visible().length === 0 ? jsx("p", { children: "(no transactions)" }) : visible().map((token) => jsx(TransactionLog, {
|
|
497
|
+
"key": token.key,
|
|
498
|
+
get "tools"() {
|
|
499
|
+
return props.tools;
|
|
500
|
+
},
|
|
501
|
+
get "token"() {
|
|
502
|
+
return token;
|
|
503
|
+
}
|
|
504
|
+
}))
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
function TransactionLog(props) {
|
|
508
|
+
const log = useValue(findInStore(props.tools.store, props.tools.transactionLogSelectors, props.token.key));
|
|
509
|
+
const [open, toggle] = useExpanded(props.tools, [props.token.key]);
|
|
510
|
+
return jsx("section", {
|
|
511
|
+
"class": "node",
|
|
512
|
+
get "data-testid"() {
|
|
513
|
+
return `transaction-${props.token.key}`;
|
|
514
|
+
},
|
|
515
|
+
children: [jsx("header", { children: [
|
|
516
|
+
jsx("button", {
|
|
517
|
+
"type": "button",
|
|
518
|
+
get "aria-expanded"() {
|
|
519
|
+
return open();
|
|
520
|
+
},
|
|
521
|
+
get "data-testid"() {
|
|
522
|
+
return `open-close-transaction-${props.token.key}`;
|
|
523
|
+
},
|
|
524
|
+
get "onClick"() {
|
|
525
|
+
return toggle;
|
|
526
|
+
},
|
|
527
|
+
children: () => open() ? `▾` : `▸`
|
|
528
|
+
}),
|
|
529
|
+
jsx("h2", { children: () => props.token.key }),
|
|
530
|
+
jsx("small", { children: [
|
|
531
|
+
"(",
|
|
532
|
+
() => log().length,
|
|
533
|
+
")"
|
|
534
|
+
] })
|
|
535
|
+
] }), () => open() ? log().map((event, index) => jsx(EventView, {
|
|
536
|
+
"key": index,
|
|
537
|
+
get "tools"() {
|
|
538
|
+
return props.tools;
|
|
539
|
+
},
|
|
540
|
+
get "event"() {
|
|
541
|
+
return event;
|
|
542
|
+
},
|
|
543
|
+
get "index"() {
|
|
544
|
+
return index;
|
|
545
|
+
}
|
|
546
|
+
})) : null]
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
function EventView(props) {
|
|
550
|
+
const [open, toggle] = useExpanded(props.tools, [
|
|
551
|
+
`event`,
|
|
552
|
+
props.event.token?.key ?? ``,
|
|
553
|
+
String(props.index)
|
|
554
|
+
], true);
|
|
555
|
+
return jsx("section", {
|
|
556
|
+
"class": "event",
|
|
557
|
+
get "data-testid"() {
|
|
558
|
+
return `${props.event.type === `transaction_outcome` ? `transaction` : `timeline`}-update-${props.event.token?.key}-${props.index}`;
|
|
559
|
+
},
|
|
560
|
+
children: [jsx("header", { children: [
|
|
561
|
+
jsx("button", {
|
|
562
|
+
"type": "button",
|
|
563
|
+
get "aria-expanded"() {
|
|
564
|
+
return open();
|
|
565
|
+
},
|
|
566
|
+
get "onClick"() {
|
|
567
|
+
return toggle;
|
|
568
|
+
},
|
|
569
|
+
children: () => open() ? `▾` : `▸`
|
|
570
|
+
}),
|
|
571
|
+
jsx("strong", { children: () => props.event.token?.key }),
|
|
572
|
+
jsx("small", { children: () => props.event.type })
|
|
573
|
+
] }), () => open() ? props.event.type === `transaction_outcome` ? jsx(null, { children: [
|
|
574
|
+
jsx("p", { children: ["params: ", jsx("code", { children: () => describe(props.event.params) })] }),
|
|
575
|
+
jsx("p", { children: ["output: ", jsx("code", { children: () => describe(props.event.output) })] }),
|
|
576
|
+
jsx("section", {
|
|
577
|
+
"class": "impact",
|
|
578
|
+
children: () => props.event.subEvents.map((event, index) => jsx(EventView, {
|
|
579
|
+
"key": index,
|
|
580
|
+
get "tools"() {
|
|
581
|
+
return props.tools;
|
|
582
|
+
},
|
|
583
|
+
get "event"() {
|
|
584
|
+
return event;
|
|
585
|
+
},
|
|
586
|
+
get "index"() {
|
|
587
|
+
return index;
|
|
588
|
+
}
|
|
589
|
+
}))
|
|
590
|
+
})
|
|
591
|
+
] }) : props.event.update ? jsx("p", { children: [
|
|
592
|
+
jsx("del", { children: () => describe(props.event.update.oldValue) }),
|
|
593
|
+
" →",
|
|
594
|
+
` `,
|
|
595
|
+
jsx("ins", { children: () => describe(props.event.update.newValue) })
|
|
596
|
+
] }) : jsx("code", { children: () => describe(props.event) }) : null]
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
function TimelineIndex(props) {
|
|
600
|
+
const index = useValue(props.tools.timelineIndex);
|
|
601
|
+
const groups = () => {
|
|
602
|
+
const result = /* @__PURE__ */ new Map();
|
|
603
|
+
for (const token of index()) {
|
|
604
|
+
if (isReservedIntrospectionKey(token.key) || token.key.startsWith(`👁🗨`)) continue;
|
|
605
|
+
const key = token.family?.key ?? ``;
|
|
606
|
+
const members = result.get(key) ?? [];
|
|
607
|
+
members.push(token);
|
|
608
|
+
result.set(key, members);
|
|
609
|
+
}
|
|
610
|
+
return [...result];
|
|
611
|
+
};
|
|
612
|
+
return jsx("article", {
|
|
613
|
+
"class": "index",
|
|
614
|
+
"data-testid": "timeline-index",
|
|
615
|
+
children: () => groups().length === 0 ? jsx("p", { children: "(no timelines)" }) : groups().map(([family, members]) => jsx("section", {
|
|
616
|
+
"key": family,
|
|
617
|
+
get "data-testid"() {
|
|
618
|
+
return family ? `timeline-family-${family}` : void 0;
|
|
619
|
+
},
|
|
620
|
+
children: [() => family ? jsx("h2", { children: () => family }) : null, () => members.map((token) => jsx(TimelineLog, {
|
|
621
|
+
"key": token.key,
|
|
622
|
+
get "tools"() {
|
|
623
|
+
return props.tools;
|
|
624
|
+
},
|
|
625
|
+
get "token"() {
|
|
626
|
+
return token;
|
|
627
|
+
}
|
|
628
|
+
}))]
|
|
629
|
+
}))
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
function TimelineLog(props) {
|
|
633
|
+
const controls = useTimeline(props.token);
|
|
634
|
+
const log = useValue(findInStore(props.tools.store, props.tools.timelineSelectors, props.token.key));
|
|
635
|
+
const [open, toggle] = useExpanded(props.tools, [props.token.key]);
|
|
636
|
+
return jsx("section", {
|
|
637
|
+
"class": "node",
|
|
638
|
+
get "data-testid"() {
|
|
639
|
+
return `timeline-${props.token.key}`;
|
|
640
|
+
},
|
|
641
|
+
children: [jsx("header", { children: [
|
|
642
|
+
jsx("button", {
|
|
643
|
+
"type": "button",
|
|
644
|
+
get "aria-expanded"() {
|
|
645
|
+
return open();
|
|
646
|
+
},
|
|
647
|
+
get "data-testid"() {
|
|
648
|
+
return `open-close-timeline-${props.token.key}`;
|
|
649
|
+
},
|
|
650
|
+
get "onClick"() {
|
|
651
|
+
return toggle;
|
|
652
|
+
},
|
|
653
|
+
children: () => open() ? `▾` : `▸`
|
|
654
|
+
}),
|
|
655
|
+
jsx("h2", { children: () => props.token.family?.subKey ?? props.token.key }),
|
|
656
|
+
jsx("small", { children: [
|
|
657
|
+
"(",
|
|
658
|
+
() => controls().at,
|
|
659
|
+
"/",
|
|
660
|
+
() => controls().length,
|
|
661
|
+
")"
|
|
662
|
+
] }),
|
|
663
|
+
jsx("button", {
|
|
664
|
+
"type": "button",
|
|
665
|
+
get "disabled"() {
|
|
666
|
+
return controls().at === 0;
|
|
667
|
+
},
|
|
668
|
+
"onClick": () => {
|
|
669
|
+
props.tools.attempt(controls().undo);
|
|
670
|
+
},
|
|
671
|
+
children: "undo"
|
|
672
|
+
}),
|
|
673
|
+
jsx("button", {
|
|
674
|
+
"type": "button",
|
|
675
|
+
get "disabled"() {
|
|
676
|
+
return controls().at === controls().length;
|
|
677
|
+
},
|
|
678
|
+
"onClick": () => {
|
|
679
|
+
props.tools.attempt(controls().redo);
|
|
680
|
+
},
|
|
681
|
+
children: "redo"
|
|
682
|
+
})
|
|
683
|
+
] }), () => open() ? jsx("section", { children: [() => log().history.map((event, index) => jsx("section", {
|
|
684
|
+
"key": index,
|
|
685
|
+
children: [() => index === log().at ? jsx("p", {
|
|
686
|
+
"class": "you-are-here",
|
|
687
|
+
children: "you are here"
|
|
688
|
+
}) : null, jsx(EventView, {
|
|
689
|
+
get "tools"() {
|
|
690
|
+
return props.tools;
|
|
691
|
+
},
|
|
692
|
+
get "event"() {
|
|
693
|
+
return event;
|
|
694
|
+
},
|
|
695
|
+
get "index"() {
|
|
696
|
+
return index;
|
|
697
|
+
}
|
|
698
|
+
})]
|
|
699
|
+
})), () => log().at === log().history.length ? jsx("p", {
|
|
700
|
+
"class": "you-are-here",
|
|
701
|
+
children: "you are here"
|
|
702
|
+
}) : null] }) : null]
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
StateIndex.spa = true;
|
|
706
|
+
StateNode.spa = true;
|
|
707
|
+
StateLeaf.spa = true;
|
|
708
|
+
TransactionIndex.spa = true;
|
|
709
|
+
TransactionLog.spa = true;
|
|
710
|
+
EventView.spa = true;
|
|
711
|
+
TimelineIndex.spa = true;
|
|
712
|
+
TimelineLog.spa = true;
|
|
713
|
+
// src/AtomIODevtools.tsx
|
|
714
|
+
/** Owns a rendering root in the inspected store, independently of its parent island. */
|
|
715
|
+
function AtomIODevtools(props) {
|
|
716
|
+
let host = null;
|
|
717
|
+
const attach = (element) => {
|
|
718
|
+
host = element;
|
|
719
|
+
};
|
|
720
|
+
onMount(() => {
|
|
721
|
+
const store = props.store ?? IMPLICIT.STORE;
|
|
722
|
+
const root = render(() => jsx(Panel, {
|
|
723
|
+
get "store"() {
|
|
724
|
+
return store;
|
|
725
|
+
},
|
|
726
|
+
get "hideByDefault"() {
|
|
727
|
+
return props.hideByDefault ?? false;
|
|
728
|
+
}
|
|
729
|
+
}), host, { store });
|
|
730
|
+
onCleanup(root.dispose);
|
|
731
|
+
});
|
|
732
|
+
return jsx("spa-devtools", { get "ref"() {
|
|
733
|
+
return attach;
|
|
734
|
+
} });
|
|
735
|
+
}
|
|
736
|
+
const VIEWS = [
|
|
737
|
+
`atoms`,
|
|
738
|
+
`selectors`,
|
|
739
|
+
`transactions`,
|
|
740
|
+
`timelines`
|
|
741
|
+
];
|
|
742
|
+
function Panel(props) {
|
|
743
|
+
const tools = useTools(props.store);
|
|
744
|
+
const [hidden, setHidden] = useSaved(props.store, `🔍 Devtools Are Hidden`, props.hideByDefault);
|
|
745
|
+
const [open, setOpen] = useSaved(props.store, `🔍 Devtools Are Open`, true);
|
|
746
|
+
const [selection, setSelection] = useSaved(props.store, `🔍 Devtools View`, `atoms`);
|
|
747
|
+
const view = () => VIEWS.includes(selection()) ? selection() : `atoms`;
|
|
748
|
+
const [position, setPosition] = useLocal(props.store, {
|
|
749
|
+
x: 0,
|
|
750
|
+
y: 0
|
|
751
|
+
});
|
|
752
|
+
const error = useValue(tools.error);
|
|
753
|
+
let panel = null;
|
|
754
|
+
const attach = (element) => {
|
|
755
|
+
panel = element;
|
|
756
|
+
};
|
|
757
|
+
let drag;
|
|
758
|
+
onMount(() => {
|
|
759
|
+
const shortcut = (event) => {
|
|
760
|
+
if (event.ctrlKey && event.shiftKey && event.code === `KeyA`) {
|
|
761
|
+
event.preventDefault();
|
|
762
|
+
setHidden((value) => !value);
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
const reset = () => {
|
|
766
|
+
setPosition({
|
|
767
|
+
x: 0,
|
|
768
|
+
y: 0
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
document.addEventListener(`keydown`, shortcut);
|
|
772
|
+
window.addEventListener(`resize`, reset);
|
|
773
|
+
onCleanup(() => {
|
|
774
|
+
document.removeEventListener(`keydown`, shortcut);
|
|
775
|
+
window.removeEventListener(`resize`, reset);
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
return jsx("main", {
|
|
779
|
+
get "ref"() {
|
|
780
|
+
return attach;
|
|
781
|
+
},
|
|
782
|
+
"data-css": "spa-devtools",
|
|
783
|
+
"data-testid": "atom-io-devtools",
|
|
784
|
+
get "hidden"() {
|
|
785
|
+
return hidden();
|
|
786
|
+
},
|
|
787
|
+
get "data-open"() {
|
|
788
|
+
return open();
|
|
789
|
+
},
|
|
790
|
+
get "style"() {
|
|
791
|
+
return `transform:translate(${position().x}px,${position().y}px)`;
|
|
792
|
+
},
|
|
793
|
+
children: [jsx("header", {
|
|
794
|
+
"class": "toolbar",
|
|
795
|
+
children: [
|
|
796
|
+
jsx("button", {
|
|
797
|
+
"type": "button",
|
|
798
|
+
"class": "drag-handle",
|
|
799
|
+
"aria-label": "Move debugger",
|
|
800
|
+
"title": "Drag to move; arrow keys to nudge",
|
|
801
|
+
"onPointerdown": (event) => {
|
|
802
|
+
if (event.button !== 0) return;
|
|
803
|
+
drag = {
|
|
804
|
+
id: event.pointerId,
|
|
805
|
+
startX: event.clientX,
|
|
806
|
+
startY: event.clientY,
|
|
807
|
+
...position()
|
|
808
|
+
};
|
|
809
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
810
|
+
},
|
|
811
|
+
"onPointermove": (event) => {
|
|
812
|
+
if (!drag || drag.id !== event.pointerId || !panel) return;
|
|
813
|
+
const rect = panel.getBoundingClientRect();
|
|
814
|
+
const next = {
|
|
815
|
+
x: drag.x + event.clientX - drag.startX,
|
|
816
|
+
y: drag.y + event.clientY - drag.startY
|
|
817
|
+
};
|
|
818
|
+
next.x = Math.max(position().x - rect.left, Math.min(next.x, position().x + window.innerWidth - rect.right));
|
|
819
|
+
next.y = Math.max(position().y - rect.top, Math.min(next.y, position().y + window.innerHeight - rect.bottom));
|
|
820
|
+
setPosition(next);
|
|
821
|
+
},
|
|
822
|
+
"onPointerup": () => {
|
|
823
|
+
drag = void 0;
|
|
824
|
+
},
|
|
825
|
+
"onPointercancel": () => {
|
|
826
|
+
drag = void 0;
|
|
827
|
+
},
|
|
828
|
+
"onKeydown": (event) => {
|
|
829
|
+
const step = {
|
|
830
|
+
ArrowLeft: [-10, 0],
|
|
831
|
+
ArrowRight: [10, 0],
|
|
832
|
+
ArrowUp: [0, -10],
|
|
833
|
+
ArrowDown: [0, 10]
|
|
834
|
+
}[event.key];
|
|
835
|
+
if (step) {
|
|
836
|
+
event.preventDefault();
|
|
837
|
+
setPosition(({ x, y }) => ({
|
|
838
|
+
x: x + step[0],
|
|
839
|
+
y: y + step[1]
|
|
840
|
+
}));
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
children: "⠿"
|
|
844
|
+
}),
|
|
845
|
+
jsx("h1", { children: "atom.io" }),
|
|
846
|
+
() => open() ? jsx("nav", {
|
|
847
|
+
"aria-label": "Debugger views",
|
|
848
|
+
children: () => VIEWS.map((name) => jsx("button", {
|
|
849
|
+
"type": "button",
|
|
850
|
+
get "aria-pressed"() {
|
|
851
|
+
return view() === name;
|
|
852
|
+
},
|
|
853
|
+
get "data-testid"() {
|
|
854
|
+
return `view-${name}`;
|
|
855
|
+
},
|
|
856
|
+
"onClick": () => {
|
|
857
|
+
setSelection(name);
|
|
858
|
+
},
|
|
859
|
+
children: () => name
|
|
860
|
+
}))
|
|
861
|
+
}) : null,
|
|
862
|
+
jsx("button", {
|
|
863
|
+
"type": "button",
|
|
864
|
+
get "aria-label"() {
|
|
865
|
+
return open() ? `Minimize debugger` : `Open debugger`;
|
|
866
|
+
},
|
|
867
|
+
"data-testid": "open-close-devtools",
|
|
868
|
+
"onClick": () => {
|
|
869
|
+
setOpen((value) => !value);
|
|
870
|
+
},
|
|
871
|
+
children: () => open() ? `−` : `🔍`
|
|
872
|
+
}),
|
|
873
|
+
jsx("button", {
|
|
874
|
+
"type": "button",
|
|
875
|
+
"aria-label": "Hide debugger (Ctrl+Shift+A)",
|
|
876
|
+
"onClick": () => {
|
|
877
|
+
setHidden(true);
|
|
878
|
+
},
|
|
879
|
+
children: "×"
|
|
880
|
+
})
|
|
881
|
+
]
|
|
882
|
+
}), () => open() ? jsx(null, { children: [() => error() ? jsx("p", {
|
|
883
|
+
"role": "alert",
|
|
884
|
+
children: () => error()
|
|
885
|
+
}) : null, () => view() === `atoms` || view() === `selectors` ? jsx(StateIndex, {
|
|
886
|
+
"key": view(),
|
|
887
|
+
get "tools"() {
|
|
888
|
+
return tools;
|
|
889
|
+
},
|
|
890
|
+
get "type"() {
|
|
891
|
+
return view();
|
|
892
|
+
}
|
|
893
|
+
}) : view() === `transactions` ? jsx(TransactionIndex, { get "tools"() {
|
|
894
|
+
return tools;
|
|
895
|
+
} }) : jsx(TimelineIndex, { get "tools"() {
|
|
896
|
+
return tools;
|
|
897
|
+
} })] }) : null]
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
AtomIODevtools.spa = true;
|
|
901
|
+
Panel.spa = true;
|
|
902
|
+
export { AtomIODevtools };
|
|
903
|
+
|
|
904
|
+
//# sourceMappingURL=index.js.map
|