ansimax 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  _Colors • Gradients • Animations • ASCII Art • Pixel Art • Trees • Components • Themes_
8
8
 
9
9
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE)
10
- [![npm](https://img.shields.io/badge/npm-v1.1.0-cb3837.svg?style=flat-square)](https://www.npmjs.com/package/ansimax)
10
+ [![npm](https://img.shields.io/badge/npm-v1.1.2-cb3837.svg?style=flat-square)](https://www.npmjs.com/package/ansimax)
11
11
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178c6.svg?style=flat-square)](tsconfig.json)
12
12
  [![Coverage](https://img.shields.io/badge/coverage-98%25-brightgreen.svg?style=flat-square)](#testing)
13
13
  [![Tests](https://img.shields.io/badge/tests-1700%2B%20passing-brightgreen.svg?style=flat-square)](#testing)
@@ -20,6 +20,27 @@ _Colors • Gradients • Animations • ASCII Art • Pixel Art • Trees • C
20
20
 
21
21
  ---
22
22
 
23
+ <div align="center">
24
+
25
+ ### 🎬 Preview
26
+
27
+ <table>
28
+ <tr>
29
+ <td align="center">
30
+ <strong>Animations</strong><br/>
31
+ <img src="media/animations.gif" alt="Ansimax animations demo" width="420"/>
32
+ </td>
33
+ <td align="center">
34
+ <strong>Loaders</strong><br/>
35
+ <img src="media/loaders.gif" alt="Ansimax loaders demo" width="420"/>
36
+ </td>
37
+ </tr>
38
+ </table>
39
+
40
+ </div>
41
+
42
+ ---
43
+
23
44
  ## 🌟 What is Ansimax?
24
45
 
25
46
  Ansimax is a **batteries-included rendering library** for building beautiful terminal UIs in Node.js. One package replaces a stack of 8+ dependencies — colors, gradients, ASCII art, spinners, progress bars, tables, menus, trees, themes, pixel art — combined into a single coherent TypeScript API with **zero runtime dependencies**.
@@ -29,14 +50,14 @@ npm install ansimax
29
50
  ```
30
51
 
31
52
  ```ts
32
- import { color, gradient, ascii, loader } from 'ansimax';
53
+ import { color, gradient, ascii, loader, sleep } from 'ansimax';
33
54
 
34
55
  console.log(ascii.banner('hello', {
35
56
  colorFn: (t) => gradient(t, ['#ff79c6', '#bd93f9', '#8be9fd']),
36
57
  }));
37
58
 
38
59
  const stop = loader.spin('Building project', { color: '#bd93f9' });
39
- await someAsyncWork();
60
+ await sleep(1500);
40
61
  stop('Build complete', true);
41
62
  ```
42
63
 
@@ -102,14 +123,14 @@ yarn add ansimax
102
123
  ## ⚡ 30-second example
103
124
 
104
125
  ```ts
105
- import { color, gradient, loader, ascii } from 'ansimax';
126
+ import { color, gradient, loader, ascii, sleep } from 'ansimax';
106
127
 
107
128
  console.log(ascii.banner('deploy', {
108
129
  colorFn: (t) => gradient(t, ['#ff6b6b', '#feca57', '#48dbfb']),
109
130
  }));
110
131
 
111
132
  const stop = loader.spin('Building project', { color: '#bd93f9' });
112
- await someAsyncWork();
133
+ await sleep(1500); // simulate async work
113
134
  stop('Build complete', true); // ✓ + success color
114
135
 
115
136
  console.log(color.green('✓') + ' Ready in ' + color.bold('1.4s'));
@@ -166,12 +187,19 @@ console.log(themes.primary('cyberpunk primary'));
166
187
  <img src="media/colors.png" alt="Colors and gradients" />
167
188
 
168
189
  ```ts
169
- import { color, gradient } from 'ansimax';
190
+ import { color, gradient, rainbow } from 'ansimax';
191
+
192
+ // Basic colors
193
+ console.log(color.red('red'), color.green('green'), color.blue('blue'));
170
194
 
171
- color.red('red'); color.green('green'); color.blue('blue');
172
- color.bold(text); color.italic(text); color.underline(text);
173
- gradient('fire to ocean', ['#ff6b6b', '#feca57', '#48dbfb']);
174
- color.rainbow('built-in rainbow preset');
195
+ // Style modifiers
196
+ console.log(color.bold('bold'), color.italic('italic'), color.underline('underlined'));
197
+
198
+ // Multi-stop gradient
199
+ console.log(gradient('fire to ocean', ['#ff6b6b', '#feca57', '#48dbfb']));
200
+
201
+ // Built-in rainbow preset
202
+ console.log(rainbow('built-in rainbow preset'));
175
203
  ```
176
204
 
177
205
  ### ASCII Art
@@ -181,13 +209,13 @@ color.rainbow('built-in rainbow preset');
181
209
  ```ts
182
210
  import { ascii, gradient } from 'ansimax';
183
211
 
184
- ascii.banner('HELLO', {
212
+ console.log(ascii.banner('HELLO', {
185
213
  font: 'big',
186
214
  align: 'center',
187
215
  colorFn: (t) => gradient(t, ['#ff79c6', '#bd93f9']),
188
- });
216
+ }));
189
217
 
190
- ascii.box('Rainbow box!', { padding: 1, borderStyle: 'rounded' });
218
+ console.log(ascii.box('Rainbow box!', { padding: 1, borderStyle: 'rounded' }));
191
219
  ```
192
220
 
193
221
  ### Trees
@@ -214,7 +242,7 @@ console.log(project.render({
214
242
  <img src="media/pixel_art.png" alt="Pixel art" />
215
243
 
216
244
  ```ts
217
- import { images, createCanvas, gradientRect } from 'ansimax';
245
+ import { images, createCanvas, gradientRect, SPRITES } from 'ansimax';
218
246
 
219
247
  // Built-in sprite
220
248
  console.log(images.sprite('heart'));
@@ -230,7 +258,8 @@ console.log(gradientRect({
230
258
  const c = createCanvas(40, 10);
231
259
  c.fill({ r: 18, g: 18, b: 38 });
232
260
  c.drawCircle(20, 5, 4, { r: 255, g: 200, b: 0 }, true);
233
- c.drawSprite(2, 2, images.sprites.star!.pixels);
261
+ const starSprite = SPRITES.star;
262
+ if (starSprite) c.drawSprite(2, 2, starSprite.pixels);
234
263
  c.print();
235
264
  ```
236
265
 
@@ -241,15 +270,15 @@ c.print();
241
270
  ```ts
242
271
  import { components, color } from 'ansimax';
243
272
 
244
- components.table([
273
+ console.log(components.table([
245
274
  ['Module', 'Status', 'Coverage'],
246
275
  ['colors', color.green('● ready'), '100%'],
247
276
  ['animations', color.green('● ready'), '100%'],
248
277
  ['loaders', color.green('● ready'), '100%'],
249
- ], { borderStyle: 'rounded' });
278
+ ], { borderStyle: 'rounded' }));
250
279
 
251
- components.badge('VERSION', 'v1.1.0');
252
- components.badge('BUILD', 'passing');
280
+ console.log(components.badge('VERSION', 'v1.1.2'));
281
+ console.log(components.badge('BUILD', 'passing'));
253
282
  ```
254
283
 
255
284
  ### Timeline
@@ -257,22 +286,24 @@ components.badge('BUILD', 'passing');
257
286
  <img src="media/timeline.png" alt="Timeline" />
258
287
 
259
288
  ```ts
260
- components.timeline([
289
+ import { components } from 'ansimax';
290
+
291
+ console.log(components.timeline([
261
292
  { label: 'Project init', done: true, time: '10:00' },
262
293
  { label: 'Build pipeline', done: true, time: '10:15' },
263
294
  { label: 'Run tests', done: false, time: '10:32' },
264
295
  { label: 'Deploy to npm', done: false },
265
- ]);
296
+ ]));
266
297
  ```
267
298
 
268
299
  ### Loaders & Progress
269
300
 
270
301
  ```ts
271
- import { loader } from 'ansimax';
302
+ import { loader, sleep } from 'ansimax';
272
303
 
273
304
  // Spinner with success/failure
274
305
  const stop = loader.spin('Loading...', { color: '#bd93f9' });
275
- await work();
306
+ await sleep(1500);
276
307
  stop('Done!', true); // ✓ green icon
277
308
 
278
309
  // Animated progress bar
@@ -282,18 +313,22 @@ await loader.progressAnimate(100, 'Downloading', {
282
313
 
283
314
  // Hierarchical tasks with parallel execution
284
315
  await loader.tasks([
285
- { text: 'Build', fn: async () => build(), subtasks: [
286
- { text: 'TypeScript', fn: async () => tsc() },
287
- { text: 'Bundle', fn: async () => bundle() },
288
- ]},
289
- { text: 'Test', fn: async () => test() },
316
+ {
317
+ text: 'Build',
318
+ fn: async () => await sleep(500),
319
+ subtasks: [
320
+ { text: 'TypeScript', fn: async () => await sleep(800) },
321
+ { text: 'Bundle', fn: async () => await sleep(600) },
322
+ ],
323
+ },
324
+ { text: 'Test', fn: async () => await sleep(700) },
290
325
  ], { parallel: true });
291
326
  ```
292
327
 
293
328
  ### Animations
294
329
 
295
330
  ```ts
296
- import { animate, gradient } from 'ansimax';
331
+ import { animate, gradient, sleep } from 'ansimax';
297
332
 
298
333
  await animate.typewriter('Welcome to the deployment wizard...', {
299
334
  speed: 30,
@@ -304,9 +339,9 @@ await animate.fadeIn('Loading complete', { duration: 600 });
304
339
 
305
340
  // Race steps against a timeout — never hang
306
341
  await animate.parallel([
307
- async () => await checkNetwork(),
308
- async () => await checkDatabase(),
309
- async () => await checkAuth(),
342
+ async () => await sleep(500), // simulated network check
343
+ async () => await sleep(700), // simulated database check
344
+ async () => await sleep(400), // simulated auth check
310
345
  ], { timeout: 5000 });
311
346
  ```
312
347
 
@@ -319,7 +354,7 @@ import { themes, createTheme } from 'ansimax';
319
354
 
320
355
  // Built-in themes
321
356
  themes.use('dracula');
322
- themes.primary('hello');
357
+ console.log(themes.primary('hello'));
323
358
 
324
359
  // Listen for changes
325
360
  const off = themes.onChange((newTheme, oldTheme) => {
@@ -329,28 +364,60 @@ const off = themes.onChange((newTheme, oldTheme) => {
329
364
  // Multi-tenant: each instance fully isolated
330
365
  const tenantA = createTheme('nord');
331
366
  const tenantB = createTheme('matrix');
332
- tenantA.register('custom', myDef); // doesn't leak to tenantB
367
+
368
+ // Define a custom theme and register it ONLY in tenantA
369
+ tenantA.register('custom', {
370
+ name: 'Custom',
371
+ primary: '#ff5e5e',
372
+ secondary: '#5e5eff',
373
+ accent: '#5eff5e',
374
+ success: '#10b981',
375
+ warning: '#fbbf24',
376
+ error: '#ef4444',
377
+ info: '#06b6d4',
378
+ muted: '#6b7280',
379
+ bg: '#1e293b',
380
+ surface: '#334155',
381
+ text: '#f1f5f9',
382
+ gradient: ['#ff5e5e', '#5eff5e', '#5e5eff'],
383
+ });
384
+
385
+ console.log('tenantA themes include custom?', tenantA.list().includes('custom'));
386
+ console.log('tenantB themes include custom?', tenantB.list().includes('custom'));
387
+ // ↑ false — full isolation
333
388
  ```
334
389
 
335
390
  ---
336
391
 
337
392
  ## 📚 Examples
338
393
 
339
- Seven production-grade examples ship in the npm package and are runnable directly. Find them in [`/examples`](./examples) once you install:
394
+ Eleven production-grade examples ship in the npm package and are runnable directly. Find them in [`/examples`](./examples) once you install:
340
395
 
341
396
  | File | What it demonstrates |
342
397
  |---|---|
343
- | `trees-basic.ts` | Minimal trees examplebuilder API + algorithms |
344
- | `01-cli-installer.ts` | npm-create style installer banner + hierarchical tasks + status icons + summary box |
345
- | `02-live-dashboard.ts` | Real-time dashboard — `frames.live` + service table + gradient bars + `onResize` + SIGINT cleanup |
346
- | `03-pixel-art-game.ts` | Bouncing rocket sprite — canvas + alpha blending + gradient + FPS counter + drift-corrected loop |
347
- | `04-interactive-deploy.ts` | Menu + multi-select + `loader.multi` + `createTheme` + `onConfigChange` |
348
- | `05-tree-visualizations.ts` | Filesystem + dependency + JSON + decision trees (`walk` + `measure` bonus) |
349
- | `06-everything-together.ts` | Comprehensive showcase every module exercised in one cohesive demo |
398
+ | `01-quick-smoke.ts` | Quick smoke testverifies every major import works |
399
+ | `02-colors-gradients.ts` | Every color fn, gradient types, presets, compose, chain API |
400
+ | `03-ascii-banners.ts` | Banners (`big`/`small`), 6 box styles, dividers, logo composer |
401
+ | `04-trees.ts` | Tree builder + plain-data API, 4 styles, palettes, algorithms (walk/find/map/filter) |
402
+ | `05-components.ts` | Tables, badges, status, sections, columns, timelines, progress bars |
403
+ | `06-pixel-art.ts` | Sprites, custom canvas, gradient rects with dither, transforms (flip/rotate) |
404
+ | `07-animations.ts` | typewriter, fadeIn/Out, slide, pulse, wave, glitch, reveal |
405
+ | `08-loaders.ts` | spinner styles, animated progress, hierarchical tasks, countdown |
406
+ | `09-themes.ts` | All 8 built-in themes, listeners, custom theme registration, per-instance isolation |
407
+ | `10-everything.ts` | Comprehensive showcase — every module exercised in one cohesive demo |
408
+ | `all-in-one.mjs` | Full demo in **ESM** (plain JS with `import`) — no TypeScript needed |
409
+ | `all-in-one.cjs` | Full demo in **CommonJS** (plain JS with `require`) — no TypeScript needed |
350
410
 
351
411
  Run any example with:
352
412
  ```bash
353
- npx tsx examples/06-everything-together.ts
413
+ # TypeScript examples
414
+ npx tsx examples/10-everything.ts
415
+
416
+ # Plain JS — ESM
417
+ node examples/all-in-one.mjs
418
+
419
+ # Plain JS — CommonJS
420
+ node examples/all-in-one.cjs
354
421
  ```
355
422
 
356
423
  ---
@@ -388,11 +455,11 @@ const off = onConfigKeyChange('theme', (newTheme, oldTheme) => {
388
455
 
389
456
  // Temporary override + auto-restore on completion or throw
390
457
  await withConfig({ animationSpeed: 'fast' }, async () => {
391
- await runDemo();
458
+ // ...your fast-mode code here...
392
459
  });
393
460
 
394
461
  // Strict mode catches config typos
395
- configure({ unknwnKey: 'x' }, { strict: true }); // throws RangeError
462
+ // configure({ unknwnKey: 'x' }, { strict: true }); // throws RangeError
396
463
  ```
397
464
 
398
465
  ---
@@ -604,7 +671,7 @@ ansimax/
604
671
  │ ├── trees/ Tree builder + algorithms
605
672
  │ ├── utils/ ANSI primitives + helpers
606
673
  │ └── configure.ts Global config + subscribers
607
- ├── examples/ 7 production-grade examples
674
+ ├── examples/ 10 examples (TS) + 2 (JS — ESM & CJS) — all features covered
608
675
  └── __tests__/ 16 test suites, 1700+ tests
609
676
  ```
610
677
 
@@ -612,6 +679,28 @@ ansimax/
612
679
 
613
680
  ## 📝 Changelog
614
681
 
682
+ ### v1.1.2 — Maturity & robustness
683
+
684
+ Patch release focused on quality refinements — no API changes.
685
+
686
+ - 🛡️ **`process.setMaxListeners` defensive bump** — prevents `MaxListenersExceededWarning` in HMR / nodemon / ts-node-dev setups where ansimax modules re-register cursor-restore handlers
687
+ - 🧪 **Uniform `TypeError` for theme validation** — `themes.register()` now consistently throws `TypeError` for structural / type errors (was a mix of `Error` and `TypeError`)
688
+ - 🎯 **`themes.use()` throws `RangeError`** for unknown theme names (was `Error`) — better semantic match for "value out of allowed set"
689
+ - 📝 **Cleaner barrel re-exports** — header comment now documents legacy aliases and recommends canonical names
690
+
691
+ Drop-in replacement for `1.1.1`.
692
+
693
+ ### v1.1.1 — Bug fixes + cleaner examples
694
+
695
+ Patch release fixing two bugs from real-world v1.1.0 testing, plus a refreshed examples folder.
696
+
697
+ - 🐛 **Fixed `box()` crash** with `padding: { x, y }` — now gracefully falls back to default for non-numeric padding (also handles NaN, Infinity, strings)
698
+ - 🐛 **Fixed `components.menu()` cursor leak** on abrupt exit (Ctrl+C, SIGTERM) — emergency cleanup handlers now restore the cursor even when the process is killed mid-menu
699
+ - 📚 **New examples** — 10 TypeScript examples + 2 plain JS variants (`all-in-one.mjs` for ESM, `all-in-one.cjs` for CommonJS)
700
+ - 📖 **READMEs updated** — preview GIFs in the header, comprehensive showcase GIF in the footer
701
+
702
+ No API changes — drop-in replacement for `1.1.0`.
703
+
615
704
  ### v1.1.0 — Comprehensive hardening + new features
616
705
 
617
706
  A massive robustness pass across every module, plus a new `trees` module. **100% backward compatible** — every existing API works identically.
@@ -676,6 +765,18 @@ If Ansimax saves you time, please star the repo on [GitHub](https://github.com/B
676
765
 
677
766
  ---
678
767
 
768
+ ## 🎬 Full showcase
769
+
770
+ <div align="center">
771
+
772
+ <img src="media/all-ansimax.gif" alt="Ansimax full showcase — everything in action" width="720"/>
773
+
774
+ _All features in action — typewriter, gradients, ASCII banners, trees, tables, spinners, themes, and pixel art_
775
+
776
+ </div>
777
+
778
+ ---
779
+
679
780
  ## 📜 License
680
781
 
681
782
  [Apache License 2.0](LICENSE) © 2026 Brashkie
package/dist/index.js CHANGED
@@ -225,6 +225,11 @@ var cursor = {
225
225
  var _exitHandlerRegistered = false;
226
226
  var _isTestEnv = () => process.env["JEST_WORKER_ID"] !== void 0 || process.env["VITEST"] !== void 0 || process.env["NODE_ENV"] === "test";
227
227
  var _installCursorRestoreImpl = () => {
228
+ try {
229
+ const current = process.getMaxListeners?.() ?? 10;
230
+ if (current < 20) process.setMaxListeners?.(20);
231
+ } catch {
232
+ }
228
233
  const restore = () => {
229
234
  try {
230
235
  safeStreamWrite(process.stdout, cursor.show());
@@ -2264,7 +2269,8 @@ var banner = (text, opts = {}) => {
2264
2269
  var box = (text, opts = {}) => {
2265
2270
  const safe = ensureString2(text, "box(text)");
2266
2271
  const { padding = 1, borderStyle = "rounded", width = null } = opts;
2267
- const safePadding = Math.max(0, Math.floor(padding));
2272
+ const padNum = typeof padding === "number" && Number.isFinite(padding) ? padding : 1;
2273
+ const safePadding = Math.max(0, Math.floor(padNum));
2268
2274
  const b = BOX_STYLES[borderStyle] ?? BOX_STYLES.rounded;
2269
2275
  const lines = safe.split("\n");
2270
2276
  const inner = width != null ? lines.map((l) => padEnd(truncateAnsi(l, width, ""), width)) : lines;
@@ -3677,7 +3683,32 @@ var menu = (items, opts = {}) => {
3677
3683
  }
3678
3684
  cursorHidden = false;
3679
3685
  }
3686
+ try {
3687
+ process.off("SIGINT", emergencyCleanup);
3688
+ process.off("SIGTERM", emergencyCleanup);
3689
+ process.off("exit", emergencyCleanup);
3690
+ } catch {
3691
+ }
3692
+ };
3693
+ const emergencyCleanup = () => {
3694
+ if (cursorHidden) {
3695
+ try {
3696
+ out.write(cursor.show());
3697
+ } catch {
3698
+ }
3699
+ cursorHidden = false;
3700
+ }
3701
+ try {
3702
+ if (inp.setRawMode) inp.setRawMode(false);
3703
+ } catch {
3704
+ }
3680
3705
  };
3706
+ try {
3707
+ process.once("SIGINT", emergencyCleanup);
3708
+ process.once("SIGTERM", emergencyCleanup);
3709
+ process.once("exit", emergencyCleanup);
3710
+ } catch {
3711
+ }
3681
3712
  try {
3682
3713
  emit(cursor.hide());
3683
3714
  cursorHidden = true;
@@ -4087,12 +4118,12 @@ var validateTheme = (t) => {
4087
4118
  }
4088
4119
  const obj = t;
4089
4120
  if (typeof obj.name !== "string" || obj.name.length === 0) {
4090
- throw new Error('Theme must have a non-empty "name" string.');
4121
+ throw new TypeError('Theme must have a non-empty "name" string.');
4091
4122
  }
4092
4123
  for (const key of REQUIRED_COLOR_KEYS) {
4093
4124
  const value = obj[key];
4094
4125
  if (typeof value !== "string" || !HEX_RE4.test(value)) {
4095
- throw new Error(
4126
+ throw new TypeError(
4096
4127
  `Invalid hex in theme "${obj.name}" \u2192 ${key}: ${JSON.stringify(value)}. Expected #RGB or #RRGGBB.`
4097
4128
  );
4098
4129
  }
@@ -4101,20 +4132,20 @@ var validateTheme = (t) => {
4101
4132
  const value = obj[key];
4102
4133
  if (value === void 0) continue;
4103
4134
  if (typeof value !== "string" || !HEX_RE4.test(value)) {
4104
- throw new Error(
4135
+ throw new TypeError(
4105
4136
  `Invalid hex in theme "${obj.name}" \u2192 ${key}: ${JSON.stringify(value)}.`
4106
4137
  );
4107
4138
  }
4108
4139
  }
4109
4140
  if (!Array.isArray(obj.gradient) || obj.gradient.length < 2) {
4110
- throw new Error(
4141
+ throw new TypeError(
4111
4142
  `Theme "${obj.name}" must define a "gradient" array with at least 2 colors.`
4112
4143
  );
4113
4144
  }
4114
4145
  for (let i = 0; i < obj.gradient.length; i++) {
4115
4146
  const stop = obj.gradient[i];
4116
4147
  if (typeof stop !== "string" || !HEX_RE4.test(stop)) {
4117
- throw new Error(
4148
+ throw new TypeError(
4118
4149
  `Invalid hex in theme "${obj.name}" \u2192 gradient[${i}]: ${JSON.stringify(stop)}.`
4119
4150
  );
4120
4151
  }
@@ -4426,8 +4457,8 @@ var themes = {
4426
4457
  use(name) {
4427
4458
  const t = _globalRegistry.get(name);
4428
4459
  if (!t) {
4429
- throw new Error(
4430
- `Theme "${name}" not found. Available: ${[..._globalRegistry.keys()].join(", ")}`
4460
+ throw new RangeError(
4461
+ `Theme "${name}" not found. Available themes: ${[..._globalRegistry.keys()].join(", ")}`
4431
4462
  );
4432
4463
  }
4433
4464
  const old = _globalActive;
package/dist/index.mjs CHANGED
@@ -53,6 +53,11 @@ var cursor = {
53
53
  var _exitHandlerRegistered = false;
54
54
  var _isTestEnv = () => process.env["JEST_WORKER_ID"] !== void 0 || process.env["VITEST"] !== void 0 || process.env["NODE_ENV"] === "test";
55
55
  var _installCursorRestoreImpl = () => {
56
+ try {
57
+ const current = process.getMaxListeners?.() ?? 10;
58
+ if (current < 20) process.setMaxListeners?.(20);
59
+ } catch {
60
+ }
56
61
  const restore = () => {
57
62
  try {
58
63
  safeStreamWrite(process.stdout, cursor.show());
@@ -2092,7 +2097,8 @@ var banner = (text, opts = {}) => {
2092
2097
  var box = (text, opts = {}) => {
2093
2098
  const safe = ensureString2(text, "box(text)");
2094
2099
  const { padding = 1, borderStyle = "rounded", width = null } = opts;
2095
- const safePadding = Math.max(0, Math.floor(padding));
2100
+ const padNum = typeof padding === "number" && Number.isFinite(padding) ? padding : 1;
2101
+ const safePadding = Math.max(0, Math.floor(padNum));
2096
2102
  const b = BOX_STYLES[borderStyle] ?? BOX_STYLES.rounded;
2097
2103
  const lines = safe.split("\n");
2098
2104
  const inner = width != null ? lines.map((l) => padEnd(truncateAnsi(l, width, ""), width)) : lines;
@@ -3505,7 +3511,32 @@ var menu = (items, opts = {}) => {
3505
3511
  }
3506
3512
  cursorHidden = false;
3507
3513
  }
3514
+ try {
3515
+ process.off("SIGINT", emergencyCleanup);
3516
+ process.off("SIGTERM", emergencyCleanup);
3517
+ process.off("exit", emergencyCleanup);
3518
+ } catch {
3519
+ }
3520
+ };
3521
+ const emergencyCleanup = () => {
3522
+ if (cursorHidden) {
3523
+ try {
3524
+ out.write(cursor.show());
3525
+ } catch {
3526
+ }
3527
+ cursorHidden = false;
3528
+ }
3529
+ try {
3530
+ if (inp.setRawMode) inp.setRawMode(false);
3531
+ } catch {
3532
+ }
3508
3533
  };
3534
+ try {
3535
+ process.once("SIGINT", emergencyCleanup);
3536
+ process.once("SIGTERM", emergencyCleanup);
3537
+ process.once("exit", emergencyCleanup);
3538
+ } catch {
3539
+ }
3509
3540
  try {
3510
3541
  emit(cursor.hide());
3511
3542
  cursorHidden = true;
@@ -3915,12 +3946,12 @@ var validateTheme = (t) => {
3915
3946
  }
3916
3947
  const obj = t;
3917
3948
  if (typeof obj.name !== "string" || obj.name.length === 0) {
3918
- throw new Error('Theme must have a non-empty "name" string.');
3949
+ throw new TypeError('Theme must have a non-empty "name" string.');
3919
3950
  }
3920
3951
  for (const key of REQUIRED_COLOR_KEYS) {
3921
3952
  const value = obj[key];
3922
3953
  if (typeof value !== "string" || !HEX_RE4.test(value)) {
3923
- throw new Error(
3954
+ throw new TypeError(
3924
3955
  `Invalid hex in theme "${obj.name}" \u2192 ${key}: ${JSON.stringify(value)}. Expected #RGB or #RRGGBB.`
3925
3956
  );
3926
3957
  }
@@ -3929,20 +3960,20 @@ var validateTheme = (t) => {
3929
3960
  const value = obj[key];
3930
3961
  if (value === void 0) continue;
3931
3962
  if (typeof value !== "string" || !HEX_RE4.test(value)) {
3932
- throw new Error(
3963
+ throw new TypeError(
3933
3964
  `Invalid hex in theme "${obj.name}" \u2192 ${key}: ${JSON.stringify(value)}.`
3934
3965
  );
3935
3966
  }
3936
3967
  }
3937
3968
  if (!Array.isArray(obj.gradient) || obj.gradient.length < 2) {
3938
- throw new Error(
3969
+ throw new TypeError(
3939
3970
  `Theme "${obj.name}" must define a "gradient" array with at least 2 colors.`
3940
3971
  );
3941
3972
  }
3942
3973
  for (let i = 0; i < obj.gradient.length; i++) {
3943
3974
  const stop = obj.gradient[i];
3944
3975
  if (typeof stop !== "string" || !HEX_RE4.test(stop)) {
3945
- throw new Error(
3976
+ throw new TypeError(
3946
3977
  `Invalid hex in theme "${obj.name}" \u2192 gradient[${i}]: ${JSON.stringify(stop)}.`
3947
3978
  );
3948
3979
  }
@@ -4254,8 +4285,8 @@ var themes = {
4254
4285
  use(name) {
4255
4286
  const t = _globalRegistry.get(name);
4256
4287
  if (!t) {
4257
- throw new Error(
4258
- `Theme "${name}" not found. Available: ${[..._globalRegistry.keys()].join(", ")}`
4288
+ throw new RangeError(
4289
+ `Theme "${name}" not found. Available themes: ${[..._globalRegistry.keys()].join(", ")}`
4259
4290
  );
4260
4291
  }
4261
4292
  const old = _globalActive;