@marktiderman/genesis-forms-web 0.1.1 → 0.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.
- package/README.md +43 -7
- package/dist/index.d.ts +89 -3
- package/dist/index.js +987 -34
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { createContext,
|
|
2
|
+
import { createContext, useState, useEffect, useRef, useId, useContext, useLayoutEffect } from 'react';
|
|
3
3
|
import { Button } from '@marktiderman/genesis-ui/button';
|
|
4
4
|
import { Progress } from '@marktiderman/genesis-ui/progress';
|
|
5
5
|
import { cn } from '@marktiderman/genesis-ui/utils';
|
|
6
6
|
import { createRendererRegistry, useForm, FormRuntime, useFormContext, useFormSelector, FieldRenderer } from '@marktiderman/genesis-forms-react';
|
|
7
7
|
import { Checkbox } from '@marktiderman/genesis-ui/checkbox';
|
|
8
|
-
import { FormDescription, FormError, FormLabel } from '@marktiderman/genesis-ui/form-field';
|
|
9
8
|
import { Input } from '@marktiderman/genesis-ui/input';
|
|
10
9
|
import { Label } from '@marktiderman/genesis-ui/label';
|
|
11
10
|
import { RadioGroup, RadioGroupItem } from '@marktiderman/genesis-ui/radio-group';
|
|
12
11
|
import { Textarea } from '@marktiderman/genesis-ui/textarea';
|
|
13
|
-
import { choiceOptions, getFieldType } from '@marktiderman/genesis-forms';
|
|
12
|
+
import { ratingDisplay, choiceOptions, numberLimits, dateLimits, listLimits, tableColumns, tableLimits, parseTimestamp, formatTimestamp, formatCalculated, getFieldType, scaleLabels } from '@marktiderman/genesis-forms';
|
|
13
|
+
import { FormDescription, FormError, FormLabel } from '@marktiderman/genesis-ui/form-field';
|
|
14
14
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
15
|
+
import { Stack } from '@marktiderman/genesis-ui/layout';
|
|
16
|
+
import { SortableList } from '@marktiderman/genesis-ui/sortable-list';
|
|
15
17
|
|
|
16
18
|
// src/Form.tsx
|
|
17
19
|
var FormPresentationContext = createContext({ presentation: "embedded", layout: "classic" });
|
|
@@ -118,6 +120,658 @@ function describedBy(props) {
|
|
|
118
120
|
].filter(Boolean);
|
|
119
121
|
return ids.length ? ids.join(" ") : void 0;
|
|
120
122
|
}
|
|
123
|
+
function CalculatedField(props) {
|
|
124
|
+
const { binding, inputId } = props;
|
|
125
|
+
const { presentation, layout } = useFormPresentation();
|
|
126
|
+
const pageFocus = presentation === "page" && layout === "focus";
|
|
127
|
+
const text = formatCalculated(binding.value, binding.field);
|
|
128
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsx(
|
|
129
|
+
"output",
|
|
130
|
+
{
|
|
131
|
+
id: inputId,
|
|
132
|
+
"aria-labelledby": `${inputId}-label`,
|
|
133
|
+
"aria-describedby": describedBy(props),
|
|
134
|
+
"aria-live": "polite",
|
|
135
|
+
"data-gf-calculated": "",
|
|
136
|
+
className: cn(
|
|
137
|
+
"block rounded-md border border-dashed bg-muted/40 px-3 py-2 font-medium tabular-nums",
|
|
138
|
+
pageFocus ? "text-2xl sm:text-3xl" : "text-base",
|
|
139
|
+
!text && "text-muted-foreground"
|
|
140
|
+
),
|
|
141
|
+
children: text || "\u2014"
|
|
142
|
+
}
|
|
143
|
+
) });
|
|
144
|
+
}
|
|
145
|
+
var calculatedRenderers = [{ type: "calculated", Component: CalculatedField }];
|
|
146
|
+
var MediaTimeContext = createContext(null);
|
|
147
|
+
function useMediaTime() {
|
|
148
|
+
return useContext(MediaTimeContext);
|
|
149
|
+
}
|
|
150
|
+
var SourceTextContext = createContext(null);
|
|
151
|
+
function useSourceText() {
|
|
152
|
+
return useContext(SourceTextContext);
|
|
153
|
+
}
|
|
154
|
+
function PlusIcon() {
|
|
155
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M12 5v14M5 12h14" }) });
|
|
156
|
+
}
|
|
157
|
+
function XIcon() {
|
|
158
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) });
|
|
159
|
+
}
|
|
160
|
+
function move(list, from, to) {
|
|
161
|
+
const next = list.slice();
|
|
162
|
+
const [item] = next.splice(from, 1);
|
|
163
|
+
next.splice(to, 0, item);
|
|
164
|
+
return next;
|
|
165
|
+
}
|
|
166
|
+
function useEntryKeys(count) {
|
|
167
|
+
const keys = useRef([]);
|
|
168
|
+
const next = useRef(0);
|
|
169
|
+
const make = () => `e${next.current++}`;
|
|
170
|
+
while (keys.current.length < count) keys.current.push(make());
|
|
171
|
+
if (keys.current.length > count) keys.current = keys.current.slice(0, count);
|
|
172
|
+
return {
|
|
173
|
+
keys: keys.current,
|
|
174
|
+
/** Add a key at `at` and return it. */
|
|
175
|
+
insert(at) {
|
|
176
|
+
const key = make();
|
|
177
|
+
keys.current = [...keys.current.slice(0, at), key, ...keys.current.slice(at)];
|
|
178
|
+
return key;
|
|
179
|
+
},
|
|
180
|
+
remove(at) {
|
|
181
|
+
keys.current = keys.current.filter((_, i) => i !== at);
|
|
182
|
+
},
|
|
183
|
+
move(from, to) {
|
|
184
|
+
keys.current = move(keys.current, from, to);
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function useFocusLater() {
|
|
189
|
+
const elements = useRef(/* @__PURE__ */ new Map());
|
|
190
|
+
const [target, setTarget] = useState(null);
|
|
191
|
+
useEffect(() => {
|
|
192
|
+
if (target === null) return;
|
|
193
|
+
elements.current.get(target)?.focus();
|
|
194
|
+
setTarget(null);
|
|
195
|
+
}, [target]);
|
|
196
|
+
return {
|
|
197
|
+
ref: (key) => (el) => {
|
|
198
|
+
if (el) elements.current.set(key, el);
|
|
199
|
+
else elements.current.delete(key);
|
|
200
|
+
},
|
|
201
|
+
focus: setTarget
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
var invalid = (props) => props.binding.error ? true : void 0;
|
|
205
|
+
function RemoveButton({ label, onClick, disabled }) {
|
|
206
|
+
return /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", size: "icon", "aria-label": label, disabled, onClick, className: "shrink-0", children: /* @__PURE__ */ jsx(XIcon, {}) });
|
|
207
|
+
}
|
|
208
|
+
function AddButton({ children, onClick, disabled }) {
|
|
209
|
+
return /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", size: "sm", disabled, onClick, className: "self-start", children: [
|
|
210
|
+
/* @__PURE__ */ jsx(PlusIcon, {}),
|
|
211
|
+
" ",
|
|
212
|
+
children
|
|
213
|
+
] });
|
|
214
|
+
}
|
|
215
|
+
function excerpt(text, max = 40) {
|
|
216
|
+
const t = text.replace(/\s+/g, " ").trim();
|
|
217
|
+
return t.length > max ? `${t.slice(0, max - 1)}\u2026` : t;
|
|
218
|
+
}
|
|
219
|
+
function ListField(props) {
|
|
220
|
+
const { binding, inputId } = props;
|
|
221
|
+
const field = binding.field;
|
|
222
|
+
const { max, itemMaxLength } = listLimits(field);
|
|
223
|
+
const stored = Array.isArray(binding.value) ? binding.value.map((v) => typeof v === "string" ? v : "") : [];
|
|
224
|
+
const items = stored.length ? stored : [""];
|
|
225
|
+
const entries = useEntryKeys(items.length);
|
|
226
|
+
const focusLater = useFocusLater();
|
|
227
|
+
const addRef = useRef(null);
|
|
228
|
+
const placeholder = typeof field.props?.placeholder === "string" ? field.props.placeholder : field.placeholder ?? void 0;
|
|
229
|
+
const save = (next) => binding.setValue(next.length ? next : void 0);
|
|
230
|
+
const add = () => {
|
|
231
|
+
if (items.length >= max) return;
|
|
232
|
+
const key = entries.insert(items.length);
|
|
233
|
+
save([...items, ""]);
|
|
234
|
+
focusLater.focus(key);
|
|
235
|
+
};
|
|
236
|
+
const remove = (i) => {
|
|
237
|
+
entries.remove(i);
|
|
238
|
+
const next = items.filter((_, j) => j !== i);
|
|
239
|
+
save(next);
|
|
240
|
+
const neighbour = entries.keys[Math.min(i, next.length - 1)];
|
|
241
|
+
if (neighbour !== void 0 && next.length) focusLater.focus(neighbour);
|
|
242
|
+
else addRef.current?.focus();
|
|
243
|
+
};
|
|
244
|
+
const onKeyDown = (i) => (e) => {
|
|
245
|
+
if (e.key !== "Enter" || e.nativeEvent.isComposing || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey) return;
|
|
246
|
+
if (i < items.length - 1) {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
focusLater.focus(entries.keys[i + 1]);
|
|
249
|
+
} else if (items.length < max) {
|
|
250
|
+
e.preventDefault();
|
|
251
|
+
add();
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const rows = items.map((text, index) => ({ key: entries.keys[index], text, index }));
|
|
255
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs(Stack, { gap: "sm", children: [
|
|
256
|
+
/* @__PURE__ */ jsx(
|
|
257
|
+
SortableList,
|
|
258
|
+
{
|
|
259
|
+
items: rows,
|
|
260
|
+
getKey: (r) => r.key,
|
|
261
|
+
getItemLabel: (r) => r.text.trim() ? excerpt(r.text, 30) : `Item ${r.index + 1}`,
|
|
262
|
+
label: `${binding.label}: items`,
|
|
263
|
+
disabled: items.length < 2,
|
|
264
|
+
onReorder: (from, to) => {
|
|
265
|
+
entries.move(from, to);
|
|
266
|
+
save(move(items, from, to));
|
|
267
|
+
},
|
|
268
|
+
renderItem: (r, { handle, index }) => /* @__PURE__ */ jsxs(Stack, { direction: "row", align: "center", gap: "sm", className: "w-full min-w-0", children: [
|
|
269
|
+
handle,
|
|
270
|
+
/* @__PURE__ */ jsx(
|
|
271
|
+
Input,
|
|
272
|
+
{
|
|
273
|
+
ref: focusLater.ref(r.key),
|
|
274
|
+
id: index === 0 ? `${inputId}-0` : void 0,
|
|
275
|
+
"data-gf-focus": index === 0 ? "" : void 0,
|
|
276
|
+
"aria-label": `Item ${index + 1}`,
|
|
277
|
+
"aria-invalid": invalid(props),
|
|
278
|
+
maxLength: itemMaxLength,
|
|
279
|
+
placeholder,
|
|
280
|
+
value: r.text,
|
|
281
|
+
onChange: (e) => save(items.map((t, j) => j === index ? e.target.value : t)),
|
|
282
|
+
onKeyDown: onKeyDown(index)
|
|
283
|
+
}
|
|
284
|
+
),
|
|
285
|
+
/* @__PURE__ */ jsx(
|
|
286
|
+
RemoveButton,
|
|
287
|
+
{
|
|
288
|
+
label: `Remove item ${index + 1}`,
|
|
289
|
+
disabled: items.length === 1 && r.text === "",
|
|
290
|
+
onClick: () => remove(index)
|
|
291
|
+
}
|
|
292
|
+
)
|
|
293
|
+
] })
|
|
294
|
+
}
|
|
295
|
+
),
|
|
296
|
+
/* @__PURE__ */ jsxs(
|
|
297
|
+
Button,
|
|
298
|
+
{
|
|
299
|
+
ref: addRef,
|
|
300
|
+
type: "button",
|
|
301
|
+
variant: "outline",
|
|
302
|
+
size: "sm",
|
|
303
|
+
className: "self-start",
|
|
304
|
+
disabled: items.length >= max,
|
|
305
|
+
onClick: add,
|
|
306
|
+
children: [
|
|
307
|
+
/* @__PURE__ */ jsx(PlusIcon, {}),
|
|
308
|
+
" Add item"
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
] }) });
|
|
313
|
+
}
|
|
314
|
+
function toRow(value) {
|
|
315
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
316
|
+
return Object.fromEntries(Object.entries(value).filter((e) => typeof e[1] === "string"));
|
|
317
|
+
}
|
|
318
|
+
function TableField(props) {
|
|
319
|
+
const { binding, inputId } = props;
|
|
320
|
+
const columns = tableColumns(binding.field);
|
|
321
|
+
const { minRows, maxRows } = tableLimits(binding.field);
|
|
322
|
+
const stored = Array.isArray(binding.value) ? binding.value.map(toRow) : [];
|
|
323
|
+
const shown = Math.min(Math.max(stored.length, minRows, 1), Math.max(maxRows, 1));
|
|
324
|
+
const rows = Array.from({ length: shown }, (_, i) => stored[i] ?? {});
|
|
325
|
+
const entries = useEntryKeys(rows.length);
|
|
326
|
+
const focusLater = useFocusLater();
|
|
327
|
+
const addRef = useRef(null);
|
|
328
|
+
const save = (next) => binding.setValue(next.length ? next : void 0);
|
|
329
|
+
const firstKey = columns[0]?.key;
|
|
330
|
+
const add = () => {
|
|
331
|
+
if (rows.length >= maxRows) return;
|
|
332
|
+
const key = entries.insert(rows.length);
|
|
333
|
+
save([...rows, {}]);
|
|
334
|
+
focusLater.focus(key);
|
|
335
|
+
};
|
|
336
|
+
const remove = (i) => {
|
|
337
|
+
entries.remove(i);
|
|
338
|
+
const next = rows.filter((_, j) => j !== i);
|
|
339
|
+
save(next);
|
|
340
|
+
const neighbour = entries.keys[Math.min(i, next.length - 1)];
|
|
341
|
+
if (neighbour !== void 0 && next.length) focusLater.focus(neighbour);
|
|
342
|
+
else addRef.current?.focus();
|
|
343
|
+
};
|
|
344
|
+
if (!columns.length) {
|
|
345
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "This question has no columns yet." }) });
|
|
346
|
+
}
|
|
347
|
+
const items = rows.map((row, index) => ({ key: entries.keys[index], row, index }));
|
|
348
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
349
|
+
/* @__PURE__ */ jsx(
|
|
350
|
+
SortableList,
|
|
351
|
+
{
|
|
352
|
+
items,
|
|
353
|
+
getKey: (r) => r.key,
|
|
354
|
+
getItemLabel: (r) => `Row ${r.index + 1}`,
|
|
355
|
+
label: `${binding.label}: rows`,
|
|
356
|
+
disabled: rows.length < 2,
|
|
357
|
+
onReorder: (from, to) => {
|
|
358
|
+
entries.move(from, to);
|
|
359
|
+
save(move(rows, from, to));
|
|
360
|
+
},
|
|
361
|
+
renderItem: (r, { handle, index }) => /* @__PURE__ */ jsxs(Stack, { direction: "row", align: "start", gap: "sm", className: "w-full min-w-0", children: [
|
|
362
|
+
handle ? /* @__PURE__ */ jsx("div", { className: "pt-7", children: handle }) : null,
|
|
363
|
+
/* @__PURE__ */ jsxs("fieldset", { className: "m-0 min-w-0 flex-1 rounded-md border border-input p-3", children: [
|
|
364
|
+
/* @__PURE__ */ jsx("legend", { className: "px-1 text-sm font-medium", children: `Row ${index + 1}` }),
|
|
365
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-3 sm:grid-cols-2", children: columns.map((column, c) => {
|
|
366
|
+
const id = `${inputId}-${r.key}-${column.key}`;
|
|
367
|
+
const common = {
|
|
368
|
+
id,
|
|
369
|
+
// The first cell of the first row is what an error summary link and the focus layout reach.
|
|
370
|
+
"data-gf-focus": index === 0 && c === 0 ? "" : void 0,
|
|
371
|
+
"aria-invalid": invalid(props),
|
|
372
|
+
value: r.row[column.key] ?? "",
|
|
373
|
+
maxLength: 2e3
|
|
374
|
+
};
|
|
375
|
+
const set = (text) => save(rows.map((row, j) => j === index ? { ...row, [column.key]: text } : row));
|
|
376
|
+
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-1.5", column.long && "sm:col-span-2"), children: [
|
|
377
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: id, children: column.label }),
|
|
378
|
+
column.long ? /* @__PURE__ */ jsx(
|
|
379
|
+
Textarea,
|
|
380
|
+
{
|
|
381
|
+
...common,
|
|
382
|
+
ref: column.key === firstKey ? focusLater.ref(r.key) : void 0,
|
|
383
|
+
rows: 2,
|
|
384
|
+
onChange: (e) => set(e.target.value)
|
|
385
|
+
}
|
|
386
|
+
) : /* @__PURE__ */ jsx(
|
|
387
|
+
Input,
|
|
388
|
+
{
|
|
389
|
+
...common,
|
|
390
|
+
ref: column.key === firstKey ? focusLater.ref(r.key) : void 0,
|
|
391
|
+
onChange: (e) => set(e.target.value)
|
|
392
|
+
}
|
|
393
|
+
)
|
|
394
|
+
] }, column.key);
|
|
395
|
+
}) })
|
|
396
|
+
] }),
|
|
397
|
+
/* @__PURE__ */ jsx("div", { className: "pt-7", children: /* @__PURE__ */ jsx(
|
|
398
|
+
RemoveButton,
|
|
399
|
+
{
|
|
400
|
+
label: `Remove row ${index + 1}`,
|
|
401
|
+
disabled: rows.length <= Math.max(minRows, 1),
|
|
402
|
+
onClick: () => remove(index)
|
|
403
|
+
}
|
|
404
|
+
) })
|
|
405
|
+
] })
|
|
406
|
+
}
|
|
407
|
+
),
|
|
408
|
+
/* @__PURE__ */ jsxs(
|
|
409
|
+
Button,
|
|
410
|
+
{
|
|
411
|
+
ref: addRef,
|
|
412
|
+
type: "button",
|
|
413
|
+
variant: "outline",
|
|
414
|
+
size: "sm",
|
|
415
|
+
className: "self-start",
|
|
416
|
+
disabled: rows.length >= maxRows,
|
|
417
|
+
onClick: add,
|
|
418
|
+
children: [
|
|
419
|
+
/* @__PURE__ */ jsx(PlusIcon, {}),
|
|
420
|
+
" Add row"
|
|
421
|
+
]
|
|
422
|
+
}
|
|
423
|
+
)
|
|
424
|
+
] }) });
|
|
425
|
+
}
|
|
426
|
+
function toNote(value) {
|
|
427
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { at: 0, text: "" };
|
|
428
|
+
return { at: typeof value.at === "number" ? value.at : 0, text: typeof value.text === "string" ? value.text : "" };
|
|
429
|
+
}
|
|
430
|
+
function MediaNotesField(props) {
|
|
431
|
+
const { binding, inputId } = props;
|
|
432
|
+
const media = useMediaTime();
|
|
433
|
+
const maxNotes = typeof binding.field.props?.maxNotes === "number" ? binding.field.props.maxNotes : 20;
|
|
434
|
+
const textMaxLength = typeof binding.field.props?.textMaxLength === "number" ? binding.field.props.textMaxLength : 1e3;
|
|
435
|
+
const notes = Array.isArray(binding.value) ? binding.value.map(toNote) : [];
|
|
436
|
+
const entries = useEntryKeys(notes.length);
|
|
437
|
+
const now = () => {
|
|
438
|
+
const t = media?.currentTime?.();
|
|
439
|
+
return typeof t === "number" && Number.isFinite(t) && t >= 0 ? formatTimestamp(t) : "";
|
|
440
|
+
};
|
|
441
|
+
const [time, setTime] = useState(now);
|
|
442
|
+
const [timeTouched, setTimeTouched] = useState(false);
|
|
443
|
+
const [text, setText] = useState("");
|
|
444
|
+
const [timeError, setTimeError] = useState(null);
|
|
445
|
+
const textRef = useRef(null);
|
|
446
|
+
const save = (next) => binding.setValue(next.length ? next : void 0);
|
|
447
|
+
const full = notes.length >= maxNotes;
|
|
448
|
+
const follow = () => {
|
|
449
|
+
if (timeTouched || !media?.currentTime) return;
|
|
450
|
+
const t = now();
|
|
451
|
+
if (t) setTime(t);
|
|
452
|
+
};
|
|
453
|
+
const parsed = parseTimestamp(time);
|
|
454
|
+
const add = () => {
|
|
455
|
+
if (full || !text.trim()) return;
|
|
456
|
+
const at = time.trim() === "" && !media?.currentTime ? 0 : parsed;
|
|
457
|
+
if (at === null) {
|
|
458
|
+
setTimeError("Enter a time like 1:23 or 1:02:03.");
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const at_ = notes.findIndex((n) => n.at > at);
|
|
462
|
+
const index = at_ === -1 ? notes.length : at_;
|
|
463
|
+
entries.insert(index);
|
|
464
|
+
save([...notes.slice(0, index), { at, text: text.trim() }, ...notes.slice(index)]);
|
|
465
|
+
setText("");
|
|
466
|
+
setTimeError(null);
|
|
467
|
+
setTimeTouched(false);
|
|
468
|
+
setTime(now());
|
|
469
|
+
textRef.current?.focus();
|
|
470
|
+
};
|
|
471
|
+
const onEnter = (e) => {
|
|
472
|
+
if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
|
|
473
|
+
e.preventDefault();
|
|
474
|
+
add();
|
|
475
|
+
};
|
|
476
|
+
const addLabel = media?.currentTime && parsed !== null ? `Add note at ${formatTimestamp(parsed)}` : "Add note";
|
|
477
|
+
const timeId = `${inputId}-time`;
|
|
478
|
+
const textId = `${inputId}-text`;
|
|
479
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
480
|
+
notes.length ? /* @__PURE__ */ jsx("ol", { "aria-label": `${binding.label}: notes`, className: "m-0 list-none space-y-2 p-0", children: notes.map((note, index) => {
|
|
481
|
+
const at = formatTimestamp(note.at);
|
|
482
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(Stack, { direction: "row", align: "center", gap: "sm", children: [
|
|
483
|
+
media?.seek ? /* @__PURE__ */ jsx(
|
|
484
|
+
Button,
|
|
485
|
+
{
|
|
486
|
+
type: "button",
|
|
487
|
+
variant: "link",
|
|
488
|
+
size: "sm",
|
|
489
|
+
className: "shrink-0 px-0 tabular-nums",
|
|
490
|
+
"aria-label": `Play from ${at}`,
|
|
491
|
+
onClick: () => media.seek?.(note.at),
|
|
492
|
+
children: at
|
|
493
|
+
}
|
|
494
|
+
) : /* @__PURE__ */ jsx("span", { className: "shrink-0 text-sm tabular-nums text-muted-foreground", children: at }),
|
|
495
|
+
/* @__PURE__ */ jsx(
|
|
496
|
+
Input,
|
|
497
|
+
{
|
|
498
|
+
"aria-label": `Note at ${at}`,
|
|
499
|
+
"aria-invalid": invalid(props),
|
|
500
|
+
maxLength: textMaxLength,
|
|
501
|
+
value: note.text,
|
|
502
|
+
onChange: (e) => save(notes.map((n, j) => j === index ? { ...n, text: e.target.value } : n)),
|
|
503
|
+
onKeyDown: (e) => {
|
|
504
|
+
if (e.key === "Enter") e.preventDefault();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
),
|
|
508
|
+
/* @__PURE__ */ jsx(
|
|
509
|
+
RemoveButton,
|
|
510
|
+
{
|
|
511
|
+
label: `Remove note at ${at}`,
|
|
512
|
+
onClick: () => {
|
|
513
|
+
entries.remove(index);
|
|
514
|
+
save(notes.filter((_, j) => j !== index));
|
|
515
|
+
textRef.current?.focus();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
)
|
|
519
|
+
] }) }, entries.keys[index]);
|
|
520
|
+
}) }) : null,
|
|
521
|
+
full ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: `You've added the most notes this question takes (${maxNotes}).` }) : /* @__PURE__ */ jsxs("div", { className: "grid gap-2 sm:grid-cols-[7rem_1fr_auto] sm:items-end", children: [
|
|
522
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
523
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: timeId, children: "Time" }),
|
|
524
|
+
/* @__PURE__ */ jsx(
|
|
525
|
+
Input,
|
|
526
|
+
{
|
|
527
|
+
id: timeId,
|
|
528
|
+
inputMode: "numeric",
|
|
529
|
+
placeholder: "m:ss",
|
|
530
|
+
value: time,
|
|
531
|
+
"aria-invalid": timeError ? true : void 0,
|
|
532
|
+
"aria-describedby": timeError ? `${timeId}-error` : void 0,
|
|
533
|
+
onChange: (e) => {
|
|
534
|
+
setTime(e.target.value);
|
|
535
|
+
setTimeTouched(true);
|
|
536
|
+
setTimeError(null);
|
|
537
|
+
},
|
|
538
|
+
onKeyDown: onEnter
|
|
539
|
+
}
|
|
540
|
+
)
|
|
541
|
+
] }),
|
|
542
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
543
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: textId, children: "Note" }),
|
|
544
|
+
/* @__PURE__ */ jsx(
|
|
545
|
+
Input,
|
|
546
|
+
{
|
|
547
|
+
ref: textRef,
|
|
548
|
+
id: textId,
|
|
549
|
+
"data-gf-focus": "",
|
|
550
|
+
maxLength: textMaxLength,
|
|
551
|
+
value: text,
|
|
552
|
+
onFocus: follow,
|
|
553
|
+
onChange: (e) => {
|
|
554
|
+
if (!text) follow();
|
|
555
|
+
setText(e.target.value);
|
|
556
|
+
},
|
|
557
|
+
onKeyDown: onEnter
|
|
558
|
+
}
|
|
559
|
+
)
|
|
560
|
+
] }),
|
|
561
|
+
/* @__PURE__ */ jsx(AddButton, { onClick: add, disabled: !text.trim(), children: addLabel }),
|
|
562
|
+
timeError ? /* @__PURE__ */ jsx("p", { id: `${timeId}-error`, className: "text-sm text-destructive sm:col-span-3", children: timeError }) : null
|
|
563
|
+
] })
|
|
564
|
+
] }) });
|
|
565
|
+
}
|
|
566
|
+
function toHighlight(value) {
|
|
567
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
568
|
+
const { start, end, quote, note } = value;
|
|
569
|
+
if (typeof start !== "number" || typeof end !== "number" || typeof quote !== "string") return null;
|
|
570
|
+
return { start, end, quote, note: typeof note === "string" ? note : "" };
|
|
571
|
+
}
|
|
572
|
+
function segments(text, highlights) {
|
|
573
|
+
const cuts = /* @__PURE__ */ new Set([0, text.length]);
|
|
574
|
+
for (const h of highlights) {
|
|
575
|
+
if (h.start >= 0 && h.end <= text.length && h.start < h.end) {
|
|
576
|
+
cuts.add(h.start);
|
|
577
|
+
cuts.add(h.end);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const points = [...cuts].sort((a, b) => a - b);
|
|
581
|
+
const out = [];
|
|
582
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
583
|
+
const start = points[i];
|
|
584
|
+
const end = points[i + 1];
|
|
585
|
+
if (end <= start) continue;
|
|
586
|
+
out.push({ start, text: text.slice(start, end), marked: highlights.some((h) => h.start <= start && h.end >= end) });
|
|
587
|
+
}
|
|
588
|
+
return out;
|
|
589
|
+
}
|
|
590
|
+
function textOffset(root, node, offset) {
|
|
591
|
+
const range = document.createRange();
|
|
592
|
+
range.selectNodeContents(root);
|
|
593
|
+
range.setEnd(node, offset);
|
|
594
|
+
return range.toString().length;
|
|
595
|
+
}
|
|
596
|
+
function TextHighlightsField(props) {
|
|
597
|
+
const { binding, inputId } = props;
|
|
598
|
+
const hosted = useSourceText();
|
|
599
|
+
const own = typeof binding.field.props?.text === "string" ? binding.field.props.text : "";
|
|
600
|
+
const text = hosted ?? own;
|
|
601
|
+
const maxHighlights = typeof binding.field.props?.maxHighlights === "number" ? binding.field.props.maxHighlights : 20;
|
|
602
|
+
const highlights = (Array.isArray(binding.value) ? binding.value : []).flatMap((v) => {
|
|
603
|
+
const h = toHighlight(v);
|
|
604
|
+
return h ? [h] : [];
|
|
605
|
+
});
|
|
606
|
+
const entries = useEntryKeys(highlights.length);
|
|
607
|
+
const textRef = useRef(null);
|
|
608
|
+
const noteRef = useRef(null);
|
|
609
|
+
const toggleRef = useRef(null);
|
|
610
|
+
const [selection, setSelection] = useState(null);
|
|
611
|
+
const [typing, setTyping] = useState(false);
|
|
612
|
+
const [passage, setPassage] = useState("");
|
|
613
|
+
const [note, setNote] = useState("");
|
|
614
|
+
const full = highlights.length >= maxHighlights;
|
|
615
|
+
const save = (next) => binding.setValue(next.length ? next : void 0);
|
|
616
|
+
const hintId = useId();
|
|
617
|
+
const typed = passage.trim();
|
|
618
|
+
const found = typing && typed ? text.indexOf(typed) : -1;
|
|
619
|
+
const draft = typing ? found >= 0 ? { start: found, end: found + typed.length } : null : selection;
|
|
620
|
+
const draftKey = draft ? `${draft.start}:${draft.end}` : "";
|
|
621
|
+
const wasSelection = useRef(false);
|
|
622
|
+
useEffect(() => {
|
|
623
|
+
if (draftKey && wasSelection.current) noteRef.current?.focus();
|
|
624
|
+
}, [draftKey]);
|
|
625
|
+
const readSelection = () => {
|
|
626
|
+
const root = textRef.current;
|
|
627
|
+
const sel = typeof window === "undefined" ? null : window.getSelection();
|
|
628
|
+
if (!root || !sel || sel.rangeCount === 0 || sel.isCollapsed || full) return;
|
|
629
|
+
const range = sel.getRangeAt(0);
|
|
630
|
+
if (!root.contains(range.startContainer) || !root.contains(range.endContainer)) return;
|
|
631
|
+
let start = textOffset(root, range.startContainer, range.startOffset);
|
|
632
|
+
let end = textOffset(root, range.endContainer, range.endOffset);
|
|
633
|
+
if (start > end) [start, end] = [end, start];
|
|
634
|
+
while (start < end && /\s/.test(text[start])) start++;
|
|
635
|
+
while (end > start && /\s/.test(text[end - 1])) end--;
|
|
636
|
+
if (end <= start) return;
|
|
637
|
+
wasSelection.current = true;
|
|
638
|
+
setTyping(false);
|
|
639
|
+
setSelection({ start, end });
|
|
640
|
+
};
|
|
641
|
+
const reset = () => {
|
|
642
|
+
setSelection(null);
|
|
643
|
+
setPassage("");
|
|
644
|
+
setNote("");
|
|
645
|
+
setTyping(false);
|
|
646
|
+
wasSelection.current = false;
|
|
647
|
+
};
|
|
648
|
+
const commit = () => {
|
|
649
|
+
if (!draft || !note.trim() || full) return;
|
|
650
|
+
const h = { start: draft.start, end: draft.end, quote: text.slice(draft.start, draft.end), note: note.trim() };
|
|
651
|
+
const at = highlights.findIndex((x) => x.start > h.start);
|
|
652
|
+
const index = at === -1 ? highlights.length : at;
|
|
653
|
+
entries.insert(index);
|
|
654
|
+
save([...highlights.slice(0, index), h, ...highlights.slice(index)]);
|
|
655
|
+
reset();
|
|
656
|
+
window.getSelection?.()?.removeAllRanges();
|
|
657
|
+
toggleRef.current?.focus();
|
|
658
|
+
};
|
|
659
|
+
if (!text) {
|
|
660
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "There is no passage to highlight." }) });
|
|
661
|
+
}
|
|
662
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs(Stack, { gap: "md", children: [
|
|
663
|
+
/* @__PURE__ */ jsx(
|
|
664
|
+
"p",
|
|
665
|
+
{
|
|
666
|
+
ref: textRef,
|
|
667
|
+
id: `${inputId}-text`,
|
|
668
|
+
"aria-describedby": hintId,
|
|
669
|
+
className: "m-0 whitespace-pre-wrap rounded-md border border-input bg-muted/30 p-3 text-base leading-relaxed",
|
|
670
|
+
onMouseUp: readSelection,
|
|
671
|
+
onTouchEnd: readSelection,
|
|
672
|
+
onKeyUp: readSelection,
|
|
673
|
+
children: segments(text, highlights).map(
|
|
674
|
+
(s) => s.marked ? /* @__PURE__ */ jsx("mark", { className: "rounded-sm bg-primary/20 text-foreground", children: s.text }, s.start) : /* @__PURE__ */ jsx("span", { children: s.text }, s.start)
|
|
675
|
+
)
|
|
676
|
+
}
|
|
677
|
+
),
|
|
678
|
+
/* @__PURE__ */ jsx("p", { id: hintId, className: "m-0 text-sm text-muted-foreground", children: full ? `You've added the most highlights this question takes (${maxHighlights}).` : "Select words in the passage to highlight them, or use Highlight a passage to type them." }),
|
|
679
|
+
!full ? /* @__PURE__ */ jsx(
|
|
680
|
+
Button,
|
|
681
|
+
{
|
|
682
|
+
ref: toggleRef,
|
|
683
|
+
type: "button",
|
|
684
|
+
variant: "outline",
|
|
685
|
+
size: "sm",
|
|
686
|
+
className: "self-start",
|
|
687
|
+
"aria-expanded": typing,
|
|
688
|
+
onClick: () => {
|
|
689
|
+
setSelection(null);
|
|
690
|
+
wasSelection.current = false;
|
|
691
|
+
setTyping((t) => !t);
|
|
692
|
+
},
|
|
693
|
+
children: "Highlight a passage"
|
|
694
|
+
}
|
|
695
|
+
) : null,
|
|
696
|
+
typing && !full ? /* @__PURE__ */ jsxs("div", { className: "space-y-1.5", children: [
|
|
697
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `${inputId}-passage`, children: "Passage" }),
|
|
698
|
+
/* @__PURE__ */ jsx(
|
|
699
|
+
Input,
|
|
700
|
+
{
|
|
701
|
+
id: `${inputId}-passage`,
|
|
702
|
+
value: passage,
|
|
703
|
+
"aria-describedby": `${inputId}-passage-hint`,
|
|
704
|
+
"aria-invalid": typed && found < 0 ? true : void 0,
|
|
705
|
+
onChange: (e) => setPassage(e.target.value),
|
|
706
|
+
onKeyDown: (e) => {
|
|
707
|
+
if (e.key === "Enter") {
|
|
708
|
+
e.preventDefault();
|
|
709
|
+
if (found >= 0) noteRef.current?.focus();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
),
|
|
714
|
+
/* @__PURE__ */ jsx("p", { id: `${inputId}-passage-hint`, className: "m-0 text-sm text-muted-foreground", children: typed && found < 0 ? "Not found in the passage. Type the words exactly as they appear." : "Type the words exactly as they appear." })
|
|
715
|
+
] }) : null,
|
|
716
|
+
draft && !full ? /* @__PURE__ */ jsxs("div", { role: "group", "aria-label": "New highlight", className: "space-y-2 rounded-md border border-input p-3", children: [
|
|
717
|
+
/* @__PURE__ */ jsx("blockquote", { className: "m-0 border-l-2 border-primary pl-3 text-sm", children: /* @__PURE__ */ jsx("mark", { className: "bg-primary/20 text-foreground", children: text.slice(draft.start, draft.end) }) }),
|
|
718
|
+
/* @__PURE__ */ jsx(Label, { htmlFor: `${inputId}-note`, children: "Your note" }),
|
|
719
|
+
/* @__PURE__ */ jsx(
|
|
720
|
+
Textarea,
|
|
721
|
+
{
|
|
722
|
+
ref: noteRef,
|
|
723
|
+
id: `${inputId}-note`,
|
|
724
|
+
rows: 2,
|
|
725
|
+
maxLength: 1e3,
|
|
726
|
+
value: note,
|
|
727
|
+
onChange: (e) => setNote(e.target.value)
|
|
728
|
+
}
|
|
729
|
+
),
|
|
730
|
+
/* @__PURE__ */ jsxs(Stack, { direction: "row", gap: "sm", children: [
|
|
731
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", disabled: !note.trim(), onClick: commit, children: "Highlight" }),
|
|
732
|
+
/* @__PURE__ */ jsx(Button, { type: "button", size: "sm", variant: "ghost", onClick: reset, children: "Cancel" })
|
|
733
|
+
] })
|
|
734
|
+
] }) : null,
|
|
735
|
+
highlights.length ? /* @__PURE__ */ jsx("ol", { "aria-label": `${binding.label}: highlights`, className: "m-0 list-none space-y-3 p-0", children: highlights.map((h, index) => {
|
|
736
|
+
const name = `\u201C${excerpt(h.quote)}\u201D`;
|
|
737
|
+
return /* @__PURE__ */ jsxs("li", { className: "space-y-1.5", children: [
|
|
738
|
+
/* @__PURE__ */ jsx("blockquote", { className: "m-0 border-l-2 border-primary pl-3 text-sm", children: h.quote }),
|
|
739
|
+
/* @__PURE__ */ jsxs(Stack, { direction: "row", align: "center", gap: "sm", children: [
|
|
740
|
+
/* @__PURE__ */ jsx(
|
|
741
|
+
Input,
|
|
742
|
+
{
|
|
743
|
+
"aria-label": `Note on ${name}`,
|
|
744
|
+
"aria-invalid": invalid(props) || (h.note.trim() ? void 0 : true),
|
|
745
|
+
maxLength: 1e3,
|
|
746
|
+
value: h.note,
|
|
747
|
+
onChange: (e) => save(highlights.map((x, j) => j === index ? { ...x, note: e.target.value } : x)),
|
|
748
|
+
onKeyDown: (e) => {
|
|
749
|
+
if (e.key === "Enter") e.preventDefault();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
),
|
|
753
|
+
/* @__PURE__ */ jsx(
|
|
754
|
+
RemoveButton,
|
|
755
|
+
{
|
|
756
|
+
label: `Remove highlight ${name}`,
|
|
757
|
+
onClick: () => {
|
|
758
|
+
entries.remove(index);
|
|
759
|
+
save(highlights.filter((_, j) => j !== index));
|
|
760
|
+
toggleRef.current?.focus();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
)
|
|
764
|
+
] })
|
|
765
|
+
] }, entries.keys[index]);
|
|
766
|
+
}) }) : null
|
|
767
|
+
] }) });
|
|
768
|
+
}
|
|
769
|
+
var structuredRenderers = [
|
|
770
|
+
{ type: "list", Component: ListField },
|
|
771
|
+
{ type: "table", Component: TableField },
|
|
772
|
+
{ type: "media-notes", Component: MediaNotesField },
|
|
773
|
+
{ type: "text-highlights", Component: TextHighlightsField }
|
|
774
|
+
];
|
|
121
775
|
function groupAria(props) {
|
|
122
776
|
return {
|
|
123
777
|
"aria-labelledby": `${props.inputId}-label`,
|
|
@@ -263,32 +917,88 @@ function scaleValues(type, max) {
|
|
|
263
917
|
if (type === "nps") return Array.from({ length: 11 }, (_, i) => i);
|
|
264
918
|
return Array.from({ length: max }, (_, i) => i + 1);
|
|
265
919
|
}
|
|
920
|
+
function endLabels(props) {
|
|
921
|
+
const own = scaleLabels(props.binding.field);
|
|
922
|
+
if (props.binding.field.type !== "nps") return own;
|
|
923
|
+
return { low: own.low ?? "Not likely", high: own.high ?? "Very likely" };
|
|
924
|
+
}
|
|
925
|
+
function EndLabels({ inputId, low, high }) {
|
|
926
|
+
if (!low && !high) return null;
|
|
927
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex justify-between gap-4 text-sm text-muted-foreground", "data-gf-scale-labels": "", children: [
|
|
928
|
+
/* @__PURE__ */ jsx("span", { id: `${inputId}-low`, children: low ?? "" }),
|
|
929
|
+
/* @__PURE__ */ jsx("span", { id: `${inputId}-high`, className: "text-right", children: high ?? "" })
|
|
930
|
+
] });
|
|
931
|
+
}
|
|
266
932
|
function ScaleField(props) {
|
|
267
933
|
const { binding, inputId } = props;
|
|
268
934
|
const max = typeof binding.field.props?.max === "number" ? binding.field.props.max : 5;
|
|
269
935
|
const values = scaleValues(binding.field.type, max);
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
936
|
+
const { low, high } = endLabels(props);
|
|
937
|
+
const stars = binding.field.type === "rating" && ratingDisplay(binding.field) === "stars";
|
|
938
|
+
const describe = (i) => low && i === 0 ? `${inputId}-low` : high && i === values.length - 1 ? `${inputId}-high` : void 0;
|
|
939
|
+
const value = typeof binding.value === "number" ? String(binding.value) : "";
|
|
940
|
+
const onValueChange = (v) => binding.setValue(Number(v));
|
|
941
|
+
if (stars) {
|
|
942
|
+
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
943
|
+
/* @__PURE__ */ jsx(StarRadioGroup, { props, values, value, onValueChange, describe }),
|
|
944
|
+
/* @__PURE__ */ jsx(EndLabels, { inputId, low, high })
|
|
945
|
+
] });
|
|
946
|
+
}
|
|
947
|
+
const options = values.map((n, i) => ({ value: String(n), label: String(n), describedBy: describe(i) }));
|
|
276
948
|
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
277
|
-
/* @__PURE__ */ jsx(
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
props,
|
|
281
|
-
options,
|
|
282
|
-
value: typeof binding.value === "number" ? String(binding.value) : "",
|
|
283
|
-
onValueChange: (v) => binding.setValue(Number(v))
|
|
284
|
-
}
|
|
285
|
-
),
|
|
286
|
-
nps ? /* @__PURE__ */ jsxs("div", { className: "flex justify-between text-sm text-muted-foreground", children: [
|
|
287
|
-
/* @__PURE__ */ jsx("span", { id: `${inputId}-low`, children: "Not likely" }),
|
|
288
|
-
/* @__PURE__ */ jsx("span", { id: `${inputId}-high`, children: "Very likely" })
|
|
289
|
-
] }) : null
|
|
949
|
+
/* @__PURE__ */ jsx(ButtonRadioGroup, { props, options, value, onValueChange }),
|
|
950
|
+
/* @__PURE__ */ jsx(EndLabels, { inputId, low, high })
|
|
290
951
|
] });
|
|
291
952
|
}
|
|
953
|
+
function StarIcon({ filled }) {
|
|
954
|
+
return /* @__PURE__ */ jsx(
|
|
955
|
+
"svg",
|
|
956
|
+
{
|
|
957
|
+
viewBox: "0 0 24 24",
|
|
958
|
+
"aria-hidden": "true",
|
|
959
|
+
className: cn("size-8 transition-colors motion-reduce:transition-none", filled ? "fill-primary text-primary" : "fill-transparent text-muted-foreground"),
|
|
960
|
+
stroke: "currentColor",
|
|
961
|
+
strokeWidth: "1.75",
|
|
962
|
+
strokeLinejoin: "round",
|
|
963
|
+
children: /* @__PURE__ */ jsx("path", { d: "M12 2.5l2.9 6.1 6.6.8-4.9 4.6 1.3 6.6L12 17.3l-5.9 3.3 1.3-6.6-4.9-4.6 6.6-.8z" })
|
|
964
|
+
}
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
function StarRadioGroup({
|
|
968
|
+
props,
|
|
969
|
+
values,
|
|
970
|
+
value,
|
|
971
|
+
onValueChange,
|
|
972
|
+
describe
|
|
973
|
+
}) {
|
|
974
|
+
const pick = useDeliberatePick(props.onAdvance);
|
|
975
|
+
const chosen = value === "" ? 0 : Number(value);
|
|
976
|
+
const max = values[values.length - 1] ?? 5;
|
|
977
|
+
return /* @__PURE__ */ jsx(
|
|
978
|
+
RadioGroup,
|
|
979
|
+
{
|
|
980
|
+
...groupAria(props),
|
|
981
|
+
...pick.rootProps,
|
|
982
|
+
value,
|
|
983
|
+
onValueChange,
|
|
984
|
+
className: "flex flex-wrap justify-start gap-1",
|
|
985
|
+
"data-gf-stars": "",
|
|
986
|
+
children: values.map((n, i) => /* @__PURE__ */ jsxs("div", { className: "relative", children: [
|
|
987
|
+
/* @__PURE__ */ jsx(
|
|
988
|
+
RadioGroupItem,
|
|
989
|
+
{
|
|
990
|
+
value: String(n),
|
|
991
|
+
"aria-label": `${n} of ${max}`,
|
|
992
|
+
"aria-describedby": describe(i),
|
|
993
|
+
onClick: pick.onItemClick,
|
|
994
|
+
className: "peer size-11 aspect-auto rounded-md border-0 bg-transparent shadow-none hover:bg-accent data-[state=checked]:bg-transparent [&_svg]:hidden"
|
|
995
|
+
}
|
|
996
|
+
),
|
|
997
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 flex items-center justify-center peer-disabled:opacity-50", children: /* @__PURE__ */ jsx(StarIcon, { filled: n <= chosen }) })
|
|
998
|
+
] }, n))
|
|
999
|
+
}
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
292
1002
|
function YesNoField(props) {
|
|
293
1003
|
const { binding } = props;
|
|
294
1004
|
const current = binding.value === true ? "yes" : binding.value === false ? "no" : "";
|
|
@@ -418,6 +1128,115 @@ function MultiChoiceField(props) {
|
|
|
418
1128
|
] }, o.value))
|
|
419
1129
|
] }) });
|
|
420
1130
|
}
|
|
1131
|
+
function useLineClass() {
|
|
1132
|
+
const { presentation, layout } = useFormPresentation();
|
|
1133
|
+
if (presentation === "page" && layout === "focus") {
|
|
1134
|
+
return "h-auto rounded-none border-0 border-b-2 border-input bg-transparent px-0 py-2 text-xl shadow-none md:text-2xl sm:text-2xl focus-visible:border-primary focus-visible:ring-0 focus-visible:ring-offset-0 aria-invalid:border-destructive";
|
|
1135
|
+
}
|
|
1136
|
+
return presentation === "page" ? "h-11 md:text-base" : void 0;
|
|
1137
|
+
}
|
|
1138
|
+
function controlAria(props) {
|
|
1139
|
+
return {
|
|
1140
|
+
id: props.inputId,
|
|
1141
|
+
"data-gf-focus": "",
|
|
1142
|
+
"aria-invalid": props.binding.error ? true : void 0,
|
|
1143
|
+
"aria-required": props.binding.field.required || void 0,
|
|
1144
|
+
"aria-describedby": describedBy(props)
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
function DropdownField(props) {
|
|
1148
|
+
const { binding } = props;
|
|
1149
|
+
const options = choiceOptions(binding.field);
|
|
1150
|
+
const line = useLineClass();
|
|
1151
|
+
const value = typeof binding.value === "string" ? binding.value : "";
|
|
1152
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs(
|
|
1153
|
+
"select",
|
|
1154
|
+
{
|
|
1155
|
+
...controlAria(props),
|
|
1156
|
+
value,
|
|
1157
|
+
onChange: (e) => binding.setValue(e.target.value === "" ? void 0 : e.target.value),
|
|
1158
|
+
className: cn(
|
|
1159
|
+
"flex h-10 w-full min-w-0 rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm aria-invalid:border-destructive",
|
|
1160
|
+
value === "" && "text-muted-foreground",
|
|
1161
|
+
line
|
|
1162
|
+
),
|
|
1163
|
+
children: [
|
|
1164
|
+
/* @__PURE__ */ jsx("option", { value: "", children: binding.field.placeholder ?? "Choose\u2026" }),
|
|
1165
|
+
options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label ?? o.value }, o.value))
|
|
1166
|
+
]
|
|
1167
|
+
}
|
|
1168
|
+
) });
|
|
1169
|
+
}
|
|
1170
|
+
function NumberField(props) {
|
|
1171
|
+
const { binding } = props;
|
|
1172
|
+
const { min, max, step } = numberLimits(binding.field);
|
|
1173
|
+
const line = useLineClass();
|
|
1174
|
+
const shown = typeof binding.value === "number" ? String(binding.value) : typeof binding.value === "string" ? binding.value : "";
|
|
1175
|
+
const [text, setText] = useState(shown);
|
|
1176
|
+
useEffect(() => {
|
|
1177
|
+
setText((typed) => typed.trim() !== "" && Number(typed.trim().replace(",", ".")) === binding.value ? typed : shown);
|
|
1178
|
+
}, [shown, binding.value]);
|
|
1179
|
+
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
1180
|
+
/* @__PURE__ */ jsx(
|
|
1181
|
+
Input,
|
|
1182
|
+
{
|
|
1183
|
+
...controlAria(props),
|
|
1184
|
+
type: "text",
|
|
1185
|
+
inputMode: min !== void 0 && min >= 0 && (step === void 0 || Number.isInteger(step)) ? "numeric" : "decimal",
|
|
1186
|
+
autoComplete: "off",
|
|
1187
|
+
value: text,
|
|
1188
|
+
placeholder: binding.field.placeholder,
|
|
1189
|
+
className: line,
|
|
1190
|
+
onChange: (e) => {
|
|
1191
|
+
const raw = e.target.value;
|
|
1192
|
+
setText(raw);
|
|
1193
|
+
const t = raw.trim().replace(",", ".");
|
|
1194
|
+
if (t === "") binding.setValue(void 0);
|
|
1195
|
+
else {
|
|
1196
|
+
const n = Number(t);
|
|
1197
|
+
binding.setValue(Number.isFinite(n) && /^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(t) ? n : raw);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
),
|
|
1202
|
+
min !== void 0 || max !== void 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-gf-number-range": "", children: min !== void 0 && max !== void 0 ? `From ${min} to ${max}` : min !== void 0 ? `${min} or more` : `${max} or less` }) : null
|
|
1203
|
+
] });
|
|
1204
|
+
}
|
|
1205
|
+
function DateField(props) {
|
|
1206
|
+
const { binding } = props;
|
|
1207
|
+
const { min, max } = dateLimits(binding.field);
|
|
1208
|
+
const line = useLineClass();
|
|
1209
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsx(
|
|
1210
|
+
Input,
|
|
1211
|
+
{
|
|
1212
|
+
...controlAria(props),
|
|
1213
|
+
type: "date",
|
|
1214
|
+
min,
|
|
1215
|
+
max,
|
|
1216
|
+
value: typeof binding.value === "string" ? binding.value : "",
|
|
1217
|
+
className: cn("w-full sm:w-auto sm:min-w-48", line),
|
|
1218
|
+
onChange: (e) => binding.setValue(e.target.value === "" ? void 0 : e.target.value)
|
|
1219
|
+
}
|
|
1220
|
+
) });
|
|
1221
|
+
}
|
|
1222
|
+
function ConsentField(props) {
|
|
1223
|
+
const { binding, inputId } = props;
|
|
1224
|
+
const text = typeof binding.field.props?.text === "string" && binding.field.props.text.trim() ? binding.field.props.text : "I agree";
|
|
1225
|
+
const look = useChoiceLook(inputId, 1);
|
|
1226
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-0`, className: look.row, children: [
|
|
1227
|
+
/* @__PURE__ */ jsx(
|
|
1228
|
+
Checkbox,
|
|
1229
|
+
{
|
|
1230
|
+
id: `${inputId}-0`,
|
|
1231
|
+
checked: binding.value === true,
|
|
1232
|
+
"aria-invalid": binding.error ? true : void 0,
|
|
1233
|
+
"aria-required": binding.field.required || void 0,
|
|
1234
|
+
onCheckedChange: (c) => binding.setValue(c === true ? true : void 0)
|
|
1235
|
+
}
|
|
1236
|
+
),
|
|
1237
|
+
/* @__PURE__ */ jsx("span", { children: props.engine.text(text) })
|
|
1238
|
+
] }) });
|
|
1239
|
+
}
|
|
421
1240
|
var builtInRenderers = [
|
|
422
1241
|
{ type: "short-text", Component: (p) => /* @__PURE__ */ jsx(TextField, { ...p }) },
|
|
423
1242
|
{ type: "long-text", Component: (p) => /* @__PURE__ */ jsx(TextField, { ...p, multiline: true }) },
|
|
@@ -429,21 +1248,128 @@ var builtInRenderers = [
|
|
|
429
1248
|
{ type: "nps", Component: ScaleField },
|
|
430
1249
|
{ type: "single-choice", Component: SingleChoiceField },
|
|
431
1250
|
{ type: "multi-choice", Component: MultiChoiceField },
|
|
432
|
-
{ type: "yes-no", Component: YesNoField }
|
|
1251
|
+
{ type: "yes-no", Component: YesNoField },
|
|
1252
|
+
{ type: "dropdown", Component: DropdownField },
|
|
1253
|
+
{ type: "number", Component: NumberField },
|
|
1254
|
+
{ type: "date", Component: DateField },
|
|
1255
|
+
{ type: "consent", Component: ConsentField },
|
|
1256
|
+
...structuredRenderers,
|
|
1257
|
+
...calculatedRenderers
|
|
433
1258
|
];
|
|
434
1259
|
var defaultRegistry = createRendererRegistry(builtInRenderers);
|
|
1260
|
+
|
|
1261
|
+
// src/theme.ts
|
|
1262
|
+
function parseColor(value) {
|
|
1263
|
+
const v = value.trim().toLowerCase();
|
|
1264
|
+
let m = /^#([0-9a-f]{3})$/.exec(v);
|
|
1265
|
+
if (m) return [...m[1]].map((c) => parseInt(c + c, 16));
|
|
1266
|
+
m = /^#([0-9a-f]{6})$/.exec(v);
|
|
1267
|
+
if (m) return [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16));
|
|
1268
|
+
m = /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/.exec(v);
|
|
1269
|
+
if (m) {
|
|
1270
|
+
const rgb = [m[1], m[2], m[3]].map(Number);
|
|
1271
|
+
return rgb.every((c) => c <= 255) ? rgb : null;
|
|
1272
|
+
}
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
function rgbToHsl([r, g, b]) {
|
|
1276
|
+
const [rn, gn, bn] = [r / 255, g / 255, b / 255];
|
|
1277
|
+
const max = Math.max(rn, gn, bn);
|
|
1278
|
+
const min = Math.min(rn, gn, bn);
|
|
1279
|
+
const l = (max + min) / 2;
|
|
1280
|
+
if (max === min) return [0, 0, l * 100];
|
|
1281
|
+
const d = max - min;
|
|
1282
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
1283
|
+
const h = max === rn ? (gn - bn) / d + (gn < bn ? 6 : 0) : max === gn ? (bn - rn) / d + 2 : (rn - gn) / d + 4;
|
|
1284
|
+
return [h * 60, s * 100, l * 100];
|
|
1285
|
+
}
|
|
1286
|
+
function hslToRgb(h, s, l) {
|
|
1287
|
+
const sn = s / 100;
|
|
1288
|
+
const ln = l / 100;
|
|
1289
|
+
const k = (n) => (n + h / 30) % 12;
|
|
1290
|
+
const a = sn * Math.min(ln, 1 - ln);
|
|
1291
|
+
const f = (n) => ln - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
|
|
1292
|
+
return [f(0), f(8), f(4)].map((x) => Math.round(x * 255));
|
|
1293
|
+
}
|
|
1294
|
+
function luminance([r, g, b]) {
|
|
1295
|
+
const c = (x) => {
|
|
1296
|
+
const n = x / 255;
|
|
1297
|
+
return n <= 0.03928 ? n / 12.92 : ((n + 0.055) / 1.055) ** 2.4;
|
|
1298
|
+
};
|
|
1299
|
+
return 0.2126 * c(r) + 0.7152 * c(g) + 0.0722 * c(b);
|
|
1300
|
+
}
|
|
1301
|
+
function contrast(a, b) {
|
|
1302
|
+
const [x, y] = [luminance(a), luminance(b)].sort((p, q) => q - p);
|
|
1303
|
+
return (x + 0.05) / (y + 0.05);
|
|
1304
|
+
}
|
|
1305
|
+
var WHITE = [255, 255, 255];
|
|
1306
|
+
var NEAR_BLACK = hslToRgb(240, 10, 3.9);
|
|
1307
|
+
var WHITE_HSL = "0 0% 100%";
|
|
1308
|
+
var NEAR_BLACK_HSL = "240 10% 3.9%";
|
|
1309
|
+
var fmt = (h, s, l) => `${Math.round(h)} ${Math.round(s)}% ${Math.round(l * 10) / 10}%`;
|
|
1310
|
+
function fit(h, s, l, page, darken) {
|
|
1311
|
+
let light = l;
|
|
1312
|
+
for (let i = 0; i < 100 && contrast(hslToRgb(h, s, light), page) < 3; i++) light = Math.max(0, Math.min(100, light + (darken ? -1 : 1)));
|
|
1313
|
+
const best = () => {
|
|
1314
|
+
const rgb = hslToRgb(h, s, light);
|
|
1315
|
+
const white = contrast(rgb, WHITE);
|
|
1316
|
+
const dark = contrast(rgb, NEAR_BLACK);
|
|
1317
|
+
return white >= dark ? { ratio: white, fg: WHITE_HSL } : { ratio: dark, fg: NEAR_BLACK_HSL };
|
|
1318
|
+
};
|
|
1319
|
+
let pick = best();
|
|
1320
|
+
for (let i = 0; i < 100 && pick.ratio < 4.5; i++) {
|
|
1321
|
+
light = Math.min(100, light + 1);
|
|
1322
|
+
pick = { ratio: contrast(hslToRgb(h, s, light), NEAR_BLACK), fg: NEAR_BLACK_HSL };
|
|
1323
|
+
}
|
|
1324
|
+
return { primary: fmt(h, s, light), primaryForeground: pick.fg };
|
|
1325
|
+
}
|
|
1326
|
+
function themeColors(accent) {
|
|
1327
|
+
const rgb = parseColor(accent);
|
|
1328
|
+
if (!rgb) return null;
|
|
1329
|
+
const [h, s, l] = rgbToHsl(rgb);
|
|
1330
|
+
return { light: fit(h, s, l, WHITE, true), dark: fit(h, s, l, NEAR_BLACK, false) };
|
|
1331
|
+
}
|
|
1332
|
+
function themeCss(theme, selector) {
|
|
1333
|
+
const colors = theme?.accent ? themeColors(theme.accent) : null;
|
|
1334
|
+
if (!colors) return "";
|
|
1335
|
+
const block = (c) => `--primary: ${c.primary}; --primary-foreground: ${c.primaryForeground}; --ring: ${c.primary};`;
|
|
1336
|
+
return `${selector} { ${block(colors.light)} }
|
|
1337
|
+
.dark ${selector}, ${selector}.dark { ${block(colors.dark)} }`;
|
|
1338
|
+
}
|
|
435
1339
|
function renderSlot(Default, override, props, extra) {
|
|
436
1340
|
if (override === null) return null;
|
|
437
1341
|
const Component = override ?? Default;
|
|
438
1342
|
return /* @__PURE__ */ jsx(Component, { ...props, ...extra });
|
|
439
1343
|
}
|
|
440
1344
|
var focusRing = "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ring-offset-background";
|
|
441
|
-
function
|
|
1345
|
+
function RequiredNote({ text, className }) {
|
|
1346
|
+
return /* @__PURE__ */ jsxs("p", { className: cn("text-sm text-muted-foreground", className), "data-gf-required-note": "", children: [
|
|
1347
|
+
/* @__PURE__ */ jsxs("span", { className: "text-destructive", "aria-hidden": "true", children: [
|
|
1348
|
+
"*",
|
|
1349
|
+
" "
|
|
1350
|
+
] }),
|
|
1351
|
+
text
|
|
1352
|
+
] });
|
|
1353
|
+
}
|
|
1354
|
+
function HeaderImage({ image, className }) {
|
|
1355
|
+
return /* @__PURE__ */ jsx(
|
|
1356
|
+
"img",
|
|
1357
|
+
{
|
|
1358
|
+
src: image.src,
|
|
1359
|
+
alt: image.alt,
|
|
1360
|
+
"data-gf-header-image": "",
|
|
1361
|
+
className: cn("block h-32 w-full object-cover sm:h-44", className)
|
|
1362
|
+
}
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
function DefaultHeader({ title, description, image, requiredNote, className }) {
|
|
442
1366
|
const { presentation, layout } = useFormPresentation();
|
|
443
1367
|
if (presentation === "page" && layout === "focus") {
|
|
444
1368
|
return /* @__PURE__ */ jsxs("header", { className: cn("space-y-1", className), children: [
|
|
1369
|
+
image ? /* @__PURE__ */ jsx(HeaderImage, { image, className: "mb-4 rounded-lg" }) : null,
|
|
445
1370
|
/* @__PURE__ */ jsx("h1", { className: "text-sm font-medium text-muted-foreground", children: title }),
|
|
446
|
-
description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description }) : null
|
|
1371
|
+
description ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: description }) : null,
|
|
1372
|
+
requiredNote ? /* @__PURE__ */ jsx(RequiredNote, { text: requiredNote }) : null
|
|
447
1373
|
] });
|
|
448
1374
|
}
|
|
449
1375
|
if (presentation === "page") {
|
|
@@ -451,19 +1377,26 @@ function DefaultHeader({ title, description, className }) {
|
|
|
451
1377
|
"header",
|
|
452
1378
|
{
|
|
453
1379
|
className: cn(
|
|
454
|
-
"rounded-xl border
|
|
1380
|
+
"overflow-hidden rounded-xl border bg-card text-card-foreground",
|
|
1381
|
+
!image && "border-t-8 border-t-primary",
|
|
455
1382
|
className
|
|
456
1383
|
),
|
|
457
1384
|
children: [
|
|
458
|
-
/* @__PURE__ */ jsx(
|
|
459
|
-
|
|
1385
|
+
image ? /* @__PURE__ */ jsx(HeaderImage, { image }) : null,
|
|
1386
|
+
/* @__PURE__ */ jsxs("div", { className: cn("px-5 py-6 sm:px-8 sm:py-8", image && "border-t-8 border-t-primary"), children: [
|
|
1387
|
+
/* @__PURE__ */ jsx("h1", { className: "text-3xl font-semibold tracking-tight text-balance sm:text-4xl", children: title }),
|
|
1388
|
+
description ? /* @__PURE__ */ jsx("p", { className: "mt-3 text-base text-muted-foreground text-pretty", children: description }) : null,
|
|
1389
|
+
requiredNote ? /* @__PURE__ */ jsx(RequiredNote, { text: requiredNote, className: "mt-4" }) : null
|
|
1390
|
+
] })
|
|
460
1391
|
]
|
|
461
1392
|
}
|
|
462
1393
|
);
|
|
463
1394
|
}
|
|
464
1395
|
return /* @__PURE__ */ jsxs("header", { className: cn("space-y-1", className), children: [
|
|
1396
|
+
image ? /* @__PURE__ */ jsx(HeaderImage, { image, className: "mb-4 rounded-lg" }) : null,
|
|
465
1397
|
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold tracking-tight", children: title }),
|
|
466
|
-
description ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: description }) : null
|
|
1398
|
+
description ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: description }) : null,
|
|
1399
|
+
requiredNote ? /* @__PURE__ */ jsx(RequiredNote, { text: requiredNote, className: "pt-1" }) : null
|
|
467
1400
|
] });
|
|
468
1401
|
}
|
|
469
1402
|
function initials(name) {
|
|
@@ -485,6 +1418,7 @@ function DefaultBrand({ name, logo, className }) {
|
|
|
485
1418
|
function DefaultProgress({ step, total, className }) {
|
|
486
1419
|
const { presentation } = useFormPresentation();
|
|
487
1420
|
const page = presentation === "page";
|
|
1421
|
+
if (!total) return null;
|
|
488
1422
|
const percent = total ? Math.round((page ? step - 1 : step) / total * 100) : 0;
|
|
489
1423
|
const bar = /* @__PURE__ */ jsx(
|
|
490
1424
|
Progress,
|
|
@@ -695,10 +1629,12 @@ function FormBody({
|
|
|
695
1629
|
layout: layoutProp,
|
|
696
1630
|
presentation = "embedded",
|
|
697
1631
|
brand,
|
|
1632
|
+
theme,
|
|
698
1633
|
onSubmit,
|
|
699
1634
|
onChange,
|
|
700
1635
|
autoAdvance = true,
|
|
701
|
-
submitLabel = "
|
|
1636
|
+
submitLabel = "Send",
|
|
1637
|
+
requiredLabel = "Required",
|
|
702
1638
|
nextLabel = "OK",
|
|
703
1639
|
retryLabel = "Try again",
|
|
704
1640
|
slots,
|
|
@@ -835,6 +1771,10 @@ function FormBody({
|
|
|
835
1771
|
};
|
|
836
1772
|
const liveRegion = /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", "aria-atomic": "true", className: "sr-only", "data-gf-live": "", children: announcement });
|
|
837
1773
|
const presentationValue = { presentation, layout };
|
|
1774
|
+
const themeId = `gf-theme-${useId().replace(/[^a-zA-Z0-9_-]/g, "")}`;
|
|
1775
|
+
const themeStyle = themeCss(theme, `[data-gf-theme="${themeId}"]`);
|
|
1776
|
+
const themed = themeStyle ? { "data-gf-theme": themeId } : {};
|
|
1777
|
+
const styleNode = themeStyle ? /* @__PURE__ */ jsx("style", { children: themeStyle }) : null;
|
|
838
1778
|
const brandNode = brand ? renderSlot(DefaultBrand, slots?.brand, { name: brand.name, logo: brand.logo }, slotProps?.brand) : null;
|
|
839
1779
|
if (showEnding) {
|
|
840
1780
|
const end = def.endings?.find((x) => x.id === ending) ?? def.endings?.[0];
|
|
@@ -849,10 +1789,12 @@ function FormBody({
|
|
|
849
1789
|
);
|
|
850
1790
|
return /* @__PURE__ */ jsxs(FormPresentationProvider, { value: presentationValue, children: [
|
|
851
1791
|
liveRegion,
|
|
1792
|
+
styleNode,
|
|
852
1793
|
page ? /* @__PURE__ */ jsxs(
|
|
853
1794
|
"div",
|
|
854
1795
|
{
|
|
855
1796
|
ref: endingRef,
|
|
1797
|
+
...themed,
|
|
856
1798
|
"data-presentation": "page",
|
|
857
1799
|
"data-testid": testID,
|
|
858
1800
|
className: cn("flex min-h-dvh w-full flex-col bg-background text-foreground", className),
|
|
@@ -861,7 +1803,7 @@ function FormBody({
|
|
|
861
1803
|
/* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center px-4 pb-24 pt-8 sm:px-6", children: /* @__PURE__ */ jsx("div", { className: "w-full max-w-xl", children: endingNode }) })
|
|
862
1804
|
]
|
|
863
1805
|
}
|
|
864
|
-
) : /* @__PURE__ */ jsxs("div", { ref: endingRef, className: cn("mx-auto w-full max-w-xl space-y-6", className), "data-testid": testID, children: [
|
|
1806
|
+
) : /* @__PURE__ */ jsxs("div", { ref: endingRef, ...themed, className: cn("mx-auto w-full max-w-xl space-y-6", className), "data-testid": testID, children: [
|
|
865
1807
|
brandNode,
|
|
866
1808
|
endingNode
|
|
867
1809
|
] })
|
|
@@ -910,10 +1852,16 @@ function FormBody({
|
|
|
910
1852
|
}));
|
|
911
1853
|
const currentField = pageFocus && current ? engine.field(current) : void 0;
|
|
912
1854
|
const keyHint = currentField ? currentField.type === "long-text" ? `${modKey} + Enter` : "Enter \u21B5" : void 0;
|
|
1855
|
+
const hasRequired = def.blocks.some((b) => b.kind === "field" && b.required === true);
|
|
913
1856
|
const header = renderSlot(
|
|
914
1857
|
DefaultHeader,
|
|
915
1858
|
slots?.header,
|
|
916
|
-
{
|
|
1859
|
+
{
|
|
1860
|
+
title: def.title,
|
|
1861
|
+
description,
|
|
1862
|
+
image: theme?.headerImage ? { src: theme.headerImage, alt: theme.headerImageAlt ?? "" } : void 0,
|
|
1863
|
+
requiredNote: hasRequired && requiredLabel ? requiredLabel : void 0
|
|
1864
|
+
},
|
|
917
1865
|
slotProps?.header
|
|
918
1866
|
);
|
|
919
1867
|
const errorSummary = errorItems.length > 0 && layout === "classic" ? renderSlot(
|
|
@@ -926,7 +1874,10 @@ function FormBody({
|
|
|
926
1874
|
},
|
|
927
1875
|
slotProps?.errorSummary
|
|
928
1876
|
) : null;
|
|
929
|
-
const progress =
|
|
1877
|
+
const progress = (
|
|
1878
|
+
// No questions on the path: no "Question 1 of 0".
|
|
1879
|
+
layout === "focus" && path.length > 0 ? renderSlot(DefaultProgress, slots?.progress, { step, total: path.length }, slotProps?.progress) : null
|
|
1880
|
+
);
|
|
930
1881
|
const fields = (
|
|
931
1882
|
// Disabled once the response is completed (sending, or after a failed send): read-only, not dead.
|
|
932
1883
|
/* @__PURE__ */ jsx("fieldset", { disabled: locked, className: "min-w-0 border-0 p-0", children: layout === "focus" ? /* @__PURE__ */ jsx(
|
|
@@ -966,6 +1917,7 @@ function FormBody({
|
|
|
966
1917
|
slotProps?.navigation
|
|
967
1918
|
);
|
|
968
1919
|
const formProps = {
|
|
1920
|
+
...themed,
|
|
969
1921
|
ref: formRef,
|
|
970
1922
|
noValidate: true,
|
|
971
1923
|
onSubmit: handleSubmit,
|
|
@@ -976,6 +1928,7 @@ function FormBody({
|
|
|
976
1928
|
};
|
|
977
1929
|
return /* @__PURE__ */ jsxs(FormPresentationProvider, { value: presentationValue, children: [
|
|
978
1930
|
liveRegion,
|
|
1931
|
+
styleNode,
|
|
979
1932
|
pageFocus ? /* @__PURE__ */ jsxs(
|
|
980
1933
|
"form",
|
|
981
1934
|
{
|
|
@@ -1019,6 +1972,6 @@ function FormBody({
|
|
|
1019
1972
|
}
|
|
1020
1973
|
var classicSection = "rounded-xl border bg-card px-5 py-6 text-card-foreground sm:px-8 has-[[aria-invalid=true]]:border-destructive";
|
|
1021
1974
|
|
|
1022
|
-
export { FieldChrome, Form, builtInRenderers, defaultRegistry, describedBy, useFormPresentation };
|
|
1975
|
+
export { FieldChrome, Form, MediaTimeContext, SourceTextContext, builtInRenderers, defaultRegistry, describedBy, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText };
|
|
1023
1976
|
//# sourceMappingURL=index.js.map
|
|
1024
1977
|
//# sourceMappingURL=index.js.map
|