@squaredr/fieldcraft-pro 1.3.0 → 1.5.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/dist/chunk-22T5PY6L.mjs +1081 -0
- package/dist/{chunk-GNBXIG63.mjs → chunk-6LRUDBC7.mjs} +1305 -89
- package/dist/{chunk-4BX7FCXH.mjs → chunk-BK4SMEW4.mjs} +840 -21
- package/dist/form-builder/index.d.mts +7 -367
- package/dist/form-builder/index.d.ts +7 -367
- package/dist/form-builder/index.js +1307 -91
- package/dist/form-builder/index.mjs +1 -1
- package/dist/index-BHJ4mqHP.d.mts +398 -0
- package/dist/index-BHJ4mqHP.d.ts +398 -0
- package/dist/index.d.mts +30 -3
- package/dist/index.d.ts +30 -3
- package/dist/index.js +3230 -331
- package/dist/index.mjs +276 -4
- package/dist/preview-schema-DFvV4y6J.d.mts +66 -0
- package/dist/preview-schema-DFvV4y6J.d.ts +66 -0
- package/dist/response-viewer/index.d.mts +9 -1
- package/dist/response-viewer/index.d.ts +9 -1
- package/dist/response-viewer/index.js +838 -19
- package/dist/response-viewer/index.mjs +1 -1
- package/dist/styles.css +1 -1
- package/dist/theme-editor/index.d.mts +30 -26
- package/dist/theme-editor/index.d.ts +30 -26
- package/dist/theme-editor/index.js +702 -22
- package/dist/theme-editor/index.mjs +1 -1
- package/package.json +18 -6
- package/theme-editor/styles.css +266 -62
- package/dist/chunk-7LQW5QYT.mjs +0 -407
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ var fieldcraftReact = require('@squaredr/fieldcraft-react');
|
|
|
8
8
|
var fieldcraftCore = require('@squaredr/fieldcraft-core');
|
|
9
9
|
var clsx = require('clsx');
|
|
10
10
|
var tailwindMerge = require('tailwind-merge');
|
|
11
|
+
var react$1 = require('@xyflow/react');
|
|
12
|
+
require('@xyflow/react/dist/style.css');
|
|
11
13
|
|
|
12
14
|
// ../license/dist/index.mjs
|
|
13
15
|
var LEGACY_TIER_MAP = {
|
|
@@ -555,19 +557,19 @@ function UnlicensedOverlay({ featureName, reason, children }) {
|
|
|
555
557
|
)
|
|
556
558
|
] });
|
|
557
559
|
}
|
|
558
|
-
function requireLicense(
|
|
560
|
+
function requireLicense(Component3, featureName) {
|
|
559
561
|
function LicenseGated(props) {
|
|
560
562
|
const { status, tier } = useLicense();
|
|
561
563
|
const isProduction = isProductionEnvironment();
|
|
562
564
|
if (!isProduction) {
|
|
563
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
565
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Component3, { ...props });
|
|
564
566
|
}
|
|
565
567
|
if (status === "validating") {
|
|
566
568
|
return null;
|
|
567
569
|
}
|
|
568
570
|
const isLicensed = status === "valid" && tier != null && tierHasFeature(tier, featureName);
|
|
569
571
|
if (isLicensed) {
|
|
570
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
572
|
+
return /* @__PURE__ */ jsxRuntime.jsx(Component3, { ...props });
|
|
571
573
|
}
|
|
572
574
|
let reason = "A valid license is required for production use.";
|
|
573
575
|
if (status === "invalid") {
|
|
@@ -581,9 +583,9 @@ function requireLicense(Component2, featureName) {
|
|
|
581
583
|
} else if (!tier || !tierHasFeature(tier, featureName)) {
|
|
582
584
|
reason = "Your license does not include this feature. Please upgrade your plan.";
|
|
583
585
|
}
|
|
584
|
-
return /* @__PURE__ */ jsxRuntime.jsx(UnlicensedOverlay, { featureName, reason, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
586
|
+
return /* @__PURE__ */ jsxRuntime.jsx(UnlicensedOverlay, { featureName, reason, children: /* @__PURE__ */ jsxRuntime.jsx(Component3, { ...props }) });
|
|
585
587
|
}
|
|
586
|
-
LicenseGated.displayName = `requireLicense(${
|
|
588
|
+
LicenseGated.displayName = `requireLicense(${Component3.displayName || Component3.name || "Component"})`;
|
|
587
589
|
return LicenseGated;
|
|
588
590
|
}
|
|
589
591
|
var MAX_HISTORY = 50;
|
|
@@ -1030,6 +1032,14 @@ var QUESTION_TYPE_INFO = {
|
|
|
1030
1032
|
icon: "Phone",
|
|
1031
1033
|
description: "US phone number input"
|
|
1032
1034
|
},
|
|
1035
|
+
phone_international: {
|
|
1036
|
+
type: "phone_international",
|
|
1037
|
+
label: "Intl Phone",
|
|
1038
|
+
category: "text",
|
|
1039
|
+
icon: "Globe2",
|
|
1040
|
+
description: "Phone with country code",
|
|
1041
|
+
defaultConfig: { type: "phone_international", defaultCountry: "US" }
|
|
1042
|
+
},
|
|
1033
1043
|
url: {
|
|
1034
1044
|
type: "url",
|
|
1035
1045
|
label: "URL",
|
|
@@ -1123,6 +1133,14 @@ var QUESTION_TYPE_INFO = {
|
|
|
1123
1133
|
requiresOptions: true,
|
|
1124
1134
|
defaultConfig: { type: "ranking", items: [] }
|
|
1125
1135
|
},
|
|
1136
|
+
country_select: {
|
|
1137
|
+
type: "country_select",
|
|
1138
|
+
label: "Country",
|
|
1139
|
+
category: "selection",
|
|
1140
|
+
icon: "Globe",
|
|
1141
|
+
description: "Country dropdown with search",
|
|
1142
|
+
defaultConfig: { type: "country_select" }
|
|
1143
|
+
},
|
|
1126
1144
|
// ── Date/Time ──
|
|
1127
1145
|
date: {
|
|
1128
1146
|
type: "date",
|
|
@@ -1149,7 +1167,97 @@ var QUESTION_TYPE_INFO = {
|
|
|
1149
1167
|
description: "Upload files",
|
|
1150
1168
|
defaultConfig: { type: "file_upload", maxFiles: 1, maxSizeMb: 10 }
|
|
1151
1169
|
},
|
|
1170
|
+
// ── Compound ──
|
|
1171
|
+
legal_name: {
|
|
1172
|
+
type: "legal_name",
|
|
1173
|
+
label: "Legal Name",
|
|
1174
|
+
category: "text",
|
|
1175
|
+
icon: "UserCheck",
|
|
1176
|
+
description: "First, middle, and last name",
|
|
1177
|
+
defaultConfig: { type: "legal_name" }
|
|
1178
|
+
},
|
|
1179
|
+
address: {
|
|
1180
|
+
type: "address",
|
|
1181
|
+
label: "Address",
|
|
1182
|
+
category: "text",
|
|
1183
|
+
icon: "MapPin",
|
|
1184
|
+
description: "Street, city, state, ZIP",
|
|
1185
|
+
defaultConfig: { type: "address", includeCountry: false }
|
|
1186
|
+
},
|
|
1187
|
+
signature: {
|
|
1188
|
+
type: "signature",
|
|
1189
|
+
label: "Signature",
|
|
1190
|
+
category: "media",
|
|
1191
|
+
icon: "PenTool",
|
|
1192
|
+
description: "Canvas signature pad",
|
|
1193
|
+
defaultConfig: { type: "signature" }
|
|
1194
|
+
},
|
|
1195
|
+
image_capture: {
|
|
1196
|
+
type: "image_capture",
|
|
1197
|
+
label: "Camera",
|
|
1198
|
+
category: "media",
|
|
1199
|
+
icon: "Camera",
|
|
1200
|
+
description: "Capture photo from camera",
|
|
1201
|
+
defaultConfig: { type: "image_capture", allowGallery: true }
|
|
1202
|
+
},
|
|
1203
|
+
date_range: {
|
|
1204
|
+
type: "date_range",
|
|
1205
|
+
label: "Date Range",
|
|
1206
|
+
category: "datetime",
|
|
1207
|
+
icon: "CalendarRange",
|
|
1208
|
+
description: "Start and end date picker",
|
|
1209
|
+
defaultConfig: { type: "date_range" }
|
|
1210
|
+
},
|
|
1211
|
+
appointment: {
|
|
1212
|
+
type: "appointment",
|
|
1213
|
+
label: "Appointment",
|
|
1214
|
+
category: "datetime",
|
|
1215
|
+
icon: "CalendarCheck",
|
|
1216
|
+
description: "Date & time slot booking",
|
|
1217
|
+
defaultConfig: { type: "appointment", duration: 30 }
|
|
1218
|
+
},
|
|
1152
1219
|
// ── Advanced ──
|
|
1220
|
+
repeater: {
|
|
1221
|
+
type: "repeater",
|
|
1222
|
+
label: "Repeater",
|
|
1223
|
+
category: "advanced",
|
|
1224
|
+
icon: "ListPlus",
|
|
1225
|
+
description: "Repeatable group of fields",
|
|
1226
|
+
defaultConfig: { type: "repeater", fields: [], minItems: 1, maxItems: 10 }
|
|
1227
|
+
},
|
|
1228
|
+
likert: {
|
|
1229
|
+
type: "likert",
|
|
1230
|
+
label: "Likert Scale",
|
|
1231
|
+
category: "advanced",
|
|
1232
|
+
icon: "AlignHorizontalSpaceAround",
|
|
1233
|
+
description: "Agreement scale (e.g. Strongly Disagree to Strongly Agree)",
|
|
1234
|
+
requiresOptions: true,
|
|
1235
|
+
defaultConfig: {
|
|
1236
|
+
type: "likert",
|
|
1237
|
+
scale: [
|
|
1238
|
+
{ label: "Strongly Disagree", value: "1" },
|
|
1239
|
+
{ label: "Disagree", value: "2" },
|
|
1240
|
+
{ label: "Neutral", value: "3" },
|
|
1241
|
+
{ label: "Agree", value: "4" },
|
|
1242
|
+
{ label: "Strongly Agree", value: "5" }
|
|
1243
|
+
]
|
|
1244
|
+
}
|
|
1245
|
+
},
|
|
1246
|
+
scoring: {
|
|
1247
|
+
type: "scoring",
|
|
1248
|
+
label: "Scoring",
|
|
1249
|
+
category: "advanced",
|
|
1250
|
+
icon: "Award",
|
|
1251
|
+
description: "Scored question with point values per option",
|
|
1252
|
+
requiresOptions: true,
|
|
1253
|
+
defaultConfig: {
|
|
1254
|
+
type: "scoring",
|
|
1255
|
+
options: [
|
|
1256
|
+
{ label: "Option 1", value: "1", score: 1 },
|
|
1257
|
+
{ label: "Option 2", value: "2", score: 2 }
|
|
1258
|
+
]
|
|
1259
|
+
}
|
|
1260
|
+
},
|
|
1153
1261
|
matrix: {
|
|
1154
1262
|
type: "matrix",
|
|
1155
1263
|
label: "Matrix",
|
|
@@ -1179,6 +1287,14 @@ var QUESTION_TYPE_INFO = {
|
|
|
1179
1287
|
description: "Hidden value from URL or static",
|
|
1180
1288
|
defaultConfig: { type: "hidden", source: "static" }
|
|
1181
1289
|
},
|
|
1290
|
+
payment: {
|
|
1291
|
+
type: "payment",
|
|
1292
|
+
label: "Payment",
|
|
1293
|
+
category: "advanced",
|
|
1294
|
+
icon: "CreditCard",
|
|
1295
|
+
description: "Collect payment via Stripe",
|
|
1296
|
+
defaultConfig: { type: "payment", provider: "stripe", publicKey: "", currency: "USD" }
|
|
1297
|
+
},
|
|
1182
1298
|
// ── Structural ──
|
|
1183
1299
|
section_header: {
|
|
1184
1300
|
type: "section_header",
|
|
@@ -1283,7 +1399,7 @@ var DEFAULT_PALETTE = [
|
|
|
1283
1399
|
{
|
|
1284
1400
|
category: "text",
|
|
1285
1401
|
label: "Text Input",
|
|
1286
|
-
types: ["short_text", "long_text", "email", "phone", "url"]
|
|
1402
|
+
types: ["short_text", "long_text", "email", "phone", "phone_international", "url", "legal_name", "address"]
|
|
1287
1403
|
},
|
|
1288
1404
|
{
|
|
1289
1405
|
category: "numeric",
|
|
@@ -1293,17 +1409,17 @@ var DEFAULT_PALETTE = [
|
|
|
1293
1409
|
{
|
|
1294
1410
|
category: "selection",
|
|
1295
1411
|
label: "Selection",
|
|
1296
|
-
types: ["single_select", "multi_select", "dropdown", "boolean", "ranking"]
|
|
1412
|
+
types: ["single_select", "multi_select", "dropdown", "boolean", "ranking", "country_select"]
|
|
1297
1413
|
},
|
|
1298
1414
|
{
|
|
1299
1415
|
category: "datetime",
|
|
1300
1416
|
label: "Date & Time",
|
|
1301
|
-
types: ["date", "time"]
|
|
1417
|
+
types: ["date", "time", "date_range", "appointment"]
|
|
1302
1418
|
},
|
|
1303
1419
|
{
|
|
1304
1420
|
category: "media",
|
|
1305
1421
|
label: "Media",
|
|
1306
|
-
types: ["file_upload"]
|
|
1422
|
+
types: ["file_upload", "signature", "image_capture"]
|
|
1307
1423
|
},
|
|
1308
1424
|
{
|
|
1309
1425
|
category: "content",
|
|
@@ -1318,7 +1434,7 @@ var DEFAULT_PALETTE = [
|
|
|
1318
1434
|
{
|
|
1319
1435
|
category: "advanced",
|
|
1320
1436
|
label: "Advanced",
|
|
1321
|
-
types: ["matrix", "calculated", "hidden"]
|
|
1437
|
+
types: ["repeater", "likert", "scoring", "matrix", "calculated", "hidden", "payment"]
|
|
1322
1438
|
}
|
|
1323
1439
|
];
|
|
1324
1440
|
|
|
@@ -2835,7 +2951,8 @@ function getFieldOptions(schema, excludeId) {
|
|
|
2835
2951
|
return fields;
|
|
2836
2952
|
}
|
|
2837
2953
|
function ConditionEditor({ question, schema, onUpdate }) {
|
|
2838
|
-
const
|
|
2954
|
+
const rawShowIf = question.showIf;
|
|
2955
|
+
const showIf = rawShowIf && rawShowIf.field && !rawShowIf.conditions ? { combine: "AND", conditions: [rawShowIf] } : rawShowIf;
|
|
2839
2956
|
const fieldOptions = getFieldOptions(schema, question.id);
|
|
2840
2957
|
const updateShowIf = (next) => {
|
|
2841
2958
|
onUpdate({ showIf: next });
|
|
@@ -2957,6 +3074,24 @@ function ConditionEditor({ question, schema, onUpdate }) {
|
|
|
2957
3074
|
}
|
|
2958
3075
|
function FormSettingsPanel({ schema, onUpdate }) {
|
|
2959
3076
|
const settings = schema.settings ?? {};
|
|
3077
|
+
const hasConditions = schema.sections.some(
|
|
3078
|
+
(s) => s.showIf || s.questions.some((q) => q.showIf)
|
|
3079
|
+
);
|
|
3080
|
+
const displayModeOptions = hasConditions ? [
|
|
3081
|
+
{ label: "Stepped", value: "stepped" },
|
|
3082
|
+
{ label: "Conversational", value: "conversational" }
|
|
3083
|
+
] : [
|
|
3084
|
+
{ label: "Stepped", value: "stepped" },
|
|
3085
|
+
{ label: "Classic", value: "classic" },
|
|
3086
|
+
{ label: "Conversational", value: "conversational" }
|
|
3087
|
+
];
|
|
3088
|
+
const displayMode = settings.displayMode ?? "stepped";
|
|
3089
|
+
const effectiveDisplayMode = hasConditions && displayMode === "classic" ? "stepped" : displayMode;
|
|
3090
|
+
react.useEffect(() => {
|
|
3091
|
+
if (effectiveDisplayMode !== displayMode) {
|
|
3092
|
+
onUpdate({ ...schema, settings: { ...settings, displayMode: effectiveDisplayMode } });
|
|
3093
|
+
}
|
|
3094
|
+
}, [effectiveDisplayMode, displayMode]);
|
|
2960
3095
|
const updateSettings = (updates) => {
|
|
2961
3096
|
onUpdate({ ...schema, settings: { ...settings, ...updates } });
|
|
2962
3097
|
};
|
|
@@ -2984,12 +3119,8 @@ function FormSettingsPanel({ schema, onUpdate }) {
|
|
|
2984
3119
|
SettingsSelect,
|
|
2985
3120
|
{
|
|
2986
3121
|
label: "Mode",
|
|
2987
|
-
value:
|
|
2988
|
-
options:
|
|
2989
|
-
{ label: "Classic", value: "classic" },
|
|
2990
|
-
{ label: "Stepped", value: "stepped" },
|
|
2991
|
-
{ label: "Conversational", value: "conversational" }
|
|
2992
|
-
],
|
|
3122
|
+
value: effectiveDisplayMode,
|
|
3123
|
+
options: displayModeOptions,
|
|
2993
3124
|
onChange: (v) => updateSettings({ displayMode: v })
|
|
2994
3125
|
}
|
|
2995
3126
|
),
|
|
@@ -3367,206 +3498,1147 @@ function FieldGroup({ label, children }) {
|
|
|
3367
3498
|
children
|
|
3368
3499
|
] });
|
|
3369
3500
|
}
|
|
3370
|
-
var
|
|
3371
|
-
|
|
3372
|
-
return react.useContext(ThemeCtx);
|
|
3373
|
-
}
|
|
3374
|
-
function themeToCssVars(theme) {
|
|
3375
|
-
const vars = {};
|
|
3376
|
-
if (theme.background) vars["--background"] = theme.background;
|
|
3377
|
-
if (theme.foreground) vars["--foreground"] = theme.foreground;
|
|
3378
|
-
if (theme.card) {
|
|
3379
|
-
vars["--card"] = theme.card;
|
|
3380
|
-
vars["--card-foreground"] = theme.foreground ?? "";
|
|
3381
|
-
vars["--popover"] = theme.card;
|
|
3382
|
-
vars["--popover-foreground"] = theme.foreground ?? "";
|
|
3383
|
-
}
|
|
3384
|
-
if (theme.primary) vars["--primary"] = theme.primary;
|
|
3385
|
-
if (theme.primaryForeground) vars["--primary-foreground"] = theme.primaryForeground;
|
|
3386
|
-
if (theme.secondary) vars["--secondary"] = theme.secondary;
|
|
3387
|
-
if (theme.secondaryForeground) vars["--secondary-foreground"] = theme.secondaryForeground;
|
|
3388
|
-
if (theme.muted) vars["--muted"] = theme.muted;
|
|
3389
|
-
if (theme.mutedForeground) vars["--muted-foreground"] = theme.mutedForeground;
|
|
3390
|
-
if (theme.accent) vars["--accent"] = theme.accent;
|
|
3391
|
-
if (theme.accentForeground) vars["--accent-foreground"] = theme.accentForeground;
|
|
3392
|
-
if (theme.destructive) vars["--destructive"] = theme.destructive;
|
|
3393
|
-
if (theme.destructiveForeground) vars["--destructive-foreground"] = theme.destructiveForeground;
|
|
3394
|
-
if (theme.border) vars["--border"] = theme.border;
|
|
3395
|
-
if (theme.input) vars["--input"] = theme.input;
|
|
3396
|
-
if (theme.ring) vars["--ring"] = theme.ring;
|
|
3397
|
-
if (theme.radius) vars["--radius"] = theme.radius;
|
|
3398
|
-
if (theme.surface) vars["--fcb-surface"] = theme.surface;
|
|
3399
|
-
if (theme.surfaceHover) vars["--fcb-surface-hover"] = theme.surfaceHover;
|
|
3400
|
-
if (theme.canvas) vars["--fcb-canvas"] = theme.canvas;
|
|
3401
|
-
if (theme.panel) vars["--fcb-panel"] = theme.panel;
|
|
3402
|
-
if (theme.borderStrong) vars["--fcb-border-strong"] = theme.borderStrong;
|
|
3403
|
-
if (theme.textDim) vars["--fcb-text-dim"] = theme.textDim;
|
|
3404
|
-
return vars;
|
|
3405
|
-
}
|
|
3406
|
-
function FormBuilderThemeProvider({ theme, children }) {
|
|
3407
|
-
const resolved = theme ?? {};
|
|
3408
|
-
const cssVars = react.useMemo(() => themeToCssVars(resolved), [resolved]);
|
|
3409
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ThemeCtx.Provider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fcb-root": "", style: cssVars, className: "w-full h-full", children }) });
|
|
3410
|
-
}
|
|
3411
|
-
var FormBuilderErrorBoundary = class extends react.Component {
|
|
3412
|
-
constructor(props) {
|
|
3413
|
-
super(props);
|
|
3414
|
-
this.state = { hasError: false, error: null };
|
|
3415
|
-
}
|
|
3501
|
+
var PreviewErrorBoundary = class extends react.Component {
|
|
3502
|
+
state = { error: null };
|
|
3416
3503
|
static getDerivedStateFromError(error) {
|
|
3417
|
-
return {
|
|
3504
|
+
return { error };
|
|
3418
3505
|
}
|
|
3419
3506
|
componentDidCatch(error, info) {
|
|
3420
|
-
console.error("[FormBuilder]
|
|
3507
|
+
console.error("[FormBuilder Preview]", error, info);
|
|
3508
|
+
}
|
|
3509
|
+
componentDidUpdate(prevProps) {
|
|
3510
|
+
if (prevProps.resetKey !== this.props.resetKey && this.state.error) {
|
|
3511
|
+
this.setState({ error: null });
|
|
3512
|
+
}
|
|
3421
3513
|
}
|
|
3422
3514
|
render() {
|
|
3423
|
-
if (this.state.
|
|
3424
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
3425
|
-
/* @__PURE__ */ jsxRuntime.jsx("
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
fieldcraftReact.Button,
|
|
3430
|
-
{
|
|
3431
|
-
onClick: () => this.setState({ hasError: false, error: null }),
|
|
3432
|
-
variant: "outline",
|
|
3433
|
-
children: "Try Again"
|
|
3434
|
-
}
|
|
3435
|
-
)
|
|
3436
|
-
] }) });
|
|
3515
|
+
if (this.state.error) {
|
|
3516
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-destructive text-sm font-mono", children: [
|
|
3517
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Render Error:" }),
|
|
3518
|
+
" ",
|
|
3519
|
+
this.state.error.message
|
|
3520
|
+
] });
|
|
3437
3521
|
}
|
|
3438
3522
|
return this.props.children;
|
|
3439
3523
|
}
|
|
3440
3524
|
};
|
|
3441
|
-
function
|
|
3442
|
-
const
|
|
3443
|
-
const
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3525
|
+
function FormPreviewPanel({ schema, preview }) {
|
|
3526
|
+
const hasSections = schema.sections && schema.sections.length > 0;
|
|
3527
|
+
const hasFields = hasSections && schema.sections.some((s) => s.questions.length > 0);
|
|
3528
|
+
if (!hasFields) {
|
|
3529
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground text-sm", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center space-y-2", children: [
|
|
3530
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "No fields to preview" }),
|
|
3531
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs", children: "Add fields in the Design view to see a live preview here." })
|
|
3532
|
+
] }) });
|
|
3533
|
+
}
|
|
3534
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "max-w-2xl mx-auto py-8 px-4", children: /* @__PURE__ */ jsxRuntime.jsx(PreviewErrorBoundary, { resetKey: schema.id + (schema.version ?? ""), children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3535
|
+
fieldcraftReact.FormEngineRenderer,
|
|
3536
|
+
{
|
|
3537
|
+
schema,
|
|
3538
|
+
theme: preview?.theme,
|
|
3539
|
+
components: preview?.components,
|
|
3540
|
+
onReady: preview?.onReady,
|
|
3541
|
+
onFieldChange: preview?.onFieldChange,
|
|
3542
|
+
onSubmit: preview?.onSubmit ?? (() => {
|
|
3543
|
+
})
|
|
3456
3544
|
}
|
|
3457
|
-
}
|
|
3545
|
+
) }) }) });
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
// src/form-builder/utils/validation-markers.ts
|
|
3549
|
+
var MarkerSeverity = {
|
|
3550
|
+
Error: 8};
|
|
3551
|
+
function errorsToMarkers(errors) {
|
|
3552
|
+
return errors.map((error) => ({
|
|
3553
|
+
severity: MarkerSeverity.Error,
|
|
3554
|
+
message: error.message,
|
|
3555
|
+
startLineNumber: error.line ?? 1,
|
|
3556
|
+
startColumn: error.column ?? 1,
|
|
3557
|
+
endLineNumber: error.endLine ?? error.line ?? 1,
|
|
3558
|
+
endColumn: error.endColumn ?? error.column ?? 1,
|
|
3559
|
+
source: "FieldCraft Schema Validator"
|
|
3560
|
+
}));
|
|
3561
|
+
}
|
|
3562
|
+
var Editor = react.lazy(() => import('@monaco-editor/react').then((m) => ({ default: m.default })));
|
|
3563
|
+
function MonacoWrapper({ value, onChange, errors }) {
|
|
3564
|
+
const editorRef = react.useRef(null);
|
|
3565
|
+
const monacoRef = react.useRef(null);
|
|
3566
|
+
const handleMount = (editor, monaco) => {
|
|
3567
|
+
editorRef.current = editor;
|
|
3568
|
+
monacoRef.current = monaco;
|
|
3569
|
+
};
|
|
3458
3570
|
react.useEffect(() => {
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3571
|
+
const editor = editorRef.current;
|
|
3572
|
+
const monaco = monacoRef.current;
|
|
3573
|
+
if (!editor || !monaco) return;
|
|
3574
|
+
const model = editor.getModel();
|
|
3575
|
+
if (!model) return;
|
|
3576
|
+
const markers = errorsToMarkers(errors);
|
|
3577
|
+
monaco.editor.setModelMarkers(model, "fieldcraft-schema", markers);
|
|
3578
|
+
}, [errors]);
|
|
3579
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3580
|
+
Editor,
|
|
3581
|
+
{
|
|
3582
|
+
height: "100%",
|
|
3583
|
+
language: "json",
|
|
3584
|
+
theme: "vs-dark",
|
|
3585
|
+
value,
|
|
3586
|
+
onChange: (val) => onChange(val ?? ""),
|
|
3587
|
+
onMount: handleMount,
|
|
3588
|
+
options: {
|
|
3589
|
+
minimap: { enabled: false },
|
|
3590
|
+
fontSize: 13,
|
|
3591
|
+
wordWrap: "on",
|
|
3592
|
+
formatOnPaste: true,
|
|
3593
|
+
automaticLayout: true,
|
|
3594
|
+
tabSize: 2,
|
|
3595
|
+
scrollBeyondLastLine: false,
|
|
3596
|
+
padding: { top: 12 }
|
|
3597
|
+
}
|
|
3467
3598
|
}
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
a.download = `${builderState.schema.title?.replace(/\s+/g, "-").toLowerCase() || "form"}-schema.json`;
|
|
3476
|
-
a.click();
|
|
3477
|
-
URL.revokeObjectURL(url);
|
|
3478
|
-
}, [builderState.schema]);
|
|
3479
|
-
const handleImport = react.useCallback(() => {
|
|
3480
|
-
fileInputRef.current?.click();
|
|
3481
|
-
}, []);
|
|
3482
|
-
const handleFileChange = react.useCallback(
|
|
3483
|
-
(e) => {
|
|
3484
|
-
const file = e.target.files?.[0];
|
|
3485
|
-
if (!file) return;
|
|
3486
|
-
const reader = new FileReader();
|
|
3487
|
-
reader.onload = (event) => {
|
|
3488
|
-
try {
|
|
3489
|
-
const parsed = JSON.parse(event.target?.result);
|
|
3490
|
-
const validated = fieldcraftCore.validateSchema(parsed);
|
|
3491
|
-
builderState.resetSchema(validated);
|
|
3492
|
-
} catch (err) {
|
|
3493
|
-
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
3494
|
-
const details = err.issues.slice(0, 3).map((i) => `\u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
3495
|
-
alert(`Invalid schema:
|
|
3496
|
-
${details}`);
|
|
3497
|
-
} else if (err instanceof SyntaxError) {
|
|
3498
|
-
alert("Failed to parse JSON file.");
|
|
3499
|
-
} else {
|
|
3500
|
-
alert("Invalid schema file.");
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
};
|
|
3504
|
-
reader.readAsText(file);
|
|
3505
|
-
e.target.value = "";
|
|
3599
|
+
);
|
|
3600
|
+
}
|
|
3601
|
+
function JsonEditorPanel({ value, onChange, errors, onValidate }) {
|
|
3602
|
+
const handleChange = react.useCallback(
|
|
3603
|
+
(newValue) => {
|
|
3604
|
+
onChange(newValue);
|
|
3605
|
+
onValidate(newValue);
|
|
3506
3606
|
},
|
|
3507
|
-
[
|
|
3607
|
+
[onChange, onValidate]
|
|
3508
3608
|
);
|
|
3509
|
-
const
|
|
3510
|
-
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
e.
|
|
3521
|
-
|
|
3522
|
-
|
|
3609
|
+
const handleFormat = react.useCallback(() => {
|
|
3610
|
+
try {
|
|
3611
|
+
const formatted = JSON.stringify(JSON.parse(value), null, 2);
|
|
3612
|
+
onChange(formatted);
|
|
3613
|
+
} catch {
|
|
3614
|
+
}
|
|
3615
|
+
}, [value, onChange]);
|
|
3616
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
|
|
3617
|
+
errors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-3 py-1.5 bg-destructive/10 border-b border-destructive/20 text-destructive text-xs font-mono overflow-x-auto", children: [
|
|
3618
|
+
errors.slice(0, 3).map((e, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3619
|
+
e.line ? `Line ${e.line}: ` : "",
|
|
3620
|
+
e.path ? `${e.path}: ` : "",
|
|
3621
|
+
e.message
|
|
3622
|
+
] }, i)),
|
|
3623
|
+
errors.length > 3 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-muted-foreground", children: [
|
|
3624
|
+
"...and ",
|
|
3625
|
+
errors.length - 3,
|
|
3626
|
+
" more"
|
|
3627
|
+
] })
|
|
3628
|
+
] }),
|
|
3629
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 flex items-center justify-between px-3 py-1.5 bg-card border-b border-border text-xs", children: [
|
|
3630
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: errors.length === 0 ? "Valid schema" : `${errors.length} error${errors.length !== 1 ? "s" : ""}` }),
|
|
3631
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3632
|
+
"button",
|
|
3633
|
+
{
|
|
3634
|
+
type: "button",
|
|
3635
|
+
onClick: handleFormat,
|
|
3636
|
+
className: "text-primary hover:underline",
|
|
3637
|
+
children: "Format JSON"
|
|
3638
|
+
}
|
|
3639
|
+
)
|
|
3640
|
+
] }),
|
|
3641
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3642
|
+
react.Suspense,
|
|
3643
|
+
{
|
|
3644
|
+
fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-muted-foreground", children: "Loading editor..." }),
|
|
3645
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(MonacoWrapper, { value, onChange: handleChange, errors })
|
|
3523
3646
|
}
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3647
|
+
) })
|
|
3648
|
+
] });
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
// src/form-builder/utils/logic-graph.ts
|
|
3652
|
+
function extractFieldRefs(expr) {
|
|
3653
|
+
const refs = [];
|
|
3654
|
+
if (expr.field) {
|
|
3655
|
+
refs.push({ field: expr.field, operator: expr.operator, value: expr.value });
|
|
3656
|
+
}
|
|
3657
|
+
if (expr.conditions) {
|
|
3658
|
+
for (const child of expr.conditions) {
|
|
3659
|
+
refs.push(...extractFieldRefs(child));
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
return refs;
|
|
3663
|
+
}
|
|
3664
|
+
function buildLogicGraph(schema) {
|
|
3665
|
+
const nodes = [];
|
|
3666
|
+
const edges = [];
|
|
3667
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
3668
|
+
for (const section of schema.sections) {
|
|
3669
|
+
nodes.push({
|
|
3670
|
+
id: section.id,
|
|
3671
|
+
label: section.title || "Untitled Section",
|
|
3672
|
+
type: "section",
|
|
3673
|
+
sectionId: section.id,
|
|
3674
|
+
sectionTitle: section.title || "Untitled Section",
|
|
3675
|
+
hasCondition: !!section.showIf
|
|
3676
|
+
});
|
|
3677
|
+
nodeIds.add(section.id);
|
|
3678
|
+
if (section.showIf) {
|
|
3679
|
+
const refs = extractFieldRefs(section.showIf);
|
|
3680
|
+
for (const ref of refs) {
|
|
3681
|
+
edges.push({ from: ref.field, to: section.id, edgeType: "showIf", operator: ref.operator, value: ref.value });
|
|
3528
3682
|
}
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
if (
|
|
3533
|
-
|
|
3534
|
-
if (sel.type === "question") {
|
|
3535
|
-
builderState.removeQuestion(sel.sectionId, sel.questionId);
|
|
3536
|
-
} else if (sel.type === "section") {
|
|
3537
|
-
builderState.removeSection(sel.sectionId);
|
|
3683
|
+
}
|
|
3684
|
+
if (section.onExit) {
|
|
3685
|
+
for (const rule of section.onExit.rules) {
|
|
3686
|
+
if (rule.jumpTo) {
|
|
3687
|
+
edges.push({ from: section.id, to: rule.jumpTo, edgeType: "onExit", jumpTo: rule.jumpTo });
|
|
3538
3688
|
}
|
|
3539
|
-
return;
|
|
3540
3689
|
}
|
|
3541
|
-
if (
|
|
3542
|
-
|
|
3543
|
-
e.preventDefault();
|
|
3544
|
-
builderState.clearSelection();
|
|
3545
|
-
}
|
|
3546
|
-
return;
|
|
3690
|
+
if (section.onExit.default) {
|
|
3691
|
+
edges.push({ from: section.id, to: section.onExit.default, edgeType: "onExit", jumpTo: section.onExit.default });
|
|
3547
3692
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3693
|
+
}
|
|
3694
|
+
for (const question of section.questions) {
|
|
3695
|
+
nodes.push({
|
|
3696
|
+
id: question.id,
|
|
3697
|
+
label: question.label || question.id,
|
|
3698
|
+
type: "field",
|
|
3699
|
+
fieldType: question.type,
|
|
3700
|
+
sectionId: section.id,
|
|
3701
|
+
sectionTitle: section.title || "Untitled Section",
|
|
3702
|
+
hasCondition: !!question.showIf
|
|
3703
|
+
});
|
|
3704
|
+
nodeIds.add(question.id);
|
|
3705
|
+
if (question.showIf) {
|
|
3706
|
+
const refs = extractFieldRefs(question.showIf);
|
|
3707
|
+
for (const ref of refs) {
|
|
3708
|
+
edges.push({ from: ref.field, to: question.id, edgeType: "showIf", operator: ref.operator, value: ref.value });
|
|
3556
3709
|
}
|
|
3557
3710
|
}
|
|
3558
|
-
}
|
|
3559
|
-
|
|
3560
|
-
);
|
|
3561
|
-
return
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
const validEdges = edges.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
|
|
3714
|
+
return { nodes, edges: validEdges };
|
|
3715
|
+
}
|
|
3716
|
+
var SECTION_COLORS = [
|
|
3717
|
+
"#3b82f6",
|
|
3718
|
+
// blue
|
|
3719
|
+
"#8b5cf6",
|
|
3720
|
+
// violet
|
|
3721
|
+
"#06b6d4",
|
|
3722
|
+
// cyan
|
|
3723
|
+
"#f59e0b",
|
|
3724
|
+
// amber
|
|
3725
|
+
"#10b981",
|
|
3726
|
+
// emerald
|
|
3727
|
+
"#ef4444",
|
|
3728
|
+
// red
|
|
3729
|
+
"#ec4899",
|
|
3730
|
+
// pink
|
|
3731
|
+
"#6366f1"
|
|
3732
|
+
// indigo
|
|
3733
|
+
];
|
|
3734
|
+
var FIELD_NODE_WIDTH = 220;
|
|
3735
|
+
var FIELD_NODE_HEIGHT = 52;
|
|
3736
|
+
var SECTION_HEADER_HEIGHT = 40;
|
|
3737
|
+
var SECTION_PADDING_X = 16;
|
|
3738
|
+
var SECTION_PADDING_TOP = 12;
|
|
3739
|
+
var SECTION_PADDING_BOTTOM = 16;
|
|
3740
|
+
var FIELD_GAP = 10;
|
|
3741
|
+
var SECTION_GAP = 80;
|
|
3742
|
+
function formatEdgeLabel(edge) {
|
|
3743
|
+
if (edge.edgeType === "onExit") return "jump";
|
|
3744
|
+
if (!edge.operator) return "";
|
|
3745
|
+
const op = edge.operator;
|
|
3746
|
+
const val = edge.value;
|
|
3747
|
+
if (op === "exists") return "has value";
|
|
3748
|
+
if (op === "notExists") return "is empty";
|
|
3749
|
+
if (val === void 0 || val === null || val === "") return op;
|
|
3750
|
+
const short = String(val).length > 12 ? String(val).slice(0, 12) + "\u2026" : String(val);
|
|
3751
|
+
return `${op} ${short}`;
|
|
3752
|
+
}
|
|
3753
|
+
function buildReactFlowGraph(schema) {
|
|
3754
|
+
const graph = buildLogicGraph(schema);
|
|
3755
|
+
const nodes = [];
|
|
3756
|
+
const edges = [];
|
|
3757
|
+
let sectionX = 0;
|
|
3758
|
+
let maxSectionHeight = 0;
|
|
3759
|
+
for (const section of schema.sections) {
|
|
3760
|
+
const fieldCount = section.questions.length;
|
|
3761
|
+
const contentHeight = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + fieldCount * FIELD_NODE_HEIGHT + Math.max(0, fieldCount - 1) * FIELD_GAP + SECTION_PADDING_BOTTOM;
|
|
3762
|
+
if (contentHeight > maxSectionHeight) maxSectionHeight = contentHeight;
|
|
3763
|
+
}
|
|
3764
|
+
if (maxSectionHeight < SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + SECTION_PADDING_BOTTOM + FIELD_NODE_HEIGHT) {
|
|
3765
|
+
maxSectionHeight = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + SECTION_PADDING_BOTTOM + FIELD_NODE_HEIGHT;
|
|
3766
|
+
}
|
|
3767
|
+
const sectionWidth = FIELD_NODE_WIDTH + SECTION_PADDING_X * 2;
|
|
3768
|
+
for (let sIdx = 0; sIdx < schema.sections.length; sIdx++) {
|
|
3769
|
+
const section = schema.sections[sIdx];
|
|
3770
|
+
const colorIndex = sIdx;
|
|
3771
|
+
nodes.push({
|
|
3772
|
+
id: section.id,
|
|
3773
|
+
type: "sectionNode",
|
|
3774
|
+
position: { x: sectionX, y: 0 },
|
|
3775
|
+
data: {
|
|
3776
|
+
label: section.title || "Untitled Section",
|
|
3777
|
+
sectionId: section.id,
|
|
3778
|
+
fieldCount: section.questions.length,
|
|
3779
|
+
hasCondition: !!section.showIf,
|
|
3780
|
+
hasExitLogic: !!section.onExit,
|
|
3781
|
+
colorIndex
|
|
3782
|
+
},
|
|
3783
|
+
style: { width: sectionWidth, height: maxSectionHeight }
|
|
3784
|
+
});
|
|
3785
|
+
for (let fIdx = 0; fIdx < section.questions.length; fIdx++) {
|
|
3786
|
+
const question = section.questions[fIdx];
|
|
3787
|
+
const fieldY = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + fIdx * (FIELD_NODE_HEIGHT + FIELD_GAP);
|
|
3788
|
+
nodes.push({
|
|
3789
|
+
id: question.id,
|
|
3790
|
+
type: "fieldNode",
|
|
3791
|
+
position: { x: SECTION_PADDING_X, y: fieldY },
|
|
3792
|
+
parentId: section.id,
|
|
3793
|
+
extent: "parent",
|
|
3794
|
+
data: {
|
|
3795
|
+
label: question.label || question.id,
|
|
3796
|
+
fieldType: question.type,
|
|
3797
|
+
sectionId: section.id,
|
|
3798
|
+
hasCondition: !!question.showIf,
|
|
3799
|
+
colorIndex
|
|
3800
|
+
},
|
|
3801
|
+
style: { width: FIELD_NODE_WIDTH }
|
|
3802
|
+
});
|
|
3803
|
+
}
|
|
3804
|
+
if (sIdx < schema.sections.length - 1) {
|
|
3805
|
+
const nextSection = schema.sections[sIdx + 1];
|
|
3806
|
+
edges.push({
|
|
3807
|
+
id: `pipeline-${section.id}-${nextSection.id}`,
|
|
3808
|
+
source: section.id,
|
|
3809
|
+
target: nextSection.id,
|
|
3810
|
+
type: "pipelineEdge",
|
|
3811
|
+
data: { edgeType: "pipeline" }
|
|
3812
|
+
});
|
|
3813
|
+
}
|
|
3814
|
+
sectionX += sectionWidth + SECTION_GAP;
|
|
3815
|
+
}
|
|
3816
|
+
for (let i = 0; i < graph.edges.length; i++) {
|
|
3817
|
+
const edge = graph.edges[i];
|
|
3818
|
+
edges.push({
|
|
3819
|
+
id: `e-${edge.from}-${edge.to}-${i}`,
|
|
3820
|
+
source: edge.from,
|
|
3821
|
+
target: edge.to,
|
|
3822
|
+
type: "conditionEdge",
|
|
3823
|
+
animated: edge.edgeType === "showIf",
|
|
3824
|
+
style: edge.edgeType === "onExit" ? { strokeDasharray: "6 3", stroke: "#f59e0b" } : void 0,
|
|
3825
|
+
label: formatEdgeLabel(edge),
|
|
3826
|
+
data: {
|
|
3827
|
+
edgeType: edge.edgeType,
|
|
3828
|
+
operator: edge.operator,
|
|
3829
|
+
value: edge.value,
|
|
3830
|
+
sourceId: edge.from,
|
|
3831
|
+
targetId: edge.to
|
|
3832
|
+
}
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
return { nodes, edges };
|
|
3836
|
+
}
|
|
3837
|
+
function SectionNodeInner({ data }) {
|
|
3838
|
+
const d = data;
|
|
3839
|
+
const color = SECTION_COLORS[d.colorIndex % SECTION_COLORS.length];
|
|
3840
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-section-group", style: { borderColor: color }, children: [
|
|
3841
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "target", position: react$1.Position.Left, className: "fc-flow-handle fc-flow-handle--section" }),
|
|
3842
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-section-group__header", style: { background: color }, children: [
|
|
3843
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__label", children: d.label.length > 30 ? d.label.slice(0, 30) + "\u2026" : d.label }),
|
|
3844
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fc-flow-section-group__meta", children: [
|
|
3845
|
+
d.fieldCount,
|
|
3846
|
+
" field",
|
|
3847
|
+
d.fieldCount !== 1 ? "s" : "",
|
|
3848
|
+
d.hasCondition && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__badge", title: "Has showIf condition", children: "?" }),
|
|
3849
|
+
d.hasExitLogic && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__badge fc-flow-section-group__badge--jump", title: "Has onExit jump", children: "\u2934" })
|
|
3850
|
+
] })
|
|
3851
|
+
] }),
|
|
3852
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "source", position: react$1.Position.Right, className: "fc-flow-handle fc-flow-handle--section" })
|
|
3853
|
+
] });
|
|
3854
|
+
}
|
|
3855
|
+
var SectionNode = react.memo(SectionNodeInner);
|
|
3856
|
+
function FieldNodeInner({ data }) {
|
|
3857
|
+
const d = data;
|
|
3858
|
+
const color = SECTION_COLORS[d.colorIndex % SECTION_COLORS.length];
|
|
3859
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3860
|
+
"div",
|
|
3861
|
+
{
|
|
3862
|
+
className: "fc-flow-field",
|
|
3863
|
+
style: {
|
|
3864
|
+
borderColor: d.hasCondition ? color : void 0,
|
|
3865
|
+
borderWidth: d.hasCondition ? 2 : 1
|
|
3866
|
+
},
|
|
3867
|
+
children: [
|
|
3868
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "target", position: react$1.Position.Left, className: "fc-flow-handle" }),
|
|
3869
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-field__content", children: [
|
|
3870
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-field__label", children: d.label.length > 26 ? d.label.slice(0, 26) + "\u2026" : d.label }),
|
|
3871
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-field__type", children: d.fieldType })
|
|
3872
|
+
] }),
|
|
3873
|
+
d.hasCondition && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fc-flow-badge", style: { background: color }, title: "Has showIf condition", children: "?" }),
|
|
3874
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "source", position: react$1.Position.Right, className: "fc-flow-handle" })
|
|
3875
|
+
]
|
|
3876
|
+
}
|
|
3877
|
+
);
|
|
3878
|
+
}
|
|
3879
|
+
var FieldNode = react.memo(FieldNodeInner);
|
|
3880
|
+
var logicFlowNodeTypes = {
|
|
3881
|
+
sectionNode: SectionNode,
|
|
3882
|
+
fieldNode: FieldNode
|
|
3883
|
+
};
|
|
3884
|
+
function ConditionEdgeInner({
|
|
3885
|
+
id,
|
|
3886
|
+
sourceX,
|
|
3887
|
+
sourceY,
|
|
3888
|
+
targetX,
|
|
3889
|
+
targetY,
|
|
3890
|
+
sourcePosition,
|
|
3891
|
+
targetPosition,
|
|
3892
|
+
label,
|
|
3893
|
+
style,
|
|
3894
|
+
data,
|
|
3895
|
+
markerEnd
|
|
3896
|
+
}) {
|
|
3897
|
+
const [edgePath, labelX, labelY] = react$1.getBezierPath({
|
|
3898
|
+
sourceX,
|
|
3899
|
+
sourceY,
|
|
3900
|
+
sourcePosition,
|
|
3901
|
+
targetX,
|
|
3902
|
+
targetY,
|
|
3903
|
+
targetPosition
|
|
3904
|
+
});
|
|
3905
|
+
const isJump = data?.edgeType === "onExit";
|
|
3906
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3907
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3908
|
+
react$1.BaseEdge,
|
|
3909
|
+
{
|
|
3910
|
+
id,
|
|
3911
|
+
path: edgePath,
|
|
3912
|
+
markerEnd,
|
|
3913
|
+
style: {
|
|
3914
|
+
stroke: isJump ? "#f59e0b" : "var(--primary, #3b82f6)",
|
|
3915
|
+
strokeWidth: 1.5,
|
|
3916
|
+
strokeOpacity: 0.7,
|
|
3917
|
+
...style
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
),
|
|
3921
|
+
label && /* @__PURE__ */ jsxRuntime.jsx(react$1.EdgeLabelRenderer, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3922
|
+
"div",
|
|
3923
|
+
{
|
|
3924
|
+
className: "fc-flow-edge-label",
|
|
3925
|
+
style: {
|
|
3926
|
+
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
|
3927
|
+
background: isJump ? "#f59e0b" : "var(--primary, #3b82f6)"
|
|
3928
|
+
},
|
|
3929
|
+
children: label
|
|
3930
|
+
}
|
|
3931
|
+
) })
|
|
3932
|
+
] });
|
|
3933
|
+
}
|
|
3934
|
+
var ConditionEdge = react.memo(ConditionEdgeInner);
|
|
3935
|
+
function PipelineEdgeInner({
|
|
3936
|
+
id,
|
|
3937
|
+
sourceX,
|
|
3938
|
+
sourceY,
|
|
3939
|
+
targetX,
|
|
3940
|
+
targetY,
|
|
3941
|
+
sourcePosition,
|
|
3942
|
+
targetPosition,
|
|
3943
|
+
markerEnd
|
|
3944
|
+
}) {
|
|
3945
|
+
const [edgePath] = react$1.getBezierPath({
|
|
3946
|
+
sourceX,
|
|
3947
|
+
sourceY,
|
|
3948
|
+
sourcePosition,
|
|
3949
|
+
targetX,
|
|
3950
|
+
targetY,
|
|
3951
|
+
targetPosition
|
|
3952
|
+
});
|
|
3953
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3954
|
+
react$1.BaseEdge,
|
|
3955
|
+
{
|
|
3956
|
+
id,
|
|
3957
|
+
path: edgePath,
|
|
3958
|
+
markerEnd,
|
|
3959
|
+
style: {
|
|
3960
|
+
stroke: "var(--muted-foreground, #71717a)",
|
|
3961
|
+
strokeWidth: 2,
|
|
3962
|
+
strokeOpacity: 0.4
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
);
|
|
3966
|
+
}
|
|
3967
|
+
var PipelineEdge = react.memo(PipelineEdgeInner);
|
|
3968
|
+
var logicFlowEdgeTypes = {
|
|
3969
|
+
conditionEdge: ConditionEdge,
|
|
3970
|
+
pipelineEdge: PipelineEdge
|
|
3971
|
+
};
|
|
3972
|
+
function findQuestion2(schema, fieldId) {
|
|
3973
|
+
for (const section of schema.sections) {
|
|
3974
|
+
for (const q of section.questions) {
|
|
3975
|
+
if (q.id === fieldId) return q;
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
return void 0;
|
|
3979
|
+
}
|
|
3980
|
+
function updateQuestionInSchema(schema, fieldId, updates) {
|
|
3981
|
+
return {
|
|
3982
|
+
...schema,
|
|
3983
|
+
sections: schema.sections.map((section) => ({
|
|
3984
|
+
...section,
|
|
3985
|
+
questions: section.questions.map(
|
|
3986
|
+
(q) => q.id === fieldId ? { ...q, ...updates } : q
|
|
3987
|
+
)
|
|
3988
|
+
}))
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
function removeConditionForSource(showIf, sourceFieldId) {
|
|
3992
|
+
if (!showIf) return void 0;
|
|
3993
|
+
if (showIf.field === sourceFieldId) return void 0;
|
|
3994
|
+
if (showIf.field && showIf.field !== sourceFieldId) return showIf;
|
|
3995
|
+
if (showIf.conditions) {
|
|
3996
|
+
const filtered = showIf.conditions.map((c) => removeConditionForSource(c, sourceFieldId)).filter((c) => c !== void 0);
|
|
3997
|
+
if (filtered.length === 0) return void 0;
|
|
3998
|
+
if (filtered.length === 1) return filtered[0];
|
|
3999
|
+
return { ...showIf, conditions: filtered };
|
|
4000
|
+
}
|
|
4001
|
+
return showIf;
|
|
4002
|
+
}
|
|
4003
|
+
var defaultEdgeOptions = {
|
|
4004
|
+
markerEnd: {
|
|
4005
|
+
type: react$1.MarkerType.ArrowClosed,
|
|
4006
|
+
width: 16,
|
|
4007
|
+
height: 12
|
|
4008
|
+
}
|
|
4009
|
+
};
|
|
4010
|
+
function LogicFlowEditor({ schema, onChange, onClose }) {
|
|
4011
|
+
const noSections = schema.sections.length === 0;
|
|
4012
|
+
const flowGraph = react.useMemo(() => buildReactFlowGraph(schema), [schema]);
|
|
4013
|
+
const [nodes, setNodes, onNodesChange] = react$1.useNodesState(flowGraph.nodes);
|
|
4014
|
+
const [edges, setEdges, onEdgesChange] = react$1.useEdgesState(flowGraph.edges);
|
|
4015
|
+
const [selectedFieldId, setSelectedFieldId] = react.useState(null);
|
|
4016
|
+
const selectedQuestion = selectedFieldId ? findQuestion2(schema, selectedFieldId) : void 0;
|
|
4017
|
+
react.useEffect(() => {
|
|
4018
|
+
setNodes(flowGraph.nodes);
|
|
4019
|
+
setEdges(flowGraph.edges);
|
|
4020
|
+
}, [flowGraph, setNodes, setEdges]);
|
|
4021
|
+
const sectionIds = react.useMemo(() => {
|
|
4022
|
+
const ids = [];
|
|
4023
|
+
for (const section of schema.sections) {
|
|
4024
|
+
ids.push(section.id);
|
|
4025
|
+
}
|
|
4026
|
+
return ids;
|
|
4027
|
+
}, [schema]);
|
|
4028
|
+
const onNodeClick = react.useCallback(
|
|
4029
|
+
(_event, node) => {
|
|
4030
|
+
if (node.type === "fieldNode") {
|
|
4031
|
+
setSelectedFieldId(node.id);
|
|
4032
|
+
}
|
|
4033
|
+
},
|
|
4034
|
+
[]
|
|
4035
|
+
);
|
|
4036
|
+
const onConnect = react.useCallback(
|
|
4037
|
+
(connection) => {
|
|
4038
|
+
if (!connection.source || !connection.target) return;
|
|
4039
|
+
const targetQuestion = findQuestion2(schema, connection.target);
|
|
4040
|
+
if (!targetQuestion) return;
|
|
4041
|
+
const newCondition = {
|
|
4042
|
+
field: connection.source,
|
|
4043
|
+
operator: "exists"
|
|
4044
|
+
};
|
|
4045
|
+
let updatedShowIf;
|
|
4046
|
+
const existing = targetQuestion.showIf;
|
|
4047
|
+
if (!existing) {
|
|
4048
|
+
updatedShowIf = { combine: "AND", conditions: [newCondition] };
|
|
4049
|
+
} else {
|
|
4050
|
+
updatedShowIf = {
|
|
4051
|
+
...existing,
|
|
4052
|
+
conditions: [...existing.conditions ?? [], newCondition]
|
|
4053
|
+
};
|
|
4054
|
+
}
|
|
4055
|
+
const updatedSchema = updateQuestionInSchema(schema, connection.target, {
|
|
4056
|
+
showIf: updatedShowIf
|
|
4057
|
+
});
|
|
4058
|
+
onChange(updatedSchema);
|
|
4059
|
+
setSelectedFieldId(connection.target);
|
|
4060
|
+
},
|
|
4061
|
+
[schema, onChange]
|
|
4062
|
+
);
|
|
4063
|
+
const onEdgesDelete = react.useCallback(
|
|
4064
|
+
(deletedEdges) => {
|
|
4065
|
+
let updatedSchema = schema;
|
|
4066
|
+
for (const edge of deletedEdges) {
|
|
4067
|
+
const edgeData = edge.data;
|
|
4068
|
+
if (!edgeData) continue;
|
|
4069
|
+
const targetId = edge.target;
|
|
4070
|
+
const sourceId = edge.source;
|
|
4071
|
+
const edgeType = edgeData.edgeType;
|
|
4072
|
+
if (edgeType === "showIf") {
|
|
4073
|
+
const question = findQuestion2(updatedSchema, targetId);
|
|
4074
|
+
if (!question) continue;
|
|
4075
|
+
const updatedShowIf = removeConditionForSource(
|
|
4076
|
+
question.showIf,
|
|
4077
|
+
sourceId
|
|
4078
|
+
);
|
|
4079
|
+
updatedSchema = updateQuestionInSchema(updatedSchema, targetId, {
|
|
4080
|
+
showIf: updatedShowIf
|
|
4081
|
+
});
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
onChange(updatedSchema);
|
|
4085
|
+
},
|
|
4086
|
+
[schema, onChange]
|
|
4087
|
+
);
|
|
4088
|
+
const handleConditionUpdate = react.useCallback(
|
|
4089
|
+
(updates) => {
|
|
4090
|
+
if (!selectedFieldId) return;
|
|
4091
|
+
const updatedSchema = updateQuestionInSchema(schema, selectedFieldId, updates);
|
|
4092
|
+
onChange(updatedSchema);
|
|
4093
|
+
},
|
|
4094
|
+
[schema, selectedFieldId, onChange]
|
|
4095
|
+
);
|
|
4096
|
+
const showIfCount = flowGraph.edges.filter(
|
|
4097
|
+
(e) => e.data?.edgeType === "showIf"
|
|
4098
|
+
).length;
|
|
4099
|
+
const jumpCount = flowGraph.edges.filter(
|
|
4100
|
+
(e) => e.data?.edgeType === "onExit"
|
|
4101
|
+
).length;
|
|
4102
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex flex-col bg-background/95 backdrop-blur-sm", children: [
|
|
4103
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-12 shrink-0 flex items-center justify-between px-4 border-b border-border bg-card", children: [
|
|
4104
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4105
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 16, className: "text-primary" }),
|
|
4106
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold", children: "Logic Flow" }),
|
|
4107
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
4108
|
+
showIfCount,
|
|
4109
|
+
" condition",
|
|
4110
|
+
showIfCount !== 1 ? "s" : "",
|
|
4111
|
+
jumpCount > 0 && ` \xB7 ${jumpCount} jump${jumpCount !== 1 ? "s" : ""}`,
|
|
4112
|
+
" \xB7 ",
|
|
4113
|
+
sectionIds.length,
|
|
4114
|
+
" section",
|
|
4115
|
+
sectionIds.length !== 1 ? "s" : ""
|
|
4116
|
+
] })
|
|
4117
|
+
] }),
|
|
4118
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
4119
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground mr-2", children: "Drag between nodes to create conditions \xB7 Select edges and press Delete to remove" }),
|
|
4120
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "icon-sm", onClick: onClose, title: "Close logic flow", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, strokeWidth: 1.75 }) })
|
|
4121
|
+
] })
|
|
4122
|
+
] }),
|
|
4123
|
+
noSections ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground text-sm", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center space-y-2", children: [
|
|
4124
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 32, className: "mx-auto opacity-40" }),
|
|
4125
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "No sections found" }),
|
|
4126
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs max-w-xs", children: "Add sections and fields to your form to see the logic flow pipeline." })
|
|
4127
|
+
] }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex", children: [
|
|
4128
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4129
|
+
react$1.ReactFlow,
|
|
4130
|
+
{
|
|
4131
|
+
nodes,
|
|
4132
|
+
edges,
|
|
4133
|
+
onNodesChange,
|
|
4134
|
+
onEdgesChange,
|
|
4135
|
+
onNodeClick,
|
|
4136
|
+
onConnect,
|
|
4137
|
+
onEdgesDelete,
|
|
4138
|
+
nodeTypes: logicFlowNodeTypes,
|
|
4139
|
+
edgeTypes: logicFlowEdgeTypes,
|
|
4140
|
+
defaultEdgeOptions,
|
|
4141
|
+
fitView: true,
|
|
4142
|
+
fitViewOptions: { padding: 0.2 },
|
|
4143
|
+
deleteKeyCode: ["Backspace", "Delete"],
|
|
4144
|
+
proOptions: { hideAttribution: true },
|
|
4145
|
+
children: [
|
|
4146
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { gap: 20, size: 1 }),
|
|
4147
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false }),
|
|
4148
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4149
|
+
react$1.MiniMap,
|
|
4150
|
+
{
|
|
4151
|
+
nodeColor: (node) => {
|
|
4152
|
+
const d = node.data;
|
|
4153
|
+
const idx = d.colorIndex ?? 0;
|
|
4154
|
+
return SECTION_COLORS[idx % SECTION_COLORS.length];
|
|
4155
|
+
},
|
|
4156
|
+
maskColor: "rgba(0, 0, 0, 0.3)"
|
|
4157
|
+
}
|
|
4158
|
+
)
|
|
4159
|
+
]
|
|
4160
|
+
}
|
|
4161
|
+
) }),
|
|
4162
|
+
selectedFieldId && selectedQuestion && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-72 border-l border-border bg-card overflow-auto", children: [
|
|
4163
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-3 border-b border-border flex items-center justify-between", children: [
|
|
4164
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4165
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-semibold truncate", children: selectedQuestion.label || selectedQuestion.id }),
|
|
4166
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground", children: selectedQuestion.type })
|
|
4167
|
+
] }),
|
|
4168
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4169
|
+
fieldcraftReact.Button,
|
|
4170
|
+
{
|
|
4171
|
+
variant: "ghost",
|
|
4172
|
+
size: "icon-xs",
|
|
4173
|
+
onClick: () => setSelectedFieldId(null),
|
|
4174
|
+
title: "Close panel",
|
|
4175
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, strokeWidth: 1.75 })
|
|
4176
|
+
}
|
|
4177
|
+
)
|
|
4178
|
+
] }),
|
|
4179
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4180
|
+
ConditionEditor,
|
|
4181
|
+
{
|
|
4182
|
+
question: selectedQuestion,
|
|
4183
|
+
schema,
|
|
4184
|
+
onUpdate: handleConditionUpdate
|
|
4185
|
+
}
|
|
4186
|
+
) })
|
|
4187
|
+
] })
|
|
4188
|
+
] }),
|
|
4189
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 border-t border-border bg-card flex items-center gap-4 text-xs text-muted-foreground flex-wrap", children: [
|
|
4190
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4191
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 rounded", style: { background: "var(--muted-foreground)", opacity: 0.4 } }),
|
|
4192
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "flow" })
|
|
4193
|
+
] }),
|
|
4194
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4195
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 bg-primary rounded" }),
|
|
4196
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "showIf" })
|
|
4197
|
+
] }),
|
|
4198
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4199
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 rounded", style: { background: "#f59e0b" } }),
|
|
4200
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "jump" })
|
|
4201
|
+
] }),
|
|
4202
|
+
sectionIds.map((id, i) => {
|
|
4203
|
+
const section = schema.sections.find((s) => s.id === id);
|
|
4204
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4205
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm", style: { background: SECTION_COLORS[i % SECTION_COLORS.length] } }),
|
|
4206
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: section?.title || id })
|
|
4207
|
+
] }, id);
|
|
4208
|
+
})
|
|
4209
|
+
] })
|
|
4210
|
+
] });
|
|
4211
|
+
}
|
|
4212
|
+
function TemplateGallery({ templates, onSelect, onClose }) {
|
|
4213
|
+
const [search, setSearch] = react.useState("");
|
|
4214
|
+
const [activeCategory, setActiveCategory] = react.useState("all");
|
|
4215
|
+
const [confirmTemplate, setConfirmTemplate] = react.useState(null);
|
|
4216
|
+
const categories = react.useMemo(() => {
|
|
4217
|
+
const cats = /* @__PURE__ */ new Set();
|
|
4218
|
+
for (const t of templates) cats.add(t.meta.category);
|
|
4219
|
+
return ["all", ...Array.from(cats)];
|
|
4220
|
+
}, [templates]);
|
|
4221
|
+
const filtered = react.useMemo(() => {
|
|
4222
|
+
return templates.filter((t) => {
|
|
4223
|
+
const matchesCategory = activeCategory === "all" || t.meta.category === activeCategory;
|
|
4224
|
+
const matchesSearch = !search || t.meta.name.toLowerCase().includes(search.toLowerCase()) || t.meta.description.toLowerCase().includes(search.toLowerCase()) || t.meta.tags?.some((tag) => tag.toLowerCase().includes(search.toLowerCase()));
|
|
4225
|
+
return matchesCategory && matchesSearch;
|
|
4226
|
+
});
|
|
4227
|
+
}, [templates, activeCategory, search]);
|
|
4228
|
+
const handleConfirm = () => {
|
|
4229
|
+
if (confirmTemplate) {
|
|
4230
|
+
onSelect(confirmTemplate);
|
|
4231
|
+
setConfirmTemplate(null);
|
|
4232
|
+
onClose();
|
|
4233
|
+
}
|
|
4234
|
+
};
|
|
4235
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full max-w-3xl max-h-[85vh] mx-4 bg-card border border-border rounded-lg shadow-xl flex flex-col overflow-hidden", children: [
|
|
4236
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-3 border-b border-border", children: [
|
|
4237
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4238
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutTemplate, { size: 18, className: "text-primary", strokeWidth: 1.75 }),
|
|
4239
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold", children: "Template Gallery" }),
|
|
4240
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
4241
|
+
"(",
|
|
4242
|
+
templates.length,
|
|
4243
|
+
" templates)"
|
|
4244
|
+
] })
|
|
4245
|
+
] }),
|
|
4246
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "icon-sm", onClick: onClose, title: "Close", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 16, strokeWidth: 1.75 }) })
|
|
4247
|
+
] }),
|
|
4248
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 space-y-2 border-b border-border", children: [
|
|
4249
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
4250
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Search, { size: 14, className: "absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground", strokeWidth: 1.75 }),
|
|
4251
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4252
|
+
"input",
|
|
4253
|
+
{
|
|
4254
|
+
type: "text",
|
|
4255
|
+
placeholder: "Search templates...",
|
|
4256
|
+
value: search,
|
|
4257
|
+
onChange: (e) => setSearch(e.target.value),
|
|
4258
|
+
className: "w-full pl-8 pr-3 py-1.5 text-sm bg-background border border-border rounded-md text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
|
4259
|
+
}
|
|
4260
|
+
)
|
|
4261
|
+
] }),
|
|
4262
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1 overflow-x-auto pb-0.5", children: categories.map((cat) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4263
|
+
"button",
|
|
4264
|
+
{
|
|
4265
|
+
type: "button",
|
|
4266
|
+
onClick: () => setActiveCategory(cat),
|
|
4267
|
+
className: `shrink-0 px-2.5 py-1 rounded-md text-xs font-medium transition-colors ${activeCategory === cat ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground hover:text-foreground"}`,
|
|
4268
|
+
children: cat === "all" ? "All" : cat.charAt(0).toUpperCase() + cat.slice(1)
|
|
4269
|
+
},
|
|
4270
|
+
cat
|
|
4271
|
+
)) })
|
|
4272
|
+
] }),
|
|
4273
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-y-auto p-4", children: filtered.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center justify-center h-40 text-muted-foreground text-sm", children: /* @__PURE__ */ jsxRuntime.jsx("p", { children: "No templates match your search." }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-3", children: filtered.map((template) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4274
|
+
"button",
|
|
4275
|
+
{
|
|
4276
|
+
type: "button",
|
|
4277
|
+
onClick: () => setConfirmTemplate(template),
|
|
4278
|
+
className: "text-left p-3 rounded-lg border border-border bg-background hover:border-primary/50 hover:bg-primary/5 transition-colors group",
|
|
4279
|
+
children: [
|
|
4280
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2 mb-1.5", children: [
|
|
4281
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium text-foreground group-hover:text-primary transition-colors", children: template.meta.name }),
|
|
4282
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 px-1.5 py-0.5 rounded text-[10px] font-medium bg-muted text-muted-foreground", children: template.meta.category })
|
|
4283
|
+
] }),
|
|
4284
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mb-2 line-clamp-2", children: template.meta.description }),
|
|
4285
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
|
|
4286
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
|
|
4287
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 11, strokeWidth: 1.75 }),
|
|
4288
|
+
template.meta.fieldCount,
|
|
4289
|
+
" field",
|
|
4290
|
+
template.meta.fieldCount !== 1 ? "s" : ""
|
|
4291
|
+
] }),
|
|
4292
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
|
|
4293
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Layers, { size: 11, strokeWidth: 1.75 }),
|
|
4294
|
+
template.meta.sectionCount,
|
|
4295
|
+
" section",
|
|
4296
|
+
template.meta.sectionCount !== 1 ? "s" : ""
|
|
4297
|
+
] })
|
|
4298
|
+
] })
|
|
4299
|
+
]
|
|
4300
|
+
},
|
|
4301
|
+
template.meta.id
|
|
4302
|
+
)) }) }),
|
|
4303
|
+
confirmTemplate && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-3 border-t border-border bg-muted/50 flex items-center justify-between gap-3", children: [
|
|
4304
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm", children: [
|
|
4305
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Use " }),
|
|
4306
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: confirmTemplate.meta.name }),
|
|
4307
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "? This will replace the current form." })
|
|
4308
|
+
] }),
|
|
4309
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
4310
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", onClick: () => setConfirmTemplate(null), children: "Cancel" }),
|
|
4311
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { size: "sm", onClick: handleConfirm, children: "Use Template" })
|
|
4312
|
+
] })
|
|
4313
|
+
] })
|
|
4314
|
+
] }) });
|
|
4315
|
+
}
|
|
4316
|
+
function useDebouncedValidation(delayMs = 500) {
|
|
4317
|
+
const [errors, setErrors] = react.useState([]);
|
|
4318
|
+
const timerRef = react.useRef(null);
|
|
4319
|
+
const validate = react.useCallback(
|
|
4320
|
+
(text) => {
|
|
4321
|
+
if (timerRef.current) {
|
|
4322
|
+
clearTimeout(timerRef.current);
|
|
4323
|
+
}
|
|
4324
|
+
timerRef.current = setTimeout(() => {
|
|
4325
|
+
const result = [];
|
|
4326
|
+
let parsed;
|
|
4327
|
+
try {
|
|
4328
|
+
parsed = JSON.parse(text);
|
|
4329
|
+
} catch (err) {
|
|
4330
|
+
if (err instanceof SyntaxError) {
|
|
4331
|
+
const posMatch = err.message.match(/position\s+(\d+)/i);
|
|
4332
|
+
let line = 1;
|
|
4333
|
+
let column = 1;
|
|
4334
|
+
if (posMatch) {
|
|
4335
|
+
const pos = parseInt(posMatch[1], 10);
|
|
4336
|
+
const upToPos = text.slice(0, pos);
|
|
4337
|
+
line = (upToPos.match(/\n/g) || []).length + 1;
|
|
4338
|
+
column = pos - upToPos.lastIndexOf("\n");
|
|
4339
|
+
}
|
|
4340
|
+
result.push({ message: err.message, line, column, endLine: line, endColumn: column + 1 });
|
|
4341
|
+
} else {
|
|
4342
|
+
result.push({ message: "Invalid JSON" });
|
|
4343
|
+
}
|
|
4344
|
+
setErrors(result);
|
|
4345
|
+
return;
|
|
4346
|
+
}
|
|
4347
|
+
try {
|
|
4348
|
+
fieldcraftCore.validateSchema(parsed);
|
|
4349
|
+
} catch (err) {
|
|
4350
|
+
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4351
|
+
for (const issue of err.issues) {
|
|
4352
|
+
result.push({
|
|
4353
|
+
message: issue.message,
|
|
4354
|
+
path: issue.path?.join(".")
|
|
4355
|
+
});
|
|
4356
|
+
}
|
|
4357
|
+
} else if (err instanceof Error) {
|
|
4358
|
+
result.push({ message: err.message });
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
setErrors(result);
|
|
4362
|
+
}, delayMs);
|
|
4363
|
+
},
|
|
4364
|
+
[delayMs]
|
|
4365
|
+
);
|
|
4366
|
+
return { errors, validate };
|
|
4367
|
+
}
|
|
4368
|
+
var ThemeCtx = react.createContext({});
|
|
4369
|
+
function useBuilderTheme() {
|
|
4370
|
+
return react.useContext(ThemeCtx);
|
|
4371
|
+
}
|
|
4372
|
+
function themeToCssVars(theme) {
|
|
4373
|
+
const vars = {};
|
|
4374
|
+
if (theme.background) vars["--background"] = theme.background;
|
|
4375
|
+
if (theme.foreground) vars["--foreground"] = theme.foreground;
|
|
4376
|
+
if (theme.card) {
|
|
4377
|
+
vars["--card"] = theme.card;
|
|
4378
|
+
vars["--card-foreground"] = theme.foreground ?? "";
|
|
4379
|
+
vars["--popover"] = theme.card;
|
|
4380
|
+
vars["--popover-foreground"] = theme.foreground ?? "";
|
|
4381
|
+
}
|
|
4382
|
+
if (theme.primary) vars["--primary"] = theme.primary;
|
|
4383
|
+
if (theme.primaryForeground) vars["--primary-foreground"] = theme.primaryForeground;
|
|
4384
|
+
if (theme.secondary) vars["--secondary"] = theme.secondary;
|
|
4385
|
+
if (theme.secondaryForeground) vars["--secondary-foreground"] = theme.secondaryForeground;
|
|
4386
|
+
if (theme.muted) vars["--muted"] = theme.muted;
|
|
4387
|
+
if (theme.mutedForeground) vars["--muted-foreground"] = theme.mutedForeground;
|
|
4388
|
+
if (theme.accent) vars["--accent"] = theme.accent;
|
|
4389
|
+
if (theme.accentForeground) vars["--accent-foreground"] = theme.accentForeground;
|
|
4390
|
+
if (theme.destructive) vars["--destructive"] = theme.destructive;
|
|
4391
|
+
if (theme.destructiveForeground) vars["--destructive-foreground"] = theme.destructiveForeground;
|
|
4392
|
+
if (theme.border) vars["--border"] = theme.border;
|
|
4393
|
+
if (theme.input) vars["--input"] = theme.input;
|
|
4394
|
+
if (theme.ring) vars["--ring"] = theme.ring;
|
|
4395
|
+
if (theme.radius) vars["--radius"] = theme.radius;
|
|
4396
|
+
if (theme.surface) vars["--fcb-surface"] = theme.surface;
|
|
4397
|
+
if (theme.surfaceHover) vars["--fcb-surface-hover"] = theme.surfaceHover;
|
|
4398
|
+
if (theme.canvas) vars["--fcb-canvas"] = theme.canvas;
|
|
4399
|
+
if (theme.panel) vars["--fcb-panel"] = theme.panel;
|
|
4400
|
+
if (theme.borderStrong) vars["--fcb-border-strong"] = theme.borderStrong;
|
|
4401
|
+
if (theme.textDim) vars["--fcb-text-dim"] = theme.textDim;
|
|
4402
|
+
return vars;
|
|
4403
|
+
}
|
|
4404
|
+
function FormBuilderThemeProvider({ theme, children }) {
|
|
4405
|
+
const resolved = theme ?? {};
|
|
4406
|
+
const cssVars = react.useMemo(() => themeToCssVars(resolved), [resolved]);
|
|
4407
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ThemeCtx.Provider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fcb-root": "", style: cssVars, className: "w-full h-full", children }) });
|
|
4408
|
+
}
|
|
4409
|
+
var FormBuilderErrorBoundary = class extends react.Component {
|
|
4410
|
+
constructor(props) {
|
|
4411
|
+
super(props);
|
|
4412
|
+
this.state = { hasError: false, error: null };
|
|
4413
|
+
}
|
|
4414
|
+
static getDerivedStateFromError(error) {
|
|
4415
|
+
return { hasError: true, error };
|
|
4416
|
+
}
|
|
4417
|
+
componentDidCatch(error, info) {
|
|
4418
|
+
console.error("[FormBuilder] Render error:", error, info.componentStack);
|
|
4419
|
+
}
|
|
4420
|
+
render() {
|
|
4421
|
+
if (this.state.hasError) {
|
|
4422
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col items-center justify-center h-full bg-background text-foreground p-8", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-w-md text-center space-y-4", children: [
|
|
4423
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold", children: "Something went wrong" }),
|
|
4424
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "The form builder encountered an unexpected error. Your schema data is preserved." }),
|
|
4425
|
+
/* @__PURE__ */ jsxRuntime.jsx("pre", { className: "text-xs text-destructive bg-destructive/10 rounded-md p-3 text-left overflow-auto max-h-32", children: this.state.error?.message }),
|
|
4426
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4427
|
+
fieldcraftReact.Button,
|
|
4428
|
+
{
|
|
4429
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
4430
|
+
variant: "outline",
|
|
4431
|
+
children: "Try Again"
|
|
4432
|
+
}
|
|
4433
|
+
)
|
|
4434
|
+
] }) });
|
|
4435
|
+
}
|
|
4436
|
+
return this.props.children;
|
|
4437
|
+
}
|
|
4438
|
+
};
|
|
4439
|
+
function FormBuilderCore(props) {
|
|
4440
|
+
const { initialSchema = DEFAULT_SCHEMA, onChange, onSave, height = "100vh", theme, className, toolbarExtra, questionTypes, palette, preview, templates, schemaUrl } = props;
|
|
4441
|
+
const [viewMode, setViewMode] = react.useState("design");
|
|
4442
|
+
const [jsonText, setJsonText] = react.useState("");
|
|
4443
|
+
const [jsonSwitchError, setJsonSwitchError] = react.useState(null);
|
|
4444
|
+
const [showLogicMap, setShowLogicMap] = react.useState(false);
|
|
4445
|
+
const [showTemplateGallery, setShowTemplateGallery] = react.useState(false);
|
|
4446
|
+
const [saveValidationErrors, setSaveValidationErrors] = react.useState(null);
|
|
4447
|
+
const [schemaUrlLoading, setSchemaUrlLoading] = react.useState(!!schemaUrl);
|
|
4448
|
+
const [schemaUrlError, setSchemaUrlError] = react.useState(null);
|
|
4449
|
+
const { errors: jsonErrors, validate: validateJson } = useDebouncedValidation(400);
|
|
4450
|
+
const mergedQuestionTypes = questionTypes ? { ...QUESTION_TYPE_INFO, ...questionTypes } : QUESTION_TYPE_INFO;
|
|
4451
|
+
const builderState = useBuilderState(initialSchema);
|
|
4452
|
+
const dragDrop = useDragDrop(builderState);
|
|
4453
|
+
const fileInputRef = react.useRef(null);
|
|
4454
|
+
const [mobilePanel, setMobilePanel] = react.useState("none");
|
|
4455
|
+
react.useEffect(() => {
|
|
4456
|
+
if (builderState.selectedItem && window.innerWidth < 768) {
|
|
4457
|
+
setMobilePanel("properties");
|
|
4458
|
+
}
|
|
4459
|
+
}, [builderState.selectedItem]);
|
|
4460
|
+
react.useEffect(() => {
|
|
4461
|
+
if (!dragDrop.activeDragItem && mobilePanel === "palette") {
|
|
4462
|
+
setMobilePanel("none");
|
|
4463
|
+
}
|
|
4464
|
+
}, [dragDrop.activeDragItem]);
|
|
4465
|
+
react.useEffect(() => {
|
|
4466
|
+
if (!schemaUrl) return;
|
|
4467
|
+
let cancelled = false;
|
|
4468
|
+
setSchemaUrlLoading(true);
|
|
4469
|
+
setSchemaUrlError(null);
|
|
4470
|
+
fetch(schemaUrl).then((res) => {
|
|
4471
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
4472
|
+
return res.json();
|
|
4473
|
+
}).then((json) => {
|
|
4474
|
+
if (cancelled) return;
|
|
4475
|
+
try {
|
|
4476
|
+
const validated = fieldcraftCore.validateSchema(json);
|
|
4477
|
+
builderState.resetSchema(validated);
|
|
4478
|
+
} catch (err) {
|
|
4479
|
+
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4480
|
+
setSchemaUrlError(`Invalid schema: ${err.issues[0]?.message ?? "validation failed"}`);
|
|
4481
|
+
} else {
|
|
4482
|
+
setSchemaUrlError(err.message);
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
setSchemaUrlLoading(false);
|
|
4486
|
+
}).catch((err) => {
|
|
4487
|
+
if (cancelled) return;
|
|
4488
|
+
setSchemaUrlError(err.message ?? "Failed to fetch schema");
|
|
4489
|
+
setSchemaUrlLoading(false);
|
|
4490
|
+
});
|
|
4491
|
+
return () => {
|
|
4492
|
+
cancelled = true;
|
|
4493
|
+
};
|
|
4494
|
+
}, [schemaUrl]);
|
|
4495
|
+
react.useEffect(() => {
|
|
4496
|
+
if (onChange && builderState.isDirty) {
|
|
4497
|
+
onChange(builderState.schema);
|
|
4498
|
+
}
|
|
4499
|
+
}, [builderState.schema, builderState.isDirty, onChange]);
|
|
4500
|
+
const handleSave = react.useCallback(() => {
|
|
4501
|
+
if (!onSave) return;
|
|
4502
|
+
try {
|
|
4503
|
+
fieldcraftCore.validateSchema(builderState.schema);
|
|
4504
|
+
setSaveValidationErrors(null);
|
|
4505
|
+
onSave(builderState.schema);
|
|
4506
|
+
builderState.markClean();
|
|
4507
|
+
} catch (err) {
|
|
4508
|
+
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4509
|
+
setSaveValidationErrors(err.issues.map((issue) => issue.message));
|
|
4510
|
+
} else {
|
|
4511
|
+
setSaveValidationErrors([err.message ?? "Unknown validation error"]);
|
|
4512
|
+
}
|
|
4513
|
+
}
|
|
4514
|
+
}, [onSave, builderState]);
|
|
4515
|
+
const handleExport = react.useCallback(() => {
|
|
4516
|
+
const json = JSON.stringify(builderState.schema, null, 2);
|
|
4517
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
4518
|
+
const url = URL.createObjectURL(blob);
|
|
4519
|
+
const a = document.createElement("a");
|
|
4520
|
+
a.href = url;
|
|
4521
|
+
a.download = `${builderState.schema.title?.replace(/\s+/g, "-").toLowerCase() || "form"}-schema.json`;
|
|
4522
|
+
a.click();
|
|
4523
|
+
URL.revokeObjectURL(url);
|
|
4524
|
+
}, [builderState.schema]);
|
|
4525
|
+
const handleImport = react.useCallback(() => {
|
|
4526
|
+
fileInputRef.current?.click();
|
|
4527
|
+
}, []);
|
|
4528
|
+
const handleFileChange = react.useCallback(
|
|
4529
|
+
(e) => {
|
|
4530
|
+
const file = e.target.files?.[0];
|
|
4531
|
+
if (!file) return;
|
|
4532
|
+
const reader = new FileReader();
|
|
4533
|
+
reader.onload = (event) => {
|
|
4534
|
+
try {
|
|
4535
|
+
const parsed = JSON.parse(event.target?.result);
|
|
4536
|
+
const validated = fieldcraftCore.validateSchema(parsed);
|
|
4537
|
+
builderState.resetSchema(validated);
|
|
4538
|
+
} catch (err) {
|
|
4539
|
+
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4540
|
+
const details = err.issues.slice(0, 3).map((i) => `\u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
4541
|
+
alert(`Invalid schema:
|
|
4542
|
+
${details}`);
|
|
4543
|
+
} else if (err instanceof SyntaxError) {
|
|
4544
|
+
alert("Failed to parse JSON file.");
|
|
4545
|
+
} else {
|
|
4546
|
+
alert("Invalid schema file.");
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
};
|
|
4550
|
+
reader.readAsText(file);
|
|
4551
|
+
e.target.value = "";
|
|
4552
|
+
},
|
|
4553
|
+
[builderState]
|
|
4554
|
+
);
|
|
4555
|
+
const handleViewModeChange = react.useCallback(
|
|
4556
|
+
(newMode) => {
|
|
4557
|
+
setJsonSwitchError(null);
|
|
4558
|
+
if (viewMode === "json" && newMode !== "json") {
|
|
4559
|
+
try {
|
|
4560
|
+
const parsed = JSON.parse(jsonText);
|
|
4561
|
+
const validated = fieldcraftCore.validateSchema(parsed);
|
|
4562
|
+
builderState.resetSchema(validated);
|
|
4563
|
+
} catch (err) {
|
|
4564
|
+
if (err instanceof SyntaxError) {
|
|
4565
|
+
setJsonSwitchError("Cannot switch: JSON has syntax errors");
|
|
4566
|
+
} else if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4567
|
+
setJsonSwitchError(`Cannot switch: ${err.issues[0]?.message ?? "Invalid schema"}`);
|
|
4568
|
+
} else {
|
|
4569
|
+
setJsonSwitchError("Cannot switch: Invalid schema");
|
|
4570
|
+
}
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
}
|
|
4574
|
+
if (newMode === "json") {
|
|
4575
|
+
setJsonText(JSON.stringify(builderState.schema, null, 2));
|
|
4576
|
+
}
|
|
4577
|
+
setViewMode(newMode);
|
|
4578
|
+
},
|
|
4579
|
+
[viewMode, jsonText, builderState]
|
|
4580
|
+
);
|
|
4581
|
+
const handleKeyDown = react.useCallback(
|
|
4582
|
+
(e) => {
|
|
4583
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
4584
|
+
const tag = e.target.tagName;
|
|
4585
|
+
const isEditing = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
4586
|
+
if (mod && e.key === "z" && !e.shiftKey) {
|
|
4587
|
+
e.preventDefault();
|
|
4588
|
+
builderState.undo();
|
|
4589
|
+
return;
|
|
4590
|
+
}
|
|
4591
|
+
if (mod && e.key === "z" && e.shiftKey) {
|
|
4592
|
+
e.preventDefault();
|
|
4593
|
+
builderState.redo();
|
|
4594
|
+
return;
|
|
4595
|
+
}
|
|
4596
|
+
if (mod && e.key === "s") {
|
|
4597
|
+
e.preventDefault();
|
|
4598
|
+
handleSave();
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
if (isEditing) return;
|
|
4602
|
+
if (e.key === "Delete" || e.key === "Backspace") {
|
|
4603
|
+
const sel = builderState.selectedItem;
|
|
4604
|
+
if (!sel) return;
|
|
4605
|
+
e.preventDefault();
|
|
4606
|
+
if (sel.type === "question") {
|
|
4607
|
+
builderState.removeQuestion(sel.sectionId, sel.questionId);
|
|
4608
|
+
} else if (sel.type === "section") {
|
|
4609
|
+
builderState.removeSection(sel.sectionId);
|
|
4610
|
+
}
|
|
4611
|
+
return;
|
|
4612
|
+
}
|
|
4613
|
+
if (e.key === "Escape") {
|
|
4614
|
+
if (builderState.selectedItem) {
|
|
4615
|
+
e.preventDefault();
|
|
4616
|
+
builderState.clearSelection();
|
|
4617
|
+
}
|
|
4618
|
+
return;
|
|
4619
|
+
}
|
|
4620
|
+
if (mod && e.key === "d") {
|
|
4621
|
+
const sel = builderState.selectedItem;
|
|
4622
|
+
if (!sel) return;
|
|
4623
|
+
e.preventDefault();
|
|
4624
|
+
if (sel.type === "question") {
|
|
4625
|
+
builderState.duplicateQuestion(sel.sectionId, sel.questionId);
|
|
4626
|
+
} else if (sel.type === "section") {
|
|
4627
|
+
builderState.duplicateSection(sel.sectionId);
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
4630
|
+
},
|
|
4631
|
+
[builderState, handleSave]
|
|
4632
|
+
);
|
|
4633
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FormBuilderThemeProvider, { theme, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4634
|
+
core.DndContext,
|
|
4635
|
+
{
|
|
4636
|
+
sensors: dragDrop.sensors,
|
|
4637
|
+
onDragStart: dragDrop.handleDragStart,
|
|
4638
|
+
onDragEnd: dragDrop.handleDragEnd,
|
|
4639
|
+
onDragCancel: dragDrop.handleDragCancel,
|
|
4640
|
+
children: [
|
|
4641
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3570
4642
|
"div",
|
|
3571
4643
|
{
|
|
3572
4644
|
className: cn("flex flex-col bg-background text-foreground", className),
|
|
@@ -3607,8 +4679,74 @@ ${details}`);
|
|
|
3607
4679
|
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-2 h-5" }),
|
|
3608
4680
|
builderState.isDirty && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-primary", role: "status", children: "Unsaved changes" })
|
|
3609
4681
|
] }),
|
|
3610
|
-
/* @__PURE__ */ jsxRuntime.
|
|
4682
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2", children: [
|
|
4683
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 bg-muted rounded-md p-0.5", children: [
|
|
4684
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4685
|
+
fieldcraftReact.Button,
|
|
4686
|
+
{
|
|
4687
|
+
variant: viewMode === "design" ? "secondary" : "ghost",
|
|
4688
|
+
size: "sm",
|
|
4689
|
+
onClick: () => handleViewModeChange("design"),
|
|
4690
|
+
className: "gap-1.5 h-7 px-2.5 text-xs",
|
|
4691
|
+
children: [
|
|
4692
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.PencilRuler, { size: 13, strokeWidth: 1.75 }),
|
|
4693
|
+
"Design"
|
|
4694
|
+
]
|
|
4695
|
+
}
|
|
4696
|
+
),
|
|
4697
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4698
|
+
fieldcraftReact.Button,
|
|
4699
|
+
{
|
|
4700
|
+
variant: viewMode === "preview" ? "secondary" : "ghost",
|
|
4701
|
+
size: "sm",
|
|
4702
|
+
onClick: () => handleViewModeChange("preview"),
|
|
4703
|
+
className: "gap-1.5 h-7 px-2.5 text-xs",
|
|
4704
|
+
children: [
|
|
4705
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, { size: 13, strokeWidth: 1.75 }),
|
|
4706
|
+
"Preview"
|
|
4707
|
+
]
|
|
4708
|
+
}
|
|
4709
|
+
),
|
|
4710
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4711
|
+
fieldcraftReact.Button,
|
|
4712
|
+
{
|
|
4713
|
+
variant: viewMode === "json" ? "secondary" : "ghost",
|
|
4714
|
+
size: "sm",
|
|
4715
|
+
onClick: () => handleViewModeChange("json"),
|
|
4716
|
+
className: "gap-1.5 h-7 px-2.5 text-xs",
|
|
4717
|
+
children: [
|
|
4718
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 13, strokeWidth: 1.75 }),
|
|
4719
|
+
"JSON"
|
|
4720
|
+
]
|
|
4721
|
+
}
|
|
4722
|
+
)
|
|
4723
|
+
] }),
|
|
4724
|
+
toolbarExtra
|
|
4725
|
+
] }),
|
|
3611
4726
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [
|
|
4727
|
+
templates && templates.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4728
|
+
fieldcraftReact.Button,
|
|
4729
|
+
{
|
|
4730
|
+
variant: "ghost",
|
|
4731
|
+
size: "icon-sm",
|
|
4732
|
+
onClick: () => setShowTemplateGallery(true),
|
|
4733
|
+
title: "Browse templates",
|
|
4734
|
+
"aria-label": "Template gallery",
|
|
4735
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutTemplate, { size: 15, strokeWidth: 1.75 })
|
|
4736
|
+
}
|
|
4737
|
+
),
|
|
4738
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4739
|
+
fieldcraftReact.Button,
|
|
4740
|
+
{
|
|
4741
|
+
variant: "ghost",
|
|
4742
|
+
size: "icon-sm",
|
|
4743
|
+
onClick: () => setShowLogicMap(true),
|
|
4744
|
+
title: "View branching logic map",
|
|
4745
|
+
"aria-label": "Logic map",
|
|
4746
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 15, strokeWidth: 1.75 })
|
|
4747
|
+
}
|
|
4748
|
+
),
|
|
4749
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-1 h-5" }),
|
|
3612
4750
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3613
4751
|
fieldcraftReact.Button,
|
|
3614
4752
|
{
|
|
@@ -3652,6 +4790,47 @@ ${details}`);
|
|
|
3652
4790
|
}
|
|
3653
4791
|
) }),
|
|
3654
4792
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1", children: [
|
|
4793
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 bg-muted rounded-md p-0.5", children: [
|
|
4794
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4795
|
+
fieldcraftReact.Button,
|
|
4796
|
+
{
|
|
4797
|
+
variant: viewMode === "design" ? "secondary" : "ghost",
|
|
4798
|
+
size: "sm",
|
|
4799
|
+
onClick: () => handleViewModeChange("design"),
|
|
4800
|
+
className: "gap-1 h-7 px-2 text-xs",
|
|
4801
|
+
children: [
|
|
4802
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.PencilRuler, { size: 12, strokeWidth: 1.75 }),
|
|
4803
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Design" })
|
|
4804
|
+
]
|
|
4805
|
+
}
|
|
4806
|
+
),
|
|
4807
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4808
|
+
fieldcraftReact.Button,
|
|
4809
|
+
{
|
|
4810
|
+
variant: viewMode === "preview" ? "secondary" : "ghost",
|
|
4811
|
+
size: "sm",
|
|
4812
|
+
onClick: () => handleViewModeChange("preview"),
|
|
4813
|
+
className: "gap-1 h-7 px-2 text-xs",
|
|
4814
|
+
children: [
|
|
4815
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, { size: 12, strokeWidth: 1.75 }),
|
|
4816
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Preview" })
|
|
4817
|
+
]
|
|
4818
|
+
}
|
|
4819
|
+
),
|
|
4820
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4821
|
+
fieldcraftReact.Button,
|
|
4822
|
+
{
|
|
4823
|
+
variant: viewMode === "json" ? "secondary" : "ghost",
|
|
4824
|
+
size: "sm",
|
|
4825
|
+
onClick: () => handleViewModeChange("json"),
|
|
4826
|
+
className: "gap-1 h-7 px-2 text-xs",
|
|
4827
|
+
children: [
|
|
4828
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 12, strokeWidth: 1.75 }),
|
|
4829
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "JSON" })
|
|
4830
|
+
]
|
|
4831
|
+
}
|
|
4832
|
+
)
|
|
4833
|
+
] }),
|
|
3655
4834
|
toolbarExtra,
|
|
3656
4835
|
/* @__PURE__ */ jsxRuntime.jsxs(fieldcraftReact.Button, { onClick: handleSave, size: "sm", className: "border-0 shadow-sm fcb-glow ml-1", "aria-label": "Save form", children: [
|
|
3657
4836
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 14, strokeWidth: 2 }),
|
|
@@ -3740,7 +4919,50 @@ ${details}`);
|
|
|
3740
4919
|
"aria-hidden": "true"
|
|
3741
4920
|
}
|
|
3742
4921
|
),
|
|
3743
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "
|
|
4922
|
+
jsonSwitchError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 bg-destructive/10 border-b border-destructive/20 text-destructive text-xs flex items-center justify-between", children: [
|
|
4923
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: jsonSwitchError }),
|
|
4924
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setJsonSwitchError(null), className: "ml-2 hover:underline", children: "Dismiss" })
|
|
4925
|
+
] }),
|
|
4926
|
+
saveValidationErrors && saveValidationErrors.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 bg-destructive/10 border-b border-destructive/20 text-xs", children: [
|
|
4927
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
|
|
4928
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-semibold text-destructive", children: [
|
|
4929
|
+
"Schema validation failed (",
|
|
4930
|
+
saveValidationErrors.length,
|
|
4931
|
+
" issue",
|
|
4932
|
+
saveValidationErrors.length !== 1 ? "s" : "",
|
|
4933
|
+
")"
|
|
4934
|
+
] }),
|
|
4935
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setSaveValidationErrors(null), className: "text-destructive hover:underline", children: "Dismiss" })
|
|
4936
|
+
] }),
|
|
4937
|
+
/* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "list-disc pl-4 text-destructive/80 space-y-0.5", children: [
|
|
4938
|
+
saveValidationErrors.slice(0, 10).map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: msg }, i)),
|
|
4939
|
+
saveValidationErrors.length > 10 && /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
|
|
4940
|
+
"...and ",
|
|
4941
|
+
saveValidationErrors.length - 10,
|
|
4942
|
+
" more"
|
|
4943
|
+
] })
|
|
4944
|
+
] })
|
|
4945
|
+
] }),
|
|
4946
|
+
schemaUrlLoading && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 border-b border-border bg-muted/50 text-xs text-muted-foreground flex items-center gap-2", children: [
|
|
4947
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block w-3 h-3 border-2 border-primary border-t-transparent rounded-full animate-spin" }),
|
|
4948
|
+
"Loading schema from URL..."
|
|
4949
|
+
] }),
|
|
4950
|
+
schemaUrlError && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 bg-destructive/10 border-b border-destructive/20 text-destructive text-xs flex items-center justify-between", children: [
|
|
4951
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
4952
|
+
"Failed to load schema: ",
|
|
4953
|
+
schemaUrlError
|
|
4954
|
+
] }),
|
|
4955
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setSchemaUrlError(null), className: "ml-2 hover:underline", children: "Dismiss" })
|
|
4956
|
+
] }),
|
|
4957
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex overflow-hidden relative", children: viewMode === "json" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4958
|
+
JsonEditorPanel,
|
|
4959
|
+
{
|
|
4960
|
+
value: jsonText,
|
|
4961
|
+
onChange: setJsonText,
|
|
4962
|
+
errors: jsonErrors,
|
|
4963
|
+
onValidate: validateJson
|
|
4964
|
+
}
|
|
4965
|
+
) : viewMode === "preview" ? /* @__PURE__ */ jsxRuntime.jsx(FormPreviewPanel, { schema: builderState.schema, preview }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3744
4966
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(QuestionPalette, { questionTypes: mergedQuestionTypes, palette }) }),
|
|
3745
4967
|
/* @__PURE__ */ jsxRuntime.jsx(FormCanvas, { builderState }),
|
|
3746
4968
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(PropertiesPanel, { builderState }) }),
|
|
@@ -3754,11 +4976,27 @@ ${details}`);
|
|
|
3754
4976
|
"aria-hidden": "true"
|
|
3755
4977
|
}
|
|
3756
4978
|
)
|
|
3757
|
-
] })
|
|
4979
|
+
] }) })
|
|
3758
4980
|
]
|
|
3759
4981
|
}
|
|
3760
4982
|
),
|
|
3761
|
-
/* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { dropAnimation: null, children: dragDrop.activeDragItem && /* @__PURE__ */ jsxRuntime.jsx(DragOverlayContent, { item: dragDrop.activeDragItem, schema: builderState.schema, questionTypes: mergedQuestionTypes }) })
|
|
4983
|
+
/* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { dropAnimation: null, children: dragDrop.activeDragItem && /* @__PURE__ */ jsxRuntime.jsx(DragOverlayContent, { item: dragDrop.activeDragItem, schema: builderState.schema, questionTypes: mergedQuestionTypes }) }),
|
|
4984
|
+
showLogicMap && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4985
|
+
LogicFlowEditor,
|
|
4986
|
+
{
|
|
4987
|
+
schema: builderState.schema,
|
|
4988
|
+
onChange: (updated) => builderState.resetSchema(updated),
|
|
4989
|
+
onClose: () => setShowLogicMap(false)
|
|
4990
|
+
}
|
|
4991
|
+
),
|
|
4992
|
+
showTemplateGallery && templates && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4993
|
+
TemplateGallery,
|
|
4994
|
+
{
|
|
4995
|
+
templates,
|
|
4996
|
+
onSelect: (template) => builderState.resetSchema(template.schema),
|
|
4997
|
+
onClose: () => setShowTemplateGallery(false)
|
|
4998
|
+
}
|
|
4999
|
+
)
|
|
3762
5000
|
]
|
|
3763
5001
|
}
|
|
3764
5002
|
) });
|
|
@@ -3790,99 +5028,343 @@ function DragOverlayContent({
|
|
|
3790
5028
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-medium", children: found.question.label })
|
|
3791
5029
|
] });
|
|
3792
5030
|
}
|
|
3793
|
-
if (item.type === "section") {
|
|
3794
|
-
const section = schema.sections.find((s) => s.id === item.sectionId);
|
|
3795
|
-
if (!section) return null;
|
|
3796
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
|
|
3797
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold", children: section.title }),
|
|
3798
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground mt-0.5", children: [
|
|
3799
|
-
section.questions.length,
|
|
3800
|
-
" field",
|
|
3801
|
-
section.questions.length !== 1 ? "s" : ""
|
|
5031
|
+
if (item.type === "section") {
|
|
5032
|
+
const section = schema.sections.find((s) => s.id === item.sectionId);
|
|
5033
|
+
if (!section) return null;
|
|
5034
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3 rounded-md border border-primary bg-card text-foreground fcb-shadow-lg cursor-grabbing max-w-sm", children: [
|
|
5035
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold", children: section.title }),
|
|
5036
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground mt-0.5", children: [
|
|
5037
|
+
section.questions.length,
|
|
5038
|
+
" field",
|
|
5039
|
+
section.questions.length !== 1 ? "s" : ""
|
|
5040
|
+
] })
|
|
5041
|
+
] });
|
|
5042
|
+
}
|
|
5043
|
+
return null;
|
|
5044
|
+
}
|
|
5045
|
+
function FormBuilderInner(props) {
|
|
5046
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FormBuilderErrorBoundary, { children: /* @__PURE__ */ jsxRuntime.jsx(FormBuilderCore, { ...props }) });
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
// src/form-builder/components/FormBuilderGated.tsx
|
|
5050
|
+
var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
|
|
5051
|
+
|
|
5052
|
+
// src/form-builder/theme/presets.ts
|
|
5053
|
+
var squaredrDarkPreset = {
|
|
5054
|
+
background: "#0F1A1F",
|
|
5055
|
+
foreground: "#E8EFF1",
|
|
5056
|
+
card: "#16242A",
|
|
5057
|
+
primary: "#63BDB4",
|
|
5058
|
+
primaryForeground: "#0F1A1F",
|
|
5059
|
+
secondary: "#182F31",
|
|
5060
|
+
secondaryForeground: "#E8EFF1",
|
|
5061
|
+
muted: "#182F31",
|
|
5062
|
+
mutedForeground: "#8CA1A9",
|
|
5063
|
+
accent: "#182F31",
|
|
5064
|
+
accentForeground: "#E8EFF1",
|
|
5065
|
+
destructive: "#E08072",
|
|
5066
|
+
destructiveForeground: "#0F1A1F",
|
|
5067
|
+
border: "#2A3B42",
|
|
5068
|
+
input: "#2A3B42",
|
|
5069
|
+
ring: "#63BDB4",
|
|
5070
|
+
radius: "0px",
|
|
5071
|
+
surface: "#16242A",
|
|
5072
|
+
surfaceHover: "#182F31",
|
|
5073
|
+
canvas: "#0F1A1F",
|
|
5074
|
+
panel: "#16242A",
|
|
5075
|
+
borderStrong: "#2F4F4C",
|
|
5076
|
+
textDim: "#5E7680"
|
|
5077
|
+
};
|
|
5078
|
+
var cleanPreset = {
|
|
5079
|
+
background: "#F4F7F8",
|
|
5080
|
+
foreground: "#12222A",
|
|
5081
|
+
card: "#FFFFFF",
|
|
5082
|
+
primary: "#1F6B6E",
|
|
5083
|
+
primaryForeground: "#FFFFFF",
|
|
5084
|
+
secondary: "#EDF3F2",
|
|
5085
|
+
secondaryForeground: "#12222A",
|
|
5086
|
+
muted: "#EDF3F2",
|
|
5087
|
+
mutedForeground: "#6A7B85",
|
|
5088
|
+
accent: "#EDF3F2",
|
|
5089
|
+
accentForeground: "#12222A",
|
|
5090
|
+
destructive: "#B04A3C",
|
|
5091
|
+
destructiveForeground: "#FFFFFF",
|
|
5092
|
+
border: "#DCE4E8",
|
|
5093
|
+
input: "#DCE4E8",
|
|
5094
|
+
ring: "#1F6B6E",
|
|
5095
|
+
radius: "0px",
|
|
5096
|
+
surface: "#FAFCFC",
|
|
5097
|
+
surfaceHover: "#EDF3F2",
|
|
5098
|
+
canvas: "#F4F7F8",
|
|
5099
|
+
panel: "#FFFFFF",
|
|
5100
|
+
borderStrong: "#B9D1CF",
|
|
5101
|
+
textDim: "#6A7B85"
|
|
5102
|
+
};
|
|
5103
|
+
function formatCurrency(amount, currency) {
|
|
5104
|
+
if (amount == null) return "";
|
|
5105
|
+
try {
|
|
5106
|
+
return new Intl.NumberFormat(void 0, {
|
|
5107
|
+
style: "currency",
|
|
5108
|
+
currency: currency ?? "USD"
|
|
5109
|
+
}).format(amount);
|
|
5110
|
+
} catch {
|
|
5111
|
+
return `${currency ?? "USD"} ${amount.toFixed(2)}`;
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
function ProPaymentField(props) {
|
|
5115
|
+
const { field, value, error, touched, disabled, readonly, onChange, onBlur, customProps } = props;
|
|
5116
|
+
const config = field.config;
|
|
5117
|
+
const current = value ?? { status: "pending" };
|
|
5118
|
+
const provider = config?.provider ?? "stripe";
|
|
5119
|
+
const publicKey = config?.publicKey;
|
|
5120
|
+
const amount = customProps?.amount ?? config?.amount;
|
|
5121
|
+
const currency = config?.currency ?? "USD";
|
|
5122
|
+
const directSecret = customProps?.clientSecret;
|
|
5123
|
+
const onCreateIntent = customProps?.onCreatePaymentIntent;
|
|
5124
|
+
const onPaymentComplete = customProps?.onPaymentComplete;
|
|
5125
|
+
const serverUrl = config?.serverUrl;
|
|
5126
|
+
const [clientSecret, setClientSecret] = react.useState(directSecret);
|
|
5127
|
+
const [intentLoading, setIntentLoading] = react.useState(false);
|
|
5128
|
+
const [intentError, setIntentError] = react.useState(null);
|
|
5129
|
+
const mode = directSecret ? "direct" : onCreateIntent ? "callback" : serverUrl ? "url" : "setup";
|
|
5130
|
+
react.useEffect(() => {
|
|
5131
|
+
if (directSecret) {
|
|
5132
|
+
setClientSecret(directSecret);
|
|
5133
|
+
return;
|
|
5134
|
+
}
|
|
5135
|
+
if (mode === "setup" || !amount || !publicKey) return;
|
|
5136
|
+
if (clientSecret) return;
|
|
5137
|
+
const fetchIntent = async () => {
|
|
5138
|
+
setIntentLoading(true);
|
|
5139
|
+
setIntentError(null);
|
|
5140
|
+
try {
|
|
5141
|
+
if (mode === "callback" && onCreateIntent) {
|
|
5142
|
+
const result = await onCreateIntent({ amount, currency, provider, metadata: { fieldId: field.id } });
|
|
5143
|
+
setClientSecret(result.clientSecret);
|
|
5144
|
+
} else if (mode === "url" && serverUrl) {
|
|
5145
|
+
const res = await fetch(serverUrl, {
|
|
5146
|
+
method: "POST",
|
|
5147
|
+
headers: { "Content-Type": "application/json" },
|
|
5148
|
+
body: JSON.stringify({ amount, currency, provider, metadata: { fieldId: field.id } })
|
|
5149
|
+
});
|
|
5150
|
+
if (!res.ok) throw new Error(`Server error (${res.status})`);
|
|
5151
|
+
const data = await res.json();
|
|
5152
|
+
setClientSecret(data.clientSecret);
|
|
5153
|
+
}
|
|
5154
|
+
} catch (err) {
|
|
5155
|
+
setIntentError(err instanceof Error ? err.message : "Failed to create payment intent");
|
|
5156
|
+
} finally {
|
|
5157
|
+
setIntentLoading(false);
|
|
5158
|
+
}
|
|
5159
|
+
};
|
|
5160
|
+
fetchIntent();
|
|
5161
|
+
}, [mode, amount, currency, provider, publicKey, directSecret, clientSecret, onCreateIntent, serverUrl, field.id]);
|
|
5162
|
+
const handleSuccess = (chargeId) => {
|
|
5163
|
+
const result = { status: "succeeded", chargeId };
|
|
5164
|
+
onChange(result);
|
|
5165
|
+
onPaymentComplete?.(result);
|
|
5166
|
+
onBlur();
|
|
5167
|
+
};
|
|
5168
|
+
const handleError = (message) => {
|
|
5169
|
+
const result = { status: "failed", error: message };
|
|
5170
|
+
onChange(result);
|
|
5171
|
+
onPaymentComplete?.(result);
|
|
5172
|
+
onBlur();
|
|
5173
|
+
};
|
|
5174
|
+
if (!publicKey) {
|
|
5175
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error, touched, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border-2 border-dashed border-input p-4 text-center", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
5176
|
+
"Payment field \u2014 configure a ",
|
|
5177
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { className: "bg-muted px-1 rounded text-xs", children: "publicKey" }),
|
|
5178
|
+
" in the field properties panel."
|
|
5179
|
+
] }) }) });
|
|
5180
|
+
}
|
|
5181
|
+
if (provider !== "stripe") {
|
|
5182
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error, touched, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-lg border border-input bg-muted/50 p-4", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground", children: [
|
|
5183
|
+
'Provider "',
|
|
5184
|
+
provider,
|
|
5185
|
+
'" \u2014 coming soon.'
|
|
5186
|
+
] }) }) });
|
|
5187
|
+
}
|
|
5188
|
+
if (current.status === "succeeded") {
|
|
5189
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error, touched, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-input p-4", children: [
|
|
5190
|
+
amount != null && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground mb-2", children: [
|
|
5191
|
+
formatCurrency(amount, currency),
|
|
5192
|
+
config?.description && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
5193
|
+
" \u2014 ",
|
|
5194
|
+
config.description
|
|
5195
|
+
] })
|
|
5196
|
+
] }),
|
|
5197
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm font-medium", style: { color: "var(--success, #22c55e)" }, children: [
|
|
5198
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, width: 16, height: 16, children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" }) }),
|
|
5199
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
5200
|
+
"Payment successful",
|
|
5201
|
+
current.chargeId ? ` (${current.chargeId})` : ""
|
|
5202
|
+
] })
|
|
5203
|
+
] })
|
|
5204
|
+
] }) });
|
|
5205
|
+
}
|
|
5206
|
+
if (current.status === "failed") {
|
|
5207
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error, touched, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-destructive/50 p-4", children: [
|
|
5208
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-destructive font-medium", children: [
|
|
5209
|
+
/* @__PURE__ */ jsxRuntime.jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, width: 16, height: 16, children: [
|
|
5210
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
5211
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
5212
|
+
] }),
|
|
5213
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
5214
|
+
"Payment failed",
|
|
5215
|
+
current.error ? `: ${current.error}` : ""
|
|
5216
|
+
] })
|
|
5217
|
+
] }),
|
|
5218
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5219
|
+
"button",
|
|
5220
|
+
{
|
|
5221
|
+
type: "button",
|
|
5222
|
+
className: "mt-2 text-xs text-primary hover:underline",
|
|
5223
|
+
onClick: () => {
|
|
5224
|
+
onChange({ status: "pending" });
|
|
5225
|
+
setClientSecret(void 0);
|
|
5226
|
+
},
|
|
5227
|
+
children: "Retry"
|
|
5228
|
+
}
|
|
5229
|
+
)
|
|
5230
|
+
] }) });
|
|
5231
|
+
}
|
|
5232
|
+
if (intentLoading || mode !== "setup" && mode !== "direct" && !clientSecret) {
|
|
5233
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error, touched, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-input p-4", children: [
|
|
5234
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-3", children: [
|
|
5235
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: "Payment" }),
|
|
5236
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs bg-muted px-2 py-0.5 rounded", children: provider })
|
|
5237
|
+
] }),
|
|
5238
|
+
amount != null && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground mb-3", children: [
|
|
5239
|
+
"Amount: ",
|
|
5240
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: formatCurrency(amount, currency) }),
|
|
5241
|
+
config?.description && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
5242
|
+
" \u2014 ",
|
|
5243
|
+
config.description
|
|
5244
|
+
] })
|
|
5245
|
+
] }),
|
|
5246
|
+
intentError ? /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
5247
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: intentError }),
|
|
5248
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5249
|
+
"button",
|
|
5250
|
+
{
|
|
5251
|
+
type: "button",
|
|
5252
|
+
className: "mt-2 text-xs text-primary hover:underline",
|
|
5253
|
+
onClick: () => {
|
|
5254
|
+
setIntentError(null);
|
|
5255
|
+
setClientSecret(void 0);
|
|
5256
|
+
},
|
|
5257
|
+
children: "Retry"
|
|
5258
|
+
}
|
|
5259
|
+
)
|
|
5260
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground", children: [
|
|
5261
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { className: "animate-spin size-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10", strokeDasharray: "60", strokeDashoffset: "20" }) }),
|
|
5262
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "Preparing payment..." })
|
|
5263
|
+
] })
|
|
5264
|
+
] }) });
|
|
5265
|
+
}
|
|
5266
|
+
if (!clientSecret) {
|
|
5267
|
+
const waitingForAmount = config?.amountField && !amount;
|
|
5268
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error, touched, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-input p-4", children: [
|
|
5269
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-3", children: [
|
|
5270
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: "Payment" }),
|
|
5271
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs bg-muted px-2 py-0.5 rounded", children: provider })
|
|
5272
|
+
] }),
|
|
5273
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: waitingForAmount ? "Select an option above to proceed with payment." : "Provide a clientSecret via customProps, an onCreatePaymentIntent callback, or a serverUrl." })
|
|
5274
|
+
] }) });
|
|
5275
|
+
}
|
|
5276
|
+
return /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.FieldWrapper, { field, error, touched, children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-input p-4", children: [
|
|
5277
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-3", children: [
|
|
5278
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: "Payment" }),
|
|
5279
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs bg-muted px-2 py-0.5 rounded", children: provider })
|
|
5280
|
+
] }),
|
|
5281
|
+
amount != null && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground mb-3", children: [
|
|
5282
|
+
"Amount: ",
|
|
5283
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: formatCurrency(amount, currency) }),
|
|
5284
|
+
config?.description && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
5285
|
+
" \u2014 ",
|
|
5286
|
+
config.description
|
|
3802
5287
|
] })
|
|
5288
|
+
] }),
|
|
5289
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5290
|
+
react.Suspense,
|
|
5291
|
+
{
|
|
5292
|
+
fallback: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground py-4", children: [
|
|
5293
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { className: "animate-spin size-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10", strokeDasharray: "60", strokeDashoffset: "20" }) }),
|
|
5294
|
+
"Loading payment form..."
|
|
5295
|
+
] }),
|
|
5296
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5297
|
+
PayKitCheckout,
|
|
5298
|
+
{
|
|
5299
|
+
publicKey,
|
|
5300
|
+
clientSecret,
|
|
5301
|
+
amount,
|
|
5302
|
+
currency,
|
|
5303
|
+
disabled: disabled || readonly,
|
|
5304
|
+
onSuccess: handleSuccess,
|
|
5305
|
+
onError: handleError
|
|
5306
|
+
}
|
|
5307
|
+
)
|
|
5308
|
+
}
|
|
5309
|
+
)
|
|
5310
|
+
] }) });
|
|
5311
|
+
}
|
|
5312
|
+
function PayKitCheckout({
|
|
5313
|
+
publicKey,
|
|
5314
|
+
clientSecret,
|
|
5315
|
+
amount,
|
|
5316
|
+
currency,
|
|
5317
|
+
disabled,
|
|
5318
|
+
onSuccess,
|
|
5319
|
+
onError
|
|
5320
|
+
}) {
|
|
5321
|
+
const [PayKit, setPayKit] = react.useState(null);
|
|
5322
|
+
const [loadError, setLoadError] = react.useState(null);
|
|
5323
|
+
react.useEffect(() => {
|
|
5324
|
+
let cancelled = false;
|
|
5325
|
+
Promise.all([
|
|
5326
|
+
// @ts-expect-error — optional peer dep, resolved at runtime
|
|
5327
|
+
import('@squaredr/paykit-react'),
|
|
5328
|
+
// @ts-expect-error — optional peer dep, resolved at runtime
|
|
5329
|
+
import('@squaredr/paykit-stripe/client')
|
|
5330
|
+
]).then(([paykit, stripe]) => {
|
|
5331
|
+
if (cancelled) return;
|
|
5332
|
+
setPayKit({
|
|
5333
|
+
Provider: paykit.PayKitProvider,
|
|
5334
|
+
Form: paykit.CheckoutForm,
|
|
5335
|
+
adapter: new stripe.StripeClientAdapter(publicKey)
|
|
5336
|
+
});
|
|
5337
|
+
}).catch((err) => {
|
|
5338
|
+
if (cancelled) return;
|
|
5339
|
+
setLoadError(
|
|
5340
|
+
`Failed to load payment SDK. Ensure @squaredr/paykit-react and @squaredr/paykit-stripe are installed. (${err.message})`
|
|
5341
|
+
);
|
|
5342
|
+
});
|
|
5343
|
+
return () => {
|
|
5344
|
+
cancelled = true;
|
|
5345
|
+
};
|
|
5346
|
+
}, [publicKey]);
|
|
5347
|
+
if (loadError) {
|
|
5348
|
+
return /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: loadError });
|
|
5349
|
+
}
|
|
5350
|
+
if (!PayKit) {
|
|
5351
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 text-sm text-muted-foreground py-2", children: [
|
|
5352
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { className: "animate-spin size-4", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "10", strokeDasharray: "60", strokeDashoffset: "20" }) }),
|
|
5353
|
+
"Loading Stripe..."
|
|
3803
5354
|
] });
|
|
3804
5355
|
}
|
|
3805
|
-
return
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
5356
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PayKit.Provider, { clientAdapter: PayKit.adapter, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5357
|
+
PayKit.Form,
|
|
5358
|
+
{
|
|
5359
|
+
clientSecret,
|
|
5360
|
+
submitLabel: disabled ? "Payment disabled" : `Pay ${formatCurrency(amount, currency)}`,
|
|
5361
|
+
onSuccess: (result) => onSuccess(result.chargeId),
|
|
5362
|
+
onError: (err) => onError(err.message)
|
|
5363
|
+
}
|
|
5364
|
+
) });
|
|
3809
5365
|
}
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
|
|
3813
|
-
|
|
3814
|
-
// src/form-builder/theme/presets.ts
|
|
3815
|
-
var squaredrDarkPreset = {
|
|
3816
|
-
background: "#0a0a0b",
|
|
3817
|
-
// ink-950
|
|
3818
|
-
foreground: "#e8e8ea",
|
|
3819
|
-
// ink-100
|
|
3820
|
-
card: "#111113",
|
|
3821
|
-
// ink-900
|
|
3822
|
-
primary: "oklch(0.82 0.14 210)",
|
|
3823
|
-
// sr-accent-cyan
|
|
3824
|
-
primaryForeground: "#0a0a0b",
|
|
3825
|
-
// ink-950
|
|
3826
|
-
secondary: "#17171a",
|
|
3827
|
-
// ink-850
|
|
3828
|
-
secondaryForeground: "#e8e8ea",
|
|
3829
|
-
// ink-100
|
|
3830
|
-
muted: "#111113",
|
|
3831
|
-
// ink-900
|
|
3832
|
-
mutedForeground: "#8a8a95",
|
|
3833
|
-
// ink-400
|
|
3834
|
-
accent: "#17171a",
|
|
3835
|
-
// ink-850
|
|
3836
|
-
accentForeground: "#e8e8ea",
|
|
3837
|
-
// ink-100
|
|
3838
|
-
destructive: "oklch(0.68 0.22 25)",
|
|
3839
|
-
// sr-error
|
|
3840
|
-
destructiveForeground: "#e8e8ea",
|
|
3841
|
-
// ink-100
|
|
3842
|
-
border: "#1c1c20",
|
|
3843
|
-
// ink-800
|
|
3844
|
-
input: "#1c1c20",
|
|
3845
|
-
// ink-800
|
|
3846
|
-
ring: "oklch(0.82 0.14 210)",
|
|
3847
|
-
// sr-accent-cyan
|
|
3848
|
-
radius: "6px",
|
|
3849
|
-
surface: "#111113",
|
|
3850
|
-
// ink-900
|
|
3851
|
-
surfaceHover: "#17171a",
|
|
3852
|
-
// ink-850
|
|
3853
|
-
canvas: "#0a0a0b",
|
|
3854
|
-
// ink-950
|
|
3855
|
-
panel: "#111113",
|
|
3856
|
-
// ink-900
|
|
3857
|
-
borderStrong: "#26262c",
|
|
3858
|
-
// ink-700
|
|
3859
|
-
textDim: "#5a5a66"
|
|
3860
|
-
// ink-500
|
|
3861
|
-
};
|
|
3862
|
-
var cleanPreset = {
|
|
3863
|
-
background: "#ffffff",
|
|
3864
|
-
foreground: "#111113",
|
|
3865
|
-
card: "#f9fafb",
|
|
3866
|
-
primary: "#0d9488",
|
|
3867
|
-
primaryForeground: "#ffffff",
|
|
3868
|
-
secondary: "#f3f4f6",
|
|
3869
|
-
secondaryForeground: "#111113",
|
|
3870
|
-
muted: "#f3f4f6",
|
|
3871
|
-
mutedForeground: "#6b7280",
|
|
3872
|
-
accent: "#f3f4f6",
|
|
3873
|
-
accentForeground: "#111113",
|
|
3874
|
-
destructive: "#ef4444",
|
|
3875
|
-
destructiveForeground: "#ffffff",
|
|
3876
|
-
border: "#e5e7eb",
|
|
3877
|
-
input: "#e5e7eb",
|
|
3878
|
-
ring: "#0d9488",
|
|
3879
|
-
radius: "6px",
|
|
3880
|
-
surface: "#f9fafb",
|
|
3881
|
-
surfaceHover: "#f3f4f6",
|
|
3882
|
-
canvas: "#ffffff",
|
|
3883
|
-
panel: "#ffffff",
|
|
3884
|
-
borderStrong: "#d1d5db",
|
|
3885
|
-
textDim: "#9ca3af"
|
|
5366
|
+
var PRO_FIELD_OVERRIDES = {
|
|
5367
|
+
payment: ProPaymentField
|
|
3886
5368
|
};
|
|
3887
5369
|
|
|
3888
5370
|
// src/response-viewer/clinical-display-data.ts
|
|
@@ -4021,10 +5503,30 @@ function getScoreSeverity(instrumentKey, score) {
|
|
|
4021
5503
|
if (!thresholds) return void 0;
|
|
4022
5504
|
return thresholds.find((t) => score >= t.min && score <= t.max);
|
|
4023
5505
|
}
|
|
4024
|
-
function ResponseTable({
|
|
5506
|
+
function ResponseTable({
|
|
5507
|
+
schema,
|
|
5508
|
+
responses,
|
|
5509
|
+
onRowClick,
|
|
5510
|
+
selectable,
|
|
5511
|
+
selectedIds,
|
|
5512
|
+
onToggleSelect,
|
|
5513
|
+
onSelectAll
|
|
5514
|
+
}) {
|
|
4025
5515
|
const questions = getAllQuestions(schema);
|
|
5516
|
+
const allPageSelected = selectable && responses.length > 0 && responses.every(
|
|
5517
|
+
(r) => r.sessionToken && selectedIds?.has(r.sessionToken)
|
|
5518
|
+
);
|
|
4026
5519
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
|
|
4027
5520
|
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "bg-muted", children: [
|
|
5521
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 w-8 border-b-2 border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5522
|
+
"input",
|
|
5523
|
+
{
|
|
5524
|
+
type: "checkbox",
|
|
5525
|
+
checked: allPageSelected,
|
|
5526
|
+
onChange: () => onSelectAll?.(),
|
|
5527
|
+
className: "accent-primary"
|
|
5528
|
+
}
|
|
5529
|
+
) }),
|
|
4028
5530
|
/* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 text-left font-semibold text-foreground border-b-2 border-border whitespace-nowrap", children: "Submitted" }),
|
|
4029
5531
|
questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4030
5532
|
"th",
|
|
@@ -4043,6 +5545,19 @@ function ResponseTable({ schema, responses, onRowClick }) {
|
|
|
4043
5545
|
onClick: () => onRowClick?.(response),
|
|
4044
5546
|
className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
|
|
4045
5547
|
children: [
|
|
5548
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 w-8", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5549
|
+
"input",
|
|
5550
|
+
{
|
|
5551
|
+
type: "checkbox",
|
|
5552
|
+
checked: !!(response.sessionToken && selectedIds?.has(response.sessionToken)),
|
|
5553
|
+
onChange: (e) => {
|
|
5554
|
+
e.stopPropagation();
|
|
5555
|
+
if (response.sessionToken) onToggleSelect?.(response.sessionToken);
|
|
5556
|
+
},
|
|
5557
|
+
onClick: (e) => e.stopPropagation(),
|
|
5558
|
+
className: "accent-primary"
|
|
5559
|
+
}
|
|
5560
|
+
) }),
|
|
4046
5561
|
/* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 text-foreground max-w-50 overflow-hidden text-ellipsis whitespace-nowrap", children: new Date(response.submittedAt).toLocaleString() }),
|
|
4047
5562
|
questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4048
5563
|
"td",
|
|
@@ -4060,7 +5575,7 @@ function ResponseTable({ schema, responses, onRowClick }) {
|
|
|
4060
5575
|
responses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4061
5576
|
"td",
|
|
4062
5577
|
{
|
|
4063
|
-
colSpan: questions.length + 2,
|
|
5578
|
+
colSpan: questions.length + 2 + (selectable ? 1 : 0),
|
|
4064
5579
|
className: "px-3 py-8 text-center text-muted-foreground",
|
|
4065
5580
|
children: "No responses yet"
|
|
4066
5581
|
}
|
|
@@ -4148,15 +5663,37 @@ function formatCellValue(value, type) {
|
|
|
4148
5663
|
if (typeof value === "object") return JSON.stringify(value);
|
|
4149
5664
|
return String(value);
|
|
4150
5665
|
}
|
|
4151
|
-
function ResponseCard({
|
|
5666
|
+
function ResponseCard({
|
|
5667
|
+
response,
|
|
5668
|
+
fields,
|
|
5669
|
+
onClick,
|
|
5670
|
+
selectable,
|
|
5671
|
+
selected,
|
|
5672
|
+
onToggleSelect
|
|
5673
|
+
}) {
|
|
4152
5674
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4153
5675
|
"div",
|
|
4154
5676
|
{
|
|
4155
5677
|
onClick,
|
|
4156
|
-
className: onClick ? "border border-border rounded-lg p-4 bg-card cursor-pointer transition-shadow hover:shadow-md" : "border border-border rounded-lg p-4 bg-card",
|
|
5678
|
+
className: (onClick ? "border border-border rounded-lg p-4 bg-card cursor-pointer transition-shadow hover:shadow-md" : "border border-border rounded-lg p-4 bg-card") + (selected ? " ring-2 ring-primary" : ""),
|
|
4157
5679
|
children: [
|
|
4158
5680
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between mb-3 text-xs text-muted-foreground", children: [
|
|
4159
|
-
/* @__PURE__ */ jsxRuntime.
|
|
5681
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5682
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
5683
|
+
"input",
|
|
5684
|
+
{
|
|
5685
|
+
type: "checkbox",
|
|
5686
|
+
checked: !!selected,
|
|
5687
|
+
onChange: (e) => {
|
|
5688
|
+
e.stopPropagation();
|
|
5689
|
+
onToggleSelect?.();
|
|
5690
|
+
},
|
|
5691
|
+
onClick: (e) => e.stopPropagation(),
|
|
5692
|
+
className: "accent-primary"
|
|
5693
|
+
}
|
|
5694
|
+
),
|
|
5695
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(response.submittedAt).toLocaleString() })
|
|
5696
|
+
] }),
|
|
4160
5697
|
response.completionTimeMs != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
4161
5698
|
Math.round(response.completionTimeMs / 1e3),
|
|
4162
5699
|
"s"
|
|
@@ -4247,18 +5784,19 @@ function formatCardValue(value, type) {
|
|
|
4247
5784
|
if (typeof value === "object") return JSON.stringify(value);
|
|
4248
5785
|
return String(value);
|
|
4249
5786
|
}
|
|
4250
|
-
function ResponseDetail({ response, fields, onBack }) {
|
|
5787
|
+
function ResponseDetail({ response, fields, onBack, onDelete }) {
|
|
4251
5788
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4252
5789
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-5 pb-3 border-b border-border", children: [
|
|
4253
5790
|
onBack && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", onClick: onBack, children: "Back" }),
|
|
4254
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
5791
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
|
|
4255
5792
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold text-foreground", children: "Response Detail" }),
|
|
4256
5793
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground", children: [
|
|
4257
5794
|
"Submitted ",
|
|
4258
5795
|
new Date(response.submittedAt).toLocaleString(),
|
|
4259
5796
|
response.completionTimeMs != null && ` \u2014 ${Math.round(response.completionTimeMs / 1e3)}s`
|
|
4260
5797
|
] })
|
|
4261
|
-
] })
|
|
5798
|
+
] }),
|
|
5799
|
+
onDelete && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "destructive", size: "sm", onClick: onDelete, children: "Delete" })
|
|
4262
5800
|
] }),
|
|
4263
5801
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-4", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4264
5802
|
"div",
|
|
@@ -4611,6 +6149,491 @@ function formatFallbackValue(value) {
|
|
|
4611
6149
|
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
4612
6150
|
return String(value);
|
|
4613
6151
|
}
|
|
6152
|
+
function TimelineView({ responses, questions, onSelect }) {
|
|
6153
|
+
const grouped = react.useMemo(() => groupByDay(responses), [responses]);
|
|
6154
|
+
if (responses.length === 0) {
|
|
6155
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground text-sm", children: "No responses to display." });
|
|
6156
|
+
}
|
|
6157
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-6", children: grouped.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
6158
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
6159
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px flex-1 bg-border" }),
|
|
6160
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-muted-foreground shrink-0", children: group.dateLabel }),
|
|
6161
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground shrink-0", children: [
|
|
6162
|
+
"(",
|
|
6163
|
+
group.responses.length,
|
|
6164
|
+
")"
|
|
6165
|
+
] }),
|
|
6166
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px flex-1 bg-border" })
|
|
6167
|
+
] }),
|
|
6168
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2 ml-4 border-l-2 border-border/50 pl-4", children: group.responses.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
6169
|
+
TimelineEntry,
|
|
6170
|
+
{
|
|
6171
|
+
response,
|
|
6172
|
+
questions,
|
|
6173
|
+
onView: () => onSelect(response)
|
|
6174
|
+
},
|
|
6175
|
+
response.sessionToken || idx
|
|
6176
|
+
)) })
|
|
6177
|
+
] }, group.dateLabel)) });
|
|
6178
|
+
}
|
|
6179
|
+
function TimelineEntry({
|
|
6180
|
+
response,
|
|
6181
|
+
questions,
|
|
6182
|
+
onView
|
|
6183
|
+
}) {
|
|
6184
|
+
const time = new Date(response.submittedAt).toLocaleTimeString([], {
|
|
6185
|
+
hour: "2-digit",
|
|
6186
|
+
minute: "2-digit"
|
|
6187
|
+
});
|
|
6188
|
+
const preview = [];
|
|
6189
|
+
for (const q of questions) {
|
|
6190
|
+
if (preview.length >= 3) break;
|
|
6191
|
+
const val = response.values[q.id];
|
|
6192
|
+
if (val != null && val !== "") {
|
|
6193
|
+
preview.push({ questionId: q.id, label: q.label, type: q.type, value: val });
|
|
6194
|
+
}
|
|
6195
|
+
}
|
|
6196
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-start gap-3 group", children: [
|
|
6197
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "absolute -left-[21px] top-2.5 w-2 h-2 rounded-full bg-border group-hover:bg-primary transition-colors" }),
|
|
6198
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 rounded border border-border bg-card p-3 hover:border-primary/40 transition-colors", children: [
|
|
6199
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
6200
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: time }),
|
|
6201
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
6202
|
+
response.totalScore != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
6203
|
+
"Score: ",
|
|
6204
|
+
response.totalScore
|
|
6205
|
+
] }),
|
|
6206
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onView, children: "View" })
|
|
6207
|
+
] })
|
|
6208
|
+
] }),
|
|
6209
|
+
preview.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-x-4 gap-y-1", children: preview.map((field) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs", children: [
|
|
6210
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
6211
|
+
field.label,
|
|
6212
|
+
": "
|
|
6213
|
+
] }),
|
|
6214
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-foreground", children: formatPreviewValue(field.value, field.type) })
|
|
6215
|
+
] }, field.questionId)) })
|
|
6216
|
+
] })
|
|
6217
|
+
] });
|
|
6218
|
+
}
|
|
6219
|
+
function groupByDay(responses) {
|
|
6220
|
+
const sorted = [...responses].sort(
|
|
6221
|
+
(a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()
|
|
6222
|
+
);
|
|
6223
|
+
const groups = /* @__PURE__ */ new Map();
|
|
6224
|
+
for (const r of sorted) {
|
|
6225
|
+
const date = new Date(r.submittedAt).toLocaleDateString(void 0, {
|
|
6226
|
+
weekday: "long",
|
|
6227
|
+
year: "numeric",
|
|
6228
|
+
month: "long",
|
|
6229
|
+
day: "numeric"
|
|
6230
|
+
});
|
|
6231
|
+
const existing = groups.get(date);
|
|
6232
|
+
if (existing) {
|
|
6233
|
+
existing.push(r);
|
|
6234
|
+
} else {
|
|
6235
|
+
groups.set(date, [r]);
|
|
6236
|
+
}
|
|
6237
|
+
}
|
|
6238
|
+
return Array.from(groups.entries()).map(([dateLabel, responses2]) => ({
|
|
6239
|
+
dateLabel,
|
|
6240
|
+
responses: responses2
|
|
6241
|
+
}));
|
|
6242
|
+
}
|
|
6243
|
+
function formatPreviewValue(value, type) {
|
|
6244
|
+
if (value == null) return "\u2014";
|
|
6245
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
6246
|
+
if (Array.isArray(value)) return value.length > 0 ? value.slice(0, 2).join(", ") + (value.length > 2 ? "..." : "") : "\u2014";
|
|
6247
|
+
if (typeof value === "object") {
|
|
6248
|
+
if (type === "legal_name") {
|
|
6249
|
+
const n = value;
|
|
6250
|
+
return [n.first, n.last].filter(Boolean).join(" ") || "\u2014";
|
|
6251
|
+
}
|
|
6252
|
+
return "...";
|
|
6253
|
+
}
|
|
6254
|
+
const str = String(value);
|
|
6255
|
+
return str.length > 40 ? str.slice(0, 40) + "..." : str;
|
|
6256
|
+
}
|
|
6257
|
+
|
|
6258
|
+
// src/response-viewer/stats-utils.ts
|
|
6259
|
+
function computeStats(responses, schema) {
|
|
6260
|
+
if (responses.length === 0) {
|
|
6261
|
+
return {
|
|
6262
|
+
totalCount: 0,
|
|
6263
|
+
completionRate: 0,
|
|
6264
|
+
avgCompletionTimeMs: null,
|
|
6265
|
+
dateRange: null,
|
|
6266
|
+
fieldSummaries: []
|
|
6267
|
+
};
|
|
6268
|
+
}
|
|
6269
|
+
const questions = getStatsQuestions(schema);
|
|
6270
|
+
const requiredIds = new Set(questions.filter((q) => q.required).map((q) => q.id));
|
|
6271
|
+
let completedCount = 0;
|
|
6272
|
+
for (const r of responses) {
|
|
6273
|
+
let allFilled = true;
|
|
6274
|
+
for (const id of requiredIds) {
|
|
6275
|
+
if (r.values[id] == null || r.values[id] === "") {
|
|
6276
|
+
allFilled = false;
|
|
6277
|
+
break;
|
|
6278
|
+
}
|
|
6279
|
+
}
|
|
6280
|
+
if (allFilled) completedCount++;
|
|
6281
|
+
}
|
|
6282
|
+
const completionRate = requiredIds.size > 0 ? completedCount / responses.length : 1;
|
|
6283
|
+
const completionTimes = responses.map((r) => r.completionTimeMs).filter((t) => t != null && t > 0);
|
|
6284
|
+
const avgCompletionTimeMs = completionTimes.length > 0 ? completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length : null;
|
|
6285
|
+
const timestamps = responses.map((r) => new Date(r.submittedAt).getTime());
|
|
6286
|
+
const earliest = new Date(Math.min(...timestamps)).toISOString();
|
|
6287
|
+
const latest = new Date(Math.max(...timestamps)).toISOString();
|
|
6288
|
+
const fieldSummaries = questions.map((q) => computeFieldSummary(q, responses));
|
|
6289
|
+
return {
|
|
6290
|
+
totalCount: responses.length,
|
|
6291
|
+
completionRate,
|
|
6292
|
+
avgCompletionTimeMs,
|
|
6293
|
+
dateRange: { earliest, latest },
|
|
6294
|
+
fieldSummaries
|
|
6295
|
+
};
|
|
6296
|
+
}
|
|
6297
|
+
function computeFieldSummary(question, responses) {
|
|
6298
|
+
const values = responses.map((r) => r.values[question.id]).filter((v) => v != null && v !== "");
|
|
6299
|
+
const filledCount = values.length;
|
|
6300
|
+
const filledRate = responses.length > 0 ? filledCount / responses.length : 0;
|
|
6301
|
+
const base = {
|
|
6302
|
+
fieldId: question.id,
|
|
6303
|
+
label: question.label,
|
|
6304
|
+
type: question.type,
|
|
6305
|
+
filledCount,
|
|
6306
|
+
filledRate
|
|
6307
|
+
};
|
|
6308
|
+
if (isNumericType(question.type)) {
|
|
6309
|
+
const nums = values.map((v) => typeof v === "number" ? v : Number(v)).filter((n) => !isNaN(n));
|
|
6310
|
+
if (nums.length > 0) {
|
|
6311
|
+
return {
|
|
6312
|
+
...base,
|
|
6313
|
+
kind: "numeric",
|
|
6314
|
+
mean: nums.reduce((a, b) => a + b, 0) / nums.length,
|
|
6315
|
+
min: Math.min(...nums),
|
|
6316
|
+
max: Math.max(...nums)
|
|
6317
|
+
};
|
|
6318
|
+
}
|
|
6319
|
+
return { ...base, kind: "other" };
|
|
6320
|
+
}
|
|
6321
|
+
if (isCategoricalType(question.type)) {
|
|
6322
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6323
|
+
for (const v of values) {
|
|
6324
|
+
const key = String(v);
|
|
6325
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6326
|
+
}
|
|
6327
|
+
let mode = "";
|
|
6328
|
+
let modeCount = 0;
|
|
6329
|
+
for (const [key, count] of counts) {
|
|
6330
|
+
if (count > modeCount) {
|
|
6331
|
+
mode = key;
|
|
6332
|
+
modeCount = count;
|
|
6333
|
+
}
|
|
6334
|
+
}
|
|
6335
|
+
return {
|
|
6336
|
+
...base,
|
|
6337
|
+
kind: "categorical",
|
|
6338
|
+
mode,
|
|
6339
|
+
modeCount,
|
|
6340
|
+
uniqueCount: counts.size
|
|
6341
|
+
};
|
|
6342
|
+
}
|
|
6343
|
+
return { ...base, kind: "other" };
|
|
6344
|
+
}
|
|
6345
|
+
function isNumericType(type) {
|
|
6346
|
+
return [
|
|
6347
|
+
"number",
|
|
6348
|
+
"slider",
|
|
6349
|
+
"rating",
|
|
6350
|
+
"nps",
|
|
6351
|
+
"opinion_scale",
|
|
6352
|
+
"pain_scale"
|
|
6353
|
+
].includes(type);
|
|
6354
|
+
}
|
|
6355
|
+
function isCategoricalType(type) {
|
|
6356
|
+
return [
|
|
6357
|
+
"single_select",
|
|
6358
|
+
"dropdown",
|
|
6359
|
+
"country_select",
|
|
6360
|
+
"boolean",
|
|
6361
|
+
"consent",
|
|
6362
|
+
"multi_select",
|
|
6363
|
+
"checkbox_group"
|
|
6364
|
+
].includes(type);
|
|
6365
|
+
}
|
|
6366
|
+
function getStatsQuestions(schema) {
|
|
6367
|
+
const questions = [];
|
|
6368
|
+
for (const section of schema.sections) {
|
|
6369
|
+
for (const q of section.questions) {
|
|
6370
|
+
if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
|
|
6371
|
+
continue;
|
|
6372
|
+
}
|
|
6373
|
+
questions.push(q);
|
|
6374
|
+
}
|
|
6375
|
+
}
|
|
6376
|
+
return questions;
|
|
6377
|
+
}
|
|
6378
|
+
function StatsPanel({ schema, responses, onClose }) {
|
|
6379
|
+
const stats = react.useMemo(() => computeStats(responses, schema), [responses, schema]);
|
|
6380
|
+
if (stats.totalCount === 0) {
|
|
6381
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3 text-sm text-muted-foreground", children: "No responses to compute statistics." });
|
|
6382
|
+
}
|
|
6383
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border bg-muted/30", children: [
|
|
6384
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
|
|
6385
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: "Statistics" }),
|
|
6386
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
|
|
6387
|
+
] }),
|
|
6388
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
6389
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-3", children: [
|
|
6390
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6391
|
+
StatCard,
|
|
6392
|
+
{
|
|
6393
|
+
label: "Total Responses",
|
|
6394
|
+
value: String(stats.totalCount)
|
|
6395
|
+
}
|
|
6396
|
+
),
|
|
6397
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6398
|
+
StatCard,
|
|
6399
|
+
{
|
|
6400
|
+
label: "Completion Rate",
|
|
6401
|
+
value: `${Math.round(stats.completionRate * 100)}%`
|
|
6402
|
+
}
|
|
6403
|
+
),
|
|
6404
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6405
|
+
StatCard,
|
|
6406
|
+
{
|
|
6407
|
+
label: "Avg. Completion Time",
|
|
6408
|
+
value: formatTime(stats.avgCompletionTimeMs)
|
|
6409
|
+
}
|
|
6410
|
+
),
|
|
6411
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6412
|
+
StatCard,
|
|
6413
|
+
{
|
|
6414
|
+
label: "Date Range",
|
|
6415
|
+
value: formatDateRange(stats.dateRange)
|
|
6416
|
+
}
|
|
6417
|
+
)
|
|
6418
|
+
] }),
|
|
6419
|
+
stats.fieldSummaries.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 grid grid-cols-2 sm:grid-cols-3 gap-2", children: stats.fieldSummaries.slice(0, 6).map((f) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
6420
|
+
"div",
|
|
6421
|
+
{
|
|
6422
|
+
className: "px-2.5 py-2 rounded border border-border bg-background text-xs",
|
|
6423
|
+
children: [
|
|
6424
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-muted-foreground truncate mb-1", children: f.label }),
|
|
6425
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
|
|
6426
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-foreground font-medium", children: [
|
|
6427
|
+
Math.round(f.filledRate * 100),
|
|
6428
|
+
"% filled"
|
|
6429
|
+
] }),
|
|
6430
|
+
f.kind === "numeric" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
6431
|
+
"avg ",
|
|
6432
|
+
f.mean.toFixed(1),
|
|
6433
|
+
" (",
|
|
6434
|
+
f.min,
|
|
6435
|
+
"\u2013",
|
|
6436
|
+
f.max,
|
|
6437
|
+
")"
|
|
6438
|
+
] }),
|
|
6439
|
+
f.kind === "categorical" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground truncate", children: [
|
|
6440
|
+
"top: ",
|
|
6441
|
+
f.mode,
|
|
6442
|
+
" (",
|
|
6443
|
+
f.modeCount,
|
|
6444
|
+
")"
|
|
6445
|
+
] })
|
|
6446
|
+
] })
|
|
6447
|
+
]
|
|
6448
|
+
},
|
|
6449
|
+
f.fieldId
|
|
6450
|
+
)) })
|
|
6451
|
+
] })
|
|
6452
|
+
] });
|
|
6453
|
+
}
|
|
6454
|
+
function StatCard({ label, value }) {
|
|
6455
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2 rounded border border-border bg-background", children: [
|
|
6456
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[11px] text-muted-foreground mb-0.5", children: label }),
|
|
6457
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold text-foreground", children: value })
|
|
6458
|
+
] });
|
|
6459
|
+
}
|
|
6460
|
+
function formatTime(ms) {
|
|
6461
|
+
if (ms == null) return "\u2014";
|
|
6462
|
+
const seconds = Math.round(ms / 1e3);
|
|
6463
|
+
if (seconds < 60) return `${seconds}s`;
|
|
6464
|
+
const minutes = Math.floor(seconds / 60);
|
|
6465
|
+
const remainingSeconds = seconds % 60;
|
|
6466
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
6467
|
+
}
|
|
6468
|
+
function formatDateRange(range) {
|
|
6469
|
+
if (!range) return "\u2014";
|
|
6470
|
+
const fmt = (iso) => new Date(iso).toLocaleDateString();
|
|
6471
|
+
return `${fmt(range.earliest)} \u2013 ${fmt(range.latest)}`;
|
|
6472
|
+
}
|
|
6473
|
+
|
|
6474
|
+
// src/response-viewer/chart-utils.ts
|
|
6475
|
+
function computeFieldDistribution(responses, fieldId) {
|
|
6476
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6477
|
+
let total = 0;
|
|
6478
|
+
for (const r of responses) {
|
|
6479
|
+
const raw = r.values[fieldId];
|
|
6480
|
+
if (raw == null || raw === "") continue;
|
|
6481
|
+
total++;
|
|
6482
|
+
if (typeof raw === "boolean") {
|
|
6483
|
+
const key = raw ? "Yes" : "No";
|
|
6484
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6485
|
+
} else if (Array.isArray(raw)) {
|
|
6486
|
+
for (const item of raw) {
|
|
6487
|
+
const key = String(item);
|
|
6488
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6489
|
+
}
|
|
6490
|
+
} else {
|
|
6491
|
+
const key = String(raw);
|
|
6492
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
if (total === 0) return [];
|
|
6496
|
+
const entries = [];
|
|
6497
|
+
for (const [value, count] of counts) {
|
|
6498
|
+
entries.push({
|
|
6499
|
+
value,
|
|
6500
|
+
count,
|
|
6501
|
+
percentage: count / total * 100
|
|
6502
|
+
});
|
|
6503
|
+
}
|
|
6504
|
+
entries.sort((a, b) => b.count - a.count);
|
|
6505
|
+
return entries;
|
|
6506
|
+
}
|
|
6507
|
+
function getChartableFieldIds(schema) {
|
|
6508
|
+
const chartable = [];
|
|
6509
|
+
const chartTypes = /* @__PURE__ */ new Set([
|
|
6510
|
+
"single_select",
|
|
6511
|
+
"dropdown",
|
|
6512
|
+
"country_select",
|
|
6513
|
+
"boolean",
|
|
6514
|
+
"consent",
|
|
6515
|
+
"multi_select",
|
|
6516
|
+
"checkbox_group",
|
|
6517
|
+
"rating",
|
|
6518
|
+
"nps",
|
|
6519
|
+
"opinion_scale",
|
|
6520
|
+
"pain_scale"
|
|
6521
|
+
]);
|
|
6522
|
+
for (const section of schema.sections) {
|
|
6523
|
+
for (const q of section.questions) {
|
|
6524
|
+
if (chartTypes.has(q.type)) {
|
|
6525
|
+
chartable.push({ id: q.id, label: q.label, type: q.type });
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
return chartable;
|
|
6530
|
+
}
|
|
6531
|
+
function ChartPanel({ schema, responses, onClose }) {
|
|
6532
|
+
const chartableFields = react.useMemo(() => getChartableFieldIds(schema), [schema]);
|
|
6533
|
+
const [selectedFieldId, setSelectedFieldId] = react.useState(chartableFields[0]?.id ?? "");
|
|
6534
|
+
const distribution = react.useMemo(() => {
|
|
6535
|
+
if (!selectedFieldId) return [];
|
|
6536
|
+
return computeFieldDistribution(responses, selectedFieldId);
|
|
6537
|
+
}, [responses, selectedFieldId]);
|
|
6538
|
+
const selectedField = chartableFields.find((f) => f.id === selectedFieldId);
|
|
6539
|
+
const maxCount = distribution.length > 0 ? Math.max(...distribution.map((d) => d.count)) : 1;
|
|
6540
|
+
if (chartableFields.length === 0) {
|
|
6541
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-6 border-b border-border bg-muted/30 text-center", children: [
|
|
6542
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "No chartable fields found in this schema." }),
|
|
6543
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "Charts work with select, dropdown, boolean, rating, and similar field types." })
|
|
6544
|
+
] });
|
|
6545
|
+
}
|
|
6546
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border bg-muted/30", children: [
|
|
6547
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
|
|
6548
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
6549
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: "Field Distribution" }),
|
|
6550
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6551
|
+
"select",
|
|
6552
|
+
{
|
|
6553
|
+
value: selectedFieldId,
|
|
6554
|
+
onChange: (e) => setSelectedFieldId(e.target.value),
|
|
6555
|
+
className: "text-xs bg-background border border-border rounded px-2 py-1 text-foreground",
|
|
6556
|
+
children: chartableFields.map((f) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: f.id, children: f.label }, f.id))
|
|
6557
|
+
}
|
|
6558
|
+
)
|
|
6559
|
+
] }),
|
|
6560
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
|
|
6561
|
+
] }),
|
|
6562
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
6563
|
+
distribution.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground text-center py-4", children: "No data for this field." }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
|
|
6564
|
+
distribution.slice(0, 10).map((entry) => /* @__PURE__ */ jsxRuntime.jsx(BarRow, { entry, maxCount }, entry.value)),
|
|
6565
|
+
distribution.length > 10 && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground mt-2", children: [
|
|
6566
|
+
"+",
|
|
6567
|
+
distribution.length - 10,
|
|
6568
|
+
" more values"
|
|
6569
|
+
] })
|
|
6570
|
+
] }),
|
|
6571
|
+
selectedField && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 text-[11px] text-muted-foreground", children: [
|
|
6572
|
+
responses.length,
|
|
6573
|
+
" response",
|
|
6574
|
+
responses.length !== 1 ? "s" : "",
|
|
6575
|
+
" \xB7 ",
|
|
6576
|
+
distribution.length,
|
|
6577
|
+
" unique value",
|
|
6578
|
+
distribution.length !== 1 ? "s" : ""
|
|
6579
|
+
] })
|
|
6580
|
+
] })
|
|
6581
|
+
] });
|
|
6582
|
+
}
|
|
6583
|
+
function BarRow({ entry, maxCount }) {
|
|
6584
|
+
const widthPct = maxCount > 0 ? entry.count / maxCount * 100 : 0;
|
|
6585
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
6586
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-24 shrink-0 text-xs text-foreground truncate", title: entry.value, children: entry.value }),
|
|
6587
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 h-5 bg-background rounded overflow-hidden border border-border/50", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6588
|
+
"div",
|
|
6589
|
+
{
|
|
6590
|
+
className: "h-full bg-primary/60 rounded transition-all",
|
|
6591
|
+
style: { width: `${widthPct}%` }
|
|
6592
|
+
}
|
|
6593
|
+
) }),
|
|
6594
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-16 shrink-0 text-xs text-muted-foreground text-right", children: [
|
|
6595
|
+
entry.count,
|
|
6596
|
+
" (",
|
|
6597
|
+
Math.round(entry.percentage),
|
|
6598
|
+
"%)"
|
|
6599
|
+
] })
|
|
6600
|
+
] });
|
|
6601
|
+
}
|
|
6602
|
+
function ConfirmDialog2({
|
|
6603
|
+
title,
|
|
6604
|
+
message,
|
|
6605
|
+
confirmLabel = "Confirm",
|
|
6606
|
+
cancelLabel = "Cancel",
|
|
6607
|
+
variant = "default",
|
|
6608
|
+
onConfirm,
|
|
6609
|
+
onCancel
|
|
6610
|
+
}) {
|
|
6611
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
6612
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6613
|
+
"div",
|
|
6614
|
+
{
|
|
6615
|
+
className: "absolute inset-0 bg-black/50",
|
|
6616
|
+
onClick: onCancel
|
|
6617
|
+
}
|
|
6618
|
+
),
|
|
6619
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative bg-background border border-border rounded-lg shadow-lg p-6 max-w-sm w-full mx-4", children: [
|
|
6620
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-foreground mb-2", children: title }),
|
|
6621
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mb-4", children: message }),
|
|
6622
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
6623
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", onClick: onCancel, children: cancelLabel }),
|
|
6624
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6625
|
+
fieldcraftReact.Button,
|
|
6626
|
+
{
|
|
6627
|
+
variant: variant === "destructive" ? "destructive" : "default",
|
|
6628
|
+
size: "sm",
|
|
6629
|
+
onClick: onConfirm,
|
|
6630
|
+
children: confirmLabel
|
|
6631
|
+
}
|
|
6632
|
+
)
|
|
6633
|
+
] })
|
|
6634
|
+
] })
|
|
6635
|
+
] });
|
|
6636
|
+
}
|
|
4614
6637
|
|
|
4615
6638
|
// src/response-viewer/export-utils.ts
|
|
4616
6639
|
function buildCsvContent(schema, responses, options = {}) {
|
|
@@ -4745,6 +6768,84 @@ function paginate(items, currentPage, pageSize) {
|
|
|
4745
6768
|
};
|
|
4746
6769
|
}
|
|
4747
6770
|
|
|
6771
|
+
// src/response-viewer/search-utils.ts
|
|
6772
|
+
function searchResponses(responses, query, schema) {
|
|
6773
|
+
if (!query.trim()) return responses;
|
|
6774
|
+
const lowerQuery = query.toLowerCase().trim();
|
|
6775
|
+
const questions = getSearchableQuestions(schema);
|
|
6776
|
+
return responses.filter((response) => {
|
|
6777
|
+
if (new Date(response.submittedAt).toLocaleString().toLowerCase().includes(lowerQuery)) {
|
|
6778
|
+
return true;
|
|
6779
|
+
}
|
|
6780
|
+
if (response.sessionToken?.toLowerCase().includes(lowerQuery)) {
|
|
6781
|
+
return true;
|
|
6782
|
+
}
|
|
6783
|
+
for (const q of questions) {
|
|
6784
|
+
const value = response.values[q.id];
|
|
6785
|
+
if (value == null) continue;
|
|
6786
|
+
const stringified = stringifyValue(value, q.type);
|
|
6787
|
+
if (stringified.toLowerCase().includes(lowerQuery)) {
|
|
6788
|
+
return true;
|
|
6789
|
+
}
|
|
6790
|
+
}
|
|
6791
|
+
if (response.totalScore != null && String(response.totalScore).includes(lowerQuery)) {
|
|
6792
|
+
return true;
|
|
6793
|
+
}
|
|
6794
|
+
return false;
|
|
6795
|
+
});
|
|
6796
|
+
}
|
|
6797
|
+
function getSearchableQuestions(schema) {
|
|
6798
|
+
const questions = [];
|
|
6799
|
+
for (const section of schema.sections) {
|
|
6800
|
+
for (const q of section.questions) {
|
|
6801
|
+
if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
|
|
6802
|
+
continue;
|
|
6803
|
+
}
|
|
6804
|
+
questions.push(q);
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6807
|
+
return questions;
|
|
6808
|
+
}
|
|
6809
|
+
function stringifyValue(value, type) {
|
|
6810
|
+
if (value == null) return "";
|
|
6811
|
+
switch (type) {
|
|
6812
|
+
case "vitals_entry": {
|
|
6813
|
+
if (typeof value !== "object" || value === null) break;
|
|
6814
|
+
const v = value;
|
|
6815
|
+
const parts = [];
|
|
6816
|
+
if (v.systolicBp && v.diastolicBp) parts.push(`BP ${v.systolicBp}/${v.diastolicBp}`);
|
|
6817
|
+
if (v.heartRate) parts.push(`HR ${v.heartRate}`);
|
|
6818
|
+
if (v.temperature) parts.push(`${v.temperature}`);
|
|
6819
|
+
if (v.oxygenSaturation) parts.push(`SpO2 ${v.oxygenSaturation}`);
|
|
6820
|
+
return parts.join(" ");
|
|
6821
|
+
}
|
|
6822
|
+
case "medication_list":
|
|
6823
|
+
if (Array.isArray(value)) {
|
|
6824
|
+
return value.map((m) => [m.name, m.dosage, m.frequency].filter(Boolean).join(" ")).join(" ");
|
|
6825
|
+
}
|
|
6826
|
+
break;
|
|
6827
|
+
case "allergy_list":
|
|
6828
|
+
if (Array.isArray(value)) {
|
|
6829
|
+
return value.map((a) => [a.allergen, a.severity].filter(Boolean).join(" ")).join(" ");
|
|
6830
|
+
}
|
|
6831
|
+
break;
|
|
6832
|
+
case "legal_name": {
|
|
6833
|
+
if (typeof value !== "object" || value === null) break;
|
|
6834
|
+
const n = value;
|
|
6835
|
+
return [n.first, n.middle, n.last].filter(Boolean).join(" ");
|
|
6836
|
+
}
|
|
6837
|
+
case "address": {
|
|
6838
|
+
if (typeof value !== "object" || value === null) break;
|
|
6839
|
+
const a = value;
|
|
6840
|
+
return [a.street, a.city, a.state, a.zip].filter(Boolean).join(" ");
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
6844
|
+
if (Array.isArray(value)) return value.map(String).join(" ");
|
|
6845
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
6846
|
+
return String(value);
|
|
6847
|
+
}
|
|
6848
|
+
|
|
4748
6849
|
// src/response-viewer/filter-utils.ts
|
|
4749
6850
|
function createEmptyFilterState() {
|
|
4750
6851
|
return { filters: [], dateRange: {} };
|
|
@@ -4843,7 +6944,11 @@ function ResponseViewerInner({
|
|
|
4843
6944
|
dateFormat,
|
|
4844
6945
|
columnLabels,
|
|
4845
6946
|
onExport,
|
|
4846
|
-
pageSize = 25
|
|
6947
|
+
pageSize = 25,
|
|
6948
|
+
onDelete,
|
|
6949
|
+
onBulkDelete,
|
|
6950
|
+
onBulkExport,
|
|
6951
|
+
selectable = false
|
|
4847
6952
|
}) {
|
|
4848
6953
|
const [viewMode, setViewMode] = react.useState("table");
|
|
4849
6954
|
const [selectedResponse, setSelectedResponse] = react.useState(null);
|
|
@@ -4851,6 +6956,11 @@ function ResponseViewerInner({
|
|
|
4851
6956
|
const [effectivePageSize, setEffectivePageSize] = react.useState(pageSize);
|
|
4852
6957
|
const [filterState, setFilterState] = react.useState(createEmptyFilterState);
|
|
4853
6958
|
const [showFilterPanel, setShowFilterPanel] = react.useState(false);
|
|
6959
|
+
const [searchQuery, setSearchQuery] = react.useState("");
|
|
6960
|
+
const [showStats, setShowStats] = react.useState(false);
|
|
6961
|
+
const [showCharts, setShowCharts] = react.useState(false);
|
|
6962
|
+
const [selectedIds, setSelectedIds] = react.useState(/* @__PURE__ */ new Set());
|
|
6963
|
+
const [confirmDialog, setConfirmDialog] = react.useState(null);
|
|
4854
6964
|
const [draftFieldId, setDraftFieldId] = react.useState("");
|
|
4855
6965
|
const [draftOperator, setDraftOperator] = react.useState("contains");
|
|
4856
6966
|
const [draftValue, setDraftValue] = react.useState("");
|
|
@@ -4861,9 +6971,18 @@ function ResponseViewerInner({
|
|
|
4861
6971
|
setEffectivePageSize(pageSize);
|
|
4862
6972
|
setCurrentPage(1);
|
|
4863
6973
|
}, [pageSize]);
|
|
6974
|
+
react.useEffect(() => {
|
|
6975
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
6976
|
+
}, [responses]);
|
|
6977
|
+
const searchedResponses = react.useMemo(
|
|
6978
|
+
() => searchResponses(responses, searchQuery, schema),
|
|
6979
|
+
[responses, searchQuery, schema]
|
|
6980
|
+
);
|
|
4864
6981
|
const isFiltered = hasActiveFilters(filterState);
|
|
4865
|
-
const filteredResponses = applyFilters(
|
|
6982
|
+
const filteredResponses = applyFilters(searchedResponses, filterState);
|
|
4866
6983
|
const pag = paginate(filteredResponses, currentPage, effectivePageSize);
|
|
6984
|
+
const questions = getAllQuestions2(schema);
|
|
6985
|
+
const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
|
|
4867
6986
|
function handleSelect(response) {
|
|
4868
6987
|
setSelectedResponse(response);
|
|
4869
6988
|
onResponseSelect?.(response);
|
|
@@ -4910,7 +7029,59 @@ function ResponseViewerInner({
|
|
|
4910
7029
|
setFilterState(createEmptyFilterState());
|
|
4911
7030
|
setCurrentPage(1);
|
|
4912
7031
|
}
|
|
4913
|
-
|
|
7032
|
+
function handleToggleSelect(token) {
|
|
7033
|
+
setSelectedIds((prev) => {
|
|
7034
|
+
const next = new Set(prev);
|
|
7035
|
+
if (next.has(token)) {
|
|
7036
|
+
next.delete(token);
|
|
7037
|
+
} else {
|
|
7038
|
+
next.add(token);
|
|
7039
|
+
}
|
|
7040
|
+
return next;
|
|
7041
|
+
});
|
|
7042
|
+
}
|
|
7043
|
+
function handleSelectAll() {
|
|
7044
|
+
const currentTokens = pag.pageItems.map((r) => r.sessionToken).filter(Boolean);
|
|
7045
|
+
const allSelected = currentTokens.every((t) => selectedIds.has(t));
|
|
7046
|
+
setSelectedIds((prev) => {
|
|
7047
|
+
const next = new Set(prev);
|
|
7048
|
+
if (allSelected) {
|
|
7049
|
+
currentTokens.forEach((t) => next.delete(t));
|
|
7050
|
+
} else {
|
|
7051
|
+
currentTokens.forEach((t) => next.add(t));
|
|
7052
|
+
}
|
|
7053
|
+
return next;
|
|
7054
|
+
});
|
|
7055
|
+
}
|
|
7056
|
+
function handleBulkDelete() {
|
|
7057
|
+
if (!onBulkDelete || selectedIds.size === 0) return;
|
|
7058
|
+
setConfirmDialog({
|
|
7059
|
+
title: "Delete selected responses",
|
|
7060
|
+
message: `Are you sure you want to delete ${selectedIds.size} response${selectedIds.size !== 1 ? "s" : ""}? This cannot be undone.`,
|
|
7061
|
+
onConfirm: () => {
|
|
7062
|
+
onBulkDelete(Array.from(selectedIds));
|
|
7063
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
7064
|
+
setConfirmDialog(null);
|
|
7065
|
+
}
|
|
7066
|
+
});
|
|
7067
|
+
}
|
|
7068
|
+
function handleBulkExport() {
|
|
7069
|
+
if (!onBulkExport || selectedIds.size === 0) return;
|
|
7070
|
+
const selected = responses.filter((r) => r.sessionToken && selectedIds.has(r.sessionToken));
|
|
7071
|
+
onBulkExport(selected);
|
|
7072
|
+
}
|
|
7073
|
+
function handleDeleteSingle(token) {
|
|
7074
|
+
if (!onDelete) return;
|
|
7075
|
+
setConfirmDialog({
|
|
7076
|
+
title: "Delete response",
|
|
7077
|
+
message: "Are you sure you want to delete this response? This cannot be undone.",
|
|
7078
|
+
onConfirm: () => {
|
|
7079
|
+
onDelete(token);
|
|
7080
|
+
setSelectedResponse(null);
|
|
7081
|
+
setConfirmDialog(null);
|
|
7082
|
+
}
|
|
7083
|
+
});
|
|
7084
|
+
}
|
|
4914
7085
|
function getFields(response) {
|
|
4915
7086
|
return questions.map((q) => ({
|
|
4916
7087
|
questionId: q.id,
|
|
@@ -4921,6 +7092,19 @@ function ResponseViewerInner({
|
|
|
4921
7092
|
}
|
|
4922
7093
|
const heightValue = typeof height === "number" ? `${height}px` : height;
|
|
4923
7094
|
const widthValue = typeof width === "number" ? `${width}px` : width;
|
|
7095
|
+
const countText = (() => {
|
|
7096
|
+
const hasSearch = searchQuery.trim().length > 0;
|
|
7097
|
+
if (hasSearch && isFiltered) {
|
|
7098
|
+
return `${filteredResponses.length} of ${responses.length} responses (searched + filtered)`;
|
|
7099
|
+
}
|
|
7100
|
+
if (hasSearch) {
|
|
7101
|
+
return `${searchedResponses.length} result${searchedResponses.length !== 1 ? "s" : ""} for "${searchQuery}"`;
|
|
7102
|
+
}
|
|
7103
|
+
if (isFiltered) {
|
|
7104
|
+
return `Showing ${filteredResponses.length} of ${responses.length} responses (filtered)`;
|
|
7105
|
+
}
|
|
7106
|
+
return `${responses.length} response${responses.length !== 1 ? "s" : ""}`;
|
|
7107
|
+
})();
|
|
4924
7108
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4925
7109
|
"div",
|
|
4926
7110
|
{
|
|
@@ -4928,7 +7112,20 @@ function ResponseViewerInner({
|
|
|
4928
7112
|
style: { height: heightValue, width: widthValue },
|
|
4929
7113
|
children: [
|
|
4930
7114
|
!selectedResponse && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-b border-border", children: [
|
|
4931
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7115
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7116
|
+
fieldcraftReact.Input,
|
|
7117
|
+
{
|
|
7118
|
+
type: "text",
|
|
7119
|
+
placeholder: "Search responses...",
|
|
7120
|
+
className: "h-7 text-xs w-40",
|
|
7121
|
+
value: searchQuery,
|
|
7122
|
+
onChange: (e) => {
|
|
7123
|
+
setSearchQuery(e.target.value);
|
|
7124
|
+
setCurrentPage(1);
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
),
|
|
7128
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground mr-auto", children: countText }),
|
|
4932
7129
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
4933
7130
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4934
7131
|
fieldcraftReact.Button,
|
|
@@ -4955,6 +7152,48 @@ function ResponseViewerInner({
|
|
|
4955
7152
|
onClick: () => setViewMode("card"),
|
|
4956
7153
|
children: "Cards"
|
|
4957
7154
|
}
|
|
7155
|
+
),
|
|
7156
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7157
|
+
fieldcraftReact.Button,
|
|
7158
|
+
{
|
|
7159
|
+
variant: viewMode === "timeline" ? "secondary" : "ghost",
|
|
7160
|
+
size: "sm",
|
|
7161
|
+
className: cn(
|
|
7162
|
+
"h-7 text-xs",
|
|
7163
|
+
viewMode === "timeline" && "font-semibold border border-ring"
|
|
7164
|
+
),
|
|
7165
|
+
onClick: () => setViewMode("timeline"),
|
|
7166
|
+
children: "Timeline"
|
|
7167
|
+
}
|
|
7168
|
+
)
|
|
7169
|
+
] }),
|
|
7170
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
|
|
7171
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
7172
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7173
|
+
fieldcraftReact.Button,
|
|
7174
|
+
{
|
|
7175
|
+
variant: showStats ? "secondary" : "ghost",
|
|
7176
|
+
size: "sm",
|
|
7177
|
+
className: cn(
|
|
7178
|
+
"h-7 text-xs",
|
|
7179
|
+
showStats && "font-semibold border border-ring"
|
|
7180
|
+
),
|
|
7181
|
+
onClick: () => setShowStats((v) => !v),
|
|
7182
|
+
children: "Stats"
|
|
7183
|
+
}
|
|
7184
|
+
),
|
|
7185
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7186
|
+
fieldcraftReact.Button,
|
|
7187
|
+
{
|
|
7188
|
+
variant: showCharts ? "secondary" : "ghost",
|
|
7189
|
+
size: "sm",
|
|
7190
|
+
className: cn(
|
|
7191
|
+
"h-7 text-xs",
|
|
7192
|
+
showCharts && "font-semibold border border-ring"
|
|
7193
|
+
),
|
|
7194
|
+
onClick: () => setShowCharts((v) => !v),
|
|
7195
|
+
children: "Charts"
|
|
7196
|
+
}
|
|
4958
7197
|
)
|
|
4959
7198
|
] }),
|
|
4960
7199
|
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
|
|
@@ -5020,6 +7259,26 @@ function ResponseViewerInner({
|
|
|
5020
7259
|
)
|
|
5021
7260
|
] })
|
|
5022
7261
|
] }),
|
|
7262
|
+
!selectedResponse && isSelectable && selectedIds.size > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-border bg-accent/50", children: [
|
|
7263
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-medium text-foreground", children: [
|
|
7264
|
+
selectedIds.size,
|
|
7265
|
+
" selected"
|
|
7266
|
+
] }),
|
|
7267
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
|
|
7268
|
+
onBulkExport && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", className: "h-6 text-xs", onClick: handleBulkExport, children: "Export Selected" }),
|
|
7269
|
+
onBulkDelete && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "destructive", size: "sm", className: "h-6 text-xs", onClick: handleBulkDelete, children: "Delete Selected" }),
|
|
7270
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7271
|
+
fieldcraftReact.Button,
|
|
7272
|
+
{
|
|
7273
|
+
variant: "ghost",
|
|
7274
|
+
size: "sm",
|
|
7275
|
+
className: "h-6 text-xs",
|
|
7276
|
+
onClick: () => setSelectedIds(/* @__PURE__ */ new Set()),
|
|
7277
|
+
children: "Clear"
|
|
7278
|
+
}
|
|
7279
|
+
)
|
|
7280
|
+
] })
|
|
7281
|
+
] }),
|
|
5023
7282
|
!selectedResponse && showFilterPanel && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-filter__panel px-3 py-2 border-b border-border bg-muted/50", children: [
|
|
5024
7283
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 mb-2", children: [
|
|
5025
7284
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Date:" }),
|
|
@@ -5148,19 +7407,47 @@ function ResponseViewerInner({
|
|
|
5148
7407
|
);
|
|
5149
7408
|
})
|
|
5150
7409
|
] }),
|
|
5151
|
-
|
|
7410
|
+
!selectedResponse && showStats && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7411
|
+
StatsPanel,
|
|
7412
|
+
{
|
|
7413
|
+
schema,
|
|
7414
|
+
responses: filteredResponses,
|
|
7415
|
+
onClose: () => setShowStats(false)
|
|
7416
|
+
}
|
|
7417
|
+
),
|
|
7418
|
+
!selectedResponse && showCharts && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7419
|
+
ChartPanel,
|
|
7420
|
+
{
|
|
7421
|
+
schema,
|
|
7422
|
+
responses: filteredResponses,
|
|
7423
|
+
onClose: () => setShowCharts(false)
|
|
7424
|
+
}
|
|
7425
|
+
),
|
|
7426
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("flex-1 min-w-0 overflow-auto", selectedResponse && "p-4"), children: selectedResponse ? /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5152
7427
|
ResponseDetail,
|
|
5153
7428
|
{
|
|
5154
7429
|
response: selectedResponse,
|
|
5155
7430
|
fields: getFields(selectedResponse),
|
|
5156
|
-
onBack: handleBack
|
|
7431
|
+
onBack: handleBack,
|
|
7432
|
+
onDelete: onDelete && selectedResponse.sessionToken ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
|
|
7433
|
+
}
|
|
7434
|
+
) }) : viewMode === "timeline" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
7435
|
+
TimelineView,
|
|
7436
|
+
{
|
|
7437
|
+
responses: filteredResponses,
|
|
7438
|
+
questions,
|
|
7439
|
+
onSelect: handleSelect
|
|
5157
7440
|
}
|
|
5158
7441
|
) : viewMode === "table" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
5159
7442
|
ResponseTable,
|
|
5160
7443
|
{
|
|
5161
7444
|
schema,
|
|
5162
7445
|
responses: pag.pageItems,
|
|
5163
|
-
onRowClick: handleSelect
|
|
7446
|
+
onRowClick: handleSelect,
|
|
7447
|
+
selectable: isSelectable,
|
|
7448
|
+
selectedIds,
|
|
7449
|
+
onToggleSelect: handleToggleSelect,
|
|
7450
|
+
onSelectAll: handleSelectAll
|
|
5164
7451
|
}
|
|
5165
7452
|
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 p-3 grid-cols-[repeat(auto-fill,minmax(280px,1fr))]", children: [
|
|
5166
7453
|
pag.pageItems.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -5168,13 +7455,16 @@ function ResponseViewerInner({
|
|
|
5168
7455
|
{
|
|
5169
7456
|
response,
|
|
5170
7457
|
fields: getFields(response),
|
|
5171
|
-
onClick: () => handleSelect(response)
|
|
7458
|
+
onClick: () => handleSelect(response),
|
|
7459
|
+
selectable: isSelectable,
|
|
7460
|
+
selected: !!(response.sessionToken && selectedIds.has(response.sessionToken)),
|
|
7461
|
+
onToggleSelect: response.sessionToken ? () => handleToggleSelect(response.sessionToken) : void 0
|
|
5172
7462
|
},
|
|
5173
7463
|
response.sessionToken || idx
|
|
5174
7464
|
)),
|
|
5175
|
-
filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered ? "No matching responses" : "No responses yet" })
|
|
7465
|
+
filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered || searchQuery.trim() ? "No matching responses" : "No responses yet" })
|
|
5176
7466
|
] }) }),
|
|
5177
|
-
!selectedResponse && pag.showPagination && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-t border-border", children: [
|
|
7467
|
+
!selectedResponse && viewMode !== "timeline" && pag.showPagination && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2 border-t border-border", children: [
|
|
5178
7468
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination__info text-xs text-muted-foreground mr-auto", children: [
|
|
5179
7469
|
"Showing ",
|
|
5180
7470
|
pag.startItem,
|
|
@@ -5228,7 +7518,18 @@ function ResponseViewerInner({
|
|
|
5228
7518
|
}
|
|
5229
7519
|
)
|
|
5230
7520
|
] })
|
|
5231
|
-
] })
|
|
7521
|
+
] }),
|
|
7522
|
+
confirmDialog && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7523
|
+
ConfirmDialog2,
|
|
7524
|
+
{
|
|
7525
|
+
title: confirmDialog.title,
|
|
7526
|
+
message: confirmDialog.message,
|
|
7527
|
+
confirmLabel: "Delete",
|
|
7528
|
+
variant: "destructive",
|
|
7529
|
+
onConfirm: confirmDialog.onConfirm,
|
|
7530
|
+
onCancel: () => setConfirmDialog(null)
|
|
7531
|
+
}
|
|
7532
|
+
)
|
|
5232
7533
|
]
|
|
5233
7534
|
}
|
|
5234
7535
|
);
|
|
@@ -5301,7 +7602,537 @@ var PREVIEW_SCHEMA = {
|
|
|
5301
7602
|
],
|
|
5302
7603
|
submitAction: { type: "adapter" }
|
|
5303
7604
|
};
|
|
7605
|
+
|
|
7606
|
+
// src/theme-editor/css-utils.ts
|
|
7607
|
+
var COLOR_MAP = {
|
|
7608
|
+
primary: "--fc-primary",
|
|
7609
|
+
primaryForeground: "--fc-primary-foreground",
|
|
7610
|
+
secondary: "--fc-secondary",
|
|
7611
|
+
secondaryForeground: "--fc-secondary-foreground",
|
|
7612
|
+
error: "--fc-error",
|
|
7613
|
+
errorForeground: "--fc-error-foreground",
|
|
7614
|
+
warning: "--fc-warning",
|
|
7615
|
+
success: "--fc-success",
|
|
7616
|
+
surface: "--fc-surface",
|
|
7617
|
+
background: "--fc-background",
|
|
7618
|
+
text: "--fc-text",
|
|
7619
|
+
textMuted: "--fc-text-muted",
|
|
7620
|
+
textDisabled: "--fc-text-disabled",
|
|
7621
|
+
border: "--fc-border",
|
|
7622
|
+
borderFocus: "--fc-border-focus",
|
|
7623
|
+
inputBackground: "--fc-input-background"
|
|
7624
|
+
};
|
|
7625
|
+
var TYPOGRAPHY_MAP = {
|
|
7626
|
+
fontFamily: "--fc-font-family",
|
|
7627
|
+
scale: "--fc-scale",
|
|
7628
|
+
questionSize: "--fc-question-size",
|
|
7629
|
+
labelSize: "--fc-label-size",
|
|
7630
|
+
helpTextSize: "--fc-help-text-size",
|
|
7631
|
+
bodySize: "--fc-body-size"
|
|
7632
|
+
};
|
|
7633
|
+
var SHAPE_MAP = {
|
|
7634
|
+
radius: "--fc-radius",
|
|
7635
|
+
inputRadius: "--fc-input-radius",
|
|
7636
|
+
buttonRadius: "--fc-button-radius",
|
|
7637
|
+
cardRadius: "--fc-card-radius"
|
|
7638
|
+
};
|
|
7639
|
+
var SPACING_MAP = {
|
|
7640
|
+
base: "--fc-spacing-base",
|
|
7641
|
+
sectionGap: "--fc-section-gap",
|
|
7642
|
+
fieldGap: "--fc-field-gap",
|
|
7643
|
+
inputPaddingX: "--fc-input-padding-x",
|
|
7644
|
+
inputPaddingY: "--fc-input-padding-y"
|
|
7645
|
+
};
|
|
7646
|
+
var LAYOUT_MAP = {
|
|
7647
|
+
maxWidth: "--fc-max-width",
|
|
7648
|
+
alignment: "--fc-alignment",
|
|
7649
|
+
progressPosition: "--fc-progress-position",
|
|
7650
|
+
sectionLayout: "--fc-section-layout"
|
|
7651
|
+
};
|
|
7652
|
+
function themeToCss(theme) {
|
|
7653
|
+
const lines = [];
|
|
7654
|
+
function addSection2(obj, map, comment) {
|
|
7655
|
+
if (!obj) return;
|
|
7656
|
+
const entries = [];
|
|
7657
|
+
for (const [key, cssVar] of Object.entries(map)) {
|
|
7658
|
+
const val = obj[key];
|
|
7659
|
+
if (val == null) continue;
|
|
7660
|
+
if (typeof val === "number") {
|
|
7661
|
+
entries.push(` ${cssVar}: ${val}px;`);
|
|
7662
|
+
} else {
|
|
7663
|
+
entries.push(` ${cssVar}: ${String(val)};`);
|
|
7664
|
+
}
|
|
7665
|
+
}
|
|
7666
|
+
if (entries.length > 0) {
|
|
7667
|
+
lines.push(` /* ${comment} */`);
|
|
7668
|
+
lines.push(...entries);
|
|
7669
|
+
lines.push("");
|
|
7670
|
+
}
|
|
7671
|
+
}
|
|
7672
|
+
lines.push(":root {");
|
|
7673
|
+
addSection2(theme.colors, COLOR_MAP, "Colors");
|
|
7674
|
+
addSection2(theme.typography, TYPOGRAPHY_MAP, "Typography");
|
|
7675
|
+
addSection2(theme.shape, SHAPE_MAP, "Shape");
|
|
7676
|
+
addSection2(theme.spacing, SPACING_MAP, "Spacing");
|
|
7677
|
+
addSection2(theme.layout, LAYOUT_MAP, "Layout");
|
|
7678
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") {
|
|
7679
|
+
lines.pop();
|
|
7680
|
+
}
|
|
7681
|
+
lines.push("}");
|
|
7682
|
+
return lines.join("\n") + "\n";
|
|
7683
|
+
}
|
|
7684
|
+
var REVERSE_MAP = {};
|
|
7685
|
+
function buildReverseMap() {
|
|
7686
|
+
if (Object.keys(REVERSE_MAP).length > 0) return;
|
|
7687
|
+
const sections = [
|
|
7688
|
+
["colors", COLOR_MAP],
|
|
7689
|
+
["typography", TYPOGRAPHY_MAP],
|
|
7690
|
+
["shape", SHAPE_MAP],
|
|
7691
|
+
["spacing", SPACING_MAP],
|
|
7692
|
+
["layout", LAYOUT_MAP]
|
|
7693
|
+
];
|
|
7694
|
+
for (const [sectionKey, map] of sections) {
|
|
7695
|
+
for (const [propKey, cssVar] of Object.entries(map)) {
|
|
7696
|
+
REVERSE_MAP[cssVar] = [sectionKey, propKey];
|
|
7697
|
+
}
|
|
7698
|
+
}
|
|
7699
|
+
}
|
|
7700
|
+
var NUMERIC_PROPS = /* @__PURE__ */ new Set([
|
|
7701
|
+
"base",
|
|
7702
|
+
"sectionGap",
|
|
7703
|
+
"fieldGap",
|
|
7704
|
+
"inputPaddingX",
|
|
7705
|
+
"inputPaddingY"
|
|
7706
|
+
]);
|
|
7707
|
+
function cssToTheme(css) {
|
|
7708
|
+
buildReverseMap();
|
|
7709
|
+
const theme = {};
|
|
7710
|
+
const varRegex = /(--fc-[\w-]+)\s*:\s*([^;]+);/g;
|
|
7711
|
+
let match;
|
|
7712
|
+
while ((match = varRegex.exec(css)) !== null) {
|
|
7713
|
+
const cssVar = match[1];
|
|
7714
|
+
let value = match[2].trim();
|
|
7715
|
+
const mapping = REVERSE_MAP[cssVar];
|
|
7716
|
+
if (!mapping) continue;
|
|
7717
|
+
const [sectionKey, propKey] = mapping;
|
|
7718
|
+
if (NUMERIC_PROPS.has(propKey)) {
|
|
7719
|
+
const num = parseFloat(value);
|
|
7720
|
+
if (!isNaN(num)) {
|
|
7721
|
+
value = num;
|
|
7722
|
+
}
|
|
7723
|
+
}
|
|
7724
|
+
if (!theme[sectionKey]) {
|
|
7725
|
+
theme[sectionKey] = {};
|
|
7726
|
+
}
|
|
7727
|
+
theme[sectionKey][propKey] = value;
|
|
7728
|
+
}
|
|
7729
|
+
return theme;
|
|
7730
|
+
}
|
|
7731
|
+
function exportCssFile(theme, filename = "fieldcraft-theme.css") {
|
|
7732
|
+
const css = themeToCss(theme);
|
|
7733
|
+
const blob = new Blob([css], { type: "text/css" });
|
|
7734
|
+
const url = URL.createObjectURL(blob);
|
|
7735
|
+
const a = document.createElement("a");
|
|
7736
|
+
a.href = url;
|
|
7737
|
+
a.download = filename;
|
|
7738
|
+
a.click();
|
|
7739
|
+
URL.revokeObjectURL(url);
|
|
7740
|
+
}
|
|
7741
|
+
function importCssFile(baseTheme, onImport) {
|
|
7742
|
+
const input = document.createElement("input");
|
|
7743
|
+
input.type = "file";
|
|
7744
|
+
input.accept = ".css";
|
|
7745
|
+
input.onchange = () => {
|
|
7746
|
+
const file = input.files?.[0];
|
|
7747
|
+
if (!file) return;
|
|
7748
|
+
const reader = new FileReader();
|
|
7749
|
+
reader.onload = () => {
|
|
7750
|
+
const cssText = reader.result;
|
|
7751
|
+
const partial = cssToTheme(cssText);
|
|
7752
|
+
const merged = deepMerge(baseTheme, partial);
|
|
7753
|
+
onImport(merged);
|
|
7754
|
+
};
|
|
7755
|
+
reader.readAsText(file);
|
|
7756
|
+
};
|
|
7757
|
+
input.click();
|
|
7758
|
+
}
|
|
7759
|
+
function deepMerge(base, partial) {
|
|
7760
|
+
const result = { ...base };
|
|
7761
|
+
for (const key of Object.keys(partial)) {
|
|
7762
|
+
const partialSection = partial[key];
|
|
7763
|
+
if (partialSection && typeof partialSection === "object") {
|
|
7764
|
+
result[key] = {
|
|
7765
|
+
...base[key],
|
|
7766
|
+
...partialSection
|
|
7767
|
+
};
|
|
7768
|
+
}
|
|
7769
|
+
}
|
|
7770
|
+
return result;
|
|
7771
|
+
}
|
|
7772
|
+
|
|
7773
|
+
// src/theme-editor/palette-generator.ts
|
|
7774
|
+
function generatePalette(baseHex) {
|
|
7775
|
+
const [h, s, l] = hexToHsl(baseHex);
|
|
7776
|
+
const secH = (h + 180) % 360;
|
|
7777
|
+
const isLightBase = l > 50;
|
|
7778
|
+
return {
|
|
7779
|
+
// Primary
|
|
7780
|
+
primary: hslToHex(h, s, clamp(l, 30, 60)),
|
|
7781
|
+
primaryForeground: isLightBase ? "#ffffff" : "#ffffff",
|
|
7782
|
+
// Secondary (complementary)
|
|
7783
|
+
secondary: hslToHex(secH, Math.max(s - 15, 10), clamp(l, 35, 55)),
|
|
7784
|
+
secondaryForeground: "#ffffff",
|
|
7785
|
+
// Surfaces
|
|
7786
|
+
surface: hslToHex(h, Math.max(s - 35, 3), 97),
|
|
7787
|
+
background: "#F4F7F8",
|
|
7788
|
+
inputBackground: hslToHex(h, Math.max(s - 40, 2), 99),
|
|
7789
|
+
// Text
|
|
7790
|
+
text: hslToHex(h, Math.max(s - 30, 5), 12),
|
|
7791
|
+
textMuted: hslToHex(h, Math.max(s - 30, 5), 45),
|
|
7792
|
+
textDisabled: hslToHex(h, Math.max(s - 35, 3), 65),
|
|
7793
|
+
// Borders
|
|
7794
|
+
border: hslToHex(h, Math.max(s - 30, 5), 85),
|
|
7795
|
+
borderFocus: hslToHex(h, s, clamp(l, 35, 55)),
|
|
7796
|
+
// Semantic — Drafting Teal palette
|
|
7797
|
+
error: "#B04A3C",
|
|
7798
|
+
errorForeground: "#FFFFFF",
|
|
7799
|
+
warning: "#C98A2E",
|
|
7800
|
+
success: "#2E7D5B"
|
|
7801
|
+
};
|
|
7802
|
+
}
|
|
7803
|
+
function applyPaletteToTheme(theme, palette) {
|
|
7804
|
+
return {
|
|
7805
|
+
...theme,
|
|
7806
|
+
colors: {
|
|
7807
|
+
...theme.colors,
|
|
7808
|
+
...palette
|
|
7809
|
+
}
|
|
7810
|
+
};
|
|
7811
|
+
}
|
|
7812
|
+
function hexToHsl(hex) {
|
|
7813
|
+
const rgb = hexToRgb(hex);
|
|
7814
|
+
const r = rgb[0] / 255;
|
|
7815
|
+
const g = rgb[1] / 255;
|
|
7816
|
+
const b = rgb[2] / 255;
|
|
7817
|
+
const max = Math.max(r, g, b);
|
|
7818
|
+
const min = Math.min(r, g, b);
|
|
7819
|
+
const l = (max + min) / 2;
|
|
7820
|
+
if (max === min) {
|
|
7821
|
+
return [0, 0, Math.round(l * 100)];
|
|
7822
|
+
}
|
|
7823
|
+
const d = max - min;
|
|
7824
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
7825
|
+
let h = 0;
|
|
7826
|
+
if (max === r) {
|
|
7827
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
7828
|
+
} else if (max === g) {
|
|
7829
|
+
h = ((b - r) / d + 2) / 6;
|
|
7830
|
+
} else {
|
|
7831
|
+
h = ((r - g) / d + 4) / 6;
|
|
7832
|
+
}
|
|
7833
|
+
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
7834
|
+
}
|
|
7835
|
+
function hslToHex(h, s, l) {
|
|
7836
|
+
const sNorm = s / 100;
|
|
7837
|
+
const lNorm = l / 100;
|
|
7838
|
+
const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
|
|
7839
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
7840
|
+
const m = lNorm - c / 2;
|
|
7841
|
+
let r = 0, g = 0, b = 0;
|
|
7842
|
+
if (h < 60) {
|
|
7843
|
+
r = c;
|
|
7844
|
+
g = x;
|
|
7845
|
+
b = 0;
|
|
7846
|
+
} else if (h < 120) {
|
|
7847
|
+
r = x;
|
|
7848
|
+
g = c;
|
|
7849
|
+
b = 0;
|
|
7850
|
+
} else if (h < 180) {
|
|
7851
|
+
r = 0;
|
|
7852
|
+
g = c;
|
|
7853
|
+
b = x;
|
|
7854
|
+
} else if (h < 240) {
|
|
7855
|
+
r = 0;
|
|
7856
|
+
g = x;
|
|
7857
|
+
b = c;
|
|
7858
|
+
} else if (h < 300) {
|
|
7859
|
+
r = x;
|
|
7860
|
+
g = 0;
|
|
7861
|
+
b = c;
|
|
7862
|
+
} else {
|
|
7863
|
+
r = c;
|
|
7864
|
+
g = 0;
|
|
7865
|
+
b = x;
|
|
7866
|
+
}
|
|
7867
|
+
return rgbToHex(
|
|
7868
|
+
Math.round((r + m) * 255),
|
|
7869
|
+
Math.round((g + m) * 255),
|
|
7870
|
+
Math.round((b + m) * 255)
|
|
7871
|
+
);
|
|
7872
|
+
}
|
|
7873
|
+
function hexToRgb(hex) {
|
|
7874
|
+
const cleaned = hex.replace("#", "");
|
|
7875
|
+
const full = cleaned.length === 3 ? cleaned[0] + cleaned[0] + cleaned[1] + cleaned[1] + cleaned[2] + cleaned[2] : cleaned;
|
|
7876
|
+
return [
|
|
7877
|
+
parseInt(full.substring(0, 2), 16),
|
|
7878
|
+
parseInt(full.substring(2, 4), 16),
|
|
7879
|
+
parseInt(full.substring(4, 6), 16)
|
|
7880
|
+
];
|
|
7881
|
+
}
|
|
7882
|
+
function rgbToHex(r, g, b) {
|
|
7883
|
+
return "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
7884
|
+
}
|
|
7885
|
+
function clamp(value, min, max) {
|
|
7886
|
+
return Math.max(min, Math.min(max, value));
|
|
7887
|
+
}
|
|
7888
|
+
var SWATCH_KEYS = [
|
|
7889
|
+
{ key: "primary", label: "Primary" },
|
|
7890
|
+
{ key: "secondary", label: "Secondary" },
|
|
7891
|
+
{ key: "surface", label: "Surface" },
|
|
7892
|
+
{ key: "background", label: "Background" },
|
|
7893
|
+
{ key: "text", label: "Text" },
|
|
7894
|
+
{ key: "textMuted", label: "Muted" },
|
|
7895
|
+
{ key: "border", label: "Border" },
|
|
7896
|
+
{ key: "error", label: "Error" },
|
|
7897
|
+
{ key: "warning", label: "Warning" },
|
|
7898
|
+
{ key: "success", label: "Success" }
|
|
7899
|
+
];
|
|
7900
|
+
function PaletteGenerator({ theme, onApply, onClose }) {
|
|
7901
|
+
const [baseColor, setBaseColor] = react.useState(theme.colors?.primary ?? "#3b82f6");
|
|
7902
|
+
const palette = react.useMemo(() => generatePalette(baseColor), [baseColor]);
|
|
7903
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette", children: [
|
|
7904
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__header", children: [
|
|
7905
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-palette__title", children: "Generate Palette" }),
|
|
7906
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "fcte-btn fcte-btn--secondary fcte-btn--sm", onClick: onClose, children: "Close" })
|
|
7907
|
+
] }),
|
|
7908
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__body", children: [
|
|
7909
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__picker", children: [
|
|
7910
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "fcte-palette__label", children: "Base Color" }),
|
|
7911
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__input-row", children: [
|
|
7912
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7913
|
+
"input",
|
|
7914
|
+
{
|
|
7915
|
+
type: "color",
|
|
7916
|
+
value: baseColor,
|
|
7917
|
+
onChange: (e) => setBaseColor(e.target.value),
|
|
7918
|
+
className: "fcte-field__swatch"
|
|
7919
|
+
}
|
|
7920
|
+
),
|
|
7921
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7922
|
+
"input",
|
|
7923
|
+
{
|
|
7924
|
+
type: "text",
|
|
7925
|
+
value: baseColor,
|
|
7926
|
+
onChange: (e) => setBaseColor(e.target.value),
|
|
7927
|
+
className: "fcte-field__text",
|
|
7928
|
+
spellCheck: false
|
|
7929
|
+
}
|
|
7930
|
+
)
|
|
7931
|
+
] })
|
|
7932
|
+
] }),
|
|
7933
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatches", children: SWATCH_KEYS.map(({ key, label }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__swatch", children: [
|
|
7934
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7935
|
+
"div",
|
|
7936
|
+
{
|
|
7937
|
+
className: "fcte-palette__swatch-color",
|
|
7938
|
+
style: { backgroundColor: palette[key] }
|
|
7939
|
+
}
|
|
7940
|
+
),
|
|
7941
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatch-label", children: label }),
|
|
7942
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatch-hex", children: palette[key] })
|
|
7943
|
+
] }, key)) }),
|
|
7944
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7945
|
+
"button",
|
|
7946
|
+
{
|
|
7947
|
+
className: "fcte-btn fcte-btn--primary",
|
|
7948
|
+
onClick: () => onApply(applyPaletteToTheme(theme, palette)),
|
|
7949
|
+
children: "Apply to Theme"
|
|
7950
|
+
}
|
|
7951
|
+
)
|
|
7952
|
+
] })
|
|
7953
|
+
] });
|
|
7954
|
+
}
|
|
7955
|
+
|
|
7956
|
+
// src/theme-editor/presets.ts
|
|
7957
|
+
var draftingTealPreset = {
|
|
7958
|
+
colors: {
|
|
7959
|
+
primary: "#1F6B6E",
|
|
7960
|
+
primaryForeground: "#FFFFFF",
|
|
7961
|
+
secondary: "#B9D1CF",
|
|
7962
|
+
secondaryForeground: "#12222A",
|
|
7963
|
+
error: "#B04A3C",
|
|
7964
|
+
errorForeground: "#FFFFFF",
|
|
7965
|
+
warning: "#C98A2E",
|
|
7966
|
+
success: "#2E7D5B",
|
|
7967
|
+
surface: "#FFFFFF",
|
|
7968
|
+
background: "#F4F7F8",
|
|
7969
|
+
text: "#12222A",
|
|
7970
|
+
textMuted: "#6A7B85",
|
|
7971
|
+
textDisabled: "#B9D1CF",
|
|
7972
|
+
border: "#DCE4E8",
|
|
7973
|
+
borderFocus: "#1F6B6E",
|
|
7974
|
+
inputBackground: "#FFFFFF"
|
|
7975
|
+
},
|
|
7976
|
+
typography: {
|
|
7977
|
+
fontFamily: "'Space Grotesk', 'IBM Plex Sans', system-ui, sans-serif",
|
|
7978
|
+
scale: "comfortable",
|
|
7979
|
+
questionSize: "1.1875rem",
|
|
7980
|
+
labelSize: "0.8125rem",
|
|
7981
|
+
helpTextSize: "0.8125rem",
|
|
7982
|
+
bodySize: "0.9375rem"
|
|
7983
|
+
},
|
|
7984
|
+
shape: {
|
|
7985
|
+
radius: "none",
|
|
7986
|
+
inputRadius: "0px",
|
|
7987
|
+
buttonRadius: "0px",
|
|
7988
|
+
cardRadius: "0px"
|
|
7989
|
+
},
|
|
7990
|
+
spacing: {
|
|
7991
|
+
base: 16,
|
|
7992
|
+
sectionGap: 32,
|
|
7993
|
+
fieldGap: 24,
|
|
7994
|
+
inputPaddingX: 12,
|
|
7995
|
+
inputPaddingY: 10
|
|
7996
|
+
},
|
|
7997
|
+
layout: {
|
|
7998
|
+
maxWidth: "640px",
|
|
7999
|
+
alignment: "left",
|
|
8000
|
+
progressPosition: "top",
|
|
8001
|
+
sectionLayout: "flat"
|
|
8002
|
+
}
|
|
8003
|
+
};
|
|
8004
|
+
var COMPARISON_PRESETS = {
|
|
8005
|
+
"drafting-teal": { label: "Drafting Teal", theme: draftingTealPreset },
|
|
8006
|
+
clean: { label: "Clean", theme: fieldcraftReact.cleanPreset },
|
|
8007
|
+
dark: { label: "Dark", theme: fieldcraftReact.darkPreset },
|
|
8008
|
+
modern: { label: "Modern", theme: fieldcraftReact.modernPreset },
|
|
8009
|
+
"high-contrast": { label: "High Contrast", theme: fieldcraftReact.highContrastPreset },
|
|
8010
|
+
clinical: { label: "Clinical", theme: fieldcraftReact.clinicalPreset },
|
|
8011
|
+
playful: { label: "Playful", theme: fieldcraftReact.playfulPreset }
|
|
8012
|
+
};
|
|
8013
|
+
function ThemeComparison({ currentTheme, onClose }) {
|
|
8014
|
+
const [compareKey, setCompareKey] = react.useState("clean");
|
|
8015
|
+
const compareTheme = COMPARISON_PRESETS[compareKey]?.theme ?? fieldcraftReact.cleanPreset;
|
|
8016
|
+
const diffs = getColorDiffs(currentTheme, compareTheme);
|
|
8017
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare", children: [
|
|
8018
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__header", children: [
|
|
8019
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__title", children: "Theme Comparison" }),
|
|
8020
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__controls", children: [
|
|
8021
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "fcte-compare__label", children: "Compare with:" }),
|
|
8022
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8023
|
+
"select",
|
|
8024
|
+
{
|
|
8025
|
+
value: compareKey,
|
|
8026
|
+
onChange: (e) => setCompareKey(e.target.value),
|
|
8027
|
+
className: "fcte-toolbar__preset",
|
|
8028
|
+
children: Object.entries(COMPARISON_PRESETS).map(([key, { label }]) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: key, children: label }, key))
|
|
8029
|
+
}
|
|
8030
|
+
),
|
|
8031
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "fcte-btn fcte-btn--secondary fcte-btn--sm", onClick: onClose, children: "Close" })
|
|
8032
|
+
] })
|
|
8033
|
+
] }),
|
|
8034
|
+
diffs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diffs", children: [
|
|
8035
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fcte-compare__diff-count", children: [
|
|
8036
|
+
diffs.length,
|
|
8037
|
+
" color",
|
|
8038
|
+
diffs.length !== 1 ? "s" : "",
|
|
8039
|
+
" differ"
|
|
8040
|
+
] }),
|
|
8041
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-list", children: [
|
|
8042
|
+
diffs.slice(0, 8).map((d) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-item", children: [
|
|
8043
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__diff-label", children: d.key }),
|
|
8044
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-swatches", children: [
|
|
8045
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8046
|
+
"div",
|
|
8047
|
+
{
|
|
8048
|
+
className: "fcte-compare__diff-swatch",
|
|
8049
|
+
style: { backgroundColor: d.current },
|
|
8050
|
+
title: `Current: ${d.current}`
|
|
8051
|
+
}
|
|
8052
|
+
),
|
|
8053
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__diff-arrow", children: "\u2192" }),
|
|
8054
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8055
|
+
"div",
|
|
8056
|
+
{
|
|
8057
|
+
className: "fcte-compare__diff-swatch",
|
|
8058
|
+
style: { backgroundColor: d.compare },
|
|
8059
|
+
title: `${COMPARISON_PRESETS[compareKey]?.label}: ${d.compare}`
|
|
8060
|
+
}
|
|
8061
|
+
)
|
|
8062
|
+
] })
|
|
8063
|
+
] }, d.key)),
|
|
8064
|
+
diffs.length > 8 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fcte-compare__diff-more", children: [
|
|
8065
|
+
"+",
|
|
8066
|
+
diffs.length - 8,
|
|
8067
|
+
" more"
|
|
8068
|
+
] })
|
|
8069
|
+
] })
|
|
8070
|
+
] }),
|
|
8071
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panels", children: [
|
|
8072
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panel", children: [
|
|
8073
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-label", children: "Current Theme" }),
|
|
8074
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-preview", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
8075
|
+
fieldcraftReact.FormEngineRenderer,
|
|
8076
|
+
{
|
|
8077
|
+
schema: PREVIEW_SCHEMA,
|
|
8078
|
+
theme: currentTheme,
|
|
8079
|
+
onSubmit: () => {
|
|
8080
|
+
}
|
|
8081
|
+
}
|
|
8082
|
+
) })
|
|
8083
|
+
] }),
|
|
8084
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panel", children: [
|
|
8085
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-label", children: COMPARISON_PRESETS[compareKey]?.label ?? "Preset" }),
|
|
8086
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-preview", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
8087
|
+
fieldcraftReact.FormEngineRenderer,
|
|
8088
|
+
{
|
|
8089
|
+
schema: PREVIEW_SCHEMA,
|
|
8090
|
+
theme: compareTheme,
|
|
8091
|
+
onSubmit: () => {
|
|
8092
|
+
}
|
|
8093
|
+
}
|
|
8094
|
+
) })
|
|
8095
|
+
] })
|
|
8096
|
+
] })
|
|
8097
|
+
] });
|
|
8098
|
+
}
|
|
8099
|
+
function getColorDiffs(a, b) {
|
|
8100
|
+
const diffs = [];
|
|
8101
|
+
const aColors = a.colors ?? {};
|
|
8102
|
+
const bColors = b.colors ?? {};
|
|
8103
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(aColors), ...Object.keys(bColors)]);
|
|
8104
|
+
for (const key of allKeys) {
|
|
8105
|
+
const aVal = aColors[key] ?? "";
|
|
8106
|
+
const bVal = bColors[key] ?? "";
|
|
8107
|
+
if (aVal.toLowerCase() !== bVal.toLowerCase() && (aVal || bVal)) {
|
|
8108
|
+
diffs.push({ key, current: aVal || "(unset)", compare: bVal || "(unset)" });
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
8111
|
+
return diffs;
|
|
8112
|
+
}
|
|
8113
|
+
var ThemeCtx2 = react.createContext({});
|
|
8114
|
+
function themeToCssVars2(theme) {
|
|
8115
|
+
const vars = {};
|
|
8116
|
+
if (theme.background) vars["--fcte-bg"] = theme.background;
|
|
8117
|
+
if (theme.surface) vars["--fcte-surface"] = theme.surface;
|
|
8118
|
+
if (theme.surfaceHover) vars["--fcte-surface-hover"] = theme.surfaceHover;
|
|
8119
|
+
if (theme.text) vars["--fcte-text"] = theme.text;
|
|
8120
|
+
if (theme.textMuted) vars["--fcte-text-muted"] = theme.textMuted;
|
|
8121
|
+
if (theme.textDim) vars["--fcte-text-dim"] = theme.textDim;
|
|
8122
|
+
if (theme.border) vars["--fcte-border"] = theme.border;
|
|
8123
|
+
if (theme.borderStrong) vars["--fcte-border-strong"] = theme.borderStrong;
|
|
8124
|
+
if (theme.inputBackground) vars["--fcte-input-bg"] = theme.inputBackground;
|
|
8125
|
+
if (theme.accent) vars["--fcte-accent"] = theme.accent;
|
|
8126
|
+
if (theme.accentForeground) vars["--fcte-accent-text"] = theme.accentForeground;
|
|
8127
|
+
return vars;
|
|
8128
|
+
}
|
|
8129
|
+
function ThemeEditorThemeProvider({ theme, children }) {
|
|
8130
|
+
const resolved = theme ?? {};
|
|
8131
|
+
const cssVars = react.useMemo(() => themeToCssVars2(resolved), [resolved]);
|
|
8132
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ThemeCtx2.Provider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fcte-root": "", style: cssVars, className: "fcte-provider-root", children }) });
|
|
8133
|
+
}
|
|
5304
8134
|
var PRESETS = {
|
|
8135
|
+
"drafting-teal": draftingTealPreset,
|
|
5305
8136
|
clean: fieldcraftReact.cleanPreset,
|
|
5306
8137
|
dark: fieldcraftReact.darkPreset,
|
|
5307
8138
|
modern: fieldcraftReact.modernPreset,
|
|
@@ -5431,15 +8262,18 @@ function ThemeEditorInner({
|
|
|
5431
8262
|
initialTheme,
|
|
5432
8263
|
onChange,
|
|
5433
8264
|
onSave,
|
|
8265
|
+
theme: chromeTheme,
|
|
5434
8266
|
height,
|
|
5435
8267
|
width,
|
|
5436
8268
|
className,
|
|
5437
8269
|
toolbarExtra,
|
|
5438
8270
|
showPreview = true
|
|
5439
8271
|
}) {
|
|
5440
|
-
const [theme, setTheme] = react.useState(initialTheme ??
|
|
8272
|
+
const [theme, setTheme] = react.useState(initialTheme ?? draftingTealPreset);
|
|
5441
8273
|
const [activeSection, setActiveSection] = react.useState("colors");
|
|
5442
|
-
const [presetKey, setPresetKey] = react.useState("custom");
|
|
8274
|
+
const [presetKey, setPresetKey] = react.useState(initialTheme ? "custom" : "drafting-teal");
|
|
8275
|
+
const [showPalette, setShowPalette] = react.useState(false);
|
|
8276
|
+
const [showComparison, setShowComparison] = react.useState(false);
|
|
5443
8277
|
const themeRef = react.useRef(theme);
|
|
5444
8278
|
themeRef.current = theme;
|
|
5445
8279
|
react.useEffect(() => {
|
|
@@ -5507,12 +8341,22 @@ function ThemeEditorInner({
|
|
|
5507
8341
|
};
|
|
5508
8342
|
input.click();
|
|
5509
8343
|
}, [onChange]);
|
|
8344
|
+
const exportCss = react.useCallback(() => {
|
|
8345
|
+
exportCssFile(theme);
|
|
8346
|
+
}, [theme]);
|
|
8347
|
+
const importCss = react.useCallback(() => {
|
|
8348
|
+
importCssFile(theme, (merged) => {
|
|
8349
|
+
setTheme(merged);
|
|
8350
|
+
setPresetKey("custom");
|
|
8351
|
+
onChange?.(merged);
|
|
8352
|
+
});
|
|
8353
|
+
}, [theme, onChange]);
|
|
5510
8354
|
const currentSection = react.useMemo(
|
|
5511
8355
|
() => SECTIONS.find((s) => s.id === activeSection),
|
|
5512
8356
|
[activeSection]
|
|
5513
8357
|
);
|
|
5514
8358
|
const sectionValues = theme[currentSection.themeKey] ?? {};
|
|
5515
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
8359
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ThemeEditorThemeProvider, { theme: chromeTheme, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5516
8360
|
"div",
|
|
5517
8361
|
{
|
|
5518
8362
|
className: `fcte-root${className ? ` ${className}` : ""}`,
|
|
@@ -5539,22 +8383,65 @@ function ThemeEditorInner({
|
|
|
5539
8383
|
] }),
|
|
5540
8384
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-toolbar__right", children: [
|
|
5541
8385
|
toolbarExtra,
|
|
5542
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: importJson, className: "fcte-btn fcte-btn--secondary", children: "Import" }),
|
|
5543
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: exportJson, className: "fcte-btn fcte-btn--secondary", children: "Export" }),
|
|
8386
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: importJson, className: "fcte-btn fcte-btn--secondary", children: "Import JSON" }),
|
|
8387
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: exportJson, className: "fcte-btn fcte-btn--secondary", children: "Export JSON" }),
|
|
8388
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: importCss, className: "fcte-btn fcte-btn--secondary", children: "Import CSS" }),
|
|
8389
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: exportCss, className: "fcte-btn fcte-btn--secondary", children: "Export CSS" }),
|
|
8390
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8391
|
+
"button",
|
|
8392
|
+
{
|
|
8393
|
+
onClick: () => {
|
|
8394
|
+
setShowPalette((v) => !v);
|
|
8395
|
+
setShowComparison(false);
|
|
8396
|
+
},
|
|
8397
|
+
className: `fcte-btn ${showPalette ? "fcte-btn--primary" : "fcte-btn--secondary"}`,
|
|
8398
|
+
children: "Palette"
|
|
8399
|
+
}
|
|
8400
|
+
),
|
|
8401
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8402
|
+
"button",
|
|
8403
|
+
{
|
|
8404
|
+
onClick: () => {
|
|
8405
|
+
setShowComparison((v) => !v);
|
|
8406
|
+
setShowPalette(false);
|
|
8407
|
+
},
|
|
8408
|
+
className: `fcte-btn ${showComparison ? "fcte-btn--primary" : "fcte-btn--secondary"}`,
|
|
8409
|
+
children: "Compare"
|
|
8410
|
+
}
|
|
8411
|
+
),
|
|
5544
8412
|
onSave && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onSave(theme), className: "fcte-btn fcte-btn--primary", children: "Save" })
|
|
5545
8413
|
] })
|
|
5546
8414
|
] }),
|
|
5547
|
-
/* @__PURE__ */ jsxRuntime.
|
|
8415
|
+
showPalette && /* @__PURE__ */ jsxRuntime.jsx(
|
|
8416
|
+
PaletteGenerator,
|
|
8417
|
+
{
|
|
8418
|
+
theme,
|
|
8419
|
+
onApply: (newTheme) => {
|
|
8420
|
+
setTheme(newTheme);
|
|
8421
|
+
setPresetKey("custom");
|
|
8422
|
+
onChange?.(newTheme);
|
|
8423
|
+
setShowPalette(false);
|
|
8424
|
+
},
|
|
8425
|
+
onClose: () => setShowPalette(false)
|
|
8426
|
+
}
|
|
8427
|
+
),
|
|
8428
|
+
showComparison ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
8429
|
+
ThemeComparison,
|
|
8430
|
+
{
|
|
8431
|
+
currentTheme: theme,
|
|
8432
|
+
onClose: () => setShowComparison(false)
|
|
8433
|
+
}
|
|
8434
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-body", children: [
|
|
5548
8435
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-editor", children: [
|
|
5549
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-
|
|
5550
|
-
"
|
|
8436
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-section-select", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
8437
|
+
"select",
|
|
5551
8438
|
{
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
)
|
|
8439
|
+
value: activeSection,
|
|
8440
|
+
onChange: (e) => setActiveSection(e.target.value),
|
|
8441
|
+
className: "fcte-field__select",
|
|
8442
|
+
children: SECTIONS.map((s) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: s.id, children: s.label }, s.id))
|
|
8443
|
+
}
|
|
8444
|
+
) }),
|
|
5558
8445
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-fields", children: currentSection.fields.map((field) => {
|
|
5559
8446
|
const val = sectionValues[field.key];
|
|
5560
8447
|
if (field.kind === "color") {
|
|
@@ -5632,19 +8519,29 @@ function ThemeEditorInner({
|
|
|
5632
8519
|
] }, field.key);
|
|
5633
8520
|
}) })
|
|
5634
8521
|
] }),
|
|
5635
|
-
showPreview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
5636
|
-
|
|
8522
|
+
showPreview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
8523
|
+
"div",
|
|
5637
8524
|
{
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
8525
|
+
className: "fcte-preview",
|
|
8526
|
+
style: {
|
|
8527
|
+
background: theme.colors?.background || void 0,
|
|
8528
|
+
color: theme.colors?.text || void 0
|
|
8529
|
+
},
|
|
8530
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-preview__inner", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
8531
|
+
fieldcraftReact.FormEngineRenderer,
|
|
8532
|
+
{
|
|
8533
|
+
schema: PREVIEW_SCHEMA,
|
|
8534
|
+
theme,
|
|
8535
|
+
onSubmit: () => {
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
) })
|
|
5642
8539
|
}
|
|
5643
|
-
)
|
|
8540
|
+
)
|
|
5644
8541
|
] })
|
|
5645
8542
|
]
|
|
5646
8543
|
}
|
|
5647
|
-
);
|
|
8544
|
+
) });
|
|
5648
8545
|
}
|
|
5649
8546
|
|
|
5650
8547
|
// src/theme-editor/ThemeEditor.tsx
|
|
@@ -5653,7 +8550,7 @@ var ThemeEditor = requireLicense(ThemeEditorInner, "ThemeEditor");
|
|
|
5653
8550
|
// src/index.ts
|
|
5654
8551
|
if (typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV !== "production") {
|
|
5655
8552
|
const _fc_banner = `
|
|
5656
|
-
%c FieldCraft Pro %c v1.
|
|
8553
|
+
%c FieldCraft Pro %c v1.5.0
|
|
5657
8554
|
|
|
5658
8555
|
%cForm Builder \xB7 Response Viewer \xB7 Theme Editor
|
|
5659
8556
|
|
|
@@ -5677,6 +8574,8 @@ exports.FieldCraftProProvider = FieldCraftProProvider;
|
|
|
5677
8574
|
exports.FormBuilder = FormBuilder;
|
|
5678
8575
|
exports.FormBuilderThemeProvider = FormBuilderThemeProvider;
|
|
5679
8576
|
exports.PREVIEW_SCHEMA = PREVIEW_SCHEMA;
|
|
8577
|
+
exports.PRO_FIELD_OVERRIDES = PRO_FIELD_OVERRIDES;
|
|
8578
|
+
exports.ProPaymentField = ProPaymentField;
|
|
5680
8579
|
exports.QUESTION_TYPE_INFO = QUESTION_TYPE_INFO;
|
|
5681
8580
|
exports.ResponseViewer = ResponseViewer;
|
|
5682
8581
|
exports.ThemeEditor = ThemeEditor;
|