@syntrologie/adapt-content 2.4.0 → 2.5.1
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/editor.d.ts.map +1 -1
- package/dist/editor.js +59 -3
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +67 -12
- package/dist/summarize.d.ts.map +1 -1
- package/dist/summarize.js +12 -1
- package/dist/types.d.ts +9 -42
- package/dist/types.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts +2 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.js +224 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +102 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.js +58 -6
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +18 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +61 -2
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +478 -7
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +54 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts +2 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.js +257 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts +2 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.js +1015 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +4 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +2 -2
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +2 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +20 -3
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +10 -8
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +350 -87
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +3 -3
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +5 -2
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +24 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/{useShowWhenStatus.js → useTriggerWhenStatus.js} +18 -15
- package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +3 -3
- package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +1 -1
- package/package.json +4 -5
- package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +0 -26
- package/node_modules/@syntrologie/sdk-contracts/dist/index.js +0 -13
- package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +0 -1428
- package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +0 -142
- package/node_modules/@syntrologie/sdk-contracts/package.json +0 -33
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts +0 -24
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts.map +0 -1
package/dist/editor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAuBH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAuBH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAkLhD,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAmkB3E;AAED;;GAEG;AACH,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC;AAEF,eAAO,MAAM,WAAW;;;;CAAe,CAAC;AAExC,eAAe,aAAa,CAAC"}
|
package/dist/editor.js
CHANGED
|
@@ -11,6 +11,42 @@ import { FileCode, Minus, Palette, Plus, Tag, Type } from 'lucide-react';
|
|
|
11
11
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
12
12
|
import { AnchorPicker } from './components/AnchorPicker';
|
|
13
13
|
import { summarizeContentChange } from './summarize';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Anchor Helpers
|
|
16
|
+
// ============================================================================
|
|
17
|
+
/** Extract the CSS selector string from an anchorId (object or legacy string). */
|
|
18
|
+
function resolveAnchorSelector(anchorId) {
|
|
19
|
+
if (!anchorId)
|
|
20
|
+
return '';
|
|
21
|
+
if (typeof anchorId === 'string')
|
|
22
|
+
return anchorId;
|
|
23
|
+
if (typeof anchorId === 'object' && 'selector' in anchorId) {
|
|
24
|
+
return anchorId.selector;
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
/** Extract the target route from an AnchorId object, ignoring wildcard '**'. */
|
|
29
|
+
function resolveAnchorRoute(anchorId) {
|
|
30
|
+
if (!anchorId || typeof anchorId !== 'object')
|
|
31
|
+
return null;
|
|
32
|
+
const route = anchorId.route;
|
|
33
|
+
if (typeof route === 'string' && route !== '**')
|
|
34
|
+
return route;
|
|
35
|
+
if (Array.isArray(route)) {
|
|
36
|
+
const first = route.find((r) => typeof r === 'string' && r !== '**');
|
|
37
|
+
return first ?? null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
/** Save a pending highlight selector to sessionStorage (inlined to avoid cross-package import). */
|
|
42
|
+
function savePendingHighlight(selector) {
|
|
43
|
+
try {
|
|
44
|
+
sessionStorage.setItem('syntro:editor:pending-highlight', selector);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Silently ignore
|
|
48
|
+
}
|
|
49
|
+
}
|
|
14
50
|
function itemKey(section, index) {
|
|
15
51
|
return `${section}:${index}`;
|
|
16
52
|
}
|
|
@@ -50,7 +86,8 @@ function flattenItems(config) {
|
|
|
50
86
|
section,
|
|
51
87
|
index: i,
|
|
52
88
|
summary: summarizeContentChange(section, rec),
|
|
53
|
-
anchorId: rec.anchorId
|
|
89
|
+
anchorId: resolveAnchorSelector(rec.anchorId),
|
|
90
|
+
rawAnchorId: rec.anchorId,
|
|
54
91
|
});
|
|
55
92
|
});
|
|
56
93
|
}
|
|
@@ -220,6 +257,25 @@ export function ContentEditor({ config, onChange, editor }) {
|
|
|
220
257
|
}
|
|
221
258
|
editor.publish();
|
|
222
259
|
}, [dismissedKeys, typedConfig, onChange, editor]);
|
|
260
|
+
const handleBadgeClick = useCallback(async (item) => {
|
|
261
|
+
const detection = detectionMap.get(item.key);
|
|
262
|
+
if (detection?.found && item.anchorId) {
|
|
263
|
+
editor.highlightElement(item.anchorId);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const route = resolveAnchorRoute(item.rawAnchorId);
|
|
267
|
+
if (route) {
|
|
268
|
+
if (item.anchorId)
|
|
269
|
+
savePendingHighlight(item.anchorId);
|
|
270
|
+
await editor.navigateTo(route);
|
|
271
|
+
if (item.anchorId)
|
|
272
|
+
editor.highlightElement(item.anchorId);
|
|
273
|
+
}
|
|
274
|
+
else if (item.anchorId) {
|
|
275
|
+
editor.highlightElement(item.anchorId);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}, [editor, detectionMap]);
|
|
223
279
|
const handleCardHover = useCallback((item) => {
|
|
224
280
|
setHoveredKey(item.key);
|
|
225
281
|
if (item.anchorId) {
|
|
@@ -284,7 +340,7 @@ export function ContentEditor({ config, onChange, editor }) {
|
|
|
284
340
|
const item = arr[index];
|
|
285
341
|
if (!item)
|
|
286
342
|
return null;
|
|
287
|
-
const anchorId = item.anchorId
|
|
343
|
+
const anchorId = resolveAnchorSelector(item.anchorId);
|
|
288
344
|
switch (section) {
|
|
289
345
|
case 'textReplacements':
|
|
290
346
|
return (_jsxs("div", { className: "se-py-1", children: [_jsx("div", { className: "se-text-sm se-font-mono se-text-text-secondary se-py-1 se-px-2 se-bg-slate-grey-3 se-rounded-lg se-mb-3", children: anchorId }), _jsx(EditorTextarea, { label: "Text", value: item.text || '', onChange: (e) => handleFieldChange(section, index, 'text', e.target.value) })] }));
|
|
@@ -338,7 +394,7 @@ export function ContentEditor({ config, onChange, editor }) {
|
|
|
338
394
|
/* ---- List mode ---- */
|
|
339
395
|
_jsxs(_Fragment, { children: [_jsx("button", { type: "button", onClick: handleStartCreate, className: "se-w-full se-h-10 se-px-4 se-py-2 se-rounded-md se-border se-border-dashed se-border-btn-primary/30 se-bg-btn-primary/5 se-text-btn-primary se-text-sm se-font-medium se-cursor-pointer se-flex se-items-center se-justify-center se-gap-2 se-mb-3", children: "+ Add Text Change" }), allItems.length === 0 && (_jsx(EmptyState, { message: "No content changes configured. Click above to add one." })), activeItems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(GroupHeader, { label: "CONTENT", count: activeItems.length }), activeItems.map((item) => {
|
|
340
396
|
const detection = detectionMap.get(item.key);
|
|
341
|
-
return (_jsxs(EditorCard, { itemKey: item.key, onClick: () => handleCardClick(item), className: "se-flex se-items-center se-gap-2", onMouseEnter: () => handleCardHover(item), onMouseLeave: handleCardLeave, children: [_jsx(DetectionBadge, { found: detection?.found ?? false }), _jsx("span", { className: "se-shrink-0 se-flex se-items-center -se-ml-1", children: _jsx(SectionIcon, { section: item.section }) }), _jsx("span", { className: "se-flex-1 se-overflow-hidden se-text-ellipsis se-whitespace-nowrap", children: item.summary }), _jsx("button", { type: "button", className: "se-py-0.5 se-px-1.5 se-rounded se-border-none se-bg-transparent se-text-slate-grey-7 se-text-sm se-cursor-pointer se-shrink-0 se-leading-none", onClick: (e) => {
|
|
397
|
+
return (_jsxs(EditorCard, { itemKey: item.key, onClick: () => handleCardClick(item), className: "se-flex se-items-center se-gap-2", onMouseEnter: () => handleCardHover(item), onMouseLeave: handleCardLeave, children: [_jsx(DetectionBadge, { found: detection?.found ?? false, onClick: () => handleBadgeClick(item) }), _jsx("span", { className: "se-shrink-0 se-flex se-items-center -se-ml-1", children: _jsx(SectionIcon, { section: item.section }) }), _jsx("span", { className: "se-flex-1 se-overflow-hidden se-text-ellipsis se-whitespace-nowrap", children: item.summary }), _jsx("button", { type: "button", className: "se-py-0.5 se-px-1.5 se-rounded se-border-none se-bg-transparent se-text-slate-grey-7 se-text-sm se-cursor-pointer se-shrink-0 se-leading-none", onClick: (e) => {
|
|
342
398
|
e.stopPropagation();
|
|
343
399
|
handleDismiss(item.key);
|
|
344
400
|
}, title: "Dismiss this change", children: "\u00D7" })] }, item.key));
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,CA+H9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CAmCxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CA4DxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,cAAc,CAkC1D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,iBAAiB,CAkChE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,cAAc,CAmD1D,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;EAOZ,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAMnB,CAAC"}
|
package/dist/runtime.js
CHANGED
|
@@ -13,15 +13,33 @@ import { sanitizeHtml } from './sanitizer';
|
|
|
13
13
|
* Execute an insertHtml action
|
|
14
14
|
*/
|
|
15
15
|
export const executeInsertHtml = async (action, context) => {
|
|
16
|
-
|
|
16
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
17
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
18
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3000);
|
|
19
|
+
}
|
|
17
20
|
if (!anchorEl) {
|
|
18
|
-
|
|
21
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
22
|
+
return { cleanup: () => { } };
|
|
19
23
|
}
|
|
20
24
|
// Sanitize HTML content using context utility
|
|
21
25
|
const sanitizedHtml = sanitizeHtml(action.html);
|
|
26
|
+
// Dedup: if a container for this action already exists, remove it first.
|
|
27
|
+
// Uses the action label as a stable identifier across re-applications.
|
|
28
|
+
// Check both inside the anchor (prepend/append) and in the parent (before/after).
|
|
29
|
+
const dedupAttr = 'data-syntro-insert-label';
|
|
30
|
+
const label = action.label;
|
|
31
|
+
if (label) {
|
|
32
|
+
const escapedLabel = CSS.escape(label);
|
|
33
|
+
const searchRoot = anchorEl.parentElement ?? anchorEl;
|
|
34
|
+
const existing = searchRoot.querySelector(`[${dedupAttr}="${escapedLabel}"]`);
|
|
35
|
+
if (existing)
|
|
36
|
+
existing.remove();
|
|
37
|
+
}
|
|
22
38
|
// Create container for inserted content
|
|
23
39
|
const container = document.createElement('div');
|
|
24
40
|
container.setAttribute('data-syntro-action-id', context.generateId());
|
|
41
|
+
if (label)
|
|
42
|
+
container.setAttribute(dedupAttr, label);
|
|
25
43
|
container.innerHTML = sanitizedHtml;
|
|
26
44
|
// Keep track of original state for replace position
|
|
27
45
|
let originalContent = null;
|
|
@@ -43,6 +61,20 @@ export const executeInsertHtml = async (action, context) => {
|
|
|
43
61
|
anchorEl.replaceWith(container);
|
|
44
62
|
break;
|
|
45
63
|
}
|
|
64
|
+
// Deep-link click handler — opens canvas + publishes deep-link event
|
|
65
|
+
let deepLinkHandler = null;
|
|
66
|
+
if (action.deepLink) {
|
|
67
|
+
const { tileId, itemId } = action.deepLink;
|
|
68
|
+
deepLinkHandler = () => {
|
|
69
|
+
const handle = window.SynOS?.handle;
|
|
70
|
+
if (handle) {
|
|
71
|
+
handle.open();
|
|
72
|
+
handle.runtime?.events?.publish('notification.deep_link', { tileId, itemId });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
container.style.cursor = 'pointer';
|
|
76
|
+
container.addEventListener('click', deepLinkHandler);
|
|
77
|
+
}
|
|
46
78
|
context.publishEvent('action.applied', {
|
|
47
79
|
id: context.generateId(),
|
|
48
80
|
kind: 'content:insertHtml',
|
|
@@ -73,6 +105,9 @@ export const executeInsertHtml = async (action, context) => {
|
|
|
73
105
|
const guardCleanup = guardAgainstReconciliation(container, anchorEl, reinsertFn);
|
|
74
106
|
return {
|
|
75
107
|
cleanup: () => {
|
|
108
|
+
if (deepLinkHandler) {
|
|
109
|
+
container.removeEventListener('click', deepLinkHandler);
|
|
110
|
+
}
|
|
76
111
|
guardCleanup();
|
|
77
112
|
if (action.position === 'replace' && originalContent !== null) {
|
|
78
113
|
// Restore original element
|
|
@@ -99,9 +134,13 @@ export const executeInsertHtml = async (action, context) => {
|
|
|
99
134
|
* Execute a setText action
|
|
100
135
|
*/
|
|
101
136
|
export const executeSetText = async (action, context) => {
|
|
102
|
-
|
|
137
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
138
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
139
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3000);
|
|
140
|
+
}
|
|
103
141
|
if (!anchorEl) {
|
|
104
|
-
|
|
142
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
143
|
+
return { cleanup: () => { } };
|
|
105
144
|
}
|
|
106
145
|
// Snapshot original text
|
|
107
146
|
const originalText = anchorEl.textContent ?? '';
|
|
@@ -127,9 +166,13 @@ export const executeSetText = async (action, context) => {
|
|
|
127
166
|
* Execute a setAttr action
|
|
128
167
|
*/
|
|
129
168
|
export const executeSetAttr = async (action, context) => {
|
|
130
|
-
|
|
169
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
170
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
171
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3000);
|
|
172
|
+
}
|
|
131
173
|
if (!anchorEl) {
|
|
132
|
-
|
|
174
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
175
|
+
return { cleanup: () => { } };
|
|
133
176
|
}
|
|
134
177
|
// Block dangerous attributes (case-insensitive)
|
|
135
178
|
const lowerAttr = action.attr.toLowerCase();
|
|
@@ -177,9 +220,13 @@ export const executeSetAttr = async (action, context) => {
|
|
|
177
220
|
* Execute an addClass action
|
|
178
221
|
*/
|
|
179
222
|
export const executeAddClass = async (action, context) => {
|
|
180
|
-
|
|
223
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
224
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
225
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3000);
|
|
226
|
+
}
|
|
181
227
|
if (!anchorEl) {
|
|
182
|
-
|
|
228
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
229
|
+
return { cleanup: () => { } };
|
|
183
230
|
}
|
|
184
231
|
// Check if class was already present
|
|
185
232
|
const hadClass = anchorEl.classList.contains(action.className);
|
|
@@ -204,9 +251,13 @@ export const executeAddClass = async (action, context) => {
|
|
|
204
251
|
* Execute a removeClass action
|
|
205
252
|
*/
|
|
206
253
|
export const executeRemoveClass = async (action, context) => {
|
|
207
|
-
|
|
254
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
255
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
256
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3000);
|
|
257
|
+
}
|
|
208
258
|
if (!anchorEl) {
|
|
209
|
-
|
|
259
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
260
|
+
return { cleanup: () => { } };
|
|
210
261
|
}
|
|
211
262
|
// Check if class was present
|
|
212
263
|
const hadClass = anchorEl.classList.contains(action.className);
|
|
@@ -231,9 +282,13 @@ export const executeRemoveClass = async (action, context) => {
|
|
|
231
282
|
* Execute a setStyle action
|
|
232
283
|
*/
|
|
233
284
|
export const executeSetStyle = async (action, context) => {
|
|
234
|
-
|
|
285
|
+
let anchorEl = context.resolveAnchor(action.anchorId);
|
|
286
|
+
if (!anchorEl && context.waitForAnchor) {
|
|
287
|
+
anchorEl = await context.waitForAnchor(action.anchorId, 3000);
|
|
288
|
+
}
|
|
235
289
|
if (!anchorEl) {
|
|
236
|
-
|
|
290
|
+
console.warn(`[adaptive-content] Anchor not found after waiting: ${action.anchorId.selector}`);
|
|
291
|
+
return { cleanup: () => { } };
|
|
237
292
|
}
|
|
238
293
|
// Snapshot original styles
|
|
239
294
|
const originalStyles = new Map();
|
package/dist/summarize.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"summarize.d.ts","sourceRoot":"","sources":["../src/summarize.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"summarize.d.ts","sourceRoot":"","sources":["../src/summarize.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAc9C;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAwBzD;AAYD;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,aAAa,EACzB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,MAAM,CAiCR"}
|
package/dist/summarize.js
CHANGED
|
@@ -4,6 +4,17 @@
|
|
|
4
4
|
* Pure functions — no DOM access, just string formatting from config data.
|
|
5
5
|
*/
|
|
6
6
|
const MAX_TEXT_LEN = 40;
|
|
7
|
+
/** Extract the CSS selector string from an anchorId (object or legacy string). */
|
|
8
|
+
function resolveAnchorSelector(anchorId) {
|
|
9
|
+
if (!anchorId)
|
|
10
|
+
return '';
|
|
11
|
+
if (typeof anchorId === 'string')
|
|
12
|
+
return anchorId;
|
|
13
|
+
if (typeof anchorId === 'object' && 'selector' in anchorId) {
|
|
14
|
+
return anchorId.selector;
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
7
18
|
/**
|
|
8
19
|
* Convert a CSS selector into a human-friendly element description.
|
|
9
20
|
*/
|
|
@@ -44,7 +55,7 @@ function truncateQuoted(text, max) {
|
|
|
44
55
|
* Generate a human-readable one-liner for a content config change.
|
|
45
56
|
*/
|
|
46
57
|
export function summarizeContentChange(type, item) {
|
|
47
|
-
const desc = describeSelector(item.anchorId
|
|
58
|
+
const desc = describeSelector(resolveAnchorSelector(item.anchorId));
|
|
48
59
|
switch (type) {
|
|
49
60
|
case 'textReplacements': {
|
|
50
61
|
const text = item.text || '';
|
package/dist/types.d.ts
CHANGED
|
@@ -4,75 +4,42 @@
|
|
|
4
4
|
* Minimal type definitions for building this app independently.
|
|
5
5
|
* These match the types exported from @syntrologie/runtime-sdk/types.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
onChange: (config: Record<string, unknown>) => void;
|
|
10
|
-
editor: {
|
|
11
|
-
setDirty: (dirty: boolean) => void;
|
|
12
|
-
navigateHome: () => Promise<boolean>;
|
|
13
|
-
save: () => Promise<void>;
|
|
14
|
-
publish: (captureScreenshot?: boolean) => Promise<void>;
|
|
15
|
-
/** Navigate the target page to a different route (preserves editor params). */
|
|
16
|
-
navigateTo: (route: string) => Promise<void>;
|
|
17
|
-
/** Show a persistent blue highlight on the element matching this selector. */
|
|
18
|
-
highlightElement: (selector: string) => void;
|
|
19
|
-
/** Remove the persistent element highlight. */
|
|
20
|
-
clearHighlight: () => void;
|
|
21
|
-
/** Get the current page route (pathname). */
|
|
22
|
-
getCurrentRoute: () => string;
|
|
23
|
-
/** Push a temporary config to the live page preview without saving to state. */
|
|
24
|
-
previewConfig: (config: Record<string, unknown>) => void;
|
|
25
|
-
/** Global before/after preview mode set by the panel's toggle. */
|
|
26
|
-
previewMode?: 'before' | 'after';
|
|
27
|
-
/** Flat action index to open in edit mode (from accordion navigation). */
|
|
28
|
-
initialEditKey?: string;
|
|
29
|
-
/** Open the editor in create mode. */
|
|
30
|
-
initialCreate?: boolean;
|
|
31
|
-
/** Clear the initial navigation state (call after consuming). */
|
|
32
|
-
clearInitialState?: () => void;
|
|
33
|
-
/** Get dismissed keys persisted in navigation context. */
|
|
34
|
-
getDismissedKeys?: () => Set<string>;
|
|
35
|
-
/** Sync dismissed keys back to navigation context. */
|
|
36
|
-
setDismissedKeys?: (keys: Set<string>) => void;
|
|
37
|
-
/** Register a back handler shown in the panel header. Pass null to clear. */
|
|
38
|
-
setBackHandler?: (handler: (() => void) | null) => void;
|
|
39
|
-
};
|
|
40
|
-
platformClient?: unknown;
|
|
41
|
-
}
|
|
42
|
-
export type InsertPosition = 'before' | 'after' | 'prepend' | 'append' | 'replace';
|
|
7
|
+
import type { AnchorId, DeepLink, EditorPanelProps, InsertPosition } from '@syntrologie/sdk-contracts';
|
|
8
|
+
export type { EditorPanelProps, InsertPosition };
|
|
43
9
|
interface BaseAction {
|
|
44
10
|
label?: string;
|
|
45
11
|
}
|
|
46
12
|
export interface InsertHtmlAction extends BaseAction {
|
|
47
13
|
kind: 'content:insertHtml';
|
|
48
|
-
anchorId:
|
|
14
|
+
anchorId: AnchorId;
|
|
49
15
|
html: string;
|
|
50
16
|
position: InsertPosition;
|
|
17
|
+
deepLink?: DeepLink;
|
|
51
18
|
}
|
|
52
19
|
export interface SetTextAction extends BaseAction {
|
|
53
20
|
kind: 'content:setText';
|
|
54
|
-
anchorId:
|
|
21
|
+
anchorId: AnchorId;
|
|
55
22
|
text: string;
|
|
56
23
|
}
|
|
57
24
|
export interface SetAttrAction extends BaseAction {
|
|
58
25
|
kind: 'content:setAttr';
|
|
59
|
-
anchorId:
|
|
26
|
+
anchorId: AnchorId;
|
|
60
27
|
attr: string;
|
|
61
28
|
value: string;
|
|
62
29
|
}
|
|
63
30
|
export interface AddClassAction extends BaseAction {
|
|
64
31
|
kind: 'content:addClass';
|
|
65
|
-
anchorId:
|
|
32
|
+
anchorId: AnchorId;
|
|
66
33
|
className: string;
|
|
67
34
|
}
|
|
68
35
|
export interface RemoveClassAction extends BaseAction {
|
|
69
36
|
kind: 'content:removeClass';
|
|
70
|
-
anchorId:
|
|
37
|
+
anchorId: AnchorId;
|
|
71
38
|
className: string;
|
|
72
39
|
}
|
|
73
40
|
export interface SetStyleAction extends BaseAction {
|
|
74
41
|
kind: 'content:setStyle';
|
|
75
|
-
anchorId:
|
|
42
|
+
anchorId: AnchorId;
|
|
76
43
|
styles: Record<string, string>;
|
|
77
44
|
}
|
|
78
45
|
export type { ActionExecutor, ExecutorCleanup, ExecutorContext, ExecutorResult, ExecutorUpdate, } from '@syntrologie/sdk-contracts';
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EACV,QAAQ,EACR,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACf,MAAM,4BAA4B,CAAC;AAEpC,YAAY,EAAE,gBAAgB,EAAE,cAAc,EAAE,CAAC;AAMjD,UAAU,UAAU;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IAClD,IAAI,EAAE,oBAAoB,CAAC;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,cAAc,CAAC;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAC;CACrB;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAc,SAAQ,UAAU;IAC/C,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,IAAI,EAAE,qBAAqB,CAAC;IAC5B,QAAQ,EAAE,QAAQ,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,IAAI,EAAE,kBAAkB,CAAC;IACzB,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAMD,YAAY,EACV,cAAc,EACd,eAAe,EACf,eAAe,EACf,cAAc,EACd,cAAc,GACf,MAAM,4BAA4B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnchorPicker.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/AnchorPicker.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { act, render } from '@testing-library/react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { AnchorPicker } from '../components/AnchorPicker';
|
|
5
|
+
// Mock ResizeObserver for jsdom
|
|
6
|
+
vi.stubGlobal('ResizeObserver', class {
|
|
7
|
+
observe() { }
|
|
8
|
+
unobserve() { }
|
|
9
|
+
disconnect() { }
|
|
10
|
+
});
|
|
11
|
+
// Mock selectorGenerator utilities
|
|
12
|
+
vi.mock('../utils/selectorGenerator', () => ({
|
|
13
|
+
generateSelector: vi.fn((el) => `mock-selector-${el.tagName.toLowerCase()}`),
|
|
14
|
+
validateSelector: vi.fn(() => true),
|
|
15
|
+
getElementDescription: vi.fn((el) => `mock-desc-${el.tagName.toLowerCase()}`),
|
|
16
|
+
}));
|
|
17
|
+
import { generateSelector, getElementDescription, validateSelector, } from '../utils/selectorGenerator';
|
|
18
|
+
describe('AnchorPicker', () => {
|
|
19
|
+
let mockElementAtPoint;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
mockElementAtPoint = document.createElement('div');
|
|
22
|
+
mockElementAtPoint.textContent = 'Target Element';
|
|
23
|
+
document.body.appendChild(mockElementAtPoint);
|
|
24
|
+
vi.spyOn(mockElementAtPoint, 'getBoundingClientRect').mockReturnValue({
|
|
25
|
+
top: 100,
|
|
26
|
+
left: 200,
|
|
27
|
+
width: 300,
|
|
28
|
+
height: 150,
|
|
29
|
+
bottom: 250,
|
|
30
|
+
right: 500,
|
|
31
|
+
x: 200,
|
|
32
|
+
y: 100,
|
|
33
|
+
toJSON: () => ({}),
|
|
34
|
+
});
|
|
35
|
+
vi.mocked(generateSelector).mockClear();
|
|
36
|
+
vi.mocked(validateSelector).mockClear();
|
|
37
|
+
vi.mocked(getElementDescription).mockClear();
|
|
38
|
+
vi.mocked(generateSelector).mockImplementation((el) => `mock-selector-${el.tagName.toLowerCase()}`);
|
|
39
|
+
vi.mocked(validateSelector).mockReturnValue(true);
|
|
40
|
+
vi.mocked(getElementDescription).mockImplementation((el) => `mock-desc-${el.tagName.toLowerCase()}`);
|
|
41
|
+
// Stub elementFromPoint on document (jsdom does not define it)
|
|
42
|
+
if (!document.elementFromPoint) {
|
|
43
|
+
document.elementFromPoint = vi.fn(() => null);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
vi.spyOn(document, 'elementFromPoint').mockReturnValue(null);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
if (mockElementAtPoint.parentNode) {
|
|
51
|
+
mockElementAtPoint.remove();
|
|
52
|
+
}
|
|
53
|
+
vi.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
it('renders nothing when isActive is false', () => {
|
|
56
|
+
render(_jsx(AnchorPicker, { isActive: false, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
57
|
+
const picker = document.body.querySelector('[data-syntro-anchor-picker]');
|
|
58
|
+
expect(picker).toBeNull();
|
|
59
|
+
});
|
|
60
|
+
it('renders overlay portal to document.body when active', () => {
|
|
61
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
62
|
+
const picker = document.body.querySelector('[data-syntro-anchor-picker]');
|
|
63
|
+
expect(picker).toBeTruthy();
|
|
64
|
+
unmount();
|
|
65
|
+
});
|
|
66
|
+
it('sets crosshair cursor on overlay', () => {
|
|
67
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
68
|
+
const picker = document.body.querySelector('[data-syntro-anchor-picker]');
|
|
69
|
+
expect(picker.style.cursor).toBe('crosshair');
|
|
70
|
+
unmount();
|
|
71
|
+
});
|
|
72
|
+
it('sets highest z-index on overlay', () => {
|
|
73
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
74
|
+
const picker = document.body.querySelector('[data-syntro-anchor-picker]');
|
|
75
|
+
expect(picker.style.zIndex).toBe('2147483644');
|
|
76
|
+
unmount();
|
|
77
|
+
});
|
|
78
|
+
it('calls onCancel when Escape key is pressed', () => {
|
|
79
|
+
const onCancel = vi.fn();
|
|
80
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: onCancel }));
|
|
81
|
+
act(() => {
|
|
82
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
83
|
+
});
|
|
84
|
+
expect(onCancel).toHaveBeenCalledTimes(1);
|
|
85
|
+
unmount();
|
|
86
|
+
});
|
|
87
|
+
it('does not call onCancel for non-Escape keys', () => {
|
|
88
|
+
const onCancel = vi.fn();
|
|
89
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: onCancel }));
|
|
90
|
+
act(() => {
|
|
91
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
92
|
+
});
|
|
93
|
+
expect(onCancel).not.toHaveBeenCalled();
|
|
94
|
+
unmount();
|
|
95
|
+
});
|
|
96
|
+
it('highlights element on mousemove via elementFromPoint', () => {
|
|
97
|
+
document.elementFromPoint.mockReturnValue(mockElementAtPoint);
|
|
98
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
99
|
+
act(() => {
|
|
100
|
+
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
|
|
101
|
+
});
|
|
102
|
+
// generateSelector should have been called with the element
|
|
103
|
+
expect(generateSelector).toHaveBeenCalledWith(mockElementAtPoint);
|
|
104
|
+
unmount();
|
|
105
|
+
});
|
|
106
|
+
it('clears hover state when elementFromPoint returns null', () => {
|
|
107
|
+
document.elementFromPoint.mockReturnValue(null);
|
|
108
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
109
|
+
act(() => {
|
|
110
|
+
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
|
|
111
|
+
});
|
|
112
|
+
// No highlight should be shown (just the overlay container + the semi-transparent bg div)
|
|
113
|
+
const picker = document.body.querySelector('[data-syntro-anchor-picker]');
|
|
114
|
+
expect(picker.children.length).toBe(1);
|
|
115
|
+
unmount();
|
|
116
|
+
});
|
|
117
|
+
it('excludes editor panel elements from picking', () => {
|
|
118
|
+
const editorPanel = document.createElement('div');
|
|
119
|
+
editorPanel.setAttribute('data-syntro-editor-panel', '');
|
|
120
|
+
const innerEl = document.createElement('span');
|
|
121
|
+
editorPanel.appendChild(innerEl);
|
|
122
|
+
document.body.appendChild(editorPanel);
|
|
123
|
+
document.elementFromPoint.mockReturnValue(innerEl);
|
|
124
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
125
|
+
act(() => {
|
|
126
|
+
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 100, bubbles: true }));
|
|
127
|
+
});
|
|
128
|
+
// Should not call generateSelector for excluded elements
|
|
129
|
+
expect(generateSelector).not.toHaveBeenCalled();
|
|
130
|
+
editorPanel.remove();
|
|
131
|
+
unmount();
|
|
132
|
+
});
|
|
133
|
+
it('excludes HTML, BODY, HEAD elements from picking', () => {
|
|
134
|
+
document.elementFromPoint.mockReturnValue(document.body);
|
|
135
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
136
|
+
act(() => {
|
|
137
|
+
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 100, bubbles: true }));
|
|
138
|
+
});
|
|
139
|
+
expect(generateSelector).not.toHaveBeenCalled();
|
|
140
|
+
unmount();
|
|
141
|
+
});
|
|
142
|
+
it('calls onPick with valid selector on click after hover', () => {
|
|
143
|
+
const onPick = vi.fn();
|
|
144
|
+
document.elementFromPoint.mockReturnValue(mockElementAtPoint);
|
|
145
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: onPick, onCancel: vi.fn() }));
|
|
146
|
+
// First hover to set the hovered element
|
|
147
|
+
act(() => {
|
|
148
|
+
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
|
|
149
|
+
});
|
|
150
|
+
// Then click
|
|
151
|
+
act(() => {
|
|
152
|
+
document.dispatchEvent(new MouseEvent('click', { clientX: 250, clientY: 150, bubbles: true }));
|
|
153
|
+
});
|
|
154
|
+
expect(onPick).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect(onPick).toHaveBeenCalledWith({
|
|
156
|
+
element: mockElementAtPoint,
|
|
157
|
+
selector: 'mock-selector-div',
|
|
158
|
+
description: 'mock-desc-div',
|
|
159
|
+
});
|
|
160
|
+
unmount();
|
|
161
|
+
});
|
|
162
|
+
it('regenerates selector when validation fails on click', () => {
|
|
163
|
+
const onPick = vi.fn();
|
|
164
|
+
document.elementFromPoint.mockReturnValue(mockElementAtPoint);
|
|
165
|
+
vi.mocked(validateSelector).mockReturnValue(false);
|
|
166
|
+
vi.mocked(generateSelector).mockReturnValue('regenerated-selector');
|
|
167
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: onPick, onCancel: vi.fn() }));
|
|
168
|
+
// Hover to set element
|
|
169
|
+
act(() => {
|
|
170
|
+
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
|
|
171
|
+
});
|
|
172
|
+
// Click
|
|
173
|
+
act(() => {
|
|
174
|
+
document.dispatchEvent(new MouseEvent('click', { clientX: 250, clientY: 150, bubbles: true }));
|
|
175
|
+
});
|
|
176
|
+
expect(onPick).toHaveBeenCalledWith(expect.objectContaining({
|
|
177
|
+
selector: 'regenerated-selector',
|
|
178
|
+
}));
|
|
179
|
+
unmount();
|
|
180
|
+
});
|
|
181
|
+
it('does not call onPick when no element is hovered on click', () => {
|
|
182
|
+
const onPick = vi.fn();
|
|
183
|
+
document.elementFromPoint.mockReturnValue(null);
|
|
184
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: onPick, onCancel: vi.fn() }));
|
|
185
|
+
act(() => {
|
|
186
|
+
document.dispatchEvent(new MouseEvent('click', { clientX: 250, clientY: 150, bubbles: true }));
|
|
187
|
+
});
|
|
188
|
+
expect(onPick).not.toHaveBeenCalled();
|
|
189
|
+
unmount();
|
|
190
|
+
});
|
|
191
|
+
it('removes event listeners when deactivated', () => {
|
|
192
|
+
const onCancel = vi.fn();
|
|
193
|
+
const { rerender, unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: onCancel }));
|
|
194
|
+
// Deactivate
|
|
195
|
+
rerender(_jsx(AnchorPicker, { isActive: false, onPick: vi.fn(), onCancel: onCancel }));
|
|
196
|
+
// Escape should not trigger onCancel after deactivation
|
|
197
|
+
act(() => {
|
|
198
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
199
|
+
});
|
|
200
|
+
expect(onCancel).not.toHaveBeenCalled();
|
|
201
|
+
unmount();
|
|
202
|
+
});
|
|
203
|
+
it('temporarily disables pointer events on overlay during mousemove detection', () => {
|
|
204
|
+
const overlayPointerEvents = [];
|
|
205
|
+
document.elementFromPoint.mockImplementation(() => {
|
|
206
|
+
// Capture overlay pointer events state during elementFromPoint call
|
|
207
|
+
const overlay = document.body.querySelector('[data-syntro-anchor-picker]');
|
|
208
|
+
if (overlay) {
|
|
209
|
+
overlayPointerEvents.push(overlay.style.pointerEvents);
|
|
210
|
+
}
|
|
211
|
+
return mockElementAtPoint;
|
|
212
|
+
});
|
|
213
|
+
const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
|
|
214
|
+
act(() => {
|
|
215
|
+
document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
|
|
216
|
+
});
|
|
217
|
+
// During elementFromPoint, overlay pointerEvents should be 'none'
|
|
218
|
+
expect(overlayPointerEvents).toContain('none');
|
|
219
|
+
// After the call, it should be restored to 'auto'
|
|
220
|
+
const overlay = document.body.querySelector('[data-syntro-anchor-picker]');
|
|
221
|
+
expect(overlay.style.pointerEvents).toBe('auto');
|
|
222
|
+
unmount();
|
|
223
|
+
});
|
|
224
|
+
});
|