@syntrologie/adapt-overlays 2.11.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.
- package/dist/WorkflowWidget.js +3 -3
- package/dist/cdn.d.ts +2 -2
- package/dist/celebrations/__tests__/reduced-motion.test.d.ts +2 -0
- package/dist/celebrations/__tests__/reduced-motion.test.d.ts.map +1 -0
- package/dist/celebrations/__tests__/reduced-motion.test.js +97 -0
- package/dist/celebrations/engine.d.ts.map +1 -1
- package/dist/celebrations/engine.js +4 -0
- package/dist/editor.d.ts +6 -6
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +6 -412
- package/dist/overlay-editor-state.d.ts +41 -0
- package/dist/overlay-editor-state.d.ts.map +1 -0
- package/dist/overlay-editor-state.js +131 -0
- package/dist/overlay-editor-ui.d.ts +9 -0
- package/dist/overlay-editor-ui.d.ts.map +1 -0
- package/dist/overlay-editor-ui.js +306 -0
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +1 -1
- package/dist/tour-types.d.ts +1 -1
- package/dist/tour-types.d.ts.map +1 -1
- package/node_modules/@syntro/design-system/dist/tailwind-preset.d.ts.map +1 -1
- package/node_modules/@syntro/design-system/dist/tailwind-preset.js +4 -2
- package/node_modules/@syntro/design-system/dist/tokens/colors.css +1 -1
- package/node_modules/@syntro/design-system/dist/tokens/colors.d.ts +2 -2
- package/node_modules/@syntro/design-system/dist/tokens/colors.js +1 -1
- package/node_modules/@syntro/design-system/dist/tokens/effects.d.ts +54 -0
- package/node_modules/@syntro/design-system/dist/tokens/effects.d.ts.map +1 -1
- package/node_modules/@syntro/design-system/dist/tokens/effects.js +44 -0
- package/node_modules/@syntro/design-system/package.json +2 -2
- 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 +12 -12
package/dist/WorkflowWidget.js
CHANGED
|
@@ -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 '
|
|
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 === '
|
|
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: "
|
|
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 @@
|
|
|
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;
|
|
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
|
|
2
|
+
* Adaptive Overlays - Editor Module (barrel)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
9
|
-
export
|
|
8
|
+
import { OverlaysEditor } from './overlay-editor-ui';
|
|
9
|
+
export { OverlaysEditor };
|
|
10
10
|
/**
|
|
11
11
|
* Editor module configuration.
|
|
12
12
|
*/
|
package/dist/editor.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/editor.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
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
|
|
2
|
+
* Adaptive Overlays - Editor Module (barrel)
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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 {
|
|
10
|
-
|
|
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
|
*/
|