@backmatter/studio 0.0.16 → 0.0.18

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.
Files changed (45) hide show
  1. package/dist/chunks/{UiKitApp-_EDqWID3.js → UiKitApp-DkuQBiQZ.js} +2 -2
  2. package/dist/chunks/{WorkspaceRoutes-B2PrFgpz.js → WorkspaceRoutes-DVvb3N-V.js} +27489 -26622
  3. package/dist/chunks/{dashboard-BnlnMnnr.js → dashboard-B4FITg6c.js} +2 -2
  4. package/dist/chunks/{define-studio-BYtYDGLX.js → define-studio-DlYjgrwr.js} +12 -16
  5. package/dist/chunks/{index-D_BHF_8q.js → index-CegtXnEI.js} +1 -1
  6. package/dist/dashboard.js +2 -2
  7. package/dist/define.js +1 -1
  8. package/dist/index.js +4 -4
  9. package/dist/site-widget.css +45 -1
  10. package/dist/site-widget.js +2799 -2791
  11. package/dist/studio.css +113 -15
  12. package/package.json +2 -2
  13. package/src/api.ts +22 -0
  14. package/src/chat/ChatProvider.tsx +6 -0
  15. package/src/chat/Composer.tsx +3 -1
  16. package/src/chrome/Toast.tsx +115 -0
  17. package/src/chrome/WorkspaceStatusBar.tsx +12 -3
  18. package/src/chrome/commit-error.ts +13 -0
  19. package/src/chrome/pull-failure.ts +61 -0
  20. package/src/{pages/code-session-routes.ts → content-routes.ts} +6 -6
  21. package/src/fields/FieldControl.tsx +771 -0
  22. package/src/fields/LinkFieldInput.tsx +315 -0
  23. package/src/fields/choose-field-input.ts +69 -0
  24. package/src/fields/link-targets.ts +60 -0
  25. package/src/fields/load-relation-documents.ts +35 -0
  26. package/src/fields/omit-field.ts +12 -0
  27. package/src/fields/relation-targets.ts +44 -0
  28. package/src/fields/resolve-object-fields.ts +21 -0
  29. package/src/host-path.ts +13 -0
  30. package/src/pages/BlockSettingsDialog.tsx +19 -115
  31. package/src/pages/BlocksList.tsx +43 -35
  32. package/src/pages/BodyProse.tsx +16 -1
  33. package/src/pages/CodeSessionPage.tsx +186 -248
  34. package/src/pages/DocEditorPage.tsx +168 -213
  35. package/src/pages/MediaFieldInput.tsx +609 -26
  36. package/src/pages/MediaGalleryPage.tsx +14 -3
  37. package/src/pages/PreviewAddress.tsx +263 -0
  38. package/src/pages/RenderedBody.tsx +17 -2
  39. package/src/pages/branch-notice.ts +16 -0
  40. package/src/pages/code-session-boot.ts +0 -12
  41. package/src/pages/code-session-context.tsx +2 -0
  42. package/src/pages/media-selection.ts +79 -0
  43. package/src/shell-resets.css +15 -0
  44. package/src/ui/Input.tsx +178 -20
  45. package/src/ui/popup.ts +11 -2
