Skip to content

Commit 9873a17

Browse files
staredclaude
andcommitted
Add HTML export UI with dropdown menu and state management
- Top toolbar with "Export as..." dropdown menu (HTML format) - Copy/Download buttons work in both markdown and export modes - Export displays in editor sidebar (read-only) - "Back to Edit" restores markdown without resetting content - Main equation display stays visible throughout - Simplified state management: no savedMarkdown variable - Export mode blocks preview updates via isExportMode flag - Comprehensive Playwright tests (5 tests, all passing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1b16941 commit 9873a17

File tree

5 files changed

+349
-15
lines changed

5 files changed

+349
-15
lines changed

index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,14 @@ <h1 id="equation-title">Interactive Equations</h1>
5858
<button id="toggle-editor-btn" class="toolbar-btn" title="Show/hide editor">
5959
<span class="icon"></span>
6060
</button>
61-
<a id="source-link" class="toolbar-link" href="#" target="_blank" rel="noopener">View source</a>
61+
<div class="export-dropdown">
62+
<button id="export-btn" class="toolbar-btn" title="Export">Export as...</button>
63+
<div id="export-menu" class="export-menu" style="display: none;">
64+
<button class="export-option" data-format="html">HTML</button>
65+
</div>
66+
</div>
67+
<button id="copy-btn" class="toolbar-btn" title="Copy to clipboard">Copy</button>
68+
<button id="download-btn" class="toolbar-btn" title="Download file">Download</button>
6269
<a href="https://github.com/stared/equations-explained-colorfully" class="toolbar-link" target="_blank" rel="noopener">Contribute</a>
6370
</div>
6471
<div id="editor-container" class="editor-container"></div>

src/exporter.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -415,15 +415,6 @@ function injectColorsIntoLatex(latex: string, termOrder: string[], colorScheme:
415415
return result;
416416
}
417417

418-
/**
419-
* Convert hex color to format supported by KaTeX
420-
* KaTeX supports hex colors directly like #RRGGBB
421-
*/
422-
function convertHexToLatexColor(hex: string): string {
423-
// KaTeX supports hex colors directly
424-
return hex;
425-
}
426-
427418
/**
428419
* Apply colors to description HTML (spans with term-X classes)
429420
*/

src/main.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { CodeJar } from 'codejar';
55
import Prism from 'prismjs';
66
import './prism-custom';
77
import { applyTermColors, markErrors } from './prism-custom';
8-
import { exportContent, type ExportFormat, type ColorScheme as ExportColorScheme } from './exporter';
8+
import { exportContent, getFileExtension, type ExportFormat, type ColorScheme as ExportColorScheme } from './exporter';
99

1010
// Equation metadata
1111
interface EquationInfo {
@@ -74,6 +74,7 @@ let parsedContent: ParsedContent | null = null;
7474
let editor: any = null;
7575
let currentMarkdown = '';
7676
let previewTimeout: number | null = null;
77+
let isExportMode = false;
7778

7879
function applyColorScheme(schemeName: string) {
7980
const scheme = colorSchemes[schemeName];
@@ -345,6 +346,11 @@ function initializeEditor() {
345346

346347
// Update preview on change (debounced)
347348
editor.onUpdate((code: string) => {
349+
// Don't update preview when in export mode
350+
if (isExportMode) {
351+
return;
352+
}
353+
348354
currentMarkdown = code;
349355

350356
// Clear previous timeout
@@ -433,6 +439,123 @@ function setupEditorControls() {
433439
}
434440
}
435441

442+
function setupExportUI() {
443+
const exportBtn = document.getElementById('export-btn');
444+
const exportMenu = document.getElementById('export-menu');
445+
const copyBtn = document.getElementById('copy-btn');
446+
const downloadBtn = document.getElementById('download-btn');
447+
const editorContainer = document.getElementById('editor-container');
448+
449+
let currentExport = '';
450+
let currentFormat: ExportFormat = 'html';
451+
452+
// Toggle export menu
453+
if (exportBtn && exportMenu) {
454+
exportBtn.addEventListener('click', (e) => {
455+
e.stopPropagation();
456+
457+
if (isExportMode) {
458+
// Back to edit mode
459+
isExportMode = false;
460+
if (editor) {
461+
editor.updateCode(currentMarkdown);
462+
editorContainer?.classList.remove('export-mode');
463+
// Make editor editable again
464+
const codeElement = editorContainer?.querySelector('code');
465+
if (codeElement) {
466+
(codeElement as HTMLElement).contentEditable = 'true';
467+
}
468+
}
469+
exportBtn.textContent = 'Export as...';
470+
exportMenu.style.display = 'none';
471+
} else {
472+
// Toggle menu
473+
const isVisible = exportMenu.style.display === 'block';
474+
exportMenu.style.display = isVisible ? 'none' : 'block';
475+
}
476+
});
477+
478+
// Close menu when clicking outside
479+
document.addEventListener('click', () => {
480+
if (!isExportMode) {
481+
exportMenu.style.display = 'none';
482+
}
483+
});
484+
485+
// Handle format selection
486+
exportMenu.querySelectorAll('.export-option').forEach((option) => {
487+
option.addEventListener('click', (e) => {
488+
e.stopPropagation();
489+
const format = (option as HTMLElement).dataset.format as ExportFormat;
490+
491+
try {
492+
// Generate export
493+
currentFormat = format;
494+
currentExport = generateExport(format);
495+
496+
// Show export in editor (read-only)
497+
if (editor) {
498+
isExportMode = true;
499+
editor.updateCode(currentExport);
500+
editorContainer?.classList.add('export-mode');
501+
// Make editor read-only
502+
const codeElement = editorContainer?.querySelector('code');
503+
if (codeElement) {
504+
(codeElement as HTMLElement).contentEditable = 'false';
505+
}
506+
exportBtn.textContent = 'Back to Edit';
507+
exportMenu.style.display = 'none';
508+
}
509+
} catch (error) {
510+
console.error('Export failed:', error);
511+
alert(`Export failed: ${error instanceof Error ? error.message : String(error)}`);
512+
}
513+
});
514+
});
515+
}
516+
517+
if (copyBtn) {
518+
copyBtn.addEventListener('click', async () => {
519+
try {
520+
const contentToCopy = isExportMode ? currentExport : currentMarkdown;
521+
await navigator.clipboard.writeText(contentToCopy);
522+
523+
const originalText = copyBtn.textContent;
524+
copyBtn.textContent = 'Copied!';
525+
setTimeout(() => {
526+
copyBtn.textContent = originalText;
527+
}, 2000);
528+
} catch (error) {
529+
console.error('Copy failed:', error);
530+
alert('Failed to copy to clipboard');
531+
}
532+
});
533+
}
534+
535+
if (downloadBtn) {
536+
downloadBtn.addEventListener('click', () => {
537+
try {
538+
const content = isExportMode ? currentExport : currentMarkdown;
539+
const extension = isExportMode ? getFileExtension(currentFormat) : 'md';
540+
const mimeType = isExportMode ? 'text/html' : 'text/markdown';
541+
542+
const blob = new Blob([content], { type: mimeType });
543+
const url = URL.createObjectURL(blob);
544+
const a = document.createElement('a');
545+
a.href = url;
546+
a.download = `${parsedContent?.title || 'equation'}.${extension}`.toLowerCase().replace(/[^a-z0-9.]+/g, '-');
547+
document.body.appendChild(a);
548+
a.click();
549+
document.body.removeChild(a);
550+
URL.revokeObjectURL(url);
551+
} catch (error) {
552+
console.error('Download failed:', error);
553+
alert('Failed to download file');
554+
}
555+
});
556+
}
557+
}
558+
436559
/**
437560
* Generate export for current content
438561
* UI will be added in Commit 6
@@ -466,6 +589,7 @@ document.addEventListener('DOMContentLoaded', async () => {
466589
// Initialize editor
467590
initializeEditor();
468591
setupEditorControls();
592+
setupExportUI();
469593

470594
// Load equation from URL hash or default
471595
const initialEquation = getEquationFromHash();

src/style.css

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,16 @@ h1 {
251251
padding: 0;
252252
border: none;
253253
background: none;
254-
color: var(--text-color);
255-
font-size: 1rem;
254+
color: #6b7280;
255+
font-size: 0.875rem;
256256
cursor: pointer;
257257
font-family: inherit;
258-
transition: opacity 0.2s ease;
258+
transition: color 0.2s ease;
259+
white-space: nowrap;
259260
}
260261

261262
.toolbar-btn:hover {
262-
opacity: 0.6;
263+
color: var(--text-color);
263264
}
264265

265266
.toolbar-link {
@@ -340,6 +341,48 @@ h1 {
340341
text-underline-offset: 3px;
341342
}
342343

344+
/* Export dropdown */
345+
.export-dropdown {
346+
position: relative;
347+
display: inline-block;
348+
}
349+
350+
.export-menu {
351+
position: absolute;
352+
top: 100%;
353+
left: 0;
354+
background-color: #ffffff;
355+
border: 1px solid var(--border-color);
356+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
357+
z-index: 1000;
358+
min-width: 120px;
359+
margin-top: 0.25rem;
360+
}
361+
362+
.export-option {
363+
display: block;
364+
width: 100%;
365+
padding: 0.5rem 1rem;
366+
border: none;
367+
background: none;
368+
color: var(--text-color);
369+
font-size: 0.875rem;
370+
cursor: pointer;
371+
font-family: inherit;
372+
text-align: left;
373+
transition: background-color 0.2s ease;
374+
}
375+
376+
.export-option:hover {
377+
background-color: #f9fafb;
378+
}
379+
380+
/* Editor in export mode */
381+
.editor-container.export-mode code {
382+
color: #6a737d;
383+
font-size: 0.75rem;
384+
}
385+
343386
/* Responsive design */
344387
@media (max-width: 1200px) {
345388
.editor-sidebar {

0 commit comments

Comments
 (0)