@lewishowles/cli-style 0.8.1 → 0.9.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,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.0 - 2026-07-16
6
+
7
+ ### Added
8
+
9
+ - Added a `rowGroup` primitive (`rowGroup()` in JS, the `row-group` renderer for `cli-style render`, and the same generic `render()`/`cli_style_render`/`CliStyle.render` entry points in Python, Bash, and Swift) that aligns a group of related rows automatically, without the caller computing `labelWidth` by hand.
10
+
11
+ ### Fixed
12
+
13
+ - Fixed the Python, Bash, and Swift adapters silently dropping `row()`, `span()`, and `divider()` options beyond their two or three main fields. `row()` now forwards `labelWidth`, `labelColour`, `valueColour`, and `separator`; `span()` forwards `weight`; `divider()` forwards `dividerWidth`, `dividerColour`, and `labelColour`.
14
+
5
15
  ## 0.8.1 - 2026-07-15
6
16
 
7
17
  ### Fixed
package/README.md CHANGED
@@ -227,6 +227,7 @@ Renderer names are stable for `cli-style render`, `cli_style_render`, and Python
227
227
  | --------------------- | ------------------------------------- | ------------------------------------ | ---------------------------------- | -------------------------------------- | --------------------------------------------------------------- |
228
228
  | `status` | `status(resultType, detail, options)` | `cli_style_status type label detail` | `status(type, label, detail)` | `CliStyle.status(type, label, detail)` | Result line with symbol, label, and detail. |
229
229
  | `row` | `row(label, value, options)` | `cli_style_row label value [result]` | `row(label, value, result)` | `CliStyle.row(label, value, result)` | Aligned label/value row, optionally marked with a result state. |
230
+ | `row-group` | `rowGroup(options)` | Use `cli_style_render` | Use `render("row-group", ...)` | Use `CliStyle.render` | Auto-aligned group of labelled value rows. |
230
231
  | `span` | `span(value, tone, options)` | `cli_style_span value [tone]` | `span(value, tone)` | `CliStyle.span(value, tone)` | Inline colour or weight for a word, command, file, or value. |
231
232
  | `hint` | `hint(message, options)` | `cli_style_hint message` | `hint(message)` | `CliStyle.hint(message)` | Informational hint line. |
232
233
  | `divider` | `divider(options)` | `cli_style_divider label` | `divider(label)` | `CliStyle.divider(label)` | Section divider with an optional label. |
@@ -282,6 +283,27 @@ Use these shapes with `cli-style render`, `cli_style_render`, or Python `render(
282
283
  - `labelWidth`: optional minimum label width
283
284
  - `labelColour`, `valueColour`, `separator`: optional presentation overrides
284
285
 
286
+ ### `row-group`
287
+
288
+ ```json
289
+ {
290
+ "rows": [
291
+ {
292
+ "label": "Package",
293
+ "value": "@lewishowles/components"
294
+ },
295
+ {
296
+ "label": "Version",
297
+ "value": "minor: 2.7.4 -> 2.8.0"
298
+ }
299
+ ]
300
+ }
301
+ ```
302
+
303
+ - `rows`: labelled values to align and render
304
+ - `labelWidth`: optional minimum width for every label
305
+ - Per-row fields match `row`, including `label`, `value`, `result`, `labelColour`, `valueColour`, and `separator`
306
+
285
307
  ### `span`
286
308
 
287
309
  ```json
