cssprobe-cli 0.2.13 → 0.2.15

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.
@@ -267,17 +267,7 @@ async function handleSessionCommand(commandName, command, args, output) {
267
267
  if (args.json)
268
268
  runArgs.json = true;
269
269
  const result = await session.run(runArgs);
270
- if (output.json) {
271
- try {
272
- console.log(JSON.parse(result.text));
273
- }
274
- catch {
275
- console.log(result.text);
276
- }
277
- }
278
- else {
279
- console.log(result.text);
280
- }
270
+ console.log(result.text);
281
271
  }
282
272
  catch (e) {
283
273
  output.error(e instanceof Error ? e.message : String(e));
@@ -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",
@@ -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 children = node.children.filter(c => !(c.metrics.rect.width < 1 && c.metrics.rect.height < 1));
44
- if (children.length === 0) {
45
- boxLines.push(`${utils_1.BOX.v}${' '.repeat(innerW)}${utils_1.BOX.v}`);
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
- else if (flat) {
48
- for (const child of children) {
49
- const childLabel = (0, utils_1.nodeLabel)(child);
50
- const childRepeat = child.repeat && child.repeat > 1 ? ` \u00D7${child.repeat}` : '';
51
- const childDim = `${Math.round(child.metrics.rect.width)}\u00D7${Math.round(child.metrics.rect.height)}`;
52
- const childLine = ` ${childLabel}${childRepeat} ${childDim} `;
53
- if (childLine.length <= innerW) {
54
- boxLines.push(`${utils_1.BOX.v}${childLine}${' '.repeat(innerW - childLine.length)}${utils_1.BOX.v}`);
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
- else {
57
- boxLines.push(`${utils_1.BOX.v}${childLine.slice(0, innerW)}${utils_1.BOX.v}`);
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 if ((0, utils_1.isMultiColumn)(node)) {
62
- // Calculate expanded count (considering repeat)
63
- const expandedCount = children.reduce((sum, c) => sum + (c.repeat && c.repeat > 1 ? c.repeat : 1), 0);
64
- if (expandedCount > 1) {
65
- const isGrid = node.props.display === 'grid' || node.props.display === 'inline-grid';
66
- const childLines = renderFlexChildren(children, innerW, depth + 1, m.width, isGrid);
67
- for (const cl of childLines) {
68
- boxLines.push(`${utils_1.BOX.v}${cl}${utils_1.BOX.v}`);
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
  }
@@ -49,6 +49,7 @@ export interface NodeProps {
49
49
  zIndex?: string;
50
50
  margin?: string;
51
51
  padding?: string;
52
+ float?: string;
52
53
  transform?: string;
53
54
  filter?: string;
54
55
  perspective?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cssprobe-cli",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "description": "Runtime CSS probe — inspect layout, colors, backgrounds, fonts and more in a live browser. AI agents and shell automation.",
5
5
  "keywords": [
6
6
  "css",