@bendyline/squisq-editor-react 2.2.0 → 2.3.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.
@@ -0,0 +1,862 @@
1
+ import {
2
+ markdownToTiptap,
3
+ tiptapToMarkdown
4
+ } from "./chunk-NITZVAXL.js";
5
+
6
+ // src/jsonEditor/JsonEditorContext.tsx
7
+ import { createContext, useContext, useEffect, useMemo } from "react";
8
+ import { setByPointer } from "@bendyline/squisq/jsonForm";
9
+ import { jsx } from "react/jsx-runtime";
10
+ var JsonEditorContext = createContext(null);
11
+ function JsonEditorProvider(props) {
12
+ const { rootSchema, rootData, onRootChange, density, validate, onValidate, children } = props;
13
+ const validationErrors = useMemo(
14
+ () => validate ? validate(rootData, rootSchema) : [],
15
+ [validate, rootData, rootSchema]
16
+ );
17
+ const errors = useMemo(() => {
18
+ const grouped = /* @__PURE__ */ new Map();
19
+ for (const e of validationErrors) {
20
+ const arr = grouped.get(e.path) ?? [];
21
+ arr.push(e);
22
+ grouped.set(e.path, arr);
23
+ }
24
+ return grouped;
25
+ }, [validationErrors]);
26
+ useEffect(() => {
27
+ if (!onValidate) return;
28
+ onValidate(validationErrors);
29
+ }, [validationErrors, onValidate]);
30
+ const setAtPath = useMemo(
31
+ () => (pointer, value2) => {
32
+ const next = setByPointer(rootData, pointer, value2);
33
+ onRootChange(next);
34
+ },
35
+ [rootData, onRootChange]
36
+ );
37
+ const value = useMemo(
38
+ () => ({ rootSchema, rootData, setAtPath, density, errors }),
39
+ [rootSchema, rootData, setAtPath, density, errors]
40
+ );
41
+ return /* @__PURE__ */ jsx(JsonEditorContext.Provider, { value, children });
42
+ }
43
+ function useJsonEditor() {
44
+ const ctx = useContext(JsonEditorContext);
45
+ if (!ctx) throw new Error("useJsonEditor must be used inside <JsonEditor>");
46
+ return ctx;
47
+ }
48
+
49
+ // src/jsonEditor/RenderNode.tsx
50
+ import {
51
+ chooseControl,
52
+ resolveFlag,
53
+ resolveRef
54
+ } from "@bendyline/squisq/jsonForm";
55
+ import { useId as useId2 } from "react";
56
+
57
+ // src/jsonEditor/editors.tsx
58
+ import { useEffect as useEffect3, useId, useRef as useRef2, useState } from "react";
59
+ import {
60
+ appendPointer,
61
+ arrayItemKind
62
+ } from "@bendyline/squisq/jsonForm";
63
+
64
+ // src/jsonEditor/EmbeddedRichTextField.tsx
65
+ import { useEffect as useEffect2, useRef } from "react";
66
+ import { useEditor, EditorContent } from "@tiptap/react";
67
+ import StarterKit from "@tiptap/starter-kit";
68
+ import Table from "@tiptap/extension-table";
69
+ import TableRow from "@tiptap/extension-table-row";
70
+ import TableCell from "@tiptap/extension-table-cell";
71
+ import TableHeader from "@tiptap/extension-table-header";
72
+ import TaskList from "@tiptap/extension-task-list";
73
+ import TaskItem from "@tiptap/extension-task-item";
74
+ import Placeholder from "@tiptap/extension-placeholder";
75
+ import { jsx as jsx2 } from "react/jsx-runtime";
76
+ function EmbeddedRichTextField(props) {
77
+ const { value, onChange, readOnly = false, placeholder, className } = props;
78
+ const isExternalUpdate = useRef(false);
79
+ const lastValueRef = useRef(value);
80
+ const editor = useEditor({
81
+ editable: !readOnly,
82
+ extensions: [
83
+ StarterKit.configure({
84
+ codeBlock: { HTMLAttributes: { class: "squisq-code-block" } }
85
+ }),
86
+ Table.configure({ resizable: false }),
87
+ TableRow,
88
+ TableCell,
89
+ TableHeader,
90
+ TaskList,
91
+ TaskItem.configure({ nested: true }),
92
+ Placeholder.configure({ placeholder: placeholder ?? "" })
93
+ ],
94
+ content: markdownToTiptap(value),
95
+ onUpdate: ({ editor: ed }) => {
96
+ if (isExternalUpdate.current) return;
97
+ const html = ed.getHTML();
98
+ const md = tiptapToMarkdown(html);
99
+ lastValueRef.current = md;
100
+ onChange(md);
101
+ },
102
+ editorProps: {
103
+ attributes: {
104
+ class: "squisq-jf-richtext-prose"
105
+ }
106
+ }
107
+ });
108
+ useEffect2(() => {
109
+ if (!editor) return;
110
+ if (value === lastValueRef.current) return;
111
+ isExternalUpdate.current = true;
112
+ try {
113
+ editor.commands.setContent(markdownToTiptap(value), false);
114
+ lastValueRef.current = value;
115
+ } finally {
116
+ isExternalUpdate.current = false;
117
+ }
118
+ }, [editor, value]);
119
+ useEffect2(() => {
120
+ if (editor) editor.setEditable(!readOnly);
121
+ }, [editor, readOnly]);
122
+ const cls = "squisq-jf-richtext" + (className ? ` ${className}` : "");
123
+ return /* @__PURE__ */ jsx2(EditorContent, { editor, className: cls });
124
+ }
125
+
126
+ // src/jsonEditor/editors.tsx
127
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
128
+ function accessibilityProps(props) {
129
+ return {
130
+ id: props.controlId,
131
+ "aria-labelledby": props.labelledBy,
132
+ "aria-describedby": props.describedBy,
133
+ "aria-invalid": props.invalid || void 0
134
+ };
135
+ }
136
+ function asString(v) {
137
+ if (v === void 0 || v === null) return "";
138
+ return String(v);
139
+ }
140
+ function asNumber(v) {
141
+ if (v === void 0 || v === null || v === "") return "";
142
+ const n = Number(v);
143
+ return Number.isFinite(n) ? n : "";
144
+ }
145
+ function TextEditor(props) {
146
+ const { value, schema, pointer, disabled } = props;
147
+ const { setAtPath } = useJsonEditor();
148
+ return /* @__PURE__ */ jsx3(
149
+ "input",
150
+ {
151
+ ...accessibilityProps(props),
152
+ type: "text",
153
+ className: "squisq-jf-input",
154
+ value: asString(value),
155
+ placeholder: schema.squisq?.placeholder,
156
+ disabled,
157
+ onChange: (e) => setAtPath(pointer, e.target.value)
158
+ }
159
+ );
160
+ }
161
+ function MultilineEditor(props) {
162
+ const { value, schema, pointer, disabled } = props;
163
+ const { setAtPath } = useJsonEditor();
164
+ const ref = useRef2(null);
165
+ useEffect3(() => {
166
+ const ta = ref.current;
167
+ if (!ta) return;
168
+ ta.style.height = "auto";
169
+ ta.style.height = `${ta.scrollHeight + 2}px`;
170
+ }, [value]);
171
+ return /* @__PURE__ */ jsx3(
172
+ "textarea",
173
+ {
174
+ ...accessibilityProps(props),
175
+ ref,
176
+ className: "squisq-jf-textarea",
177
+ value: asString(value),
178
+ placeholder: schema.squisq?.placeholder,
179
+ disabled,
180
+ onChange: (e) => setAtPath(pointer, e.target.value)
181
+ }
182
+ );
183
+ }
184
+ function RichTextEditor({ value, schema, pointer, disabled }) {
185
+ const { setAtPath } = useJsonEditor();
186
+ return /* @__PURE__ */ jsx3(
187
+ EmbeddedRichTextField,
188
+ {
189
+ value: asString(value),
190
+ placeholder: schema.squisq?.placeholder,
191
+ readOnly: disabled,
192
+ onChange: (md) => setAtPath(pointer, md)
193
+ }
194
+ );
195
+ }
196
+ function NumberStepperEditor(props) {
197
+ const { value, schema, pointer, disabled } = props;
198
+ const { setAtPath } = useJsonEditor();
199
+ const isInt = schema.type === "integer";
200
+ const step = schema.squisq?.step ?? schema.multipleOf ?? (isInt ? 1 : 0.1);
201
+ const num = asNumber(value);
202
+ const update = (next) => {
203
+ const min = schema.minimum ?? schema.exclusiveMinimum;
204
+ const max = schema.maximum ?? schema.exclusiveMaximum;
205
+ let clamped = next;
206
+ if (typeof min === "number") clamped = Math.max(min, clamped);
207
+ if (typeof max === "number") clamped = Math.min(max, clamped);
208
+ setAtPath(pointer, isInt ? Math.round(clamped) : clamped);
209
+ };
210
+ return /* @__PURE__ */ jsxs("span", { className: "squisq-jf-stepper", children: [
211
+ /* @__PURE__ */ jsx3(
212
+ "button",
213
+ {
214
+ type: "button",
215
+ className: "squisq-jf-stepper__btn",
216
+ disabled,
217
+ onClick: () => update((typeof num === "number" ? num : 0) - step),
218
+ "aria-label": "Decrease",
219
+ children: "\u2212"
220
+ }
221
+ ),
222
+ /* @__PURE__ */ jsx3(
223
+ "input",
224
+ {
225
+ ...accessibilityProps(props),
226
+ className: "squisq-jf-stepper__input",
227
+ type: "number",
228
+ value: num === "" ? "" : num,
229
+ step,
230
+ disabled,
231
+ onChange: (e) => {
232
+ if (e.target.value === "") {
233
+ setAtPath(pointer, void 0);
234
+ return;
235
+ }
236
+ const n = Number(e.target.value);
237
+ if (Number.isFinite(n)) update(n);
238
+ }
239
+ }
240
+ ),
241
+ /* @__PURE__ */ jsx3(
242
+ "button",
243
+ {
244
+ type: "button",
245
+ className: "squisq-jf-stepper__btn",
246
+ disabled,
247
+ onClick: () => update((typeof num === "number" ? num : 0) + step),
248
+ "aria-label": "Increase",
249
+ children: "+"
250
+ }
251
+ )
252
+ ] });
253
+ }
254
+ function SliderEditor(props) {
255
+ const { value, schema, pointer, disabled } = props;
256
+ const { setAtPath } = useJsonEditor();
257
+ const min = schema.minimum ?? schema.exclusiveMinimum ?? 0;
258
+ const max = schema.maximum ?? schema.exclusiveMaximum ?? 100;
259
+ const step = schema.squisq?.step ?? schema.multipleOf ?? (schema.type === "integer" ? 1 : 1);
260
+ const num = asNumber(value);
261
+ const display = typeof num === "number" ? num : min;
262
+ return /* @__PURE__ */ jsxs("span", { className: "squisq-jf-slider-row", children: [
263
+ /* @__PURE__ */ jsx3(
264
+ "input",
265
+ {
266
+ ...accessibilityProps(props),
267
+ type: "range",
268
+ className: "squisq-jf-slider",
269
+ min,
270
+ max,
271
+ step,
272
+ value: display,
273
+ disabled,
274
+ onChange: (e) => setAtPath(pointer, Number(e.target.value))
275
+ }
276
+ ),
277
+ /* @__PURE__ */ jsx3("span", { className: "squisq-jf-slider-readout", children: display })
278
+ ] });
279
+ }
280
+ function ToggleEditor(props) {
281
+ const { value, schema, pointer, disabled } = props;
282
+ const { setAtPath } = useJsonEditor();
283
+ const on = Boolean(value);
284
+ return /* @__PURE__ */ jsxs(
285
+ "button",
286
+ {
287
+ ...accessibilityProps(props),
288
+ type: "button",
289
+ className: `squisq-jf-toggle${on ? " squisq-jf-toggle--on" : ""}`,
290
+ disabled,
291
+ onClick: () => setAtPath(pointer, !on),
292
+ "aria-pressed": on,
293
+ children: [
294
+ /* @__PURE__ */ jsx3("span", { className: "squisq-jf-toggle__track", children: /* @__PURE__ */ jsx3("span", { className: "squisq-jf-toggle__thumb" }) }),
295
+ /* @__PURE__ */ jsx3("span", { className: "squisq-jf-toggle__label", children: on ? schema.squisq?.label ? `${schema.squisq.label}: On` : "On" : schema.squisq?.label ? `${schema.squisq.label}: Off` : "Off" })
296
+ ]
297
+ }
298
+ );
299
+ }
300
+ function CheckboxEditor(props) {
301
+ const { value, schema, pointer, disabled } = props;
302
+ const { setAtPath } = useJsonEditor();
303
+ const generatedId = useId();
304
+ const id = props.controlId ?? generatedId;
305
+ return /* @__PURE__ */ jsxs("label", { htmlFor: id, className: "squisq-jf-toggle", children: [
306
+ /* @__PURE__ */ jsx3(
307
+ "input",
308
+ {
309
+ "aria-labelledby": props.labelledBy,
310
+ "aria-describedby": props.describedBy,
311
+ "aria-invalid": props.invalid || void 0,
312
+ id,
313
+ type: "checkbox",
314
+ checked: Boolean(value),
315
+ disabled,
316
+ onChange: (e) => setAtPath(pointer, e.target.checked)
317
+ }
318
+ ),
319
+ schema.squisq?.label ?? schema.title ?? ""
320
+ ] });
321
+ }
322
+ function enumOptions(schema) {
323
+ const labels = schema.squisq?.enumLabels;
324
+ return (schema.enum ?? []).map((v) => ({
325
+ value: v,
326
+ label: labels && typeof v === "string" ? labels[v] ?? String(v) : String(v)
327
+ }));
328
+ }
329
+ function SegmentedEditor(props) {
330
+ const { value, schema, pointer, disabled } = props;
331
+ const { setAtPath } = useJsonEditor();
332
+ const options = enumOptions(schema);
333
+ return /* @__PURE__ */ jsx3("span", { ...accessibilityProps(props), className: "squisq-jf-segmented", role: "group", children: options.map((opt, i) => {
334
+ const active = value === opt.value;
335
+ return /* @__PURE__ */ jsx3(
336
+ "button",
337
+ {
338
+ type: "button",
339
+ className: `squisq-jf-segmented__btn${active ? " squisq-jf-segmented__btn--active" : ""}`,
340
+ disabled,
341
+ onClick: () => setAtPath(pointer, opt.value),
342
+ "aria-pressed": active,
343
+ children: opt.label
344
+ },
345
+ i
346
+ );
347
+ }) });
348
+ }
349
+ function RadioEditor(props) {
350
+ const { value, schema, pointer, disabled } = props;
351
+ const { setAtPath } = useJsonEditor();
352
+ const options = enumOptions(schema);
353
+ const name = useId();
354
+ return /* @__PURE__ */ jsx3("div", { ...accessibilityProps(props), className: "squisq-jf-radio", role: "radiogroup", children: options.map((opt, i) => {
355
+ const active = value === opt.value;
356
+ return /* @__PURE__ */ jsxs("label", { className: "squisq-jf-radio__option", children: [
357
+ /* @__PURE__ */ jsx3(
358
+ "input",
359
+ {
360
+ type: "radio",
361
+ name,
362
+ checked: active,
363
+ disabled,
364
+ onChange: () => setAtPath(pointer, opt.value)
365
+ }
366
+ ),
367
+ opt.label
368
+ ] }, i);
369
+ }) });
370
+ }
371
+ function ComboboxEditor(props) {
372
+ const { value, schema, pointer, disabled } = props;
373
+ const { setAtPath } = useJsonEditor();
374
+ const options = enumOptions(schema);
375
+ return /* @__PURE__ */ jsx3(
376
+ "select",
377
+ {
378
+ ...accessibilityProps(props),
379
+ className: "squisq-jf-select",
380
+ value: asString(value),
381
+ disabled,
382
+ onChange: (e) => {
383
+ const raw = e.target.value;
384
+ const matched = options.find((opt) => String(opt.value) === raw);
385
+ setAtPath(pointer, matched ? matched.value : raw);
386
+ },
387
+ children: options.map((opt, i) => /* @__PURE__ */ jsx3("option", { value: String(opt.value), children: opt.label }, i))
388
+ }
389
+ );
390
+ }
391
+ function ColorEditor(props) {
392
+ const { value, pointer, disabled } = props;
393
+ const { setAtPath } = useJsonEditor();
394
+ const hex = typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value) ? value : "#000000";
395
+ return /* @__PURE__ */ jsxs("span", { className: "squisq-jf-color", children: [
396
+ /* @__PURE__ */ jsx3(
397
+ "input",
398
+ {
399
+ ...accessibilityProps(props),
400
+ type: "color",
401
+ className: "squisq-jf-color__input",
402
+ value: hex,
403
+ disabled,
404
+ onChange: (e) => setAtPath(pointer, e.target.value)
405
+ }
406
+ ),
407
+ /* @__PURE__ */ jsx3(
408
+ "input",
409
+ {
410
+ type: "text",
411
+ className: "squisq-jf-input squisq-jf-color__hex",
412
+ value: asString(value),
413
+ disabled,
414
+ onChange: (e) => setAtPath(pointer, e.target.value)
415
+ }
416
+ )
417
+ ] });
418
+ }
419
+ function DateEditor(props) {
420
+ const { value, schema, pointer, disabled } = props;
421
+ const { setAtPath } = useJsonEditor();
422
+ const fmt = schema.format;
423
+ const inputType = fmt === "time" ? "time" : fmt === "date" ? "date" : "datetime-local";
424
+ const display = inputType === "datetime-local" && typeof value === "string" && value.includes("T") ? value.replace(/Z$|:\d{2}\.\d+Z?$/, "").slice(0, 16) : asString(value);
425
+ return /* @__PURE__ */ jsx3(
426
+ "input",
427
+ {
428
+ ...accessibilityProps(props),
429
+ type: inputType,
430
+ className: "squisq-jf-input",
431
+ value: display,
432
+ disabled,
433
+ onChange: (e) => setAtPath(pointer, e.target.value)
434
+ }
435
+ );
436
+ }
437
+ function GroupEditor(props) {
438
+ const { value, schema, pointer, disabled, suppressTitle } = props;
439
+ const title = !suppressTitle ? schema.squisq?.label ?? schema.title : void 0;
440
+ const help = schema.squisq?.help ?? schema.description;
441
+ const obj = (value && typeof value === "object" ? value : {}) ?? {};
442
+ const propEntries = Object.entries(schema.properties ?? {});
443
+ return /* @__PURE__ */ jsxs("section", { className: "squisq-jf-group", children: [
444
+ title ? /* @__PURE__ */ jsx3("h3", { className: "squisq-jf-group__title", children: title }) : null,
445
+ help ? /* @__PURE__ */ jsx3("p", { className: "squisq-jf-group__help", children: help }) : null,
446
+ propEntries.map(([key, propSchema]) => /* @__PURE__ */ jsx3(
447
+ RenderNode,
448
+ {
449
+ value: obj[key],
450
+ schema: propSchema,
451
+ pointer: appendPointer(pointer, key),
452
+ parentDisabled: disabled
453
+ },
454
+ key
455
+ ))
456
+ ] });
457
+ }
458
+ function ChipBinEditor({ value, schema, pointer, disabled }) {
459
+ const { setAtPath } = useJsonEditor();
460
+ const items = Array.isArray(value) ? value : [];
461
+ const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {
462
+ type: "string"
463
+ };
464
+ const labels = itemSchema.squisq?.enumLabels;
465
+ const enumOpts = itemSchema.enum;
466
+ const [draft, setDraft] = useState("");
467
+ const remove = (index) => {
468
+ const next = items.slice();
469
+ next.splice(index, 1);
470
+ setAtPath(pointer, next);
471
+ };
472
+ const add = (raw) => {
473
+ if (raw === "") return;
474
+ const coerced = coerceToSchema(raw, itemSchema);
475
+ setAtPath(pointer, [...items, coerced]);
476
+ setDraft("");
477
+ };
478
+ return /* @__PURE__ */ jsxs("div", { className: "squisq-jf-chip-bin", children: [
479
+ items.map((item, i) => {
480
+ const display = labels && typeof item === "string" ? labels[item] ?? String(item) : String(item);
481
+ return /* @__PURE__ */ jsxs("span", { className: "squisq-jf-chip", children: [
482
+ display,
483
+ /* @__PURE__ */ jsx3(
484
+ "button",
485
+ {
486
+ type: "button",
487
+ className: "squisq-jf-chip__remove",
488
+ disabled,
489
+ onClick: () => remove(i),
490
+ "aria-label": `Remove ${display}`,
491
+ children: "\xD7"
492
+ }
493
+ )
494
+ ] }, i);
495
+ }),
496
+ !disabled && enumOpts && enumOpts.length > 0 ? /* @__PURE__ */ jsxs(
497
+ "select",
498
+ {
499
+ className: "squisq-jf-select",
500
+ value: "",
501
+ onChange: (e) => add(e.target.value),
502
+ style: { width: "auto" },
503
+ children: [
504
+ /* @__PURE__ */ jsx3("option", { value: "", disabled: true, children: schema.squisq?.addLabel ?? "+ Add" }),
505
+ enumOpts.filter((v) => !items.includes(v)).map((v, i) => /* @__PURE__ */ jsx3("option", { value: String(v), children: labels && typeof v === "string" ? labels[v] ?? String(v) : String(v) }, i))
506
+ ]
507
+ }
508
+ ) : !disabled ? /* @__PURE__ */ jsx3(
509
+ "input",
510
+ {
511
+ type: "text",
512
+ className: "squisq-jf-chip-bin__add-input",
513
+ placeholder: schema.squisq?.addLabel ?? "+ Add",
514
+ value: draft,
515
+ onChange: (e) => setDraft(e.target.value),
516
+ onKeyDown: (e) => {
517
+ if (e.key === "Enter" || e.key === ",") {
518
+ e.preventDefault();
519
+ add(draft.trim());
520
+ }
521
+ },
522
+ onBlur: () => add(draft.trim())
523
+ }
524
+ ) : null
525
+ ] });
526
+ }
527
+ function coerceToSchema(raw, schema) {
528
+ switch (schema.type) {
529
+ case "integer":
530
+ return parseInt(raw, 10);
531
+ case "number":
532
+ return Number(raw);
533
+ case "boolean":
534
+ return raw === "true" || raw === "1";
535
+ default:
536
+ return raw;
537
+ }
538
+ }
539
+ function CardStackEditor({ value, schema, pointer, disabled }) {
540
+ const { setAtPath } = useJsonEditor();
541
+ const items = Array.isArray(value) ? value : [];
542
+ const itemSchema = (Array.isArray(schema.items) ? schema.items[0] : schema.items) ?? {
543
+ type: "object"
544
+ };
545
+ const itemLabel = itemSchema.squisq?.itemLabel;
546
+ const addLabel = schema.squisq?.addLabel ?? "+ Add";
547
+ const updateItems = (next) => setAtPath(pointer, next);
548
+ const addItem = () => updateItems([...items, defaultForSchema(itemSchema)]);
549
+ const removeItem = (i) => {
550
+ const next = items.slice();
551
+ next.splice(i, 1);
552
+ updateItems(next);
553
+ };
554
+ const moveItem = (i, delta) => {
555
+ const j = i + delta;
556
+ if (j < 0 || j >= items.length) return;
557
+ const next = items.slice();
558
+ [next[i], next[j]] = [next[j], next[i]];
559
+ updateItems(next);
560
+ };
561
+ return /* @__PURE__ */ jsxs("div", { children: [
562
+ /* @__PURE__ */ jsx3("div", { className: "squisq-jf-card-stack", children: items.map((item, i) => {
563
+ const title = resolveItemTitle(itemLabel, item, i);
564
+ return /* @__PURE__ */ jsxs("div", { className: "squisq-jf-card", children: [
565
+ /* @__PURE__ */ jsxs("div", { className: "squisq-jf-card__header", children: [
566
+ /* @__PURE__ */ jsx3("h4", { className: "squisq-jf-card__title", children: title }),
567
+ !disabled ? /* @__PURE__ */ jsxs("span", { className: "squisq-jf-card__actions", children: [
568
+ /* @__PURE__ */ jsx3(
569
+ "button",
570
+ {
571
+ type: "button",
572
+ className: "squisq-jf-icon-btn",
573
+ onClick: () => moveItem(i, -1),
574
+ disabled: i === 0,
575
+ "aria-label": "Move up",
576
+ title: "Move up",
577
+ children: "\u2191"
578
+ }
579
+ ),
580
+ /* @__PURE__ */ jsx3(
581
+ "button",
582
+ {
583
+ type: "button",
584
+ className: "squisq-jf-icon-btn",
585
+ onClick: () => moveItem(i, 1),
586
+ disabled: i === items.length - 1,
587
+ "aria-label": "Move down",
588
+ title: "Move down",
589
+ children: "\u2193"
590
+ }
591
+ ),
592
+ /* @__PURE__ */ jsx3(
593
+ "button",
594
+ {
595
+ type: "button",
596
+ className: "squisq-jf-icon-btn squisq-jf-icon-btn--danger",
597
+ onClick: () => removeItem(i),
598
+ "aria-label": schema.squisq?.removeLabel ?? "Remove",
599
+ title: schema.squisq?.removeLabel ?? "Remove",
600
+ children: "\xD7"
601
+ }
602
+ )
603
+ ] }) : null
604
+ ] }),
605
+ /* @__PURE__ */ jsx3(
606
+ RenderNode,
607
+ {
608
+ value: item,
609
+ schema: itemSchema,
610
+ pointer: `${pointer}/${i}`,
611
+ parentDisabled: disabled,
612
+ suppressTopGroupTitle: true
613
+ }
614
+ )
615
+ ] }, i);
616
+ }) }),
617
+ !disabled ? /* @__PURE__ */ jsx3(
618
+ "button",
619
+ {
620
+ type: "button",
621
+ className: "squisq-jf-add-btn",
622
+ onClick: addItem,
623
+ style: { marginTop: 8 },
624
+ children: addLabel
625
+ }
626
+ ) : null
627
+ ] });
628
+ }
629
+ function resolveItemTitle(spec, item, index) {
630
+ if (!spec) return `Item ${index + 1}`;
631
+ if (typeof spec === "string") return spec;
632
+ if (item && typeof item === "object") {
633
+ const v = item[spec.fromField];
634
+ if (typeof v === "string" && v !== "") return v;
635
+ if (typeof v === "number") return String(v);
636
+ }
637
+ return `Item ${index + 1}`;
638
+ }
639
+ function defaultForSchema(schema) {
640
+ if (schema.default !== void 0) return schema.default;
641
+ const t = Array.isArray(schema.type) ? schema.type.find((x) => x !== "null") : schema.type;
642
+ switch (t) {
643
+ case "object": {
644
+ const out = {};
645
+ for (const [k, v] of Object.entries(schema.properties ?? {})) {
646
+ out[k] = defaultForSchema(v);
647
+ }
648
+ return out;
649
+ }
650
+ case "array":
651
+ return [];
652
+ case "string":
653
+ return "";
654
+ case "number":
655
+ case "integer":
656
+ return schema.minimum ?? 0;
657
+ case "boolean":
658
+ return false;
659
+ default:
660
+ return void 0;
661
+ }
662
+ }
663
+ function TabsEditor(props) {
664
+ const { value, schema, pointer, disabled } = props;
665
+ const tabsId = useId().replace(/:/g, "");
666
+ const branches = schema.oneOf ?? schema.anyOf ?? [];
667
+ const initial = pickMatchingBranch(branches, value);
668
+ const [active, setActive] = useState(initial);
669
+ useEffect3(() => {
670
+ setActive(pickMatchingBranch(branches, value));
671
+ }, [value]);
672
+ const branch = branches[active];
673
+ if (!branch) return null;
674
+ const activateTab = (index) => {
675
+ const next = (index + branches.length) % branches.length;
676
+ setActive(next);
677
+ requestAnimationFrame(() => document.getElementById(`${tabsId}-tab-${next}`)?.focus());
678
+ };
679
+ return /* @__PURE__ */ jsxs("div", { ...accessibilityProps(props), children: [
680
+ /* @__PURE__ */ jsx3("div", { className: "squisq-jf-tabs__strip", role: "tablist", children: branches.map((b, i) => /* @__PURE__ */ jsx3(
681
+ "button",
682
+ {
683
+ type: "button",
684
+ className: `squisq-jf-tabs__tab${i === active ? " squisq-jf-tabs__tab--active" : ""}`,
685
+ disabled,
686
+ onClick: () => setActive(i),
687
+ onKeyDown: (event) => {
688
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
689
+ event.preventDefault();
690
+ activateTab(i + 1);
691
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
692
+ event.preventDefault();
693
+ activateTab(i - 1);
694
+ } else if (event.key === "Home") {
695
+ event.preventDefault();
696
+ activateTab(0);
697
+ } else if (event.key === "End") {
698
+ event.preventDefault();
699
+ activateTab(branches.length - 1);
700
+ }
701
+ },
702
+ role: "tab",
703
+ id: `${tabsId}-tab-${i}`,
704
+ "aria-selected": i === active,
705
+ "aria-controls": `${tabsId}-panel`,
706
+ tabIndex: i === active ? 0 : -1,
707
+ children: b.squisq?.label ?? b.title ?? `Option ${i + 1}`
708
+ },
709
+ i
710
+ )) }),
711
+ /* @__PURE__ */ jsx3("div", { id: `${tabsId}-panel`, role: "tabpanel", "aria-labelledby": `${tabsId}-tab-${active}`, children: /* @__PURE__ */ jsx3(RenderNode, { value, schema: branch, pointer, parentDisabled: disabled }) })
712
+ ] });
713
+ }
714
+ function pickMatchingBranch(branches, value) {
715
+ for (let i = 0; i < branches.length; i++) {
716
+ if (matchesShape(branches[i], value)) return i;
717
+ }
718
+ return 0;
719
+ }
720
+ function matchesShape(schema, value) {
721
+ const t = schema.type;
722
+ const target = Array.isArray(t) ? t.find((x) => x !== "null") : t;
723
+ if (!target) return true;
724
+ switch (target) {
725
+ case "string":
726
+ return typeof value === "string";
727
+ case "number":
728
+ case "integer":
729
+ return typeof value === "number";
730
+ case "boolean":
731
+ return typeof value === "boolean";
732
+ case "null":
733
+ return value === null;
734
+ case "array":
735
+ return Array.isArray(value);
736
+ case "object":
737
+ return value !== null && typeof value === "object" && !Array.isArray(value);
738
+ default:
739
+ return false;
740
+ }
741
+ }
742
+ var EDITORS = {
743
+ text: TextEditor,
744
+ multiline: MultilineEditor,
745
+ richtext: RichTextEditor,
746
+ color: ColorEditor,
747
+ date: DateEditor,
748
+ time: DateEditor,
749
+ datetime: DateEditor,
750
+ slider: SliderEditor,
751
+ stepper: NumberStepperEditor,
752
+ segmented: SegmentedEditor,
753
+ radio: RadioEditor,
754
+ combobox: ComboboxEditor,
755
+ toggle: ToggleEditor,
756
+ checkbox: CheckboxEditor,
757
+ card: GroupEditor,
758
+ group: GroupEditor,
759
+ "card-stack": CardStackEditor,
760
+ "chip-bin": ChipBinEditor,
761
+ tabs: TabsEditor
762
+ };
763
+ function isCompositeControl(kind, schema) {
764
+ if (kind === "group" || kind === "card" || kind === "tabs" || kind === "card-stack") return true;
765
+ if (kind === "richtext") return true;
766
+ if (kind === "chip-bin") return arrayItemKind(schema) === "object";
767
+ return false;
768
+ }
769
+
770
+ // src/jsonEditor/RenderNode.tsx
771
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
772
+ function RenderNode(props) {
773
+ const generatedId = useId2().replace(/:/g, "");
774
+ const { rootSchema, rootData, errors } = useJsonEditor();
775
+ const resolved = resolveRef(props.schema, rootSchema) ?? props.schema;
776
+ if (resolveFlag(resolved.squisq?.hidden, rootData)) return null;
777
+ const disabled = (props.parentDisabled ?? false) || resolveFlag(resolved.squisq?.disabled, rootData);
778
+ const kind = chooseControl(resolved);
779
+ const Editor = EDITORS[kind];
780
+ const label = resolved.squisq?.label ?? resolved.title;
781
+ const help = resolved.squisq?.help ?? resolved.description;
782
+ const fieldErrors = errors.get(props.pointer) ?? [];
783
+ const controlId = `squisq-json-control-${generatedId}`;
784
+ const labelId = label ? `${controlId}-label` : void 0;
785
+ const helpId = help ? `${controlId}-help` : void 0;
786
+ const errorId = fieldErrors.length > 0 ? `${controlId}-error` : void 0;
787
+ const describedBy = [helpId, errorId].filter(Boolean).join(" ") || void 0;
788
+ const editorEl = /* @__PURE__ */ jsx4(
789
+ Editor,
790
+ {
791
+ value: props.value,
792
+ schema: resolved,
793
+ pointer: props.pointer,
794
+ disabled,
795
+ controlId,
796
+ labelledBy: labelId,
797
+ describedBy,
798
+ invalid: fieldErrors.length > 0,
799
+ suppressTitle: props.suppressTopGroupTitle
800
+ }
801
+ );
802
+ if (isCompositeControl(kind, resolved)) {
803
+ return editorEl;
804
+ }
805
+ return /* @__PURE__ */ jsxs2("div", { className: `squisq-jf-field${disabled ? " squisq-jf-field--disabled" : ""}`, children: [
806
+ label ? /* @__PURE__ */ jsx4("label", { id: labelId, htmlFor: controlId, className: "squisq-jf-field__label", children: label }) : null,
807
+ editorEl,
808
+ help ? /* @__PURE__ */ jsx4("p", { id: helpId, className: "squisq-jf-field__help", children: help }) : null,
809
+ fieldErrors.length > 0 ? /* @__PURE__ */ jsx4("p", { id: errorId, className: "squisq-jf-field__error", role: "alert", children: fieldErrors[0].message }) : null
810
+ ] });
811
+ }
812
+
813
+ // src/jsonEditor/useJsonEditorTokens.ts
814
+ import { useMemo as useMemo2 } from "react";
815
+ import { buildJsonFormTokens, resolveJsonFormTheme } from "@bendyline/squisq/jsonForm";
816
+ import { useAutoSurface } from "@bendyline/squisq-react";
817
+ function useJsonEditorTokens(theme, surface) {
818
+ const auto = useAutoSurface(surface === "auto");
819
+ const effectiveSurface = surface === "auto" ? auto : surface ?? void 0;
820
+ return useMemo2(() => {
821
+ const style = buildJsonFormTokens(theme, effectiveSurface, {
822
+ prefix: "--squisq-jsonform"
823
+ });
824
+ return { style, theme: resolveJsonFormTheme(theme, effectiveSurface) };
825
+ }, [theme, effectiveSurface]);
826
+ }
827
+
828
+ // src/jsonEditor/JsonEditor.tsx
829
+ import { jsx as jsx5 } from "react/jsx-runtime";
830
+ function JsonEditor(props) {
831
+ const {
832
+ schema,
833
+ value,
834
+ onChange,
835
+ theme,
836
+ surface,
837
+ density = "comfortable",
838
+ validate,
839
+ onValidate,
840
+ className
841
+ } = props;
842
+ const { style } = useJsonEditorTokens(theme, surface);
843
+ const handleChange = onChange ?? (() => {
844
+ });
845
+ const cls = "squisq-jsonform" + (density === "compact" ? " squisq-jsonform--compact" : "") + (className ? ` ${className}` : "");
846
+ return /* @__PURE__ */ jsx5("div", { className: cls, style, children: /* @__PURE__ */ jsx5(
847
+ JsonEditorProvider,
848
+ {
849
+ rootSchema: schema,
850
+ rootData: value,
851
+ onRootChange: handleChange,
852
+ density,
853
+ validate,
854
+ onValidate,
855
+ children: /* @__PURE__ */ jsx5(RenderNode, { value, schema, pointer: "", parentDisabled: !onChange })
856
+ }
857
+ ) });
858
+ }
859
+
860
+ export {
861
+ JsonEditor
862
+ };