@nextlyhq/plugin-form-builder 0.0.2-alpha.35 → 0.0.2-alpha.37

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.
@@ -2,8 +2,9 @@ import {
2
2
  DEFAULT_FORM_SETTINGS,
3
3
  evaluateConditions,
4
4
  formatExportValue,
5
+ isKnownFormField,
5
6
  normalizeFormSettings
6
- } from "../chunk-I4VFGPNR.js";
7
+ } from "../chunk-WVI7N3QO.js";
7
8
 
8
9
  // src/admin/index.ts
9
10
  import {
@@ -265,7 +266,8 @@ function SubmissionSheet({
265
266
  ] })
266
267
  ] }),
267
268
  /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-6", children: [
268
- submission.status === "spam" && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border border-destructive/40 bg-destructive/5 p-3 text-sm text-destructive", children: [
269
+ submission.status === "spam" && // Full-strength destructive border so the spam banner boundary is perceivable over its tinted fill.
270
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border border-destructive bg-destructive/5 p-3 text-sm text-destructive", children: [
269
271
  /* @__PURE__ */ jsx(TriangleAlert, { className: "h-4 w-4 shrink-0", "aria-hidden": "true" }),
270
272
  "Flagged as spam",
271
273
  submission.spamReason ? ` (${submission.spamReason})` : "",