@@ -159,6 +159,11 @@ cli_style_row() {
159
159
  local label="${1:-}"
160
160
  local value="${2:-}"
161
161
  local result=""
162
+ local label_width=""
163
+ local label_colour_json="null"
164
+ local value_colour_json="null"
165
+ local separator_json="null"
166
+ local -a render_args=()
162
167
  local json
163
168
 
164
169
  if (($# >= 3)) && [[ "${3:-}" != --* ]]; then
@@ -170,9 +175,60 @@ cli_style_row() {
170
175
  set --
171
176
  fi
172
177
 
173
- json="{\"label\":$(cli_style_json_string "$label"),\"value\":$(cli_style_json_string "$value"),\"result\":$(cli_style_json_string "$result")}"
178
+ while (($# > 0)); do
179
+ case "$1" in
180
+ --label-width)
181
+ label_width="${2:-}"
182
+ shift 2
183
+ ;;
184
+ --label-width=*)
185
+ label_width="${1#*=}"
186
+ shift
187
+ ;;
188
+ --label-colour)
189
+ label_colour_json="$(cli_style_json_string "${2:-}")"
190
+ shift 2
191
+ ;;
192
+ --label-colour=*)
193
+ label_colour_json="$(cli_style_json_string "${1#*=}")"
194
+ shift
195
+ ;;
196
+ --value-colour)
197
+ value_colour_json="$(cli_style_json_string "${2:-}")"
198
+ shift 2
199
+ ;;
200
+ --value-colour=*)
201
+ value_colour_json="$(cli_style_json_string "${1#*=}")"
202
+ shift
203
+ ;;
204
+ --separator)
205
+ separator_json="$(cli_style_json_string "${2:-}")"
206
+ shift 2
207
+ ;;
208
+ --separator=*)
209
+ separator_json="$(cli_style_json_string "${1#*=}")"
210
+ shift
211
+ ;;
212
+ *)
213
+ render_args+=("$1")
214
+ shift
215
+ ;;
216
+ esac
217
+ done
218
+
219
+ json="{\"label\":$(cli_style_json_string "$label"),\"value\":$(cli_style_json_string "$value"),\"result\":$(cli_style_json_string "$result"),\"labelWidth\":$(cli_style_json_integer_or_null "$label_width"),\"separator\":$separator_json"
220
+
221
+ if [[ "$label_colour_json" != "null" ]]; then
222
+ json="${json},\"labelColour\":$label_colour_json"
223
+ fi
174
224
 
175
- cli_style_render_json row "$json" "$@"
225
+ if [[ "$value_colour_json" != "null" ]]; then
226
+ json="${json},\"valueColour\":$value_colour_json"
227
+ fi
228
+
229
+ json="${json}}"
230
+
231
+ cli_style_render_json row "$json" "${render_args[@]}"
176
232
  }
177
233
 
178
234
  # Render an inline span from Bash string values.
