@squaredr/fieldcraft-pro 1.6.4 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # @squaredr/fieldcraft-pro
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@squaredr/fieldcraft-pro.svg)](https://www.npmjs.com/package/@squaredr/fieldcraft-pro)
4
- [![license](https://img.shields.io/badge/license-Commercial-blue.svg)](https://squaredr.tech/products/fieldcraft/admin-pro#pricing)
4
+ [![license](https://img.shields.io/badge/license-Commercial-blue.svg)](https://fieldcraft.squaredr.tech/pro#pricing)
5
5
 
6
- Pro components for [FieldCraft](https://squaredr.tech/products/fieldcraft) — a schema-driven form engine for React. Build forms visually, view responses, and customize themes without writing code.
6
+ Pro components for [FieldCraft](https://fieldcraft.squaredr.tech) — a schema-driven form engine for React. Build forms visually, view responses, and customize themes without writing code.
7
7
 
8
8
  ## What's Included
9
9
 
10
10
  | Component | Description |
11
11
  |-----------|-------------|
12
- | **FormBuilder** | Drag-and-drop visual form designer with 32 field types, undo/redo, import/export, and live preview |
12
+ | **FormBuilder** | Drag-and-drop visual form designer with 44 field types, undo/redo, import/export, and live preview |
13
13
  | **ResponseViewer** | Read-only viewer for submitted form data with table and detail views, CSV/JSON export |
14
14
  | **ThemeEditor** | Visual theme editor for form styling with color pickers, typography controls, and live preview |
15
15
 
@@ -67,18 +67,18 @@ import { FormBuilder } from "@squaredr/fieldcraft-pro";
67
67
  />
68
68
  ```
69
69
 
70
- ### Field Types (32)
70
+ ### Field Types (42)
71
71
 
72
72
  | Category | Fields |
73
73
  |----------|--------|
74
- | Text | Short Text, Long Text, Email, Phone, URL |
75
- | Numeric | Number, Slider, Rating, NPS, Opinion Scale |
76
- | Selection | Single Select, Multi Select, Dropdown, Yes/No, Ranking |
77
- | Date/Time | Date, Time |
78
- | Media | File Upload |
74
+ | Text | Short Text, Long Text, Email, Phone, URL, Password |
75
+ | Numeric | Number, Slider, Rating, NPS, Opinion Scale, Currency |
76
+ | Selection | Single Select, Multi Select, Dropdown, Yes/No, Ranking, Checkbox Group |
77
+ | Date/Time | Date, Time, Date Range, Date of Birth |
78
+ | Media | File Upload, Signature, Image Capture |
79
79
  | Content | Welcome Screen, Thank You Screen, Rich Text, Image, Video, Divider, Spacer |
80
80
  | Structural | Section Header, Info Block, Page Break, Consent |
81
- | Advanced | Matrix, Calculated, Hidden |
81
+ | Advanced | Matrix, Calculated, Hidden, Repeater, Address, Legal Name, Country Select |
82
82
 
83
83
  ### Extending with Custom Fields
84
84
 
@@ -125,8 +125,8 @@ Includes presets: Clean, Dark, Modern, High Contrast, Clinical, Playful.
125
125
  ## Requirements
126
126
 
127
127
  - React 18 or 19
128
- - `@squaredr/fieldcraft-core` >= 1.3.3
129
- - `@squaredr/fieldcraft-react` >= 1.2.8
128
+ - `@squaredr/fieldcraft-core` >= 1.6.0
129
+ - `@squaredr/fieldcraft-react` >= 1.6.0
130
130
 
131
131
  ## License
132
132
 
@@ -136,4 +136,4 @@ See [LICENSE.txt](./LICENSE.txt) for details.
136
136
 
137
137
  ---
138
138
 
139
- [Get a license](https://squaredr.tech/products/fieldcraft/admin-pro#pricing) | [Documentation](https://squaredr.tech/products/fieldcraft/docs/pro) | [GitHub](https://github.com/SquaredR98/fieldcraft-pro) | [Discord](https://discord.gg/squaredr)
139
+ [Get a license](https://fieldcraft.squaredr.tech/pro#pricing) | [Documentation](https://fieldcraft.squaredr.tech/docs) | [GitHub](https://github.com/SquaredR98/fieldcraft-pro) | [Discord](https://discord.gg/squaredr)
@@ -2,37 +2,16 @@ import { createContext, useState, useEffect, useContext } from 'react';
2
2
  import { jsx, jsxs } from 'react/jsx-runtime';
3
3
 
4
4
  // ../license/dist/index.mjs
5
+ var PRODUCT_IDS = {
6
+ FIELDCRAFT_PRO: 1
7
+ };
5
8
  var LEGACY_TIER_MAP = {
6
9
  "01": "starter",
7
10
  "02": "pro",
8
11
  "03": "business",
9
12
  "04": "enterprise"
10
13
  };
11
- var TIER_MAP_NUMERIC = {
12
- 1: "starter",
13
- 2: "pro",
14
- 3: "business",
15
- 4: "enterprise"
16
- };
17
- var PRO_FEATURES = [
18
- "SchemaEditor",
19
- "ResponseViewer",
20
- "ThemeEditor",
21
- "FormBuilder",
22
- "ApiManager",
23
- "Analytics"
24
- ];
25
- var ALL_FEATURES = [
26
- ...PRO_FEATURES,
27
- "Telehealth"
28
- ];
29
- var TIER_FEATURES = {
30
- starter: ["SchemaEditor", "ResponseViewer"],
31
- pro: PRO_FEATURES,
32
- business: ALL_FEATURES,
33
- enterprise: ALL_FEATURES
34
- };
35
- var HMAC_SECRET = "e50a657813a5ab9563050728a605b9ab58b911d5c1a9f48c6825f379a533ccd7685d11d01937d9f135776a0db3efeefe29b3e5282b27ede8cee93758f94f2d75";
14
+ var HMAC_SECRET = "15bb3af8d82d01de70ab556c5785fd92461e336b941ed65cf99834d5e99924e742db9a9197056b531bf0f429df621be17cb71d8ba5e892075e1b4283969657dc3bbf4d0287cf15804d5482510b8a405bb33a69249e8007a8a3c2af0d499423623dafecb719d985a4ad8e2edf8623936db90387ea3139f8d0eeb2556e408a0d6de69c034e33e1e6a0633af0eeeb63dd13f4711ba0a0aacc3af6b87bdace13a7886013d3a5410817966f0af7230f277b6207ad5de2916f6c0bfab5c2a18ccfe72d513bb726206d8929e8b01d91f6bf38572c0f238302c4fdf0263c7bd0c48cf0e1678f94de727b61388c672c11e82ecb96591d264731177d7588d92cdf70ea46a2";
36
15
  var B32_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
37
16
  function base32hexDecode(str) {
38
17
  const upper = str.toUpperCase();
@@ -66,14 +45,15 @@ function parseOpaqueKey(key) {
66
45
  if (!stripped.startsWith("FCPRO")) return null;
67
46
  const encoded = stripped.slice(5);
68
47
  const decoded = base32hexDecode(encoded);
69
- if (!decoded || decoded.length < 17) return null;
70
- const tier = decoded[0];
71
- if (!(tier in TIER_MAP_NUMERIC)) return null;
72
- const expiryTimestamp = decoded[1] << 24 | decoded[2] << 16 | decoded[3] << 8 | decoded[4];
73
- const expiryUnsigned = expiryTimestamp >>> 0;
74
- const nonce = decoded.slice(5, 9);
75
- const signature = decoded.slice(9, 17);
76
- return { format: "opaque", tier, expiryTimestamp: expiryUnsigned, nonce, signature };
48
+ if (!decoded || decoded.length < 25) return null;
49
+ const productId = decoded[0];
50
+ if (productId !== PRODUCT_IDS.FIELDCRAFT_PRO) return null;
51
+ const purchaseTimestamp = (decoded[1] << 24 | decoded[2] << 16 | decoded[3] << 8 | decoded[4]) >>> 0;
52
+ const expiryTimestamp = (decoded[5] << 24 | decoded[6] << 16 | decoded[7] << 8 | decoded[8]) >>> 0;
53
+ const emailHash = decoded.slice(9, 13);
54
+ const nonce = decoded.slice(13, 17);
55
+ const signature = decoded.slice(17, 25);
56
+ return { format: "opaque", productId, purchaseTimestamp, expiryTimestamp, emailHash, nonce, signature };
77
57
  }
78
58
  function parseKey(key) {
79
59
  if (!key || typeof key !== "string") return null;
@@ -119,54 +99,52 @@ function isExpiredOpaque(expiryTimestamp) {
119
99
  if (expiryTimestamp === 4294967295) return false;
120
100
  return Date.now() / 1e3 > expiryTimestamp;
121
101
  }
102
+ function timingSafeEqual(a, b) {
103
+ if (a.length !== b.length) return false;
104
+ let result = 0;
105
+ for (let i = 0; i < a.length; i++) {
106
+ result |= a[i] ^ b[i];
107
+ }
108
+ return result === 0;
109
+ }
122
110
  async function validateLicense(key) {
123
111
  if (!key || typeof key !== "string") {
124
- return { status: "invalid", tier: null };
112
+ return { status: "invalid" };
125
113
  }
126
114
  const parsed = parseKey(key);
127
115
  if (!parsed) {
128
- return { status: "invalid", tier: null };
116
+ return { status: "invalid" };
129
117
  }
130
118
  if (parsed.format === "opaque") {
131
- const payload = new Uint8Array(9);
132
- payload[0] = parsed.tier;
133
- payload[1] = parsed.expiryTimestamp >>> 24 & 255;
134
- payload[2] = parsed.expiryTimestamp >>> 16 & 255;
135
- payload[3] = parsed.expiryTimestamp >>> 8 & 255;
136
- payload[4] = parsed.expiryTimestamp & 255;
137
- payload.set(parsed.nonce, 5);
119
+ const payload = new Uint8Array(17);
120
+ payload[0] = parsed.productId;
121
+ payload[1] = parsed.purchaseTimestamp >>> 24 & 255;
122
+ payload[2] = parsed.purchaseTimestamp >>> 16 & 255;
123
+ payload[3] = parsed.purchaseTimestamp >>> 8 & 255;
124
+ payload[4] = parsed.purchaseTimestamp & 255;
125
+ payload[5] = parsed.expiryTimestamp >>> 24 & 255;
126
+ payload[6] = parsed.expiryTimestamp >>> 16 & 255;
127
+ payload[7] = parsed.expiryTimestamp >>> 8 & 255;
128
+ payload[8] = parsed.expiryTimestamp & 255;
129
+ payload.set(parsed.emailHash, 9);
130
+ payload.set(parsed.nonce, 13);
138
131
  const expectedSig = await computeHmacOpaque(payload);
139
- let match = true;
140
- for (let i = 0; i < 8; i++) {
141
- if (parsed.signature[i] !== expectedSig[i]) {
142
- match = false;
143
- break;
144
- }
145
- }
146
- if (!match) {
147
- return { status: "invalid", tier: null };
148
- }
149
- const tier2 = TIER_MAP_NUMERIC[parsed.tier];
150
- if (!tier2) {
151
- return { status: "invalid", tier: null };
132
+ if (!timingSafeEqual(parsed.signature, expectedSig)) {
133
+ return { status: "invalid" };
152
134
  }
153
135
  if (isExpiredOpaque(parsed.expiryTimestamp)) {
154
- return { status: "expired", tier: tier2 };
136
+ return { status: "expired" };
155
137
  }
156
- return { status: "valid", tier: tier2 };
138
+ return { status: "valid" };
157
139
  }
158
140
  const expectedHash = await computeHmacLegacy(parsed.tierHex, parsed.expiryHex);
159
141
  if (parsed.hash.toUpperCase() !== expectedHash) {
160
- return { status: "invalid", tier: null };
142
+ return { status: "invalid" };
161
143
  }
162
- const tier = LEGACY_TIER_MAP[parsed.tierHex];
163
144
  if (isExpiredLegacy(parsed.expiryHex)) {
164
- return { status: "expired", tier };
145
+ return { status: "expired" };
165
146
  }
166
- return { status: "valid", tier };
167
- }
168
- function tierHasFeature(tier, featureName) {
169
- return TIER_FEATURES[tier]?.includes(featureName) ?? false;
147
+ return { status: "valid" };
170
148
  }
171
149
  var DEV_HOSTNAMES = /* @__PURE__ */ new Set([
172
150
  "localhost",
@@ -239,7 +217,7 @@ function isProductionEnvironment() {
239
217
  }
240
218
  return false;
241
219
  }
242
- var PING_URL = "https://squaredr.tech/api/license/ping";
220
+ var PING_URL = typeof process !== "undefined" && process.env?.NEXT_PUBLIC_FCPRO_PING_URL || "https://fieldcraft.dev/api/license/ping";
243
221
  var CACHE_KEY = "fcpro_lp";
244
222
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
245
223
  function hashKey(key) {
@@ -267,10 +245,10 @@ function readCache(key) {
267
245
  return null;
268
246
  }
269
247
  }
270
- function writeCache(key, status, tier, reason = null) {
248
+ function writeCache(key, status, reason = null) {
271
249
  if (typeof window === "undefined" || !window.localStorage) return;
272
250
  try {
273
- const entry = { s: status, t: tier, r: reason, ts: Date.now() };
251
+ const entry = { s: status, r: reason, ts: Date.now() };
274
252
  localStorage.setItem(cacheId(key), JSON.stringify(entry));
275
253
  } catch {
276
254
  }
@@ -278,12 +256,12 @@ function writeCache(key, status, tier, reason = null) {
278
256
  function getCachedPingResult(key) {
279
257
  const cached = readCache(key);
280
258
  if (!cached) return null;
281
- return { status: cached.s, tier: cached.t, reason: cached.r || void 0 };
259
+ return { status: cached.s, reason: cached.r || void 0 };
282
260
  }
283
261
  async function performPing(key) {
284
262
  const cached = readCache(key);
285
263
  if (cached) {
286
- return { status: cached.s, tier: cached.t };
264
+ return { status: cached.s };
287
265
  }
288
266
  try {
289
267
  const controller = new AbortController();
@@ -297,29 +275,28 @@ async function performPing(key) {
297
275
  clearTimeout(timeout);
298
276
  if (!res.ok) return null;
299
277
  const data = await res.json();
300
- const tier = data.tier || null;
301
278
  if (data.status === "active") {
302
- writeCache(key, "valid", tier);
303
- return { status: "valid", tier };
279
+ writeCache(key, "valid");
280
+ return { status: "valid" };
304
281
  }
305
282
  if (data.status === "revoked") {
306
- writeCache(key, "revoked", null, data.reason);
307
- return { status: "revoked", tier: null, reason: data.reason };
283
+ writeCache(key, "revoked", data.reason);
284
+ return { status: "revoked", reason: data.reason };
308
285
  }
309
286
  if (data.status === "expired") {
310
- writeCache(key, "expired", tier);
311
- return { status: "expired", tier };
287
+ writeCache(key, "expired");
288
+ return { status: "expired" };
312
289
  }
313
290
  if (data.status === "domain_mismatch") {
314
- writeCache(key, "domain_mismatch", null, data.reason);
315
- return { status: "domain_mismatch", tier: null, reason: data.reason };
291
+ writeCache(key, "domain_mismatch", data.reason);
292
+ return { status: "domain_mismatch", reason: data.reason };
316
293
  }
317
294
  return null;
318
295
  } catch {
319
296
  return null;
320
297
  }
321
298
  }
322
- var defaultContext = { status: "validating", tier: null };
299
+ var defaultContext = { status: "validating" };
323
300
  var LicenseCtx = createContext(defaultContext);
324
301
  function FieldCraftProProvider({ licenseKey, children }) {
325
302
  const [license, setLicense] = useState(defaultContext);
@@ -333,7 +310,7 @@ function FieldCraftProProvider({ licenseKey, children }) {
333
310
  const cached = getCachedPingResult(licenseKey);
334
311
  if (cached) {
335
312
  if (cached.status === "revoked" || cached.status === "domain_mismatch") {
336
- setLicense({ status: cached.status, tier: null });
313
+ setLicense({ status: cached.status });
337
314
  }
338
315
  return;
339
316
  }
@@ -341,7 +318,7 @@ function FieldCraftProProvider({ licenseKey, children }) {
341
318
  if (cancelled) return;
342
319
  if (!pingResult) return;
343
320
  if (pingResult.status === "revoked" || pingResult.status === "domain_mismatch") {
344
- setLicense({ status: pingResult.status, tier: null });
321
+ setLicense({ status: pingResult.status });
345
322
  }
346
323
  });
347
324
  });
@@ -549,7 +526,7 @@ function UnlicensedOverlay({ featureName, reason, children }) {
549
526
  }
550
527
  function requireLicense(Component, featureName) {
551
528
  function LicenseGated(props) {
552
- const { status, tier } = useLicense();
529
+ const { status } = useLicense();
553
530
  const isProduction = isProductionEnvironment();
554
531
  if (!isProduction) {
555
532
  return /* @__PURE__ */ jsx(Component, { ...props });
@@ -557,8 +534,7 @@ function requireLicense(Component, featureName) {
557
534
  if (status === "validating") {
558
535
  return null;
559
536
  }
560
- const isLicensed = status === "valid" && tier != null && tierHasFeature(tier, featureName);
561
- if (isLicensed) {
537
+ if (status === "valid") {
562
538
  return /* @__PURE__ */ jsx(Component, { ...props });
563
539
  }
564
540
  let reason = "A valid license is required for production use.";
@@ -570,8 +546,6 @@ function requireLicense(Component, featureName) {
570
546
  reason = "This license key has been revoked. Please contact support or purchase a new license.";
571
547
  } else if (status === "domain_mismatch") {
572
548
  reason = "This license key is registered to a different domain. Each key can only be used on one production domain. Please purchase a new license for this domain.";
573
- } else if (!tier || !tierHasFeature(tier, featureName)) {
574
- reason = "Your license does not include this feature. Please upgrade your plan.";
575
549
  }
576
550
  return /* @__PURE__ */ jsx(UnlicensedOverlay, { featureName, reason, children: /* @__PURE__ */ jsx(Component, { ...props }) });
577
551
  }
@@ -1,4 +1,4 @@
1
- import { requireLicense } from './chunk-VECQKSWS.mjs';
1
+ import { requireLicense } from './chunk-4MMKB2EW.mjs';
2
2
  import { createContext, useMemo, useState, useRef, useEffect, useCallback, useContext } from 'react';
3
3
  import { FormEngineRenderer } from '@squaredr/fieldcraft-react';
4
4
  import { jsx, jsxs } from 'react/jsx-runtime';
@@ -1,5 +1,5 @@
1
1
  import { cn } from './chunk-QQ4JZGTD.mjs';
2
- import { requireLicense } from './chunk-VECQKSWS.mjs';
2
+ import { requireLicense } from './chunk-4MMKB2EW.mjs';
3
3
  import { useState, useEffect, useMemo } from 'react';
4
4
  import { Input, Button, Separator, Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@squaredr/fieldcraft-react';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
@@ -1,12 +1,12 @@
1
1
  import { cn } from './chunk-QQ4JZGTD.mjs';
2
- import { requireLicense } from './chunk-VECQKSWS.mjs';
2
+ import { requireLicense } from './chunk-4MMKB2EW.mjs';
3
3
  import { forwardRef, lazy, memo, createContext, Component, useState, useRef, useEffect, useCallback, useMemo, Suspense, useContext } from 'react';
4
4
  import { DndContext, DragOverlay, useSensors, useSensor, MouseSensor, TouchSensor, useDraggable, useDroppable } from '@dnd-kit/core';
5
- import { ChevronDown, Undo2, Redo2, PencilRuler, Eye, Code, LayoutTemplate, GitBranch, Upload, Download, Save, X, PanelLeft, PanelRight, Search, ChevronRight, Plus, Settings, FileText, Layers, Copy, Trash2, HelpCircle, MoveVertical, Minus, Video, Image, PartyPopper, Hand, ShieldCheck, SeparatorHorizontal, Info, Heading, Trophy, EyeOff, Calculator, CreditCard, MapPin, Repeat, Grid3X3, Camera, PenTool, Paperclip, CalendarCheck, CalendarRange, Clock, Calendar, ArrowUpDown, Globe, ToggleLeft, CheckSquare, CircleDot, ListOrdered, TrendingUp, BarChart3, Star, SlidersHorizontal, Hash, UserCheck, Link, PhoneCall, Phone, Mail, AlignLeft, Type, GripVertical } from 'lucide-react';
5
+ import { ChevronDown, Undo2, Redo2, PencilRuler, Eye, Code, GitBranch, LayoutTemplate, Upload, Download, Save, X, PanelLeft, PanelRight, Search, ChevronRight, Plus, Settings, FileText, Layers, ZoomOut, ZoomIn, Maximize, Copy, Trash2, HelpCircle, MoveVertical, Minus, Video, Image, PartyPopper, Hand, ShieldCheck, SeparatorHorizontal, Info, Heading, Trophy, EyeOff, Calculator, CreditCard, MapPin, Repeat, Grid3X3, Camera, PenTool, Paperclip, CalendarCheck, CalendarRange, Clock, Calendar, ArrowUpDown, Globe, ToggleLeft, CheckSquare, CircleDot, ListOrdered, TrendingUp, BarChart3, Star, SlidersHorizontal, Hash, UserCheck, Link, PhoneCall, Phone, Mail, AlignLeft, Type, GripVertical } from 'lucide-react';
6
6
  import { Button, Separator, FormEngineRenderer, Input, Badge, Textarea, Label, Switch } from '@squaredr/fieldcraft-react';
7
7
  import { validateSchema, FormEngineSchemaError } from '@squaredr/fieldcraft-core';
8
8
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
9
- import { MarkerType, Handle, Position, getBezierPath, BaseEdge, EdgeLabelRenderer, useNodesState, useEdgesState, ReactFlow, Background, Controls, MiniMap } from '@xyflow/react';
9
+ import { MarkerType, Handle, Position, getBezierPath, BaseEdge, EdgeLabelRenderer, useNodesState, useEdgesState, ReactFlowProvider, useReactFlow, ReactFlow, Background, MiniMap } from '@xyflow/react';
10
10
  import '@xyflow/react/dist/style.css';
11
11
 
12
12
  var MAX_HISTORY = 50;
@@ -3425,7 +3425,7 @@ var defaultEdgeOptions = {
3425
3425
  height: 12
3426
3426
  }
3427
3427
  };
3428
- function LogicFlowEditor({ schema, onChange, onClose }) {
3428
+ function LogicFlowEditor({ schema, onChange }) {
3429
3429
  const noSections = schema.sections.length === 0;
3430
3430
  const flowGraph = useMemo(() => buildReactFlowGraph(schema), [schema]);
3431
3431
  const [nodes, setNodes, onNodesChange] = useNodesState(flowGraph.nodes);
@@ -3517,33 +3517,89 @@ function LogicFlowEditor({ schema, onChange, onClose }) {
3517
3517
  const jumpCount = flowGraph.edges.filter(
3518
3518
  (e) => e.data?.edgeType === "onExit"
3519
3519
  ).length;
3520
- return /* @__PURE__ */ jsxs("div", { className: "fixed inset-0 z-50 flex flex-col bg-background/95 backdrop-blur-sm", children: [
3521
- /* @__PURE__ */ jsxs("div", { className: "h-12 shrink-0 flex items-center justify-between px-4 border-b border-border bg-card", children: [
3522
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
3523
- /* @__PURE__ */ jsx(GitBranch, { size: 16, className: "text-primary" }),
3524
- /* @__PURE__ */ jsx("h2", { className: "text-sm font-semibold", children: "Logic Flow" }),
3525
- /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
3526
- showIfCount,
3527
- " condition",
3528
- showIfCount !== 1 ? "s" : "",
3529
- jumpCount > 0 && ` \xB7 ${jumpCount} jump${jumpCount !== 1 ? "s" : ""}`,
3530
- " \xB7 ",
3531
- sectionIds.length,
3532
- " section",
3533
- sectionIds.length !== 1 ? "s" : ""
3534
- ] })
3535
- ] }),
3520
+ const totalFields = useMemo(
3521
+ () => schema.sections.reduce((sum, s) => sum + s.questions.length, 0),
3522
+ [schema]
3523
+ );
3524
+ const conditionalFields = useMemo(
3525
+ () => schema.sections.reduce(
3526
+ (sum, s) => sum + s.questions.filter((q) => !!q.showIf).length,
3527
+ 0
3528
+ ),
3529
+ [schema]
3530
+ );
3531
+ return /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(
3532
+ LogicFlowInner,
3533
+ {
3534
+ noSections,
3535
+ nodes,
3536
+ edges,
3537
+ onNodesChange,
3538
+ onEdgesChange,
3539
+ onNodeClick,
3540
+ onConnect,
3541
+ onEdgesDelete,
3542
+ selectedFieldId,
3543
+ selectedQuestion,
3544
+ setSelectedFieldId,
3545
+ handleConditionUpdate,
3546
+ schema,
3547
+ sectionIds,
3548
+ showIfCount,
3549
+ jumpCount,
3550
+ totalFields,
3551
+ conditionalFields
3552
+ }
3553
+ ) });
3554
+ }
3555
+ function LogicFlowInner({
3556
+ noSections,
3557
+ nodes,
3558
+ edges,
3559
+ onNodesChange,
3560
+ onEdgesChange,
3561
+ onNodeClick,
3562
+ onConnect,
3563
+ onEdgesDelete,
3564
+ selectedFieldId,
3565
+ selectedQuestion,
3566
+ setSelectedFieldId,
3567
+ handleConditionUpdate,
3568
+ schema,
3569
+ sectionIds,
3570
+ showIfCount,
3571
+ jumpCount,
3572
+ totalFields,
3573
+ conditionalFields
3574
+ }) {
3575
+ const { zoomIn, zoomOut, fitView } = useReactFlow();
3576
+ return /* @__PURE__ */ jsxs("div", { className: "flex-1 flex flex-col bg-background min-h-0", children: [
3577
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 h-9 flex items-center justify-between px-4 border-b border-border bg-card/50", children: [
3578
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-3", children: /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
3579
+ sectionIds.length,
3580
+ " section",
3581
+ sectionIds.length !== 1 ? "s" : "",
3582
+ " \xB7 ",
3583
+ totalFields,
3584
+ " field",
3585
+ totalFields !== 1 ? "s" : "",
3586
+ conditionalFields > 0 && ` \xB7 ${conditionalFields} conditional`,
3587
+ showIfCount > 0 && ` \xB7 ${showIfCount} rule${showIfCount !== 1 ? "s" : ""}`,
3588
+ jumpCount > 0 && ` \xB7 ${jumpCount} jump${jumpCount !== 1 ? "s" : ""}`
3589
+ ] }) }),
3536
3590
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1", children: [
3537
- /* @__PURE__ */ 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" }),
3538
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onClose, title: "Close logic flow", children: /* @__PURE__ */ jsx(X, { size: 16, strokeWidth: 1.75 }) })
3591
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground mr-2 hidden md:inline", children: "Drag between nodes to create conditions \xB7 Select edges + Delete to remove" }),
3592
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-xs", onClick: () => zoomOut(), title: "Zoom out", children: /* @__PURE__ */ jsx(ZoomOut, { size: 14, strokeWidth: 1.75 }) }),
3593
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-xs", onClick: () => zoomIn(), title: "Zoom in", children: /* @__PURE__ */ jsx(ZoomIn, { size: 14, strokeWidth: 1.75 }) }),
3594
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-xs", onClick: () => fitView({ padding: 0.2 }), title: "Fit to view", children: /* @__PURE__ */ jsx(Maximize, { size: 14, strokeWidth: 1.75 }) })
3539
3595
  ] })
3540
3596
  ] }),
3541
- noSections ? /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground text-sm", children: /* @__PURE__ */ jsxs("div", { className: "text-center space-y-2", children: [
3542
- /* @__PURE__ */ jsx(GitBranch, { size: 32, className: "mx-auto opacity-40" }),
3543
- /* @__PURE__ */ jsx("p", { className: "font-medium", children: "No sections found" }),
3544
- /* @__PURE__ */ jsx("p", { className: "text-xs max-w-xs", children: "Add sections and fields to your form to see the logic flow pipeline." })
3545
- ] }) }) : /* @__PURE__ */ jsxs("div", { className: "flex-1 flex", children: [
3546
- /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxs(
3597
+ noSections ? /* @__PURE__ */ jsx("div", { className: "flex-1 flex items-center justify-center text-muted-foreground text-sm", children: /* @__PURE__ */ jsxs("div", { className: "text-center space-y-3", children: [
3598
+ /* @__PURE__ */ jsx(GitBranch, { size: 40, className: "mx-auto opacity-30" }),
3599
+ /* @__PURE__ */ jsx("p", { className: "font-medium", children: "No logic to display" }),
3600
+ /* @__PURE__ */ jsx("p", { className: "text-xs max-w-xs mx-auto", children: "Switch to Design mode and add sections with fields. Then come back here to visualize and edit branching logic." })
3601
+ ] }) }) : /* @__PURE__ */ jsxs("div", { className: "flex-1 flex min-h-0", children: [
3602
+ /* @__PURE__ */ jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxs(
3547
3603
  ReactFlow,
3548
3604
  {
3549
3605
  nodes,
@@ -3562,7 +3618,6 @@ function LogicFlowEditor({ schema, onChange, onClose }) {
3562
3618
  proOptions: { hideAttribution: true },
3563
3619
  children: [
3564
3620
  /* @__PURE__ */ jsx(Background, { gap: 20, size: 1 }),
3565
- /* @__PURE__ */ jsx(Controls, { showInteractive: false }),
3566
3621
  /* @__PURE__ */ jsx(
3567
3622
  MiniMap,
3568
3623
  {
@@ -3577,9 +3632,9 @@ function LogicFlowEditor({ schema, onChange, onClose }) {
3577
3632
  ]
3578
3633
  }
3579
3634
  ) }),
3580
- selectedFieldId && selectedQuestion && /* @__PURE__ */ jsxs("div", { className: "w-72 border-l border-border bg-card overflow-auto", children: [
3635
+ selectedFieldId && selectedQuestion && /* @__PURE__ */ jsxs("div", { className: "w-72 shrink-0 border-l border-border bg-card overflow-auto", children: [
3581
3636
  /* @__PURE__ */ jsxs("div", { className: "px-3 py-3 border-b border-border flex items-center justify-between", children: [
3582
- /* @__PURE__ */ jsxs("div", { children: [
3637
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
3583
3638
  /* @__PURE__ */ jsx("p", { className: "text-xs font-semibold truncate", children: selectedQuestion.label || selectedQuestion.id }),
3584
3639
  /* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground", children: selectedQuestion.type })
3585
3640
  ] }),
@@ -3590,7 +3645,7 @@ function LogicFlowEditor({ schema, onChange, onClose }) {
3590
3645
  size: "icon-xs",
3591
3646
  onClick: () => setSelectedFieldId(null),
3592
3647
  title: "Close panel",
3593
- children: /* @__PURE__ */ jsx(X, { size: 14, strokeWidth: 1.75 })
3648
+ children: /* @__PURE__ */ jsx("span", { className: "text-xs", children: "\xD7" })
3594
3649
  }
3595
3650
  )
3596
3651
  ] }),
@@ -3604,7 +3659,7 @@ function LogicFlowEditor({ schema, onChange, onClose }) {
3604
3659
  ) })
3605
3660
  ] })
3606
3661
  ] }),
3607
- /* @__PURE__ */ 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: [
3662
+ /* @__PURE__ */ jsxs("div", { className: "shrink-0 px-4 py-1.5 border-t border-border bg-card/50 flex items-center gap-4 text-[11px] text-muted-foreground flex-wrap", children: [
3608
3663
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
3609
3664
  /* @__PURE__ */ jsx("div", { className: "w-5 h-0.5 rounded", style: { background: "var(--muted-foreground)", opacity: 0.4 } }),
3610
3665
  /* @__PURE__ */ jsx("span", { children: "flow" })
@@ -3859,7 +3914,6 @@ function FormBuilderCore(props) {
3859
3914
  const [viewMode, setViewMode] = useState("design");
3860
3915
  const [jsonText, setJsonText] = useState("");
3861
3916
  const [jsonSwitchError, setJsonSwitchError] = useState(null);
3862
- const [showLogicMap, setShowLogicMap] = useState(false);
3863
3917
  const [showTemplateGallery, setShowTemplateGallery] = useState(false);
3864
3918
  const [saveValidationErrors, setSaveValidationErrors] = useState(null);
3865
3919
  const [schemaUrlLoading, setSchemaUrlLoading] = useState(!!schemaUrl);
@@ -4016,6 +4070,11 @@ ${details}`);
4016
4070
  handleSave();
4017
4071
  return;
4018
4072
  }
4073
+ if (mod && e.key === "l") {
4074
+ e.preventDefault();
4075
+ handleViewModeChange(viewMode === "logic" ? "design" : "logic");
4076
+ return;
4077
+ }
4019
4078
  if (isEditing) return;
4020
4079
  if (e.key === "Delete" || e.key === "Backspace") {
4021
4080
  const sel = builderState.selectedItem;
@@ -4046,7 +4105,7 @@ ${details}`);
4046
4105
  }
4047
4106
  }
4048
4107
  },
4049
- [builderState, handleSave]
4108
+ [builderState, handleSave, handleViewModeChange, viewMode]
4050
4109
  );
4051
4110
  return /* @__PURE__ */ jsx(FormBuilderThemeProvider, { theme, children: /* @__PURE__ */ jsxs(
4052
4111
  DndContext,
@@ -4137,6 +4196,19 @@ ${details}`);
4137
4196
  "JSON"
4138
4197
  ]
4139
4198
  }
4199
+ ),
4200
+ /* @__PURE__ */ jsxs(
4201
+ Button,
4202
+ {
4203
+ variant: viewMode === "logic" ? "secondary" : "ghost",
4204
+ size: "sm",
4205
+ onClick: () => handleViewModeChange("logic"),
4206
+ className: "gap-1.5 h-7 px-2.5 text-xs",
4207
+ children: [
4208
+ /* @__PURE__ */ jsx(GitBranch, { size: 13, strokeWidth: 1.75 }),
4209
+ "Logic"
4210
+ ]
4211
+ }
4140
4212
  )
4141
4213
  ] }),
