@lewishowles/cli-style 0.8.1 → 0.10.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.
@@ -1,6 +1,11 @@
1
+ import { isTheme, themes } from "../theme/colours.js";
2
+
1
3
  // Default terminal width when no stream width is available.
2
4
  const defaultWidth = 80;
3
5
 
6
+ // Known terminal signals support a conservative dark palette when no theme is explicit.
7
+ const terminalThemeSignals = ["COLORTERM", "TERM_PROGRAM"];
8
+
4
9
  /**
5
10
  * Resolve terminal output capabilities from injected args, env, and stream data.
6
11
  *
@@ -47,11 +52,104 @@ export function resolveTerminalCapabilities(options = {}) {
47
52
  isCi,
48
53
  isDumb,
49
54
  isTty,
55
+ theme: resolveTheme({
56
+ argv,
57
+ env,
58
+ theme: options.theme,
59
+ }),
50
60
  unicode,
51
61
  width,
52
62
  };
53
63
  }
54
64
 
65
+ /**
66
+ * Resolve a terminal theme from explicit overrides and terminal signals.
67
+ *
68
+ * @param {object} options
69
+ * Theme resolution inputs.
70
+ * @returns {string}
71
+ * Resolved light or dark theme.
72
+ */
73
+ export function resolveTheme(options = {}) {
74
+ const flagTheme = readThemeFlag(options.argv ?? []);
75
+
76
+ if (flagTheme !== undefined) {
77
+ return flagTheme;
78
+ }
79
+
80
+ if (isTheme(options.theme) && options.theme !== themes.AUTO) {
81
+ return options.theme;
82
+ }
83
+
84
+ const colourFgBgTheme = resolveColourFgBgTheme(options.env?.COLORFGBG);
85
+
86
+ if (colourFgBgTheme !== themes.AUTO) {
87
+ return colourFgBgTheme;
88
+ }
89
+
90
+ return resolveTerminalSignalTheme(options.env) ?? themes.DARK;
91
+ }
92
+
93
+ /**
94
+ * Resolve a conservative theme from best-effort terminal signals.
95
+ *
96
+ * @param {object} env
97
+ * Environment values to inspect.
98
+ * @returns {string|undefined}
99
+ * Dark theme when a known terminal signal is present.
100
+ */
101
+ function resolveTerminalSignalTheme(env = {}) {
102
+ if (terminalThemeSignals.some((signal) => env[signal])) {
103
+ return themes.DARK;
104
+ }
105
+
106
+ return undefined;
107
+ }
108
+
109
+ /**
110
+ * Resolve known COLORFGBG background slots without guessing custom palettes.
111
+ *
112
+ * @param {string} colourFgBg
113
+ * Terminal foreground and background capability string.
114
+ * @returns {string}
115
+ * Resolved light, dark, or automatic theme.
116
+ */
117
+ function resolveColourFgBgTheme(colourFgBg) {
118
+ const background = colourFgBg?.split(";").at(-1);
119
+
120
+ if (background === "0" || background === "8") {
121
+ return themes.DARK;
122
+ }
123
+
124
+ if (background === "7" || background === "15") {
125
+ return themes.LIGHT;
126
+ }
127
+
128
+ return themes.AUTO;
129
+ }
130
+
131
+ /**
132
+ * Read the final explicit theme flag so later command arguments can override earlier ones.
133
+ *
134
+ * @param {string[]} argv
135
+ * CLI arguments to inspect.
136
+ * @returns {string|undefined}
137
+ * Explicit theme when present.
138
+ */
139
+ function readThemeFlag(argv) {
140
+ let theme;
141
+
142
+ for (const argument of argv) {
143
+ if (argument === "--dark") {
144
+ theme = themes.DARK;
145
+ } else if (argument === "--light") {
146
+ theme = themes.LIGHT;
147
+ }
148
+ }
149
+
150
+ return theme;
151
+ }
152
+
55
153
  /**
56
154
  * Resolve colour support from explicit flags and terminal state.
57
155
  *
@@ -0,0 +1,62 @@
1
+ // Static renderer metadata shared by CLI discovery and render validation.
2
+ export const rendererCatalogue = [
3
+ ["agent-transcript", "pattern", "agentTranscript", ["entries"], "agent-transcript"],
4
+ ["audit-finding", "pattern", "auditFinding", ["finding", "result"], "audit-finding"],
5
+ ["bar-chart", "primitive", "barChart", ["rows"], undefined],
6
+ ["chip", "primitive", "chip", ["label", "tone"], undefined],
7
+ ["command-result", "pattern", "commandResult", ["result", "summary"], "command-result"],
8
+ ["compact-data-table", "pattern", "compactDataTable", ["columns", "rows"], "compact-data-table"],
9
+ [
10
+ "confirmation-result",
11
+ "pattern",
12
+ "confirmationResult",
13
+ ["action", "state"],
14
+ "confirmation-result",
15
+ ],
16
+ [
17
+ "diagnostic-report",
18
+ "pattern",
19
+ "diagnosticReport",
20
+ ["findings", "summary"],
21
+ "diagnostic-report",
22
+ ],
23
+ ["diff-block", "pattern", "diffBlock", ["lines"], "diff-block"],
24
+ ["divider", "primitive", "divider", ["label"], undefined],
25
+ ["empty-state", "primitive", "emptyState", ["detail", "title"], undefined],
26
+ ["error-block", "primitive", "errorBlock", ["lines", "title"], undefined],
27
+ ["hint", "primitive", "hint", ["message"], undefined],
28
+ ["next-step-block", "pattern", "nextStepBlock", ["next", "reason"], "next-step-block"],
29
+ ["panel", "primitive", "panel", ["lines", "title", "tone"], undefined],
30
+ ["progress-bar", "primitive", "progressBar", ["max", "value"], undefined],
31
+ ["row", "primitive", "row", ["label", "value"], undefined],
32
+ ["row-group", "primitive", "rowGroup", ["rows"], undefined],
33
+ ["sparkline", "primitive", "sparkline", ["values"], "sparkline"],
34
+ ["span", "primitive", "span", ["tone", "value"], undefined],
35
+ ["status", "primitive", "status", ["detail", "type"], undefined],
36
+ ["step", "primitive", "step", ["label", "state"], undefined],
37
+ ["step-progress", "primitive", "stepProgress", ["current", "steps"], undefined],
38
+ ["table", "primitive", "table", ["columns", "rows"], undefined],
39
+ ["task-summary", "pattern", "taskSummary", ["result", "task"], "task-summary"],
40
+ ].map(([name, category, api, fields, fixture]) => ({
41
+ api,
42
+ category,
43
+ fields,
44
+ fixture,
45
+ name,
46
+ purpose: `${api} renderer.`,
47
+ }));
48
+
49
+ // Stable CLI names derived from the catalogue.
50
+ export const rendererNames = rendererCatalogue.map((renderer) => renderer.name);
51
+
52
+ /**
53
+ * Find renderer metadata by its stable CLI name.
54
+ *
55
+ * @param {string} name
56
+ * Renderer name to look up.
57
+ * @returns {object|undefined}
58
+ * Renderer metadata when found.
59
+ */
60
+ export function getRendererMetadata(name) {
61
+ return rendererCatalogue.find((renderer) => renderer.name === name);
62
+ }
@@ -0,0 +1,108 @@
1
+ import { getRendererMetadata, rendererCatalogue } from "../catalogue/renderer-catalogue.js";
2
+
3
+ // Rendering flags are resolved before catalogue command parsing.
4
+ const globalRenderingFlags = new Set([
5
+ "--dark",
6
+ "--json",
7
+ "--light",
8
+ "--no-color",
9
+ "--no-colour",
10
+ "--no-unicode",
11
+ "--plain",
12
+ ]);
13
+
14
+ /**
15
+ * Parse a catalogue discovery request.
16
+ *
17
+ * @param {string} command
18
+ * Catalogue command name.
19
+ * @param {string[]} args
20
+ * Arguments after the command.
21
+ * @returns {object}
22
+ * Parsed catalogue request.
23
+ */
24
+ export function parseCatalogueRequest(command, args = []) {
25
+ const commandArgs = args.filter((argument) => !globalRenderingFlags.has(argument));
26
+
27
+ if (command === "list" && commandArgs.length === 0) {
28
+ return {
29
+ command,
30
+ };
31
+ }
32
+
33
+ if (
34
+ command === "describe" &&
35
+ commandArgs.length === 1 &&
36
+ getRendererMetadata(commandArgs[0]) !== undefined
37
+ ) {
38
+ return {
39
+ command,
40
+ renderer: commandArgs[0],
41
+ };
42
+ }
43
+
44
+ if (command === "describe" && commandArgs.length === 1) {
45
+ throw new Error(`Unknown renderer: ${commandArgs[0]}`);
46
+ }
47
+
48
+ throw new Error(
49
+ command === "describe" ? "Usage: cli-style describe <renderer>" : "Usage: cli-style list",
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Render catalogue data for the requested output profile.
55
+ *
56
+ * @param {object} request
57
+ * Parsed catalogue request.
58
+ * @param {object} options
59
+ * Resolved rendering options.
60
+ * @returns {string}
61
+ * Text or JSON catalogue output.
62
+ */
63
+ export function renderCatalogue(request, options = {}) {
64
+ const data =
65
+ request.command === "list" ? rendererCatalogue : getRendererMetadata(request.renderer);
66
+
67
+ if (options.profile === "json") {
68
+ return JSON.stringify(data, null, 2);
69
+ }
70
+
71
+ if (request.command === "describe") {
72
+ return renderDescription(data);
73
+ }
74
+
75
+ return ["primitive", "pattern"]
76
+ .flatMap((category) => [
77
+ category,
78
+ ...rendererCatalogue
79
+ .filter((renderer) => renderer.category === category)
80
+ .map((renderer) => ` ${renderer.name} ${renderer.api}`),
81
+ "",
82
+ ])
83
+ .slice(0, -1)
84
+ .join("\n");
85
+ }
86
+
87
+ /**
88
+ * Render one renderer's discovery details.
89
+ *
90
+ * @param {object} renderer
91
+ * Renderer metadata.
92
+ * @returns {string}
93
+ * Human-readable renderer description.
94
+ */
95
+ function renderDescription(renderer) {
96
+ const gallery =
97
+ renderer.fixture === undefined
98
+ ? "No focused fixture."
99
+ : `cli-style gallery --fixture ${renderer.fixture}`;
100
+
101
+ return [
102
+ renderer.name,
103
+ `Category: ${renderer.category}`,
104
+ `JavaScript: ${renderer.api}`,
105
+ `Fields: ${renderer.fields.join(", ")}`,
106
+ `Gallery: ${gallery}`,
107
+ ].join("\n");
108
+ }
@@ -4,7 +4,14 @@ import { galleryFixtures, gallerySections, galleryVariants } from "../gallery/re
4
4
  import { isProfile, profiles } from "../profiles/profiles.js";
5
5
 
6
6
  // Global rendering flags accepted after `gallery` for focused review commands.
7
- const globalRenderingFlags = new Set(["--no-color", "--no-colour", "--no-unicode", "--plain"]);
7
+ const globalRenderingFlags = new Set([
8
+ "--dark",
9
+ "--light",
10
+ "--no-color",
11
+ "--no-colour",
12
+ "--no-unicode",
13
+ "--plain",
14
+ ]);
8
15
 
9
16
  /**
10
17
  * Parse gallery command arguments.
package/src/cli/help.js CHANGED
@@ -5,18 +5,23 @@ const helpLines = [
5
5
  "Usage:",
6
6
  " cli-style --help",
7
7
  " cli-style adapter-path bash|python",
8
+ " cli-style list [options]",
9
+ " cli-style describe <renderer> [options]",
8
10
  " cli-style gallery [variant] [options]",
9
11
  " cli-style render <renderer> [options] < input.json",
10
12
  "",
11
13
  "Commands:",
12
14
  " adapter-path Print a Bash or Python adapter path.",
13
15
  " gallery Print the read-only style gallery.",
16
+ " list List available primitives and patterns.",
17
+ " describe Describe one renderer and its input fields.",
14
18
  " render Render caller-provided JSON from stdin.",
15
19
  "",
16
20
  "Gallery variants:",
17
21
  " current, no-colour, no-unicode, plain",
18
22
  "",
19
23
  "Options:",
24
+ " --light, --dark Override automatic terminal-theme detection.",
20
25
  " --section <name> Show primitives or patterns.",
21
26
  " --fixture <name> Show one named pattern fixture.",
22
27
  " --profile <name> Render with a text profile.",
@@ -4,6 +4,7 @@ import { commandResult } from "../patterns/command-result.js";
4
4
  import { compactDataTable } from "../patterns/compact-data-table.js";
5
5
  import { confirmationResult } from "../patterns/confirmation-result.js";
6
6
  import { diagnosticReport } from "../patterns/diagnostic-report.js";
7
+ import { diffBlock } from "../patterns/diff-block.js";
7
8
  import { nextStepBlock } from "../patterns/next-step-block.js";
8
9
  import { taskSummary } from "../patterns/task-summary.js";
9
10
  import { barChart } from "../primitives/bar-chart.js";
@@ -13,11 +14,14 @@ import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
13
14
  import { panel } from "../primitives/panel.js";
14
15
  import { progressBar } from "../primitives/progress-bar.js";
15
16
  import { row } from "../primitives/row.js";
17
+ import { rowGroup } from "../primitives/row-group.js";
18
+ import { sparkline } from "../primitives/sparkline.js";
16
19
  import { span } from "../primitives/span.js";
17
20
  import { status } from "../primitives/status.js";
18
21
  import { step, stepProgress } from "../primitives/step-progress.js";
19
22
  import { table } from "../primitives/table.js";
20
23
  import { isProfile, profiles } from "../profiles/profiles.js";
24
+ export { rendererNames } from "../catalogue/renderer-catalogue.js";
21
25
 
22
26
  // Renderer names accepted by the custom-input render command.
23
27
  const renderers = {
@@ -37,6 +41,7 @@ const renderers = {
37
41
  "compact-data-table": (input, options) => compactDataTable(input, options),
38
42
  "confirmation-result": (input, options) => confirmationResult(input, options),
39
43
  "diagnostic-report": (input, options) => diagnosticReport(input, options),
44
+ "diff-block": (input, options) => diffBlock(input, options),
40
45
  divider: (input, options) =>
41
46
  divider({
42
47
  ...input,
@@ -73,6 +78,16 @@ const renderers = {
73
78
  ...input,
74
79
  ...options,
75
80
  }),
81
+ "row-group": (input, options) =>
82
+ rowGroup({
83
+ ...input,
84
+ ...options,
85
+ }),
86
+ sparkline: (input, options) =>
87
+ sparkline({
88
+ ...input,
89
+ ...options,
90
+ }),
76
91
  span: (input, options) =>
77
92
  span(input.value, input.tone, {
78
93
  ...input,
@@ -102,10 +117,16 @@ const renderers = {
102
117
  };
103
118
 
104
119
  // Global rendering flags accepted after `render <renderer>`.
105
- const globalRenderingFlags = new Set(["--no-color", "--no-colour", "--no-unicode", "--plain"]);
120
+ const globalRenderingFlags = new Set([
121
+ "--dark",
122
+ "--light",
123
+ "--no-color",
124
+ "--no-colour",
125
+ "--no-unicode",
126
+ "--plain",
127
+ ]);
106
128
 
107
129
  // Renderer names exposed for help text and tests.
108
- export const rendererNames = Object.keys(renderers);
109
130
 
110
131
  /**
111
132
  * Parse render command arguments.
@@ -100,7 +100,7 @@ export function stripAnsi(value) {
100
100
  * Coloured text, or the original text when colour is disabled.
101
101
  */
102
102
  function wrapColour(value, colour, options, createCode) {
103
- const resolvedColour = resolveColourValue(colour);
103
+ const resolvedColour = resolveColourValue(colour, options);
104
104
 
105
105
  if (options.colour !== true || resolvedColour === null) {
106
106
  return value;
@@ -118,6 +118,14 @@ function wrapColour(value, colour, options, createCode) {
118
118
  * ANSI foreground sequence.
119
119
  */
120
120
  function createForegroundCode(colour) {
121
+ if (colour === "default-foreground") {
122
+ return "\u001b[39m";
123
+ }
124
+
125
+ if (colour.startsWith("ansi-256:")) {
126
+ return `\u001b[38;5;${colour.slice("ansi-256:".length)}m`;
127
+ }
128
+
121
129
  const { blue, green, red } = parseHexColour(colour);
122
130
 
123
131
  return `\u001b[38;2;${red};${green};${blue}m`;
@@ -132,6 +140,14 @@ function createForegroundCode(colour) {
132
140
  * ANSI background sequence.
133
141
  */
134
142
  function createBackgroundCode(colour) {
143
+ if (colour === "default-background") {
144
+ return "\u001b[49m";
145
+ }
146
+
147
+ if (colour.startsWith("ansi-256:")) {
148
+ return `\u001b[48;5;${colour.slice("ansi-256:".length)}m`;
149
+ }
150
+
135
151
  const { blue, green, red } = parseHexColour(colour);
136
152
 
137
153
  return `\u001b[48;2;${red};${green};${blue}m`;
@@ -4,6 +4,7 @@ import { commandResult } from "../patterns/command-result.js";
4
4
  import { compactDataTable } from "../patterns/compact-data-table.js";
5
5
  import { confirmationResult } from "../patterns/confirmation-result.js";
6
6
  import { diagnosticReport } from "../patterns/diagnostic-report.js";
7
+ import { diffBlock } from "../patterns/diff-block.js";
7
8
  import { nextStepBlock } from "../patterns/next-step-block.js";
8
9
  import { taskSummary } from "../patterns/task-summary.js";
9
10
  import { stripAnsi } from "../formatters/ansi.js";
@@ -14,6 +15,8 @@ import { emptyState, errorBlock, hint } from "../primitives/feedback.js";
14
15
  import { panel } from "../primitives/panel.js";
15
16
  import { progressBar } from "../primitives/progress-bar.js";
16
17
  import { row } from "../primitives/row.js";
18
+ import { rowGroup } from "../primitives/row-group.js";
19
+ import { sparkline } from "../primitives/sparkline.js";
17
20
  import { span } from "../primitives/span.js";
18
21
  import { status } from "../primitives/status.js";
19
22
  import { step, stepProgress, stepStates } from "../primitives/step-progress.js";
@@ -114,7 +117,7 @@ export const galleryVariants = ["current", "no-colour", "no-unicode", "plain"];
114
117
  // Gallery sections available for focused output.
115
118
  export const gallerySections = ["primitives", "patterns"];
116
119
 
117
- // Pattern fixtures available for focused output and interactive search.
120
+ // Fixtures available for focused output and interactive search.
118
121
  export const galleryFixtures = [
119
122
  "diagnostic-report",
120
123
  "reporter",
@@ -126,6 +129,8 @@ export const galleryFixtures = [
126
129
  "compact-data-table-narrow",
127
130
  "confirmation-result",
128
131
  "next-step-block",
132
+ "diff-block",
133
+ "sparkline",
129
134
  ];
130
135
 
131
136
  /**
@@ -178,7 +183,11 @@ function renderVariant(variant, options, request) {
178
183
  const sections = [];
179
184
 
180
185
  if (request.fixture !== undefined) {
181
- sections.push(renderPatterns(resolvedVariant.options, request.fixture));
186
+ sections.push(
187
+ request.fixture === "sparkline"
188
+ ? renderSparklineFixture(resolvedVariant.options)
189
+ : renderPatterns(resolvedVariant.options, request.fixture),
190
+ );
182
191
  } else {
183
192
  if (request.section === undefined || request.section === "primitives") {
184
193
  sections.push(renderPrimitives(resolvedVariant.options));
@@ -433,6 +442,36 @@ function renderPatterns(options, fixture) {
433
442
  options,
434
443
  );
435
444
 
445
+ const diff = diffBlock(
446
+ {
447
+ lines: [
448
+ {
449
+ text: "render()",
450
+ type: "header",
451
+ },
452
+ {
453
+ newLineNumber: 102,
454
+ text: "return nextStep;",
455
+ type: "added",
456
+ },
457
+ {
458
+ oldLineNumber: 101,
459
+ text: "return currentStep;",
460
+ type: "removed",
461
+ },
462
+ {
463
+ newLineNumber: 100,
464
+ oldLineNumber: 100,
465
+ text: "const currentStep = getStep();",
466
+ type: "context",
467
+ },
468
+ ],
469
+ path: "src/render.js",
470
+ title: "Renderer change",
471
+ },
472
+ options,
473
+ );
474
+
436
475
  const reporter = createReporter(options);
437
476
 
438
477
  reporter.divider("Project setup", "Claude + Codex");
@@ -560,6 +599,7 @@ function renderPatterns(options, fixture) {
560
599
  "compact-data-table-narrow": ["Compact data table (narrow)", narrowCompactTable],
561
600
  "confirmation-result": ["Confirmation result", confirmation],
562
601
  "diagnostic-report": ["Diagnostic report", report],
602
+ "diff-block": ["Diff block", diff],
563
603
  "next-step-block": ["Next-step block", nextStep],
564
604
  reporter: ["Reporter", reporterOutput],
565
605
  "task-summary": ["Task summary", summary],
@@ -593,6 +633,9 @@ function renderPatterns(options, fixture) {
593
633
  "Audit finding",
594
634
  frameExample(audit, options),
595
635
  "",
636
+ "Diff block",
637
+ frameExample(diff, options),
638
+ "",
596
639
  "Compact data table",
597
640
  frameExample(compactTable, options),
598
641
  "",
@@ -621,6 +664,18 @@ function renderPatternFixture(fixture, options) {
621
664
  return [fixture[0], frameExample(fixture[1], options)].join("\n");
622
665
  }
623
666
 
667
+ /**
668
+ * Render the focused sparkline fixture.
669
+ *
670
+ * @param {object} options
671
+ * Rendering options.
672
+ * @returns {string}
673
+ * Framed sparkline fixture output.
674
+ */
675
+ function renderSparklineFixture(options) {
676
+ return ["Sparkline", frameExample(renderSparklineExample(options), options)].join("\n");
677
+ }
678
+
624
679
  /**
625
680
  * Add gallery-only framing around one rendered example.
626
681
  *
@@ -667,6 +722,9 @@ function renderPrimitives(options) {
667
722
  "Bar charts",
668
723
  renderBarChartExample(options),
669
724
  "",
725
+ "Sparklines",
726
+ renderSparklineExample(options),
727
+ "",
670
728
  "Tables",
671
729
  renderTableExample(options),
672
730
  "",
@@ -715,6 +773,30 @@ function renderPrimitives(options) {
715
773
  labelWidth: 8,
716
774
  result: resultTypes.FAILED,
717
775
  }),
776
+ "",
777
+ "Row groups",
778
+ rowGroup({
779
+ ...options,
780
+ rows: [
781
+ {
782
+ label: "Package",
783
+ value: "@lewishowles/components",
784
+ },
785
+ {
786
+ label: "Version",
787
+ value: "minor: 2.7.4 -> 2.8.0",
788
+ },
789
+ {
790
+ label: "Checks",
791
+ value: "type-check, unit, build",
792
+ },
793
+ {
794
+ label: "Bundle",
795
+ result: resultTypes.FAILED,
796
+ value: "is 22.3 KB, above the 12.0 KB budget",
797
+ },
798
+ ],
799
+ }),
718
800
  ].join("\n");
719
801
  }
720
802
 
@@ -781,6 +863,55 @@ function renderBarChartExample(options) {
781
863
  });
782
864
  }
783
865
 
866
+ /**
867
+ * Render representative sparkline sequences.
868
+ *
869
+ * @param {object} options
870
+ * Rendering options.
871
+ * @returns {string}
872
+ * Sparkline examples.
873
+ */
874
+ function renderSparklineExample(options) {
875
+ const examples = [
876
+ {
877
+ label: "Rising",
878
+ tone: "success",
879
+ values: [1, 2, 3, 4, 5, 6],
880
+ },
881
+ {
882
+ label: "Falling",
883
+ tone: "danger",
884
+ values: [6, 5, 4, 3, 2, 1],
885
+ },
886
+ {
887
+ label: "Flat",
888
+ tone: "info",
889
+ values: [4, 4, 4, 4, 4, 4],
890
+ },
891
+ {
892
+ label: "Negative",
893
+ tone: "warning",
894
+ values: [-6, -4, -5, -2, -3, -1],
895
+ },
896
+ {
897
+ label: "Narrow mixed",
898
+ tone: "info",
899
+ values: [0, 10, -5, 5, 2, 8, -2],
900
+ width: 4,
901
+ },
902
+ ];
903
+
904
+ return examples
905
+ .map((example) =>
906
+ sparkline({
907
+ ...options,
908
+ ...example,
909
+ width: example.width ?? 16,
910
+ }),
911
+ )
912
+ .join("\n");
913
+ }
914
+
784
915
  /**
785
916
  * Render common progress values and tones.
786
917
  *