@teammates/consolonia 0.2.0 → 0.2.7

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.
@@ -297,9 +297,10 @@ function renderList(token, lines, theme, synTheme, width, indent, ctx) {
297
297
  lines.push([{ text: indent, style: theme.text }]);
298
298
  }
299
299
  // ── Tables ───────────────────────────────────────────────────────
300
- function renderTable(token, lines, theme, _width, indent) {
300
+ function renderTable(token, lines, theme, width, indent) {
301
301
  const numCols = token.header.length;
302
- // Compute column widths from content
302
+ const avail = width - indent.length;
303
+ // Compute natural column widths from content
303
304
  const colWidths = token.header.map((h) => plainText(h.tokens).length);
304
305
  for (const row of token.rows) {
305
306
  for (let c = 0; c < numCols; c++) {
@@ -312,42 +313,132 @@ function renderTable(token, lines, theme, _width, indent) {
312
313
  for (let c = 0; c < numCols; c++) {
313
314
  colWidths[c] = Math.max(3, colWidths[c]) + 2;
314
315
  }
316
+ // Shrink columns if total table width exceeds available width
317
+ // Total = sum(colWidths) + numCols + 1 (for │ borders)
318
+ const MIN_COL = 6; // minimum inner width to be readable
319
+ const totalBorders = numCols + 1;
320
+ const totalNatural = colWidths.reduce((a, b) => a + b, 0) + totalBorders;
321
+ if (totalNatural > avail && avail > totalBorders + numCols * MIN_COL) {
322
+ const budgetForCols = avail - totalBorders;
323
+ // Proportional shrink, respecting minimum
324
+ let remaining = budgetForCols;
325
+ const fixed = new Array(numCols).fill(false);
326
+ // First pass: lock columns that are already small (at or below fair share)
327
+ // so they keep their natural width instead of being shrunk further
328
+ const fairShare = Math.floor(budgetForCols / numCols);
329
+ for (let c = 0; c < numCols; c++) {
330
+ if (colWidths[c] <= fairShare) {
331
+ fixed[c] = true;
332
+ remaining -= colWidths[c];
333
+ }
334
+ }
335
+ // Second pass: distribute remaining budget proportionally among shrinkable columns
336
+ const shrinkSum = colWidths
337
+ .filter((_w, i) => !fixed[i])
338
+ .reduce((a, b) => a + b, 0);
339
+ if (shrinkSum > 0) {
340
+ let distributed = 0;
341
+ const shrinkable = colWidths.map((_w, i) => !fixed[i]);
342
+ for (let c = 0; c < numCols; c++) {
343
+ if (shrinkable[c]) {
344
+ const share = Math.max(MIN_COL, Math.floor((colWidths[c] / shrinkSum) * remaining));
345
+ colWidths[c] = share;
346
+ distributed += share;
347
+ }
348
+ }
349
+ // Give any leftover pixels to the last shrinkable column
350
+ const leftover = remaining - distributed;
351
+ if (leftover !== 0) {
352
+ for (let c = numCols - 1; c >= 0; c--) {
353
+ if (shrinkable[c]) {
354
+ colWidths[c] += leftover;
355
+ break;
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
315
361
  const border = theme.tableBorder;
316
362
  // Helper to build a horizontal rule
317
363
  const hRule = (left, mid, right) => {
318
364
  const parts = colWidths.map((w) => "─".repeat(w));
319
365
  return left + parts.join(mid) + right;
320
366
  };
321
- // Helper to build a data row
322
- const dataRow = (cells, style) => {
323
- const segs = [
324
- { text: indent, style: theme.text },
325
- { text: "│", style: border },
326
- ];
367
+ // Word-wrap text to fit within a column width (breaks on spaces)
368
+ const wrapCellText = (text, maxW) => {
369
+ if (maxW <= 0)
370
+ return [text];
371
+ if (text.length <= maxW)
372
+ return [text];
373
+ const wrapped = [];
374
+ let rest = text;
375
+ while (rest.length > maxW) {
376
+ // Find last space within the limit
377
+ let breakAt = rest.lastIndexOf(" ", maxW);
378
+ if (breakAt <= 0) {
379
+ // No space — hard break
380
+ breakAt = maxW;
381
+ wrapped.push(rest.slice(0, breakAt));
382
+ rest = rest.slice(breakAt);
383
+ }
384
+ else {
385
+ wrapped.push(rest.slice(0, breakAt));
386
+ rest = rest.slice(breakAt + 1); // skip the space
387
+ }
388
+ }
389
+ if (rest.length > 0)
390
+ wrapped.push(rest);
391
+ return wrapped;
392
+ };
393
+ // Helper to build a (possibly multi-line) data row
394
+ const dataRows = (cells, style) => {
395
+ // Get wrapped lines for each cell
396
+ const cellLines = [];
397
+ let maxLines = 1;
327
398
  for (let c = 0; c < numCols; c++) {
328
399
  const cellText = cells[c] ? plainText(cells[c].tokens) : "";
329
- const align = token.header[c]?.align;
330
- const padded = padCell(cellText, colWidths[c], align);
331
- segs.push({ text: padded, style });
332
- segs.push({ text: "│", style: border });
400
+ const innerW = colWidths[c] - 2; // 1 char padding each side
401
+ const wrapped = wrapCellText(cellText, innerW);
402
+ cellLines.push(wrapped);
403
+ if (wrapped.length > maxLines)
404
+ maxLines = wrapped.length;
333
405
  }
334
- return segs;
406
+ const result = [];
407
+ for (let row = 0; row < maxLines; row++) {
408
+ const segs = [
409
+ { text: indent, style: theme.text },
410
+ { text: "│", style: border },
411
+ ];
412
+ for (let c = 0; c < numCols; c++) {
413
+ const lineText = row < cellLines[c].length ? cellLines[c][row] : "";
414
+ const align = token.header[c]?.align;
415
+ const padded = padCell(lineText, colWidths[c], align);
416
+ segs.push({ text: padded, style });
417
+ segs.push({ text: "│", style: border });
418
+ }
419
+ result.push(segs);
420
+ }
421
+ return result;
335
422
  };
336
423
  // Top border
337
424
  lines.push([
338
425
  { text: indent, style: theme.text },
339
426
  { text: hRule("┌", "┬", "┐"), style: border },
340
427
  ]);
341
- // Header row
342
- lines.push(dataRow(token.header, theme.tableHeader));
428
+ // Header row (may be multi-line)
429
+ for (const line of dataRows(token.header, theme.tableHeader)) {
430
+ lines.push(line);
431
+ }
343
432
  // Header separator
344
433
  lines.push([
345
434
  { text: indent, style: theme.text },
346
435
  { text: hRule("├", "┼", "┤"), style: border },
347
436
  ]);
348
- // Data rows
437
+ // Data rows (may be multi-line each)
349
438
  for (const row of token.rows) {
350
- lines.push(dataRow(row, theme.text));
439
+ for (const line of dataRows(row, theme.text)) {
440
+ lines.push(line);
441
+ }
351
442
  }
352
443
  // Bottom border
353
444
  lines.push([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teammates/consolonia",
3
- "version": "0.2.0",
3
+ "version": "0.2.7",
4
4
  "description": "Terminal UI rendering engine inspired by Consolonia. Pixel-level compositing with ANSI output.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",