4142
4214
  toolbarExtra
@@ -4153,17 +4225,6 @@ ${details}`);
4153
4225
  children: /* @__PURE__ */ jsx(LayoutTemplate, { size: 15, strokeWidth: 1.75 })
4154
4226
  }
4155
4227
  ),
4156
- /* @__PURE__ */ jsx(
4157
- Button,
4158
- {
4159
- variant: "ghost",
4160
- size: "icon-sm",
4161
- onClick: () => setShowLogicMap(true),
4162
- title: "View branching logic map",
4163
- "aria-label": "Logic map",
4164
- children: /* @__PURE__ */ jsx(GitBranch, { size: 15, strokeWidth: 1.75 })
4165
- }
4166
- ),
4167
4228
  /* @__PURE__ */ jsx(Separator, { orientation: "vertical", className: "mx-1 h-5" }),
4168
4229
  /* @__PURE__ */ jsx(
4169
4230
  Button,
@@ -4247,6 +4308,19 @@ ${details}`);
4247
4308
  /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "JSON" })
4248
4309
  ]
4249
4310
  }
4311
+ ),
4312
+ /* @__PURE__ */ jsxs(
4313
+ Button,
4314
+ {
4315
+ variant: viewMode === "logic" ? "secondary" : "ghost",
4316
+ size: "sm",
4317
+ onClick: () => handleViewModeChange("logic"),
4318
+ className: "gap-1 h-7 px-2 text-xs",
4319
+ children: [
4320
+ /* @__PURE__ */ jsx(GitBranch, { size: 12, strokeWidth: 1.75 }),
4321
+ /* @__PURE__ */ jsx("span", { className: "hidden sm:inline", children: "Logic" })
4322
+ ]
4323
+ }
4250
4324
  )
