@squaredr/fieldcraft-pro 1.2.0 → 1.4.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-4BX7FCXH.mjs → chunk-BK4SMEW4.mjs} +840 -21
- package/dist/{chunk-GNBXIG63.mjs → chunk-BYT2P3I6.mjs} +1264 -88
- package/dist/form-builder/index.d.mts +7 -367
- package/dist/form-builder/index.d.ts +7 -367
- package/dist/form-builder/index.js +1266 -90
- package/dist/form-builder/index.mjs +1 -1
- package/dist/index-nvIvXlmc.d.mts +398 -0
- package/dist/index-nvIvXlmc.d.ts +398 -0
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2926 -335
- package/dist/index.mjs +4 -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 +8 -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;
|
|
@@ -1149,7 +1151,81 @@ var QUESTION_TYPE_INFO = {
|
|
|
1149
1151
|
description: "Upload files",
|
|
1150
1152
|
defaultConfig: { type: "file_upload", maxFiles: 1, maxSizeMb: 10 }
|
|
1151
1153
|
},
|
|
1154
|
+
// ── Compound ──
|
|
1155
|
+
legal_name: {
|
|
1156
|
+
type: "legal_name",
|
|
1157
|
+
label: "Legal Name",
|
|
1158
|
+
category: "text",
|
|
1159
|
+
icon: "UserCheck",
|
|
1160
|
+
description: "First, middle, and last name",
|
|
1161
|
+
defaultConfig: { type: "legal_name" }
|
|
1162
|
+
},
|
|
1163
|
+
address: {
|
|
1164
|
+
type: "address",
|
|
1165
|
+
label: "Address",
|
|
1166
|
+
category: "text",
|
|
1167
|
+
icon: "MapPin",
|
|
1168
|
+
description: "Street, city, state, ZIP",
|
|
1169
|
+
defaultConfig: { type: "address", includeCountry: false }
|
|
1170
|
+
},
|
|
1171
|
+
signature: {
|
|
1172
|
+
type: "signature",
|
|
1173
|
+
label: "Signature",
|
|
1174
|
+
category: "media",
|
|
1175
|
+
icon: "PenTool",
|
|
1176
|
+
description: "Canvas signature pad",
|
|
1177
|
+
defaultConfig: { type: "signature" }
|
|
1178
|
+
},
|
|
1179
|
+
date_range: {
|
|
1180
|
+
type: "date_range",
|
|
1181
|
+
label: "Date Range",
|
|
1182
|
+
category: "datetime",
|
|
1183
|
+
icon: "CalendarRange",
|
|
1184
|
+
description: "Start and end date picker",
|
|
1185
|
+
defaultConfig: { type: "date_range" }
|
|
1186
|
+
},
|
|
1152
1187
|
// ── Advanced ──
|
|
1188
|
+
repeater: {
|
|
1189
|
+
type: "repeater",
|
|
1190
|
+
label: "Repeater",
|
|
1191
|
+
category: "advanced",
|
|
1192
|
+
icon: "ListPlus",
|
|
1193
|
+
description: "Repeatable group of fields",
|
|
1194
|
+
defaultConfig: { type: "repeater", fields: [], minItems: 1, maxItems: 10 }
|
|
1195
|
+
},
|
|
1196
|
+
likert: {
|
|
1197
|
+
type: "likert",
|
|
1198
|
+
label: "Likert Scale",
|
|
1199
|
+
category: "advanced",
|
|
1200
|
+
icon: "AlignHorizontalSpaceAround",
|
|
1201
|
+
description: "Agreement scale (e.g. Strongly Disagree to Strongly Agree)",
|
|
1202
|
+
requiresOptions: true,
|
|
1203
|
+
defaultConfig: {
|
|
1204
|
+
type: "likert",
|
|
1205
|
+
scale: [
|
|
1206
|
+
{ label: "Strongly Disagree", value: "1" },
|
|
1207
|
+
{ label: "Disagree", value: "2" },
|
|
1208
|
+
{ label: "Neutral", value: "3" },
|
|
1209
|
+
{ label: "Agree", value: "4" },
|
|
1210
|
+
{ label: "Strongly Agree", value: "5" }
|
|
1211
|
+
]
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
scoring: {
|
|
1215
|
+
type: "scoring",
|
|
1216
|
+
label: "Scoring",
|
|
1217
|
+
category: "advanced",
|
|
1218
|
+
icon: "Award",
|
|
1219
|
+
description: "Scored question with point values per option",
|
|
1220
|
+
requiresOptions: true,
|
|
1221
|
+
defaultConfig: {
|
|
1222
|
+
type: "scoring",
|
|
1223
|
+
options: [
|
|
1224
|
+
{ label: "Option 1", value: "1", score: 1 },
|
|
1225
|
+
{ label: "Option 2", value: "2", score: 2 }
|
|
1226
|
+
]
|
|
1227
|
+
}
|
|
1228
|
+
},
|
|
1153
1229
|
matrix: {
|
|
1154
1230
|
type: "matrix",
|
|
1155
1231
|
label: "Matrix",
|
|
@@ -1283,7 +1359,7 @@ var DEFAULT_PALETTE = [
|
|
|
1283
1359
|
{
|
|
1284
1360
|
category: "text",
|
|
1285
1361
|
label: "Text Input",
|
|
1286
|
-
types: ["short_text", "long_text", "email", "phone", "url"]
|
|
1362
|
+
types: ["short_text", "long_text", "email", "phone", "url", "legal_name", "address"]
|
|
1287
1363
|
},
|
|
1288
1364
|
{
|
|
1289
1365
|
category: "numeric",
|
|
@@ -1298,12 +1374,12 @@ var DEFAULT_PALETTE = [
|
|
|
1298
1374
|
{
|
|
1299
1375
|
category: "datetime",
|
|
1300
1376
|
label: "Date & Time",
|
|
1301
|
-
types: ["date", "time"]
|
|
1377
|
+
types: ["date", "time", "date_range"]
|
|
1302
1378
|
},
|
|
1303
1379
|
{
|
|
1304
1380
|
category: "media",
|
|
1305
1381
|
label: "Media",
|
|
1306
|
-
types: ["file_upload"]
|
|
1382
|
+
types: ["file_upload", "signature"]
|
|
1307
1383
|
},
|
|
1308
1384
|
{
|
|
1309
1385
|
category: "content",
|
|
@@ -1318,7 +1394,7 @@ var DEFAULT_PALETTE = [
|
|
|
1318
1394
|
{
|
|
1319
1395
|
category: "advanced",
|
|
1320
1396
|
label: "Advanced",
|
|
1321
|
-
types: ["matrix", "calculated", "hidden"]
|
|
1397
|
+
types: ["repeater", "likert", "scoring", "matrix", "calculated", "hidden"]
|
|
1322
1398
|
}
|
|
1323
1399
|
];
|
|
1324
1400
|
|
|
@@ -2835,7 +2911,8 @@ function getFieldOptions(schema, excludeId) {
|
|
|
2835
2911
|
return fields;
|
|
2836
2912
|
}
|
|
2837
2913
|
function ConditionEditor({ question, schema, onUpdate }) {
|
|
2838
|
-
const
|
|
2914
|
+
const rawShowIf = question.showIf;
|
|
2915
|
+
const showIf = rawShowIf && rawShowIf.field && !rawShowIf.conditions ? { combine: "AND", conditions: [rawShowIf] } : rawShowIf;
|
|
2839
2916
|
const fieldOptions = getFieldOptions(schema, question.id);
|
|
2840
2917
|
const updateShowIf = (next) => {
|
|
2841
2918
|
onUpdate({ showIf: next });
|
|
@@ -2957,6 +3034,24 @@ function ConditionEditor({ question, schema, onUpdate }) {
|
|
|
2957
3034
|
}
|
|
2958
3035
|
function FormSettingsPanel({ schema, onUpdate }) {
|
|
2959
3036
|
const settings = schema.settings ?? {};
|
|
3037
|
+
const hasConditions = schema.sections.some(
|
|
3038
|
+
(s) => s.showIf || s.questions.some((q) => q.showIf)
|
|
3039
|
+
);
|
|
3040
|
+
const displayModeOptions = hasConditions ? [
|
|
3041
|
+
{ label: "Stepped", value: "stepped" },
|
|
3042
|
+
{ label: "Conversational", value: "conversational" }
|
|
3043
|
+
] : [
|
|
3044
|
+
{ label: "Stepped", value: "stepped" },
|
|
3045
|
+
{ label: "Classic", value: "classic" },
|
|
3046
|
+
{ label: "Conversational", value: "conversational" }
|
|
3047
|
+
];
|
|
3048
|
+
const displayMode = settings.displayMode ?? "stepped";
|
|
3049
|
+
const effectiveDisplayMode = hasConditions && displayMode === "classic" ? "stepped" : displayMode;
|
|
3050
|
+
react.useEffect(() => {
|
|
3051
|
+
if (effectiveDisplayMode !== displayMode) {
|
|
3052
|
+
onUpdate({ ...schema, settings: { ...settings, displayMode: effectiveDisplayMode } });
|
|
3053
|
+
}
|
|
3054
|
+
}, [effectiveDisplayMode, displayMode]);
|
|
2960
3055
|
const updateSettings = (updates) => {
|
|
2961
3056
|
onUpdate({ ...schema, settings: { ...settings, ...updates } });
|
|
2962
3057
|
};
|
|
@@ -2984,12 +3079,8 @@ function FormSettingsPanel({ schema, onUpdate }) {
|
|
|
2984
3079
|
SettingsSelect,
|
|
2985
3080
|
{
|
|
2986
3081
|
label: "Mode",
|
|
2987
|
-
value:
|
|
2988
|
-
options:
|
|
2989
|
-
{ label: "Classic", value: "classic" },
|
|
2990
|
-
{ label: "Stepped", value: "stepped" },
|
|
2991
|
-
{ label: "Conversational", value: "conversational" }
|
|
2992
|
-
],
|
|
3082
|
+
value: effectiveDisplayMode,
|
|
3083
|
+
options: displayModeOptions,
|
|
2993
3084
|
onChange: (v) => updateSettings({ displayMode: v })
|
|
2994
3085
|
}
|
|
2995
3086
|
),
|
|
@@ -3367,228 +3458,1241 @@ function FieldGroup({ label, children }) {
|
|
|
3367
3458
|
children
|
|
3368
3459
|
] });
|
|
3369
3460
|
}
|
|
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
|
-
}
|
|
3461
|
+
var PreviewErrorBoundary = class extends react.Component {
|
|
3462
|
+
state = { error: null };
|
|
3416
3463
|
static getDerivedStateFromError(error) {
|
|
3417
|
-
return {
|
|
3464
|
+
return { error };
|
|
3418
3465
|
}
|
|
3419
3466
|
componentDidCatch(error, info) {
|
|
3420
|
-
console.error("[FormBuilder]
|
|
3467
|
+
console.error("[FormBuilder Preview]", error, info);
|
|
3468
|
+
}
|
|
3469
|
+
componentDidUpdate(prevProps) {
|
|
3470
|
+
if (prevProps.resetKey !== this.props.resetKey && this.state.error) {
|
|
3471
|
+
this.setState({ error: null });
|
|
3472
|
+
}
|
|
3421
3473
|
}
|
|
3422
3474
|
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
|
-
] }) });
|
|
3475
|
+
if (this.state.error) {
|
|
3476
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-4 text-destructive text-sm font-mono", children: [
|
|
3477
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: "Render Error:" }),
|
|
3478
|
+
" ",
|
|
3479
|
+
this.state.error.message
|
|
3480
|
+
] });
|
|
3437
3481
|
}
|
|
3438
3482
|
return this.props.children;
|
|
3439
3483
|
}
|
|
3440
3484
|
};
|
|
3441
|
-
function
|
|
3442
|
-
const
|
|
3443
|
-
const
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3485
|
+
function FormPreviewPanel({ schema, preview }) {
|
|
3486
|
+
const hasSections = schema.sections && schema.sections.length > 0;
|
|
3487
|
+
const hasFields = hasSections && schema.sections.some((s) => s.questions.length > 0);
|
|
3488
|
+
if (!hasFields) {
|
|
3489
|
+
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: [
|
|
3490
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "No fields to preview" }),
|
|
3491
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs", children: "Add fields in the Design view to see a live preview here." })
|
|
3492
|
+
] }) });
|
|
3493
|
+
}
|
|
3494
|
+
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(
|
|
3495
|
+
fieldcraftReact.FormEngineRenderer,
|
|
3496
|
+
{
|
|
3497
|
+
schema,
|
|
3498
|
+
theme: preview?.theme,
|
|
3499
|
+
components: preview?.components,
|
|
3500
|
+
onReady: preview?.onReady,
|
|
3501
|
+
onFieldChange: preview?.onFieldChange,
|
|
3502
|
+
onSubmit: preview?.onSubmit ?? (() => {
|
|
3503
|
+
})
|
|
3456
3504
|
}
|
|
3457
|
-
}
|
|
3505
|
+
) }) }) });
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
// src/form-builder/utils/validation-markers.ts
|
|
3509
|
+
var MarkerSeverity = {
|
|
3510
|
+
Error: 8};
|
|
3511
|
+
function errorsToMarkers(errors) {
|
|
3512
|
+
return errors.map((error) => ({
|
|
3513
|
+
severity: MarkerSeverity.Error,
|
|
3514
|
+
message: error.message,
|
|
3515
|
+
startLineNumber: error.line ?? 1,
|
|
3516
|
+
startColumn: error.column ?? 1,
|
|
3517
|
+
endLineNumber: error.endLine ?? error.line ?? 1,
|
|
3518
|
+
endColumn: error.endColumn ?? error.column ?? 1,
|
|
3519
|
+
source: "FieldCraft Schema Validator"
|
|
3520
|
+
}));
|
|
3521
|
+
}
|
|
3522
|
+
var Editor = react.lazy(() => import('@monaco-editor/react').then((m) => ({ default: m.default })));
|
|
3523
|
+
function MonacoWrapper({ value, onChange, errors }) {
|
|
3524
|
+
const editorRef = react.useRef(null);
|
|
3525
|
+
const monacoRef = react.useRef(null);
|
|
3526
|
+
const handleMount = (editor, monaco) => {
|
|
3527
|
+
editorRef.current = editor;
|
|
3528
|
+
monacoRef.current = monaco;
|
|
3529
|
+
};
|
|
3458
3530
|
react.useEffect(() => {
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3531
|
+
const editor = editorRef.current;
|
|
3532
|
+
const monaco = monacoRef.current;
|
|
3533
|
+
if (!editor || !monaco) return;
|
|
3534
|
+
const model = editor.getModel();
|
|
3535
|
+
if (!model) return;
|
|
3536
|
+
const markers = errorsToMarkers(errors);
|
|
3537
|
+
monaco.editor.setModelMarkers(model, "fieldcraft-schema", markers);
|
|
3538
|
+
}, [errors]);
|
|
3539
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3540
|
+
Editor,
|
|
3541
|
+
{
|
|
3542
|
+
height: "100%",
|
|
3543
|
+
language: "json",
|
|
3544
|
+
theme: "vs-dark",
|
|
3545
|
+
value,
|
|
3546
|
+
onChange: (val) => onChange(val ?? ""),
|
|
3547
|
+
onMount: handleMount,
|
|
3548
|
+
options: {
|
|
3549
|
+
minimap: { enabled: false },
|
|
3550
|
+
fontSize: 13,
|
|
3551
|
+
wordWrap: "on",
|
|
3552
|
+
formatOnPaste: true,
|
|
3553
|
+
automaticLayout: true,
|
|
3554
|
+
tabSize: 2,
|
|
3555
|
+
scrollBeyondLastLine: false,
|
|
3556
|
+
padding: { top: 12 }
|
|
3557
|
+
}
|
|
3467
3558
|
}
|
|
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 = "";
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
function JsonEditorPanel({ value, onChange, errors, onValidate }) {
|
|
3562
|
+
const handleChange = react.useCallback(
|
|
3563
|
+
(newValue) => {
|
|
3564
|
+
onChange(newValue);
|
|
3565
|
+
onValidate(newValue);
|
|
3506
3566
|
},
|
|
3507
|
-
[
|
|
3567
|
+
[onChange, onValidate]
|
|
3508
3568
|
);
|
|
3509
|
-
const
|
|
3510
|
-
|
|
3511
|
-
const
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
e.
|
|
3521
|
-
|
|
3522
|
-
|
|
3569
|
+
const handleFormat = react.useCallback(() => {
|
|
3570
|
+
try {
|
|
3571
|
+
const formatted = JSON.stringify(JSON.parse(value), null, 2);
|
|
3572
|
+
onChange(formatted);
|
|
3573
|
+
} catch {
|
|
3574
|
+
}
|
|
3575
|
+
}, [value, onChange]);
|
|
3576
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex flex-col overflow-hidden", children: [
|
|
3577
|
+
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: [
|
|
3578
|
+
errors.slice(0, 3).map((e, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3579
|
+
e.line ? `Line ${e.line}: ` : "",
|
|
3580
|
+
e.path ? `${e.path}: ` : "",
|
|
3581
|
+
e.message
|
|
3582
|
+
] }, i)),
|
|
3583
|
+
errors.length > 3 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-muted-foreground", children: [
|
|
3584
|
+
"...and ",
|
|
3585
|
+
errors.length - 3,
|
|
3586
|
+
" more"
|
|
3587
|
+
] })
|
|
3588
|
+
] }),
|
|
3589
|
+
/* @__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: [
|
|
3590
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: errors.length === 0 ? "Valid schema" : `${errors.length} error${errors.length !== 1 ? "s" : ""}` }),
|
|
3591
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3592
|
+
"button",
|
|
3593
|
+
{
|
|
3594
|
+
type: "button",
|
|
3595
|
+
onClick: handleFormat,
|
|
3596
|
+
className: "text-primary hover:underline",
|
|
3597
|
+
children: "Format JSON"
|
|
3598
|
+
}
|
|
3599
|
+
)
|
|
3600
|
+
] }),
|
|
3601
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3602
|
+
react.Suspense,
|
|
3603
|
+
{
|
|
3604
|
+
fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center h-full text-sm text-muted-foreground", children: "Loading editor..." }),
|
|
3605
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(MonacoWrapper, { value, onChange: handleChange, errors })
|
|
3523
3606
|
}
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3607
|
+
) })
|
|
3608
|
+
] });
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
// src/form-builder/utils/logic-graph.ts
|
|
3612
|
+
function extractFieldRefs(expr) {
|
|
3613
|
+
const refs = [];
|
|
3614
|
+
if (expr.field) {
|
|
3615
|
+
refs.push({ field: expr.field, operator: expr.operator, value: expr.value });
|
|
3616
|
+
}
|
|
3617
|
+
if (expr.conditions) {
|
|
3618
|
+
for (const child of expr.conditions) {
|
|
3619
|
+
refs.push(...extractFieldRefs(child));
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
return refs;
|
|
3623
|
+
}
|
|
3624
|
+
function buildLogicGraph(schema) {
|
|
3625
|
+
const nodes = [];
|
|
3626
|
+
const edges = [];
|
|
3627
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
3628
|
+
for (const section of schema.sections) {
|
|
3629
|
+
nodes.push({
|
|
3630
|
+
id: section.id,
|
|
3631
|
+
label: section.title || "Untitled Section",
|
|
3632
|
+
type: "section",
|
|
3633
|
+
sectionId: section.id,
|
|
3634
|
+
sectionTitle: section.title || "Untitled Section",
|
|
3635
|
+
hasCondition: !!section.showIf
|
|
3636
|
+
});
|
|
3637
|
+
nodeIds.add(section.id);
|
|
3638
|
+
if (section.showIf) {
|
|
3639
|
+
const refs = extractFieldRefs(section.showIf);
|
|
3640
|
+
for (const ref of refs) {
|
|
3641
|
+
edges.push({ from: ref.field, to: section.id, edgeType: "showIf", operator: ref.operator, value: ref.value });
|
|
3528
3642
|
}
|
|
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);
|
|
3643
|
+
}
|
|
3644
|
+
if (section.onExit) {
|
|
3645
|
+
for (const rule of section.onExit.rules) {
|
|
3646
|
+
if (rule.jumpTo) {
|
|
3647
|
+
edges.push({ from: section.id, to: rule.jumpTo, edgeType: "onExit", jumpTo: rule.jumpTo });
|
|
3538
3648
|
}
|
|
3539
|
-
return;
|
|
3540
3649
|
}
|
|
3541
|
-
if (
|
|
3542
|
-
|
|
3543
|
-
e.preventDefault();
|
|
3544
|
-
builderState.clearSelection();
|
|
3545
|
-
}
|
|
3546
|
-
return;
|
|
3650
|
+
if (section.onExit.default) {
|
|
3651
|
+
edges.push({ from: section.id, to: section.onExit.default, edgeType: "onExit", jumpTo: section.onExit.default });
|
|
3547
3652
|
}
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3653
|
+
}
|
|
3654
|
+
for (const question of section.questions) {
|
|
3655
|
+
nodes.push({
|
|
3656
|
+
id: question.id,
|
|
3657
|
+
label: question.label || question.id,
|
|
3658
|
+
type: "field",
|
|
3659
|
+
fieldType: question.type,
|
|
3660
|
+
sectionId: section.id,
|
|
3661
|
+
sectionTitle: section.title || "Untitled Section",
|
|
3662
|
+
hasCondition: !!question.showIf
|
|
3663
|
+
});
|
|
3664
|
+
nodeIds.add(question.id);
|
|
3665
|
+
if (question.showIf) {
|
|
3666
|
+
const refs = extractFieldRefs(question.showIf);
|
|
3667
|
+
for (const ref of refs) {
|
|
3668
|
+
edges.push({ from: ref.field, to: question.id, edgeType: "showIf", operator: ref.operator, value: ref.value });
|
|
3556
3669
|
}
|
|
3557
3670
|
}
|
|
3558
|
-
}
|
|
3559
|
-
|
|
3560
|
-
);
|
|
3561
|
-
return
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
const validEdges = edges.filter((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
|
|
3674
|
+
return { nodes, edges: validEdges };
|
|
3675
|
+
}
|
|
3676
|
+
var SECTION_COLORS = [
|
|
3677
|
+
"#3b82f6",
|
|
3678
|
+
// blue
|
|
3679
|
+
"#8b5cf6",
|
|
3680
|
+
// violet
|
|
3681
|
+
"#06b6d4",
|
|
3682
|
+
// cyan
|
|
3683
|
+
"#f59e0b",
|
|
3684
|
+
// amber
|
|
3685
|
+
"#10b981",
|
|
3686
|
+
// emerald
|
|
3687
|
+
"#ef4444",
|
|
3688
|
+
// red
|
|
3689
|
+
"#ec4899",
|
|
3690
|
+
// pink
|
|
3691
|
+
"#6366f1"
|
|
3692
|
+
// indigo
|
|
3693
|
+
];
|
|
3694
|
+
var FIELD_NODE_WIDTH = 220;
|
|
3695
|
+
var FIELD_NODE_HEIGHT = 52;
|
|
3696
|
+
var SECTION_HEADER_HEIGHT = 40;
|
|
3697
|
+
var SECTION_PADDING_X = 16;
|
|
3698
|
+
var SECTION_PADDING_TOP = 12;
|
|
3699
|
+
var SECTION_PADDING_BOTTOM = 16;
|
|
3700
|
+
var FIELD_GAP = 10;
|
|
3701
|
+
var SECTION_GAP = 80;
|
|
3702
|
+
function formatEdgeLabel(edge) {
|
|
3703
|
+
if (edge.edgeType === "onExit") return "jump";
|
|
3704
|
+
if (!edge.operator) return "";
|
|
3705
|
+
const op = edge.operator;
|
|
3706
|
+
const val = edge.value;
|
|
3707
|
+
if (op === "exists") return "has value";
|
|
3708
|
+
if (op === "notExists") return "is empty";
|
|
3709
|
+
if (val === void 0 || val === null || val === "") return op;
|
|
3710
|
+
const short = String(val).length > 12 ? String(val).slice(0, 12) + "\u2026" : String(val);
|
|
3711
|
+
return `${op} ${short}`;
|
|
3712
|
+
}
|
|
3713
|
+
function buildReactFlowGraph(schema) {
|
|
3714
|
+
const graph = buildLogicGraph(schema);
|
|
3715
|
+
const nodes = [];
|
|
3716
|
+
const edges = [];
|
|
3717
|
+
let sectionX = 0;
|
|
3718
|
+
let maxSectionHeight = 0;
|
|
3719
|
+
for (const section of schema.sections) {
|
|
3720
|
+
const fieldCount = section.questions.length;
|
|
3721
|
+
const contentHeight = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + fieldCount * FIELD_NODE_HEIGHT + Math.max(0, fieldCount - 1) * FIELD_GAP + SECTION_PADDING_BOTTOM;
|
|
3722
|
+
if (contentHeight > maxSectionHeight) maxSectionHeight = contentHeight;
|
|
3723
|
+
}
|
|
3724
|
+
if (maxSectionHeight < SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + SECTION_PADDING_BOTTOM + FIELD_NODE_HEIGHT) {
|
|
3725
|
+
maxSectionHeight = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + SECTION_PADDING_BOTTOM + FIELD_NODE_HEIGHT;
|
|
3726
|
+
}
|
|
3727
|
+
const sectionWidth = FIELD_NODE_WIDTH + SECTION_PADDING_X * 2;
|
|
3728
|
+
for (let sIdx = 0; sIdx < schema.sections.length; sIdx++) {
|
|
3729
|
+
const section = schema.sections[sIdx];
|
|
3730
|
+
const colorIndex = sIdx;
|
|
3731
|
+
nodes.push({
|
|
3732
|
+
id: section.id,
|
|
3733
|
+
type: "sectionNode",
|
|
3734
|
+
position: { x: sectionX, y: 0 },
|
|
3735
|
+
data: {
|
|
3736
|
+
label: section.title || "Untitled Section",
|
|
3737
|
+
sectionId: section.id,
|
|
3738
|
+
fieldCount: section.questions.length,
|
|
3739
|
+
hasCondition: !!section.showIf,
|
|
3740
|
+
hasExitLogic: !!section.onExit,
|
|
3741
|
+
colorIndex
|
|
3742
|
+
},
|
|
3743
|
+
style: { width: sectionWidth, height: maxSectionHeight }
|
|
3744
|
+
});
|
|
3745
|
+
for (let fIdx = 0; fIdx < section.questions.length; fIdx++) {
|
|
3746
|
+
const question = section.questions[fIdx];
|
|
3747
|
+
const fieldY = SECTION_HEADER_HEIGHT + SECTION_PADDING_TOP + fIdx * (FIELD_NODE_HEIGHT + FIELD_GAP);
|
|
3748
|
+
nodes.push({
|
|
3749
|
+
id: question.id,
|
|
3750
|
+
type: "fieldNode",
|
|
3751
|
+
position: { x: SECTION_PADDING_X, y: fieldY },
|
|
3752
|
+
parentId: section.id,
|
|
3753
|
+
extent: "parent",
|
|
3754
|
+
data: {
|
|
3755
|
+
label: question.label || question.id,
|
|
3756
|
+
fieldType: question.type,
|
|
3757
|
+
sectionId: section.id,
|
|
3758
|
+
hasCondition: !!question.showIf,
|
|
3759
|
+
colorIndex
|
|
3760
|
+
},
|
|
3761
|
+
style: { width: FIELD_NODE_WIDTH }
|
|
3762
|
+
});
|
|
3763
|
+
}
|
|
3764
|
+
if (sIdx < schema.sections.length - 1) {
|
|
3765
|
+
const nextSection = schema.sections[sIdx + 1];
|
|
3766
|
+
edges.push({
|
|
3767
|
+
id: `pipeline-${section.id}-${nextSection.id}`,
|
|
3768
|
+
source: section.id,
|
|
3769
|
+
target: nextSection.id,
|
|
3770
|
+
type: "pipelineEdge",
|
|
3771
|
+
data: { edgeType: "pipeline" }
|
|
3772
|
+
});
|
|
3773
|
+
}
|
|
3774
|
+
sectionX += sectionWidth + SECTION_GAP;
|
|
3775
|
+
}
|
|
3776
|
+
for (let i = 0; i < graph.edges.length; i++) {
|
|
3777
|
+
const edge = graph.edges[i];
|
|
3778
|
+
edges.push({
|
|
3779
|
+
id: `e-${edge.from}-${edge.to}-${i}`,
|
|
3780
|
+
source: edge.from,
|
|
3781
|
+
target: edge.to,
|
|
3782
|
+
type: "conditionEdge",
|
|
3783
|
+
animated: edge.edgeType === "showIf",
|
|
3784
|
+
style: edge.edgeType === "onExit" ? { strokeDasharray: "6 3", stroke: "#f59e0b" } : void 0,
|
|
3785
|
+
label: formatEdgeLabel(edge),
|
|
3786
|
+
data: {
|
|
3787
|
+
edgeType: edge.edgeType,
|
|
3788
|
+
operator: edge.operator,
|
|
3789
|
+
value: edge.value,
|
|
3790
|
+
sourceId: edge.from,
|
|
3791
|
+
targetId: edge.to
|
|
3792
|
+
}
|
|
3793
|
+
});
|
|
3794
|
+
}
|
|
3795
|
+
return { nodes, edges };
|
|
3796
|
+
}
|
|
3797
|
+
function SectionNodeInner({ data }) {
|
|
3798
|
+
const d = data;
|
|
3799
|
+
const color = SECTION_COLORS[d.colorIndex % SECTION_COLORS.length];
|
|
3800
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-section-group", style: { borderColor: color }, children: [
|
|
3801
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "target", position: react$1.Position.Left, className: "fc-flow-handle fc-flow-handle--section" }),
|
|
3802
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-section-group__header", style: { background: color }, children: [
|
|
3803
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__label", children: d.label.length > 30 ? d.label.slice(0, 30) + "\u2026" : d.label }),
|
|
3804
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fc-flow-section-group__meta", children: [
|
|
3805
|
+
d.fieldCount,
|
|
3806
|
+
" field",
|
|
3807
|
+
d.fieldCount !== 1 ? "s" : "",
|
|
3808
|
+
d.hasCondition && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__badge", title: "Has showIf condition", children: "?" }),
|
|
3809
|
+
d.hasExitLogic && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-section-group__badge fc-flow-section-group__badge--jump", title: "Has onExit jump", children: "\u2934" })
|
|
3810
|
+
] })
|
|
3811
|
+
] }),
|
|
3812
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "source", position: react$1.Position.Right, className: "fc-flow-handle fc-flow-handle--section" })
|
|
3813
|
+
] });
|
|
3814
|
+
}
|
|
3815
|
+
var SectionNode = react.memo(SectionNodeInner);
|
|
3816
|
+
function FieldNodeInner({ data }) {
|
|
3817
|
+
const d = data;
|
|
3818
|
+
const color = SECTION_COLORS[d.colorIndex % SECTION_COLORS.length];
|
|
3819
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3820
|
+
"div",
|
|
3821
|
+
{
|
|
3822
|
+
className: "fc-flow-field",
|
|
3823
|
+
style: {
|
|
3824
|
+
borderColor: d.hasCondition ? color : void 0,
|
|
3825
|
+
borderWidth: d.hasCondition ? 2 : 1
|
|
3826
|
+
},
|
|
3827
|
+
children: [
|
|
3828
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "target", position: react$1.Position.Left, className: "fc-flow-handle" }),
|
|
3829
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-flow-field__content", children: [
|
|
3830
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-field__label", children: d.label.length > 26 ? d.label.slice(0, 26) + "\u2026" : d.label }),
|
|
3831
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fc-flow-field__type", children: d.fieldType })
|
|
3832
|
+
] }),
|
|
3833
|
+
d.hasCondition && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fc-flow-badge", style: { background: color }, title: "Has showIf condition", children: "?" }),
|
|
3834
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Handle, { type: "source", position: react$1.Position.Right, className: "fc-flow-handle" })
|
|
3835
|
+
]
|
|
3836
|
+
}
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
var FieldNode = react.memo(FieldNodeInner);
|
|
3840
|
+
var logicFlowNodeTypes = {
|
|
3841
|
+
sectionNode: SectionNode,
|
|
3842
|
+
fieldNode: FieldNode
|
|
3843
|
+
};
|
|
3844
|
+
function ConditionEdgeInner({
|
|
3845
|
+
id,
|
|
3846
|
+
sourceX,
|
|
3847
|
+
sourceY,
|
|
3848
|
+
targetX,
|
|
3849
|
+
targetY,
|
|
3850
|
+
sourcePosition,
|
|
3851
|
+
targetPosition,
|
|
3852
|
+
label,
|
|
3853
|
+
style,
|
|
3854
|
+
data,
|
|
3855
|
+
markerEnd
|
|
3856
|
+
}) {
|
|
3857
|
+
const [edgePath, labelX, labelY] = react$1.getBezierPath({
|
|
3858
|
+
sourceX,
|
|
3859
|
+
sourceY,
|
|
3860
|
+
sourcePosition,
|
|
3861
|
+
targetX,
|
|
3862
|
+
targetY,
|
|
3863
|
+
targetPosition
|
|
3864
|
+
});
|
|
3865
|
+
const isJump = data?.edgeType === "onExit";
|
|
3866
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3867
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3868
|
+
react$1.BaseEdge,
|
|
3869
|
+
{
|
|
3870
|
+
id,
|
|
3871
|
+
path: edgePath,
|
|
3872
|
+
markerEnd,
|
|
3873
|
+
style: {
|
|
3874
|
+
stroke: isJump ? "#f59e0b" : "var(--primary, #3b82f6)",
|
|
3875
|
+
strokeWidth: 1.5,
|
|
3876
|
+
strokeOpacity: 0.7,
|
|
3877
|
+
...style
|
|
3878
|
+
}
|
|
3879
|
+
}
|
|
3880
|
+
),
|
|
3881
|
+
label && /* @__PURE__ */ jsxRuntime.jsx(react$1.EdgeLabelRenderer, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3882
|
+
"div",
|
|
3883
|
+
{
|
|
3884
|
+
className: "fc-flow-edge-label",
|
|
3885
|
+
style: {
|
|
3886
|
+
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
|
3887
|
+
background: isJump ? "#f59e0b" : "var(--primary, #3b82f6)"
|
|
3888
|
+
},
|
|
3889
|
+
children: label
|
|
3890
|
+
}
|
|
3891
|
+
) })
|
|
3892
|
+
] });
|
|
3893
|
+
}
|
|
3894
|
+
var ConditionEdge = react.memo(ConditionEdgeInner);
|
|
3895
|
+
function PipelineEdgeInner({
|
|
3896
|
+
id,
|
|
3897
|
+
sourceX,
|
|
3898
|
+
sourceY,
|
|
3899
|
+
targetX,
|
|
3900
|
+
targetY,
|
|
3901
|
+
sourcePosition,
|
|
3902
|
+
targetPosition,
|
|
3903
|
+
markerEnd
|
|
3904
|
+
}) {
|
|
3905
|
+
const [edgePath] = react$1.getBezierPath({
|
|
3906
|
+
sourceX,
|
|
3907
|
+
sourceY,
|
|
3908
|
+
sourcePosition,
|
|
3909
|
+
targetX,
|
|
3910
|
+
targetY,
|
|
3911
|
+
targetPosition
|
|
3912
|
+
});
|
|
3913
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3914
|
+
react$1.BaseEdge,
|
|
3915
|
+
{
|
|
3916
|
+
id,
|
|
3917
|
+
path: edgePath,
|
|
3918
|
+
markerEnd,
|
|
3919
|
+
style: {
|
|
3920
|
+
stroke: "var(--muted-foreground, #71717a)",
|
|
3921
|
+
strokeWidth: 2,
|
|
3922
|
+
strokeOpacity: 0.4
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
);
|
|
3926
|
+
}
|
|
3927
|
+
var PipelineEdge = react.memo(PipelineEdgeInner);
|
|
3928
|
+
var logicFlowEdgeTypes = {
|
|
3929
|
+
conditionEdge: ConditionEdge,
|
|
3930
|
+
pipelineEdge: PipelineEdge
|
|
3931
|
+
};
|
|
3932
|
+
function findQuestion2(schema, fieldId) {
|
|
3933
|
+
for (const section of schema.sections) {
|
|
3934
|
+
for (const q of section.questions) {
|
|
3935
|
+
if (q.id === fieldId) return q;
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
return void 0;
|
|
3939
|
+
}
|
|
3940
|
+
function updateQuestionInSchema(schema, fieldId, updates) {
|
|
3941
|
+
return {
|
|
3942
|
+
...schema,
|
|
3943
|
+
sections: schema.sections.map((section) => ({
|
|
3944
|
+
...section,
|
|
3945
|
+
questions: section.questions.map(
|
|
3946
|
+
(q) => q.id === fieldId ? { ...q, ...updates } : q
|
|
3947
|
+
)
|
|
3948
|
+
}))
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
function removeConditionForSource(showIf, sourceFieldId) {
|
|
3952
|
+
if (!showIf) return void 0;
|
|
3953
|
+
if (showIf.field === sourceFieldId) return void 0;
|
|
3954
|
+
if (showIf.field && showIf.field !== sourceFieldId) return showIf;
|
|
3955
|
+
if (showIf.conditions) {
|
|
3956
|
+
const filtered = showIf.conditions.map((c) => removeConditionForSource(c, sourceFieldId)).filter((c) => c !== void 0);
|
|
3957
|
+
if (filtered.length === 0) return void 0;
|
|
3958
|
+
if (filtered.length === 1) return filtered[0];
|
|
3959
|
+
return { ...showIf, conditions: filtered };
|
|
3960
|
+
}
|
|
3961
|
+
return showIf;
|
|
3962
|
+
}
|
|
3963
|
+
var defaultEdgeOptions = {
|
|
3964
|
+
markerEnd: {
|
|
3965
|
+
type: react$1.MarkerType.ArrowClosed,
|
|
3966
|
+
width: 16,
|
|
3967
|
+
height: 12
|
|
3968
|
+
}
|
|
3969
|
+
};
|
|
3970
|
+
function LogicFlowEditor({ schema, onChange, onClose }) {
|
|
3971
|
+
const noSections = schema.sections.length === 0;
|
|
3972
|
+
const flowGraph = react.useMemo(() => buildReactFlowGraph(schema), [schema]);
|
|
3973
|
+
const [nodes, setNodes, onNodesChange] = react$1.useNodesState(flowGraph.nodes);
|
|
3974
|
+
const [edges, setEdges, onEdgesChange] = react$1.useEdgesState(flowGraph.edges);
|
|
3975
|
+
const [selectedFieldId, setSelectedFieldId] = react.useState(null);
|
|
3976
|
+
const selectedQuestion = selectedFieldId ? findQuestion2(schema, selectedFieldId) : void 0;
|
|
3977
|
+
react.useEffect(() => {
|
|
3978
|
+
setNodes(flowGraph.nodes);
|
|
3979
|
+
setEdges(flowGraph.edges);
|
|
3980
|
+
}, [flowGraph, setNodes, setEdges]);
|
|
3981
|
+
const sectionIds = react.useMemo(() => {
|
|
3982
|
+
const ids = [];
|
|
3983
|
+
for (const section of schema.sections) {
|
|
3984
|
+
ids.push(section.id);
|
|
3985
|
+
}
|
|
3986
|
+
return ids;
|
|
3987
|
+
}, [schema]);
|
|
3988
|
+
const onNodeClick = react.useCallback(
|
|
3989
|
+
(_event, node) => {
|
|
3990
|
+
if (node.type === "fieldNode") {
|
|
3991
|
+
setSelectedFieldId(node.id);
|
|
3992
|
+
}
|
|
3993
|
+
},
|
|
3994
|
+
[]
|
|
3995
|
+
);
|
|
3996
|
+
const onConnect = react.useCallback(
|
|
3997
|
+
(connection) => {
|
|
3998
|
+
if (!connection.source || !connection.target) return;
|
|
3999
|
+
const targetQuestion = findQuestion2(schema, connection.target);
|
|
4000
|
+
if (!targetQuestion) return;
|
|
4001
|
+
const newCondition = {
|
|
4002
|
+
field: connection.source,
|
|
4003
|
+
operator: "exists"
|
|
4004
|
+
};
|
|
4005
|
+
let updatedShowIf;
|
|
4006
|
+
const existing = targetQuestion.showIf;
|
|
4007
|
+
if (!existing) {
|
|
4008
|
+
updatedShowIf = { combine: "AND", conditions: [newCondition] };
|
|
4009
|
+
} else {
|
|
4010
|
+
updatedShowIf = {
|
|
4011
|
+
...existing,
|
|
4012
|
+
conditions: [...existing.conditions ?? [], newCondition]
|
|
4013
|
+
};
|
|
4014
|
+
}
|
|
4015
|
+
const updatedSchema = updateQuestionInSchema(schema, connection.target, {
|
|
4016
|
+
showIf: updatedShowIf
|
|
4017
|
+
});
|
|
4018
|
+
onChange(updatedSchema);
|
|
4019
|
+
setSelectedFieldId(connection.target);
|
|
4020
|
+
},
|
|
4021
|
+
[schema, onChange]
|
|
4022
|
+
);
|
|
4023
|
+
const onEdgesDelete = react.useCallback(
|
|
4024
|
+
(deletedEdges) => {
|
|
4025
|
+
let updatedSchema = schema;
|
|
4026
|
+
for (const edge of deletedEdges) {
|
|
4027
|
+
const edgeData = edge.data;
|
|
4028
|
+
if (!edgeData) continue;
|
|
4029
|
+
const targetId = edge.target;
|
|
4030
|
+
const sourceId = edge.source;
|
|
4031
|
+
const edgeType = edgeData.edgeType;
|
|
4032
|
+
if (edgeType === "showIf") {
|
|
4033
|
+
const question = findQuestion2(updatedSchema, targetId);
|
|
4034
|
+
if (!question) continue;
|
|
4035
|
+
const updatedShowIf = removeConditionForSource(
|
|
4036
|
+
question.showIf,
|
|
4037
|
+
sourceId
|
|
4038
|
+
);
|
|
4039
|
+
updatedSchema = updateQuestionInSchema(updatedSchema, targetId, {
|
|
4040
|
+
showIf: updatedShowIf
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
onChange(updatedSchema);
|
|
4045
|
+
},
|
|
4046
|
+
[schema, onChange]
|
|
4047
|
+
);
|
|
4048
|
+
const handleConditionUpdate = react.useCallback(
|
|
4049
|
+
(updates) => {
|
|
4050
|
+
if (!selectedFieldId) return;
|
|
4051
|
+
const updatedSchema = updateQuestionInSchema(schema, selectedFieldId, updates);
|
|
4052
|
+
onChange(updatedSchema);
|
|
4053
|
+
},
|
|
4054
|
+
[schema, selectedFieldId, onChange]
|
|
4055
|
+
);
|
|
4056
|
+
const showIfCount = flowGraph.edges.filter(
|
|
4057
|
+
(e) => e.data?.edgeType === "showIf"
|
|
4058
|
+
).length;
|
|
4059
|
+
const jumpCount = flowGraph.edges.filter(
|
|
4060
|
+
(e) => e.data?.edgeType === "onExit"
|
|
4061
|
+
).length;
|
|
4062
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex flex-col bg-background/95 backdrop-blur-sm", children: [
|
|
4063
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-12 shrink-0 flex items-center justify-between px-4 border-b border-border bg-card", children: [
|
|
4064
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
4065
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 16, className: "text-primary" }),
|
|
4066
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold", children: "Logic Flow" }),
|
|
4067
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
4068
|
+
showIfCount,
|
|
4069
|
+
" condition",
|
|
4070
|
+
showIfCount !== 1 ? "s" : "",
|
|
4071
|
+
jumpCount > 0 && ` \xB7 ${jumpCount} jump${jumpCount !== 1 ? "s" : ""}`,
|
|
4072
|
+
" \xB7 ",
|
|
4073
|
+
sectionIds.length,
|
|
4074
|
+
" section",
|
|
4075
|
+
sectionIds.length !== 1 ? "s" : ""
|
|
4076
|
+
] })
|
|
4077
|
+
] }),
|
|
4078
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
4079
|
+
/* @__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" }),
|
|
4080
|
+
/* @__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 }) })
|
|
4081
|
+
] })
|
|
4082
|
+
] }),
|
|
4083
|
+
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: [
|
|
4084
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 32, className: "mx-auto opacity-40" }),
|
|
4085
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: "No sections found" }),
|
|
4086
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs max-w-xs", children: "Add sections and fields to your form to see the logic flow pipeline." })
|
|
4087
|
+
] }) }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 flex", children: [
|
|
4088
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4089
|
+
react$1.ReactFlow,
|
|
4090
|
+
{
|
|
4091
|
+
nodes,
|
|
4092
|
+
edges,
|
|
4093
|
+
onNodesChange,
|
|
4094
|
+
onEdgesChange,
|
|
4095
|
+
onNodeClick,
|
|
4096
|
+
onConnect,
|
|
4097
|
+
onEdgesDelete,
|
|
4098
|
+
nodeTypes: logicFlowNodeTypes,
|
|
4099
|
+
edgeTypes: logicFlowEdgeTypes,
|
|
4100
|
+
defaultEdgeOptions,
|
|
4101
|
+
fitView: true,
|
|
4102
|
+
fitViewOptions: { padding: 0.2 },
|
|
4103
|
+
deleteKeyCode: ["Backspace", "Delete"],
|
|
4104
|
+
proOptions: { hideAttribution: true },
|
|
4105
|
+
children: [
|
|
4106
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Background, { gap: 20, size: 1 }),
|
|
4107
|
+
/* @__PURE__ */ jsxRuntime.jsx(react$1.Controls, { showInteractive: false }),
|
|
4108
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4109
|
+
react$1.MiniMap,
|
|
4110
|
+
{
|
|
4111
|
+
nodeColor: (node) => {
|
|
4112
|
+
const d = node.data;
|
|
4113
|
+
const idx = d.colorIndex ?? 0;
|
|
4114
|
+
return SECTION_COLORS[idx % SECTION_COLORS.length];
|
|
4115
|
+
},
|
|
4116
|
+
maskColor: "rgba(0, 0, 0, 0.3)"
|
|
4117
|
+
}
|
|
4118
|
+
)
|
|
4119
|
+
]
|
|
4120
|
+
}
|
|
4121
|
+
) }),
|
|
4122
|
+
selectedFieldId && selectedQuestion && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-72 border-l border-border bg-card overflow-auto", children: [
|
|
4123
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-3 border-b border-border flex items-center justify-between", children: [
|
|
4124
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4125
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-semibold truncate", children: selectedQuestion.label || selectedQuestion.id }),
|
|
4126
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground", children: selectedQuestion.type })
|
|
4127
|
+
] }),
|
|
4128
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4129
|
+
fieldcraftReact.Button,
|
|
4130
|
+
{
|
|
4131
|
+
variant: "ghost",
|
|
4132
|
+
size: "icon-xs",
|
|
4133
|
+
onClick: () => setSelectedFieldId(null),
|
|
4134
|
+
title: "Close panel",
|
|
4135
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { size: 14, strokeWidth: 1.75 })
|
|
4136
|
+
}
|
|
4137
|
+
)
|
|
4138
|
+
] }),
|
|
4139
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4140
|
+
ConditionEditor,
|
|
4141
|
+
{
|
|
4142
|
+
question: selectedQuestion,
|
|
4143
|
+
schema,
|
|
4144
|
+
onUpdate: handleConditionUpdate
|
|
4145
|
+
}
|
|
4146
|
+
) })
|
|
4147
|
+
] })
|
|
4148
|
+
] }),
|
|
4149
|
+
/* @__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: [
|
|
4150
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4151
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 rounded", style: { background: "var(--muted-foreground)", opacity: 0.4 } }),
|
|
4152
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "flow" })
|
|
4153
|
+
] }),
|
|
4154
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4155
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 bg-primary rounded" }),
|
|
4156
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "showIf" })
|
|
4157
|
+
] }),
|
|
4158
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4159
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-5 h-0.5 rounded", style: { background: "#f59e0b" } }),
|
|
4160
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "jump" })
|
|
4161
|
+
] }),
|
|
4162
|
+
sectionIds.map((id, i) => {
|
|
4163
|
+
const section = schema.sections.find((s) => s.id === id);
|
|
4164
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
4165
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-3 h-3 rounded-sm", style: { background: SECTION_COLORS[i % SECTION_COLORS.length] } }),
|
|
4166
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: section?.title || id })
|
|
4167
|
+
] }, id);
|
|
4168
|
+
})
|
|
4169
|
+
] })
|
|
4170
|
+
] });
|
|
4171
|
+
}
|
|
4172
|
+
function TemplateGallery({ templates, onSelect, onClose }) {
|
|
4173
|
+
const [search, setSearch] = react.useState("");
|
|
4174
|
+
const [activeCategory, setActiveCategory] = react.useState("all");
|
|
4175
|
+
const [confirmTemplate, setConfirmTemplate] = react.useState(null);
|
|
4176
|
+
const categories = react.useMemo(() => {
|
|
4177
|
+
const cats = /* @__PURE__ */ new Set();
|
|
4178
|
+
for (const t of templates) cats.add(t.meta.category);
|
|
4179
|
+
return ["all", ...Array.from(cats)];
|
|
4180
|
+
}, [templates]);
|
|
4181
|
+
const filtered = react.useMemo(() => {
|
|
4182
|
+
return templates.filter((t) => {
|
|
4183
|
+
const matchesCategory = activeCategory === "all" || t.meta.category === activeCategory;
|
|
4184
|
+
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()));
|
|
4185
|
+
return matchesCategory && matchesSearch;
|
|
4186
|
+
});
|
|
4187
|
+
}, [templates, activeCategory, search]);
|
|
4188
|
+
const handleConfirm = () => {
|
|
4189
|
+
if (confirmTemplate) {
|
|
4190
|
+
onSelect(confirmTemplate);
|
|
4191
|
+
setConfirmTemplate(null);
|
|
4192
|
+
onClose();
|
|
4193
|
+
}
|
|
4194
|
+
};
|
|
4195
|
+
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: [
|
|
4196
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 flex items-center justify-between px-4 py-3 border-b border-border", children: [
|
|
4197
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
4198
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutTemplate, { size: 18, className: "text-primary", strokeWidth: 1.75 }),
|
|
4199
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold", children: "Template Gallery" }),
|
|
4200
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
4201
|
+
"(",
|
|
4202
|
+
templates.length,
|
|
4203
|
+
" templates)"
|
|
4204
|
+
] })
|
|
4205
|
+
] }),
|
|
4206
|
+
/* @__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 }) })
|
|
4207
|
+
] }),
|
|
4208
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "shrink-0 px-4 py-2 space-y-2 border-b border-border", children: [
|
|
4209
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative", children: [
|
|
4210
|
+
/* @__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 }),
|
|
4211
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4212
|
+
"input",
|
|
4213
|
+
{
|
|
4214
|
+
type: "text",
|
|
4215
|
+
placeholder: "Search templates...",
|
|
4216
|
+
value: search,
|
|
4217
|
+
onChange: (e) => setSearch(e.target.value),
|
|
4218
|
+
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"
|
|
4219
|
+
}
|
|
4220
|
+
)
|
|
4221
|
+
] }),
|
|
4222
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-1 overflow-x-auto pb-0.5", children: categories.map((cat) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4223
|
+
"button",
|
|
4224
|
+
{
|
|
4225
|
+
type: "button",
|
|
4226
|
+
onClick: () => setActiveCategory(cat),
|
|
4227
|
+
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"}`,
|
|
4228
|
+
children: cat === "all" ? "All" : cat.charAt(0).toUpperCase() + cat.slice(1)
|
|
4229
|
+
},
|
|
4230
|
+
cat
|
|
4231
|
+
)) })
|
|
4232
|
+
] }),
|
|
4233
|
+
/* @__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(
|
|
4234
|
+
"button",
|
|
4235
|
+
{
|
|
4236
|
+
type: "button",
|
|
4237
|
+
onClick: () => setConfirmTemplate(template),
|
|
4238
|
+
className: "text-left p-3 rounded-lg border border-border bg-background hover:border-primary/50 hover:bg-primary/5 transition-colors group",
|
|
4239
|
+
children: [
|
|
4240
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-2 mb-1.5", children: [
|
|
4241
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-medium text-foreground group-hover:text-primary transition-colors", children: template.meta.name }),
|
|
4242
|
+
/* @__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 })
|
|
4243
|
+
] }),
|
|
4244
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mb-2 line-clamp-2", children: template.meta.description }),
|
|
4245
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-xs text-muted-foreground", children: [
|
|
4246
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
|
|
4247
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { size: 11, strokeWidth: 1.75 }),
|
|
4248
|
+
template.meta.fieldCount,
|
|
4249
|
+
" field",
|
|
4250
|
+
template.meta.fieldCount !== 1 ? "s" : ""
|
|
4251
|
+
] }),
|
|
4252
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex items-center gap-1", children: [
|
|
4253
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Layers, { size: 11, strokeWidth: 1.75 }),
|
|
4254
|
+
template.meta.sectionCount,
|
|
4255
|
+
" section",
|
|
4256
|
+
template.meta.sectionCount !== 1 ? "s" : ""
|
|
4257
|
+
] })
|
|
4258
|
+
] })
|
|
4259
|
+
]
|
|
4260
|
+
},
|
|
4261
|
+
template.meta.id
|
|
4262
|
+
)) }) }),
|
|
4263
|
+
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: [
|
|
4264
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm", children: [
|
|
4265
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Use " }),
|
|
4266
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: confirmTemplate.meta.name }),
|
|
4267
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "? This will replace the current form." })
|
|
4268
|
+
] }),
|
|
4269
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 shrink-0", children: [
|
|
4270
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", onClick: () => setConfirmTemplate(null), children: "Cancel" }),
|
|
4271
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { size: "sm", onClick: handleConfirm, children: "Use Template" })
|
|
4272
|
+
] })
|
|
4273
|
+
] })
|
|
4274
|
+
] }) });
|
|
4275
|
+
}
|
|
4276
|
+
function useDebouncedValidation(delayMs = 500) {
|
|
4277
|
+
const [errors, setErrors] = react.useState([]);
|
|
4278
|
+
const timerRef = react.useRef(null);
|
|
4279
|
+
const validate = react.useCallback(
|
|
4280
|
+
(text) => {
|
|
4281
|
+
if (timerRef.current) {
|
|
4282
|
+
clearTimeout(timerRef.current);
|
|
4283
|
+
}
|
|
4284
|
+
timerRef.current = setTimeout(() => {
|
|
4285
|
+
const result = [];
|
|
4286
|
+
let parsed;
|
|
4287
|
+
try {
|
|
4288
|
+
parsed = JSON.parse(text);
|
|
4289
|
+
} catch (err) {
|
|
4290
|
+
if (err instanceof SyntaxError) {
|
|
4291
|
+
const posMatch = err.message.match(/position\s+(\d+)/i);
|
|
4292
|
+
let line = 1;
|
|
4293
|
+
let column = 1;
|
|
4294
|
+
if (posMatch) {
|
|
4295
|
+
const pos = parseInt(posMatch[1], 10);
|
|
4296
|
+
const upToPos = text.slice(0, pos);
|
|
4297
|
+
line = (upToPos.match(/\n/g) || []).length + 1;
|
|
4298
|
+
column = pos - upToPos.lastIndexOf("\n");
|
|
4299
|
+
}
|
|
4300
|
+
result.push({ message: err.message, line, column, endLine: line, endColumn: column + 1 });
|
|
4301
|
+
} else {
|
|
4302
|
+
result.push({ message: "Invalid JSON" });
|
|
4303
|
+
}
|
|
4304
|
+
setErrors(result);
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4307
|
+
try {
|
|
4308
|
+
fieldcraftCore.validateSchema(parsed);
|
|
4309
|
+
} catch (err) {
|
|
4310
|
+
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4311
|
+
for (const issue of err.issues) {
|
|
4312
|
+
result.push({
|
|
4313
|
+
message: issue.message,
|
|
4314
|
+
path: issue.path?.join(".")
|
|
4315
|
+
});
|
|
4316
|
+
}
|
|
4317
|
+
} else if (err instanceof Error) {
|
|
4318
|
+
result.push({ message: err.message });
|
|
4319
|
+
}
|
|
4320
|
+
}
|
|
4321
|
+
setErrors(result);
|
|
4322
|
+
}, delayMs);
|
|
4323
|
+
},
|
|
4324
|
+
[delayMs]
|
|
4325
|
+
);
|
|
4326
|
+
return { errors, validate };
|
|
4327
|
+
}
|
|
4328
|
+
var ThemeCtx = react.createContext({});
|
|
4329
|
+
function useBuilderTheme() {
|
|
4330
|
+
return react.useContext(ThemeCtx);
|
|
4331
|
+
}
|
|
4332
|
+
function themeToCssVars(theme) {
|
|
4333
|
+
const vars = {};
|
|
4334
|
+
if (theme.background) vars["--background"] = theme.background;
|
|
4335
|
+
if (theme.foreground) vars["--foreground"] = theme.foreground;
|
|
4336
|
+
if (theme.card) {
|
|
4337
|
+
vars["--card"] = theme.card;
|
|
4338
|
+
vars["--card-foreground"] = theme.foreground ?? "";
|
|
4339
|
+
vars["--popover"] = theme.card;
|
|
4340
|
+
vars["--popover-foreground"] = theme.foreground ?? "";
|
|
4341
|
+
}
|
|
4342
|
+
if (theme.primary) vars["--primary"] = theme.primary;
|
|
4343
|
+
if (theme.primaryForeground) vars["--primary-foreground"] = theme.primaryForeground;
|
|
4344
|
+
if (theme.secondary) vars["--secondary"] = theme.secondary;
|
|
4345
|
+
if (theme.secondaryForeground) vars["--secondary-foreground"] = theme.secondaryForeground;
|
|
4346
|
+
if (theme.muted) vars["--muted"] = theme.muted;
|
|
4347
|
+
if (theme.mutedForeground) vars["--muted-foreground"] = theme.mutedForeground;
|
|
4348
|
+
if (theme.accent) vars["--accent"] = theme.accent;
|
|
4349
|
+
if (theme.accentForeground) vars["--accent-foreground"] = theme.accentForeground;
|
|
4350
|
+
if (theme.destructive) vars["--destructive"] = theme.destructive;
|
|
4351
|
+
if (theme.destructiveForeground) vars["--destructive-foreground"] = theme.destructiveForeground;
|
|
4352
|
+
if (theme.border) vars["--border"] = theme.border;
|
|
4353
|
+
if (theme.input) vars["--input"] = theme.input;
|
|
4354
|
+
if (theme.ring) vars["--ring"] = theme.ring;
|
|
4355
|
+
if (theme.radius) vars["--radius"] = theme.radius;
|
|
4356
|
+
if (theme.surface) vars["--fcb-surface"] = theme.surface;
|
|
4357
|
+
if (theme.surfaceHover) vars["--fcb-surface-hover"] = theme.surfaceHover;
|
|
4358
|
+
if (theme.canvas) vars["--fcb-canvas"] = theme.canvas;
|
|
4359
|
+
if (theme.panel) vars["--fcb-panel"] = theme.panel;
|
|
4360
|
+
if (theme.borderStrong) vars["--fcb-border-strong"] = theme.borderStrong;
|
|
4361
|
+
if (theme.textDim) vars["--fcb-text-dim"] = theme.textDim;
|
|
4362
|
+
return vars;
|
|
4363
|
+
}
|
|
4364
|
+
function FormBuilderThemeProvider({ theme, children }) {
|
|
4365
|
+
const resolved = theme ?? {};
|
|
4366
|
+
const cssVars = react.useMemo(() => themeToCssVars(resolved), [resolved]);
|
|
4367
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ThemeCtx.Provider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fcb-root": "", style: cssVars, className: "w-full h-full", children }) });
|
|
4368
|
+
}
|
|
4369
|
+
var FormBuilderErrorBoundary = class extends react.Component {
|
|
4370
|
+
constructor(props) {
|
|
4371
|
+
super(props);
|
|
4372
|
+
this.state = { hasError: false, error: null };
|
|
4373
|
+
}
|
|
4374
|
+
static getDerivedStateFromError(error) {
|
|
4375
|
+
return { hasError: true, error };
|
|
4376
|
+
}
|
|
4377
|
+
componentDidCatch(error, info) {
|
|
4378
|
+
console.error("[FormBuilder] Render error:", error, info.componentStack);
|
|
4379
|
+
}
|
|
4380
|
+
render() {
|
|
4381
|
+
if (this.state.hasError) {
|
|
4382
|
+
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: [
|
|
4383
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold", children: "Something went wrong" }),
|
|
4384
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "The form builder encountered an unexpected error. Your schema data is preserved." }),
|
|
4385
|
+
/* @__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 }),
|
|
4386
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4387
|
+
fieldcraftReact.Button,
|
|
4388
|
+
{
|
|
4389
|
+
onClick: () => this.setState({ hasError: false, error: null }),
|
|
4390
|
+
variant: "outline",
|
|
4391
|
+
children: "Try Again"
|
|
4392
|
+
}
|
|
4393
|
+
)
|
|
4394
|
+
] }) });
|
|
4395
|
+
}
|
|
4396
|
+
return this.props.children;
|
|
4397
|
+
}
|
|
4398
|
+
};
|
|
4399
|
+
function FormBuilderCore(props) {
|
|
4400
|
+
const { initialSchema = DEFAULT_SCHEMA, onChange, onSave, height = "100vh", theme, className, toolbarExtra, questionTypes, palette, preview, templates, schemaUrl } = props;
|
|
4401
|
+
const [viewMode, setViewMode] = react.useState("design");
|
|
4402
|
+
const [jsonText, setJsonText] = react.useState("");
|
|
4403
|
+
const [jsonSwitchError, setJsonSwitchError] = react.useState(null);
|
|
4404
|
+
const [showLogicMap, setShowLogicMap] = react.useState(false);
|
|
4405
|
+
const [showTemplateGallery, setShowTemplateGallery] = react.useState(false);
|
|
4406
|
+
const [saveValidationErrors, setSaveValidationErrors] = react.useState(null);
|
|
4407
|
+
const [schemaUrlLoading, setSchemaUrlLoading] = react.useState(!!schemaUrl);
|
|
4408
|
+
const [schemaUrlError, setSchemaUrlError] = react.useState(null);
|
|
4409
|
+
const { errors: jsonErrors, validate: validateJson } = useDebouncedValidation(400);
|
|
4410
|
+
const mergedQuestionTypes = questionTypes ? { ...QUESTION_TYPE_INFO, ...questionTypes } : QUESTION_TYPE_INFO;
|
|
4411
|
+
const builderState = useBuilderState(initialSchema);
|
|
4412
|
+
const dragDrop = useDragDrop(builderState);
|
|
4413
|
+
const fileInputRef = react.useRef(null);
|
|
4414
|
+
const [mobilePanel, setMobilePanel] = react.useState("none");
|
|
4415
|
+
react.useEffect(() => {
|
|
4416
|
+
if (builderState.selectedItem && window.innerWidth < 768) {
|
|
4417
|
+
setMobilePanel("properties");
|
|
4418
|
+
}
|
|
4419
|
+
}, [builderState.selectedItem]);
|
|
4420
|
+
react.useEffect(() => {
|
|
4421
|
+
if (!dragDrop.activeDragItem && mobilePanel === "palette") {
|
|
4422
|
+
setMobilePanel("none");
|
|
4423
|
+
}
|
|
4424
|
+
}, [dragDrop.activeDragItem]);
|
|
4425
|
+
react.useEffect(() => {
|
|
4426
|
+
if (!schemaUrl) return;
|
|
4427
|
+
let cancelled = false;
|
|
4428
|
+
setSchemaUrlLoading(true);
|
|
4429
|
+
setSchemaUrlError(null);
|
|
4430
|
+
fetch(schemaUrl).then((res) => {
|
|
4431
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
4432
|
+
return res.json();
|
|
4433
|
+
}).then((json) => {
|
|
4434
|
+
if (cancelled) return;
|
|
4435
|
+
try {
|
|
4436
|
+
const validated = fieldcraftCore.validateSchema(json);
|
|
4437
|
+
builderState.resetSchema(validated);
|
|
4438
|
+
} catch (err) {
|
|
4439
|
+
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4440
|
+
setSchemaUrlError(`Invalid schema: ${err.issues[0]?.message ?? "validation failed"}`);
|
|
4441
|
+
} else {
|
|
4442
|
+
setSchemaUrlError(err.message);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
setSchemaUrlLoading(false);
|
|
4446
|
+
}).catch((err) => {
|
|
4447
|
+
if (cancelled) return;
|
|
4448
|
+
setSchemaUrlError(err.message ?? "Failed to fetch schema");
|
|
4449
|
+
setSchemaUrlLoading(false);
|
|
4450
|
+
});
|
|
4451
|
+
return () => {
|
|
4452
|
+
cancelled = true;
|
|
4453
|
+
};
|
|
4454
|
+
}, [schemaUrl]);
|
|
4455
|
+
react.useEffect(() => {
|
|
4456
|
+
if (onChange && builderState.isDirty) {
|
|
4457
|
+
onChange(builderState.schema);
|
|
4458
|
+
}
|
|
4459
|
+
}, [builderState.schema, builderState.isDirty, onChange]);
|
|
4460
|
+
const handleSave = react.useCallback(() => {
|
|
4461
|
+
if (!onSave) return;
|
|
4462
|
+
try {
|
|
4463
|
+
fieldcraftCore.validateSchema(builderState.schema);
|
|
4464
|
+
setSaveValidationErrors(null);
|
|
4465
|
+
onSave(builderState.schema);
|
|
4466
|
+
builderState.markClean();
|
|
4467
|
+
} catch (err) {
|
|
4468
|
+
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4469
|
+
setSaveValidationErrors(err.issues.map((issue) => issue.message));
|
|
4470
|
+
} else {
|
|
4471
|
+
setSaveValidationErrors([err.message ?? "Unknown validation error"]);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
}, [onSave, builderState]);
|
|
4475
|
+
const handleExport = react.useCallback(() => {
|
|
4476
|
+
const json = JSON.stringify(builderState.schema, null, 2);
|
|
4477
|
+
const blob = new Blob([json], { type: "application/json" });
|
|
4478
|
+
const url = URL.createObjectURL(blob);
|
|
4479
|
+
const a = document.createElement("a");
|
|
4480
|
+
a.href = url;
|
|
4481
|
+
a.download = `${builderState.schema.title?.replace(/\s+/g, "-").toLowerCase() || "form"}-schema.json`;
|
|
4482
|
+
a.click();
|
|
4483
|
+
URL.revokeObjectURL(url);
|
|
4484
|
+
}, [builderState.schema]);
|
|
4485
|
+
const handleImport = react.useCallback(() => {
|
|
4486
|
+
fileInputRef.current?.click();
|
|
4487
|
+
}, []);
|
|
4488
|
+
const handleFileChange = react.useCallback(
|
|
4489
|
+
(e) => {
|
|
4490
|
+
const file = e.target.files?.[0];
|
|
4491
|
+
if (!file) return;
|
|
4492
|
+
const reader = new FileReader();
|
|
4493
|
+
reader.onload = (event) => {
|
|
4494
|
+
try {
|
|
4495
|
+
const parsed = JSON.parse(event.target?.result);
|
|
4496
|
+
const validated = fieldcraftCore.validateSchema(parsed);
|
|
4497
|
+
builderState.resetSchema(validated);
|
|
4498
|
+
} catch (err) {
|
|
4499
|
+
if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4500
|
+
const details = err.issues.slice(0, 3).map((i) => `\u2022 ${i.path.join(".")}: ${i.message}`).join("\n");
|
|
4501
|
+
alert(`Invalid schema:
|
|
4502
|
+
${details}`);
|
|
4503
|
+
} else if (err instanceof SyntaxError) {
|
|
4504
|
+
alert("Failed to parse JSON file.");
|
|
4505
|
+
} else {
|
|
4506
|
+
alert("Invalid schema file.");
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
};
|
|
4510
|
+
reader.readAsText(file);
|
|
4511
|
+
e.target.value = "";
|
|
4512
|
+
},
|
|
4513
|
+
[builderState]
|
|
4514
|
+
);
|
|
4515
|
+
const handleViewModeChange = react.useCallback(
|
|
4516
|
+
(newMode) => {
|
|
4517
|
+
setJsonSwitchError(null);
|
|
4518
|
+
if (viewMode === "json" && newMode !== "json") {
|
|
4519
|
+
try {
|
|
4520
|
+
const parsed = JSON.parse(jsonText);
|
|
4521
|
+
const validated = fieldcraftCore.validateSchema(parsed);
|
|
4522
|
+
builderState.resetSchema(validated);
|
|
4523
|
+
} catch (err) {
|
|
4524
|
+
if (err instanceof SyntaxError) {
|
|
4525
|
+
setJsonSwitchError("Cannot switch: JSON has syntax errors");
|
|
4526
|
+
} else if (err instanceof fieldcraftCore.FormEngineSchemaError) {
|
|
4527
|
+
setJsonSwitchError(`Cannot switch: ${err.issues[0]?.message ?? "Invalid schema"}`);
|
|
4528
|
+
} else {
|
|
4529
|
+
setJsonSwitchError("Cannot switch: Invalid schema");
|
|
4530
|
+
}
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
if (newMode === "json") {
|
|
4535
|
+
setJsonText(JSON.stringify(builderState.schema, null, 2));
|
|
4536
|
+
}
|
|
4537
|
+
setViewMode(newMode);
|
|
4538
|
+
},
|
|
4539
|
+
[viewMode, jsonText, builderState]
|
|
4540
|
+
);
|
|
4541
|
+
const handleKeyDown = react.useCallback(
|
|
4542
|
+
(e) => {
|
|
4543
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
4544
|
+
const tag = e.target.tagName;
|
|
4545
|
+
const isEditing = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
|
|
4546
|
+
if (mod && e.key === "z" && !e.shiftKey) {
|
|
4547
|
+
e.preventDefault();
|
|
4548
|
+
builderState.undo();
|
|
4549
|
+
return;
|
|
4550
|
+
}
|
|
4551
|
+
if (mod && e.key === "z" && e.shiftKey) {
|
|
4552
|
+
e.preventDefault();
|
|
4553
|
+
builderState.redo();
|
|
4554
|
+
return;
|
|
4555
|
+
}
|
|
4556
|
+
if (mod && e.key === "s") {
|
|
4557
|
+
e.preventDefault();
|
|
4558
|
+
handleSave();
|
|
4559
|
+
return;
|
|
4560
|
+
}
|
|
4561
|
+
if (isEditing) return;
|
|
4562
|
+
if (e.key === "Delete" || e.key === "Backspace") {
|
|
4563
|
+
const sel = builderState.selectedItem;
|
|
4564
|
+
if (!sel) return;
|
|
4565
|
+
e.preventDefault();
|
|
4566
|
+
if (sel.type === "question") {
|
|
4567
|
+
builderState.removeQuestion(sel.sectionId, sel.questionId);
|
|
4568
|
+
} else if (sel.type === "section") {
|
|
4569
|
+
builderState.removeSection(sel.sectionId);
|
|
4570
|
+
}
|
|
4571
|
+
return;
|
|
4572
|
+
}
|
|
4573
|
+
if (e.key === "Escape") {
|
|
4574
|
+
if (builderState.selectedItem) {
|
|
4575
|
+
e.preventDefault();
|
|
4576
|
+
builderState.clearSelection();
|
|
4577
|
+
}
|
|
4578
|
+
return;
|
|
4579
|
+
}
|
|
4580
|
+
if (mod && e.key === "d") {
|
|
4581
|
+
const sel = builderState.selectedItem;
|
|
4582
|
+
if (!sel) return;
|
|
4583
|
+
e.preventDefault();
|
|
4584
|
+
if (sel.type === "question") {
|
|
4585
|
+
builderState.duplicateQuestion(sel.sectionId, sel.questionId);
|
|
4586
|
+
} else if (sel.type === "section") {
|
|
4587
|
+
builderState.duplicateSection(sel.sectionId);
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
},
|
|
4591
|
+
[builderState, handleSave]
|
|
4592
|
+
);
|
|
4593
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FormBuilderThemeProvider, { theme, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4594
|
+
core.DndContext,
|
|
4595
|
+
{
|
|
4596
|
+
sensors: dragDrop.sensors,
|
|
4597
|
+
onDragStart: dragDrop.handleDragStart,
|
|
4598
|
+
onDragEnd: dragDrop.handleDragEnd,
|
|
4599
|
+
onDragCancel: dragDrop.handleDragCancel,
|
|
4600
|
+
children: [
|
|
4601
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4602
|
+
"div",
|
|
4603
|
+
{
|
|
4604
|
+
className: cn("flex flex-col bg-background text-foreground", className),
|
|
4605
|
+
style: { height: typeof height === "number" ? `${height}px` : height },
|
|
4606
|
+
onKeyDown: handleKeyDown,
|
|
4607
|
+
tabIndex: -1,
|
|
4608
|
+
role: "application",
|
|
4609
|
+
"aria-label": "Form Builder",
|
|
4610
|
+
children: [
|
|
4611
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-12 shrink-0 hidden md:grid px-4 bg-card border-b border-border", style: { gridTemplateColumns: "1fr auto 1fr" }, role: "toolbar", "aria-label": "Builder toolbar", children: [
|
|
3580
4612
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
3581
4613
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3582
4614
|
fieldcraftReact.Button,
|
|
3583
4615
|
{
|
|
3584
4616
|
variant: "ghost",
|
|
3585
4617
|
size: "icon-sm",
|
|
3586
|
-
onClick: builderState.undo,
|
|
3587
|
-
disabled: !builderState.canUndo,
|
|
3588
|
-
className: "disabled:opacity-30",
|
|
3589
|
-
title: "Undo (Ctrl+Z)",
|
|
3590
|
-
"aria-label": "Undo",
|
|
3591
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { size: 16, strokeWidth: 1.75 })
|
|
4618
|
+
onClick: builderState.undo,
|
|
4619
|
+
disabled: !builderState.canUndo,
|
|
4620
|
+
className: "disabled:opacity-30",
|
|
4621
|
+
title: "Undo (Ctrl+Z)",
|
|
4622
|
+
"aria-label": "Undo",
|
|
4623
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Undo2, { size: 16, strokeWidth: 1.75 })
|
|
4624
|
+
}
|
|
4625
|
+
),
|
|
4626
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4627
|
+
fieldcraftReact.Button,
|
|
4628
|
+
{
|
|
4629
|
+
variant: "ghost",
|
|
4630
|
+
size: "icon-sm",
|
|
4631
|
+
onClick: builderState.redo,
|
|
4632
|
+
disabled: !builderState.canRedo,
|
|
4633
|
+
className: "disabled:opacity-30",
|
|
4634
|
+
title: "Redo (Ctrl+Shift+Z)",
|
|
4635
|
+
"aria-label": "Redo",
|
|
4636
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { size: 16, strokeWidth: 1.75 })
|
|
4637
|
+
}
|
|
4638
|
+
),
|
|
4639
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-2 h-5" }),
|
|
4640
|
+
builderState.isDirty && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-primary", role: "status", children: "Unsaved changes" })
|
|
4641
|
+
] }),
|
|
4642
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-2", children: [
|
|
4643
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 bg-muted rounded-md p-0.5", children: [
|
|
4644
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4645
|
+
fieldcraftReact.Button,
|
|
4646
|
+
{
|
|
4647
|
+
variant: viewMode === "design" ? "secondary" : "ghost",
|
|
4648
|
+
size: "sm",
|
|
4649
|
+
onClick: () => handleViewModeChange("design"),
|
|
4650
|
+
className: "gap-1.5 h-7 px-2.5 text-xs",
|
|
4651
|
+
children: [
|
|
4652
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.PencilRuler, { size: 13, strokeWidth: 1.75 }),
|
|
4653
|
+
"Design"
|
|
4654
|
+
]
|
|
4655
|
+
}
|
|
4656
|
+
),
|
|
4657
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4658
|
+
fieldcraftReact.Button,
|
|
4659
|
+
{
|
|
4660
|
+
variant: viewMode === "preview" ? "secondary" : "ghost",
|
|
4661
|
+
size: "sm",
|
|
4662
|
+
onClick: () => handleViewModeChange("preview"),
|
|
4663
|
+
className: "gap-1.5 h-7 px-2.5 text-xs",
|
|
4664
|
+
children: [
|
|
4665
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, { size: 13, strokeWidth: 1.75 }),
|
|
4666
|
+
"Preview"
|
|
4667
|
+
]
|
|
4668
|
+
}
|
|
4669
|
+
),
|
|
4670
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4671
|
+
fieldcraftReact.Button,
|
|
4672
|
+
{
|
|
4673
|
+
variant: viewMode === "json" ? "secondary" : "ghost",
|
|
4674
|
+
size: "sm",
|
|
4675
|
+
onClick: () => handleViewModeChange("json"),
|
|
4676
|
+
className: "gap-1.5 h-7 px-2.5 text-xs",
|
|
4677
|
+
children: [
|
|
4678
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 13, strokeWidth: 1.75 }),
|
|
4679
|
+
"JSON"
|
|
4680
|
+
]
|
|
4681
|
+
}
|
|
4682
|
+
)
|
|
4683
|
+
] }),
|
|
4684
|
+
toolbarExtra
|
|
4685
|
+
] }),
|
|
4686
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [
|
|
4687
|
+
templates && templates.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4688
|
+
fieldcraftReact.Button,
|
|
4689
|
+
{
|
|
4690
|
+
variant: "ghost",
|
|
4691
|
+
size: "icon-sm",
|
|
4692
|
+
onClick: () => setShowTemplateGallery(true),
|
|
4693
|
+
title: "Browse templates",
|
|
4694
|
+
"aria-label": "Template gallery",
|
|
4695
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutTemplate, { size: 15, strokeWidth: 1.75 })
|
|
3592
4696
|
}
|
|
3593
4697
|
),
|
|
3594
4698
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3596,19 +4700,13 @@ ${details}`);
|
|
|
3596
4700
|
{
|
|
3597
4701
|
variant: "ghost",
|
|
3598
4702
|
size: "icon-sm",
|
|
3599
|
-
onClick:
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
"aria-label": "Redo",
|
|
3604
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Redo2, { size: 16, strokeWidth: 1.75 })
|
|
4703
|
+
onClick: () => setShowLogicMap(true),
|
|
4704
|
+
title: "View branching logic map",
|
|
4705
|
+
"aria-label": "Logic map",
|
|
4706
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.GitBranch, { size: 15, strokeWidth: 1.75 })
|
|
3605
4707
|
}
|
|
3606
4708
|
),
|
|
3607
|
-
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-
|
|
3608
|
-
builderState.isDirty && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-primary", role: "status", children: "Unsaved changes" })
|
|
3609
|
-
] }),
|
|
3610
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center", children: toolbarExtra }),
|
|
3611
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-end gap-1.5", children: [
|
|
4709
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "mx-1 h-5" }),
|
|
3612
4710
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3613
4711
|
fieldcraftReact.Button,
|
|
3614
4712
|
{
|
|
@@ -3652,6 +4750,47 @@ ${details}`);
|
|
|
3652
4750
|
}
|
|
3653
4751
|
) }),
|
|
3654
4752
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center gap-1", children: [
|
|
4753
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-0.5 bg-muted rounded-md p-0.5", children: [
|
|
4754
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4755
|
+
fieldcraftReact.Button,
|
|
4756
|
+
{
|
|
4757
|
+
variant: viewMode === "design" ? "secondary" : "ghost",
|
|
4758
|
+
size: "sm",
|
|
4759
|
+
onClick: () => handleViewModeChange("design"),
|
|
4760
|
+
className: "gap-1 h-7 px-2 text-xs",
|
|
4761
|
+
children: [
|
|
4762
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.PencilRuler, { size: 12, strokeWidth: 1.75 }),
|
|
4763
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Design" })
|
|
4764
|
+
]
|
|
4765
|
+
}
|
|
4766
|
+
),
|
|
4767
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4768
|
+
fieldcraftReact.Button,
|
|
4769
|
+
{
|
|
4770
|
+
variant: viewMode === "preview" ? "secondary" : "ghost",
|
|
4771
|
+
size: "sm",
|
|
4772
|
+
onClick: () => handleViewModeChange("preview"),
|
|
4773
|
+
className: "gap-1 h-7 px-2 text-xs",
|
|
4774
|
+
children: [
|
|
4775
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, { size: 12, strokeWidth: 1.75 }),
|
|
4776
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Preview" })
|
|
4777
|
+
]
|
|
4778
|
+
}
|
|
4779
|
+
),
|
|
4780
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4781
|
+
fieldcraftReact.Button,
|
|
4782
|
+
{
|
|
4783
|
+
variant: viewMode === "json" ? "secondary" : "ghost",
|
|
4784
|
+
size: "sm",
|
|
4785
|
+
onClick: () => handleViewModeChange("json"),
|
|
4786
|
+
className: "gap-1 h-7 px-2 text-xs",
|
|
4787
|
+
children: [
|
|
4788
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 12, strokeWidth: 1.75 }),
|
|
4789
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "JSON" })
|
|
4790
|
+
]
|
|
4791
|
+
}
|
|
4792
|
+
)
|
|
4793
|
+
] }),
|
|
3655
4794
|
toolbarExtra,
|
|
3656
4795
|
/* @__PURE__ */ jsxRuntime.jsxs(fieldcraftReact.Button, { onClick: handleSave, size: "sm", className: "border-0 shadow-sm fcb-glow ml-1", "aria-label": "Save form", children: [
|
|
3657
4796
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Save, { size: 14, strokeWidth: 2 }),
|
|
@@ -3740,7 +4879,50 @@ ${details}`);
|
|
|
3740
4879
|
"aria-hidden": "true"
|
|
3741
4880
|
}
|
|
3742
4881
|
),
|
|
3743
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "
|
|
4882
|
+
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: [
|
|
4883
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: jsonSwitchError }),
|
|
4884
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setJsonSwitchError(null), className: "ml-2 hover:underline", children: "Dismiss" })
|
|
4885
|
+
] }),
|
|
4886
|
+
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: [
|
|
4887
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
|
|
4888
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-semibold text-destructive", children: [
|
|
4889
|
+
"Schema validation failed (",
|
|
4890
|
+
saveValidationErrors.length,
|
|
4891
|
+
" issue",
|
|
4892
|
+
saveValidationErrors.length !== 1 ? "s" : "",
|
|
4893
|
+
")"
|
|
4894
|
+
] }),
|
|
4895
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setSaveValidationErrors(null), className: "text-destructive hover:underline", children: "Dismiss" })
|
|
4896
|
+
] }),
|
|
4897
|
+
/* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "list-disc pl-4 text-destructive/80 space-y-0.5", children: [
|
|
4898
|
+
saveValidationErrors.slice(0, 10).map((msg, i) => /* @__PURE__ */ jsxRuntime.jsx("li", { children: msg }, i)),
|
|
4899
|
+
saveValidationErrors.length > 10 && /* @__PURE__ */ jsxRuntime.jsxs("li", { children: [
|
|
4900
|
+
"...and ",
|
|
4901
|
+
saveValidationErrors.length - 10,
|
|
4902
|
+
" more"
|
|
4903
|
+
] })
|
|
4904
|
+
] })
|
|
4905
|
+
] }),
|
|
4906
|
+
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: [
|
|
4907
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-block w-3 h-3 border-2 border-primary border-t-transparent rounded-full animate-spin" }),
|
|
4908
|
+
"Loading schema from URL..."
|
|
4909
|
+
] }),
|
|
4910
|
+
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: [
|
|
4911
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
4912
|
+
"Failed to load schema: ",
|
|
4913
|
+
schemaUrlError
|
|
4914
|
+
] }),
|
|
4915
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", onClick: () => setSchemaUrlError(null), className: "ml-2 hover:underline", children: "Dismiss" })
|
|
4916
|
+
] }),
|
|
4917
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 flex overflow-hidden relative", children: viewMode === "json" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4918
|
+
JsonEditorPanel,
|
|
4919
|
+
{
|
|
4920
|
+
value: jsonText,
|
|
4921
|
+
onChange: setJsonText,
|
|
4922
|
+
errors: jsonErrors,
|
|
4923
|
+
onValidate: validateJson
|
|
4924
|
+
}
|
|
4925
|
+
) : viewMode === "preview" ? /* @__PURE__ */ jsxRuntime.jsx(FormPreviewPanel, { schema: builderState.schema, preview }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3744
4926
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(QuestionPalette, { questionTypes: mergedQuestionTypes, palette }) }),
|
|
3745
4927
|
/* @__PURE__ */ jsxRuntime.jsx(FormCanvas, { builderState }),
|
|
3746
4928
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsxRuntime.jsx(PropertiesPanel, { builderState }) }),
|
|
@@ -3754,11 +4936,27 @@ ${details}`);
|
|
|
3754
4936
|
"aria-hidden": "true"
|
|
3755
4937
|
}
|
|
3756
4938
|
)
|
|
3757
|
-
] })
|
|
4939
|
+
] }) })
|
|
3758
4940
|
]
|
|
3759
4941
|
}
|
|
3760
4942
|
),
|
|
3761
|
-
/* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { dropAnimation: null, children: dragDrop.activeDragItem && /* @__PURE__ */ jsxRuntime.jsx(DragOverlayContent, { item: dragDrop.activeDragItem, schema: builderState.schema, questionTypes: mergedQuestionTypes }) })
|
|
4943
|
+
/* @__PURE__ */ jsxRuntime.jsx(core.DragOverlay, { dropAnimation: null, children: dragDrop.activeDragItem && /* @__PURE__ */ jsxRuntime.jsx(DragOverlayContent, { item: dragDrop.activeDragItem, schema: builderState.schema, questionTypes: mergedQuestionTypes }) }),
|
|
4944
|
+
showLogicMap && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4945
|
+
LogicFlowEditor,
|
|
4946
|
+
{
|
|
4947
|
+
schema: builderState.schema,
|
|
4948
|
+
onChange: (updated) => builderState.resetSchema(updated),
|
|
4949
|
+
onClose: () => setShowLogicMap(false)
|
|
4950
|
+
}
|
|
4951
|
+
),
|
|
4952
|
+
showTemplateGallery && templates && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4953
|
+
TemplateGallery,
|
|
4954
|
+
{
|
|
4955
|
+
templates,
|
|
4956
|
+
onSelect: (template) => builderState.resetSchema(template.schema),
|
|
4957
|
+
onClose: () => setShowTemplateGallery(false)
|
|
4958
|
+
}
|
|
4959
|
+
)
|
|
3762
4960
|
]
|
|
3763
4961
|
}
|
|
3764
4962
|
) });
|
|
@@ -3813,76 +5011,54 @@ var FormBuilder = requireLicense(FormBuilderInner, "FormBuilder");
|
|
|
3813
5011
|
|
|
3814
5012
|
// src/form-builder/theme/presets.ts
|
|
3815
5013
|
var squaredrDarkPreset = {
|
|
3816
|
-
background: "#
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
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
|
|
5014
|
+
background: "#0F1A1F",
|
|
5015
|
+
foreground: "#E8EFF1",
|
|
5016
|
+
card: "#16242A",
|
|
5017
|
+
primary: "#63BDB4",
|
|
5018
|
+
primaryForeground: "#0F1A1F",
|
|
5019
|
+
secondary: "#182F31",
|
|
5020
|
+
secondaryForeground: "#E8EFF1",
|
|
5021
|
+
muted: "#182F31",
|
|
5022
|
+
mutedForeground: "#8CA1A9",
|
|
5023
|
+
accent: "#182F31",
|
|
5024
|
+
accentForeground: "#E8EFF1",
|
|
5025
|
+
destructive: "#E08072",
|
|
5026
|
+
destructiveForeground: "#0F1A1F",
|
|
5027
|
+
border: "#2A3B42",
|
|
5028
|
+
input: "#2A3B42",
|
|
5029
|
+
ring: "#63BDB4",
|
|
5030
|
+
radius: "0px",
|
|
5031
|
+
surface: "#16242A",
|
|
5032
|
+
surfaceHover: "#182F31",
|
|
5033
|
+
canvas: "#0F1A1F",
|
|
5034
|
+
panel: "#16242A",
|
|
5035
|
+
borderStrong: "#2F4F4C",
|
|
5036
|
+
textDim: "#5E7680"
|
|
3861
5037
|
};
|
|
3862
5038
|
var cleanPreset = {
|
|
3863
|
-
background: "#
|
|
3864
|
-
foreground: "#
|
|
3865
|
-
card: "#
|
|
3866
|
-
primary: "#
|
|
3867
|
-
primaryForeground: "#
|
|
3868
|
-
secondary: "#
|
|
3869
|
-
secondaryForeground: "#
|
|
3870
|
-
muted: "#
|
|
3871
|
-
mutedForeground: "#
|
|
3872
|
-
accent: "#
|
|
3873
|
-
accentForeground: "#
|
|
3874
|
-
destructive: "#
|
|
3875
|
-
destructiveForeground: "#
|
|
3876
|
-
border: "#
|
|
3877
|
-
input: "#
|
|
3878
|
-
ring: "#
|
|
3879
|
-
radius: "
|
|
3880
|
-
surface: "#
|
|
3881
|
-
surfaceHover: "#
|
|
3882
|
-
canvas: "#
|
|
3883
|
-
panel: "#
|
|
3884
|
-
borderStrong: "#
|
|
3885
|
-
textDim: "#
|
|
5039
|
+
background: "#F4F7F8",
|
|
5040
|
+
foreground: "#12222A",
|
|
5041
|
+
card: "#FFFFFF",
|
|
5042
|
+
primary: "#1F6B6E",
|
|
5043
|
+
primaryForeground: "#FFFFFF",
|
|
5044
|
+
secondary: "#EDF3F2",
|
|
5045
|
+
secondaryForeground: "#12222A",
|
|
5046
|
+
muted: "#EDF3F2",
|
|
5047
|
+
mutedForeground: "#6A7B85",
|
|
5048
|
+
accent: "#EDF3F2",
|
|
5049
|
+
accentForeground: "#12222A",
|
|
5050
|
+
destructive: "#B04A3C",
|
|
5051
|
+
destructiveForeground: "#FFFFFF",
|
|
5052
|
+
border: "#DCE4E8",
|
|
5053
|
+
input: "#DCE4E8",
|
|
5054
|
+
ring: "#1F6B6E",
|
|
5055
|
+
radius: "0px",
|
|
5056
|
+
surface: "#FAFCFC",
|
|
5057
|
+
surfaceHover: "#EDF3F2",
|
|
5058
|
+
canvas: "#F4F7F8",
|
|
5059
|
+
panel: "#FFFFFF",
|
|
5060
|
+
borderStrong: "#B9D1CF",
|
|
5061
|
+
textDim: "#6A7B85"
|
|
3886
5062
|
};
|
|
3887
5063
|
|
|
3888
5064
|
// src/response-viewer/clinical-display-data.ts
|
|
@@ -4021,10 +5197,30 @@ function getScoreSeverity(instrumentKey, score) {
|
|
|
4021
5197
|
if (!thresholds) return void 0;
|
|
4022
5198
|
return thresholds.find((t) => score >= t.min && score <= t.max);
|
|
4023
5199
|
}
|
|
4024
|
-
function ResponseTable({
|
|
5200
|
+
function ResponseTable({
|
|
5201
|
+
schema,
|
|
5202
|
+
responses,
|
|
5203
|
+
onRowClick,
|
|
5204
|
+
selectable,
|
|
5205
|
+
selectedIds,
|
|
5206
|
+
onToggleSelect,
|
|
5207
|
+
onSelectAll
|
|
5208
|
+
}) {
|
|
4025
5209
|
const questions = getAllQuestions(schema);
|
|
5210
|
+
const allPageSelected = selectable && responses.length > 0 && responses.every(
|
|
5211
|
+
(r) => r.sessionToken && selectedIds?.has(r.sessionToken)
|
|
5212
|
+
);
|
|
4026
5213
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "overflow-auto", children: /* @__PURE__ */ jsxRuntime.jsxs("table", { className: "w-full border-collapse text-[13px]", children: [
|
|
4027
5214
|
/* @__PURE__ */ jsxRuntime.jsx("thead", { children: /* @__PURE__ */ jsxRuntime.jsxs("tr", { className: "bg-muted", children: [
|
|
5215
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx("th", { className: "px-3 py-2 w-8 border-b-2 border-border", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5216
|
+
"input",
|
|
5217
|
+
{
|
|
5218
|
+
type: "checkbox",
|
|
5219
|
+
checked: allPageSelected,
|
|
5220
|
+
onChange: () => onSelectAll?.(),
|
|
5221
|
+
className: "accent-primary"
|
|
5222
|
+
}
|
|
5223
|
+
) }),
|
|
4028
5224
|
/* @__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
5225
|
questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4030
5226
|
"th",
|
|
@@ -4043,6 +5239,19 @@ function ResponseTable({ schema, responses, onRowClick }) {
|
|
|
4043
5239
|
onClick: () => onRowClick?.(response),
|
|
4044
5240
|
className: onRowClick ? "cursor-pointer border-b border-border hover:bg-accent transition-colors" : "border-b border-border",
|
|
4045
5241
|
children: [
|
|
5242
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx("td", { className: "px-3 py-2 w-8", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
5243
|
+
"input",
|
|
5244
|
+
{
|
|
5245
|
+
type: "checkbox",
|
|
5246
|
+
checked: !!(response.sessionToken && selectedIds?.has(response.sessionToken)),
|
|
5247
|
+
onChange: (e) => {
|
|
5248
|
+
e.stopPropagation();
|
|
5249
|
+
if (response.sessionToken) onToggleSelect?.(response.sessionToken);
|
|
5250
|
+
},
|
|
5251
|
+
onClick: (e) => e.stopPropagation(),
|
|
5252
|
+
className: "accent-primary"
|
|
5253
|
+
}
|
|
5254
|
+
) }),
|
|
4046
5255
|
/* @__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
5256
|
questions.map((q) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4048
5257
|
"td",
|
|
@@ -4060,7 +5269,7 @@ function ResponseTable({ schema, responses, onRowClick }) {
|
|
|
4060
5269
|
responses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("tr", { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4061
5270
|
"td",
|
|
4062
5271
|
{
|
|
4063
|
-
colSpan: questions.length + 2,
|
|
5272
|
+
colSpan: questions.length + 2 + (selectable ? 1 : 0),
|
|
4064
5273
|
className: "px-3 py-8 text-center text-muted-foreground",
|
|
4065
5274
|
children: "No responses yet"
|
|
4066
5275
|
}
|
|
@@ -4148,15 +5357,37 @@ function formatCellValue(value, type) {
|
|
|
4148
5357
|
if (typeof value === "object") return JSON.stringify(value);
|
|
4149
5358
|
return String(value);
|
|
4150
5359
|
}
|
|
4151
|
-
function ResponseCard({
|
|
5360
|
+
function ResponseCard({
|
|
5361
|
+
response,
|
|
5362
|
+
fields,
|
|
5363
|
+
onClick,
|
|
5364
|
+
selectable,
|
|
5365
|
+
selected,
|
|
5366
|
+
onToggleSelect
|
|
5367
|
+
}) {
|
|
4152
5368
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4153
5369
|
"div",
|
|
4154
5370
|
{
|
|
4155
5371
|
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",
|
|
5372
|
+
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
5373
|
children: [
|
|
4158
5374
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between mb-3 text-xs text-muted-foreground", children: [
|
|
4159
|
-
/* @__PURE__ */ jsxRuntime.
|
|
5375
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5376
|
+
selectable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
5377
|
+
"input",
|
|
5378
|
+
{
|
|
5379
|
+
type: "checkbox",
|
|
5380
|
+
checked: !!selected,
|
|
5381
|
+
onChange: (e) => {
|
|
5382
|
+
e.stopPropagation();
|
|
5383
|
+
onToggleSelect?.();
|
|
5384
|
+
},
|
|
5385
|
+
onClick: (e) => e.stopPropagation(),
|
|
5386
|
+
className: "accent-primary"
|
|
5387
|
+
}
|
|
5388
|
+
),
|
|
5389
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: new Date(response.submittedAt).toLocaleString() })
|
|
5390
|
+
] }),
|
|
4160
5391
|
response.completionTimeMs != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
4161
5392
|
Math.round(response.completionTimeMs / 1e3),
|
|
4162
5393
|
"s"
|
|
@@ -4247,18 +5478,19 @@ function formatCardValue(value, type) {
|
|
|
4247
5478
|
if (typeof value === "object") return JSON.stringify(value);
|
|
4248
5479
|
return String(value);
|
|
4249
5480
|
}
|
|
4250
|
-
function ResponseDetail({ response, fields, onBack }) {
|
|
5481
|
+
function ResponseDetail({ response, fields, onBack, onDelete }) {
|
|
4251
5482
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
4252
5483
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-5 pb-3 border-b border-border", children: [
|
|
4253
5484
|
onBack && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", onClick: onBack, children: "Back" }),
|
|
4254
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
5485
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1", children: [
|
|
4255
5486
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold text-foreground", children: "Response Detail" }),
|
|
4256
5487
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground", children: [
|
|
4257
5488
|
"Submitted ",
|
|
4258
5489
|
new Date(response.submittedAt).toLocaleString(),
|
|
4259
5490
|
response.completionTimeMs != null && ` \u2014 ${Math.round(response.completionTimeMs / 1e3)}s`
|
|
4260
5491
|
] })
|
|
4261
|
-
] })
|
|
5492
|
+
] }),
|
|
5493
|
+
onDelete && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "destructive", size: "sm", onClick: onDelete, children: "Delete" })
|
|
4262
5494
|
] }),
|
|
4263
5495
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-col gap-4", children: fields.map((field) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4264
5496
|
"div",
|
|
@@ -4611,6 +5843,491 @@ function formatFallbackValue(value) {
|
|
|
4611
5843
|
if (typeof value === "object") return JSON.stringify(value, null, 2);
|
|
4612
5844
|
return String(value);
|
|
4613
5845
|
}
|
|
5846
|
+
function TimelineView({ responses, questions, onSelect }) {
|
|
5847
|
+
const grouped = react.useMemo(() => groupByDay(responses), [responses]);
|
|
5848
|
+
if (responses.length === 0) {
|
|
5849
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground text-sm", children: "No responses to display." });
|
|
5850
|
+
}
|
|
5851
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 space-y-6", children: grouped.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
5852
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 mb-3", children: [
|
|
5853
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px flex-1 bg-border" }),
|
|
5854
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-muted-foreground shrink-0", children: group.dateLabel }),
|
|
5855
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground shrink-0", children: [
|
|
5856
|
+
"(",
|
|
5857
|
+
group.responses.length,
|
|
5858
|
+
")"
|
|
5859
|
+
] }),
|
|
5860
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-px flex-1 bg-border" })
|
|
5861
|
+
] }),
|
|
5862
|
+
/* @__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(
|
|
5863
|
+
TimelineEntry,
|
|
5864
|
+
{
|
|
5865
|
+
response,
|
|
5866
|
+
questions,
|
|
5867
|
+
onView: () => onSelect(response)
|
|
5868
|
+
},
|
|
5869
|
+
response.sessionToken || idx
|
|
5870
|
+
)) })
|
|
5871
|
+
] }, group.dateLabel)) });
|
|
5872
|
+
}
|
|
5873
|
+
function TimelineEntry({
|
|
5874
|
+
response,
|
|
5875
|
+
questions,
|
|
5876
|
+
onView
|
|
5877
|
+
}) {
|
|
5878
|
+
const time = new Date(response.submittedAt).toLocaleTimeString([], {
|
|
5879
|
+
hour: "2-digit",
|
|
5880
|
+
minute: "2-digit"
|
|
5881
|
+
});
|
|
5882
|
+
const preview = [];
|
|
5883
|
+
for (const q of questions) {
|
|
5884
|
+
if (preview.length >= 3) break;
|
|
5885
|
+
const val = response.values[q.id];
|
|
5886
|
+
if (val != null && val !== "") {
|
|
5887
|
+
preview.push({ questionId: q.id, label: q.label, type: q.type, value: val });
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative flex items-start gap-3 group", children: [
|
|
5891
|
+
/* @__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" }),
|
|
5892
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 rounded border border-border bg-card p-3 hover:border-primary/40 transition-colors", children: [
|
|
5893
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-2", children: [
|
|
5894
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: time }),
|
|
5895
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
5896
|
+
response.totalScore != null && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
5897
|
+
"Score: ",
|
|
5898
|
+
response.totalScore
|
|
5899
|
+
] }),
|
|
5900
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onView, children: "View" })
|
|
5901
|
+
] })
|
|
5902
|
+
] }),
|
|
5903
|
+
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: [
|
|
5904
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
5905
|
+
field.label,
|
|
5906
|
+
": "
|
|
5907
|
+
] }),
|
|
5908
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-foreground", children: formatPreviewValue(field.value, field.type) })
|
|
5909
|
+
] }, field.questionId)) })
|
|
5910
|
+
] })
|
|
5911
|
+
] });
|
|
5912
|
+
}
|
|
5913
|
+
function groupByDay(responses) {
|
|
5914
|
+
const sorted = [...responses].sort(
|
|
5915
|
+
(a, b) => new Date(b.submittedAt).getTime() - new Date(a.submittedAt).getTime()
|
|
5916
|
+
);
|
|
5917
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5918
|
+
for (const r of sorted) {
|
|
5919
|
+
const date = new Date(r.submittedAt).toLocaleDateString(void 0, {
|
|
5920
|
+
weekday: "long",
|
|
5921
|
+
year: "numeric",
|
|
5922
|
+
month: "long",
|
|
5923
|
+
day: "numeric"
|
|
5924
|
+
});
|
|
5925
|
+
const existing = groups.get(date);
|
|
5926
|
+
if (existing) {
|
|
5927
|
+
existing.push(r);
|
|
5928
|
+
} else {
|
|
5929
|
+
groups.set(date, [r]);
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
return Array.from(groups.entries()).map(([dateLabel, responses2]) => ({
|
|
5933
|
+
dateLabel,
|
|
5934
|
+
responses: responses2
|
|
5935
|
+
}));
|
|
5936
|
+
}
|
|
5937
|
+
function formatPreviewValue(value, type) {
|
|
5938
|
+
if (value == null) return "\u2014";
|
|
5939
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
5940
|
+
if (Array.isArray(value)) return value.length > 0 ? value.slice(0, 2).join(", ") + (value.length > 2 ? "..." : "") : "\u2014";
|
|
5941
|
+
if (typeof value === "object") {
|
|
5942
|
+
if (type === "legal_name") {
|
|
5943
|
+
const n = value;
|
|
5944
|
+
return [n.first, n.last].filter(Boolean).join(" ") || "\u2014";
|
|
5945
|
+
}
|
|
5946
|
+
return "...";
|
|
5947
|
+
}
|
|
5948
|
+
const str = String(value);
|
|
5949
|
+
return str.length > 40 ? str.slice(0, 40) + "..." : str;
|
|
5950
|
+
}
|
|
5951
|
+
|
|
5952
|
+
// src/response-viewer/stats-utils.ts
|
|
5953
|
+
function computeStats(responses, schema) {
|
|
5954
|
+
if (responses.length === 0) {
|
|
5955
|
+
return {
|
|
5956
|
+
totalCount: 0,
|
|
5957
|
+
completionRate: 0,
|
|
5958
|
+
avgCompletionTimeMs: null,
|
|
5959
|
+
dateRange: null,
|
|
5960
|
+
fieldSummaries: []
|
|
5961
|
+
};
|
|
5962
|
+
}
|
|
5963
|
+
const questions = getStatsQuestions(schema);
|
|
5964
|
+
const requiredIds = new Set(questions.filter((q) => q.required).map((q) => q.id));
|
|
5965
|
+
let completedCount = 0;
|
|
5966
|
+
for (const r of responses) {
|
|
5967
|
+
let allFilled = true;
|
|
5968
|
+
for (const id of requiredIds) {
|
|
5969
|
+
if (r.values[id] == null || r.values[id] === "") {
|
|
5970
|
+
allFilled = false;
|
|
5971
|
+
break;
|
|
5972
|
+
}
|
|
5973
|
+
}
|
|
5974
|
+
if (allFilled) completedCount++;
|
|
5975
|
+
}
|
|
5976
|
+
const completionRate = requiredIds.size > 0 ? completedCount / responses.length : 1;
|
|
5977
|
+
const completionTimes = responses.map((r) => r.completionTimeMs).filter((t) => t != null && t > 0);
|
|
5978
|
+
const avgCompletionTimeMs = completionTimes.length > 0 ? completionTimes.reduce((a, b) => a + b, 0) / completionTimes.length : null;
|
|
5979
|
+
const timestamps = responses.map((r) => new Date(r.submittedAt).getTime());
|
|
5980
|
+
const earliest = new Date(Math.min(...timestamps)).toISOString();
|
|
5981
|
+
const latest = new Date(Math.max(...timestamps)).toISOString();
|
|
5982
|
+
const fieldSummaries = questions.map((q) => computeFieldSummary(q, responses));
|
|
5983
|
+
return {
|
|
5984
|
+
totalCount: responses.length,
|
|
5985
|
+
completionRate,
|
|
5986
|
+
avgCompletionTimeMs,
|
|
5987
|
+
dateRange: { earliest, latest },
|
|
5988
|
+
fieldSummaries
|
|
5989
|
+
};
|
|
5990
|
+
}
|
|
5991
|
+
function computeFieldSummary(question, responses) {
|
|
5992
|
+
const values = responses.map((r) => r.values[question.id]).filter((v) => v != null && v !== "");
|
|
5993
|
+
const filledCount = values.length;
|
|
5994
|
+
const filledRate = responses.length > 0 ? filledCount / responses.length : 0;
|
|
5995
|
+
const base = {
|
|
5996
|
+
fieldId: question.id,
|
|
5997
|
+
label: question.label,
|
|
5998
|
+
type: question.type,
|
|
5999
|
+
filledCount,
|
|
6000
|
+
filledRate
|
|
6001
|
+
};
|
|
6002
|
+
if (isNumericType(question.type)) {
|
|
6003
|
+
const nums = values.map((v) => typeof v === "number" ? v : Number(v)).filter((n) => !isNaN(n));
|
|
6004
|
+
if (nums.length > 0) {
|
|
6005
|
+
return {
|
|
6006
|
+
...base,
|
|
6007
|
+
kind: "numeric",
|
|
6008
|
+
mean: nums.reduce((a, b) => a + b, 0) / nums.length,
|
|
6009
|
+
min: Math.min(...nums),
|
|
6010
|
+
max: Math.max(...nums)
|
|
6011
|
+
};
|
|
6012
|
+
}
|
|
6013
|
+
return { ...base, kind: "other" };
|
|
6014
|
+
}
|
|
6015
|
+
if (isCategoricalType(question.type)) {
|
|
6016
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6017
|
+
for (const v of values) {
|
|
6018
|
+
const key = String(v);
|
|
6019
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6020
|
+
}
|
|
6021
|
+
let mode = "";
|
|
6022
|
+
let modeCount = 0;
|
|
6023
|
+
for (const [key, count] of counts) {
|
|
6024
|
+
if (count > modeCount) {
|
|
6025
|
+
mode = key;
|
|
6026
|
+
modeCount = count;
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
return {
|
|
6030
|
+
...base,
|
|
6031
|
+
kind: "categorical",
|
|
6032
|
+
mode,
|
|
6033
|
+
modeCount,
|
|
6034
|
+
uniqueCount: counts.size
|
|
6035
|
+
};
|
|
6036
|
+
}
|
|
6037
|
+
return { ...base, kind: "other" };
|
|
6038
|
+
}
|
|
6039
|
+
function isNumericType(type) {
|
|
6040
|
+
return [
|
|
6041
|
+
"number",
|
|
6042
|
+
"slider",
|
|
6043
|
+
"rating",
|
|
6044
|
+
"nps",
|
|
6045
|
+
"opinion_scale",
|
|
6046
|
+
"pain_scale"
|
|
6047
|
+
].includes(type);
|
|
6048
|
+
}
|
|
6049
|
+
function isCategoricalType(type) {
|
|
6050
|
+
return [
|
|
6051
|
+
"single_select",
|
|
6052
|
+
"dropdown",
|
|
6053
|
+
"country_select",
|
|
6054
|
+
"boolean",
|
|
6055
|
+
"consent",
|
|
6056
|
+
"multi_select",
|
|
6057
|
+
"checkbox_group"
|
|
6058
|
+
].includes(type);
|
|
6059
|
+
}
|
|
6060
|
+
function getStatsQuestions(schema) {
|
|
6061
|
+
const questions = [];
|
|
6062
|
+
for (const section of schema.sections) {
|
|
6063
|
+
for (const q of section.questions) {
|
|
6064
|
+
if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
|
|
6065
|
+
continue;
|
|
6066
|
+
}
|
|
6067
|
+
questions.push(q);
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
return questions;
|
|
6071
|
+
}
|
|
6072
|
+
function StatsPanel({ schema, responses, onClose }) {
|
|
6073
|
+
const stats = react.useMemo(() => computeStats(responses, schema), [responses, schema]);
|
|
6074
|
+
if (stats.totalCount === 0) {
|
|
6075
|
+
return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-3 text-sm text-muted-foreground", children: "No responses to compute statistics." });
|
|
6076
|
+
}
|
|
6077
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border bg-muted/30", children: [
|
|
6078
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
|
|
6079
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: "Statistics" }),
|
|
6080
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
|
|
6081
|
+
] }),
|
|
6082
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
6083
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-3", children: [
|
|
6084
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6085
|
+
StatCard,
|
|
6086
|
+
{
|
|
6087
|
+
label: "Total Responses",
|
|
6088
|
+
value: String(stats.totalCount)
|
|
6089
|
+
}
|
|
6090
|
+
),
|
|
6091
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6092
|
+
StatCard,
|
|
6093
|
+
{
|
|
6094
|
+
label: "Completion Rate",
|
|
6095
|
+
value: `${Math.round(stats.completionRate * 100)}%`
|
|
6096
|
+
}
|
|
6097
|
+
),
|
|
6098
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6099
|
+
StatCard,
|
|
6100
|
+
{
|
|
6101
|
+
label: "Avg. Completion Time",
|
|
6102
|
+
value: formatTime(stats.avgCompletionTimeMs)
|
|
6103
|
+
}
|
|
6104
|
+
),
|
|
6105
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6106
|
+
StatCard,
|
|
6107
|
+
{
|
|
6108
|
+
label: "Date Range",
|
|
6109
|
+
value: formatDateRange(stats.dateRange)
|
|
6110
|
+
}
|
|
6111
|
+
)
|
|
6112
|
+
] }),
|
|
6113
|
+
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(
|
|
6114
|
+
"div",
|
|
6115
|
+
{
|
|
6116
|
+
className: "px-2.5 py-2 rounded border border-border bg-background text-xs",
|
|
6117
|
+
children: [
|
|
6118
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-muted-foreground truncate mb-1", children: f.label }),
|
|
6119
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-baseline gap-2", children: [
|
|
6120
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-foreground font-medium", children: [
|
|
6121
|
+
Math.round(f.filledRate * 100),
|
|
6122
|
+
"% filled"
|
|
6123
|
+
] }),
|
|
6124
|
+
f.kind === "numeric" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground", children: [
|
|
6125
|
+
"avg ",
|
|
6126
|
+
f.mean.toFixed(1),
|
|
6127
|
+
" (",
|
|
6128
|
+
f.min,
|
|
6129
|
+
"\u2013",
|
|
6130
|
+
f.max,
|
|
6131
|
+
")"
|
|
6132
|
+
] }),
|
|
6133
|
+
f.kind === "categorical" && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-muted-foreground truncate", children: [
|
|
6134
|
+
"top: ",
|
|
6135
|
+
f.mode,
|
|
6136
|
+
" (",
|
|
6137
|
+
f.modeCount,
|
|
6138
|
+
")"
|
|
6139
|
+
] })
|
|
6140
|
+
] })
|
|
6141
|
+
]
|
|
6142
|
+
},
|
|
6143
|
+
f.fieldId
|
|
6144
|
+
)) })
|
|
6145
|
+
] })
|
|
6146
|
+
] });
|
|
6147
|
+
}
|
|
6148
|
+
function StatCard({ label, value }) {
|
|
6149
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-3 py-2 rounded border border-border bg-background", children: [
|
|
6150
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[11px] text-muted-foreground mb-0.5", children: label }),
|
|
6151
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm font-semibold text-foreground", children: value })
|
|
6152
|
+
] });
|
|
6153
|
+
}
|
|
6154
|
+
function formatTime(ms) {
|
|
6155
|
+
if (ms == null) return "\u2014";
|
|
6156
|
+
const seconds = Math.round(ms / 1e3);
|
|
6157
|
+
if (seconds < 60) return `${seconds}s`;
|
|
6158
|
+
const minutes = Math.floor(seconds / 60);
|
|
6159
|
+
const remainingSeconds = seconds % 60;
|
|
6160
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
6161
|
+
}
|
|
6162
|
+
function formatDateRange(range) {
|
|
6163
|
+
if (!range) return "\u2014";
|
|
6164
|
+
const fmt = (iso) => new Date(iso).toLocaleDateString();
|
|
6165
|
+
return `${fmt(range.earliest)} \u2013 ${fmt(range.latest)}`;
|
|
6166
|
+
}
|
|
6167
|
+
|
|
6168
|
+
// src/response-viewer/chart-utils.ts
|
|
6169
|
+
function computeFieldDistribution(responses, fieldId) {
|
|
6170
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6171
|
+
let total = 0;
|
|
6172
|
+
for (const r of responses) {
|
|
6173
|
+
const raw = r.values[fieldId];
|
|
6174
|
+
if (raw == null || raw === "") continue;
|
|
6175
|
+
total++;
|
|
6176
|
+
if (typeof raw === "boolean") {
|
|
6177
|
+
const key = raw ? "Yes" : "No";
|
|
6178
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6179
|
+
} else if (Array.isArray(raw)) {
|
|
6180
|
+
for (const item of raw) {
|
|
6181
|
+
const key = String(item);
|
|
6182
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6183
|
+
}
|
|
6184
|
+
} else {
|
|
6185
|
+
const key = String(raw);
|
|
6186
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
6187
|
+
}
|
|
6188
|
+
}
|
|
6189
|
+
if (total === 0) return [];
|
|
6190
|
+
const entries = [];
|
|
6191
|
+
for (const [value, count] of counts) {
|
|
6192
|
+
entries.push({
|
|
6193
|
+
value,
|
|
6194
|
+
count,
|
|
6195
|
+
percentage: count / total * 100
|
|
6196
|
+
});
|
|
6197
|
+
}
|
|
6198
|
+
entries.sort((a, b) => b.count - a.count);
|
|
6199
|
+
return entries;
|
|
6200
|
+
}
|
|
6201
|
+
function getChartableFieldIds(schema) {
|
|
6202
|
+
const chartable = [];
|
|
6203
|
+
const chartTypes = /* @__PURE__ */ new Set([
|
|
6204
|
+
"single_select",
|
|
6205
|
+
"dropdown",
|
|
6206
|
+
"country_select",
|
|
6207
|
+
"boolean",
|
|
6208
|
+
"consent",
|
|
6209
|
+
"multi_select",
|
|
6210
|
+
"checkbox_group",
|
|
6211
|
+
"rating",
|
|
6212
|
+
"nps",
|
|
6213
|
+
"opinion_scale",
|
|
6214
|
+
"pain_scale"
|
|
6215
|
+
]);
|
|
6216
|
+
for (const section of schema.sections) {
|
|
6217
|
+
for (const q of section.questions) {
|
|
6218
|
+
if (chartTypes.has(q.type)) {
|
|
6219
|
+
chartable.push({ id: q.id, label: q.label, type: q.type });
|
|
6220
|
+
}
|
|
6221
|
+
}
|
|
6222
|
+
}
|
|
6223
|
+
return chartable;
|
|
6224
|
+
}
|
|
6225
|
+
function ChartPanel({ schema, responses, onClose }) {
|
|
6226
|
+
const chartableFields = react.useMemo(() => getChartableFieldIds(schema), [schema]);
|
|
6227
|
+
const [selectedFieldId, setSelectedFieldId] = react.useState(chartableFields[0]?.id ?? "");
|
|
6228
|
+
const distribution = react.useMemo(() => {
|
|
6229
|
+
if (!selectedFieldId) return [];
|
|
6230
|
+
return computeFieldDistribution(responses, selectedFieldId);
|
|
6231
|
+
}, [responses, selectedFieldId]);
|
|
6232
|
+
const selectedField = chartableFields.find((f) => f.id === selectedFieldId);
|
|
6233
|
+
const maxCount = distribution.length > 0 ? Math.max(...distribution.map((d) => d.count)) : 1;
|
|
6234
|
+
if (chartableFields.length === 0) {
|
|
6235
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-6 border-b border-border bg-muted/30 text-center", children: [
|
|
6236
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "No chartable fields found in this schema." }),
|
|
6237
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mt-1", children: "Charts work with select, dropdown, boolean, rating, and similar field types." })
|
|
6238
|
+
] });
|
|
6239
|
+
}
|
|
6240
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b border-border bg-muted/30", children: [
|
|
6241
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-2 border-b border-border/50", children: [
|
|
6242
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
|
|
6243
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-foreground", children: "Field Distribution" }),
|
|
6244
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6245
|
+
"select",
|
|
6246
|
+
{
|
|
6247
|
+
value: selectedFieldId,
|
|
6248
|
+
onChange: (e) => setSelectedFieldId(e.target.value),
|
|
6249
|
+
className: "text-xs bg-background border border-border rounded px-2 py-1 text-foreground",
|
|
6250
|
+
children: chartableFields.map((f) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: f.id, children: f.label }, f.id))
|
|
6251
|
+
}
|
|
6252
|
+
)
|
|
6253
|
+
] }),
|
|
6254
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "ghost", size: "sm", className: "h-6 text-xs", onClick: onClose, children: "Close" })
|
|
6255
|
+
] }),
|
|
6256
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 py-3", children: [
|
|
6257
|
+
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: [
|
|
6258
|
+
distribution.slice(0, 10).map((entry) => /* @__PURE__ */ jsxRuntime.jsx(BarRow, { entry, maxCount }, entry.value)),
|
|
6259
|
+
distribution.length > 10 && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-xs text-muted-foreground mt-2", children: [
|
|
6260
|
+
"+",
|
|
6261
|
+
distribution.length - 10,
|
|
6262
|
+
" more values"
|
|
6263
|
+
] })
|
|
6264
|
+
] }),
|
|
6265
|
+
selectedField && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-3 text-[11px] text-muted-foreground", children: [
|
|
6266
|
+
responses.length,
|
|
6267
|
+
" response",
|
|
6268
|
+
responses.length !== 1 ? "s" : "",
|
|
6269
|
+
" \xB7 ",
|
|
6270
|
+
distribution.length,
|
|
6271
|
+
" unique value",
|
|
6272
|
+
distribution.length !== 1 ? "s" : ""
|
|
6273
|
+
] })
|
|
6274
|
+
] })
|
|
6275
|
+
] });
|
|
6276
|
+
}
|
|
6277
|
+
function BarRow({ entry, maxCount }) {
|
|
6278
|
+
const widthPct = maxCount > 0 ? entry.count / maxCount * 100 : 0;
|
|
6279
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
6280
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-24 shrink-0 text-xs text-foreground truncate", title: entry.value, children: entry.value }),
|
|
6281
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 h-5 bg-background rounded overflow-hidden border border-border/50", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
6282
|
+
"div",
|
|
6283
|
+
{
|
|
6284
|
+
className: "h-full bg-primary/60 rounded transition-all",
|
|
6285
|
+
style: { width: `${widthPct}%` }
|
|
6286
|
+
}
|
|
6287
|
+
) }),
|
|
6288
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-16 shrink-0 text-xs text-muted-foreground text-right", children: [
|
|
6289
|
+
entry.count,
|
|
6290
|
+
" (",
|
|
6291
|
+
Math.round(entry.percentage),
|
|
6292
|
+
"%)"
|
|
6293
|
+
] })
|
|
6294
|
+
] });
|
|
6295
|
+
}
|
|
6296
|
+
function ConfirmDialog2({
|
|
6297
|
+
title,
|
|
6298
|
+
message,
|
|
6299
|
+
confirmLabel = "Confirm",
|
|
6300
|
+
cancelLabel = "Cancel",
|
|
6301
|
+
variant = "default",
|
|
6302
|
+
onConfirm,
|
|
6303
|
+
onCancel
|
|
6304
|
+
}) {
|
|
6305
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed inset-0 z-50 flex items-center justify-center", children: [
|
|
6306
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6307
|
+
"div",
|
|
6308
|
+
{
|
|
6309
|
+
className: "absolute inset-0 bg-black/50",
|
|
6310
|
+
onClick: onCancel
|
|
6311
|
+
}
|
|
6312
|
+
),
|
|
6313
|
+
/* @__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: [
|
|
6314
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold text-foreground mb-2", children: title }),
|
|
6315
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mb-4", children: message }),
|
|
6316
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
|
|
6317
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", onClick: onCancel, children: cancelLabel }),
|
|
6318
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6319
|
+
fieldcraftReact.Button,
|
|
6320
|
+
{
|
|
6321
|
+
variant: variant === "destructive" ? "destructive" : "default",
|
|
6322
|
+
size: "sm",
|
|
6323
|
+
onClick: onConfirm,
|
|
6324
|
+
children: confirmLabel
|
|
6325
|
+
}
|
|
6326
|
+
)
|
|
6327
|
+
] })
|
|
6328
|
+
] })
|
|
6329
|
+
] });
|
|
6330
|
+
}
|
|
4614
6331
|
|
|
4615
6332
|
// src/response-viewer/export-utils.ts
|
|
4616
6333
|
function buildCsvContent(schema, responses, options = {}) {
|
|
@@ -4745,6 +6462,84 @@ function paginate(items, currentPage, pageSize) {
|
|
|
4745
6462
|
};
|
|
4746
6463
|
}
|
|
4747
6464
|
|
|
6465
|
+
// src/response-viewer/search-utils.ts
|
|
6466
|
+
function searchResponses(responses, query, schema) {
|
|
6467
|
+
if (!query.trim()) return responses;
|
|
6468
|
+
const lowerQuery = query.toLowerCase().trim();
|
|
6469
|
+
const questions = getSearchableQuestions(schema);
|
|
6470
|
+
return responses.filter((response) => {
|
|
6471
|
+
if (new Date(response.submittedAt).toLocaleString().toLowerCase().includes(lowerQuery)) {
|
|
6472
|
+
return true;
|
|
6473
|
+
}
|
|
6474
|
+
if (response.sessionToken?.toLowerCase().includes(lowerQuery)) {
|
|
6475
|
+
return true;
|
|
6476
|
+
}
|
|
6477
|
+
for (const q of questions) {
|
|
6478
|
+
const value = response.values[q.id];
|
|
6479
|
+
if (value == null) continue;
|
|
6480
|
+
const stringified = stringifyValue(value, q.type);
|
|
6481
|
+
if (stringified.toLowerCase().includes(lowerQuery)) {
|
|
6482
|
+
return true;
|
|
6483
|
+
}
|
|
6484
|
+
}
|
|
6485
|
+
if (response.totalScore != null && String(response.totalScore).includes(lowerQuery)) {
|
|
6486
|
+
return true;
|
|
6487
|
+
}
|
|
6488
|
+
return false;
|
|
6489
|
+
});
|
|
6490
|
+
}
|
|
6491
|
+
function getSearchableQuestions(schema) {
|
|
6492
|
+
const questions = [];
|
|
6493
|
+
for (const section of schema.sections) {
|
|
6494
|
+
for (const q of section.questions) {
|
|
6495
|
+
if (q.type === "info-block" || q.type === "section-header" || q.type === "page-break") {
|
|
6496
|
+
continue;
|
|
6497
|
+
}
|
|
6498
|
+
questions.push(q);
|
|
6499
|
+
}
|
|
6500
|
+
}
|
|
6501
|
+
return questions;
|
|
6502
|
+
}
|
|
6503
|
+
function stringifyValue(value, type) {
|
|
6504
|
+
if (value == null) return "";
|
|
6505
|
+
switch (type) {
|
|
6506
|
+
case "vitals_entry": {
|
|
6507
|
+
if (typeof value !== "object" || value === null) break;
|
|
6508
|
+
const v = value;
|
|
6509
|
+
const parts = [];
|
|
6510
|
+
if (v.systolicBp && v.diastolicBp) parts.push(`BP ${v.systolicBp}/${v.diastolicBp}`);
|
|
6511
|
+
if (v.heartRate) parts.push(`HR ${v.heartRate}`);
|
|
6512
|
+
if (v.temperature) parts.push(`${v.temperature}`);
|
|
6513
|
+
if (v.oxygenSaturation) parts.push(`SpO2 ${v.oxygenSaturation}`);
|
|
6514
|
+
return parts.join(" ");
|
|
6515
|
+
}
|
|
6516
|
+
case "medication_list":
|
|
6517
|
+
if (Array.isArray(value)) {
|
|
6518
|
+
return value.map((m) => [m.name, m.dosage, m.frequency].filter(Boolean).join(" ")).join(" ");
|
|
6519
|
+
}
|
|
6520
|
+
break;
|
|
6521
|
+
case "allergy_list":
|
|
6522
|
+
if (Array.isArray(value)) {
|
|
6523
|
+
return value.map((a) => [a.allergen, a.severity].filter(Boolean).join(" ")).join(" ");
|
|
6524
|
+
}
|
|
6525
|
+
break;
|
|
6526
|
+
case "legal_name": {
|
|
6527
|
+
if (typeof value !== "object" || value === null) break;
|
|
6528
|
+
const n = value;
|
|
6529
|
+
return [n.first, n.middle, n.last].filter(Boolean).join(" ");
|
|
6530
|
+
}
|
|
6531
|
+
case "address": {
|
|
6532
|
+
if (typeof value !== "object" || value === null) break;
|
|
6533
|
+
const a = value;
|
|
6534
|
+
return [a.street, a.city, a.state, a.zip].filter(Boolean).join(" ");
|
|
6535
|
+
}
|
|
6536
|
+
}
|
|
6537
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
6538
|
+
if (Array.isArray(value)) return value.map(String).join(" ");
|
|
6539
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
6540
|
+
return String(value);
|
|
6541
|
+
}
|
|
6542
|
+
|
|
4748
6543
|
// src/response-viewer/filter-utils.ts
|
|
4749
6544
|
function createEmptyFilterState() {
|
|
4750
6545
|
return { filters: [], dateRange: {} };
|
|
@@ -4843,7 +6638,11 @@ function ResponseViewerInner({
|
|
|
4843
6638
|
dateFormat,
|
|
4844
6639
|
columnLabels,
|
|
4845
6640
|
onExport,
|
|
4846
|
-
pageSize = 25
|
|
6641
|
+
pageSize = 25,
|
|
6642
|
+
onDelete,
|
|
6643
|
+
onBulkDelete,
|
|
6644
|
+
onBulkExport,
|
|
6645
|
+
selectable = false
|
|
4847
6646
|
}) {
|
|
4848
6647
|
const [viewMode, setViewMode] = react.useState("table");
|
|
4849
6648
|
const [selectedResponse, setSelectedResponse] = react.useState(null);
|
|
@@ -4851,6 +6650,11 @@ function ResponseViewerInner({
|
|
|
4851
6650
|
const [effectivePageSize, setEffectivePageSize] = react.useState(pageSize);
|
|
4852
6651
|
const [filterState, setFilterState] = react.useState(createEmptyFilterState);
|
|
4853
6652
|
const [showFilterPanel, setShowFilterPanel] = react.useState(false);
|
|
6653
|
+
const [searchQuery, setSearchQuery] = react.useState("");
|
|
6654
|
+
const [showStats, setShowStats] = react.useState(false);
|
|
6655
|
+
const [showCharts, setShowCharts] = react.useState(false);
|
|
6656
|
+
const [selectedIds, setSelectedIds] = react.useState(/* @__PURE__ */ new Set());
|
|
6657
|
+
const [confirmDialog, setConfirmDialog] = react.useState(null);
|
|
4854
6658
|
const [draftFieldId, setDraftFieldId] = react.useState("");
|
|
4855
6659
|
const [draftOperator, setDraftOperator] = react.useState("contains");
|
|
4856
6660
|
const [draftValue, setDraftValue] = react.useState("");
|
|
@@ -4861,9 +6665,18 @@ function ResponseViewerInner({
|
|
|
4861
6665
|
setEffectivePageSize(pageSize);
|
|
4862
6666
|
setCurrentPage(1);
|
|
4863
6667
|
}, [pageSize]);
|
|
6668
|
+
react.useEffect(() => {
|
|
6669
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
6670
|
+
}, [responses]);
|
|
6671
|
+
const searchedResponses = react.useMemo(
|
|
6672
|
+
() => searchResponses(responses, searchQuery, schema),
|
|
6673
|
+
[responses, searchQuery, schema]
|
|
6674
|
+
);
|
|
4864
6675
|
const isFiltered = hasActiveFilters(filterState);
|
|
4865
|
-
const filteredResponses = applyFilters(
|
|
6676
|
+
const filteredResponses = applyFilters(searchedResponses, filterState);
|
|
4866
6677
|
const pag = paginate(filteredResponses, currentPage, effectivePageSize);
|
|
6678
|
+
const questions = getAllQuestions2(schema);
|
|
6679
|
+
const isSelectable = selectable && (!!onBulkDelete || !!onBulkExport);
|
|
4867
6680
|
function handleSelect(response) {
|
|
4868
6681
|
setSelectedResponse(response);
|
|
4869
6682
|
onResponseSelect?.(response);
|
|
@@ -4910,7 +6723,59 @@ function ResponseViewerInner({
|
|
|
4910
6723
|
setFilterState(createEmptyFilterState());
|
|
4911
6724
|
setCurrentPage(1);
|
|
4912
6725
|
}
|
|
4913
|
-
|
|
6726
|
+
function handleToggleSelect(token) {
|
|
6727
|
+
setSelectedIds((prev) => {
|
|
6728
|
+
const next = new Set(prev);
|
|
6729
|
+
if (next.has(token)) {
|
|
6730
|
+
next.delete(token);
|
|
6731
|
+
} else {
|
|
6732
|
+
next.add(token);
|
|
6733
|
+
}
|
|
6734
|
+
return next;
|
|
6735
|
+
});
|
|
6736
|
+
}
|
|
6737
|
+
function handleSelectAll() {
|
|
6738
|
+
const currentTokens = pag.pageItems.map((r) => r.sessionToken).filter(Boolean);
|
|
6739
|
+
const allSelected = currentTokens.every((t) => selectedIds.has(t));
|
|
6740
|
+
setSelectedIds((prev) => {
|
|
6741
|
+
const next = new Set(prev);
|
|
6742
|
+
if (allSelected) {
|
|
6743
|
+
currentTokens.forEach((t) => next.delete(t));
|
|
6744
|
+
} else {
|
|
6745
|
+
currentTokens.forEach((t) => next.add(t));
|
|
6746
|
+
}
|
|
6747
|
+
return next;
|
|
6748
|
+
});
|
|
6749
|
+
}
|
|
6750
|
+
function handleBulkDelete() {
|
|
6751
|
+
if (!onBulkDelete || selectedIds.size === 0) return;
|
|
6752
|
+
setConfirmDialog({
|
|
6753
|
+
title: "Delete selected responses",
|
|
6754
|
+
message: `Are you sure you want to delete ${selectedIds.size} response${selectedIds.size !== 1 ? "s" : ""}? This cannot be undone.`,
|
|
6755
|
+
onConfirm: () => {
|
|
6756
|
+
onBulkDelete(Array.from(selectedIds));
|
|
6757
|
+
setSelectedIds(/* @__PURE__ */ new Set());
|
|
6758
|
+
setConfirmDialog(null);
|
|
6759
|
+
}
|
|
6760
|
+
});
|
|
6761
|
+
}
|
|
6762
|
+
function handleBulkExport() {
|
|
6763
|
+
if (!onBulkExport || selectedIds.size === 0) return;
|
|
6764
|
+
const selected = responses.filter((r) => r.sessionToken && selectedIds.has(r.sessionToken));
|
|
6765
|
+
onBulkExport(selected);
|
|
6766
|
+
}
|
|
6767
|
+
function handleDeleteSingle(token) {
|
|
6768
|
+
if (!onDelete) return;
|
|
6769
|
+
setConfirmDialog({
|
|
6770
|
+
title: "Delete response",
|
|
6771
|
+
message: "Are you sure you want to delete this response? This cannot be undone.",
|
|
6772
|
+
onConfirm: () => {
|
|
6773
|
+
onDelete(token);
|
|
6774
|
+
setSelectedResponse(null);
|
|
6775
|
+
setConfirmDialog(null);
|
|
6776
|
+
}
|
|
6777
|
+
});
|
|
6778
|
+
}
|
|
4914
6779
|
function getFields(response) {
|
|
4915
6780
|
return questions.map((q) => ({
|
|
4916
6781
|
questionId: q.id,
|
|
@@ -4921,6 +6786,19 @@ function ResponseViewerInner({
|
|
|
4921
6786
|
}
|
|
4922
6787
|
const heightValue = typeof height === "number" ? `${height}px` : height;
|
|
4923
6788
|
const widthValue = typeof width === "number" ? `${width}px` : width;
|
|
6789
|
+
const countText = (() => {
|
|
6790
|
+
const hasSearch = searchQuery.trim().length > 0;
|
|
6791
|
+
if (hasSearch && isFiltered) {
|
|
6792
|
+
return `${filteredResponses.length} of ${responses.length} responses (searched + filtered)`;
|
|
6793
|
+
}
|
|
6794
|
+
if (hasSearch) {
|
|
6795
|
+
return `${searchedResponses.length} result${searchedResponses.length !== 1 ? "s" : ""} for "${searchQuery}"`;
|
|
6796
|
+
}
|
|
6797
|
+
if (isFiltered) {
|
|
6798
|
+
return `Showing ${filteredResponses.length} of ${responses.length} responses (filtered)`;
|
|
6799
|
+
}
|
|
6800
|
+
return `${responses.length} response${responses.length !== 1 ? "s" : ""}`;
|
|
6801
|
+
})();
|
|
4924
6802
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4925
6803
|
"div",
|
|
4926
6804
|
{
|
|
@@ -4928,7 +6806,20 @@ function ResponseViewerInner({
|
|
|
4928
6806
|
style: { height: heightValue, width: widthValue },
|
|
4929
6807
|
children: [
|
|
4930
6808
|
!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(
|
|
6809
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6810
|
+
fieldcraftReact.Input,
|
|
6811
|
+
{
|
|
6812
|
+
type: "text",
|
|
6813
|
+
placeholder: "Search responses...",
|
|
6814
|
+
className: "h-7 text-xs w-40",
|
|
6815
|
+
value: searchQuery,
|
|
6816
|
+
onChange: (e) => {
|
|
6817
|
+
setSearchQuery(e.target.value);
|
|
6818
|
+
setCurrentPage(1);
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6821
|
+
),
|
|
6822
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-xs text-muted-foreground mr-auto", children: countText }),
|
|
4932
6823
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
4933
6824
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4934
6825
|
fieldcraftReact.Button,
|
|
@@ -4955,6 +6846,48 @@ function ResponseViewerInner({
|
|
|
4955
6846
|
onClick: () => setViewMode("card"),
|
|
4956
6847
|
children: "Cards"
|
|
4957
6848
|
}
|
|
6849
|
+
),
|
|
6850
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6851
|
+
fieldcraftReact.Button,
|
|
6852
|
+
{
|
|
6853
|
+
variant: viewMode === "timeline" ? "secondary" : "ghost",
|
|
6854
|
+
size: "sm",
|
|
6855
|
+
className: cn(
|
|
6856
|
+
"h-7 text-xs",
|
|
6857
|
+
viewMode === "timeline" && "font-semibold border border-ring"
|
|
6858
|
+
),
|
|
6859
|
+
onClick: () => setViewMode("timeline"),
|
|
6860
|
+
children: "Timeline"
|
|
6861
|
+
}
|
|
6862
|
+
)
|
|
6863
|
+
] }),
|
|
6864
|
+
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
|
|
6865
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1", children: [
|
|
6866
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6867
|
+
fieldcraftReact.Button,
|
|
6868
|
+
{
|
|
6869
|
+
variant: showStats ? "secondary" : "ghost",
|
|
6870
|
+
size: "sm",
|
|
6871
|
+
className: cn(
|
|
6872
|
+
"h-7 text-xs",
|
|
6873
|
+
showStats && "font-semibold border border-ring"
|
|
6874
|
+
),
|
|
6875
|
+
onClick: () => setShowStats((v) => !v),
|
|
6876
|
+
children: "Stats"
|
|
6877
|
+
}
|
|
6878
|
+
),
|
|
6879
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6880
|
+
fieldcraftReact.Button,
|
|
6881
|
+
{
|
|
6882
|
+
variant: showCharts ? "secondary" : "ghost",
|
|
6883
|
+
size: "sm",
|
|
6884
|
+
className: cn(
|
|
6885
|
+
"h-7 text-xs",
|
|
6886
|
+
showCharts && "font-semibold border border-ring"
|
|
6887
|
+
),
|
|
6888
|
+
onClick: () => setShowCharts((v) => !v),
|
|
6889
|
+
children: "Charts"
|
|
6890
|
+
}
|
|
4958
6891
|
)
|
|
4959
6892
|
] }),
|
|
4960
6893
|
/* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Separator, { orientation: "vertical", className: "h-5 hidden sm:block" }),
|
|
@@ -5020,6 +6953,26 @@ function ResponseViewerInner({
|
|
|
5020
6953
|
)
|
|
5021
6954
|
] })
|
|
5022
6955
|
] }),
|
|
6956
|
+
!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: [
|
|
6957
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs font-medium text-foreground", children: [
|
|
6958
|
+
selectedIds.size,
|
|
6959
|
+
" selected"
|
|
6960
|
+
] }),
|
|
6961
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-1", children: [
|
|
6962
|
+
onBulkExport && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "outline", size: "sm", className: "h-6 text-xs", onClick: handleBulkExport, children: "Export Selected" }),
|
|
6963
|
+
onBulkDelete && /* @__PURE__ */ jsxRuntime.jsx(fieldcraftReact.Button, { variant: "destructive", size: "sm", className: "h-6 text-xs", onClick: handleBulkDelete, children: "Delete Selected" }),
|
|
6964
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6965
|
+
fieldcraftReact.Button,
|
|
6966
|
+
{
|
|
6967
|
+
variant: "ghost",
|
|
6968
|
+
size: "sm",
|
|
6969
|
+
className: "h-6 text-xs",
|
|
6970
|
+
onClick: () => setSelectedIds(/* @__PURE__ */ new Set()),
|
|
6971
|
+
children: "Clear"
|
|
6972
|
+
}
|
|
6973
|
+
)
|
|
6974
|
+
] })
|
|
6975
|
+
] }),
|
|
5023
6976
|
!selectedResponse && showFilterPanel && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-filter__panel px-3 py-2 border-b border-border bg-muted/50", children: [
|
|
5024
6977
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-2 mb-2", children: [
|
|
5025
6978
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground w-16 shrink-0", children: "Date:" }),
|
|
@@ -5148,19 +7101,47 @@ function ResponseViewerInner({
|
|
|
5148
7101
|
);
|
|
5149
7102
|
})
|
|
5150
7103
|
] }),
|
|
5151
|
-
|
|
7104
|
+
!selectedResponse && showStats && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7105
|
+
StatsPanel,
|
|
7106
|
+
{
|
|
7107
|
+
schema,
|
|
7108
|
+
responses: filteredResponses,
|
|
7109
|
+
onClose: () => setShowStats(false)
|
|
7110
|
+
}
|
|
7111
|
+
),
|
|
7112
|
+
!selectedResponse && showCharts && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7113
|
+
ChartPanel,
|
|
7114
|
+
{
|
|
7115
|
+
schema,
|
|
7116
|
+
responses: filteredResponses,
|
|
7117
|
+
onClose: () => setShowCharts(false)
|
|
7118
|
+
}
|
|
7119
|
+
),
|
|
7120
|
+
/* @__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
7121
|
ResponseDetail,
|
|
5153
7122
|
{
|
|
5154
7123
|
response: selectedResponse,
|
|
5155
7124
|
fields: getFields(selectedResponse),
|
|
5156
|
-
onBack: handleBack
|
|
7125
|
+
onBack: handleBack,
|
|
7126
|
+
onDelete: onDelete && selectedResponse.sessionToken ? () => handleDeleteSingle(selectedResponse.sessionToken) : void 0
|
|
7127
|
+
}
|
|
7128
|
+
) }) : viewMode === "timeline" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
7129
|
+
TimelineView,
|
|
7130
|
+
{
|
|
7131
|
+
responses: filteredResponses,
|
|
7132
|
+
questions,
|
|
7133
|
+
onSelect: handleSelect
|
|
5157
7134
|
}
|
|
5158
7135
|
) : viewMode === "table" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
5159
7136
|
ResponseTable,
|
|
5160
7137
|
{
|
|
5161
7138
|
schema,
|
|
5162
7139
|
responses: pag.pageItems,
|
|
5163
|
-
onRowClick: handleSelect
|
|
7140
|
+
onRowClick: handleSelect,
|
|
7141
|
+
selectable: isSelectable,
|
|
7142
|
+
selectedIds,
|
|
7143
|
+
onToggleSelect: handleToggleSelect,
|
|
7144
|
+
onSelectAll: handleSelectAll
|
|
5164
7145
|
}
|
|
5165
7146
|
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-3 p-3 grid-cols-[repeat(auto-fill,minmax(280px,1fr))]", children: [
|
|
5166
7147
|
pag.pageItems.map((response, idx) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -5168,13 +7149,16 @@ function ResponseViewerInner({
|
|
|
5168
7149
|
{
|
|
5169
7150
|
response,
|
|
5170
7151
|
fields: getFields(response),
|
|
5171
|
-
onClick: () => handleSelect(response)
|
|
7152
|
+
onClick: () => handleSelect(response),
|
|
7153
|
+
selectable: isSelectable,
|
|
7154
|
+
selected: !!(response.sessionToken && selectedIds.has(response.sessionToken)),
|
|
7155
|
+
onToggleSelect: response.sessionToken ? () => handleToggleSelect(response.sessionToken) : void 0
|
|
5172
7156
|
},
|
|
5173
7157
|
response.sessionToken || idx
|
|
5174
7158
|
)),
|
|
5175
|
-
filteredResponses.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-8 text-center text-muted-foreground", children: isFiltered ? "No matching responses" : "No responses yet" })
|
|
7159
|
+
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
7160
|
] }) }),
|
|
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: [
|
|
7161
|
+
!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
7162
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fc-rv-pagination__info text-xs text-muted-foreground mr-auto", children: [
|
|
5179
7163
|
"Showing ",
|
|
5180
7164
|
pag.startItem,
|
|
@@ -5228,7 +7212,18 @@ function ResponseViewerInner({
|
|
|
5228
7212
|
}
|
|
5229
7213
|
)
|
|
5230
7214
|
] })
|
|
5231
|
-
] })
|
|
7215
|
+
] }),
|
|
7216
|
+
confirmDialog && /* @__PURE__ */ jsxRuntime.jsx(
|
|
7217
|
+
ConfirmDialog2,
|
|
7218
|
+
{
|
|
7219
|
+
title: confirmDialog.title,
|
|
7220
|
+
message: confirmDialog.message,
|
|
7221
|
+
confirmLabel: "Delete",
|
|
7222
|
+
variant: "destructive",
|
|
7223
|
+
onConfirm: confirmDialog.onConfirm,
|
|
7224
|
+
onCancel: () => setConfirmDialog(null)
|
|
7225
|
+
}
|
|
7226
|
+
)
|
|
5232
7227
|
]
|
|
5233
7228
|
}
|
|
5234
7229
|
);
|
|
@@ -5301,7 +7296,537 @@ var PREVIEW_SCHEMA = {
|
|
|
5301
7296
|
],
|
|
5302
7297
|
submitAction: { type: "adapter" }
|
|
5303
7298
|
};
|
|
7299
|
+
|
|
7300
|
+
// src/theme-editor/css-utils.ts
|
|
7301
|
+
var COLOR_MAP = {
|
|
7302
|
+
primary: "--fc-primary",
|
|
7303
|
+
primaryForeground: "--fc-primary-foreground",
|
|
7304
|
+
secondary: "--fc-secondary",
|
|
7305
|
+
secondaryForeground: "--fc-secondary-foreground",
|
|
7306
|
+
error: "--fc-error",
|
|
7307
|
+
errorForeground: "--fc-error-foreground",
|
|
7308
|
+
warning: "--fc-warning",
|
|
7309
|
+
success: "--fc-success",
|
|
7310
|
+
surface: "--fc-surface",
|
|
7311
|
+
background: "--fc-background",
|
|
7312
|
+
text: "--fc-text",
|
|
7313
|
+
textMuted: "--fc-text-muted",
|
|
7314
|
+
textDisabled: "--fc-text-disabled",
|
|
7315
|
+
border: "--fc-border",
|
|
7316
|
+
borderFocus: "--fc-border-focus",
|
|
7317
|
+
inputBackground: "--fc-input-background"
|
|
7318
|
+
};
|
|
7319
|
+
var TYPOGRAPHY_MAP = {
|
|
7320
|
+
fontFamily: "--fc-font-family",
|
|
7321
|
+
scale: "--fc-scale",
|
|
7322
|
+
questionSize: "--fc-question-size",
|
|
7323
|
+
labelSize: "--fc-label-size",
|
|
7324
|
+
helpTextSize: "--fc-help-text-size",
|
|
7325
|
+
bodySize: "--fc-body-size"
|
|
7326
|
+
};
|
|
7327
|
+
var SHAPE_MAP = {
|
|
7328
|
+
radius: "--fc-radius",
|
|
7329
|
+
inputRadius: "--fc-input-radius",
|
|
7330
|
+
buttonRadius: "--fc-button-radius",
|
|
7331
|
+
cardRadius: "--fc-card-radius"
|
|
7332
|
+
};
|
|
7333
|
+
var SPACING_MAP = {
|
|
7334
|
+
base: "--fc-spacing-base",
|
|
7335
|
+
sectionGap: "--fc-section-gap",
|
|
7336
|
+
fieldGap: "--fc-field-gap",
|
|
7337
|
+
inputPaddingX: "--fc-input-padding-x",
|
|
7338
|
+
inputPaddingY: "--fc-input-padding-y"
|
|
7339
|
+
};
|
|
7340
|
+
var LAYOUT_MAP = {
|
|
7341
|
+
maxWidth: "--fc-max-width",
|
|
7342
|
+
alignment: "--fc-alignment",
|
|
7343
|
+
progressPosition: "--fc-progress-position",
|
|
7344
|
+
sectionLayout: "--fc-section-layout"
|
|
7345
|
+
};
|
|
7346
|
+
function themeToCss(theme) {
|
|
7347
|
+
const lines = [];
|
|
7348
|
+
function addSection2(obj, map, comment) {
|
|
7349
|
+
if (!obj) return;
|
|
7350
|
+
const entries = [];
|
|
7351
|
+
for (const [key, cssVar] of Object.entries(map)) {
|
|
7352
|
+
const val = obj[key];
|
|
7353
|
+
if (val == null) continue;
|
|
7354
|
+
if (typeof val === "number") {
|
|
7355
|
+
entries.push(` ${cssVar}: ${val}px;`);
|
|
7356
|
+
} else {
|
|
7357
|
+
entries.push(` ${cssVar}: ${String(val)};`);
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
if (entries.length > 0) {
|
|
7361
|
+
lines.push(` /* ${comment} */`);
|
|
7362
|
+
lines.push(...entries);
|
|
7363
|
+
lines.push("");
|
|
7364
|
+
}
|
|
7365
|
+
}
|
|
7366
|
+
lines.push(":root {");
|
|
7367
|
+
addSection2(theme.colors, COLOR_MAP, "Colors");
|
|
7368
|
+
addSection2(theme.typography, TYPOGRAPHY_MAP, "Typography");
|
|
7369
|
+
addSection2(theme.shape, SHAPE_MAP, "Shape");
|
|
7370
|
+
addSection2(theme.spacing, SPACING_MAP, "Spacing");
|
|
7371
|
+
addSection2(theme.layout, LAYOUT_MAP, "Layout");
|
|
7372
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") {
|
|
7373
|
+
lines.pop();
|
|
7374
|
+
}
|
|
7375
|
+
lines.push("}");
|
|
7376
|
+
return lines.join("\n") + "\n";
|
|
7377
|
+
}
|
|
7378
|
+
var REVERSE_MAP = {};
|
|
7379
|
+
function buildReverseMap() {
|
|
7380
|
+
if (Object.keys(REVERSE_MAP).length > 0) return;
|
|
7381
|
+
const sections = [
|
|
7382
|
+
["colors", COLOR_MAP],
|
|
7383
|
+
["typography", TYPOGRAPHY_MAP],
|
|
7384
|
+
["shape", SHAPE_MAP],
|
|
7385
|
+
["spacing", SPACING_MAP],
|
|
7386
|
+
["layout", LAYOUT_MAP]
|
|
7387
|
+
];
|
|
7388
|
+
for (const [sectionKey, map] of sections) {
|
|
7389
|
+
for (const [propKey, cssVar] of Object.entries(map)) {
|
|
7390
|
+
REVERSE_MAP[cssVar] = [sectionKey, propKey];
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
}
|
|
7394
|
+
var NUMERIC_PROPS = /* @__PURE__ */ new Set([
|
|
7395
|
+
"base",
|
|
7396
|
+
"sectionGap",
|
|
7397
|
+
"fieldGap",
|
|
7398
|
+
"inputPaddingX",
|
|
7399
|
+
"inputPaddingY"
|
|
7400
|
+
]);
|
|
7401
|
+
function cssToTheme(css) {
|
|
7402
|
+
buildReverseMap();
|
|
7403
|
+
const theme = {};
|
|
7404
|
+
const varRegex = /(--fc-[\w-]+)\s*:\s*([^;]+);/g;
|
|
7405
|
+
let match;
|
|
7406
|
+
while ((match = varRegex.exec(css)) !== null) {
|
|
7407
|
+
const cssVar = match[1];
|
|
7408
|
+
let value = match[2].trim();
|
|
7409
|
+
const mapping = REVERSE_MAP[cssVar];
|
|
7410
|
+
if (!mapping) continue;
|
|
7411
|
+
const [sectionKey, propKey] = mapping;
|
|
7412
|
+
if (NUMERIC_PROPS.has(propKey)) {
|
|
7413
|
+
const num = parseFloat(value);
|
|
7414
|
+
if (!isNaN(num)) {
|
|
7415
|
+
value = num;
|
|
7416
|
+
}
|
|
7417
|
+
}
|
|
7418
|
+
if (!theme[sectionKey]) {
|
|
7419
|
+
theme[sectionKey] = {};
|
|
7420
|
+
}
|
|
7421
|
+
theme[sectionKey][propKey] = value;
|
|
7422
|
+
}
|
|
7423
|
+
return theme;
|
|
7424
|
+
}
|
|
7425
|
+
function exportCssFile(theme, filename = "fieldcraft-theme.css") {
|
|
7426
|
+
const css = themeToCss(theme);
|
|
7427
|
+
const blob = new Blob([css], { type: "text/css" });
|
|
7428
|
+
const url = URL.createObjectURL(blob);
|
|
7429
|
+
const a = document.createElement("a");
|
|
7430
|
+
a.href = url;
|
|
7431
|
+
a.download = filename;
|
|
7432
|
+
a.click();
|
|
7433
|
+
URL.revokeObjectURL(url);
|
|
7434
|
+
}
|
|
7435
|
+
function importCssFile(baseTheme, onImport) {
|
|
7436
|
+
const input = document.createElement("input");
|
|
7437
|
+
input.type = "file";
|
|
7438
|
+
input.accept = ".css";
|
|
7439
|
+
input.onchange = () => {
|
|
7440
|
+
const file = input.files?.[0];
|
|
7441
|
+
if (!file) return;
|
|
7442
|
+
const reader = new FileReader();
|
|
7443
|
+
reader.onload = () => {
|
|
7444
|
+
const cssText = reader.result;
|
|
7445
|
+
const partial = cssToTheme(cssText);
|
|
7446
|
+
const merged = deepMerge(baseTheme, partial);
|
|
7447
|
+
onImport(merged);
|
|
7448
|
+
};
|
|
7449
|
+
reader.readAsText(file);
|
|
7450
|
+
};
|
|
7451
|
+
input.click();
|
|
7452
|
+
}
|
|
7453
|
+
function deepMerge(base, partial) {
|
|
7454
|
+
const result = { ...base };
|
|
7455
|
+
for (const key of Object.keys(partial)) {
|
|
7456
|
+
const partialSection = partial[key];
|
|
7457
|
+
if (partialSection && typeof partialSection === "object") {
|
|
7458
|
+
result[key] = {
|
|
7459
|
+
...base[key],
|
|
7460
|
+
...partialSection
|
|
7461
|
+
};
|
|
7462
|
+
}
|
|
7463
|
+
}
|
|
7464
|
+
return result;
|
|
7465
|
+
}
|
|
7466
|
+
|
|
7467
|
+
// src/theme-editor/palette-generator.ts
|
|
7468
|
+
function generatePalette(baseHex) {
|
|
7469
|
+
const [h, s, l] = hexToHsl(baseHex);
|
|
7470
|
+
const secH = (h + 180) % 360;
|
|
7471
|
+
const isLightBase = l > 50;
|
|
7472
|
+
return {
|
|
7473
|
+
// Primary
|
|
7474
|
+
primary: hslToHex(h, s, clamp(l, 30, 60)),
|
|
7475
|
+
primaryForeground: isLightBase ? "#ffffff" : "#ffffff",
|
|
7476
|
+
// Secondary (complementary)
|
|
7477
|
+
secondary: hslToHex(secH, Math.max(s - 15, 10), clamp(l, 35, 55)),
|
|
7478
|
+
secondaryForeground: "#ffffff",
|
|
7479
|
+
// Surfaces
|
|
7480
|
+
surface: hslToHex(h, Math.max(s - 35, 3), 97),
|
|
7481
|
+
background: "#F4F7F8",
|
|
7482
|
+
inputBackground: hslToHex(h, Math.max(s - 40, 2), 99),
|
|
7483
|
+
// Text
|
|
7484
|
+
text: hslToHex(h, Math.max(s - 30, 5), 12),
|
|
7485
|
+
textMuted: hslToHex(h, Math.max(s - 30, 5), 45),
|
|
7486
|
+
textDisabled: hslToHex(h, Math.max(s - 35, 3), 65),
|
|
7487
|
+
// Borders
|
|
7488
|
+
border: hslToHex(h, Math.max(s - 30, 5), 85),
|
|
7489
|
+
borderFocus: hslToHex(h, s, clamp(l, 35, 55)),
|
|
7490
|
+
// Semantic — Drafting Teal palette
|
|
7491
|
+
error: "#B04A3C",
|
|
7492
|
+
errorForeground: "#FFFFFF",
|
|
7493
|
+
warning: "#C98A2E",
|
|
7494
|
+
success: "#2E7D5B"
|
|
7495
|
+
};
|
|
7496
|
+
}
|
|
7497
|
+
function applyPaletteToTheme(theme, palette) {
|
|
7498
|
+
return {
|
|
7499
|
+
...theme,
|
|
7500
|
+
colors: {
|
|
7501
|
+
...theme.colors,
|
|
7502
|
+
...palette
|
|
7503
|
+
}
|
|
7504
|
+
};
|
|
7505
|
+
}
|
|
7506
|
+
function hexToHsl(hex) {
|
|
7507
|
+
const rgb = hexToRgb(hex);
|
|
7508
|
+
const r = rgb[0] / 255;
|
|
7509
|
+
const g = rgb[1] / 255;
|
|
7510
|
+
const b = rgb[2] / 255;
|
|
7511
|
+
const max = Math.max(r, g, b);
|
|
7512
|
+
const min = Math.min(r, g, b);
|
|
7513
|
+
const l = (max + min) / 2;
|
|
7514
|
+
if (max === min) {
|
|
7515
|
+
return [0, 0, Math.round(l * 100)];
|
|
7516
|
+
}
|
|
7517
|
+
const d = max - min;
|
|
7518
|
+
const s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
7519
|
+
let h = 0;
|
|
7520
|
+
if (max === r) {
|
|
7521
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
|
|
7522
|
+
} else if (max === g) {
|
|
7523
|
+
h = ((b - r) / d + 2) / 6;
|
|
7524
|
+
} else {
|
|
7525
|
+
h = ((r - g) / d + 4) / 6;
|
|
7526
|
+
}
|
|
7527
|
+
return [Math.round(h * 360), Math.round(s * 100), Math.round(l * 100)];
|
|
7528
|
+
}
|
|
7529
|
+
function hslToHex(h, s, l) {
|
|
7530
|
+
const sNorm = s / 100;
|
|
7531
|
+
const lNorm = l / 100;
|
|
7532
|
+
const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
|
|
7533
|
+
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
7534
|
+
const m = lNorm - c / 2;
|
|
7535
|
+
let r = 0, g = 0, b = 0;
|
|
7536
|
+
if (h < 60) {
|
|
7537
|
+
r = c;
|
|
7538
|
+
g = x;
|
|
7539
|
+
b = 0;
|
|
7540
|
+
} else if (h < 120) {
|
|
7541
|
+
r = x;
|
|
7542
|
+
g = c;
|
|
7543
|
+
b = 0;
|
|
7544
|
+
} else if (h < 180) {
|
|
7545
|
+
r = 0;
|
|
7546
|
+
g = c;
|
|
7547
|
+
b = x;
|
|
7548
|
+
} else if (h < 240) {
|
|
7549
|
+
r = 0;
|
|
7550
|
+
g = x;
|
|
7551
|
+
b = c;
|
|
7552
|
+
} else if (h < 300) {
|
|
7553
|
+
r = x;
|
|
7554
|
+
g = 0;
|
|
7555
|
+
b = c;
|
|
7556
|
+
} else {
|
|
7557
|
+
r = c;
|
|
7558
|
+
g = 0;
|
|
7559
|
+
b = x;
|
|
7560
|
+
}
|
|
7561
|
+
return rgbToHex(
|
|
7562
|
+
Math.round((r + m) * 255),
|
|
7563
|
+
Math.round((g + m) * 255),
|
|
7564
|
+
Math.round((b + m) * 255)
|
|
7565
|
+
);
|
|
7566
|
+
}
|
|
7567
|
+
function hexToRgb(hex) {
|
|
7568
|
+
const cleaned = hex.replace("#", "");
|
|
7569
|
+
const full = cleaned.length === 3 ? cleaned[0] + cleaned[0] + cleaned[1] + cleaned[1] + cleaned[2] + cleaned[2] : cleaned;
|
|
7570
|
+
return [
|
|
7571
|
+
parseInt(full.substring(0, 2), 16),
|
|
7572
|
+
parseInt(full.substring(2, 4), 16),
|
|
7573
|
+
parseInt(full.substring(4, 6), 16)
|
|
7574
|
+
];
|
|
7575
|
+
}
|
|
7576
|
+
function rgbToHex(r, g, b) {
|
|
7577
|
+
return "#" + [r, g, b].map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
7578
|
+
}
|
|
7579
|
+
function clamp(value, min, max) {
|
|
7580
|
+
return Math.max(min, Math.min(max, value));
|
|
7581
|
+
}
|
|
7582
|
+
var SWATCH_KEYS = [
|
|
7583
|
+
{ key: "primary", label: "Primary" },
|
|
7584
|
+
{ key: "secondary", label: "Secondary" },
|
|
7585
|
+
{ key: "surface", label: "Surface" },
|
|
7586
|
+
{ key: "background", label: "Background" },
|
|
7587
|
+
{ key: "text", label: "Text" },
|
|
7588
|
+
{ key: "textMuted", label: "Muted" },
|
|
7589
|
+
{ key: "border", label: "Border" },
|
|
7590
|
+
{ key: "error", label: "Error" },
|
|
7591
|
+
{ key: "warning", label: "Warning" },
|
|
7592
|
+
{ key: "success", label: "Success" }
|
|
7593
|
+
];
|
|
7594
|
+
function PaletteGenerator({ theme, onApply, onClose }) {
|
|
7595
|
+
const [baseColor, setBaseColor] = react.useState(theme.colors?.primary ?? "#3b82f6");
|
|
7596
|
+
const palette = react.useMemo(() => generatePalette(baseColor), [baseColor]);
|
|
7597
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette", children: [
|
|
7598
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__header", children: [
|
|
7599
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-palette__title", children: "Generate Palette" }),
|
|
7600
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "fcte-btn fcte-btn--secondary fcte-btn--sm", onClick: onClose, children: "Close" })
|
|
7601
|
+
] }),
|
|
7602
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__body", children: [
|
|
7603
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__picker", children: [
|
|
7604
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "fcte-palette__label", children: "Base Color" }),
|
|
7605
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__input-row", children: [
|
|
7606
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7607
|
+
"input",
|
|
7608
|
+
{
|
|
7609
|
+
type: "color",
|
|
7610
|
+
value: baseColor,
|
|
7611
|
+
onChange: (e) => setBaseColor(e.target.value),
|
|
7612
|
+
className: "fcte-field__swatch"
|
|
7613
|
+
}
|
|
7614
|
+
),
|
|
7615
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7616
|
+
"input",
|
|
7617
|
+
{
|
|
7618
|
+
type: "text",
|
|
7619
|
+
value: baseColor,
|
|
7620
|
+
onChange: (e) => setBaseColor(e.target.value),
|
|
7621
|
+
className: "fcte-field__text",
|
|
7622
|
+
spellCheck: false
|
|
7623
|
+
}
|
|
7624
|
+
)
|
|
7625
|
+
] })
|
|
7626
|
+
] }),
|
|
7627
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatches", children: SWATCH_KEYS.map(({ key, label }) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-palette__swatch", children: [
|
|
7628
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7629
|
+
"div",
|
|
7630
|
+
{
|
|
7631
|
+
className: "fcte-palette__swatch-color",
|
|
7632
|
+
style: { backgroundColor: palette[key] }
|
|
7633
|
+
}
|
|
7634
|
+
),
|
|
7635
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatch-label", children: label }),
|
|
7636
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-palette__swatch-hex", children: palette[key] })
|
|
7637
|
+
] }, key)) }),
|
|
7638
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7639
|
+
"button",
|
|
7640
|
+
{
|
|
7641
|
+
className: "fcte-btn fcte-btn--primary",
|
|
7642
|
+
onClick: () => onApply(applyPaletteToTheme(theme, palette)),
|
|
7643
|
+
children: "Apply to Theme"
|
|
7644
|
+
}
|
|
7645
|
+
)
|
|
7646
|
+
] })
|
|
7647
|
+
] });
|
|
7648
|
+
}
|
|
7649
|
+
|
|
7650
|
+
// src/theme-editor/presets.ts
|
|
7651
|
+
var draftingTealPreset = {
|
|
7652
|
+
colors: {
|
|
7653
|
+
primary: "#1F6B6E",
|
|
7654
|
+
primaryForeground: "#FFFFFF",
|
|
7655
|
+
secondary: "#B9D1CF",
|
|
7656
|
+
secondaryForeground: "#12222A",
|
|
7657
|
+
error: "#B04A3C",
|
|
7658
|
+
errorForeground: "#FFFFFF",
|
|
7659
|
+
warning: "#C98A2E",
|
|
7660
|
+
success: "#2E7D5B",
|
|
7661
|
+
surface: "#FFFFFF",
|
|
7662
|
+
background: "#F4F7F8",
|
|
7663
|
+
text: "#12222A",
|
|
7664
|
+
textMuted: "#6A7B85",
|
|
7665
|
+
textDisabled: "#B9D1CF",
|
|
7666
|
+
border: "#DCE4E8",
|
|
7667
|
+
borderFocus: "#1F6B6E",
|
|
7668
|
+
inputBackground: "#FFFFFF"
|
|
7669
|
+
},
|
|
7670
|
+
typography: {
|
|
7671
|
+
fontFamily: "'Space Grotesk', 'IBM Plex Sans', system-ui, sans-serif",
|
|
7672
|
+
scale: "comfortable",
|
|
7673
|
+
questionSize: "1.1875rem",
|
|
7674
|
+
labelSize: "0.8125rem",
|
|
7675
|
+
helpTextSize: "0.8125rem",
|
|
7676
|
+
bodySize: "0.9375rem"
|
|
7677
|
+
},
|
|
7678
|
+
shape: {
|
|
7679
|
+
radius: "none",
|
|
7680
|
+
inputRadius: "0px",
|
|
7681
|
+
buttonRadius: "0px",
|
|
7682
|
+
cardRadius: "0px"
|
|
7683
|
+
},
|
|
7684
|
+
spacing: {
|
|
7685
|
+
base: 16,
|
|
7686
|
+
sectionGap: 32,
|
|
7687
|
+
fieldGap: 24,
|
|
7688
|
+
inputPaddingX: 12,
|
|
7689
|
+
inputPaddingY: 10
|
|
7690
|
+
},
|
|
7691
|
+
layout: {
|
|
7692
|
+
maxWidth: "640px",
|
|
7693
|
+
alignment: "left",
|
|
7694
|
+
progressPosition: "top",
|
|
7695
|
+
sectionLayout: "flat"
|
|
7696
|
+
}
|
|
7697
|
+
};
|
|
7698
|
+
var COMPARISON_PRESETS = {
|
|
7699
|
+
"drafting-teal": { label: "Drafting Teal", theme: draftingTealPreset },
|
|
7700
|
+
clean: { label: "Clean", theme: fieldcraftReact.cleanPreset },
|
|
7701
|
+
dark: { label: "Dark", theme: fieldcraftReact.darkPreset },
|
|
7702
|
+
modern: { label: "Modern", theme: fieldcraftReact.modernPreset },
|
|
7703
|
+
"high-contrast": { label: "High Contrast", theme: fieldcraftReact.highContrastPreset },
|
|
7704
|
+
clinical: { label: "Clinical", theme: fieldcraftReact.clinicalPreset },
|
|
7705
|
+
playful: { label: "Playful", theme: fieldcraftReact.playfulPreset }
|
|
7706
|
+
};
|
|
7707
|
+
function ThemeComparison({ currentTheme, onClose }) {
|
|
7708
|
+
const [compareKey, setCompareKey] = react.useState("clean");
|
|
7709
|
+
const compareTheme = COMPARISON_PRESETS[compareKey]?.theme ?? fieldcraftReact.cleanPreset;
|
|
7710
|
+
const diffs = getColorDiffs(currentTheme, compareTheme);
|
|
7711
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare", children: [
|
|
7712
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__header", children: [
|
|
7713
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__title", children: "Theme Comparison" }),
|
|
7714
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__controls", children: [
|
|
7715
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "fcte-compare__label", children: "Compare with:" }),
|
|
7716
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7717
|
+
"select",
|
|
7718
|
+
{
|
|
7719
|
+
value: compareKey,
|
|
7720
|
+
onChange: (e) => setCompareKey(e.target.value),
|
|
7721
|
+
className: "fcte-toolbar__preset",
|
|
7722
|
+
children: Object.entries(COMPARISON_PRESETS).map(([key, { label }]) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: key, children: label }, key))
|
|
7723
|
+
}
|
|
7724
|
+
),
|
|
7725
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { className: "fcte-btn fcte-btn--secondary fcte-btn--sm", onClick: onClose, children: "Close" })
|
|
7726
|
+
] })
|
|
7727
|
+
] }),
|
|
7728
|
+
diffs.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diffs", children: [
|
|
7729
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fcte-compare__diff-count", children: [
|
|
7730
|
+
diffs.length,
|
|
7731
|
+
" color",
|
|
7732
|
+
diffs.length !== 1 ? "s" : "",
|
|
7733
|
+
" differ"
|
|
7734
|
+
] }),
|
|
7735
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-list", children: [
|
|
7736
|
+
diffs.slice(0, 8).map((d) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-item", children: [
|
|
7737
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__diff-label", children: d.key }),
|
|
7738
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__diff-swatches", children: [
|
|
7739
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7740
|
+
"div",
|
|
7741
|
+
{
|
|
7742
|
+
className: "fcte-compare__diff-swatch",
|
|
7743
|
+
style: { backgroundColor: d.current },
|
|
7744
|
+
title: `Current: ${d.current}`
|
|
7745
|
+
}
|
|
7746
|
+
),
|
|
7747
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "fcte-compare__diff-arrow", children: "\u2192" }),
|
|
7748
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
7749
|
+
"div",
|
|
7750
|
+
{
|
|
7751
|
+
className: "fcte-compare__diff-swatch",
|
|
7752
|
+
style: { backgroundColor: d.compare },
|
|
7753
|
+
title: `${COMPARISON_PRESETS[compareKey]?.label}: ${d.compare}`
|
|
7754
|
+
}
|
|
7755
|
+
)
|
|
7756
|
+
] })
|
|
7757
|
+
] }, d.key)),
|
|
7758
|
+
diffs.length > 8 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "fcte-compare__diff-more", children: [
|
|
7759
|
+
"+",
|
|
7760
|
+
diffs.length - 8,
|
|
7761
|
+
" more"
|
|
7762
|
+
] })
|
|
7763
|
+
] })
|
|
7764
|
+
] }),
|
|
7765
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panels", children: [
|
|
7766
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panel", children: [
|
|
7767
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-label", children: "Current Theme" }),
|
|
7768
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-preview", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
7769
|
+
fieldcraftReact.FormEngineRenderer,
|
|
7770
|
+
{
|
|
7771
|
+
schema: PREVIEW_SCHEMA,
|
|
7772
|
+
theme: currentTheme,
|
|
7773
|
+
onSubmit: () => {
|
|
7774
|
+
}
|
|
7775
|
+
}
|
|
7776
|
+
) })
|
|
7777
|
+
] }),
|
|
7778
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-compare__panel", children: [
|
|
7779
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-label", children: COMPARISON_PRESETS[compareKey]?.label ?? "Preset" }),
|
|
7780
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-compare__panel-preview", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
7781
|
+
fieldcraftReact.FormEngineRenderer,
|
|
7782
|
+
{
|
|
7783
|
+
schema: PREVIEW_SCHEMA,
|
|
7784
|
+
theme: compareTheme,
|
|
7785
|
+
onSubmit: () => {
|
|
7786
|
+
}
|
|
7787
|
+
}
|
|
7788
|
+
) })
|
|
7789
|
+
] })
|
|
7790
|
+
] })
|
|
7791
|
+
] });
|
|
7792
|
+
}
|
|
7793
|
+
function getColorDiffs(a, b) {
|
|
7794
|
+
const diffs = [];
|
|
7795
|
+
const aColors = a.colors ?? {};
|
|
7796
|
+
const bColors = b.colors ?? {};
|
|
7797
|
+
const allKeys = /* @__PURE__ */ new Set([...Object.keys(aColors), ...Object.keys(bColors)]);
|
|
7798
|
+
for (const key of allKeys) {
|
|
7799
|
+
const aVal = aColors[key] ?? "";
|
|
7800
|
+
const bVal = bColors[key] ?? "";
|
|
7801
|
+
if (aVal.toLowerCase() !== bVal.toLowerCase() && (aVal || bVal)) {
|
|
7802
|
+
diffs.push({ key, current: aVal || "(unset)", compare: bVal || "(unset)" });
|
|
7803
|
+
}
|
|
7804
|
+
}
|
|
7805
|
+
return diffs;
|
|
7806
|
+
}
|
|
7807
|
+
var ThemeCtx2 = react.createContext({});
|
|
7808
|
+
function themeToCssVars2(theme) {
|
|
7809
|
+
const vars = {};
|
|
7810
|
+
if (theme.background) vars["--fcte-bg"] = theme.background;
|
|
7811
|
+
if (theme.surface) vars["--fcte-surface"] = theme.surface;
|
|
7812
|
+
if (theme.surfaceHover) vars["--fcte-surface-hover"] = theme.surfaceHover;
|
|
7813
|
+
if (theme.text) vars["--fcte-text"] = theme.text;
|
|
7814
|
+
if (theme.textMuted) vars["--fcte-text-muted"] = theme.textMuted;
|
|
7815
|
+
if (theme.textDim) vars["--fcte-text-dim"] = theme.textDim;
|
|
7816
|
+
if (theme.border) vars["--fcte-border"] = theme.border;
|
|
7817
|
+
if (theme.borderStrong) vars["--fcte-border-strong"] = theme.borderStrong;
|
|
7818
|
+
if (theme.inputBackground) vars["--fcte-input-bg"] = theme.inputBackground;
|
|
7819
|
+
if (theme.accent) vars["--fcte-accent"] = theme.accent;
|
|
7820
|
+
if (theme.accentForeground) vars["--fcte-accent-text"] = theme.accentForeground;
|
|
7821
|
+
return vars;
|
|
7822
|
+
}
|
|
7823
|
+
function ThemeEditorThemeProvider({ theme, children }) {
|
|
7824
|
+
const resolved = theme ?? {};
|
|
7825
|
+
const cssVars = react.useMemo(() => themeToCssVars2(resolved), [resolved]);
|
|
7826
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ThemeCtx2.Provider, { value: resolved, children: /* @__PURE__ */ jsxRuntime.jsx("div", { "data-fcte-root": "", style: cssVars, className: "fcte-provider-root", children }) });
|
|
7827
|
+
}
|
|
5304
7828
|
var PRESETS = {
|
|
7829
|
+
"drafting-teal": draftingTealPreset,
|
|
5305
7830
|
clean: fieldcraftReact.cleanPreset,
|
|
5306
7831
|
dark: fieldcraftReact.darkPreset,
|
|
5307
7832
|
modern: fieldcraftReact.modernPreset,
|
|
@@ -5431,15 +7956,18 @@ function ThemeEditorInner({
|
|
|
5431
7956
|
initialTheme,
|
|
5432
7957
|
onChange,
|
|
5433
7958
|
onSave,
|
|
7959
|
+
theme: chromeTheme,
|
|
5434
7960
|
height,
|
|
5435
7961
|
width,
|
|
5436
7962
|
className,
|
|
5437
7963
|
toolbarExtra,
|
|
5438
7964
|
showPreview = true
|
|
5439
7965
|
}) {
|
|
5440
|
-
const [theme, setTheme] = react.useState(initialTheme ??
|
|
7966
|
+
const [theme, setTheme] = react.useState(initialTheme ?? draftingTealPreset);
|
|
5441
7967
|
const [activeSection, setActiveSection] = react.useState("colors");
|
|
5442
|
-
const [presetKey, setPresetKey] = react.useState("custom");
|
|
7968
|
+
const [presetKey, setPresetKey] = react.useState(initialTheme ? "custom" : "drafting-teal");
|
|
7969
|
+
const [showPalette, setShowPalette] = react.useState(false);
|
|
7970
|
+
const [showComparison, setShowComparison] = react.useState(false);
|
|
5443
7971
|
const themeRef = react.useRef(theme);
|
|
5444
7972
|
themeRef.current = theme;
|
|
5445
7973
|
react.useEffect(() => {
|
|
@@ -5507,12 +8035,22 @@ function ThemeEditorInner({
|
|
|
5507
8035
|
};
|
|
5508
8036
|
input.click();
|
|
5509
8037
|
}, [onChange]);
|
|
8038
|
+
const exportCss = react.useCallback(() => {
|
|
8039
|
+
exportCssFile(theme);
|
|
8040
|
+
}, [theme]);
|
|
8041
|
+
const importCss = react.useCallback(() => {
|
|
8042
|
+
importCssFile(theme, (merged) => {
|
|
8043
|
+
setTheme(merged);
|
|
8044
|
+
setPresetKey("custom");
|
|
8045
|
+
onChange?.(merged);
|
|
8046
|
+
});
|
|
8047
|
+
}, [theme, onChange]);
|
|
5510
8048
|
const currentSection = react.useMemo(
|
|
5511
8049
|
() => SECTIONS.find((s) => s.id === activeSection),
|
|
5512
8050
|
[activeSection]
|
|
5513
8051
|
);
|
|
5514
8052
|
const sectionValues = theme[currentSection.themeKey] ?? {};
|
|
5515
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
8053
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ThemeEditorThemeProvider, { theme: chromeTheme, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5516
8054
|
"div",
|
|
5517
8055
|
{
|
|
5518
8056
|
className: `fcte-root${className ? ` ${className}` : ""}`,
|
|
@@ -5539,22 +8077,65 @@ function ThemeEditorInner({
|
|
|
5539
8077
|
] }),
|
|
5540
8078
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-toolbar__right", children: [
|
|
5541
8079
|
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" }),
|
|
8080
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: importJson, className: "fcte-btn fcte-btn--secondary", children: "Import JSON" }),
|
|
8081
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: exportJson, className: "fcte-btn fcte-btn--secondary", children: "Export JSON" }),
|
|
8082
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: importCss, className: "fcte-btn fcte-btn--secondary", children: "Import CSS" }),
|
|
8083
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: exportCss, className: "fcte-btn fcte-btn--secondary", children: "Export CSS" }),
|
|
8084
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8085
|
+
"button",
|
|
8086
|
+
{
|
|
8087
|
+
onClick: () => {
|
|
8088
|
+
setShowPalette((v) => !v);
|
|
8089
|
+
setShowComparison(false);
|
|
8090
|
+
},
|
|
8091
|
+
className: `fcte-btn ${showPalette ? "fcte-btn--primary" : "fcte-btn--secondary"}`,
|
|
8092
|
+
children: "Palette"
|
|
8093
|
+
}
|
|
8094
|
+
),
|
|
8095
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8096
|
+
"button",
|
|
8097
|
+
{
|
|
8098
|
+
onClick: () => {
|
|
8099
|
+
setShowComparison((v) => !v);
|
|
8100
|
+
setShowPalette(false);
|
|
8101
|
+
},
|
|
8102
|
+
className: `fcte-btn ${showComparison ? "fcte-btn--primary" : "fcte-btn--secondary"}`,
|
|
8103
|
+
children: "Compare"
|
|
8104
|
+
}
|
|
8105
|
+
),
|
|
5544
8106
|
onSave && /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: () => onSave(theme), className: "fcte-btn fcte-btn--primary", children: "Save" })
|
|
5545
8107
|
] })
|
|
5546
8108
|
] }),
|
|
5547
|
-
/* @__PURE__ */ jsxRuntime.
|
|
8109
|
+
showPalette && /* @__PURE__ */ jsxRuntime.jsx(
|
|
8110
|
+
PaletteGenerator,
|
|
8111
|
+
{
|
|
8112
|
+
theme,
|
|
8113
|
+
onApply: (newTheme) => {
|
|
8114
|
+
setTheme(newTheme);
|
|
8115
|
+
setPresetKey("custom");
|
|
8116
|
+
onChange?.(newTheme);
|
|
8117
|
+
setShowPalette(false);
|
|
8118
|
+
},
|
|
8119
|
+
onClose: () => setShowPalette(false)
|
|
8120
|
+
}
|
|
8121
|
+
),
|
|
8122
|
+
showComparison ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
8123
|
+
ThemeComparison,
|
|
8124
|
+
{
|
|
8125
|
+
currentTheme: theme,
|
|
8126
|
+
onClose: () => setShowComparison(false)
|
|
8127
|
+
}
|
|
8128
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-body", children: [
|
|
5548
8129
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fcte-editor", children: [
|
|
5549
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-
|
|
5550
|
-
"
|
|
8130
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-section-select", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
8131
|
+
"select",
|
|
5551
8132
|
{
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
)
|
|
8133
|
+
value: activeSection,
|
|
8134
|
+
onChange: (e) => setActiveSection(e.target.value),
|
|
8135
|
+
className: "fcte-field__select",
|
|
8136
|
+
children: SECTIONS.map((s) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: s.id, children: s.label }, s.id))
|
|
8137
|
+
}
|
|
8138
|
+
) }),
|
|
5558
8139
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-fields", children: currentSection.fields.map((field) => {
|
|
5559
8140
|
const val = sectionValues[field.key];
|
|
5560
8141
|
if (field.kind === "color") {
|
|
@@ -5632,19 +8213,29 @@ function ThemeEditorInner({
|
|
|
5632
8213
|
] }, field.key);
|
|
5633
8214
|
}) })
|
|
5634
8215
|
] }),
|
|
5635
|
-
showPreview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
5636
|
-
|
|
8216
|
+
showPreview && /* @__PURE__ */ jsxRuntime.jsx(
|
|
8217
|
+
"div",
|
|
5637
8218
|
{
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
8219
|
+
className: "fcte-preview",
|
|
8220
|
+
style: {
|
|
8221
|
+
background: theme.colors?.background || void 0,
|
|
8222
|
+
color: theme.colors?.text || void 0
|
|
8223
|
+
},
|
|
8224
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fcte-preview__inner", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
8225
|
+
fieldcraftReact.FormEngineRenderer,
|
|
8226
|
+
{
|
|
8227
|
+
schema: PREVIEW_SCHEMA,
|
|
8228
|
+
theme,
|
|
8229
|
+
onSubmit: () => {
|
|
8230
|
+
}
|
|
8231
|
+
}
|
|
8232
|
+
) })
|
|
5642
8233
|
}
|
|
5643
|
-
)
|
|
8234
|
+
)
|
|
5644
8235
|
] })
|
|
5645
8236
|
]
|
|
5646
8237
|
}
|
|
5647
|
-
);
|
|
8238
|
+
) });
|
|
5648
8239
|
}
|
|
5649
8240
|
|
|
5650
8241
|
// src/theme-editor/ThemeEditor.tsx
|
|
@@ -5653,7 +8244,7 @@ var ThemeEditor = requireLicense(ThemeEditorInner, "ThemeEditor");
|
|
|
5653
8244
|
// src/index.ts
|
|
5654
8245
|
if (typeof globalThis !== "undefined" && typeof globalThis.process !== "undefined" && globalThis.process.env?.NODE_ENV !== "production") {
|
|
5655
8246
|
const _fc_banner = `
|
|
5656
|
-
%c FieldCraft Pro %c v1.0
|
|
8247
|
+
%c FieldCraft Pro %c v1.4.0
|
|
5657
8248
|
|
|
5658
8249
|
%cForm Builder \xB7 Response Viewer \xB7 Theme Editor
|
|
5659
8250
|
|