@syntrologie/adapt-content 2.4.1 → 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.
Files changed (54) hide show
  1. package/dist/editor.d.ts.map +1 -1
  2. package/dist/editor.js +59 -3
  3. package/dist/runtime.d.ts.map +1 -1
  4. package/dist/runtime.js +67 -12
  5. package/dist/summarize.d.ts.map +1 -1
  6. package/dist/summarize.js +12 -1
  7. package/dist/types.d.ts +9 -42
  8. package/dist/types.d.ts.map +1 -1
  9. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts +2 -0
  10. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts.map +1 -0
  11. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.js +224 -0
  12. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +102 -0
  13. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.js +58 -6
  14. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +18 -0
  15. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +61 -2
  16. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +478 -7
  17. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +54 -0
  18. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts +2 -0
  19. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts.map +1 -0
  20. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.js +257 -0
  21. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts +2 -0
  22. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts.map +1 -0
  23. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.js +1015 -0
  24. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +1 -1
  25. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +4 -4
  26. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +1 -1
  27. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +2 -2
  28. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +2 -1
  29. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +1 -1
  30. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +20 -3
  31. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +10 -8
  32. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +1 -1
  33. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +350 -87
  34. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +1 -1
  35. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +3 -3
  36. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +1 -1
  37. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +1 -1
  38. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +1 -1
  39. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +1 -1
  40. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +5 -2
  41. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +24 -0
  42. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -0
  43. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/{useShowWhenStatus.js → useTriggerWhenStatus.js} +18 -15
  44. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +3 -3
  45. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +1 -1
  46. package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +1 -1
  47. package/package.json +4 -5
  48. package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +0 -26
  49. package/node_modules/@syntrologie/sdk-contracts/dist/index.js +0 -13
  50. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +0 -1428
  51. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +0 -142
  52. package/node_modules/@syntrologie/sdk-contracts/package.json +0 -33
  53. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts +0 -24
  54. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts.map +0 -1
@@ -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;AA4IhD,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CA6iB3E;AAED;;GAEG;AACH,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC;AAEF,eAAO,MAAM,WAAW;;;;CAAe,CAAC;AAExC,eAAe,aAAa,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));
@@ -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,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"}
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
- const anchorEl = context.resolveAnchor(action.anchorId);
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
- throw new Error(`Anchor not found: ${action.anchorId}`);
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
- const anchorEl = context.resolveAnchor(action.anchorId);
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
- throw new Error(`Anchor not found: ${action.anchorId}`);
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
- const anchorEl = context.resolveAnchor(action.anchorId);
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
- throw new Error(`Anchor not found: ${action.anchorId}`);
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
- const anchorEl = context.resolveAnchor(action.anchorId);
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
- throw new Error(`Anchor not found: ${action.anchorId}`);
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
- const anchorEl = context.resolveAnchor(action.anchorId);
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
- throw new Error(`Anchor not found: ${action.anchorId}`);
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
- const anchorEl = context.resolveAnchor(action.anchorId);
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
- throw new Error(`Anchor not found: ${action.anchorId}`);
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();
@@ -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;AAI9C;;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"}
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
- export interface EditorPanelProps {
8
- config: Record<string, unknown>;
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: string;
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: string;
21
+ anchorId: AnchorId;
55
22
  text: string;
56
23
  }
57
24
  export interface SetAttrAction extends BaseAction {
58
25
  kind: 'content:setAttr';
59
- anchorId: string;
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: string;
32
+ anchorId: AnchorId;
66
33
  className: string;
67
34
  }
68
35
  export interface RemoveClassAction extends BaseAction {
69
36
  kind: 'content:removeClass';
70
- anchorId: string;
37
+ anchorId: AnchorId;
71
38
  className: string;
72
39
  }
73
40
  export interface SetStyleAction extends BaseAction {
74
41
  kind: 'content:setStyle';
75
- anchorId: string;
42
+ anchorId: AnchorId;
76
43
  styles: Record<string, string>;
77
44
  }
78
45
  export type { ActionExecutor, ExecutorCleanup, ExecutorContext, ExecutorResult, ExecutorUpdate, } from '@syntrologie/sdk-contracts';
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AnchorPicker.test.d.ts.map
@@ -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
+ });