@@ -371,7 +373,7 @@ function SubmissionSheet({
371
373
  /* @__PURE__ */ jsxs("p", { children: [
372
374
  "IP: ",
373
375
  submission.ipAddress || "\u2014",
374
- /* @__PURE__ */ jsx("span", { className: "mx-1.5 text-muted-foreground/40", children: "\xB7" }),
376
+ /* @__PURE__ */ jsx("span", { className: "mx-1.5 text-muted-foreground", children: "\xB7" }),
375
377
  "ID: ",
376
378
  /* @__PURE__ */ jsx("span", { className: "font-mono", children: submission.id })
377
379
  ] }),
@@ -444,7 +446,8 @@ function StatusBadge({ status }) {
444
446
  Badge2,
445
447
  {
446
448
  variant: "outline",
447
- className: `rounded-none px-1.5 py-0 text-[10px] font-semibold uppercase tracking-wider ${status === "spam" ? "border-destructive/40 text-destructive" : status === "new" ? "border-primary/40 text-primary" : "border-border text-muted-foreground"}`,
449
+ className: `rounded-none px-1.5 py-0 text-[10px] font-semibold uppercase tracking-wider ${// Full-strength status borders (destructive/primary) so each badge boundary is perceivable; the default falls back to border-border, keeping the states distinct.
450
+ status === "spam" ? "border-destructive text-destructive" : status === "new" ? "border-primary text-primary" : "border-border text-muted-foreground"}`,
448
451
  children: label
449
452
  }
450
453
  );
@@ -908,6 +911,7 @@ import {
908
911
  verticalListSortingStrategy
909
912
  } from "@dnd-kit/sortable";
910
913
  import { CSS } from "@dnd-kit/utilities";
914
+ import { usePluginFieldTypeEntries } from "@nextlyhq/plugin-sdk/admin";
911
915
  import {
912
916
  Button as Button5,
913
917
  DropdownMenu as DropdownMenu2,
@@ -994,15 +998,18 @@ function createNotification() {
994
998
  }
995
999
  var FormBuilderContext = createContext(null);
996
1000
  function generateFieldName(type, existingNames = []) {
1001
+ const base = type.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+/, "").replace(/^([0-9])/, "field_$1").replace(/_+$/, "") || "field";
997
1002
  const taken = new Set(existingNames);
998
- if (!taken.has(type)) return type;
1003
+ if (!taken.has(base)) return base;
999
1004
  let suffix = 2;
1000
- while (taken.has(`${type}_${suffix}`)) suffix += 1;
1001
- return `${type}_${suffix}`;
1005
+ while (taken.has(`${base}_${suffix}`)) suffix += 1;
1006
+ return `${base}_${suffix}`;
1002
1007
  }
1003
1008
  function generateFieldLabel(type) {
1004
1009
  const entry = FORM_FIELD_TYPE_CATALOG.find((row) => row.type === type);
1005
- return entry?.label ?? "New Field";
1010
+ if (entry) return entry.label;
1011
+ const titleCased = type.split(/[-_\s]+/).filter(Boolean).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
1012
+ return titleCased || "New Field";
1006
1013
  }
1007
1014
  function createFieldFromType(type, existingNames = []) {
1008
1015
  const name = generateFieldName(type, existingNames);
@@ -1054,7 +1061,7 @@ function createFieldFromType(type, existingNames = []) {
1054
1061
  case "hidden":
1055
1062
  return { ...baseField, type: "hidden" };
1056
1063
  default:
1057
- return { ...baseField, type: "text" };
1064
+ return { ...baseField, type };
1058
1065
  }
1059
1066
  }
1060
1067
  function FormBuilderProvider({
@@ -1099,9 +1106,7 @@ function FormBuilderProvider({
1099
1106
  const updateField = useCallback2(
1100
1107
  (fieldName, updates) => {
1101
1108
  setFieldsState(
1102
- (prev) => prev.map(
1103
- (f) => f.name === fieldName ? { ...f, ...updates } : f
1104
- )
1109
+ (prev) => prev.map((f) => f.name === fieldName ? { ...f, ...updates } : f)
1105
1110
  );
1106
1111
  if (updates.name && updates.name !== fieldName) {
1107
1112
  setSelectedFieldId(updates.name);
@@ -1341,7 +1346,8 @@ function AddFieldDialog({
1341
1346
 
1342
1347
  // src/admin/components/builder/FieldEditor.tsx
1343
1348
  import {
1344
- FieldOptionsEditor
1349
+ FieldOptionsEditor,
1350
+ withOptionIds
1345
1351
  } from "@nextlyhq/plugin-sdk/admin";
1346
1352
  import {
1347
1353
  FormLabelWithTooltip as FormLabelWithTooltip2,
@@ -1463,7 +1469,8 @@ function ConditionalLogicEditor({
1463
1469
  }
1464
1470
  )
1465
1471
  ] }) }),
1466
- logic.enabled && /* @__PURE__ */ jsxs4("div", { className: "space-y-4 pt-4 border-t border-primary/5", children: [
1472
+ logic.enabled && // Semantic border token so this top divider is visible at the 3:1 UI minimum.
1473
+ /* @__PURE__ */ jsxs4("div", { className: "space-y-4 pt-4 border-t border-border", children: [
1467
1474
  /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2 text-sm text-muted-foreground", children: [
1468
1475
  /* @__PURE__ */ jsxs4(
1469
1476
  Select3,
@@ -1496,66 +1503,72 @@ function ConditionalLogicEditor({
1496
1503
  ),
1497
1504
  /* @__PURE__ */ jsx5("span", { children: "of the following conditions are met:" })
1498
1505
  ] }),
1499
- /* @__PURE__ */ jsx5("div", { className: "space-y-3 pt-4", children: logic.conditions.length === 0 ? /* @__PURE__ */ jsx5("div", { className: "p-4 bg-muted rounded-none border border-dashed border-primary/5 text-center text-xs text-muted-foreground", children: "No conditions defined. Add a condition to get started." }) : logic.conditions.map((condition, index) => /* @__PURE__ */ jsxs4(
1500
- "div",
1501
- {
1502
- className: "flex flex-col gap-2 p-3 rounded-none bg-muted border border-primary/5 relative group",
1503
- children: [
1504
- /* @__PURE__ */ jsxs4("div", { className: "grid grid-cols-1 gap-2", children: [
1505
- /* @__PURE__ */ jsxs4(
1506
- Select3,
1507
- {
1508
- value: condition.field,
1509
- onValueChange: (value) => handleUpdateCondition(index, { field: value }),
1510
- children: [
1511
- /* @__PURE__ */ jsx5(SelectTrigger3, { className: "w-full bg-transparent border-input dark:bg-muted/50", children: /* @__PURE__ */ jsx5(SelectValue3, { placeholder: "Select field" }) }),
1512
- /* @__PURE__ */ jsx5(SelectContent3, { children: availableFields.map((f) => /* @__PURE__ */ jsx5(SelectItem3, { value: f.name, children: f.label || f.name }, f.name)) })
1513
- ]
1514
- }
1515
- ),
1516
- /* @__PURE__ */ jsxs4("div", { className: "flex gap-2", children: [
1506
+ /* @__PURE__ */ jsx5("div", { className: "space-y-3 pt-4", children: logic.conditions.length === 0 ? (
1507
+ // Semantic border token so this empty-state boundary is visible at the 3:1 UI minimum.
1508
+ /* @__PURE__ */ jsx5("div", { className: "p-4 bg-muted rounded-none border border-dashed border-border text-center text-xs text-muted-foreground", children: "No conditions defined. Add a condition to get started." })
1509
+ ) : logic.conditions.map((condition, index) => (
1510
+ // Semantic border token so this condition card boundary is visible at the 3:1 UI minimum.
1511
+ /* @__PURE__ */ jsxs4(
1512
+ "div",
1513
+ {
1514
+ className: "flex flex-col gap-2 p-3 rounded-none bg-muted border border-border relative group",
1515
+ children: [
1516
+ /* @__PURE__ */ jsxs4("div", { className: "grid grid-cols-1 gap-2", children: [
1517
1517
  /* @__PURE__ */ jsxs4(
1518
1518
  Select3,
1519
1519
  {
1520
- value: condition.comparison,
1521
- onValueChange: (value) => handleUpdateCondition(index, {
1522
- comparison: value
1523
- }),
1520
+ value: condition.field,
1521
+ onValueChange: (value) => handleUpdateCondition(index, { field: value }),
1524
1522
  children: [
1525
- /* @__PURE__ */ jsx5(SelectTrigger3, { className: "flex-1 h-9 bg-transparent border-input dark:bg-muted/50", children: /* @__PURE__ */ jsx5(SelectValue3, { placeholder: "Compare operator" }) }),
1526
- /* @__PURE__ */ jsx5(SelectContent3, { children: COMPARISON_OPERATORS.map((op) => /* @__PURE__ */ jsx5(SelectItem3, { value: op.value, children: op.label }, op.value)) })
1523
+ /* @__PURE__ */ jsx5(SelectTrigger3, { className: "w-full bg-transparent border-input dark:bg-muted/50", children: /* @__PURE__ */ jsx5(SelectValue3, { placeholder: "Select field" }) }),
1524
+ /* @__PURE__ */ jsx5(SelectContent3, { children: availableFields.map((f) => /* @__PURE__ */ jsx5(SelectItem3, { value: f.name, children: f.label || f.name }, f.name)) })
1527
1525
  ]
1528
1526
  }
1529
1527
  ),
1530
- needsValue(condition.comparison) && /* @__PURE__ */ jsx5(
1531
- Input2,
1532
- {
1533
- type: "text",
1534
- value: conditionValueToString(condition.value),
1535
- onChange: (e) => handleUpdateCondition(index, {
1536
- value: e.target.value
1537
- }),
1538
- placeholder: "Value",
1539
- className: "flex-1 h-9 bg-transparent"
1540
- }
1541
- )
1542
- ] })
1543
- ] }),
1544
- /* @__PURE__ */ jsx5(
1545
- Button4,
1546
- {
1547
- type: "button",
1548
- variant: "ghost",
1549
- size: "icon-sm",
1550
- onClick: () => handleRemoveCondition(index),
1551
- className: "absolute -top-2 -right-2 h-6 w-6 rounded-full bg-background border border-primary/5 opacity-0 group-hover:opacity-100 transition-opacity",
1552
- title: "Remove condition",
1553
- children: /* @__PURE__ */ jsx5("span", { className: "text-xs", children: "\xD7" })
1554
- }
1555
- )
1556
- ]
1557
- },
1558
- index
1528
+ /* @__PURE__ */ jsxs4("div", { className: "flex gap-2", children: [
1529
+ /* @__PURE__ */ jsxs4(
1530
+ Select3,
1531
+ {
1532
+ value: condition.comparison,
1533
+ onValueChange: (value) => handleUpdateCondition(index, {
1534
+ comparison: value
1535
+ }),
1536
+ children: [
1537
+ /* @__PURE__ */ jsx5(SelectTrigger3, { className: "flex-1 h-9 bg-transparent border-input dark:bg-muted/50", children: /* @__PURE__ */ jsx5(SelectValue3, { placeholder: "Compare operator" }) }),
1538
+ /* @__PURE__ */ jsx5(SelectContent3, { children: COMPARISON_OPERATORS.map((op) => /* @__PURE__ */ jsx5(SelectItem3, { value: op.value, children: op.label }, op.value)) })
1539
+ ]
1540
+ }
1541
+ ),
1542
+ needsValue(condition.comparison) && /* @__PURE__ */ jsx5(
1543
+ Input2,
1544
+ {
1545
+ type: "text",
1546
+ value: conditionValueToString(condition.value),
1547
+ onChange: (e) => handleUpdateCondition(index, {
1548
+ value: e.target.value
1549
+ }),
1550
+ placeholder: "Value",
1551
+ className: "flex-1 h-9 bg-transparent"
1552
+ }
1553
+ )
1554
+ ] })
1555
+ ] }),
1556
+ /* @__PURE__ */ jsx5(
1557
+ Button4,
1558
+ {
1559
+ type: "button",
1560
+ variant: "ghost",
1561
+ size: "icon-sm",
1562
+ onClick: () => handleRemoveCondition(index),
1563
+ className: "absolute -top-2 -right-2 h-6 w-6 rounded-full bg-background border border-border opacity-0 group-hover:opacity-100 transition-opacity",
1564
+ title: "Remove condition",
1565
+ children: /* @__PURE__ */ jsx5("span", { className: "text-xs", children: "\xD7" })
1566
+ }
1567
+ )
1568
+ ]
1569
+ },
1570
+ index
1571
+ )
1559
1572
  )) }),
1560
1573
  availableFields.length > 0 ? /* @__PURE__ */ jsx5(
1561
1574
  Button4,
@@ -1567,7 +1580,10 @@ function ConditionalLogicEditor({
1567
1580
  className: "w-full mt-4 border-dashed",
1568
1581
  children: "+ Add Condition"
1569
1582
  }
1570
- ) : /* @__PURE__ */ jsx5("p", { className: "mt-4 text-center text-xs text-warning font-medium bg-warning/10 p-2 rounded-none border border-warning/20", children: "Add more fields to the form to create conditions." })
1583
+ ) : (
1584
+ // Full-strength warning border so this notice boundary is perceivable over its tinted fill.
1585
+ /* @__PURE__ */ jsx5("p", { className: "mt-4 text-center text-xs text-warning font-medium bg-warning/10 p-2 rounded-none border border-warning", children: "Add more fields to the form to create conditions." })
1586
+ )
1571
1587
  ] })
1572
1588
  ] });
1573
1589
  }
@@ -1719,7 +1735,7 @@ function GeneralTab({
1719
1735
  }
1720
1736
  )
1721
1737
  ] }),
1722
- /* @__PURE__ */ jsx6(TypeSpecificOptions, { field, onUpdate })
1738
+ isKnownFormField(field) && /* @__PURE__ */ jsx6(TypeSpecificOptions, { field, onUpdate })
1723
1739
  ] });
1724
1740
  }
1725
1741
  function TypeSpecificOptions({
@@ -1924,11 +1940,7 @@ function OptionsEditor({
1924
1940
  }) {
1925
1941
  const optionsField = field;
1926
1942
  const [kitOptions, setKitOptions] = useState5(
1927
- () => (optionsField.options || []).map((opt, index) => ({
1928
- id: `opt_initial_${index}`,
1929
- label: opt.label,
1930
- value: opt.value
1931
- }))
1943
+ () => withOptionIds(optionsField.options || [])
1932
1944
  );
1933
1945
  const handleOptionsChange = useCallback4(
1934
1946
  (next) => {
@@ -1951,8 +1963,7 @@ function OptionsEditor({
1951
1963
  FieldOptionsEditor,
1952
1964
  {
1953
1965
  options: kitOptions,
1954
- onOptionsChange: handleOptionsChange,
1955
- fieldType: field.type === "radio" ? "radio" : "select"
1966
+ onOptionsChange: handleOptionsChange
1956
1967
  }
1957
1968
  )
1958
1969
  ] });
@@ -2135,7 +2146,8 @@ function ValidationTab({
2135
2146
  }
2136
2147
  )
2137
2148
  ] }),
2138
- (field.type === "checkbox" || field.type === "hidden") && /* @__PURE__ */ jsx6("div", { className: "p-3 bg-muted rounded-none text-xs text-muted-foreground text-center border border-dashed border-primary/5", children: "No additional validation options for this field type." })
2149
+ (field.type === "checkbox" || field.type === "hidden") && // Semantic border token so this info-box boundary is visible at the 3:1 UI minimum.
2150
+ /* @__PURE__ */ jsx6("div", { className: "p-3 bg-muted rounded-none text-xs text-muted-foreground text-center border border-dashed border-border", children: "No additional validation options for this field type." })
2139
2151
  ] });
