@syntrologie/adapt-content 2.2.0-canary.9 → 2.2.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.
Files changed (52) hide show
  1. package/dist/reconciliation-guard.d.ts +29 -0
  2. package/dist/reconciliation-guard.d.ts.map +1 -0
  3. package/dist/reconciliation-guard.js +72 -0
  4. package/dist/runtime.d.ts.map +1 -1
  5. package/dist/runtime.js +24 -0
  6. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/BeforeAfterToggle.test.js +1 -0
  7. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.d.ts +2 -0
  8. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.d.ts.map +1 -0
  9. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +158 -0
  10. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +6 -0
  11. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +1 -1
  12. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorHeader.test.js +4 -5
  13. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.d.ts +2 -0
  14. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.d.ts.map +1 -0
  15. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +25 -0
  16. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +22 -0
  17. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/TriggerJourney.test.js +77 -14
  18. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/formatConditionLabel.test.js +1 -1
  19. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts +1 -2
  20. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts.map +1 -1
  21. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +4 -4
  22. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts.map +1 -1
  23. package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.js +4 -4
  24. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +5 -5
  25. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.js +1 -1
  26. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.d.ts.map +1 -1
  27. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.js +1 -1
  28. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.d.ts.map +1 -1
  29. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.js +10 -1
  30. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.d.ts.map +1 -1
  31. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.js +1 -1
  32. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts +1 -1
  33. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts.map +1 -1
  34. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.js +5 -2
  35. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +1 -1
  36. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +4 -4
  37. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts +1 -1
  38. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts.map +1 -1
  39. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.js +5 -2
  40. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts +1 -1
  41. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts.map +1 -1
  42. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.js +6 -4
  43. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.d.ts.map +1 -1
  44. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +24 -12
  45. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +1 -1
  46. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +4 -4
  47. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +1 -1
  48. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +12 -20
  49. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +1 -1
  50. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +1 -1
  51. package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +1 -1
  52. package/package.json +1 -1
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Reconciliation Guard - MutationObserver defense against React DOM removal.
3
+ *
4
+ * When the Syntrologie SDK inserts DOM nodes into a React-managed subtree
5
+ * (via content:insertHtml), React's reconciliation will silently remove them
6
+ * on the next render because they don't exist in React's virtual DOM.
7
+ *
8
+ * This guard watches for the removal of our inserted container and re-inserts
9
+ * it using a debounced retry mechanism with a maximum retry count to prevent
10
+ * infinite loops (e.g., in React StrictMode which double-invokes effects).
11
+ */
12
+ export interface ReconciliationGuardOptions {
13
+ /** Maximum re-insertion attempts before giving up. Default: 3 */
14
+ maxRetries?: number;
15
+ /** Debounce interval in ms to coalesce rapid removals. Default: 50 */
16
+ debounceMs?: number;
17
+ }
18
+ /**
19
+ * Watch for a container element being removed from the DOM by an external
20
+ * framework (React, Vue, etc.) and call `reinsertFn` to re-insert it.
21
+ *
22
+ * @param container The element we inserted (has data-syntro-action-id)
23
+ * @param anchor The anchor element our container is positioned relative to
24
+ * @param reinsertFn Called when the container is removed — should re-insert it
25
+ * @param opts Configuration for retry limits and debounce timing
26
+ * @returns Cleanup function that disconnects the observer
27
+ */
28
+ export declare function guardAgainstReconciliation(container: HTMLElement, anchor: HTMLElement, reinsertFn: () => void, opts?: ReconciliationGuardOptions): () => void;
29
+ //# sourceMappingURL=reconciliation-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reconciliation-guard.d.ts","sourceRoot":"","sources":["../src/reconciliation-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,0BAA0B;IACzC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,WAAW,EACtB,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,IAAI,EACtB,IAAI,CAAC,EAAE,0BAA0B,GAChC,MAAM,IAAI,CAoDZ"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Reconciliation Guard - MutationObserver defense against React DOM removal.
3
+ *
4
+ * When the Syntrologie SDK inserts DOM nodes into a React-managed subtree
5
+ * (via content:insertHtml), React's reconciliation will silently remove them
6
+ * on the next render because they don't exist in React's virtual DOM.
7
+ *
8
+ * This guard watches for the removal of our inserted container and re-inserts
9
+ * it using a debounced retry mechanism with a maximum retry count to prevent
10
+ * infinite loops (e.g., in React StrictMode which double-invokes effects).
11
+ */
12
+ /**
13
+ * Watch for a container element being removed from the DOM by an external
14
+ * framework (React, Vue, etc.) and call `reinsertFn` to re-insert it.
15
+ *
16
+ * @param container The element we inserted (has data-syntro-action-id)
17
+ * @param anchor The anchor element our container is positioned relative to
18
+ * @param reinsertFn Called when the container is removed — should re-insert it
19
+ * @param opts Configuration for retry limits and debounce timing
20
+ * @returns Cleanup function that disconnects the observer
21
+ */
22
+ export function guardAgainstReconciliation(container, anchor, reinsertFn, opts) {
23
+ const maxRetries = opts?.maxRetries ?? 3;
24
+ const debounceMs = opts?.debounceMs ?? 50;
25
+ // Find the nearest parent to observe. Prefer container's parent, then anchor's.
26
+ const observeTarget = container.parentElement ?? anchor.parentElement;
27
+ if (!observeTarget)
28
+ return () => { };
29
+ let retries = 0;
30
+ let debounceTimer = null;
31
+ let disconnected = false;
32
+ const observer = new MutationObserver((mutations) => {
33
+ if (disconnected)
34
+ return;
35
+ for (const mutation of mutations) {
36
+ for (const removed of mutation.removedNodes) {
37
+ // Check if the removed node is our container
38
+ if (removed !== container)
39
+ continue;
40
+ if (retries >= maxRetries) {
41
+ observer.disconnect();
42
+ disconnected = true;
43
+ return;
44
+ }
45
+ // Debounce to coalesce rapid React re-renders
46
+ if (debounceTimer)
47
+ clearTimeout(debounceTimer);
48
+ debounceTimer = setTimeout(() => {
49
+ if (disconnected)
50
+ return;
51
+ retries++;
52
+ try {
53
+ reinsertFn();
54
+ }
55
+ catch {
56
+ // Re-insertion failed — stop trying
57
+ observer.disconnect();
58
+ disconnected = true;
59
+ }
60
+ }, debounceMs);
61
+ return; // Found our container, no need to check further
62
+ }
63
+ }
64
+ });
65
+ observer.observe(observeTarget, { childList: true, subtree: true });
66
+ return () => {
67
+ disconnected = true;
68
+ observer.disconnect();
69
+ if (debounceTimer)
70
+ clearTimeout(debounceTimer);
71
+ };
72
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EAEd,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,aAAa,EACd,MAAM,SAAS,CAAC;AAMjB;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,cAAc,CAAC,gBAAgB,CAmE9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CA+BxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CAwDxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,cAAc,CA8B1D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,iBAAiB,CA8BhE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,cAAc,CA+C1D,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;EAOZ,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAMnB,CAAC"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EAEd,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,aAAa,EACd,MAAM,SAAS,CAAC;AAMjB;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,cAAc,CAAC,gBAAgB,CA4F9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CA+BxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CAwDxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,cAAc,CA8B1D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,iBAAiB,CA8BhE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,cAAc,CA+C1D,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;EAOZ,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAMnB,CAAC"}
package/dist/runtime.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * DOM manipulation actions: insertHtml, setText, setAttr, addClass, removeClass, setStyle.
5
5
  * These follow the hostPatcher snapshot pattern for safe reversibility.
