@atom.io/spa-devtools 0.0.0-stage → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +67 -3
- 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/src/Indexes.tsx
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import type { VNode } from "@atom.io/spa"
|
|
2
|
+
import { useSetter, useTimeline, useValue } from "@atom.io/spa"
|
|
3
|
+
import type {
|
|
4
|
+
ReadableToken,
|
|
5
|
+
TimelineToken,
|
|
6
|
+
TransactionToken,
|
|
7
|
+
WritableToken,
|
|
8
|
+
} from "atom.io"
|
|
9
|
+
import {
|
|
10
|
+
disposeFromStore,
|
|
11
|
+
findInStore,
|
|
12
|
+
isReservedIntrospectionKey,
|
|
13
|
+
} from "atom.io/internal"
|
|
14
|
+
import type { FamilyNode } from "atom.io/introspection"
|
|
15
|
+
|
|
16
|
+
import { JsonEditor } from "./JsonEditor.tsx"
|
|
17
|
+
import type { Tools } from "./store.ts"
|
|
18
|
+
import { useExpanded } from "./store.ts"
|
|
19
|
+
import { describe } from "./value.ts"
|
|
20
|
+
|
|
21
|
+
type Token = ReadableToken<unknown, any, any>
|
|
22
|
+
export function StateIndex(props: {
|
|
23
|
+
tools: Tools
|
|
24
|
+
type: `atoms` | `selectors`
|
|
25
|
+
}): VNode {
|
|
26
|
+
const index = useValue<Map<string, Token | FamilyNode<Token>>>(
|
|
27
|
+
props.type === `atoms` ? props.tools.atomIndex : props.tools.selectorIndex,
|
|
28
|
+
)
|
|
29
|
+
return (
|
|
30
|
+
<article class="index" data-testid="state-index">
|
|
31
|
+
{index().size === 0 ? (
|
|
32
|
+
<p>(no {props.type})</p>
|
|
33
|
+
) : (
|
|
34
|
+
[...index().values()].map((node) => (
|
|
35
|
+
<StateNode key={node.key} tools={props.tools} node={node} />
|
|
36
|
+
))
|
|
37
|
+
)}
|
|
38
|
+
</article>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
function StateNode(props: {
|
|
42
|
+
tools: Tools
|
|
43
|
+
node: Token | FamilyNode<Token>
|
|
44
|
+
}): VNode {
|
|
45
|
+
const [open, toggle] = useExpanded(props.tools, [props.node.key])
|
|
46
|
+
return (
|
|
47
|
+
<section class="node" data-testid={`state-${props.node.key}`}>
|
|
48
|
+
{`familyMembers` in props.node ? (
|
|
49
|
+
<>
|
|
50
|
+
<header>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
aria-expanded={open()}
|
|
54
|
+
data-testid={`open-close-state-family-${props.node.key}`}
|
|
55
|
+
onClick={toggle}
|
|
56
|
+
>
|
|
57
|
+
{open() ? `▾` : `▸`}
|
|
58
|
+
</button>
|
|
59
|
+
<h2>{props.node.key}</h2>
|
|
60
|
+
<small>family</small>
|
|
61
|
+
</header>
|
|
62
|
+
{open()
|
|
63
|
+
? [...props.node.familyMembers.values()].map((token) => (
|
|
64
|
+
<StateLeaf key={token.key} tools={props.tools} token={token} />
|
|
65
|
+
))
|
|
66
|
+
: null}
|
|
67
|
+
</>
|
|
68
|
+
) : (
|
|
69
|
+
<StateLeaf tools={props.tools} token={props.node} />
|
|
70
|
+
)}
|
|
71
|
+
</section>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
function StateLeaf(props: { tools: Tools; token: Token }): VNode {
|
|
75
|
+
const read = useValue(props.token)
|
|
76
|
+
const writable = !props.token.type.startsWith(`readonly`)
|
|
77
|
+
const set = writable
|
|
78
|
+
? useSetter(props.token as WritableToken<unknown, any, any>)
|
|
79
|
+
: undefined
|
|
80
|
+
const value = () => {
|
|
81
|
+
try {
|
|
82
|
+
return read()
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
88
|
+
<section class="state-leaf" data-testid={`state-value-${props.token.key}`}>
|
|
89
|
+
<JsonEditor
|
|
90
|
+
label={<h3>{props.token.family?.subKey ?? props.token.key}</h3>}
|
|
91
|
+
actions={
|
|
92
|
+
props.token.family ? (
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
aria-label={`Dispose ${props.token.key}`}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
props.tools.attempt(() => {
|
|
98
|
+
disposeFromStore(props.tools.store, props.token)
|
|
99
|
+
})
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
×
|
|
103
|
+
</button>
|
|
104
|
+
) : null
|
|
105
|
+
}
|
|
106
|
+
tools={props.tools}
|
|
107
|
+
value={value()}
|
|
108
|
+
change={
|
|
109
|
+
set
|
|
110
|
+
? (update) => {
|
|
111
|
+
// Validate edits before opening an atom.io write operation.
|
|
112
|
+
const next = update(read())
|
|
113
|
+
set(() => next)
|
|
114
|
+
}
|
|
115
|
+
: undefined
|
|
116
|
+
}
|
|
117
|
+
path={
|
|
118
|
+
props.token.family
|
|
119
|
+
? [props.token.family.key, props.token.family.subKey]
|
|
120
|
+
: [props.token.key]
|
|
121
|
+
}
|
|
122
|
+
/>
|
|
123
|
+
</section>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
export function TransactionIndex(props: { tools: Tools }): VNode {
|
|
127
|
+
const index = useValue(props.tools.transactionIndex)
|
|
128
|
+
const visible = () =>
|
|
129
|
+
index().filter((token) => !isReservedIntrospectionKey(token.key))
|
|
130
|
+
return (
|
|
131
|
+
<article class="index" data-testid="transaction-index">
|
|
132
|
+
{visible().length === 0 ? (
|
|
133
|
+
<p>(no transactions)</p>
|
|
134
|
+
) : (
|
|
135
|
+
visible().map((token) => (
|
|
136
|
+
<TransactionLog key={token.key} tools={props.tools} token={token} />
|
|
137
|
+
))
|
|
138
|
+
)}
|
|
139
|
+
</article>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
function TransactionLog(props: {
|
|
143
|
+
tools: Tools
|
|
144
|
+
token: TransactionToken<any>
|
|
145
|
+
}): VNode {
|
|
146
|
+
const log = useValue(
|
|
147
|
+
findInStore(
|
|
148
|
+
props.tools.store,
|
|
149
|
+
props.tools.transactionLogSelectors,
|
|
150
|
+
props.token.key,
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
const [open, toggle] = useExpanded(props.tools, [props.token.key])
|
|
154
|
+
return (
|
|
155
|
+
<section class="node" data-testid={`transaction-${props.token.key}`}>
|
|
156
|
+
<header>
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
aria-expanded={open()}
|
|
160
|
+
data-testid={`open-close-transaction-${props.token.key}`}
|
|
161
|
+
onClick={toggle}
|
|
162
|
+
>
|
|
163
|
+
{open() ? `▾` : `▸`}
|
|
164
|
+
</button>
|
|
165
|
+
<h2>{props.token.key}</h2>
|
|
166
|
+
<small>({log().length})</small>
|
|
167
|
+
</header>
|
|
168
|
+
{open()
|
|
169
|
+
? log().map((event, index) => (
|
|
170
|
+
<EventView
|
|
171
|
+
key={index}
|
|
172
|
+
tools={props.tools}
|
|
173
|
+
event={event}
|
|
174
|
+
index={index}
|
|
175
|
+
/>
|
|
176
|
+
))
|
|
177
|
+
: null}
|
|
178
|
+
</section>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
// Events are recursive discriminated records, including user-defined transaction outputs.
|
|
182
|
+
export function EventView(props: {
|
|
183
|
+
tools: Tools
|
|
184
|
+
event: any
|
|
185
|
+
index: number
|
|
186
|
+
}): VNode {
|
|
187
|
+
const [open, toggle] = useExpanded(
|
|
188
|
+
props.tools,
|
|
189
|
+
[`event`, props.event.token?.key ?? ``, String(props.index)],
|
|
190
|
+
true,
|
|
191
|
+
)
|
|
192
|
+
return (
|
|
193
|
+
<section
|
|
194
|
+
class="event"
|
|
195
|
+
data-testid={`${props.event.type === `transaction_outcome` ? `transaction` : `timeline`}-update-${props.event.token?.key}-${props.index}`}
|
|
196
|
+
>
|
|
197
|
+
<header>
|
|
198
|
+
<button type="button" aria-expanded={open()} onClick={toggle}>
|
|
199
|
+
{open() ? `▾` : `▸`}
|
|
200
|
+
</button>
|
|
201
|
+
<strong>{props.event.token?.key}</strong>
|
|
202
|
+
<small>{props.event.type}</small>
|
|
203
|
+
</header>
|
|
204
|
+
{open() ? (
|
|
205
|
+
props.event.type === `transaction_outcome` ? (
|
|
206
|
+
<>
|
|
207
|
+
<p>
|
|
208
|
+
params: <code>{describe(props.event.params)}</code>
|
|
209
|
+
</p>
|
|
210
|
+
<p>
|
|
211
|
+
output: <code>{describe(props.event.output)}</code>
|
|
212
|
+
</p>
|
|
213
|
+
<section class="impact">
|
|
214
|
+
{props.event.subEvents.map((event: any, index: number) => (
|
|
215
|
+
<EventView
|
|
216
|
+
key={index}
|
|
217
|
+
tools={props.tools}
|
|
218
|
+
event={event}
|
|
219
|
+
index={index}
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
</section>
|
|
223
|
+
</>
|
|
224
|
+
) : props.event.update ? (
|
|
225
|
+
<p>
|
|
226
|
+
<del>{describe(props.event.update.oldValue)}</del> →{` `}
|
|
227
|
+
<ins>{describe(props.event.update.newValue)}</ins>
|
|
228
|
+
</p>
|
|
229
|
+
) : (
|
|
230
|
+
<code>{describe(props.event)}</code>
|
|
231
|
+
)
|
|
232
|
+
) : null}
|
|
233
|
+
</section>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
export function TimelineIndex(props: { tools: Tools }): VNode {
|
|
237
|
+
const index = useValue(props.tools.timelineIndex)
|
|
238
|
+
const groups = () => {
|
|
239
|
+
const result = new Map<string, TimelineToken<any>[]>()
|
|
240
|
+
for (const token of index()) {
|
|
241
|
+
if (isReservedIntrospectionKey(token.key) || token.key.startsWith(`👁🗨`))
|
|
242
|
+
continue
|
|
243
|
+
const key = token.family?.key ?? ``
|
|
244
|
+
const members = result.get(key) ?? []
|
|
245
|
+
members.push(token)
|
|
246
|
+
result.set(key, members)
|
|
247
|
+
}
|
|
248
|
+
return [...result]
|
|
249
|
+
}
|
|
250
|
+
return (
|
|
251
|
+
<article class="index" data-testid="timeline-index">
|
|
252
|
+
{groups().length === 0 ? (
|
|
253
|
+
<p>(no timelines)</p>
|
|
254
|
+
) : (
|
|
255
|
+
groups().map(([family, members]) => (
|
|
256
|
+
<section
|
|
257
|
+
key={family}
|
|
258
|
+
data-testid={family ? `timeline-family-${family}` : undefined}
|
|
259
|
+
>
|
|
260
|
+
{family ? <h2>{family}</h2> : null}
|
|
261
|
+
{members.map((token) => (
|
|
262
|
+
<TimelineLog key={token.key} tools={props.tools} token={token} />
|
|
263
|
+
))}
|
|
264
|
+
</section>
|
|
265
|
+
))
|
|
266
|
+
)}
|
|
267
|
+
</article>
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
function TimelineLog(props: { tools: Tools; token: TimelineToken<any> }): VNode {
|
|
271
|
+
const controls = useTimeline(props.token)
|
|
272
|
+
const log = useValue(
|
|
273
|
+
findInStore(
|
|
274
|
+
props.tools.store,
|
|
275
|
+
props.tools.timelineSelectors,
|
|
276
|
+
props.token.key,
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
const [open, toggle] = useExpanded(props.tools, [props.token.key])
|
|
280
|
+
return (
|
|
281
|
+
<section class="node" data-testid={`timeline-${props.token.key}`}>
|
|
282
|
+
<header>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
aria-expanded={open()}
|
|
286
|
+
data-testid={`open-close-timeline-${props.token.key}`}
|
|
287
|
+
onClick={toggle}
|
|
288
|
+
>
|
|
289
|
+
{open() ? `▾` : `▸`}
|
|
290
|
+
</button>
|
|
291
|
+
<h2>{props.token.family?.subKey ?? props.token.key}</h2>
|
|
292
|
+
<small>
|
|
293
|
+
({controls().at}/{controls().length})
|
|
294
|
+
</small>
|
|
295
|
+
<button
|
|
296
|
+
type="button"
|
|
297
|
+
disabled={controls().at === 0}
|
|
298
|
+
onClick={() => {
|
|
299
|
+
props.tools.attempt(controls().undo)
|
|
300
|
+
}}
|
|
301
|
+
>
|
|
302
|
+
undo
|
|
303
|
+
</button>
|
|
304
|
+
<button
|
|
305
|
+
type="button"
|
|
306
|
+
disabled={controls().at === controls().length}
|
|
307
|
+
onClick={() => {
|
|
308
|
+
props.tools.attempt(controls().redo)
|
|
309
|
+
}}
|
|
310
|
+
>
|
|
311
|
+
redo
|
|
312
|
+
</button>
|
|
313
|
+
</header>
|
|
314
|
+
{open() ? (
|
|
315
|
+
<section>
|
|
316
|
+
{log().history.map((event, index) => (
|
|
317
|
+
<section key={index}>
|
|
318
|
+
{index === log().at ? (
|
|
319
|
+
<p class="you-are-here">you are here</p>
|
|
320
|
+
) : null}
|
|
321
|
+
<EventView tools={props.tools} event={event} index={index} />
|
|
322
|
+
</section>
|
|
323
|
+
))}
|
|
324
|
+
{log().at === log().history.length ? (
|
|
325
|
+
<p class="you-are-here">you are here</p>
|
|
326
|
+
) : null}
|
|
327
|
+
</section>
|
|
328
|
+
) : null}
|
|
329
|
+
</section>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import type { Child, VNode } from "@atom.io/spa"
|
|
2
|
+
|
|
3
|
+
import type { Path, Tools } from "./store.ts"
|
|
4
|
+
import { useExpanded } from "./store.ts"
|
|
5
|
+
import type { Change, JsonKind } from "./value.ts"
|
|
6
|
+
import { convert, describe, kind } from "./value.ts"
|
|
7
|
+
|
|
8
|
+
interface EditorProps {
|
|
9
|
+
tools: Tools
|
|
10
|
+
value: unknown
|
|
11
|
+
change?: Change | undefined
|
|
12
|
+
path: Path
|
|
13
|
+
ancestors?: readonly unknown[]
|
|
14
|
+
label?: Child
|
|
15
|
+
actions?: Child
|
|
16
|
+
}
|
|
17
|
+
const TYPES: JsonKind[] = [
|
|
18
|
+
`string`,
|
|
19
|
+
`number`,
|
|
20
|
+
`boolean`,
|
|
21
|
+
`null`,
|
|
22
|
+
`array`,
|
|
23
|
+
`object`,
|
|
24
|
+
]
|
|
25
|
+
export function JsonEditor(props: EditorProps): VNode {
|
|
26
|
+
const [expanded, toggle] = useExpanded(props.tools, props.path)
|
|
27
|
+
const circular = () =>
|
|
28
|
+
(props.ancestors?.includes(props.value) ?? false) || props.path.length > 50
|
|
29
|
+
const container = () =>
|
|
30
|
+
!circular() &&
|
|
31
|
+
(kind(props.value) === `object` || kind(props.value) === `array`)
|
|
32
|
+
const entries = () => Object.entries(props.value as Record<string, unknown>)
|
|
33
|
+
return (
|
|
34
|
+
<section
|
|
35
|
+
class="json-editor"
|
|
36
|
+
data-testid={`${props.path.join(`.`)}-state-editor`}
|
|
37
|
+
>
|
|
38
|
+
<header>
|
|
39
|
+
{container() ? (
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
aria-label={`Expand ${props.path.at(-1)}`}
|
|
43
|
+
aria-expanded={expanded()}
|
|
44
|
+
onClick={toggle}
|
|
45
|
+
>
|
|
46
|
+
{expanded() ? `▾` : `▸`}
|
|
47
|
+
</button>
|
|
48
|
+
) : (
|
|
49
|
+
<span class="carat-spacer" />
|
|
50
|
+
)}
|
|
51
|
+
<span class="field-name">{props.label}</span>
|
|
52
|
+
{props.change ? (
|
|
53
|
+
<select
|
|
54
|
+
aria-label={`Type of ${props.path.at(-1)}`}
|
|
55
|
+
value={kind(props.value)}
|
|
56
|
+
onChange={(event) => {
|
|
57
|
+
const type = event.currentTarget.value as JsonKind
|
|
58
|
+
props.tools.attempt(() => {
|
|
59
|
+
props.change?.((previous) => convert(previous, type))
|
|
60
|
+
})
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<option value="other" disabled>
|
|
64
|
+
other
|
|
65
|
+
</option>
|
|
66
|
+
{TYPES.map((type) => (
|
|
67
|
+
<option value={type}>{type}</option>
|
|
68
|
+
))}
|
|
69
|
+
</select>
|
|
70
|
+
) : (
|
|
71
|
+
<small>{kind(props.value)}</small>
|
|
72
|
+
)}
|
|
73
|
+
{container() ? (
|
|
74
|
+
<code title={describe(props.value)}>{describe(props.value)}</code>
|
|
75
|
+
) : circular() ? (
|
|
76
|
+
<code>[Circular]</code>
|
|
77
|
+
) : (
|
|
78
|
+
<Primitive
|
|
79
|
+
value={props.value}
|
|
80
|
+
change={props.change}
|
|
81
|
+
tools={props.tools}
|
|
82
|
+
label={props.path.at(-1) ?? `value`}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
{props.actions}
|
|
86
|
+
</header>
|
|
87
|
+
{container() && expanded() ? (
|
|
88
|
+
<section class="properties">
|
|
89
|
+
{entries().map(([key]) => (
|
|
90
|
+
<Property key={key} name={key} editor={props} />
|
|
91
|
+
))}
|
|
92
|
+
{props.change ? (
|
|
93
|
+
<footer>
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => {
|
|
97
|
+
props.tools.attempt(() => {
|
|
98
|
+
props.change?.((previous) => {
|
|
99
|
+
if (Array.isArray(previous)) return [...previous, null]
|
|
100
|
+
const object = previous as Record<string, unknown>
|
|
101
|
+
let name = `new`
|
|
102
|
+
while (Object.hasOwn(object, name)) name += `_`
|
|
103
|
+
return { ...object, [name]: null }
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
add {Array.isArray(props.value) ? `item` : `property`}
|
|
109
|
+
</button>
|
|
110
|
+
{Array.isArray(props.value) ? (
|
|
111
|
+
<button
|
|
112
|
+
type="button"
|
|
113
|
+
onClick={() => {
|
|
114
|
+
props.tools.attempt(() => {
|
|
115
|
+
props.change?.((previous) =>
|
|
116
|
+
[...(previous as unknown[])].sort((a, b) =>
|
|
117
|
+
typeof a === `number` && typeof b === `number`
|
|
118
|
+
? a - b
|
|
119
|
+
: describe(a).localeCompare(describe(b)),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
sort
|
|
126
|
+
</button>
|
|
127
|
+
) : null}
|
|
128
|
+
</footer>
|
|
129
|
+
) : null}
|
|
130
|
+
</section>
|
|
131
|
+
) : null}
|
|
132
|
+
</section>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
function Primitive(props: {
|
|
136
|
+
value: unknown
|
|
137
|
+
change?: Change | undefined
|
|
138
|
+
tools: Tools
|
|
139
|
+
label: string
|
|
140
|
+
}): VNode {
|
|
141
|
+
const set = (value: unknown) => {
|
|
142
|
+
props.tools.attempt(() => {
|
|
143
|
+
props.change?.(() => value)
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
return (
|
|
147
|
+
<span class="primitive">
|
|
148
|
+
{kind(props.value) === `boolean` ? (
|
|
149
|
+
<input
|
|
150
|
+
aria-label={props.label}
|
|
151
|
+
type="checkbox"
|
|
152
|
+
checked={Boolean(props.value)}
|
|
153
|
+
disabled={!props.change}
|
|
154
|
+
onChange={(event) => {
|
|
155
|
+
set(event.currentTarget.checked)
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
) : kind(props.value) === `string` || kind(props.value) === `number` ? (
|
|
159
|
+
<input
|
|
160
|
+
aria-label={props.label}
|
|
161
|
+
type="text"
|
|
162
|
+
inputmode={kind(props.value) === `number` ? `decimal` : `text`}
|
|
163
|
+
value={String(props.value)}
|
|
164
|
+
readOnly={!props.change}
|
|
165
|
+
onInput={(event) => {
|
|
166
|
+
const text = event.currentTarget.value
|
|
167
|
+
if (kind(props.value) === `string`) set(text)
|
|
168
|
+
else if (text.trim() && Number.isFinite(Number(text)))
|
|
169
|
+
set(Number(text))
|
|
170
|
+
}}
|
|
171
|
+
onBlur={(event) => {
|
|
172
|
+
event.currentTarget.value = String(props.value)
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
) : (
|
|
176
|
+
<code>{describe(props.value)}</code>
|
|
177
|
+
)}
|
|
178
|
+
</span>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
function Property(props: { name: string; editor: EditorProps }): VNode {
|
|
182
|
+
const parent = props.editor
|
|
183
|
+
const read = () => (parent.value as Record<string, unknown>)[props.name]
|
|
184
|
+
const change: Change = (update) => {
|
|
185
|
+
parent.change?.((previous) => {
|
|
186
|
+
if (Array.isArray(previous))
|
|
187
|
+
return previous.map((value, index) =>
|
|
188
|
+
index === Number(props.name) ? update(value) : value,
|
|
189
|
+
)
|
|
190
|
+
const object = previous as Record<string, unknown>
|
|
191
|
+
return { ...object, [props.name]: update(object[props.name]) }
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
return (
|
|
195
|
+
<section class="property">
|
|
196
|
+
<JsonEditor
|
|
197
|
+
label={
|
|
198
|
+
parent.change && !Array.isArray(parent.value) ? (
|
|
199
|
+
<input
|
|
200
|
+
aria-label={`Rename ${props.name}`}
|
|
201
|
+
style={`width:calc(${Math.max(1, props.name.length)}ch + 6px)`}
|
|
202
|
+
value={props.name}
|
|
203
|
+
onChange={(event) => {
|
|
204
|
+
const input = event.currentTarget
|
|
205
|
+
const name = input.value
|
|
206
|
+
parent.tools.attempt(() => {
|
|
207
|
+
parent.change?.((previous) => {
|
|
208
|
+
const object = previous as Record<string, unknown>
|
|
209
|
+
if (name !== props.name && Object.hasOwn(object, name))
|
|
210
|
+
throw new Error(`Property "${name}" already exists`)
|
|
211
|
+
return Object.fromEntries(
|
|
212
|
+
Object.entries(object).map(([key, value]) => [
|
|
213
|
+
key === props.name ? name : key,
|
|
214
|
+
value,
|
|
215
|
+
]),
|
|
216
|
+
)
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
input.value = props.name
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
) : (
|
|
223
|
+
<strong>{props.name}</strong>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
actions={
|
|
227
|
+
parent.change ? (
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
aria-label={`Delete ${props.name}`}
|
|
231
|
+
onClick={() => {
|
|
232
|
+
parent.tools.attempt(() => {
|
|
233
|
+
parent.change?.((previous) =>
|
|
234
|
+
Array.isArray(previous)
|
|
235
|
+
? previous.filter(
|
|
236
|
+
(_, index) => index !== Number(props.name),
|
|
237
|
+
)
|
|
238
|
+
: Object.fromEntries(
|
|
239
|
+
Object.entries(previous as object).filter(
|
|
240
|
+
([key]) => key !== props.name,
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
})
|
|
245
|
+
}}
|
|
246
|
+
>
|
|
247
|
+
×
|
|
248
|
+
</button>
|
|
249
|
+
) : null
|
|
250
|
+
}
|
|
251
|
+
tools={parent.tools}
|
|
252
|
+
path={[...parent.path, props.name]}
|
|
253
|
+
value={read()}
|
|
254
|
+
change={parent.change ? change : undefined}
|
|
255
|
+
ancestors={[...(parent.ancestors ?? []), parent.value]}
|
|
256
|
+
/>
|
|
257
|
+
</section>
|
|
258
|
+
)
|
|
259
|
+
}
|
package/src/css.d.ts
ADDED