cssprobe-cli 0.2.14 → 0.2.16
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/README.md +106 -189
- package/dist/collector-bundle.js +3 -0
- package/dist/engine/collector.js +4 -1
- package/dist/engine/renderers/layout.js +270 -39
- package/dist/engine/types.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,22 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
Runtime CSS probe — inspect layout, scroll, colors, backgrounds, fonts and more in a live browser. Built for both humans and AI agents.
|
|
4
4
|
|
|
5
|
+
[](https://opensource.org/licenses/MIT) [](https://nodejs.org/)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Why cssprobe-cli?
|
|
10
|
+
|
|
11
|
+
When an AI agent needs to debug CSS — why is this element overflowing, what's the actual rendered layout, is this container scrollable — it can't see the browser. It guesses from static code, missing runtime-only issues like overflow, scroll chains, and containing block hijacks.
|
|
12
|
+
|
|
13
|
+
**cssprobe-cli gives the agent eyes.** It opens a real browser, collects computed styles and metrics via injected JavaScript, analyzes them with a confidence model, and returns structured findings — all in one command. The agent asks a question about any selector and gets back the DOM tree, ASCII layout diagram, overflow warnings, and scroll diagnosis without leaving the terminal.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
5
17
|
## Installation
|
|
6
18
|
|
|
7
19
|
### For Humans
|
|
8
20
|
|
|
9
|
-
|
|
10
|
-
|
|
21
|
+
Copy and paste this prompt to your LLM agent (Claude Code, Cursor, Codex, etc.):
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Install and configure cssprobe-cli by following the instructions here:
|
|
25
|
+
https://raw.githubusercontent.com/mack-peng/cssprobe-cli/main/docs/guide/installation.md
|
|
11
26
|
```
|
|
12
27
|
|
|
28
|
+
Or read the [Installation Guide](docs/guide/installation.md), but seriously, let an agent do it. Humans fat-finger configs.
|
|
29
|
+
|
|
13
30
|
### For LLM Agents
|
|
14
31
|
|
|
32
|
+
Fetch the installation guide and follow it:
|
|
33
|
+
|
|
15
34
|
```bash
|
|
35
|
+
curl -s https://raw.githubusercontent.com/mack-peng/cssprobe-cli/main/docs/guide/installation.md
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or install directly:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g cssprobe-cli
|
|
16
42
|
cssprobe-cli skill-install
|
|
17
43
|
```
|
|
18
44
|
|
|
19
45
|
---
|
|
20
46
|
|
|
47
|
+
## How It Works
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
┌───────────────────────────────────────────────────────────┐
|
|
51
|
+
│ AI Agent │
|
|
52
|
+
│ │
|
|
53
|
+
│ "Is .sidebar overflowing?" │
|
|
54
|
+
│ calls: cssprobe-cli inspect .sidebar │
|
|
55
|
+
│ │ │
|
|
56
|
+
└─────────────────────────────┬─────────────────────────────┘
|
|
57
|
+
│
|
|
58
|
+
▼
|
|
59
|
+
┌───────────────────────────────────────────────────────────┐
|
|
60
|
+
│ cssprobe-cli daemon │
|
|
61
|
+
│ │
|
|
62
|
+
│ collector (IIFE) → analyzer (pure) → renderer (md/json) │
|
|
63
|
+
│ │ │
|
|
64
|
+
│ ▼ │
|
|
65
|
+
│ Playwright browser (Chromium) │
|
|
66
|
+
│ getBoundingClientRect · getComputedStyle · DOM walk │
|
|
67
|
+
└───────────────────────────────────────────────────────────┘
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
1. **Session** — `open` spawns a daemon process managing a browser. Commands connect via Unix socket.
|
|
71
|
+
2. **Collector** — Injected as IIFE into the page. Walks the DOM, reads computed styles, declared values, and element metrics.
|
|
72
|
+
3. **Analyzer** — Pure Node.js functions. Classifies findings with confidence levels (DEFINITE / INDEFINITE / UNVERIFIABLE).
|
|
73
|
+
4. **Renderer** — Outputs Markdown (default) or JSON. Includes ASCII layout diagram, DOM tree, and findings.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
21
77
|
## Quick Start
|
|
22
78
|
|
|
23
79
|
### 1. Inspect a Page
|
|
@@ -26,15 +82,9 @@ cssprobe-cli skill-install
|
|
|
26
82
|
# Open browser (non-blocking, starts daemon session)
|
|
27
83
|
cssprobe-cli open https://getbootstrap.com/docs/5.3/examples/checkout
|
|
28
84
|
|
|
29
|
-
# Open with custom viewport
|
|
30
|
-
cssprobe-cli open https://example.com --viewport 1280x720
|
|
31
|
-
|
|
32
85
|
# Inspect with CSS selector
|
|
33
86
|
cssprobe-cli inspect .container
|
|
34
87
|
|
|
35
|
-
# Show DOM tree
|
|
36
|
-
cssprobe-cli tree .container --depth 3
|
|
37
|
-
|
|
38
88
|
# Show ASCII layout diagram
|
|
39
89
|
cssprobe-cli layout .container
|
|
40
90
|
|
|
@@ -53,65 +103,18 @@ cssprobe-cli open https://mysite.com --headed
|
|
|
53
103
|
|
|
54
104
|
# User manually logs in...
|
|
55
105
|
|
|
56
|
-
#
|
|
57
|
-
cssprobe-cli
|
|
58
|
-
|
|
59
|
-
# Inject CSS to test changes
|
|
60
|
-
cssprobe-cli inject-css ".sidebar { background: #f0f0f0; }"
|
|
61
|
-
|
|
62
|
-
# Take screenshot
|
|
63
|
-
cssprobe-cli screenshot
|
|
106
|
+
# Save session state for future use
|
|
107
|
+
cssprobe-cli playwright "page.context().storageState({path: 'state.json'})"
|
|
64
108
|
|
|
65
|
-
#
|
|
66
|
-
cssprobe-cli
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### 3. Import Cookies
|
|
70
|
-
|
|
71
|
-
```bash
|
|
72
|
-
# Import cookies from browser export (Netscape format)
|
|
73
|
-
cssprobe-cli state-import cookies.txt --out ~/.cssprobe-cli/states/mysite.json
|
|
74
|
-
|
|
75
|
-
# Or use --name for simpler syntax
|
|
76
|
-
cssprobe-cli state-import cookies.txt --name mysite
|
|
77
|
-
|
|
78
|
-
# Open browser with saved state
|
|
79
|
-
cssprobe-cli open https://mysite.com --state ~/.cssprobe-cli/states/mysite.json
|
|
109
|
+
# Reopen with saved state (no login needed)
|
|
110
|
+
cssprobe-cli open https://mysite.com --state state.json --headed
|
|
80
111
|
```
|
|
81
112
|
|
|
82
|
-
###
|
|
113
|
+
### 3. JSON Output
|
|
83
114
|
|
|
84
115
|
```bash
|
|
85
|
-
cssprobe-cli open https://getbootstrap.com/docs/5.3/examples/checkout
|
|
86
116
|
cssprobe-cli inspect body --json
|
|
87
117
|
cssprobe-cli inspect body --json | jq '.findings[] | {id, confidence, message}'
|
|
88
|
-
cssprobe-cli close
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
---
|
|
92
|
-
|
|
93
|
-
## Session Mode
|
|
94
|
-
|
|
95
|
-
cssprobe-cli uses a session-based architecture for non-browser interaction:
|
|
96
|
-
|
|
97
|
-
```bash
|
|
98
|
-
# Start session (opens browser, returns immediately)
|
|
99
|
-
cssprobe-cli open <url>
|
|
100
|
-
|
|
101
|
-
# Run commands (each command is independent, non-blocking)
|
|
102
|
-
cssprobe-cli inspect <selector>
|
|
103
|
-
cssprobe-cli tree <selector>
|
|
104
|
-
cssprobe-cli layout <selector>
|
|
105
|
-
cssprobe-cli findings <selector>
|
|
106
|
-
cssprobe-cli inject-css <css>
|
|
107
|
-
cssprobe-cli screenshot
|
|
108
|
-
cssprobe-cli eval <expression>
|
|
109
|
-
|
|
110
|
-
# Check session status
|
|
111
|
-
cssprobe-cli status
|
|
112
|
-
|
|
113
|
-
# End session (closes browser)
|
|
114
|
-
cssprobe-cli close
|
|
115
118
|
```
|
|
116
119
|
|
|
117
120
|
---
|
|
@@ -120,122 +123,58 @@ cssprobe-cli close
|
|
|
120
123
|
|
|
121
124
|
### Session Management
|
|
122
125
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
| Command | Description |
|
|
127
|
+
|---------|-------------|
|
|
128
|
+
| `open [url]` | Open browser in session mode (non-blocking) |
|
|
129
|
+
| `open [url] --headed` | Show browser window |
|
|
130
|
+
| `open [url] --state <file>` | Open with saved cookies + localStorage |
|
|
131
|
+
| `open [url] --viewport 1280x720` | Custom viewport size |
|
|
132
|
+
| `close` | Close browser session |
|
|
133
|
+
| `status` | Show session status |
|
|
130
134
|
|
|
131
135
|
### CSS Inspection
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
| Command | Description |
|
|
138
|
+
|---------|-------------|
|
|
139
|
+
| `inspect <selector>` | Full CSS diagnosis (computed values, declared values, findings) |
|
|
140
|
+
| `inspect <selector> --layout` | Include ASCII layout diagram |
|
|
141
|
+
| `inspect <selector> --brief` | Compact: tree sketch + warnings/errors only |
|
|
142
|
+
| `inspect <selector> --json` | Structured JSON output |
|
|
143
|
+
| `tree <selector>` | DOM tree structure |
|
|
144
|
+
| `layout <selector>` | ASCII layout diagram |
|
|
145
|
+
| `findings <selector>` | Only issues/warnings/errors |
|
|
139
146
|
|
|
140
147
|
### CSS Injection
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
| Command | Description |
|
|
150
|
+
|---------|-------------|
|
|
151
|
+
| `inject-css <css>` | Inject CSS into current page |
|
|
145
152
|
|
|
146
153
|
### Browser
|
|
147
154
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
| Command | Description |
|
|
156
|
+
|---------|-------------|
|
|
157
|
+
| `resize <width> <height>` | Resize browser viewport |
|
|
158
|
+
| `eval <expression>` | Evaluate JavaScript (browser context) |
|
|
159
|
+
| `playwright <call>` | Execute Playwright API (Node.js context) |
|
|
160
|
+
| `screenshot` | Take screenshot |
|
|
154
161
|
|
|
155
162
|
### State
|
|
156
163
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
| Command | Description |
|
|
165
|
+
|---------|-------------|
|
|
166
|
+
| `state-import <file>` | Import cookies from Netscape format |
|
|
160
167
|
|
|
161
168
|
### Configuration
|
|
162
169
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
---
|
|
173
|
-
|
|
174
|
-
## inspect Options
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
cssprobe-cli inspect <selector> [options]
|
|
178
|
-
|
|
179
|
-
Arguments:
|
|
180
|
-
<selector> CSS selector for root element
|
|
181
|
-
|
|
182
|
-
Options:
|
|
183
|
-
--json Output structured JSON
|
|
184
|
-
--brief Compact output: tree sketch + warnings/errors only
|
|
185
|
-
--layout ASCII layout diagram
|
|
186
|
-
--depth <n> DOM tree depth (default: auto, max 20)
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
---
|
|
190
|
-
|
|
191
|
-
## tree Options
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
cssprobe-cli tree <selector> [options]
|
|
195
|
-
|
|
196
|
-
Arguments:
|
|
197
|
-
<selector> CSS selector for root element
|
|
198
|
-
|
|
199
|
-
Options:
|
|
200
|
-
--depth <n> Tree depth (default: auto, max 20)
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
---
|
|
204
|
-
|
|
205
|
-
## inject-css
|
|
206
|
-
|
|
207
|
-
```bash
|
|
208
|
-
cssprobe-cli inject-css <css>
|
|
209
|
-
|
|
210
|
-
Arguments:
|
|
211
|
-
<css> CSS code to inject into the current page
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
---
|
|
215
|
-
|
|
216
|
-
## state-import
|
|
217
|
-
|
|
218
|
-
Import cookies from Netscape format (browser export standard) into a Playwright state file.
|
|
219
|
-
|
|
220
|
-
```bash
|
|
221
|
-
# From file
|
|
222
|
-
cssprobe-cli state-import cookies.txt --out mystate.json
|
|
223
|
-
|
|
224
|
-
# From file with custom name (saves to ~/.cssprobe-cli/states/<name>.json)
|
|
225
|
-
cssprobe-cli state-import cookies.txt --name mysite
|
|
226
|
-
|
|
227
|
-
# From stdin
|
|
228
|
-
cat cookies.txt | cssprobe-cli state-import --out mystate.json
|
|
229
|
-
|
|
230
|
-
# Merge into existing state
|
|
231
|
-
cssprobe-cli state-import new-cookies.txt --merge existing.json --out merged.json
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
Netscape format (tab-separated):
|
|
235
|
-
```
|
|
236
|
-
.example.com TRUE / TRUE 1813025057 session_id abc123
|
|
237
|
-
.example.com TRUE / FALSE -1 lang zh-CN
|
|
238
|
-
```
|
|
170
|
+
| Command | Description |
|
|
171
|
+
|---------|-------------|
|
|
172
|
+
| `config-show` | Show current config |
|
|
173
|
+
| `config-set <key> <value>` | Set config value |
|
|
174
|
+
| `config-list` | List all profiles |
|
|
175
|
+
| `config-use <name>` | Switch active profile |
|
|
176
|
+
| `config-new <name>` | Create new profile |
|
|
177
|
+
| `config-path` | Show config file path |
|
|
239
178
|
|
|
240
179
|
---
|
|
241
180
|
|
|
@@ -261,14 +200,10 @@ cssprobe-cli config-set browser chromium
|
|
|
261
200
|
cssprobe-cli config-set depth 8
|
|
262
201
|
cssprobe-cli config-set viewport 375x812
|
|
263
202
|
|
|
264
|
-
# Show current config
|
|
265
|
-
cssprobe-cli config-show
|
|
266
|
-
|
|
267
203
|
# Profile management
|
|
268
204
|
cssprobe-cli config-new staging
|
|
269
205
|
cssprobe-cli -p staging config-set browser firefox
|
|
270
206
|
cssprobe-cli config-use staging
|
|
271
|
-
cssprobe-cli config-list
|
|
272
207
|
```
|
|
273
208
|
|
|
274
209
|
Priority: CLI flags > Environment variables > Config file
|
|
@@ -285,31 +220,13 @@ Config file: `~/.cssprobe-clirc`
|
|
|
285
220
|
|
|
286
221
|
---
|
|
287
222
|
|
|
288
|
-
##
|
|
289
|
-
|
|
290
|
-
```bash
|
|
291
|
-
# Install skill for all detected agents
|
|
292
|
-
cssprobe-cli skill-install
|
|
293
|
-
|
|
294
|
-
# Install for a specific agent
|
|
295
|
-
cssprobe-cli skill-install --target opencode
|
|
296
|
-
cssprobe-cli skill-install --target claude
|
|
297
|
-
|
|
298
|
-
# Remove installed skills
|
|
299
|
-
cssprobe-cli skill-uninstall
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
---
|
|
303
|
-
|
|
304
|
-
## Global Options
|
|
223
|
+
## Supported Platforms
|
|
305
224
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
-p, --profile Use named config profile
|
|
312
|
-
```
|
|
225
|
+
| Platform | Architectures |
|
|
226
|
+
|----------|---------------|
|
|
227
|
+
| macOS | x64, arm64 |
|
|
228
|
+
| Linux | x64, arm64 |
|
|
229
|
+
| Windows | x64, arm64 |
|
|
313
230
|
|
|
314
231
|
---
|
|
315
232
|
|
package/dist/collector-bundle.js
CHANGED
|
@@ -161,6 +161,7 @@ var __cssprobe_cli = (() => {
|
|
|
161
161
|
const display = cs.display;
|
|
162
162
|
const position = cs.position;
|
|
163
163
|
const flexDir = cs.flexDirection;
|
|
164
|
+
const floatVal = cs.float;
|
|
164
165
|
const overflowY = cs.overflowY;
|
|
165
166
|
const overflowX = cs.overflowX;
|
|
166
167
|
let role = "block";
|
|
@@ -168,6 +169,7 @@ var __cssprobe_cli = (() => {
|
|
|
168
169
|
else if (position === "absolute") role = "absolute";
|
|
169
170
|
else if (position === "sticky") role = "sticky";
|
|
170
171
|
else if (display === "grid" || display === "inline-grid") role = "grid";
|
|
172
|
+
else if (floatVal && floatVal !== "none") role = "float";
|
|
171
173
|
else if (display === "flex" || display === "inline-flex") {
|
|
172
174
|
role = flexDir === "column" ? "flex-col" : "flex-row";
|
|
173
175
|
} else if (display === "inline" || display === "inline-block") role = "inline";
|
|
@@ -216,6 +218,7 @@ var __cssprobe_cli = (() => {
|
|
|
216
218
|
"boxSizing",
|
|
217
219
|
"margin",
|
|
218
220
|
"padding",
|
|
221
|
+
"float",
|
|
219
222
|
"flexDirection",
|
|
220
223
|
"flexGrow",
|
|
221
224
|
"flexShrink",
|
package/dist/engine/collector.js
CHANGED
|
@@ -79,6 +79,7 @@ function classifyShape(cs, declared) {
|
|
|
79
79
|
const display = cs.display;
|
|
80
80
|
const position = cs.position;
|
|
81
81
|
const flexDir = cs.flexDirection;
|
|
82
|
+
const floatVal = cs.float;
|
|
82
83
|
const overflowY = cs.overflowY;
|
|
83
84
|
const overflowX = cs.overflowX;
|
|
84
85
|
let role = 'block';
|
|
@@ -90,6 +91,8 @@ function classifyShape(cs, declared) {
|
|
|
90
91
|
role = 'sticky';
|
|
91
92
|
else if (display === 'grid' || display === 'inline-grid')
|
|
92
93
|
role = 'grid';
|
|
94
|
+
else if (floatVal && floatVal !== 'none')
|
|
95
|
+
role = 'float';
|
|
93
96
|
else if (display === 'flex' || display === 'inline-flex') {
|
|
94
97
|
role = flexDir === 'column' ? 'flex-col' : 'flex-row';
|
|
95
98
|
}
|
|
@@ -139,7 +142,7 @@ function collectNode(el, maxText, crossOriginBlocked, blockedSheetUrls) {
|
|
|
139
142
|
const props = {};
|
|
140
143
|
const LAYOUT_PROPS = [
|
|
141
144
|
'position', 'display', 'height', 'width', 'minHeight', 'maxHeight',
|
|
142
|
-
'overflowX', 'overflowY', 'boxSizing', 'margin', 'padding',
|
|
145
|
+
'overflowX', 'overflowY', 'boxSizing', 'margin', 'padding', 'float',
|
|
143
146
|
'flexDirection', 'flexGrow', 'flexShrink', 'flexBasis', 'alignItems', 'justifyContent',
|
|
144
147
|
'transform', 'filter', 'perspective', 'willChange', 'contain',
|
|
145
148
|
'zIndex', 'top', 'left', 'right', 'bottom', 'visibility', 'opacity',
|
|
@@ -10,6 +10,7 @@ function renderLayout(snapshot, findings) {
|
|
|
10
10
|
const indentUnit = 2;
|
|
11
11
|
const maxDepth = 8;
|
|
12
12
|
const boxPad = 4;
|
|
13
|
+
const extractedAbs = new Set();
|
|
13
14
|
const issueLocations = new Set();
|
|
14
15
|
for (const f of findings) {
|
|
15
16
|
if (f.location && (f.level === 'warning' || f.level === 'error'))
|
|
@@ -21,13 +22,26 @@ function renderLayout(snapshot, findings) {
|
|
|
21
22
|
const ratio = nodeW / vpW;
|
|
22
23
|
return Math.max(Math.round(available * ratio), 14);
|
|
23
24
|
}
|
|
25
|
+
/** Recursively collect all position:absolute nodes in a subtree. */
|
|
26
|
+
function collectAbsDescendants(node, result) {
|
|
27
|
+
for (const child of node.children) {
|
|
28
|
+
if (child.props.position === 'absolute') {
|
|
29
|
+
result.push(child);
|
|
30
|
+
// Don't recurse into absolute children — they're collected as units
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
// Recurse into non-absolute children to find deeper absolute descendants
|
|
34
|
+
collectAbsDescendants(child, result);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
24
38
|
const lines = [];
|
|
25
|
-
function renderBox(node, charW, depth, flat = false) {
|
|
39
|
+
function renderBox(node, charW, depth, flat = false, absMark = '') {
|
|
26
40
|
const m = node.metrics.rect;
|
|
27
41
|
const dimStr = `${Math.round(m.width)}\u00D7${Math.round(m.height)}`;
|
|
28
42
|
const innerW = Math.max(charW - 2, 1);
|
|
29
|
-
const label = (0, utils_1.smartLabel)(node, innerW - 4);
|
|
30
|
-
const hasIssue = issueLocations.has(label);
|
|
43
|
+
const label = absMark + (0, utils_1.smartLabel)(node, innerW - 4 - absMark.length);
|
|
44
|
+
const hasIssue = issueLocations.has(label.replace(absMark, ''));
|
|
31
45
|
const issueMark = hasIssue ? ' \u26A0' : '';
|
|
32
46
|
const repeat = node.repeat && node.repeat > 1 ? node.repeat : 1;
|
|
33
47
|
const boxLines = [];
|
|
@@ -39,33 +53,264 @@ function renderLayout(snapshot, findings) {
|
|
|
39
53
|
const dimPart = ` ${dimStr} `;
|
|
40
54
|
const topFillLen = Math.max(0, innerW - labelPart.length - dimPart.length);
|
|
41
55
|
boxLines.push(`${utils_1.BOX.tl}${utils_1.BOX.h}${labelPart}${utils_1.BOX.h.repeat(topFillLen)}${utils_1.BOX.tr}${dimPart}${issueMark}`);
|
|
42
|
-
// Children
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
// Children — split into normal flow and absolutely positioned (direct children only)
|
|
57
|
+
const allChildren = node.children.filter(c => !(c.metrics.rect.width < 1 && c.metrics.rect.height < 1));
|
|
58
|
+
const flowChildren = allChildren.filter(c => c.props.position !== 'absolute');
|
|
59
|
+
const absChildren = allChildren.filter(c => c.props.position === 'absolute');
|
|
60
|
+
const hasFloatedChildren = flowChildren.some(c => c.props.float && c.props.float !== 'none');
|
|
61
|
+
// If this node is a containing block, find all absolute descendants for column extraction
|
|
62
|
+
const isContainingBlock = node.props.position !== 'static' || node.containingBlockModifiers.length > 0;
|
|
63
|
+
let extractedDescendants = [];
|
|
64
|
+
if (isContainingBlock && !flat) {
|
|
65
|
+
// Collect from ALL children (not just flow) — absolute children in any subtree
|
|
66
|
+
for (const child of allChildren) {
|
|
67
|
+
if (child.props.position !== 'absolute') {
|
|
68
|
+
collectAbsDescendants(child, extractedDescendants);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Filter out full-width absolute elements (headers, footers, toolbars)
|
|
72
|
+
// These should be rendered inline, not as separate columns
|
|
73
|
+
extractedDescendants = extractedDescendants.filter(abs => {
|
|
74
|
+
return abs.metrics.rect.width < m.width * 0.8;
|
|
75
|
+
});
|
|
76
|
+
for (const abs of extractedDescendants) {
|
|
77
|
+
extractedAbs.add(abs);
|
|
78
|
+
}
|
|
46
79
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
// Merge: all absolute elements (direct + extracted) as columns alongside flow children
|
|
81
|
+
// Mark direct abs children as extracted so they're skipped in nested rendering
|
|
82
|
+
// Filter out full-width absolute elements (they should be rendered inline)
|
|
83
|
+
const filteredAbsChildren = absChildren.filter(abs => abs.metrics.rect.width < m.width * 0.8);
|
|
84
|
+
const filteredExtracted = extractedDescendants.filter(abs => abs.metrics.rect.width < m.width * 0.8);
|
|
85
|
+
for (const abs of filteredAbsChildren)
|
|
86
|
+
extractedAbs.add(abs);
|
|
87
|
+
const allAbsMerged = [...filteredAbsChildren, ...filteredExtracted];
|
|
88
|
+
if (allAbsMerged.length > 0 && !flat) {
|
|
89
|
+
// Group ONLY flow children by horizontal overlap — overlapping flow elements stack in the same column
|
|
90
|
+
const flowGroups = [];
|
|
91
|
+
const sortedFlow = [...flowChildren].sort((a, b) => a.metrics.rect.x - b.metrics.rect.x);
|
|
92
|
+
for (const child of sortedFlow) {
|
|
93
|
+
const cx = child.metrics.rect.x;
|
|
94
|
+
const cr = cx + child.metrics.rect.width;
|
|
95
|
+
let placed = false;
|
|
96
|
+
for (const group of flowGroups) {
|
|
97
|
+
const gx = group[0].metrics.rect.x;
|
|
98
|
+
const gr = group[0].metrics.rect.x + group[0].metrics.rect.width;
|
|
99
|
+
// Check if ranges overlap
|
|
100
|
+
if (cx < gr && gx < cr) {
|
|
101
|
+
group.push(child);
|
|
102
|
+
placed = true;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
55
105
|
}
|
|
56
|
-
|
|
57
|
-
|
|
106
|
+
if (!placed)
|
|
107
|
+
flowGroups.push([child]);
|
|
108
|
+
}
|
|
109
|
+
const columns = [];
|
|
110
|
+
for (const group of flowGroups) {
|
|
111
|
+
const maxW = Math.max(...group.map(c => c.metrics.rect.width));
|
|
112
|
+
// Find the largest marginLeft in the subtree — this indicates reserved space for abs elements
|
|
113
|
+
let maxMarginLeft = 0;
|
|
114
|
+
function findMaxMargin(node) {
|
|
115
|
+
// Parse margin shorthand: "top right bottom left"
|
|
116
|
+
const margin = node.props?.margin || '';
|
|
117
|
+
if (margin) {
|
|
118
|
+
const parts = margin.split(/\s+/);
|
|
119
|
+
if (parts.length === 4) {
|
|
120
|
+
const ml = parseFloat(parts[3]);
|
|
121
|
+
if (!isNaN(ml) && ml > maxMarginLeft)
|
|
122
|
+
maxMarginLeft = ml;
|
|
123
|
+
}
|
|
124
|
+
else if (parts.length === 2) {
|
|
125
|
+
const ml = parseFloat(parts[1]);
|
|
126
|
+
if (!isNaN(ml) && ml > maxMarginLeft)
|
|
127
|
+
maxMarginLeft = ml;
|
|
128
|
+
}
|
|
129
|
+
else if (parts.length === 1) {
|
|
130
|
+
const ml = parseFloat(parts[0]);
|
|
131
|
+
if (!isNaN(ml) && ml > maxMarginLeft)
|
|
132
|
+
maxMarginLeft = ml;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const child of node.children)
|
|
136
|
+
findMaxMargin(child);
|
|
58
137
|
}
|
|
138
|
+
for (const g of group)
|
|
139
|
+
findMaxMargin(g);
|
|
140
|
+
const effectiveX = group[0].metrics.rect.x + maxMarginLeft;
|
|
141
|
+
columns.push({ nodes: group, isAbs: false, width: maxW, sortX: effectiveX });
|
|
142
|
+
}
|
|
143
|
+
for (const abs of allAbsMerged) {
|
|
144
|
+
columns.push({ nodes: [abs], isAbs: true, width: abs.metrics.rect.width, sortX: abs.metrics.rect.x });
|
|
145
|
+
}
|
|
146
|
+
columns.sort((a, b) => a.sortX - b.sortX);
|
|
147
|
+
// Assign character widths proportional to pixel widths
|
|
148
|
+
const totalPx = columns.reduce((sum, c) => sum + c.width, 0);
|
|
149
|
+
const colWidths = [];
|
|
150
|
+
let totalUsed = 0;
|
|
151
|
+
for (let i = 0; i < columns.length; i++) {
|
|
152
|
+
const ratio = columns[i].width / totalPx;
|
|
153
|
+
const w = Math.max(Math.round(innerW * ratio), 14);
|
|
154
|
+
colWidths.push(w);
|
|
155
|
+
totalUsed += w;
|
|
156
|
+
}
|
|
157
|
+
if (colWidths.length > 0 && innerW > totalUsed) {
|
|
158
|
+
colWidths[colWidths.length - 1] += innerW - totalUsed;
|
|
159
|
+
}
|
|
160
|
+
// Render each column
|
|
161
|
+
const colBoxes = [];
|
|
162
|
+
let maxLines = 0;
|
|
163
|
+
for (let i = 0; i < columns.length; i++) {
|
|
164
|
+
const col = columns[i];
|
|
165
|
+
const lines = [];
|
|
166
|
+
for (const node of col.nodes) {
|
|
167
|
+
const mark = col.isAbs ? '[abs] ' : '';
|
|
168
|
+
const box = renderBox(node, colWidths[i], depth + 1, true, mark);
|
|
169
|
+
lines.push(...box);
|
|
170
|
+
}
|
|
171
|
+
colBoxes.push(lines);
|
|
172
|
+
if (lines.length > maxLines)
|
|
173
|
+
maxLines = lines.length;
|
|
174
|
+
}
|
|
175
|
+
// Merge columns side by side
|
|
176
|
+
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
177
|
+
let merged = '';
|
|
178
|
+
for (let i = 0; i < colBoxes.length; i++) {
|
|
179
|
+
const w = colWidths[i];
|
|
180
|
+
if (i > 0)
|
|
181
|
+
merged += ' ';
|
|
182
|
+
if (lineIdx < colBoxes[i].length) {
|
|
183
|
+
const line = colBoxes[i][lineIdx];
|
|
184
|
+
if (line.length < w) {
|
|
185
|
+
merged += line + ' '.repeat(w - line.length);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
merged += line.slice(0, w);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
merged += ' '.repeat(w);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
boxLines.push(`${utils_1.BOX.v}${merged}${utils_1.BOX.v}`);
|
|
59
196
|
}
|
|
60
197
|
}
|
|
61
|
-
else
|
|
62
|
-
//
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
198
|
+
else {
|
|
199
|
+
// Standard rendering — skip extracted absolute descendants
|
|
200
|
+
const children = allChildren.filter(c => !extractedAbs.has(c));
|
|
201
|
+
if (children.length === 0) {
|
|
202
|
+
boxLines.push(`${utils_1.BOX.v}${' '.repeat(innerW)}${utils_1.BOX.v}`);
|
|
203
|
+
}
|
|
204
|
+
else if (flat) {
|
|
205
|
+
for (const child of children) {
|
|
206
|
+
const childLabel = (0, utils_1.nodeLabel)(child);
|
|
207
|
+
const childRepeat = child.repeat && child.repeat > 1 ? ` \u00D7${child.repeat}` : '';
|
|
208
|
+
const childDim = `${Math.round(child.metrics.rect.width)}\u00D7${Math.round(child.metrics.rect.height)}`;
|
|
209
|
+
const childLine = ` ${childLabel}${childRepeat} ${childDim} `;
|
|
210
|
+
if (childLine.length <= innerW) {
|
|
211
|
+
boxLines.push(`${utils_1.BOX.v}${childLine}${' '.repeat(innerW - childLine.length)}${utils_1.BOX.v}`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
boxLines.push(`${utils_1.BOX.v}${childLine.slice(0, innerW)}${utils_1.BOX.v}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
else if ((0, utils_1.isMultiColumn)(node)) {
|
|
219
|
+
const expandedCount = children.reduce((sum, c) => sum + (c.repeat && c.repeat > 1 ? c.repeat : 1), 0);
|
|
220
|
+
if (expandedCount > 1) {
|
|
221
|
+
const isGrid = node.props.display === 'grid' || node.props.display === 'inline-grid';
|
|
222
|
+
const childLines = renderFlexChildren(children, innerW, depth + 1, m.width, isGrid);
|
|
223
|
+
for (const cl of childLines) {
|
|
224
|
+
boxLines.push(`${utils_1.BOX.v}${cl}${utils_1.BOX.v}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
for (const child of children) {
|
|
229
|
+
const childBox = renderBox(child, Math.min(boxCharW(child.metrics.rect.width, depth + 1), innerW - 2), depth + 1);
|
|
230
|
+
for (const cl of childBox) {
|
|
231
|
+
const line = ` ${cl}`;
|
|
232
|
+
if (line.length <= innerW + 1) {
|
|
233
|
+
boxLines.push(`${utils_1.BOX.v}${line}${' '.repeat(Math.max(0, innerW - line.length + 1))}${utils_1.BOX.v}`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
boxLines.push(`${utils_1.BOX.v}${line.slice(0, innerW)}${utils_1.BOX.v}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else if (hasFloatedChildren) {
|
|
243
|
+
// Float layout — render floated children as columns
|
|
244
|
+
const floated = children.filter(c => c.props.float && c.props.float !== 'none');
|
|
245
|
+
// Group floated children by x position overlap
|
|
246
|
+
const sortedFloated = [...floated].sort((a, b) => a.metrics.rect.x - b.metrics.rect.x);
|
|
247
|
+
const floatGroups = [];
|
|
248
|
+
for (const child of sortedFloated) {
|
|
249
|
+
const cx = child.metrics.rect.x;
|
|
250
|
+
const cr = cx + child.metrics.rect.width;
|
|
251
|
+
let placed = false;
|
|
252
|
+
for (const group of floatGroups) {
|
|
253
|
+
const gx = group[0].metrics.rect.x;
|
|
254
|
+
const gr = group[0].metrics.rect.x + group[0].metrics.rect.width;
|
|
255
|
+
if (cx < gr && gx < cr) {
|
|
256
|
+
group.push(child);
|
|
257
|
+
placed = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!placed)
|
|
262
|
+
floatGroups.push([child]);
|
|
263
|
+
}
|
|
264
|
+
const floatCols = [];
|
|
265
|
+
for (const group of floatGroups) {
|
|
266
|
+
const maxW = Math.max(...group.map(c => c.metrics.rect.width));
|
|
267
|
+
floatCols.push({ nodes: group, width: maxW });
|
|
268
|
+
}
|
|
269
|
+
floatCols.sort((a, b) => a.nodes[0].metrics.rect.x - b.nodes[0].metrics.rect.x);
|
|
270
|
+
const totalPx = floatCols.reduce((sum, c) => sum + c.width, 0);
|
|
271
|
+
const colWidths = [];
|
|
272
|
+
let totalUsed = 0;
|
|
273
|
+
for (let i = 0; i < floatCols.length; i++) {
|
|
274
|
+
const ratio = floatCols[i].width / totalPx;
|
|
275
|
+
const w = Math.max(Math.round(innerW * ratio), 14);
|
|
276
|
+
colWidths.push(w);
|
|
277
|
+
totalUsed += w;
|
|
278
|
+
}
|
|
279
|
+
if (colWidths.length > 0 && innerW > totalUsed) {
|
|
280
|
+
colWidths[colWidths.length - 1] += innerW - totalUsed;
|
|
281
|
+
}
|
|
282
|
+
const colBoxes = [];
|
|
283
|
+
let maxLines = 0;
|
|
284
|
+
for (let i = 0; i < floatCols.length; i++) {
|
|
285
|
+
const lines = [];
|
|
286
|
+
for (const n of floatCols[i].nodes) {
|
|
287
|
+
const box = renderBox(n, colWidths[i], depth + 1, true);
|
|
288
|
+
lines.push(...box);
|
|
289
|
+
}
|
|
290
|
+
colBoxes.push(lines);
|
|
291
|
+
if (lines.length > maxLines)
|
|
292
|
+
maxLines = lines.length;
|
|
293
|
+
}
|
|
294
|
+
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
|
|
295
|
+
let merged = '';
|
|
296
|
+
for (let i = 0; i < colBoxes.length; i++) {
|
|
297
|
+
const w = colWidths[i];
|
|
298
|
+
if (i > 0)
|
|
299
|
+
merged += ' ';
|
|
300
|
+
if (lineIdx < colBoxes[i].length) {
|
|
301
|
+
const line = colBoxes[i][lineIdx];
|
|
302
|
+
if (line.length < w) {
|
|
303
|
+
merged += line + ' '.repeat(w - line.length);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
merged += line.slice(0, w);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
merged += ' '.repeat(w);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
boxLines.push(`${utils_1.BOX.v}${merged}${utils_1.BOX.v}`);
|
|
69
314
|
}
|
|
70
315
|
}
|
|
71
316
|
else {
|
|
@@ -83,20 +328,6 @@ function renderLayout(snapshot, findings) {
|
|
|
83
328
|
}
|
|
84
329
|
}
|
|
85
330
|
}
|
|
86
|
-
else {
|
|
87
|
-
for (const child of children) {
|
|
88
|
-
const childBox = renderBox(child, Math.min(boxCharW(child.metrics.rect.width, depth + 1), innerW - 2), depth + 1);
|
|
89
|
-
for (const cl of childBox) {
|
|
90
|
-
const line = ` ${cl}`;
|
|
91
|
-
if (line.length <= innerW + 1) {
|
|
92
|
-
boxLines.push(`${utils_1.BOX.v}${line}${' '.repeat(Math.max(0, innerW - line.length + 1))}${utils_1.BOX.v}`);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
boxLines.push(`${utils_1.BOX.v}${line.slice(0, innerW)}${utils_1.BOX.v}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
331
|
boxLines.push(`${utils_1.BOX.bl}${utils_1.BOX.h.repeat(innerW)}${utils_1.BOX.br}`);
|
|
101
332
|
return boxLines;
|
|
102
333
|
}
|
package/dist/engine/types.d.ts
CHANGED
package/package.json
CHANGED