ansimax 1.0.0 → 1.1.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.
@@ -0,0 +1,183 @@
1
+ // ─────────────────────────────────────────────
2
+ // EXAMPLE 5 — Tree visualizations
3
+ //
4
+ // Demonstrates the new `trees` module with 4 real-world scenarios:
5
+ // 1. Filesystem tree with icons + per-node colors
6
+ // 2. Dependency tree with depth-based palette + collapsed nodes
7
+ // 3. JSON-as-tree with multiline values + max-depth truncation
8
+ // 4. Heavy-style decision tree with guide colors
9
+ //
10
+ // Run:
11
+ // npx ts-node examples/05-tree-visualizations.ts
12
+ // ─────────────────────────────────────────────
13
+
14
+ import {
15
+ trees, tree,
16
+ themes, color,
17
+ components,
18
+ } from '../dist/index.js';
19
+
20
+ const main = (): void => {
21
+ // ─────────────────────────────────────────────
22
+ // Scenario 1: Filesystem tree
23
+ // ─────────────────────────────────────────────
24
+ console.log();
25
+ console.log(components.section(themes.primary('Project structure'), { width: 60 }));
26
+ console.log();
27
+
28
+ const fs = tree({ label: 'my-app', icon: '📁', color: themes.primary });
29
+ const src = fs.add({ label: 'src', icon: '📁' });
30
+ src.addLeaf({ label: 'index.ts', icon: '📄', color: themes.accent })
31
+ .addLeaf({ label: 'app.ts', icon: '📄', color: themes.accent })
32
+ .addLeaf({ label: 'utils', icon: '📁' });
33
+ src.children![2]!.children = [
34
+ { label: 'helpers.ts', icon: '📄', color: themes.accent },
35
+ { label: 'constants.ts', icon: '📄', color: themes.accent },
36
+ ];
37
+
38
+ const tests = fs.add({ label: 'tests', icon: '📁' });
39
+ tests.addLeaf({ label: 'app.test.ts', icon: '🧪', color: themes.warning })
40
+ .addLeaf({ label: 'utils.test.ts', icon: '🧪', color: themes.warning });
41
+
42
+ fs.add({ label: 'package.json', icon: '📦', color: themes.muted });
43
+ fs.add({ label: 'README.md', icon: '📖', color: themes.muted });
44
+ fs.add({ label: 'tsconfig.json', icon: '⚙️', color: themes.muted });
45
+
46
+ console.log(fs.render({ guideColor: themes.muted }));
47
+
48
+ // ─────────────────────────────────────────────
49
+ // Scenario 2: Dependency tree with collapsed branches
50
+ // ─────────────────────────────────────────────
51
+ console.log();
52
+ console.log(components.section(themes.primary('Dependency tree (with collapse)'), { width: 60 }));
53
+ console.log();
54
+
55
+ const deps = tree('myapp@1.0.0');
56
+ const react = deps.add('react@18.2.0');
57
+ react.addLeaf('scheduler@0.23.0').addLeaf('loose-envify@1.4.0');
58
+
59
+ const express = deps.add('express@4.18.2');
60
+ // 14 transitive deps — collapse to show only the last 3
61
+ express.add({
62
+ label: 'transitive dependencies',
63
+ collapse: 11,
64
+ children: [
65
+ { label: 'accepts@1.3.8' },
66
+ { label: 'array-flatten@1.1.1' },
67
+ { label: 'body-parser@1.20.1' },
68
+ { label: 'content-disposition@0.5.4' },
69
+ { label: 'content-type@1.0.5' },
70
+ { label: 'cookie@0.5.0' },
71
+ { label: 'cookie-signature@1.0.6' },
72
+ { label: 'debug@2.6.9' },
73
+ { label: 'depd@2.0.0' },
74
+ { label: 'encodeurl@1.0.2' },
75
+ { label: 'escape-html@1.0.3' },
76
+ { label: 'etag@1.8.1' },
77
+ { label: 'finalhandler@1.2.0' },
78
+ { label: 'fresh@0.5.2' },
79
+ ],
80
+ });
81
+
82
+ deps.add('lodash@4.17.21');
83
+
84
+ console.log(deps.render({
85
+ palette: [
86
+ color.cyan, // depth 0 (root)
87
+ color.yellow, // depth 1 (direct deps)
88
+ color.gray, // depth 2+ (transitive)
89
+ ],
90
+ guideColor: themes.muted,
91
+ style: 'rounded',
92
+ }));
93
+
94
+ // ─────────────────────────────────────────────
95
+ // Scenario 3: JSON-as-tree with maxDepth
96
+ // ─────────────────────────────────────────────
97
+ console.log();
98
+ console.log(components.section(themes.primary('Config tree (maxDepth: 2)'), { width: 60 }));
99
+ console.log();
100
+
101
+ const cfg = tree({ label: 'tsconfig.json', icon: '⚙️' });
102
+ const compiler = cfg.add({ label: 'compilerOptions', icon: '🔧' });
103
+ compiler.addLeaf('target: "ES2022"')
104
+ .addLeaf('module: "ESNext"')
105
+ .addLeaf({ label: 'lib', children: [
106
+ { label: '"ES2022"' },
107
+ { label: '"DOM"' },
108
+ { label: '"DOM.Iterable"' },
109
+ ]})
110
+ .addLeaf({ label: 'paths', children: [
111
+ { label: '"@/*": ["./src/*"]' },
112
+ { label: '"@utils/*": ["./src/utils/*"]' },
113
+ ]});
114
+
115
+ cfg.add({ label: 'include', children: [
116
+ { label: '"src/**/*"' },
117
+ { label: '"tests/**/*"' },
118
+ ]});
119
+ cfg.add({ label: 'exclude', children: [
120
+ { label: '"node_modules"' },
121
+ { label: '"dist"' },
122
+ ]});
123
+
124
+ console.log(cfg.render({
125
+ maxDepth: 2,
126
+ guideColor: themes.muted,
127
+ }));
128
+
129
+ // ─────────────────────────────────────────────
130
+ // Scenario 4: Decision tree with heavy style + multiline
131
+ // ─────────────────────────────────────────────
132
+ console.log();
133
+ console.log(components.section(themes.primary('Decision flow (heavy + multiline)'), { width: 60 }));
134
+ console.log();
135
+
136
+ const decision = tree({
137
+ label: 'User clicks "Deploy"',
138
+ icon: '🚀',
139
+ color: themes.primary,
140
+ });
141
+
142
+ const valid = decision.add({
143
+ label: 'Tests pass?\n(must include integration suite)',
144
+ color: color.cyan,
145
+ });
146
+ valid.add({ label: 'Yes → Deploy to production', color: themes.accent, icon: '✓' });
147
+ valid.add({
148
+ label: 'No → Block deployment\nNotify team via Slack',
149
+ color: themes.error,
150
+ icon: '✗',
151
+ });
152
+
153
+ decision.add({
154
+ label: 'Build artifact > 100MB?',
155
+ color: color.cyan,
156
+ });
157
+
158
+ console.log(decision.render({
159
+ style: 'heavy',
160
+ guideColor: themes.warning,
161
+ }));
162
+
163
+ // ─────────────────────────────────────────────
164
+ // Bonus: walkTree to count nodes
165
+ // ─────────────────────────────────────────────
166
+ console.log();
167
+ let nodeCount = 0;
168
+ let leafCount = 0;
169
+ trees.walk(fs, (node) => {
170
+ nodeCount++;
171
+ if (!node.children || node.children.length === 0) leafCount++;
172
+ });
173
+ console.log(themes.muted(`📊 Project tree: ${nodeCount} nodes total, ${leafCount} leaves`));
174
+
175
+ // ─────────────────────────────────────────────
176
+ // Bonus: measureTree for layout
177
+ // ─────────────────────────────────────────────
178
+ const dims = trees.measure(fs, { guideColor: themes.muted });
179
+ console.log(themes.muted(`📐 Renders as ${dims.width}×${dims.height} characters`));
180
+ console.log();
181
+ };
182
+
183
+ main();
@@ -0,0 +1,466 @@
1
+ /**
2
+ * ansimax — comprehensive showcase
3
+ *
4
+ * A fictional CLI for deploying a microservices stack. Demonstrates EVERY
5
+ * module of ansimax in a cohesive narrative.
6
+ *
7
+ * Run: npx tsx examples/06-everything-together.ts
8
+ */
9
+
10
+ import {
11
+ // Config
12
+ configure, getConfig, onConfigKeyChange, withConfig,
13
+ // Themes
14
+ createTheme, themes,
15
+ // Color
16
+ color, gradient, registerPreset,
17
+ // ASCII
18
+ ascii,
19
+ // Animate
20
+ animate,
21
+ // Loader
22
+ loader,
23
+ // Frames
24
+ frames,
25
+ // Components
26
+ components,
27
+ // Images
28
+ images, createCanvas, gradientRect,
29
+ // Trees
30
+ tree, findInTree, countNodes, walkTree,
31
+ // Utils — ANSI
32
+ setTitle, link, bell, cursor, createOutputBuffer, write,
33
+ // Utils — helpers
34
+ padBoth, safeJson, once, escapeRegex, sleep,
35
+ } from '../dist/index.js';
36
+
37
+ // ────────────────────────────────────────────────────────────────
38
+ // 0. Bootstrap
39
+ // ────────────────────────────────────────────────────────────────
40
+
41
+ process.stdout.write(setTitle('ansimax demo — deploying stack'));
42
+
43
+ configure({
44
+ colorMode: 'auto',
45
+ animationSpeed: 'normal',
46
+ theme: 'dracula',
47
+ });
48
+
49
+ const offThemeListener = onConfigKeyChange('theme', (newTheme, oldTheme) => {
50
+ console.log(color.dim(` [config] theme: ${oldTheme} → ${newTheme}`));
51
+ });
52
+
53
+ // Custom gradient preset
54
+ registerPreset('sunsetSky', ['#ff6b6b', '#feca57', '#48dbfb']);
55
+
56
+ // Reusable stops for section headers
57
+ const SECTION_STOPS = ['#ff6b6b', '#feca57', '#48dbfb'];
58
+
59
+ // ────────────────────────────────────────────────────────────────
60
+ // 1. Banner
61
+ // ────────────────────────────────────────────────────────────────
62
+
63
+ const printBanner = (): void => {
64
+ console.clear();
65
+ const banner = ascii.banner('ansimax', {
66
+ font: 'big',
67
+ align: 'center',
68
+ colorFn: (t: string) => gradient(t, ['#bd93f9', '#ff79c6', '#50fa7b']),
69
+ });
70
+ console.log(banner);
71
+
72
+ const subtitle = color.dim('comprehensive showcase — every module in one demo');
73
+ console.log(padBoth(subtitle, 80));
74
+
75
+ // ascii.divider — DividerOptions has only char/width/label/style (no color).
76
+ // Apply color externally via color.hex().
77
+ const { width } = ascii.measure('ansimax', 'big');
78
+ console.log(color.hex('#6272a4')(
79
+ ascii.divider({ width: Math.max(width, 60), char: '═' }),
80
+ ));
81
+ console.log();
82
+ };
83
+
84
+ // ────────────────────────────────────────────────────────────────
85
+ // 2. Sections helper
86
+ // ────────────────────────────────────────────────────────────────
87
+
88
+ const section = (title: string): void => {
89
+ console.log();
90
+ console.log(gradient(`▸ ${title}`, SECTION_STOPS));
91
+ console.log(color.dim('─'.repeat(60)));
92
+ };
93
+
94
+ // ────────────────────────────────────────────────────────────────
95
+ // 3. Theme demo
96
+ // ────────────────────────────────────────────────────────────────
97
+
98
+ const themeDemo = (): void => {
99
+ section('1 · Themes (per-instance isolation)');
100
+
101
+ const tenantA = createTheme('dracula');
102
+ const tenantB = createTheme('nord');
103
+
104
+ tenantA.register('corporate', {
105
+ name: 'Corporate',
106
+ primary: '#0066cc',
107
+ secondary: '#ffcc00',
108
+ accent: '#00aa66',
109
+ success: '#00aa66',
110
+ warning: '#ff8800',
111
+ error: '#cc0033',
112
+ info: '#0099ff',
113
+ muted: '#999999',
114
+ bg: '#ffffff',
115
+ surface: '#f5f5f5',
116
+ text: '#222222',
117
+ gradient: ['#0066cc', '#00aa66'],
118
+ });
119
+
120
+ console.log(` ${tenantA.primary('tenantA')} themes: ${tenantA.list().length}`);
121
+ console.log(` ${tenantB.secondary('tenantB')} themes: ${tenantB.list().length} ${color.dim("(no 'corporate')")}`);
122
+
123
+ const switched = tenantA.tryUse('not-a-theme');
124
+ console.log(` tryUse('not-a-theme') → ${switched ? color.green('ok') : color.red('false (handled)')}`);
125
+
126
+ console.log(` ${tenantA.bgPrimary(' PRIMARY ')}${tenantA.bgAccent(' ACCENT ')}${tenantA.bgError(' ERROR ')}`);
127
+
128
+ console.log();
129
+ console.log(themes.preview());
130
+ };
131
+
132
+ // ────────────────────────────────────────────────────────────────
133
+ // 4. Color showcase
134
+ // ────────────────────────────────────────────────────────────────
135
+
136
+ const colorDemo = (): void => {
137
+ section('2 · Colors (gradients, presets, composition)');
138
+
139
+ console.log(
140
+ ' ' + color.red('red ') + color.green('green ') + color.blue('blue ') +
141
+ color.bold('bold ') + color.italic('italic ') + color.underline('underline'),
142
+ );
143
+
144
+ console.log(' ' + gradient('multi-stop gradient — fire to ocean', ['#ff6b6b', '#feca57', '#48dbfb']));
145
+
146
+ // Built-in rainbow preset
147
+ console.log(' ' + color.rainbow('built-in rainbow preset'));
148
+
149
+ // Custom registered preset — registerPreset adds a stop list, accessed via gradient()
150
+ console.log(' ' + gradient('custom registered preset (sunsetSky)', ['#ff6b6b', '#feca57', '#48dbfb']));
151
+
152
+ // Compose: bold + red + underline
153
+ const danger = (s: string): string => color.bold(color.red(color.underline(s)));
154
+ console.log(' ' + danger('composed: bold + red + underline'));
155
+
156
+ // ColorFn signature is (text: string) — coerce non-strings ourselves
157
+ console.log(' ' + color.cyan(String(42)) + ' ' + color.magenta(String(true)));
158
+ };
159
+
160
+ // ────────────────────────────────────────────────────────────────
161
+ // 5. Animations
162
+ // ────────────────────────────────────────────────────────────────
163
+
164
+ const animateDemo = async (): Promise<void> => {
165
+ section('3 · Animations (typewriter, parallel, delay)');
166
+
167
+ // TypewriterOptions: { speed, newline, colorFn, signal, reducedMotion, hooks }
168
+ await animate.typewriter(' Welcome to the deployment wizard...', { speed: 18 });
169
+
170
+ console.log(' ' + color.dim('Running 3 parallel checks (timeout=2s)...'));
171
+ await animate.parallel([
172
+ async () => { await sleep(400); console.log(' ' + color.green(' ✓') + ' network'); },
173
+ async () => { await sleep(600); console.log(' ' + color.green(' ✓') + ' DNS'); },
174
+ async () => { await sleep(800); console.log(' ' + color.green(' ✓') + ' TLS'); },
175
+ ], { timeout: 2000 });
176
+
177
+ await animate.delay(200);
178
+ };
179
+
180
+ // ────────────────────────────────────────────────────────────────
181
+ // 6. Loaders — real Task shape: { text, fn, subtasks? }
182
+ // ────────────────────────────────────────────────────────────────
183
+
184
+ const loaderDemo = async (): Promise<void> => {
185
+ section('4 · Loaders (spin, progress, hierarchical tasks)');
186
+
187
+ const stop = loader.spin('Connecting to registry', { color: '#bd93f9' });
188
+ await sleep(700);
189
+ stop('Registry connected', true);
190
+
191
+ console.log();
192
+ await loader.progressAnimate(20, 'Downloading manifests', {
193
+ color: '#50fa7b', delay: 25,
194
+ });
195
+
196
+ console.log();
197
+ await loader.tasks([
198
+ {
199
+ text: 'Backend',
200
+ fn: async () => undefined,
201
+ subtasks: [
202
+ { text: 'API gateway', fn: async () => sleep(300) },
203
+ { text: 'Auth service', fn: async () => sleep(400) },
204
+ { text: 'User service', fn: async () => sleep(350) },
205
+ ],
206
+ },
207
+ {
208
+ text: 'Frontend',
209
+ fn: async () => undefined,
210
+ subtasks: [
211
+ { text: 'Build bundle', fn: async () => sleep(500) },
212
+ { text: 'Optimize SVGs', fn: async () => sleep(200) },
213
+ ],
214
+ },
215
+ ], { parallel: true });
216
+ };
217
+
218
+ // ────────────────────────────────────────────────────────────────
219
+ // 7. Components
220
+ // ────────────────────────────────────────────────────────────────
221
+
222
+ const componentsDemo = (): void => {
223
+ section('5 · Components (table, badge, status, columns, timeline)');
224
+
225
+ console.log();
226
+ console.log(components.table([
227
+ ['Service', 'Status', 'Replicas', 'Region'],
228
+ ['api-gateway', color.green('● running'), '3', 'us-east-1'],
229
+ ['auth-service', color.green('● running'), '2', 'us-east-1'],
230
+ ['user-service', color.yellow('● starting'),'1', 'us-west-2'],
231
+ ['payments', color.red('● failed'), '0', 'eu-west-1'],
232
+ ], { borderStyle: 'rounded' }));
233
+
234
+ console.log();
235
+ console.log(' ' + components.badge('env', 'production') +
236
+ ' ' + components.badge('version', 'v1.2.3') +
237
+ ' ' + components.badge('build', '#1234'));
238
+
239
+ console.log();
240
+ console.log(' ' + components.status('success', 'Database migrations complete'));
241
+ console.log(' ' + components.status('warn', 'Cache hit rate below threshold'));
242
+ console.log(' ' + components.status('error', 'Connection pool exhausted'));
243
+ console.log(' ' + components.status('info', 'Deployment scheduled for 14:30'));
244
+
245
+ console.log();
246
+ console.log(components.columns([
247
+ 'api-gateway', 'auth-service', 'user-service',
248
+ 'payments', 'notifications', 'analytics',
249
+ ], { cols: 3, gap: 4 }));
250
+
251
+ console.log();
252
+ console.log(components.timeline([
253
+ { label: 'Pull image', done: true, time: '14:00' },
254
+ { label: 'Stop old version', done: true, time: '14:02' },
255
+ { label: 'Health check', done: false, time: '14:03' },
256
+ { label: 'Switch traffic', done: false, time: '14:05' },
257
+ ]));
258
+ };
259
+
260
+ // ────────────────────────────────────────────────────────────────
261
+ // 8. Images
262
+ // ────────────────────────────────────────────────────────────────
263
+
264
+ const imagesDemo = (): void => {
265
+ section('6 · Images (gradient rect + canvas + sprite)');
266
+
267
+ console.log();
268
+ console.log(gradientRect({
269
+ width: 50, height: 5,
270
+ colors: ['#ff79c6', '#bd93f9', '#8be9fd'],
271
+ style: 'horizontal',
272
+ dither: 'bayer',
273
+ }));
274
+
275
+ console.log();
276
+ const canvas = createCanvas(30, 8, { r: 18, g: 18, b: 38 });
277
+
278
+ for (let y = 0; y < 8; y++) {
279
+ const t = y / 7;
280
+ const r = Math.round(40 + 100 * t);
281
+ const g = Math.round(20 + 60 * t);
282
+ const b = Math.round(80 + 80 * (1 - t));
283
+ canvas.drawRect(0, y, 30, 1, { r, g, b }, true);
284
+ }
285
+
286
+ if (images.sprites.heart) canvas.drawSprite(2, 2, images.sprites.heart.pixels);
287
+ if (images.sprites.star) canvas.drawSprite(12, 2, images.sprites.star.pixels);
288
+ if (images.sprites.smiley) canvas.drawSprite(22, 2, images.sprites.smiley.pixels);
289
+ canvas.print();
290
+ };
291
+
292
+ // ────────────────────────────────────────────────────────────────
293
+ // 9. Frames
294
+ // ────────────────────────────────────────────────────────────────
295
+
296
+ const framesDemo = async (): Promise<void> => {
297
+ section('7 · Frames (live render, morph, presets)');
298
+
299
+ console.log();
300
+ console.log(color.dim(' Loading bar preset:'));
301
+ const ctrl = frames.play(frames.presets.loadingBar({ width: 20, label: ' Progress' }), {
302
+ fps: 30, repeat: 1,
303
+ });
304
+ await ctrl.done;
305
+
306
+ console.log(color.dim(' Morph (text decryption):'));
307
+ const morphFrames = frames.morph('SECRET CODE', 'DEPLOY OK ', 12);
308
+ for (const f of morphFrames) {
309
+ process.stdout.write('\r ' + color.cyan(f));
310
+ await sleep(60);
311
+ }
312
+ console.log();
313
+ };
314
+
315
+ // ────────────────────────────────────────────────────────────────
316
+ // 10. Trees
317
+ // ────────────────────────────────────────────────────────────────
318
+
319
+ const treesDemo = (): void => {
320
+ section('8 · Trees (builder + algorithms)');
321
+
322
+ const project = tree({ label: 'my-app', icon: '📦', color: color.bold });
323
+
324
+ const src = project.add({ label: 'src', icon: '📁' });
325
+ src.addLeaf({ label: 'index.ts', icon: '📄' });
326
+ src.addLeaf({ label: 'app.ts', icon: '📄' });
327
+ const utils = src.add({ label: 'utils', icon: '📁' });
328
+ utils.addLeaf({ label: 'helpers.ts', icon: '📄' });
329
+ utils.addLeaf({ label: 'ansi.ts', icon: '📄' });
330
+
331
+ const tests = project.add({ label: 'tests', icon: '🧪' });
332
+ tests.addLeaf({ label: 'app.test.ts', icon: '📄' });
333
+
334
+ project.addLeaf({ label: 'package.json', icon: '📋', color: color.yellow });
335
+ project.addLeaf({ label: 'README.md', icon: '📖' });
336
+
337
+ console.log();
338
+ console.log(project.render({
339
+ style: 'rounded',
340
+ palette: [color.cyan, color.green, color.magenta],
341
+ guideColor: color.dim,
342
+ }));
343
+
344
+ const found = findInTree(project, (n) => n.label === 'helpers.ts');
345
+ const total = countNodes(project);
346
+
347
+ let leafCount = 0;
348
+ walkTree(project, (n) => {
349
+ if (!n.children || n.children.length === 0) leafCount++;
350
+ });
351
+
352
+ console.log();
353
+ // ColorFn expects string — coerce numbers via String()
354
+ console.log(` ${color.dim('•')} total nodes: ${color.cyan(String(total))}`);
355
+ console.log(` ${color.dim('•')} leaf files: ${color.cyan(String(leafCount))}`);
356
+ console.log(` ${color.dim('•')} found: ${found ? color.green(found.label) : color.red('null')}`);
357
+ };
358
+
359
+ // ────────────────────────────────────────────────────────────────
360
+ // 11. Utils
361
+ // ────────────────────────────────────────────────────────────────
362
+
363
+ const utilsDemo = (): void => {
364
+ section('9 · Utils (OutputBuffer, helpers, hyperlinks)');
365
+
366
+ const buf = createOutputBuffer();
367
+ const verbose = process.env.VERBOSE !== undefined;
368
+ buf
369
+ .push(' ')
370
+ .push(color.bold('Built with: '))
371
+ .push(link('ansimax', 'https://github.com/Brashkie/ansimax'))
372
+ .push(' · ')
373
+ .pushIf(verbose, color.dim('(verbose mode) '))
374
+ .push(color.dim('TypeScript + Node.js'))
375
+ .pushln();
376
+ buf.flush();
377
+
378
+ const obj: { name: string; count: bigint; self?: unknown } = {
379
+ name: 'demo', count: 9876543210123456789n,
380
+ };
381
+ obj.self = obj;
382
+ console.log(' ' + color.dim('safeJson:'));
383
+ console.log(' ' + safeJson(obj, 2).split('\n').join('\n '));
384
+
385
+ const userInput = 'a.b+c?(x)';
386
+ const re = new RegExp(escapeRegex(userInput));
387
+ console.log(' ' + color.dim('escapeRegex:'));
388
+ console.log(` /${color.cyan(escapeRegex(userInput))}/ matches "${userInput}": ${color.green(String(re.test(userInput)))}`);
389
+
390
+ let count = 0;
391
+ const init = once(() => { count++; return 'initialized'; });
392
+ init(); init(); init();
393
+ // String() to satisfy ColorFn signature
394
+ console.log(` ${color.dim('once:')} called 3x, fn ran ${color.cyan(String(count))} time(s)`);
395
+
396
+ console.log(' ' + color.dim('padBoth:'));
397
+ console.log(' ' + color.bgBlue(padBoth(' centered ', 30)));
398
+ };
399
+
400
+ // ────────────────────────────────────────────────────────────────
401
+ // 12. Finale
402
+ // ────────────────────────────────────────────────────────────────
403
+
404
+ const finale = async (): Promise<void> => {
405
+ section('10 · Finale (withConfig, bell)');
406
+
407
+ await withConfig({ animationSpeed: 'fast' }, async () => {
408
+ console.log();
409
+ console.log(' Speed temporarily: ' + color.green(getConfig().animationSpeed));
410
+ // TypewriterOptions uses `speed`, not `delay`
411
+ await animate.typewriter(' Fast typewriter inside withConfig block.', { speed: 8 });
412
+ });
413
+ console.log(' Speed restored to: ' + color.green(getConfig().animationSpeed));
414
+
415
+ // BoxOptions: padding/borderStyle/width only — no borderColor.
416
+ // Apply color externally with color.hex() wrapping.
417
+ const summary = color.hex('#50fa7b')(ascii.box(
418
+ color.bold(color.green('✓ Demo complete')) + '\n' +
419
+ color.dim('All modules exercised:') + '\n' +
420
+ ' configure · themes · colors · ascii · animate\n' +
421
+ ' loader · frames · components · images · trees · utils',
422
+ { padding: 1 },
423
+ ));
424
+ console.log();
425
+ console.log(summary);
426
+
427
+ process.stdout.write(bell());
428
+
429
+ offThemeListener();
430
+ };
431
+
432
+ // ────────────────────────────────────────────────────────────────
433
+ // Main
434
+ // ────────────────────────────────────────────────────────────────
435
+
436
+ const main = async (): Promise<void> => {
437
+ const handleSigint = once((): void => {
438
+ write(cursor.show());
439
+ console.log('\n' + color.dim(' Interrupted. Cleaning up...'));
440
+ process.exit(130);
441
+ });
442
+ process.on('SIGINT', handleSigint);
443
+
444
+ try {
445
+ printBanner();
446
+ themeDemo();
447
+ colorDemo();
448
+ await animateDemo();
449
+ await loaderDemo();
450
+ componentsDemo();
451
+ imagesDemo();
452
+ await framesDemo();
453
+ treesDemo();
454
+ utilsDemo();
455
+ await finale();
456
+ } catch (err) {
457
+ write(cursor.show());
458
+ console.error('\n' + color.red('Demo error:'), err);
459
+ process.exit(1);
460
+ }
461
+ };
462
+
463
+ main().catch((err) => {
464
+ console.error(color.red('Fatal:'), err);
465
+ process.exit(1);
466
+ });