@developer_tribe/react-builder 1.2.18 → 1.2.20

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.
Files changed (62) hide show
  1. package/dist/build-components/OnboardDot/OnboardDotProps.generated.d.ts +2 -1
  2. package/dist/build-components/patterns.generated.d.ts +23 -8
  3. package/dist/index.cjs.js +3 -3
  4. package/dist/index.cjs.js.map +1 -1
  5. package/dist/index.esm.js +1 -1
  6. package/dist/index.esm.js.map +1 -1
  7. package/dist/index.web.cjs.js +4 -4
  8. package/dist/index.web.cjs.js.map +1 -1
  9. package/dist/index.web.esm.js +3 -3
  10. package/dist/index.web.esm.js.map +1 -1
  11. package/dist/pages/ProjectDebug.d.ts +9 -1
  12. package/dist/pages/ProjectMigrationPage.d.ts +3 -1
  13. package/dist/pages/ProjectValidationPage.d.ts +3 -2
  14. package/dist/styles.css +1 -1
  15. package/dist/utils/applyJsonTransform.d.ts +13 -0
  16. package/dist/utils/repairNodeKeys.d.ts +11 -0
  17. package/dist/utils/safeJsonStringify.d.ts +1 -0
  18. package/dist/utils/wrapNodeInMain.d.ts +2 -0
  19. package/package.json +1 -1
  20. package/src/RenderPage.tsx +17 -46
  21. package/src/assets/meta.json +1 -1
  22. package/src/assets/samples/carousel-sample.json +51 -51
  23. package/src/assets/samples/paywall-1.json +77 -77
  24. package/src/assets/samples/paywall-2.json +76 -76
  25. package/src/assets/samples/simple-1.json +13 -13
  26. package/src/assets/samples/simple-2.json +97 -97
  27. package/src/assets/samples/unmigrated-builder-1.1.1.json +25 -25
  28. package/src/assets/samples/unmigrated-builder1.json +1 -1
  29. package/src/assets/samples/unvalidated-builder1.json +15 -15
  30. package/src/assets/samples/unvalidated-crash1.json +4 -4
  31. package/src/assets/samples/vpn-onboard-1.json +100 -78
  32. package/src/assets/samples/vpn-onboard-2.json +97 -75
  33. package/src/assets/samples/vpn-onboard-3.json +103 -79
  34. package/src/assets/samples/vpn-onboard-4.json +103 -79
  35. package/src/assets/samples/vpn-onboard-5.json +139 -108
  36. package/src/assets/samples/vpn-onboard-6.json +100 -81
  37. package/src/build-components/CarouselDots/CarouselDots.tsx +112 -12
  38. package/src/build-components/OnboardDot/OnboardDot.tsx +74 -40
  39. package/src/build-components/OnboardDot/OnboardDotProps.generated.ts +2 -1
  40. package/src/build-components/OnboardDot/pattern.json +28 -10
  41. package/src/build-components/PaywallProvider/PaywallProvider.tsx +2 -3
  42. package/src/build-components/Text/Text.tsx +4 -9
  43. package/src/build-components/patterns.generated.ts +23 -8
  44. package/src/build-components/useNode.ts +20 -4
  45. package/src/components/AttributesEditorPanel.tsx +13 -1
  46. package/src/components/Builder.tsx +19 -5
  47. package/src/components/EditorHeader.tsx +16 -6
  48. package/src/components/JsonTextEditor.tsx +41 -0
  49. package/src/pages/DebugJsonPage.tsx +104 -4
  50. package/src/pages/ProjectDebug.tsx +66 -28
  51. package/src/pages/ProjectMigrationPage.tsx +15 -0
  52. package/src/pages/ProjectPage.tsx +160 -23
  53. package/src/pages/ProjectValidationPage.tsx +64 -1
  54. package/src/styles/layout/_project-validation.scss +29 -0
  55. package/src/styles/utilities/_carousel.scss +0 -32
  56. package/src/utils/__special_exceptions.ts +9 -3
  57. package/src/utils/analyseNodeByPatterns.ts +16 -6
  58. package/src/utils/applyJsonTransform.ts +19 -0
  59. package/src/utils/novaToJson.ts +7 -3
  60. package/src/utils/repairNodeKeys.ts +90 -0
  61. package/src/utils/safeJsonStringify.ts +18 -0
  62. package/src/utils/wrapNodeInMain.ts +67 -0
