linecraft 0.2.0 → 0.2.2

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 (110) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +34 -8
  3. package/lib/component.d.ts +34 -0
  4. package/lib/component.d.ts.map +1 -0
  5. package/lib/component.js +42 -0
  6. package/lib/component.js.map +1 -0
  7. package/lib/components/code-debug.d.ts +35 -0
  8. package/lib/components/code-debug.d.ts.map +1 -0
  9. package/lib/components/code-debug.js +294 -0
  10. package/lib/components/code-debug.js.map +1 -0
  11. package/lib/components/fill.d.ts +15 -0
  12. package/lib/components/fill.d.ts.map +1 -0
  13. package/lib/components/fill.js +37 -0
  14. package/lib/components/fill.js.map +1 -0
  15. package/lib/components/index.d.ts +2 -2
  16. package/lib/components/index.d.ts.map +1 -1
  17. package/lib/components/index.js +2 -2
  18. package/lib/components/index.js.map +1 -1
  19. package/lib/components/progress-bar-grid.d.ts +1 -1
  20. package/lib/components/progress-bar-grid.d.ts.map +1 -1
  21. package/lib/components/progress-bar-grid.js +6 -6
  22. package/lib/components/progress-bar-grid.js.map +1 -1
  23. package/lib/components/prompt.d.ts +4 -5
  24. package/lib/components/prompt.d.ts.map +1 -1
  25. package/lib/components/prompt.js +17 -69
  26. package/lib/components/prompt.js.map +1 -1
  27. package/lib/components/section.d.ts +33 -0
  28. package/lib/components/section.d.ts.map +1 -0
  29. package/lib/components/section.js +178 -0
  30. package/lib/components/section.js.map +1 -0
  31. package/lib/components/segments.d.ts +26 -0
  32. package/lib/components/segments.d.ts.map +1 -0
  33. package/lib/components/segments.js +105 -0
  34. package/lib/components/segments.js.map +1 -0
  35. package/lib/components/spinner.d.ts +18 -16
  36. package/lib/components/spinner.d.ts.map +1 -1
  37. package/lib/components/spinner.js +63 -47
  38. package/lib/components/spinner.js.map +1 -1
  39. package/lib/components/style.test.js +11 -11
  40. package/lib/components/style.test.js.map +1 -1
  41. package/lib/components/styled.d.ts +17 -0
  42. package/lib/components/styled.d.ts.map +1 -0
  43. package/lib/components/styled.js +107 -0
  44. package/lib/components/styled.js.map +1 -0
  45. package/lib/components/styled.test.d.ts +2 -0
  46. package/lib/components/styled.test.d.ts.map +1 -0
  47. package/lib/components/styled.test.js +135 -0
  48. package/lib/components/styled.test.js.map +1 -0
  49. package/lib/index.d.ts +17 -13
  50. package/lib/index.d.ts.map +1 -1
  51. package/lib/index.js +13 -13
  52. package/lib/index.js.map +1 -1
  53. package/lib/index.test.js +17 -11
  54. package/lib/index.test.js.map +1 -1
  55. package/lib/layout/grid.d.ts +31 -35
  56. package/lib/layout/grid.d.ts.map +1 -1
  57. package/lib/layout/grid.js +437 -216
  58. package/lib/layout/grid.js.map +1 -1
  59. package/lib/layout/grid.test.js +332 -36
  60. package/lib/layout/grid.test.js.map +1 -1
  61. package/lib/native/ansi.d.ts +9 -0
  62. package/lib/native/ansi.d.ts.map +1 -1
  63. package/lib/native/ansi.js +9 -0
  64. package/lib/native/ansi.js.map +1 -1
  65. package/lib/native/diff.d.ts +5 -1
  66. package/lib/native/diff.d.ts.map +1 -1
  67. package/lib/native/diff.js +25 -7
  68. package/lib/native/diff.js.map +1 -1
  69. package/lib/native/region-renderer-debug.test.d.ts +2 -0
  70. package/lib/native/region-renderer-debug.test.d.ts.map +1 -0
  71. package/lib/native/region-renderer-debug.test.js +45 -0
  72. package/lib/native/region-renderer-debug.test.js.map +1 -0
  73. package/lib/native/region-renderer.d.ts +57 -148
  74. package/lib/native/region-renderer.d.ts.map +1 -1
  75. package/lib/native/region-renderer.js +455 -1124
  76. package/lib/native/region-renderer.js.map +1 -1
  77. package/lib/native/region.test.js +2 -20
  78. package/lib/native/region.test.js.map +1 -1
  79. package/lib/region-resize.test.d.ts +2 -0
  80. package/lib/region-resize.test.d.ts.map +1 -0
  81. package/lib/region-resize.test.js +124 -0
  82. package/lib/region-resize.test.js.map +1 -0
  83. package/lib/region.d.ts +97 -9
  84. package/lib/region.d.ts.map +1 -1
  85. package/lib/region.js +591 -185
  86. package/lib/region.js.map +1 -1
  87. package/lib/region.test.js +3 -3
  88. package/lib/region.test.js.map +1 -1
  89. package/lib/types.d.ts +9 -0
  90. package/lib/types.d.ts.map +1 -1
  91. package/lib/utils/file-link.d.ts +16 -0
  92. package/lib/utils/file-link.d.ts.map +1 -0
  93. package/lib/utils/file-link.js +23 -0
  94. package/lib/utils/file-link.js.map +1 -0
  95. package/lib/utils/prompt.d.ts +15 -0
  96. package/lib/utils/prompt.d.ts.map +1 -0
  97. package/lib/utils/prompt.js +128 -0
  98. package/lib/utils/prompt.js.map +1 -0
  99. package/lib/utils/terminal-theme.d.ts +36 -0
  100. package/lib/utils/terminal-theme.d.ts.map +1 -0
  101. package/lib/utils/terminal-theme.js +61 -0
  102. package/lib/utils/terminal-theme.js.map +1 -0
  103. package/lib/utils/text.d.ts +53 -3
  104. package/lib/utils/text.d.ts.map +1 -1
  105. package/lib/utils/text.js +194 -36
  106. package/lib/utils/text.js.map +1 -1
  107. package/lib/utils/wait-for-spacebar.d.ts.map +1 -1
  108. package/lib/utils/wait-for-spacebar.js +9 -6
  109. package/lib/utils/wait-for-spacebar.js.map +1 -1
  110. package/package.json +17 -13
