@terrafirmagreg-team/mdx 1.2.0 → 1.2.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.
- package/package.json +3 -3
- package/src/components/docs-image.tsx +62 -62
- package/src/components/mdx-components.tsx +158 -158
- package/src/components/story-fields.tsx +318 -318
- package/src/components/story-panel.tsx +181 -181
- package/src/lib/md-plugins.ts +1 -0
- package/src/lib/mdx/directive-utils.ts +118 -118
- package/src/lib/pack-assets.ts +46 -46
- package/src/lib/story-remark.ts +468 -468
- package/src/lib/story.ts +11 -11
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useDataEngine, useFieldValue } from '@fumari/stf';
|
|
4
|
-
import {
|
|
5
|
-
typeToString,
|
|
6
|
-
validate,
|
|
7
|
-
type TypeNode,
|
|
8
|
-
} from '@fumadocs/story/type-tree';
|
|
9
|
-
import { directiveMeta } from '../lib/story-remark';
|
|
10
|
-
import {
|
|
11
|
-
Select,
|
|
12
|
-
SelectContent,
|
|
13
|
-
SelectItem,
|
|
14
|
-
SelectTrigger,
|
|
15
|
-
SelectValue,
|
|
16
|
-
} from '@terrafirmagreg-team/ui/components/ui/select';
|
|
17
|
-
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
18
|
-
import { X } from 'lucide-react';
|
|
19
|
-
import { useState } from 'react';
|
|
20
|
-
|
|
21
|
-
function resolveNode(node: TypeNode, value: unknown): TypeNode {
|
|
22
|
-
if (node.type === 'intersection') return resolveNode(node.intersection, value);
|
|
23
|
-
if (node.type !== 'union') return node;
|
|
24
|
-
return resolveNode(
|
|
25
|
-
node.types.find((item) => validate(item, value)) ?? node.types[0],
|
|
26
|
-
value,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function selectMembers(
|
|
31
|
-
node: TypeNode,
|
|
32
|
-
): { label: string; value: unknown }[] | undefined {
|
|
33
|
-
if (node.type === 'enum') return node.members;
|
|
34
|
-
if (
|
|
35
|
-
node.type === 'union' &&
|
|
36
|
-
node.types.every((t) => t.type === 'literal')
|
|
37
|
-
) {
|
|
38
|
-
return node.types.map((t) => ({
|
|
39
|
-
label: String(t.type === 'literal' ? t.value : ''),
|
|
40
|
-
value: t.type === 'literal' ? t.value : undefined,
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const inputClass =
|
|
47
|
-
'min-w-0 flex-1 bg-transparent px-3 text-fd-foreground outline-none placeholder:text-fd-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-fd-primary';
|
|
48
|
-
|
|
49
|
-
function StringEditor({
|
|
50
|
-
name,
|
|
51
|
-
value,
|
|
52
|
-
setValue,
|
|
53
|
-
multiline,
|
|
54
|
-
}: {
|
|
55
|
-
name: string;
|
|
56
|
-
value: unknown;
|
|
57
|
-
setValue: (v: unknown) => void;
|
|
58
|
-
multiline?: boolean;
|
|
59
|
-
}) {
|
|
60
|
-
if (multiline) {
|
|
61
|
-
const lineCount =
|
|
62
|
-
typeof value === 'string' ? value.split('\n').length : 0;
|
|
63
|
-
return (
|
|
64
|
-
<textarea
|
|
65
|
-
aria-label={name}
|
|
66
|
-
rows={Math.min(12, Math.max(4, lineCount + 1))}
|
|
67
|
-
value={value == null ? '' : String(value)}
|
|
68
|
-
placeholder="string"
|
|
69
|
-
onChange={(e) => setValue(e.target.value)}
|
|
70
|
-
className={cn(inputClass, 'min-h-12 resize-y py-2 font-mono text-xs')}
|
|
71
|
-
/>
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
return (
|
|
75
|
-
<input
|
|
76
|
-
aria-label={name}
|
|
77
|
-
value={value == null ? '' : String(value)}
|
|
78
|
-
placeholder="string"
|
|
79
|
-
onChange={(e) => setValue(e.target.value)}
|
|
80
|
-
className={inputClass}
|
|
81
|
-
/>
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function NumberEditor({
|
|
86
|
-
name,
|
|
87
|
-
value,
|
|
88
|
-
setValue,
|
|
89
|
-
}: {
|
|
90
|
-
name: string;
|
|
91
|
-
value: unknown;
|
|
92
|
-
setValue: (v: unknown) => void;
|
|
93
|
-
}) {
|
|
94
|
-
return (
|
|
95
|
-
<input
|
|
96
|
-
aria-label={name}
|
|
97
|
-
type="number"
|
|
98
|
-
value={value == null ? '' : String(value)}
|
|
99
|
-
placeholder="number"
|
|
100
|
-
onChange={(e) =>
|
|
101
|
-
setValue(e.target.value === '' ? undefined : Number(e.target.value))
|
|
102
|
-
}
|
|
103
|
-
className={inputClass}
|
|
104
|
-
/>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function BooleanEditor({
|
|
109
|
-
name,
|
|
110
|
-
value,
|
|
111
|
-
setValue,
|
|
112
|
-
}: {
|
|
113
|
-
name: string;
|
|
114
|
-
value: unknown;
|
|
115
|
-
setValue: (v: unknown) => void;
|
|
116
|
-
}) {
|
|
117
|
-
return (
|
|
118
|
-
<input
|
|
119
|
-
aria-label={name}
|
|
120
|
-
type="checkbox"
|
|
121
|
-
checked={value === true}
|
|
122
|
-
onChange={(e) => setValue(e.target.checked)}
|
|
123
|
-
className="mx-3 size-3.5 shrink-0 accent-fd-primary"
|
|
124
|
-
/>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function SelectEditor({
|
|
129
|
-
name,
|
|
130
|
-
value,
|
|
131
|
-
setValue,
|
|
132
|
-
members,
|
|
133
|
-
}: {
|
|
134
|
-
name: string;
|
|
135
|
-
value: unknown;
|
|
136
|
-
setValue: (v: unknown) => void;
|
|
137
|
-
members: { label: string; value: unknown }[];
|
|
138
|
-
}) {
|
|
139
|
-
const index = members.findIndex((m) => Object.is(m.value, value));
|
|
140
|
-
return (
|
|
141
|
-
<Select
|
|
142
|
-
value={index < 0 ? '' : String(index)}
|
|
143
|
-
onValueChange={(v) => {
|
|
144
|
-
const next = members[Number(v)];
|
|
145
|
-
if (next) setValue(next.value);
|
|
146
|
-
}}
|
|
147
|
-
>
|
|
148
|
-
<SelectTrigger
|
|
149
|
-
aria-label={name}
|
|
150
|
-
className="h-8 flex-1 gap-1 border-0 bg-transparent px-3 text-xs shadow-none focus:ring-0 focus-visible:ring-1"
|
|
151
|
-
>
|
|
152
|
-
<SelectValue placeholder="select">
|
|
153
|
-
{(v: unknown) =>
|
|
154
|
-
typeof v === 'string' && v !== ''
|
|
155
|
-
? members[Number(v)]?.label
|
|
156
|
-
: null
|
|
157
|
-
}
|
|
158
|
-
</SelectValue>
|
|
159
|
-
</SelectTrigger>
|
|
160
|
-
<SelectContent>
|
|
161
|
-
{members.map((m, i) => (
|
|
162
|
-
<SelectItem key={i} value={String(i)} className="text-xs">
|
|
163
|
-
{m.label}
|
|
164
|
-
</SelectItem>
|
|
165
|
-
))}
|
|
166
|
-
</SelectContent>
|
|
167
|
-
</Select>
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function JsonEditor({
|
|
172
|
-
name,
|
|
173
|
-
value,
|
|
174
|
-
setValue,
|
|
175
|
-
}: {
|
|
176
|
-
name: string;
|
|
177
|
-
value: unknown;
|
|
178
|
-
setValue: (v: unknown) => void;
|
|
179
|
-
}) {
|
|
180
|
-
const [text, setText] = useState(() =>
|
|
181
|
-
value === undefined ? '' : JSON.stringify(value),
|
|
182
|
-
);
|
|
183
|
-
const [invalid, setInvalid] = useState(false);
|
|
184
|
-
const [prevValue, setPrevValue] = useState(value);
|
|
185
|
-
if (prevValue !== value) {
|
|
186
|
-
setPrevValue(value);
|
|
187
|
-
setText(value === undefined ? '' : JSON.stringify(value));
|
|
188
|
-
setInvalid(false);
|
|
189
|
-
}
|
|
190
|
-
return (
|
|
191
|
-
<textarea
|
|
192
|
-
aria-label={name}
|
|
193
|
-
rows={2}
|
|
194
|
-
value={text}
|
|
195
|
-
placeholder="JSON"
|
|
196
|
-
onChange={(e) => {
|
|
197
|
-
const next = e.target.value;
|
|
198
|
-
setText(next);
|
|
199
|
-
if (next.trim() === '') {
|
|
200
|
-
setValue(undefined);
|
|
201
|
-
setInvalid(false);
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
try {
|
|
205
|
-
setValue(JSON.parse(next));
|
|
206
|
-
setInvalid(false);
|
|
207
|
-
} catch {
|
|
208
|
-
setInvalid(true);
|
|
209
|
-
}
|
|
210
|
-
}}
|
|
211
|
-
className={cn(
|
|
212
|
-
inputClass,
|
|
213
|
-
'min-h-10 resize-y py-2 font-mono text-xs',
|
|
214
|
-
invalid && 'text-fd-error',
|
|
215
|
-
)}
|
|
216
|
-
/>
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function Field({
|
|
221
|
-
name,
|
|
222
|
-
node,
|
|
223
|
-
required,
|
|
224
|
-
multiline,
|
|
225
|
-
description,
|
|
226
|
-
}: {
|
|
227
|
-
name: string;
|
|
228
|
-
node: TypeNode;
|
|
229
|
-
required?: boolean;
|
|
230
|
-
multiline?: boolean;
|
|
231
|
-
description?: string;
|
|
232
|
-
}) {
|
|
233
|
-
const engine = useDataEngine();
|
|
234
|
-
const [value, setValue] = useFieldValue([name]);
|
|
235
|
-
const resolved = resolveNode(node, value);
|
|
236
|
-
const members = selectMembers(resolved);
|
|
237
|
-
|
|
238
|
-
let editor: React.ReactNode;
|
|
239
|
-
if (members) {
|
|
240
|
-
editor = (
|
|
241
|
-
<SelectEditor
|
|
242
|
-
name={name}
|
|
243
|
-
value={value}
|
|
244
|
-
setValue={setValue}
|
|
245
|
-
members={members}
|
|
246
|
-
/>
|
|
247
|
-
);
|
|
248
|
-
} else if (resolved.type === 'boolean') {
|
|
249
|
-
editor = <BooleanEditor name={name} value={value} setValue={setValue} />;
|
|
250
|
-
} else if (resolved.type === 'number' || resolved.type === 'bigint') {
|
|
251
|
-
editor = <NumberEditor name={name} value={value} setValue={setValue} />;
|
|
252
|
-
} else if (resolved.type === 'string' || resolved.type === 'literal') {
|
|
253
|
-
editor = (
|
|
254
|
-
<StringEditor
|
|
255
|
-
name={name}
|
|
256
|
-
value={value}
|
|
257
|
-
setValue={setValue}
|
|
258
|
-
multiline={multiline}
|
|
259
|
-
/>
|
|
260
|
-
);
|
|
261
|
-
} else {
|
|
262
|
-
editor = <JsonEditor name={name} value={value} setValue={setValue} />;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return (
|
|
266
|
-
<div className="flex min-h-10 items-center border-b last:border-0">
|
|
267
|
-
<span
|
|
268
|
-
className="flex w-28 shrink-0 items-center gap-1 px-3 font-mono text-[0.8125rem] text-fd-muted-foreground"
|
|
269
|
-
title={description}
|
|
270
|
-
>
|
|
271
|
-
<span className="truncate">{name}</span>
|
|
272
|
-
{!required && <span className="text-fd-muted-foreground/60">?</span>}
|
|
273
|
-
</span>
|
|
274
|
-
{editor}
|
|
275
|
-
<span className="hidden w-24 shrink-0 truncate px-2 font-mono text-xs text-fd-muted-foreground/70 @3xl:block">
|
|
276
|
-
{typeToString(resolved)}
|
|
277
|
-
</span>
|
|
278
|
-
{!required && (
|
|
279
|
-
<button
|
|
280
|
-
type="button"
|
|
281
|
-
aria-label={`Unset ${name}`}
|
|
282
|
-
onClick={() => engine.delete([name])}
|
|
283
|
-
className="me-2 flex size-6 shrink-0 items-center justify-center rounded-md text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-foreground"
|
|
284
|
-
>
|
|
285
|
-
<X className="size-3" />
|
|
286
|
-
</button>
|
|
287
|
-
)}
|
|
288
|
-
</div>
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function StoryFields({
|
|
293
|
-
node,
|
|
294
|
-
storyKey,
|
|
295
|
-
}: {
|
|
296
|
-
node: TypeNode;
|
|
297
|
-
storyKey: string;
|
|
298
|
-
}) {
|
|
299
|
-
const meta = directiveMeta(storyKey);
|
|
300
|
-
if (node.type !== 'object') return null;
|
|
301
|
-
const multiline = new Set(meta?.multiline ?? []);
|
|
302
|
-
const descriptions = meta?.descriptions ?? {};
|
|
303
|
-
return (
|
|
304
|
-
<div className="text-[0.8125rem]">
|
|
305
|
-
{node.properties.map((prop) => (
|
|
306
|
-
<Field
|
|
307
|
-
key={prop.name}
|
|
308
|
-
name={prop.name}
|
|
309
|
-
node={prop.type}
|
|
310
|
-
required={prop.required}
|
|
311
|
-
multiline={multiline.has(prop.name)}
|
|
312
|
-
description={descriptions[prop.name]}
|
|
313
|
-
/>
|
|
314
|
-
))}
|
|
315
|
-
</div>
|
|
316
|
-
);
|
|
317
|
-
}
|
|
318
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useDataEngine, useFieldValue } from '@fumari/stf';
|
|
4
|
+
import {
|
|
5
|
+
typeToString,
|
|
6
|
+
validate,
|
|
7
|
+
type TypeNode,
|
|
8
|
+
} from '@fumadocs/story/type-tree';
|
|
9
|
+
import { directiveMeta } from '../lib/story-remark';
|
|
10
|
+
import {
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
} from '@terrafirmagreg-team/ui/components/ui/select';
|
|
17
|
+
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
18
|
+
import { X } from 'lucide-react';
|
|
19
|
+
import { useState } from 'react';
|
|
20
|
+
|
|
21
|
+
function resolveNode(node: TypeNode, value: unknown): TypeNode {
|
|
22
|
+
if (node.type === 'intersection') return resolveNode(node.intersection, value);
|
|
23
|
+
if (node.type !== 'union') return node;
|
|
24
|
+
return resolveNode(
|
|
25
|
+
node.types.find((item) => validate(item, value)) ?? node.types[0],
|
|
26
|
+
value,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function selectMembers(
|
|
31
|
+
node: TypeNode,
|
|
32
|
+
): { label: string; value: unknown }[] | undefined {
|
|
33
|
+
if (node.type === 'enum') return node.members;
|
|
34
|
+
if (
|
|
35
|
+
node.type === 'union' &&
|
|
36
|
+
node.types.every((t) => t.type === 'literal')
|
|
37
|
+
) {
|
|
38
|
+
return node.types.map((t) => ({
|
|
39
|
+
label: String(t.type === 'literal' ? t.value : ''),
|
|
40
|
+
value: t.type === 'literal' ? t.value : undefined,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const inputClass =
|
|
47
|
+
'min-w-0 flex-1 bg-transparent px-3 text-fd-foreground outline-none placeholder:text-fd-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-fd-primary';
|
|
48
|
+
|
|
49
|
+
function StringEditor({
|
|
50
|
+
name,
|
|
51
|
+
value,
|
|
52
|
+
setValue,
|
|
53
|
+
multiline,
|
|
54
|
+
}: {
|
|
55
|
+
name: string;
|
|
56
|
+
value: unknown;
|
|
57
|
+
setValue: (v: unknown) => void;
|
|
58
|
+
multiline?: boolean;
|
|
59
|
+
}) {
|
|
60
|
+
if (multiline) {
|
|
61
|
+
const lineCount =
|
|
62
|
+
typeof value === 'string' ? value.split('\n').length : 0;
|
|
63
|
+
return (
|
|
64
|
+
<textarea
|
|
65
|
+
aria-label={name}
|
|
66
|
+
rows={Math.min(12, Math.max(4, lineCount + 1))}
|
|
67
|
+
value={value == null ? '' : String(value)}
|
|
68
|
+
placeholder="string"
|
|
69
|
+
onChange={(e) => setValue(e.target.value)}
|
|
70
|
+
className={cn(inputClass, 'min-h-12 resize-y py-2 font-mono text-xs')}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return (
|
|
75
|
+
<input
|
|
76
|
+
aria-label={name}
|
|
77
|
+
value={value == null ? '' : String(value)}
|
|
78
|
+
placeholder="string"
|
|
79
|
+
onChange={(e) => setValue(e.target.value)}
|
|
80
|
+
className={inputClass}
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function NumberEditor({
|
|
86
|
+
name,
|
|
87
|
+
value,
|
|
88
|
+
setValue,
|
|
89
|
+
}: {
|
|
90
|
+
name: string;
|
|
91
|
+
value: unknown;
|
|
92
|
+
setValue: (v: unknown) => void;
|
|
93
|
+
}) {
|
|
94
|
+
return (
|
|
95
|
+
<input
|
|
96
|
+
aria-label={name}
|
|
97
|
+
type="number"
|
|
98
|
+
value={value == null ? '' : String(value)}
|
|
99
|
+
placeholder="number"
|
|
100
|
+
onChange={(e) =>
|
|
101
|
+
setValue(e.target.value === '' ? undefined : Number(e.target.value))
|
|
102
|
+
}
|
|
103
|
+
className={inputClass}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function BooleanEditor({
|
|
109
|
+
name,
|
|
110
|
+
value,
|
|
111
|
+
setValue,
|
|
112
|
+
}: {
|
|
113
|
+
name: string;
|
|
114
|
+
value: unknown;
|
|
115
|
+
setValue: (v: unknown) => void;
|
|
116
|
+
}) {
|
|
117
|
+
return (
|
|
118
|
+
<input
|
|
119
|
+
aria-label={name}
|
|
120
|
+
type="checkbox"
|
|
121
|
+
checked={value === true}
|
|
122
|
+
onChange={(e) => setValue(e.target.checked)}
|
|
123
|
+
className="mx-3 size-3.5 shrink-0 accent-fd-primary"
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function SelectEditor({
|
|
129
|
+
name,
|
|
130
|
+
value,
|
|
131
|
+
setValue,
|
|
132
|
+
members,
|
|
133
|
+
}: {
|
|
134
|
+
name: string;
|
|
135
|
+
value: unknown;
|
|
136
|
+
setValue: (v: unknown) => void;
|
|
137
|
+
members: { label: string; value: unknown }[];
|
|
138
|
+
}) {
|
|
139
|
+
const index = members.findIndex((m) => Object.is(m.value, value));
|
|
140
|
+
return (
|
|
141
|
+
<Select
|
|
142
|
+
value={index < 0 ? '' : String(index)}
|
|
143
|
+
onValueChange={(v) => {
|
|
144
|
+
const next = members[Number(v)];
|
|
145
|
+
if (next) setValue(next.value);
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<SelectTrigger
|
|
149
|
+
aria-label={name}
|
|
150
|
+
className="h-8 flex-1 gap-1 border-0 bg-transparent px-3 text-xs shadow-none focus:ring-0 focus-visible:ring-1"
|
|
151
|
+
>
|
|
152
|
+
<SelectValue placeholder="select">
|
|
153
|
+
{(v: unknown) =>
|
|
154
|
+
typeof v === 'string' && v !== ''
|
|
155
|
+
? members[Number(v)]?.label
|
|
156
|
+
: null
|
|
157
|
+
}
|
|
158
|
+
</SelectValue>
|
|
159
|
+
</SelectTrigger>
|
|
160
|
+
<SelectContent>
|
|
161
|
+
{members.map((m, i) => (
|
|
162
|
+
<SelectItem key={i} value={String(i)} className="text-xs">
|
|
163
|
+
{m.label}
|
|
164
|
+
</SelectItem>
|
|
165
|
+
))}
|
|
166
|
+
</SelectContent>
|
|
167
|
+
</Select>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function JsonEditor({
|
|
172
|
+
name,
|
|
173
|
+
value,
|
|
174
|
+
setValue,
|
|
175
|
+
}: {
|
|
176
|
+
name: string;
|
|
177
|
+
value: unknown;
|
|
178
|
+
setValue: (v: unknown) => void;
|
|
179
|
+
}) {
|
|
180
|
+
const [text, setText] = useState(() =>
|
|
181
|
+
value === undefined ? '' : JSON.stringify(value),
|
|
182
|
+
);
|
|
183
|
+
const [invalid, setInvalid] = useState(false);
|
|
184
|
+
const [prevValue, setPrevValue] = useState(value);
|
|
185
|
+
if (prevValue !== value) {
|
|
186
|
+
setPrevValue(value);
|
|
187
|
+
setText(value === undefined ? '' : JSON.stringify(value));
|
|
188
|
+
setInvalid(false);
|
|
189
|
+
}
|
|
190
|
+
return (
|
|
191
|
+
<textarea
|
|
192
|
+
aria-label={name}
|
|
193
|
+
rows={2}
|
|
194
|
+
value={text}
|
|
195
|
+
placeholder="JSON"
|
|
196
|
+
onChange={(e) => {
|
|
197
|
+
const next = e.target.value;
|
|
198
|
+
setText(next);
|
|
199
|
+
if (next.trim() === '') {
|
|
200
|
+
setValue(undefined);
|
|
201
|
+
setInvalid(false);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
setValue(JSON.parse(next));
|
|
206
|
+
setInvalid(false);
|
|
207
|
+
} catch {
|
|
208
|
+
setInvalid(true);
|
|
209
|
+
}
|
|
210
|
+
}}
|
|
211
|
+
className={cn(
|
|
212
|
+
inputClass,
|
|
213
|
+
'min-h-10 resize-y py-2 font-mono text-xs',
|
|
214
|
+
invalid && 'text-fd-error',
|
|
215
|
+
)}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function Field({
|
|
221
|
+
name,
|
|
222
|
+
node,
|
|
223
|
+
required,
|
|
224
|
+
multiline,
|
|
225
|
+
description,
|
|
226
|
+
}: {
|
|
227
|
+
name: string;
|
|
228
|
+
node: TypeNode;
|
|
229
|
+
required?: boolean;
|
|
230
|
+
multiline?: boolean;
|
|
231
|
+
description?: string;
|
|
232
|
+
}) {
|
|
233
|
+
const engine = useDataEngine();
|
|
234
|
+
const [value, setValue] = useFieldValue([name]);
|
|
235
|
+
const resolved = resolveNode(node, value);
|
|
236
|
+
const members = selectMembers(resolved);
|
|
237
|
+
|
|
238
|
+
let editor: React.ReactNode;
|
|
239
|
+
if (members) {
|
|
240
|
+
editor = (
|
|
241
|
+
<SelectEditor
|
|
242
|
+
name={name}
|
|
243
|
+
value={value}
|
|
244
|
+
setValue={setValue}
|
|
245
|
+
members={members}
|
|
246
|
+
/>
|
|
247
|
+
);
|
|
248
|
+
} else if (resolved.type === 'boolean') {
|
|
249
|
+
editor = <BooleanEditor name={name} value={value} setValue={setValue} />;
|
|
250
|
+
} else if (resolved.type === 'number' || resolved.type === 'bigint') {
|
|
251
|
+
editor = <NumberEditor name={name} value={value} setValue={setValue} />;
|
|
252
|
+
} else if (resolved.type === 'string' || resolved.type === 'literal') {
|
|
253
|
+
editor = (
|
|
254
|
+
<StringEditor
|
|
255
|
+
name={name}
|
|
256
|
+
value={value}
|
|
257
|
+
setValue={setValue}
|
|
258
|
+
multiline={multiline}
|
|
259
|
+
/>
|
|
260
|
+
);
|
|
261
|
+
} else {
|
|
262
|
+
editor = <JsonEditor name={name} value={value} setValue={setValue} />;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<div className="flex min-h-10 items-center border-b last:border-0">
|
|
267
|
+
<span
|
|
268
|
+
className="flex w-28 shrink-0 items-center gap-1 px-3 font-mono text-[0.8125rem] text-fd-muted-foreground"
|
|
269
|
+
title={description}
|
|
270
|
+
>
|
|
271
|
+
<span className="truncate">{name}</span>
|
|
272
|
+
{!required && <span className="text-fd-muted-foreground/60">?</span>}
|
|
273
|
+
</span>
|
|
274
|
+
{editor}
|
|
275
|
+
<span className="hidden w-24 shrink-0 truncate px-2 font-mono text-xs text-fd-muted-foreground/70 @3xl:block">
|
|
276
|
+
{typeToString(resolved)}
|
|
277
|
+
</span>
|
|
278
|
+
{!required && (
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
aria-label={`Unset ${name}`}
|
|
282
|
+
onClick={() => engine.delete([name])}
|
|
283
|
+
className="me-2 flex size-6 shrink-0 items-center justify-center rounded-md text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-foreground"
|
|
284
|
+
>
|
|
285
|
+
<X className="size-3" />
|
|
286
|
+
</button>
|
|
287
|
+
)}
|
|
288
|
+
</div>
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function StoryFields({
|
|
293
|
+
node,
|
|
294
|
+
storyKey,
|
|
295
|
+
}: {
|
|
296
|
+
node: TypeNode;
|
|
297
|
+
storyKey: string;
|
|
298
|
+
}) {
|
|
299
|
+
const meta = directiveMeta(storyKey);
|
|
300
|
+
if (node.type !== 'object') return null;
|
|
301
|
+
const multiline = new Set(meta?.multiline ?? []);
|
|
302
|
+
const descriptions = meta?.descriptions ?? {};
|
|
303
|
+
return (
|
|
304
|
+
<div className="text-[0.8125rem]">
|
|
305
|
+
{node.properties.map((prop) => (
|
|
306
|
+
<Field
|
|
307
|
+
key={prop.name}
|
|
308
|
+
name={prop.name}
|
|
309
|
+
node={prop.type}
|
|
310
|
+
required={prop.required}
|
|
311
|
+
multiline={multiline.has(prop.name)}
|
|
312
|
+
description={descriptions[prop.name]}
|
|
313
|
+
/>
|
|
314
|
+
))}
|
|
315
|
+
</div>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|