@capillarytech/creatives-library 8.0.207 → 8.0.209

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 (77) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/package.json +16 -2
  4. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  6. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  8. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  9. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  10. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  11. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  12. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  14. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  15. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  19. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  20. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  21. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +389 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  29. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  30. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  31. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  32. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  35. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  36. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  38. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  39. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  42. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  43. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  44. package/v2Components/HtmlEditor/constants.js +241 -0
  45. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  46. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  52. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  53. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  54. package/v2Components/HtmlEditor/index.js +29 -0
  55. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  56. package/v2Components/HtmlEditor/messages.js +389 -0
  57. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  58. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  63. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  64. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  65. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  66. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  67. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  68. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  69. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  70. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  71. package/v2Containers/EmailWrapper/index.js +8 -1
  72. package/v2Containers/Templates/constants.js +8 -0
  73. package/v2Containers/Templates/index.js +56 -28
  74. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
  75. package/v2Containers/Whatsapp/constants.js +26 -2
  76. package/v2Containers/Whatsapp/index.js +4 -1
  77. package/v2Containers/Whatsapp/tests/index.test.js +460 -18
@@ -0,0 +1,276 @@
1
+ /**
2
+ * SplitContainer - Smooth and functional resizable split panel
3
+ *
4
+ * Features:
5
+ * - Smooth dragging with proper event handling
6
+ * - Percentage-based sizing that works correctly
7
+ * - Proper constraints and boundaries
8
+ * - Clean visual feedback
9
+ * - Touch support for mobile devices
10
+ * - Memory of split position
11
+ */
12
+
13
+ import React, { useState, useRef, useCallback, useEffect } from 'react';
14
+ import PropTypes from 'prop-types';
15
+
16
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
17
+
18
+ // Styles
19
+ import './_splitContainer.scss';
20
+
21
+ // Constants
22
+ const SPLIT_CONSTRAINTS = {
23
+ MIN_PERCENT: 10,
24
+ MAX_PERCENT: 90,
25
+ DEFAULT_MIN_SIZE: 200
26
+ };
27
+
28
+ const DIRECTIONS = {
29
+ HORIZONTAL: 'horizontal',
30
+ VERTICAL: 'vertical'
31
+ };
32
+
33
+ const SplitContainer = ({
34
+ children,
35
+ className = '',
36
+ defaultSplit = 50,
37
+ minSize = SPLIT_CONSTRAINTS.DEFAULT_MIN_SIZE,
38
+ maxSize = null,
39
+ direction = DIRECTIONS.HORIZONTAL,
40
+ onSplitChange = null
41
+ }) => {
42
+ const [splitSize, setSplitSize] = useState(defaultSplit);
43
+ const [isDragging, setIsDragging] = useState(false);
44
+
45
+ // Refs for drag handling
46
+ const containerRef = useRef(null);
47
+ const startPositionRef = useRef(0);
48
+ const startSplitRef = useRef(0);
49
+
50
+ const handleMouseDown = useCallback((e) => {
51
+ e.preventDefault();
52
+ e.stopPropagation();
53
+
54
+ setIsDragging(true);
55
+ startPositionRef.current = direction === DIRECTIONS.HORIZONTAL ? e.clientX : e.clientY;
56
+ startSplitRef.current = splitSize;
57
+
58
+ // Add global styles for dragging
59
+ const { body } = document;
60
+ body.style.cursor = direction === DIRECTIONS.HORIZONTAL ? 'col-resize' : 'row-resize';
61
+ body.style.userSelect = 'none';
62
+ body.style.pointerEvents = 'none';
63
+
64
+ // Prevent text selection during drag
65
+ document.onselectstart = () => false;
66
+
67
+ // Re-enable pointer events on the container
68
+ if (containerRef.current) {
69
+ containerRef.current.style.pointerEvents = 'auto';
70
+ }
71
+ }, [direction, splitSize]);
72
+
73
+ const handleMouseMove = useCallback((e) => {
74
+ if (!isDragging || !containerRef.current) return;
75
+
76
+ e.preventDefault();
77
+ e.stopPropagation();
78
+
79
+ const container = containerRef.current;
80
+ const containerRect = container.getBoundingClientRect();
81
+ const isHorizontal = direction === DIRECTIONS.HORIZONTAL;
82
+ const containerSize = isHorizontal ? containerRect.width : containerRect.height;
83
+
84
+ if (containerSize === 0) return; // Avoid division by zero
85
+
86
+ const currentPosition = isHorizontal ? e.clientX : e.clientY;
87
+ const containerStart = isHorizontal ? containerRect.left : containerRect.top;
88
+ const relativePosition = currentPosition - containerStart;
89
+
90
+ // Calculate new split percentage
91
+ let newSplit = (relativePosition / containerSize) * 100;
92
+
93
+ // Apply constraints
94
+ const minPercent = (minSize / containerSize) * 100;
95
+ const maxPercent = maxSize
96
+ ? ((containerSize - maxSize) / containerSize) * 100
97
+ : SPLIT_CONSTRAINTS.MAX_PERCENT;
98
+
99
+ // Clamp the values
100
+ const { MIN_PERCENT, MAX_PERCENT } = SPLIT_CONSTRAINTS;
101
+ newSplit = Math.max(MIN_PERCENT, Math.min(MAX_PERCENT, newSplit)); // Overall limits
102
+ newSplit = Math.max(minPercent, Math.min(100 - minPercent, newSplit)); // Min size limits
103
+
104
+ if (maxSize) {
105
+ newSplit = Math.min(maxPercent, newSplit);
106
+ }
107
+
108
+ setSplitSize(newSplit);
109
+ onSplitChange?.(newSplit); // Optional chaining
110
+ }, [isDragging, direction, minSize, maxSize, onSplitChange]);
111
+
112
+ const handleMouseUp = useCallback(() => {
113
+ if (!isDragging) return;
114
+
115
+ setIsDragging(false);
116
+
117
+ // Reset global styles
118
+ const { body } = document;
119
+ body.style.cursor = '';
120
+ body.style.userSelect = '';
121
+ body.style.pointerEvents = '';
122
+
123
+ // Reset text selection prevention
124
+ document.onselectstart = null;
125
+
126
+ containerRef.current?.style && (containerRef.current.style.pointerEvents = '');
127
+ }, [isDragging]);
128
+
129
+ // Touch support - convert touch events to mouse events
130
+ const handleTouchStart = useCallback((e) => {
131
+ const touch = e.touches?.[0];
132
+ if (!touch) return;
133
+
134
+ const mouseEvent = new MouseEvent('mousedown', {
135
+ clientX: touch.clientX,
136
+ clientY: touch.clientY
137
+ });
138
+ handleMouseDown(mouseEvent);
139
+ }, [handleMouseDown]);
140
+
141
+ const handleTouchMove = useCallback((e) => {
142
+ if (!isDragging) return;
143
+ e.preventDefault();
144
+
145
+ const touch = e.touches?.[0];
146
+ if (!touch) return;
147
+
148
+ const mouseEvent = new MouseEvent('mousemove', {
149
+ clientX: touch.clientX,
150
+ clientY: touch.clientY
151
+ });
152
+ handleMouseMove(mouseEvent);
153
+ }, [isDragging, handleMouseMove]);
154
+
155
+ const handleTouchEnd = useCallback(() => {
156
+ handleMouseUp();
157
+ }, [handleMouseUp]);
158
+
159
+ // Event listeners
160
+ useEffect(() => {
161
+ if (isDragging) {
162
+ document.addEventListener('mousemove', handleMouseMove, { passive: false });
163
+ document.addEventListener('mouseup', handleMouseUp);
164
+ document.addEventListener('touchmove', handleTouchMove, { passive: false });
165
+ document.addEventListener('touchend', handleTouchEnd);
166
+
167
+ return () => {
168
+ document.removeEventListener('mousemove', handleMouseMove);
169
+ document.removeEventListener('mouseup', handleMouseUp);
170
+ document.removeEventListener('touchmove', handleTouchMove);
171
+ document.removeEventListener('touchend', handleTouchEnd);
172
+ };
173
+ }
174
+ }, [isDragging, handleMouseMove, handleMouseUp, handleTouchMove, handleTouchEnd]);
175
+
176
+ // Prevent text selection during drag
177
+ useEffect(() => {
178
+ if (isDragging) {
179
+ // Named handlers to prevent text selection and dragging during resize
180
+ const selectStartHandler = (e) => {
181
+ e.preventDefault();
182
+ return false;
183
+ };
184
+
185
+ const dragStartHandler = (e) => {
186
+ e.preventDefault();
187
+ return false;
188
+ };
189
+
190
+ document.addEventListener('selectstart', selectStartHandler);
191
+ document.addEventListener('dragstart', dragStartHandler);
192
+
193
+ return () => {
194
+ document.removeEventListener('selectstart', selectStartHandler);
195
+ document.removeEventListener('dragstart', dragStartHandler);
196
+ };
197
+ }
198
+ }, [isDragging]);
199
+
200
+ // Cleanup effect to reset global styles on unmount
201
+ useEffect(() => {
202
+ return () => {
203
+ // Reset global body styles if component unmounts while dragging
204
+ if (isDragging) {
205
+ const { body } = document;
206
+ body.style.cursor = '';
207
+ body.style.userSelect = '';
208
+ body.style.pointerEvents = '';
209
+
210
+ // Reset container pointer events if ref is still available
211
+ if (containerRef.current) {
212
+ containerRef.current.style.pointerEvents = '';
213
+ }
214
+ }
215
+ };
216
+ }, [isDragging]);
217
+
218
+ // Generate CSS classes
219
+ const containerClasses = [
220
+ 'split-container',
221
+ `split-container--${direction}`,
222
+ isDragging ? 'split-container--dragging' : '',
223
+ className
224
+ ].filter(Boolean).join(' ');
225
+
226
+ // Generate CSS custom properties for dynamic sizing
227
+ const containerStyle = {
228
+ '--split-size': `${splitSize}%`,
229
+ '--remaining-size': `${100 - splitSize}%`
230
+ };
231
+
232
+ return (
233
+ <div
234
+ ref={containerRef}
235
+ className={containerClasses}
236
+ style={containerStyle}
237
+ >
238
+ <CapRow className="split-container__panel split-container__panel--first">
239
+ {children?.[0]}
240
+ </CapRow>
241
+
242
+ <div
243
+ className="split-container__splitter"
244
+ onMouseDown={handleMouseDown}
245
+ onTouchStart={handleTouchStart}
246
+ role="separator"
247
+ tabIndex={0}
248
+ aria-label={`Resize ${direction === DIRECTIONS.HORIZONTAL ? 'horizontal' : 'vertical'} split`}
249
+ aria-orientation={direction === DIRECTIONS.HORIZONTAL ? 'vertical' : 'horizontal'}
250
+ aria-valuenow={Math.round(splitSize)}
251
+ aria-valuemin={SPLIT_CONSTRAINTS.MIN_PERCENT}
252
+ aria-valuemax={SPLIT_CONSTRAINTS.MAX_PERCENT}
253
+ >
254
+ <div className="split-container__splitter-handle">
255
+ <div className="split-container__splitter-line" />
256
+ </div>
257
+ </div>
258
+
259
+ <CapRow className="split-container__panel split-container__panel--second">
260
+ {children?.[1]}
261
+ </CapRow>
262
+ </div>
263
+ );
264
+ };
265
+
266
+ SplitContainer.propTypes = {
267
+ children: PropTypes.arrayOf(PropTypes.node).isRequired,
268
+ className: PropTypes.string,
269
+ defaultSplit: PropTypes.number,
270
+ minSize: PropTypes.number,
271
+ maxSize: PropTypes.number,
272
+ direction: PropTypes.oneOf(Object.values(DIRECTIONS)),
273
+ onSplitChange: PropTypes.func
274
+ };
275
+
276
+ export default SplitContainer;
@@ -0,0 +1,295 @@
1
+ /**
2
+ * SplitContainer Component Tests
3
+ *
4
+ * Tests for the SplitContainer component that provides resizable split panels.
5
+ */
6
+
7
+ import React from 'react';
8
+ import { render, screen, fireEvent } from '@testing-library/react';
9
+ import '@testing-library/jest-dom';
10
+ import SplitContainer from '../SplitContainer';
11
+
12
+ describe('SplitContainer', () => {
13
+ const defaultProps = {
14
+ children: [
15
+ <div key="left" data-testid="left-panel">Left Panel</div>,
16
+ <div key="right" data-testid="right-panel">Right Panel</div>
17
+ ]
18
+ };
19
+
20
+ beforeEach(() => {
21
+ // Setup window.getComputedStyle mock - returns a CSSStyleDeclaration-like object
22
+ window.getComputedStyle = jest.fn(() => ({
23
+ getPropertyValue: (prop) => {
24
+ const values = {
25
+ 'width': '800px',
26
+ 'height': '600px',
27
+ };
28
+ return values[prop] || '0px';
29
+ },
30
+ width: '800px',
31
+ height: '600px',
32
+ }));
33
+
34
+ // Mock getBoundingClientRect
35
+ Element.prototype.getBoundingClientRect = jest.fn(() => ({
36
+ width: 800,
37
+ height: 600,
38
+ left: 0,
39
+ top: 0,
40
+ right: 800,
41
+ bottom: 600
42
+ }));
43
+ });
44
+
45
+ afterEach(() => {
46
+ // Clean up any global state
47
+ document.body.innerHTML = '';
48
+
49
+ // Reset any global mocks
50
+ if (document.onselectstart) {
51
+ document.onselectstart = null;
52
+ }
53
+ if (document.ondragstart) {
54
+ document.ondragstart = null;
55
+ }
56
+ });
57
+
58
+ it('renders with default horizontal layout', () => {
59
+ render(<SplitContainer {...defaultProps} />);
60
+
61
+ expect(screen.getByTestId('left-panel')).toBeInTheDocument();
62
+ expect(screen.getByTestId('right-panel')).toBeInTheDocument();
63
+
64
+ const container = screen.getByTestId('left-panel').closest('.split-container');
65
+ expect(container).toHaveClass('split-container--horizontal');
66
+ });
67
+
68
+ it('renders with vertical layout when specified', () => {
69
+ render(<SplitContainer {...defaultProps} direction="vertical" />);
70
+
71
+ const container = screen.getByTestId('left-panel').closest('.split-container');
72
+ expect(container).toHaveClass('split-container--vertical');
73
+ });
74
+
75
+ it('applies default 50/50 split', () => {
76
+ render(<SplitContainer {...defaultProps} />);
77
+
78
+ const container = screen.getByTestId('left-panel').closest('.split-container');
79
+
80
+ // Check CSS custom properties are set correctly
81
+ expect(container.style.getPropertyValue('--split-size')).toBe('50%');
82
+ expect(container.style.getPropertyValue('--remaining-size')).toBe('50%');
83
+ });
84
+
85
+ it('applies custom default split', () => {
86
+ render(<SplitContainer {...defaultProps} defaultSplit={30} />);
87
+
88
+ const container = screen.getByTestId('left-panel').closest('.split-container');
89
+
90
+ // Check CSS custom properties are set correctly
91
+ expect(container.style.getPropertyValue('--split-size')).toBe('30%');
92
+ expect(container.style.getPropertyValue('--remaining-size')).toBe('70%');
93
+ });
94
+
95
+ it('renders splitter handle', () => {
96
+ render(<SplitContainer {...defaultProps} />);
97
+
98
+ const splitter = document.querySelector('.split-container__splitter');
99
+ const handle = document.querySelector('.split-container__splitter-handle');
100
+ const line = document.querySelector('.split-container__splitter-line');
101
+
102
+ expect(splitter).toBeInTheDocument();
103
+ expect(handle).toBeInTheDocument();
104
+ expect(line).toBeInTheDocument();
105
+ });
106
+
107
+ it('handles mouse down on splitter', () => {
108
+ render(<SplitContainer {...defaultProps} />);
109
+
110
+ const splitter = document.querySelector('.split-container__splitter');
111
+
112
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
113
+
114
+ const container = screen.getByTestId('left-panel').closest('.split-container');
115
+ expect(container).toHaveClass('split-container--dragging');
116
+ });
117
+
118
+ it('handles mouse move during drag', () => {
119
+ render(<SplitContainer {...defaultProps} />);
120
+
121
+ const splitter = document.querySelector('.split-container__splitter');
122
+ const container = screen.getByTestId('left-panel').closest('.split-container');
123
+
124
+ // Start drag
125
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
126
+
127
+ // Move mouse
128
+ fireEvent.mouseMove(document, { clientX: 300, clientY: 300 });
129
+
130
+ // Should update split position via CSS custom properties
131
+ const splitSize = container.style.getPropertyValue('--split-size');
132
+ expect(splitSize).toBeTruthy();
133
+ expect(splitSize).toMatch(/^\d+(\.\d+)?%$/); // Should be a percentage
134
+ });
135
+
136
+ it('handles mouse up to end drag', () => {
137
+ render(<SplitContainer {...defaultProps} />);
138
+
139
+ const splitter = document.querySelector('.split-container__splitter');
140
+
141
+ // Start and end drag
142
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
143
+ fireEvent.mouseUp(document);
144
+
145
+ const container = screen.getByTestId('left-panel').closest('.split-container');
146
+ expect(container).not.toHaveClass('split-container--dragging');
147
+ });
148
+
149
+ it('handles touch events', () => {
150
+ render(<SplitContainer {...defaultProps} />);
151
+
152
+ const splitter = document.querySelector('.split-container__splitter');
153
+
154
+ // Touch start
155
+ fireEvent.touchStart(splitter, {
156
+ touches: [{ clientX: 400, clientY: 300 }]
157
+ });
158
+
159
+ const container = screen.getByTestId('left-panel').closest('.split-container');
160
+ expect(container).toHaveClass('split-container--dragging');
161
+ });
162
+
163
+ it('respects minimum size constraints', () => {
164
+ render(<SplitContainer {...defaultProps} minSize={100} />);
165
+
166
+ const splitter = document.querySelector('.split-container__splitter');
167
+
168
+ // Try to drag to a very small size
169
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
170
+ fireEvent.mouseMove(document, { clientX: 50, clientY: 300 }); // Very small
171
+
172
+ const firstPanel = screen.getByTestId('left-panel').closest('.split-container__panel--first');
173
+ const container = firstPanel.closest('.split-container');
174
+
175
+ // Read the CSS variable directly from the inline style
176
+ const splitSizeValue = container.style.getPropertyValue('--split-size');
177
+ const parsedPercent = parseFloat(splitSizeValue.trim().replace('%', ''));
178
+
179
+ // Should respect minimum size (100px out of 800px = 12.5%)
180
+ expect(parsedPercent).toBeGreaterThanOrEqual(12.5);
181
+ });
182
+
183
+ it('respects maximum size constraints', () => {
184
+ render(<SplitContainer {...defaultProps} maxSize={100} />);
185
+
186
+ const splitter = document.querySelector('.split-container__splitter');
187
+ const container = screen.getByTestId('left-panel').closest('.split-container');
188
+
189
+ // Try to drag to a very large size
190
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
191
+ fireEvent.mouseMove(document, { clientX: 750, clientY: 300 }); // Very large
192
+
193
+ const splitSize = container.style.getPropertyValue('--split-size');
194
+ const widthPercent = parseFloat(splitSize);
195
+
196
+ // Should respect maximum size constraint
197
+ expect(widthPercent).toBeLessThanOrEqual(90); // Should be clamped
198
+ });
199
+
200
+ it('calls onSplitChange when split changes', () => {
201
+ const onSplitChange = jest.fn();
202
+ render(<SplitContainer {...defaultProps} onSplitChange={onSplitChange} />);
203
+
204
+ const splitter = document.querySelector('.split-container__splitter');
205
+
206
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
207
+ fireEvent.mouseMove(document, { clientX: 300, clientY: 300 });
208
+
209
+ expect(onSplitChange).toHaveBeenCalled();
210
+ });
211
+
212
+ it('applies custom className', () => {
213
+ render(<SplitContainer {...defaultProps} className="custom-split" />);
214
+
215
+ const container = screen.getByTestId('left-panel').closest('.split-container');
216
+ expect(container).toHaveClass('custom-split');
217
+ });
218
+
219
+ it('handles vertical direction correctly', () => {
220
+ render(<SplitContainer {...defaultProps} direction="vertical" />);
221
+
222
+ const container = screen.getByTestId('left-panel').closest('.split-container');
223
+
224
+ // In vertical mode, CSS custom properties are still used the same way
225
+ expect(container.style.getPropertyValue('--split-size')).toBe('50%');
226
+ expect(container.style.getPropertyValue('--remaining-size')).toBe('50%');
227
+ });
228
+
229
+ it('handles drag in vertical direction', () => {
230
+ render(<SplitContainer {...defaultProps} direction="vertical" />);
231
+
232
+ const splitter = document.querySelector('.split-container__splitter');
233
+ const container = screen.getByTestId('left-panel').closest('.split-container');
234
+
235
+ // Start vertical drag
236
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
237
+ fireEvent.mouseMove(document, { clientX: 400, clientY: 200 }); // Move vertically
238
+
239
+ // Should update split position via CSS custom properties
240
+ const splitSize = container.style.getPropertyValue('--split-size');
241
+ expect(splitSize).toBeTruthy();
242
+ expect(splitSize).toMatch(/^\d+(\.\d+)?%$/); // Should be a percentage
243
+ });
244
+
245
+ it('prevents text selection during drag', () => {
246
+ render(<SplitContainer {...defaultProps} />);
247
+
248
+ const splitter = document.querySelector('.split-container__splitter');
249
+
250
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
251
+
252
+ // Should set document.onselectstart to prevent selection
253
+ expect(document.onselectstart).toBeDefined();
254
+ expect(document.onselectstart()).toBe(false);
255
+ });
256
+
257
+ it('cleans up event listeners on unmount', () => {
258
+ const removeEventListenerSpy = jest.spyOn(document, 'removeEventListener');
259
+
260
+ const { unmount } = render(<SplitContainer {...defaultProps} />);
261
+
262
+ const splitter = document.querySelector('.split-container__splitter');
263
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
264
+
265
+ unmount();
266
+
267
+ // Should clean up listeners
268
+ expect(removeEventListenerSpy).toHaveBeenCalled();
269
+
270
+ removeEventListenerSpy.mockRestore();
271
+ });
272
+
273
+ it('handles edge case with zero container size', () => {
274
+ // Mock zero-size container
275
+ Element.prototype.getBoundingClientRect = jest.fn(() => ({
276
+ width: 0,
277
+ height: 0,
278
+ left: 0,
279
+ top: 0,
280
+ right: 0,
281
+ bottom: 0
282
+ }));
283
+
284
+ render(<SplitContainer {...defaultProps} />);
285
+
286
+ const splitter = document.querySelector('.split-container__splitter');
287
+
288
+ // Should not crash with zero-size container
289
+ fireEvent.mouseDown(splitter, { clientX: 400, clientY: 300 });
290
+ fireEvent.mouseMove(document, { clientX: 300, clientY: 300 });
291
+
292
+ expect(screen.getByTestId('left-panel')).toBeInTheDocument();
293
+ });
294
+ });
295
+