@ampless/admin 1.0.0-alpha.52 → 1.0.0-alpha.54

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,589 @@
1
+ 'use client';
2
+ import {
3
+ invalidateSiteSettingsCache
4
+ } from "./chunk-D72XF3Q3.js";
5
+ import {
6
+ useLocale,
7
+ useT
8
+ } from "./chunk-2SVJONQ7.js";
9
+
10
+ // src/components/plugin-settings-form.tsx
11
+ import { useState as useState2 } from "react";
12
+ import {
13
+ resolveLocalized as resolveLocalized2
14
+ } from "ampless";
15
+ import { Button, Input, Label as Label2, Textarea } from "@ampless/runtime/ui";
16
+
17
+ // src/lib/plugin-settings.ts
18
+ import {
19
+ getKvStore,
20
+ SITE_CONFIG_PK,
21
+ isValidPluginKey,
22
+ validatePluginSettingValue
23
+ } from "ampless";
24
+ function pluginSettingKey(instanceId, fieldKey) {
25
+ return `plugins.${instanceId}.${fieldKey}`;
26
+ }
27
+ async function setPluginPublicSetting(instanceId, field, rawValue) {
28
+ if (!isValidPluginKey(instanceId)) {
29
+ throw new Error(`Invalid plugin instanceId: "${instanceId}"`);
30
+ }
31
+ if (!isValidPluginKey(field.key)) {
32
+ throw new Error(`Invalid plugin field key: "${field.key}"`);
33
+ }
34
+ const validated = validatePluginSettingValue(field, rawValue, "strict");
35
+ if (validated === null) {
36
+ throw new Error(`Invalid value for plugin field "${field.key}"`);
37
+ }
38
+ const store = getKvStore();
39
+ await store.put(SITE_CONFIG_PK, pluginSettingKey(instanceId, field.key), validated);
40
+ }
41
+ async function deletePluginPublicSetting(instanceId, field) {
42
+ if (!isValidPluginKey(instanceId)) {
43
+ throw new Error(`Invalid plugin instanceId: "${instanceId}"`);
44
+ }
45
+ if (!isValidPluginKey(field.key)) {
46
+ throw new Error(`Invalid plugin field key: "${field.key}"`);
47
+ }
48
+ const store = getKvStore();
49
+ await store.remove(SITE_CONFIG_PK, pluginSettingKey(instanceId, field.key));
50
+ }
51
+
52
+ // src/components/repeatable-field-editor.tsx
53
+ import { useEffect, useState } from "react";
54
+ import { resolveLocalized } from "ampless";
55
+ import { Label } from "@ampless/runtime/ui";
56
+
57
+ // src/lib/repeatable-field.ts
58
+ function parseRepeatableValue(raw) {
59
+ if (!raw || raw.trim() === "") return [];
60
+ try {
61
+ const parsed = JSON.parse(raw);
62
+ if (!Array.isArray(parsed)) return [];
63
+ return parsed.filter(
64
+ (item) => item !== null && typeof item === "object" && !Array.isArray(item)
65
+ );
66
+ } catch {
67
+ return [];
68
+ }
69
+ }
70
+ function serializeRepeatableValue(items) {
71
+ return JSON.stringify(items);
72
+ }
73
+ function subFieldValueToFormString(_subField, value) {
74
+ if (value === void 0 || value === null) return "";
75
+ if (typeof value === "boolean") return value ? "true" : "false";
76
+ if (typeof value === "number") return String(value);
77
+ if (typeof value === "string") return value;
78
+ return String(value);
79
+ }
80
+ function formStringToSubFieldValue(subField, formString) {
81
+ switch (subField.type) {
82
+ case "boolean":
83
+ return formString === "true";
84
+ case "number": {
85
+ if (formString.trim() === "") return void 0;
86
+ const n = Number(formString);
87
+ return Number.isNaN(n) ? void 0 : n;
88
+ }
89
+ default:
90
+ return formString;
91
+ }
92
+ }
93
+ function makeEmptyItem(field) {
94
+ const item = {};
95
+ for (const sf of field.fields) {
96
+ if (sf.default !== void 0) {
97
+ item[sf.key] = sf.default;
98
+ continue;
99
+ }
100
+ if (sf.type === "boolean") {
101
+ item[sf.key] = false;
102
+ }
103
+ }
104
+ return item;
105
+ }
106
+ function itemLabel(field, item, index) {
107
+ if (field.itemLabelKey) {
108
+ const v = item[field.itemLabelKey];
109
+ if (v !== void 0 && v !== null && String(v).trim() !== "") {
110
+ return String(v);
111
+ }
112
+ }
113
+ return `Item ${index + 1}`;
114
+ }
115
+ function canAddItem(field, currentCount) {
116
+ const max = field.maxItems ?? 50;
117
+ return currentCount < max;
118
+ }
119
+
120
+ // src/components/repeatable-field-editor.tsx
121
+ import { jsx, jsxs } from "react/jsx-runtime";
122
+ function RepeatableFieldEditor({
123
+ field,
124
+ id,
125
+ value,
126
+ invalid,
127
+ onChange
128
+ }) {
129
+ const locale = useLocale();
130
+ const [items, setItems] = useState(
131
+ () => parseRepeatableValue(value)
132
+ );
133
+ useEffect(() => {
134
+ const incoming = parseRepeatableValue(value);
135
+ if (JSON.stringify(incoming) !== JSON.stringify(items)) {
136
+ setItems(incoming);
137
+ }
138
+ }, [value]);
139
+ function commit(next) {
140
+ setItems(next);
141
+ onChange(serializeRepeatableValue(next));
142
+ }
143
+ function updateCell(itemIdx, key, cellValue) {
144
+ const next = items.map(
145
+ (item, idx) => idx === itemIdx ? { ...item, [key]: cellValue } : item
146
+ );
147
+ commit(next);
148
+ }
149
+ function removeItem(itemIdx) {
150
+ commit(items.filter((_, idx) => idx !== itemIdx));
151
+ }
152
+ function addItem() {
153
+ if (!canAddItem(field, items.length)) return;
154
+ commit([...items, makeEmptyItem(field)]);
155
+ }
156
+ const addLabelText = field.addLabel ? resolveLocalized(field.addLabel, locale) : "+ Add item";
157
+ return /* @__PURE__ */ jsxs(
158
+ "div",
159
+ {
160
+ id,
161
+ "aria-invalid": invalid,
162
+ className: [
163
+ "space-y-3 rounded-md border p-3",
164
+ invalid ? "border-destructive" : "border-border"
165
+ ].join(" "),
166
+ children: [
167
+ items.length === 0 && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "No items yet." }),
168
+ items.map((item, itemIdx) => {
169
+ const label = itemLabel(field, item, itemIdx);
170
+ return /* @__PURE__ */ jsxs(
171
+ "div",
172
+ {
173
+ className: "space-y-2 rounded-md border border-border bg-muted/30 p-3",
174
+ children: [
175
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
176
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium", children: label }),
177
+ /* @__PURE__ */ jsx(
178
+ "button",
179
+ {
180
+ type: "button",
181
+ className: "text-xs text-muted-foreground underline-offset-2 hover:text-destructive hover:underline",
182
+ onClick: () => removeItem(itemIdx),
183
+ "aria-label": `Remove ${label}`,
184
+ children: "\xD7 Remove"
185
+ }
186
+ )
187
+ ] }),
188
+ /* @__PURE__ */ jsx("div", { className: "space-y-2", children: field.fields.map((sf) => {
189
+ const cellId = `${id}-${itemIdx}-${sf.key}`;
190
+ const formString = subFieldValueToFormString(sf, item[sf.key]);
191
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
192
+ /* @__PURE__ */ jsxs(Label, { htmlFor: cellId, className: "text-xs", children: [
193
+ resolveLocalized(sf.label, locale),
194
+ sf.required && /* @__PURE__ */ jsx("span", { className: "ml-1 text-destructive", children: "*" })
195
+ ] }),
196
+ renderScalarInput(
197
+ sf,
198
+ cellId,
199
+ formString,
200
+ /* invalid */
201
+ false,
202
+ (s) => updateCell(itemIdx, sf.key, formStringToSubFieldValue(sf, s))
203
+ )
204
+ ] }, sf.key);
205
+ }) })
206
+ ]
207
+ },
208
+ itemIdx
209
+ );
210
+ }),
211
+ /* @__PURE__ */ jsx(
212
+ "button",
213
+ {
214
+ type: "button",
215
+ className: "text-xs text-muted-foreground underline-offset-2 hover:underline disabled:cursor-not-allowed disabled:opacity-50",
216
+ onClick: addItem,
217
+ disabled: !canAddItem(field, items.length),
218
+ children: addLabelText
219
+ }
220
+ )
221
+ ]
222
+ }
223
+ );
224
+ }
225
+
226
+ // src/components/plugin-settings-form.tsx
227
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
228
+ var CACHE_REBUILD_DELAY_MS = 8e3;
229
+ function stringify(field, raw) {
230
+ if (raw === void 0 || raw === null) return "";
231
+ switch (field.type) {
232
+ case "boolean":
233
+ return raw === true ? "true" : raw === false ? "false" : String(raw);
234
+ case "json":
235
+ if (typeof raw === "string") return raw;
236
+ try {
237
+ return JSON.stringify(raw, null, 2);
238
+ } catch {
239
+ return "";
240
+ }
241
+ case "repeatable":
242
+ if (typeof raw === "string") return raw;
243
+ try {
244
+ return JSON.stringify(raw);
245
+ } catch {
246
+ return "[]";
247
+ }
248
+ default:
249
+ return typeof raw === "string" ? raw : String(raw);
250
+ }
251
+ }
252
+ function parse(field, raw) {
253
+ switch (field.type) {
254
+ case "boolean":
255
+ if (raw === "true") return true;
256
+ if (raw === "false") return false;
257
+ return null;
258
+ case "number": {
259
+ const trimmed = raw.trim();
260
+ if (trimmed === "") return null;
261
+ const n = Number(trimmed);
262
+ return Number.isNaN(n) ? null : n;
263
+ }
264
+ case "json": {
265
+ const trimmed = raw.trim();
266
+ if (trimmed === "") return null;
267
+ try {
268
+ return JSON.parse(trimmed);
269
+ } catch {
270
+ return null;
271
+ }
272
+ }
273
+ case "repeatable": {
274
+ const trimmed = raw.trim();
275
+ if (trimmed === "") return [];
276
+ try {
277
+ const parsed = JSON.parse(trimmed);
278
+ return Array.isArray(parsed) ? parsed : null;
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+ default:
284
+ return raw;
285
+ }
286
+ }
287
+ function PluginSettingsForm({
288
+ instanceId,
289
+ displayName,
290
+ fields,
291
+ initialValues
292
+ }) {
293
+ const t = useT();
294
+ const locale = useLocale();
295
+ function defaultDisplay(field) {
296
+ return field.default !== void 0 ? stringify(field, field.default) : "";
297
+ }
298
+ const [state, setState] = useState2(() => {
299
+ const values = {};
300
+ for (const field of fields) {
301
+ const has = Object.prototype.hasOwnProperty.call(initialValues, field.key);
302
+ values[field.key] = has ? stringify(field, initialValues[field.key]) : defaultDisplay(field);
303
+ }
304
+ return { values, touched: {}, invalid: {} };
305
+ });
306
+ const [storedKeys, setStoredKeys] = useState2(
307
+ () => new Set(Object.keys(initialValues))
308
+ );
309
+ const [saving, setSaving] = useState2(false);
310
+ const [error, setError] = useState2(null);
311
+ const [info, setInfo] = useState2(null);
312
+ function update(key, value) {
313
+ setState((prev) => ({
314
+ values: { ...prev.values, [key]: value },
315
+ touched: { ...prev.touched, [key]: true },
316
+ invalid: { ...prev.invalid, [key]: false }
317
+ }));
318
+ setInfo(null);
319
+ }
320
+ function scheduleCacheInvalidation() {
321
+ setTimeout(async () => {
322
+ try {
323
+ await invalidateSiteSettingsCache();
324
+ } catch (err) {
325
+ console.warn("[plugin] cache invalidation failed", err);
326
+ }
327
+ }, CACHE_REBUILD_DELAY_MS);
328
+ }
329
+ async function reset(field) {
330
+ setError(null);
331
+ setInfo(null);
332
+ try {
333
+ await deletePluginPublicSetting(instanceId, field);
334
+ setState((prev) => ({
335
+ values: { ...prev.values, [field.key]: defaultDisplay(field) },
336
+ touched: { ...prev.touched, [field.key]: false },
337
+ invalid: { ...prev.invalid, [field.key]: false }
338
+ }));
339
+ setStoredKeys((prev) => {
340
+ if (!prev.has(field.key)) return prev;
341
+ const next = new Set(prev);
342
+ next.delete(field.key);
343
+ return next;
344
+ });
345
+ setInfo(t("plugins.resetDone"));
346
+ scheduleCacheInvalidation();
347
+ } catch (err) {
348
+ console.error("[plugin] reset failed", err);
349
+ setError(err instanceof Error ? err.message : String(err));
350
+ }
351
+ }
352
+ async function save(e) {
353
+ e.preventDefault();
354
+ setSaving(true);
355
+ setError(null);
356
+ setInfo(null);
357
+ const newInvalid = {};
358
+ const writes = [];
359
+ const writtenKeys = [];
360
+ for (const field of fields) {
361
+ if (!state.touched[field.key]) continue;
362
+ const raw = state.values[field.key] ?? "";
363
+ const parsed = parse(field, raw);
364
+ if (parsed === null && raw !== "") {
365
+ newInvalid[field.key] = true;
366
+ continue;
367
+ }
368
+ if (parsed === null) continue;
369
+ writes.push(setPluginPublicSetting(instanceId, field, parsed));
370
+ writtenKeys.push(field.key);
371
+ }
372
+ if (Object.keys(newInvalid).length > 0) {
373
+ setState((prev) => ({ ...prev, invalid: newInvalid }));
374
+ setSaving(false);
375
+ setError(t("plugins.invalidValue"));
376
+ return;
377
+ }
378
+ try {
379
+ await Promise.all(writes);
380
+ setInfo(t("plugins.saved"));
381
+ setState((prev) => ({ ...prev, touched: {}, invalid: {} }));
382
+ if (writtenKeys.length > 0) {
383
+ setStoredKeys((prev) => {
384
+ const next = new Set(prev);
385
+ for (const k of writtenKeys) next.add(k);
386
+ return next;
387
+ });
388
+ }
389
+ scheduleCacheInvalidation();
390
+ } catch (err) {
391
+ console.error("[plugin] save failed", err);
392
+ setError(err instanceof Error ? err.message : String(err));
393
+ } finally {
394
+ setSaving(false);
395
+ }
396
+ }
397
+ return /* @__PURE__ */ jsxs2("form", { onSubmit: save, className: "max-w-2xl space-y-5 rounded-md border p-4", children: [
398
+ /* @__PURE__ */ jsxs2("div", { children: [
399
+ /* @__PURE__ */ jsx2("h2", { className: "text-base font-semibold", children: displayName ? resolveLocalized2(displayName, locale) : instanceId }),
400
+ /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: instanceId })
401
+ ] }),
402
+ fields.map((field) => /* @__PURE__ */ jsx2(
403
+ FieldRow,
404
+ {
405
+ field,
406
+ value: state.values[field.key] ?? "",
407
+ invalid: !!state.invalid[field.key],
408
+ onChange: (v) => update(field.key, v),
409
+ onReset: () => void reset(field),
410
+ hasStoredValue: storedKeys.has(field.key)
411
+ },
412
+ field.key
413
+ )),
414
+ info && /* @__PURE__ */ jsx2("p", { className: "text-sm text-muted-foreground", children: info }),
415
+ error && /* @__PURE__ */ jsx2("p", { className: "text-sm text-destructive", children: error }),
416
+ /* @__PURE__ */ jsx2(Button, { type: "submit", disabled: saving, children: saving ? t("plugins.saving") : t("plugins.save") })
417
+ ] });
418
+ }
419
+ function FieldRow({ field, value, invalid, onChange, onReset, hasStoredValue }) {
420
+ const t = useT();
421
+ const locale = useLocale();
422
+ const id = `plugin-${field.key}`;
423
+ const labelEl = /* @__PURE__ */ jsxs2(Label2, { htmlFor: id, className: invalid ? "text-destructive" : void 0, children: [
424
+ resolveLocalized2(field.label, locale),
425
+ field.required && /* @__PURE__ */ jsx2("span", { className: "ml-1 text-destructive", children: "*" })
426
+ ] });
427
+ const description = field.description ? /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: resolveLocalized2(field.description, locale) }) : null;
428
+ const input = renderInput(field, id, value, invalid, onChange);
429
+ const placeholder = renderDefaultHint(field, locale);
430
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-1.5", children: [
431
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
432
+ labelEl,
433
+ hasStoredValue && /* @__PURE__ */ jsx2(
434
+ "button",
435
+ {
436
+ type: "button",
437
+ className: "text-xs text-muted-foreground underline-offset-2 hover:underline",
438
+ onClick: onReset,
439
+ children: t("plugins.resetToDefault")
440
+ }
441
+ )
442
+ ] }),
443
+ description,
444
+ input,
445
+ placeholder
446
+ ] });
447
+ }
448
+ function renderDefaultHint(field, _locale) {
449
+ if (field.default === void 0) return null;
450
+ let preview;
451
+ if (typeof field.default === "string") preview = field.default;
452
+ else if (typeof field.default === "boolean" || typeof field.default === "number") {
453
+ preview = String(field.default);
454
+ } else {
455
+ try {
456
+ preview = JSON.stringify(field.default);
457
+ } catch {
458
+ return null;
459
+ }
460
+ }
461
+ if (!preview) return null;
462
+ return /* @__PURE__ */ jsx2("p", { className: "text-xs text-muted-foreground", children: /* @__PURE__ */ jsx2("code", { children: preview }) });
463
+ }
464
+ function renderScalarInput(field, id, value, invalid, onChange) {
465
+ switch (field.type) {
466
+ case "text":
467
+ case "url":
468
+ return /* @__PURE__ */ jsx2(
469
+ Input,
470
+ {
471
+ id,
472
+ value,
473
+ maxLength: field.type === "text" ? field.maxLength : void 0,
474
+ placeholder: "placeholder" in field ? field.placeholder : void 0,
475
+ onChange: (e) => onChange(e.target.value),
476
+ "aria-invalid": invalid,
477
+ type: field.type === "url" ? "url" : "text"
478
+ }
479
+ );
480
+ case "textarea":
481
+ return /* @__PURE__ */ jsx2(
482
+ Textarea,
483
+ {
484
+ id,
485
+ value,
486
+ rows: field.rows ?? 4,
487
+ maxLength: field.maxLength,
488
+ placeholder: field.placeholder,
489
+ onChange: (e) => onChange(e.target.value),
490
+ "aria-invalid": invalid
491
+ }
492
+ );
493
+ case "code":
494
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-1", children: [
495
+ field.language && /* @__PURE__ */ jsx2("p", { className: "text-[10px] uppercase tracking-wide text-muted-foreground", children: field.language }),
496
+ /* @__PURE__ */ jsx2(
497
+ Textarea,
498
+ {
499
+ id,
500
+ value,
501
+ rows: field.rows ?? 8,
502
+ maxLength: field.maxLength,
503
+ placeholder: field.placeholder,
504
+ onChange: (e) => onChange(e.target.value),
505
+ "aria-invalid": invalid,
506
+ className: "font-mono text-xs"
507
+ }
508
+ )
509
+ ] });
510
+ case "boolean":
511
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2", children: [
512
+ /* @__PURE__ */ jsx2(
513
+ "input",
514
+ {
515
+ id,
516
+ type: "checkbox",
517
+ checked: value === "true",
518
+ onChange: (e) => onChange(e.target.checked ? "true" : "false"),
519
+ className: "h-4 w-4"
520
+ }
521
+ ),
522
+ /* @__PURE__ */ jsx2("span", { className: "text-xs text-muted-foreground", children: value === "true" ? "on" : "off" })
523
+ ] });
524
+ case "number":
525
+ return /* @__PURE__ */ jsx2(
526
+ Input,
527
+ {
528
+ id,
529
+ type: "number",
530
+ value,
531
+ min: field.min,
532
+ max: field.max,
533
+ step: field.step,
534
+ onChange: (e) => onChange(e.target.value),
535
+ "aria-invalid": invalid
536
+ }
537
+ );
538
+ case "select":
539
+ return /* @__PURE__ */ jsxs2(
540
+ "select",
541
+ {
542
+ id,
543
+ className: "w-full rounded-md border bg-background px-2 py-1.5 text-sm",
544
+ value,
545
+ onChange: (e) => onChange(e.target.value),
546
+ "aria-invalid": invalid,
547
+ children: [
548
+ /* @__PURE__ */ jsx2("option", { value: "", children: "\u2014" }),
549
+ field.options.map((opt) => /* @__PURE__ */ jsx2("option", { value: opt.value, children: typeof opt.label === "string" ? opt.label : opt.value }, opt.value))
550
+ ]
551
+ }
552
+ );
553
+ case "json":
554
+ return /* @__PURE__ */ jsx2(
555
+ Textarea,
556
+ {
557
+ id,
558
+ value,
559
+ rows: field.rows ?? 8,
560
+ placeholder: field.placeholder ?? "{}",
561
+ onChange: (e) => onChange(e.target.value),
562
+ "aria-invalid": invalid,
563
+ className: "font-mono text-xs"
564
+ }
565
+ );
566
+ }
567
+ }
568
+ function renderInput(field, id, value, invalid, onChange) {
569
+ switch (field.type) {
570
+ case "repeatable":
571
+ return /* @__PURE__ */ jsx2(
572
+ RepeatableFieldEditor,
573
+ {
574
+ field,
575
+ id,
576
+ value,
577
+ invalid,
578
+ onChange
579
+ }
580
+ );
581
+ default:
582
+ return renderScalarInput(field, id, value, invalid, onChange);
583
+ }
584
+ }
585
+
586
+ export {
587
+ PluginSettingsForm,
588
+ renderScalarInput
589
+ };
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { LocalizedString, PluginSettingField } from 'ampless';
2
+ import { LocalizedString, PluginSettingField, PluginRepeatableField } from 'ampless';
3
3
 
