@syntrologie/adapt-content 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/editor.d.ts.map +1 -1
  2. package/dist/editor.js +59 -3
  3. package/dist/runtime.d.ts.map +1 -1
  4. package/dist/runtime.js +67 -12
  5. package/dist/summarize.d.ts.map +1 -1
  6. package/dist/summarize.js +12 -1
  7. package/dist/types.d.ts +9 -42
  8. package/dist/types.d.ts.map +1 -1
  9. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts +2 -0
  10. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts.map +1 -0
  11. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.js +224 -0
  12. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +102 -0
  13. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.js +58 -6
  14. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +18 -0
  15. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +61 -2
  16. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +478 -7
  17. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +54 -0
  18. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts +2 -0
  19. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts.map +1 -0
  20. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.js +257 -0
  21. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts +2 -0
  22. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts.map +1 -0
  23. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.js +1015 -0
  24. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +1 -1
  25. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +4 -4
  26. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +1 -1
  27. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +2 -2
  28. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +2 -1
  29. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +1 -1
  30. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +20 -3
  31. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +10 -8
  32. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +1 -1
  33. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +350 -87
  34. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +1 -1
  35. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +3 -3
  36. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +1 -1
  37. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +1 -1
  38. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +1 -1
  39. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +1 -1
  40. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +5 -2
  41. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +24 -0
  42. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -0
  43. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/{useShowWhenStatus.js → useTriggerWhenStatus.js} +18 -15
  44. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +3 -3
  45. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +1 -1
  46. package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +1 -1
  47. package/package.json +4 -5
  48. package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +0 -26
  49. package/node_modules/@syntrologie/sdk-contracts/dist/index.js +0 -13
  50. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +0 -1428
  51. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +0 -142
  52. package/node_modules/@syntrologie/sdk-contracts/package.json +0 -33
  53. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts +0 -24
  54. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts.map +0 -1
