@syntrologie/adapt-overlays 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/WorkflowWidget.js +3 -3
  2. package/dist/cdn.d.ts +2 -2
  3. package/dist/celebrations/__tests__/reduced-motion.test.d.ts +2 -0
  4. package/dist/celebrations/__tests__/reduced-motion.test.d.ts.map +1 -0
  5. package/dist/celebrations/__tests__/reduced-motion.test.js +97 -0
  6. package/dist/celebrations/engine.d.ts.map +1 -1
  7. package/dist/celebrations/engine.js +4 -0
  8. package/dist/editor.d.ts +6 -6
  9. package/dist/editor.d.ts.map +1 -1
  10. package/dist/editor.js +6 -412
  11. package/dist/overlay-editor-state.d.ts +41 -0
  12. package/dist/overlay-editor-state.d.ts.map +1 -0
  13. package/dist/overlay-editor-state.js +131 -0
  14. package/dist/overlay-editor-ui.d.ts +9 -0
  15. package/dist/overlay-editor-ui.d.ts.map +1 -0
  16. package/dist/overlay-editor-ui.js +306 -0
  17. package/dist/runtime.d.ts +2 -2
  18. package/dist/runtime.js +1 -1
  19. package/dist/tour-types.d.ts +1 -1
  20. package/dist/tour-types.d.ts.map +1 -1
  21. package/node_modules/@syntro/design-system/dist/tailwind-preset.d.ts.map +1 -1
  22. package/node_modules/@syntro/design-system/dist/tailwind-preset.js +4 -2
  23. package/node_modules/@syntro/design-system/dist/tokens/colors.css +1 -1
  24. package/node_modules/@syntro/design-system/dist/tokens/colors.d.ts +2 -2
  25. package/node_modules/@syntro/design-system/dist/tokens/colors.js +1 -1
  26. package/node_modules/@syntro/design-system/dist/tokens/effects.d.ts +54 -0
  27. package/node_modules/@syntro/design-system/dist/tokens/effects.d.ts.map +1 -1
  28. package/node_modules/@syntro/design-system/dist/tokens/effects.js +44 -0
  29. package/node_modules/@syntro/design-system/package.json +2 -2
  30. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +2 -4
  31. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +2 -4
  32. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -1
  33. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.js +0 -1
  34. package/node_modules/@syntrologie/shared-editor-ui/package.json +11 -9
  35. package/package.json +12 -12
@@ -79,13 +79,13 @@ function showWorkflowToast(notification) {
79
79
  }
80
80
  /**
81
81
  * Extract workflow-enabled tours from active actions (runtime.actions.getActive()).
82
- * Only actions with kind 'core:tour', a tourId, and a workflow field are included.
82
+ * Only actions with kind 'overlays:tour', a tourId, and a workflow field are included.
83
83
  */