4
4
  interface Props {
5
5
  instanceId: string;
@@ -13,5 +13,13 @@ interface Props {
13
13
  initialValues: Record<string, unknown>;
14
14
  }
15
15
  declare function PluginSettingsForm({ instanceId, displayName, fields, initialValues, }: Props): react_jsx_runtime.JSX.Element;
16
+ /**
17
+ * Render a scalar (non-repeatable) plugin field input. Handles the 8
18
+ * scalar variant types: text, url, textarea, code, boolean, number,
19
+ * select, json. Factored out of `renderInput` so the repeatable case
20
+ * can call back into it per sub-field cell without interleaving with
21
+ * the repeatable branch.
22
+ */
23
+ declare function renderScalarInput(field: Exclude<PluginSettingField, PluginRepeatableField>, id: string, value: string, invalid: boolean, onChange: (v: string) => void): React.ReactNode;
16
24
 
17
- export { PluginSettingsForm };
25
+ export { PluginSettingsForm, renderScalarInput };
@@ -1,10 +1,12 @@
1
1
  "use client";
2
2
  import {
3
- PluginSettingsForm
4
- } from "../chunk-6VT6V3XF.js";
3
+ PluginSettingsForm,
4
+ renderScalarInput
5
+ } from "../chunk-OPRC3VJQ.js";
5
6
  import "../chunk-D72XF3Q3.js";
6
7
  import "../chunk-2SVJONQ7.js";
7
8
  import "../chunk-KYFSM7MS.js";
8
9
  export {
9
- PluginSettingsForm
10
+ PluginSettingsForm,
11
+ renderScalarInput
10
12
  };