@syntrologie/adapt-content 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdn.d.ts +1 -1
- package/dist/content-editor-state.d.ts +45 -0
- package/dist/content-editor-state.d.ts.map +1 -0
- package/dist/content-editor-state.js +123 -0
- package/dist/content-editor-ui.d.ts +26 -0
- package/dist/content-editor-ui.d.ts.map +1 -0
- package/dist/content-editor-ui.js +291 -0
- package/dist/editor.d.ts +4 -22
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +4 -413
- package/dist/sanitizer.d.ts.map +1 -1
- package/dist/sanitizer.js +7 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +2 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +2 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.js +0 -1
- package/node_modules/@syntrologie/shared-editor-ui/package.json +11 -9
- package/package.json +14 -14
package/dist/cdn.d.ts
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Content - Editor State & Logic
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers for the content editor: anchor resolution, types, section config,
|
|
5
|
+
* flatten/filter logic, anchor detection hook, and dismissal management.
|
|
6
|
+
*/
|
|
7
|
+
import type { ContentConfig } from './schema';
|
|
8
|
+
/** Extract the CSS selector string from an anchorId (object or legacy string). */
|
|
9
|
+
export declare function resolveAnchorSelector(anchorId: unknown): string;
|
|
10
|
+
/** Extract the target route from an AnchorId object, ignoring wildcard '**'. */
|
|
11
|
+
export declare function resolveAnchorRoute(anchorId: unknown): string | null;
|
|
12
|
+
/** Save a pending highlight selector to sessionStorage (inlined to avoid cross-package import). */
|
|
13
|
+
export declare function savePendingHighlight(selector: string): void;
|
|
14
|
+
export type SectionKey = keyof ContentConfig;
|
|
15
|
+
export interface ItemRef {
|
|
16
|
+
section: SectionKey;
|
|
17
|
+
index: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function itemKey(section: SectionKey, index: number): string;
|
|
20
|
+
export declare function parseItemKey(key: string): ItemRef;
|
|
21
|
+
export declare const SECTION_ICON_MAP: Record<SectionKey, React.ComponentType<{
|
|
22
|
+
size?: number;
|
|
23
|
+
className?: string;
|
|
24
|
+
}>>;
|
|
25
|
+
export interface FlatItem {
|
|
26
|
+
key: string;
|
|
27
|
+
section: SectionKey;
|
|
28
|
+
index: number;
|
|
29
|
+
summary: string;
|
|
30
|
+
anchorId: string;
|
|
31
|
+
rawAnchorId: unknown;
|
|
32
|
+
}
|
|
33
|
+
/** Build a flat list of all items across all section types. */
|
|
34
|
+
export declare function flattenItems(config: ContentConfig): FlatItem[];
|
|
35
|
+
/** Remove items by key set from a config, returning a new config. */
|
|
36
|
+
export declare function filterConfig(config: ContentConfig, dismissedKeys: Set<string>): ContentConfig;
|
|
37
|
+
export interface DetectionEntry {
|
|
38
|
+
found: boolean;
|
|
39
|
+
element: HTMLElement | null;
|
|
40
|
+
}
|
|
41
|
+
export declare function useAnchorDetection(items: Array<{
|
|
42
|
+
key: string;
|
|
43
|
+
anchorId: string;
|
|
44
|
+
}>): Map<string, DetectionEntry>;
|
|
45
|
+
//# sourceMappingURL=content-editor-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-editor-state.d.ts","sourceRoot":"","sources":["../src/content-editor-state.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAO9C,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAK/D;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CASnE;AAED,mGAAmG;AACnG,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,QAMpD;AAMD,MAAM,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC;AAE7C,MAAM,WAAW,OAAO;IACtB,OAAO,EAAE,UAAU,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAElE;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGjD;AAMD,eAAO,MAAM,gBAAgB,EAAE,MAAM,CACnC,UAAU,EACV,KAAK,CAAC,aAAa,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQ3D,CAAC;AAMF,MAAM,WAAW,QAAQ;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,UAAU,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,QAAQ,EAAE,CAkB9D;AAED,qEAAqE;AACrE,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,EAAE,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,aAAa,CAW7F;AAMD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;CAC7B;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,GAC9C,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CA6B7B"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Content - Editor State & Logic
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers for the content editor: anchor resolution, types, section config,
|
|
5
|
+
* flatten/filter logic, anchor detection hook, and dismissal management.
|
|
6
|
+
*/
|
|
7
|
+
import { FileCode, Minus, Palette, Plus, Tag, Type } from 'lucide-react';
|
|
8
|
+
import { useEffect, useRef, useState } from 'react';
|
|
9
|
+
import { summarizeContentChange } from './summarize';
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Anchor Helpers
|
|
12
|
+
// ============================================================================
|
|
13
|
+
/** Extract the CSS selector string from an anchorId (object or legacy string). */
|
|
14
|
+
export function resolveAnchorSelector(anchorId) {
|
|
15
|
+
if (!anchorId)
|
|
16
|
+
return '';
|
|
17
|
+
if (typeof anchorId === 'string')
|
|
18
|
+
return anchorId;
|
|
19
|
+
if (typeof anchorId === 'object')
|
|
20
|
+
return anchorId.selector ?? '';
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
/** Extract the target route from an AnchorId object, ignoring wildcard '**'. */
|
|
24
|
+
export function resolveAnchorRoute(anchorId) {
|
|
25
|
+
if (!anchorId || typeof anchorId !== 'object')
|
|
26
|
+
return null;
|
|
27
|
+
const route = anchorId.route;
|
|
28
|
+
if (typeof route === 'string' && route !== '**')
|
|
29
|
+
return route;
|
|
30
|
+
if (Array.isArray(route)) {
|
|
31
|
+
const first = route.find((r) => typeof r === 'string' && r !== '**');
|
|
32
|
+
return first ?? null;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
/** Save a pending highlight selector to sessionStorage (inlined to avoid cross-package import). */
|
|
37
|
+
export function savePendingHighlight(selector) {
|
|
38
|
+
try {
|
|
39
|
+
sessionStorage.setItem('syntro:editor:pending-highlight', selector);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// Silently ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function itemKey(section, index) {
|
|
46
|
+
return `${section}:${index}`;
|
|
47
|
+
}
|
|
48
|
+
export function parseItemKey(key) {
|
|
49
|
+
const [section, indexStr] = key.split(':');
|
|
50
|
+
return { section: section, index: Number(indexStr) };
|
|
51
|
+
}
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Section Config
|
|
54
|
+
// ============================================================================
|
|
55
|
+
export const SECTION_ICON_MAP = {
|
|
56
|
+
textReplacements: Type,
|
|
57
|
+
attributeChanges: Tag,
|
|
58
|
+
styleChanges: Palette,
|
|
59
|
+
htmlInsertions: FileCode,
|
|
60
|
+
classAdditions: Plus,
|
|
61
|
+
classRemovals: Minus,
|
|
62
|
+
};
|
|
63
|
+
/** Build a flat list of all items across all section types. */
|
|
64
|
+
export function flattenItems(config) {
|
|
65
|
+
const items = [];
|
|
66
|
+
const sections = Object.keys(SECTION_ICON_MAP);
|
|
67
|
+
for (const section of sections) {
|
|
68
|
+
const arr = config[section] || [];
|
|
69
|
+
arr.forEach((item, i) => {
|
|
70
|
+
const rec = item;
|
|
71
|
+
items.push({
|
|
72
|
+
key: itemKey(section, i),
|
|
73
|
+
section,
|
|
74
|
+
index: i,
|
|
75
|
+
summary: summarizeContentChange(section, rec),
|
|
76
|
+
anchorId: resolveAnchorSelector(rec.anchorId),
|
|
77
|
+
rawAnchorId: rec.anchorId,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return items;
|
|
82
|
+
}
|
|
83
|
+
/** Remove items by key set from a config, returning a new config. */
|
|
84
|
+
export function filterConfig(config, dismissedKeys) {
|
|
85
|
+
const result = { ...config };
|
|
86
|
+
const sections = Object.keys(SECTION_ICON_MAP);
|
|
87
|
+
for (const section of sections) {
|
|
88
|
+
const arr = config[section] || [];
|
|
89
|
+
const filtered = arr.filter((_, i) => !dismissedKeys.has(itemKey(section, i)));
|
|
90
|
+
if (filtered.length > 0 || config[section] !== undefined) {
|
|
91
|
+
result[section] = filtered;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
export function useAnchorDetection(items) {
|
|
97
|
+
const [detectionMap, setDetectionMap] = useState(new Map());
|
|
98
|
+
const itemsRef = useRef(items);
|
|
99
|
+
itemsRef.current = items;
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const runDetection = () => {
|
|
102
|
+
const map = new Map();
|
|
103
|
+
for (const item of itemsRef.current) {
|
|
104
|
+
if (!item.anchorId) {
|
|
105
|
+
map.set(item.key, { found: false, element: null });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const el = document.querySelector(item.anchorId);
|
|
110
|
+
map.set(item.key, { found: el !== null, element: el });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
map.set(item.key, { found: false, element: null });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
setDetectionMap(map);
|
|
117
|
+
};
|
|
118
|
+
runDetection();
|
|
119
|
+
const interval = setInterval(runDetection, 2000);
|
|
120
|
+
return () => clearInterval(interval);
|
|
121
|
+
}, []);
|
|
122
|
+
return detectionMap;
|
|
123
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Content - Editor UI Component
|
|
3
|
+
*
|
|
4
|
+
* Main editor component with tab switching, item selection, form inputs,
|
|
5
|
+
* save/publish, and highlight on page.
|
|
6
|
+
*/
|
|
7
|
+
import type { EditorPanelProps } from './types';
|
|
8
|
+
export declare function ContentEditor({ config, onChange, editor }: EditorPanelProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
/**
|
|
10
|
+
* Editor module configuration.
|
|
11
|
+
*/
|
|
12
|
+
export declare const editor: {
|
|
13
|
+
panel: {
|
|
14
|
+
title: string;
|
|
15
|
+
icon: string;
|
|
16
|
+
description: string;
|
|
17
|
+
};
|
|
18
|
+
component: typeof ContentEditor;
|
|
19
|
+
};
|
|
20
|
+
export declare const editorPanel: {
|
|
21
|
+
title: string;
|
|
22
|
+
icon: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
export default ContentEditor;
|
|
26
|
+
//# sourceMappingURL=content-editor-ui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-editor-ui.d.ts","sourceRoot":"","sources":["../src/content-editor-ui.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgCH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAgBhD,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAsjB3E;AAED;;GAEG;AACH,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC;AAEF,eAAO,MAAM,WAAW;;;;CAAe,CAAC;AAExC,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Adaptive Content - Editor UI Component
|
|
4
|
+
*
|
|
5
|
+
* Main editor component with tab switching, item selection, form inputs,
|
|
6
|
+
* save/publish, and highlight on page.
|
|
7
|
+
*/
|
|
8
|
+
import { DetectionBadge, DismissedSection, EditorBody, EditorCard, EditorFooter, EditorHeader, EditorInput, EditorLayout, EditorSelect, EditorTextarea, EmptyState, GroupHeader, } from '@syntrologie/shared-editor-ui';
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
10
|
+
import { AnchorPicker } from './components/AnchorPicker';
|
|
11
|
+
import { filterConfig, flattenItems, parseItemKey, resolveAnchorRoute, resolveAnchorSelector, SECTION_ICON_MAP, savePendingHighlight, useAnchorDetection, } from './content-editor-state';
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Section Icon Component
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/** Renders the appropriate Lucide icon for a section type */
|
|
16
|
+
function SectionIcon({ section, className }) {
|
|
17
|
+
const IconComponent = SECTION_ICON_MAP[section];
|
|
18
|
+
return _jsx(IconComponent, { size: 16, className: className });
|
|
19
|
+
}
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// ContentEditor Component
|
|
22
|
+
// ============================================================================
|
|
23
|
+
export function ContentEditor({ config, onChange, editor }) {
|
|
24
|
+
const typedConfig = config;
|
|
25
|
+
const [dismissedKeys, setDismissedKeys] = useState(() => editor.getDismissedKeys?.() ?? new Set());
|
|
26
|
+
const [editingKey, setEditingKey] = useState(null);
|
|
27
|
+
const [, setPreviewMode] = useState('after');
|
|
28
|
+
// Sync dismissed keys back to navigation context on every change
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
editor.setDismissedKeys?.(dismissedKeys);
|
|
31
|
+
}, [dismissedKeys, editor]);
|
|
32
|
+
// Create mode state
|
|
33
|
+
const [createMode, setCreateMode] = useState(null);
|
|
34
|
+
const [createAnchorId, setCreateAnchorId] = useState('');
|
|
35
|
+
const [createText, setCreateText] = useState('');
|
|
36
|
+
const [createDescription, setCreateDescription] = useState('');
|
|
37
|
+
// React to global before/after toggle from the panel
|
|
38
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally omitted — adding config/typedConfig/previewConfig would cause infinite re-renders since previewConfig triggers state updates
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const mode = editor.previewMode;
|
|
41
|
+
if (!mode)
|
|
42
|
+
return;
|
|
43
|
+
if (mode === 'before') {
|
|
44
|
+
// Remove all content changes — push a config with every item filtered out
|
|
45
|
+
const allKeys = new Set(flattenItems(typedConfig).map((item) => item.key));
|
|
46
|
+
const empty = filterConfig(typedConfig, allKeys);
|
|
47
|
+
editor.previewConfig(empty);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Restore the full config
|
|
51
|
+
editor.previewConfig(config);
|
|
52
|
+
}
|
|
53
|
+
}, [editor.previewMode]);
|
|
54
|
+
// Consume initialEditKey from accordion navigation on mount
|
|
55
|
+
const initialConsumed = useRef(false);
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (editor.initialEditKey != null && !initialConsumed.current) {
|
|
58
|
+
initialConsumed.current = true;
|
|
59
|
+
const allFlat = flattenItems(typedConfig);
|
|
60
|
+
const targetIdx = Number(editor.initialEditKey);
|
|
61
|
+
if (targetIdx >= 0 && targetIdx < allFlat.length) {
|
|
62
|
+
const target = allFlat[targetIdx];
|
|
63
|
+
setEditingKey(target.key);
|
|
64
|
+
if (target.anchorId) {
|
|
65
|
+
editor.highlightElement(target.anchorId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
editor.clearInitialState?.();
|
|
69
|
+
}
|
|
70
|
+
else if (editor.initialCreate && !initialConsumed.current) {
|
|
71
|
+
initialConsumed.current = true;
|
|
72
|
+
setCreateMode('form');
|
|
73
|
+
editor.clearInitialState?.();
|
|
74
|
+
}
|
|
75
|
+
}, [editor, typedConfig]);
|
|
76
|
+
const allItems = flattenItems(typedConfig);
|
|
77
|
+
const activeItems = allItems.filter((item) => !dismissedKeys.has(item.key));
|
|
78
|
+
const dismissedItems = allItems.filter((item) => dismissedKeys.has(item.key));
|
|
79
|
+
const totalChanges = activeItems.length;
|
|
80
|
+
const [, setHoveredKey] = useState(null);
|
|
81
|
+
const detectionMap = useAnchorDetection(allItems);
|
|
82
|
+
const foundCount = activeItems.filter((item) => detectionMap.get(item.key)?.found).length;
|
|
83
|
+
const handleDismiss = useCallback((key) => {
|
|
84
|
+
setDismissedKeys((prev) => {
|
|
85
|
+
const next = new Set(prev);
|
|
86
|
+
next.add(key);
|
|
87
|
+
return next;
|
|
88
|
+
});
|
|
89
|
+
if (editingKey === key)
|
|
90
|
+
setEditingKey(null);
|
|
91
|
+
}, [editingKey]);
|
|
92
|
+
const handleRestore = useCallback((key) => {
|
|
93
|
+
setDismissedKeys((prev) => {
|
|
94
|
+
const next = new Set(prev);
|
|
95
|
+
next.delete(key);
|
|
96
|
+
return next;
|
|
97
|
+
});
|
|
98
|
+
}, []);
|
|
99
|
+
const handleCardClick = useCallback((item) => {
|
|
100
|
+
if (item.anchorId) {
|
|
101
|
+
editor.highlightElement(item.anchorId);
|
|
102
|
+
}
|
|
103
|
+
setEditingKey(item.key);
|
|
104
|
+
}, [editor]);
|
|
105
|
+
const handleBackToList = useCallback(() => {
|
|
106
|
+
setEditingKey(null);
|
|
107
|
+
setPreviewMode('after');
|
|
108
|
+
editor.previewConfig(config);
|
|
109
|
+
editor.clearHighlight();
|
|
110
|
+
}, [editor, config]);
|
|
111
|
+
// Register back handler in panel header when editing
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
editor.setBackHandler?.(editingKey !== null ? handleBackToList : null);
|
|
114
|
+
return () => editor.setBackHandler?.(null);
|
|
115
|
+
}, [editingKey, handleBackToList, editor]);
|
|
116
|
+
const handleFieldChange = useCallback((section, index, field, value) => {
|
|
117
|
+
const arr = (typedConfig[section] || []).slice();
|
|
118
|
+
const item = { ...arr[index] };
|
|
119
|
+
item[field] = value;
|
|
120
|
+
arr[index] = item;
|
|
121
|
+
const updated = { ...typedConfig, [section]: arr };
|
|
122
|
+
onChange(updated);
|
|
123
|
+
editor.setDirty(true);
|
|
124
|
+
}, [typedConfig, onChange, editor]);
|
|
125
|
+
const handlePublish = useCallback(() => {
|
|
126
|
+
// Filter dismissed items before publishing
|
|
127
|
+
if (dismissedKeys.size > 0) {
|
|
128
|
+
const filtered = filterConfig(typedConfig, dismissedKeys);
|
|
129
|
+
onChange(filtered);
|
|
130
|
+
}
|
|
131
|
+
editor.publish();
|
|
132
|
+
}, [dismissedKeys, typedConfig, onChange, editor]);
|
|
133
|
+
const handleBadgeClick = useCallback(async (item) => {
|
|
134
|
+
const detection = detectionMap.get(item.key);
|
|
135
|
+
if (detection?.found && item.anchorId) {
|
|
136
|
+
editor.highlightElement(item.anchorId);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
const route = resolveAnchorRoute(item.rawAnchorId);
|
|
140
|
+
if (route) {
|
|
141
|
+
if (item.anchorId)
|
|
142
|
+
savePendingHighlight(item.anchorId);
|
|
143
|
+
await editor.navigateTo(route);
|
|
144
|
+
if (item.anchorId)
|
|
145
|
+
editor.highlightElement(item.anchorId);
|
|
146
|
+
}
|
|
147
|
+
else if (item.anchorId) {
|
|
148
|
+
editor.highlightElement(item.anchorId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}, [editor, detectionMap]);
|
|
152
|
+
const handleCardHover = useCallback((item) => {
|
|
153
|
+
setHoveredKey(item.key);
|
|
154
|
+
if (item.anchorId) {
|
|
155
|
+
editor.highlightElement(item.anchorId);
|
|
156
|
+
}
|
|
157
|
+
}, [editor]);
|
|
158
|
+
const handleCardLeave = useCallback(() => {
|
|
159
|
+
setHoveredKey(null);
|
|
160
|
+
editor.clearHighlight();
|
|
161
|
+
}, [editor]);
|
|
162
|
+
// ---- Create flow handlers ----
|
|
163
|
+
const handleStartCreate = useCallback(() => {
|
|
164
|
+
setEditingKey(null);
|
|
165
|
+
editor.clearHighlight();
|
|
166
|
+
setCreateAnchorId('');
|
|
167
|
+
setCreateText('');
|
|
168
|
+
setCreateDescription('');
|
|
169
|
+
setCreateMode('form');
|
|
170
|
+
}, [editor]);
|
|
171
|
+
const handleElementPicked = useCallback((picked) => {
|
|
172
|
+
setCreateAnchorId(picked.selector);
|
|
173
|
+
setCreateDescription(picked.description);
|
|
174
|
+
// Pre-fill with the element's current text content
|
|
175
|
+
const text = picked.element.textContent?.trim() || '';
|
|
176
|
+
setCreateText(text);
|
|
177
|
+
setCreateMode('form');
|
|
178
|
+
editor.highlightElement(picked.selector);
|
|
179
|
+
}, [editor]);
|
|
180
|
+
const handleCancelCreate = useCallback(() => {
|
|
181
|
+
setCreateMode(null);
|
|
182
|
+
setCreateAnchorId('');
|
|
183
|
+
setCreateText('');
|
|
184
|
+
setCreateDescription('');
|
|
185
|
+
editor.clearHighlight();
|
|
186
|
+
}, [editor]);
|
|
187
|
+
const handleSaveCreate = useCallback(() => {
|
|
188
|
+
if (!createAnchorId)
|
|
189
|
+
return;
|
|
190
|
+
// Add a new textReplacement to the config
|
|
191
|
+
const existing = typedConfig.textReplacements || [];
|
|
192
|
+
const newItem = {
|
|
193
|
+
anchorId: { selector: createAnchorId, route: '**' },
|
|
194
|
+
text: createText,
|
|
195
|
+
summary: `Set text on ${createAnchorId}`,
|
|
196
|
+
};
|
|
197
|
+
const updated = {
|
|
198
|
+
...typedConfig,
|
|
199
|
+
textReplacements: [...existing, newItem],
|
|
200
|
+
};
|
|
201
|
+
onChange(updated);
|
|
202
|
+
editor.setDirty(true);
|
|
203
|
+
// Return to list
|
|
204
|
+
setCreateMode(null);
|
|
205
|
+
setCreateAnchorId('');
|
|
206
|
+
setCreateText('');
|
|
207
|
+
setCreateDescription('');
|
|
208
|
+
editor.clearHighlight();
|
|
209
|
+
}, [createAnchorId, createText, typedConfig, onChange, editor]);
|
|
210
|
+
// ---- Edit form renderers per section type ----
|
|
211
|
+
const renderEditFields = (section, index) => {
|
|
212
|
+
const arr = typedConfig[section] || [];
|
|
213
|
+
const item = arr[index];
|
|
214
|
+
if (!item)
|
|
215
|
+
return null;
|
|
216
|
+
const anchorId = resolveAnchorSelector(item.anchorId);
|
|
217
|
+
switch (section) {
|
|
218
|
+
case 'textReplacements':
|
|
219
|
+
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) })] }));
|
|
220
|
+
case 'attributeChanges':
|
|
221
|
+
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(EditorInput, { label: "Attribute", value: item.attr || '', onChange: (e) => handleFieldChange(section, index, 'attr', e.target.value) }), _jsx(EditorInput, { label: "Value", value: item.value || '', onChange: (e) => handleFieldChange(section, index, 'value', e.target.value) })] }));
|
|
222
|
+
case 'styleChanges': {
|
|
223
|
+
const styleObj = item.styles || {};
|
|
224
|
+
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("label", { className: "se-text-[11px] se-font-semibold se-text-slate-grey-7 se-mb-1 se-block", children: "Styles" }), Object.entries(styleObj).map(([prop, val]) => (_jsxs("div", { className: "se-flex se-gap-1 se-mb-1", children: [_jsx("input", { className: "se-flex-1 se-py-1.5 se-px-2 se-rounded-lg se-border se-border-input-field-border se-bg-slate-grey-3 se-text-text-primary se-text-sm se-font-[inherit] se-box-border", value: prop, readOnly: true }), _jsx("input", { className: "se-flex-1 se-py-1.5 se-px-2 se-rounded-lg se-border se-border-input-field-border se-bg-slate-grey-3 se-text-text-primary se-text-sm se-font-[inherit] se-box-border", value: val, onChange: (e) => {
|
|
225
|
+
const newStyles = { ...styleObj, [prop]: e.target.value };
|
|
226
|
+
handleFieldChange(section, index, 'styles', newStyles);
|
|
227
|
+
} })] }, prop)))] }));
|
|
228
|
+
}
|
|
229
|
+
case 'htmlInsertions':
|
|
230
|
+
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 }), _jsxs(EditorSelect, { label: "Position", value: item.position || 'after', onChange: (e) => handleFieldChange(section, index, 'position', e.target.value), children: [_jsx("option", { value: "before", children: "Before" }), _jsx("option", { value: "after", children: "After" }), _jsx("option", { value: "prepend", children: "Prepend" }), _jsx("option", { value: "append", children: "Append" }), _jsx("option", { value: "replace", children: "Replace" })] }), _jsx(EditorTextarea, { label: "HTML", value: item.html || '', onChange: (e) => handleFieldChange(section, index, 'html', e.target.value), className: "se-font-mono" })] }));
|
|
231
|
+
case 'classAdditions':
|
|
232
|
+
case 'classRemovals':
|
|
233
|
+
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(EditorInput, { label: "Class Name", value: item.className || '', onChange: (e) => handleFieldChange(section, index, 'className', e.target.value) })] }));
|
|
234
|
+
default:
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const headerTitle = createMode === 'form' || createMode === 'picking' ? 'Add Text Change' : 'Content';
|
|
239
|
+
const headerSubtitle = createMode === 'picking'
|
|
240
|
+
? 'Click an element on the page to select it. Press ESC to go back.'
|
|
241
|
+
: createMode === 'form'
|
|
242
|
+
? 'Pick an element and set its new text'
|
|
243
|
+
: `${totalChanges} change${totalChanges !== 1 ? 's' : ''}${totalChanges > 0 ? ` (${foundCount} found on this page)` : ''}`;
|
|
244
|
+
const handleHeaderBack = () => {
|
|
245
|
+
if (createMode === 'picking') {
|
|
246
|
+
setCreateMode('form');
|
|
247
|
+
}
|
|
248
|
+
else if (createMode === 'form') {
|
|
249
|
+
handleCancelCreate();
|
|
250
|
+
}
|
|
251
|
+
else if (editingKey !== null) {
|
|
252
|
+
handleBackToList();
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
editor.navigateHome();
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
return (_jsxs(EditorLayout, { children: [_jsx(EditorHeader, { title: headerTitle, subtitle: headerSubtitle, onBack: handleHeaderBack }), _jsx(EditorBody, { children: createMode === 'form' || createMode === 'picking' ? (
|
|
259
|
+
/* ---- Create form mode ---- */
|
|
260
|
+
_jsxs("div", { className: "se-flex se-flex-col se-gap-4", children: [_jsxs("div", { className: "se-flex se-flex-col se-gap-1.5", children: [_jsx("span", { className: "se-text-sm se-font-semibold se-text-text-primary se-uppercase se-tracking-wide", children: "Target Element" }), createAnchorId ? (_jsxs("div", { className: "se-flex se-gap-2 se-items-center", children: [_jsx("code", { className: "se-flex-1 se-py-1.5 se-px-2 se-rounded-lg se-border se-border-input-field-border se-bg-slate-grey-3 se-text-text-primary se-text-sm se-overflow-hidden se-text-ellipsis se-whitespace-nowrap", children: createAnchorId }), _jsx("button", { type: "button", onClick: () => setCreateMode('picking'), className: "se-py-1.5 se-px-3 se-rounded-lg se-border se-border-btn-neutral-border se-bg-btn-neutral se-text-btn-neutral-text se-text-sm se-cursor-pointer se-shrink-0 hover:se-text-btn-neutral-text-hover", children: "Re-pick" })] })) : (_jsx("button", { type: "button", onClick: () => setCreateMode('picking'), className: "se-w-full se-h-12 se-px-4 se-py-2 se-rounded-lg se-border-2 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-inline-flex se-items-center se-justify-center se-gap-2 hover:se-bg-btn-primary/10 hover:se-border-btn-primary/50", children: "+ Pick Target Element" })), createDescription && (_jsx("span", { className: "se-text-sm se-text-text-secondary", children: createDescription }))] }), _jsxs("div", { className: "se-flex se-flex-col se-gap-1.5", children: [_jsx("span", { className: "se-text-sm se-font-semibold se-text-text-primary se-uppercase se-tracking-wide", children: "Text Content" }), _jsx(EditorTextarea, { value: createText, onChange: (e) => setCreateText(e.target.value) })] }), _jsxs("div", { className: "se-flex se-gap-2 se-mt-2", children: [_jsx("button", { type: "button", onClick: handleCancelCreate, className: "se-flex-1 se-h-10 se-px-4 se-py-2 se-rounded-md se-border se-border-btn-neutral-border se-bg-btn-neutral se-text-btn-neutral-text se-text-sm se-font-medium se-cursor-pointer se-inline-flex se-items-center se-justify-center hover:se-text-btn-neutral-text-hover", children: "Cancel" }), _jsx("button", { type: "button", onClick: handleSaveCreate, disabled: !createAnchorId, className: "se-flex-1 se-h-10 se-px-4 se-py-2 se-rounded-md se-border-none se-bg-btn-primary se-text-btn-primary-text se-text-sm se-font-medium se-cursor-pointer se-inline-flex se-items-center se-justify-center hover:se-bg-btn-primary-hover disabled:se-opacity-50 disabled:se-pointer-events-none", children: "Add Change" })] })] })) : editingKey !== null ? (
|
|
261
|
+
/* ---- Edit mode ---- */
|
|
262
|
+
(() => {
|
|
263
|
+
const ref = parseItemKey(editingKey);
|
|
264
|
+
const editItem = allItems.find((it) => it.key === editingKey);
|
|
265
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "se-flex se-items-center se-gap-2 se-mb-3 se-text-lg se-font-semibold se-text-text-primary", children: [_jsx("span", { children: editItem && _jsx(SectionIcon, { section: editItem.section }) }), _jsx("span", { children: editItem?.summary })] }), renderEditFields(ref.section, ref.index)] }));
|
|
266
|
+
})()) : (
|
|
267
|
+
/* ---- List mode ---- */
|
|
268
|
+
_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) => {
|
|
269
|
+
const detection = detectionMap.get(item.key);
|
|
270
|
+
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) => {
|
|
271
|
+
e.stopPropagation();
|
|
272
|
+
handleDismiss(item.key);
|
|
273
|
+
}, title: "Dismiss this change", children: "\u00D7" })] }, item.key));
|
|
274
|
+
})] })), dismissedItems.length > 0 && (_jsx(DismissedSection, { count: dismissedItems.length, children: dismissedItems.map((item) => (_jsxs("div", { className: "se-flex se-items-center se-gap-2 se-py-1.5 se-px-2.5 se-rounded-lg se-border se-border-white/[0.03] se-bg-transparent se-mb-0.5 se-cursor-pointer se-text-sm se-text-text-tertiary se-opacity-60", children: [_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 se-line-through", 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-blue-5 se-text-[11px] se-cursor-pointer se-shrink-0 se-leading-none", onClick: (e) => {
|
|
275
|
+
e.stopPropagation();
|
|
276
|
+
handleRestore(item.key);
|
|
277
|
+
}, children: "Restore" })] }, item.key))) }))] })) }), _jsx(EditorFooter, { onSave: () => editor.save(), onPublish: handlePublish }), _jsx(AnchorPicker, { isActive: createMode === 'picking', onPick: handleElementPicked, onCancel: () => setCreateMode('form') })] }));
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Editor module configuration.
|
|
281
|
+
*/
|
|
282
|
+
export const editor = {
|
|
283
|
+
panel: {
|
|
284
|
+
title: 'Content',
|
|
285
|
+
icon: '\u{1f4dd}',
|
|
286
|
+
description: 'Text and attribute modifications',
|
|
287
|
+
},
|
|
288
|
+
component: ContentEditor,
|
|
289
|
+
};
|
|
290
|
+
export const editorPanel = editor.panel;
|
|
291
|
+
export default ContentEditor;
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Adaptive Content - Editor
|
|
2
|
+
* Adaptive Content - Editor Module (barrel)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* Displays a scannable list of one-liner change summaries.
|
|
6
|
-
* Clicking a card navigates to the element and shows a floating edit panel.
|
|
4
|
+
* Re-exports from the split state and UI modules for backward compatibility.
|
|
7
5
|
*/
|
|
8
|
-
import
|
|
9
|
-
export
|
|
10
|
-
/**
|
|
11
|
-
* Editor module configuration.
|
|
12
|
-
*/
|
|
13
|
-
export declare const editor: {
|
|
14
|
-
panel: {
|
|
15
|
-
title: string;
|
|
16
|
-
icon: string;
|
|
17
|
-
description: string;
|
|
18
|
-
};
|
|
19
|
-
component: typeof ContentEditor;
|
|
20
|
-
};
|
|
21
|
-
export declare const editorPanel: {
|
|
22
|
-
title: string;
|
|
23
|
-
icon: string;
|
|
24
|
-
description: string;
|
|
25
|
-
};
|
|
6
|
+
import { ContentEditor } from './content-editor-ui';
|
|
7
|
+
export { ContentEditor, editor, editorPanel } from './content-editor-ui';
|
|
26
8
|
export default ContentEditor;
|
|
27
9
|
//# sourceMappingURL=editor.d.ts.map
|
package/dist/editor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACzE,eAAe,aAAa,CAAC"}
|
package/dist/editor.js
CHANGED
|
@@ -1,417 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
1
|
/**
|
|
3
|
-
* Adaptive Content - Editor
|
|
2
|
+
* Adaptive Content - Editor Module (barrel)
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* Displays a scannable list of one-liner change summaries.
|
|
7
|
-
* Clicking a card navigates to the element and shows a floating edit panel.
|
|
4
|
+
* Re-exports from the split state and UI modules for backward compatibility.
|
|
8
5
|
*/
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
12
|
-
import { AnchorPicker } from './components/AnchorPicker';
|
|
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')
|
|
24
|
-
return anchorId.selector ?? '';
|
|
25
|
-
return '';
|
|
26
|
-
}
|
|
27
|
-
/** Extract the target route from an AnchorId object, ignoring wildcard '**'. */
|
|
28
|
-
function resolveAnchorRoute(anchorId) {
|
|
29
|
-
if (!anchorId || typeof anchorId !== 'object')
|
|
30
|
-
return null;
|
|
31
|
-
const route = anchorId.route;
|
|
32
|
-
if (typeof route === 'string' && route !== '**')
|
|
33
|
-
return route;
|
|
34
|
-
if (Array.isArray(route)) {
|
|
35
|
-
const first = route.find((r) => typeof r === 'string' && r !== '**');
|
|
36
|
-
return first ?? null;
|
|
37
|
-
}
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
/** Save a pending highlight selector to sessionStorage (inlined to avoid cross-package import). */
|
|
41
|
-
function savePendingHighlight(selector) {
|
|
42
|
-
try {
|
|
43
|
-
sessionStorage.setItem('syntro:editor:pending-highlight', selector);
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// Silently ignore
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function itemKey(section, index) {
|
|
50
|
-
return `${section}:${index}`;
|
|
51
|
-
}
|
|
52
|
-
function parseItemKey(key) {
|
|
53
|
-
const [section, indexStr] = key.split(':');
|
|
54
|
-
return { section: section, index: Number(indexStr) };
|
|
55
|
-
}
|
|
56
|
-
// ============================================================================
|
|
57
|
-
// Section Config
|
|
58
|
-
// ============================================================================
|
|
59
|
-
const SECTION_ICON_MAP = {
|
|
60
|
-
textReplacements: Type,
|
|
61
|
-
attributeChanges: Tag,
|
|
62
|
-
styleChanges: Palette,
|
|
63
|
-
htmlInsertions: FileCode,
|
|
64
|
-
classAdditions: Plus,
|
|
65
|
-
classRemovals: Minus,
|
|
66
|
-
};
|
|
67
|
-
/** Renders the appropriate Lucide icon for a section type */
|
|
68
|
-
function SectionIcon({ section, className }) {
|
|
69
|
-
const IconComponent = SECTION_ICON_MAP[section];
|
|
70
|
-
return _jsx(IconComponent, { size: 16, className: className });
|
|
71
|
-
}
|
|
72
|
-
// ============================================================================
|
|
73
|
-
// Helpers
|
|
74
|
-
// ============================================================================
|
|
75
|
-
/** Build a flat list of all items across all section types. */
|
|
76
|
-
function flattenItems(config) {
|
|
77
|
-
const items = [];
|
|
78
|
-
const sections = Object.keys(SECTION_ICON_MAP);
|
|
79
|
-
for (const section of sections) {
|
|
80
|
-
const arr = config[section] || [];
|
|
81
|
-
arr.forEach((item, i) => {
|
|
82
|
-
const rec = item;
|
|
83
|
-
items.push({
|
|
84
|
-
key: itemKey(section, i),
|
|
85
|
-
section,
|
|
86
|
-
index: i,
|
|
87
|
-
summary: summarizeContentChange(section, rec),
|
|
88
|
-
anchorId: resolveAnchorSelector(rec.anchorId),
|
|
89
|
-
rawAnchorId: rec.anchorId,
|
|
90
|
-
});
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
return items;
|
|
94
|
-
}
|
|
95
|
-
/** Remove items by key set from a config, returning a new config. */
|
|
96
|
-
function filterConfig(config, dismissedKeys) {
|
|
97
|
-
const result = { ...config };
|
|
98
|
-
const sections = Object.keys(SECTION_ICON_MAP);
|
|
99
|
-
for (const section of sections) {
|
|
100
|
-
const arr = config[section] || [];
|
|
101
|
-
const filtered = arr.filter((_, i) => !dismissedKeys.has(itemKey(section, i)));
|
|
102
|
-
if (filtered.length > 0 || config[section] !== undefined) {
|
|
103
|
-
result[section] = filtered;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
function useAnchorDetection(items) {
|
|
109
|
-
const [detectionMap, setDetectionMap] = useState(new Map());
|
|
110
|
-
const itemsRef = useRef(items);
|
|
111
|
-
itemsRef.current = items;
|
|
112
|
-
useEffect(() => {
|
|
113
|
-
const runDetection = () => {
|
|
114
|
-
const map = new Map();
|
|
115
|
-
for (const item of itemsRef.current) {
|
|
116
|
-
if (!item.anchorId) {
|
|
117
|
-
map.set(item.key, { found: false, element: null });
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
try {
|
|
121
|
-
const el = document.querySelector(item.anchorId);
|
|
122
|
-
map.set(item.key, { found: el !== null, element: el });
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
map.set(item.key, { found: false, element: null });
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
setDetectionMap(map);
|
|
129
|
-
};
|
|
130
|
-
runDetection();
|
|
131
|
-
const interval = setInterval(runDetection, 2000);
|
|
132
|
-
return () => clearInterval(interval);
|
|
133
|
-
}, []);
|
|
134
|
-
return detectionMap;
|
|
135
|
-
}
|
|
136
|
-
// ============================================================================
|
|
137
|
-
// ContentEditor Component
|
|
138
|
-
// ============================================================================
|
|
139
|
-
export function ContentEditor({ config, onChange, editor }) {
|
|
140
|
-
const typedConfig = config;
|
|
141
|
-
const [dismissedKeys, setDismissedKeys] = useState(() => editor.getDismissedKeys?.() ?? new Set());
|
|
142
|
-
const [editingKey, setEditingKey] = useState(null);
|
|
143
|
-
const [_previewMode, setPreviewMode] = useState('after');
|
|
144
|
-
// Sync dismissed keys back to navigation context on every change
|
|
145
|
-
useEffect(() => {
|
|
146
|
-
editor.setDismissedKeys?.(dismissedKeys);
|
|
147
|
-
}, [dismissedKeys, editor]);
|
|
148
|
-
// Create mode state
|
|
149
|
-
const [createMode, setCreateMode] = useState(null);
|
|
150
|
-
const [createAnchorId, setCreateAnchorId] = useState('');
|
|
151
|
-
const [createText, setCreateText] = useState('');
|
|
152
|
-
const [createDescription, setCreateDescription] = useState('');
|
|
153
|
-
// React to global before/after toggle from the panel
|
|
154
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally omitted — adding config/typedConfig/previewConfig would cause infinite re-renders since previewConfig triggers state updates
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
const mode = editor.previewMode;
|
|
157
|
-
if (!mode)
|
|
158
|
-
return;
|
|
159
|
-
if (mode === 'before') {
|
|
160
|
-
// Remove all content changes — push a config with every item filtered out
|
|
161
|
-
const allKeys = new Set(flattenItems(typedConfig).map((item) => item.key));
|
|
162
|
-
const empty = filterConfig(typedConfig, allKeys);
|
|
163
|
-
editor.previewConfig(empty);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
// Restore the full config
|
|
167
|
-
editor.previewConfig(config);
|
|
168
|
-
}
|
|
169
|
-
}, [editor.previewMode]);
|
|
170
|
-
// Consume initialEditKey from accordion navigation on mount
|
|
171
|
-
const initialConsumed = useRef(false);
|
|
172
|
-
useEffect(() => {
|
|
173
|
-
if (editor.initialEditKey != null && !initialConsumed.current) {
|
|
174
|
-
initialConsumed.current = true;
|
|
175
|
-
const allFlat = flattenItems(typedConfig);
|
|
176
|
-
const targetIdx = Number(editor.initialEditKey);
|
|
177
|
-
if (targetIdx >= 0 && targetIdx < allFlat.length) {
|
|
178
|
-
const target = allFlat[targetIdx];
|
|
179
|
-
setEditingKey(target.key);
|
|
180
|
-
if (target.anchorId) {
|
|
181
|
-
editor.highlightElement(target.anchorId);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
editor.clearInitialState?.();
|
|
185
|
-
}
|
|
186
|
-
else if (editor.initialCreate && !initialConsumed.current) {
|
|
187
|
-
initialConsumed.current = true;
|
|
188
|
-
setCreateMode('form');
|
|
189
|
-
editor.clearInitialState?.();
|
|
190
|
-
}
|
|
191
|
-
}, [editor, typedConfig]);
|
|
192
|
-
const allItems = flattenItems(typedConfig);
|
|
193
|
-
const activeItems = allItems.filter((item) => !dismissedKeys.has(item.key));
|
|
194
|
-
const dismissedItems = allItems.filter((item) => dismissedKeys.has(item.key));
|
|
195
|
-
const totalChanges = activeItems.length;
|
|
196
|
-
const [_hoveredKey, setHoveredKey] = useState(null);
|
|
197
|
-
const detectionMap = useAnchorDetection(allItems);
|
|
198
|
-
const foundCount = activeItems.filter((item) => detectionMap.get(item.key)?.found).length;
|
|
199
|
-
const handleDismiss = useCallback((key) => {
|
|
200
|
-
setDismissedKeys((prev) => {
|
|
201
|
-
const next = new Set(prev);
|
|
202
|
-
next.add(key);
|
|
203
|
-
return next;
|
|
204
|
-
});
|
|
205
|
-
if (editingKey === key)
|
|
206
|
-
setEditingKey(null);
|
|
207
|
-
}, [editingKey]);
|
|
208
|
-
const handleRestore = useCallback((key) => {
|
|
209
|
-
setDismissedKeys((prev) => {
|
|
210
|
-
const next = new Set(prev);
|
|
211
|
-
next.delete(key);
|
|
212
|
-
return next;
|
|
213
|
-
});
|
|
214
|
-
}, []);
|
|
215
|
-
const handleCardClick = useCallback((item) => {
|
|
216
|
-
if (item.anchorId) {
|
|
217
|
-
editor.highlightElement(item.anchorId);
|
|
218
|
-
}
|
|
219
|
-
setEditingKey(item.key);
|
|
220
|
-
}, [editor]);
|
|
221
|
-
const handleBackToList = useCallback(() => {
|
|
222
|
-
setEditingKey(null);
|
|
223
|
-
setPreviewMode('after');
|
|
224
|
-
editor.previewConfig(config);
|
|
225
|
-
editor.clearHighlight();
|
|
226
|
-
}, [editor, config]);
|
|
227
|
-
// Register back handler in panel header when editing
|
|
228
|
-
useEffect(() => {
|
|
229
|
-
editor.setBackHandler?.(editingKey !== null ? handleBackToList : null);
|
|
230
|
-
return () => editor.setBackHandler?.(null);
|
|
231
|
-
}, [editingKey, handleBackToList, editor]);
|
|
232
|
-
const _handleBeforeAfter = useCallback((mode) => {
|
|
233
|
-
setPreviewMode(mode);
|
|
234
|
-
if (mode === 'before') {
|
|
235
|
-
const filtered = filterConfig(typedConfig, new Set([editingKey]));
|
|
236
|
-
editor.previewConfig(filtered);
|
|
237
|
-
}
|
|
238
|
-
else {
|
|
239
|
-
editor.previewConfig(config);
|
|
240
|
-
}
|
|
241
|
-
}, [typedConfig, editingKey, editor, config]);
|
|
242
|
-
const handleFieldChange = useCallback((section, index, field, value) => {
|
|
243
|
-
const arr = (typedConfig[section] || []).slice();
|
|
244
|
-
const item = { ...arr[index] };
|
|
245
|
-
item[field] = value;
|
|
246
|
-
arr[index] = item;
|
|
247
|
-
const updated = { ...typedConfig, [section]: arr };
|
|
248
|
-
onChange(updated);
|
|
249
|
-
editor.setDirty(true);
|
|
250
|
-
}, [typedConfig, onChange, editor]);
|
|
251
|
-
const handlePublish = useCallback(() => {
|
|
252
|
-
// Filter dismissed items before publishing
|
|
253
|
-
if (dismissedKeys.size > 0) {
|
|
254
|
-
const filtered = filterConfig(typedConfig, dismissedKeys);
|
|
255
|
-
onChange(filtered);
|
|
256
|
-
}
|
|
257
|
-
editor.publish();
|
|
258
|
-
}, [dismissedKeys, typedConfig, onChange, editor]);
|
|
259
|
-
const handleBadgeClick = useCallback(async (item) => {
|
|
260
|
-
const detection = detectionMap.get(item.key);
|
|
261
|
-
if (detection?.found && item.anchorId) {
|
|
262
|
-
editor.highlightElement(item.anchorId);
|
|
263
|
-
}
|
|
264
|
-
else {
|
|
265
|
-
const route = resolveAnchorRoute(item.rawAnchorId);
|
|
266
|
-
if (route) {
|
|
267
|
-
if (item.anchorId)
|
|
268
|
-
savePendingHighlight(item.anchorId);
|
|
269
|
-
await editor.navigateTo(route);
|
|
270
|
-
if (item.anchorId)
|
|
271
|
-
editor.highlightElement(item.anchorId);
|
|
272
|
-
}
|
|
273
|
-
else if (item.anchorId) {
|
|
274
|
-
editor.highlightElement(item.anchorId);
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}, [editor, detectionMap]);
|
|
278
|
-
const handleCardHover = useCallback((item) => {
|
|
279
|
-
setHoveredKey(item.key);
|
|
280
|
-
if (item.anchorId) {
|
|
281
|
-
editor.highlightElement(item.anchorId);
|
|
282
|
-
}
|
|
283
|
-
}, [editor]);
|
|
284
|
-
const handleCardLeave = useCallback(() => {
|
|
285
|
-
setHoveredKey(null);
|
|
286
|
-
editor.clearHighlight();
|
|
287
|
-
}, [editor]);
|
|
288
|
-
// ---- Create flow handlers ----
|
|
289
|
-
const handleStartCreate = useCallback(() => {
|
|
290
|
-
setEditingKey(null);
|
|
291
|
-
editor.clearHighlight();
|
|
292
|
-
setCreateAnchorId('');
|
|
293
|
-
setCreateText('');
|
|
294
|
-
setCreateDescription('');
|
|
295
|
-
setCreateMode('form');
|
|
296
|
-
}, [editor]);
|
|
297
|
-
const handleElementPicked = useCallback((picked) => {
|
|
298
|
-
setCreateAnchorId(picked.selector);
|
|
299
|
-
setCreateDescription(picked.description);
|
|
300
|
-
// Pre-fill with the element's current text content
|
|
301
|
-
const text = picked.element.textContent?.trim() || '';
|
|
302
|
-
setCreateText(text);
|
|
303
|
-
setCreateMode('form');
|
|
304
|
-
editor.highlightElement(picked.selector);
|
|
305
|
-
}, [editor]);
|
|
306
|
-
const handleCancelCreate = useCallback(() => {
|
|
307
|
-
setCreateMode(null);
|
|
308
|
-
setCreateAnchorId('');
|
|
309
|
-
setCreateText('');
|
|
310
|
-
setCreateDescription('');
|
|
311
|
-
editor.clearHighlight();
|
|
312
|
-
}, [editor]);
|
|
313
|
-
const handleSaveCreate = useCallback(() => {
|
|
314
|
-
if (!createAnchorId)
|
|
315
|
-
return;
|
|
316
|
-
// Add a new textReplacement to the config
|
|
317
|
-
const existing = typedConfig.textReplacements || [];
|
|
318
|
-
const newItem = {
|
|
319
|
-
anchorId: { selector: createAnchorId, route: '**' },
|
|
320
|
-
text: createText,
|
|
321
|
-
summary: `Set text on ${createAnchorId}`,
|
|
322
|
-
};
|
|
323
|
-
const updated = {
|
|
324
|
-
...typedConfig,
|
|
325
|
-
textReplacements: [...existing, newItem],
|
|
326
|
-
};
|
|
327
|
-
onChange(updated);
|
|
328
|
-
editor.setDirty(true);
|
|
329
|
-
// Return to list
|
|
330
|
-
setCreateMode(null);
|
|
331
|
-
setCreateAnchorId('');
|
|
332
|
-
setCreateText('');
|
|
333
|
-
setCreateDescription('');
|
|
334
|
-
editor.clearHighlight();
|
|
335
|
-
}, [createAnchorId, createText, typedConfig, onChange, editor]);
|
|
336
|
-
// ---- Edit form renderers per section type ----
|
|
337
|
-
const renderEditFields = (section, index) => {
|
|
338
|
-
const arr = typedConfig[section] || [];
|
|
339
|
-
const item = arr[index];
|
|
340
|
-
if (!item)
|
|
341
|
-
return null;
|
|
342
|
-
const anchorId = resolveAnchorSelector(item.anchorId);
|
|
343
|
-
switch (section) {
|
|
344
|
-
case 'textReplacements':
|
|
345
|
-
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) })] }));
|
|
346
|
-
case 'attributeChanges':
|
|
347
|
-
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(EditorInput, { label: "Attribute", value: item.attr || '', onChange: (e) => handleFieldChange(section, index, 'attr', e.target.value) }), _jsx(EditorInput, { label: "Value", value: item.value || '', onChange: (e) => handleFieldChange(section, index, 'value', e.target.value) })] }));
|
|
348
|
-
case 'styleChanges': {
|
|
349
|
-
const styleObj = item.styles || {};
|
|
350
|
-
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("label", { className: "se-text-[11px] se-font-semibold se-text-slate-grey-7 se-mb-1 se-block", children: "Styles" }), Object.entries(styleObj).map(([prop, val]) => (_jsxs("div", { className: "se-flex se-gap-1 se-mb-1", children: [_jsx("input", { className: "se-flex-1 se-py-1.5 se-px-2 se-rounded-lg se-border se-border-input-field-border se-bg-slate-grey-3 se-text-text-primary se-text-sm se-font-[inherit] se-box-border", value: prop, readOnly: true }), _jsx("input", { className: "se-flex-1 se-py-1.5 se-px-2 se-rounded-lg se-border se-border-input-field-border se-bg-slate-grey-3 se-text-text-primary se-text-sm se-font-[inherit] se-box-border", value: val, onChange: (e) => {
|
|
351
|
-
const newStyles = { ...styleObj, [prop]: e.target.value };
|
|
352
|
-
handleFieldChange(section, index, 'styles', newStyles);
|
|
353
|
-
} })] }, prop)))] }));
|
|
354
|
-
}
|
|
355
|
-
case 'htmlInsertions':
|
|
356
|
-
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 }), _jsxs(EditorSelect, { label: "Position", value: item.position || 'after', onChange: (e) => handleFieldChange(section, index, 'position', e.target.value), children: [_jsx("option", { value: "before", children: "Before" }), _jsx("option", { value: "after", children: "After" }), _jsx("option", { value: "prepend", children: "Prepend" }), _jsx("option", { value: "append", children: "Append" }), _jsx("option", { value: "replace", children: "Replace" })] }), _jsx(EditorTextarea, { label: "HTML", value: item.html || '', onChange: (e) => handleFieldChange(section, index, 'html', e.target.value), className: "se-font-mono" })] }));
|
|
357
|
-
case 'classAdditions':
|
|
358
|
-
case 'classRemovals':
|
|
359
|
-
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(EditorInput, { label: "Class Name", value: item.className || '', onChange: (e) => handleFieldChange(section, index, 'className', e.target.value) })] }));
|
|
360
|
-
default:
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
};
|
|
364
|
-
const headerTitle = createMode === 'form' || createMode === 'picking' ? 'Add Text Change' : 'Content';
|
|
365
|
-
const headerSubtitle = createMode === 'picking'
|
|
366
|
-
? 'Click an element on the page to select it. Press ESC to go back.'
|
|
367
|
-
: createMode === 'form'
|
|
368
|
-
? 'Pick an element and set its new text'
|
|
369
|
-
: `${totalChanges} change${totalChanges !== 1 ? 's' : ''}${totalChanges > 0 ? ` (${foundCount} found on this page)` : ''}`;
|
|
370
|
-
const handleHeaderBack = () => {
|
|
371
|
-
if (createMode === 'picking') {
|
|
372
|
-
setCreateMode('form');
|
|
373
|
-
}
|
|
374
|
-
else if (createMode === 'form') {
|
|
375
|
-
handleCancelCreate();
|
|
376
|
-
}
|
|
377
|
-
else if (editingKey !== null) {
|
|
378
|
-
handleBackToList();
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
editor.navigateHome();
|
|
382
|
-
}
|
|
383
|
-
};
|
|
384
|
-
return (_jsxs(EditorLayout, { children: [_jsx(EditorHeader, { title: headerTitle, subtitle: headerSubtitle, onBack: handleHeaderBack }), _jsx(EditorBody, { children: createMode === 'form' || createMode === 'picking' ? (
|
|
385
|
-
/* ---- Create form mode ---- */
|
|
386
|
-
_jsxs("div", { className: "se-flex se-flex-col se-gap-4", children: [_jsxs("div", { className: "se-flex se-flex-col se-gap-1.5", children: [_jsx("span", { className: "se-text-sm se-font-semibold se-text-text-primary se-uppercase se-tracking-wide", children: "Target Element" }), createAnchorId ? (_jsxs("div", { className: "se-flex se-gap-2 se-items-center", children: [_jsx("code", { className: "se-flex-1 se-py-1.5 se-px-2 se-rounded-lg se-border se-border-input-field-border se-bg-slate-grey-3 se-text-text-primary se-text-sm se-overflow-hidden se-text-ellipsis se-whitespace-nowrap", children: createAnchorId }), _jsx("button", { type: "button", onClick: () => setCreateMode('picking'), className: "se-py-1.5 se-px-3 se-rounded-lg se-border se-border-btn-neutral-border se-bg-btn-neutral se-text-btn-neutral-text se-text-sm se-cursor-pointer se-shrink-0 hover:se-text-btn-neutral-text-hover", children: "Re-pick" })] })) : (_jsx("button", { type: "button", onClick: () => setCreateMode('picking'), className: "se-w-full se-h-12 se-px-4 se-py-2 se-rounded-lg se-border-2 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-inline-flex se-items-center se-justify-center se-gap-2 hover:se-bg-btn-primary/10 hover:se-border-btn-primary/50", children: "+ Pick Target Element" })), createDescription && (_jsx("span", { className: "se-text-sm se-text-text-secondary", children: createDescription }))] }), _jsxs("div", { className: "se-flex se-flex-col se-gap-1.5", children: [_jsx("span", { className: "se-text-sm se-font-semibold se-text-text-primary se-uppercase se-tracking-wide", children: "Text Content" }), _jsx(EditorTextarea, { value: createText, onChange: (e) => setCreateText(e.target.value) })] }), _jsxs("div", { className: "se-flex se-gap-2 se-mt-2", children: [_jsx("button", { type: "button", onClick: handleCancelCreate, className: "se-flex-1 se-h-10 se-px-4 se-py-2 se-rounded-md se-border se-border-btn-neutral-border se-bg-btn-neutral se-text-btn-neutral-text se-text-sm se-font-medium se-cursor-pointer se-inline-flex se-items-center se-justify-center hover:se-text-btn-neutral-text-hover", children: "Cancel" }), _jsx("button", { type: "button", onClick: handleSaveCreate, disabled: !createAnchorId, className: "se-flex-1 se-h-10 se-px-4 se-py-2 se-rounded-md se-border-none se-bg-btn-primary se-text-btn-primary-text se-text-sm se-font-medium se-cursor-pointer se-inline-flex se-items-center se-justify-center hover:se-bg-btn-primary-hover disabled:se-opacity-50 disabled:se-pointer-events-none", children: "Add Change" })] })] })) : editingKey !== null ? (
|
|
387
|
-
/* ---- Edit mode ---- */
|
|
388
|
-
(() => {
|
|
389
|
-
const ref = parseItemKey(editingKey);
|
|
390
|
-
const editItem = allItems.find((it) => it.key === editingKey);
|
|
391
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "se-flex se-items-center se-gap-2 se-mb-3 se-text-lg se-font-semibold se-text-text-primary", children: [_jsx("span", { children: editItem && _jsx(SectionIcon, { section: editItem.section }) }), _jsx("span", { children: editItem?.summary })] }), renderEditFields(ref.section, ref.index)] }));
|
|
392
|
-
})()) : (
|
|
393
|
-
/* ---- List mode ---- */
|
|
394
|
-
_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) => {
|
|
395
|
-
const detection = detectionMap.get(item.key);
|
|
396
|
-
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) => {
|
|
397
|
-
e.stopPropagation();
|
|
398
|
-
handleDismiss(item.key);
|
|
399
|
-
}, title: "Dismiss this change", children: "\u00D7" })] }, item.key));
|
|
400
|
-
})] })), dismissedItems.length > 0 && (_jsx(DismissedSection, { count: dismissedItems.length, children: dismissedItems.map((item) => (_jsxs("div", { className: "se-flex se-items-center se-gap-2 se-py-1.5 se-px-2.5 se-rounded-lg se-border se-border-white/[0.03] se-bg-transparent se-mb-0.5 se-cursor-pointer se-text-sm se-text-text-tertiary se-opacity-60", children: [_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 se-line-through", 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-blue-5 se-text-[11px] se-cursor-pointer se-shrink-0 se-leading-none", onClick: (e) => {
|
|
401
|
-
e.stopPropagation();
|
|
402
|
-
handleRestore(item.key);
|
|
403
|
-
}, children: "Restore" })] }, item.key))) }))] })) }), _jsx(EditorFooter, { onSave: () => editor.save(), onPublish: handlePublish }), _jsx(AnchorPicker, { isActive: createMode === 'picking', onPick: handleElementPicked, onCancel: () => setCreateMode('form') })] }));
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Editor module configuration.
|
|
407
|
-
*/
|
|
408
|
-
export const editor = {
|
|
409
|
-
panel: {
|
|
410
|
-
title: 'Content',
|
|
411
|
-
icon: '\u{1f4dd}',
|
|
412
|
-
description: 'Text and attribute modifications',
|
|
413
|
-
},
|
|
414
|
-
component: ContentEditor,
|
|
415
|
-
};
|
|
416
|
-
export const editorPanel = editor.panel;
|
|
6
|
+
import { ContentEditor } from './content-editor-ui';
|
|
7
|
+
export { ContentEditor, editor, editorPanel } from './content-editor-ui';
|
|
417
8
|
export default ContentEditor;
|
package/dist/sanitizer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sanitizer.d.ts","sourceRoot":"","sources":["../src/sanitizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiCH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"sanitizer.d.ts","sourceRoot":"","sources":["../src/sanitizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiCH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkEjD"}
|
package/dist/sanitizer.js
CHANGED
|
@@ -77,6 +77,13 @@ export function sanitizeHtml(html) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
+
// Strip SVG elements that contained script children (XSS vector)
|
|
81
|
+
const svgs = Array.from(root.querySelectorAll('svg'));
|
|
82
|
+
for (const svg of svgs) {
|
|
83
|
+
if (toRemove.some((el) => svg.contains(el) && el.tagName.toLowerCase() === 'script')) {
|
|
84
|
+
toRemove.push(svg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
80
87
|
// Remove disallowed elements but keep their children
|
|
81
88
|
for (const el of toRemove) {
|
|
82
89
|
while (el.firstChild) {
|
|
@@ -125,7 +125,7 @@ export function AnchorPicker({ isActive, onPick, onCancel, passthroughClicks = f
|
|
|
125
125
|
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.2)',
|
|
126
126
|
pointerEvents: 'none',
|
|
127
127
|
transition: 'all 0.1s ease-out',
|
|
128
|
-
} })), hoveredElement && rect && (_jsxs("div", { style: {
|
|
128
|
+
} })), hoveredElement && rect && (_jsxs("div", { className: "se-text-xs", style: {
|
|
129
129
|
position: 'fixed',
|
|
130
130
|
left: Math.max(8, Math.min(rect.left, window.innerWidth - 320)),
|
|
131
131
|
top: Math.max(8, rect.top - 68),
|
|
@@ -136,11 +136,9 @@ export function AnchorPicker({ isActive, onPick, onCancel, passthroughClicks = f
|
|
|
136
136
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
|
137
137
|
zIndex: 1,
|
|
138
138
|
fontFamily: 'monospace',
|
|
139
|
-
fontSize: '13px',
|
|
140
139
|
maxWidth: '300px',
|
|
141
140
|
pointerEvents: 'none',
|
|
142
|
-
}, children: [_jsx("div", { style: {
|
|
143
|
-
fontSize: '12px',
|
|
141
|
+
}, children: [_jsx("div", { className: "se-text-xs", style: {
|
|
144
142
|
textTransform: 'uppercase',
|
|
145
143
|
letterSpacing: '0.05em',
|
|
146
144
|
marginBottom: '4px',
|
|
@@ -47,7 +47,7 @@ export function ElementHighlight({ element, color, bgColor = 'transparent', bord
|
|
|
47
47
|
cursor: onClick ? 'pointer' : 'default',
|
|
48
48
|
transition: 'all 0.05s ease-out',
|
|
49
49
|
boxSizing: 'border-box',
|
|
50
|
-
}, children: displayLabel && (_jsxs("div", { "data-syntro-highlight-label": true, "data-syntro-editor-ui": "highlight-label", style: {
|
|
50
|
+
}, children: displayLabel && (_jsxs("div", { "data-syntro-highlight-label": true, "data-syntro-editor-ui": "highlight-label", className: "se-text-xs", style: {
|
|
51
51
|
position: 'absolute',
|
|
52
52
|
top: '-22px',
|
|
53
53
|
left: 0,
|
|
@@ -55,7 +55,6 @@ export function ElementHighlight({ element, color, bgColor = 'transparent', bord
|
|
|
55
55
|
? `${color.replace(')', ',0.85)').replace('rgb(', 'rgba(')}`
|
|
56
56
|
: color,
|
|
57
57
|
color: '#fff',
|
|
58
|
-
fontSize: showDimensions ? '11px' : '12px',
|
|
59
58
|
fontWeight: 600,
|
|
60
59
|
fontFamily: showDimensions ? 'monospace' : 'inherit',
|
|
61
60
|
padding: '1px 6px',
|
|
@@ -65,7 +64,7 @@ export function ElementHighlight({ element, color, bgColor = 'transparent', bord
|
|
|
65
64
|
alignItems: 'center',
|
|
66
65
|
gap: '6px',
|
|
67
66
|
pointerEvents: 'auto',
|
|
68
|
-
}, children: [labelIcon, displayLabel, showRemove && onRemove && (_jsx("button", { type: "button", "data-syntro-highlight-remove": true, "data-syntro-editor-ui": "highlight-remove", onClick: (e) => {
|
|
67
|
+
}, children: [labelIcon, displayLabel, showRemove && onRemove && (_jsx("button", { type: "button", className: "se-text-xs", "data-syntro-highlight-remove": true, "data-syntro-editor-ui": "highlight-remove", onClick: (e) => {
|
|
69
68
|
e.stopPropagation();
|
|
70
69
|
e.preventDefault();
|
|
71
70
|
onRemove();
|
|
@@ -76,7 +75,6 @@ export function ElementHighlight({ element, color, bgColor = 'transparent', bord
|
|
|
76
75
|
borderRadius: '50%',
|
|
77
76
|
width: '16px',
|
|
78
77
|
height: '16px',
|
|
79
|
-
fontSize: '12px',
|
|
80
78
|
cursor: 'pointer',
|
|
81
79
|
display: 'flex',
|
|
82
80
|
alignItems: 'center',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useTriggerWhenStatus.d.ts","sourceRoot":"","sources":["../../src/hooks/useTriggerWhenStatus.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAmB,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAG5F;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,KAAK,CAAC;YACZ,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;YAC3C,KAAK,EAAE,OAAO,CAAC;SAChB,CAAC,CAAC;QACH,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,GAAG,IAAI,CAAC;CACV;
|
|
1
|
+
{"version":3,"file":"useTriggerWhenStatus.d.ts","sourceRoot":"","sources":["../../src/hooks/useTriggerWhenStatus.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAmB,iBAAiB,EAAE,MAAM,mCAAmC,CAAC;AAG5F;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,CAAC,EAAE;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,KAAK,CAAC;YACZ,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;YAC3C,KAAK,EAAE,OAAO,CAAC;SAChB,CAAC,CAAC;QACH,OAAO,CAAC,EAAE,OAAO,CAAC;KACnB,GAAG,IAAI,CAAC;CACV;AAqED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,eAAe,EAAE,GACvB,GAAG,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC,CA0CvC"}
|
|
@@ -15,7 +15,6 @@ import { formatConditionLabel } from '../formatConditionLabel';
|
|
|
15
15
|
* Build a RuntimeLike from the real SynOS.handle.runtime.
|
|
16
16
|
*/
|
|
17
17
|
function getRuntime() {
|
|
18
|
-
// biome-ignore lint: window.SynOS is set by the runtime SDK
|
|
19
18
|
const rt = window.SynOS?.handle?.runtime;
|
|
20
19
|
if (!rt?.evaluateSync)
|
|
21
20
|
return null;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"test:watch": "vitest"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"css-selector-generator": "
|
|
28
|
+
"css-selector-generator": "3.8.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"lucide-react": ">=0.400.0",
|
|
@@ -33,13 +33,15 @@
|
|
|
33
33
|
"react-dom": ">=18.0.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
|
-
"@testing-library/dom": "
|
|
37
|
-
"@testing-library/react": "
|
|
38
|
-
"@types/react": "
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"react
|
|
42
|
-
"
|
|
43
|
-
"
|
|
36
|
+
"@testing-library/dom": "10.4.1",
|
|
37
|
+
"@testing-library/react": "16.3.2",
|
|
38
|
+
"@types/react": "19.2.14",
|
|
39
|
+
"@types/react-dom": "19.2.3",
|
|
40
|
+
"jsdom": "26.1.0",
|
|
41
|
+
"lucide-react": "0.576.0",
|
|
42
|
+
"react": "19.2.1",
|
|
43
|
+
"react-dom": "19.2.1",
|
|
44
|
+
"typescript": "5.9.3",
|
|
45
|
+
"vitest": "4.0.18"
|
|
44
46
|
}
|
|
45
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syntrologie/adapt-content",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Adaptive Content app - DOM manipulation actions for text, attributes, and styles",
|
|
5
5
|
"license": "Proprietary",
|
|
6
6
|
"private": false,
|
|
@@ -50,21 +50,21 @@
|
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@syntrologie/shared-editor-ui": "*",
|
|
53
|
-
"css-selector-generator": "
|
|
54
|
-
"data-urls": "
|
|
53
|
+
"css-selector-generator": "3.8.0",
|
|
54
|
+
"data-urls": "5.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@floating-ui/dom": "
|
|
57
|
+
"@floating-ui/dom": "1.7.5",
|
|
58
58
|
"@syntrologie/sdk-contracts": "*",
|
|
59
|
-
"@testing-library/react": "
|
|
60
|
-
"@types/react": "
|
|
61
|
-
"@types/react-dom": "
|
|
62
|
-
"jsdom": "
|
|
63
|
-
"lucide-react": "
|
|
64
|
-
"react": "
|
|
65
|
-
"react-dom": "
|
|
66
|
-
"typescript": "
|
|
67
|
-
"vitest": "
|
|
68
|
-
"zod": "
|
|
59
|
+
"@testing-library/react": "16.3.2",
|
|
60
|
+
"@types/react": "19.2.14",
|
|
61
|
+
"@types/react-dom": "19.2.3",
|
|
62
|
+
"jsdom": "26.1.0",
|
|
63
|
+
"lucide-react": "0.576.0",
|
|
64
|
+
"react": "19.2.1",
|
|
65
|
+
"react-dom": "19.2.1",
|
|
66
|
+
"typescript": "5.9.3",
|
|
67
|
+
"vitest": "4.0.18",
|
|
68
|
+
"zod": "3.25.76"
|
|
69
69
|
}
|
|
70
70
|
}
|