@tuicomponents/gauge 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,408 @@
1
+ // src/gauge.ts
2
+ import {
3
+ BaseTuiComponent,
4
+ measureLines,
5
+ registry
6
+ } from "@tuicomponents/core";
7
+ import { zodToJsonSchema } from "zod-to-json-schema";
8
+
9
+ // src/schema.ts
10
+ import { z } from "zod";
11
+ var gaugeZoneColorSchema = z.enum(["success", "warning", "error"]);
12
+ var gaugeZoneSchema = z.object({
13
+ /**
14
+ * Upper bound of this zone (inclusive).
15
+ * Value must be greater than the previous zone's threshold.
16
+ */
17
+ threshold: z.number(),
18
+ /**
19
+ * Optional semantic color for this zone.
20
+ * If not specified, uses default bar appearance.
21
+ */
22
+ color: gaugeZoneColorSchema.optional()
23
+ });
24
+ var gaugeStyleSchema = z.enum(["bar", "segments", "blocks"]);
25
+ var gaugeInputSchema = z.object({
26
+ /**
27
+ * Current gauge value.
28
+ */
29
+ value: z.number(),
30
+ /**
31
+ * Minimum value on the scale.
32
+ * @default 0
33
+ */
34
+ min: z.number().default(0),
35
+ /**
36
+ * Maximum value on the scale.
37
+ * @default 100
38
+ */
39
+ max: z.number().default(100),
40
+ /**
41
+ * Optional zones for colored thresholds.
42
+ * Zones should be ordered by increasing threshold.
43
+ */
44
+ zones: z.array(gaugeZoneSchema).optional(),
45
+ /**
46
+ * Width of the gauge bar in characters.
47
+ * @default 20
48
+ */
49
+ width: z.number().int().positive().default(20),
50
+ /**
51
+ * Style for gauge bar characters.
52
+ * @default "bar"
53
+ */
54
+ style: gaugeStyleSchema.default("bar"),
55
+ /**
56
+ * Optional label to display before the gauge.
57
+ */
58
+ label: z.string().optional(),
59
+ /**
60
+ * Whether to show the current value.
61
+ * @default true
62
+ */
63
+ showValue: z.boolean().default(true),
64
+ /**
65
+ * Unit string to display after the value (e.g., "%", "°C").
66
+ * @default ""
67
+ */
68
+ unit: z.string().default("")
69
+ });
70
+
71
+ // src/chars.ts
72
+ var barChars = {
73
+ filled: "\u2588",
74
+ empty: "\u2591"
75
+ };
76
+ var segmentsChars = {
77
+ filled: "\u25B0",
78
+ empty: "\u25B1"
79
+ };
80
+ var blocksChars = {
81
+ filled: "\u25A0",
82
+ empty: "\u25A1"
83
+ };
84
+ function getGaugeChars(style) {
85
+ switch (style) {
86
+ case "bar":
87
+ return barChars;
88
+ case "segments":
89
+ return segmentsChars;
90
+ case "blocks":
91
+ return blocksChars;
92
+ }
93
+ }
94
+
95
+ // src/layout.ts
96
+ function computeGaugeLayout(input, chars) {
97
+ const { value, min, max, width, zones, unit } = input;
98
+ const clampedValue = Math.min(max, Math.max(min, value));
99
+ const range = max - min;
100
+ const percentage = range > 0 ? (clampedValue - min) / range * 100 : 0;
101
+ const totalFilled = Math.round(percentage / 100 * width);
102
+ const totalEmpty = width - totalFilled;
103
+ const segments = [];
104
+ if (!zones || zones.length === 0) {
105
+ if (totalFilled > 0) {
106
+ segments.push({ length: totalFilled, filled: true });
107
+ }
108
+ if (totalEmpty > 0) {
109
+ segments.push({ length: totalEmpty, filled: false });
110
+ }
111
+ } else {
112
+ const zonePositions = [];
113
+ for (const zone of zones) {
114
+ const zonePercentage = range > 0 ? (zone.threshold - min) / range * 100 : 0;
115
+ const position = Math.round(zonePercentage / 100 * width);
116
+ zonePositions.push({ position, zone });
117
+ }
118
+ let currentPos = 0;
119
+ let remainingFilled = totalFilled;
120
+ for (let i = 0; i < zonePositions.length && remainingFilled > 0; i++) {
121
+ const zonePos = zonePositions[i];
122
+ if (!zonePos) continue;
123
+ const { position, zone } = zonePos;
124
+ const zoneEnd = Math.min(position, totalFilled);
125
+ const segmentLength = zoneEnd - currentPos;
126
+ if (segmentLength > 0) {
127
+ segments.push({
128
+ length: segmentLength,
129
+ filled: true,
130
+ color: zone.color
131
+ });
132
+ remainingFilled -= segmentLength;
133
+ currentPos = zoneEnd;
134
+ }
135
+ }
136
+ if (remainingFilled > 0 && zonePositions.length > 0) {
137
+ const lastZonePos = zonePositions[zonePositions.length - 1];
138
+ if (lastZonePos) {
139
+ const lastZone = lastZonePos.zone;
140
+ segments.push({
141
+ length: remainingFilled,
142
+ filled: true,
143
+ color: lastZone.color
144
+ });
145
+ }
146
+ }
147
+ if (totalEmpty > 0) {
148
+ segments.push({ length: totalEmpty, filled: false });
149
+ }
150
+ }
151
+ const valueStr = unit ? `${String(clampedValue)}${unit}` : String(clampedValue);
152
+ return {
153
+ label: input.label ?? "",
154
+ segments,
155
+ filledChar: chars.filled,
156
+ emptyChar: chars.empty,
157
+ displayValue: clampedValue,
158
+ valueStr,
159
+ barWidth: width,
160
+ percentage
161
+ };
162
+ }
163
+
164
+ // src/renderers.ts
165
+ import { anchorLine, DEFAULT_ANCHOR } from "@tuicomponents/core";
166
+ function getZoneColorFunction(color, theme) {
167
+ if (!color) {
168
+ return void 0;
169
+ }
170
+ switch (color) {
171
+ case "success":
172
+ return theme.semantic.success;
173
+ case "warning":
174
+ return theme.semantic.warning;
175
+ case "error":
176
+ return theme.semantic.error;
177
+ }
178
+ }
179
+ function renderSegmentAnsi(segment, layout, theme) {
180
+ const char = segment.filled ? layout.filledChar : layout.emptyChar;
181
+ const segmentStr = char.repeat(segment.length);
182
+ if (!theme) {
183
+ return segmentStr;
184
+ }
185
+ if (!segment.filled) {
186
+ return theme.semantic.secondary(segmentStr);
187
+ }
188
+ const colorFn = getZoneColorFunction(segment.color, theme);
189
+ if (colorFn) {
190
+ return colorFn(segmentStr);
191
+ }
192
+ return theme.semantic.primary(segmentStr);
193
+ }
194
+ function renderGaugeAnsi(layout, input, theme) {
195
+ const parts = [];
196
+ if (layout.label) {
197
+ const coloredLabel = theme ? theme.semantic.header(layout.label) : layout.label;
198
+ parts.push(coloredLabel);
199
+ parts.push(" ");
200
+ }
201
+ for (const segment of layout.segments) {
202
+ parts.push(renderSegmentAnsi(segment, layout, theme));
203
+ }
204
+ if (input.showValue) {
205
+ const coloredValue = theme ? theme.semantic.secondary(layout.valueStr) : layout.valueStr;
206
+ parts.push(" ");
207
+ parts.push(coloredValue);
208
+ }
209
+ return parts.join("");
210
+ }
211
+ function segmentNeedsEmphasis(segment) {
212
+ return segment.filled && !!segment.color && segment.color !== "success";
213
+ }
214
+ function renderGaugeMarkdown(layout, input) {
215
+ const parts = [];
216
+ if (layout.label) {
217
+ parts.push(layout.label);
218
+ parts.push(" ");
219
+ }
220
+ let i = 0;
221
+ while (i < layout.segments.length) {
222
+ const segment = layout.segments[i];
223
+ if (!segment) {
224
+ i++;
225
+ continue;
226
+ }
227
+ const needsEmphasis = segmentNeedsEmphasis(segment);
228
+ const char = segment.filled ? layout.filledChar : layout.emptyChar;
229
+ if (needsEmphasis) {
230
+ let combinedStr = char.repeat(segment.length);
231
+ let j = i + 1;
232
+ while (j < layout.segments.length) {
233
+ const nextSegment = layout.segments[j];
234
+ if (!nextSegment) break;
235
+ if (!segmentNeedsEmphasis(nextSegment)) {
236
+ break;
237
+ }
238
+ const nextChar = nextSegment.filled ? layout.filledChar : layout.emptyChar;
239
+ combinedStr += nextChar.repeat(nextSegment.length);
240
+ j++;
241
+ }
242
+ parts.push(`\`${combinedStr}\``);
243
+ i = j;
244
+ } else {
245
+ parts.push(char.repeat(segment.length));
246
+ i++;
247
+ }
248
+ }
249
+ if (input.showValue) {
250
+ parts.push(" ");
251
+ parts.push(layout.valueStr);
252
+ }
253
+ return anchorLine(parts.join(""), DEFAULT_ANCHOR);
254
+ }
255
+
256
+ // src/gauge.ts
257
+ var GaugeComponent = class extends BaseTuiComponent {
258
+ metadata = {
259
+ name: "gauge",
260
+ description: "Renders meters with threshold zones for status display",
261
+ version: "0.1.0",
262
+ supportedModes: ["ansi", "markdown"],
263
+ examples: [
264
+ {
265
+ name: "basic",
266
+ description: "Simple gauge at 75%",
267
+ input: {
268
+ value: 75,
269
+ min: 0,
270
+ max: 100
271
+ }
272
+ },
273
+ {
274
+ name: "with-label",
275
+ description: "Gauge with label",
276
+ input: {
277
+ value: 60,
278
+ label: "CPU:"
279
+ }
280
+ },
281
+ {
282
+ name: "with-unit",
283
+ description: "Gauge with unit",
284
+ input: {
285
+ value: 85,
286
+ label: "Memory:",
287
+ unit: "%"
288
+ }
289
+ },
290
+ {
291
+ name: "with-zones",
292
+ description: "Gauge with colored zones",
293
+ input: {
294
+ value: 75,
295
+ label: "Load:",
296
+ zones: [
297
+ { threshold: 30, color: "success" },
298
+ { threshold: 70, color: "warning" },
299
+ { threshold: 100, color: "error" }
300
+ ]
301
+ }
302
+ },
303
+ {
304
+ name: "temperature",
305
+ description: "Temperature gauge with zones",
306
+ input: {
307
+ value: 65,
308
+ min: 0,
309
+ max: 100,
310
+ label: "Temp:",
311
+ unit: "\xB0C",
312
+ zones: [
313
+ { threshold: 50, color: "success" },
314
+ { threshold: 80, color: "warning" },
315
+ { threshold: 100, color: "error" }
316
+ ]
317
+ }
318
+ },
319
+ {
320
+ name: "battery",
321
+ description: "Battery gauge",
322
+ input: {
323
+ value: 100,
324
+ label: "Battery:",
325
+ unit: "%",
326
+ style: "blocks"
327
+ }
328
+ },
329
+ {
330
+ name: "segments-style",
331
+ description: "Gauge with segments style",
332
+ input: {
333
+ value: 40,
334
+ style: "segments"
335
+ }
336
+ },
337
+ {
338
+ name: "blocks-style",
339
+ description: "Gauge with blocks style",
340
+ input: {
341
+ value: 60,
342
+ style: "blocks"
343
+ }
344
+ },
345
+ {
346
+ name: "wide-gauge",
347
+ description: "Wide gauge",
348
+ input: {
349
+ value: 45,
350
+ width: 40,
351
+ label: "Progress:"
352
+ }
353
+ },
354
+ {
355
+ name: "low-value",
356
+ description: "Gauge in success zone",
357
+ input: {
358
+ value: 15,
359
+ label: "Usage:",
360
+ zones: [
361
+ { threshold: 30, color: "success" },
362
+ { threshold: 70, color: "warning" },
363
+ { threshold: 100, color: "error" }
364
+ ]
365
+ }
366
+ }
367
+ ]
368
+ };
369
+ schema = gaugeInputSchema;
370
+ /**
371
+ * Override getJsonSchema to use direct schema generation.
372
+ */
373
+ getJsonSchema() {
374
+ return zodToJsonSchema(this.schema, {
375
+ name: this.metadata.name,
376
+ $refStrategy: "none"
377
+ });
378
+ }
379
+ render(input, context) {
380
+ const parsed = this.schema.parse(input);
381
+ const chars = getGaugeChars(parsed.style);
382
+ const layout = computeGaugeLayout(parsed, chars);
383
+ const output = context.renderMode === "markdown" ? renderGaugeMarkdown(layout, parsed) : renderGaugeAnsi(layout, parsed, context.theme);
384
+ const measured = measureLines(output);
385
+ return {
386
+ output,
387
+ actualWidth: measured.maxWidth,
388
+ lineCount: measured.lineCount
389
+ };
390
+ }
391
+ };
392
+ function createGauge() {
393
+ return new GaugeComponent();
394
+ }
395
+ registry.register(createGauge);
396
+ export {
397
+ GaugeComponent,
398
+ computeGaugeLayout,
399
+ createGauge,
400
+ gaugeInputSchema,
401
+ gaugeStyleSchema,
402
+ gaugeZoneColorSchema,
403
+ gaugeZoneSchema,
404
+ getGaugeChars,
405
+ renderGaugeAnsi,
406
+ renderGaugeMarkdown
407
+ };
408
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/gauge.ts","../src/schema.ts","../src/chars.ts","../src/layout.ts","../src/renderers.ts"],"sourcesContent":["import {\n BaseTuiComponent,\n type ComponentMetadata,\n type RenderContext,\n type RenderResult,\n measureLines,\n registry,\n} from \"@tuicomponents/core\";\nimport { zodToJsonSchema } from \"zod-to-json-schema\";\nimport { gaugeInputSchema, type GaugeInput } from \"./schema.js\";\nimport { getGaugeChars } from \"./chars.js\";\nimport { computeGaugeLayout } from \"./layout.js\";\nimport { renderGaugeAnsi, renderGaugeMarkdown } from \"./renderers.js\";\n\n/**\n * Gauge component for rendering meters with threshold zones.\n */\nclass GaugeComponent extends BaseTuiComponent<\n GaugeInput,\n typeof gaugeInputSchema\n> {\n readonly metadata: ComponentMetadata<GaugeInput> = {\n name: \"gauge\",\n description: \"Renders meters with threshold zones for status display\",\n version: \"0.1.0\",\n supportedModes: [\"ansi\", \"markdown\"],\n examples: [\n {\n name: \"basic\",\n description: \"Simple gauge at 75%\",\n input: {\n value: 75,\n min: 0,\n max: 100,\n },\n },\n {\n name: \"with-label\",\n description: \"Gauge with label\",\n input: {\n value: 60,\n label: \"CPU:\",\n },\n },\n {\n name: \"with-unit\",\n description: \"Gauge with unit\",\n input: {\n value: 85,\n label: \"Memory:\",\n unit: \"%\",\n },\n },\n {\n name: \"with-zones\",\n description: \"Gauge with colored zones\",\n input: {\n value: 75,\n label: \"Load:\",\n zones: [\n { threshold: 30, color: \"success\" },\n { threshold: 70, color: \"warning\" },\n { threshold: 100, color: \"error\" },\n ],\n },\n },\n {\n name: \"temperature\",\n description: \"Temperature gauge with zones\",\n input: {\n value: 65,\n min: 0,\n max: 100,\n label: \"Temp:\",\n unit: \"°C\",\n zones: [\n { threshold: 50, color: \"success\" },\n { threshold: 80, color: \"warning\" },\n { threshold: 100, color: \"error\" },\n ],\n },\n },\n {\n name: \"battery\",\n description: \"Battery gauge\",\n input: {\n value: 100,\n label: \"Battery:\",\n unit: \"%\",\n style: \"blocks\",\n },\n },\n {\n name: \"segments-style\",\n description: \"Gauge with segments style\",\n input: {\n value: 40,\n style: \"segments\",\n },\n },\n {\n name: \"blocks-style\",\n description: \"Gauge with blocks style\",\n input: {\n value: 60,\n style: \"blocks\",\n },\n },\n {\n name: \"wide-gauge\",\n description: \"Wide gauge\",\n input: {\n value: 45,\n width: 40,\n label: \"Progress:\",\n },\n },\n {\n name: \"low-value\",\n description: \"Gauge in success zone\",\n input: {\n value: 15,\n label: \"Usage:\",\n zones: [\n { threshold: 30, color: \"success\" },\n { threshold: 70, color: \"warning\" },\n { threshold: 100, color: \"error\" },\n ],\n },\n },\n ],\n };\n\n readonly schema = gaugeInputSchema;\n\n /**\n * Override getJsonSchema to use direct schema generation.\n */\n override getJsonSchema(): object {\n return zodToJsonSchema(this.schema, {\n name: this.metadata.name,\n $refStrategy: \"none\",\n });\n }\n\n render(input: GaugeInput, context: RenderContext): RenderResult {\n const parsed = this.schema.parse(input);\n\n // Get character set based on style\n const chars = getGaugeChars(parsed.style);\n\n // Compute layout once, share between renderers\n const layout = computeGaugeLayout(parsed, chars);\n\n // Choose renderer based on render mode\n const output =\n context.renderMode === \"markdown\"\n ? renderGaugeMarkdown(layout, parsed)\n : renderGaugeAnsi(layout, parsed, context.theme);\n\n const measured = measureLines(output);\n\n return {\n output,\n actualWidth: measured.maxWidth,\n lineCount: measured.lineCount,\n };\n }\n}\n\n/**\n * Factory function to create a gauge component.\n */\nexport function createGauge(): GaugeComponent {\n return new GaugeComponent();\n}\n\n// Register with global registry\nregistry.register(createGauge);\n\nexport { GaugeComponent };\n","import { z } from \"zod\";\n\n/**\n * Semantic color options for gauge zones.\n */\nexport const gaugeZoneColorSchema = z.enum([\"success\", \"warning\", \"error\"]);\n\nexport type GaugeZoneColor = z.infer<typeof gaugeZoneColorSchema>;\n\n/**\n * Schema for a zone in the gauge.\n * Zones define colored threshold regions.\n */\nexport const gaugeZoneSchema = z.object({\n /**\n * Upper bound of this zone (inclusive).\n * Value must be greater than the previous zone's threshold.\n */\n threshold: z.number(),\n\n /**\n * Optional semantic color for this zone.\n * If not specified, uses default bar appearance.\n */\n color: gaugeZoneColorSchema.optional(),\n});\n\nexport type GaugeZone = z.infer<typeof gaugeZoneSchema>;\n\n/**\n * Style options for gauge bar characters.\n */\nexport const gaugeStyleSchema = z.enum([\"bar\", \"segments\", \"blocks\"]);\n\nexport type GaugeStyle = z.infer<typeof gaugeStyleSchema>;\n\n/**\n * Schema for gauge component input.\n */\nexport const gaugeInputSchema = z.object({\n /**\n * Current gauge value.\n */\n value: z.number(),\n\n /**\n * Minimum value on the scale.\n * @default 0\n */\n min: z.number().default(0),\n\n /**\n * Maximum value on the scale.\n * @default 100\n */\n max: z.number().default(100),\n\n /**\n * Optional zones for colored thresholds.\n * Zones should be ordered by increasing threshold.\n */\n zones: z.array(gaugeZoneSchema).optional(),\n\n /**\n * Width of the gauge bar in characters.\n * @default 20\n */\n width: z.number().int().positive().default(20),\n\n /**\n * Style for gauge bar characters.\n * @default \"bar\"\n */\n style: gaugeStyleSchema.default(\"bar\"),\n\n /**\n * Optional label to display before the gauge.\n */\n label: z.string().optional(),\n\n /**\n * Whether to show the current value.\n * @default true\n */\n showValue: z.boolean().default(true),\n\n /**\n * Unit string to display after the value (e.g., \"%\", \"°C\").\n * @default \"\"\n */\n unit: z.string().default(\"\"),\n});\n\nexport type GaugeInput = z.input<typeof gaugeInputSchema>;\nexport type GaugeInputWithDefaults = z.output<typeof gaugeInputSchema>;\n","import type { GaugeStyle } from \"./schema.js\";\n\n/**\n * Characters used for gauge rendering.\n */\nexport interface GaugeChars {\n /** Filled portion character */\n filled: string;\n /** Empty portion character */\n empty: string;\n}\n\n/**\n * Bar style: █░\n */\nconst barChars: GaugeChars = {\n filled: \"█\",\n empty: \"░\",\n};\n\n/**\n * Segments style: ▰▱\n */\nconst segmentsChars: GaugeChars = {\n filled: \"▰\",\n empty: \"▱\",\n};\n\n/**\n * Blocks style: ■□\n */\nconst blocksChars: GaugeChars = {\n filled: \"■\",\n empty: \"□\",\n};\n\n/**\n * Get gauge characters for a given style.\n */\nexport function getGaugeChars(style: GaugeStyle): GaugeChars {\n switch (style) {\n case \"bar\":\n return barChars;\n case \"segments\":\n return segmentsChars;\n case \"blocks\":\n return blocksChars;\n }\n}\n","import type {\n GaugeInputWithDefaults,\n GaugeZone,\n GaugeZoneColor,\n} from \"./schema.js\";\nimport type { GaugeChars } from \"./chars.js\";\n\n/**\n * A segment of the gauge bar with its color.\n */\nexport interface GaugeSegment {\n /** Number of characters in this segment */\n length: number;\n /** Whether this segment is filled */\n filled: boolean;\n /** Optional zone color for this segment */\n color?: GaugeZoneColor | undefined;\n}\n\n/**\n * Pre-computed layout for the gauge.\n */\nexport interface GaugeLayout {\n /** Label to display (empty string if none) */\n label: string;\n /** Segments of the gauge bar (filled and empty portions with colors) */\n segments: GaugeSegment[];\n /** Character for filled portions */\n filledChar: string;\n /** Character for empty portions */\n emptyChar: string;\n /** Current value (clamped to min/max) */\n displayValue: number;\n /** Formatted value string (e.g., \"75%\") */\n valueStr: string;\n /** Total width of the bar */\n barWidth: number;\n /** Percentage (0-100) of fill */\n percentage: number;\n}\n\n/**\n * Find which zone a value belongs to.\n */\nfunction _findZoneForValue(\n value: number,\n zones: GaugeZone[] | undefined,\n _min: number\n): GaugeZone | undefined {\n if (!zones || zones.length === 0) {\n return undefined;\n }\n\n // Find the first zone where value <= threshold\n for (const zone of zones) {\n if (value <= zone.threshold) {\n return zone;\n }\n }\n\n // Value is above all zones, use last zone\n return zones[zones.length - 1];\n}\n\n/**\n * Compute the layout for a gauge.\n *\n * @param input - Validated gauge input with defaults applied\n * @param chars - Character set to use\n * @returns Computed layout ready for rendering\n */\nexport function computeGaugeLayout(\n input: GaugeInputWithDefaults,\n chars: GaugeChars\n): GaugeLayout {\n const { value, min, max, width, zones, unit } = input;\n\n // Clamp value to min/max range\n const clampedValue = Math.min(max, Math.max(min, value));\n\n // Calculate percentage (0-100)\n const range = max - min;\n const percentage = range > 0 ? ((clampedValue - min) / range) * 100 : 0;\n\n // Calculate total filled character count\n const totalFilled = Math.round((percentage / 100) * width);\n const totalEmpty = width - totalFilled;\n\n // Build segments based on zones\n const segments: GaugeSegment[] = [];\n\n if (!zones || zones.length === 0) {\n // No zones - simple filled/empty segments\n if (totalFilled > 0) {\n segments.push({ length: totalFilled, filled: true });\n }\n if (totalEmpty > 0) {\n segments.push({ length: totalEmpty, filled: false });\n }\n } else {\n // With zones - build colored segments\n // Calculate character positions for each zone boundary\n const zonePositions: { position: number; zone: GaugeZone }[] = [];\n\n for (const zone of zones) {\n // Convert zone threshold to character position\n const zonePercentage =\n range > 0 ? ((zone.threshold - min) / range) * 100 : 0;\n const position = Math.round((zonePercentage / 100) * width);\n zonePositions.push({ position, zone });\n }\n\n // Build filled segments with zone colors\n let currentPos = 0;\n let remainingFilled = totalFilled;\n\n for (let i = 0; i < zonePositions.length && remainingFilled > 0; i++) {\n const zonePos = zonePositions[i];\n if (!zonePos) continue;\n const { position, zone } = zonePos;\n const zoneEnd = Math.min(position, totalFilled);\n const segmentLength = zoneEnd - currentPos;\n\n if (segmentLength > 0) {\n segments.push({\n length: segmentLength,\n filled: true,\n color: zone.color,\n });\n remainingFilled -= segmentLength;\n currentPos = zoneEnd;\n }\n }\n\n // If there's remaining filled area beyond all zones, use the last zone's color\n if (remainingFilled > 0 && zonePositions.length > 0) {\n const lastZonePos = zonePositions[zonePositions.length - 1];\n if (lastZonePos) {\n const lastZone = lastZonePos.zone;\n segments.push({\n length: remainingFilled,\n filled: true,\n color: lastZone.color,\n });\n }\n }\n\n // Add empty segment\n if (totalEmpty > 0) {\n segments.push({ length: totalEmpty, filled: false });\n }\n }\n\n // Format value string\n const valueStr = unit\n ? `${String(clampedValue)}${unit}`\n : String(clampedValue);\n\n return {\n label: input.label ?? \"\",\n segments,\n filledChar: chars.filled,\n emptyChar: chars.empty,\n displayValue: clampedValue,\n valueStr,\n barWidth: width,\n percentage,\n };\n}\n","import { type TuiTheme, anchorLine, DEFAULT_ANCHOR } from \"@tuicomponents/core\";\nimport type { GaugeLayout, GaugeSegment } from \"./layout.js\";\nimport type { GaugeInputWithDefaults, GaugeZoneColor } from \"./schema.js\";\n\n/**\n * Get the theme color function for a zone color.\n */\nfunction getZoneColorFunction(\n color: GaugeZoneColor | undefined,\n theme: TuiTheme\n): ((text: string) => string) | undefined {\n if (!color) {\n return undefined;\n }\n\n switch (color) {\n case \"success\":\n return theme.semantic.success;\n case \"warning\":\n return theme.semantic.warning;\n case \"error\":\n return theme.semantic.error;\n }\n}\n\n/**\n * Render a segment in ANSI mode.\n */\nfunction renderSegmentAnsi(\n segment: GaugeSegment,\n layout: GaugeLayout,\n theme?: TuiTheme\n): string {\n const char = segment.filled ? layout.filledChar : layout.emptyChar;\n const segmentStr = char.repeat(segment.length);\n\n if (!theme) {\n return segmentStr;\n }\n\n if (!segment.filled) {\n // Empty segments use secondary color\n return theme.semantic.secondary(segmentStr);\n }\n\n // Filled segments use zone color or primary\n const colorFn = getZoneColorFunction(segment.color, theme);\n if (colorFn) {\n return colorFn(segmentStr);\n }\n\n return theme.semantic.primary(segmentStr);\n}\n\n/**\n * Render a gauge using ANSI escape codes for rich terminal output.\n *\n * @param layout - Pre-computed gauge layout\n * @param input - Original input with defaults\n * @param theme - Optional theme for colors\n * @returns ANSI-formatted gauge string\n */\nexport function renderGaugeAnsi(\n layout: GaugeLayout,\n input: GaugeInputWithDefaults,\n theme?: TuiTheme\n): string {\n const parts: string[] = [];\n\n // Add label if present\n if (layout.label) {\n const coloredLabel = theme\n ? theme.semantic.header(layout.label)\n : layout.label;\n parts.push(coloredLabel);\n parts.push(\" \");\n }\n\n // Render each segment\n for (const segment of layout.segments) {\n parts.push(renderSegmentAnsi(segment, layout, theme));\n }\n\n // Add value if enabled\n if (input.showValue) {\n const coloredValue = theme\n ? theme.semantic.secondary(layout.valueStr)\n : layout.valueStr;\n parts.push(\" \");\n parts.push(coloredValue);\n }\n\n return parts.join(\"\");\n}\n\n/**\n * Check if a segment should be emphasized with backticks in markdown.\n */\nfunction segmentNeedsEmphasis(segment: GaugeSegment): boolean {\n return segment.filled && !!segment.color && segment.color !== \"success\";\n}\n\n/**\n * Render a gauge using markdown-friendly output.\n *\n * Consolidates adjacent emphasized segments to avoid double-backtick issues\n * (e.g., `warning``error` renders incorrectly as two adjacent inline code spans).\n *\n * @param layout - Pre-computed gauge layout\n * @param input - Original input with defaults\n * @returns Markdown-friendly gauge string\n */\nexport function renderGaugeMarkdown(\n layout: GaugeLayout,\n input: GaugeInputWithDefaults\n): string {\n const parts: string[] = [];\n\n // Add label if present\n if (layout.label) {\n parts.push(layout.label);\n parts.push(\" \");\n }\n\n // Group consecutive segments by whether they need emphasis\n let i = 0;\n while (i < layout.segments.length) {\n const segment = layout.segments[i];\n if (!segment) {\n i++;\n continue;\n }\n const needsEmphasis = segmentNeedsEmphasis(segment);\n const char = segment.filled ? layout.filledChar : layout.emptyChar;\n\n if (needsEmphasis) {\n // Collect all consecutive emphasized segments\n let combinedStr = char.repeat(segment.length);\n let j = i + 1;\n while (j < layout.segments.length) {\n const nextSegment = layout.segments[j];\n if (!nextSegment) break;\n if (!segmentNeedsEmphasis(nextSegment)) {\n break;\n }\n const nextChar = nextSegment.filled\n ? layout.filledChar\n : layout.emptyChar;\n combinedStr += nextChar.repeat(nextSegment.length);\n j++;\n }\n // Wrap the entire combined string in backticks\n parts.push(`\\`${combinedStr}\\``);\n i = j;\n } else {\n // Non-emphasized segment - render as-is\n parts.push(char.repeat(segment.length));\n i++;\n }\n }\n\n // Add value if enabled\n if (input.showValue) {\n parts.push(\" \");\n parts.push(layout.valueStr);\n }\n\n return anchorLine(parts.join(\"\"), DEFAULT_ANCHOR);\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@tuicomponents/gauge",
3
+ "version": "0.1.1",
4
+ "description": "Gauge meter component with threshold zones for TUI",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "keywords": [
25
+ "tui",
26
+ "terminal",
27
+ "gauge",
28
+ "meter",
29
+ "visualization"
30
+ ],
31
+ "license": "UNLICENSED",
32
+ "dependencies": {
33
+ "zod": "^3.25.56",
34
+ "zod-to-json-schema": "^3.24.5",
35
+ "@tuicomponents/core": "0.1.1"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.0.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup",
42
+ "typecheck": "tsc --noEmit",
43
+ "lint": "eslint src",
44
+ "test": "vitest run",
45
+ "test:watch": "vitest",
46
+ "api-report": "api-extractor run",
47
+ "api-report:update": "api-extractor run --local",
48
+ "clean": "rm -rf dist"
49
+ }
50
+ }