@@ -186,6 +242,8 @@ cli_style_row() {
186
242
  cli_style_span() {
187
243
  local value="${1:-}"
188
244
  local tone="info"
245
+ local weight_json="null"
246
+ local -a render_args=()
189
247
  local json
190
248
 
191
249
  if (($# >= 2)) && [[ "${2:-}" != --* ]]; then
@@ -197,9 +255,26 @@ cli_style_span() {
197
255
  set --
198
256
  fi
199
257
 
200
- json="{\"value\":$(cli_style_json_string "$value"),\"tone\":$(cli_style_json_string "$tone")}"
258
+ while (($# > 0)); do
259
+ case "$1" in
260
+ --weight)
261
+ weight_json="$(cli_style_json_string "${2:-}")"
262
+ shift 2
263
+ ;;
264
+ --weight=*)
265
+ weight_json="$(cli_style_json_string "${1#*=}")"
266
+ shift
267
+ ;;
268
+ *)
269
+ render_args+=("$1")
270
+ shift
271
+ ;;
272
+ esac
273
+ done
274
+
275
+ json="{\"value\":$(cli_style_json_string "$value"),\"tone\":$(cli_style_json_string "$tone"),\"weight\":$weight_json}"
201
276
 
202
- cli_style_render_json span "$json" "$@"
277
+ cli_style_render_json span "$json" "${render_args[@]}"
203
278
  }
204
279
 
205
280
  # Render a divider from a Bash string value.
@@ -210,6 +285,10 @@ cli_style_span() {
210
285
  # Render flags passed through to `cli-style render`.
211
286
  cli_style_divider() {
212
287
  local label="${1:-}"
288
+ local divider_width=""
289
+ local divider_colour_json="null"
290
+ local label_colour_json="null"
291
+ local -a render_args=()
213
292
  local json
214
293
 
215
294
  if (($# >= 1)); then
@@ -218,9 +297,52 @@ cli_style_divider() {
218
297
  set --
219
298
  fi
220
299
 
221
- json="{\"label\":$(cli_style_json_string "$label")}"
300
+ while (($# > 0)); do
301
+ case "$1" in
302
+ --divider-width)
303
+ divider_width="${2:-}"
304
+ shift 2
305
+ ;;
306
+ --divider-width=*)
307
+ divider_width="${1#*=}"
308
+ shift
309
+ ;;
310
+ --divider-colour)
311
+ divider_colour_json="$(cli_style_json_string "${2:-}")"
312
+ shift 2
313
+ ;;
314
+ --divider-colour=*)
315
+ divider_colour_json="$(cli_style_json_string "${1#*=}")"
316
+ shift
317
+ ;;
318
+ --label-colour)
319
+ label_colour_json="$(cli_style_json_string "${2:-}")"
320
+ shift 2
321
+ ;;
322
+ --label-colour=*)
323
+ label_colour_json="$(cli_style_json_string "${1#*=}")"
324
+ shift
325
+ ;;
326
+ *)
327
+ render_args+=("$1")
328
+ shift
329
+ ;;
330
+ esac
331
+ done
332
+
333
+ json="{\"label\":$(cli_style_json_string "$label"),\"dividerWidth\":$(cli_style_json_integer_or_null "$divider_width")"
334
+
335
+ if [[ "$divider_colour_json" != "null" ]]; then
336
+ json="${json},\"dividerColour\":$divider_colour_json"
337
+ fi
338
+
339
+ if [[ "$label_colour_json" != "null" ]]; then
340
+ json="${json},\"labelColour\":$label_colour_json"
341
+ fi
342
+
343
+ json="${json}}"
222
344
 
223
- cli_style_render_json divider "$json" "$@"
345
+ cli_style_render_json divider "$json" "${render_args[@]}"
224
346
  }
225
347
 
226
348
  # Render a hint from a Bash string value.
@@ -124,13 +124,39 @@ def status(
124
124
  # Row value.
125
125
  # @param {str} result
126
126
  # Optional result type for a symbol and tone.
127
+ # @param {int|None} label_width
128
+ # Optional minimum label width.
129
+ # @param {str|None} label_colour
130
+ # Optional label colour token.
131
+ # @param {str|None} value_colour
132
+ # Optional value colour token.
133
+ # @param {str|None} separator
134
+ # Optional text between the label and value.
127
135
  def row(
128
136
  label: str,
129
137
  value: str,
130
138
  result: str = "",
139
+ label_width: int | None = None,
140
+ label_colour: str | None = None,
141
+ value_colour: str | None = None,
142
+ separator: str | None = None,
131
143
  **kwargs: Any,
132
144
  ) -> str:
133
- return render("row", {"label": label, "value": value, "result": result}, **kwargs)
145
+ data = {
146
+ "label": label,
147
+ "value": value,
148
+ "result": result,
149
+ "labelWidth": label_width,
150
+ "separator": separator,
151
+ }
152
+
153
+ if label_colour is not None:
154
+ data["labelColour"] = label_colour
155
+
156
+ if value_colour is not None:
157
+ data["valueColour"] = value_colour
158
+
159
+ return render("row", data, **kwargs)
134
160
 
135
161
 
136
162
  # Render an inline span from Python values.
@@ -139,23 +165,46 @@ def row(
139
165
  # Span text.
140
166
  # @param {str} tone
141
167
  # Optional colour tone, defaulting to info.
168
+ # @param {str|None} weight
169
+ # Optional ANSI text weight.
142
170
  def span(
143
171
  value: str,
144
172
  tone: str = "info",
173
+ weight: str | None = None,
145
174
  **kwargs: Any,
146
175
  ) -> str:
147
- return render("span", {"value": value, "tone": tone}, **kwargs)
176
+ return render("span", {"value": value, "tone": tone, "weight": weight}, **kwargs)
148
177
 
149
178
 
150
179
  # Render a divider from a Python value.
151
180
  #
152
181
  # @param {str} label
153
182
  # Optional divider label.
183
+ # @param {int|None} divider_width
184
+ # Optional divider width.
185
+ # @param {str|None} divider_colour
186
+ # Optional divider colour token.
187
+ # @param {str|None} label_colour
188
+ # Optional label colour token.
154
189
  def divider(
155
190
  label: str = "",
191
+ divider_width: int | None = None,
192
+ divider_colour: str | None = None,
193
+ label_colour: str | None = None,
156
194
  **kwargs: Any,
157
195
  ) -> str:
158
- return render("divider", {"label": label}, **kwargs)
196
+ data = {
197
+ "label": label,
198
+ "dividerWidth": divider_width,
199
+ }
200
+
201
+ if divider_colour is not None:
202
+ data["dividerColour"] = divider_colour
203
+
204
+ if label_colour is not None:
205
+ data["labelColour"] = label_colour
206
+
207
+ return render("divider", data, **kwargs)
159
208
 
160
209
 
161
210
  # Render a hint from a Python value.
@@ -175,6 +175,10 @@ enum CliStyle {
175
175
  - parameter label: Row label.
176
176
  - parameter value: Row value.
177
177
  - parameter result: Optional result type for a symbol and tone.
178
+ - parameter labelWidth: Optional minimum label width.
179
+ - parameter labelColour: Optional label colour token.
180
+ - parameter valueColour: Optional value colour token.
181
+ - parameter separator: Optional text between the label and value.
178
182
  - parameter options: Render options.
179
183
  - returns: Rendered row line.
180
184
  - throws: `CliStyleError` on failure.
@@ -183,13 +187,35 @@ enum CliStyle {
183
187
  label: String,
184
188
  value: String,
185
189
  result: String = "",
190
+ labelWidth: Int? = nil,
191
+ labelColour: String? = nil,
192
+ valueColour: String? = nil,
193
+ separator: String? = nil,
186
194
  options: CliStyleOptions = .init()
187
195
  ) throws -> String {
188
- try render("row", data: [
196
+ var data: [String: Any] = [
189
197
  "label": label,
190
198
  "value": value,
191
199
  "result": result,
192
- ], options: options)
200
+ ]
201
+
202
+ if let labelWidth = labelWidth {
203
+ data["labelWidth"] = labelWidth
204
+ }
205
+
206
+ if let labelColour = labelColour {
207
+ data["labelColour"] = labelColour
208
+ }
209
+
210
+ if let valueColour = valueColour {
211
+ data["valueColour"] = valueColour
212
+ }
213
+
214
+ if let separator = separator {
215
+ data["separator"] = separator
216
+ }
217
+
218
+ return try render("row", data: data, options: options)
193
219
  }
194
220
 
195
221
  /**
@@ -197,6 +223,7 @@ enum CliStyle {
197
223
 
198
224
  - parameter value: Span text.
199
225
  - parameter tone: Optional colour tone, defaulting to info.
226
+ - parameter weight: Optional ANSI text weight.
200
227
  - parameter options: Render options.
201
228
  - returns: Rendered span string.
202
229
  - throws: `CliStyleError` on failure.
@@ -204,29 +231,56 @@ enum CliStyle {
204
231
  static func span(
205
232
  value: String,
206
233
  tone: String = "info",
234
+ weight: String? = nil,
207
235
  options: CliStyleOptions = .init()
208
236
  ) throws -> String {
209
- try render("span", data: [
237
+ var data: [String: Any] = [
210
238
  "value": value,
211
239
  "tone": tone,
212
- ], options: options)
240
+ ]
241
+
242
+ if let weight = weight {
243
+ data["weight"] = weight
244
+ }
245
+
246
+ return try render("span", data: data, options: options)
213
247
  }
214
248
 
215
249
  /**
216
250
  Render a divider.
217
251
 
218
252
  - parameter label: Optional divider label.
253
+ - parameter dividerWidth: Optional divider width.
254
+ - parameter dividerColour: Optional divider colour token.
255
+ - parameter labelColour: Optional label colour token.
219
256
  - parameter options: Render options.
220
257
  - returns: Rendered divider line.
221
258
  - throws: `CliStyleError` on failure.
222
259
  */
223
260
  static func divider(
224
261
  label: String = "",
262
+ dividerWidth: Int? = nil,
263
+ dividerColour: String? = nil,
264
+ labelColour: String? = nil,
225
265
  options: CliStyleOptions = .init()
226
266
  ) throws -> String {
227
- try render("divider", data: [
267
+ var data: [String: Any] = [
228
268
  "label": label,
229
- ], options: options)
269
+ ]
270
+
271
+ if let dividerWidth = dividerWidth {
272
+ data["dividerWidth"] = dividerWidth
273
+ }
274
+
275
+ if let dividerColour = dividerColour {
276
+ data["dividerColour"] = dividerColour
277
+ }
278
+
279
+ if let labelColour = labelColour {
280
+ data["labelColour"] = labelColour
281
+ }
282
+
283
+ return try render("divider", data: data, options: options)
230
284
  }
231
285
 
232
286
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/cli-style",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Shared terminal output styles for Lewis Howles tools, diagnostics, and package CLIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -13,6 +13,7 @@ import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
13
13
  import { panel } from "../primitives/panel.js";
14
14
  import { progressBar } from "../primitives/progress-bar.js";
15
15
  import { row } from "../primitives/row.js";
16
+ import { rowGroup } from "../primitives/row-group.js";
16
17
  import { span } from "../primitives/span.js";
17
18
  import { status } from "../primitives/status.js";
18
19
  import { step, stepProgress } from "../primitives/step-progress.js";
@@ -73,6 +74,11 @@ const renderers = {
73
74
  ...input,
74
75
  ...options,
75
76
  }),
77
+ "row-group": (input, options) =>
78
+ rowGroup({
79
+ ...input,
80
+ ...options,
81
+ }),
76
82
  span: (input, options) =>
77
83
  span(input.value, input.tone, {
78
84
  ...input,
@@ -14,6 +14,7 @@ import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
14
14
  import { panel } from "../primitives/panel.js";
15
15
  import { progressBar } from "../primitives/progress-bar.js";
16
16
  import { row } from "../primitives/row.js";
17
+ import { rowGroup } from "../primitives/row-group.js";
17
18
  import { span } from "../primitives/span.js";
18
19
  import { status } from "../primitives/status.js";
19
20
  import { step, stepProgress, stepStates } from "../primitives/step-progress.js";
@@ -715,6 +716,30 @@ function renderPrimitives(options) {
715
716
  labelWidth: 8,
716
717
  result: resultTypes.FAILED,
717
718
  }),
719
+ "",
720
+ "Row groups",
721
+ rowGroup({
722
+ ...options,
723
+ rows: [
724
+ {
725
+ label: "Package",
726
+ value: "@lewishowles/components",
727
+ },
728
+ {
729
+ label: "Version",
730
+ value: "minor: 2.7.4 -> 2.8.0",
731
+ },
732
+ {
733
+ label: "Checks",
734
+ value: "type-check, unit, build",
735
+ },
736
+ {
737
+ label: "Bundle",
738
+ result: resultTypes.FAILED,
739
+ value: "is 22.3 KB, above the 12.0 KB budget",
740
+ },
741
+ ],
742
+ }),
718
743
  ].join("\n");
719
744
  }
720
745
 
package/src/index.js CHANGED
@@ -35,6 +35,7 @@ export { emptyState, errorBlock, hint } from "./primitives/feedback.js";
35
35
  export { panel } from "./primitives/panel.js";
36
36
  export { progressBar } from "./primitives/progress-bar.js";
37
37
  export { row } from "./primitives/row.js";
38
+ export { rowGroup } from "./primitives/row-group.js";
38
39
  export { span } from "./primitives/span.js";
39
40
  export { status } from "./primitives/status.js";
40
41
  export { step, stepProgress, stepStates } from "./primitives/step-progress.js";
@@ -0,0 +1,84 @@
1
+ import { getResultSymbol } from "../theme/results.js";
2
+ import { row } from "./row.js";
3
+
4
+ /**
5
+ * Render related labelled values against a shared label width.
6
+ *
7
+ * @param {object} options
8
+ * Rendering options.
9
+ * @param {number} options.labelWidth
10
+ * Optional minimum label width.
11
+ * @param {object[]} options.rows
12
+ * Labelled values to render.
13
+ * @returns {string}
14
+ * Rendered row group.
15
+ */
16
+ export function rowGroup(options = {}) {
17
+ const rows = normaliseRows(options.rows);
18
+
19
+ if (rows.length === 0) {
20
+ return "";
21
+ }
22
+
23
+ const labelWidth = Math.max(
24
+ ...rows.map((entry) => displayedLabelLength(entry, options)),
25
+ options.labelWidth ?? 0,
26
+ );
27
+
28
+ return rows
29
+ .map((entry) =>
30
+ row(entry.label, entry.value ?? "", {
31
+ ...options,
32
+ ...entry,
33
+ labelWidth,
34
+ }),
35
+ )
36
+ .join("\n");
37
+ }
38
+
39
+ /**
40
+ * Return rows with usable labels.
41
+ *
42
+ * @param {object[]} rows
43
+ * Requested row entries.
44
+ * @returns {object[]}
45
+ * Valid row entries.
46
+ */
47
+ function normaliseRows(rows) {
48
+ if (!Array.isArray(rows)) {
49
+ return [];
50
+ }
51
+
52
+ return rows.filter(
53
+ (entry) =>
54
+ entry !== null &&
55
+ typeof entry === "object" &&
56
+ typeof entry.label === "string" &&
57
+ entry.label !== "",
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Return the visible width of a row label.
63
+ *
64
+ * @param {object} entry
65
+ * Row entry.
66
+ * @param {object} options
67
+ * Group rendering options.
68
+ * @returns {number}
69
+ * Displayed label length.
70
+ */
71
+ function displayedLabelLength(entry, options) {
72
+ if (typeof entry.result !== "string" || entry.result === "") {
73
+ return entry.label.length;
74
+ }
75
+
76
+ const entryOptions = {
77
+ ...options,
78
+ ...entry,
79
+ };
80
+
81
+ const symbol = getResultSymbol(entry.result, entryOptions);
82
+
83
+ return [symbol, entry.label].join(" ").length;
84
+ }