6
6
  */
7
+ import { guardAgainstReconciliation } from './reconciliation-guard';
7
8
  import { sanitizeHtml } from './sanitizer';
8
9
  // ============================================================================
9
10
  // Executors
@@ -48,8 +49,31 @@ export const executeInsertHtml = async (action, context) => {
48
49
  anchorId: action.anchorId,
49
50
  position: action.position,
50
51
  });
52
+ // Guard against React reconciliation removing our container.
53
+ // The reinsert function re-applies the same insertion strategy.
54
+ const reinsertFn = () => {
55
+ switch (action.position) {
56
+ case 'before':
57
+ anchorEl.insertAdjacentElement('beforebegin', container);
58
+ break;
59
+ case 'after':
60
+ anchorEl.insertAdjacentElement('afterend', container);
61
+ break;
62
+ case 'prepend':
63
+ anchorEl.insertBefore(container, anchorEl.firstChild);
64
+ break;
65
+ case 'append':
66
+ anchorEl.appendChild(container);
67
+ break;
68
+ case 'replace':
69
+ // Cannot re-insert for replace — anchor was already replaced
70
+ break;
71
+ }
72
+ };
73
+ const guardCleanup = guardAgainstReconciliation(container, anchorEl, reinsertFn);
51
74
  return {
52
75
  cleanup: () => {
76
+ guardCleanup();
53
77
  if (action.position === 'replace' && originalContent !== null) {
54
78
  // Restore original element
55
79
  const restoredEl = document.createElement(anchorEl.tagName);
@@ -23,6 +23,7 @@ describe('BeforeAfterToggle', () => {
23
23
  it('highlights active mode with blue class', () => {
24
24
  const { getByText } = render(_jsx(BeforeAfterToggle, { mode: "before", onToggle: () => { } }));
25
25
  expect(getByText('Before').className).toContain('se-text-blue-5');
26
+ expect(getByText('Before').className).toContain('se-bg-blue-5/30');
26
27
  expect(getByText('After').className).toContain('se-text-text-secondary');
27
28
  });
28
29
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ConditionStatusLine.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConditionStatusLine.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ConditionStatusLine.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,158 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { ConditionStatusLine } from '../components/ConditionStatusLine';
5
+ describe('ConditionStatusLine', () => {
6
+ it('returns null when status is null', () => {
7
+ const { container } = render(_jsx(ConditionStatusLine, { status: null }));
8
+ expect(container.innerHTML).toBe('');
9
+ });
10
+ it('renders single condition inline', () => {
11
+ const status = {
12
+ visible: true,
13
+ isFallback: false,
14
+ conditions: [
15
+ {
16
+ type: 'page_url',
17
+ passed: true,
18
+ formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
19
+ },
20
+ ],
21
+ };
22
+ render(_jsx(ConditionStatusLine, { status: status }));
23
+ expect(screen.getByText('page_url:')).toBeTruthy();
24
+ expect(screen.getByText('/pricing')).toBeTruthy();
25
+ });
26
+ it('uses at least 12px font size for condition rows', () => {
27
+ const status = {
28
+ visible: true,
29
+ isFallback: false,
30
+ conditions: [
31
+ {
32
+ type: 'page_url',
33
+ passed: true,
34
+ formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
35
+ },
36
+ ],
37
+ };
38
+ const { container } = render(_jsx(ConditionStatusLine, { status: status }));
39
+ const row = container.firstElementChild;
40
+ expect(row.className).toContain('se-text-[12px]');
41
+ expect(row.className).not.toContain('se-text-[10px]');
42
+ });
43
+ it('uses secondary text color for readability', () => {
44
+ const status = {
45
+ visible: true,
46
+ isFallback: false,
47
+ conditions: [
48
+ {
49
+ type: 'page_url',
50
+ passed: true,
51
+ formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
52
+ },
53
+ ],
54
+ };
55
+ const { container } = render(_jsx(ConditionStatusLine, { status: status }));
56
+ const row = container.firstElementChild;
57
+ expect(row.className).toContain('se-text-text-secondary');
58
+ });
59
+ it('uses medium font weight for small text', () => {
60
+ const status = {
61
+ visible: true,
62
+ isFallback: false,
63
+ conditions: [
64
+ {
65
+ type: 'page_url',
66
+ passed: true,
67
+ formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
68
+ },
69
+ ],
70
+ };
71
+ const { container } = render(_jsx(ConditionStatusLine, { status: status }));
72
+ const row = container.firstElementChild;
73
+ expect(row.className).toContain('se-font-medium');
74
+ });
75
+ it('renders multi-condition summary with expand button', () => {
76
+ const status = {
77
+ visible: false,
78
+ isFallback: false,
79
+ conditions: [
80
+ {
81
+ type: 'page_url',
82
+ passed: true,
83
+ formatted: { label: '/p', instruction: 'A', shortLabel: 'A' },
84
+ },
85
+ {
86
+ type: 'event_count',
87
+ passed: false,
88
+ formatted: { label: 'views >= 2', instruction: 'B', shortLabel: 'B' },
89
+ },
90
+ ],
91
+ };
92
+ render(_jsx(ConditionStatusLine, { status: status }));
93
+ expect(screen.getByText('1 of 2 conditions met')).toBeTruthy();
94
+ });
95
+ it('uses at least 11px for expand indicator', () => {
96
+ const status = {
97
+ visible: false,
98
+ isFallback: false,
99
+ conditions: [
100
+ {
101
+ type: 'a',
102
+ passed: true,
103
+ formatted: { label: 'a', instruction: 'A', shortLabel: 'A' },
104
+ },
105
+ {
106
+ type: 'b',
107
+ passed: false,
108
+ formatted: { label: 'b', instruction: 'B', shortLabel: 'B' },
109
+ },
110
+ ],
111
+ };
112
+ const { container } = render(_jsx(ConditionStatusLine, { status: status }));
113
+ // The expand indicator is the last span in the button
114
+ const button = container.querySelector('button');
115
+ const indicator = button.querySelector('span:last-child');
116
+ expect(indicator.className).toContain('se-text-[11px]');
117
+ expect(indicator.className).not.toContain('se-text-[8px]');
118
+ });
119
+ it('expands to show individual condition rows on click', () => {
120
+ const status = {
121
+ visible: false,
122
+ isFallback: false,
123
+ conditions: [
124
+ {
125
+ type: 'page_url',
126
+ passed: true,
127
+ formatted: { label: '/p', instruction: 'A', shortLabel: 'A' },
128
+ },
129
+ {
130
+ type: 'event_count',
131
+ passed: false,
132
+ formatted: { label: 'views >= 2', instruction: 'B', shortLabel: 'B' },
133
+ },
134
+ ],
135
+ };
136
+ render(_jsx(ConditionStatusLine, { status: status }));
137
+ fireEvent.click(screen.getByText('1 of 2 conditions met'));
138
+ expect(screen.getByText('page_url:')).toBeTruthy();
139
+ expect(screen.getByText('event_count:')).toBeTruthy();
140
+ });
141
+ it('uses secondary color (not tertiary) for condition detail labels', () => {
142
+ const status = {
143
+ visible: true,
144
+ isFallback: false,
145
+ conditions: [
146
+ {
147
+ type: 'page_url',
148
+ passed: true,
149
+ formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
150
+ },
151
+ ],
152
+ };
153
+ render(_jsx(ConditionStatusLine, { status: status }));
154
+ const label = screen.getByText('/pricing');
155
+ expect(label.className).toContain('se-text-text-secondary');
156
+ expect(label.className).not.toContain('se-text-text-tertiary');
157
+ });
158
+ });
@@ -12,6 +12,12 @@ describe('DismissedSection', () => {
12
12
  fireEvent.click(getByText('Dismissed (2)'));
13
13
  expect(queryByText('revealed')).toBeTruthy();
14
14
  });
15
+ it('uses secondary text color for readability', () => {
16
+ const { getByText } = render(_jsx(DismissedSection, { count: 2, children: _jsx("span", { children: "child" }) }));
17
+ const button = getByText('Dismissed (2)').closest('[role="button"]');
18
+ expect(button.className).toContain('se-text-text-secondary');
19
+ expect(button.className).not.toContain('se-text-text-tertiary');
20
+ });
15
21
  it('collapses again on second click', () => {
16
22
  const { getByText, queryByText } = render(_jsx(DismissedSection, { count: 1, children: _jsx("span", { children: "content" }) }));
17
23
  fireEvent.click(getByText('Dismissed (1)'));
@@ -15,7 +15,7 @@ describe('EditorCard', () => {
15
15
  it('applies green border class when validated is true', () => {
16
16
  const { container } = render(_jsx(EditorCard, { itemKey: "k", validated: true, children: "child" }));
17
17
  const card = container.firstElementChild;
18
- expect(card.className).toContain('se-border-green-4/40');
18
+ expect(card.className).toContain('se-border-green-4/50');
19
19
  });
20
20
  it('applies default border class when validated is false', () => {
21
21
  const { container } = render(_jsx(EditorCard, { itemKey: "k", validated: false, children: "child" }));
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { fireEvent, render } from '@testing-library/react';
2
+ import { render } from '@testing-library/react';
3
3
  import { describe, expect, it, vi } from 'vitest';
4
4
  import { EditorHeader } from '../components/EditorHeader';
5
5
  describe('EditorHeader', () => {
@@ -15,10 +15,9 @@ describe('EditorHeader', () => {
15
15
  const { queryByText } = render(_jsx(EditorHeader, { title: "T", onBack: () => { } }));
16
16
  expect(queryByText('Sub')).toBeNull();
17
17
  });
18
- it('calls onBack when back button is clicked', () => {
18
+ it('accepts deprecated onBack prop without rendering a back button', () => {
19
19
  const onBack = vi.fn();
20
- const { getByText } = render(_jsx(EditorHeader, { title: "T", onBack: onBack }));
21
- fireEvent.click(getByText('← Back'));
22
- expect(onBack).toHaveBeenCalledOnce();
20
+ const { queryByText } = render(_jsx(EditorHeader, { title: "T", onBack: onBack }));
21
+ expect(queryByText('← Back')).toBeNull();
23
22
  });
24
23
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=EditorPanelShell.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditorPanelShell.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/EditorPanelShell.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render } from '@testing-library/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { EditorPanelShell } from '../components/EditorPanelShell';
5
+ // Mock ResizeObserver and pointer capture for jsdom
6
+ vi.stubGlobal('ResizeObserver', class {
7
+ observe() { }
8
+ unobserve() { }
9
+ disconnect() { }
10
+ });
11
+ Element.prototype.setPointerCapture = vi.fn();
12
+ Element.prototype.releasePointerCapture = vi.fn();
13
+ describe('EditorPanelShell', () => {
14
+ it('applies antialiased font smoothing to the panel', () => {
15
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, position: "right", children: _jsx("div", { children: "Content" }) }));
16
+ const panel = container.querySelector('[data-syntro-editor-panel]');
17
+ expect(panel).toBeTruthy();
18
+ expect(panel.className).toContain('se-antialiased');
19
+ });
20
+ it('does not render panel when closed', () => {
21
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: false, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
22
+ const panel = container.querySelector('[data-syntro-editor-panel]');
23
+ expect(panel).toBeNull();
24
+ });
25
+ });
@@ -92,6 +92,28 @@ describe('ElementHighlight', () => {
92
92
  expect(label.textContent).toContain('300');
93
93
  expect(label.textContent).toContain('150');
94
94
  });
95
+ it('uses at least 11px font for highlight labels', () => {
96
+ render(_jsx(ElementHighlight, { element: mockElement, color: "rgba(99,102,241,0.6)", showDimensions: true }));
97
+ const label = document.body.querySelector('[data-syntro-highlight-label]');
98
+ expect(label).toBeTruthy();
99
+ // showDimensions uses the smaller size (11px); non-dimensions uses 12px
100
+ expect(label.style.fontSize).toBe('11px');
101
+ });
102
+ it('uses 12px font for non-dimensions labels', () => {
103
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#ef4444", label: "Test Label" }));
104
+ const label = document.body.querySelector('[data-syntro-highlight-label]');
105
+ expect(label).toBeTruthy();
106
+ expect(label.style.fontSize).toBe('12px');
107
+ });
108
+ it('uses 12px font for remove button', () => {
109
+ const onRemove = vi.fn();
110
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#ef4444", label: "Test", showRemove: true, onRemove: onRemove }));
111
+ const removeBtn = document.body.querySelector('[data-syntro-highlight-remove]');
112
+ expect(removeBtn).toBeTruthy();
113
+ expect(removeBtn.style.fontSize).toBe('12px');
114
+ expect(removeBtn.style.width).toBe('16px');
115
+ expect(removeBtn.style.height).toBe('16px');
116
+ });
95
117
  it('does not show remove button when showRemove is false', () => {
96
118
  render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6", label: "Test" }));
97
119
  const removeBtn = document.body.querySelector('[data-syntro-highlight-remove]');
@@ -1,26 +1,28 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describe, it, expect } from 'vitest';
3
2
  import { render, screen } from '@testing-library/react';
3
+ import { describe, expect, it } from 'vitest';
4
4
  import { TriggerJourney } from '../components/TriggerJourney';
5
5
  describe('TriggerJourney', () => {
6
- it('returns null when status is null', () => {
7
- const { container } = render(_jsx(TriggerJourney, { status: null }));
8
- expect(container.innerHTML).toBe('');
6
+ it('renders "Always Present" when status is null', () => {
7
+ render(_jsx(TriggerJourney, { status: null }));
8
+ expect(screen.getByText('Always Present')).toBeTruthy();
9
9
  });
10
- it('returns null when conditions array is empty', () => {
10
+ it('renders "Always Present" when conditions array is empty', () => {
11
11
  const status = { visible: true, isFallback: true, conditions: [] };
12
- const { container } = render(_jsx(TriggerJourney, { status: status }));
13
- expect(container.innerHTML).toBe('');
12
+ render(_jsx(TriggerJourney, { status: status }));
13
+ expect(screen.getByText('Always Present')).toBeTruthy();
14
14
  });
15
15
  it('renders a single node for one condition', () => {
16
16
  const status = {
17
17
  visible: true,
18
18
  isFallback: false,
19
- conditions: [{
19
+ conditions: [
20
+ {
20
21
  type: 'page_url',
21
22
  passed: true,
22
23
  formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
23
- }],
24
+ },
25
+ ],
24
26
  };
25
27
  render(_jsx(TriggerJourney, { status: status }));
26
28
  expect(screen.getByText('/pricing')).toBeTruthy();
@@ -33,7 +35,11 @@ describe('TriggerJourney', () => {
33
35
  {
34
36
  type: 'page_url',
35
37
  passed: true,
36
- formatted: { label: '/fine-arts/', instruction: 'Visit a /fine-arts/ page', shortLabel: '/fine-arts/' },
38
+ formatted: {
39
+ label: '/fine-arts/',
40
+ instruction: 'Visit a /fine-arts/ page',
41
+ shortLabel: '/fine-arts/',
42
+ },
37
43
  },
38
44
  {
39
45
  type: 'event_count',
@@ -57,7 +63,8 @@ describe('TriggerJourney', () => {
57
63
  const status = {
58
64
  visible: false,
59
65
  isFallback: false,
60
- conditions: [{
66
+ conditions: [
67
+ {
61
68
  type: 'event_count',
62
69
  passed: false,
63
70
  formatted: {
@@ -66,7 +73,8 @@ describe('TriggerJourney', () => {
66
73
  shortLabel: '3+ clicks',
67
74
  progress: { current: 1, target: 3, operator: 'gte' },
68
75
  },
69
- }],
76
+ },
77
+ ],
70
78
  };
71
79
  render(_jsx(TriggerJourney, { status: status }));
72
80
  expect(screen.getByText('1/3')).toBeTruthy();
@@ -109,15 +117,70 @@ describe('TriggerJourney', () => {
109
117
  render(_jsx(TriggerJourney, { status: status }));
110
118
  expect(screen.queryByText('All conditions met')).toBeNull();
111
119
  });
120
+ it('uses secondary text color and medium weight for node labels', () => {
121
+ const status = {
122
+ visible: false,
123
+ isFallback: false,
124
+ conditions: [
125
+ {
126
+ type: 'page_url',
127
+ passed: false,
128
+ formatted: { label: '/p', instruction: 'Visit pricing', shortLabel: '/pricing' },
129
+ },
130
+ ],
131
+ };
132
+ render(_jsx(TriggerJourney, { status: status }));
133
+ const label = screen.getByText('/pricing');
134
+ expect(label.className).toContain('se-text-text-secondary');
135
+ expect(label.className).not.toContain('se-text-text-tertiary');
136
+ expect(label.className).toContain('se-font-medium');
137
+ });
138
+ it('uses at least 11px font size for node labels', () => {
139
+ const status = {
140
+ visible: false,
141
+ isFallback: false,
142
+ conditions: [
143
+ {
144
+ type: 'page_url',
145
+ passed: false,
146
+ formatted: { label: '/p', instruction: 'Visit pricing', shortLabel: '/pricing' },
147
+ },
148
+ ],
149
+ };
150
+ render(_jsx(TriggerJourney, { status: status }));
151
+ const label = screen.getByText('/pricing');
152
+ expect(label.className).toContain('se-text-[11px]');
153
+ });
154
+ it('uses secondary text color for "Always Present" label', () => {
155
+ render(_jsx(TriggerJourney, { status: null }));
156
+ const label = screen.getByText('Always Present');
157
+ expect(label.className).toContain('se-text-text-secondary');
158
+ expect(label.className).not.toContain('se-text-text-tertiary');
159
+ });
160
+ it('uses at least 12px for "All conditions met" text', () => {
161
+ const status = {
162
+ visible: true,
163
+ isFallback: false,
164
+ conditions: [
165
+ { type: 'a', passed: true, formatted: { label: 'a', instruction: 'A', shortLabel: 'A' } },
166
+ { type: 'b', passed: true, formatted: { label: 'b', instruction: 'B', shortLabel: 'B' } },
167
+ ],
168
+ };
169
+ const { container } = render(_jsx(TriggerJourney, { status: status }));
170
+ const badge = screen.getByText('All conditions met').closest('div');
171
+ expect(badge.className).toContain('se-text-[12px]');
172
+ });
112
173
  it('uses instruction as title attribute for tooltip', () => {
113
174
  const status = {
114
175
  visible: false,
115
176
  isFallback: false,
116
- conditions: [{
177
+ conditions: [
178
+ {
117
179
  type: 'page_url',
118
180
  passed: false,
119
181
  formatted: { label: '/p', instruction: 'Visit /pricing page', shortLabel: '/pricing' },
120
- }],
182
+ },
183
+ ],
121
184
  };
122
185
  render(_jsx(TriggerJourney, { status: status }));
123
186
  const label = screen.getByText('/pricing');
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, expect, it } from 'vitest';
2
2
  import { formatConditionLabel } from '../formatConditionLabel';
3
3
  describe('formatConditionLabel', () => {
4
4
  it('event_count: returns label with progress data', () => {
@@ -15,7 +15,6 @@
15
15
  * Instructions / cancel UI should be rendered by the consumer in their own
16
16
  * panel — this component only provides the page overlay.
17
17
  */
18
- import React from 'react';
19
18
  export interface PickedElement {
20
19
  element: Element;
21
20
  selector: string;
@@ -27,5 +26,5 @@ export interface AnchorPickerProps {
27
26
  onCancel: () => void;
28
27
  excludeSelector?: string;
29
28
  }
30
- export declare function AnchorPicker({ isActive, onPick, onCancel, excludeSelector, }: AnchorPickerProps): React.ReactPortal | null;
29
+ export declare function AnchorPicker({ isActive, onPick, onCancel, excludeSelector, }: AnchorPickerProps): import("react").ReactPortal | null;
31
30
  //# sourceMappingURL=AnchorPicker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AnchorPicker.d.ts","sourceRoot":"","sources":["../../src/components/AnchorPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAmD,MAAM,OAAO,CAAC;AASxE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAKD,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,eAAuJ,GACxJ,EAAE,iBAAiB,4BAgMnB"}
1
+ {"version":3,"file":"AnchorPicker.d.ts","sourceRoot":"","sources":["../../src/components/AnchorPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAWH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAKD,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,eAAuJ,GACxJ,EAAE,iBAAiB,sCAgMnB"}
@@ -103,7 +103,7 @@ export function AnchorPicker({ isActive, onPick, onCancel, excludeSelector = '[d
103
103
  }, children: [_jsx("div", { style: {
104
104
  position: 'absolute',
105
105
  inset: 0,
106
- background: 'rgba(0, 0, 0, 0.05)',
106
+ background: 'rgba(0, 0, 0, 0.08)',
107
107
  pointerEvents: 'none',
108
108
  } }), hoveredElement && rect && (_jsx("div", { style: {
109
109
  position: 'fixed',
@@ -114,7 +114,7 @@ export function AnchorPicker({ isActive, onPick, onCancel, excludeSelector = '[d
114
114
  border: `2px solid ${HIGHLIGHT_COLOR}`,
115
115
  backgroundColor: HIGHLIGHT_BG,
116
116
  borderRadius: '4px',
117
- boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.15)',
117
+ boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.2)',
118
118
  pointerEvents: 'none',
119
119
  transition: 'all 0.1s ease-out',
120
120
  } })), hoveredElement && rect && (_jsxs("div", { style: {
@@ -128,11 +128,11 @@ export function AnchorPicker({ isActive, onPick, onCancel, excludeSelector = '[d
128
128
  boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
129
129
  zIndex: 1,
130
130
  fontFamily: 'monospace',
131
- fontSize: '12px',
131
+ fontSize: '13px',
132
132
  maxWidth: '300px',
133
133
  pointerEvents: 'none',
134
134
  }, children: [_jsx("div", { style: {
135
- fontSize: '11px',
135
+ fontSize: '12px',
136
136
  textTransform: 'uppercase',
137
137
  letterSpacing: '0.05em',
138
138
  marginBottom: '4px',
@@ -1 +1 @@
1
- {"version":3,"file":"BeforeAfterToggle.d.ts","sourceRoot":"","sources":["../../src/components/BeforeAfterToggle.tsx"],"names":[],"mappings":"AAEA,UAAU,sBAAsB;IAC9B,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CA2B3E"}
1
+ {"version":3,"file":"BeforeAfterToggle.d.ts","sourceRoot":"","sources":["../../src/components/BeforeAfterToggle.tsx"],"names":[],"mappings":"AAEA,UAAU,sBAAsB;IAC9B,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CA6B3E"}