@@ -0,0 +1,771 @@
1
+ // packages/studio/src/fields/FieldControl.tsx
2
+ import { useEffect, useState, type ReactNode } from "react";
3
+ import * as stylex from "@stylexjs/stylex";
4
+ import { documentLabel, matchesWhere } from "@backmatter/core";
5
+ import type { SchemaField } from "../api";
6
+ import { resolveFieldInput, useStudioDefinition } from "../define-studio";
7
+ import { colors, radius, space, type } from "../styles/tokens.stylex";
8
+ import { Input, Select, Textarea } from "../ui/Input";
9
+ import { MediaFieldInput } from "../pages/MediaFieldInput";
10
+ import { chooseFieldInput } from "./choose-field-input";
11
+ import { LinkFieldInput } from "./LinkFieldInput";
12
+ import { nestedStartsCollapsed, resolveObjectFields } from "./resolve-object-fields";
13
+
14
+ function clamp(n: number, min: number, max: number): number {
15
+ return Math.min(max, Math.max(min, n));
16
+ }
17
+
18
+ function hexColorValue(value: unknown): string {
19
+ if (typeof value === "string" && /^#[0-9a-fA-F]{6}$/.test(value)) {
20
+ return value.toLowerCase();
21
+ }
22
+ return "#000000";
23
+ }
24
+
25
+ function nestFieldName(name: string): string {
26
+ const dot = name.lastIndexOf(".");
27
+ if (dot >= 0) return name.slice(dot + 1);
28
+ return name;
29
+ }
30
+
31
+ function objectRecord(value: unknown): Record<string, unknown> {
32
+ if (value && typeof value === "object" && !Array.isArray(value)) {
33
+ return value as Record<string, unknown>;
34
+ }
35
+ return {};
36
+ }
37
+
38
+ function arrayValue(value: unknown): unknown[] {
39
+ return Array.isArray(value) ? value : [];
40
+ }
41
+
42
+ function jsonFieldText(value: unknown, kind: "object" | "array"): string {
43
+ if (typeof value === "string") return value;
44
+ return JSON.stringify(value ?? (kind === "array" ? [] : {}), null, 2);
45
+ }
46
+
47
+ export function JsonField({
48
+ value,
49
+ kind,
50
+ readOnly,
51
+ onValidChange,
52
+ onDraftChange,
53
+ }: {
54
+ value: unknown;
55
+ kind: "object" | "array";
56
+ readOnly?: boolean;
57
+ onValidChange: (next: unknown) => void;
58
+ onDraftChange?: (text: string) => void;
59
+ }) {
60
+ const serialized = jsonFieldText(value, kind);
61
+ const [draft, setDraft] = useState(serialized);
62
+ const [focused, setFocused] = useState(false);
63
+ const [invalid, setInvalid] = useState(false);
64
+ useEffect(() => {
65
+ if (!focused) {
66
+ setDraft(serialized);
67
+ setInvalid(false);
68
+ }
69
+ }, [serialized, focused]);
70
+ return (
71
+ <Textarea
72
+ value={draft}
73
+ readOnly={readOnly}
74
+ aria-invalid={invalid || undefined}
75
+ onFocus={() => setFocused(true)}
76
+ onBlur={() => setFocused(false)}
77
+ onChange={(e) => {
78
+ const next = e.target.value;
79
+ setDraft(next);
80
+ onDraftChange?.(next);
81
+ try {
82
+ onValidChange(JSON.parse(next || (kind === "array" ? "[]" : "{}")));
83
+ setInvalid(false);
84
+ } catch {
85
+ setInvalid(true);
86
+ }
87
+ }}
88
+ />
89
+ );
90
+ }
91
+
92
+ type ModelRef = {
93
+ type: string;
94
+ external?: boolean;
95
+ route?: string | false;
96
+ indexSlug?: string;
97
+ fields?: Record<string, SchemaField>;
98
+ };
99
+ type RelationDocument = { slug: string; frontmatter: Record<string, unknown> };
100
+ type RelationDocumentLists = Record<string, RelationDocument[]>;
101
+
102
+ function relationDocumentsForField(
103
+ field: SchemaField,
104
+ documents: RelationDocument[] | undefined,
105
+ documentLists: RelationDocumentLists | undefined,
106
+ ): RelationDocument[] | undefined {
107
+ if (documents !== undefined) return documents;
108
+ if (field.kind === "relation" && field.relationTo && documentLists) {
109
+ return documentLists[field.relationTo];
110
+ }
111
+ return undefined;
112
+ }
113
+
114
+ type SharedFieldProps = {
115
+ name: string;
116
+ field: SchemaField;
117
+ value: unknown;
118
+ models: ModelRef[];
119
+ objects: Array<{ name: string; fields: Record<string, SchemaField> }>;
120
+ documents?: RelationDocument[];
121
+ documentLists?: RelationDocumentLists;
122
+ readOnly?: boolean;
123
+ ancestorObjectNames: string[];
124
+ blocksEditor?: (props: {
125
+ value: unknown;
126
+ readOnly?: boolean;
127
+ onChange: (value: unknown) => void;
128
+ }) => ReactNode;
129
+ onChange: (value: unknown) => void;
130
+ };
131
+
132
+ function RelationFieldInput({
133
+ name,
134
+ field,
135
+ value,
136
+ models,
137
+ documents,
138
+ readOnly,
139
+ onChange,
140
+ }: {
141
+ name: string;
142
+ field: SchemaField;
143
+ value: unknown;
144
+ models: ModelRef[];
145
+ documents?: RelationDocument[];
146
+ readOnly?: boolean;
147
+ onChange: (value: unknown) => void;
148
+ }) {
149
+ const slugValue = value == null || value === "" ? "" : String(value);
150
+ const targetModel = models.find((model) => model.type === field.relationTo);
151
+ const labelFor = (doc: RelationDocument) =>
152
+ documentLabel(doc.frontmatter, targetModel ?? null) ?? doc.slug;
153
+ const offered =
154
+ documents === undefined
155
+ ? []
156
+ : documents.filter((doc) =>
157
+ matchesWhere({ slug: doc.slug, frontmatter: doc.frontmatter }, field.where),
158
+ );
159
+ const options = offered.map((doc) => ({ value: doc.slug, label: labelFor(doc) }));
160
+ if (slugValue && !options.some((option) => option.value === slugValue)) {
161
+ options.unshift({ value: slugValue, label: slugValue });
162
+ }
163
+
164
+ return (
165
+ <Select
166
+ name={name}
167
+ chip
168
+ searchable
169
+ clearable={!field.required}
170
+ placeholder="Select a value"
171
+ options={options}
172
+ value={slugValue || null}
173
+ disabled={readOnly}
174
+ onValueChange={(next) => {
175
+ if (readOnly) return;
176
+ onChange(next == null ? undefined : next);
177
+ }}
178
+ />
179
+ );
180
+ }
181
+
182
+ function CollapsibleNest({
183
+ fieldName,
184
+ objectName,
185
+ ancestorObjectNames,
186
+ children,
187
+ }: {
188
+ fieldName: string;
189
+ objectName: string | undefined;
190
+ ancestorObjectNames: string[];
191
+ readOnly?: boolean;
192
+ children: ReactNode;
193
+ }) {
194
+ const startsCollapsed = nestedStartsCollapsed(ancestorObjectNames, objectName);
195
+ const [expanded, setExpanded] = useState(!startsCollapsed);
196
+ if (!objectName) {
197
+ return <>{children}</>;
198
+ }
199
+ return (
200
+ <div {...stylex.props(styles.nest)}>
201
+ <button
202
+ type="button"
203
+ data-nest-toggle={fieldName}
204
+ {...stylex.props(styles.nestToggle)}
205
+ onClick={() => setExpanded((open) => !open)}
206
+ >
207
+ {expanded ? "Collapse" : "Expand"} {fieldName}
208
+ </button>
209
+ {expanded ? (
210
+ <div data-nest={fieldName} {...stylex.props(styles.nestBody)}>
211
+ {children}
212
+ </div>
213
+ ) : null}
214
+ </div>
215
+ );
216
+ }
217
+
218
+ function ObjectFieldEditor({
219
+ name,
220
+ field,
221
+ value,
222
+ models,
223
+ objects,
224
+ documentLists,
225
+ readOnly,
226
+ ancestorObjectNames,
227
+ blocksEditor,
228
+ onChange,
229
+ }: SharedFieldProps) {
230
+ const resolved = resolveObjectFields(field, objects);
231
+ if (!resolved) {
232
+ return (
233
+ <JsonField
234
+ kind="object"
235
+ value={value}
236
+ readOnly={readOnly}
237
+ onValidChange={onChange}
238
+ />
239
+ );
240
+ }
241
+
242
+ const record = objectRecord(value);
243
+ const objectName = field.object;
244
+ const nextAncestors = objectName ? [...ancestorObjectNames, objectName] : ancestorObjectNames;
245
+
246
+ const body = (
247
+ <div {...stylex.props(styles.objectFields)}>
248
+ {Object.entries(resolved).map(([fieldName, nestedField]) => (
249
+ <FieldControl
250
+ key={fieldName}
251
+ name={`${name}.${fieldName}`}
252
+ field={nestedField}
253
+ value={record[fieldName]}
254
+ models={models}
255
+ objects={objects}
256
+ documentLists={documentLists}
257
+ readOnly={readOnly}
258
+ ancestorObjectNames={nextAncestors}
259
+ blocksEditor={blocksEditor}
260
+ onChange={(next) => {
261
+ onChange({ ...record, [fieldName]: next });
262
+ }}
263
+ />
264
+ ))}
265
+ </div>
266
+ );
267
+
268
+ if (
269
+ !objectName ||
270
+ !nestedStartsCollapsed(ancestorObjectNames, objectName) ||
271
+ /\[\d+\]$/.test(name)
272
+ ) {
273
+ return body;
274
+ }
275
+
276
+ return (
277
+ <CollapsibleNest
278
+ fieldName={nestFieldName(name)}
279
+ objectName={objectName}
280
+ ancestorObjectNames={ancestorObjectNames}
281
+ readOnly={readOnly}
282
+ >
283
+ {body}
284
+ </CollapsibleNest>
285
+ );
286
+ }
287
+
288
+ function ArrayFieldEditor({
289
+ name,
290
+ field,
291
+ value,
292
+ models,
293
+ objects,
294
+ documentLists,
295
+ readOnly,
296
+ ancestorObjectNames,
297
+ blocksEditor,
298
+ onChange,
299
+ }: SharedFieldProps) {
300
+ const resolved = resolveObjectFields(field, objects);
301
+ if (!resolved) {
302
+ return (
303
+ <JsonField
304
+ kind="array"
305
+ value={value}
306
+ readOnly={readOnly}
307
+ onValidChange={onChange}
308
+ />
309
+ );
310
+ }
311
+
312
+ const items = arrayValue(value);
313
+ const itemField: SchemaField = { kind: "object", object: field.object, fields: resolved };
314
+ const objectName = field.object;
315
+ const nextAncestors = objectName ? [...ancestorObjectNames, objectName] : ancestorObjectNames;
316
+
317
+ const list = (
318
+ <div {...stylex.props(styles.arrayList)}>
319
+ {items.map((item, index) => (
320
+ <div key={index} {...stylex.props(styles.arrayItem)}>
321
+ <FieldControl
322
+ name={`${name}[${index}]`}
323
+ field={itemField}
324
+ value={item}
325
+ models={models}
326
+ objects={objects}
327
+ documentLists={documentLists}
328
+ readOnly={readOnly}
329
+ ancestorObjectNames={nextAncestors}
330
+ blocksEditor={blocksEditor}
331
+ onChange={(next) => {
332
+ const nextItems = [...items];
333
+ nextItems[index] = next;
334
+ onChange(nextItems);
335
+ }}
336
+ />
337
+ <div {...stylex.props(styles.arrayActions)}>
338
+ <button
339
+ type="button"
340
+ data-action="array-up"
341
+ disabled={readOnly || index === 0}
342
+ onClick={() => {
343
+ if (index === 0) return;
344
+ const nextItems = [...items];
345
+ const prev = nextItems[index - 1];
346
+ nextItems[index - 1] = nextItems[index]!;
347
+ nextItems[index] = prev!;
348
+ onChange(nextItems);
349
+ }}
350
+ >
351
+ Up
352
+ </button>
353
+ <button
354
+ type="button"
355
+ data-action="array-down"
356
+ disabled={readOnly || index === items.length - 1}
357
+ onClick={() => {
358
+ if (index >= items.length - 1) return;
359
+ const nextItems = [...items];
360
+ const next = nextItems[index + 1];
361
+ nextItems[index + 1] = nextItems[index]!;
362
+ nextItems[index] = next!;
363
+ onChange(nextItems);
364
+ }}
365
+ >
366
+ Down
367
+ </button>
368
+ <button
369
+ type="button"
370
+ data-action="array-remove"
371
+ disabled={readOnly}
372
+ onClick={() => {
373
+ onChange(items.filter((_, i) => i !== index));
374
+ }}
375
+ >
376
+ Remove
377
+ </button>
378
+ </div>
379
+ </div>
380
+ ))}
381
+ <button
382
+ type="button"
383
+ data-action="array-add"
384
+ disabled={readOnly}
385
+ onClick={() => onChange([...items, {}])}
386
+ >
387
+ Add
388
+ </button>
389
+ </div>
390
+ );
391
+
392
+ if (!objectName || !nestedStartsCollapsed(ancestorObjectNames, objectName)) {
393
+ return list;
394
+ }
395
+
396
+ return (
397
+ <CollapsibleNest
398
+ fieldName={nestFieldName(name)}
399
+ objectName={objectName}
400
+ ancestorObjectNames={ancestorObjectNames}
401
+ readOnly={readOnly}
402
+ >
403
+ {list}
404
+ </CollapsibleNest>
405
+ );
406
+ }
407
+
408
+ function commitNumber(raw: string, onChange: (value: unknown) => void): void {
409
+ if (raw.trim() === "") {
410
+ onChange(undefined);
411
+ return;
412
+ }
413
+ const parsed = Number(raw);
414
+ if (!Number.isFinite(parsed)) return;
415
+ onChange(parsed);
416
+ }
417
+
418
+ export function FieldControl({
419
+ name,
420
+ field,
421
+ value,
422
+ models,
423
+ objects = [],
424
+ documents,
425
+ documentLists,
426
+ readOnly,
427
+ ancestorObjectNames = [],
428
+ blocksEditor,
429
+ onChange,
430
+ }: {
431
+ name: string;
432
+ field: SchemaField;
433
+ value: unknown;
434
+ models: ModelRef[];
435
+ objects?: Array<{ name: string; fields: Record<string, SchemaField> }>;
436
+ documents?: RelationDocument[];
437
+ documentLists?: RelationDocumentLists;
438
+ readOnly?: boolean;
439
+ ancestorObjectNames?: string[];
440
+ blocksEditor?: (props: {
441
+ value: unknown;
442
+ readOnly?: boolean;
443
+ onChange: (value: unknown) => void;
444
+ }) => ReactNode;
445
+ onChange: (value: unknown) => void;
446
+ }) {
447
+ const definition = useStudioDefinition();
448
+ const kind = chooseFieldInput(definition, field, models);
449
+
450
+ if (kind === "custom") {
451
+ const CustomInput = resolveFieldInput(definition, field);
452
+ if (CustomInput) {
453
+ return (
454
+ <CustomInput name={name} field={field} value={value} readOnly={readOnly} onChange={onChange} />
455
+ );
456
+ }
457
+ }
458
+
459
+ if (kind === "select" && field.options) {
460
+ const options = field.options.map((opt) => ({ value: opt, label: opt }));
461
+ const selectValue = value == null || value === "" ? null : String(value);
462
+ return (
463
+ <Select
464
+ name={name}
465
+ chip
466
+ clearable={!field.required}
467
+ placeholder="Select a value"
468
+ options={options}
469
+ value={selectValue}
470
+ disabled={readOnly}
471
+ onValueChange={(next) => {
472
+ if (readOnly) return;
473
+ if (next == null) {
474
+ onChange(undefined);
475
+ return;
476
+ }
477
+ onChange(next);
478
+ }}
479
+ />
480
+ );
481
+ }
482
+
483
+ if (kind === "slider" && typeof field.min === "number" && typeof field.max === "number") {
484
+ const min = field.min;
485
+ const max = field.max;
486
+ const step = typeof field.step === "number" ? field.step : "any";
487
+ const numeric = typeof value === "number" ? value : value === "" ? NaN : Number(value);
488
+ const rangeValue = Number.isFinite(numeric) ? clamp(numeric, min, max) : min;
489
+ const textValue = value == null || value === "" ? "" : String(value);
490
+ return (
491
+ <div {...stylex.props(styles.sliderRow)}>
492
+ <input
493
+ type="range"
494
+ min={min}
495
+ max={max}
496
+ step={step}
497
+ value={String(rangeValue)}
498
+ disabled={readOnly}
499
+ aria-valuemin={min}
500
+ aria-valuemax={max}
501
+ aria-valuenow={rangeValue}
502
+ {...stylex.props(styles.range)}
503
+ onChange={(event) => {
504
+ if (readOnly) return;
505
+ onChange(Number(event.target.value));
506
+ }}
507
+ />
508
+ <input
509
+ type="text"
510
+ inputMode="decimal"
511
+ value={textValue}
512
+ readOnly={readOnly}
513
+ disabled={readOnly}
514
+ aria-label={`${name} value`}
515
+ {...stylex.props(styles.sliderText)}
516
+ onChange={(event) => {
517
+ if (readOnly) return;
518
+ commitNumber(event.target.value, onChange);
519
+ }}
520
+ />
521
+ </div>
522
+ );
523
+ }
524
+
525
+ if (kind === "color") {
526
+ const textValue = value == null ? "" : String(value);
527
+ return (
528
+ <div {...stylex.props(styles.colorRow)}>
529
+ <input
530
+ type="color"
531
+ value={hexColorValue(value)}
532
+ disabled={readOnly}
533
+ {...stylex.props(styles.colorPicker)}
534
+ onChange={(event) => {
535
+ if (readOnly) return;
536
+ onChange(event.target.value);
537
+ }}
538
+ />
539
+ <Input
540
+ value={textValue}
541
+ readOnly={readOnly}
542
+ onChange={(event) => {
543
+ if (readOnly) return;
544
+ onChange(event.target.value);
545
+ }}
546
+ />
547
+ </div>
548
+ );
549
+ }
550
+
551
+ if (kind === "media") {
552
+ return (
553
+ <MediaFieldInput name={name} field={field} value={value} readOnly={readOnly} onChange={onChange} />
554
+ );
555
+ }
556
+
557
+ if (kind === "relation") {
558
+ return (
559
+ <RelationFieldInput
560
+ name={name}
561
+ field={field}
562
+ value={value}
563
+ models={models}
564
+ documents={relationDocumentsForField(field, documents, documentLists)}
565
+ readOnly={readOnly}
566
+ onChange={onChange}
567
+ />
568
+ );
569
+ }
570
+
571
+ if (kind === "link") {
572
+ return (
573
+ <LinkFieldInput
574
+ name={name}
575
+ field={field}
576
+ value={value}
577
+ models={models}
578
+ documentLists={documentLists}
579
+ readOnly={readOnly}
580
+ onChange={onChange}
581
+ />
582
+ );
583
+ }
584
+
585
+ if (kind === "boolean") {
586
+ return (
587
+ <input
588
+ type="checkbox"
589
+ checked={value === true}
590
+ disabled={readOnly}
591
+ {...stylex.props(styles.check)}
592
+ onChange={(event) => {
593
+ if (readOnly) return;
594
+ onChange(event.target.checked);
595
+ }}
596
+ />
597
+ );
598
+ }
599
+
600
+ if (kind === "number") {
601
+ return (
602
+ <Input
603
+ type="number"
604
+ value={value == null || value === "" ? "" : String(value)}
605
+ readOnly={readOnly}
606
+ onChange={(event) => {
607
+ if (readOnly) return;
608
+ commitNumber(event.target.value, onChange);
609
+ }}
610
+ />
611
+ );
612
+ }
613
+
614
+ if (kind === "blocks") {
615
+ if (blocksEditor) {
616
+ return <>{blocksEditor({ value, readOnly, onChange })}</>;
617
+ }
618
+ return (
619
+ <JsonField kind="array" value={value} readOnly={readOnly} onValidChange={onChange} />
620
+ );
621
+ }
622
+
623
+ if (kind === "object") {
624
+ return (
625
+ <ObjectFieldEditor
626
+ name={name}
627
+ field={field}
628
+ value={value}
629
+ models={models}
630
+ objects={objects}
631
+ documentLists={documentLists}
632
+ readOnly={readOnly}
633
+ ancestorObjectNames={ancestorObjectNames}
634
+ blocksEditor={blocksEditor}
635
+ onChange={onChange}
636
+ />
637
+ );
638
+ }
639
+
640
+ if (kind === "array") {
641
+ return (
642
+ <ArrayFieldEditor
643
+ name={name}
644
+ field={field}
645
+ value={value}
646
+ models={models}
647
+ objects={objects}
648
+ documentLists={documentLists}
649
+ readOnly={readOnly}
650
+ ancestorObjectNames={ancestorObjectNames}
651
+ blocksEditor={blocksEditor}
652
+ onChange={onChange}
653
+ />
654
+ );
655
+ }
656
+
657
+ if ("multiline" in field && field.multiline) {
658
+ return (
659
+ <Textarea
660
+ rows={4}
661
+ value={value == null ? "" : String(value)}
662
+ readOnly={readOnly}
663
+ onChange={(event) => {
664
+ if (readOnly) return;
665
+ onChange(event.target.value);
666
+ }}
667
+ />
668
+ );
669
+ }
670
+
671
+ return (
672
+ <Input
673
+ value={value == null ? "" : String(value)}
674
+ readOnly={readOnly}
675
+ onChange={(event) => {
676
+ if (readOnly) return;
677
+ onChange(event.target.value);
678
+ }}
679
+ />
680
+ );
681
+ }
682
+
683
+ const styles = stylex.create({
684
+ sliderRow: {
685
+ display: "flex",
686
+ alignItems: "center",
687
+ gap: space.md,
688
+ },
689
+ range: {
690
+ flex: 1,
691
+ minWidth: 0,
692
+ height: "16px",
693
+ margin: 0,
694
+ accentColor: colors.text,
695
+ cursor: "pointer",
696
+ },
697
+ sliderText: {
698
+ boxSizing: "border-box",
699
+ width: "4.5rem",
700
+ flexShrink: 0,
701
+ minHeight: "32px",
702
+ paddingInline: space.md,
703
+ paddingBlock: space.sm,
704
+ borderRadius: radius.md,
705
+ borderWidth: 1,
706
+ borderStyle: "solid",
707
+ borderColor: colors.border,
708
+ backgroundColor: colors.bgInput,
709
+ color: colors.text,
710
+ fontSize: type.md,
711
+ fontVariantNumeric: "tabular-nums",
712
+ outline: "none",
713
+ ":focus": {
714
+ borderColor: colors.focus,
715
+ outlineWidth: 2,
716
+ outlineStyle: "solid",
717
+ outlineColor: colors.focus,
718
+ outlineOffset: -1,
719
+ },
720
+ },
721
+ colorRow: {
722
+ display: "flex",
723
+ alignItems: "center",
724
+ gap: space.sm,
725
+ },
726
+ colorPicker: {
727
+ width: "2.5rem",
728
+ height: "2rem",
729
+ padding: 0,
730
+ borderWidth: 0,
731
+ backgroundColor: "transparent",
732
+ cursor: "pointer",
733
+ },
734
+ check: {
735
+ width: "1rem",
736
+ height: "1rem",
737
+ },
738
+ objectFields: {
739
+ display: "flex",
740
+ flexDirection: "column",
741
+ gap: space.sm,
742
+ },
743
+ nest: {
744
+ display: "flex",
745
+ flexDirection: "column",
746
+ gap: space.xs,
747
+ },
748
+ nestToggle: {
749
+ alignSelf: "flex-start",
750
+ },
751
+ nestBody: {
752
+ display: "flex",
753
+ flexDirection: "column",
754
+ gap: space.sm,
755
+ },
756
+ arrayList: {
757
+ display: "flex",
758
+ flexDirection: "column",
759
+ gap: space.md,
760
+ },
761
+ arrayItem: {
762
+ display: "flex",
763
+ flexDirection: "column",
764
+ gap: space.sm,
765
+ },
766
+ arrayActions: {
767
+ display: "flex",
768
+ flexWrap: "wrap",
769
+ gap: space.xs,
770
+ },
771
+ });