@@ -1,17 +1,44 @@
1
1
  // CSS Grid-based layout system for terminal components
2
2
  // Simplified grid that eliminates circular measurement complexity
3
3
  import { applyStyle } from '../utils/colors';
4
+ import { getTrimmedTextWidth, stripAnsi } from '../utils/text';
5
+ import { callComponent, createChildContext } from '../component';
6
+ /**
7
+ * Extract character and color from spaceBetween option for a specific gap index
8
+ * spaceBetween can be a FillChar, or an array of FillChar values
9
+ */
10
+ function getSpaceBetweenChar(spaceBetween, gapIndex) {
11
+ if (typeof spaceBetween === 'string') {
12
+ return { char: spaceBetween };
13
+ }
14
+ if (Array.isArray(spaceBetween)) {
15
+ const item = spaceBetween[gapIndex] ?? spaceBetween[spaceBetween.length - 1];
16
+ if (typeof item === 'string') {
17
+ return { char: item };
18
+ }
19
+ return { char: item.char, color: item.color };
20
+ }
21
+ // Single object
22
+ return { char: spaceBetween.char, color: spaceBetween.color };
23
+ }
4
24
  function parseTemplateEntry(entry) {
5
25
  if (typeof entry === 'number') {
6
26
  return { type: 'fixed', value: entry };
7
27
  }
28
+ if (entry === 'auto') {
29
+ return { type: 'auto', value: 0 };
30
+ }
8
31
  if (typeof entry === 'string') {
32
+ // Shorthand: '*' means '1*'
33
+ if (entry === '*') {
34
+ return { type: 'flex', value: 0, flexRatio: 1 };
35
+ }
9
36
  // Parse flex unit: '1*', '2*', etc.
10
37
  const match = entry.match(/^(\d+)\*$/);
11
38
  if (match) {
12
39
  return { type: 'flex', value: 0, flexRatio: parseInt(match[1], 10) };
13
40
  }
14
- throw new Error(`Invalid flex unit: ${entry}. Use format like '1*', '2*'`);
41
+ throw new Error(`Invalid flex unit: ${entry}. Use format like '*', '1*', '2*'`);
15
42
  }
16
43
  // Minmax: { min: 40, width: '2*' }
17
44
  const match = entry.width.match(/^(\d+)\*$/);
@@ -28,24 +55,44 @@ function parseTemplateEntry(entry) {
28
55
  * Calculate column widths from template
29
56
  * Reference: https://www.w3.org/TR/css-grid-1/#track-sizing
30
57
  */
31
- function calculateColumnWidths(template, availableWidth, columnGap, numChildren) {
32
- // Expand template if needed (repeat last value)
33
- const expandedTemplate = [];
34
- for (let i = 0; i < numChildren; i++) {
35
- if (i < template.length) {
36
- expandedTemplate.push(template[i]);
58
+ function expandTemplate(columns, autoColumns, template, count) {
59
+ // Priority: if columns/autoColumns are specified, use those (new API)
60
+ // Otherwise, if template is specified, use it where last value acts as autoColumns
61
+ // Otherwise, default to empty columns with '1*' autoColumns
62
+ let explicitColumns;
63
+ let autoColumn;
64
+ if (columns !== undefined || autoColumns !== undefined) {
65
+ // New API: explicit columns and/or autoColumns
66
+ explicitColumns = columns ?? [];
67
+ autoColumn = autoColumns ?? '1*';
68
+ }
69
+ else if (template !== undefined) {
70
+ // Deprecated API: template where last value acts as autoColumns
71
+ explicitColumns = template;
72
+ autoColumn = template.length > 0 ? template[template.length - 1] : '1*';
73
+ }
74
+ else {
75
+ // No columns specified: default to empty with '1*' autoColumns
76
+ explicitColumns = [];
77
+ autoColumn = '1*';
78
+ }
79
+ const expanded = [];
80
+ for (let i = 0; i < count; i++) {
81
+ if (i < explicitColumns.length) {
82
+ expanded.push(explicitColumns[i]);
37
83
  }
38
84
  else {
39
- // Repeat last template value
40
- expandedTemplate.push(template[template.length - 1] ?? '1*');
85
+ // Apply the same autoColumn value to all implicitly created columns
86
+ // (CSS Grid's grid-auto-columns is a single value applied to all auto columns)
87
+ expanded.push(autoColumn);
41
88
  }
42
89
  }
43
- // If template is empty, use equal flex for all
44
- if (expandedTemplate.length === 0) {
45
- expandedTemplate.push('1*');
90
+ if (expanded.length === 0) {
91
+ expanded.push('1*');
46
92
  }
47
- // Parse all template entries
48
- const tracks = expandedTemplate.map(parseTemplateEntry);
93
+ return expanded;
94
+ }
95
+ function calculateColumnWidths(tracks, availableWidth, columnGap, autoContentWidths) {
49
96
  // Step 1: Calculate fixed track sizes
50
97
  let fixedTotal = 0;
51
98
  let flexTotal = 0;
@@ -65,6 +112,11 @@ function calculateColumnWidths(template, availableWidth, columnGap, numChildren)
65
112
  fixedTotal += track.value;
66
113
  flexTotal += track.flexRatio ?? 1;
67
114
  }
115
+ else if (track.type === 'auto') {
116
+ const autoWidth = autoContentWidths[i] ?? 0;
117
+ widths[i] = autoWidth;
118
+ fixedTotal += autoWidth;
119
+ }
68
120
  }
69
121
  // Step 2: Calculate gap space
70
122
  const gapSpace = columnGap * (tracks.length - 1);
@@ -84,172 +136,213 @@ function calculateColumnWidths(template, availableWidth, columnGap, numChildren)
84
136
  }
85
137
  }
86
138
  // Step 4: Round to integers
87
- return widths.map(w => Math.floor(w));
88
- }
89
- /**
90
- * Create a grid component that renders to region
91
- */
92
- export function createGrid(region, options, ...children) {
93
- return {
94
- getHeight() {
95
- // Calculate height by rendering children and checking for multi-line
96
- const { template, columnGap = 0 } = options;
97
- const validChildren = children.filter(c => c !== null && c !== undefined);
98
- if (validChildren.length === 0) {
99
- return 0;
139
+ const rounded = widths.map(w => Math.max(0, Math.floor(w)));
140
+ // Step 5: Clamp total width to available space (minus gaps)
141
+ // CRITICAL: Preserve auto column widths - only adjust flex columns
142
+ const maxContentWidth = Math.max(0, availableWidth - gapSpace);
143
+ let totalRounded = rounded.reduce((sum, w) => sum + w, 0);
144
+ if (totalRounded > maxContentWidth && totalRounded > 0) {
145
+ // Calculate auto/fixed total (these should not be scaled)
146
+ let autoFixedTotal = 0;
147
+ for (let i = 0; i < tracks.length; i++) {
148
+ if (tracks[i].type === 'auto' || tracks[i].type === 'fixed') {
149
+ autoFixedTotal += rounded[i];
100
150
  }
101
- const columnWidths = calculateColumnWidths(template, region.width, columnGap, validChildren.length);
102
- let maxLines = 1;
103
- for (let i = 0; i < validChildren.length; i++) {
104
- const child = validChildren[i];
105
- const width = columnWidths[i] ?? 0;
106
- const childCtx = {
107
- availableWidth: width,
108
- region: region,
109
- columnIndex: i,
110
- rowIndex: 0,
111
- };
112
- const result = child(childCtx);
113
- if (result !== null) {
114
- const lines = Array.isArray(result) ? result.length : 1;
115
- maxLines = Math.max(maxLines, lines);
151
+ }
152
+ // Only scale flex columns if needed
153
+ const flexTotal = totalRounded - autoFixedTotal;
154
+ if (flexTotal > 0) {
155
+ const availableForFlex = Math.max(0, maxContentWidth - autoFixedTotal);
156
+ const scale = availableForFlex / flexTotal;
157
+ let adjustedTotal = autoFixedTotal;
158
+ for (let i = 0; i < rounded.length; i++) {
159
+ if (tracks[i].type === 'flex' || tracks[i].type === 'minmax') {
160
+ rounded[i] = Math.max(0, Math.floor(rounded[i] * scale));
161
+ adjustedTotal += rounded[i];
116
162
  }
117
163
  }
118
- return maxLines;
119
- },
120
- render(x, y, width) {
121
- const { template, columnGap = 0, spaceBetween, justify = 'start' } = options;
122
- // Filter out null children
123
- const validChildren = children.filter(c => c !== null && c !== undefined);
124
- if (validChildren.length === 0) {
125
- return;
126
- }
127
- // Calculate column widths
128
- const columnWidths = calculateColumnWidths(template, width, columnGap, validChildren.length);
129
- // Handle justify: 'space-between'
130
- let startX = x;
131
- let actualWidths = columnWidths;
132
- if (justify === 'space-between' && validChildren.length > 1) {
133
- const firstWidth = columnWidths[0];
134
- const lastWidth = columnWidths[columnWidths.length - 1];
135
- const middleTotal = columnWidths.slice(1, -1).reduce((sum, w) => sum + w, 0);
136
- const middleFlex = width - firstWidth - lastWidth - (columnGap * (validChildren.length - 1));
137
- actualWidths = [firstWidth];
138
- for (let i = 1; i < columnWidths.length - 1; i++) {
139
- const ratio = middleTotal > 0 ? columnWidths[i] / middleTotal : 1 / (columnWidths.length - 2);
140
- actualWidths.push(Math.floor(middleFlex * ratio));
164
+ let remainder = maxContentWidth - adjustedTotal;
165
+ let idx = 0;
166
+ while (remainder > 0 && rounded.length > 0) {
167
+ const targetIndex = idx % rounded.length;
168
+ // Only add to flex columns
169
+ if (tracks[targetIndex].type === 'flex' || tracks[targetIndex].type === 'minmax') {
170
+ rounded[targetIndex] += 1;
171
+ remainder -= 1;
141
172
  }
142
- actualWidths.push(lastWidth);
173
+ idx++;
174
+ if (idx > rounded.length * 10)
175
+ break; // Safety limit
143
176
  }
144
- // Render each child and collect results
145
- const results = [];
146
- let currentX = startX;
147
- for (let i = 0; i < validChildren.length; i++) {
148
- const child = validChildren[i];
149
- const childWidth = actualWidths[i] ?? 0;
150
- // Create context for child
151
- const childCtx = {
152
- availableWidth: childWidth,
153
- region: region,
154
- columnIndex: i,
155
- rowIndex: 0,
156
- };
157
- // Render child
158
- const result = child(childCtx);
159
- results.push(result);
160
- // Handle spaceBetween
161
- if (i < validChildren.length - 1 && spaceBetween) {
162
- const spaceChar = typeof spaceBetween === 'string'
163
- ? spaceBetween
164
- : Array.isArray(spaceBetween)
165
- ? (spaceBetween[i] ?? spaceBetween[spaceBetween.length - 1])
166
- : spaceBetween.char;
167
- const spaceColor = typeof spaceBetween === 'object' && !Array.isArray(spaceBetween)
168
- ? spaceBetween.color
169
- : undefined;
170
- const gapText = spaceChar.repeat(columnGap);
171
- results.push(spaceColor ? applyStyle(gapText, { color: spaceColor }) : gapText);
172
- }
173
- currentX += childWidth + columnGap;
174
- }
175
- // Handle multi-line: if any result is string[], expand grid vertically
176
- const maxLines = Math.max(...results.map(r => Array.isArray(r) ? r.length : 1), 1);
177
- // Render line by line
178
- for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
179
- const lineParts = [];
180
- let partIdx = 0;
181
- for (let i = 0; i < validChildren.length; i++) {
182
- const result = results[partIdx];
183
- partIdx++;
184
- const columnWidth = actualWidths[i] ?? 0;
185
- if (result === null) {
186
- // Null result - pad to column width
187
- lineParts.push(' '.repeat(columnWidth));
188
- continue;
189
- }
190
- let columnContent;
191
- if (Array.isArray(result)) {
192
- columnContent = result[lineIdx] ?? '';
193
- }
194
- else {
195
- columnContent = lineIdx === 0 ? result : '';
177
+ }
178
+ else {
179
+ // No flex columns - scale down fixed and auto columns proportionally if needed
180
+ if (autoFixedTotal > maxContentWidth) {
181
+ const scale = maxContentWidth / autoFixedTotal;
182
+ let adjustedTotal = 0;
183
+ for (let i = 0; i < rounded.length; i++) {
184
+ if (tracks[i].type === 'auto' || tracks[i].type === 'fixed') {
185
+ rounded[i] = Math.max(0, Math.floor(rounded[i] * scale));
186
+ adjustedTotal += rounded[i];
196
187
  }
197
- // CRITICAL: Pad each column to its allocated width
198
- // This ensures flex columns actually fill their allocated space
199
- const plainContent = columnContent.replace(/\x1b\[[0-9;]*m/g, '');
200
- const paddedContent = plainContent.length < columnWidth
201
- ? columnContent + ' '.repeat(columnWidth - plainContent.length)
202
- : columnContent;
203
- lineParts.push(paddedContent);
204
- // Add gap (spaceBetween or spaces) if not last
205
- if (i < validChildren.length - 1 && columnGap > 0) {
206
- if (spaceBetween) {
207
- const spaceChar = typeof spaceBetween === 'string'
208
- ? spaceBetween
209
- : Array.isArray(spaceBetween)
210
- ? (spaceBetween[i] ?? spaceBetween[spaceBetween.length - 1])
211
- : spaceBetween.char;
212
- const spaceColor = typeof spaceBetween === 'object' && !Array.isArray(spaceBetween)
213
- ? spaceBetween.color
214
- : undefined;
215
- const gapText = spaceChar.repeat(columnGap);
216
- lineParts.push(spaceColor ? applyStyle(gapText, { color: spaceColor }) : gapText);
217
- partIdx++; // spaceBetween adds an extra result
218
- }
219
- else {
220
- // Just add spaces for columnGap
221
- lineParts.push(' '.repeat(columnGap));
222
- }
188
+ }
189
+ // Distribute remainder to fixed columns (prefer fixed over auto)
190
+ let remainder = maxContentWidth - adjustedTotal;
191
+ let idx = 0;
192
+ while (remainder > 0 && rounded.length > 0) {
193
+ const targetIndex = idx % rounded.length;
194
+ if (tracks[targetIndex].type === 'fixed' || tracks[targetIndex].type === 'auto') {
195
+ rounded[targetIndex] += 1;
196
+ remainder -= 1;
223
197
  }
198
+ idx++;
199
+ if (idx > rounded.length * 10)
200
+ break; // Safety limit
224
201
  }
225
- const line = lineParts.join('');
226
- const lineY = y + lineIdx;
227
- // CRITICAL: Pad line to full width to ensure grid fills the region
228
- // This ensures grids are always full-width by default
229
- const plainLine = line.replace(/\x1b\[[0-9;]*m/g, '');
230
- const paddedLine = plainLine.length < width
231
- ? line + ' '.repeat(width - plainLine.length)
232
- : line;
233
- // For now, just set the line (we can add merging later if needed)
234
- region.setLine(lineY, paddedLine);
235
202
  }
236
- },
237
- };
203
+ }
204
+ }
205
+ return rounded;
206
+ }
207
+ function getRenderedWidth(result) {
208
+ if (result === null) {
209
+ return 0;
210
+ }
211
+ if (Array.isArray(result)) {
212
+ return result.reduce((max, line) => Math.max(max, getTrimmedTextWidth(line)), 0);
213
+ }
214
+ return getTrimmedTextWidth(result);
215
+ }
216
+ function measureAutoContentWidths(tracks, children, ctxFactory) {
217
+ const widths = new Array(tracks.length).fill(0);
218
+ for (let i = 0; i < tracks.length; i++) {
219
+ if (tracks[i].type !== 'auto') {
220
+ continue;
221
+ }
222
+ const result = children[i] ? callComponent(children[i], ctxFactory(i)) : null;
223
+ widths[i] = getRenderedWidth(result);
224
+ }
225
+ return widths;
238
226
  }
239
227
  /**
240
228
  * Create a grid component (function-based API)
241
229
  * This returns a Component that can be used in other grids
230
+ * Accepts Component children, strings (converted to Styled components), or objects with render methods
242
231
  */
243
232
  export function grid(options, ...children) {
233
+ // Convert children to Components
234
+ const convertedChildren = children.map(child => {
235
+ if (typeof child === 'string') {
236
+ // Import Styled dynamically to avoid circular dependency
237
+ const { Styled } = require('../components/styled');
238
+ return Styled({}, child);
239
+ }
240
+ // If it's an object with a render method, extract the render function
241
+ if (typeof child === 'object' && child !== null && 'render' in child && typeof child.render === 'function') {
242
+ return child.render;
243
+ }
244
+ // Otherwise it's already a Component
245
+ return child;
246
+ });
244
247
  return (ctx) => {
245
- const { template, columnGap = 0, spaceBetween, justify = 'start' } = options;
248
+ const { columns, autoColumns, template, rows, autoRows = 1, columnGap = 0, rowGap = 0, spaceBetween, justify = 'start' } = options;
246
249
  // Filter out null children (from when conditions)
247
- const validChildren = children.filter(c => c !== null && c !== undefined);
250
+ const validChildren = convertedChildren.filter(c => c !== null && c !== undefined);
248
251
  if (validChildren.length === 0) {
249
252
  return null;
250
253
  }
254
+ // Determine number of explicit columns
255
+ const explicitColumns = columns ?? template ?? [];
256
+ const numColumns = explicitColumns.length > 0 ? explicitColumns.length : 1;
257
+ // Group children into rows (wrap when exceeding numColumns)
258
+ const rowsData = [];
259
+ for (let i = 0; i < validChildren.length; i += numColumns) {
260
+ rowsData.push(validChildren.slice(i, i + numColumns));
261
+ }
262
+ // If we have multiple rows, render them separately and stack vertically
263
+ if (rowsData.length > 1) {
264
+ // Calculate column widths based on the first row (all rows use same column widths for alignment)
265
+ const firstRowChildren = rowsData[0] ?? [];
266
+ const expandedTemplate = expandTemplate(columns, autoColumns, template, firstRowChildren.length);
267
+ const tracks = expandedTemplate.map(parseTemplateEntry);
268
+ // Measure content widths for column sizing (use first row as reference)
269
+ const autoContentWidths = measureAutoContentWidths(tracks, firstRowChildren, (index) => createChildContext(ctx, {
270
+ availableWidth: Number.POSITIVE_INFINITY,
271
+ columnIndex: index,
272
+ rowIndex: 0,
273
+ }));
274
+ const columnWidths = calculateColumnWidths(tracks, ctx.availableWidth, columnGap, autoContentWidths);
275
+ // Render each row
276
+ const allRowLines = [];
277
+ for (let rowIdx = 0; rowIdx < rowsData.length; rowIdx++) {
278
+ const rowChildren = rowsData[rowIdx];
279
+ const rowHeight = rows?.[rowIdx] ?? autoRows;
280
+ // Render this row's children
281
+ const rowResults = [];
282
+ for (let colIdx = 0; colIdx < rowChildren.length; colIdx++) {
283
+ const child = rowChildren[colIdx];
284
+ const width = columnWidths[colIdx] ?? 0;
285
+ const childCtx = createChildContext(ctx, {
286
+ availableWidth: width,
287
+ columnIndex: colIdx,
288
+ rowIndex: rowIdx,
289
+ });
290
+ const result = callComponent(child, childCtx);
291
+ rowResults.push(result);
292
+ }
293
+ // Build row lines (handle multi-line children)
294
+ const maxRowLines = Math.max(...rowResults.map(r => Array.isArray(r) ? r.length : 1), rowHeight);
295
+ for (let lineIdx = 0; lineIdx < maxRowLines; lineIdx++) {
296
+ const lineParts = [];
297
+ for (let colIdx = 0; colIdx < rowChildren.length; colIdx++) {
298
+ const result = rowResults[colIdx];
299
+ const columnWidth = columnWidths[colIdx] ?? 0;
300
+ let columnContent;
301
+ if (result === null) {
302
+ columnContent = ' '.repeat(columnWidth);
303
+ }
304
+ else if (Array.isArray(result)) {
305
+ columnContent = result[lineIdx] ?? '';
306
+ }
307
+ else {
308
+ columnContent = lineIdx === 0 ? result : '';
309
+ }
310
+ const plainContent = stripAnsi(columnContent);
311
+ const paddedContent = plainContent.length < columnWidth
312
+ ? columnContent + ' '.repeat(columnWidth - plainContent.length)
313
+ : columnContent;
314
+ lineParts.push(paddedContent);
315
+ // Add column gap
316
+ if (colIdx < rowChildren.length - 1 && columnGap > 0) {
317
+ lineParts.push(' '.repeat(columnGap));
318
+ }
319
+ }
320
+ const line = lineParts.join('');
321
+ const plainLine = stripAnsi(line);
322
+ const paddedLine = plainLine.length < ctx.availableWidth
323
+ ? line + ' '.repeat(ctx.availableWidth - plainLine.length)
324
+ : line;
325
+ allRowLines.push(paddedLine);
326
+ }
327
+ // Add row gap (except after last row)
328
+ if (rowIdx < rowsData.length - 1 && rowGap > 0) {
329
+ for (let i = 0; i < rowGap; i++) {
330
+ allRowLines.push(' '.repeat(ctx.availableWidth));
331
+ }
332
+ }
333
+ }
334
+ return allRowLines;
335
+ }
336
+ // Single row rendering (original logic)
251
337
  // Calculate column widths
252
- const columnWidths = calculateColumnWidths(template, ctx.availableWidth, columnGap, validChildren.length);
338
+ const expandedTemplate = expandTemplate(columns, autoColumns, template, validChildren.length);
339
+ const tracks = expandedTemplate.map(parseTemplateEntry);
340
+ const autoContentWidths = measureAutoContentWidths(tracks, validChildren, (index) => createChildContext(ctx, {
341
+ availableWidth: Number.POSITIVE_INFINITY,
342
+ columnIndex: index,
343
+ rowIndex: 0,
344
+ }));
345
+ const columnWidths = calculateColumnWidths(tracks, ctx.availableWidth, columnGap, autoContentWidths);
253
346
  // Handle justify: 'space-between'
254
347
  let startX = 0;
255
348
  let actualWidths = columnWidths;
@@ -269,31 +362,27 @@ export function grid(options, ...children) {
269
362
  }
270
363
  // Render each child
271
364
  const results = [];
365
+ let anyRenderableChild = false;
272
366
  let currentX = startX;
273
367
  for (let i = 0; i < validChildren.length; i++) {
274
368
  const child = validChildren[i];
275
369
  const width = actualWidths[i] ?? 0;
276
370
  // Create context for child
277
- const childCtx = {
371
+ const childCtx = createChildContext(ctx, {
278
372
  availableWidth: width,
279
- region: ctx.region,
280
373
  columnIndex: i,
281
374
  rowIndex: 0,
282
- };
375
+ });
283
376
  // Render child
284
- const result = child(childCtx);
377
+ const result = callComponent(child, childCtx);
285
378
  results.push(result);
379
+ if (result !== null) {
380
+ anyRenderableChild = true;
381
+ }
286
382
  // Handle gap (spaceBetween or spaces)
287
383
  if (i < validChildren.length - 1 && columnGap > 0) {
288
384
  if (spaceBetween) {
289
- const spaceChar = typeof spaceBetween === 'string'
290
- ? spaceBetween
291
- : Array.isArray(spaceBetween)
292
- ? (spaceBetween[i] ?? spaceBetween[spaceBetween.length - 1])
293
- : spaceBetween.char;
294
- const spaceColor = typeof spaceBetween === 'object' && !Array.isArray(spaceBetween)
295
- ? spaceBetween.color
296
- : undefined;
385
+ const { char: spaceChar, color: spaceColor } = getSpaceBetweenChar(spaceBetween, i);
297
386
  // Fill gap with space character
298
387
  const gapText = spaceChar.repeat(columnGap);
299
388
  results.push(spaceColor ? applyStyle(gapText, { color: spaceColor }) : gapText);
@@ -305,47 +394,149 @@ export function grid(options, ...children) {
305
394
  }
306
395
  currentX += width + columnGap;
307
396
  }
397
+ if (!anyRenderableChild) {
398
+ return null;
399
+ }
308
400
  // Handle multi-line: if any result is string[], expand grid vertically
309
401
  const maxLines = Math.max(...results.map(r => Array.isArray(r) ? r.length : 1), 1);
310
402
  if (maxLines === 1) {
311
- // Single line - join all results with proper column padding
403
+ // Special handling for spaceBetween with auto columns (CSS justify-content: space-between)
404
+ const hasAutoColumns = tracks.some(t => t.type === 'auto');
405
+ const allAuto = tracks.every(t => t.type === 'auto' || t.type === 'flex');
406
+ if (spaceBetween && hasAutoColumns && allAuto) {
407
+ // Special case: spaceBetween with auto columns (CSS justify-content: space-between)
408
+ // Collect all auto column contents (skip gap results in results array)
409
+ const autoContents = [];
410
+ let totalAutoWidth = 0;
411
+ let partIdx = 0;
412
+ // Debug: log what we're working with
413
+ if (process.env.DEBUG_GRID) {
414
+ console.log('DEBUG: spaceBetween with auto columns');
415
+ console.log('DEBUG: validChildren.length:', validChildren.length);
416
+ console.log('DEBUG: results.length:', results.length);
417
+ console.log('DEBUG: tracks:', tracks.map(t => t.type));
418
+ }
419
+ for (let i = 0; i < validChildren.length; i++) {
420
+ const track = tracks[i];
421
+ if (track.type === 'auto') {
422
+ if (process.env.DEBUG_GRID) {
423
+ console.log(`DEBUG: Processing auto column ${i}, partIdx=${partIdx}, results[partIdx]=`, results[partIdx]);
424
+ }
425
+ const result = results[partIdx];
426
+ partIdx++;
427
+ const content = result === null ? '' : (typeof result === 'string' ? result : '');
428
+ const plainContent = stripAnsi(content);
429
+ totalAutoWidth += plainContent.length;
430
+ autoContents.push(content);
431
+ if (process.env.DEBUG_GRID) {
432
+ console.log(`DEBUG: Collected auto column ${i}: "${plainContent}", totalAutoWidth=${totalAutoWidth}, autoContents.length=${autoContents.length}`);
433
+ }
434
+ // Skip gap result if present (spaceBetween adds gap results between columns)
435
+ // Only skip if this is not the last column and there's a gap result
436
+ if (i < validChildren.length - 1 && columnGap > 0 && partIdx < results.length) {
437
+ if (process.env.DEBUG_GRID) {
438
+ console.log(`DEBUG: Skipping gap result at partIdx=${partIdx}`);
439
+ }
440
+ partIdx++; // Skip the gap result
441
+ }
442
+ }
443
+ else {
444
+ // Not an auto column, skip it
445
+ partIdx++;
446
+ if (i < validChildren.length - 1 && columnGap > 0 && partIdx < results.length) {
447
+ partIdx++; // Skip gap if present
448
+ }
449
+ }
450
+ }
451
+ if (process.env.DEBUG_GRID) {
452
+ console.log('DEBUG: Final autoContents.length:', autoContents.length);
453
+ console.log('DEBUG: Final totalAutoWidth:', totalAutoWidth);
454
+ }
455
+ // Calculate spaceBetween fill width
456
+ const spaceBetweenWidth = Math.max(0, ctx.availableWidth - totalAutoWidth);
457
+ const { char: spaceChar, color: spaceColor } = getSpaceBetweenChar(spaceBetween, 0);
458
+ const fillText = spaceChar.repeat(spaceBetweenWidth);
459
+ const spaceBetweenContent = spaceColor ? applyStyle(fillText, { color: spaceColor }) : fillText;
460
+ // Build line: first column + spaceBetween + remaining columns
461
+ const lineParts = [];
462
+ if (autoContents.length > 0) {
463
+ lineParts.push(autoContents[0]);
464
+ if (autoContents.length > 1) {
465
+ lineParts.push(spaceBetweenContent);
466
+ for (let i = 1; i < autoContents.length; i++) {
467
+ lineParts.push(autoContents[i]);
468
+ }
469
+ }
470
+ }
471
+ const line = lineParts.join('');
472
+ // Pad to full width to ensure right column is at the end
473
+ const plainLine = stripAnsi(line);
474
+ const paddedLine = plainLine.length < ctx.availableWidth
475
+ ? line + ' '.repeat(ctx.availableWidth - plainLine.length)
476
+ : line;
477
+ return paddedLine;
478
+ }
479
+ // Standard rendering for other cases
312
480
  const lineParts = [];
313
481
  let partIdx = 0;
314
482
  for (let i = 0; i < validChildren.length; i++) {
315
483
  const result = results[partIdx];
316
484
  partIdx++;
317
485
  const columnWidth = actualWidths[i] ?? 0;
486
+ const track = tracks[i];
487
+ const isAuto = track?.type === 'auto';
488
+ const isFlex = track?.type === 'flex' || track?.type === 'minmax';
318
489
  if (result === null) {
319
490
  // Null result - pad to column width
320
- lineParts.push(' '.repeat(columnWidth));
491
+ // If spaceBetween is set, use it to fill empty columns (especially flex columns)
492
+ if (!isAuto && spaceBetween && columnWidth > 0) {
493
+ const { char: spaceChar, color: spaceColor } = getSpaceBetweenChar(spaceBetween, i);
494
+ const fillText = spaceChar.repeat(columnWidth);
495
+ lineParts.push(spaceColor ? applyStyle(fillText, { color: spaceColor }) : fillText);
496
+ }
497
+ else if (!isAuto) {
498
+ lineParts.push(' '.repeat(columnWidth));
499
+ }
500
+ else {
501
+ lineParts.push('');
502
+ }
321
503
  }
322
504
  else {
323
505
  const columnContent = typeof result === 'string' ? result : '';
324
- // CRITICAL: Pad each column to its allocated width
325
- // This ensures flex columns actually fill their allocated space
326
- const plainContent = columnContent.replace(/\x1b\[[0-9;]*m/g, '');
327
- const paddedContent = plainContent.length < columnWidth
328
- ? columnContent + ' '.repeat(columnWidth - plainContent.length)
329
- : columnContent;
330
- lineParts.push(paddedContent);
506
+ if (isAuto) {
507
+ lineParts.push(columnContent);
508
+ }
509
+ else {
510
+ const plainContent = stripAnsi(columnContent);
511
+ let paddedContent;
512
+ // If content is empty and this is a flex column with spaceBetween, fill it
513
+ if (plainContent.length === 0 && spaceBetween && columnWidth > 0 && isFlex) {
514
+ const { char: spaceChar, color: spaceColor } = getSpaceBetweenChar(spaceBetween, i);
515
+ const fillText = spaceChar.repeat(columnWidth);
516
+ paddedContent = spaceColor ? applyStyle(fillText, { color: spaceColor }) : fillText;
517
+ }
518
+ else if (plainContent.length === 0 && spaceBetween && columnWidth > 0) {
519
+ const { char: spaceChar, color: spaceColor } = getSpaceBetweenChar(spaceBetween, i);
520
+ const fillText = spaceChar.repeat(columnWidth);
521
+ paddedContent = spaceColor ? applyStyle(fillText, { color: spaceColor }) : fillText;
522
+ }
523
+ else {
524
+ paddedContent = plainContent.length < columnWidth
525
+ ? columnContent + ' '.repeat(columnWidth - plainContent.length)
526
+ : columnContent;
527
+ }
528
+ lineParts.push(paddedContent);
529
+ }
331
530
  }
332
- // Add gap (spaceBetween or spaces) if not last
531
+ // Skip gap result if present (gap results are added to results array between columns)
532
+ // Then add gap to lineParts if not using spaceBetween
333
533
  if (i < validChildren.length - 1 && columnGap > 0) {
334
- if (spaceBetween) {
335
- const spaceChar = typeof spaceBetween === 'string'
336
- ? spaceBetween
337
- : Array.isArray(spaceBetween)
338
- ? (spaceBetween[i] ?? spaceBetween[spaceBetween.length - 1])
339
- : spaceBetween.char;
340
- const spaceColor = typeof spaceBetween === 'object' && !Array.isArray(spaceBetween)
341
- ? spaceBetween.color
342
- : undefined;
343
- const gapText = spaceChar.repeat(columnGap);
344
- lineParts.push(spaceColor ? applyStyle(gapText, { color: spaceColor }) : gapText);
345
- partIdx++; // spaceBetween adds an extra result
534
+ // Skip the gap result in the results array
535
+ if (partIdx < results.length) {
536
+ partIdx++; // Skip the gap result
346
537
  }
347
- else {
348
- // Just add spaces for columnGap
538
+ // Add gap to line (unless spaceBetween is set, which handles gaps differently)
539
+ if (!spaceBetween) {
349
540
  lineParts.push(' '.repeat(columnGap));
350
541
  }
351
542
  }
@@ -367,9 +558,27 @@ export function grid(options, ...children) {
367
558
  const result = results[partIdx];
368
559
  partIdx++;
369
560
  const columnWidth = actualWidths[i] ?? 0;
561
+ const track = tracks[i];
562
+ const isAuto = track?.type === 'auto';
563
+ const isFlex = track?.type === 'flex' || track?.type === 'minmax';
370
564
  if (result === null) {
371
565
  // Null result - pad to column width
372
- lineParts.push(' '.repeat(columnWidth));
566
+ // If spaceBetween is set, use it to fill empty columns (especially flex columns)
567
+ if (!isAuto && spaceBetween && columnWidth > 0) {
568
+ const { char: spaceChar, color: spaceColor } = getSpaceBetweenChar(spaceBetween, i);
569
+ const fillText = spaceChar.repeat(columnWidth);
570
+ lineParts.push(spaceColor ? applyStyle(fillText, { color: spaceColor }) : fillText);
571
+ }
572
+ else if (!isAuto) {
573
+ lineParts.push(' '.repeat(columnWidth));
574
+ }
575
+ else {
576
+ lineParts.push('');
577
+ }
578
+ // Skip gap result if present
579
+ if (i < validChildren.length - 1 && columnGap > 0 && partIdx < results.length) {
580
+ partIdx++;
581
+ }
373
582
  continue;
374
583
  }
375
584
  let columnContent;
@@ -381,28 +590,40 @@ export function grid(options, ...children) {
381
590
  }
382
591
  // CRITICAL: Pad each column to its allocated width
383
592
  // This ensures flex columns actually fill their allocated space
384
- const plainContent = columnContent.replace(/\x1b\[[0-9;]*m/g, '');
385
- const paddedContent = plainContent.length < columnWidth
386
- ? columnContent + ' '.repeat(columnWidth - plainContent.length)
387
- : columnContent;
388
- lineParts.push(paddedContent);
389
- // Add gap (spaceBetween or spaces) if not last
390
- if (i < validChildren.length - 1 && columnGap > 0) {
391
- if (spaceBetween) {
392
- const spaceChar = typeof spaceBetween === 'string'
393
- ? spaceBetween
394
- : Array.isArray(spaceBetween)
395
- ? (spaceBetween[i] ?? spaceBetween[spaceBetween.length - 1])
396
- : spaceBetween.char;
397
- const spaceColor = typeof spaceBetween === 'object' && !Array.isArray(spaceBetween)
398
- ? spaceBetween.color
399
- : undefined;
400
- const gapText = spaceChar.repeat(columnGap);
401
- lineParts.push(spaceColor ? applyStyle(gapText, { color: spaceColor }) : gapText);
402
- partIdx++; // spaceBetween adds an extra result
593
+ // If spaceBetween is set and content is empty, fill with spaceBetween character
594
+ if (isAuto) {
595
+ lineParts.push(columnContent);
596
+ }
597
+ else {
598
+ const plainContent = columnContent.replace(/\x1b\[[0-9;]*m/g, '');
599
+ let paddedContent;
600
+ // If content is empty and this is a flex column with spaceBetween, fill it
601
+ if (plainContent.length === 0 && spaceBetween && columnWidth > 0 && isFlex) {
602
+ const { char: spaceChar, color: spaceColor } = getSpaceBetweenChar(spaceBetween, i);
603
+ const fillText = spaceChar.repeat(columnWidth);
604
+ paddedContent = spaceColor ? applyStyle(fillText, { color: spaceColor }) : fillText;
605
+ }
606
+ else if (plainContent.length === 0 && spaceBetween && columnWidth > 0) {
607
+ const { char: spaceChar, color: spaceColor } = getSpaceBetweenChar(spaceBetween, i);
608
+ const fillText = spaceChar.repeat(columnWidth);
609
+ paddedContent = spaceColor ? applyStyle(fillText, { color: spaceColor }) : fillText;
403
610
  }
404
611
  else {
405
- // Just add spaces for columnGap
612
+ paddedContent = plainContent.length < columnWidth
613
+ ? columnContent + ' '.repeat(columnWidth - plainContent.length)
614
+ : columnContent;
615
+ }
616
+ lineParts.push(paddedContent);
617
+ }
618
+ // Skip gap result if present (gap results are added to results array between columns)
619
+ // Then add gap to lineParts if not using spaceBetween
620
+ if (i < validChildren.length - 1 && columnGap > 0) {
621
+ // Skip the gap result in the results array
622
+ if (partIdx < results.length) {
623
+ partIdx++; // Skip the gap result
624
+ }
625
+ // Add gap to line (unless spaceBetween is set, which handles gaps differently)
626
+ if (!spaceBetween) {
406
627
  lineParts.push(' '.repeat(columnGap));
407
628
  }
408
629
  }