rad-coder 1.0.2 → 1.0.4

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
@@ -2,6 +2,12 @@
2
2
 
3
3
  A development environment for testing ResponsiveAds creative custom JavaScript with hot-reload.
4
4
 
5
+
6
+
7
+ https://github.com/user-attachments/assets/ce7515c5-0920-4f02-b430-6af69fc2d44d
8
+
9
+
10
+
5
11
  ## Quick Start
6
12
 
7
13
  ```bash
package/bin/cli.js CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  const path = require('path');
4
4
  const fs = require('fs');
5
- const readline = require('readline');
6
5
 
7
6
  // Get the package root directory
8
7
  const packageRoot = path.join(__dirname, '..');
@@ -15,50 +14,142 @@ function extractCreativeId(input) {
15
14
  }
16
15
 
17
16
  /**
18
- * Prompt user with a question and choices
17
+ * Prompt user with a question and arrow-key selectable choices
19
18
  * @param {string} question - The question to ask
20
19
  * @param {string[]} choices - Array of choices
21
20
  * @returns {Promise<number>} - The index of the selected choice (0-based)
22
21
  */
23
22
  function promptUser(question, choices) {
24
23
  return new Promise((resolve) => {
25
- const rl = readline.createInterface({
26
- input: process.stdin,
27
- output: process.stdout
28
- });
29
-
30
- console.log(`\n${question}`);
31
- choices.forEach((choice, index) => {
32
- console.log(` [${index + 1}] ${choice}`);
33
- });
34
-
35
- const ask = () => {
36
- rl.question('\nChoice (enter number): ', (answer) => {
37
- const num = parseInt(answer, 10);
38
- if (num >= 1 && num <= choices.length) {
39
- rl.close();
40
- resolve(num - 1);
24
+ let selected = 0;
25
+
26
+ // Print question
27
+ console.log(`\n${question}\n`);
28
+
29
+ function draw() {
30
+ // Move cursor up to overwrite previous menu lines
31
+ if (selected !== -1) {
32
+ process.stdout.write(`\x1B[${choices.length}A`);
33
+ }
34
+ for (let i = 0; i < choices.length; i++) {
35
+ process.stdout.write('\x1B[2K'); // clear line
36
+ if (i === selected) {
37
+ process.stdout.write(` \x1B[36m\x1B[1m❯ ${choices[i]}\x1B[0m\n`);
41
38
  } else {
42
- console.log(`Please enter a number between 1 and ${choices.length}`);
43
- ask();
39
+ process.stdout.write(` ${choices[i]}\n`);
44
40
  }
45
- });
46
- };
41
+ }
42
+ }
43
+
44
+ // Initial draw (set selected to -1 so it doesn't move cursor up first time)
45
+ const initial = selected;
46
+ selected = -1;
47
+ // Print placeholder lines first
48
+ for (let i = 0; i < choices.length; i++) {
49
+ process.stdout.write('\n');
50
+ }
51
+ // Move back up and draw properly
52
+ process.stdout.write(`\x1B[${choices.length}A`);
53
+ selected = initial;
54
+ draw();
55
+
56
+ // Enable raw mode for keypress detection
57
+ if (process.stdin.isTTY) {
58
+ process.stdin.setRawMode(true);
59
+ }
60
+ process.stdin.resume();
61
+
62
+ function onData(data) {
63
+ const key = data.toString();
64
+
65
+ // Ctrl+C
66
+ if (key === '\x03') {
67
+ cleanup();
68
+ process.exit(0);
69
+ return;
70
+ }
71
+
72
+ // Up arrow
73
+ if (key === '\x1B[A') {
74
+ selected = Math.max(0, selected - 1);
75
+ draw();
76
+ return;
77
+ }
78
+
79
+ // Down arrow
80
+ if (key === '\x1B[B') {
81
+ selected = Math.min(choices.length - 1, selected + 1);
82
+ draw();
83
+ return;
84
+ }
47
85
 
48
- ask();
86
+ // Enter
87
+ if (key === '\r' || key === '\n') {
88
+ cleanup();
89
+ resolve(selected);
90
+ return;
91
+ }
92
+
93
+ // Number keys for quick select
94
+ const num = parseInt(key, 10);
95
+ if (num >= 1 && num <= choices.length) {
96
+ selected = num - 1;
97
+ draw();
98
+ cleanup();
99
+ resolve(selected);
100
+ return;
101
+ }
102
+ }
103
+
104
+ function cleanup() {
105
+ process.stdin.removeListener('data', onData);
106
+ if (process.stdin.isTTY) {
107
+ process.stdin.setRawMode(false);
108
+ }
109
+ process.stdin.pause();
110
+ }
111
+
112
+ process.stdin.on('data', onData);
49
113
  });
50
114
  }
51
115
 
52
- // Get creative ID from command line argument
53
- const input = process.argv[2];
116
+ // Get creative ID from command line argument (skip flags)
117
+ const args = process.argv.slice(2);
118
+ let input = null;
119
+ let editorFlag = null;
120
+ let noEditor = false;
121
+
122
+ for (const arg of args) {
123
+ if (arg.startsWith('--editor=')) {
124
+ editorFlag = arg.split('=')[1];
125
+ } else if (arg === '--no-editor') {
126
+ noEditor = true;
127
+ } else if (!arg.startsWith('--')) {
128
+ input = arg;
129
+ }
130
+ }
131
+
132
+ // Pass editor preferences via environment variables
133
+ if (editorFlag) {
134
+ process.env.RAD_CODER_EDITOR = editorFlag;
135
+ }
136
+ if (noEditor) {
137
+ process.env.RAD_CODER_NO_EDITOR = '1';
138
+ }
139
+
54
140
  const creativeId = extractCreativeId(input);
55
141
 
56
142
  if (!creativeId) {
57
- console.log('Usage: npx rad-coder <creativeId or previewUrl>');
143
+ console.log('Usage: npx rad-coder <creativeId or previewUrl> [options]');
144
+ console.log('');
145
+ console.log('Options:');
146
+ console.log(' --editor=<cmd> Set code editor command (default: code)');
147
+ console.log(' --no-editor Don\'t auto-open code editor');
58
148
  console.log('');
59
149
  console.log('Examples:');
60
150
  console.log(' npx rad-coder 697b80fcc6e904025f5147a0');
61
151
  console.log(' npx rad-coder https://studio.responsiveads.com/creatives/697b80fcc6e904025f5147a0/preview');
152
+ console.log(' npx rad-coder 697b80fcc6e904025f5147a0 --editor=cursor');
62
153
  process.exit(1);
63
154
  }
64
155
 
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "rad-coder",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Development environment for testing ResponsiveAds creative custom JS with hot-reload",
5
5
  "bin": {
6
- "rad-coder": "./bin/cli.js"
6
+ "rad-coder": "bin/cli.js"
7
7
  },
8
8
  "scripts": {
9
9
  "dev": "node bin/cli.js",
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "repository": {
28
28
  "type": "git",
29
- "url": "https://github.com/ResponsiveAds/rad-coder"
29
+ "url": "git+https://github.com/ResponsiveAds/rad-coder.git"
30
30
  },
31
31
  "author": "ResponsiveAds",
32
32
  "license": "MIT",
package/public/test.html CHANGED
@@ -87,23 +87,8 @@
87
87
  }
88
88
 
89
89
  .ad-container {
90
- padding: 20px;
91
- display: flex;
92
- justify-content: center;
93
- align-items: flex-start;
94
- }
95
-
96
- .ad-wrapper {
97
- background: #fff;
98
- border-radius: 8px;
99
- overflow: hidden;
100
- box-shadow: 0 10px 40px rgba(0,0,0,0.3);
101
90
  }
102
91
 
103
- .inner-content {
104
- width: 100%;
105
- min-height: 300px;
106
- }
107
92
 
108
93
  /* Toast notification for reload */
109
94
  .toast {
@@ -180,6 +165,8 @@
180
165
  document.getElementById('creative-id').textContent = 'Loading...';
181
166
 
182
167
  const config = await fetchConfigWithRetry();
168
+ //not sure why we get URL that does not work https://edit.responsiveads.com/flowlines
169
+ config.flSource = 'https://s3-eu-west-1.amazonaws.com/publish.responsiveads.com/flowlines/';
183
170
 
184
171
  // Update UI
185
172
  document.getElementById('creative-id').textContent = `ID: ${config.creativeId}`;
@@ -231,7 +218,7 @@
231
218
  tracking: false,
232
219
  isFluid: config.isFluid,
233
220
  screenshot: false,
234
- crossOrigin: true,
221
+ crossOrigin: false,
235
222
  flSource: config.flSource,
236
223
  config: {
237
224
  _default: {
package/server/index.js CHANGED
@@ -5,6 +5,50 @@ const chokidar = require('chokidar');
5
5
  const path = require('path');
6
6
  const fs = require('fs');
7
7
  const http = require('http');
8
+ const { spawn } = require('child_process');
9
+ const TUI = require('./tui');
10
+
11
+ // ============================================================
12
+ // TUI Instance & Logging
13
+ // ============================================================
14
+
15
+ let tui = null;
16
+
17
+ /**
18
+ * Log a message — routes to TUI scroll area when active, otherwise console.log
19
+ */
20
+ function log(message) {
21
+ if (tui && !tui.destroyed) {
22
+ tui.log(message);
23
+ } else {
24
+ console.log(message);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Open the user's code editor
30
+ */
31
+ function openEditor() {
32
+ const editorCmd = process.env.RAD_CODER_EDITOR
33
+ || process.env.VISUAL
34
+ || process.env.EDITOR
35
+ || 'code';
36
+
37
+ try {
38
+ const child = spawn(editorCmd, [userDir], {
39
+ detached: true,
40
+ stdio: 'ignore',
41
+ shell: process.platform === 'win32'
42
+ });
43
+ child.unref();
44
+ child.on('error', (err) => {
45
+ log(` ✗ Could not open editor "${editorCmd}": ${err.message}`);
46
+ });
47
+ log(` Editor (${editorCmd}) opened`);
48
+ } catch (err) {
49
+ log(` ✗ Could not open editor "${editorCmd}": ${err.message}`);
50
+ }
51
+ }
8
52
 
9
53
  // ============================================================
10
54
  // Directory Configuration
@@ -113,8 +157,8 @@ function extractJsonObject(html, startMarker) {
113
157
  async function fetchCreativeConfig(creativeId) {
114
158
  const previewUrl = `https://studio.responsiveads.com/creatives/${creativeId}/preview`;
115
159
 
116
- console.log(` Fetching creative config from studio...`);
117
- console.log(` URL: ${previewUrl}\n`);
160
+ log(` Fetching creative config from studio...`);
161
+ log(` URL: ${previewUrl}\n`);
118
162
 
119
163
  try {
120
164
  const response = await fetch(previewUrl);
@@ -261,7 +305,7 @@ async function fetchCreativeConfig(creativeId) {
261
305
  };
262
306
 
263
307
  } catch (error) {
264
- console.error(`\n Failed to fetch creative config: ${error.message}\n`);
308
+ log(`\n Failed to fetch creative config: ${error.message}\n`);
265
309
  process.exit(1);
266
310
  }
267
311
  }
@@ -281,11 +325,11 @@ const clients = new Set();
281
325
 
282
326
  wss.on('connection', (ws) => {
283
327
  clients.add(ws);
284
- console.log('Browser connected for hot-reload');
328
+ log(' Browser connected for hot-reload');
285
329
 
286
330
  ws.on('close', () => {
287
331
  clients.delete(ws);
288
- console.log('Browser disconnected');
332
+ log(' Browser disconnected');
289
333
  });
290
334
  });
291
335
 
@@ -341,14 +385,14 @@ const watcher = chokidar.watch(customJsWatchPath, {
341
385
  ignoreInitial: true
342
386
  });
343
387
 
344
- watcher.on('change', (filePath) => {
345
- console.log(`\n File changed: ${path.basename(filePath)}`);
346
- console.log(' Reloading browsers...\n');
388
+ watcher.on('change', (changedPath) => {
389
+ log(` File changed: ${path.basename(changedPath)}`);
390
+ log(' Reloading browsers...');
347
391
  broadcastReload();
348
392
  });
349
393
 
350
394
  watcher.on('error', (error) => {
351
- console.error('Watcher error:', error);
395
+ log(` Watcher error: ${error.message}`);
352
396
  });
353
397
 
354
398
  // ============================================================
@@ -390,7 +434,6 @@ async function start(prefetchedConfig = null) {
390
434
  console.log(` Test page: http://${host}:${port}/test.html`);
391
435
  console.log(`\n Working directory: ${userDir}`);
392
436
  console.log(' Edit custom.js and save to hot-reload\n');
393
- console.log(' Press Ctrl+C to stop\n');
394
437
 
395
438
  // Small delay to ensure server is fully ready before opening browser
396
439
  await new Promise(resolve => setTimeout(resolve, 500));
@@ -399,11 +442,152 @@ async function start(prefetchedConfig = null) {
399
442
  try {
400
443
  const open = (await import('open')).default;
401
444
  await open(`http://${host}:${port}/test.html`);
402
- console.log(' Browser opened automatically\n');
445
+ console.log(' Browser opened automatically');
403
446
  } catch (err) {
404
- console.log(' Could not auto-open browser:', err.message);
405
- console.log(` Please open http://${host}:${port}/test.html manually\n`);
447
+ console.log(` Could not auto-open browser: ${err.message}`);
448
+ console.log(` Please open http://${host}:${port}/test.html manually`);
449
+ }
450
+
451
+ // Auto-open editor (unless --no-editor)
452
+ if (!process.env.RAD_CODER_NO_EDITOR) {
453
+ openEditor();
454
+ }
455
+
456
+ // Start interactive TUI (only if stdin is a TTY)
457
+ if (process.stdin.isTTY) {
458
+ await new Promise(resolve => setTimeout(resolve, 300));
459
+ startInteractiveMenu();
460
+ } else {
461
+ console.log(' Press Ctrl+C to stop\n');
462
+ }
463
+ });
464
+ }
465
+
466
+ // ============================================================
467
+ // Interactive Menu
468
+ // ============================================================
469
+
470
+ function getMainMenuItems() {
471
+ const items = [
472
+ { label: 'Open Browser', id: 'open-browser' },
473
+ { label: 'Open Editor', id: 'open-editor' },
474
+ ];
475
+
476
+ if (creativeConfig && creativeConfig.allFlowlines.length > 1) {
477
+ items.push({ label: 'Switch Flowline', id: 'switch-flowline', description: `(${creativeConfig.flowlineName})` });
478
+ }
479
+
480
+ items.push(
481
+ { label: 'Server Status', id: 'status' },
482
+ { label: 'Clear Logs', id: 'clear' },
483
+ { label: 'Restart Server', id: 'restart' },
484
+ { label: 'Stop Server', id: 'stop' },
485
+ );
486
+
487
+ return items;
488
+ }
489
+
490
+ function getFlowlineMenuItems() {
491
+ const items = [{ label: '← Back', id: 'back' }];
492
+ creativeConfig.allFlowlines.forEach((fl, i) => {
493
+ const marker = fl.id === creativeConfig.flowlineId ? ' ✓' : '';
494
+ items.push({ label: `${fl.name}${marker}`, id: `flowline-${i}`, flowlineIndex: i });
495
+ });
496
+ return items;
497
+ }
498
+
499
+ function startInteractiveMenu() {
500
+ tui = new TUI();
501
+
502
+ let inSubMenu = false;
503
+
504
+ function handleSelect(item) {
505
+ // Sub-menu: flowline selection
506
+ if (inSubMenu) {
507
+ if (item.id === 'back') {
508
+ inSubMenu = false;
509
+ tui.updateMenu(getMainMenuItems());
510
+ return;
511
+ }
512
+ // Switch flowline
513
+ const fl = creativeConfig.allFlowlines[item.flowlineIndex];
514
+ if (fl) {
515
+ creativeConfig.flowlineId = fl.id;
516
+ creativeConfig.flowlineName = fl.name;
517
+ creativeConfig.sizes = fl.sizes || [];
518
+ creativeConfig.isFluid = fl.isFluid || false;
519
+ log(` Switched to flowline: ${fl.name}`);
520
+ broadcastReload();
521
+ }
522
+ inSubMenu = false;
523
+ tui.updateMenu(getMainMenuItems());
524
+ return;
525
+ }
526
+
527
+ // Main menu actions
528
+ switch (item.id) {
529
+ case 'open-browser': {
530
+ const { port, host } = creativeConfig.server;
531
+ import('open').then(mod => {
532
+ mod.default(`http://${host}:${port}/test.html`);
533
+ log(' Browser opened');
534
+ }).catch(err => {
535
+ log(` Could not open browser: ${err.message}`);
536
+ });
537
+ break;
538
+ }
539
+
540
+ case 'open-editor':
541
+ openEditor();
542
+ break;
543
+
544
+ case 'switch-flowline':
545
+ inSubMenu = true;
546
+ tui.updateMenu(getFlowlineMenuItems());
547
+ break;
548
+
549
+ case 'status': {
550
+ const { port, host } = creativeConfig.server;
551
+ log('');
552
+ log(' ── Server Status ──────────────────');
553
+ log(` Creative ID : ${creativeConfig.creativeId}`);
554
+ log(` Flowline : ${creativeConfig.flowlineName}`);
555
+ log(` Flowline ID : ${creativeConfig.flowlineId}`);
556
+ log(` Sizes : ${creativeConfig.sizes.join(', ') || 'N/A'}`);
557
+ log(` Is Fluid : ${creativeConfig.isFluid}`);
558
+ log(` Server : http://${host}:${port}`);
559
+ log(` Directory : ${userDir}`);
560
+ log(` Browsers : ${clients.size} connected`);
561
+ log(' ───────────────────────────────────');
562
+ log('');
563
+ break;
564
+ }
565
+
566
+ case 'clear':
567
+ tui.clearLogs();
568
+ break;
569
+
570
+ case 'restart': {
571
+ log(' Restarting server...');
572
+ const { port, host } = creativeConfig.server;
573
+ server.close(() => {
574
+ server.listen(port, host, () => {
575
+ log(` Server restarted on http://${host}:${port}`);
576
+ broadcastReload();
577
+ });
578
+ });
579
+ break;
580
+ }
581
+
582
+ case 'stop':
583
+ gracefulShutdown();
584
+ break;
406
585
  }
586
+ }
587
+
588
+ tui.init({
589
+ menuItems: getMainMenuItems(),
590
+ onSelect: handleSelect,
407
591
  });
408
592
  }
409
593
 
@@ -423,7 +607,10 @@ if (!isModule) {
423
607
  }
424
608
 
425
609
  // Graceful shutdown
426
- process.on('SIGINT', () => {
610
+ function gracefulShutdown() {
611
+ if (tui) {
612
+ tui.destroy();
613
+ }
427
614
  console.log('\n Shutting down...');
428
615
  watcher.close();
429
616
  wss.close();
@@ -431,4 +618,6 @@ process.on('SIGINT', () => {
431
618
  console.log(' Server stopped\n');
432
619
  process.exit(0);
433
620
  });
434
- });
621
+ }
622
+
623
+ process.on('SIGINT', gracefulShutdown);
package/server/tui.js ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Terminal UI Module
3
+ *
4
+ * Provides an interactive arrow-key menu pinned at the bottom of the terminal
5
+ * with a scrolling log area above it. Uses raw ANSI escape codes — zero dependencies.
6
+ */
7
+
8
+ const readline = require('readline');
9
+
10
+ // ANSI escape helpers
11
+ const ESC = '\x1B';
12
+ const CSI = `${ESC}[`;
13
+
14
+ const ansi = {
15
+ clearScreen: `${CSI}2J`,
16
+ clearLine: `${CSI}2K`,
17
+ cursorTo: (row, col) => `${CSI}${row};${col}H`,
18
+ cursorSave: `${ESC}7`,
19
+ cursorRestore: `${ESC}8`,
20
+ scrollRegion: (top, bottom) => `${CSI}${top};${bottom}r`,
21
+ resetScrollRegion: `${CSI}r`,
22
+ showCursor: `${CSI}?25h`,
23
+ hideCursor: `${CSI}?25l`,
24
+ bold: `${CSI}1m`,
25
+ dim: `${CSI}2m`,
26
+ cyan: `${CSI}36m`,
27
+ green: `${CSI}32m`,
28
+ yellow: `${CSI}33m`,
29
+ inverse: `${CSI}7m`,
30
+ reset: `${CSI}0m`,
31
+ };
32
+
33
+ class TUI {
34
+ constructor() {
35
+ this.menuItems = [];
36
+ this.selectedIndex = 0;
37
+ this.onSelect = null;
38
+ this.destroyed = false;
39
+ this.menuHeight = 0; // computed from items + header + border lines
40
+ this._boundKeyHandler = this._handleKeypress.bind(this);
41
+ this._boundResize = this._handleResize.bind(this);
42
+ }
43
+
44
+ /**
45
+ * Initialize the TUI
46
+ * @param {Object} options
47
+ * @param {Array<{label: string, description?: string}>} options.menuItems
48
+ * @param {Function} options.onSelect - callback(item, index)
49
+ */
50
+ init(options) {
51
+ this.menuItems = options.menuItems || [];
52
+ this.onSelect = options.onSelect || (() => {});
53
+ this.selectedIndex = 0;
54
+ this.menuHeight = this.menuItems.length + 3; // items + header + top/bottom borders
55
+
56
+ // Set raw mode for keypress detection
57
+ if (process.stdin.isTTY) {
58
+ process.stdin.setRawMode(true);
59
+ process.stdin.resume();
60
+ process.stdin.on('data', this._boundKeyHandler);
61
+ }
62
+
63
+ // Handle terminal resize
64
+ process.stdout.on('resize', this._boundResize);
65
+
66
+ // Initial draw
67
+ this._setupScreen();
68
+ this._drawMenu();
69
+ }
70
+
71
+ /**
72
+ * Set up ANSI scrolling region (log area = top, menu = bottom)
73
+ */
74
+ _setupScreen() {
75
+ const rows = process.stdout.rows || 24;
76
+ const logBottom = rows - this.menuHeight;
77
+
78
+ // Set scrolling region to the top portion only
79
+ process.stdout.write(ansi.scrollRegion(1, logBottom));
80
+
81
+ // Position cursor in the log area
82
+ process.stdout.write(ansi.cursorTo(logBottom, 1));
83
+ }
84
+
85
+ /**
86
+ * Handle terminal resize
87
+ */
88
+ _handleResize() {
89
+ if (this.destroyed) return;
90
+ this._setupScreen();
91
+ this._drawMenu();
92
+ }
93
+
94
+ /**
95
+ * Handle raw keypress data
96
+ */
97
+ _handleKeypress(data) {
98
+ if (this.destroyed) return;
99
+
100
+ const key = data.toString();
101
+
102
+ // Ctrl+C
103
+ if (key === '\x03') {
104
+ this.destroy();
105
+ process.emit('SIGINT');
106
+ return;
107
+ }
108
+
109
+ // Up arrow
110
+ if (key === `${CSI}A`) {
111
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
112
+ this._drawMenu();
113
+ return;
114
+ }
115
+
116
+ // Down arrow
117
+ if (key === `${CSI}B`) {
118
+ this.selectedIndex = Math.min(this.menuItems.length - 1, this.selectedIndex + 1);
119
+ this._drawMenu();
120
+ return;
121
+ }
122
+
123
+ // Enter
124
+ if (key === '\r' || key === '\n') {
125
+ const item = this.menuItems[this.selectedIndex];
126
+ if (item && this.onSelect) {
127
+ this.onSelect(item, this.selectedIndex);
128
+ }
129
+ return;
130
+ }
131
+
132
+ // Number keys 1-9 for quick select
133
+ const num = parseInt(key, 10);
134
+ if (num >= 1 && num <= this.menuItems.length) {
135
+ this.selectedIndex = num - 1;
136
+ this._drawMenu();
137
+ const item = this.menuItems[this.selectedIndex];
138
+ if (item && this.onSelect) {
139
+ this.onSelect(item, this.selectedIndex);
140
+ }
141
+ return;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Draw the menu at the bottom of the terminal
147
+ */
148
+ _drawMenu() {
149
+ if (this.destroyed) return;
150
+
151
+ const rows = process.stdout.rows || 24;
152
+ const cols = process.stdout.cols || 80;
153
+ const menuStartRow = rows - this.menuHeight + 1;
154
+
155
+ // Save cursor position (in log area)
156
+ let output = ansi.cursorSave;
157
+ output += ansi.hideCursor;
158
+
159
+ // Draw top border
160
+ output += ansi.cursorTo(menuStartRow, 1);
161
+ output += ansi.clearLine;
162
+ output += `${ansi.dim}${'─'.repeat(Math.min(cols, 60))}${ansi.reset}`;
163
+
164
+ // Draw header
165
+ output += ansi.cursorTo(menuStartRow + 1, 1);
166
+ output += ansi.clearLine;
167
+ output += `${ansi.bold} ↑↓ Navigate Enter Select 1-${this.menuItems.length} Quick Select${ansi.reset}`;
168
+
169
+ // Draw menu items
170
+ for (let i = 0; i < this.menuItems.length; i++) {
171
+ const row = menuStartRow + 2 + i;
172
+ const item = this.menuItems[i];
173
+ const isSelected = i === this.selectedIndex;
174
+
175
+ output += ansi.cursorTo(row, 1);
176
+ output += ansi.clearLine;
177
+
178
+ if (isSelected) {
179
+ output += `${ansi.cyan}${ansi.bold} ❯ ${item.label}${ansi.reset}`;
180
+ if (item.description) {
181
+ output += `${ansi.dim} ${item.description}${ansi.reset}`;
182
+ }
183
+ } else {
184
+ output += ` ${item.label}`;
185
+ if (item.description) {
186
+ output += `${ansi.dim} ${item.description}${ansi.reset}`;
187
+ }
188
+ }
189
+ }
190
+
191
+ // Restore cursor position (back to log area)
192
+ output += ansi.cursorRestore;
193
+ output += ansi.showCursor;
194
+
195
+ process.stdout.write(output);
196
+ }
197
+
198
+ /**
199
+ * Update menu items (e.g., for sub-menus)
200
+ */
201
+ updateMenu(items) {
202
+ const oldHeight = this.menuHeight;
203
+ this.menuItems = items;
204
+ this.selectedIndex = 0;
205
+ this.menuHeight = items.length + 3;
206
+
207
+ if (this.menuHeight !== oldHeight) {
208
+ // Clear old menu area
209
+ const rows = process.stdout.rows || 24;
210
+ const oldMenuStart = rows - oldHeight + 1;
211
+ let clear = '';
212
+ for (let i = oldMenuStart; i <= rows; i++) {
213
+ clear += ansi.cursorTo(i, 1) + ansi.clearLine;
214
+ }
215
+ process.stdout.write(clear);
216
+
217
+ // Reconfigure scroll region
218
+ this._setupScreen();
219
+ }
220
+
221
+ this._drawMenu();
222
+ }
223
+
224
+ /**
225
+ * Add a log message to the scrolling log area
226
+ */
227
+ log(message) {
228
+ if (this.destroyed) {
229
+ console.log(message);
230
+ return;
231
+ }
232
+
233
+ const rows = process.stdout.rows || 24;
234
+ const logBottom = rows - this.menuHeight;
235
+
236
+ // Save cursor, move to bottom of log area, print message (scrolls within region)
237
+ let output = ansi.cursorSave;
238
+ output += ansi.cursorTo(logBottom, 1);
239
+ output += '\n' + message;
240
+ output += ansi.cursorRestore;
241
+
242
+ process.stdout.write(output);
243
+ }
244
+
245
+ /**
246
+ * Clear the log area
247
+ */
248
+ clearLogs() {
249
+ const rows = process.stdout.rows || 24;
250
+ const logBottom = rows - this.menuHeight;
251
+
252
+ let output = '';
253
+ for (let i = 1; i <= logBottom; i++) {
254
+ output += ansi.cursorTo(i, 1) + ansi.clearLine;
255
+ }
256
+ output += ansi.cursorTo(1, 1);
257
+ process.stdout.write(output);
258
+ }
259
+
260
+ /**
261
+ * Destroy the TUI and restore terminal state
262
+ */
263
+ destroy() {
264
+ if (this.destroyed) return;
265
+ this.destroyed = true;
266
+
267
+ // Remove listeners
268
+ process.stdin.removeListener('data', this._boundKeyHandler);
269
+ process.stdout.removeListener('resize', this._boundResize);
270
+
271
+ // Restore terminal
272
+ process.stdout.write(ansi.resetScrollRegion);
273
+ process.stdout.write(ansi.showCursor);
274
+
275
+ if (process.stdin.isTTY) {
276
+ process.stdin.setRawMode(false);
277
+ }
278
+ process.stdin.pause();
279
+ }
280
+ }
281
+
282
+ module.exports = TUI;
@@ -4,6 +4,9 @@ You are an agent writing only JS for a responsive creative. The creative was bui
4
4
 
5
5
  To do this you can only edit the `custom.js` file in this directory. When you edit and save this file the creative will be automatically loaded on the test page: http://localhost:3000/test.html. The code you wrote in `custom.js` will be applied to the creative.
6
6
 
7
+ Use the http://localhost:3000/test.html URL to open creative in the browser. Inspect the HTML dom so that you can use IDs from elements inside the custom.js code. Alo use the browser to test the code and make sure there are no console.log errors.
8
+
9
+
7
10
  Use modern JS standards and code practices.
8
11
 
9
12
  We can use custom in situations when we want to add extra interactivity to our responsive creative. Use the Radical API to access elements added from the editor, update their behavior, and add custom functionalities to your ad.
@@ -16,6 +19,32 @@ You can use all available JavaScript functions to manipulate element position an
16
19
 
17
20
  This document outlines the specific implementation patterns and lifecycle hooks for the Radical API. Use this guide to programmatically control elements, manage dynamic data (DCO), and handle cross-window interactions in ResponsiveAds creatives.
18
21
 
22
+ Use this as a good starting point always call functions from `!rad.getMergedContent().inEditor` check to prevent messing up the editor code.
23
+
24
+ ```
25
+ var rad = Radical.getAdByWindow(window);
26
+ var container = rad.getContainer();
27
+
28
+ var inScreenshot = window.location.href.indexOf('preview?screenshot=1') > -1 ? true : false;
29
+
30
+
31
+ if (!rad.getMergedContent().inEditor) {
32
+ rad.onLoad(onAdLoaded);
33
+ rad.onBeforeRender(onBeforeRender);
34
+ rad.onRender(onAdRender);
35
+
36
+ }
37
+ function onAdLoaded() {}
38
+ function onBeforeRender(arg) {
39
+ console.log('onBeforeRender', arg);
40
+ }
41
+ function onAdRender() {
42
+ console.log('onAdRender');
43
+ const el = rad.getElementById('a2');
44
+ console.log(el);
45
+ }
46
+ ```
47
+
19
48
  ## 1. Initializing the Controller
20
49
 
21
50
  Every script must first reference the ad instance and its container.