cursorflow 1.2.0__py3-none-any.whl

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.
@@ -0,0 +1,397 @@
1
+ """
2
+ CSS Iterator
3
+
4
+ Visual development support for rapid CSS iteration with instant feedback.
5
+ Captures visual states, applies CSS changes, and provides comparison data
6
+ for Cursor to analyze layout improvements.
7
+ """
8
+
9
+ import asyncio
10
+ import time
11
+ import json
12
+ from typing import Dict, List, Optional, Any
13
+ from pathlib import Path
14
+ import logging
15
+
16
+
17
+ class CSSIterator:
18
+ """
19
+ CSS iteration support - captures visual data for Cursor analysis
20
+
21
+ Provides visual comparison data without interpretation - Cursor decides
22
+ which CSS changes are improvements.
23
+ """
24
+
25
+ def __init__(self):
26
+ """Initialize CSS iterator"""
27
+ self.logger = logging.getLogger(__name__)
28
+
29
+ # Create artifacts in current working directory (user's project)
30
+ self.artifacts_base = Path.cwd() / ".cursorflow" / "artifacts"
31
+ self.artifacts_base.mkdir(parents=True, exist_ok=True)
32
+
33
+ # Ensure subdirectories exist
34
+ (self.artifacts_base / "css_iterations").mkdir(exist_ok=True)
35
+ (self.artifacts_base / "screenshots").mkdir(exist_ok=True)
36
+ (self.artifacts_base / "sessions").mkdir(exist_ok=True)
37
+
38
+ async def capture_baseline(self, page) -> Dict[str, Any]:
39
+ """
40
+ Capture baseline visual state before CSS changes
41
+
42
+ Args:
43
+ page: Playwright page object
44
+
45
+ Returns:
46
+ Baseline data for Cursor to compare against
47
+ """
48
+ try:
49
+ timestamp = int(time.time())
50
+ baseline_name = f"baseline_{timestamp}"
51
+
52
+ # Capture screenshot
53
+ screenshot_path = str(self.artifacts_base / "css_iterations" / f"{baseline_name}.png")
54
+ await page.screenshot(path=screenshot_path, full_page=True)
55
+
56
+ # Capture layout metrics
57
+ layout_metrics = await self._capture_layout_metrics(page)
58
+
59
+ # Capture computed styles for key elements
60
+ computed_styles = await self._capture_computed_styles(page)
61
+
62
+ # Capture performance baseline
63
+ performance_metrics = await self._capture_performance_metrics(page)
64
+
65
+ baseline_data = {
66
+ "timestamp": time.time(),
67
+ "screenshot": screenshot_path,
68
+ "layout_metrics": layout_metrics,
69
+ "computed_styles": computed_styles,
70
+ "performance_metrics": performance_metrics,
71
+ "name": baseline_name
72
+ }
73
+
74
+ self.logger.info(f"Captured baseline: {baseline_name}")
75
+ return baseline_data
76
+
77
+ except Exception as e:
78
+ self.logger.error(f"Baseline capture failed: {e}")
79
+ return {"error": str(e)}
80
+
81
+ async def apply_css_and_capture(
82
+ self,
83
+ page,
84
+ css_change: Dict,
85
+ baseline: Dict,
86
+ suffix: str = ""
87
+ ) -> Dict[str, Any]:
88
+ """
89
+ Apply CSS change and capture resulting visual state
90
+
91
+ Args:
92
+ page: Playwright page object
93
+ css_change: {"name": "fix-name", "css": ".class { prop: value; }", "rationale": "why"}
94
+ baseline: Baseline data for comparison
95
+ suffix: Optional suffix for file naming (e.g., "_mobile")
96
+
97
+ Returns:
98
+ Iteration data for Cursor to analyze
99
+ """
100
+ try:
101
+ iteration_name = css_change.get("name", "unnamed")
102
+ css_code = css_change.get("css", "")
103
+
104
+ if not css_code:
105
+ return {"error": "No CSS code provided"}
106
+
107
+ timestamp = int(time.time())
108
+ iteration_file_name = f"{iteration_name}_{timestamp}{suffix}"
109
+
110
+ # Apply CSS to page
111
+ await page.add_style_tag(content=css_code)
112
+
113
+ # Wait for layout to stabilize
114
+ await page.wait_for_timeout(200)
115
+
116
+ # Capture new visual state
117
+ screenshot_path = str(self.artifacts_base / "css_iterations" / f"{iteration_file_name}.png")
118
+ await page.screenshot(path=screenshot_path, full_page=True)
119
+
120
+ # Capture new layout metrics
121
+ new_layout_metrics = await self._capture_layout_metrics(page)
122
+
123
+ # Capture new computed styles
124
+ new_computed_styles = await self._capture_computed_styles(page)
125
+
126
+ # Capture performance impact
127
+ new_performance_metrics = await self._capture_performance_metrics(page)
128
+
129
+ # Capture any console errors introduced by CSS
130
+ console_errors = await self._capture_console_errors(page)
131
+
132
+ # Create comparison data (simple differences, no analysis)
133
+ layout_changes = self._calculate_layout_differences(
134
+ baseline.get("layout_metrics", {}),
135
+ new_layout_metrics
136
+ )
137
+
138
+ style_changes = self._calculate_style_differences(
139
+ baseline.get("computed_styles", {}),
140
+ new_computed_styles
141
+ )
142
+
143
+ iteration_data = {
144
+ "timestamp": time.time(),
145
+ "name": iteration_name,
146
+ "css_applied": css_code,
147
+ "rationale": css_change.get("rationale", ""),
148
+ "screenshot": screenshot_path,
149
+ "layout_metrics": new_layout_metrics,
150
+ "computed_styles": new_computed_styles,
151
+ "performance_metrics": new_performance_metrics,
152
+ "console_errors": console_errors,
153
+ "changes": {
154
+ "layout_differences": layout_changes,
155
+ "style_differences": style_changes
156
+ }
157
+ }
158
+
159
+ self.logger.info(f"Applied CSS iteration: {iteration_name}")
160
+ return iteration_data
161
+
162
+ except Exception as e:
163
+ self.logger.error(f"CSS iteration failed: {e}")
164
+ return {"error": str(e), "name": css_change.get("name", "unknown")}
165
+
166
+ async def _capture_layout_metrics(self, page) -> Dict[str, Any]:
167
+ """Capture layout metrics for comparison"""
168
+ try:
169
+ metrics = await page.evaluate("""
170
+ () => {
171
+ const body = document.body;
172
+ const html = document.documentElement;
173
+
174
+ // Document dimensions
175
+ const documentHeight = Math.max(
176
+ body.scrollHeight, body.offsetHeight,
177
+ html.clientHeight, html.scrollHeight, html.offsetHeight
178
+ );
179
+
180
+ const documentWidth = Math.max(
181
+ body.scrollWidth, body.offsetWidth,
182
+ html.clientWidth, html.scrollWidth, html.offsetWidth
183
+ );
184
+
185
+ // Viewport info
186
+ const viewport = {
187
+ width: window.innerWidth,
188
+ height: window.innerHeight
189
+ };
190
+
191
+ // Scroll info
192
+ const scroll = {
193
+ x: window.pageXOffset,
194
+ y: window.pageYOffset,
195
+ maxX: documentWidth - viewport.width,
196
+ maxY: documentHeight - viewport.height
197
+ };
198
+
199
+ // Element positions for key elements
200
+ const elements = [];
201
+ const selectors = ['main', 'header', 'nav', 'aside', 'footer', '.container', '#content'];
202
+
203
+ selectors.forEach(selector => {
204
+ const el = document.querySelector(selector);
205
+ if (el) {
206
+ const rect = el.getBoundingClientRect();
207
+ elements.push({
208
+ selector: selector,
209
+ x: rect.x,
210
+ y: rect.y,
211
+ width: rect.width,
212
+ height: rect.height,
213
+ top: rect.top,
214
+ left: rect.left
215
+ });
216
+ }
217
+ });
218
+
219
+ return {
220
+ document: {
221
+ width: documentWidth,
222
+ height: documentHeight
223
+ },
224
+ viewport: viewport,
225
+ scroll: scroll,
226
+ elements: elements
227
+ };
228
+ }
229
+ """)
230
+
231
+ return metrics
232
+
233
+ except Exception as e:
234
+ self.logger.error(f"Layout metrics capture failed: {e}")
235
+ return {}
236
+
237
+ async def _capture_computed_styles(self, page) -> Dict[str, Any]:
238
+ """Capture computed styles for key elements"""
239
+ try:
240
+ styles = await page.evaluate("""
241
+ () => {
242
+ const elements = {};
243
+
244
+ // Key selectors to monitor
245
+ const selectors = [
246
+ 'body', 'main', 'header', 'nav', 'aside', 'footer',
247
+ '.container', '.content', '.sidebar', '.wrapper',
248
+ '#main', '#content', '#sidebar'
249
+ ];
250
+
251
+ selectors.forEach(selector => {
252
+ const el = document.querySelector(selector);
253
+ if (el) {
254
+ const computed = window.getComputedStyle(el);
255
+ elements[selector] = {
256
+ display: computed.display,
257
+ position: computed.position,
258
+ flexDirection: computed.flexDirection,
259
+ justifyContent: computed.justifyContent,
260
+ alignItems: computed.alignItems,
261
+ gridTemplateColumns: computed.gridTemplateColumns,
262
+ gridTemplateRows: computed.gridTemplateRows,
263
+ width: computed.width,
264
+ height: computed.height,
265
+ margin: computed.margin,
266
+ padding: computed.padding,
267
+ backgroundColor: computed.backgroundColor,
268
+ color: computed.color,
269
+ fontSize: computed.fontSize,
270
+ lineHeight: computed.lineHeight
271
+ };
272
+ }
273
+ });
274
+
275
+ return elements;
276
+ }
277
+ """)
278
+
279
+ return styles
280
+
281
+ except Exception as e:
282
+ self.logger.error(f"Computed styles capture failed: {e}")
283
+ return {}
284
+
285
+ async def _capture_performance_metrics(self, page) -> Dict[str, Any]:
286
+ """Capture performance metrics"""
287
+ try:
288
+ metrics = await page.evaluate("""
289
+ () => {
290
+ const perf = performance;
291
+ const navigation = perf.getEntriesByType('navigation')[0];
292
+ const paint = perf.getEntriesByType('paint');
293
+
294
+ return {
295
+ renderTime: navigation ? navigation.loadEventEnd - navigation.loadEventStart : 0,
296
+ domContentLoaded: navigation ? navigation.domContentLoadedEventEnd - navigation.navigationStart : 0,
297
+ firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
298
+ firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
299
+ layoutShifts: perf.getEntriesByType('layout-shift').length
300
+ };
301
+ }
302
+ """)
303
+
304
+ return metrics
305
+
306
+ except Exception as e:
307
+ self.logger.error(f"Performance metrics capture failed: {e}")
308
+ return {}
309
+
310
+ async def _capture_console_errors(self, page) -> List[Dict]:
311
+ """Capture any console errors that occurred"""
312
+ # This would be captured by the browser controller's console monitoring
313
+ # For now, return empty - browser controller handles console errors
314
+ return []
315
+
316
+ def _calculate_layout_differences(self, baseline: Dict, current: Dict) -> Dict[str, Any]:
317
+ """Calculate simple layout differences - NO ANALYSIS"""
318
+ differences = {}
319
+
320
+ # Document size changes
321
+ baseline_doc = baseline.get("document", {})
322
+ current_doc = current.get("document", {})
323
+
324
+ if baseline_doc and current_doc:
325
+ differences["document_size"] = {
326
+ "width_change": current_doc.get("width", 0) - baseline_doc.get("width", 0),
327
+ "height_change": current_doc.get("height", 0) - baseline_doc.get("height", 0)
328
+ }
329
+
330
+ # Element position changes
331
+ baseline_elements = {el["selector"]: el for el in baseline.get("elements", [])}
332
+ current_elements = {el["selector"]: el for el in current.get("elements", [])}
333
+
334
+ element_changes = {}
335
+ for selector in set(baseline_elements.keys()) | set(current_elements.keys()):
336
+ baseline_el = baseline_elements.get(selector, {})
337
+ current_el = current_elements.get(selector, {})
338
+
339
+ if baseline_el and current_el:
340
+ element_changes[selector] = {
341
+ "x_change": current_el.get("x", 0) - baseline_el.get("x", 0),
342
+ "y_change": current_el.get("y", 0) - baseline_el.get("y", 0),
343
+ "width_change": current_el.get("width", 0) - baseline_el.get("width", 0),
344
+ "height_change": current_el.get("height", 0) - baseline_el.get("height", 0)
345
+ }
346
+
347
+ differences["elements"] = element_changes
348
+ return differences
349
+
350
+ def _calculate_style_differences(self, baseline: Dict, current: Dict) -> Dict[str, Any]:
351
+ """Calculate simple style differences - NO ANALYSIS"""
352
+ differences = {}
353
+
354
+ all_selectors = set(baseline.keys()) | set(current.keys())
355
+
356
+ for selector in all_selectors:
357
+ baseline_styles = baseline.get(selector, {})
358
+ current_styles = current.get(selector, {})
359
+
360
+ selector_changes = {}
361
+ all_properties = set(baseline_styles.keys()) | set(current_styles.keys())
362
+
363
+ for prop in all_properties:
364
+ baseline_value = baseline_styles.get(prop, "")
365
+ current_value = current_styles.get(prop, "")
366
+
367
+ if baseline_value != current_value:
368
+ selector_changes[prop] = {
369
+ "from": baseline_value,
370
+ "to": current_value
371
+ }
372
+
373
+ if selector_changes:
374
+ differences[selector] = selector_changes
375
+
376
+ return differences
377
+
378
+ def create_comparison_summary(self, baseline: Dict, iterations: List[Dict]) -> Dict[str, Any]:
379
+ """Create simple comparison data for Cursor analysis"""
380
+ return {
381
+ "baseline": {
382
+ "screenshot": baseline.get("screenshot"),
383
+ "timestamp": baseline.get("timestamp")
384
+ },
385
+ "iterations": [
386
+ {
387
+ "name": iteration.get("name"),
388
+ "screenshot": iteration.get("screenshot"),
389
+ "css_applied": iteration.get("css_applied"),
390
+ "has_console_errors": len(iteration.get("console_errors", [])) > 0,
391
+ "layout_changed": bool(iteration.get("changes", {}).get("layout_differences", {})),
392
+ "styles_changed": bool(iteration.get("changes", {}).get("style_differences", {}))
393
+ }
394
+ for iteration in iterations
395
+ ],
396
+ "total_iterations": len(iterations)
397
+ }