feat(reader): implement FEAT-01 Focus Zen Mode (100% vh/vw, auto-hiding HUD, hotkeys F/Esc, native fullscreen)

This commit is contained in:
Antigravity committed 2026-10-08 10:45:09 +03:00
1 parent 9bbc9473de
commit f10965eff5
13 files changed
+3157 -792

No files matched your search

+113
View File
@@ -49,6 +49,111 @@ export function formatTranslationWithSpoiler(text) {
return `${escapeHtml(cleanText)} <span class="grammar-hint-badge" title="Нажмите, чтобы показать форму" onclick="this.classList.toggle('active')"><span class="badge-label">ℹ️ форма</span><span class="badge-body">${escapeHtml(noteText)}</span></span>`;
}
// -----------------------------------------------------------------------
// Lightweight Native Markdown Parsers (Tables & Notes for Murphy Grammar)
// Zero-dependency, safe escaping first, pure ES6
// -----------------------------------------------------------------------
export function renderMarkdownInline(text) {
if (!text) return '';
let str = escapeHtml(text);
// Bold: **text**
str = str.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Inline code: `text`
str = str.replace(/`([^`]+)`/g, '<code class="grammar-inline-code">$1</code>');
// Italic: *text* (excluding already formatted strong)
str = str.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
return str;
}
export function renderMarkdownTable(mdText) {
if (!mdText || typeof mdText !== 'string' || !mdText.trim()) return '';
const rawLines = mdText.trim().split(/\r?\n/).map(l => l.trim()).filter(l => l.length > 0);
const tableLines = rawLines.filter(l => l.startsWith('|') && l.endsWith('|'));
if (tableLines.length < 2) {
return `<div class="grammar-table-raw"><pre>${escapeHtml(mdText)}</pre></div>`;
}
const splitRow = (rowStr) => {
const inner = rowStr.replace(/^\|/, '').replace(/\|$/, '');
return inner.split(/(?<!\\)\|/).map(c => c.trim().replace(/\\\|/g, '|'));
};
const headers = splitRow(tableLines[0]);
const alignRow = splitRow(tableLines[1]);
const alignments = alignRow.map(col => {
const tc = col.trim();
if (tc.startsWith(':') && tc.endsWith(':')) return 'center';
if (tc.endsWith(':')) return 'right';
return 'left';
});
const rows = [];
for (let r = 2; r < tableLines.length; r++) {
rows.push(splitRow(tableLines[r]));
}
let html = '<div class="grammar-table-wrap"><table class="grammar-cheatsheet-table"><thead><tr>';
headers.forEach((h, idx) => {
const align = alignments[idx] || 'left';
html += `<th style="text-align: ${align}">${renderMarkdownInline(h)}</th>`;
});
html += '</tr></thead><tbody>';
rows.forEach(row => {
html += '<tr>';
row.forEach((cell, idx) => {
const align = alignments[idx] || 'left';
html += `<td style="text-align: ${align}">${renderMarkdownInline(cell)}</td>`;
});
html += '</tr>';
});
html += '</tbody></table></div>';
return html;
}
export function renderMarkdownNotes(mdText) {
if (!mdText || typeof mdText !== 'string' || !mdText.trim()) return '';
const lines = mdText.split(/\r?\n/);
const out = [];
let inList = false;
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) {
if (!inList) {
out.push('<ul class="grammar-notes-list">');
inList = true;
}
const itemText = trimmed.replace(/^[-*]\s+/, '');
out.push(`<li>${renderMarkdownInline(itemText)}</li>`);
continue;
} else if (inList) {
out.push('</ul>');
inList = false;
}
if (trimmed.startsWith('#### ')) {
out.push(`<h5 class="grammar-note-h5">${renderMarkdownInline(trimmed.replace(/^####\s+/, ''))}</h5>`);
} else if (trimmed.startsWith('### ')) {
out.push(`<h4 class="grammar-note-h4">${renderMarkdownInline(trimmed.replace(/^###\s+/, ''))}</h4>`);
} else if (trimmed.startsWith('## ')) {
out.push(`<h3 class="grammar-note-h3">${renderMarkdownInline(trimmed.replace(/^##\s+/, ''))}</h3>`);
} else if (trimmed.startsWith('> ')) {
out.push(`<blockquote class="grammar-note-quote">${renderMarkdownInline(trimmed.replace(/^>\s+/, ''))}</blockquote>`);
} else if (trimmed === '') {
// blank line separator
} else {
out.push(`<p class="grammar-note-p">${renderMarkdownInline(trimmed)}</p>`);
}
}
if (inList) {
out.push('</ul>');
}
return `<div class="grammar-notes-box">${out.join('\n')}</div>`;
}
// -----------------------------------------------------------------------
// Toast Notifications
// -----------------------------------------------------------------------
@@ -168,6 +273,11 @@ export function closeModal(modalElem) {
// View Switcher Helper
// -----------------------------------------------------------------------
export function switchView(targetViewId) {
if (targetViewId !== 'reader-view') {
document.body.classList.remove('zen-mode-active');
const hud = document.getElementById('reader-zen-hud');
if (hud) hud.classList.add('hidden');
}
const allViews = [
'dictionary-view',
'srs-view',
@@ -286,6 +396,9 @@ export const ui = {
playTextSpeech,
cleanPromptTranslation,
formatTranslationWithSpoiler,
renderMarkdownInline,
renderMarkdownTable,
renderMarkdownNotes,
};
export default ui;