Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width,initial-scale=1" /> | |
<title>LaTeX Notepad</title> | |
<!-- ──────── MathJax ──────── --> | |
<script> | |
window.MathJax = { | |
tex: { | |
inlineMath: [['$', '$'], ['\\(', '\\)']], | |
displayMath: [['$$', '$$'], ['\\[', '\\]']], | |
processEscapes: true, | |
processEnvironments: true, | |
}, | |
options: { skipHtmlTags: ['script','noscript','style','textarea','pre'] } | |
}; | |
</script> | |
<script | |
id="MathJax-script" | |
async | |
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" | |
></script> | |
<!-- marked.js for Markdown --> | |
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
<!-- Google fonts --> | |
<link | |
href="https://fonts.googleapis.com/css2?family=Crimson+Text:wght@400;600&family=Libre+Baskerville:wght@400;700&family=PT+Mono&display=swap" | |
rel="stylesheet" | |
/> | |
<style> | |
/* ───────────────────────────────────────── | |
COLOR SYSTEM (light / dark via variables) | |
───────────────────────────────────────── */ | |
:root { | |
--desk-bg: #fbf9f5; | |
--desk-dot: #e2dccd; | |
--paper-bg: #fffefa; | |
--paper-text: #222; | |
--shadow: rgba(0,0,0,.35); | |
--line: rgba(201,190,170,.18); | |
--perforation: #d0c6b7; | |
--tbl-border: #d7cebf; | |
--blockquote-bg: #fbf8f1; | |
--blockquote-bar: #ccbfae; | |
--code-bg: #f4f2ec; | |
--code-border: #e6e0d2; | |
--inline-code-bg: #f2efe8; | |
} | |
body.dark { | |
--desk-bg: #2c2a27; | |
--desk-dot: #3a3733; | |
--paper-bg: #302e2b; | |
--paper-text: #e9e7e2; | |
--shadow: rgba(0,0,0,.55); | |
--line: rgba(110,103,94,.28); | |
--perforation: #6d6456; | |
--tbl-border: #555048; | |
--blockquote-bg: #38332e; | |
--blockquote-bar: #6a604e; | |
--code-bg: #3a3530; | |
--code-border: #514b42; | |
--inline-code-bg: #4a443d; | |
} | |
/* ───────────────────────────────────────── | |
GLOBAL “DESK” BACKGROUND | |
───────────────────────────────────────── */ | |
html,body{height:100%} | |
body{ | |
margin:0; | |
background: var(--desk-bg); | |
background-image: radial-gradient(var(--desk-dot) 1px,transparent 1px); | |
background-size:14px 14px; | |
font-family:'Crimson Text','Times New Roman',serif; | |
color:var(--paper-text); | |
line-height:1.8; | |
-webkit-font-smoothing:antialiased; | |
} | |
/* ───────────────────────────────────────── | |
PAPER SHEET | |
───────────────────────────────────────── */ | |
.container{ | |
max-width:840px; | |
margin:40px auto; | |
padding:40px 60px 60px; | |
background:var(--paper-bg); | |
color:var(--paper-text); | |
border:1px solid rgba(0,0,0,.05); | |
border-radius:12px 12px 10px 10px; | |
position:relative; | |
box-shadow:0 18px 40px -22px var(--shadow), | |
inset 0 2px 6px rgba(0,0,0,.06); | |
background-size:160px 160px,100% 100%; | |
} | |
/* perforation holes */ | |
.container::before{ | |
content:''; | |
position:absolute;top:26px;bottom:26px;left:30px;width:9px; | |
background-image:radial-gradient(circle var(--perforation) 0%,var(--perforation) 2px,transparent 3px); | |
background-size:9px 28px; | |
background-repeat:repeat-y; | |
pointer-events:none; | |
} | |
/* curled corner */ | |
.container::after{ | |
content:'';position:absolute;top:0;right:0;width:110px;height:110px; | |
background: | |
linear-gradient(135deg,rgba(0,0,0,.08) 0%,rgba(0,0,0,0) 42%), | |
linear-gradient(135deg,var(--paper-bg) 0%,var(--paper-bg) 50%,rgba(255,255,255,0) 51%); | |
background-size:100% 100%; | |
border-bottom-left-radius:12px; | |
transform:translate(1px,-1px); | |
pointer-events:none; | |
} | |
/* dark-mode gradient uses the *same* var so stays consistent */ | |
/* ───────── theme toggle ───────── */ | |
#themeToggle{ | |
position:absolute;top:12px;right:14px; | |
font-size:20px;background:none;border:none;cursor:pointer; | |
transition:transform .25s; | |
user-select:none; | |
} | |
/* put this anywhere after the existing #themeToggle rule */ | |
#themeToggle{ | |
position:absolute; /* you already have this */ | |
z-index:10; /* NEW – lift it above the curl */ | |
} | |
#themeToggle:hover{transform:rotate(20deg)scale(1.15)} | |
/* ───────── header ───────── */ | |
.header{text-align:center;margin-bottom:34px;padding-bottom:18px;border-bottom:1px solid rgba(0,0,0,.05)} | |
h1{font-family:'Libre Baskerville',serif;margin:0;font-size:30px;letter-spacing:.5px} | |
.subtitle{font-family:'PT Mono',monospace;font-size:14px;color:#666;margin-top:6px;letter-spacing:1px} | |
/* ───────── content area ───────── */ | |
#content{ | |
min-height:520px;font-size:18px;position:relative; | |
padding:10px 0 10px 26px;overflow-wrap:break-word;hyphens:auto; | |
} | |
#content::before{ | |
content:'';position:absolute;inset:0; | |
background:repeating-linear-gradient( | |
0deg, | |
transparent,transparent 2.65em, | |
var(--line) 2.65em,var(--line) 2.7em); | |
pointer-events:none;z-index:1; | |
} | |
#content *{position:relative;z-index:2} | |
.placeholder{color:#888;font-style:italic;text-align:center;padding:110px 20px;user-select:none} | |
/* ───────── markdown tweaks ───────── */ | |
blockquote{ | |
border-left:4px solid var(--blockquote-bar); | |
margin:20px 0;padding:15px 26px; | |
background:var(--blockquote-bg);font-style:italic | |
} | |
code{font-family:'PT Mono',monospace;background:var(--inline-code-bg); | |
padding:2px 6px;border-radius:3px;font-size:.9em} | |
pre{background:var(--code-bg);padding:16px 20px;border:1px solid var(--code-border); | |
border-radius:6px;overflow-x:auto;font-family:'PT Mono',monospace} | |
/* lists */ | |
ol{counter-reset:item;padding-left:0;list-style:none} | |
ol>li{counter-increment:item;margin:.5em 0 .5em 2em} | |
ol>li::before{content:counter(item)')';display:inline-block;width:1.5em;margin-left:-2em;text-align:right;font-weight:600} | |
ol ol>li::before{content:counter(item,lower-alpha)')'} | |
/* ───────── TABLES ───────── */ | |
table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums;margin:1.2em 0} | |
thead tr{border-bottom:1px solid var(--tbl-border)} | |
tbody tr:not(:last-child){border-bottom:1px solid var(--tbl-border)} | |
th,td{padding:.55em .8em;text-align:right} | |
th{font-weight:600} | |
/* ───────── processing badge ───────── */ | |
.processing{ | |
position:fixed;top:20px;right:20px;background:#333;color:#fff; | |
padding:10px 20px;border-radius:6px;font-family:'PT Mono',monospace; | |
font-size:14px;opacity:0;transition:opacity .25s;z-index:2000 | |
} | |
.processing.show{opacity:.9} | |
/* responsive & print */ | |
@media(max-width:768px){ | |
.container{margin:20px 16px;padding:28px} | |
#content{font-size:16px} | |
h1{font-size:24px} | |
} | |
@media print{ | |
body{background:#fff} | |
.container{box-shadow:none;border:none} | |
.header,.processing,.instructions,#themeToggle{display:none} | |
} | |
</style> | |
</head> | |
<body> | |
<div class="container"> | |
<!-- theme icon --> | |
<button id="themeToggle" title="Toggle dark / light">🌙</button> | |
<div class="header"> | |
<h1>LaTeX Notepad</h1> | |
<div class="subtitle">press ctrl+v anywhere to render</div> | |
</div> | |
<div id="content"> | |
<div class="placeholder"> | |
Press <kbd>Ctrl</kbd>+<kbd>V</kbd> (or <kbd>⌘</kbd>+<kbd>V</kbd>) to paste and render Markdown / LaTeX | |
</div> | |
</div> | |
<div class="instructions" style="text-align:center;font-family:'PT Mono',monospace;font-size:14px;color:#666;font-style:italic;margin-top:22px;"> | |
Tip: you can paste raw Markdown or TeX – it will be rendered instantly ✨ | |
</div> | |
</div> | |
<div class="processing">Processing…</div> | |
<script> | |
/* ======= processing badge helpers ======= */ | |
const content = document.getElementById('content'); | |
const processingNode = document.querySelector('.processing'); | |
function showProcessing(){processingNode.classList.add('show')} | |
function hideProcessing(){setTimeout(()=>processingNode.classList.remove('show'),300)} | |
/* ======= markdown + latex pipeline ======= */ | |
function processContent(text){ | |
showProcessing(); | |
const store=[], PL=i=>`%%LATEX_${i}%%`; let idx=0; | |
const keep=m=>(store.push(m),PL(idx++)); | |
text = text | |
.replace(/\\\[[\s\S]*?\\\]/g, keep) // \[ ... \] | |
.replace(/\$\$[\s\S]*?\$\$/g, keep) // $$ ... $$ | |
.replace(/\\\([\s\S]*?\\\)/g, keep) // \( ... \) | |
.replace(/\$([^\$\n]+?)\$/g, keep); // $ ... $ | |
let html = marked.parse(text); | |
store.forEach((latex,i)=>{html=html.replaceAll(PL(i),latex)}); | |
content.innerHTML = html; | |
if(window.MathJax?.typesetPromise){ | |
MathJax.typesetPromise([content]).then(hideProcessing) | |
.catch(e=>{console.error('MathJax error:',e);hideProcessing()}); | |
}else{hideProcessing()} | |
} | |
/* ======= paste listener ======= */ | |
document.addEventListener('paste',e=>{ | |
e.preventDefault(); | |
const txt=e.clipboardData.getData('text/plain'); | |
if(txt.trim())processContent(txt); | |
}); | |
/* small “bounce” on placeholder click */ | |
content.addEventListener('click',()=>{ | |
const ph=content.querySelector('.placeholder'); | |
if(ph){ | |
ph.style.transform='scale(.97)'; | |
ph.style.transition='transform .12s'; | |
setTimeout(()=>ph.style.transform='scale(1)',120); | |
} | |
}); | |
/* smooth fade in */ | |
document.addEventListener('DOMContentLoaded',()=>{ | |
const sheet=document.querySelector('.container'); | |
sheet.style.opacity='0'; | |
setTimeout(()=>{sheet.style.transition='opacity .6s ease';sheet.style.opacity='1'},80); | |
}); | |
/* ======= theme toggler ======= */ | |
const btn = document.getElementById('themeToggle'); | |
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)'); | |
const savedTheme = localStorage.getItem('note-theme'); | |
initTheme(); | |
btn.addEventListener('click',()=>{ | |
document.body.classList.toggle('dark'); | |
updateIcon(); | |
localStorage.setItem('note-theme',document.body.classList.contains('dark')?'dark':'light'); | |
}); | |
function initTheme(){ | |
if(savedTheme){ | |
document.body.classList.toggle('dark',savedTheme==='dark'); | |
}else if(prefersDark.matches){ | |
document.body.classList.add('dark'); | |
} | |
updateIcon(); | |
} | |
function updateIcon(){ | |
btn.textContent=document.body.classList.contains('dark')?'☀️':'🌙'; | |
} | |
</script> | |
</body> | |
</html> |