fix(ascii-art): wire Box/Templates mode + correct standalone <link href>; v4.9.2

- src/ascii-generator.html line 7: ../fonts.css -> fonts.css (src/-relative;
  fixes silent stylesheet miss — same class as the v4.9.0->v4.9.1 script-tag fix)
- src/renderer/ascii-controller.js: wire all 3 mode tabs, 18 template
  buttons, and the 3 box form fields. setMode() toggles .active on tabs
  and matching .mode-section. Templates route through the orchestrator's
  template:<id> font namespace. Box mode renders text with single/double/
  rounded/bold/ascii borders via a pure renderBox() helper. All 11 T9
  behaviours preserved.
- tests/ascii-controller.test.js: 6 tests covering stylesheet path,
  pure box renderer, mode-tab switching, and template button wiring.
- package.json 4.9.1 -> 4.9.2; README + UPDATES updated.

Amit Haridas
This commit is contained in:
2026-09-14 22:37:02 +05:30
parent 8c708b7abb
commit ac31162d3b
6 changed files with 354 additions and 3 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ASCII Art Generator - MarkdownConverter</title>
<link rel="stylesheet" href="../fonts.css" />
<link rel="stylesheet" href="fonts.css" />
<style>
:root {
--ci-dark-gray: #464646;
+191
View File
@@ -6,6 +6,11 @@
* Insert / Copy / Save actions. Last-used font is restored from
* electron-store (via main) and saved on every change.
*
* Wires three mode tabs (Text Banner / Box-Frame / Templates) plus the
* 18 template buttons and the Box form fields (#box-text, #box-style,
* #box-padding). Templates route through `api.generate({ font: 'template:<id>' })`
* so the orchestrator owns template content.
*
* No bundler, no framework — pure DOM + window.electronAPI bridge.
*/
(function () {
@@ -23,12 +28,26 @@
btnSave: document.getElementById('btn-save'),
btnGenerate: document.getElementById('btn-generate'),
warning: document.getElementById('ascii-warning'),
modeTabs: document.querySelectorAll('.mode-tab[data-mode]'),
modeSections: document.querySelectorAll('.mode-section'),
templateButtons: document.querySelectorAll('.template-btn[data-template]'),
boxText: document.getElementById('box-text'),
boxStyle: document.getElementById('box-style'),
boxPadding: document.getElementById('box-padding'),
};
// Section ID for each mode. Keep in sync with ascii-generator.html IDs.
const MODE_SECTION_ID = {
text: 'text-mode',
box: 'box-mode',
templates: 'templates-mode',
};
let _fonts = [];
let _currentFont = 'standard';
let _previewText = '';
let _searchDebounce = null;
let _currentMode = 'text';
function showWarning(msg) {
if (!els.warning) return;
@@ -45,8 +64,124 @@
};
}
/**
* Switch the active mode tab and the visible mode section.
* Toggles the `.active` class on tabs and on the matching `.mode-section`.
* @param {'text'|'box'|'templates'} mode
*/
function setMode(mode) {
if (!MODE_SECTION_ID[mode]) return;
_currentMode = mode;
// Toggle tab buttons.
els.modeTabs.forEach((tab) => {
if (tab.dataset.mode === mode) tab.classList.add('active');
else tab.classList.remove('active');
});
// Toggle mode sections.
els.modeSections.forEach((sec) => {
if (sec.id === MODE_SECTION_ID[mode]) sec.classList.add('active');
else sec.classList.remove('active');
});
// Each mode has its own preview pipeline; trigger a refresh.
refreshPreview();
}
/**
* Build a simple box border around the user's text using the chosen style.
* Pure function — exported on window for tests via ASCIIBoxRenderer.
* @param {string} text
* @param {'single'|'double'|'rounded'|'bold'|'ascii'} style
* @param {number} padding 0..10 spaces on each side
* @returns {string}
*/
function renderBox(text, style, padding) {
const lines = String(text ?? '').split('\n');
const pad = Math.max(0, Math.min(10, Number(padding) || 0));
const padded = lines.map((l) => ' '.repeat(pad) + l + ' '.repeat(pad));
const width = padded.reduce((w, l) => Math.max(w, visibleLength(l)), 0);
let tl, tr, bl, br, h, v;
switch (style) {
case 'double':
tl = '╔';
tr = '╗';
bl = '╚';
br = '╝';
h = '═';
v = '║';
break;
case 'rounded':
tl = '╭';
tr = '╮';
bl = '╰';
br = '╯';
h = '─';
v = '│';
break;
case 'bold':
tl = '┏';
tr = '┓';
bl = '┗';
br = '┛';
h = '━';
v = '┃';
break;
case 'ascii':
tl = '+';
tr = '+';
bl = '+';
br = '+';
h = '-';
v = '|';
break;
case 'single':
default:
tl = '┌';
tr = '┐';
bl = '└';
br = '┘';
h = '─';
v = '│';
break;
}
const top = tl + h.repeat(width + 2) + tr;
const bot = bl + h.repeat(width + 2) + br;
const body = padded.map((l) => v + ' ' + l.padEnd(width, ' ') + ' ' + v).join('\n');
return [top, body, bot].join('\n');
}
/**
* Count printable character width, ignoring box-drawing characters and
* spaces (which all occupy a single monospace column).
*/
function visibleLength(s) {
// Box-drawing chars and most printable ASCII occupy one column in our
// monospace font. This is a deliberate simplification — emoji or wide
// CJK would need a more sophisticated width table.
return Array.from(s).length;
}
async function refreshPreview() {
if (!api) return;
if (_currentMode === 'templates') {
// Template mode is button-driven; no live preview here.
return;
}
if (_currentMode === 'box') {
const text = els.boxText ? els.boxText.value : '';
const style = els.boxStyle ? els.boxStyle.value : 'single';
const padding = els.boxPadding ? els.boxPadding.value : 2;
_previewText = text;
const out = renderBox(text, style, padding);
els.preview.textContent = out;
showWarning('');
return;
}
// text mode (default)
const text = els.textInput ? els.textInput.value : 'HELLO';
_previewText = text;
try {
@@ -93,6 +228,26 @@
renderFontList('');
}
/**
* Render a template by id: fetch its content via the orchestrator's
* template:<id> font namespace and update the preview.
*/
async function renderTemplate(templateId) {
if (!api) return;
try {
const out = await api.generate({ text: '', font: `template:${templateId}` });
els.preview.textContent = out;
showWarning('');
// Mark the clicked button as active.
els.templateButtons.forEach((btn) => {
if (btn.dataset.template === templateId) btn.classList.add('active');
else btn.classList.remove('active');
});
} catch (err) {
showWarning(`Template render failed: ${err.message || err}`);
}
}
function wireEvents() {
if (els.textInput) {
els.textInput.addEventListener('input', debounce(refreshPreview, 200));
@@ -151,6 +306,39 @@
}
});
}
// ---- mode-tab switching (Text Banner / Box-Frame / Templates) ----
els.modeTabs.forEach((tab) => {
tab.addEventListener('click', () => {
const mode = tab.dataset.mode;
if (mode) setMode(mode);
});
});
// ---- Box form: any change re-renders the box preview ----
if (els.boxText) {
els.boxText.addEventListener('input', () => {
if (_currentMode === 'box') refreshPreview();
});
}
if (els.boxStyle) {
els.boxStyle.addEventListener('change', () => {
if (_currentMode === 'box') refreshPreview();
});
}
if (els.boxPadding) {
els.boxPadding.addEventListener('input', () => {
if (_currentMode === 'box') refreshPreview();
});
}
// ---- Template buttons: render the chosen template into the preview ----
els.templateButtons.forEach((btn) => {
btn.addEventListener('click', () => {
const id = btn.dataset.template;
if (id) renderTemplate(id);
});
});
}
async function bootstrap() {
@@ -159,7 +347,10 @@
await refreshPreview();
}
// Expose pure helpers for tests; do not pollute window in production beyond
// what the bootstrap entry-point needs.
window.ASCIIController = { bootstrap };
window.ASCIIBoxRenderer = { renderBox };
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', bootstrap);
} else {