4251
4325
  ] }),
4252
4326
  toolbarExtra,
@@ -4380,7 +4454,13 @@ ${details}`);
4380
4454
  errors: jsonErrors,
4381
4455
  onValidate: validateJson
4382
4456
  }
4383
- ) : viewMode === "preview" ? /* @__PURE__ */ jsx(FormPreviewPanel, { schema: builderState.schema, preview }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4457
+ ) : viewMode === "preview" ? /* @__PURE__ */ jsx(FormPreviewPanel, { schema: builderState.schema, preview }) : viewMode === "logic" ? /* @__PURE__ */ jsx(
4458
+ LogicFlowEditor,
4459
+ {
4460
+ schema: builderState.schema,
4461
+ onChange: (updated) => builderState.resetSchema(updated)
4462
+ }
4463
+ ) : /* @__PURE__ */ jsxs(Fragment, { children: [
4384
4464
  /* @__PURE__ */ jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsx(QuestionPalette, { questionTypes: mergedQuestionTypes, palette }) }),
4385
4465
  /* @__PURE__ */ jsx(FormCanvas, { builderState }),
4386
4466
  /* @__PURE__ */ jsx("div", { className: "hidden md:flex", children: /* @__PURE__ */ jsx(PropertiesPanel, { builderState }) }),
@@ -4399,14 +4479,6 @@ ${details}`);
4399
4479
  }
4400
4480
  ),
4401
4481
  /* @__PURE__ */ jsx(DragOverlay, { dropAnimation: null, children: dragDrop.activeDragItem && /* @__PURE__ */ jsx(DragOverlayContent, { item: dragDrop.activeDragItem, schema: builderState.schema, questionTypes: mergedQuestionTypes }) }),
4402
- showLogicMap && /* @__PURE__ */ jsx(
4403
- LogicFlowEditor,
4404
- {
4405
- schema: builderState.schema,
4406
- onChange: (updated) => builderState.resetSchema(updated),
4407
- onClose: () => setShowLogicMap(false)
4408
- }
4409
- ),
4410
4482
  showTemplateGallery && templates && /* @__PURE__ */ jsx(
4411
4483
  TemplateGallery,
4412
4484
  {