@lewishowles/cli-style 0.15.0 → 0.16.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.16.0 - 2026-09-21
6
+
7
+ ### Breaking changes
8
+
9
+ - BREAKING: Removed the `panelWidth` option from `panel()` and the `panel` renderer. Use `width` instead.
10
+ - BREAKING: Panels now honour the requested total `width`, raise widths below 6 to the minimum usable width, and wrap long lines to fit.
11
+
12
+ ### Fixed
13
+
14
+ - An explicit JSON `width` now takes precedence over the terminal width for width-aware renderers, so piped output stays within the caller's requested bounds.
15
+
5
16
  ## 0.15.0 - 2026-09-20
6
17
 
7
18
  ### Added
package/README.md CHANGED
@@ -86,6 +86,10 @@ ui.print(output);
86
86
 
87
87
  Renderer methods return strings. `ui.print()`, `ui.write()`, and CLI commands handle stdout and stderr.
88
88
 
89
+ For `render` commands, width follows this priority: JSON payload `width`, `--width`,
90
+ `stdout.columns`, positive `COLUMNS`, then `80`, so piped output stays within the caller's requested
91
+ size when the payload supplies a width.
92
+
89
93
  ### CLI render
90
94
 
91
95
  Use `cli-style render <renderer>` when a script needs rendered text from JSON input:
@@ -383,19 +387,19 @@ Pre-render a `span` when only part of the hint needs emphasis.
383
387
 
384
388
  ### Structured primitives
385
389
 
386
- | Renderer | Main fields |
387
- | --------------- | -------------------------------------- |
388
- | `divider` | `label`, `dividerWidth`, `character` |
389
- | `chip` | `label`, `tone` |
390
- | `panel` | `title`, `lines`, `tone`, `panelWidth` |
391
- | `table` | `columns`, `rows`, `width` |
392
- | `progress-bar` | `value`, `max`, `barWidth`, `tone` |
393
- | `bar-chart` | `rows`, `barWidth` |
394
- | `sparkline` | `values`, `label`, `tone`, `width` |
395
- | `step` | `label`, `state` |
396
- | `step-progress` | `steps`, `current` |
397
- | `empty-state` | `title`, `detail` |
398
- | `error-block` | `title`, `lines` |
390
+ | Renderer | Main fields |
391
+ | --------------- | ------------------------------------ |
392
+ | `divider` | `label`, `dividerWidth`, `character` |
393
+ | `chip` | `label`, `tone` |
394
+ | `panel` | `title`, `lines`, `tone`, `width` |
395
+ | `table` | `columns`, `rows`, `width` |
396
+ | `progress-bar` | `value`, `max`, `barWidth`, `tone` |
397
+ | `bar-chart` | `rows`, `barWidth` |
398
+ | `sparkline` | `values`, `label`, `tone`, `width` |
399
+ | `step` | `label`, `state` |
400
+ | `step-progress` | `steps`, `current` |
401
+ | `empty-state` | `title`, `detail` |
402
+ | `error-block` | `title`, `lines` |
399
403
 
400
404
  For visual examples, run `cli-style gallery --section primitives`.
401
405
 
@@ -297,7 +297,7 @@ def panel(
297
297
  lines: list[str] | None = None,
298
298
  title: str | None = None,
299
299
  tone: str = "info",
300
- panel_width: int | float | None = None,
300
+ width: int | float | None = None,
301
301
  **kwargs: Any,
302
302
  ) -> str:
303
303
  """Return panel output; extra keyword arguments go to render()."""