@@ -1,8 +1,19 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render } from '@testing-library/react';
3
- import { describe, expect, it, vi } from 'vitest';
2
+ import { act, fireEvent, render } from '@testing-library/react';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { EditorPanelShell } from '../components/EditorPanelShell';
5
- // Mock ResizeObserver and pointer capture for jsdom
5
+ // jsdom does not support PointerEvent; polyfill it so fireEvent creates events
6
+ // with clientX/clientY/pointerId correctly.
7
+ if (typeof globalThis.PointerEvent === 'undefined') {
8
+ class PointerEventPolyfill extends MouseEvent {
9
+ constructor(type, init = {}) {
10
+ super(type, init);
11
+ this.pointerId = init.pointerId ?? 0;
12
+ this.pointerType = init.pointerType ?? '';
13
+ }
14
+ }
15
+ vi.stubGlobal('PointerEvent', PointerEventPolyfill);
16
+ }
6
17
  vi.stubGlobal('ResizeObserver', class {
7
18
  observe() { }
8
19
  unobserve() { }
@@ -10,16 +21,476 @@ vi.stubGlobal('ResizeObserver', class {
10
21
  });
11
22
  Element.prototype.setPointerCapture = vi.fn();
12
23
  Element.prototype.releasePointerCapture = vi.fn();
24
+ // Node 25+ exposes a native `localStorage` that shadows jsdom's when
25
+ // `--localstorage-file` is absent. Stub with a proper in-memory Storage.
26
+ const storageMap = new Map();
27
+ vi.stubGlobal('localStorage', {
28
+ getItem: (key) => storageMap.get(key) ?? null,
29
+ setItem: (key, value) => storageMap.set(key, value),
30
+ removeItem: (key) => storageMap.delete(key),
31
+ clear: () => storageMap.clear(),
32
+ get length() {
33
+ return storageMap.size;
34
+ },
35
+ key: (index) => [...storageMap.keys()][index] ?? null,
36
+ });
13
37
  describe('EditorPanelShell', () => {
14
- it('applies antialiased font smoothing to the panel', () => {
15
- const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, position: "right", children: _jsx("div", { children: "Content" }) }));
38
+ beforeEach(() => {
39
+ localStorage.clear();
40
+ // Set default viewport dimensions
41
+ Object.defineProperty(window, 'innerWidth', { value: 1280, writable: true });
42
+ Object.defineProperty(window, 'innerHeight', { value: 800, writable: true });
43
+ });
44
+ it('renders panel docked left by default', () => {
45
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
16
46
  const panel = container.querySelector('[data-syntro-editor-panel]');
17
47
  expect(panel).toBeTruthy();
18
- expect(panel.className).toContain('se-antialiased');
48
+ expect(panel.style.left).toBe('0px');
49
+ expect(panel.style.top).toBe('0px');
50
+ });
51
+ it('renders panel with minimum width of 480px', () => {
52
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
53
+ const panel = container.querySelector('[data-syntro-editor-panel]');
54
+ expect(panel.style.width).toBe('480px');
55
+ });
56
+ it('renders FAB when panel is open', () => {
57
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
58
+ const fab = container.querySelector('[data-syntro-fab]');
59
+ expect(fab).toBeTruthy();
19
60
  });
20
- it('does not render panel when closed', () => {
61
+ it('renders FAB when panel is closed (minimized)', () => {
21
62
  const { container } = render(_jsx(EditorPanelShell, { isOpen: false, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
63
+ const fab = container.querySelector('[data-syntro-fab]');
64
+ expect(fab).toBeTruthy();
22
65
  const panel = container.querySelector('[data-syntro-editor-panel]');
23
66
  expect(panel).toBeNull();
24
67
  });
68
+ it('renders 8 resize handles when panel is open', () => {
69
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
70
+ const handles = container.querySelectorAll('[data-syntro-editor-ui^="resize-"]');
71
+ expect(handles.length).toBe(8);
72
+ });
73
+ it('does not render resize handles when closed', () => {
74
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: false, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
75
+ const handles = container.querySelectorAll('[data-syntro-editor-ui^="resize-"]');
76
+ expect(handles.length).toBe(0);
77
+ });
78
+ it('saves geometry to localStorage on mount', () => {
79
+ render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
80
+ const stored = localStorage.getItem('syntro:editor-panel');
81
+ expect(stored).toBeTruthy();
82
+ const geo = JSON.parse(stored);
83
+ expect(geo.width).toBe(480);
84
+ expect(geo.docked).toBe('left');
85
+ });
86
+ it('loads geometry from localStorage', () => {
87
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 600, height: 500, docked: null }));
88
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
89
+ const panel = container.querySelector('[data-syntro-editor-panel]');
90
+ expect(panel.style.left).toBe('100px');
91
+ expect(panel.style.top).toBe('50px');
92
+ expect(panel.style.width).toBe('600px');
93
+ });
94
+ it('applies antialiased font smoothing', () => {
95
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
96
+ const panel = container.querySelector('[data-syntro-editor-panel]');
97
+ expect(panel.className).toContain('se-antialiased');
98
+ });
99
+ it('applies rounded corners when undocked', () => {
100
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 600, height: 500, docked: null }));
101
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
102
+ const panel = container.querySelector('[data-syntro-editor-panel]');
103
+ expect(panel.className).toContain('se-rounded-lg');
104
+ });
105
+ it('applies edge border when docked left', () => {
106
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
107
+ const panel = container.querySelector('[data-syntro-editor-panel]');
108
+ expect(panel.className).toContain('se-border-r');
109
+ });
110
+ it('uses namespaced localStorage key when panelId is provided', () => {
111
+ render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, panelId: "my-panel", children: _jsx("div", { children: "Content" }) }));
112
+ const stored = localStorage.getItem('syntro:editor-panel:my-panel');
113
+ expect(stored).toBeTruthy();
114
+ const geo = JSON.parse(stored);
115
+ expect(geo.width).toBe(480);
116
+ expect(geo.docked).toBe('left');
117
+ });
118
+ it('loads geometry from namespaced localStorage key when panelId is provided', () => {
119
+ localStorage.setItem('syntro:editor-panel:my-panel', JSON.stringify({ x: 200, y: 75, width: 700, height: 550, docked: null }));
120
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, panelId: "my-panel", children: _jsx("div", { children: "Content" }) }));
121
+ const panel = container.querySelector('[data-syntro-editor-panel]');
122
+ expect(panel.style.left).toBe('200px');
123
+ expect(panel.style.top).toBe('75px');
124
+ expect(panel.style.width).toBe('700px');
125
+ });
126
+ it('does not use default key when panelId is provided', () => {
127
+ render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, panelId: "my-panel", children: _jsx("div", { children: "Content" }) }));
128
+ const defaultStored = localStorage.getItem('syntro:editor-panel');
129
+ expect(defaultStored).toBeNull();
130
+ });
131
+ it('FAB zIndex matches panel zIndex prop', () => {
132
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, zIndex: 10000, children: _jsx("div", { children: "Content" }) }));
133
+ const fab = container.querySelector('[data-syntro-fab]');
134
+ const panel = container.querySelector('[data-syntro-editor-panel]');
135
+ expect(fab.style.zIndex).toBe('10000');
136
+ expect(panel.style.zIndex).toBe('10000');
137
+ });
138
+ it('resize handles have data-resize-dir attributes', () => {
139
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
140
+ const handles = container.querySelectorAll('[data-resize-dir]');
141
+ expect(handles.length).toBe(8);
142
+ const dirs = Array.from(handles).map((h) => h.dataset.resizeDir);
143
+ expect(dirs).toContain('n');
144
+ expect(dirs).toContain('se');
145
+ expect(dirs).toContain('nw');
146
+ });
147
+ it('renders docked right with left border and correct position', () => {
148
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 0, y: 0, width: 500, height: 800, docked: 'right' }));
149
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
150
+ const panel = container.querySelector('[data-syntro-editor-panel]');
151
+ expect(panel.className).toContain('se-border-l');
152
+ expect(panel.style.right).toBe('0px');
153
+ });
154
+ it('gracefully handles corrupt localStorage data', () => {
155
+ localStorage.setItem('syntro:editor-panel', 'not-valid-json');
156
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
157
+ const panel = container.querySelector('[data-syntro-editor-panel]');
158
+ // Falls back to default geometry
159
+ expect(panel).toBeTruthy();
160
+ expect(panel.style.width).toBe('480px');
161
+ });
162
+ it('enforces minimum width when localStorage has smaller value', () => {
163
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 0, y: 0, width: 100, height: 200, docked: null }));
164
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
165
+ const panel = container.querySelector('[data-syntro-editor-panel]');
166
+ // Width should be clamped to MIN_WIDTH (480)
167
+ expect(panel.style.width).toBe('480px');
168
+ });
169
+ it('enforces minimum height when localStorage has smaller value', () => {
170
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 0, y: 0, width: 600, height: 100, docked: null }));
171
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
172
+ const panel = container.querySelector('[data-syntro-editor-panel]');
173
+ // Height should be clamped to MIN_HEIGHT (400)
174
+ expect(panel.style.height).toBe('400px');
175
+ });
176
+ it('handles localStorage data with missing fields by using defaults', () => {
177
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ docked: null }));
178
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
179
+ const panel = container.querySelector('[data-syntro-editor-panel]');
180
+ expect(panel).toBeTruthy();
181
+ // x/y should default to 0, width to MIN_WIDTH, height to max(MIN_HEIGHT, innerHeight)
182
+ expect(panel.style.left).toBe('0px');
183
+ expect(panel.style.top).toBe('0px');
184
+ expect(panel.style.width).toBe('480px');
185
+ });
186
+ describe('FAB drag handlers', () => {
187
+ it('calls onToggle on FAB click (pointerDown + pointerUp without move)', () => {
188
+ const onToggle = vi.fn();
189
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: onToggle, children: _jsx("div", { children: "Content" }) }));
190
+ const fab = container.querySelector('[data-syntro-fab]');
191
+ fireEvent.pointerDown(fab, { clientX: 100, clientY: 100, pointerId: 1 });
192
+ fireEvent.pointerUp(fab, { clientX: 100, clientY: 100, pointerId: 1 });
193
+ expect(onToggle).toHaveBeenCalledTimes(1);
194
+ });
195
+ it('does not call onToggle when FAB is dragged beyond threshold', () => {
196
+ const onToggle = vi.fn();
197
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: onToggle, children: _jsx("div", { children: "Content" }) }));
198
+ const fab = container.querySelector('[data-syntro-fab]');
199
+ fireEvent.pointerDown(fab, { clientX: 100, clientY: 100, pointerId: 1 });
200
+ // Move beyond DRAG_THRESHOLD (5px)
201
+ fireEvent.pointerMove(fab, { clientX: 200, clientY: 200, pointerId: 1 });
202
+ fireEvent.pointerUp(fab, { clientX: 200, clientY: 200, pointerId: 1 });
203
+ expect(onToggle).not.toHaveBeenCalled();
204
+ });
205
+ it('does not count as drag when movement is below threshold', () => {
206
+ const onToggle = vi.fn();
207
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: onToggle, children: _jsx("div", { children: "Content" }) }));
208
+ const fab = container.querySelector('[data-syntro-fab]');
209
+ fireEvent.pointerDown(fab, { clientX: 100, clientY: 100, pointerId: 1 });
210
+ // Move less than DRAG_THRESHOLD (5px)
211
+ fireEvent.pointerMove(fab, { clientX: 102, clientY: 102, pointerId: 1 });
212
+ fireEvent.pointerUp(fab, { clientX: 102, clientY: 102, pointerId: 1 });
213
+ // Still treated as click
214
+ expect(onToggle).toHaveBeenCalledTimes(1);
215
+ });
216
+ it('ignores pointerMove when not dragging', () => {
217
+ const onToggle = vi.fn();
218
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: onToggle, children: _jsx("div", { children: "Content" }) }));
219
+ const fab = container.querySelector('[data-syntro-fab]');
220
+ // Move without pointerDown should be ignored
221
+ fireEvent.pointerMove(fab, { clientX: 200, clientY: 200, pointerId: 1 });
222
+ // Panel should not have moved
223
+ const panel = container.querySelector('[data-syntro-editor-panel]');
224
+ expect(panel.style.left).toBe('0px');
225
+ });
226
+ it('moves panel DOM directly during drag for performance', () => {
227
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
228
+ const fab = container.querySelector('[data-syntro-fab]');
229
+ const panel = container.querySelector('[data-syntro-editor-panel]');
230
+ fireEvent.pointerDown(fab, { clientX: 12, clientY: 12, pointerId: 1 });
231
+ // Move beyond threshold
232
+ fireEvent.pointerMove(fab, { clientX: 112, clientY: 112, pointerId: 1 });
233
+ // Panel DOM should be updated directly
234
+ expect(panel.style.left).toBe('100px');
235
+ expect(panel.style.top).toBe('100px');
236
+ expect(panel.style.right).toBe('auto');
237
+ });
238
+ it('updates FAB position during drag', () => {
239
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
240
+ const fab = container.querySelector('[data-syntro-fab]');
241
+ fireEvent.pointerDown(fab, { clientX: 12, clientY: 12, pointerId: 1 });
242
+ fireEvent.pointerMove(fab, { clientX: 112, clientY: 112, pointerId: 1 });
243
+ // FAB should be positioned with FAB_INSET (12px) offset from panel top-left
244
+ expect(fab.style.left).toBe('112px'); // 100 + 12
245
+ expect(fab.style.top).toBe('112px'); // 100 + 12
246
+ });
247
+ it('snaps to left edge when dragged close to left boundary', () => {
248
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 200, y: 100, width: 500, height: 600, docked: null }));
249
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
250
+ const fab = container.querySelector('[data-syntro-fab]');
251
+ // Start drag from the panel's position
252
+ fireEvent.pointerDown(fab, { clientX: 212, clientY: 112, pointerId: 1 });
253
+ // Move to near left edge (x < SNAP_THRESHOLD=20)
254
+ fireEvent.pointerMove(fab, { clientX: 22, clientY: 112, pointerId: 1 });
255
+ fireEvent.pointerUp(fab, { clientX: 22, clientY: 112, pointerId: 1 });
256
+ // After snap, should be docked left
257
+ const stored = JSON.parse(localStorage.getItem('syntro:editor-panel'));
258
+ expect(stored.docked).toBe('left');
259
+ });
260
+ it('snaps to right edge when dragged close to right boundary', () => {
261
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 200, y: 100, width: 500, height: 600, docked: null }));
262
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
263
+ const fab = container.querySelector('[data-syntro-fab]');
264
+ // Start drag
265
+ fireEvent.pointerDown(fab, { clientX: 212, clientY: 112, pointerId: 1 });
266
+ // Move to near right edge (x + width >= innerWidth - SNAP_THRESHOLD)
267
+ // Panel width is 500, innerWidth is 1280: need x >= 1280 - 20 - 500 = 760
268
+ fireEvent.pointerMove(fab, { clientX: 982, clientY: 112, pointerId: 1 });
269
+ fireEvent.pointerUp(fab, { clientX: 982, clientY: 112, pointerId: 1 });
270
+ const stored = JSON.parse(localStorage.getItem('syntro:editor-panel'));
271
+ expect(stored.docked).toBe('right');
272
+ });
273
+ it('sets undocked geometry when dragged to middle of screen', () => {
274
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
275
+ const fab = container.querySelector('[data-syntro-fab]');
276
+ fireEvent.pointerDown(fab, { clientX: 12, clientY: 12, pointerId: 1 });
277
+ // Move to center area (not near edges)
278
+ fireEvent.pointerMove(fab, { clientX: 312, clientY: 112, pointerId: 1 });
279
+ fireEvent.pointerUp(fab, { clientX: 312, clientY: 112, pointerId: 1 });
280
+ const stored = JSON.parse(localStorage.getItem('syntro:editor-panel'));
281
+ expect(stored.docked).toBeNull();
282
+ expect(stored.x).toBe(300);
283
+ expect(stored.y).toBe(100);
284
+ });
285
+ });
286
+ describe('resize handlers', () => {
287
+ it('resizes panel east (wider)', () => {
288
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 500, height: 600, docked: null }));
289
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
290
+ const handle = container.querySelector('[data-resize-dir="e"]');
291
+ const panel = container.querySelector('[data-syntro-editor-panel]');
292
+ fireEvent.pointerDown(handle, { clientX: 600, clientY: 300, pointerId: 1 });
293
+ fireEvent.pointerMove(handle, { clientX: 700, clientY: 300, pointerId: 1 });
294
+ // Width should increase by 100
295
+ expect(panel.style.width).toBe('600px');
296
+ fireEvent.pointerUp(handle, { clientX: 700, clientY: 300, pointerId: 1 });
297
+ const stored = JSON.parse(localStorage.getItem('syntro:editor-panel'));
298
+ expect(stored.width).toBe(600);
299
+ });
300
+ it('resizes panel south (taller)', () => {
301
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 500, height: 600, docked: null }));
302
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
303
+ const handle = container.querySelector('[data-resize-dir="s"]');
304
+ const panel = container.querySelector('[data-syntro-editor-panel]');
305
+ fireEvent.pointerDown(handle, { clientX: 300, clientY: 650, pointerId: 1 });
306
+ fireEvent.pointerMove(handle, { clientX: 300, clientY: 750, pointerId: 1 });
307
+ expect(panel.style.height).toBe('700px');
308
+ fireEvent.pointerUp(handle, { clientX: 300, clientY: 750, pointerId: 1 });
309
+ });
310
+ it('resizes panel west (moves left edge)', () => {
311
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 200, y: 50, width: 500, height: 600, docked: null }));
312
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
313
+ const handle = container.querySelector('[data-resize-dir="w"]');
314
+ const panel = container.querySelector('[data-syntro-editor-panel]');
315
+ fireEvent.pointerDown(handle, { clientX: 200, clientY: 300, pointerId: 1 });
316
+ fireEvent.pointerMove(handle, { clientX: 100, clientY: 300, pointerId: 1 });
317
+ // Width increases by 100, x decreases by 100
318
+ expect(panel.style.width).toBe('600px');
319
+ expect(panel.style.left).toBe('100px');
320
+ fireEvent.pointerUp(handle, { clientX: 100, clientY: 300, pointerId: 1 });
321
+ });
322
+ it('resizes panel north (moves top edge)', () => {
323
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 200, width: 500, height: 600, docked: null }));
324
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
325
+ const handle = container.querySelector('[data-resize-dir="n"]');
326
+ const panel = container.querySelector('[data-syntro-editor-panel]');
327
+ fireEvent.pointerDown(handle, { clientX: 300, clientY: 200, pointerId: 1 });
328
+ fireEvent.pointerMove(handle, { clientX: 300, clientY: 100, pointerId: 1 });
329
+ expect(panel.style.height).toBe('700px');
330
+ expect(panel.style.top).toBe('100px');
331
+ fireEvent.pointerUp(handle, { clientX: 300, clientY: 100, pointerId: 1 });
332
+ });
333
+ it('resizes panel southeast (corner)', () => {
334
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 500, height: 600, docked: null }));
335
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
336
+ const handle = container.querySelector('[data-resize-dir="se"]');
337
+ const panel = container.querySelector('[data-syntro-editor-panel]');
338
+ fireEvent.pointerDown(handle, { clientX: 600, clientY: 650, pointerId: 1 });
339
+ fireEvent.pointerMove(handle, { clientX: 700, clientY: 750, pointerId: 1 });
340
+ expect(panel.style.width).toBe('600px');
341
+ expect(panel.style.height).toBe('700px');
342
+ fireEvent.pointerUp(handle, { clientX: 700, clientY: 750, pointerId: 1 });
343
+ });
344
+ it('resizes panel northwest (corner, moves both edges)', () => {
345
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 200, y: 200, width: 500, height: 600, docked: null }));
346
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
347
+ const handle = container.querySelector('[data-resize-dir="nw"]');
348
+ const panel = container.querySelector('[data-syntro-editor-panel]');
349
+ fireEvent.pointerDown(handle, { clientX: 200, clientY: 200, pointerId: 1 });
350
+ fireEvent.pointerMove(handle, { clientX: 100, clientY: 100, pointerId: 1 });
351
+ expect(panel.style.width).toBe('600px');
352
+ expect(panel.style.height).toBe('700px');
353
+ expect(panel.style.left).toBe('100px');
354
+ expect(panel.style.top).toBe('100px');
355
+ fireEvent.pointerUp(handle, { clientX: 100, clientY: 100, pointerId: 1 });
356
+ });
357
+ it('enforces minimum width during resize', () => {
358
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 500, height: 600, docked: null }));
359
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
360
+ const handle = container.querySelector('[data-resize-dir="e"]');
361
+ const panel = container.querySelector('[data-syntro-editor-panel]');
362
+ fireEvent.pointerDown(handle, { clientX: 600, clientY: 300, pointerId: 1 });
363
+ // Try to shrink below MIN_WIDTH (480)
364
+ fireEvent.pointerMove(handle, { clientX: 200, clientY: 300, pointerId: 1 });
365
+ expect(panel.style.width).toBe('480px');
366
+ fireEvent.pointerUp(handle, { clientX: 200, clientY: 300, pointerId: 1 });
367
+ });
368
+ it('enforces minimum height during resize', () => {
369
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 500, height: 600, docked: null }));
370
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
371
+ const handle = container.querySelector('[data-resize-dir="s"]');
372
+ const panel = container.querySelector('[data-syntro-editor-panel]');
373
+ fireEvent.pointerDown(handle, { clientX: 300, clientY: 650, pointerId: 1 });
374
+ // Try to shrink below MIN_HEIGHT (400)
375
+ fireEvent.pointerMove(handle, { clientX: 300, clientY: 100, pointerId: 1 });
376
+ expect(panel.style.height).toBe('400px');
377
+ fireEvent.pointerUp(handle, { clientX: 300, clientY: 100, pointerId: 1 });
378
+ });
379
+ it('undocks panel when resize is started on a docked panel', () => {
380
+ // Start with docked left
381
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
382
+ const handle = container.querySelector('[data-resize-dir="e"]');
383
+ fireEvent.pointerDown(handle, { clientX: 480, clientY: 300, pointerId: 1 });
384
+ // After starting resize on docked panel, it should undock
385
+ const stored = JSON.parse(localStorage.getItem('syntro:editor-panel'));
386
+ expect(stored.docked).toBeNull();
387
+ });
388
+ it('ignores pointerMove when no resize is active', () => {
389
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 500, height: 600, docked: null }));
390
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
391
+ const handle = container.querySelector('[data-resize-dir="e"]');
392
+ const panel = container.querySelector('[data-syntro-editor-panel]');
393
+ // Move without pointerDown
394
+ fireEvent.pointerMove(handle, { clientX: 700, clientY: 300, pointerId: 1 });
395
+ // Width should not change
396
+ expect(panel.style.width).toBe('500px');
397
+ });
398
+ it('updates FAB position during resize', () => {
399
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 500, height: 600, docked: null }));
400
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
401
+ const handle = container.querySelector('[data-resize-dir="s"]');
402
+ const fab = container.querySelector('[data-syntro-fab]');
403
+ fireEvent.pointerDown(handle, { clientX: 300, clientY: 650, pointerId: 1 });
404
+ fireEvent.pointerMove(handle, { clientX: 300, clientY: 750, pointerId: 1 });
405
+ // FAB top should stay at top-left of panel during resize
406
+ // FAB top = y + FAB_INSET = 50 + 12 = 62
407
+ expect(fab.style.top).toBe('62px');
408
+ // FAB left = x + FAB_INSET = 100 + 12 = 112
409
+ expect(fab.style.left).toBe('112px');
410
+ fireEvent.pointerUp(handle, { clientX: 300, clientY: 750, pointerId: 1 });
411
+ });
412
+ it('does not commit geometry on pointerUp when no pending geo exists', () => {
413
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 50, width: 500, height: 600, docked: null }));
414
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
415
+ const handle = container.querySelector('[data-resize-dir="e"]');
416
+ // pointerDown then immediate pointerUp without pointerMove
417
+ fireEvent.pointerDown(handle, { clientX: 600, clientY: 300, pointerId: 1 });
418
+ fireEvent.pointerUp(handle, { clientX: 600, clientY: 300, pointerId: 1 });
419
+ const stored = JSON.parse(localStorage.getItem('syntro:editor-panel'));
420
+ // Should remain undocked with original dimensions (but undocked since it was docked)
421
+ expect(stored.docked).toBeNull();
422
+ });
423
+ it('resizes northeast corner correctly', () => {
424
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 100, y: 200, width: 500, height: 600, docked: null }));
425
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
426
+ const handle = container.querySelector('[data-resize-dir="ne"]');
427
+ const panel = container.querySelector('[data-syntro-editor-panel]');
428
+ fireEvent.pointerDown(handle, { clientX: 600, clientY: 200, pointerId: 1 });
429
+ fireEvent.pointerMove(handle, { clientX: 700, clientY: 100, pointerId: 1 });
430
+ // East: width increases by 100
431
+ expect(panel.style.width).toBe('600px');
432
+ // North: height increases by 100, top decreases by 100
433
+ expect(panel.style.height).toBe('700px');
434
+ expect(panel.style.top).toBe('100px');
435
+ fireEvent.pointerUp(handle, { clientX: 700, clientY: 100, pointerId: 1 });
436
+ });
437
+ it('resizes southwest corner correctly', () => {
438
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 200, y: 50, width: 500, height: 600, docked: null }));
439
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
440
+ const handle = container.querySelector('[data-resize-dir="sw"]');
441
+ const panel = container.querySelector('[data-syntro-editor-panel]');
442
+ fireEvent.pointerDown(handle, { clientX: 200, clientY: 650, pointerId: 1 });
443
+ fireEvent.pointerMove(handle, { clientX: 100, clientY: 750, pointerId: 1 });
444
+ // West: width increases by 100, left decreases by 100
445
+ expect(panel.style.width).toBe('600px');
446
+ expect(panel.style.left).toBe('100px');
447
+ // South: height increases by 100
448
+ expect(panel.style.height).toBe('700px');
449
+ fireEvent.pointerUp(handle, { clientX: 100, clientY: 750, pointerId: 1 });
450
+ });
451
+ });
452
+ describe('viewport clamping', () => {
453
+ it('clamps panel position on window resize', () => {
454
+ localStorage.setItem('syntro:editor-panel', JSON.stringify({ x: 1200, y: 700, width: 500, height: 600, docked: null }));
455
+ render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
456
+ // Shrink viewport
457
+ Object.defineProperty(window, 'innerWidth', { value: 800, writable: true });
458
+ Object.defineProperty(window, 'innerHeight', { value: 600, writable: true });
459
+ act(() => {
460
+ window.dispatchEvent(new Event('resize'));
461
+ });
462
+ const stored = JSON.parse(localStorage.getItem('syntro:editor-panel'));
463
+ // x should be clamped so FAB stays visible
464
+ expect(stored.x).toBeLessThanOrEqual(800 - 56); // vw - FAB_SIZE
465
+ });
466
+ it('does not clamp docked panels on window resize', () => {
467
+ // Docked panels ignore clampToViewport
468
+ render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
469
+ act(() => {
470
+ window.dispatchEvent(new Event('resize'));
471
+ });
472
+ const stored = JSON.parse(localStorage.getItem('syntro:editor-panel'));
473
+ expect(stored.docked).toBe('left');
474
+ });
475
+ });
476
+ it('sets id attribute on panel when panelId is provided', () => {
477
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, panelId: "test-panel", children: _jsx("div", { children: "Content" }) }));
478
+ const panel = container.querySelector('#test-panel');
479
+ expect(panel).toBeTruthy();
480
+ });
481
+ it('does not set id attribute on panel when panelId is not provided', () => {
482
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
483
+ const panel = container.querySelector('[data-syntro-editor-panel]');
484
+ expect(panel.id).toBe('');
485
+ });
486
+ it('FAB title is "Minimize panel" when open', () => {
487
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
488
+ const fab = container.querySelector('[data-syntro-fab]');
489
+ expect(fab.title).toBe('Minimize panel');
490
+ });
491
+ it('FAB title is "Open panel" when closed', () => {
492
+ const { container } = render(_jsx(EditorPanelShell, { isOpen: false, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
493
+ const fab = container.querySelector('[data-syntro-fab]');
494
+ expect(fab.title).toBe('Open panel');
495
+ });
25
496
  });
@@ -119,4 +119,58 @@ describe('ElementHighlight', () => {
119
119
  const removeBtn = document.body.querySelector('[data-syntro-highlight-remove]');
120
120
  expect(removeBtn).toBeNull();
121
121
  });
122
+ it('calls onClick when Enter key is pressed', () => {
123
+ const onClick = vi.fn();
124
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6", onClick: onClick }));
125
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
126
+ fireEvent.keyDown(overlay, { key: 'Enter' });
127
+ expect(onClick).toHaveBeenCalledTimes(1);
128
+ });
129
+ it('calls onClick when Space key is pressed', () => {
130
+ const onClick = vi.fn();
131
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6", onClick: onClick }));
132
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
133
+ fireEvent.keyDown(overlay, { key: ' ' });
134
+ expect(onClick).toHaveBeenCalledTimes(1);
135
+ });
136
+ it('does not fire onClick on other key presses', () => {
137
+ const onClick = vi.fn();
138
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6", onClick: onClick }));
139
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
140
+ fireEvent.keyDown(overlay, { key: 'Tab' });
141
+ expect(onClick).not.toHaveBeenCalled();
142
+ });
143
+ it('does not attach onKeyDown when onClick is not provided', () => {
144
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6" }));
145
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
146
+ // Should not throw and should have no role/tabIndex
147
+ expect(overlay.getAttribute('role')).toBeNull();
148
+ expect(overlay.getAttribute('tabindex')).toBeNull();
149
+ });
150
+ it('has role="button" and tabIndex when onClick is provided', () => {
151
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6", onClick: () => { } }));
152
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
153
+ expect(overlay.getAttribute('role')).toBe('button');
154
+ expect(overlay.getAttribute('tabindex')).toBe('0');
155
+ });
156
+ it('sets pointer cursor when onClick is provided', () => {
157
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6", onClick: () => { } }));
158
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
159
+ expect(overlay.style.cursor).toBe('pointer');
160
+ });
161
+ it('sets default cursor when onClick is not provided', () => {
162
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6" }));
163
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
164
+ expect(overlay.style.cursor).toBe('default');
165
+ });
166
+ it('sets pointerEvents to auto when onClick is provided', () => {
167
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6", onClick: () => { } }));
168
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
169
+ expect(overlay.style.pointerEvents).toBe('auto');
170
+ });
171
+ it('sets pointerEvents to none when no onClick or onRemove', () => {
172
+ render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6" }));
173
+ const overlay = document.body.querySelector('[data-syntro-highlight]');
174
+ expect(overlay.style.pointerEvents).toBe('none');
175
+ });
122
176
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=selectorGenerator.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selectorGenerator.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/selectorGenerator.test.ts"],"names":[],"mappings":""}