2140
2152
  }
2141
2153
  function FieldEditor({ field, allFields, onUpdate }) {
@@ -2147,7 +2159,7 @@ function FieldEditor({ field, allFields, onUpdate }) {
2147
2159
  onValueChange: setActiveTab,
2148
2160
  className: "flex-1 flex flex-col",
2149
2161
  children: [
2150
- /* @__PURE__ */ jsx6("div", { className: "border-b bg-muted border-primary/5", children: /* @__PURE__ */ jsxs5(TabsList2, { className: "w-full justify-start gap-0", children: [
2162
+ /* @__PURE__ */ jsx6("div", { className: "border-b bg-muted border-border", children: /* @__PURE__ */ jsxs5(TabsList2, { className: "w-full justify-start gap-0", children: [
2151
2163
  /* @__PURE__ */ jsx6(
2152
2164
  TabsTrigger2,
2153
2165
  {
@@ -2182,7 +2194,7 @@ function FieldEditor({ field, allFields, onUpdate }) {
2182
2194
  onUpdate
2183
2195
  }
2184
2196
  ) }),
2185
- /* @__PURE__ */ jsx6(TabsContent, { value: "validation", className: "mt-0", children: /* @__PURE__ */ jsx6(ValidationTab, { field, onUpdate }) }),
2197
+ /* @__PURE__ */ jsx6(TabsContent, { value: "validation", className: "mt-0", children: isKnownFormField(field) ? /* @__PURE__ */ jsx6(ValidationTab, { field, onUpdate }) : /* @__PURE__ */ jsx6("p", { className: "py-4 text-sm text-muted-foreground", children: "This field type manages its own validation." }) }),
2186
2198
  /* @__PURE__ */ jsx6(TabsContent, { value: "conditional", className: "mt-0", children: /* @__PURE__ */ jsx6(
2187
2199
  ConditionalLogicEditor,
2188
2200
  {
@@ -2239,7 +2251,10 @@ function FieldCard({
2239
2251
  {
2240
2252
  ref: setNodeRef,
2241
2253
  style: { transform: CSS.Transform.toString(transform), transition },
2242
- className: isDragging ? "border border-primary/40 bg-card opacity-80 relative z-10" : "border border-border bg-card",
2254
+ className: (
2255
+ // Dragging uses full-strength border-primary so the lifted card stands out; the resting state uses border-border, keeping the two distinct.
2256
+ isDragging ? "border border-primary bg-card opacity-80 relative z-10" : "border border-border bg-card"
2257
+ ),
2243
2258
  children: [
2244
2259
  /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 px-3 py-2.5", children: [
2245
2260
  /* @__PURE__ */ jsx7(
@@ -2249,7 +2264,7 @@ function FieldCard({
2249
2264
  ref: setActivatorNodeRef,
2250
2265
  ...attributes,
2251
2266
  ...listeners,
2252
- className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-none text-muted-foreground/60 hover:text-foreground hover:bg-primary/5 cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
2267
+ className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-none text-muted-foreground hover:text-foreground hover:bg-primary/5 cursor-grab active:cursor-grabbing focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
2253
2268
  "aria-label": `Reorder ${field.label || field.name}. Press Space to lift, arrow keys to move, Space to drop.`,
2254
2269
  children: /* @__PURE__ */ jsx7(Lucide.GripVertical, { className: "h-4 w-4", "aria-hidden": "true" })
2255
2270
  }
@@ -2262,14 +2277,14 @@ function FieldCard({
2262
2277
  "aria-expanded": expanded,
2263
2278
  "aria-controls": bodyId,
2264
2279
  id: headerId,
2265
- className: "flex min-w-0 flex-1 items-center gap-3 rounded-none py-1 text-left cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
2280
+ className: "flex min-w-0 flex-1 items-center gap-3 rounded-none py-1 text-left cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
2266
2281
  children: [
2267
2282
  /* @__PURE__ */ jsx7("span", { className: "flex h-7 w-7 shrink-0 items-center justify-center bg-primary/5 text-primary", children: /* @__PURE__ */ jsx7(CatalogIcon, { name: entry?.icon, className: "h-3.5 w-3.5" }) }),
2268
2283
  /* @__PURE__ */ jsxs6("span", { className: "min-w-0 flex-1", children: [
2269
2284
  /* @__PURE__ */ jsx7("span", { className: "block truncate text-sm font-medium text-foreground", children: field.label || field.name }),
2270
2285
  /* @__PURE__ */ jsxs6("span", { className: "block truncate text-xs text-muted-foreground", children: [
2271
2286
  entry?.label ?? field.type,
2272
- /* @__PURE__ */ jsx7("span", { className: "mx-1.5 text-muted-foreground/40", children: "\xB7" }),
2287
+ /* @__PURE__ */ jsx7("span", { className: "mx-1.5 text-muted-foreground", children: "\xB7" }),
2273
2288
  /* @__PURE__ */ jsx7("span", { className: "font-mono", children: field.name })
2274
2289
  ] })
2275
2290
  ] }),
@@ -2392,7 +2407,11 @@ function FieldCard({
2392
2407
  }
2393
2408
  );
2394
2409
  }
2395
- function FieldCards({ enabledTypes, onAddField }) {
2410
+ function FieldCards({
2411
+ enabledTypes,
2412
+ disabledFieldTypes,
2413
+ onAddField
2414
+ }) {
2396
2415
  const {
2397
2416
  fields,
2398
2417
  notifications,
@@ -2408,11 +2427,22 @@ function FieldCards({ enabledTypes, onAddField }) {
2408
2427
  useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
2409
2428
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
2410
2429
  );
2430
+ const pluginEntries = usePluginFieldTypeEntries("forms");
2411
2431
  const entries = useMemo5(() => {
2412
2432
  if (enabledTypes === null) return null;
2413
2433
  const allowed = new Set(enabledTypes);
2414
- return FORM_FIELD_TYPE_CATALOG2.filter((entry) => allowed.has(entry.type));
2415
- }, [enabledTypes]);
2434
+ const builtinTypes = new Set(
2435
+ FORM_FIELD_TYPE_CATALOG2.map((entry) => entry.type)
2436
+ );
2437
+ const builtins = FORM_FIELD_TYPE_CATALOG2.filter((entry) => allowed.has(entry.type));
2438
+ return [
2439
+ ...builtins,
2440
+ // A plugin type a built-in shadows, or one the host disabled, is excluded.
2441
+ ...pluginEntries.filter(
2442
+ (entry) => !builtinTypes.has(entry.type) && !disabledFieldTypes?.has(entry.type)
2443
+ )
2444
+ ];
2445
+ }, [enabledTypes, pluginEntries, disabledFieldTypes]);
2416
2446
  const referenceMap = useMemo5(
2417
2447
  () => buildFieldReferenceMap(fields, notifications),
2418
2448
  [fields, notifications]
@@ -2642,7 +2672,7 @@ function NotificationCard({
2642
2672
  {
2643
2673
  type: "button",
2644
2674
  onClick: onEdit,
2645
- className: "flex min-w-0 flex-1 items-center gap-3 rounded-none py-1 text-left cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40",
2675
+ className: "flex min-w-0 flex-1 items-center gap-3 rounded-none py-1 text-left cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
2646
2676
  "aria-label": `Edit notification ${notification.name}`,
2647
2677
  children: [
2648
2678
  /* @__PURE__ */ jsxs7("span", { className: "min-w-0 flex-1", children: [
@@ -2663,12 +2693,12 @@ function NotificationCard({
2663
2693
  /* @__PURE__ */ jsxs7("span", { className: "block truncate text-xs text-muted-foreground", children: [
2664
2694
  /* @__PURE__ */ jsx8("span", { className: recipient.missing ? "text-destructive" : "", children: recipient.text }),
2665
2695
  ccCount > 0 && /* @__PURE__ */ jsxs7("span", { children: [
2666
- /* @__PURE__ */ jsx8("span", { className: "mx-1.5 text-muted-foreground/40", children: "\xB7" }),
2696
+ /* @__PURE__ */ jsx8("span", { className: "mx-1.5 text-muted-foreground", children: "\xB7" }),
2667
2697
  "+",
2668
2698
  ccCount,
2669
2699
  " cc/bcc"
2670
2700
  ] }),
2671
- /* @__PURE__ */ jsx8("span", { className: "mx-1.5 text-muted-foreground/40", children: "\xB7" }),
2701
+ /* @__PURE__ */ jsx8("span", { className: "mx-1.5 text-muted-foreground", children: "\xB7" }),
2672
2702
  providerName
2673
2703
  ] })
2674
2704
  ] }),
@@ -2780,7 +2810,7 @@ function AddressChipList({
2780
2810
  "button",
2781
2811
  {
2782
2812
  type: "button",
2783
- className: "text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary/40",
2813
+ className: "text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-primary",
2784
2814
  onClick: () => onChange(addresses.filter((_, i) => i !== index)),
2785
2815
  "aria-label": `Remove ${email}`,
2786
2816
  children: /* @__PURE__ */ jsx8(X, { className: "h-3 w-3", "aria-hidden": "true" })
@@ -3525,6 +3555,20 @@ function PreviewFieldInput({
3525
3555
  );
3526
3556
  }
3527
3557
  }
3558
+ function PluginFieldPreview({ field }) {
3559
+ return /* @__PURE__ */ jsxs8("div", { className: "space-y-1.5", children: [
3560
+ /* @__PURE__ */ jsxs8(Label3, { htmlFor: `preview-${field.name}`, children: [
3561
+ field.label || field.name,
3562
+ field.required && /* @__PURE__ */ jsx9("span", { className: "ml-1 text-destructive", children: "*" })
3563
+ ] }),
3564
+ /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2 border border-dashed border-border bg-muted/40 px-3 py-2 text-sm text-muted-foreground", children: [
3565
+ "Custom field (",
3566
+ field.type,
3567
+ ") \u2014 renders from its plugin component in the live form."
3568
+ ] }),
3569
+ field.helpText && /* @__PURE__ */ jsx9("p", { className: "text-xs text-muted-foreground", children: field.helpText })
3570
+ ] });
3571
+ }
3528
3572
  function FormPreview({ fields, formData }) {
3529
3573
  const { settings } = useFormBuilder();
3530
3574
  const [values, setValues] = useState8({});
@@ -3558,6 +3602,7 @@ function FormPreview({ fields, formData }) {
3558
3602
  const missing = new Set(
3559
3603
  visibleFields.filter((field) => {
3560
3604
  if (!field.required || field.type === "file") return false;
3605
+ if (!isKnownFormField(field)) return false;
3561
3606
  const value = values[field.name];
3562
3607
  return value === void 0 || value === "" || value === false;
3563
3608
  }).map((field) => field.name)
@@ -3614,38 +3659,40 @@ function FormPreview({ fields, formData }) {
3614
3659
  /* @__PURE__ */ jsx9(Button7, { type: "button", variant: "outline", onClick: reset, children: "Fill again" })
3615
3660
  ] }) : /* @__PURE__ */ jsxs8(Fragment3, { children: [
3616
3661
  visibleFields.length === 0 && /* @__PURE__ */ jsx9("p", { className: "text-sm text-muted-foreground", children: "No visible fields \u2014 add fields in the Builder tab." }),
3617
- visibleFields.map((field) => /* @__PURE__ */ jsxs8("div", { className: "space-y-1.5", children: [
3618
- field.type !== "checkbox" && /* @__PURE__ */ jsxs8(Label3, { htmlFor: `preview-${field.name}`, children: [
3619
- field.label || field.name,
3620
- field.required && /* @__PURE__ */ jsx9("span", { className: "ml-1 text-destructive", children: "*" })
3621
- ] }),
3622
- field.type === "checkbox" ? /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
3623
- /* @__PURE__ */ jsx9(
3662
+ visibleFields.map(
3663
+ (field) => !isKnownFormField(field) ? /* @__PURE__ */ jsx9(PluginFieldPreview, { field }, field.name) : /* @__PURE__ */ jsxs8("div", { className: "space-y-1.5", children: [
3664
+ field.type !== "checkbox" && /* @__PURE__ */ jsxs8(Label3, { htmlFor: `preview-${field.name}`, children: [
3665
+ field.label || field.name,
3666
+ field.required && /* @__PURE__ */ jsx9("span", { className: "ml-1 text-destructive", children: "*" })
3667
+ ] }),
3668
+ field.type === "checkbox" ? /* @__PURE__ */ jsxs8("div", { className: "flex items-center gap-2", children: [
3669
+ /* @__PURE__ */ jsx9(
3670
+ PreviewFieldInput,
3671
+ {
3672
+ field,
3673
+ value: values[field.name],
3674
+ onChange: (value) => setValues((prev) => ({
3675
+ ...prev,
3676
+ [field.name]: value
3677
+ }))
3678
+ }
3679
+ ),
3680
+ /* @__PURE__ */ jsxs8(Label3, { htmlFor: `preview-${field.name}`, children: [
3681
+ field.label || field.name,
3682
+ field.required && /* @__PURE__ */ jsx9("span", { className: "ml-1 text-destructive", children: "*" })
3683
+ ] })
3684
+ ] }) : /* @__PURE__ */ jsx9(
3624
3685
  PreviewFieldInput,
3625
3686
  {
3626
3687
  field,
3627
3688
  value: values[field.name],
3628
- onChange: (value) => setValues((prev) => ({
3629
- ...prev,
3630
- [field.name]: value
3631
- }))
3689
+ onChange: (value) => setValues((prev) => ({ ...prev, [field.name]: value }))
3632
3690
  }
3633
3691
  ),
3634
- /* @__PURE__ */ jsxs8(Label3, { htmlFor: `preview-${field.name}`, children: [
3635
- field.label || field.name,
3636
- field.required && /* @__PURE__ */ jsx9("span", { className: "ml-1 text-destructive", children: "*" })
3637
- ] })
3638
- ] }) : /* @__PURE__ */ jsx9(
3639
- PreviewFieldInput,
3640
- {
3641
- field,
3642
- value: values[field.name],
3643
- onChange: (value) => setValues((prev) => ({ ...prev, [field.name]: value }))
3644
- }
3645
- ),
3646
- field.helpText && /* @__PURE__ */ jsx9("p", { className: "text-xs text-muted-foreground", children: field.helpText }),
3647
- missingRequired.has(field.name) && /* @__PURE__ */ jsx9("p", { className: "text-xs text-destructive", children: "This field is required." })
3648
- ] }, field.name)),
3692
+ field.helpText && /* @__PURE__ */ jsx9("p", { className: "text-xs text-muted-foreground", children: field.helpText }),
3693
+ missingRequired.has(field.name) && /* @__PURE__ */ jsx9("p", { className: "text-xs text-destructive", children: "This field is required." })
3694
+ ] }, field.name)
3695
+ ),
3649
3696
  /* @__PURE__ */ jsx9(
3650
3697
  Button7,
3651
3698
  {
@@ -3945,6 +3992,9 @@ function FormBuilderViewInner({
3945
3992
  const [isSaving, setIsSaving] = useState9(false);
3946
3993
  const [saveError, setSaveError] = useState9(null);
3947
3994
  const [enabledTypes, setEnabledTypes] = useState9(null);
3995
+ const [disabledFieldTypes, setDisabledFieldTypes] = useState9(
3996
+ () => /* @__PURE__ */ new Set()
3997
+ );
3948
3998
  const [notificationDefaults, setNotificationDefaults] = useState9(null);
3949
3999
  const [spamDefaults, setSpamDefaults] = useState9(null);
3950
4000
  useEffect4(() => {
@@ -3959,6 +4009,11 @@ function FormBuilderViewInner({
3959
4009
  setEnabledTypes(
3960
4010
  config?.fields ? allTypes.filter((type) => config.fields?.[type] !== false) : allTypes
3961
4011
  );
4012
+ setDisabledFieldTypes(
4013
+ new Set(
4014
+ Object.entries(config?.fields ?? {}).filter(([, enabled]) => enabled === false).map(([type]) => type)
4015
+ )
4016
+ );
3962
4017
  setNotificationDefaults(config?.notifications ?? {});
3963
4018
  setSpamDefaults(config?.spamProtection ?? {});
3964
4019
  }
@@ -4103,7 +4158,8 @@ function FormBuilderViewInner({
4103
4158
  /* @__PURE__ */ jsx11("p", { className: "text-sm text-muted-foreground mt-1", children: isCreating ? "Design a new form with drag-and-drop fields." : "Modify your form fields and settings." })
4104
4159
  ] }),
4105
4160
  /* @__PURE__ */ jsxs10("div", { className: "flex items-center gap-2 shrink-0", children: [
4106
- isDirty && /* @__PURE__ */ jsx11("span", { className: "text-xs font-medium text-warning bg-warning/10 border border-warning/20 px-2.5 py-1 rounded-none whitespace-nowrap", children: "Unsaved changes" }),
4161
+ isDirty && // Full-strength warning border so the unsaved-changes badge boundary is perceivable over its tinted fill.
4162
+ /* @__PURE__ */ jsx11("span", { className: "text-xs font-medium bg-warning-100 text-warning-700 dark:bg-warning-900 dark:text-warning-100 border border-warning px-2.5 py-1 rounded-none whitespace-nowrap", children: "Unsaved changes" }),
4107
4163
  /* @__PURE__ */ jsx11(
4108
4164
  Button8,
4109
4165
  {
@@ -4202,7 +4258,7 @@ function FormBuilderViewInner({
4202
4258
  value: formData.slug || "",
4203
4259
  onChange: (e) => updateFormData({ slug: e.target.value }),
4204
4260
  placeholder: "e.g., contact-form",
4205
- className: "bg-transparent placeholder:text-muted-foreground/50"
4261
+ className: "bg-transparent placeholder:text-muted-foreground"
4206
4262
  }
4207
4263
  )
4208
4264
  ] }),
@@ -4246,7 +4302,7 @@ function FormBuilderViewInner({
4246
4302
  style: {
4247
4303
  borderBottomColor: activeTab === tab.value ? "var(--nx-primary)" : "transparent"
4248
4304
  },
4249
- className: "shrink-0 whitespace-nowrap border-b-2 relative -mb-0.5 data-[state=active]:text-primary data-[state=inactive]:text-muted-foreground hover:text-primary hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/20 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer",
4305
+ className: "shrink-0 whitespace-nowrap border-b-2 relative -mb-0.5 data-[state=active]:text-primary data-[state=inactive]:text-muted-foreground hover:text-primary hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer",
4250
4306
  children: [
4251
4307
  tab.label,
4252
4308
  tab.count !== null && /* @__PURE__ */ jsx11(
@@ -4267,13 +4323,15 @@ function FormBuilderViewInner({
4267
4323
  FieldCards,
4268
4324
  {
4269
4325
  enabledTypes,
4326
+ disabledFieldTypes,
4270
4327
  onAddField: handleAddField
4271
4328
  }
4272
4329
  ),
4273
4330
  activeTab === "preview" && /* @__PURE__ */ jsx11(FormPreview, { fields, formData }),
4274
4331
  activeTab === "settings" && /* @__PURE__ */ jsx11("div", { className: "w-full", children: /* @__PURE__ */ jsx11(FormSettingsTab, { spamDefaults }) }),
4275
4332
  activeTab === "notifications" && /* @__PURE__ */ jsx11("div", { className: "w-full", children: /* @__PURE__ */ jsx11(FormNotificationsTab, { defaults: notificationDefaults }) }),
4276
- saveError && /* @__PURE__ */ jsx11("div", { className: "p-3 text-sm text-destructive bg-destructive/10 border border-destructive/20 rounded-none", children: saveError })
4333
+ saveError && // Full-strength destructive border so the error box boundary is perceivable over its tinted fill.
4334
+ /* @__PURE__ */ jsx11("div", { className: "p-3 text-sm text-destructive bg-destructive/10 border border-destructive rounded-none", children: saveError })
4277
4335
  ] }) }) })
4278
4336
  ] });
4279
4337
  }