@lewishowles/cli-style 0.17.1 → 0.18.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
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.18.0 - 2026-10-20
4
+
5
+ ### Breaking changes
6
+
7
+ - BREAKING: Panels without a `width` are now 80 columns wide instead of 40. Pass `width: 40` to keep the narrower panel.
8
+
9
+ ### Fixed
10
+
11
+ - Panels, chips, statuses, spans, feedback and labelled lines now keep their background, text colour and bold or dim text after text coloured by chalk, picocolors or `util.styleText`, which switches off one style at a time instead of resetting everything.
12
+
3
13
  ## 0.17.1 - 2026-10-20
4
14
 
5
15
  ### Changed
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "lewishowles-cli-style"
3
- version = "0.17.1"
3
+ version = "0.18.0"
4
4
  description = "Python adapter for the shared cli-style binary."
5
5
  authors = [{ name = "Lewis Howles" }]
6
6
  license = { text = "MIT" }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lewishowles/cli-style",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "Shared terminal output styles for Lewis Howles tools, diagnostics, and package CLIs.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,6 +29,18 @@ const hyperlinkPattern = new RegExp(
29
29
  // The sequences that clear every colour and text style at once.
30
30
  const clearStyleCodes = [reset, "\u001b[m"];
31
31
 
32
+ // The sequence that switches the text colour back to the terminal default.
33
+ const foregroundClose = "\u001b[39m";
34
+
35
+ // The sequence that switches the background colour back to the terminal default.
36
+ const backgroundClose = "\u001b[49m";
37
+
38
+ // The sequence that switches off both bold and dim text.
39
+ const weightClose = "\u001b[22m";
40
+
41
+ // Matches a full reset or a sequence that switches off only the text colour, background or bold and dim.
42
+ const closingStylePattern = new RegExp(`${escapeCharacter}\\[(?:0|22|39|49)?m`, "gu");
43
+
32
44
  // Splits text into the characters people see, so a joined emoji or an accented letter counts as one.
33
45
  const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
34
46
 
@@ -105,10 +117,12 @@ export function style(value, style, options = {}) {
105
117
  }
106
118
 
107
119
  /**
108
- * Switch the given styles back on after every full reset inside text, so an outer style
109
- * carries on past coloured text placed inside it. Coloured text ends with a full reset,
110
- * written as `\u001b[0m` or the shorter `\u001b[m`, which would otherwise turn the outer
111
- * style off as well.
120
+ * Switch the given styles back on wherever coloured text inside switches them off, so an
121
+ * outer style carries on past text coloured by this package, chalk, picocolors or
122
+ * `util.styleText`. After a full reset, written as `\u001b[0m` or `\u001b[m`, every given
123
+ * style comes back on. After a sequence that ends only the text colour, the background,
124
+ * or bold and dim, only the given styles of that kind come back on, so other colours the
125
+ * inner text left on stay on.
112
126
  *
113
127
  * @param {string} value
114
128
  * Text that may hold coloured parts.
@@ -116,22 +130,57 @@ export function style(value, style, options = {}) {
116
130
  * The styles to switch back on, each rendered on an empty string, such as
117
131
  * `style("", "bold", options)`. An empty entry adds nothing.
118
132
  * @returns {string}
119
- * The text with the styles switched back on after each full reset.
133
+ * The text with each switched-off style turned back on.
120
134
  */
121
135
  export function restoreStyles(value, stylesToRestore) {
122
- // The codes that turn the styles on, with each closing reset cut off.
123
- const openingCodes = stylesToRestore
124
- .map((styleToRestore) => styleToRestore.slice(0, -reset.length))
125
- .join("");
136
+ // The codes that turn each style on, with its closing reset cut off.
137
+ const openingCodes = stylesToRestore.map((styleToRestore) =>
138
+ styleToRestore.slice(0, -reset.length),
139
+ );
140
+
141
+ // Every style, switched back on together after a full reset.
142
+ const allOpeningCodes = openingCodes.join("");
143
+
144
+ // The text colour styles, switched back on after a text colour close.
145
+ let foregroundCodes = "";
146
+ // The background styles, switched back on after a background close.
147
+ let backgroundCodes = "";
148
+ // The bold and dim styles, switched back on after a bold and dim close.
149
+ let weightCodes = "";
150
+
151
+ // Sorts each style by the number at the start of its code. The text colour and
152
+ // background ranges include 39 and 49 because the default colour opens with them.
153
+ for (const openingCode of openingCodes) {
154
+ // The first number after the escape and bracket, such as 31 for red text.
155
+ const styleNumber = Number.parseInt(openingCode.slice(2), 10);
156
+
157
+ if ((styleNumber >= 30 && styleNumber <= 39) || (styleNumber >= 90 && styleNumber <= 97)) {
158
+ foregroundCodes += openingCode;
159
+ } else if (
160
+ (styleNumber >= 40 && styleNumber <= 49) ||
161
+ (styleNumber >= 100 && styleNumber <= 107)
162
+ ) {
163
+ backgroundCodes += openingCode;
164
+ } else if (styleNumber === 1 || styleNumber === 2) {
165
+ weightCodes += openingCode;
166
+ }
167
+ }
126
168
 
127
- // The text with the styles switched back on after each reset form handled so far.
128
- let restoredValue = value;
169
+ return value.replace(closingStylePattern, (closingCode) => {
170
+ if (closingCode === foregroundClose) {
171
+ return `${closingCode}${foregroundCodes}`;
172
+ }
129
173
 
130
- for (const clearStyleCode of clearStyleCodes) {
131
- restoredValue = restoredValue.replaceAll(clearStyleCode, `${clearStyleCode}${openingCodes}`);
132
- }
174
+ if (closingCode === backgroundClose) {
175
+ return `${closingCode}${backgroundCodes}`;
176
+ }
177
+
178
+ if (closingCode === weightClose) {
179
+ return `${closingCode}${weightCodes}`;
180
+ }
133
181
 
134
- return restoredValue;
182
+ return `${closingCode}${allOpeningCodes}`;
183
+ });
135
184
  }
136
185
 
137
186
  /**
@@ -1179,12 +1179,15 @@ function renderPanelExamples(options) {
1179
1179
  }),
1180
1180
  );
1181
1181
 
1182
- // A panel holding a coloured status line and coloured values, to show the panel
1183
- // background carrying on after them.
1182
+ // A panel holding a coloured status line, coloured values and a word coloured the way
1183
+ // chalk colours it, to show the panel background carrying on after them. The word is
1184
+ // written out by hand because chalk ends a colour by switching off only the text
1185
+ // colour, while this package's own colours end with a full reset.
1184
1186
  const nestedStylesExample = panel({
1185
1187
  ...options,
1186
1188
  lines: [
1187
1189
  status(resultTypes.SUCCESS, "Checks passed", options),
1190
+ options.colour === true ? "A \u001b[31mred\u001b[39m word" : "A red word",
1188
1191
  rowGroup({
1189
1192
  ...options,
1190
1193
  rows: [
@@ -1,11 +1,9 @@
1
1
  import { background, foreground, restoreStyles, style, visibleWidth } from "../formatters/ansi.js";
2
+ import { defaultWidth } from "../formatters/width.js";
2
3
  import { wrapText } from "../formatters/wrap.js";
3
4
  import { profiles } from "../profiles/profiles.js";
4
5
  import { panelColours } from "../theme/colours.js";
5
6
 
6
- // The width used when no width is given, such as a direct library call outside a terminal.
7
- const defaultWidth = 40;
8
-
9
7
  // The narrowest panel that still leaves one column for content beside the accent and padding.
10
8
  const minimumWidth = 6;
11
9
 
@@ -22,8 +20,9 @@ const defaultTone = "info";
22
20
  * @param {string[]} options.lines
23
21
  * Content lines to render.
24
22
  * @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.
23
+ * Total panel width, including the accent and padding. Defaults to 80 columns.
24
+ * Widths below 6 are raised to 6, which leaves room for one character of content.
25
+ * Longer lines wrap to fit.
27
26
  * @param {string} options.profile
28
27
  * Active output profile.
29
28
  * @param {string} options.title
@@ -86,8 +85,8 @@ function renderLine(line, width, isTitle, options) {
86
85
 
87
86
  const renderedAccent = renderPanelColour(accent, options);
88
87
 
89
- // The content with the panel colours switched back on after each reset inside it, so
90
- // the panel background carries on to the right edge.
88
+ // The content with the panel colours switched back on wherever coloured text inside
89
+ // switches them off, so the panel background carries on to the right edge.
91
90
  const restoredContent = restoreStyles(content, [
92
91
  background("", panelColours.background, options),
93
92
  foreground("", panelColours.body, options),