@@ -308,8 +308,8 @@ def panel(
308
308
 
309
309
  if title is not None:
310
310
  data["title"] = title
311
- if panel_width is not None:
312
- data["panelWidth"] = panel_width
311
+ if width is not None:
312
+ data["width"] = width
313
313
 
314
314
  return render("panel", data, **kwargs)
315
315
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lewishowles-cli-style"
3
- version = "0.15.0"
3
+ version = "0.16.0"
4
4
  description = "Python adapter for the shared cli-style binary."
5
5
  authors = [{ name = "Lewis Howles" }]
6
6
  license = { text = "MIT" }
@@ -341,7 +341,7 @@ extension CliStyle {
341
341
  lines: [String]? = nil,
342
342
  title: String? = nil,
343
343
  tone: String = "info",
344
- panelWidth: Int? = nil,
344
+ width: Int? = nil,
345
345
  options: CliStyleOptions = .init()
346
346
  ) throws -> String {
347
347
  var data: [String: Any] = [
@@ -352,8 +352,8 @@ extension CliStyle {
352
352
  if let title = title {
353
353
  data["title"] = title
354
354
  }
355
- if let panelWidth = panelWidth {
356
- data["panelWidth"] = panelWidth
355
+ if let width = width {
356
+ data["width"] = width
357
357
  }
358
358
 
359
359
  return try render("panel", data: data, options: options)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/cli-style",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Shared terminal output styles for Lewis Howles tools, diagnostics, and package CLIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,7 +29,7 @@ export function resolveTerminalCapabilities(options = {}) {
29
29
  const isCi = env.CI === "true" || env.CI === "1";
30
30
  const isDumb = env.TERM === "dumb";
31
31
  const forceColour = env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "0";
32
- const width = resolveWidth(stdout.columns);
32
+ const width = resolveWidth(stdout.columns, env.COLUMNS);
33
33
  const usesPlain = argv.includes("--plain");
34
34
 
35
35
  const colour = resolveColour({
@@ -189,16 +189,25 @@ function resolveUnicode(options) {
189
189
  }
190
190
 
191
191
  /**
192
- * Resolve terminal width with a stable fallback.
192
+ * Resolve the terminal width from the output stream, then `COLUMNS`, then a fixed fallback.
193
193
  *
194
- * @param {number} width
195
- * Stream-reported column count.
194
+ * @param {number} streamWidth
195
+ * The column count the output stream reports when it is a terminal.
196
+ * @param {string} columns
197
+ * The `COLUMNS` environment value, used when the stream reports no width, such as when
198
+ * output is piped. Anything other than a positive whole number is ignored.
196
199
  * @returns {number}
197
200
  * Usable terminal width.
198
201
  */
199
- function resolveWidth(width) {
200
- if (Number.isInteger(width) && width > 0) {
201
- return width;
202
+ function resolveWidth(streamWidth, columns) {
203
+ if (Number.isInteger(streamWidth) && streamWidth > 0) {
204
+ return streamWidth;
205
+ }
206
+
207
+ const environmentWidth = Number(columns);
208
+
209
+ if (Number.isInteger(environmentWidth) && environmentWidth > 0) {
210
+ return environmentWidth;
202
211
  }
203
212
 
204
213
  return defaultWidth;
@@ -595,7 +595,7 @@ const rendererDefinitions = [
595
595
  default: "info",
596
596
  },
597
597
  {
598
- name: "panelWidth",
598
+ name: "width",
599
599
  type: "number",
600
600
  required: false,
601
601
  default: null,
@@ -38,6 +38,7 @@ const renderers = {
38
38
  bulletList(input.items, {
39
39
  ...input,
40
40
  ...options,
41
+ width: input.width ?? options.width,
41
42
  }),
42
43
  chip: (input, options) =>
43
44
  chip(input.label, input.tone, {
@@ -63,6 +64,7 @@ const renderers = {
63
64
  errorBlock(input.title, input.lines, {
64
65
  ...input,
65
66
  ...options,
67
+ width: input.width ?? options.width,
66
68
  }),
67
69
  hint: (input, options) =>
68
70
  hint(input.message, {
@@ -73,12 +75,14 @@ const renderers = {
73
75
  labelledLine(input.label, input.message, {
74
76
  ...input,
75
77
  ...options,
78
+ width: input.width ?? options.width,
76
79
  }),
77
80
  "next-step-block": (input, options) => nextStepBlock(input, options),
78
81
  panel: (input, options) =>
79
82
  panel({
80
83
  ...input,
81
84
  ...options,
85
+ width: input.width ?? options.width,
82
86
  }),
83
87
  "progress-bar": (input, options) =>
84
88
  progressBar({
@@ -89,16 +93,19 @@ const renderers = {
89
93
  row(input.label, input.value, {
90
94
  ...input,
91
95
  ...options,
96
+ width: input.width ?? options.width,
92
97
  }),
93
98
  "row-group": (input, options) =>
94
99
  rowGroup({
95
100
  ...input,
96
101
  ...options,
102
+ width: input.width ?? options.width,
97
103
  }),
98
104
  sparkline: (input, options) =>
99
105
  sparkline({
100
106
  ...input,
101
107
  ...options,
108
+ width: input.width ?? options.width,
102
109
  }),
103
110
  span: (input, options) =>
104
111
  span(input.value, input.tone, {
@@ -124,6 +131,7 @@ const renderers = {
124
131
  table({
125
132
  ...input,
126
133
  ...options,
134
+ width: input.width ?? options.width,
127
135
  }),
128
136
  "task-summary": (input, options) => taskSummary(input, options),
129
137
  };
@@ -280,7 +280,6 @@ function galleryWidthOptions(width) {
280
280
  return {
281
281
  barWidth: Math.max(width - 18, 1),
282
282
  dividerWidth: width,
283
- panelWidth: width,
284
283
  };
285
284
  }
286
285
 
@@ -790,7 +789,7 @@ function renderPrimitives(options) {
790
789
  emptyState("No results", "tone: muted", options),
791
790
  errorBlock("Failed", ["tone: danger"], {
792
791
  ...options,
793
- panelWidth: 40,
792
+ width: 40,
794
793
  }),
795
794
  "",
796
795
  "Steps",
@@ -1,10 +1,14 @@
1
1
  import { background, foreground, style } from "../formatters/ansi.js";
2
+ import { wrapText } from "../formatters/wrap.js";
2
3
  import { profiles } from "../profiles/profiles.js";
3
4
  import { panelColours } from "../theme/colours.js";
4
5
 
5
- // Panels use a stable width so grouped output aligns across a report.
6
+ // The width used when no width is given, such as a direct library call outside a terminal.
6
7
  const defaultWidth = 40;
7
8
 
9
+ // The narrowest panel that still leaves one column for content beside the accent and padding.
10
+ const minimumWidth = 6;
11
+
8
12
  // Info is the neutral semantic accent for grouped explanatory content.
9
13
  const defaultTone = "info";
10
14
 
@@ -17,8 +21,9 @@ const defaultTone = "info";
17
21
  * Whether ANSI colour should be applied.
18
22
  * @param {string[]} options.lines
19
23
  * Content lines to render.
20
- * @param {number} options.panelWidth
21
- * Total panel width.
24
+ * @param {number} options.width
25
+ * Total panel width, including the accent and padding. Widths below 6 are raised to 6,
26
+ * which leaves room for one character of content. Longer lines wrap to fit.
22
27
  * @param {string} options.profile
23
28
  * Active output profile.
24
29
  * @param {string} options.title
@@ -37,18 +42,18 @@ export function panel(options = {}) {
37
42
  return [...(options.title === undefined ? [] : [options.title]), ...lines].join("\n");
38
43
  }
39
44
 
40
- const width = Math.max(options.panelWidth ?? defaultWidth, 4);
45
+ const width = Math.max(options.width ?? defaultWidth, minimumWidth);
41
46
  const panelLines = ["", ...(options.title === undefined ? [] : [options.title]), ...lines, ""];
42
47
 
43
48
  return panelLines
44
- .map((line, index) =>
49
+ .flatMap((line, index) =>
45
50
  renderLine(line, width, index === 1 && options.title !== undefined, options),
46
51
  )
47
52
  .join("\n");
48
53
  }
49
54
 
50
55
  /**
51
- * Render one fixed-width panel line.
56
+ * Render one content line, wrapped across as many panel lines as it needs.
52
57
  *
53
58
  * @param {string} line
54
59
  * Content line.
@@ -58,28 +63,30 @@ export function panel(options = {}) {
58
63
  * Whether the line is the panel title.
59
64
  * @param {object} options
60
65
  * Rendering options.
61
- * @returns {string}
62
- * Rendered content line.
66
+ * @returns {string[]}
67
+ * The panel lines, each exactly `width` columns wide.
63
68
  */
64
69
  function renderLine(line, width, isTitle, options) {
65
70
  const accent = options.unicode === false ? "|" : "▌";
66
71
  const contentWidth = width - 5;
67
- const visibleLine = line.slice(0, contentWidth);
68
- const content = ` ${visibleLine.padEnd(contentWidth, " ")} `;
69
72
 
70
- if (options.colour !== true) {
71
- return `${accent}${content}`;
72
- }
73
+ return wrapText(line, contentWidth).map((visibleLine) => {
74
+ const content = ` ${visibleLine.padEnd(contentWidth, " ")} `;
75
+
76
+ if (options.colour !== true) {
77
+ return `${accent}${content}`;
78
+ }
73
79
 
74
- const renderedAccent = renderPanelColour(accent, options);
80
+ const renderedAccent = renderPanelColour(accent, options);
75
81
 
76
- const renderedContent = foreground(
77
- background(content, panelColours.background, options),
78
- panelColours.body,
79
- options,
80
- );
82
+ const renderedContent = foreground(
83
+ background(content, panelColours.background, options),
84
+ panelColours.body,
85
+ options,
86
+ );
81
87
 
82
- return `${renderedAccent}${isTitle ? style(renderedContent, "bold", options) : renderedContent}`;
88
+ return `${renderedAccent}${isTitle ? style(renderedContent, "bold", options) : renderedContent}`;
89
+ });
83
90
  }
84
91
 
85
92
  /**