84
84
  function extractWorkflowsFromActive(activeActions) {
85
85
  const workflows = new Map();
86
86
  for (const entry of activeActions) {
87
87
  const action = entry.action;
88
- if (action.kind === 'core:tour' && action.workflow && action.tourId) {
88
+ if (action.kind === 'overlays:tour' && action.workflow && action.tourId) {
89
89
  const meta = action.workflow;
90
90
  const rawSteps = action.steps || [];
91
91
  const steps = rawSteps.map((s) => ({
@@ -101,10 +101,10 @@ export function WorkflowWidgetInner({ runtime }) {
101
101
  // Scan active actions for workflow-enabled tours.
102
102
  // Re-scans when actionVersion bumps (triggered by tour.started events).
103
103
  const [actionVersion, setActionVersion] = useState(0);
104
+ // biome-ignore lint/correctness/useExhaustiveDependencies: actionVersion is an intentional cache-buster that triggers re-scan when tour.started events fire
104
105
  const tourWorkflows = useMemo(() => {
105
106
  const active = runtime?.actions?.getActive?.() || [];
106
107
  return extractWorkflowsFromActive(active);
107
- // eslint-disable-next-line react-hooks/exhaustive-deps
108
108
  }, [runtime, actionVersion]);
109
109
  // Keep a ref so event handlers always see the latest tourWorkflows
110
110
  const tourWorkflowsRef = useRef(tourWorkflows);
package/dist/cdn.d.ts CHANGED
@@ -15,7 +15,7 @@ export declare const manifest: {
15
15
  description: string;
16
16
  runtime: {
17
17
  actions: {
18
- kind: "core:tour" | "overlays:celebrate" | "overlays:highlight" | "overlays:pulse" | "overlays:badge" | "overlays:tooltip" | "overlays:modal";
18
+ kind: "overlays:tour" | "overlays:celebrate" | "overlays:highlight" | "overlays:pulse" | "overlays:badge" | "overlays:tooltip" | "overlays:modal";
19
19
  executor: import("packages/sdk-contracts/dist").ActionExecutor<import("./types").CelebrateAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./tour-types").TourAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").ModalAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").HighlightAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").PulseAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").BadgeAction> | import("packages/sdk-contracts/dist").ActionExecutor<import("./types").TooltipAction>;
20
20
  }[];
21
21
  };
@@ -25,7 +25,7 @@ export declare const manifest: {
25
25
  icon: string;
26
26
  description: string;
27
27
  };
28
- component: typeof import("./editor").OverlaysEditor;
28
+ component: typeof import("./overlay-editor-ui").OverlaysEditor;
29
29
  };
30
30
  metadata: {
31
31
  isBuiltIn: boolean;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=reduced-motion.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reduced-motion.test.d.ts","sourceRoot":"","sources":["../../../src/celebrations/__tests__/reduced-motion.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,97 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ /** Stub CanvasRenderingContext2D for jsdom (which lacks canvas support) */
3
+ function stubCanvasContext() {
4
+ const ctx = {
5
+ scale: vi.fn(),
6
+ clearRect: vi.fn(),
7
+ save: vi.fn(),
8
+ restore: vi.fn(),
9
+ translate: vi.fn(),
10
+ rotate: vi.fn(),
11
+ fillRect: vi.fn(),
12
+ beginPath: vi.fn(),
13
+ arc: vi.fn(),
14
+ fill: vi.fn(),
15
+ fillText: vi.fn(),
16
+ globalAlpha: 1,
17
+ fillStyle: '',
18
+ font: '',
19
+ shadowBlur: 0,
20
+ shadowColor: '',
21
+ };
22
+ vi.spyOn(HTMLCanvasElement.prototype, 'getContext').mockReturnValue(ctx);
23
+ return ctx;
24
+ }
25
+ function mockEffect() {
26
+ return {
27
+ init: vi.fn(() => [
28
+ {
29
+ x: 100,
30
+ y: 0,
31
+ vx: 0,
32
+ vy: 1,
33
+ rotation: 0,
34
+ rotationSpeed: 0,
35
+ size: 5,
36
+ color: '#ff0000',
37
+ opacity: 1,
38
+ },
39
+ ]),
40
+ update: vi.fn(() => true),
41
+ render: vi.fn(),
42
+ };
43
+ }
44
+ const defaultConfig = {
45
+ duration: 3000,
46
+ intensity: 'medium',
47
+ colors: ['#ff0000', '#00ff00'],
48
+ };
49
+ describe('CelebrationEngine prefers-reduced-motion', () => {
50
+ let container;
51
+ beforeEach(() => {
52
+ container = document.createElement('div');
53
+ document.body.appendChild(container);
54
+ stubCanvasContext();
55
+ vi.useFakeTimers();
56
+ });
57
+ afterEach(() => {
58
+ vi.useRealTimers();
59
+ vi.restoreAllMocks();
60
+ container.remove();
61
+ });
62
+ it('should skip animation when prefers-reduced-motion is "reduce"', async () => {
63
+ window.matchMedia = vi.fn().mockReturnValue({ matches: true });
64
+ const { CelebrationEngine } = await import('../engine');
65
+ const engine = new CelebrationEngine();
66
+ const effect = mockEffect();
67
+ engine.start(container, effect, defaultConfig);
68
+ // Effect should never be initialized or rendered
69
+ expect(effect.init).not.toHaveBeenCalled();
70
+ expect(effect.render).not.toHaveBeenCalled();
71
+ // matchMedia should have been queried with the right media query
72
+ expect(window.matchMedia).toHaveBeenCalledWith('(prefers-reduced-motion: reduce)');
73
+ });
74
+ it('should run animation when prefers-reduced-motion is not set', async () => {
75
+ window.matchMedia = vi.fn().mockReturnValue({ matches: false });
76
+ const { CelebrationEngine } = await import('../engine');
77
+ const engine = new CelebrationEngine();
78
+ const effect = mockEffect();
79
+ engine.start(container, effect, defaultConfig);
80
+ // Effect should be initialized normally
81
+ expect(effect.init).toHaveBeenCalled();
82
+ // Canvas should be created
83
+ const canvas = container.querySelector('canvas[data-syntro-celebrate]');
84
+ expect(canvas).not.toBeNull();
85
+ engine.stop();
86
+ });
87
+ it('should not create canvas element when motion is reduced', async () => {
88
+ window.matchMedia = vi.fn().mockReturnValue({ matches: true });
89
+ const { CelebrationEngine } = await import('../engine');
90
+ const engine = new CelebrationEngine();
91
+ const effect = mockEffect();
92
+ engine.start(container, effect, defaultConfig);
93
+ // No canvas should exist in the container
94
+ const canvas = container.querySelector('canvas[data-syntro-celebrate]');
95
+ expect(canvas).toBeNull();
96
+ });
97
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/celebrations/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAY,MAAM,SAAS,CAAC;AAE9E,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,GAAG,CAAyC;IACpD,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,SAAS,CAA4B;IAE7C,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI;IA2CzF,IAAI,IAAI,IAAI;IAeZ,OAAO,CAAC,IAAI;CA+Bb"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/celebrations/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAY,MAAM,SAAS,CAAC;AAE9E,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,GAAG,CAAyC;IACpD,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,SAAS,CAAkB;IACnC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,SAAS,CAA4B;IAE7C,KAAK,CAAC,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,EAAE,iBAAiB,GAAG,IAAI;IAgDzF,IAAI,IAAI,IAAI;IAeZ,OAAO,CAAC,IAAI;CA+Bb"}
@@ -11,6 +11,10 @@ export class CelebrationEngine {
11
11
  this.container = null;
12
12
  }
13
13
  start(container, effect, config) {
14
+ const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
15
+ if (prefersReducedMotion) {
16
+ return;
17
+ }
14
18
  this.container = container;
15
19
  this.effect = effect;
16
20
  this.duration = config.duration;
package/dist/editor.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /**
2
- * Adaptive Overlays - Editor Component
2
+ * Adaptive Overlays - Editor Module (barrel)
3
3
  *
4
- * Review & tweak editor for AI-generated overlay configurations.
5
- * Displays a scannable list of one-liner summaries grouped by type.
6
- * Tours show a drill-in step list. Clicking an item highlights it on page.
4
+ * Re-exports from the split modules for backward compatibility.
5
+ * - State/helpers: overlay-editor-state.ts
6
+ * - UI component: overlay-editor-ui.tsx
7
7
  */
8
- import type { EditorPanelProps } from './types';
9
- export declare function OverlaysEditor({ config, onChange, editor }: EditorPanelProps): import("react/jsx-runtime").JSX.Element;
8
+ import { OverlaysEditor } from './overlay-editor-ui';
9
+ export { OverlaysEditor };
10
10
  /**
11
11
  * Editor module configuration.
12
12
  */
@@ -1 +1 @@
1
- {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH,OAAO,KAAK,EAAE,gBAAgB,EAAY,MAAM,SAAS,CAAC;AA0N1D,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,gBAAgB,2CAif5E;AAED;;GAEG;AACH,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC;AAEF,eAAO,MAAM,WAAW;;;;CAAe,CAAC;AAExC,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAAE,cAAc,EAAE,CAAC;AAE1B;;GAEG;AACH,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAC;AAEF,eAAO,MAAM,WAAW;;;;CAAe,CAAC;AAExC,eAAe,cAAc,CAAC"}
package/dist/editor.js CHANGED
@@ -1,418 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
1
  /**
3
- * Adaptive Overlays - Editor Component
2
+ * Adaptive Overlays - Editor Module (barrel)
4
3
  *
5
- * Review & tweak editor for AI-generated overlay configurations.
6
- * Displays a scannable list of one-liner summaries grouped by type.
7
- * Tours show a drill-in step list. Clicking an item highlights it on page.
4
+ * Re-exports from the split modules for backward compatibility.
5
+ * - State/helpers: overlay-editor-state.ts
6
+ * - UI component: overlay-editor-ui.tsx
8
7
  */
9
- import { DetectionBadge, DismissedSection, EditorBody, EditorCard, EditorFooter, EditorHeader, EditorInput, EditorLayout, EditorTextarea, EmptyState, GroupHeader, } from '@syntrologie/shared-editor-ui';
10
- import { MessageSquare, Route, Sparkles, Square, Tag, Zap } from 'lucide-react';
11
- import { useCallback, useEffect, useRef, useState } from 'react';
12
- import { summarizeOverlayItem } from './summarize';
13
- /** Extract the CSS selector string from an anchorId object. */
14
- 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
- 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
- function savePendingHighlight(selector) {
38
- try {
39
- sessionStorage.setItem('syntro:editor:pending-highlight', selector);
40
- }
41
- catch {
42
- // Silently ignore
43
- }
44
- }
45
- function itemKey(section, index) {
46
- return `${section}:${index}`;
47
- }
48
- // ============================================================================
49
- // Section Config
50
- // ============================================================================
51
- const OVERLAY_SECTIONS = ['tooltips', 'highlights', 'badges', 'pulses', 'modals'];
52
- const SECTION_ICON_MAP = {
53
- tooltips: MessageSquare,
54
- highlights: Sparkles,
55
- badges: Tag,
56
- pulses: Zap,
57
- modals: Square,
58
- tours: Route,
59
- };
60
- /** Renders the appropriate Lucide icon for a section type */
61
- function SectionIcon({ section, className }) {
62
- const IconComponent = SECTION_ICON_MAP[section];
63
- return _jsx(IconComponent, { size: 16, className: className });
64
- }
65
- function flattenItems(config) {
66
- const items = [];
67
- for (const section of OVERLAY_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: summarizeOverlayItem(section, rec),
76
- anchorId: resolveAnchorSelector(rec.anchorId),
77
- rawAnchorId: rec.anchorId,
78
- isTour: false,
79
- });
80
- });
81
- }
82
- // Tours
83
- const tours = config.tours || [];
84
- tours.forEach((tour, i) => {
85
- items.push({
86
- key: itemKey('tours', i),
87
- section: 'tours',
88
- index: i,
89
- summary: summarizeOverlayItem('tours', tour),
90
- anchorId: '',
91
- rawAnchorId: undefined,
92
- isTour: true,
93
- });
94
- });
95
- return items;
96
- }
97
- function filterConfig(config, dismissedKeys) {
98
- const result = { ...config };
99
- const allSections = [...OVERLAY_SECTIONS, 'tours'];
100
- for (const section of allSections) {
101
- const arr = config[section] || [];
102
- const filtered = arr.filter((_, i) => !dismissedKeys.has(itemKey(section, i)));
103
- if (filtered.length > 0 || config[section] !== undefined) {
104
- result[section] = filtered;
105
- }
106
- }
107
- return result;
108
- }
109
- function getStepIcon(step) {
110
- const action = step.action;
111
- const kind = action.kind || '';
112
- if (kind.includes('tooltip'))
113
- return '\u{1f4ac}';
114
- if (kind.includes('highlight'))
115
- return '\u{2728}';
116
- if (kind.includes('modal'))
117
- return '\u{1f4e6}';
118
- if (kind.includes('badge'))
119
- return '\u{1f3f7}\u{fe0f}';
120
- if (kind.includes('pulse'))
121
- return '\u{1f4ab}';
122
- return '\u{25cf}';
123
- }
124
- function getStepLabel(step) {
125
- const action = step.action;
126
- const anchor = resolveAnchorSelector(action.anchorId);
127
- if (anchor)
128
- return anchor;
129
- const content = action.content;
130
- if (content?.title)
131
- return content.title;
132
- if (content?.body)
133
- return content.body.slice(0, 30);
134
- return step.id;
135
- }
136
- function useAnchorDetection(items, config) {
137
- const [detectionMap, setDetectionMap] = useState(new Map());
138
- const itemsRef = useRef(items);
139
- const configRef = useRef(config);
140
- itemsRef.current = items;
141
- configRef.current = config;
142
- useEffect(() => {
143
- const runDetection = () => {
144
- const map = new Map();
145
- for (const item of itemsRef.current) {
146
- let selectorToCheck = item.anchorId;
147
- // For tours, detect the first step's anchor
148
- if (item.isTour && !selectorToCheck) {
149
- const tours = configRef.current.tours || [];
150
- const tour = tours[item.index];
151
- if (tour && tour.steps.length > 0) {
152
- const firstStep = tour.steps[0];
153
- const action = firstStep.action;
154
- selectorToCheck = resolveAnchorSelector(action?.anchorId);
155
- }
156
- }
157
- if (!selectorToCheck) {
158
- map.set(item.key, { found: false, element: null });
159
- continue;
160
- }
161
- try {
162
- const el = document.querySelector(selectorToCheck);
163
- map.set(item.key, { found: el !== null, element: el });
164
- }
165
- catch {
166
- map.set(item.key, { found: false, element: null });
167
- }
168
- }
169
- setDetectionMap(map);
170
- };
171
- runDetection();
172
- const interval = setInterval(runDetection, 2000);
173
- return () => clearInterval(interval);
174
- }, []);
175
- return detectionMap;
176
- }
177
- // ============================================================================
178
- // OverlaysEditor Component
179
- // ============================================================================
180
- function parseOverlayItemKey(key) {
181
- const [section, indexStr] = key.split(':');
182
- return { section: section, index: Number(indexStr) };
183
- }
184
- export function OverlaysEditor({ config, onChange, editor }) {
185
- const typedConfig = config;
186
- const [dismissedKeys, setDismissedKeys] = useState(() => editor.getDismissedKeys?.() ?? new Set());
187
- const [expandedTour, setExpandedTour] = useState(null);
188
- const [editingKey, setEditingKey] = useState(null);
189
- const [_previewMode, setPreviewMode] = useState('after');
190
- // Sync dismissed keys back to navigation context on every change
191
- useEffect(() => {
192
- editor.setDismissedKeys?.(dismissedKeys);
193
- }, [dismissedKeys, editor]);
194
- // React to global before/after toggle from the panel
195
- // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally omitted — adding config/typedConfig/previewConfig would cause infinite re-renders since previewConfig triggers state updates
196
- useEffect(() => {
197
- const mode = editor.previewMode;
198
- if (!mode)
199
- return;
200
- if (mode === 'before') {
201
- // Remove all overlay changes — push a config with every item filtered out
202
- const allKeys = new Set(flattenItems(typedConfig).map((item) => item.key));
203
- const empty = filterConfig(typedConfig, allKeys);
204
- editor.previewConfig(empty);
205
- }
206
- else {
207
- // Restore the full config
208
- editor.previewConfig(config);
209
- }
210
- }, [editor.previewMode]);
211
- // Consume initialEditKey from accordion navigation on mount
212
- const initialConsumed = useRef(false);
213
- useEffect(() => {
214
- if (editor.initialEditKey != null && !initialConsumed.current) {
215
- initialConsumed.current = true;
216
- const allFlat = flattenItems(typedConfig);
217
- const targetIdx = Number(editor.initialEditKey);
218
- if (targetIdx >= 0 && targetIdx < allFlat.length) {
219
- const target = allFlat[targetIdx];
220
- if (target.isTour) {
221
- setExpandedTour(target.key);
222
- }
223
- else {
224
- setEditingKey(target.key);
225
- if (target.anchorId) {
226
- editor.highlightElement(target.anchorId);
227
- }
228
- }
229
- }
230
- editor.clearInitialState?.();
231
- }
232
- else if (editor.initialCreate && !initialConsumed.current) {
233
- initialConsumed.current = true;
234
- editor.clearInitialState?.();
235
- }
236
- }, [editor, typedConfig]);
237
- const allItems = flattenItems(typedConfig);
238
- const activeItems = allItems.filter((item) => !dismissedKeys.has(item.key));
239
- const dismissedItems = allItems.filter((item) => dismissedKeys.has(item.key));
240
- const overlayItems = activeItems.filter((item) => !item.isTour);
241
- const tourItems = activeItems.filter((item) => item.isTour);
242
- const totalItems = activeItems.length;
243
- const [_hoveredKey, setHoveredKey] = useState(null);
244
- const detectionMap = useAnchorDetection(allItems, typedConfig);
245
- const foundCount = activeItems.filter((item) => detectionMap.get(item.key)?.found).length;
246
- const handleDismiss = useCallback((key) => {
247
- setDismissedKeys((prev) => {
248
- const next = new Set(prev);
249
- next.add(key);
250
- return next;
251
- });
252
- if (expandedTour === key)
253
- setExpandedTour(null);
254
- if (editingKey === key)
255
- setEditingKey(null);
256
- }, [expandedTour, editingKey]);
257
- const handleRestore = useCallback((key) => {
258
- setDismissedKeys((prev) => {
259
- const next = new Set(prev);
260
- next.delete(key);
261
- return next;
262
- });
263
- }, []);
264
- const handleCardClick = useCallback((item) => {
265
- if (item.isTour) {
266
- setExpandedTour((prev) => (prev === item.key ? null : item.key));
267
- }
268
- else {
269
- if (item.anchorId) {
270
- editor.highlightElement(item.anchorId);
271
- }
272
- setEditingKey(item.key);
273
- }
274
- }, [editor]);
275
- const handleBackToList = useCallback(() => {
276
- setEditingKey(null);
277
- setPreviewMode('after');
278
- editor.previewConfig(config);
279
- editor.clearHighlight();
280
- }, [editor, config]);
281
- // Register back handler in panel header when editing
282
- useEffect(() => {
283
- editor.setBackHandler?.(editingKey !== null ? handleBackToList : null);
284
- return () => editor.setBackHandler?.(null);
285
- }, [editingKey, handleBackToList, editor]);
286
- const _handleBeforeAfter = useCallback((mode) => {
287
- setPreviewMode(mode);
288
- if (mode === 'before') {
289
- const filtered = filterConfig(typedConfig, new Set([editingKey]));
290
- editor.previewConfig(filtered);
291
- }
292
- else {
293
- editor.previewConfig(config);
294
- }
295
- }, [typedConfig, editingKey, editor, config]);
296
- const handleFieldChange = useCallback((section, index, updater) => {
297
- const arr = (typedConfig[section] || []).slice();
298
- const item = { ...arr[index] };
299
- arr[index] = updater(item);
300
- const updated = { ...typedConfig, [section]: arr };
301
- onChange(updated);
302
- editor.setDirty(true);
303
- }, [typedConfig, onChange, editor]);
304
- const handlePublish = useCallback(() => {
305
- if (dismissedKeys.size > 0) {
306
- const filtered = filterConfig(typedConfig, dismissedKeys);
307
- onChange(filtered);
308
- }
309
- editor.publish();
310
- }, [dismissedKeys, typedConfig, onChange, editor]);
311
- const handleBadgeClick = useCallback(async (item) => {
312
- const detection = detectionMap.get(item.key);
313
- if (detection?.found && item.anchorId) {
314
- editor.highlightElement(item.anchorId);
315
- }
316
- else {
317
- const route = resolveAnchorRoute(item.rawAnchorId);
318
- if (route) {
319
- if (item.anchorId)
320
- savePendingHighlight(item.anchorId);
321
- await editor.navigateTo(route);
322
- if (item.anchorId)
323
- editor.highlightElement(item.anchorId);
324
- }
325
- else if (item.anchorId) {
326
- editor.highlightElement(item.anchorId);
327
- }
328
- }
329
- }, [editor, detectionMap]);
330
- const handleCardHover = useCallback((item) => {
331
- setHoveredKey(item.key);
332
- if (item.anchorId) {
333
- editor.highlightElement(item.anchorId);
334
- }
335
- }, [editor]);
336
- const handleCardLeave = useCallback(() => {
337
- setHoveredKey(null);
338
- editor.clearHighlight();
339
- }, [editor]);
340
- // ---- Edit form renderers per overlay type ----
341
- const renderEditFields = (section, index) => {
342
- const arr = typedConfig[section] || [];
343
- const item = arr[index];
344
- if (!item)
345
- return null;
346
- const anchorId = resolveAnchorSelector(item.anchorId);
347
- switch (section) {
348
- case 'tooltips': {
349
- const content = item.content || {};
350
- return (_jsxs("div", { className: "se-py-1", children: [_jsx("div", { className: "se-text-[11px] se-font-mono se-text-slate-grey-8 se-py-1 se-px-2 se-bg-white/[0.04] se-rounded se-mb-3", children: anchorId }), _jsx(EditorInput, { label: "Title", value: content.title || '', onChange: (e) => handleFieldChange(section, index, (it) => ({
351
- ...it,
352
- content: { ...it.content, title: e.target.value },
353
- })) }), _jsx(EditorTextarea, { label: "Body", value: content.body || '', onChange: (e) => handleFieldChange(section, index, (it) => ({
354
- ...it,
355
- content: { ...it.content, body: e.target.value },
356
- })) })] }));
357
- }
358
- case 'highlights':
359
- return (_jsxs("div", { className: "se-py-1", children: [_jsx("div", { className: "se-text-[11px] se-font-mono se-text-slate-grey-8 se-py-1 se-px-2 se-bg-white/[0.04] se-rounded se-mb-3", children: anchorId }), _jsx(EditorInput, { label: "Color", value: item.style?.color || '', onChange: (e) => handleFieldChange(section, index, (it) => ({
360
- ...it,
361
- style: {
362
- ...(it.style || {}),
363
- color: e.target.value,
364
- },
365
- })) })] }));
366
- case 'badges':
367
- return (_jsxs("div", { className: "se-py-1", children: [_jsx("div", { className: "se-text-[11px] se-font-mono se-text-slate-grey-8 se-py-1 se-px-2 se-bg-white/[0.04] se-rounded se-mb-3", children: anchorId }), _jsx(EditorInput, { label: "Content", value: item.content || '', onChange: (e) => handleFieldChange(section, index, (it) => ({
368
- ...it,
369
- content: e.target.value,
370
- })) })] }));
371
- case 'pulses':
372
- return (_jsxs("div", { className: "se-py-1", children: [_jsx("div", { className: "se-text-[11px] se-font-mono se-text-slate-grey-8 se-py-1 se-px-2 se-bg-white/[0.04] se-rounded se-mb-3", children: anchorId }), _jsx(EditorInput, { label: "Duration (ms)", type: "number", value: item.duration || '', onChange: (e) => handleFieldChange(section, index, (it) => ({
373
- ...it,
374
- duration: Number(e.target.value) || undefined,
375
- })) })] }));
376
- case 'modals': {
377
- const content = item.content || {};
378
- return (_jsxs("div", { className: "se-py-1", children: [_jsx(EditorInput, { label: "Title", value: content.title || '', onChange: (e) => handleFieldChange(section, index, (it) => ({
379
- ...it,
380
- content: { ...it.content, title: e.target.value },
381
- })) }), _jsx(EditorTextarea, { label: "Body", value: content.body || '', onChange: (e) => handleFieldChange(section, index, (it) => ({
382
- ...it,
383
- content: { ...it.content, body: e.target.value },
384
- })) })] }));
385
- }
386
- default:
387
- return null;
388
- }
389
- };
390
- const renderTourDrillIn = (tourIdx) => {
391
- const tours = typedConfig.tours || [];
392
- const tour = tours[tourIdx];
393
- if (!tour)
394
- return null;
395
- return (_jsxs("div", { className: "se-p-3 se-rounded-lg se-border se-border-white/[0.08] se-bg-white/[0.02] se-mt-1 se-mb-2", children: [_jsxs("div", { className: "se-text-[13px] se-font-semibold se-text-slate-grey-10 se-mb-2", children: ['\u{1f3af}', " Tour: ", tour.tourId] }), _jsxs("label", { className: "se-flex se-items-center se-gap-2 se-text-xs se-text-[#d1d5db] se-mb-2", children: [_jsx("input", { type: "checkbox", checked: tour.autoStart || false, readOnly: true }), "Auto-start tour"] }), tour.steps.map((step, stepIdx) => (_jsxs("div", { className: "se-flex se-items-center se-gap-2 se-py-1.5 se-px-2 se-rounded se-border se-border-white/[0.04] se-mb-1 se-text-xs se-text-[#d1d5db]", children: [_jsxs("span", { className: "se-text-[11px] se-font-bold se-text-slate-grey-7 se-min-w-[18px]", children: [stepIdx + 1, "."] }), _jsx("span", { children: getStepIcon(step) }), _jsxs("div", { className: "se-flex-1 se-overflow-hidden", children: [_jsx("div", { children: getStepLabel(step) }), step.route && (_jsx("div", { className: "se-text-[10px] se-text-slate-grey-7 se-font-mono", children: step.route }))] })] }, step.id || stepIdx))), tour.steps.length === 0 && (_jsx("div", { className: "se-text-xs se-text-slate-grey-7 se-py-2", children: "No steps in this tour." })), _jsx("button", { type: "button", className: "se-py-1 se-px-2.5 se-rounded se-border se-border-white/10 se-bg-transparent se-text-slate-grey-8 se-text-[11px] se-cursor-pointer se-mt-2", onClick: () => setExpandedTour(null), children: "\u2190 Back to list" })] }));
396
- };
397
- const renderCard = (item) => {
398
- const detection = detectionMap.get(item.key);
399
- return (_jsxs("div", { children: [_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", onClick: (e) => {
400
- e.stopPropagation();
401
- handleCardClick(item);
402
- }, children: _jsx(SectionIcon, { section: item.section }) }), _jsx("span", { className: "se-flex-1 se-overflow-hidden se-text-ellipsis se-whitespace-nowrap", onClick: () => handleCardClick(item), 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) => {
403
- e.stopPropagation();
404
- handleDismiss(item.key);
405
- }, title: "Dismiss", children: "\u00D7" })] }), item.isTour && expandedTour === item.key && renderTourDrillIn(item.index)] }, item.key));
406
- };
407
- return (_jsxs(EditorLayout, { children: [_jsx(EditorHeader, { title: "Review Changes", subtitle: `${totalItems} item${totalItems !== 1 ? 's' : ''}${totalItems > 0 ? ` (${foundCount} found on this page)` : ''}`, onBack: () => editor.navigateHome() }), _jsx(EditorBody, { children: editingKey !== null ? ((() => {
408
- const ref = parseOverlayItemKey(editingKey);
409
- const editItem = allItems.find((it) => it.key === editingKey);
410
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "se-flex se-items-center se-gap-2 se-mb-3 se-text-[13px] se-font-semibold se-text-slate-grey-10", children: [_jsx("span", { children: editItem && _jsx(SectionIcon, { section: editItem.section }) }), _jsx("span", { children: editItem?.summary })] }), renderEditFields(ref.section, ref.index)] }));
411
- })()) : (_jsxs(_Fragment, { children: [allItems.length === 0 && _jsx(EmptyState, { message: "No overlays configured." }), overlayItems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(GroupHeader, { label: "OVERLAYS", count: overlayItems.length }), overlayItems.map(renderCard)] })), tourItems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(GroupHeader, { label: "TOURS", count: tourItems.length, className: overlayItems.length > 0 ? 'se-mt-4' : '' }), tourItems.map(renderCard)] })), 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-md se-border se-border-white/[0.03] se-bg-transparent se-mb-0.5 se-cursor-pointer se-text-xs se-text-slate-grey-6 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) => {
412
- e.stopPropagation();
413
- handleRestore(item.key);
414
- }, children: "Restore" })] }, item.key))) }))] })) }), _jsx(EditorFooter, { onSave: () => editor.save(), onPublish: handlePublish })] }));
415
- }
8
+ import { OverlaysEditor } from './overlay-editor-ui';
9
+ export { OverlaysEditor };
416
10
  /**
417
11
  * Editor module configuration.
418
12
  */