fix: align published article with template design
Browse filesTheme toggle:
- Replace emoji button with SVG sun/moon icons from ThemeToggle.astro
- Fix id mismatch (class="theme-toggle" -> id="theme-toggle")
- Add spin animation and system theme detection
Hero:
- Add .hero-title class to h1 so clamp font-size styles apply
DOM structure:
- Change <article> to <main> in content-grid to match template
selectors (.content-grid main) used across _base.css, _layout.css,
_table.css for typography, figures, references
Component CSS:
- Add styles for note, quoteBlock, sidenote, stack, reference,
wide, fullWidth components (div[data-component="..."])
Code highlighting:
- Load highlight.js runtime + call hljs.highlightAll() for syntax
coloring in published code blocks
Performance:
- Debounce CommentsSidebar position updates and TOC heading extraction
Made-with: Cursor
|
@@ -29,6 +29,7 @@ export interface PublishMeta {
|
|
| 29 |
affiliations: PublishAffiliation[];
|
| 30 |
date: string;
|
| 31 |
doi?: string;
|
|
|
|
| 32 |
ogImage?: string;
|
| 33 |
pdfUrl?: string;
|
| 34 |
}
|
|
@@ -82,8 +83,10 @@ export function renderArticleHTML(
|
|
| 82 |
integrity="sha384-zh0CIslj3dQfMxK3pZnPJAb3bprHitCE2vBz9TyOTICAn3fso5GYa90qPNMBclov"
|
| 83 |
crossorigin="anonymous">
|
| 84 |
|
| 85 |
-
<!-- highlight.js
|
| 86 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
|
|
|
|
|
|
|
| 87 |
|
| 88 |
<!-- Mermaid (renders <pre class="mermaid"> blocks) -->
|
| 89 |
<script type="module">
|
|
@@ -109,6 +112,106 @@ ${css.print}
|
|
| 109 |
|
| 110 |
/* Publisher-only overrides */
|
| 111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
/* Accordion as details/summary */
|
| 113 |
details[data-component="accordion"] {
|
| 114 |
border: 1px solid var(--border-color);
|
|
@@ -138,6 +241,122 @@ details[data-component="accordion"] > summary::before {
|
|
| 138 |
details[data-component="accordion"][open] > summary::before { transform: rotate(90deg); }
|
| 139 |
details[data-component="accordion"] > .accordion-content { padding: 0 1rem 1rem; }
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
/* PDF download link */
|
| 142 |
a.pdf-link {
|
| 143 |
display: inline-flex;
|
|
@@ -157,7 +376,16 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
|
|
| 157 |
</style>
|
| 158 |
</head>
|
| 159 |
<body>
|
| 160 |
-
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
|
| 162 |
<!-- Mobile TOC toggle -->
|
| 163 |
<button class="toc-mobile-toggle" aria-label="Open table of contents" aria-expanded="false">
|
|
@@ -180,7 +408,7 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
|
|
| 180 |
|
| 181 |
<!-- Hero -->
|
| 182 |
<section class="hero">
|
| 183 |
-
<h1>${safeTitle}</h1>
|
| 184 |
${meta.subtitle ? `<p class="hero-desc">${escapeHtml(meta.subtitle)}</p>` : ""}
|
| 185 |
</section>
|
| 186 |
|
|
@@ -193,13 +421,16 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
|
|
| 193 |
<div id="toc-placeholder"></div>
|
| 194 |
</nav>
|
| 195 |
|
| 196 |
-
<
|
| 197 |
<div class="tiptap">
|
| 198 |
${enrichedBody}
|
| 199 |
</div>
|
| 200 |
-
</
|
| 201 |
</section>
|
| 202 |
|
|
|
|
|
|
|
|
|
|
| 203 |
<!-- Image lightbox -->
|
| 204 |
<dialog class="lightbox" id="lightbox">
|
| 205 |
<img id="lightbox-img" src="" alt="">
|
|
@@ -207,14 +438,44 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
|
|
| 207 |
|
| 208 |
<script>
|
| 209 |
(function() {
|
| 210 |
-
// Theme
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
|
|
|
| 216 |
var saved = localStorage.getItem('theme');
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
// Lightbox
|
| 220 |
document.querySelectorAll('.tiptap img').forEach(function(img) {
|
|
@@ -232,7 +493,7 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
|
|
| 232 |
// ---- Table of Contents ----
|
| 233 |
var holder = document.getElementById('toc-placeholder');
|
| 234 |
var holderMobile = document.getElementById('toc-mobile-placeholder');
|
| 235 |
-
var articleRoot = document.querySelector('.content-grid
|
| 236 |
if (!articleRoot) return;
|
| 237 |
var headings = articleRoot.querySelectorAll('h2, h3, h4');
|
| 238 |
if (!headings.length) return;
|
|
@@ -425,6 +686,67 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
|
|
| 425 |
if (holderMobile) holderMobile.addEventListener('click', function(e) { if (e.target.closest && e.target.closest('a')) closeSidebar(); });
|
| 426 |
document.addEventListener('keydown', function(e) { if (e.key==='Escape' && sidebar.classList.contains('open')) closeSidebar(); });
|
| 427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
// Hash navigation
|
| 429 |
if (window.location.hash) {
|
| 430 |
var target = document.querySelector(window.location.hash);
|
|
@@ -600,3 +922,89 @@ function formatDate(dateStr: string): string {
|
|
| 600 |
return dateStr;
|
| 601 |
}
|
| 602 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
affiliations: PublishAffiliation[];
|
| 30 |
date: string;
|
| 31 |
doi?: string;
|
| 32 |
+
licence?: string;
|
| 33 |
ogImage?: string;
|
| 34 |
pdfUrl?: string;
|
| 35 |
}
|
|
|
|
| 83 |
integrity="sha384-zh0CIslj3dQfMxK3pZnPJAb3bprHitCE2vBz9TyOTICAn3fso5GYa90qPNMBclov"
|
| 84 |
crossorigin="anonymous">
|
| 85 |
|
| 86 |
+
<!-- highlight.js for code syntax highlighting -->
|
| 87 |
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
|
| 88 |
+
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
| 89 |
+
<script>hljs.highlightAll();</script>
|
| 90 |
|
| 91 |
<!-- Mermaid (renders <pre class="mermaid"> blocks) -->
|
| 92 |
<script type="module">
|
|
|
|
| 112 |
|
| 113 |
/* Publisher-only overrides */
|
| 114 |
|
| 115 |
+
/* Theme toggle icon wrapper (from ThemeToggle.astro) */
|
| 116 |
+
#theme-toggle .icon-wrapper {
|
| 117 |
+
display: grid;
|
| 118 |
+
place-items: center;
|
| 119 |
+
width: 20px;
|
| 120 |
+
height: 20px;
|
| 121 |
+
}
|
| 122 |
+
#theme-toggle .icon-wrapper .icon {
|
| 123 |
+
grid-area: 1 / 1;
|
| 124 |
+
filter: none !important;
|
| 125 |
+
}
|
| 126 |
+
#theme-toggle .icon-wrapper.animated .icon {
|
| 127 |
+
transition: opacity 0.35s ease;
|
| 128 |
+
}
|
| 129 |
+
#theme-toggle .icon-wrapper.spin-cw { animation: spin-cw 0.5s cubic-bezier(0.4,0,0.2,1); }
|
| 130 |
+
#theme-toggle .icon-wrapper.spin-ccw { animation: spin-ccw 0.5s cubic-bezier(0.4,0,0.2,1); }
|
| 131 |
+
@keyframes spin-cw { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
| 132 |
+
@keyframes spin-ccw { from { transform: rotate(0deg); } to { transform: rotate(-360deg); } }
|
| 133 |
+
|
| 134 |
+
/* Note component */
|
| 135 |
+
div[data-component="note"] {
|
| 136 |
+
border: 1px solid var(--border-color);
|
| 137 |
+
border-radius: 8px;
|
| 138 |
+
padding: 1rem 1.25rem;
|
| 139 |
+
margin: 1em 0;
|
| 140 |
+
background: var(--surface-bg, rgba(0,0,0,0.02));
|
| 141 |
+
}
|
| 142 |
+
div[data-component="note"][data-variant="warning"] {
|
| 143 |
+
border-color: #f59e0b;
|
| 144 |
+
background: rgba(245,158,11,0.06);
|
| 145 |
+
}
|
| 146 |
+
div[data-component="note"][data-variant="danger"] {
|
| 147 |
+
border-color: #ef4444;
|
| 148 |
+
background: rgba(239,68,68,0.06);
|
| 149 |
+
}
|
| 150 |
+
div[data-component="note"][data-variant="success"] {
|
| 151 |
+
border-color: #22c55e;
|
| 152 |
+
background: rgba(34,197,94,0.06);
|
| 153 |
+
}
|
| 154 |
+
div[data-component="note"] > *:first-child { margin-top: 0; }
|
| 155 |
+
div[data-component="note"] > *:last-child { margin-bottom: 0; }
|
| 156 |
+
|
| 157 |
+
/* Quote component */
|
| 158 |
+
div[data-component="quoteBlock"] {
|
| 159 |
+
border-left: 3px solid var(--primary-color, var(--border-color));
|
| 160 |
+
padding: 1rem 1.25rem;
|
| 161 |
+
margin: 1.5em 0;
|
| 162 |
+
font-style: italic;
|
| 163 |
+
color: var(--muted-color);
|
| 164 |
+
}
|
| 165 |
+
div[data-component="quoteBlock"] > *:first-child { margin-top: 0; }
|
| 166 |
+
div[data-component="quoteBlock"] > *:last-child { margin-bottom: 0; }
|
| 167 |
+
|
| 168 |
+
/* Sidenote component */
|
| 169 |
+
div[data-component="sidenote"] {
|
| 170 |
+
font-size: 0.85em;
|
| 171 |
+
color: var(--muted-color);
|
| 172 |
+
border-left: 2px solid var(--border-color);
|
| 173 |
+
padding-left: 0.75rem;
|
| 174 |
+
margin: 1em 0;
|
| 175 |
+
}
|
| 176 |
+
div[data-component="sidenote"] > *:first-child { margin-top: 0; }
|
| 177 |
+
div[data-component="sidenote"] > *:last-child { margin-bottom: 0; }
|
| 178 |
+
|
| 179 |
+
/* Reference wrapper */
|
| 180 |
+
div[data-component="reference"] {
|
| 181 |
+
margin: 1.5em 0;
|
| 182 |
+
}
|
| 183 |
+
div[data-component="reference"] > *:first-child { margin-top: 0; }
|
| 184 |
+
div[data-component="reference"] > *:last-child { margin-bottom: 0; }
|
| 185 |
+
|
| 186 |
+
/* Stack / multi-column layout */
|
| 187 |
+
div[data-component="stack"] {
|
| 188 |
+
display: grid;
|
| 189 |
+
gap: 1rem;
|
| 190 |
+
margin: 1.5em 0;
|
| 191 |
+
}
|
| 192 |
+
div[data-component="stack"][data-layout="2-column"] { grid-template-columns: 1fr 1fr; }
|
| 193 |
+
div[data-component="stack"][data-layout="3-column"] { grid-template-columns: 1fr 1fr 1fr; }
|
| 194 |
+
div[data-component="stack"][data-layout="4-column"] { grid-template-columns: 1fr 1fr 1fr 1fr; }
|
| 195 |
+
div[data-component="stack"][data-gap="small"] { gap: 0.5rem; }
|
| 196 |
+
div[data-component="stack"][data-gap="large"] { gap: 2rem; }
|
| 197 |
+
div[data-type="stack-column"] { min-width: 0; }
|
| 198 |
+
div[data-type="stack-column"] > *:first-child { margin-top: 0; }
|
| 199 |
+
div[data-type="stack-column"] > *:last-child { margin-bottom: 0; }
|
| 200 |
+
@media (max-width: 640px) {
|
| 201 |
+
div[data-component="stack"] { grid-template-columns: 1fr !important; }
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
/* Wide / full-width helpers for published content (if not already from _layout.css) */
|
| 205 |
+
div[data-component="wide"] {
|
| 206 |
+
width: min(1100px, 100vw - 64px);
|
| 207 |
+
margin-left: 50%;
|
| 208 |
+
transform: translateX(-50%);
|
| 209 |
+
}
|
| 210 |
+
div[data-component="fullWidth"] {
|
| 211 |
+
width: 100vw;
|
| 212 |
+
margin-left: calc(50% - 50vw);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
/* Accordion as details/summary */
|
| 216 |
details[data-component="accordion"] {
|
| 217 |
border: 1px solid var(--border-color);
|
|
|
|
| 241 |
details[data-component="accordion"][open] > summary::before { transform: rotate(90deg); }
|
| 242 |
details[data-component="accordion"] > .accordion-content { padding: 0 1rem 1rem; }
|
| 243 |
|
| 244 |
+
/* Footer */
|
| 245 |
+
.footer {
|
| 246 |
+
contain: layout style;
|
| 247 |
+
font-size: 0.8em;
|
| 248 |
+
line-height: 1.7em;
|
| 249 |
+
margin-top: 60px;
|
| 250 |
+
margin-bottom: 0;
|
| 251 |
+
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
| 252 |
+
color: rgba(0, 0, 0, 0.5);
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.footer-inner {
|
| 256 |
+
max-width: 1280px;
|
| 257 |
+
margin: 0 auto;
|
| 258 |
+
padding: 60px 16px 48px;
|
| 259 |
+
display: grid;
|
| 260 |
+
grid-template-columns: 220px minmax(0, 680px) 260px;
|
| 261 |
+
gap: 32px;
|
| 262 |
+
align-items: start;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.citation-block,
|
| 266 |
+
.references-block,
|
| 267 |
+
.reuse-block,
|
| 268 |
+
.doi-block {
|
| 269 |
+
display: contents;
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
.citation-block > .footer-heading,
|
| 273 |
+
.references-block > .footer-heading,
|
| 274 |
+
.reuse-block > .footer-heading,
|
| 275 |
+
.doi-block > .footer-heading {
|
| 276 |
+
grid-column: 1;
|
| 277 |
+
font-size: 15px;
|
| 278 |
+
font-weight: 600;
|
| 279 |
+
margin: 0;
|
| 280 |
+
text-align: right;
|
| 281 |
+
padding-right: 30px;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.citation-block > :not(.footer-heading),
|
| 285 |
+
.references-block > :not(.footer-heading),
|
| 286 |
+
.reuse-block > :not(.footer-heading),
|
| 287 |
+
.doi-block > :not(.footer-heading) {
|
| 288 |
+
grid-column: 2;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.citation-block .footer-heading { margin: 0 0 8px; }
|
| 292 |
+
|
| 293 |
+
.citation-block p,
|
| 294 |
+
.reuse-block p,
|
| 295 |
+
.doi-block p,
|
| 296 |
+
.footnotes ol,
|
| 297 |
+
.footnotes ol p,
|
| 298 |
+
.references { margin-top: 0; }
|
| 299 |
+
|
| 300 |
+
.citation {
|
| 301 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 302 |
+
font-size: 11px;
|
| 303 |
+
line-height: 15px;
|
| 304 |
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 305 |
+
background: rgba(0, 0, 0, 0.02);
|
| 306 |
+
padding: 10px 18px;
|
| 307 |
+
border-radius: 3px;
|
| 308 |
+
color: rgba(150, 150, 150, 1);
|
| 309 |
+
overflow: hidden;
|
| 310 |
+
margin-top: -12px;
|
| 311 |
+
white-space: pre-wrap;
|
| 312 |
+
word-wrap: break-word;
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.citation a { color: rgba(0, 0, 0, 0.6); text-decoration: underline; }
|
| 316 |
+
.citation.short { margin-top: -4px; }
|
| 317 |
+
|
| 318 |
+
.references-block .footer-heading { margin: 0; }
|
| 319 |
+
.references-block ol { padding: 0 0 0 15px; }
|
| 320 |
+
.references-block li { margin-bottom: 1em; }
|
| 321 |
+
.references-block a { color: var(--text-color); }
|
| 322 |
+
|
| 323 |
+
.footer a {
|
| 324 |
+
color: var(--primary-color);
|
| 325 |
+
border-bottom: 1px solid var(--link-underline);
|
| 326 |
+
text-decoration: none;
|
| 327 |
+
}
|
| 328 |
+
.footer a:hover {
|
| 329 |
+
color: var(--primary-color-hover);
|
| 330 |
+
border-bottom-color: var(--link-underline-hover);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
.template-credit { display: contents; }
|
| 334 |
+
.template-credit p {
|
| 335 |
+
grid-column: 2;
|
| 336 |
+
margin: 24px 0 0 0;
|
| 337 |
+
font-size: 0.85em;
|
| 338 |
+
color: rgba(0, 0, 0, 0.5);
|
| 339 |
+
}
|
| 340 |
+
.template-credit a { color: rgba(0, 0, 0, 0.6); border-bottom: 1px solid rgba(0, 0, 0, 0.15); }
|
| 341 |
+
.template-credit a:hover { color: rgba(0, 0, 0, 0.8); border-bottom-color: rgba(0, 0, 0, 0.3); }
|
| 342 |
+
|
| 343 |
+
[data-theme="dark"] .footer { border-top-color: rgba(255, 255, 255, 0.15); color: rgba(200, 200, 200, 0.8); }
|
| 344 |
+
[data-theme="dark"] .citation { background: rgba(255, 255, 255, 0.04); border-color: rgba(255, 255, 255, 0.15); color: rgba(200, 200, 200, 1); }
|
| 345 |
+
[data-theme="dark"] .citation a { color: rgba(255, 255, 255, 0.75); }
|
| 346 |
+
[data-theme="dark"] .footer a { color: var(--primary-color); }
|
| 347 |
+
[data-theme="dark"] .template-credit p { color: rgba(200, 200, 200, 0.6); }
|
| 348 |
+
[data-theme="dark"] .template-credit a { color: rgba(200, 200, 200, 0.7); border-bottom-color: rgba(255, 255, 255, 0.2); }
|
| 349 |
+
[data-theme="dark"] .template-credit a:hover { color: rgba(200, 200, 200, 0.9); border-bottom-color: rgba(255, 255, 255, 0.35); }
|
| 350 |
+
|
| 351 |
+
@media (max-width: 1100px) {
|
| 352 |
+
.footer-inner { grid-template-columns: 1fr; gap: 16px; display: block; padding: 40px 16px; }
|
| 353 |
+
.footer-inner > .footer-heading { grid-column: auto; margin-top: 16px; }
|
| 354 |
+
}
|
| 355 |
+
|
| 356 |
+
@media (min-width: 768px) {
|
| 357 |
+
.references-block ol { padding: 0 0 0 30px; margin-left: -30px; }
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
/* PDF download link */
|
| 361 |
a.pdf-link {
|
| 362 |
display: inline-flex;
|
|
|
|
| 376 |
</style>
|
| 377 |
</head>
|
| 378 |
<body>
|
| 379 |
+
<button id="theme-toggle" aria-label="Toggle color theme">
|
| 380 |
+
<span class="icon-wrapper">
|
| 381 |
+
<svg class="icon icon--sun" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 382 |
+
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="23"/><line x1="1" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="23" y2="12"/><line x1="4.22" y1="4.22" x2="6.34" y2="6.34"/><line x1="17.66" y1="17.66" x2="19.78" y2="19.78"/><line x1="4.22" y1="19.78" x2="6.34" y2="17.66"/><line x1="17.66" y1="6.34" x2="19.78" y2="4.22"/>
|
| 383 |
+
</svg>
|
| 384 |
+
<svg class="icon icon--moon" style="opacity:0" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
| 385 |
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
| 386 |
+
</svg>
|
| 387 |
+
</span>
|
| 388 |
+
</button>
|
| 389 |
|
| 390 |
<!-- Mobile TOC toggle -->
|
| 391 |
<button class="toc-mobile-toggle" aria-label="Open table of contents" aria-expanded="false">
|
|
|
|
| 408 |
|
| 409 |
<!-- Hero -->
|
| 410 |
<section class="hero">
|
| 411 |
+
<h1 class="hero-title">${safeTitle}</h1>
|
| 412 |
${meta.subtitle ? `<p class="hero-desc">${escapeHtml(meta.subtitle)}</p>` : ""}
|
| 413 |
</section>
|
| 414 |
|
|
|
|
| 421 |
<div id="toc-placeholder"></div>
|
| 422 |
</nav>
|
| 423 |
|
| 424 |
+
<main>
|
| 425 |
<div class="tiptap">
|
| 426 |
${enrichedBody}
|
| 427 |
</div>
|
| 428 |
+
</main>
|
| 429 |
</section>
|
| 430 |
|
| 431 |
+
<!-- Footer -->
|
| 432 |
+
${renderFooter(meta)}
|
| 433 |
+
|
| 434 |
<!-- Image lightbox -->
|
| 435 |
<dialog class="lightbox" id="lightbox">
|
| 436 |
<img id="lightbox-img" src="" alt="">
|
|
|
|
| 438 |
|
| 439 |
<script>
|
| 440 |
(function() {
|
| 441 |
+
// Theme toggle (matches template ThemeToggle.astro)
|
| 442 |
+
var btn = document.getElementById('theme-toggle');
|
| 443 |
+
var sunIcon = btn && btn.querySelector('.icon--sun');
|
| 444 |
+
var moonIcon = btn && btn.querySelector('.icon--moon');
|
| 445 |
+
var wrapper = btn && btn.querySelector('.icon-wrapper');
|
| 446 |
+
var media = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)');
|
| 447 |
+
var prefersDark = media && media.matches;
|
| 448 |
var saved = localStorage.getItem('theme');
|
| 449 |
+
|
| 450 |
+
function applyTheme(mode) {
|
| 451 |
+
document.documentElement.dataset.theme = mode;
|
| 452 |
+
if (sunIcon && moonIcon) {
|
| 453 |
+
sunIcon.style.opacity = mode === 'dark' ? '0' : '1';
|
| 454 |
+
moonIcon.style.opacity = mode === 'dark' ? '1' : '0';
|
| 455 |
+
}
|
| 456 |
+
}
|
| 457 |
+
applyTheme(saved || (prefersDark ? 'dark' : 'light'));
|
| 458 |
+
requestAnimationFrame(function() { if (wrapper) wrapper.classList.add('animated'); });
|
| 459 |
+
|
| 460 |
+
if (!saved && media) {
|
| 461 |
+
var syncSystem = function(e) { applyTheme(e.matches ? 'dark' : 'light'); };
|
| 462 |
+
if (media.addEventListener) media.addEventListener('change', syncSystem);
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
if (btn) {
|
| 466 |
+
btn.addEventListener('click', function() {
|
| 467 |
+
var next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
|
| 468 |
+
localStorage.setItem('theme', next);
|
| 469 |
+
if (wrapper) {
|
| 470 |
+
var cls = next === 'dark' ? 'spin-cw' : 'spin-ccw';
|
| 471 |
+
wrapper.classList.remove('spin-cw', 'spin-ccw');
|
| 472 |
+
void wrapper.offsetWidth;
|
| 473 |
+
wrapper.classList.add(cls);
|
| 474 |
+
wrapper.addEventListener('animationend', function() { wrapper.classList.remove(cls); }, { once: true });
|
| 475 |
+
}
|
| 476 |
+
applyTheme(next);
|
| 477 |
+
});
|
| 478 |
+
}
|
| 479 |
|
| 480 |
// Lightbox
|
| 481 |
document.querySelectorAll('.tiptap img').forEach(function(img) {
|
|
|
|
| 493 |
// ---- Table of Contents ----
|
| 494 |
var holder = document.getElementById('toc-placeholder');
|
| 495 |
var holderMobile = document.getElementById('toc-mobile-placeholder');
|
| 496 |
+
var articleRoot = document.querySelector('.content-grid main');
|
| 497 |
if (!articleRoot) return;
|
| 498 |
var headings = articleRoot.querySelectorAll('h2, h3, h4');
|
| 499 |
if (!headings.length) return;
|
|
|
|
| 686 |
if (holderMobile) holderMobile.addEventListener('click', function(e) { if (e.target.closest && e.target.closest('a')) closeSidebar(); });
|
| 687 |
document.addEventListener('keydown', function(e) { if (e.key==='Escape' && sidebar.classList.contains('open')) closeSidebar(); });
|
| 688 |
|
| 689 |
+
// ---- Footer: move references & footnotes ----
|
| 690 |
+
(function() {
|
| 691 |
+
var footer = document.querySelector('footer.footer');
|
| 692 |
+
if (!footer) return;
|
| 693 |
+
var target = footer.querySelector('.references-block');
|
| 694 |
+
if (!target) return;
|
| 695 |
+
var contentRoot = document.querySelector('.content-grid main') || document.body;
|
| 696 |
+
|
| 697 |
+
var ensureHeading = function(text) {
|
| 698 |
+
var exists = Array.from(target.children).some(function(c) {
|
| 699 |
+
return c.classList.contains('footer-heading') && c.textContent.trim().toLowerCase() === text.toLowerCase();
|
| 700 |
+
});
|
| 701 |
+
if (!exists) {
|
| 702 |
+
var h = document.createElement('p');
|
| 703 |
+
h.className = 'footer-heading';
|
| 704 |
+
h.setAttribute('role', 'heading');
|
| 705 |
+
h.setAttribute('aria-level', '2');
|
| 706 |
+
h.textContent = text;
|
| 707 |
+
target.appendChild(h);
|
| 708 |
+
}
|
| 709 |
+
};
|
| 710 |
+
|
| 711 |
+
var moveIntoFooter = function(el, headingText) {
|
| 712 |
+
if (!el) return false;
|
| 713 |
+
var firstH = el.querySelector(':scope > h1, :scope > h2, :scope > h3, :scope > .footer-heading, :scope > .bibliography-title');
|
| 714 |
+
if (firstH) {
|
| 715 |
+
var t = (firstH.textContent || '').trim().toLowerCase();
|
| 716 |
+
if (t === headingText.toLowerCase() || t.includes('reference') || t.includes('bibliograph')) firstH.remove();
|
| 717 |
+
}
|
| 718 |
+
ensureHeading(headingText);
|
| 719 |
+
target.appendChild(el);
|
| 720 |
+
return true;
|
| 721 |
+
};
|
| 722 |
+
|
| 723 |
+
var findAllOutsideFooter = function(selectors) {
|
| 724 |
+
var results = [];
|
| 725 |
+
for (var i = 0; i < selectors.length; i++) {
|
| 726 |
+
var els = contentRoot.querySelectorAll(selectors[i]);
|
| 727 |
+
els.forEach(function(el) { if (!footer.contains(el) && results.indexOf(el) === -1) results.push(el); });
|
| 728 |
+
}
|
| 729 |
+
return results;
|
| 730 |
+
};
|
| 731 |
+
|
| 732 |
+
var findFirst = function(selectors) { var a = findAllOutsideFooter(selectors); return a.length ? a[0] : null; };
|
| 733 |
+
|
| 734 |
+
var run = function() {
|
| 735 |
+
if (footer.dataset.processed === 'true') return;
|
| 736 |
+
var refs = findAllOutsideFooter(['[data-type="bibliography"]', '#bibliography-references-list', '#references', '#refs', '.bibliography']);
|
| 737 |
+
var notes = findFirst(['.footnotes', 'section.footnotes', 'div.footnotes']);
|
| 738 |
+
var moved = false;
|
| 739 |
+
refs.forEach(function(el) { if (moveIntoFooter(el, 'References')) moved = true; });
|
| 740 |
+
if (moveIntoFooter(notes, 'Footnotes')) moved = true;
|
| 741 |
+
if (moved) footer.dataset.processed = 'true';
|
| 742 |
+
};
|
| 743 |
+
|
| 744 |
+
run();
|
| 745 |
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run, { once: true });
|
| 746 |
+
window.addEventListener('load', function() { setTimeout(run, 100); }, { once: true });
|
| 747 |
+
setTimeout(run, 300);
|
| 748 |
+
})();
|
| 749 |
+
|
| 750 |
// Hash navigation
|
| 751 |
if (window.location.hash) {
|
| 752 |
var target = document.querySelector(window.location.hash);
|
|
|
|
| 922 |
return dateStr;
|
| 923 |
}
|
| 924 |
}
|
| 925 |
+
|
| 926 |
+
function extractYear(dateStr?: string): number | undefined {
|
| 927 |
+
if (!dateStr) return undefined;
|
| 928 |
+
const d = new Date(dateStr);
|
| 929 |
+
if (!Number.isNaN(d.getTime())) return d.getFullYear();
|
| 930 |
+
const m = dateStr.match(/(19|20)\d{2}/);
|
| 931 |
+
return m ? Number(m[0]) : undefined;
|
| 932 |
+
}
|
| 933 |
+
|
| 934 |
+
function buildCitationText(meta: PublishMeta): string {
|
| 935 |
+
const authorNames = meta.authors.map((a) => a.name);
|
| 936 |
+
const authorsStr = authorNames.join(", ");
|
| 937 |
+
const year = extractYear(meta.date);
|
| 938 |
+
const title = meta.title.replace(/\s+/g, " ").trim();
|
| 939 |
+
return `${authorsStr}${year ? ` (${year})` : ""}. "${title}".`;
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
function buildBibtex(meta: PublishMeta): string {
|
| 943 |
+
const authorNames = meta.authors.map((a) => a.name);
|
| 944 |
+
const authorsBib = authorNames.join(" and ");
|
| 945 |
+
const title = meta.title.replace(/\s+/g, " ").trim();
|
| 946 |
+
const year = extractYear(meta.date);
|
| 947 |
+
const keyAuthor = (authorNames[0] || "article")
|
| 948 |
+
.split(/\s+/)
|
| 949 |
+
.slice(-1)[0]
|
| 950 |
+
.toLowerCase();
|
| 951 |
+
const keyTitle = title
|
| 952 |
+
.toLowerCase()
|
| 953 |
+
.replace(/[^a-z0-9]+/g, "_")
|
| 954 |
+
.replace(/^_|_$/g, "");
|
| 955 |
+
const bibKey = `${keyAuthor}${year ?? ""}_${keyTitle}`;
|
| 956 |
+
const parts = [
|
| 957 |
+
` title={${title}}`,
|
| 958 |
+
` author={${authorsBib}}`,
|
| 959 |
+
];
|
| 960 |
+
if (year) parts.push(` year={${year}}`);
|
| 961 |
+
if (meta.doi) parts.push(` doi={${meta.doi}}`);
|
| 962 |
+
return `@misc{${bibKey},\n${parts.join(",\n")}\n}`;
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
function renderFooter(meta: PublishMeta): string {
|
| 966 |
+
const citationText = buildCitationText(meta);
|
| 967 |
+
const bibtex = buildBibtex(meta);
|
| 968 |
+
|
| 969 |
+
let sections = "";
|
| 970 |
+
|
| 971 |
+
// Citation block
|
| 972 |
+
sections += `
|
| 973 |
+
<section class="citation-block">
|
| 974 |
+
<p class="footer-heading" role="heading" aria-level="2">Citation</p>
|
| 975 |
+
<p>For attribution in academic contexts, please cite this work as</p>
|
| 976 |
+
<pre class="citation short">${escapeHtml(citationText)}</pre>
|
| 977 |
+
<p>BibTeX citation</p>
|
| 978 |
+
<pre class="citation long">${escapeHtml(bibtex)}</pre>
|
| 979 |
+
</section>`;
|
| 980 |
+
|
| 981 |
+
// DOI block
|
| 982 |
+
if (meta.doi) {
|
| 983 |
+
sections += `
|
| 984 |
+
<section class="doi-block">
|
| 985 |
+
<p class="footer-heading" role="heading" aria-level="2">DOI</p>
|
| 986 |
+
<p><a href="https://doi.org/${escapeHtml(meta.doi)}" target="_blank" rel="noopener noreferrer">${escapeHtml(meta.doi)}</a></p>
|
| 987 |
+
</section>`;
|
| 988 |
+
}
|
| 989 |
+
|
| 990 |
+
// Licence / Reuse block
|
| 991 |
+
if (meta.licence) {
|
| 992 |
+
sections += `
|
| 993 |
+
<section class="reuse-block">
|
| 994 |
+
<p class="footer-heading" role="heading" aria-level="2">Reuse</p>
|
| 995 |
+
<p>${meta.licence}</p>
|
| 996 |
+
</section>`;
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
// References placeholder (JS will move bibliography + footnotes here)
|
| 1000 |
+
sections += `
|
| 1001 |
+
<section class="references-block"></section>`;
|
| 1002 |
+
|
| 1003 |
+
// Template credit
|
| 1004 |
+
sections += `
|
| 1005 |
+
<div class="template-credit">
|
| 1006 |
+
<p>Made with ❤️ with <a href="https://huggingface.co/spaces/tfrere/research-article-template" target="_blank" rel="noopener noreferrer">research article template</a></p>
|
| 1007 |
+
</div>`;
|
| 1008 |
+
|
| 1009 |
+
return `<footer class="footer"><div class="footer-inner">${sections}</div></footer>`;
|
| 1010 |
+
}
|
|
@@ -236,6 +236,7 @@ export async function publishDocument(docName: string, token?: string): Promise<
|
|
| 236 |
affiliations: affiliations.map((a) => ({ name: a.name, url: a.url })),
|
| 237 |
date: (frontmatter.published as string) || (frontmatter.date as string) || new Date().toISOString(),
|
| 238 |
doi: (frontmatter.doi as string) || undefined,
|
|
|
|
| 239 |
};
|
| 240 |
|
| 241 |
// Pre-compute public URLs so og:image and PDF link are embedded in HTML
|
|
|
|
| 236 |
affiliations: affiliations.map((a) => ({ name: a.name, url: a.url })),
|
| 237 |
date: (frontmatter.published as string) || (frontmatter.date as string) || new Date().toISOString(),
|
| 238 |
doi: (frontmatter.doi as string) || undefined,
|
| 239 |
+
licence: (frontmatter.licence as string) || (frontmatter.license as string) || undefined,
|
| 240 |
};
|
| 241 |
|
| 242 |
// Pre-compute public URLs so og:image and PDF link are embedded in HTML
|
|
@@ -86,22 +86,29 @@ export function CommentsSidebar({ editor, commentStore, user, editorContainer }:
|
|
| 86 |
setPositioned(result);
|
| 87 |
}, [editor, editorContainer, comments]);
|
| 88 |
|
|
|
|
|
|
|
| 89 |
useEffect(() => {
|
| 90 |
updatePositions();
|
| 91 |
|
| 92 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
const editorEl = editor?.view?.dom;
|
| 94 |
const scrollTarget = editorContainer;
|
| 95 |
|
| 96 |
if (editorEl) {
|
| 97 |
if (scrollTarget) {
|
| 98 |
-
scrollTarget.addEventListener("scroll",
|
| 99 |
}
|
| 100 |
-
const observer = new MutationObserver(
|
| 101 |
observer.observe(editorEl, { childList: true, subtree: true, characterData: true });
|
| 102 |
return () => {
|
| 103 |
-
scrollTarget?.removeEventListener("scroll",
|
| 104 |
observer.disconnect();
|
|
|
|
| 105 |
};
|
| 106 |
}
|
| 107 |
}, [editor, editorContainer, updatePositions]);
|
|
|
|
| 86 |
setPositioned(result);
|
| 87 |
}, [editor, editorContainer, comments]);
|
| 88 |
|
| 89 |
+
const posTimerRef = useRef(0);
|
| 90 |
+
|
| 91 |
useEffect(() => {
|
| 92 |
updatePositions();
|
| 93 |
|
| 94 |
+
const debouncedPositions = () => {
|
| 95 |
+
clearTimeout(posTimerRef.current);
|
| 96 |
+
posTimerRef.current = window.setTimeout(updatePositions, 200);
|
| 97 |
+
};
|
| 98 |
+
|
| 99 |
const editorEl = editor?.view?.dom;
|
| 100 |
const scrollTarget = editorContainer;
|
| 101 |
|
| 102 |
if (editorEl) {
|
| 103 |
if (scrollTarget) {
|
| 104 |
+
scrollTarget.addEventListener("scroll", debouncedPositions, { passive: true });
|
| 105 |
}
|
| 106 |
+
const observer = new MutationObserver(debouncedPositions);
|
| 107 |
observer.observe(editorEl, { childList: true, subtree: true, characterData: true });
|
| 108 |
return () => {
|
| 109 |
+
scrollTarget?.removeEventListener("scroll", debouncedPositions);
|
| 110 |
observer.disconnect();
|
| 111 |
+
clearTimeout(posTimerRef.current);
|
| 112 |
};
|
| 113 |
}
|
| 114 |
}, [editor, editorContainer, updatePositions]);
|
|
@@ -60,12 +60,23 @@ export function TableOfContents({ editor }: TableOfContentsProps) {
|
|
| 60 |
const [activePos, setActivePos] = useState<number | null>(null);
|
| 61 |
const rafRef = useRef(0);
|
| 62 |
|
|
|
|
|
|
|
| 63 |
useEffect(() => {
|
| 64 |
if (!editor) return;
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
}, [editor]);
|
| 70 |
|
| 71 |
useEffect(() => {
|
|
|
|
| 60 |
const [activePos, setActivePos] = useState<number | null>(null);
|
| 61 |
const rafRef = useRef(0);
|
| 62 |
|
| 63 |
+
const debounceRef = useRef(0);
|
| 64 |
+
|
| 65 |
useEffect(() => {
|
| 66 |
if (!editor) return;
|
| 67 |
+
setHeadings(extractHeadings(editor));
|
| 68 |
+
const debouncedUpdate = () => {
|
| 69 |
+
clearTimeout(debounceRef.current);
|
| 70 |
+
debounceRef.current = window.setTimeout(
|
| 71 |
+
() => setHeadings(extractHeadings(editor)),
|
| 72 |
+
300,
|
| 73 |
+
);
|
| 74 |
+
};
|
| 75 |
+
editor.on("update", debouncedUpdate);
|
| 76 |
+
return () => {
|
| 77 |
+
editor.off("update", debouncedUpdate);
|
| 78 |
+
clearTimeout(debounceRef.current);
|
| 79 |
+
};
|
| 80 |
}, [editor]);
|
| 81 |
|
| 82 |
useEffect(() => {
|