@@ -1,7 +1,10 @@
1
- import type { ReactNode } from 'react';
1
+ import { type ReactNode, useEffect, useState } from 'react';
2
2
  import type { Product } from '../paywall/types/paywall-types';
3
3
  import type { PaywallBenefits } from '../paywall/types/benefits';
4
4
  import { ProjectDebug } from './ProjectDebug';
5
+ import { safeJsonStringify } from '../utils/safeJsonStringify';
6
+ import type { Node } from '../types/Node';
7
+ import { wrapNodeInMain } from '../utils/wrapNodeInMain';
5
8
 
6
9
  export type ProjectValidationPageProps = {
7
10
  name: string;
@@ -13,6 +16,7 @@ export type ProjectValidationPageProps = {
13
16
  canvasBg?: string;
14
17
  belowName?: ReactNode;
15
18
  onContinueWithoutValidation?: () => void;
19
+ onSaveEditedRawData?: (nextRawData: unknown) => void;
16
20
  };
17
21
 
18
22
  export function ProjectValidationPage({
@@ -25,7 +29,40 @@ export function ProjectValidationPage({
25
29
  canvasBg,
26
30
  belowName,
27
31
  onContinueWithoutValidation,
32
+ onSaveEditedRawData,
28
33
  }: ProjectValidationPageProps) {
34
+ const [jsonText, setJsonText] = useState<string>(() =>
35
+ safeJsonStringify(rawData),
36
+ );
37
+ const [jsonParseError, setJsonParseError] = useState<string | null>(null);
38
+
39
+ const wrapInMain = () => {
40
+ try {
41
+ const parsed = JSON.parse(jsonText) as unknown;
42
+ const isRecord = (v: unknown): v is Record<string, unknown> =>
43
+ typeof v === 'object' && v !== null && !Array.isArray(v);
44
+
45
+ const nodeCandidate: unknown =
46
+ isRecord(parsed) && 'data' in parsed ? (parsed as any).data : parsed;
47
+
48
+ const wrapped = wrapNodeInMain(nodeCandidate as Node);
49
+ const nextValue =
50
+ isRecord(parsed) && 'data' in parsed
51
+ ? { ...(parsed as any), data: wrapped }
52
+ : wrapped;
53
+
54
+ setJsonText(JSON.stringify(nextValue, null, 2));
55
+ setJsonParseError(null);
56
+ } catch (e) {
57
+ setJsonParseError(e instanceof Error ? e.message : String(e));
58
+ }
59
+ };
60
+
61
+ useEffect(() => {
62
+ setJsonText(safeJsonStringify(rawData));
63
+ setJsonParseError(null);
64
+ }, [rawData]);
65
+
29
66
  return (
30
67
  <ProjectDebug
31
68
  name={name}
@@ -35,6 +72,25 @@ export function ProjectValidationPage({
35
72
  products={products}
36
73
  benefits={benefits}
37
74
  canvasBg={canvasBg}
75
+ jsonEditor={{
76
+ value: jsonText,
77
+ onChange: (next) => {
78
+ setJsonText(next);
79
+ setJsonParseError(null);
80
+ },
81
+ error: jsonParseError,
82
+ onSave: () => {
83
+ try {
84
+ const parsed = JSON.parse(jsonText) as unknown;
85
+ setJsonParseError(null);
86
+ onSaveEditedRawData?.(parsed);
87
+ } catch (e) {
88
+ setJsonParseError(e instanceof Error ? e.message : String(e));
89
+ }
90
+ },
91
+ saveDisabled: !onSaveEditedRawData,
92
+ saveLabel: 'Save JSON',
93
+ }}
38
94
  belowName={
39
95
  <>
40
96
  <div className="rb-project-validation__actions">
@@ -45,6 +101,13 @@ export function ProjectValidationPage({
45
101
  >
46
102
  Continue without validation
47
103
  </button>
104
+ <button
105
+ type="button"
106
+ className="editor-button"
107
+ onClick={() => wrapInMain()}
108
+ >
109
+ Wrap in Main
110
+ </button>
48
111
  </div>
49
112
  {belowName}
50
113
  </>
@@ -163,6 +163,35 @@
163
163
  background: colors.$canvasColor;
164
164
  }
165
165
 
166
+ .rb-project-debug__code-editor {
167
+ @include card;
168
+ width: 100%;
169
+ margin: 0;
170
+ padding: sizes.$spaceCozy;
171
+ border: 1px solid colors.$borderColor;
172
+ font-family:
173
+ ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
174
+ 'Courier New', monospace;
175
+ font-size: 12px;
176
+ line-height: 1.5;
177
+ white-space: pre;
178
+ overflow: auto;
179
+ resize: vertical;
180
+ min-height: 220px;
181
+ max-height: calc(100vh - 60px - 160px);
182
+ background: colors.$canvasColor;
183
+ color: colors.$textColor;
184
+ }
185
+
186
+ .rb-project-debug__json-error {
187
+ margin: sizes.$spaceTight 0 0 0;
188
+ font-size: 12px;
189
+ line-height: 1.35;
190
+ color: colors.$dangerColor;
191
+ font-weight: 600;
192
+ word-break: break-word;
193
+ }
194
+
166
195
  /* Legacy selectors kept for backward compatibility (no-op if unused) */
167
196
  .rb-project-validation__error {
168
197
  margin: 0 0 sizes.$spaceCozy 0;
@@ -78,38 +78,6 @@
78
78
  height: 50px;
79
79
  margin: 0 auto;
80
80
  }
81
- .embla__dot {
82
- -webkit-tap-highlight-color: rgba(var(--text-high-contrast-rgb-value), 0.5);
83
- -webkit-appearance: none;
84
- appearance: none;
85
- background-color: transparent;
86
- --embla-dot-color: var(--detail-medium-contrast);
87
- touch-action: manipulation;
88
- display: inline-flex;
89
- text-decoration: none;
90
- cursor: pointer;
91
- border: 0;
92
- padding: 0;
93
- margin: 0;
94
- /* Keep a reasonable hit-area, but allow bigger dots to grow it. */
95
- width: max(2.6rem, calc(var(--embla-dot-size, 1.4rem) + 1.2rem));
96
- height: max(2.6rem, calc(var(--embla-dot-size, 1.4rem) + 1.2rem));
97
- display: flex;
98
- align-items: center;
99
- border-radius: 50%;
100
- }
101
- .embla__dot:after {
102
- background-color: var(--embla-dot-color);
103
- width: var(--embla-dot-size, 1.4rem);
104
- height: var(--embla-dot-size, 1.4rem);
105
- border-radius: 50%;
106
- display: flex;
107
- align-items: center;
108
- content: '';
109
- }
110
- .embla__dot--selected {
111
- --embla-dot-color: var(--text-body);
112
- }
113
81
  .carousel-provider {
114
82
  height: 100%;
115
83
  }
@@ -69,9 +69,15 @@ export function normalizeNodeForValidation(
69
69
 
70
70
  const recordData = node as NodeData<NodeDefaultAttribute>;
71
71
 
72
- const attributes = isPlainObject(recordData.attributes)
73
- ? (normalizeUnknownValue(recordData.attributes) as Record<string, unknown>)
74
- : recordData.attributes;
72
+ let attributes: unknown = recordData.attributes;
73
+ // Common persisted mistake: attributes saved as an empty array (`[]`).
74
+ // Treat it as "no attributes" rather than failing validation.
75
+ if (Array.isArray(attributes) && attributes.length === 0) {
76
+ attributes = {};
77
+ }
78
+ attributes = isPlainObject(attributes)
79
+ ? (normalizeUnknownValue(attributes) as Record<string, unknown>)
80
+ : attributes;
75
81
 
76
82
  const children =
77
83
  recordData.children !== undefined
@@ -387,14 +387,24 @@ function validateAttributesByPattern(
387
387
  {}) as AttributeSchema;
388
388
  const styleSchema = getStyleSubSchema(schema);
389
389
 
390
- // Validate nested `attributes.style` as an object; validate any style keys that also exist in schema.
391
390
  const maybeStyle = (attrs as Record<string, unknown>).style;
391
+ const maybeStyles = (attrs as Record<string, unknown>).styles;
392
+
393
+ // schemaVersion=2 requires `attributes.styles` (plural), not `attributes.style` (singular)
392
394
  if (maybeStyle != null) {
393
- if (!isPlainObject(maybeStyle)) {
394
- return fail(`style must be an object`, joinPath(path, 'style'));
395
+ return fail(
396
+ `Legacy attribute "style" detected. Use "styles" instead (schemaVersion=2). Found at ${joinPath(path, 'style')}`,
397
+ joinPath(path, 'style'),
398
+ );
399
+ }
400
+
401
+ // Validate nested `attributes.styles` (canonical store for AttributesEditor).
402
+ if (maybeStyles != null) {
403
+ if (!isPlainObject(maybeStyles)) {
404
+ return fail(`styles must be an object`, joinPath(path, 'styles'));
395
405
  }
396
406
  for (const [styleKey, styleValue] of Object.entries(
397
- maybeStyle as Record<string, unknown>,
407
+ maybeStyles as Record<string, unknown>,
398
408
  )) {
399
409
  const spec = (styleSchema?.[styleKey] ?? schema?.[styleKey]) as
400
410
  | AttributeTypeSpec
@@ -404,14 +414,14 @@ function validateAttributesByPattern(
404
414
  pattern.pattern.type,
405
415
  styleValue,
406
416
  spec,
407
- joinPath(joinPath(path, 'style'), styleKey),
417
+ joinPath(joinPath(path, 'styles'), styleKey),
408
418
  );
409
419
  if (!res.valid) return res;
410
420
  }
411
421
  }
412
422
 
413
423
  for (const [attrName, attrValue] of Object.entries(attrs)) {
414
- if (attrName === 'style') continue;
424
+ if (attrName === 'style' || attrName === 'styles') continue;
415
425
  const attrSpec = schema?.[attrName] as AttributeTypeSpec | undefined;
416
426
  if (!attrSpec) {
417
427
  if (attrName === 'title' || attrName === 'description') {
@@ -0,0 +1,19 @@
1
+ export type ApplyJsonTransformResult =
2
+ | { ok: true; nextText: string; nextValue: unknown }
3
+ | { ok: false; error: string };
4
+
5
+ export function applyJsonTransform(args: {
6
+ text: string;
7
+ transform: (value: unknown) => unknown;
8
+ indent?: number;
9
+ }): ApplyJsonTransformResult {
10
+ const { text, transform, indent = 2 } = args;
11
+ try {
12
+ const parsed = JSON.parse(text) as unknown;
13
+ const nextValue = transform(parsed);
14
+ const nextText = JSON.stringify(nextValue, null, indent);
15
+ return { ok: true, nextText, nextValue };
16
+ } catch (e) {
17
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
18
+ }
19
+ }
@@ -474,7 +474,7 @@ function mapDotsFromGeneralComponents(generalComponents: any[]): Node | null {
474
474
  };
475
475
 
476
476
  const inactiveDotOpacity = parseNumber(dotAttrs?.inactive_dot_opacity);
477
- const expandingDotWidth = parseNumber(dotAttrs?.expanding_dot_width);
477
+ const dotThickness = parseNumber(dotAttrs?.dot_thickness);
478
478
  const dot_style =
479
479
  typeof dotAttrs?.dot_style === 'string'
480
480
  ? (dotAttrs.dot_style as string)
@@ -487,16 +487,20 @@ function mapDotsFromGeneralComponents(generalComponents: any[]): Node | null {
487
487
  typeof dotAttrs?.active_dot_color === 'string'
488
488
  ? (dotAttrs.active_dot_color as string)
489
489
  : undefined;
490
+ const inactive_dot_color =
491
+ typeof dotAttrs?.inactive_dot_color === 'string'
492
+ ? (dotAttrs.inactive_dot_color as string)
493
+ : undefined;
490
494
 
491
495
  const attributes: Record<string, unknown> = {};
492
496
  if (dotType) attributes.dotType = dotType;
493
497
  if (inactiveDotOpacity !== undefined)
494
498
  attributes.inactive_dot_opacity = inactiveDotOpacity;
495
- if (expandingDotWidth !== undefined)
496
- attributes.expanding_dot_width = expandingDotWidth;
499
+ if (dotThickness !== undefined) attributes.dot_thickness = dotThickness;
497
500
  if (dot_style) attributes.dot_style = dot_style;
498
501
  if (container_style) attributes.container_style = container_style;
499
502
  if (active_dot_color) attributes.active_dot_color = active_dot_color;
503
+ if (inactive_dot_color) attributes.inactive_dot_color = inactive_dot_color;
500
504
  return {
501
505
  type: 'OnboardDot',
502
506
  attributes: Object.keys(attributes).length ? attributes : undefined,
@@ -0,0 +1,90 @@
1
+ import type { Node, NodeData } from '../types/Node';
2
+ import { generateRandomKeyForNode } from './generateRandomKeyForNode';
3
+
4
+ function isNodeDataLike(value: unknown): value is NodeData {
5
+ return (
6
+ typeof value === 'object' &&
7
+ value !== null &&
8
+ !Array.isArray(value) &&
9
+ 'type' in (value as any) &&
10
+ 'children' in (value as any)
11
+ );
12
+ }
13
+
14
+ function normalizeKey(value: unknown): string | undefined {
15
+ if (typeof value !== 'string') return undefined;
16
+ const trimmed = value.trim();
17
+ return trimmed.length > 0 ? trimmed : undefined;
18
+ }
19
+
20
+ function collectNodeKeysInto(node: Node, keys: Set<string>) {
21
+ if (node === null || node === undefined) return;
22
+ if (typeof node === 'string') return;
23
+ if (Array.isArray(node)) {
24
+ node.forEach((child) => collectNodeKeysInto(child, keys));
25
+ return;
26
+ }
27
+ if (!isNodeDataLike(node)) return;
28
+
29
+ const key = normalizeKey(node.key);
30
+ if (key) keys.add(key);
31
+ collectNodeKeysInto(node.children as Node, keys);
32
+ }
33
+
34
+ export function collectNodeKeys(root: Node): Set<string> {
35
+ const keys = new Set<string>();
36
+ collectNodeKeysInto(root, keys);
37
+ return keys;
38
+ }
39
+
40
+ function generateUniqueKey(type: string, usedKeys: Set<string>): string {
41
+ let next = '';
42
+ do {
43
+ next = generateRandomKeyForNode(type || 'Node');
44
+ } while (usedKeys.has(next));
45
+ return next;
46
+ }
47
+
48
+ /**
49
+ * Ensures that all NodeData-like records in the tree have unique non-empty `key`s.
50
+ * - Missing keys are generated.
51
+ * - Duplicate keys (against `usedKeys`) are repaired by generating a new unique key.
52
+ * - Non NodeData-like objects are left untouched.
53
+ *
54
+ * `usedKeys` is mutated by design (to track uniqueness across recursion).
55
+ */
56
+ export function repairNodeKeys(
57
+ root: Node,
58
+ usedKeys: Set<string> = new Set(),
59
+ ): Node {
60
+ if (root === null || root === undefined) return root;
61
+ if (typeof root === 'string') return root;
62
+
63
+ if (Array.isArray(root)) {
64
+ let changed = false;
65
+ const next: Node[] = root.map((child): Node => {
66
+ const repaired: Node = repairNodeKeys(child, usedKeys);
67
+ if (repaired !== child) changed = true;
68
+ return repaired;
69
+ });
70
+ return changed ? next : root;
71
+ }
72
+
73
+ if (!isNodeDataLike(root)) return root;
74
+
75
+ const prev = root as NodeData;
76
+ const prevKey = normalizeKey(prev.key);
77
+ const needsNewKey = !prevKey || usedKeys.has(prevKey);
78
+ const nextKey = needsNewKey
79
+ ? generateUniqueKey(prev.type, usedKeys)
80
+ : prevKey;
81
+ usedKeys.add(nextKey);
82
+
83
+ const prevChildren = prev.children as Node;
84
+ const nextChildren = repairNodeKeys(prevChildren, usedKeys) as Node;
85
+
86
+ if (nextKey !== prevKey || nextChildren !== prevChildren) {
87
+ return { ...prev, key: nextKey, children: nextChildren } as Node;
88
+ }
89
+ return root;
90
+ }
@@ -0,0 +1,18 @@
1
+ export function safeJsonStringify(value: unknown): string {
2
+ try {
3
+ const seen = new WeakSet<object>();
4
+ return JSON.stringify(
5
+ value,
6
+ (_key, v) => {
7
+ if (typeof v === 'object' && v !== null) {
8
+ if (seen.has(v as object)) return '[Circular]';
9
+ seen.add(v as object);
10
+ }
11
+ return v;
12
+ },
13
+ 2,
14
+ );
15
+ } catch (e) {
16
+ return `<< Unable to stringify value: ${String(e)} >>`;
17
+ }
18
+ }
@@ -0,0 +1,67 @@
1
+ import type { Node, NodeData } from '../types/Node';
2
+
3
+ function isRecord(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
5
+ }
6
+
7
+ function isNodeData(value: unknown): value is NodeData {
8
+ return (
9
+ isRecord(value) && typeof value.type === 'string' && 'children' in value
10
+ );
11
+ }
12
+
13
+ function isMainLike(node: NodeData): boolean {
14
+ const t = node.type;
15
+ // Only treat actual Main nodes as "already wrapped".
16
+ // Some payloads may incorrectly carry `isMain: true` on non-Main nodes.
17
+ return t === 'Main' || t === 'main';
18
+ }
19
+
20
+ function stripNestedIsMain(node: Node, isRoot: boolean): Node {
21
+ if (node == null || typeof node === 'string') return node;
22
+
23
+ if (Array.isArray(node)) {
24
+ let changed = false;
25
+ const next = node.map((c) => {
26
+ const out = stripNestedIsMain(c, false);
27
+ if (out !== c) changed = true;
28
+ return out;
29
+ });
30
+ return changed ? next : node;
31
+ }
32
+
33
+ if (!isNodeData(node)) return node;
34
+
35
+ const prevChildren = node.children as Node;
36
+ const nextChildren = stripNestedIsMain(prevChildren, false);
37
+ const shouldDemote = node.isMain === true && !isRoot;
38
+
39
+ if (!shouldDemote && nextChildren === prevChildren) return node;
40
+
41
+ return {
42
+ ...(node as any),
43
+ ...(shouldDemote ? { isMain: false } : null),
44
+ children: nextChildren,
45
+ } as NodeData;
46
+ }
47
+
48
+ export function wrapNodeInMain(node: Node): NodeData {
49
+ if (isNodeData(node) && isMainLike(node)) {
50
+ // Ensure no nested nodes keep `isMain: true`.
51
+ const cleaned = stripNestedIsMain(node, true) as NodeData;
52
+ // Keep root as main.
53
+ if (cleaned.isMain !== true) return { ...(cleaned as any), isMain: true };
54
+ return cleaned;
55
+ }
56
+
57
+ const cleanedChild = stripNestedIsMain(node, false);
58
+ const children: Node =
59
+ node === null || typeof node === 'undefined' ? [] : cleanedChild;
60
+
61
+ return {
62
+ type: 'Main',
63
+ isMain: true,
64
+ attributes: {},
65
+ children,
66
+ };
67
+ }