@terrafirmagreg-team/mdx 1.2.1 → 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.
@@ -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
+