tfrere HF Staff commited on
Commit
9b7d64e
·
1 Parent(s): 1e2f7dc

feat: adopt template CSS architecture, hero/TOC/color alignment

Browse files

- Copy template foundation CSS (_variables, _reset, _base, _layout,
_print) and component CSS (_code, _table, _tag, _card, _mermaid)
- Extract hero styles from HeroArticle.astro into shared _hero.css
- Extract TOC styles from TableOfContents.astro into shared _toc.css
- Create _editor-tokens.css for editor-specific design tokens
- Scope _reset.css and _base.css global styles to article area
to avoid overriding MUI portals (Dialog, Menu, Tooltip)
- Exclude _button.css and _form.css from editor (bare element
selectors conflict with MUI components)
- Align FrontmatterHero class names with template (.hero, .meta,
.meta-container-cell, .authors, .affiliations)
- Align publisher HTML renderer class names accordingly
- Fix TOC sticky positioning (align-self:stretch + sticky wrapper)
- Add hue-based color picker (OKLCH slider like template ColorPicker)
synced via Yjs settings, driving both MUI theme and CSS variables
- Align editor link styles with template (--primary-color)
- Add PostCSS config for @custom-media support
- Remove "Welcome to Collaborative Editor" from default content

Made-with: Cursor

Files changed (36) hide show
  1. backend/src/hf-storage.ts +35 -23
  2. backend/src/publisher/extensions.ts +7 -0
  3. backend/src/publisher/html-renderer.ts +58 -318
  4. backend/src/publisher/index.ts +66 -15
  5. frontend/package-lock.json +1381 -26
  6. frontend/package.json +2 -0
  7. frontend/postcss.config.js +5 -0
  8. frontend/src/App.tsx +43 -27
  9. frontend/src/components/TableOfContents.tsx +21 -72
  10. frontend/src/editor/components/MermaidView.tsx +179 -0
  11. frontend/src/editor/components/factory.ts +3 -1
  12. frontend/src/editor/components/registry.ts +11 -0
  13. frontend/src/editor/default-content.ts +2 -4
  14. frontend/src/editor/frontmatter/FrontmatterHero.tsx +72 -154
  15. frontend/src/editor/frontmatter/HueSlider.tsx +196 -0
  16. frontend/src/editor/frontmatter/SettingsDrawer.tsx +18 -2
  17. frontend/src/main.tsx +24 -7
  18. frontend/src/styles/_base.css +174 -0
  19. frontend/src/styles/_editor-tokens.css +105 -0
  20. frontend/src/styles/_layout.css +363 -0
  21. frontend/src/styles/_print.css +128 -0
  22. frontend/src/styles/_reset.css +25 -0
  23. frontend/src/styles/_variables.css +126 -0
  24. frontend/src/styles/article.css +27 -26
  25. frontend/src/styles/components/_button.css +58 -0
  26. frontend/src/styles/components/_card.css +138 -0
  27. frontend/src/styles/components/_code.css +357 -0
  28. frontend/src/styles/components/_form.css +243 -0
  29. frontend/src/styles/components/_hero.css +136 -0
  30. frontend/src/styles/components/_mermaid.css +250 -0
  31. frontend/src/styles/components/_table.css +121 -0
  32. frontend/src/styles/components/_tag.css +14 -0
  33. frontend/src/styles/components/_toc.css +55 -0
  34. frontend/src/styles/editing.css +103 -32
  35. frontend/src/styles/toc.css +52 -0
  36. frontend/src/theme.ts +39 -28
backend/src/hf-storage.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { uploadFile, downloadFile, createRepo, type RepoDesignation } from "@huggingface/hub";
2
 
3
  const SPACE_ID = process.env.SPACE_ID || "";
4
 
@@ -188,6 +188,15 @@ export async function pullPublishedAssets(
188
 
189
  // ---------- Published Assets ----------
190
 
 
 
 
 
 
 
 
 
 
191
  interface PublishedPayload {
192
  html: string;
193
  pdf: Buffer | null;
@@ -206,46 +215,49 @@ export async function uploadPublishedAssets(
206
  const safeName = sanitizeName(docName);
207
  const base = `published/${safeName}`;
208
 
209
- await uploadFile({
210
- repo,
211
- file: { path: `${base}/index.html`, content: new Blob([payload.html]) },
212
- accessToken,
213
- commitTitle: `publish ${safeName}: HTML`,
214
- });
 
 
 
 
 
 
215
 
216
  const baseUrl = `https://huggingface.co/datasets/${HF_DATASET_ID}/resolve/main/${base}`;
217
  let pdfUrl: string | null = null;
218
  let thumbUrl: string | null = null;
219
 
220
  if (payload.pdf) {
221
- await uploadFile({
222
- repo,
223
- file: { path: `${base}/article.pdf`, content: new Blob([new Uint8Array(payload.pdf)]) },
224
- accessToken,
225
- commitTitle: `publish ${safeName}: PDF`,
226
  });
227
  pdfUrl = `${baseUrl}/article.pdf`;
228
  }
229
 
230
  if (payload.thumbnail) {
231
- await uploadFile({
232
- repo,
233
- file: { path: `${base}/thumb.jpg`, content: new Blob([new Uint8Array(payload.thumbnail)]) },
234
- accessToken,
235
- commitTitle: `publish ${safeName}: thumbnail`,
236
  });
237
  thumbUrl = `${baseUrl}/thumb.jpg`;
238
  }
239
 
240
- await uploadFile({
241
  repo,
242
- file: {
243
- path: `${base}/meta.json`,
244
- content: new Blob([JSON.stringify(payload.meta, null, 2)]),
245
- },
246
  accessToken,
247
- commitTitle: `publish ${safeName}: metadata`,
248
  });
249
 
 
 
250
  return { htmlUrl: `${baseUrl}/index.html`, pdfUrl, thumbUrl };
251
  }
 
1
+ import { uploadFile, downloadFile, createRepo, commit, type CommitFile, type RepoDesignation } from "@huggingface/hub";
2
 
3
  const SPACE_ID = process.env.SPACE_ID || "";
4
 
 
188
 
189
  // ---------- Published Assets ----------
190
 
191
+ /**
192
+ * Build the public resolve URL for a published asset.
193
+ * Useful to pre-compute og:image / pdf URLs before HTML generation.
194
+ */
195
+ export function getPublishedAssetUrl(docName: string, filename: string): string {
196
+ const safeName = sanitizeName(docName);
197
+ return `https://huggingface.co/datasets/${HF_DATASET_ID}/resolve/main/published/${safeName}/${filename}`;
198
+ }
199
+
200
  interface PublishedPayload {
201
  html: string;
202
  pdf: Buffer | null;
 
215
  const safeName = sanitizeName(docName);
216
  const base = `published/${safeName}`;
217
 
218
+ const operations: CommitFile[] = [
219
+ {
220
+ operation: "addOrUpdate",
221
+ path: `${base}/index.html`,
222
+ content: new Blob([payload.html]),
223
+ },
224
+ {
225
+ operation: "addOrUpdate",
226
+ path: `${base}/meta.json`,
227
+ content: new Blob([JSON.stringify(payload.meta, null, 2)]),
228
+ },
229
+ ];
230
 
231
  const baseUrl = `https://huggingface.co/datasets/${HF_DATASET_ID}/resolve/main/${base}`;
232
  let pdfUrl: string | null = null;
233
  let thumbUrl: string | null = null;
234
 
235
  if (payload.pdf) {
236
+ operations.push({
237
+ operation: "addOrUpdate",
238
+ path: `${base}/article.pdf`,
239
+ content: new Blob([new Uint8Array(payload.pdf)]),
 
240
  });
241
  pdfUrl = `${baseUrl}/article.pdf`;
242
  }
243
 
244
  if (payload.thumbnail) {
245
+ operations.push({
246
+ operation: "addOrUpdate",
247
+ path: `${base}/thumb.jpg`,
248
+ content: new Blob([new Uint8Array(payload.thumbnail)]),
 
249
  });
250
  thumbUrl = `${baseUrl}/thumb.jpg`;
251
  }
252
 
253
+ await commit({
254
  repo,
255
+ operations,
256
+ title: `publish ${safeName}`,
 
 
257
  accessToken,
 
258
  });
259
 
260
+ console.log(`[hf-storage] published ${safeName} (${operations.length} files, single commit)`);
261
+
262
  return { htmlUrl: `${baseUrl}/index.html`, pdfUrl, thumbUrl };
263
  }
backend/src/publisher/extensions.ts CHANGED
@@ -246,6 +246,13 @@ const COMPONENT_DEFS: ComponentDefLite[] = [
246
  { name: "html", type: "string", default: "" },
247
  ],
248
  },
 
 
 
 
 
 
 
249
  ];
250
 
251
  function makeServerWrapperExt(def: ComponentDefLite) {
 
246
  { name: "html", type: "string", default: "" },
247
  ],
248
  },
249
+ {
250
+ name: "mermaid",
251
+ kind: "atomic",
252
+ fields: [
253
+ { name: "code", type: "string", default: "" },
254
+ ],
255
+ },
256
  ];
257
 
258
  function makeServerWrapperExt(def: ComponentDefLite) {
backend/src/publisher/html-renderer.ts CHANGED
@@ -7,6 +7,7 @@
7
 
8
  import { generateHTML } from "@tiptap/html";
9
  import { getServerExtensions } from "./extensions.js";
 
10
 
11
  export interface PublishAuthor {
12
  name: string;
@@ -29,6 +30,7 @@ export interface PublishMeta {
29
  date: string;
30
  doi?: string;
31
  ogImage?: string;
 
32
  }
33
 
34
  /**
@@ -37,8 +39,7 @@ export interface PublishMeta {
37
  export function renderArticleHTML(
38
  json: Record<string, unknown>,
39
  meta: PublishMeta,
40
- cssTokens: string,
41
- cssArticle: string
42
  ): string {
43
  const extensions = getServerExtensions();
44
 
@@ -84,315 +85,33 @@ export function renderArticleHTML(
84
  <!-- highlight.js theme -->
85
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/styles/github-dark.min.css">
86
 
87
- <style>
88
- ${cssTokens}
89
- ${cssArticle}
90
-
91
- /* Published article layout */
92
- body {
93
- margin: 0;
94
- padding: 0;
95
- background: var(--bg-primary);
96
- color: var(--text-primary);
97
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
98
- }
99
-
100
- /* Grid: TOC | Article | (empty right) */
101
- .content-grid {
102
- max-width: 1280px;
103
- margin: 0 auto;
104
- padding: 0 24px;
105
- margin-top: 40px;
106
- display: grid;
107
- grid-template-columns: 260px minmax(0, 680px) 260px;
108
- gap: 32px;
109
- align-items: start;
110
- }
111
-
112
- /* Hero section */
113
- .article-hero {
114
- width: 100%;
115
- padding: 64px 16px 16px;
116
- text-align: center;
117
- }
118
-
119
- .article-hero h1 {
120
- font-size: clamp(34px, 5vw, 54px);
121
- font-weight: 800;
122
- line-height: 1.12;
123
- letter-spacing: -0.02em;
124
- margin: 0 auto 8px;
125
- max-width: 780px;
126
- text-wrap: balance;
127
- color: var(--text-heading);
128
- }
129
-
130
- .article-hero .hero-desc {
131
- color: var(--text-tertiary);
132
- font-style: italic;
133
- margin: 0 auto 16px;
134
- max-width: 55%;
135
- font-size: 1.15em;
136
- line-height: 1.6;
137
- }
138
-
139
- /* Meta bar */
140
- .article-meta {
141
- border-top: 1px solid var(--border);
142
- border-bottom: 1px solid var(--border);
143
- padding: 1rem 0;
144
- font-size: 0.9rem;
145
- }
146
-
147
- .article-meta .meta-container {
148
- max-width: 980px;
149
- margin: 0 auto;
150
- padding: 0 16px;
151
- display: flex;
152
- flex-direction: row;
153
- justify-content: space-between;
154
- gap: 8px;
155
- flex-wrap: wrap;
156
- row-gap: 12px;
157
- }
158
-
159
- .article-meta .meta-cell {
160
- display: flex;
161
- flex-direction: column;
162
- gap: 8px;
163
- max-width: 400px;
164
- }
165
-
166
- .article-meta .meta-cell h3 {
167
- margin: 0;
168
- font-size: 12px;
169
- font-weight: 400;
170
- color: var(--text-tertiary);
171
- text-transform: uppercase;
172
- letter-spacing: 0.02em;
173
- }
174
-
175
- .article-meta .meta-cell p { margin: 0; }
176
-
177
- .article-meta .authors-list {
178
- margin: 0;
179
- list-style: none;
180
- padding-left: 0;
181
- display: flex;
182
- flex-wrap: wrap;
183
- }
184
-
185
- .article-meta .authors-list li {
186
- white-space: nowrap;
187
- padding: 0;
188
- }
189
-
190
- .article-meta .author-link {
191
- color: var(--accent-light);
192
- text-decoration: underline;
193
- text-underline-offset: 2px;
194
- text-decoration-thickness: 0.06em;
195
- }
196
-
197
- .article-meta .author-link:hover {
198
- color: var(--text-primary);
199
- }
200
-
201
- .article-meta .affiliations-list {
202
- margin: 0;
203
- padding-left: 1.25em;
204
- }
205
-
206
- .article-meta .affiliations-list li { margin: 0; }
207
-
208
- .article-meta .doi-link {
209
- color: var(--accent-light);
210
- text-decoration: underline;
211
- text-underline-offset: 2px;
212
- font-family: monospace;
213
- }
214
-
215
- @media (max-width: 768px) {
216
- .article-hero .hero-desc { max-width: 90%; }
217
- .article-meta .meta-container { flex-direction: column; }
218
- }
219
-
220
- /* Theme toggle */
221
- .theme-toggle {
222
- position: fixed;
223
- top: 1rem;
224
- right: 1rem;
225
- background: var(--bg-secondary);
226
- border: 1px solid var(--border);
227
- border-radius: 8px;
228
- padding: 0.5rem;
229
- cursor: pointer;
230
- font-size: 1.2rem;
231
- line-height: 1;
232
- z-index: 100;
233
- }
234
-
235
- /* ---------- Table of Contents (desktop sticky) ---------- */
236
- .table-of-contents {
237
- position: sticky;
238
- top: 32px;
239
- margin-top: 12px;
240
- }
241
-
242
- .table-of-contents .title {
243
- font-weight: 600;
244
- font-size: 14px;
245
- margin-bottom: 8px;
246
- color: var(--text-primary);
247
- }
248
-
249
- .table-of-contents nav {
250
- border-left: 1px solid var(--border);
251
- padding-left: 16px;
252
- font-size: 13px;
253
- }
254
-
255
- .table-of-contents nav ul {
256
- margin: 0 0 6px;
257
- padding-left: 1em;
258
- }
259
-
260
- .table-of-contents nav li {
261
- list-style: none;
262
- margin: 0.25em 0;
263
- }
264
-
265
- .table-of-contents nav a,
266
- .table-of-contents nav a:link,
267
- .table-of-contents nav a:visited {
268
- color: var(--text-primary);
269
- text-decoration: none;
270
- border-bottom: none;
271
- }
272
-
273
- .table-of-contents nav > ul > li > a {
274
- font-weight: 700;
275
- }
276
-
277
- .table-of-contents nav a:hover {
278
- text-decoration: underline solid var(--text-tertiary);
279
- }
280
-
281
- .table-of-contents nav a.active {
282
- text-decoration: underline;
283
- }
284
 
285
- /* Collapsible sub-sections */
286
- .table-of-contents nav.toc-collapsible li > ul,
287
- .toc-mobile-sidebar nav.toc-collapsible li > ul {
288
- overflow: hidden;
289
- transition: height 200ms ease;
290
- }
291
 
292
- /* ---------- Mobile TOC toggle ---------- */
293
- .toc-mobile-toggle {
294
- display: none;
295
- position: fixed;
296
- top: 1rem;
297
- left: 1rem;
298
- z-index: 200;
299
- width: 40px;
300
- height: 40px;
301
- border-radius: 50%;
302
- border: 1px solid var(--border);
303
- background: var(--bg-primary);
304
- color: var(--text-primary);
305
- cursor: pointer;
306
- align-items: center;
307
- justify-content: center;
308
- box-shadow: 0 2px 12px rgba(0,0,0,.08);
309
- transition: transform 150ms ease, box-shadow 150ms ease;
310
- }
311
- .toc-mobile-toggle:active { transform: scale(0.92); }
312
-
313
- /* ---------- Mobile backdrop ---------- */
314
- .toc-mobile-backdrop {
315
- display: none;
316
- position: fixed;
317
- inset: 0;
318
- z-index: 201;
319
- background: rgba(0,0,0,.4);
320
- opacity: 0;
321
- pointer-events: none;
322
- transition: opacity 250ms ease;
323
- }
324
- .toc-mobile-backdrop.open { opacity: 1; pointer-events: auto; }
325
-
326
- /* ---------- Mobile sidebar ---------- */
327
- .toc-mobile-sidebar {
328
- display: none;
329
- position: fixed;
330
- top: 0; left: 0; bottom: 0;
331
- z-index: 202;
332
- width: min(320px, 85vw);
333
- background: var(--bg-primary);
334
- border-right: 1px solid var(--border);
335
- transform: translateX(-100%);
336
- transition: transform 300ms cubic-bezier(.4,0,.2,1);
337
- flex-direction: column;
338
- }
339
- .toc-mobile-sidebar.open { transform: translateX(0); }
340
 
341
- .toc-mobile-sidebar__header {
342
- display: flex;
343
- align-items: center;
344
- justify-content: space-between;
345
- padding: 0.75rem 1rem;
346
- border-bottom: 1px solid var(--border);
347
- flex-shrink: 0;
348
- }
349
- .toc-mobile-sidebar__title { font-weight: 700; font-size: 15px; color: var(--text-primary); }
350
- .toc-mobile-sidebar__close {
351
- background: none; border: none; color: var(--text-tertiary);
352
- cursor: pointer; padding: 6px; border-radius: 6px;
353
- display: flex; align-items: center; justify-content: center;
354
- transition: color 150ms ease, background 150ms ease;
355
- }
356
- .toc-mobile-sidebar__close:hover { color: var(--text-primary); background: var(--bg-secondary); }
357
 
358
- .toc-mobile-sidebar__body {
359
- flex: 1;
360
- overflow-y: auto;
361
- -webkit-overflow-scrolling: touch;
362
- padding: 0.75rem 1rem 1.5rem;
363
- }
364
- .toc-mobile-sidebar nav ul { margin: 0 0 6px; padding-left: 1em; }
365
- .toc-mobile-sidebar nav li { list-style: none; margin: 0.35em 0; }
366
- .toc-mobile-sidebar nav a,
367
- .toc-mobile-sidebar nav a:link,
368
- .toc-mobile-sidebar nav a:visited {
369
- color: var(--text-primary);
370
- text-decoration: none;
371
- border-bottom: none;
372
- font-size: 14px;
373
- line-height: 1.5;
374
- }
375
- .toc-mobile-sidebar nav > ul > li > a { font-weight: 700; }
376
- .toc-mobile-sidebar nav a:hover { text-decoration: underline solid var(--text-tertiary); }
377
- .toc-mobile-sidebar nav a.active { color: var(--accent-light); text-decoration: underline; }
378
-
379
- /* ---------- Responsive: collapse below 1100px ---------- */
380
- @media (max-width: 1100px) {
381
- .content-grid {
382
- overflow: hidden;
383
- display: block;
384
- padding: 0 16px;
385
- margin-top: 0.5rem;
386
- }
387
- .table-of-contents { position: static; display: none; }
388
- .toc-mobile-toggle { display: flex; }
389
- .toc-mobile-backdrop { display: block; }
390
- .toc-mobile-sidebar { display: flex; }
391
- }
392
 
393
  /* Accordion as details/summary */
394
  details[data-component="accordion"] {
395
- border: 1px solid var(--border);
396
  border-radius: 8px;
397
  padding: 0;
398
  margin: 1em 0;
@@ -402,7 +121,7 @@ details[data-component="accordion"] > summary {
402
  padding: 0.75rem 1rem;
403
  cursor: pointer;
404
  font-weight: 600;
405
- color: var(--text-heading-secondary);
406
  list-style: none;
407
  user-select: none;
408
  }
@@ -419,16 +138,22 @@ details[data-component="accordion"] > summary::before {
419
  details[data-component="accordion"][open] > summary::before { transform: rotate(90deg); }
420
  details[data-component="accordion"] > .accordion-content { padding: 0 1rem 1rem; }
421
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  /* Image lightbox dialog */
423
  dialog.lightbox { border: none; background: transparent; padding: 0; max-width: 95vw; max-height: 95vh; }
424
  dialog.lightbox::backdrop { background: rgba(0, 0, 0, 0.85); }
425
  dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; border-radius: 4px; }
426
-
427
- /* Print */
428
- @media print {
429
- .table-of-contents, .toc-mobile-toggle, .toc-mobile-backdrop, .toc-mobile-sidebar, .theme-toggle { display: none !important; }
430
- .content-grid { display: block !important; }
431
- }
432
  </style>
433
  </head>
434
  <body>
@@ -454,7 +179,7 @@ dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; bo
454
  </aside>
455
 
456
  <!-- Hero -->
457
- <section class="article-hero">
458
  <h1>${safeTitle}</h1>
459
  ${meta.subtitle ? `<p class="hero-desc">${escapeHtml(meta.subtitle)}</p>` : ""}
460
  </section>
@@ -770,6 +495,16 @@ function postProcess(html: string, biblioHtml: string): string {
770
  }
771
  );
772
 
 
 
 
 
 
 
 
 
 
 
773
  // HtmlEmbed: div[data-component="htmlEmbed"] → iframe with srcdoc
774
  result = result.replace(
775
  /<div[^>]*data-component="htmlEmbed"[^>]*data-src="([^"]*)"[^>]*><\/div>/g,
@@ -813,7 +548,7 @@ function renderMetaBar(meta: PublishMeta): string {
813
  const sep = i < meta.authors.length - 1 ? ",&nbsp;" : "";
814
  return `<li>${name}${sup}${sep}</li>`;
815
  }).join("");
816
- cells.push(`<div class="meta-cell"><h3>Authors</h3><ul class="authors-list">${items}</ul></div>`);
817
  }
818
 
819
  // Affiliations cell
@@ -826,27 +561,32 @@ function renderMetaBar(meta: PublishMeta): string {
826
  : escapeHtml(aff.name);
827
  return `<li>${name}</li>`;
828
  }).join("");
829
- affContent = `<ol class="affiliations-list">${items}</ol>`;
830
  } else {
831
  const aff = meta.affiliations[0];
832
  affContent = aff.url
833
  ? `<p><a href="${escapeHtml(aff.url)}" target="_blank" rel="noopener">${escapeHtml(aff.name)}</a></p>`
834
  : `<p>${escapeHtml(aff.name)}</p>`;
835
  }
836
- cells.push(`<div class="meta-cell"><h3>Affiliations</h3>${affContent}</div>`);
837
  }
838
 
839
  // Published cell
840
  if (meta.date) {
841
- cells.push(`<div class="meta-cell"><h3>Published</h3><p>${escapeHtml(formatDate(meta.date))}</p></div>`);
842
  }
843
 
844
  // DOI cell
845
  if (meta.doi) {
846
- cells.push(`<div class="meta-cell"><h3>DOI</h3><p><a href="https://doi.org/${escapeHtml(meta.doi)}" class="doi-link" target="_blank" rel="noopener">${escapeHtml(meta.doi)}</a></p></div>`);
 
 
 
 
 
847
  }
848
 
849
- return `<header class="article-meta"><div class="meta-container">${cells.join("")}</div></header>`;
850
  }
851
 
852
  function formatDate(dateStr: string): string {
 
7
 
8
  import { generateHTML } from "@tiptap/html";
9
  import { getServerExtensions } from "./extensions.js";
10
+ import type { PublishCSS } from "./index.js";
11
 
12
  export interface PublishAuthor {
13
  name: string;
 
30
  date: string;
31
  doi?: string;
32
  ogImage?: string;
33
+ pdfUrl?: string;
34
  }
35
 
36
  /**
 
39
  export function renderArticleHTML(
40
  json: Record<string, unknown>,
41
  meta: PublishMeta,
42
+ css: PublishCSS
 
43
  ): string {
44
  const extensions = getServerExtensions();
45
 
 
85
  <!-- highlight.js theme -->
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">
90
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
91
+ mermaid.initialize({ startOnLoad: true, theme: 'neutral' });
92
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
 
94
+ <style>
95
+ /* Template foundation */
96
+ ${css.variables}
97
+ ${css.reset}
 
 
98
 
99
+ /* Published page global reset (not needed in editor where MUI handles body) */
100
+ body { font-family: var(--default-font-family); color: var(--text-color); }
101
+ html { font-size: 16px; line-height: 1.6; background-color: var(--page-bg); overflow-x: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
 
103
+ ${css.editorTokens}
104
+ ${css.base}
105
+ ${css.layout}
106
+ ${css.components}
107
+ ${css.article}
108
+ ${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);
115
  border-radius: 8px;
116
  padding: 0;
117
  margin: 1em 0;
 
121
  padding: 0.75rem 1rem;
122
  cursor: pointer;
123
  font-weight: 600;
124
+ color: var(--text-color);
125
  list-style: none;
126
  user-select: none;
127
  }
 
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;
144
+ align-items: center;
145
+ gap: 0.4em;
146
+ color: var(--accent-color, #4493f8);
147
+ text-decoration: none;
148
+ font-weight: 500;
149
+ }
150
+ a.pdf-link:hover { text-decoration: underline; }
151
+ a.pdf-link::before { content: "\\1F4C4"; }
152
+
153
  /* Image lightbox dialog */
154
  dialog.lightbox { border: none; background: transparent; padding: 0; max-width: 95vw; max-height: 95vh; }
155
  dialog.lightbox::backdrop { background: rgba(0, 0, 0, 0.85); }
156
  dialog.lightbox img { max-width: 95vw; max-height: 90vh; object-fit: contain; border-radius: 4px; }
 
 
 
 
 
 
157
  </style>
158
  </head>
159
  <body>
 
179
  </aside>
180
 
181
  <!-- Hero -->
182
+ <section class="hero">
183
  <h1>${safeTitle}</h1>
184
  ${meta.subtitle ? `<p class="hero-desc">${escapeHtml(meta.subtitle)}</p>` : ""}
185
  </section>
 
495
  }
496
  );
497
 
498
+ // Mermaid: div[data-component="mermaid"] → <pre class="mermaid">code</pre>
499
+ result = result.replace(
500
+ /<div[^>]*data-component="mermaid"[^>]*><\/div>/g,
501
+ (match) => {
502
+ const codeMatch = match.match(/data-code="([^"]*)"/);
503
+ const code = codeMatch ? codeMatch[1].replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#039;/g, "'") : "";
504
+ return `<pre class="mermaid">${escapeHtml(code)}</pre>`;
505
+ }
506
+ );
507
+
508
  // HtmlEmbed: div[data-component="htmlEmbed"] → iframe with srcdoc
509
  result = result.replace(
510
  /<div[^>]*data-component="htmlEmbed"[^>]*data-src="([^"]*)"[^>]*><\/div>/g,
 
548
  const sep = i < meta.authors.length - 1 ? ",&nbsp;" : "";
549
  return `<li>${name}${sup}${sep}</li>`;
550
  }).join("");
551
+ cells.push(`<div class="meta-container-cell"><h3>Authors</h3><ul class="authors">${items}</ul></div>`);
552
  }
553
 
554
  // Affiliations cell
 
561
  : escapeHtml(aff.name);
562
  return `<li>${name}</li>`;
563
  }).join("");
564
+ affContent = `<ol class="affiliations">${items}</ol>`;
565
  } else {
566
  const aff = meta.affiliations[0];
567
  affContent = aff.url
568
  ? `<p><a href="${escapeHtml(aff.url)}" target="_blank" rel="noopener">${escapeHtml(aff.name)}</a></p>`
569
  : `<p>${escapeHtml(aff.name)}</p>`;
570
  }
571
+ cells.push(`<div class="meta-container-cell"><h3>Affiliations</h3>${affContent}</div>`);
572
  }
573
 
574
  // Published cell
575
  if (meta.date) {
576
+ cells.push(`<div class="meta-container-cell"><h3>Published</h3><p>${escapeHtml(formatDate(meta.date))}</p></div>`);
577
  }
578
 
579
  // DOI cell
580
  if (meta.doi) {
581
+ cells.push(`<div class="meta-container-cell"><h3>DOI</h3><p><a href="https://doi.org/${escapeHtml(meta.doi)}" class="doi-link" target="_blank" rel="noopener">${escapeHtml(meta.doi)}</a></p></div>`);
582
+ }
583
+
584
+ // PDF download cell
585
+ if (meta.pdfUrl) {
586
+ cells.push(`<div class="meta-container-cell"><h3>PDF</h3><p><a href="${escapeHtml(meta.pdfUrl)}" class="pdf-link" target="_blank" rel="noopener" download>Download PDF</a></p></div>`);
587
  }
588
 
589
+ return `<header class="meta"><div class="meta-container">${cells.join("")}</div></header>`;
590
  }
591
 
592
  function formatDate(dateStr: string): string {
backend/src/publisher/index.ts CHANGED
@@ -15,6 +15,7 @@ import { isPdfEnabled, generatePdfAndThumbnail } from "./pdf-generator.js";
15
  import {
16
  isHfStorageEnabled,
17
  uploadPublishedAssets,
 
18
  } from "../hf-storage.js";
19
 
20
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -24,31 +25,60 @@ function docPath(name: string) {
24
  return join(DATA_DIR, `${name.replace(/[^a-zA-Z0-9_-]/g, "_")}.yjs`);
25
  }
26
 
 
 
 
 
 
 
 
 
 
 
 
27
  /**
28
  * Load CSS files for injection into the published HTML.
 
29
  * Tries multiple paths: dev source, Docker copy, relative fallback.
30
  */
31
- function loadCSS(): { tokens: string; article: string } {
32
  const candidates = [
33
  join(__dirname, "..", "..", "..", "frontend", "src", "styles"),
34
  join(__dirname, "..", "..", "frontend-styles"),
35
  join(__dirname, "..", "frontend-styles"),
36
  ];
37
 
 
 
38
  for (const dir of candidates) {
39
- const tokensPath = join(dir, "tokens.css");
40
- const articlePath = join(dir, "article.css");
41
- if (existsSync(tokensPath) && existsSync(articlePath)) {
42
  console.log("[publish] CSS found at:", dir);
 
 
 
 
 
 
 
 
 
43
  return {
44
- tokens: readFileSync(tokensPath, "utf-8"),
45
- article: readFileSync(articlePath, "utf-8"),
 
 
 
 
 
 
46
  };
47
  }
48
  }
49
 
50
  console.warn("[publish] CSS files not found, tried:", candidates);
51
- return { tokens: "", article: "" };
52
  }
53
 
54
  interface AuthorObj {
@@ -169,14 +199,22 @@ export async function publishDocument(docName: string, token?: string): Promise<
169
  doi: (frontmatter.doi as string) || undefined,
170
  };
171
 
 
 
 
 
 
 
 
172
  const css = loadCSS();
173
- console.log("[publish] CSS tokens length:", css.tokens.length, "article length:", css.article.length);
174
  console.log("[publish] Meta:", JSON.stringify(meta));
175
 
176
- const html = renderArticleHTML(json, meta, css.tokens, css.article);
 
177
  console.log("[publish] Generated HTML length:", html.length);
178
 
179
- // Generate PDF + thumbnail
180
  let pdf: Buffer | null = null;
181
  let thumbnail: Buffer | null = null;
182
 
@@ -187,11 +225,15 @@ export async function publishDocument(docName: string, token?: string): Promise<
187
  thumbnail = assets.thumbnail;
188
  } catch (err) {
189
  console.error("[publish] PDF generation failed:", err);
 
 
 
 
190
  }
191
  }
192
 
193
  // Try uploading to HF dataset
194
- if (isHfStorageEnabled()) {
195
  try {
196
  const urls = await uploadPublishedAssets(docName, {
197
  html,
@@ -207,17 +249,26 @@ export async function publishDocument(docName: string, token?: string): Promise<
207
 
208
  // Local storage (primary when HF disabled, fallback when HF fails)
209
  const { writeFileSync, mkdirSync } = await import("fs");
210
- const publishDir = join(DATA_DIR, "published", docName.replace(/[^a-zA-Z0-9_-]/g, "_"));
 
211
  mkdirSync(publishDir, { recursive: true });
 
 
 
 
 
 
 
 
212
  writeFileSync(join(publishDir, "index.html"), html);
213
  if (pdf) writeFileSync(join(publishDir, "article.pdf"), pdf);
214
  if (thumbnail) writeFileSync(join(publishDir, "thumb.jpg"), thumbnail);
215
  writeFileSync(join(publishDir, "meta.json"), JSON.stringify(meta, null, 2));
216
 
217
  return {
218
- htmlUrl: `/published/${docName}/index.html`,
219
- pdfUrl: pdf ? `/published/${docName}/article.pdf` : null,
220
- thumbUrl: thumbnail ? `/published/${docName}/thumb.jpg` : null,
221
  success: true,
222
  };
223
  }
 
15
  import {
16
  isHfStorageEnabled,
17
  uploadPublishedAssets,
18
+ getPublishedAssetUrl,
19
  } from "../hf-storage.js";
20
 
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
 
25
  return join(DATA_DIR, `${name.replace(/[^a-zA-Z0-9_-]/g, "_")}.yjs`);
26
  }
27
 
28
+ export interface PublishCSS {
29
+ variables: string;
30
+ reset: string;
31
+ base: string;
32
+ layout: string;
33
+ print: string;
34
+ editorTokens: string;
35
+ article: string;
36
+ components: string;
37
+ }
38
+
39
  /**
40
  * Load CSS files for injection into the published HTML.
41
+ * Reads the template CSS foundation + editor extensions.
42
  * Tries multiple paths: dev source, Docker copy, relative fallback.
43
  */
44
+ function loadCSS(): PublishCSS {
45
  const candidates = [
46
  join(__dirname, "..", "..", "..", "frontend", "src", "styles"),
47
  join(__dirname, "..", "..", "frontend-styles"),
48
  join(__dirname, "..", "frontend-styles"),
49
  ];
50
 
51
+ const readSafe = (p: string) => (existsSync(p) ? readFileSync(p, "utf-8") : "");
52
+
53
  for (const dir of candidates) {
54
+ const varsPath = join(dir, "_variables.css");
55
+ const basePath = join(dir, "_base.css");
56
+ if (existsSync(varsPath) && existsSync(basePath)) {
57
  console.log("[publish] CSS found at:", dir);
58
+
59
+ const componentFiles = [
60
+ "_code.css", "_button.css", "_table.css", "_tag.css",
61
+ "_card.css", "_form.css", "_mermaid.css", "_hero.css", "_toc.css",
62
+ ];
63
+ const componentsCss = componentFiles
64
+ .map((f) => readSafe(join(dir, "components", f)))
65
+ .join("\n");
66
+
67
  return {
68
+ variables: readFileSync(varsPath, "utf-8"),
69
+ reset: readSafe(join(dir, "_reset.css")),
70
+ base: readFileSync(basePath, "utf-8"),
71
+ layout: readSafe(join(dir, "_layout.css")),
72
+ print: readSafe(join(dir, "_print.css")),
73
+ editorTokens: readSafe(join(dir, "_editor-tokens.css")),
74
+ article: readSafe(join(dir, "article.css")),
75
+ components: componentsCss,
76
  };
77
  }
78
  }
79
 
80
  console.warn("[publish] CSS files not found, tried:", candidates);
81
+ return { variables: "", reset: "", base: "", layout: "", print: "", editorTokens: "", article: "", components: "" };
82
  }
83
 
84
  interface AuthorObj {
 
199
  doi: (frontmatter.doi as string) || undefined,
200
  };
201
 
202
+ // Pre-compute public URLs so og:image and PDF link are embedded in HTML
203
+ const useHf = isHfStorageEnabled();
204
+ if (useHf && isPdfEnabled()) {
205
+ meta.ogImage = getPublishedAssetUrl(docName, "thumb.jpg");
206
+ meta.pdfUrl = getPublishedAssetUrl(docName, "article.pdf");
207
+ }
208
+
209
  const css = loadCSS();
210
+ console.log("[publish] CSS variables length:", css.variables.length, "article length:", css.article.length);
211
  console.log("[publish] Meta:", JSON.stringify(meta));
212
 
213
+ // First pass: generate HTML with og:image + PDF link pre-injected
214
+ let html = renderArticleHTML(json, meta, css);
215
  console.log("[publish] Generated HTML length:", html.length);
216
 
217
+ // Generate PDF + thumbnail from the HTML
218
  let pdf: Buffer | null = null;
219
  let thumbnail: Buffer | null = null;
220
 
 
225
  thumbnail = assets.thumbnail;
226
  } catch (err) {
227
  console.error("[publish] PDF generation failed:", err);
228
+ // Clear URLs from meta if generation failed
229
+ if (!thumbnail) delete meta.ogImage;
230
+ if (!pdf) delete meta.pdfUrl;
231
+ html = renderArticleHTML(json, meta, css);
232
  }
233
  }
234
 
235
  // Try uploading to HF dataset
236
+ if (useHf) {
237
  try {
238
  const urls = await uploadPublishedAssets(docName, {
239
  html,
 
249
 
250
  // Local storage (primary when HF disabled, fallback when HF fails)
251
  const { writeFileSync, mkdirSync } = await import("fs");
252
+ const safeDirName = docName.replace(/[^a-zA-Z0-9_-]/g, "_");
253
+ const publishDir = join(DATA_DIR, "published", safeDirName);
254
  mkdirSync(publishDir, { recursive: true });
255
+
256
+ // For local storage, rewrite meta with local URLs
257
+ if (thumbnail) meta.ogImage = `/published/${safeDirName}/thumb.jpg`;
258
+ else delete meta.ogImage;
259
+ if (pdf) meta.pdfUrl = `/published/${safeDirName}/article.pdf`;
260
+ else delete meta.pdfUrl;
261
+ html = renderArticleHTML(json, meta, css);
262
+
263
  writeFileSync(join(publishDir, "index.html"), html);
264
  if (pdf) writeFileSync(join(publishDir, "article.pdf"), pdf);
265
  if (thumbnail) writeFileSync(join(publishDir, "thumb.jpg"), thumbnail);
266
  writeFileSync(join(publishDir, "meta.json"), JSON.stringify(meta, null, 2));
267
 
268
  return {
269
+ htmlUrl: `/published/${safeDirName}/index.html`,
270
+ pdfUrl: pdf ? `/published/${safeDirName}/article.pdf` : null,
271
+ thumbUrl: thumbnail ? `/published/${safeDirName}/thumb.jpg` : null,
272
  success: true,
273
  };
274
  }
frontend/package-lock.json CHANGED
@@ -1,11 +1,11 @@
1
  {
2
- "name": "collab-editor-frontend",
3
  "version": "0.1.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
- "name": "collab-editor-frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
11
  "@ai-sdk/react": "^3.0.160",
@@ -36,6 +36,7 @@
36
  "ai": "^6.0.158",
37
  "katex": "^0.16.45",
38
  "lowlight": "^3.2.0",
 
39
  "react": "^18.3.0",
40
  "react-dom": "^18.3.0",
41
  "tippy.js": "^6.3.7",
@@ -46,6 +47,7 @@
46
  "@types/react": "^18.3.0",
47
  "@types/react-dom": "^18.3.0",
48
  "@vitejs/plugin-react": "^4.3.0",
 
49
  "vite": "^6.0.0"
50
  }
51
  },
@@ -113,6 +115,19 @@
113
  "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1"
114
  }
115
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  "node_modules/@babel/code-frame": {
117
  "version": "7.29.0",
118
  "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
@@ -402,6 +417,142 @@
402
  "node": ">=6.9.0"
403
  }
404
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  "node_modules/@emotion/babel-plugin": {
406
  "version": "11.13.5",
407
  "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
@@ -1064,6 +1215,23 @@
1064
  }
1065
  }
1066
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1067
  "node_modules/@jridgewell/gen-mapping": {
1068
  "version": "0.3.13",
1069
  "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1116,6 +1284,15 @@
1116
  "integrity": "sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==",
1117
  "license": "MIT"
1118
  },
 
 
 
 
 
 
 
 
 
1119
  "node_modules/@mui/core-downloads-tracker": {
1120
  "version": "9.0.0",
1121
  "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.0.tgz",
@@ -2385,6 +2562,259 @@
2385
  "@babel/types": "^7.28.2"
2386
  }
2387
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2388
  "node_modules/@types/estree": {
2389
  "version": "1.0.8",
2390
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -2392,6 +2822,12 @@
2392
  "dev": true,
2393
  "license": "MIT"
2394
  },
 
 
 
 
 
 
2395
  "node_modules/@types/hast": {
2396
  "version": "3.0.4",
2397
  "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
@@ -2471,6 +2907,13 @@
2471
  "@types/react": "*"
2472
  }
2473
  },
 
 
 
 
 
 
 
2474
  "node_modules/@types/unist": {
2475
  "version": "3.0.3",
2476
  "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2483,6 +2926,16 @@
2483
  "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
2484
  "license": "MIT"
2485
  },
 
 
 
 
 
 
 
 
 
 
2486
  "node_modules/@vercel/oidc": {
2487
  "version": "3.1.0",
2488
  "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz",
@@ -2513,6 +2966,18 @@
2513
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
2514
  }
2515
  },
 
 
 
 
 
 
 
 
 
 
 
 
2516
  "node_modules/ai": {
2517
  "version": "6.0.158",
2518
  "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.158.tgz",
@@ -2630,6 +3095,35 @@
2630
  ],
2631
  "license": "CC-BY-4.0"
2632
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2633
  "node_modules/clsx": {
2634
  "version": "2.1.1",
2635
  "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -2648,12 +3142,27 @@
2648
  "node": ">= 12"
2649
  }
2650
  },
 
 
 
 
 
 
2651
  "node_modules/convert-source-map": {
2652
  "version": "1.9.0",
2653
  "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
2654
  "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
2655
  "license": "MIT"
2656
  },
 
 
 
 
 
 
 
 
 
2657
  "node_modules/cosmiconfig": {
2658
  "version": "7.1.0",
2659
  "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
@@ -2691,39 +3200,564 @@
2691
  "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
2692
  "license": "MIT"
2693
  },
2694
- "node_modules/debug": {
2695
- "version": "4.4.3",
2696
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
2697
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
2698
  "license": "MIT",
2699
- "dependencies": {
2700
- "ms": "^2.1.3"
2701
- },
2702
  "engines": {
2703
- "node": ">=6.0"
2704
- },
2705
- "peerDependenciesMeta": {
2706
- "supports-color": {
2707
- "optional": true
2708
- }
2709
  }
2710
  },
2711
- "node_modules/dequal": {
2712
- "version": "2.0.3",
2713
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
2714
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
2715
  "license": "MIT",
2716
- "engines": {
2717
- "node": ">=6"
 
 
 
2718
  }
2719
  },
2720
- "node_modules/devlop": {
2721
- "version": "1.1.0",
2722
- "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
2723
- "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
2724
  "license": "MIT",
2725
  "dependencies": {
2726
- "dequal": "^2.0.0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2727
  },
2728
  "funding": {
2729
  "type": "github",
@@ -2740,6 +3774,15 @@
2740
  "csstype": "^3.0.2"
2741
  }
2742
  },
 
 
 
 
 
 
 
 
 
2743
  "node_modules/electron-to-chromium": {
2744
  "version": "1.5.335",
2745
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
@@ -2917,6 +3960,12 @@
2917
  "node": ">=6.9.0"
2918
  }
2919
  },
 
 
 
 
 
 
2920
  "node_modules/hasown": {
2921
  "version": "2.0.2",
2922
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -2954,6 +4003,18 @@
2954
  "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
2955
  "license": "MIT"
2956
  },
 
 
 
 
 
 
 
 
 
 
 
 
2957
  "node_modules/import-fresh": {
2958
  "version": "3.3.1",
2959
  "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -2970,6 +4031,15 @@
2970
  "url": "https://github.com/sponsors/sindresorhus"
2971
  }
2972
  },
 
 
 
 
 
 
 
 
 
2973
  "node_modules/is-arrayish": {
2974
  "version": "0.2.1",
2975
  "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
@@ -3061,6 +4131,35 @@
3061
  "katex": "cli.js"
3062
  }
3063
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3064
  "node_modules/lib0": {
3065
  "version": "0.2.117",
3066
  "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
@@ -3103,6 +4202,12 @@
3103
  "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
3104
  "license": "MIT"
3105
  },
 
 
 
 
 
 
3106
  "node_modules/loose-envify": {
3107
  "version": "1.4.0",
3108
  "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -3158,12 +4263,71 @@
3158
  "markdown-it": "bin/markdown-it.mjs"
3159
  }
3160
  },
 
 
 
 
 
 
 
 
 
 
 
 
3161
  "node_modules/mdurl": {
3162
  "version": "2.0.0",
3163
  "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
3164
  "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
3165
  "license": "MIT"
3166
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3167
  "node_modules/ms": {
3168
  "version": "2.1.3",
3169
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3211,6 +4375,12 @@
3211
  "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
3212
  "license": "MIT"
3213
  },
 
 
 
 
 
 
3214
  "node_modules/parent-module": {
3215
  "version": "1.0.1",
3216
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3241,6 +4411,12 @@
3241
  "url": "https://github.com/sponsors/sindresorhus"
3242
  }
3243
  },
 
 
 
 
 
 
3244
  "node_modules/path-parse": {
3245
  "version": "1.0.7",
3246
  "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
@@ -3256,6 +4432,12 @@
3256
  "node": ">=8"
3257
  }
3258
  },
 
 
 
 
 
 
3259
  "node_modules/picocolors": {
3260
  "version": "1.1.1",
3261
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -3276,6 +4458,33 @@
3276
  "url": "https://github.com/sponsors/jonschlinkert"
3277
  }
3278
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3279
  "node_modules/postcss": {
3280
  "version": "8.5.9",
3281
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
@@ -3296,6 +4505,7 @@
3296
  }
3297
  ],
3298
  "license": "MIT",
 
3299
  "dependencies": {
3300
  "nanoid": "^3.3.11",
3301
  "picocolors": "^1.1.1",
@@ -3305,6 +4515,35 @@
3305
  "node": "^10 || ^12 || >=14"
3306
  }
3307
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3308
  "node_modules/prop-types": {
3309
  "version": "15.8.1",
3310
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -3618,6 +4857,12 @@
3618
  "node": ">=4"
3619
  }
3620
  },
 
 
 
 
 
 
3621
  "node_modules/rollup": {
3622
  "version": "4.60.1",
3623
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
@@ -3669,6 +4914,30 @@
3669
  "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
3670
  "license": "MIT"
3671
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3672
  "node_modules/scheduler": {
3673
  "version": "0.23.2",
3674
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -3750,6 +5019,15 @@
3750
  "url": "https://github.com/sponsors/sindresorhus"
3751
  }
3752
  },
 
 
 
 
 
 
 
 
 
3753
  "node_modules/tinyglobby": {
3754
  "version": "0.2.16",
3755
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
@@ -3776,12 +5054,27 @@
3776
  "@popperjs/core": "^2.9.0"
3777
  }
3778
  },
 
 
 
 
 
 
 
 
 
3779
  "node_modules/uc.micro": {
3780
  "version": "2.1.0",
3781
  "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
3782
  "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
3783
  "license": "MIT"
3784
  },
 
 
 
 
 
 
3785
  "node_modules/update-browserslist-db": {
3786
  "version": "1.2.3",
3787
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -3822,6 +5115,19 @@
3822
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
3823
  }
3824
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
3825
  "node_modules/vite": {
3826
  "version": "6.4.2",
3827
  "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
@@ -3898,6 +5204,55 @@
3898
  }
3899
  }
3900
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3901
  "node_modules/w3c-keyname": {
3902
  "version": "2.2.8",
3903
  "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
 
1
  {
2
+ "name": "research-article-template-editor-frontend",
3
  "version": "0.1.0",
4
  "lockfileVersion": 3,
5
  "requires": true,
6
  "packages": {
7
  "": {
8
+ "name": "research-article-template-editor-frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
11
  "@ai-sdk/react": "^3.0.160",
 
36
  "ai": "^6.0.158",
37
  "katex": "^0.16.45",
38
  "lowlight": "^3.2.0",
39
+ "mermaid": "^11.14.0",
40
  "react": "^18.3.0",
41
  "react-dom": "^18.3.0",
42
  "tippy.js": "^6.3.7",
 
47
  "@types/react": "^18.3.0",
48
  "@types/react-dom": "^18.3.0",
49
  "@vitejs/plugin-react": "^4.3.0",
50
+ "postcss-custom-media": "^12.0.1",
51
  "vite": "^6.0.0"
52
  }
53
  },
 
115
  "react": "^18 || ~19.0.1 || ~19.1.2 || ^19.2.1"
116
  }
117
  },
118
+ "node_modules/@antfu/install-pkg": {
119
+ "version": "1.1.0",
120
+ "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz",
121
+ "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==",
122
+ "license": "MIT",
123
+ "dependencies": {
124
+ "package-manager-detector": "^1.3.0",
125
+ "tinyexec": "^1.0.1"
126
+ },
127
+ "funding": {
128
+ "url": "https://github.com/sponsors/antfu"
129
+ }
130
+ },
131
  "node_modules/@babel/code-frame": {
132
  "version": "7.29.0",
133
  "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
 
417
  "node": ">=6.9.0"
418
  }
419
  },
420
+ "node_modules/@braintree/sanitize-url": {
421
+ "version": "7.1.2",
422
+ "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz",
423
+ "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==",
424
+ "license": "MIT"
425
+ },
426
+ "node_modules/@chevrotain/cst-dts-gen": {
427
+ "version": "12.0.0",
428
+ "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz",
429
+ "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==",
430
+ "license": "Apache-2.0",
431
+ "dependencies": {
432
+ "@chevrotain/gast": "12.0.0",
433
+ "@chevrotain/types": "12.0.0"
434
+ }
435
+ },
436
+ "node_modules/@chevrotain/gast": {
437
+ "version": "12.0.0",
438
+ "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz",
439
+ "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==",
440
+ "license": "Apache-2.0",
441
+ "dependencies": {
442
+ "@chevrotain/types": "12.0.0"
443
+ }
444
+ },
445
+ "node_modules/@chevrotain/regexp-to-ast": {
446
+ "version": "12.0.0",
447
+ "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz",
448
+ "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==",
449
+ "license": "Apache-2.0"
450
+ },
451
+ "node_modules/@chevrotain/types": {
452
+ "version": "12.0.0",
453
+ "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz",
454
+ "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==",
455
+ "license": "Apache-2.0"
456
+ },
457
+ "node_modules/@chevrotain/utils": {
458
+ "version": "12.0.0",
459
+ "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz",
460
+ "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==",
461
+ "license": "Apache-2.0"
462
+ },
463
+ "node_modules/@csstools/cascade-layer-name-parser": {
464
+ "version": "3.0.0",
465
+ "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-3.0.0.tgz",
466
+ "integrity": "sha512-/3iksyevwRfSJx5yH0RkcrcYXwuhMQx3Juqf40t97PeEy2/Mz2TItZ/z/216qpe4GgOyFBP8MKIwVvytzHmfIQ==",
467
+ "dev": true,
468
+ "funding": [
469
+ {
470
+ "type": "github",
471
+ "url": "https://github.com/sponsors/csstools"
472
+ },
473
+ {
474
+ "type": "opencollective",
475
+ "url": "https://opencollective.com/csstools"
476
+ }
477
+ ],
478
+ "license": "MIT",
479
+ "engines": {
480
+ "node": ">=20.19.0"
481
+ },
482
+ "peerDependencies": {
483
+ "@csstools/css-parser-algorithms": "^4.0.0",
484
+ "@csstools/css-tokenizer": "^4.0.0"
485
+ }
486
+ },
487
+ "node_modules/@csstools/css-parser-algorithms": {
488
+ "version": "4.0.0",
489
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
490
+ "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
491
+ "dev": true,
492
+ "funding": [
493
+ {
494
+ "type": "github",
495
+ "url": "https://github.com/sponsors/csstools"
496
+ },
497
+ {
498
+ "type": "opencollective",
499
+ "url": "https://opencollective.com/csstools"
500
+ }
501
+ ],
502
+ "license": "MIT",
503
+ "peer": true,
504
+ "engines": {
505
+ "node": ">=20.19.0"
506
+ },
507
+ "peerDependencies": {
508
+ "@csstools/css-tokenizer": "^4.0.0"
509
+ }
510
+ },
511
+ "node_modules/@csstools/css-tokenizer": {
512
+ "version": "4.0.0",
513
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
514
+ "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
515
+ "dev": true,
516
+ "funding": [
517
+ {
518
+ "type": "github",
519
+ "url": "https://github.com/sponsors/csstools"
520
+ },
521
+ {
522
+ "type": "opencollective",
523
+ "url": "https://opencollective.com/csstools"
524
+ }
525
+ ],
526
+ "license": "MIT",
527
+ "peer": true,
528
+ "engines": {
529
+ "node": ">=20.19.0"
530
+ }
531
+ },
532
+ "node_modules/@csstools/media-query-list-parser": {
533
+ "version": "5.0.0",
534
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz",
535
+ "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==",
536
+ "dev": true,
537
+ "funding": [
538
+ {
539
+ "type": "github",
540
+ "url": "https://github.com/sponsors/csstools"
541
+ },
542
+ {
543
+ "type": "opencollective",
544
+ "url": "https://opencollective.com/csstools"
545
+ }
546
+ ],
547
+ "license": "MIT",
548
+ "engines": {
549
+ "node": ">=20.19.0"
550
+ },
551
+ "peerDependencies": {
552
+ "@csstools/css-parser-algorithms": "^4.0.0",
553
+ "@csstools/css-tokenizer": "^4.0.0"
554
+ }
555
+ },
556
  "node_modules/@emotion/babel-plugin": {
557
  "version": "11.13.5",
558
  "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
 
1215
  }
1216
  }
1217
  },
1218
+ "node_modules/@iconify/types": {
1219
+ "version": "2.0.0",
1220
+ "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz",
1221
+ "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==",
1222
+ "license": "MIT"
1223
+ },
1224
+ "node_modules/@iconify/utils": {
1225
+ "version": "3.1.0",
1226
+ "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz",
1227
+ "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==",
1228
+ "license": "MIT",
1229
+ "dependencies": {
1230
+ "@antfu/install-pkg": "^1.1.0",
1231
+ "@iconify/types": "^2.0.0",
1232
+ "mlly": "^1.8.0"
1233
+ }
1234
+ },
1235
  "node_modules/@jridgewell/gen-mapping": {
1236
  "version": "0.3.13",
1237
  "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
 
1284
  "integrity": "sha512-QZqem4QuAnAyzfz+Gj5/+SLxqwCAw2qmt7732ZXodr6VDWGeYLG6w1i/vYLa55JQM9wRuBKLmXmiZ2P0LtE5rw==",
1285
  "license": "MIT"
1286
  },
1287
+ "node_modules/@mermaid-js/parser": {
1288
+ "version": "1.1.0",
1289
+ "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz",
1290
+ "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==",
1291
+ "license": "MIT",
1292
+ "dependencies": {
1293
+ "langium": "^4.0.0"
1294
+ }
1295
+ },
1296
  "node_modules/@mui/core-downloads-tracker": {
1297
  "version": "9.0.0",
1298
  "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.0.0.tgz",
 
2562
  "@babel/types": "^7.28.2"
2563
  }
2564
  },
2565
+ "node_modules/@types/d3": {
2566
+ "version": "7.4.3",
2567
+ "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
2568
+ "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
2569
+ "license": "MIT",
2570
+ "dependencies": {
2571
+ "@types/d3-array": "*",
2572
+ "@types/d3-axis": "*",
2573
+ "@types/d3-brush": "*",
2574
+ "@types/d3-chord": "*",
2575
+ "@types/d3-color": "*",
2576
+ "@types/d3-contour": "*",
2577
+ "@types/d3-delaunay": "*",
2578
+ "@types/d3-dispatch": "*",
2579
+ "@types/d3-drag": "*",
2580
+ "@types/d3-dsv": "*",
2581
+ "@types/d3-ease": "*",
2582
+ "@types/d3-fetch": "*",
2583
+ "@types/d3-force": "*",
2584
+ "@types/d3-format": "*",
2585
+ "@types/d3-geo": "*",
2586
+ "@types/d3-hierarchy": "*",
2587
+ "@types/d3-interpolate": "*",
2588
+ "@types/d3-path": "*",
2589
+ "@types/d3-polygon": "*",
2590
+ "@types/d3-quadtree": "*",
2591
+ "@types/d3-random": "*",
2592
+ "@types/d3-scale": "*",
2593
+ "@types/d3-scale-chromatic": "*",
2594
+ "@types/d3-selection": "*",
2595
+ "@types/d3-shape": "*",
2596
+ "@types/d3-time": "*",
2597
+ "@types/d3-time-format": "*",
2598
+ "@types/d3-timer": "*",
2599
+ "@types/d3-transition": "*",
2600
+ "@types/d3-zoom": "*"
2601
+ }
2602
+ },
2603
+ "node_modules/@types/d3-array": {
2604
+ "version": "3.2.2",
2605
+ "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
2606
+ "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
2607
+ "license": "MIT"
2608
+ },
2609
+ "node_modules/@types/d3-axis": {
2610
+ "version": "3.0.6",
2611
+ "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
2612
+ "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
2613
+ "license": "MIT",
2614
+ "dependencies": {
2615
+ "@types/d3-selection": "*"
2616
+ }
2617
+ },
2618
+ "node_modules/@types/d3-brush": {
2619
+ "version": "3.0.6",
2620
+ "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
2621
+ "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
2622
+ "license": "MIT",
2623
+ "dependencies": {
2624
+ "@types/d3-selection": "*"
2625
+ }
2626
+ },
2627
+ "node_modules/@types/d3-chord": {
2628
+ "version": "3.0.6",
2629
+ "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
2630
+ "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
2631
+ "license": "MIT"
2632
+ },
2633
+ "node_modules/@types/d3-color": {
2634
+ "version": "3.1.3",
2635
+ "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
2636
+ "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
2637
+ "license": "MIT"
2638
+ },
2639
+ "node_modules/@types/d3-contour": {
2640
+ "version": "3.0.6",
2641
+ "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
2642
+ "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
2643
+ "license": "MIT",
2644
+ "dependencies": {
2645
+ "@types/d3-array": "*",
2646
+ "@types/geojson": "*"
2647
+ }
2648
+ },
2649
+ "node_modules/@types/d3-delaunay": {
2650
+ "version": "6.0.4",
2651
+ "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
2652
+ "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
2653
+ "license": "MIT"
2654
+ },
2655
+ "node_modules/@types/d3-dispatch": {
2656
+ "version": "3.0.7",
2657
+ "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
2658
+ "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
2659
+ "license": "MIT"
2660
+ },
2661
+ "node_modules/@types/d3-drag": {
2662
+ "version": "3.0.7",
2663
+ "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
2664
+ "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
2665
+ "license": "MIT",
2666
+ "dependencies": {
2667
+ "@types/d3-selection": "*"
2668
+ }
2669
+ },
2670
+ "node_modules/@types/d3-dsv": {
2671
+ "version": "3.0.7",
2672
+ "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
2673
+ "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
2674
+ "license": "MIT"
2675
+ },
2676
+ "node_modules/@types/d3-ease": {
2677
+ "version": "3.0.2",
2678
+ "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
2679
+ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
2680
+ "license": "MIT"
2681
+ },
2682
+ "node_modules/@types/d3-fetch": {
2683
+ "version": "3.0.7",
2684
+ "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
2685
+ "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
2686
+ "license": "MIT",
2687
+ "dependencies": {
2688
+ "@types/d3-dsv": "*"
2689
+ }
2690
+ },
2691
+ "node_modules/@types/d3-force": {
2692
+ "version": "3.0.10",
2693
+ "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
2694
+ "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
2695
+ "license": "MIT"
2696
+ },
2697
+ "node_modules/@types/d3-format": {
2698
+ "version": "3.0.4",
2699
+ "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
2700
+ "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
2701
+ "license": "MIT"
2702
+ },
2703
+ "node_modules/@types/d3-geo": {
2704
+ "version": "3.1.0",
2705
+ "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
2706
+ "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
2707
+ "license": "MIT",
2708
+ "dependencies": {
2709
+ "@types/geojson": "*"
2710
+ }
2711
+ },
2712
+ "node_modules/@types/d3-hierarchy": {
2713
+ "version": "3.1.7",
2714
+ "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
2715
+ "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
2716
+ "license": "MIT"
2717
+ },
2718
+ "node_modules/@types/d3-interpolate": {
2719
+ "version": "3.0.4",
2720
+ "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
2721
+ "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
2722
+ "license": "MIT",
2723
+ "dependencies": {
2724
+ "@types/d3-color": "*"
2725
+ }
2726
+ },
2727
+ "node_modules/@types/d3-path": {
2728
+ "version": "3.1.1",
2729
+ "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
2730
+ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
2731
+ "license": "MIT"
2732
+ },
2733
+ "node_modules/@types/d3-polygon": {
2734
+ "version": "3.0.2",
2735
+ "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
2736
+ "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
2737
+ "license": "MIT"
2738
+ },
2739
+ "node_modules/@types/d3-quadtree": {
2740
+ "version": "3.0.6",
2741
+ "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
2742
+ "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
2743
+ "license": "MIT"
2744
+ },
2745
+ "node_modules/@types/d3-random": {
2746
+ "version": "3.0.3",
2747
+ "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
2748
+ "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
2749
+ "license": "MIT"
2750
+ },
2751
+ "node_modules/@types/d3-scale": {
2752
+ "version": "4.0.9",
2753
+ "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
2754
+ "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
2755
+ "license": "MIT",
2756
+ "dependencies": {
2757
+ "@types/d3-time": "*"
2758
+ }
2759
+ },
2760
+ "node_modules/@types/d3-scale-chromatic": {
2761
+ "version": "3.1.0",
2762
+ "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
2763
+ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
2764
+ "license": "MIT"
2765
+ },
2766
+ "node_modules/@types/d3-selection": {
2767
+ "version": "3.0.11",
2768
+ "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
2769
+ "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
2770
+ "license": "MIT"
2771
+ },
2772
+ "node_modules/@types/d3-shape": {
2773
+ "version": "3.1.8",
2774
+ "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz",
2775
+ "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
2776
+ "license": "MIT",
2777
+ "dependencies": {
2778
+ "@types/d3-path": "*"
2779
+ }
2780
+ },
2781
+ "node_modules/@types/d3-time": {
2782
+ "version": "3.0.4",
2783
+ "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
2784
+ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
2785
+ "license": "MIT"
2786
+ },
2787
+ "node_modules/@types/d3-time-format": {
2788
+ "version": "4.0.3",
2789
+ "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
2790
+ "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
2791
+ "license": "MIT"
2792
+ },
2793
+ "node_modules/@types/d3-timer": {
2794
+ "version": "3.0.2",
2795
+ "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
2796
+ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
2797
+ "license": "MIT"
2798
+ },
2799
+ "node_modules/@types/d3-transition": {
2800
+ "version": "3.0.9",
2801
+ "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
2802
+ "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
2803
+ "license": "MIT",
2804
+ "dependencies": {
2805
+ "@types/d3-selection": "*"
2806
+ }
2807
+ },
2808
+ "node_modules/@types/d3-zoom": {
2809
+ "version": "3.0.8",
2810
+ "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
2811
+ "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
2812
+ "license": "MIT",
2813
+ "dependencies": {
2814
+ "@types/d3-interpolate": "*",
2815
+ "@types/d3-selection": "*"
2816
+ }
2817
+ },
2818
  "node_modules/@types/estree": {
2819
  "version": "1.0.8",
2820
  "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
 
2822
  "dev": true,
2823
  "license": "MIT"
2824
  },
2825
+ "node_modules/@types/geojson": {
2826
+ "version": "7946.0.16",
2827
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
2828
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
2829
+ "license": "MIT"
2830
+ },
2831
  "node_modules/@types/hast": {
2832
  "version": "3.0.4",
2833
  "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
 
2907
  "@types/react": "*"
2908
  }
2909
  },
2910
+ "node_modules/@types/trusted-types": {
2911
+ "version": "2.0.7",
2912
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
2913
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
2914
+ "license": "MIT",
2915
+ "optional": true
2916
+ },
2917
  "node_modules/@types/unist": {
2918
  "version": "3.0.3",
2919
  "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
 
2926
  "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
2927
  "license": "MIT"
2928
  },
2929
+ "node_modules/@upsetjs/venn.js": {
2930
+ "version": "2.0.0",
2931
+ "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz",
2932
+ "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==",
2933
+ "license": "MIT",
2934
+ "optionalDependencies": {
2935
+ "d3-selection": "^3.0.0",
2936
+ "d3-transition": "^3.0.1"
2937
+ }
2938
+ },
2939
  "node_modules/@vercel/oidc": {
2940
  "version": "3.1.0",
2941
  "resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz",
 
2966
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
2967
  }
2968
  },
2969
+ "node_modules/acorn": {
2970
+ "version": "8.16.0",
2971
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
2972
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
2973
+ "license": "MIT",
2974
+ "bin": {
2975
+ "acorn": "bin/acorn"
2976
+ },
2977
+ "engines": {
2978
+ "node": ">=0.4.0"
2979
+ }
2980
+ },
2981
  "node_modules/ai": {
2982
  "version": "6.0.158",
2983
  "resolved": "https://registry.npmjs.org/ai/-/ai-6.0.158.tgz",
 
3095
  ],
3096
  "license": "CC-BY-4.0"
3097
  },
3098
+ "node_modules/chevrotain": {
3099
+ "version": "12.0.0",
3100
+ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz",
3101
+ "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==",
3102
+ "license": "Apache-2.0",
3103
+ "peer": true,
3104
+ "dependencies": {
3105
+ "@chevrotain/cst-dts-gen": "12.0.0",
3106
+ "@chevrotain/gast": "12.0.0",
3107
+ "@chevrotain/regexp-to-ast": "12.0.0",
3108
+ "@chevrotain/types": "12.0.0",
3109
+ "@chevrotain/utils": "12.0.0"
3110
+ },
3111
+ "engines": {
3112
+ "node": ">=22.0.0"
3113
+ }
3114
+ },
3115
+ "node_modules/chevrotain-allstar": {
3116
+ "version": "0.4.1",
3117
+ "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz",
3118
+ "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==",
3119
+ "license": "MIT",
3120
+ "dependencies": {
3121
+ "lodash-es": "^4.17.21"
3122
+ },
3123
+ "peerDependencies": {
3124
+ "chevrotain": "^12.0.0"
3125
+ }
3126
+ },
3127
  "node_modules/clsx": {
3128
  "version": "2.1.1",
3129
  "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
 
3142
  "node": ">= 12"
3143
  }
3144
  },
3145
+ "node_modules/confbox": {
3146
+ "version": "0.1.8",
3147
+ "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
3148
+ "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==",
3149
+ "license": "MIT"
3150
+ },
3151
  "node_modules/convert-source-map": {
3152
  "version": "1.9.0",
3153
  "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
3154
  "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
3155
  "license": "MIT"
3156
  },
3157
+ "node_modules/cose-base": {
3158
+ "version": "1.0.3",
3159
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
3160
+ "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
3161
+ "license": "MIT",
3162
+ "dependencies": {
3163
+ "layout-base": "^1.0.0"
3164
+ }
3165
+ },
3166
  "node_modules/cosmiconfig": {
3167
  "version": "7.1.0",
3168
  "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
 
3200
  "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
3201
  "license": "MIT"
3202
  },
3203
+ "node_modules/cytoscape": {
3204
+ "version": "3.33.2",
3205
+ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz",
3206
+ "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==",
3207
  "license": "MIT",
3208
+ "peer": true,
 
 
3209
  "engines": {
3210
+ "node": ">=0.10"
 
 
 
 
 
3211
  }
3212
  },
3213
+ "node_modules/cytoscape-cose-bilkent": {
3214
+ "version": "4.1.0",
3215
+ "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
3216
+ "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
3217
  "license": "MIT",
3218
+ "dependencies": {
3219
+ "cose-base": "^1.0.0"
3220
+ },
3221
+ "peerDependencies": {
3222
+ "cytoscape": "^3.2.0"
3223
  }
3224
  },
3225
+ "node_modules/cytoscape-fcose": {
3226
+ "version": "2.2.0",
3227
+ "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
3228
+ "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
3229
  "license": "MIT",
3230
  "dependencies": {
3231
+ "cose-base": "^2.2.0"
3232
+ },
3233
+ "peerDependencies": {
3234
+ "cytoscape": "^3.2.0"
3235
+ }
3236
+ },
3237
+ "node_modules/cytoscape-fcose/node_modules/cose-base": {
3238
+ "version": "2.2.0",
3239
+ "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
3240
+ "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
3241
+ "license": "MIT",
3242
+ "dependencies": {
3243
+ "layout-base": "^2.0.0"
3244
+ }
3245
+ },
3246
+ "node_modules/cytoscape-fcose/node_modules/layout-base": {
3247
+ "version": "2.0.1",
3248
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
3249
+ "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
3250
+ "license": "MIT"
3251
+ },
3252
+ "node_modules/d3": {
3253
+ "version": "7.9.0",
3254
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
3255
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
3256
+ "license": "ISC",
3257
+ "dependencies": {
3258
+ "d3-array": "3",
3259
+ "d3-axis": "3",
3260
+ "d3-brush": "3",
3261
+ "d3-chord": "3",
3262
+ "d3-color": "3",
3263
+ "d3-contour": "4",
3264
+ "d3-delaunay": "6",
3265
+ "d3-dispatch": "3",
3266
+ "d3-drag": "3",
3267
+ "d3-dsv": "3",
3268
+ "d3-ease": "3",
3269
+ "d3-fetch": "3",
3270
+ "d3-force": "3",
3271
+ "d3-format": "3",
3272
+ "d3-geo": "3",
3273
+ "d3-hierarchy": "3",
3274
+ "d3-interpolate": "3",
3275
+ "d3-path": "3",
3276
+ "d3-polygon": "3",
3277
+ "d3-quadtree": "3",
3278
+ "d3-random": "3",
3279
+ "d3-scale": "4",
3280
+ "d3-scale-chromatic": "3",
3281
+ "d3-selection": "3",
3282
+ "d3-shape": "3",
3283
+ "d3-time": "3",
3284
+ "d3-time-format": "4",
3285
+ "d3-timer": "3",
3286
+ "d3-transition": "3",
3287
+ "d3-zoom": "3"
3288
+ },
3289
+ "engines": {
3290
+ "node": ">=12"
3291
+ }
3292
+ },
3293
+ "node_modules/d3-array": {
3294
+ "version": "3.2.4",
3295
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
3296
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
3297
+ "license": "ISC",
3298
+ "dependencies": {
3299
+ "internmap": "1 - 2"
3300
+ },
3301
+ "engines": {
3302
+ "node": ">=12"
3303
+ }
3304
+ },
3305
+ "node_modules/d3-axis": {
3306
+ "version": "3.0.0",
3307
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
3308
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
3309
+ "license": "ISC",
3310
+ "engines": {
3311
+ "node": ">=12"
3312
+ }
3313
+ },
3314
+ "node_modules/d3-brush": {
3315
+ "version": "3.0.0",
3316
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
3317
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
3318
+ "license": "ISC",
3319
+ "dependencies": {
3320
+ "d3-dispatch": "1 - 3",
3321
+ "d3-drag": "2 - 3",
3322
+ "d3-interpolate": "1 - 3",
3323
+ "d3-selection": "3",
3324
+ "d3-transition": "3"
3325
+ },
3326
+ "engines": {
3327
+ "node": ">=12"
3328
+ }
3329
+ },
3330
+ "node_modules/d3-chord": {
3331
+ "version": "3.0.1",
3332
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
3333
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
3334
+ "license": "ISC",
3335
+ "dependencies": {
3336
+ "d3-path": "1 - 3"
3337
+ },
3338
+ "engines": {
3339
+ "node": ">=12"
3340
+ }
3341
+ },
3342
+ "node_modules/d3-color": {
3343
+ "version": "3.1.0",
3344
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
3345
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
3346
+ "license": "ISC",
3347
+ "engines": {
3348
+ "node": ">=12"
3349
+ }
3350
+ },
3351
+ "node_modules/d3-contour": {
3352
+ "version": "4.0.2",
3353
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
3354
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
3355
+ "license": "ISC",
3356
+ "dependencies": {
3357
+ "d3-array": "^3.2.0"
3358
+ },
3359
+ "engines": {
3360
+ "node": ">=12"
3361
+ }
3362
+ },
3363
+ "node_modules/d3-delaunay": {
3364
+ "version": "6.0.4",
3365
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
3366
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
3367
+ "license": "ISC",
3368
+ "dependencies": {
3369
+ "delaunator": "5"
3370
+ },
3371
+ "engines": {
3372
+ "node": ">=12"
3373
+ }
3374
+ },
3375
+ "node_modules/d3-dispatch": {
3376
+ "version": "3.0.1",
3377
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
3378
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
3379
+ "license": "ISC",
3380
+ "engines": {
3381
+ "node": ">=12"
3382
+ }
3383
+ },
3384
+ "node_modules/d3-drag": {
3385
+ "version": "3.0.0",
3386
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
3387
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
3388
+ "license": "ISC",
3389
+ "dependencies": {
3390
+ "d3-dispatch": "1 - 3",
3391
+ "d3-selection": "3"
3392
+ },
3393
+ "engines": {
3394
+ "node": ">=12"
3395
+ }
3396
+ },
3397
+ "node_modules/d3-dsv": {
3398
+ "version": "3.0.1",
3399
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
3400
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
3401
+ "license": "ISC",
3402
+ "dependencies": {
3403
+ "commander": "7",
3404
+ "iconv-lite": "0.6",
3405
+ "rw": "1"
3406
+ },
3407
+ "bin": {
3408
+ "csv2json": "bin/dsv2json.js",
3409
+ "csv2tsv": "bin/dsv2dsv.js",
3410
+ "dsv2dsv": "bin/dsv2dsv.js",
3411
+ "dsv2json": "bin/dsv2json.js",
3412
+ "json2csv": "bin/json2dsv.js",
3413
+ "json2dsv": "bin/json2dsv.js",
3414
+ "json2tsv": "bin/json2dsv.js",
3415
+ "tsv2csv": "bin/dsv2dsv.js",
3416
+ "tsv2json": "bin/dsv2json.js"
3417
+ },
3418
+ "engines": {
3419
+ "node": ">=12"
3420
+ }
3421
+ },
3422
+ "node_modules/d3-dsv/node_modules/commander": {
3423
+ "version": "7.2.0",
3424
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
3425
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
3426
+ "license": "MIT",
3427
+ "engines": {
3428
+ "node": ">= 10"
3429
+ }
3430
+ },
3431
+ "node_modules/d3-ease": {
3432
+ "version": "3.0.1",
3433
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
3434
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
3435
+ "license": "BSD-3-Clause",
3436
+ "engines": {
3437
+ "node": ">=12"
3438
+ }
3439
+ },
3440
+ "node_modules/d3-fetch": {
3441
+ "version": "3.0.1",
3442
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
3443
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
3444
+ "license": "ISC",
3445
+ "dependencies": {
3446
+ "d3-dsv": "1 - 3"
3447
+ },
3448
+ "engines": {
3449
+ "node": ">=12"
3450
+ }
3451
+ },
3452
+ "node_modules/d3-force": {
3453
+ "version": "3.0.0",
3454
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
3455
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
3456
+ "license": "ISC",
3457
+ "dependencies": {
3458
+ "d3-dispatch": "1 - 3",
3459
+ "d3-quadtree": "1 - 3",
3460
+ "d3-timer": "1 - 3"
3461
+ },
3462
+ "engines": {
3463
+ "node": ">=12"
3464
+ }
3465
+ },
3466
+ "node_modules/d3-format": {
3467
+ "version": "3.1.2",
3468
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
3469
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
3470
+ "license": "ISC",
3471
+ "engines": {
3472
+ "node": ">=12"
3473
+ }
3474
+ },
3475
+ "node_modules/d3-geo": {
3476
+ "version": "3.1.1",
3477
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
3478
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
3479
+ "license": "ISC",
3480
+ "dependencies": {
3481
+ "d3-array": "2.5.0 - 3"
3482
+ },
3483
+ "engines": {
3484
+ "node": ">=12"
3485
+ }
3486
+ },
3487
+ "node_modules/d3-hierarchy": {
3488
+ "version": "3.1.2",
3489
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
3490
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
3491
+ "license": "ISC",
3492
+ "engines": {
3493
+ "node": ">=12"
3494
+ }
3495
+ },
3496
+ "node_modules/d3-interpolate": {
3497
+ "version": "3.0.1",
3498
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
3499
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
3500
+ "license": "ISC",
3501
+ "dependencies": {
3502
+ "d3-color": "1 - 3"
3503
+ },
3504
+ "engines": {
3505
+ "node": ">=12"
3506
+ }
3507
+ },
3508
+ "node_modules/d3-path": {
3509
+ "version": "3.1.0",
3510
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
3511
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
3512
+ "license": "ISC",
3513
+ "engines": {
3514
+ "node": ">=12"
3515
+ }
3516
+ },
3517
+ "node_modules/d3-polygon": {
3518
+ "version": "3.0.1",
3519
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
3520
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
3521
+ "license": "ISC",
3522
+ "engines": {
3523
+ "node": ">=12"
3524
+ }
3525
+ },
3526
+ "node_modules/d3-quadtree": {
3527
+ "version": "3.0.1",
3528
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
3529
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
3530
+ "license": "ISC",
3531
+ "engines": {
3532
+ "node": ">=12"
3533
+ }
3534
+ },
3535
+ "node_modules/d3-random": {
3536
+ "version": "3.0.1",
3537
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
3538
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
3539
+ "license": "ISC",
3540
+ "engines": {
3541
+ "node": ">=12"
3542
+ }
3543
+ },
3544
+ "node_modules/d3-sankey": {
3545
+ "version": "0.12.3",
3546
+ "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz",
3547
+ "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==",
3548
+ "license": "BSD-3-Clause",
3549
+ "dependencies": {
3550
+ "d3-array": "1 - 2",
3551
+ "d3-shape": "^1.2.0"
3552
+ }
3553
+ },
3554
+ "node_modules/d3-sankey/node_modules/d3-array": {
3555
+ "version": "2.12.1",
3556
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz",
3557
+ "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==",
3558
+ "license": "BSD-3-Clause",
3559
+ "dependencies": {
3560
+ "internmap": "^1.0.0"
3561
+ }
3562
+ },
3563
+ "node_modules/d3-sankey/node_modules/d3-path": {
3564
+ "version": "1.0.9",
3565
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
3566
+ "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
3567
+ "license": "BSD-3-Clause"
3568
+ },
3569
+ "node_modules/d3-sankey/node_modules/d3-shape": {
3570
+ "version": "1.3.7",
3571
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
3572
+ "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
3573
+ "license": "BSD-3-Clause",
3574
+ "dependencies": {
3575
+ "d3-path": "1"
3576
+ }
3577
+ },
3578
+ "node_modules/d3-sankey/node_modules/internmap": {
3579
+ "version": "1.0.1",
3580
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz",
3581
+ "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==",
3582
+ "license": "ISC"
3583
+ },
3584
+ "node_modules/d3-scale": {
3585
+ "version": "4.0.2",
3586
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
3587
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
3588
+ "license": "ISC",
3589
+ "dependencies": {
3590
+ "d3-array": "2.10.0 - 3",
3591
+ "d3-format": "1 - 3",
3592
+ "d3-interpolate": "1.2.0 - 3",
3593
+ "d3-time": "2.1.1 - 3",
3594
+ "d3-time-format": "2 - 4"
3595
+ },
3596
+ "engines": {
3597
+ "node": ">=12"
3598
+ }
3599
+ },
3600
+ "node_modules/d3-scale-chromatic": {
3601
+ "version": "3.1.0",
3602
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
3603
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
3604
+ "license": "ISC",
3605
+ "dependencies": {
3606
+ "d3-color": "1 - 3",
3607
+ "d3-interpolate": "1 - 3"
3608
+ },
3609
+ "engines": {
3610
+ "node": ">=12"
3611
+ }
3612
+ },
3613
+ "node_modules/d3-selection": {
3614
+ "version": "3.0.0",
3615
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
3616
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
3617
+ "license": "ISC",
3618
+ "peer": true,
3619
+ "engines": {
3620
+ "node": ">=12"
3621
+ }
3622
+ },
3623
+ "node_modules/d3-shape": {
3624
+ "version": "3.2.0",
3625
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
3626
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
3627
+ "license": "ISC",
3628
+ "dependencies": {
3629
+ "d3-path": "^3.1.0"
3630
+ },
3631
+ "engines": {
3632
+ "node": ">=12"
3633
+ }
3634
+ },
3635
+ "node_modules/d3-time": {
3636
+ "version": "3.1.0",
3637
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
3638
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
3639
+ "license": "ISC",
3640
+ "dependencies": {
3641
+ "d3-array": "2 - 3"
3642
+ },
3643
+ "engines": {
3644
+ "node": ">=12"
3645
+ }
3646
+ },
3647
+ "node_modules/d3-time-format": {
3648
+ "version": "4.1.0",
3649
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
3650
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
3651
+ "license": "ISC",
3652
+ "dependencies": {
3653
+ "d3-time": "1 - 3"
3654
+ },
3655
+ "engines": {
3656
+ "node": ">=12"
3657
+ }
3658
+ },
3659
+ "node_modules/d3-timer": {
3660
+ "version": "3.0.1",
3661
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
3662
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
3663
+ "license": "ISC",
3664
+ "engines": {
3665
+ "node": ">=12"
3666
+ }
3667
+ },
3668
+ "node_modules/d3-transition": {
3669
+ "version": "3.0.1",
3670
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
3671
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
3672
+ "license": "ISC",
3673
+ "dependencies": {
3674
+ "d3-color": "1 - 3",
3675
+ "d3-dispatch": "1 - 3",
3676
+ "d3-ease": "1 - 3",
3677
+ "d3-interpolate": "1 - 3",
3678
+ "d3-timer": "1 - 3"
3679
+ },
3680
+ "engines": {
3681
+ "node": ">=12"
3682
+ },
3683
+ "peerDependencies": {
3684
+ "d3-selection": "2 - 3"
3685
+ }
3686
+ },
3687
+ "node_modules/d3-zoom": {
3688
+ "version": "3.0.0",
3689
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
3690
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
3691
+ "license": "ISC",
3692
+ "dependencies": {
3693
+ "d3-dispatch": "1 - 3",
3694
+ "d3-drag": "2 - 3",
3695
+ "d3-interpolate": "1 - 3",
3696
+ "d3-selection": "2 - 3",
3697
+ "d3-transition": "2 - 3"
3698
+ },
3699
+ "engines": {
3700
+ "node": ">=12"
3701
+ }
3702
+ },
3703
+ "node_modules/dagre-d3-es": {
3704
+ "version": "7.0.14",
3705
+ "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz",
3706
+ "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==",
3707
+ "license": "MIT",
3708
+ "dependencies": {
3709
+ "d3": "^7.9.0",
3710
+ "lodash-es": "^4.17.21"
3711
+ }
3712
+ },
3713
+ "node_modules/dayjs": {
3714
+ "version": "1.11.20",
3715
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz",
3716
+ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
3717
+ "license": "MIT"
3718
+ },
3719
+ "node_modules/debug": {
3720
+ "version": "4.4.3",
3721
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
3722
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
3723
+ "license": "MIT",
3724
+ "dependencies": {
3725
+ "ms": "^2.1.3"
3726
+ },
3727
+ "engines": {
3728
+ "node": ">=6.0"
3729
+ },
3730
+ "peerDependenciesMeta": {
3731
+ "supports-color": {
3732
+ "optional": true
3733
+ }
3734
+ }
3735
+ },
3736
+ "node_modules/delaunator": {
3737
+ "version": "5.1.0",
3738
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
3739
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
3740
+ "license": "ISC",
3741
+ "dependencies": {
3742
+ "robust-predicates": "^3.0.2"
3743
+ }
3744
+ },
3745
+ "node_modules/dequal": {
3746
+ "version": "2.0.3",
3747
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
3748
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
3749
+ "license": "MIT",
3750
+ "engines": {
3751
+ "node": ">=6"
3752
+ }
3753
+ },
3754
+ "node_modules/devlop": {
3755
+ "version": "1.1.0",
3756
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
3757
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
3758
+ "license": "MIT",
3759
+ "dependencies": {
3760
+ "dequal": "^2.0.0"
3761
  },
3762
  "funding": {
3763
  "type": "github",
 
3774
  "csstype": "^3.0.2"
3775
  }
3776
  },
3777
+ "node_modules/dompurify": {
3778
+ "version": "3.3.3",
3779
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz",
3780
+ "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==",
3781
+ "license": "(MPL-2.0 OR Apache-2.0)",
3782
+ "optionalDependencies": {
3783
+ "@types/trusted-types": "^2.0.7"
3784
+ }
3785
+ },
3786
  "node_modules/electron-to-chromium": {
3787
  "version": "1.5.335",
3788
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz",
 
3960
  "node": ">=6.9.0"
3961
  }
3962
  },
3963
+ "node_modules/hachure-fill": {
3964
+ "version": "0.5.2",
3965
+ "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz",
3966
+ "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==",
3967
+ "license": "MIT"
3968
+ },
3969
  "node_modules/hasown": {
3970
  "version": "2.0.2",
3971
  "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
 
4003
  "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
4004
  "license": "MIT"
4005
  },
4006
+ "node_modules/iconv-lite": {
4007
+ "version": "0.6.3",
4008
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
4009
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
4010
+ "license": "MIT",
4011
+ "dependencies": {
4012
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
4013
+ },
4014
+ "engines": {
4015
+ "node": ">=0.10.0"
4016
+ }
4017
+ },
4018
  "node_modules/import-fresh": {
4019
  "version": "3.3.1",
4020
  "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
 
4031
  "url": "https://github.com/sponsors/sindresorhus"
4032
  }
4033
  },
4034
+ "node_modules/internmap": {
4035
+ "version": "2.0.3",
4036
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
4037
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
4038
+ "license": "ISC",
4039
+ "engines": {
4040
+ "node": ">=12"
4041
+ }
4042
+ },
4043
  "node_modules/is-arrayish": {
4044
  "version": "0.2.1",
4045
  "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
 
4131
  "katex": "cli.js"
4132
  }
4133
  },
4134
+ "node_modules/khroma": {
4135
+ "version": "2.1.0",
4136
+ "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz",
4137
+ "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="
4138
+ },
4139
+ "node_modules/langium": {
4140
+ "version": "4.2.2",
4141
+ "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.2.tgz",
4142
+ "integrity": "sha512-JUshTRAfHI4/MF9dH2WupvjSXyn8JBuUEWazB8ZVJUtXutT0doDlAv1XKbZ1Pb5sMexa8FF4CFBc0iiul7gbUQ==",
4143
+ "license": "MIT",
4144
+ "dependencies": {
4145
+ "@chevrotain/regexp-to-ast": "~12.0.0",
4146
+ "chevrotain": "~12.0.0",
4147
+ "chevrotain-allstar": "~0.4.1",
4148
+ "vscode-languageserver": "~9.0.1",
4149
+ "vscode-languageserver-textdocument": "~1.0.11",
4150
+ "vscode-uri": "~3.1.0"
4151
+ },
4152
+ "engines": {
4153
+ "node": ">=20.10.0",
4154
+ "npm": ">=10.2.3"
4155
+ }
4156
+ },
4157
+ "node_modules/layout-base": {
4158
+ "version": "1.0.2",
4159
+ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
4160
+ "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
4161
+ "license": "MIT"
4162
+ },
4163
  "node_modules/lib0": {
4164
  "version": "0.2.117",
4165
  "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.117.tgz",
 
4202
  "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
4203
  "license": "MIT"
4204
  },
4205
+ "node_modules/lodash-es": {
4206
+ "version": "4.18.1",
4207
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
4208
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
4209
+ "license": "MIT"
4210
+ },
4211
  "node_modules/loose-envify": {
4212
  "version": "1.4.0",
4213
  "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
 
4263
  "markdown-it": "bin/markdown-it.mjs"
4264
  }
4265
  },
4266
+ "node_modules/marked": {
4267
+ "version": "16.4.2",
4268
+ "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz",
4269
+ "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==",
4270
+ "license": "MIT",
4271
+ "bin": {
4272
+ "marked": "bin/marked.js"
4273
+ },
4274
+ "engines": {
4275
+ "node": ">= 20"
4276
+ }
4277
+ },
4278
  "node_modules/mdurl": {
4279
  "version": "2.0.0",
4280
  "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
4281
  "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
4282
  "license": "MIT"
4283
  },
4284
+ "node_modules/mermaid": {
4285
+ "version": "11.14.0",
4286
+ "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz",
4287
+ "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==",
4288
+ "license": "MIT",
4289
+ "dependencies": {
4290
+ "@braintree/sanitize-url": "^7.1.1",
4291
+ "@iconify/utils": "^3.0.2",
4292
+ "@mermaid-js/parser": "^1.1.0",
4293
+ "@types/d3": "^7.4.3",
4294
+ "@upsetjs/venn.js": "^2.0.0",
4295
+ "cytoscape": "^3.33.1",
4296
+ "cytoscape-cose-bilkent": "^4.1.0",
4297
+ "cytoscape-fcose": "^2.2.0",
4298
+ "d3": "^7.9.0",
4299
+ "d3-sankey": "^0.12.3",
4300
+ "dagre-d3-es": "7.0.14",
4301
+ "dayjs": "^1.11.19",
4302
+ "dompurify": "^3.3.1",
4303
+ "katex": "^0.16.25",
4304
+ "khroma": "^2.1.0",
4305
+ "lodash-es": "^4.17.23",
4306
+ "marked": "^16.3.0",
4307
+ "roughjs": "^4.6.6",
4308
+ "stylis": "^4.3.6",
4309
+ "ts-dedent": "^2.2.0",
4310
+ "uuid": "^11.1.0"
4311
+ }
4312
+ },
4313
+ "node_modules/mermaid/node_modules/stylis": {
4314
+ "version": "4.3.6",
4315
+ "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
4316
+ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
4317
+ "license": "MIT"
4318
+ },
4319
+ "node_modules/mlly": {
4320
+ "version": "1.8.2",
4321
+ "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
4322
+ "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==",
4323
+ "license": "MIT",
4324
+ "dependencies": {
4325
+ "acorn": "^8.16.0",
4326
+ "pathe": "^2.0.3",
4327
+ "pkg-types": "^1.3.1",
4328
+ "ufo": "^1.6.3"
4329
+ }
4330
+ },
4331
  "node_modules/ms": {
4332
  "version": "2.1.3",
4333
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
 
4375
  "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
4376
  "license": "MIT"
4377
  },
4378
+ "node_modules/package-manager-detector": {
4379
+ "version": "1.6.0",
4380
+ "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz",
4381
+ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==",
4382
+ "license": "MIT"
4383
+ },
4384
  "node_modules/parent-module": {
4385
  "version": "1.0.1",
4386
  "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
 
4411
  "url": "https://github.com/sponsors/sindresorhus"
4412
  }
4413
  },
4414
+ "node_modules/path-data-parser": {
4415
+ "version": "0.1.0",
4416
+ "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz",
4417
+ "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==",
4418
+ "license": "MIT"
4419
+ },
4420
  "node_modules/path-parse": {
4421
  "version": "1.0.7",
4422
  "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
 
4432
  "node": ">=8"
4433
  }
4434
  },
4435
+ "node_modules/pathe": {
4436
+ "version": "2.0.3",
4437
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
4438
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
4439
+ "license": "MIT"
4440
+ },
4441
  "node_modules/picocolors": {
4442
  "version": "1.1.1",
4443
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
 
4458
  "url": "https://github.com/sponsors/jonschlinkert"
4459
  }
4460
  },
4461
+ "node_modules/pkg-types": {
4462
+ "version": "1.3.1",
4463
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz",
4464
+ "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==",
4465
+ "license": "MIT",
4466
+ "dependencies": {
4467
+ "confbox": "^0.1.8",
4468
+ "mlly": "^1.7.4",
4469
+ "pathe": "^2.0.1"
4470
+ }
4471
+ },
4472
+ "node_modules/points-on-curve": {
4473
+ "version": "0.2.0",
4474
+ "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz",
4475
+ "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==",
4476
+ "license": "MIT"
4477
+ },
4478
+ "node_modules/points-on-path": {
4479
+ "version": "0.2.1",
4480
+ "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz",
4481
+ "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==",
4482
+ "license": "MIT",
4483
+ "dependencies": {
4484
+ "path-data-parser": "0.1.0",
4485
+ "points-on-curve": "0.2.0"
4486
+ }
4487
+ },
4488
  "node_modules/postcss": {
4489
  "version": "8.5.9",
4490
  "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
 
4505
  }
4506
  ],
4507
  "license": "MIT",
4508
+ "peer": true,
4509
  "dependencies": {
4510
  "nanoid": "^3.3.11",
4511
  "picocolors": "^1.1.1",
 
4515
  "node": "^10 || ^12 || >=14"
4516
  }
4517
  },
4518
+ "node_modules/postcss-custom-media": {
4519
+ "version": "12.0.1",
4520
+ "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-12.0.1.tgz",
4521
+ "integrity": "sha512-66syE14+VeqkUf0rRX0bvbTCbNRJF132jD+ceo8th1dap2YJEAqpdh5uG98CE3IbgHT7m9XM0GIlOazNWqQdeA==",
4522
+ "dev": true,
4523
+ "funding": [
4524
+ {
4525
+ "type": "github",
4526
+ "url": "https://github.com/sponsors/csstools"
4527
+ },
4528
+ {
4529
+ "type": "opencollective",
4530
+ "url": "https://opencollective.com/csstools"
4531
+ }
4532
+ ],
4533
+ "license": "MIT",
4534
+ "dependencies": {
4535
+ "@csstools/cascade-layer-name-parser": "^3.0.0",
4536
+ "@csstools/css-parser-algorithms": "^4.0.0",
4537
+ "@csstools/css-tokenizer": "^4.0.0",
4538
+ "@csstools/media-query-list-parser": "^5.0.0"
4539
+ },
4540
+ "engines": {
4541
+ "node": ">=20.19.0"
4542
+ },
4543
+ "peerDependencies": {
4544
+ "postcss": "^8.4"
4545
+ }
4546
+ },
4547
  "node_modules/prop-types": {
4548
  "version": "15.8.1",
4549
  "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
 
4857
  "node": ">=4"
4858
  }
4859
  },
4860
+ "node_modules/robust-predicates": {
4861
+ "version": "3.0.3",
4862
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
4863
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
4864
+ "license": "Unlicense"
4865
+ },
4866
  "node_modules/rollup": {
4867
  "version": "4.60.1",
4868
  "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
 
4914
  "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
4915
  "license": "MIT"
4916
  },
4917
+ "node_modules/roughjs": {
4918
+ "version": "4.6.6",
4919
+ "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz",
4920
+ "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==",
4921
+ "license": "MIT",
4922
+ "dependencies": {
4923
+ "hachure-fill": "^0.5.2",
4924
+ "path-data-parser": "^0.1.0",
4925
+ "points-on-curve": "^0.2.0",
4926
+ "points-on-path": "^0.2.1"
4927
+ }
4928
+ },
4929
+ "node_modules/rw": {
4930
+ "version": "1.3.3",
4931
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
4932
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
4933
+ "license": "BSD-3-Clause"
4934
+ },
4935
+ "node_modules/safer-buffer": {
4936
+ "version": "2.1.2",
4937
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
4938
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
4939
+ "license": "MIT"
4940
+ },
4941
  "node_modules/scheduler": {
4942
  "version": "0.23.2",
4943
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
 
5019
  "url": "https://github.com/sponsors/sindresorhus"
5020
  }
5021
  },
5022
+ "node_modules/tinyexec": {
5023
+ "version": "1.1.1",
5024
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz",
5025
+ "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==",
5026
+ "license": "MIT",
5027
+ "engines": {
5028
+ "node": ">=18"
5029
+ }
5030
+ },
5031
  "node_modules/tinyglobby": {
5032
  "version": "0.2.16",
5033
  "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
 
5054
  "@popperjs/core": "^2.9.0"
5055
  }
5056
  },
5057
+ "node_modules/ts-dedent": {
5058
+ "version": "2.2.0",
5059
+ "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz",
5060
+ "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
5061
+ "license": "MIT",
5062
+ "engines": {
5063
+ "node": ">=6.10"
5064
+ }
5065
+ },
5066
  "node_modules/uc.micro": {
5067
  "version": "2.1.0",
5068
  "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
5069
  "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
5070
  "license": "MIT"
5071
  },
5072
+ "node_modules/ufo": {
5073
+ "version": "1.6.3",
5074
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz",
5075
+ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==",
5076
+ "license": "MIT"
5077
+ },
5078
  "node_modules/update-browserslist-db": {
5079
  "version": "1.2.3",
5080
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
 
5115
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
5116
  }
5117
  },
5118
+ "node_modules/uuid": {
5119
+ "version": "11.1.0",
5120
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
5121
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
5122
+ "funding": [
5123
+ "https://github.com/sponsors/broofa",
5124
+ "https://github.com/sponsors/ctavan"
5125
+ ],
5126
+ "license": "MIT",
5127
+ "bin": {
5128
+ "uuid": "dist/esm/bin/uuid"
5129
+ }
5130
+ },
5131
  "node_modules/vite": {
5132
  "version": "6.4.2",
5133
  "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
 
5204
  }
5205
  }
5206
  },
5207
+ "node_modules/vscode-jsonrpc": {
5208
+ "version": "8.2.0",
5209
+ "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
5210
+ "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
5211
+ "license": "MIT",
5212
+ "engines": {
5213
+ "node": ">=14.0.0"
5214
+ }
5215
+ },
5216
+ "node_modules/vscode-languageserver": {
5217
+ "version": "9.0.1",
5218
+ "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz",
5219
+ "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==",
5220
+ "license": "MIT",
5221
+ "dependencies": {
5222
+ "vscode-languageserver-protocol": "3.17.5"
5223
+ },
5224
+ "bin": {
5225
+ "installServerIntoExtension": "bin/installServerIntoExtension"
5226
+ }
5227
+ },
5228
+ "node_modules/vscode-languageserver-protocol": {
5229
+ "version": "3.17.5",
5230
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
5231
+ "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
5232
+ "license": "MIT",
5233
+ "dependencies": {
5234
+ "vscode-jsonrpc": "8.2.0",
5235
+ "vscode-languageserver-types": "3.17.5"
5236
+ }
5237
+ },
5238
+ "node_modules/vscode-languageserver-textdocument": {
5239
+ "version": "1.0.12",
5240
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz",
5241
+ "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==",
5242
+ "license": "MIT"
5243
+ },
5244
+ "node_modules/vscode-languageserver-types": {
5245
+ "version": "3.17.5",
5246
+ "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
5247
+ "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==",
5248
+ "license": "MIT"
5249
+ },
5250
+ "node_modules/vscode-uri": {
5251
+ "version": "3.1.0",
5252
+ "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz",
5253
+ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
5254
+ "license": "MIT"
5255
+ },
5256
  "node_modules/w3c-keyname": {
5257
  "version": "2.2.8",
5258
  "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
frontend/package.json CHANGED
@@ -37,6 +37,7 @@
37
  "ai": "^6.0.158",
38
  "katex": "^0.16.45",
39
  "lowlight": "^3.2.0",
 
40
  "react": "^18.3.0",
41
  "react-dom": "^18.3.0",
42
  "tippy.js": "^6.3.7",
@@ -47,6 +48,7 @@
47
  "@types/react": "^18.3.0",
48
  "@types/react-dom": "^18.3.0",
49
  "@vitejs/plugin-react": "^4.3.0",
 
50
  "vite": "^6.0.0"
51
  }
52
  }
 
37
  "ai": "^6.0.158",
38
  "katex": "^0.16.45",
39
  "lowlight": "^3.2.0",
40
+ "mermaid": "^11.14.0",
41
  "react": "^18.3.0",
42
  "react-dom": "^18.3.0",
43
  "tippy.js": "^6.3.7",
 
48
  "@types/react": "^18.3.0",
49
  "@types/react-dom": "^18.3.0",
50
  "@vitejs/plugin-react": "^4.3.0",
51
+ "postcss-custom-media": "^12.0.1",
52
  "vite": "^6.0.0"
53
  }
54
  }
frontend/postcss.config.js ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ "postcss-custom-media": {},
4
+ },
5
+ };
frontend/src/App.tsx CHANGED
@@ -1,7 +1,8 @@
1
- import { useRef, useState, useCallback, useEffect } from "react";
2
  import { Editor as TiptapEditor } from "@tiptap/core";
3
  import { UndoManager } from "yjs";
4
  import type * as Y from "yjs";
 
5
  import {
6
  Box,
7
  Chip,
@@ -13,6 +14,8 @@ import UndoIcon from "@mui/icons-material/Undo";
13
  import RedoIcon from "@mui/icons-material/Redo";
14
  import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
15
  import PublishOutlinedIcon from "@mui/icons-material/PublishOutlined";
 
 
16
  import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutlined";
17
  import CloseIcon from "@mui/icons-material/Close";
18
  import {
@@ -74,6 +77,27 @@ export default function App() {
74
  const [hasSelection, setHasSelection] = useState(false);
75
  const [undoManager, setUndoManager] = useState<UndoManager | null>(null);
76
  const [chatOpen, setChatOpen] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  const agentChat = useAgentChat({ editor: editorInstance, undoManager, frontmatterStore });
79
 
@@ -168,7 +192,9 @@ export default function App() {
168
  }, [commentStore, user]);
169
 
170
  return (
 
171
  <Box
 
172
  sx={{
173
  height: "100vh",
174
  display: "flex",
@@ -238,36 +264,25 @@ export default function App() {
238
  </Box>
239
 
240
  {/* Single scroll container */}
241
- <Box
242
  ref={editorContainerCallback}
243
- sx={{
244
- flex: 1,
245
- overflowY: "auto",
246
- overflowX: "hidden",
247
- }}
248
  >
249
- <Box
250
- sx={{
251
- display: "grid",
252
- gridTemplateColumns: "200px 1fr 240px",
253
- gridTemplateRows: "auto 1fr",
254
- minHeight: "100%",
255
- }}
256
- >
257
  {/* Hero - center column only, row 1 */}
258
- <Box sx={{ gridColumn: 2, gridRow: 1, pt: 4 }}>
259
  <FrontmatterHero store={frontmatterStore} />
260
- </Box>
261
 
262
  {/* TOC - left column, row 2, sticky */}
263
- <Box sx={{ gridColumn: 1, gridRow: 2 }}>
264
- <Box sx={{ position: "sticky", top: 16 }}>
265
  <TableOfContents editor={editorInstance} />
266
- </Box>
267
- </Box>
268
 
269
  {/* Editor - center column, row 2 */}
270
- <Box sx={{ gridColumn: 2, gridRow: 2, pb: 4 }}>
271
  <Editor
272
  docName={docName}
273
  user={user}
@@ -279,19 +294,19 @@ export default function App() {
279
  onUndoManagerReady={setUndoManager}
280
  onAddComment={handleAddComment}
281
  />
282
- </Box>
283
 
284
  {/* Comments - right column, row 2 */}
285
- <Box sx={{ gridColumn: 3, gridRow: 2, pl: 2, py: 2 }}>
286
  <CommentsSidebar
287
  editor={editorInstance}
288
  commentStore={commentStore}
289
  user={user}
290
  editorContainer={containerEl}
291
  />
292
- </Box>
293
- </Box>
294
- </Box>
295
 
296
  {/* Floating chat - bottom left */}
297
  {chatOpen ? (
@@ -442,5 +457,6 @@ export default function App() {
442
  </DialogActions>
443
  </Dialog>
444
  </Box>
 
445
  );
446
  }
 
1
+ import { useRef, useState, useCallback, useEffect, useMemo } from "react";
2
  import { Editor as TiptapEditor } from "@tiptap/core";
3
  import { UndoManager } from "yjs";
4
  import type * as Y from "yjs";
5
+ import { ThemeProvider } from "@mui/material/styles";
6
  import {
7
  Box,
8
  Chip,
 
14
  import RedoIcon from "@mui/icons-material/Redo";
15
  import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
16
  import PublishOutlinedIcon from "@mui/icons-material/PublishOutlined";
17
+ import { buildTheme, DEFAULT_PRIMARY, DEFAULT_HUE } from "./theme";
18
+ import { oklchToHex, OKLCH_L, OKLCH_C } from "./editor/frontmatter/HueSlider";
19
  import ChatBubbleOutlineIcon from "@mui/icons-material/ChatBubbleOutlined";
20
  import CloseIcon from "@mui/icons-material/Close";
21
  import {
 
77
  const [hasSelection, setHasSelection] = useState(false);
78
  const [undoManager, setUndoManager] = useState<UndoManager | null>(null);
79
  const [chatOpen, setChatOpen] = useState(false);
80
+ const [primaryHue, setPrimaryHue] = useState(DEFAULT_HUE);
81
+
82
+ const primaryHex = useMemo(() => oklchToHex(OKLCH_L, OKLCH_C, primaryHue), [primaryHue]);
83
+ const muiTheme = useMemo(() => buildTheme(primaryHex), [primaryHex]);
84
+
85
+ // Sync primary hue from Yjs settings + push to CSS variables
86
+ useEffect(() => {
87
+ if (!settingsMap) return;
88
+ const sync = () => {
89
+ const h = settingsMap.get("primaryHue") as number | undefined;
90
+ if (h !== undefined) {
91
+ setPrimaryHue(h);
92
+ const oklch = `oklch(${OKLCH_L} ${OKLCH_C} ${h})`;
93
+ document.documentElement.style.setProperty("--primary-color", oklch);
94
+ document.documentElement.style.setProperty("--primary-color-hover", oklch);
95
+ }
96
+ };
97
+ sync();
98
+ settingsMap.observe(sync);
99
+ return () => settingsMap.unobserve(sync);
100
+ }, [settingsMap]);
101
 
102
  const agentChat = useAgentChat({ editor: editorInstance, undoManager, frontmatterStore });
103
 
 
192
  }, [commentStore, user]);
193
 
194
  return (
195
+ <ThemeProvider theme={muiTheme}>
196
  <Box
197
+ className="editor-app"
198
  sx={{
199
  height: "100vh",
200
  display: "flex",
 
264
  </Box>
265
 
266
  {/* Single scroll container */}
267
+ <div
268
  ref={editorContainerCallback}
269
+ style={{ flex: 1, overflowY: "auto", overflowX: "hidden" }}
 
 
 
 
270
  >
271
+ <div className="content-grid">
 
 
 
 
 
 
 
272
  {/* Hero - center column only, row 1 */}
273
+ <div className="content-grid__hero">
274
  <FrontmatterHero store={frontmatterStore} />
275
+ </div>
276
 
277
  {/* TOC - left column, row 2, sticky */}
278
+ <div className="content-grid__toc">
279
+ <div className="table-of-contents--sticky">
280
  <TableOfContents editor={editorInstance} />
281
+ </div>
282
+ </div>
283
 
284
  {/* Editor - center column, row 2 */}
285
+ <div className="content-grid__editor">
286
  <Editor
287
  docName={docName}
288
  user={user}
 
294
  onUndoManagerReady={setUndoManager}
295
  onAddComment={handleAddComment}
296
  />
297
+ </div>
298
 
299
  {/* Comments - right column, row 2 */}
300
+ <div className="content-grid__comments">
301
  <CommentsSidebar
302
  editor={editorInstance}
303
  commentStore={commentStore}
304
  user={user}
305
  editorContainer={containerEl}
306
  />
307
+ </div>
308
+ </div>
309
+ </div>
310
 
311
  {/* Floating chat - bottom left */}
312
  {chatOpen ? (
 
457
  </DialogActions>
458
  </Dialog>
459
  </Box>
460
+ </ThemeProvider>
461
  );
462
  }
frontend/src/components/TableOfContents.tsx CHANGED
@@ -1,5 +1,4 @@
1
  import { useState, useEffect, useCallback, useMemo, useRef } from "react";
2
- import { Box, Typography } from "@mui/material";
3
  import type { Editor } from "@tiptap/core";
4
 
5
  const SCROLL_OFFSET_PX = 80;
@@ -133,103 +132,53 @@ export function TableOfContents({ editor }: TableOfContentsProps) {
133
 
134
  if (!editor || headings.length === 0) {
135
  return (
136
- <Box sx={{ px: 2, pt: 3 }}>
137
- <Typography variant="caption" sx={{ color: "text.disabled", fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.05em", fontSize: "0.65rem" }}>
138
- Table of contents
139
- </Typography>
140
- <Typography variant="caption" sx={{ color: "text.disabled", display: "block", mt: 1, fontSize: "0.7rem" }}>
141
- Headings will appear here as you write.
142
- </Typography>
143
- </Box>
144
  );
145
  }
146
 
147
  const renderList = (nodes: TocNode[], isRoot: boolean) => (
148
- <Box
149
- component="ul"
150
- sx={{
151
- m: 0,
152
- mb: isRoot ? 0 : "6px",
153
- pl: isRoot ? 0 : "1em",
154
- listStyle: "none",
155
- }}
156
- >
157
  {nodes.map((node) => {
158
  const isExpanded = expandedPositions.has(node.pos);
159
  const hasChildren = node.children.length > 0;
160
 
161
  return (
162
- <Box component="li" key={node.id} sx={{ my: "0.25em" }}>
163
- <Box
164
- component="a"
165
- onClick={(e: React.MouseEvent) => {
166
  e.preventDefault();
167
  scrollTo(node.pos);
168
  }}
169
- sx={{
170
- cursor: "pointer",
171
- fontSize: "13px",
172
- lineHeight: 1.5,
173
- fontWeight: isRoot ? 700 : 400,
174
- color: "text.secondary",
175
- textDecoration: activePos === node.pos ? "underline" : "none",
176
- textUnderlineOffset: "3px",
177
- transition: "color 120ms ease",
178
- "&:hover": {
179
- textDecoration: "underline solid",
180
- textDecorationColor: "text.disabled",
181
- },
182
- }}
183
  >
184
  {node.text}
185
- </Box>
186
  {hasChildren && (
187
- <Box
188
- sx={{
189
- overflow: "hidden",
190
  maxHeight: isExpanded ? 500 : 0,
191
  opacity: isExpanded ? 1 : 0,
192
- transition: "max-height 200ms ease, opacity 200ms ease",
193
  }}
194
  >
195
  {renderList(node.children, false)}
196
- </Box>
197
  )}
198
- </Box>
199
  );
200
  })}
201
- </Box>
202
  );
203
 
204
  return (
205
- <Box sx={{ px: 1, pt: 3 }}>
206
- <Typography
207
- variant="caption"
208
- sx={{
209
- color: "text.disabled",
210
- fontWeight: 600,
211
- textTransform: "uppercase",
212
- letterSpacing: "0.05em",
213
- fontSize: "0.65rem",
214
- pl: "16px",
215
- mb: 1,
216
- display: "block",
217
- }}
218
- >
219
- Table of contents
220
- </Typography>
221
-
222
- <Box
223
- component="nav"
224
- sx={{
225
- borderLeft: "1px solid",
226
- borderColor: "divider",
227
- pl: "16px",
228
- fontSize: "13px",
229
- }}
230
- >
231
  {renderList(tree, true)}
232
- </Box>
233
- </Box>
234
  );
235
  }
 
1
  import { useState, useEffect, useCallback, useMemo, useRef } from "react";
 
2
  import type { Editor } from "@tiptap/core";
3
 
4
  const SCROLL_OFFSET_PX = 80;
 
132
 
133
  if (!editor || headings.length === 0) {
134
  return (
135
+ <div className="table-of-contents">
136
+ <div className="toc-title">Table of contents</div>
137
+ <div className="toc-empty">Headings will appear here as you write.</div>
138
+ </div>
 
 
 
 
139
  );
140
  }
141
 
142
  const renderList = (nodes: TocNode[], isRoot: boolean) => (
143
+ <ul>
 
 
 
 
 
 
 
 
144
  {nodes.map((node) => {
145
  const isExpanded = expandedPositions.has(node.pos);
146
  const hasChildren = node.children.length > 0;
147
 
148
  return (
149
+ <li key={node.id}>
150
+ <a
151
+ className={activePos === node.pos ? "active" : undefined}
152
+ onClick={(e) => {
153
  e.preventDefault();
154
  scrollTo(node.pos);
155
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  >
157
  {node.text}
158
+ </a>
159
  {hasChildren && (
160
+ <div
161
+ className="toc-children"
162
+ style={{
163
  maxHeight: isExpanded ? 500 : 0,
164
  opacity: isExpanded ? 1 : 0,
 
165
  }}
166
  >
167
  {renderList(node.children, false)}
168
+ </div>
169
  )}
170
+ </li>
171
  );
172
  })}
173
+ </ul>
174
  );
175
 
176
  return (
177
+ <div className="table-of-contents">
178
+ <div className="toc-title">Table of contents</div>
179
+ <nav>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  {renderList(tree, true)}
181
+ </nav>
182
+ </div>
183
  );
184
  }
frontend/src/editor/components/MermaidView.tsx ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useRef, useCallback } from "react";
2
+ import { NodeViewWrapper } from "@tiptap/react";
3
+ import type { NodeViewProps } from "@tiptap/react";
4
+ import mermaid from "mermaid";
5
+
6
+ let mermaidInitialized = false;
7
+
8
+ function ensureMermaidInit() {
9
+ if (mermaidInitialized) return;
10
+ mermaid.initialize({
11
+ startOnLoad: false,
12
+ theme: "neutral",
13
+ securityLevel: "loose",
14
+ fontFamily: "var(--default-font-family)",
15
+ });
16
+ mermaidInitialized = true;
17
+ }
18
+
19
+ export function MermaidNodeView({ node, updateAttributes }: NodeViewProps) {
20
+ const code = (node.attrs.code as string) || "";
21
+ const [editing, setEditing] = useState(!code);
22
+ const [draft, setDraft] = useState(code);
23
+ const [svgHtml, setSvgHtml] = useState("");
24
+ const [error, setError] = useState("");
25
+ const renderIdRef = useRef(0);
26
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
27
+
28
+ useEffect(() => {
29
+ if (!editing) setDraft(code);
30
+ }, [code, editing]);
31
+
32
+ useEffect(() => {
33
+ if (editing && textareaRef.current) {
34
+ textareaRef.current.focus();
35
+ textareaRef.current.selectionStart = textareaRef.current.value.length;
36
+ }
37
+ }, [editing]);
38
+
39
+ const renderDiagram = useCallback(async (src: string) => {
40
+ if (!src.trim()) {
41
+ setSvgHtml("");
42
+ setError("");
43
+ return;
44
+ }
45
+ ensureMermaidInit();
46
+ const id = `mermaid-${++renderIdRef.current}`;
47
+ try {
48
+ const { svg } = await mermaid.render(id, src);
49
+ setSvgHtml(svg);
50
+ setError("");
51
+ } catch (err: any) {
52
+ setError(err?.message || "Invalid diagram");
53
+ const stale = document.getElementById(id);
54
+ stale?.remove();
55
+ }
56
+ }, []);
57
+
58
+ useEffect(() => {
59
+ renderDiagram(code);
60
+ }, [code, renderDiagram]);
61
+
62
+ const commit = useCallback(() => {
63
+ setEditing(false);
64
+ if (draft !== code) {
65
+ updateAttributes({ code: draft });
66
+ }
67
+ }, [draft, code, updateAttributes]);
68
+
69
+ return (
70
+ <NodeViewWrapper data-component="mermaid">
71
+ <div
72
+ contentEditable={false}
73
+ style={{
74
+ border: "1px solid var(--border-color)",
75
+ borderRadius: 8,
76
+ overflow: "hidden",
77
+ margin: "0.75em 0",
78
+ userSelect: "none",
79
+ }}
80
+ >
81
+ {/* Header */}
82
+ <div
83
+ style={{
84
+ display: "flex",
85
+ alignItems: "center",
86
+ justifyContent: "space-between",
87
+ padding: "6px 12px",
88
+ borderBottom: "1px solid var(--border-color)",
89
+ background: "var(--surface-bg)",
90
+ }}
91
+ >
92
+ <span style={{ fontSize: 12, fontWeight: 600, color: "var(--muted-color)" }}>
93
+ ◈ Mermaid
94
+ </span>
95
+ <button
96
+ onClick={() => {
97
+ if (editing) commit();
98
+ else setEditing(true);
99
+ }}
100
+ style={{
101
+ background: "none",
102
+ border: "1px solid var(--border-color)",
103
+ borderRadius: 4,
104
+ padding: "2px 10px",
105
+ fontSize: 11,
106
+ color: "var(--muted-color)",
107
+ cursor: "pointer",
108
+ }}
109
+ >
110
+ {editing ? "Done" : "Edit"}
111
+ </button>
112
+ </div>
113
+
114
+ {/* Code editor (shown when editing) */}
115
+ {editing && (
116
+ <div style={{ borderBottom: "1px solid var(--border-color)" }}>
117
+ <textarea
118
+ ref={textareaRef}
119
+ value={draft}
120
+ onChange={(e) => setDraft(e.target.value)}
121
+ onBlur={commit}
122
+ onKeyDown={(e) => {
123
+ if (e.key === "Escape") {
124
+ setDraft(code);
125
+ setEditing(false);
126
+ }
127
+ if (e.key === "Tab") {
128
+ e.preventDefault();
129
+ const ta = e.currentTarget;
130
+ const start = ta.selectionStart;
131
+ const end = ta.selectionEnd;
132
+ const val = ta.value;
133
+ const updated = val.substring(0, start) + " " + val.substring(end);
134
+ setDraft(updated);
135
+ requestAnimationFrame(() => {
136
+ ta.selectionStart = ta.selectionEnd = start + 2;
137
+ });
138
+ }
139
+ }}
140
+ spellCheck={false}
141
+ style={{
142
+ width: "100%",
143
+ minHeight: 120,
144
+ padding: "12px 16px",
145
+ border: "none",
146
+ outline: "none",
147
+ resize: "vertical",
148
+ fontFamily: "var(--font-mono)",
149
+ fontSize: 13,
150
+ lineHeight: 1.5,
151
+ background: "var(--code-bg)",
152
+ color: "var(--text-color)",
153
+ boxSizing: "border-box",
154
+ }}
155
+ />
156
+ </div>
157
+ )}
158
+
159
+ {/* Preview */}
160
+ {error ? (
161
+ <div style={{ padding: "16px", color: "var(--danger-color, #dc2626)", fontSize: 12 }}>
162
+ {error}
163
+ </div>
164
+ ) : svgHtml ? (
165
+ <div
166
+ style={{ padding: "16px", display: "flex", justifyContent: "center" }}
167
+ dangerouslySetInnerHTML={{ __html: svgHtml }}
168
+ />
169
+ ) : (
170
+ <div style={{ padding: "24px", textAlign: "center", color: "var(--muted-color)", fontSize: 13 }}>
171
+ Click "Edit" to write a Mermaid diagram
172
+ </div>
173
+ )}
174
+ </div>
175
+ </NodeViewWrapper>
176
+ );
177
+ }
178
+
179
+ MermaidNodeView.displayName = "MermaidView";
frontend/src/editor/components/factory.ts CHANGED
@@ -9,6 +9,7 @@ import { ReactNodeViewRenderer } from "@tiptap/react";
9
  import type { ComponentDef } from "./registry";
10
  import { makeWrapperView } from "./WrapperView";
11
  import { makeAtomicView } from "./AtomicView";
 
12
 
13
  /**
14
  * Build a TipTap Node for a "wrapper" component (has editable children).
@@ -128,7 +129,8 @@ export function createAtomicExtension(def: ComponentDef) {
128
  },
129
 
130
  addNodeView() {
131
- return ReactNodeViewRenderer(makeAtomicView(def));
 
132
  },
133
  });
134
  }
 
9
  import type { ComponentDef } from "./registry";
10
  import { makeWrapperView } from "./WrapperView";
11
  import { makeAtomicView } from "./AtomicView";
12
+ import { MermaidNodeView } from "./MermaidView";
13
 
14
  /**
15
  * Build a TipTap Node for a "wrapper" component (has editable children).
 
129
  },
130
 
131
  addNodeView() {
132
+ const View = def.name === "mermaid" ? MermaidNodeView : makeAtomicView(def);
133
+ return ReactNodeViewRenderer(View);
134
  },
135
  });
136
  }
frontend/src/editor/components/registry.ts CHANGED
@@ -160,4 +160,15 @@ export const COMPONENTS: ComponentDef[] = [
160
  { name: "html", type: "string", label: "HTML", default: "", placeholder: "<div>…</div>" },
161
  ],
162
  },
 
 
 
 
 
 
 
 
 
 
 
163
  ];
 
160
  { name: "html", type: "string", label: "HTML", default: "", placeholder: "<div>…</div>" },
161
  ],
162
  },
163
+ {
164
+ name: "mermaid",
165
+ tag: "Mermaid",
166
+ icon: "◈",
167
+ label: "Mermaid diagram",
168
+ description: "Flowchart, sequence, Gantt, etc.",
169
+ kind: "atomic",
170
+ fields: [
171
+ { name: "code", type: "string", label: "Code", default: "graph TD\n A[Start] --> B{Decision}\n B -->|Yes| C[OK]\n B -->|No| D[End]", placeholder: "graph TD\\n A --> B" },
172
+ ],
173
+ },
174
  ];
frontend/src/editor/default-content.ts CHANGED
@@ -3,10 +3,6 @@
3
  * Covers every block/mark type supported by the editor.
4
  */
5
  export const DEFAULT_CONTENT = `
6
- <h1>Welcome to the Collaborative Editor</h1>
7
-
8
- <p>This demo article showcases <strong>every content type</strong> available in the editor. Feel free to edit, delete, or rewrite anything — the AI assistant in the left panel can help you.</p>
9
-
10
  <h2>Text formatting</h2>
11
 
12
  <p>You can make text <strong>bold</strong>, <em>italic</em>, or <s>strikethrough</s>. Combine them for <strong><em>bold italic</em></strong>. Use <code>inline code</code> for technical terms, and add <a href="https://huggingface.co">links</a> to external resources.</p>
@@ -149,6 +145,8 @@ print(tokenizer.decode(outputs[0], skip_special_tokens=True))</code></pre>
149
 
150
  <div data-component="htmlEmbed" src="d3-scaling-chart.html" title="Interactive scaling law visualization" desc="Explore how model size, data, and compute interact." wide="false" downloadable="true"></div>
151
 
 
 
152
  <p>The concept of <span data-type="glossary" term="Scaling law" definition="A power-law relationship between model size, dataset size, compute budget, and performance."></span> has become central to modern AI research. Early work<span data-type="footnote" content="Hestness et al. (2017) observed similar scaling behavior in vision and translation tasks before the scaling laws paper formalized the framework."></span> suggested these relationships hold across modalities.</p>
153
 
154
  <div data-component="stack" layout="2-column" gap="medium"><div data-type="stack-column"><p><strong>Column A</strong></p><p>Multi-column layouts let you place content side by side. Each column is fully editable.</p></div><div data-type="stack-column"><p><strong>Column B</strong></p><p>Use the layout selector in the header to switch between 2, 3, or 4 columns.</p></div></div>
 
3
  * Covers every block/mark type supported by the editor.
4
  */
5
  export const DEFAULT_CONTENT = `
 
 
 
 
6
  <h2>Text formatting</h2>
7
 
8
  <p>You can make text <strong>bold</strong>, <em>italic</em>, or <s>strikethrough</s>. Combine them for <strong><em>bold italic</em></strong>. Use <code>inline code</code> for technical terms, and add <a href="https://huggingface.co">links</a> to external resources.</p>
 
145
 
146
  <div data-component="htmlEmbed" src="d3-scaling-chart.html" title="Interactive scaling law visualization" desc="Explore how model size, data, and compute interact." wide="false" downloadable="true"></div>
147
 
148
+ <div data-component="mermaid" code="graph TD\n A[Data Collection] --> B[Preprocessing]\n B --> C[Training]\n C --> D{Evaluation}\n D -->|Pass| E[Deploy]\n D -->|Fail| B"></div>
149
+
150
  <p>The concept of <span data-type="glossary" term="Scaling law" definition="A power-law relationship between model size, dataset size, compute budget, and performance."></span> has become central to modern AI research. Early work<span data-type="footnote" content="Hestness et al. (2017) observed similar scaling behavior in vision and translation tasks before the scaling laws paper formalized the framework."></span> suggested these relationships hold across modalities.</p>
151
 
152
  <div data-component="stack" layout="2-column" gap="medium"><div data-type="stack-column"><p><strong>Column A</strong></p><p>Multi-column layouts let you place content side by side. Each column is fully editable.</p></div><div data-type="stack-column"><p><strong>Column B</strong></p><p>Use the layout selector in the header to switch between 2, 3, or 4 columns.</p></div></div>
frontend/src/editor/frontmatter/FrontmatterHero.tsx CHANGED
@@ -22,7 +22,7 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
22
  const [showAuthorForm, setShowAuthorForm] = useState(false);
23
 
24
  if (!data) {
25
- return <Box sx={{ width: "100%", mb: 4 }} />;
26
  }
27
 
28
  const hasAuthors = data.authors.length > 0;
@@ -31,170 +31,126 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
31
  const hasMeta = hasAuthors || data.published || data.doi;
32
 
33
  return (
34
- <Box sx={{ width: "100%", mb: 2 }}>
35
- {/* Hero section - centered title + subtitle */}
36
- <Box sx={{ textAlign: "center", pt: 4, pb: 2, px: 2 }}>
37
- <Box sx={{ maxWidth: 780, mx: "auto" }}>
38
- <EditableText
39
- value={data.title}
40
- placeholder="Article title"
41
- onChange={(v) => update("title", v)}
42
- typographySx={{
43
- fontSize: "clamp(34px, 5vw, 54px)",
44
- fontWeight: 800,
45
- lineHeight: 1.12,
46
- letterSpacing: "-0.02em",
47
- color: "text.primary",
48
- mb: 1,
49
- textWrap: "balance",
50
- }}
51
- multiline
52
- center
53
- />
54
- </Box>
55
 
56
- <Box sx={{ maxWidth: "55%", mx: "auto" }}>
57
- <EditableText
58
- value={data.subtitle}
59
- placeholder="Subtitle (optional)"
60
- onChange={(v) => update("subtitle", v)}
61
- typographySx={{
62
- fontSize: "1.15em",
63
- fontWeight: 400,
64
- lineHeight: 1.6,
65
- color: "text.secondary",
66
- fontStyle: "italic",
67
- }}
68
- center
69
- />
70
- </Box>
71
- </Box>
72
 
73
- {/* Meta bar - matches published template */}
74
  {hasMeta && (
75
- <Box
76
- sx={{
77
- borderTop: "1px solid",
78
- borderBottom: "1px solid",
79
- borderColor: "divider",
80
- py: 2,
81
- fontSize: "0.9rem",
82
- }}
83
- >
84
- <Box
85
- sx={{
86
- maxWidth: 980,
87
- mx: "auto",
88
- px: 2,
89
- display: "flex",
90
- flexDirection: "row",
91
- justifyContent: "space-between",
92
- gap: 1,
93
- flexWrap: "wrap",
94
- rowGap: 1.5,
95
- }}
96
- >
97
  {/* Authors cell */}
98
- <MetaCell label="Authors">
99
- <Box sx={{ display: "flex", flexWrap: "wrap", m: 0, p: 0, listStyle: "none" }} component="ul">
 
100
  {data.authors.map((author, i) => (
101
- <Box component="li" key={`author-${i}`} sx={{ whiteSpace: "nowrap", p: 0 }}>
102
- <Box
103
- component="span"
104
  onClick={() => setEditingAuthorIdx(i)}
105
- sx={{
106
- cursor: "pointer",
107
- borderRadius: 0.5,
108
- transition: "background 0.15s",
109
- "&:hover": { bgcolor: "rgba(255,255,255,0.06)" },
110
- }}
111
  >
112
  {author.url ? (
113
- <Box component="span" sx={{ color: "primary.main", textDecoration: "underline", textUnderlineOffset: "2px", textDecorationThickness: "0.06em" }}>
114
  {author.name}
115
- </Box>
116
  ) : (
117
  author.name
118
  )}
119
  {multipleAffiliations && author.affiliations?.length > 0 && (
120
  <sup>{author.affiliations.join(",")}</sup>
121
  )}
122
- </Box>
123
  {i < data.authors.length - 1 && <>,&nbsp;</>}
124
- </Box>
125
  ))}
126
- <Box component="li" sx={{ p: 0, display: "inline-flex", alignItems: "center", ml: 0.5 }}>
127
  <Tooltip title="Add author" arrow>
128
  <IconButton
129
  size="small"
130
  onClick={() => setShowAuthorForm(true)}
131
- sx={{ color: "text.disabled", p: 0.25 }}
132
  >
133
  <AddIcon sx={{ fontSize: 14 }} />
134
  </IconButton>
135
  </Tooltip>
136
- </Box>
137
- </Box>
138
- </MetaCell>
139
 
140
  {/* Affiliations cell */}
141
  {hasAffiliations && (
142
- <MetaCell label="Affiliations">
 
143
  {multipleAffiliations ? (
144
- <Box component="ol" sx={{ m: 0, pl: "1.25em" }}>
145
  {data.affiliations.map((aff, i) => (
146
- <Box component="li" key={`aff-${i}`} sx={{ m: 0 }}>
147
  {aff.url ? (
148
- <a href={aff.url} target="_blank" rel="noopener noreferrer" style={{ color: "inherit" }}>
149
  {aff.name}
150
  </a>
151
  ) : (
152
  aff.name
153
  )}
154
- </Box>
155
  ))}
156
- </Box>
157
  ) : (
158
- <Box>
159
  {data.affiliations[0].url ? (
160
- <a href={data.affiliations[0].url} target="_blank" rel="noopener noreferrer" style={{ color: "inherit" }}>
161
  {data.affiliations[0].name}
162
  </a>
163
  ) : (
164
  data.affiliations[0].name
165
  )}
166
- </Box>
167
  )}
168
- </MetaCell>
169
  )}
170
 
171
  {/* Published cell */}
172
- <MetaCell label="Published">
 
173
  <EditableText
174
  value={data.published}
175
  placeholder="Date"
176
  onChange={(v) => update("published", v)}
177
- typographySx={{ fontSize: "0.9rem", color: "text.primary" }}
178
  inline
179
  />
180
- </MetaCell>
181
 
182
  {/* DOI cell */}
183
- <MetaCell label="DOI">
 
184
  <EditableText
185
  value={data.doi}
186
  placeholder="10.xxxx/xxxxx"
187
  onChange={(v) => update("doi", v)}
188
- typographySx={{ fontSize: "0.9rem", color: "text.primary", fontFamily: "monospace" }}
189
  inline
190
  />
191
- </MetaCell>
192
- </Box>
193
- </Box>
194
  )}
195
 
196
- {/* Author forms */}
197
- <Box sx={{ maxWidth: 680, mx: "auto", px: 2 }}>
198
  {showAuthorForm && (
199
  <AuthorInlineForm
200
  affiliations={data.affiliations}
@@ -225,47 +181,23 @@ export function FrontmatterHero({ store }: FrontmatterHeroProps) {
225
  onCancel={() => setEditingAuthorIdx(null)}
226
  />
227
  )}
228
- </Box>
229
- </Box>
230
  );
231
  }
232
 
233
- // ---- Meta cell (labeled column in the meta bar) ----
234
-
235
- function MetaCell({ label, children }: { label: string; children: React.ReactNode }) {
236
- return (
237
- <Box sx={{ display: "flex", flexDirection: "column", gap: 1, maxWidth: 400 }}>
238
- <Typography
239
- component="h3"
240
- sx={{
241
- m: 0,
242
- fontSize: "12px",
243
- fontWeight: 400,
244
- color: "text.disabled",
245
- textTransform: "uppercase",
246
- letterSpacing: "0.02em",
247
- }}
248
- >
249
- {label}
250
- </Typography>
251
- <Box sx={{ m: 0 }}>{children}</Box>
252
- </Box>
253
- );
254
- }
255
-
256
- // ---- Editable text field (click-to-edit) ----
257
 
258
  interface EditableTextProps {
259
  value: string;
260
  placeholder: string;
261
  onChange: (value: string) => void;
262
- typographySx?: Record<string, any>;
263
  multiline?: boolean;
264
  inline?: boolean;
265
- center?: boolean;
266
  }
267
 
268
- function EditableText({ value, placeholder, onChange, typographySx = {}, multiline, inline, center }: EditableTextProps) {
269
  const [editing, setEditing] = useState(false);
270
  const [draft, setDraft] = useState(value);
271
  const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
@@ -302,11 +234,10 @@ function EditableText({ value, placeholder, onChange, typographySx = {}, multili
302
 
303
  const displayValue = value || placeholder;
304
  const isEmpty = !value;
305
- const textAlign = center ? "center" : undefined;
306
 
307
  if (editing) {
308
  return (
309
- <Box sx={{ display: inline ? "inline-flex" : "flex" }}>
310
  <TextField
311
  inputRef={inputRef}
312
  value={draft}
@@ -320,43 +251,30 @@ function EditableText({ value, placeholder, onChange, typographySx = {}, multili
320
  slotProps={{
321
  input: {
322
  disableUnderline: true,
323
- sx: {
324
- ...typographySx,
325
- textAlign,
326
- p: 0,
327
- m: 0,
328
- },
329
  },
330
  }}
331
  />
332
- </Box>
333
  );
334
  }
335
 
336
  return (
337
- <Box
 
338
  onClick={() => setEditing(true)}
339
- sx={{
340
- ...typographySx,
341
- textAlign,
342
- cursor: "text",
343
- opacity: isEmpty ? 0.35 : 1,
344
- borderRadius: 1,
345
- transition: "background 0.15s",
346
- px: 0.5,
347
- mx: -0.5,
348
  display: inline ? "inline" : "block",
349
- "&:hover": {
350
- bgcolor: "rgba(255,255,255,0.04)",
351
- },
352
  }}
353
  >
354
  {displayValue}
355
- </Box>
356
  );
357
  }
358
 
359
- // ---- Author inline form ----
360
 
361
  function AuthorInlineForm({
362
  initial,
 
22
  const [showAuthorForm, setShowAuthorForm] = useState(false);
23
 
24
  if (!data) {
25
+ return <div className="hero" />;
26
  }
27
 
28
  const hasAuthors = data.authors.length > 0;
 
31
  const hasMeta = hasAuthors || data.published || data.doi;
32
 
33
  return (
34
+ <div>
35
+ {/* Hero section - uses .hero from _hero.css (same as HeroArticle.astro) */}
36
+ <section className="hero">
37
+ <EditableText
38
+ value={data.title}
39
+ placeholder="Article title"
40
+ onChange={(v) => update("title", v)}
41
+ className="hero-title"
42
+ multiline
43
+ />
 
 
 
 
 
 
 
 
 
 
 
44
 
45
+ <EditableText
46
+ value={data.subtitle}
47
+ placeholder="Subtitle (optional)"
48
+ onChange={(v) => update("subtitle", v)}
49
+ className="hero-desc"
50
+ />
51
+ </section>
 
 
 
 
 
 
 
 
 
52
 
53
+ {/* Meta bar - uses .meta from _hero.css (same as HeroArticle.astro) */}
54
  {hasMeta && (
55
+ <header className="meta" aria-label="Article meta information">
56
+ <div className="meta-container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  {/* Authors cell */}
58
+ <div className="meta-container-cell">
59
+ <h3>Authors</h3>
60
+ <ul className="authors">
61
  {data.authors.map((author, i) => (
62
+ <li key={`author-${i}`}>
63
+ <span
64
+ className="author-editable"
65
  onClick={() => setEditingAuthorIdx(i)}
 
 
 
 
 
 
66
  >
67
  {author.url ? (
68
+ <a href={author.url} target="_blank" rel="noopener noreferrer">
69
  {author.name}
70
+ </a>
71
  ) : (
72
  author.name
73
  )}
74
  {multipleAffiliations && author.affiliations?.length > 0 && (
75
  <sup>{author.affiliations.join(",")}</sup>
76
  )}
77
+ </span>
78
  {i < data.authors.length - 1 && <>,&nbsp;</>}
79
+ </li>
80
  ))}
81
+ <li className="author-add-btn">
82
  <Tooltip title="Add author" arrow>
83
  <IconButton
84
  size="small"
85
  onClick={() => setShowAuthorForm(true)}
86
+ sx={{ color: "var(--muted-color)", p: 0.25 }}
87
  >
88
  <AddIcon sx={{ fontSize: 14 }} />
89
  </IconButton>
90
  </Tooltip>
91
+ </li>
92
+ </ul>
93
+ </div>
94
 
95
  {/* Affiliations cell */}
96
  {hasAffiliations && (
97
+ <div className="meta-container-cell">
98
+ <h3>Affiliations</h3>
99
  {multipleAffiliations ? (
100
+ <ol className="affiliations">
101
  {data.affiliations.map((aff, i) => (
102
+ <li key={`aff-${i}`}>
103
  {aff.url ? (
104
+ <a href={aff.url} target="_blank" rel="noopener noreferrer">
105
  {aff.name}
106
  </a>
107
  ) : (
108
  aff.name
109
  )}
110
+ </li>
111
  ))}
112
+ </ol>
113
  ) : (
114
+ <p>
115
  {data.affiliations[0].url ? (
116
+ <a href={data.affiliations[0].url} target="_blank" rel="noopener noreferrer">
117
  {data.affiliations[0].name}
118
  </a>
119
  ) : (
120
  data.affiliations[0].name
121
  )}
122
+ </p>
123
  )}
124
+ </div>
125
  )}
126
 
127
  {/* Published cell */}
128
+ <div className="meta-container-cell">
129
+ <h3>Published</h3>
130
  <EditableText
131
  value={data.published}
132
  placeholder="Date"
133
  onChange={(v) => update("published", v)}
 
134
  inline
135
  />
136
+ </div>
137
 
138
  {/* DOI cell */}
139
+ <div className="meta-container-cell">
140
+ <h3>DOI</h3>
141
  <EditableText
142
  value={data.doi}
143
  placeholder="10.xxxx/xxxxx"
144
  onChange={(v) => update("doi", v)}
 
145
  inline
146
  />
147
+ </div>
148
+ </div>
149
+ </header>
150
  )}
151
 
152
+ {/* Author forms (editor-only, MUI is fine here) */}
153
+ <div style={{ maxWidth: 680, margin: "0 auto", padding: "0 16px" }}>
154
  {showAuthorForm && (
155
  <AuthorInlineForm
156
  affiliations={data.affiliations}
 
181
  onCancel={() => setEditingAuthorIdx(null)}
182
  />
183
  )}
184
+ </div>
185
+ </div>
186
  );
187
  }
188
 
189
+ // ---- Editable text field (click-to-edit, editor-only behavior) ----
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
  interface EditableTextProps {
192
  value: string;
193
  placeholder: string;
194
  onChange: (value: string) => void;
195
+ className?: string;
196
  multiline?: boolean;
197
  inline?: boolean;
 
198
  }
199
 
200
+ function EditableText({ value, placeholder, onChange, className, multiline, inline }: EditableTextProps) {
201
  const [editing, setEditing] = useState(false);
202
  const [draft, setDraft] = useState(value);
203
  const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
 
234
 
235
  const displayValue = value || placeholder;
236
  const isEmpty = !value;
 
237
 
238
  if (editing) {
239
  return (
240
+ <span className={className} style={{ display: inline ? "inline-flex" : "flex" }}>
241
  <TextField
242
  inputRef={inputRef}
243
  value={draft}
 
251
  slotProps={{
252
  input: {
253
  disableUnderline: true,
254
+ sx: { p: 0, m: 0, font: "inherit", color: "inherit", textAlign: "inherit" },
 
 
 
 
 
255
  },
256
  }}
257
  />
258
+ </span>
259
  );
260
  }
261
 
262
  return (
263
+ <span
264
+ className={`editable-text ${className || ""}`}
265
  onClick={() => setEditing(true)}
266
+ style={{
 
 
 
 
 
 
 
 
267
  display: inline ? "inline" : "block",
268
+ opacity: isEmpty ? 0.35 : 1,
269
+ cursor: "text",
 
270
  }}
271
  >
272
  {displayValue}
273
+ </span>
274
  );
275
  }
276
 
277
+ // ---- Author inline form (editor-only, MUI is fine) ----
278
 
279
  function AuthorInlineForm({
280
  initial,
frontend/src/editor/frontmatter/HueSlider.tsx ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useRef, useState, useEffect, useCallback } from "react";
2
+ import { Box, Typography } from "@mui/material";
3
+
4
+ const L = 0.75;
5
+ const C = 0.12;
6
+
7
+ const COLOR_NAMES = [
8
+ { name: "Candy Apple Red", hue: 0 },
9
+ { name: "Boiling Magma", hue: 15 },
10
+ { name: "Aerospace Orange", hue: 25 },
11
+ { name: "Burtuqali Orange", hue: 33 },
12
+ { name: "American Orange", hue: 40 },
13
+ { name: "Cheese", hue: 48 },
14
+ { name: "Amber", hue: 55 },
15
+ { name: "Demonic Yellow", hue: 65 },
16
+ { name: "Bat-Signal", hue: 72 },
17
+ { name: "Bright Yellow Green", hue: 85 },
18
+ { name: "Lasting Lime", hue: 95 },
19
+ { name: "Bright Green", hue: 110 },
20
+ { name: "Chlorophyll Green", hue: 120 },
21
+ { name: "Green Screen", hue: 130 },
22
+ { name: "Cathode Green", hue: 148 },
23
+ { name: "Booger Buster", hue: 160 },
24
+ { name: "Enthusiasm", hue: 170 },
25
+ { name: "Ice Ice Baby", hue: 180 },
26
+ { name: "Vivid Sky Blue", hue: 195 },
27
+ { name: "Azure", hue: 214 },
28
+ { name: "Blue Ribbon", hue: 225 },
29
+ { name: "Icelandic Water", hue: 238 },
30
+ { name: "Blue Pencil", hue: 250 },
31
+ { name: "Electric Ultramarine", hue: 260 },
32
+ { name: "Purple Climax", hue: 275 },
33
+ { name: "Amethyst Ganzstar", hue: 285 },
34
+ { name: "Electric Purple", hue: 295 },
35
+ { name: "Phlox", hue: 305 },
36
+ { name: "Brusque Pink", hue: 315 },
37
+ { name: "Bright Magenta", hue: 325 },
38
+ { name: "Pink", hue: 335 },
39
+ { name: "Hot Flamingoes", hue: 345 },
40
+ { name: "Carmine Red", hue: 355 },
41
+ ];
42
+
43
+ function getColorName(hue: number): string {
44
+ let best = COLOR_NAMES[0].name;
45
+ let bestDist = 361;
46
+ for (const c of COLOR_NAMES) {
47
+ const d = Math.abs(c.hue - hue);
48
+ const dist = Math.min(d, 360 - d);
49
+ if (dist < bestDist) {
50
+ bestDist = dist;
51
+ best = c.name;
52
+ }
53
+ }
54
+ return best;
55
+ }
56
+
57
+ // OKLCH -> sRGB -> hex
58
+ function oklchToHex(l: number, c: number, h: number): string {
59
+ const hRad = (h * Math.PI) / 180;
60
+ const a = c * Math.cos(hRad);
61
+ const b = c * Math.sin(hRad);
62
+
63
+ const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
64
+ const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
65
+ const s_ = l - 0.0894841775 * a - 1.291485548 * b;
66
+
67
+ const ll = l_ * l_ * l_;
68
+ const mm = m_ * m_ * m_;
69
+ const ss = s_ * s_ * s_;
70
+
71
+ const toSrgb = (u: number) =>
72
+ u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055;
73
+
74
+ const r = toSrgb(+4.0767416621 * ll - 3.3077115913 * mm + 0.2309699292 * ss);
75
+ const g = toSrgb(-1.2684380046 * ll + 2.6097574011 * mm - 0.3413193965 * ss);
76
+ const bVal = toSrgb(-0.0041960863 * ll - 0.7034186147 * mm + 1.707614701 * ss);
77
+
78
+ const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v * 255)));
79
+ const hex = (v: number) => clamp(v).toString(16).padStart(2, "0");
80
+ return `#${hex(r)}${hex(g)}${hex(bVal)}`;
81
+ }
82
+
83
+ interface HueSliderProps {
84
+ hue: number;
85
+ onChange: (hue: number) => void;
86
+ }
87
+
88
+ export function HueSlider({ hue, onChange }: HueSliderProps) {
89
+ const sliderRef = useRef<HTMLDivElement>(null);
90
+ const [dragging, setDragging] = useState(false);
91
+
92
+ const hexColor = oklchToHex(L, C, hue);
93
+ const colorName = getColorName(hue);
94
+
95
+ const hueFromClientX = useCallback((clientX: number) => {
96
+ const rect = sliderRef.current?.getBoundingClientRect();
97
+ if (!rect) return hue;
98
+ const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
99
+ return (x / rect.width) * 360;
100
+ }, [hue]);
101
+
102
+ const handlePointerDown = useCallback((e: React.PointerEvent) => {
103
+ e.preventDefault();
104
+ (e.target as HTMLElement).setPointerCapture(e.pointerId);
105
+ setDragging(true);
106
+ onChange(hueFromClientX(e.clientX));
107
+ }, [hueFromClientX, onChange]);
108
+
109
+ const handlePointerMove = useCallback((e: React.PointerEvent) => {
110
+ if (!dragging) return;
111
+ onChange(hueFromClientX(e.clientX));
112
+ }, [dragging, hueFromClientX, onChange]);
113
+
114
+ const handlePointerUp = useCallback(() => {
115
+ setDragging(false);
116
+ }, []);
117
+
118
+ // Keyboard support
119
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
120
+ const step = e.shiftKey ? 10 : 2;
121
+ if (e.key === "ArrowLeft") { e.preventDefault(); onChange(((hue - step) % 360 + 360) % 360); }
122
+ if (e.key === "ArrowRight") { e.preventDefault(); onChange((hue + step) % 360); }
123
+ }, [hue, onChange]);
124
+
125
+ return (
126
+ <Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
127
+ {/* Swatch + info */}
128
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1.5 }}>
129
+ <Box
130
+ sx={{
131
+ width: 44,
132
+ height: 44,
133
+ borderRadius: "8px",
134
+ bgcolor: hexColor,
135
+ border: "1px solid",
136
+ borderColor: "divider",
137
+ flexShrink: 0,
138
+ }}
139
+ />
140
+ <Box sx={{ minWidth: 0 }}>
141
+ <Typography variant="body2" sx={{ fontWeight: 700, fontSize: "0.8rem", lineHeight: 1.3 }}>
142
+ {colorName}
143
+ </Typography>
144
+ <Typography variant="caption" sx={{ color: "text.secondary", fontSize: "0.7rem", fontFamily: "monospace" }}>
145
+ {hexColor.toUpperCase()} - {Math.round(hue)}°
146
+ </Typography>
147
+ </Box>
148
+ </Box>
149
+
150
+ {/* Hue slider */}
151
+ <Box
152
+ ref={sliderRef}
153
+ role="slider"
154
+ tabIndex={0}
155
+ aria-label="Hue"
156
+ aria-valuemin={0}
157
+ aria-valuemax={360}
158
+ aria-valuenow={Math.round(hue)}
159
+ onPointerDown={handlePointerDown}
160
+ onPointerMove={handlePointerMove}
161
+ onPointerUp={handlePointerUp}
162
+ onKeyDown={handleKeyDown}
163
+ sx={{
164
+ position: "relative",
165
+ height: 16,
166
+ borderRadius: "10px",
167
+ border: "1px solid",
168
+ borderColor: "divider",
169
+ background: "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
170
+ cursor: "ew-resize",
171
+ touchAction: "none",
172
+ "&:focus-visible": { outline: "2px solid", outlineColor: "primary.main", outlineOffset: 2 },
173
+ }}
174
+ >
175
+ {/* Knob */}
176
+ <Box
177
+ sx={{
178
+ position: "absolute",
179
+ top: "50%",
180
+ left: `${(hue / 360) * 100}%`,
181
+ width: 14,
182
+ height: 14,
183
+ borderRadius: "50%",
184
+ border: "2px solid #fff",
185
+ transform: "translate(-50%, -50%)",
186
+ bgcolor: hexColor,
187
+ boxShadow: "0 0 0 1px rgba(0,0,0,0.15), 0 1px 3px rgba(0,0,0,0.3)",
188
+ pointerEvents: "none",
189
+ }}
190
+ />
191
+ </Box>
192
+ </Box>
193
+ );
194
+ }
195
+
196
+ export { oklchToHex, L as OKLCH_L, C as OKLCH_C };
frontend/src/editor/frontmatter/SettingsDrawer.tsx CHANGED
@@ -15,6 +15,7 @@ import { useState, useEffect } from "react";
15
  import { useFrontmatter } from "./useFrontmatter";
16
  import type { FrontmatterStore, FrontmatterData } from "./frontmatter-store";
17
  import type * as Y from "yjs";
 
18
 
19
  interface SettingsDrawerProps {
20
  open: boolean;
@@ -26,12 +27,15 @@ interface SettingsDrawerProps {
26
  export function SettingsDrawer({ open, onClose, store, settingsMap }: SettingsDrawerProps) {
27
  const { data, update } = useFrontmatter(store);
28
  const [citationStyle, setCitationStyle] = useState("apa");
 
29
 
30
  useEffect(() => {
31
  if (!settingsMap) return;
32
  const sync = () => {
33
- const val = settingsMap.get("citationStyle");
34
- if (val) setCitationStyle(val as string);
 
 
35
  };
36
  sync();
37
  settingsMap.observe(sync);
@@ -127,6 +131,17 @@ export function SettingsDrawer({ open, onClose, store, settingsMap }: SettingsDr
127
  </Select>
128
  </FieldGroup>
129
 
 
 
 
 
 
 
 
 
 
 
 
130
  <Divider />
131
 
132
  {/* Toggles */}
@@ -206,3 +221,4 @@ function FieldGroup({ label, children }: { label: string; children: React.ReactN
206
  </Box>
207
  );
208
  }
 
 
15
  import { useFrontmatter } from "./useFrontmatter";
16
  import type { FrontmatterStore, FrontmatterData } from "./frontmatter-store";
17
  import type * as Y from "yjs";
18
+ import { HueSlider, oklchToHex, OKLCH_L, OKLCH_C } from "./HueSlider";
19
 
20
  interface SettingsDrawerProps {
21
  open: boolean;
 
27
  export function SettingsDrawer({ open, onClose, store, settingsMap }: SettingsDrawerProps) {
28
  const { data, update } = useFrontmatter(store);
29
  const [citationStyle, setCitationStyle] = useState("apa");
30
+ const [hue, setHue] = useState(47);
31
 
32
  useEffect(() => {
33
  if (!settingsMap) return;
34
  const sync = () => {
35
+ const cit = settingsMap.get("citationStyle");
36
+ if (cit) setCitationStyle(cit as string);
37
+ const h = settingsMap.get("primaryHue");
38
+ if (h !== undefined) setHue(h as number);
39
  };
40
  sync();
41
  settingsMap.observe(sync);
 
131
  </Select>
132
  </FieldGroup>
133
 
134
+ {/* Primary color (hue slider like the template) */}
135
+ <FieldGroup label="Primary color">
136
+ <HueSlider
137
+ hue={hue}
138
+ onChange={(h) => {
139
+ setHue(h);
140
+ settingsMap?.set("primaryHue", h);
141
+ }}
142
+ />
143
+ </FieldGroup>
144
+
145
  <Divider />
146
 
147
  {/* Toggles */}
 
221
  </Box>
222
  );
223
  }
224
+
frontend/src/main.tsx CHANGED
@@ -1,17 +1,34 @@
1
  import React from "react";
2
  import ReactDOM from "react-dom/client";
3
- import { ThemeProvider, CssBaseline } from "@mui/material";
4
- import { theme } from "./theme";
5
  import App from "./App";
6
- import "./styles/tokens.css";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  import "./styles/article.css";
 
8
  import "./styles/editing.css";
9
 
10
  ReactDOM.createRoot(document.getElementById("root")!).render(
11
  <React.StrictMode>
12
- <ThemeProvider theme={theme}>
13
- <CssBaseline />
14
- <App />
15
- </ThemeProvider>
16
  </React.StrictMode>
17
  );
 
1
  import React from "react";
2
  import ReactDOM from "react-dom/client";
3
+ import { CssBaseline } from "@mui/material";
 
4
  import App from "./App";
5
+
6
+ // Template foundation (source of truth - same as research-article-template)
7
+ import "./styles/_variables.css";
8
+ import "./styles/_reset.css";
9
+ import "./styles/_base.css";
10
+ import "./styles/_layout.css";
11
+ import "./styles/_print.css";
12
+ import "./styles/components/_code.css";
13
+ import "./styles/components/_table.css";
14
+ import "./styles/components/_tag.css";
15
+ import "./styles/components/_card.css";
16
+ import "./styles/components/_mermaid.css";
17
+ import "./styles/components/_hero.css";
18
+ import "./styles/components/_toc.css";
19
+ // _button.css and _form.css are NOT imported here: they style bare
20
+ // <button>, <input>, <select>, <label> globally and would override MUI.
21
+ // The publisher injects them in the static HTML where there is no MUI.
22
+
23
+ // Editor extensions
24
+ import "./styles/_editor-tokens.css";
25
  import "./styles/article.css";
26
+ import "./styles/toc.css";
27
  import "./styles/editing.css";
28
 
29
  ReactDOM.createRoot(document.getElementById("root")!).render(
30
  <React.StrictMode>
31
+ <CssBaseline />
32
+ <App />
 
 
33
  </React.StrictMode>
34
  );
frontend/src/styles/_base.css ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200..900;1,200..900&display=swap";
2
+
3
+ /*
4
+ * Template originally sets html { font-size; line-height; background-color;
5
+ * overflow-x }. In the editor we scope font-size/line-height to the article
6
+ * area so MUI portals (Dialog, Menu, Tooltip) keep their own defaults.
7
+ */
8
+
9
+ .content-grid,
10
+ .hero,
11
+ .meta {
12
+ font-size: 16px;
13
+ line-height: 1.6;
14
+ }
15
+
16
+ .content-grid main {
17
+ color: var(--text-color);
18
+ }
19
+
20
+ .content-grid main p {
21
+ margin: 0 0 var(--spacing-3);
22
+ }
23
+
24
+ .content-grid main h2 {
25
+ font-weight: 600;
26
+ font-size: clamp(22px, 2.6vw, 32px);
27
+ line-height: 1.2;
28
+ margin: var(--spacing-10) 0 var(--spacing-5);
29
+ padding-bottom: var(--spacing-2);
30
+ border-bottom: 1px solid var(--border-color);
31
+ }
32
+
33
+ .content-grid main h3 {
34
+ font-weight: 700;
35
+ font-size: clamp(18px, 2.1vw, 22px);
36
+ line-height: 1.25;
37
+ margin: var(--spacing-8) 0 var(--spacing-4);
38
+ }
39
+
40
+ .content-grid main h4 {
41
+ font-weight: 600;
42
+ text-transform: uppercase;
43
+ font-size: 16px;
44
+ line-height: 1.2;
45
+ margin: var(--spacing-6) 0 var(--spacing-4);
46
+ }
47
+
48
+ .content-grid main a {
49
+ color: var(--primary-color);
50
+ text-decoration: none;
51
+ background: var(--surface-bg);
52
+ border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent);
53
+ }
54
+
55
+ .content-grid main a:hover {
56
+ color: var(--primary-color-hover);
57
+ border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent);
58
+ }
59
+
60
+ /* Do not underline heading links inside the article (not the TOC) */
61
+ .content-grid main h2 a,
62
+ .content-grid main h3 a,
63
+ .content-grid main h4 a,
64
+ .content-grid main h5 a,
65
+ .content-grid main h6 a {
66
+ color: inherit;
67
+ background: none;
68
+ border-bottom: none;
69
+ text-decoration: none;
70
+ }
71
+
72
+ .content-grid main h2 a:hover,
73
+ .content-grid main h3 a:hover,
74
+ .content-grid main h4 a:hover,
75
+ .content-grid main h5 a:hover,
76
+ .content-grid main h6 a:hover {
77
+ color: inherit;
78
+ border-bottom: none;
79
+ text-decoration: none;
80
+ }
81
+
82
+ .content-grid main ul,
83
+ .content-grid main ol {
84
+ padding-left: 24px;
85
+ margin: 0 0 var(--spacing-3);
86
+ }
87
+
88
+ .content-grid main li {
89
+ margin-bottom: var(--spacing-2);
90
+ }
91
+
92
+ .content-grid main li:last-child {
93
+ margin-bottom: 0;
94
+ }
95
+
96
+ .content-grid main blockquote {
97
+ border-left: 2px solid var(--border-color);
98
+ padding-left: var(--spacing-4);
99
+ font-style: italic;
100
+ color: var(--muted-color);
101
+ margin: var(--spacing-4) 0;
102
+ }
103
+
104
+ .content-grid main hr {
105
+ border: none;
106
+ border-bottom: 1px solid var(--border-color);
107
+ margin: var(--spacing-5) 0;
108
+ }
109
+
110
+ .muted {
111
+ color: var(--muted-color);
112
+ }
113
+
114
+ [data-footnote-ref] {
115
+ margin-left: 4px;
116
+ }
117
+
118
+ .content-grid main mark {
119
+ background-color: color-mix(in srgb, var(--primary-color, #007AFF) 10%, transparent);
120
+ border: 1px solid color-mix(in srgb, var(--primary-color) 18%, transparent);
121
+ color: inherit;
122
+ padding: 4px 6px;
123
+ border-radius: 4px;
124
+ font-weight: 500;
125
+ box-decoration-break: clone;
126
+ -webkit-box-decoration-break: clone;
127
+ }
128
+
129
+ .feature-grid {
130
+ display: grid;
131
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
132
+ gap: 12px;
133
+ margin: 46px 0;
134
+ }
135
+
136
+ .feature-card {
137
+ display: flex;
138
+ flex-direction: column;
139
+ padding: 16px;
140
+ border: 1px solid color-mix(in srgb, var(--primary-color) 40%, transparent);
141
+ ;
142
+ background: color-mix(in srgb, var(--primary-color, #007AFF) 05%, transparent) !important;
143
+ border-radius: 8px;
144
+ text-decoration: none;
145
+ color: inherit;
146
+ transition: all 0.2s ease;
147
+ }
148
+
149
+ .feature-card:hover {
150
+ transform: translateY(-2px);
151
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
152
+ }
153
+
154
+ .feature-card strong {
155
+ font-size: 14px;
156
+ font-weight: 600;
157
+ color: var(--text-color);
158
+ color: var(--primary-color) !important;
159
+ margin-bottom: 0px !important;
160
+ }
161
+
162
+ .feature-card span {
163
+ font-size: 12px;
164
+ color: var(--muted-color);
165
+ color: var(--primary-color) !important;
166
+ margin-bottom: 0px !important;
167
+ opacity: 1;
168
+ }
169
+
170
+ .katex .tag {
171
+ background: none;
172
+ border: none;
173
+ opacity: 0.4;
174
+ }
frontend/src/styles/_editor-tokens.css ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* -----------------------------------------------------------------------
2
+ Editor-specific design tokens
3
+
4
+ Extends _variables.css with tokens needed only by the collaborative
5
+ editor UI (not the published article). Syntax highlighting, hover
6
+ states, accent variants, shadows, tooltips, etc.
7
+ ----------------------------------------------------------------------- */
8
+
9
+ :root {
10
+ /* Text variants (editor chrome) */
11
+ --text-heading: var(--neutral-900);
12
+ --text-heading-secondary: #222;
13
+ --text-strong: #000;
14
+ --text-faint: #aaa;
15
+
16
+ /* Background variants (editor chrome) */
17
+ --bg-hover: rgba(0, 0, 0, 0.03);
18
+ --bg-overlay: rgba(0, 0, 0, 0.5);
19
+ --bg-tooltip: #ffffff;
20
+ --bg-code-block: #fafafa;
21
+
22
+ /* Border variants */
23
+ --border-light: #eeeeee;
24
+ --border-focus: rgba(124, 58, 237, 0.5);
25
+
26
+ /* Accent (editor interactive elements - links, citations) */
27
+ --accent: #7c3aed;
28
+ --accent-light: #958DF1;
29
+ --accent-bg: rgba(124, 58, 237, 0.08);
30
+ --accent-bg-hover: rgba(124, 58, 237, 0.15);
31
+ --accent-text: #6d28d9;
32
+
33
+ /* Syntax highlighting (One Dark inspired) */
34
+ --code-text: #d63384;
35
+ --code-comment: #6a737d;
36
+ --code-keyword: #8250df;
37
+ --code-string: #0a6640;
38
+ --code-number: #b35900;
39
+ --code-variable: #d63384;
40
+ --code-type: #b35900;
41
+ --code-function: #0550ae;
42
+ --code-tag: #d63384;
43
+ --code-attr: #b35900;
44
+ --code-symbol: #0a6640;
45
+ --code-meta: #0550ae;
46
+ --code-deletion: #d63384;
47
+
48
+ /* Danger */
49
+ --danger-color: #dc2626;
50
+ --danger-bg: rgba(220, 38, 38, 0.08);
51
+ --danger-bg-hover: rgba(220, 38, 38, 0.15);
52
+
53
+ /* Shadows */
54
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.08);
55
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
56
+ --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
57
+
58
+ /* KaTeX */
59
+ --katex-color: #1a1a1a;
60
+ }
61
+
62
+ [data-theme="dark"] {
63
+ --text-heading: #fff;
64
+ --text-heading-secondary: rgba(255, 255, 255, 0.9);
65
+ --text-strong: rgba(255, 255, 255, 0.95);
66
+ --text-faint: rgba(255, 255, 255, 0.25);
67
+
68
+ --bg-hover: rgba(255, 255, 255, 0.03);
69
+ --bg-overlay: rgba(0, 0, 0, 0.5);
70
+ --bg-tooltip: #1e1e2e;
71
+ --bg-code-block: rgba(255, 255, 255, 0.03);
72
+
73
+ --border-light: rgba(255, 255, 255, 0.06);
74
+ --border-focus: rgba(149, 141, 241, 0.5);
75
+
76
+ --accent: #7c3aed;
77
+ --accent-light: #958DF1;
78
+ --accent-bg: rgba(149, 141, 241, 0.15);
79
+ --accent-bg-hover: rgba(149, 141, 241, 0.25);
80
+ --accent-text: #b4aef7;
81
+
82
+ --code-text: #e06c75;
83
+ --code-comment: #5c6370;
84
+ --code-keyword: #c678dd;
85
+ --code-string: #98c379;
86
+ --code-number: #d19a66;
87
+ --code-variable: #e06c75;
88
+ --code-type: #e5c07b;
89
+ --code-function: #61afef;
90
+ --code-tag: #e06c75;
91
+ --code-attr: #d19a66;
92
+ --code-symbol: #56b6c2;
93
+ --code-meta: #61afef;
94
+ --code-deletion: #e06c75;
95
+
96
+ --danger-color: #e06c75;
97
+ --danger-bg: rgba(224, 108, 117, 0.12);
98
+ --danger-bg-hover: rgba(224, 108, 117, 0.25);
99
+
100
+ --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
101
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
102
+ --shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.5);
103
+
104
+ --katex-color: rgba(255, 255, 255, 0.85);
105
+ }
frontend/src/styles/_layout.css ADDED
@@ -0,0 +1,363 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================ */
2
+ /* Layout – 3-column grid (Table of Contents / Article / Aside) */
3
+ /* ============================================================================ */
4
+
5
+ .content-grid {
6
+ max-width: 1280px;
7
+ margin: 0 auto;
8
+ padding: 0 var(--content-padding-x);
9
+ margin-top: 40px;
10
+ display: grid;
11
+ grid-template-columns: 260px minmax(0, 680px) 260px;
12
+ gap: 32px;
13
+ align-items: start;
14
+ }
15
+
16
+ .content-grid>main {
17
+ max-width: 100%;
18
+ margin: 0;
19
+ padding: 0;
20
+ }
21
+
22
+ .content-grid>main>*:first-child {
23
+ margin-top: 0;
24
+ }
25
+
26
+ @media (--bp-content-collapse) {
27
+ .content-grid {
28
+ overflow: hidden;
29
+ display: block;
30
+ margin-top: var(--spacing-2);
31
+ }
32
+
33
+ .content-grid {
34
+ grid-template-columns: 1fr;
35
+ }
36
+
37
+ .table-of-contents {
38
+ position: static;
39
+ display: none;
40
+ }
41
+
42
+ .toc-mobile-toggle {
43
+ display: flex;
44
+ }
45
+
46
+ .toc-mobile-backdrop {
47
+ display: block;
48
+ }
49
+
50
+ .toc-mobile-sidebar {
51
+ display: flex;
52
+ }
53
+
54
+ .footer-inner {
55
+ grid-template-columns: 1fr;
56
+ gap: 16px;
57
+ }
58
+
59
+ .footer-inner>.footer-heading {
60
+ grid-column: auto;
61
+ margin-top: 16px;
62
+ }
63
+
64
+ .footer-inner {
65
+ display: block;
66
+ padding: 40px 16px;
67
+ }
68
+ }
69
+
70
+
71
+
72
+ /* ============================================================================ */
73
+ /* Width helpers – slightly wider than main column, and full-width to viewport */
74
+ /* ---------------------------------------------------------------------------- */
75
+
76
+ .wide,
77
+ .full-width {
78
+ box-sizing: border-box;
79
+ position: relative;
80
+ z-index: var(--z-elevated);
81
+ background-color: var(--page-bg);
82
+ }
83
+
84
+ .wide {
85
+ /* Target up to ~1100px while staying within viewport minus page gutters */
86
+ width: min(1100px, 100vw - var(--content-padding-x) * 4);
87
+ margin-left: 50%;
88
+ transform: translateX(-50%);
89
+ padding: calc(var(--content-padding-x)*4);
90
+ border-radius: calc(var(--button-radius)*4);
91
+ background-color: var(--page-bg);
92
+ -webkit-mask:
93
+ linear-gradient(to right, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%),
94
+ linear-gradient(to bottom, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%);
95
+ -webkit-mask-composite: intersect;
96
+ mask:
97
+ linear-gradient(to right, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%),
98
+ linear-gradient(to bottom, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%);
99
+ mask-composite: intersect;
100
+ }
101
+
102
+ .wide>* {
103
+ margin-bottom: 0 !important;
104
+ }
105
+
106
+ .full-width {
107
+ /* Span the full viewport width and center relative to viewport */
108
+ width: 100vw;
109
+ margin-left: calc(50% - 50vw);
110
+ margin-right: calc(50% - 50vw);
111
+ padding: calc(var(--content-padding-x)*4);
112
+ border-radius: calc(var(--button-radius)*4);
113
+ background-color: var(--page-bg);
114
+ -webkit-mask:
115
+ linear-gradient(to bottom, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%);
116
+ mask:
117
+ linear-gradient(to bottom, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%);
118
+ }
119
+
120
+ .full-width figure figcaption {
121
+ text-align: center !important;
122
+ }
123
+
124
+ @media (--bp-content-collapse) {
125
+
126
+ .wide,
127
+ .full-width {
128
+ width: 100%;
129
+ margin-left: 0;
130
+ margin-right: 0;
131
+ padding: 0;
132
+ transform: none;
133
+ }
134
+ }
135
+
136
+ /* ============================================================================ */
137
+ /* Theme toggle placement */
138
+ /* ============================================================================ */
139
+
140
+ #theme-toggle {
141
+ position: absolute;
142
+ top: var(--spacing-4);
143
+ left: var(--spacing-4);
144
+ margin: 0;
145
+ z-index: var(--z-overlay);
146
+ width: 40px;
147
+ height: 40px;
148
+ border-radius: 50%;
149
+ border: 1px solid var(--border-color);
150
+ background: var(--page-bg);
151
+ box-shadow: 0 2px 12px rgba(0,0,0,.08);
152
+ display: flex;
153
+ align-items: center;
154
+ justify-content: center;
155
+ padding: 0;
156
+ cursor: pointer;
157
+ color: var(--text-color);
158
+ transition: transform 150ms ease, box-shadow 150ms ease;
159
+ }
160
+ #theme-toggle:active {
161
+ transform: scale(0.92);
162
+ }
163
+
164
+ @media (--bp-content-collapse) {
165
+ #theme-toggle {
166
+ display: none;
167
+ }
168
+ }
169
+
170
+ /* ------------------------------------------------------------------------- */
171
+ /* Hero meta bar responsiveness */
172
+ /* Two columns at collapse breakpoint, then one column below small screens */
173
+ /* ------------------------------------------------------------------------- */
174
+ @media (--bp-md) {
175
+ header.meta .meta-container {
176
+ display: flex;
177
+ flex-wrap: wrap;
178
+ row-gap: 12px;
179
+ column-gap: 8px;
180
+ max-width: 100%;
181
+ padding: 0 var(--spacing-4);
182
+ }
183
+
184
+ header.meta .meta-container .meta-container-cell {
185
+ flex: 1 1 calc(50% - 8px);
186
+ min-width: 0;
187
+ }
188
+ }
189
+
190
+ @media (--bp-xxs) {
191
+ header.meta .meta-container .meta-container-cell {
192
+ flex-basis: 100%;
193
+ text-align: center;
194
+ }
195
+
196
+ /* Center ordered list numbers within meta (e.g., affiliations) */
197
+ header.meta .affiliations {
198
+ list-style-position: inside;
199
+ padding-left: 0;
200
+ margin-left: 0;
201
+ }
202
+
203
+ header.meta .affiliations li {
204
+ text-align: center;
205
+ }
206
+ }
207
+
208
+
209
+ /* ------------------------------------------------------------------------- */
210
+ /* D3 neural embed responsiveness */
211
+ /* Stack canvas (left) over network (right) on small screens */
212
+ /* ------------------------------------------------------------------------- */
213
+ @media (--bp-md) {
214
+ .d3-neural .panel {
215
+ flex-direction: column;
216
+ }
217
+
218
+ .d3-neural .panel .left {
219
+ flex: 0 0 auto;
220
+ width: 100%;
221
+ }
222
+
223
+ .d3-neural .panel .right {
224
+ flex: 0 0 auto;
225
+ width: 100%;
226
+ min-width: 0;
227
+ }
228
+ }
229
+
230
+ /* ============================================================================ */
231
+ /* Auto figure numbering */
232
+ /* Applies a CSS counter across all figure-bearing components: */
233
+ /* Reference (.reference-wrapper), Image (.image-wrapper figure), */
234
+ /* and HtmlEmbed (.html-embed) */
235
+ /* ============================================================================ */
236
+
237
+ .content-grid > main {
238
+ counter-reset: figure;
239
+ }
240
+
241
+ /* Increment on every figure-like component */
242
+ .content-grid > main .reference-wrapper,
243
+ .content-grid > main .image-wrapper > figure,
244
+ .content-grid > main figure.html-embed {
245
+ counter-increment: figure;
246
+ }
247
+
248
+ /* Display "Figure N · " before each figcaption */
249
+ .content-grid > main .reference-wrapper .reference__caption::before,
250
+ .content-grid > main .image-wrapper > figure > figcaption::before,
251
+ .content-grid > main figure.html-embed > .html-embed__desc::before {
252
+ content: "Figure " counter(figure) " · ";
253
+ font-weight: 600;
254
+ }
255
+
256
+ /* ============================================================================ */
257
+ /* "paper" template - academic project-page layout (single centered column) */
258
+ /* Activated by frontmatter `template: "paper"` -> data-template="paper" */
259
+ /* Reference: academic project pages like diffusion-cot.github.io */
260
+ /* ============================================================================ */
261
+
262
+ /* ---- Paper hero banner: placed between meta and content in index.astro ---- */
263
+
264
+ .paper-hero-banner {
265
+ max-width: 920px;
266
+ margin: 32px auto 0;
267
+ padding: 0 16px;
268
+ }
269
+
270
+ .paper-hero-banner :global(.html-embed) {
271
+ border-radius: 8px;
272
+ overflow: hidden;
273
+ }
274
+
275
+ .paper-hero-banner :global(.html-embed__card) {
276
+ border-radius: 8px;
277
+ }
278
+
279
+ /* Thin separator between banner area and content */
280
+ [data-template="paper"] .paper-hero-banner + .content-grid {
281
+ margin-top: 48px;
282
+ border-top: 1px solid var(--border-color);
283
+ padding-top: 40px;
284
+ }
285
+
286
+ /* When no banner, separator is directly below meta */
287
+ [data-template="paper"] .meta + .content-grid,
288
+ [data-template="paper"] .hero-paper-meta + .content-grid {
289
+ margin-top: 32px;
290
+ border-top: 1px solid var(--border-color);
291
+ padding-top: 40px;
292
+ }
293
+
294
+ /* ---- Content grid: single centered column, no TOC ---- */
295
+
296
+ [data-template="paper"] .content-grid {
297
+ grid-template-columns: minmax(0, 780px);
298
+ justify-content: center;
299
+ margin-top: 0;
300
+ }
301
+
302
+ [data-template="paper"] .table-of-contents {
303
+ display: none;
304
+ }
305
+
306
+ /* ---- Typography: clean left-aligned headings ---- */
307
+
308
+ [data-template="paper"] .content-grid > main h2 {
309
+ text-align: left;
310
+ font-size: 1.75em;
311
+ font-weight: 700;
312
+ margin-top: 2.5em;
313
+ margin-bottom: 0.8em;
314
+ border-bottom: none;
315
+ padding-bottom: 0;
316
+ }
317
+
318
+ [data-template="paper"] .content-grid > main h2 a {
319
+ background: none;
320
+ }
321
+
322
+ [data-template="paper"] .content-grid > main h2:first-child,
323
+ [data-template="paper"] .content-grid > main > *:first-child h2 {
324
+ margin-top: 0;
325
+ }
326
+
327
+ [data-template="paper"] .content-grid > main h3 {
328
+ font-size: 1.3em;
329
+ font-weight: 600;
330
+ margin-top: 2em;
331
+ margin-bottom: 0.6em;
332
+ }
333
+
334
+ /* ---- No figure numbering ---- */
335
+
336
+ [data-template="paper"] .content-grid > main {
337
+ counter-reset: none;
338
+ }
339
+
340
+ [data-template="paper"] .content-grid > main .reference-wrapper .reference__caption::before,
341
+ [data-template="paper"] .content-grid > main .image-wrapper > figure > figcaption::before,
342
+ [data-template="paper"] .content-grid > main figure.html-embed > .html-embed__desc::before {
343
+ content: none;
344
+ }
345
+
346
+ /* ---- Footer: simple single-column block layout ---- */
347
+
348
+ [data-template="paper"] .footer-inner {
349
+ display: block;
350
+ max-width: 780px;
351
+ }
352
+
353
+ [data-template="paper"] .references-block > .footer-heading,
354
+ [data-template="paper"] .reuse-block > .footer-heading {
355
+ text-align: left;
356
+ padding-right: 0;
357
+ margin-top: 16px;
358
+ margin-bottom: 8px;
359
+ }
360
+
361
+ [data-template="paper"] .template-credit p {
362
+ margin-top: 32px;
363
+ }
frontend/src/styles/_print.css ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================ */
2
+ /* Print styles */
3
+ /* ========================================================================= */
4
+ @media print {
5
+
6
+ html,
7
+ body {
8
+ background: #fff;
9
+ }
10
+
11
+ /* Margins handled by Playwright; avoid extra global margins */
12
+ body {
13
+ margin: 0;
14
+ }
15
+
16
+ /* Keep the banner (hero), hide non-essential UI elements */
17
+ #theme-toggle {
18
+ display: none !important;
19
+ }
20
+
21
+ /* Links: remove underline and background */
22
+ .content-grid main a {
23
+ text-decoration: none;
24
+ background: none;
25
+ border-bottom: 1px solid rgba(0, 0, 0, .2);
26
+ }
27
+
28
+ /* Avoid breaks inside complex blocks */
29
+ .content-grid main pre,
30
+ .content-grid main blockquote,
31
+ .content-grid main table,
32
+ .content-grid main figure {
33
+ break-inside: avoid;
34
+ page-break-inside: avoid;
35
+ }
36
+
37
+ /* Soft page breaks around main headings */
38
+ .content-grid main h2 {
39
+ page-break-before: auto;
40
+ page-break-after: avoid;
41
+ break-after: avoid-page;
42
+ }
43
+
44
+ /* Small icon labels not needed when printing */
45
+ .code-lang-chip {
46
+ display: none !important;
47
+ }
48
+
49
+ /* Adjust more contrasty colors for print */
50
+ :root {
51
+ --surface-bg: #fff;
52
+ --border-color: rgba(0, 0, 0, .2);
53
+ --link-underline: rgba(0, 0, 0, .3);
54
+ --link-underline-hover: rgba(0, 0, 0, .4);
55
+ }
56
+
57
+ /* Force single column to reduce widows/orphans and awkward breaks */
58
+ .content-grid {
59
+ grid-template-columns: 1fr !important;
60
+ }
61
+
62
+ .table-of-contents,
63
+ .right-aside,
64
+ .toc-mobile-toggle,
65
+ .toc-mobile-backdrop,
66
+ .toc-mobile-sidebar {
67
+ display: none !important;
68
+ }
69
+
70
+ main>nav:first-of-type {
71
+ display: none !important;
72
+ }
73
+
74
+ /* Avoid page breaks inside complex visual blocks */
75
+ .hero,
76
+ .hero-banner,
77
+ .html-embed__card,
78
+ .js-plotly-plot,
79
+ figure,
80
+ pre,
81
+ table,
82
+ blockquote,
83
+ .wide,
84
+ .full-width,
85
+ .stack > *,
86
+ .card,
87
+ .palettes,
88
+ .palette-card,
89
+ .color-picker,
90
+ .note {
91
+ break-inside: avoid;
92
+ page-break-inside: avoid;
93
+ }
94
+
95
+ /* Prefer keeping header+lead together */
96
+ .hero {
97
+ page-break-after: avoid;
98
+ }
99
+
100
+ /* Center the hero banner and constrain all its children generically */
101
+ .hero-banner {
102
+ width: 100% !important;
103
+ max-width: 980px !important;
104
+ margin-left: auto !important;
105
+ margin-right: auto !important;
106
+ overflow: hidden;
107
+ }
108
+
109
+ .hero-banner > *,
110
+ .hero-banner svg,
111
+ .hero-banner canvas,
112
+ .hero-banner img,
113
+ .hero-banner video {
114
+ max-width: 100% !important;
115
+ width: 100% !important;
116
+ height: auto !important;
117
+ margin-left: auto !important;
118
+ margin-right: auto !important;
119
+ display: block;
120
+ }
121
+ }
122
+
123
+
124
+ @media print {
125
+ .meta-container-cell--pdf {
126
+ display: none !important;
127
+ }
128
+ }
frontend/src/styles/_reset.css ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ html { box-sizing: border-box; }
2
+ *, *::before, *::after { box-sizing: inherit; }
3
+ body { margin: 0; }
4
+ audio { display: block; width: 100%; }
5
+
6
+ img,
7
+ picture {
8
+ max-width: 100%;
9
+ height: auto;
10
+ display: block;
11
+ position: relative;
12
+ z-index: var(--z-elevated);
13
+ }
14
+
15
+ /*
16
+ * Article-specific font and color are scoped to the article area
17
+ * to avoid overriding MUI theme on portals (Dialog, Menu, Tooltip).
18
+ * The template's original `body { font-family; color }` is moved here.
19
+ */
20
+ .content-grid,
21
+ .hero,
22
+ .meta {
23
+ font-family: var(--default-font-family);
24
+ color: var(--text-color);
25
+ }
frontend/src/styles/_variables.css ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================ */
2
+ /* Design Tokens */
3
+ /* ============================================================================ */
4
+ :root {
5
+ /* Neutrals */
6
+ --neutral-900: rgb(17, 24, 39);
7
+ --neutral-600: rgb(107, 114, 128);
8
+ --neutral-400: rgb(185, 185, 185);
9
+ --neutral-300: rgb(228, 228, 228);
10
+ --neutral-200: rgb(245, 245, 245);
11
+ --neutral-50: rgb(249, 250, 251);
12
+
13
+ --default-font-family: Source Sans Pro, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
14
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
15
+
16
+ /* Brand (OKLCH base + derived states) */
17
+ --primary-base: oklch(0.75 0.12 47);
18
+ --primary-color: var(--primary-base);
19
+ --primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h);
20
+ --primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
21
+ --on-primary: #ffffff;
22
+
23
+ /* Semantic colors */
24
+ --danger-color: oklch(0.65 0.19 25);
25
+ --success-color: oklch(0.65 0.15 145);
26
+ --info-color: oklch(0.65 0.12 230);
27
+
28
+ /* Text & Surfaces */
29
+ --page-bg: #fff;
30
+ --surface-bg: #f9f9f9;
31
+ --text-color: rgba(0, 0, 0, .85);
32
+ --transparent-page-contrast: rgba(255, 255, 255, .85);
33
+ --muted-color: rgba(0, 0, 0, .6);
34
+ --border-color: rgba(0, 0, 0, .1);
35
+ --code-bg: #f6f8fa;
36
+
37
+ /* Links */
38
+ --link-underline: var(--primary-color);
39
+ --link-underline-hover: var(--primary-color-hover);
40
+
41
+ /* Spacing scale */
42
+ --spacing-1: 8px;
43
+ --spacing-2: 12px;
44
+ --spacing-3: 16px;
45
+ --spacing-4: 24px;
46
+ --spacing-5: 32px;
47
+ --spacing-6: 40px;
48
+ --spacing-7: 48px;
49
+ --spacing-8: 56px;
50
+ --spacing-9: 64px;
51
+ --spacing-10: 72px;
52
+
53
+ /* Custom Media aliases compiled by PostCSS */
54
+ @custom-media --bp-xxs (max-width: 320px);
55
+ @custom-media --bp-xs (max-width: 480px);
56
+ @custom-media --bp-sm (max-width: 640px);
57
+ @custom-media --bp-md (max-width: 768px);
58
+ @custom-media --bp-lg (max-width: 1024px);
59
+ @custom-media --bp-xl (max-width: 1280px);
60
+ @custom-media --bp-content-collapse (max-width: 1100px);
61
+
62
+ /* Layout */
63
+ --content-padding-x: 16px;
64
+ /* default page gutter */
65
+ --block-spacing-y: var(--spacing-4);
66
+ /* default vertical spacing between block components */
67
+
68
+ /* Config */
69
+ --palette-count: 8;
70
+
71
+ /* Button tokens */
72
+ --button-radius: 6px;
73
+ --button-padding-x: 16px;
74
+ --button-padding-y: 10px;
75
+ --button-font-size: 14px;
76
+ --button-icon-padding: 8px;
77
+ /* Big button */
78
+ --button-big-padding-x: 16px;
79
+ --button-big-padding-y: 12px;
80
+ --button-big-font-size: 16px;
81
+ --button-big-icon-padding: 12px;
82
+
83
+ /* Table tokens */
84
+ --table-border-radius: 8px;
85
+ --table-header-bg: oklch(from var(--surface-bg) calc(l - 0.02) c h);
86
+ --table-row-odd-bg: oklch(from var(--surface-bg) calc(l - 0.01) c h);
87
+
88
+ /* Z-index */
89
+ --z-base: 0;
90
+ --z-content: 1;
91
+ --z-elevated: 10;
92
+ --z-overlay: 1000;
93
+ --z-modal: 1100;
94
+ --z-tooltip: 1200;
95
+
96
+ /* Charts (global) */
97
+ --axis-color: var(--muted-color);
98
+ --tick-color: var(--text-color);
99
+ --grid-color: rgba(0, 0, 0, .08);
100
+ }
101
+
102
+ /* ============================================================================ */
103
+ /* Dark Theme Overrides */
104
+ /* ============================================================================ */
105
+ [data-theme="dark"] {
106
+ --page-bg: #0f1115;
107
+ --surface-bg: #07080a;
108
+ --text-color: rgba(255, 255, 255, .9);
109
+ --muted-color: rgba(255, 255, 255, .7);
110
+ --border-color: rgba(255, 255, 255, .15);
111
+ --code-bg: #12151b;
112
+ --transparent-page-contrast: rgba(0, 0, 0, .85);
113
+
114
+ /* Charts (global) */
115
+ --axis-color: var(--muted-color);
116
+ --tick-color: var(--muted-color);
117
+ --grid-color: rgba(255, 255, 255, .10);
118
+
119
+ /* Primary (lower L in dark) */
120
+ --primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h);
121
+ --primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
122
+ --on-primary: #0f1115;
123
+
124
+ color-scheme: dark;
125
+ background: var(--page-bg);
126
+ }
frontend/src/styles/article.css CHANGED
@@ -8,10 +8,10 @@
8
  /* Base typography */
9
  .tiptap {
10
  outline: none;
11
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
12
  font-size: 1rem;
13
  line-height: 1.7;
14
- color: var(--text-primary);
15
  max-width: 780px;
16
  margin-left: auto;
17
  margin-right: auto;
@@ -51,7 +51,7 @@
51
  .tiptap h3 {
52
  font-size: 1.2rem;
53
  font-weight: 600;
54
- color: var(--text-primary);
55
  line-height: 1.4;
56
  margin: 1.5em 0 0.3em;
57
  }
@@ -78,10 +78,10 @@
78
 
79
  /* Blockquote */
80
  .tiptap blockquote {
81
- border-left: 3px solid var(--border);
82
  padding-left: 1rem;
83
  margin: 1em 0;
84
- color: var(--text-secondary);
85
  }
86
 
87
  .tiptap blockquote p {
@@ -91,7 +91,7 @@
91
  /* Inline code */
92
  .tiptap code {
93
  font-family: "SFMono-Regular", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
94
- background: var(--bg-code);
95
  padding: 0.15em 0.4em;
96
  border-radius: 4px;
97
  font-size: 0.88em;
@@ -111,7 +111,7 @@
111
  .tiptap pre code {
112
  background: none;
113
  padding: 0;
114
- color: var(--text-secondary);
115
  font-size: 0.875rem;
116
  line-height: 1.6;
117
  }
@@ -196,7 +196,7 @@
196
  /* Horizontal rule */
197
  .tiptap hr {
198
  border: none;
199
- border-top: 1px solid var(--border);
200
  margin: 2em 0;
201
  }
202
 
@@ -220,8 +220,8 @@
220
  }
221
 
222
  .tiptap th {
223
- background: var(--bg-code);
224
- color: var(--text-secondary);
225
  font-weight: 600;
226
  font-size: 0.8125rem;
227
  text-transform: uppercase;
@@ -229,26 +229,27 @@
229
  }
230
 
231
  .tiptap td {
232
- color: var(--text-primary);
233
  }
234
 
235
  .tiptap tr:hover td {
236
  background: var(--bg-hover);
237
  }
238
 
239
- /* Links */
240
  .tiptap a,
241
  .tiptap .editor-link {
242
- color: var(--accent-light);
243
- text-decoration: underline;
244
- text-decoration-color: rgba(149, 141, 241, 0.4);
245
  text-underline-offset: 2px;
246
- transition: text-decoration-color 0.15s;
247
  }
248
 
249
  .tiptap a:hover,
250
  .tiptap .editor-link:hover {
251
- text-decoration-color: rgba(149, 141, 241, 0.8);
 
252
  }
253
 
254
  /* Bold */
@@ -273,7 +274,7 @@
273
  }
274
 
275
  .tiptap [data-type="inline-math"] .tiptap-mathematics-render:hover {
276
- background: var(--bg-code);
277
  }
278
 
279
  /* Math - block */
@@ -322,7 +323,7 @@
322
  left: 50%;
323
  transform: translateX(-50%);
324
  background: var(--bg-tooltip);
325
- border: 1px solid var(--border);
326
  border-radius: 8px;
327
  padding: 0.75rem;
328
  min-width: 240px;
@@ -342,7 +343,7 @@
342
  }
343
 
344
  .citation-tooltip-journal {
345
- color: var(--text-secondary);
346
  font-size: 0.75rem;
347
  font-style: italic;
348
  line-height: 1.3;
@@ -380,7 +381,7 @@
380
  .bibliography-block {
381
  margin: 2em 0 1em;
382
  padding-top: 1.5em;
383
- border-top: 1px solid var(--border);
384
  }
385
 
386
  .bibliography-title {
@@ -399,7 +400,7 @@
399
  .bibliography-content {
400
  font-size: 0.875rem;
401
  line-height: 1.7;
402
- color: var(--text-secondary);
403
  }
404
 
405
  .bibliography-content .csl-entry {
@@ -436,7 +437,7 @@
436
  left: 50%;
437
  transform: translateX(-50%);
438
  background: var(--bg-tooltip);
439
- border: 1px solid var(--border);
440
  border-radius: 8px;
441
  padding: 10px 14px;
442
  min-width: 200px;
@@ -454,7 +455,7 @@
454
 
455
  .glossary-tooltip-def {
456
  font-size: 12px;
457
- color: var(--text-secondary);
458
  line-height: 1.5;
459
  }
460
 
@@ -493,7 +494,7 @@
493
  left: 50%;
494
  transform: translateX(-50%);
495
  background: var(--bg-tooltip);
496
- border: 1px solid var(--border);
497
  border-radius: 8px;
498
  padding: 10px 14px;
499
  min-width: 220px;
@@ -504,7 +505,7 @@
504
 
505
  .footnote-tooltip-content {
506
  font-size: 12px;
507
- color: var(--text-secondary);
508
  line-height: 1.5;
509
  }
510
 
 
8
  /* Base typography */
9
  .tiptap {
10
  outline: none;
11
+ font-family: var(--default-font-family);
12
  font-size: 1rem;
13
  line-height: 1.7;
14
+ color: var(--text-color);
15
  max-width: 780px;
16
  margin-left: auto;
17
  margin-right: auto;
 
51
  .tiptap h3 {
52
  font-size: 1.2rem;
53
  font-weight: 600;
54
+ color: var(--text-color);
55
  line-height: 1.4;
56
  margin: 1.5em 0 0.3em;
57
  }
 
78
 
79
  /* Blockquote */
80
  .tiptap blockquote {
81
+ border-left: 3px solid var(--border-color);
82
  padding-left: 1rem;
83
  margin: 1em 0;
84
+ color: var(--muted-color);
85
  }
86
 
87
  .tiptap blockquote p {
 
91
  /* Inline code */
92
  .tiptap code {
93
  font-family: "SFMono-Regular", "Fira Code", "Fira Mono", Menlo, Consolas, monospace;
94
+ background: var(--code-bg);
95
  padding: 0.15em 0.4em;
96
  border-radius: 4px;
97
  font-size: 0.88em;
 
111
  .tiptap pre code {
112
  background: none;
113
  padding: 0;
114
+ color: var(--muted-color);
115
  font-size: 0.875rem;
116
  line-height: 1.6;
117
  }
 
196
  /* Horizontal rule */
197
  .tiptap hr {
198
  border: none;
199
+ border-top: 1px solid var(--border-color);
200
  margin: 2em 0;
201
  }
202
 
 
220
  }
221
 
222
  .tiptap th {
223
+ background: var(--code-bg);
224
+ color: var(--muted-color);
225
  font-weight: 600;
226
  font-size: 0.8125rem;
227
  text-transform: uppercase;
 
229
  }
230
 
231
  .tiptap td {
232
+ color: var(--text-color);
233
  }
234
 
235
  .tiptap tr:hover td {
236
  background: var(--bg-hover);
237
  }
238
 
239
+ /* Links - aligned with template (_base.css .content-grid main a) */
240
  .tiptap a,
241
  .tiptap .editor-link {
242
+ color: var(--primary-color);
243
+ text-decoration: none;
244
+ border-bottom: 1px solid color-mix(in srgb, var(--primary-color) 40%, transparent);
245
  text-underline-offset: 2px;
246
+ transition: border-color 0.15s;
247
  }
248
 
249
  .tiptap a:hover,
250
  .tiptap .editor-link:hover {
251
+ color: var(--primary-color-hover);
252
+ border-bottom-color: color-mix(in srgb, var(--primary-color) 70%, transparent);
253
  }
254
 
255
  /* Bold */
 
274
  }
275
 
276
  .tiptap [data-type="inline-math"] .tiptap-mathematics-render:hover {
277
+ background: var(--code-bg);
278
  }
279
 
280
  /* Math - block */
 
323
  left: 50%;
324
  transform: translateX(-50%);
325
  background: var(--bg-tooltip);
326
+ border: 1px solid var(--border-color);
327
  border-radius: 8px;
328
  padding: 0.75rem;
329
  min-width: 240px;
 
343
  }
344
 
345
  .citation-tooltip-journal {
346
+ color: var(--muted-color);
347
  font-size: 0.75rem;
348
  font-style: italic;
349
  line-height: 1.3;
 
381
  .bibliography-block {
382
  margin: 2em 0 1em;
383
  padding-top: 1.5em;
384
+ border-top: 1px solid var(--border-color);
385
  }
386
 
387
  .bibliography-title {
 
400
  .bibliography-content {
401
  font-size: 0.875rem;
402
  line-height: 1.7;
403
+ color: var(--muted-color);
404
  }
405
 
406
  .bibliography-content .csl-entry {
 
437
  left: 50%;
438
  transform: translateX(-50%);
439
  background: var(--bg-tooltip);
440
+ border: 1px solid var(--border-color);
441
  border-radius: 8px;
442
  padding: 10px 14px;
443
  min-width: 200px;
 
455
 
456
  .glossary-tooltip-def {
457
  font-size: 12px;
458
+ color: var(--muted-color);
459
  line-height: 1.5;
460
  }
461
 
 
494
  left: 50%;
495
  transform: translateX(-50%);
496
  background: var(--bg-tooltip);
497
+ border: 1px solid var(--border-color);
498
  border-radius: 8px;
499
  padding: 10px 14px;
500
  min-width: 220px;
 
505
 
506
  .footnote-tooltip-content {
507
  font-size: 12px;
508
+ color: var(--muted-color);
509
  line-height: 1.5;
510
  }
511
 
frontend/src/styles/components/_button.css ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ button, .button {
2
+ appearance: none;
3
+ background: linear-gradient(15deg, var(--primary-color) 0%, var(--primary-color-hover) 35%);
4
+ color: white;
5
+ border: 1px solid transparent;
6
+ border-radius: var(--button-radius);
7
+ padding: var(--button-padding-y) var(--button-padding-x);
8
+ font-size: var(--button-font-size);
9
+ line-height: 1;
10
+ cursor: pointer;
11
+ display: inline-block;
12
+ text-decoration: none;
13
+ transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease;
14
+ }
15
+ /* Icon-only buttons: equal X/Y padding */
16
+ button:has(> svg:only-child),
17
+ .button:has(> svg:only-child) {
18
+ padding: var(--button-icon-padding);
19
+ }
20
+ button:hover, .button:hover {
21
+ filter: brightness(96%);
22
+ }
23
+ button:active, .button:active {
24
+ transform: translateY(1px);
25
+ }
26
+ button:focus-visible, .button:focus-visible {
27
+ outline: none;
28
+ }
29
+ button:disabled, .button:disabled {
30
+ opacity: .6;
31
+ cursor: not-allowed;
32
+ }
33
+
34
+ /* Ghost/Muted button: subtle outline, primary color text/border */
35
+ .button--ghost {
36
+ background: transparent !important;
37
+ color: var(--primary-color) !important;
38
+ border-color: var(--primary-color) !important;
39
+ }
40
+ .button--ghost:hover {
41
+ color: var(--primary-color-hover) !important;
42
+ border-color: var(--primary-color-hover) !important;
43
+ filter: none;
44
+ }
45
+
46
+ /* Big button: larger padding and font size */
47
+ .button.button--big {
48
+ padding: var(--button-big-padding-y) var(--button-big-padding-x);
49
+ font-size: var(--button-big-font-size);
50
+ }
51
+ .button.button--big:has(> svg:only-child) {
52
+ padding: var(--button-big-icon-padding);
53
+ }
54
+
55
+ .button-group .button {
56
+ margin: 5px;
57
+ }
58
+
frontend/src/styles/components/_card.css ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .card {
2
+ background: var(--surface-bg);
3
+ border: 1px solid var(--border-color) !important;
4
+ border-radius: 12px;
5
+ padding: var(--spacing-3);
6
+ z-index: calc(var(--z-elevated) + 1);
7
+ position: relative;
8
+ margin-bottom: 0;
9
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
10
+ transition: all 0.2s ease;
11
+ overflow: hidden;
12
+ text-decoration: none;
13
+ color: inherit;
14
+ display: block;
15
+ }
16
+
17
+ .card:hover {
18
+ text-decoration: none;
19
+ color: inherit;
20
+ }
21
+
22
+ .card.no-padding,
23
+ .card.card--p0 {
24
+ padding: 0;
25
+ }
26
+
27
+ .card:hover {
28
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
29
+ }
30
+
31
+
32
+ .card figcaption {
33
+ color: var(--text-color) !important;
34
+ font-weight: 600 !important;
35
+ font-size: 1rem !important;
36
+ text-align: center !important;
37
+ padding: var(--spacing-3) !important;
38
+ margin: 0 !important;
39
+ }
40
+
41
+ .card .image-wrapper,
42
+ .card .image-wrapper figure {
43
+ margin: 0 !important;
44
+ width: 100% !important;
45
+ display: block !important;
46
+ }
47
+
48
+ .card img {
49
+ border-radius: 8px 8px 0 0 !important;
50
+ width: 100% !important;
51
+ max-width: 100% !important;
52
+ height: 120px !important;
53
+ object-fit: cover !important;
54
+ display: block !important;
55
+ border-bottom: 1px solid var(--border-color) !important;
56
+ transition: all 0.2s ease;
57
+ }
58
+
59
+ .card.template-card img {
60
+ height: auto !important;
61
+ object-fit: contain !important;
62
+ }
63
+
64
+
65
+ .card h3,
66
+ .card h4,
67
+ .card h5 {
68
+ margin-top: 0 !important;
69
+ font-weight: 900 !important;
70
+ width: 100%;
71
+ }
72
+
73
+ .card::after {
74
+ display: none !important;
75
+ }
76
+
77
+ .card-title-container {
78
+ padding: var(--spacing-3) var(--spacing-4) var(--spacing-4) var(--spacing-4);
79
+ }
80
+
81
+ .card-title {
82
+ color: var(--text-color);
83
+ font-size: 0.9rem;
84
+ margin: 0 0 var(--spacing-1) 0 !important;
85
+ font-weight: 600;
86
+ line-height: 1.4;
87
+ white-space: normal;
88
+ }
89
+
90
+ .card-subtitle {
91
+ color: var(--muted-color);
92
+ font-size: 0.75rem;
93
+ margin: 0 !important;
94
+ line-height: 1.5;
95
+ white-space: normal;
96
+ }
97
+
98
+ .card-cta {
99
+ flex: 1;
100
+ min-width: 0;
101
+ display: flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ background: var(--surface-bg);
105
+ border: 2px dashed var(--border-color) !important;
106
+ }
107
+
108
+ .card-cta:hover {
109
+ transform: none;
110
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
111
+ border: 2px dashed var(--border-color) !important;
112
+ }
113
+
114
+ .card-cta-content {
115
+ text-align: center;
116
+ padding: var(--spacing-4);
117
+ }
118
+
119
+ .card-cta h3,
120
+ .card-cta h4,
121
+ .card-cta h5 {
122
+ color: var(--text-color);
123
+ font-weight: 900 !important;
124
+ margin-bottom: var(--spacing-1) !important;
125
+ }
126
+
127
+ .card-cta p {
128
+ color: var(--muted-color);
129
+ font-size: 0.9rem;
130
+ margin: 0 !important;
131
+ }
132
+
133
+ @media print {
134
+ .card,
135
+ .card-cta {
136
+ box-shadow: none;
137
+ }
138
+ }
frontend/src/styles/components/_code.css ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================ */
2
+ /* Inline code */
3
+ /* ============================================================================ */
4
+ code {
5
+ font-size: 14px;
6
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
7
+ border-radius: 0.3em;
8
+ border: 1px solid var(--border-color);
9
+ color: var(--text-color);
10
+ font-weight: 400;
11
+ line-height: 1.5;
12
+ }
13
+
14
+ p code,
15
+ .note code {
16
+ white-space: nowrap;
17
+ padding: calc(var(--spacing-1)/3) calc(var(--spacing-1)/2);
18
+ }
19
+
20
+
21
+ /* ============================================================================ */
22
+ /* Shiki code blocks */
23
+ /* ============================================================================ */
24
+ .astro-code {
25
+ position: relative;
26
+ border: 1px solid var(--border-color);
27
+ background-color: var(--surface-bg) !important;
28
+ border-radius: 6px;
29
+ padding: 0;
30
+ font-size: 14px;
31
+ --code-gutter-width: 2.5em;
32
+ }
33
+
34
+ /* Shared sizing & horizontal scroll for code containers */
35
+ .astro-code,
36
+ section.content-grid pre {
37
+ width: 100%;
38
+ max-width: 100%;
39
+ box-sizing: border-box;
40
+ -webkit-overflow-scrolling: touch;
41
+ padding: 0;
42
+ margin-bottom: var(--block-spacing-y) !important;
43
+ overflow-x: auto;
44
+ }
45
+
46
+ section.content-grid pre.astro-code {
47
+ margin: 0;
48
+ padding: var(--spacing-1) 0;
49
+ }
50
+
51
+ section.content-grid pre code {
52
+ display: inline-block;
53
+ min-width: 100%;
54
+ }
55
+
56
+ /* Plain-text blocks: always wrap lines (prompts, prose, etc.) */
57
+ .astro-code[data-language="text"] {
58
+ white-space: pre-wrap;
59
+ overflow-wrap: anywhere;
60
+ word-break: break-word;
61
+ }
62
+
63
+ .astro-code[data-language="text"] .line {
64
+ display: block;
65
+ }
66
+
67
+ /* Wrap long lines only on small screens to prevent layout overflow */
68
+ @media (--bp-content-collapse) {
69
+
70
+ .astro-code,
71
+ section.content-grid pre {
72
+ white-space: pre-wrap;
73
+ overflow-wrap: anywhere;
74
+ word-break: break-word;
75
+ }
76
+
77
+ section.content-grid pre code {
78
+ white-space: pre-wrap;
79
+ display: block;
80
+ min-width: 0;
81
+ }
82
+ }
83
+
84
+ /* Themes */
85
+ [data-theme='light'] .astro-code {
86
+ background-color: var(--code-bg);
87
+ }
88
+
89
+ /* Apply token color from per-span vars exposed by Shiki dual themes */
90
+ [data-theme='light'] .astro-code span {
91
+ color: var(--shiki-light) !important;
92
+ }
93
+
94
+ [data-theme='dark'] .astro-code span {
95
+ color: var(--shiki-dark) !important;
96
+ }
97
+
98
+ /* Optional: boost contrast for light theme */
99
+ [data-theme='light'] .astro-code {
100
+ --shiki-foreground: #24292f;
101
+ --shiki-background: #ffffff;
102
+ }
103
+
104
+ /* Line numbers for Shiki-rendered code blocks */
105
+ .astro-code code {
106
+ counter-reset: astro-code-line;
107
+ display: block;
108
+ background: none;
109
+ border: none;
110
+ }
111
+
112
+ .astro-code .line {
113
+ display: inline-block;
114
+ position: relative;
115
+ padding-left: calc(var(--code-gutter-width) + var(--spacing-1));
116
+ min-height: 1.25em;
117
+ }
118
+
119
+ .astro-code .line::before {
120
+ counter-increment: astro-code-line;
121
+ content: counter(astro-code-line);
122
+ position: absolute;
123
+ left: 0;
124
+ top: 0;
125
+ bottom: 0;
126
+ width: calc(var(--code-gutter-width));
127
+ text-align: right;
128
+ color: var(--muted-color);
129
+ opacity: .3;
130
+ user-select: none;
131
+ padding-right: var(--spacing-2);
132
+ border-right: 1px solid var(--border-color);
133
+ }
134
+
135
+ .astro-code .line:empty::after {
136
+ content: "\00a0";
137
+ }
138
+
139
+ /* Hide trailing empty line added by parsers */
140
+ .astro-code code>.line:last-child:empty {
141
+ display: none;
142
+ }
143
+
144
+ /* ============================================================================ */
145
+ /* Non-Shiki pre wrapper (rehype) */
146
+ /* ============================================================================ */
147
+ .code-card {
148
+ position: relative;
149
+ }
150
+
151
+ .code-card .code-copy {
152
+ position: absolute;
153
+ top: var(--spacing-2);
154
+ right: var(--spacing-2);
155
+ z-index: 3;
156
+ display: none;
157
+ }
158
+
159
+ .code-card:hover .code-copy {
160
+ display: block;
161
+ }
162
+
163
+ .code-card .code-copy svg {
164
+ width: 16px;
165
+ height: 16px;
166
+ display: block;
167
+ fill: currentColor;
168
+ }
169
+
170
+ .code-card pre {
171
+ margin: 0 0 var(--spacing-1);
172
+ }
173
+
174
+ /* When no copy button (single-line), keep the label in the top-right corner */
175
+ .code-card.no-copy::after {
176
+ top: 8px;
177
+ right: 8px;
178
+ }
179
+
180
+ /* ============================================================================ */
181
+ /* Contextual overrides */
182
+ /* ============================================================================ */
183
+ /* Inside Accordions: remove padding and border on code containers */
184
+ .accordion .astro-code {
185
+ padding: 0;
186
+ border: none;
187
+ }
188
+
189
+ .accordion .astro-code {
190
+ margin-bottom: 0 !important;
191
+ }
192
+
193
+ .accordion .code-output {
194
+ border: none;
195
+ border-top: 1px solid var(--border-color) !important;
196
+ }
197
+
198
+ .accordion pre {
199
+ margin-bottom: 0 !important;
200
+ }
201
+
202
+ .accordion .code-card pre {
203
+ margin: 0 !important;
204
+ }
205
+
206
+ /* ============================================================================ */
207
+ /* Language/extension vignette (bottom-right, discreet) */
208
+ /* ============================================================================ */
209
+ /* .astro-code::after {
210
+ content: attr(data-language);
211
+ position: absolute;
212
+ right: 0;
213
+ bottom: 0;
214
+ font-size: 10px;
215
+ line-height: 1;
216
+ text-transform: uppercase;
217
+ color: var(--muted-color);
218
+ background: var(--surface-bg);
219
+ border-top: 1px solid var(--border-color);
220
+ border-left: 1px solid var(--border-color);
221
+ opacity: 1;
222
+ border-radius: 8px 0 0 0;
223
+ padding: 4px 6px;
224
+ pointer-events: none;
225
+ } */
226
+
227
+ /* Fallback if Shiki uses data-lang instead of data-language */
228
+ /* .astro-code[data-lang]::after { content: attr(data-lang); }
229
+ .astro-code[data-language="typescript"]::after { content: "ts"; }
230
+ .astro-code[data-language="tsx"]::after { content: "tsx"; }
231
+ .astro-code[data-language="javascript"]::after,
232
+ .astro-code[data-language="node"]::after,
233
+ .astro-code[data-language="jsx"]::after { content: "js"; }
234
+ .astro-code[data-language="python"]::after { content: "py"; }
235
+ .astro-code[data-language="bash"]::after,
236
+ .astro-code[data-language="shell"]::after,
237
+ .astro-code[data-language="sh"]::after { content: "sh"; }
238
+ .astro-code[data-language="markdown"]::after { content: "md"; }
239
+ .astro-code[data-language="yaml"]::after,
240
+ .astro-code[data-language="yml"]::after { content: "yml"; }
241
+ .astro-code[data-language="html"]::after { content: "html"; }
242
+ .astro-code[data-language="css"]::after { content: "css"; }
243
+ .astro-code[data-language="json"]::after { content: "json"; } */
244
+
245
+ /* In Accordions, keep same bottom-right placement */
246
+ .accordion .astro-code::after {
247
+ right: 0;
248
+ bottom: 0;
249
+ }
250
+
251
+ /* ============================================================================ */
252
+ /* Results blocks glued to code blocks */
253
+ /* ============================================================================ */
254
+ .code-output {
255
+ position: relative;
256
+ background: oklch(from var(--code-bg) calc(l - 0.005) c h);
257
+ border: 1px solid var(--border-color);
258
+ border-radius: 6px;
259
+ margin-top: 0;
260
+ margin-bottom: var(--block-spacing-y);
261
+ padding: 0 !important;
262
+ }
263
+
264
+ .code-output pre {
265
+ padding: calc(var(--spacing-3) + 6px) var(--spacing-3) var(--spacing-3) var(--spacing-3) !important;
266
+ }
267
+
268
+ /* If immediately following a code container, keep tight visual connection */
269
+ .code-card+.code-output,
270
+ .astro-code+.code-output,
271
+ section.content-grid pre+.code-output {
272
+ margin-top: 0;
273
+ border-top: none;
274
+ border-top-left-radius: 0;
275
+ border-top-right-radius: 0;
276
+ box-shadow: inset 0 8px 12px -12px rgba(0, 0, 0, 0.15);
277
+ }
278
+
279
+ /* Remove bottom margin on code when immediately followed by results */
280
+ .astro-code:has(+ .code-output) {
281
+ margin-bottom: 0 !important;
282
+ }
283
+
284
+ .code-card:has(+ .code-output) .astro-code {
285
+ margin-bottom: 0 !important;
286
+ }
287
+
288
+ section.content-grid pre:has(+ .code-output) {
289
+ margin-bottom: 0 !important;
290
+ }
291
+
292
+ /* Remove bottom border radius on code when followed by results */
293
+ .astro-code:has(+ .code-output) {
294
+ border-bottom-left-radius: 0;
295
+ border-bottom-right-radius: 0;
296
+ }
297
+
298
+ .code-card:has(+ .code-output) .astro-code {
299
+ border-bottom-left-radius: 0;
300
+ border-bottom-right-radius: 0;
301
+ }
302
+
303
+ section.content-grid pre:has(+ .code-output) {
304
+ border-bottom-left-radius: 0;
305
+ border-bottom-right-radius: 0;
306
+ }
307
+
308
+ /* Small top-left label */
309
+ .code-output::before {
310
+ content: "Output";
311
+ position: absolute;
312
+ top: 0;
313
+ right: 0;
314
+ font-size: 10px;
315
+ line-height: 1;
316
+ color: var(--muted-color);
317
+ text-transform: uppercase;
318
+ letter-spacing: 0.04em;
319
+ border-top: none;
320
+ border-right: none;
321
+ border-radius: 0 0 0 6px;
322
+ padding: 10px 10px;
323
+ }
324
+
325
+ /* Very subtle top shadow so results feels under the code */
326
+ .code-output {}
327
+
328
+ .code-output> :where(*):first-child {
329
+ margin-top: 0 !important;
330
+ }
331
+
332
+ .code-output> :where(*):last-child {
333
+ margin-bottom: 0 !important;
334
+ }
335
+
336
+ /* ============================================================================ */
337
+ /* Optional filename tag above code blocks */
338
+ /* ============================================================================ */
339
+ .code-filename {
340
+ display: inline-block;
341
+ font-size: 12px;
342
+ line-height: 1;
343
+ color: var(--muted-color);
344
+ background: var(--surface-bg);
345
+ border: 1px solid var(--border-color);
346
+ border-bottom: none;
347
+ border-radius: 6px 6px 0 0;
348
+ padding: 4px 8px;
349
+ margin: 0;
350
+ }
351
+
352
+ .code-filename+.code-card .astro-code,
353
+ .code-filename+.astro-code,
354
+ .code-filename+section.content-grid pre {
355
+ border-top-left-radius: 0;
356
+ border-top-right-radius: 6px;
357
+ }
frontend/src/styles/components/_form.css ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================ */
2
+ /* Form Elements - Modern Minimal Design */
3
+ /* ============================================================================ */
4
+
5
+ /* Select styling with modern chevron */
6
+ select {
7
+ background-color: var(--page-bg);
8
+ border: 1px solid color-mix(in srgb, var(--primary-color) 50%, var(--border-color));
9
+ border-radius: var(--button-radius);
10
+ padding: var(--button-padding-y) calc(var(--button-padding-x)*2) var(--button-padding-y) var(--button-padding-x);
11
+ font-family: var(--default-font-family);
12
+ font-size: var(--button-font-size);
13
+ color: var(--text-color);
14
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E");
15
+ background-repeat: no-repeat;
16
+ background-position: right calc(var(--button-padding-x) + 2px) center;
17
+ background-size: 12px;
18
+ cursor: pointer;
19
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
20
+ -webkit-appearance: none;
21
+ -moz-appearance: none;
22
+ appearance: none;
23
+ }
24
+
25
+ select:hover,
26
+ select:focus,
27
+ select:active {
28
+ border-color: var(--primary-color);
29
+ }
30
+
31
+ select:focus {
32
+ outline: none;
33
+ border-color: var(--primary-color);
34
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
35
+ }
36
+
37
+ select:disabled {
38
+ opacity: 0.6;
39
+ cursor: not-allowed;
40
+ background-color: var(--surface-bg);
41
+ }
42
+
43
+ /* Dark theme select chevron */
44
+ [data-theme="dark"] select {
45
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23bbb' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E");
46
+ }
47
+
48
+ /* Checkbox styling */
49
+ input[type="checkbox"] {
50
+ appearance: none;
51
+ width: 16px;
52
+ height: 16px;
53
+ border: 2px solid var(--border-color);
54
+ border-radius: 3px;
55
+ background-color: var(--page-bg);
56
+ cursor: pointer;
57
+ position: relative;
58
+ transition: all 0.2s ease;
59
+ margin-right: var(--spacing-2);
60
+ }
61
+
62
+ input[type="checkbox"]:hover {
63
+ border-color: var(--primary-color);
64
+ }
65
+
66
+ input[type="checkbox"]:focus {
67
+ outline: none;
68
+ border-color: var(--primary-color);
69
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
70
+ }
71
+
72
+ input[type="checkbox"]:checked {
73
+ background-color: var(--primary-color);
74
+ border-color: var(--primary-color);
75
+ }
76
+
77
+ input[type="checkbox"]:checked::before {
78
+ content: '';
79
+ position: absolute;
80
+ top: 1px;
81
+ left: 4px;
82
+ width: 4px;
83
+ height: 8px;
84
+ border: solid var(--on-primary);
85
+ border-width: 0 2px 2px 0;
86
+ transform: rotate(45deg);
87
+ }
88
+
89
+ input[type="checkbox"]:disabled {
90
+ opacity: 0.6;
91
+ cursor: not-allowed;
92
+ }
93
+
94
+ /* Radio button styling */
95
+ input[type="radio"] {
96
+ appearance: none;
97
+ width: 16px;
98
+ height: 16px;
99
+ border: 2px solid var(--border-color);
100
+ border-radius: 50%;
101
+ background-color: var(--page-bg);
102
+ cursor: pointer;
103
+ position: relative;
104
+ transition: all 0.2s ease;
105
+ margin-right: var(--spacing-2);
106
+ }
107
+
108
+ input[type="radio"]:hover {
109
+ border-color: var(--primary-color);
110
+ }
111
+
112
+ input[type="radio"]:focus {
113
+ outline: none;
114
+ border-color: var(--primary-color);
115
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
116
+ }
117
+
118
+ input[type="radio"]:checked {
119
+ border-color: var(--primary-color);
120
+ }
121
+
122
+ input[type="radio"]:checked::before {
123
+ content: '';
124
+ position: absolute;
125
+ top: 2px;
126
+ left: 2px;
127
+ width: 8px;
128
+ height: 8px;
129
+ border-radius: 50%;
130
+ background-color: var(--primary-color);
131
+ }
132
+
133
+ input[type="radio"]:disabled {
134
+ opacity: 0.6;
135
+ cursor: not-allowed;
136
+ }
137
+
138
+ /* Input text styling for consistency */
139
+ input[type="text"],
140
+ input[type="email"],
141
+ input[type="password"],
142
+ input[type="number"],
143
+ input[type="url"],
144
+ input[type="search"],
145
+ textarea {
146
+ appearance: none;
147
+ background-color: var(--page-bg);
148
+ border: 1px solid var(--border-color);
149
+ border-radius: var(--button-radius);
150
+ padding: var(--button-padding-y) var(--button-padding-x);
151
+ font-family: var(--default-font-family);
152
+ font-size: var(--button-font-size);
153
+ color: var(--text-color);
154
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
155
+ width: 100%;
156
+ }
157
+
158
+ input[type="text"]:hover,
159
+ input[type="email"]:hover,
160
+ input[type="password"]:hover,
161
+ input[type="number"]:hover,
162
+ input[type="url"]:hover,
163
+ input[type="search"]:hover,
164
+ textarea:hover {
165
+ border-color: var(--primary-color);
166
+ }
167
+
168
+ input[type="text"]:focus,
169
+ input[type="email"]:focus,
170
+ input[type="password"]:focus,
171
+ input[type="number"]:focus,
172
+ input[type="url"]:focus,
173
+ input[type="search"]:focus,
174
+ textarea:focus {
175
+ outline: none;
176
+ border-color: var(--primary-color);
177
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
178
+ }
179
+
180
+ input[type="text"]:disabled,
181
+ input[type="email"]:disabled,
182
+ input[type="password"]:disabled,
183
+ input[type="number"]:disabled,
184
+ input[type="url"]:disabled,
185
+ input[type="search"]:disabled,
186
+ textarea:disabled {
187
+ opacity: 0.6;
188
+ cursor: not-allowed;
189
+ background-color: var(--surface-bg);
190
+ }
191
+
192
+ /* Label styling */
193
+ label {
194
+ display: flex;
195
+ align-items: center;
196
+ font-size: var(--button-font-size);
197
+ color: var(--text-color);
198
+ cursor: pointer;
199
+ margin-bottom: 0;
200
+ line-height: 1.4;
201
+ user-select: none;
202
+ }
203
+
204
+ /* Form group spacing */
205
+ .form-group {
206
+ margin-bottom: var(--spacing-4);
207
+ display: flex;
208
+ align-items: center;
209
+ gap: var(--spacing-2);
210
+ }
211
+
212
+ .form-group label {
213
+ margin-bottom: 0;
214
+ }
215
+
216
+ /* Alternative: for vertical form groups */
217
+ .form-group.vertical {
218
+ flex-direction: column;
219
+ align-items: flex-start;
220
+ }
221
+
222
+ .form-group.vertical label {
223
+ margin-bottom: var(--spacing-1);
224
+ }
225
+
226
+ /* For inline form elements */
227
+ .form-inline {
228
+ display: flex;
229
+ align-items: center;
230
+ gap: var(--spacing-2);
231
+ margin-bottom: var(--spacing-3);
232
+ }
233
+
234
+ .form-inline label {
235
+ margin-bottom: 0;
236
+ }
237
+
238
+ /* Ensure labels in flex containers don't break alignment */
239
+ div[style*="display: flex"] label,
240
+ div[class*="flex"] label {
241
+ margin-bottom: 0 !important;
242
+ align-self: center;
243
+ }
frontend/src/styles/components/_hero.css ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================ */
2
+ /* Hero + Meta bar */
3
+ /* Extracted from HeroArticle.astro scoped styles. */
4
+ /* Shared between editor (FrontmatterHero.tsx) and publisher (html-renderer). */
5
+ /* ============================================================================ */
6
+
7
+ /* ---- Hero section ---- */
8
+
9
+ .hero {
10
+ width: 100%;
11
+ padding: 64px 16px 16px;
12
+ text-align: center;
13
+ }
14
+
15
+ .hero-title {
16
+ font-size: clamp(34px, 5vw, 54px);
17
+ font-weight: 800;
18
+ line-height: 1.12;
19
+ letter-spacing: -0.02em;
20
+ margin: 0 auto 8px;
21
+ max-width: 780px;
22
+ text-wrap: balance;
23
+ }
24
+
25
+ .hero-title[data-title-size="md"] {
26
+ font-size: clamp(28px, 4vw, 44px);
27
+ }
28
+
29
+ .hero-title[data-title-size="sm"] {
30
+ font-size: clamp(24px, 3.2vw, 38px);
31
+ }
32
+
33
+ .hero-banner {
34
+ max-width: 980px;
35
+ margin: 0 auto;
36
+ aspect-ratio: 5 / 2;
37
+ overflow: hidden;
38
+ }
39
+
40
+ .hero-desc {
41
+ color: var(--muted-color);
42
+ font-style: italic;
43
+ margin: 0 auto 16px;
44
+ max-width: 55%;
45
+ }
46
+
47
+ @media (max-width: 768px) {
48
+ .hero-banner { aspect-ratio: auto; }
49
+ .hero-desc { max-width: 90%; }
50
+ }
51
+
52
+ /* ---- Meta bar ---- */
53
+
54
+ .meta {
55
+ border-top: 1px solid var(--border-color);
56
+ border-bottom: 1px solid var(--border-color);
57
+ padding: 1rem 0;
58
+ font-size: 0.9rem;
59
+ }
60
+
61
+ .meta-container {
62
+ max-width: 980px;
63
+ display: flex;
64
+ flex-direction: row;
65
+ justify-content: space-between;
66
+ margin: 0 auto;
67
+ padding: 0 var(--content-padding-x, 16px);
68
+ gap: 8px;
69
+ flex-wrap: wrap;
70
+ row-gap: 12px;
71
+ }
72
+
73
+ .meta-container a:not(.button) {
74
+ color: var(--primary-color);
75
+ text-decoration: underline;
76
+ text-underline-offset: 2px;
77
+ text-decoration-thickness: 0.06em;
78
+ text-decoration-color: var(--link-underline);
79
+ transition: text-decoration-color 0.15s ease-in-out;
80
+ }
81
+
82
+ .meta-container a:hover {
83
+ text-decoration-color: var(--link-underline-hover);
84
+ }
85
+
86
+ .meta-container-cell {
87
+ display: flex;
88
+ flex-direction: column;
89
+ gap: 8px;
90
+ max-width: 400px;
91
+ }
92
+
93
+ .meta-container-cell h3 {
94
+ margin: 0;
95
+ font-size: 12px;
96
+ font-weight: 400;
97
+ color: var(--muted-color);
98
+ text-transform: uppercase;
99
+ letter-spacing: 0.02em;
100
+ }
101
+
102
+ .meta-container-cell p {
103
+ margin: 0;
104
+ }
105
+
106
+ .authors {
107
+ margin: 0;
108
+ list-style-type: none;
109
+ padding-left: 0;
110
+ display: flex;
111
+ flex-wrap: wrap;
112
+ }
113
+
114
+ .authors li {
115
+ white-space: nowrap;
116
+ padding: 0;
117
+ }
118
+
119
+ .affiliations {
120
+ margin: 0;
121
+ padding-left: 1.25em;
122
+ }
123
+
124
+ .affiliations li {
125
+ margin: 0;
126
+ }
127
+
128
+ @media (max-width: 768px) {
129
+ .meta-container-cell:nth-child(even) { text-align: right; }
130
+ .meta-container-cell:last-child:nth-child(odd) {
131
+ flex-grow: 0;
132
+ flex-basis: auto;
133
+ margin-left: auto;
134
+ text-align: right;
135
+ }
136
+ }
frontend/src/styles/components/_mermaid.css ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .mermaid .nodeLabel p {
2
+ color: var(--text-color) !important;
3
+ }
4
+
5
+ .mermaid .cluster-label .nodeLabel {
6
+ color: var(--text-color) !important;
7
+ }
8
+
9
+ /* Styles pour le texte des subgraphs/clusters - gérer les foreignObject */
10
+ .mermaid .cluster-label text,
11
+ .mermaid .cluster-label .nodeLabel text,
12
+ .mermaid .cluster-label foreignObject,
13
+ .mermaid .cluster-label foreignObject div,
14
+ .mermaid .cluster-label foreignObject span {
15
+ color: var(--text-color) !important;
16
+ fill: var(--text-color) !important;
17
+ }
18
+
19
+ /* Styles spécifiques pour les clusters/subgraphs */
20
+ .mermaid .cluster rect {
21
+ fill: transparent !important;
22
+ stroke: var(--border-color) !important;
23
+ stroke-width: 1px !important;
24
+ }
25
+
26
+ .mermaid .cluster-label {
27
+ color: var(--text-color) !important;
28
+ fill: var(--text-color) !important;
29
+ }
30
+
31
+ /* Masquer le flicker pendant la conversion */
32
+ .mermaid-zoom-wrapper.converting {
33
+ opacity: 0.7;
34
+ transition: opacity 0.2s ease;
35
+ }
36
+
37
+ .mermaid-zoom-wrapper.converting .mermaid {
38
+ opacity: 0.7;
39
+ transition: opacity 0.2s ease;
40
+ }
41
+
42
+ /* Assurer que les diagrammes Mermaid prennent toute la largeur */
43
+ .mermaid {
44
+ width: 100% !important;
45
+ max-width: 100% !important;
46
+ display: block !important;
47
+ }
48
+
49
+ /* Styles pour les SVG Mermaid */
50
+ .mermaid svg {
51
+ width: 100% !important;
52
+ max-width: 100% !important;
53
+ height: auto !important;
54
+ }
55
+
56
+ /* Styles pour les wrappers de zoom Mermaid */
57
+ .mermaid-zoom-wrapper {
58
+ display: block !important;
59
+ width: 100% !important;
60
+ max-width: 100% !important;
61
+ cursor: zoom-in;
62
+ position: relative;
63
+ }
64
+
65
+ .mermaid-zoom-wrapper .mermaid {
66
+ width: 100% !important;
67
+ max-width: 100% !important;
68
+ display: block !important;
69
+ }
70
+
71
+ .mermaid-zoom-wrapper .mermaid svg {
72
+ width: 100% !important;
73
+ max-width: 100% !important;
74
+ height: auto !important;
75
+ display: block !important;
76
+ }
77
+
78
+ /* Styles pour les nœuds avec bords arrondis et bordure interne */
79
+ .mermaid rect:not(.flowchart-link),
80
+ .mermaid .node rect,
81
+ .mermaid .nodeLabel rect,
82
+ .mermaid .cluster rect {
83
+ rx: 8px !important;
84
+ ry: 8px !important;
85
+ stroke: color-mix(in srgb, var(--text-color) 20%, transparent) !important;
86
+ stroke-width: 1px !important;
87
+ /* Décalage pour créer l'effet de bordure interne */
88
+ }
89
+
90
+ /* Styles pour les diagrammes Mermaid zoomables */
91
+ .mermaid-zoom-wrapper:hover {
92
+ opacity: 0.95;
93
+ transition: opacity 0.2s ease;
94
+ }
95
+
96
+ /* Appliquer le même style que les images zoomables */
97
+ .mermaid-zoom-wrapper[data-zoomable="1"] {
98
+ cursor: zoom-in;
99
+ }
100
+
101
+ /* Styles pour le mode zoom ouvert */
102
+ .medium-zoom--opened .mermaid-zoom-wrapper {
103
+ cursor: zoom-out;
104
+ }
105
+
106
+ /* Exclure les images Mermaid du filtre d'inversion en mode sombre */
107
+ /* Le filtre global s'applique à .medium-zoom-image--opened */
108
+ /* Nous devons être plus spécifique pour les images Mermaid */
109
+
110
+ /* Exception pour les images Mermaid - pas de filtre d'inversion */
111
+ :global([data-theme="dark"]) .mermaid-zoom-wrapper img:global(.medium-zoom-image--opened) {
112
+ filter: none !important;
113
+ }
114
+
115
+ /* Alternative: cibler directement l'image dans le wrapper Mermaid */
116
+ :global([data-theme="dark"]) .mermaid-zoom-wrapper :global(.medium-zoom-image--opened) {
117
+ filter: none !important;
118
+ }
119
+
120
+ /* Encore plus spécifique: cibler l'image générée par notre script */
121
+ :global([data-theme="dark"]) .mermaid-zoom-wrapper img[data-zoomable="1"]:global(.medium-zoom-image--opened) {
122
+ filter: none !important;
123
+ }
124
+
125
+ /* Forcer un z-index très élevé pour medium-zoom (au-dessus de tout) */
126
+ :global(.medium-zoom-overlay) {
127
+ z-index: 9999999 !important;
128
+ }
129
+
130
+ :global(.medium-zoom-image--opened) {
131
+ z-index: 10000000 !important;
132
+ }
133
+
134
+ /* Cibler la classe spécifique mermaid-zoom-image */
135
+ :global([data-theme="dark"]) .mermaid-zoom-image:global(.medium-zoom-image--opened) {
136
+ filter: none !important;
137
+ }
138
+
139
+ /* Overlay medium-zoom avec z-index très élevé pour Mermaid */
140
+ :global(.medium-zoom-overlay):has(.mermaid-zoom-wrapper) {
141
+ z-index: 9999999 !important;
142
+ }
143
+
144
+ /* Z-index très élevé pour les images Mermaid */
145
+ :global(.mermaid-zoom-image):global(.medium-zoom-image--opened) {
146
+ z-index: 10000000 !important;
147
+ }
148
+
149
+ :global(.mermaid-zoom-wrapper):has(:global(.medium-zoom-image--opened)) {
150
+ z-index: 10000000 !important;
151
+ }
152
+
153
+ /* Masquer les autres diagrammes quand un est zoomé */
154
+ :global(.medium-zoom--opened) .mermaid-zoom-wrapper {
155
+ opacity: 0;
156
+ z-index: calc(var(--z-base) - 1);
157
+ transition: opacity 0.3s ease;
158
+ }
159
+
160
+ /* Le diagramme zoomé reste visible */
161
+ :global(.medium-zoom--opened) .mermaid-zoom-wrapper:has(.medium-zoom--opened) {
162
+ opacity: 1;
163
+ z-index: var(--z-overlay);
164
+ }
165
+
166
+ /* Fallback pour les navigateurs sans support :has() */
167
+ :global(.medium-zoom--opened) .mermaid-zoom-wrapper.zoom-active {
168
+ opacity: 1 !important;
169
+ z-index: var(--z-overlay) !important;
170
+ }
171
+
172
+ /* Styles spécifiques pour différents types de diagrammes */
173
+
174
+ /* Flowchart/Graph - styles généraux */
175
+ .mermaid .node rect,
176
+ .mermaid .node circle,
177
+ .mermaid .node ellipse,
178
+ .mermaid .label rect {
179
+ rx: 8px !important;
180
+ ry: 8px !important;
181
+ }
182
+
183
+ /* Flowchart - nœuds principaux */
184
+ .mermaid .flowchart-node rect,
185
+ .mermaid .flowchart-label rect {
186
+ rx: 8px !important;
187
+ ry: 8px !important;
188
+ }
189
+
190
+ /* Sequence diagram */
191
+ .mermaid .actor rect,
192
+ .mermaid .participant rect {
193
+ rx: 8px !important;
194
+ ry: 8px !important;
195
+ }
196
+
197
+ /* ER diagram */
198
+ .mermaid .entityBox rect,
199
+ .mermaid .er .entityBox rect {
200
+ rx: 6px !important;
201
+ ry: 6px !important;
202
+ }
203
+
204
+ /* Gantt chart */
205
+ .mermaid .section0 rect,
206
+ .mermaid .section1 rect,
207
+ .mermaid .section2 rect,
208
+ .mermaid .section3 rect {
209
+ rx: 4px !important;
210
+ ry: 4px !important;
211
+ }
212
+
213
+ /* Gitgraph */
214
+ .mermaid .commit rect {
215
+ rx: 12px !important;
216
+ ry: 12px !important;
217
+ }
218
+
219
+ /* Mindmap */
220
+ .mermaid .mindmap-node rect {
221
+ rx: 10px !important;
222
+ ry: 10px !important;
223
+ }
224
+
225
+ /* Sankey */
226
+ .mermaid .sankey .node rect {
227
+ rx: 4px !important;
228
+ ry: 4px !important;
229
+ }
230
+
231
+ /* Timeline */
232
+ .mermaid .timeline rect {
233
+ rx: 6px !important;
234
+ ry: 6px !important;
235
+ }
236
+
237
+ /* Pie chart */
238
+ .mermaid .pieTitleText {
239
+ rx: 8px !important;
240
+ ry: 8px !important;
241
+ }
242
+
243
+ /* Journey */
244
+ .mermaid .journey .section0 rect,
245
+ .mermaid .journey .section1 rect,
246
+ .mermaid .journey .section2 rect,
247
+ .mermaid .journey .section3 rect {
248
+ rx: 8px !important;
249
+ ry: 8px !important;
250
+ }
frontend/src/styles/components/_table.css ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .content-grid main table {
2
+ border-collapse: collapse;
3
+ table-layout: auto;
4
+ margin: 0;
5
+ background-color: var(--surface-bg);
6
+ }
7
+
8
+ .content-grid main th,
9
+ .content-grid main td {
10
+ border-bottom: 1px solid var(--border-color);
11
+ padding: 6px 8px;
12
+ font-size: 15px;
13
+ white-space: nowrap;
14
+ /* prevent squashing; allow horizontal scroll instead */
15
+ word-break: auto-phrase;
16
+ /* white-space: break-spaces; */
17
+ vertical-align: top;
18
+ }
19
+
20
+ .content-grid main thead th {
21
+ border-bottom: 1px solid var(--border-color);
22
+ }
23
+
24
+ .content-grid main thead th {
25
+ border-bottom: 1px solid var(--border-color);
26
+ }
27
+
28
+ .content-grid main thead th {
29
+ background: var(--table-header-bg);
30
+ padding-top: 10px;
31
+ padding-bottom: 10px;
32
+ font-weight: 600;
33
+ }
34
+
35
+ .content-grid main hr {
36
+ border: none;
37
+ border-bottom: 1px solid var(--border-color);
38
+ margin: var(--spacing-5) 0;
39
+ }
40
+
41
+ /* Scroll wrapper: keeps table 100% width but enables horizontal scroll when needed */
42
+ .content-grid main .table-scroll {
43
+ width: 100%;
44
+ overflow-x: auto;
45
+ -webkit-overflow-scrolling: touch;
46
+ border: 1px solid var(--border-color);
47
+ border-radius: var(--table-border-radius);
48
+ background: var(--surface-bg);
49
+ margin: 0 0 var(--block-spacing-y);
50
+ }
51
+
52
+ .content-grid main .table-scroll>table {
53
+ width: fit-content;
54
+ min-width: 100%;
55
+ max-width: none;
56
+ }
57
+
58
+ /* Vertical dividers between columns (no outer right border) */
59
+ .content-grid main .table-scroll>table th,
60
+ .content-grid main .table-scroll>table td {
61
+ border-right: 1px solid var(--border-color);
62
+ }
63
+
64
+ .content-grid main .table-scroll>table th:last-child,
65
+ .content-grid main .table-scroll>table td:last-child {
66
+ border-right: none;
67
+ }
68
+
69
+ .content-grid main .table-scroll>table thead th:first-child {
70
+ border-top-left-radius: var(--table-border-radius);
71
+ }
72
+
73
+ .content-grid main .table-scroll>table thead th:last-child {
74
+ border-top-right-radius: var(--table-border-radius);
75
+ }
76
+
77
+ .content-grid main .table-scroll>table tbody tr:last-child td:first-child {
78
+ border-bottom-left-radius: var(--table-border-radius);
79
+ }
80
+
81
+ .content-grid main .table-scroll>table tbody tr:last-child td:last-child {
82
+ border-bottom-right-radius: var(--table-border-radius);
83
+ }
84
+
85
+ /* Zebra striping for odd rows */
86
+ .content-grid main .table-scroll>table tbody tr:nth-child(odd) td {
87
+ background: var(--table-row-odd-bg);
88
+ }
89
+
90
+ /* Remove bottom border on last row */
91
+ .content-grid main .table-scroll>table tbody tr:last-child td {
92
+ border-bottom: none;
93
+ }
94
+
95
+ /* Accordion context: remove outer borders/radius and fit content flush */
96
+ .accordion .accordion__content .table-scroll {
97
+ border: none;
98
+ border-radius: 0;
99
+ margin: 0;
100
+ margin-bottom: 0 !important;
101
+ }
102
+
103
+ /* Ensure no bottom margin even if table isn't wrapped (fallback) */
104
+ .accordion .accordion__content table {
105
+ margin: 0 !important;
106
+ }
107
+
108
+ .accordion .accordion__content .table-scroll>table thead th:first-child,
109
+ .accordion .accordion__content .table-scroll>table thead th:last-child,
110
+ .accordion .accordion__content .table-scroll>table tbody tr:last-child td:first-child,
111
+ .accordion .accordion__content .table-scroll>table tbody tr:last-child td:last-child {
112
+ border-radius: 0;
113
+ }
114
+
115
+ /* Fallback for browsers without fit-content support */
116
+ @supports not (width: fit-content) {
117
+ .content-grid main .table-scroll>table {
118
+ width: max-content;
119
+ min-width: 100%;
120
+ }
121
+ }
frontend/src/styles/components/_tag.css ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .tag-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 16px; }
2
+
3
+ .tag {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ gap: 6px;
7
+ padding: 8px 12px;
8
+ font-size: 12px;
9
+ line-height: 1;
10
+ border-radius: var(--button-radius);
11
+ background: var(--surface-bg);
12
+ border: 1px solid var(--border-color);
13
+ color: var(--text-color);
14
+ }
frontend/src/styles/components/_toc.css ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ============================================================================ */
2
+ /* Table of Contents */
3
+ /* Extracted from TableOfContents.astro scoped styles. */
4
+ /* Shared between editor and publisher. */
5
+ /* ============================================================================ */
6
+
7
+ .table-of-contents {
8
+ position: sticky;
9
+ top: 32px;
10
+ margin-top: 12px;
11
+ }
12
+
13
+ .table-of-contents nav {
14
+ border-left: 1px solid var(--border-color);
15
+ padding-left: 16px;
16
+ font-size: 13px;
17
+ }
18
+
19
+ .table-of-contents .title {
20
+ font-weight: 600;
21
+ font-size: 14px;
22
+ margin-bottom: 8px;
23
+ }
24
+
25
+ .table-of-contents nav ul {
26
+ margin: 0 0 6px;
27
+ padding-left: 1em;
28
+ }
29
+
30
+ .table-of-contents nav li {
31
+ list-style: none;
32
+ margin: 0.25em 0;
33
+ }
34
+
35
+ .table-of-contents nav a,
36
+ .table-of-contents nav a:link,
37
+ .table-of-contents nav a:visited {
38
+ color: var(--text-color);
39
+ text-decoration: none;
40
+ border-bottom: none;
41
+ background: none;
42
+ cursor: pointer;
43
+ }
44
+
45
+ .table-of-contents nav > ul > li > a {
46
+ font-weight: 700;
47
+ }
48
+
49
+ .table-of-contents nav a:hover {
50
+ text-decoration: underline solid var(--muted-color);
51
+ }
52
+
53
+ .table-of-contents nav a.active {
54
+ text-decoration: underline;
55
+ }
frontend/src/styles/editing.css CHANGED
@@ -6,6 +6,43 @@
6
  block handles, slash menu, upload UI, panels, etc.
7
  ----------------------------------------------------------------------- */
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  /* Math editing */
10
  .tiptap [data-type="inline-math"] {
11
  cursor: pointer;
@@ -19,7 +56,7 @@
19
  font-family: "SFMono-Regular", "Fira Code", monospace;
20
  font-size: 0.85em;
21
  color: var(--code-text);
22
- background: var(--bg-code);
23
  padding: 0.1em 0.4em;
24
  border-radius: 3px;
25
  outline: none;
@@ -60,7 +97,7 @@
60
 
61
  .comment-mark.resolved {
62
  background: transparent;
63
- border-bottom: 1px dashed var(--border);
64
  }
65
 
66
  /* Collaboration cursors */
@@ -99,7 +136,7 @@
99
 
100
  /* Image upload card */
101
  .image-upload-card {
102
- border: 2px dashed var(--border);
103
  border-radius: 12px;
104
  padding: 2.5rem 1.5rem;
105
  margin: 1em 0;
@@ -112,7 +149,7 @@
112
  }
113
 
114
  .image-upload-card:hover {
115
- border-color: var(--text-tertiary);
116
  background: var(--bg-hover);
117
  }
118
 
@@ -151,13 +188,13 @@
151
  }
152
 
153
  .image-upload-btn.secondary {
154
- background: var(--bg-code);
155
- color: var(--text-secondary);
156
  }
157
 
158
  .image-upload-btn.secondary:hover {
159
- background: var(--bg-code);
160
- color: var(--text-primary);
161
  }
162
 
163
  .image-upload-card-hint {
@@ -178,10 +215,10 @@
178
  .image-upload-url-input {
179
  width: 100%;
180
  padding: 0.5rem 0.75rem;
181
- border: 1px solid var(--border);
182
  border-radius: 6px;
183
- background: var(--bg-code);
184
- color: var(--text-primary);
185
  font-size: 0.8125rem;
186
  font-family: inherit;
187
  outline: none;
@@ -235,8 +272,8 @@
235
  }
236
 
237
  .block-handle-btn:hover {
238
- color: var(--text-secondary);
239
- background: var(--bg-code);
240
  }
241
 
242
  .block-handle-grip {
@@ -250,7 +287,7 @@
250
  /* Slash menu */
251
  .slash-menu {
252
  background: var(--bg-surface);
253
- border: 1px solid var(--border);
254
  border-radius: 10px;
255
  padding: 0.35rem;
256
  min-width: 220px;
@@ -270,14 +307,14 @@
270
  background: none;
271
  width: 100%;
272
  text-align: left;
273
- color: var(--text-primary);
274
  font-size: 0.875rem;
275
  font-family: inherit;
276
  }
277
 
278
  .slash-menu-item:hover,
279
  .slash-menu-item.is-selected {
280
- background: var(--bg-code);
281
  }
282
 
283
  .slash-menu-item-icon {
@@ -287,8 +324,8 @@
287
  align-items: center;
288
  justify-content: center;
289
  border-radius: 6px;
290
- background: var(--bg-code);
291
- color: var(--text-secondary);
292
  font-size: 0.9rem;
293
  flex-shrink: 0;
294
  }
@@ -300,22 +337,22 @@
300
 
301
  .slash-menu-item-title {
302
  font-weight: 500;
303
- color: var(--text-primary);
304
  font-size: 0.875rem;
305
  }
306
 
307
  .slash-menu-item-desc {
308
  font-size: 0.75rem;
309
- color: var(--text-tertiary);
310
  }
311
 
312
  /* Footnote editing controls */
313
  .footnote-tooltip-input {
314
  width: 100%;
315
- background: var(--bg-code);
316
- border: 1px solid var(--border);
317
  border-radius: 4px;
318
- color: var(--text-primary);
319
  font-size: 12px;
320
  padding: 6px 8px;
321
  resize: vertical;
@@ -369,7 +406,7 @@
369
 
370
  .citation-panel {
371
  background: var(--bg-surface);
372
- border: 1px solid var(--border);
373
  border-radius: 12px;
374
  width: 480px;
375
  max-height: 80vh;
@@ -397,7 +434,7 @@
397
  .citation-panel-close {
398
  background: none;
399
  border: none;
400
- color: var(--text-tertiary);
401
  font-size: 1.4rem;
402
  cursor: pointer;
403
  padding: 0;
@@ -405,7 +442,7 @@
405
  }
406
 
407
  .citation-panel-close:hover {
408
- color: var(--text-primary);
409
  }
410
 
411
  .citation-panel-body {
@@ -419,10 +456,10 @@
419
  .citation-input {
420
  width: 100%;
421
  padding: 0.55rem 0.75rem;
422
- border: 1px solid var(--border);
423
  border-radius: 6px;
424
- background: var(--bg-code);
425
- color: var(--text-primary);
426
  font-size: 0.8125rem;
427
  font-family: inherit;
428
  outline: none;
@@ -504,7 +541,7 @@
504
  }
505
 
506
  .citation-library-item:hover {
507
- background: var(--bg-code);
508
  }
509
 
510
  .citation-library-info {
@@ -516,13 +553,13 @@
516
  }
517
 
518
  .citation-library-meta {
519
- color: var(--text-secondary);
520
  font-size: 0.75rem;
521
  font-weight: 500;
522
  }
523
 
524
  .citation-library-title {
525
- color: var(--text-tertiary);
526
  font-size: 0.75rem;
527
  white-space: nowrap;
528
  overflow: hidden;
@@ -554,3 +591,37 @@
554
  .citation-library-insert:hover {
555
  background: var(--accent-bg-hover);
556
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  block handles, slash menu, upload UI, panels, etc.
7
  ----------------------------------------------------------------------- */
8
 
9
+ /* ---- Editor grid overrides ---- */
10
+ /* The template _layout.css defines .content-grid for the published
11
+ 3-column layout (260px toc | 680px article | 260px).
12
+ The editor uses a different column ratio for the editing UI. */
13
+
14
+ .editor-app .content-grid {
15
+ grid-template-columns: 200px 1fr 240px;
16
+ grid-template-rows: auto 1fr;
17
+ max-width: none;
18
+ margin-top: 0;
19
+ padding: 0;
20
+ gap: 0;
21
+ }
22
+
23
+ .content-grid__hero {
24
+ grid-column: 2;
25
+ grid-row: 1;
26
+ }
27
+
28
+ .content-grid__toc {
29
+ grid-column: 1;
30
+ grid-row: 2;
31
+ }
32
+
33
+ .content-grid__editor {
34
+ grid-column: 2;
35
+ grid-row: 2;
36
+ padding-top: 24px;
37
+ padding-bottom: 32px;
38
+ }
39
+
40
+ .content-grid__comments {
41
+ grid-column: 3;
42
+ grid-row: 2;
43
+ padding: 16px 0 16px 16px;
44
+ }
45
+
46
  /* Math editing */
47
  .tiptap [data-type="inline-math"] {
48
  cursor: pointer;
 
56
  font-family: "SFMono-Regular", "Fira Code", monospace;
57
  font-size: 0.85em;
58
  color: var(--code-text);
59
+ background: var(--code-bg);
60
  padding: 0.1em 0.4em;
61
  border-radius: 3px;
62
  outline: none;
 
97
 
98
  .comment-mark.resolved {
99
  background: transparent;
100
+ border-bottom: 1px dashed var(--border-color);
101
  }
102
 
103
  /* Collaboration cursors */
 
136
 
137
  /* Image upload card */
138
  .image-upload-card {
139
+ border: 2px dashed var(--border-color);
140
  border-radius: 12px;
141
  padding: 2.5rem 1.5rem;
142
  margin: 1em 0;
 
149
  }
150
 
151
  .image-upload-card:hover {
152
+ border-color: var(--muted-color);
153
  background: var(--bg-hover);
154
  }
155
 
 
188
  }
189
 
190
  .image-upload-btn.secondary {
191
+ background: var(--code-bg);
192
+ color: var(--muted-color);
193
  }
194
 
195
  .image-upload-btn.secondary:hover {
196
+ background: var(--code-bg);
197
+ color: var(--text-color);
198
  }
199
 
200
  .image-upload-card-hint {
 
215
  .image-upload-url-input {
216
  width: 100%;
217
  padding: 0.5rem 0.75rem;
218
+ border: 1px solid var(--border-color);
219
  border-radius: 6px;
220
+ background: var(--code-bg);
221
+ color: var(--text-color);
222
  font-size: 0.8125rem;
223
  font-family: inherit;
224
  outline: none;
 
272
  }
273
 
274
  .block-handle-btn:hover {
275
+ color: var(--muted-color);
276
+ background: var(--code-bg);
277
  }
278
 
279
  .block-handle-grip {
 
287
  /* Slash menu */
288
  .slash-menu {
289
  background: var(--bg-surface);
290
+ border: 1px solid var(--border-color);
291
  border-radius: 10px;
292
  padding: 0.35rem;
293
  min-width: 220px;
 
307
  background: none;
308
  width: 100%;
309
  text-align: left;
310
+ color: var(--text-color);
311
  font-size: 0.875rem;
312
  font-family: inherit;
313
  }
314
 
315
  .slash-menu-item:hover,
316
  .slash-menu-item.is-selected {
317
+ background: var(--code-bg);
318
  }
319
 
320
  .slash-menu-item-icon {
 
324
  align-items: center;
325
  justify-content: center;
326
  border-radius: 6px;
327
+ background: var(--code-bg);
328
+ color: var(--muted-color);
329
  font-size: 0.9rem;
330
  flex-shrink: 0;
331
  }
 
337
 
338
  .slash-menu-item-title {
339
  font-weight: 500;
340
+ color: var(--text-color);
341
  font-size: 0.875rem;
342
  }
343
 
344
  .slash-menu-item-desc {
345
  font-size: 0.75rem;
346
+ color: var(--muted-color);
347
  }
348
 
349
  /* Footnote editing controls */
350
  .footnote-tooltip-input {
351
  width: 100%;
352
+ background: var(--code-bg);
353
+ border: 1px solid var(--border-color);
354
  border-radius: 4px;
355
+ color: var(--text-color);
356
  font-size: 12px;
357
  padding: 6px 8px;
358
  resize: vertical;
 
406
 
407
  .citation-panel {
408
  background: var(--bg-surface);
409
+ border: 1px solid var(--border-color);
410
  border-radius: 12px;
411
  width: 480px;
412
  max-height: 80vh;
 
434
  .citation-panel-close {
435
  background: none;
436
  border: none;
437
+ color: var(--muted-color);
438
  font-size: 1.4rem;
439
  cursor: pointer;
440
  padding: 0;
 
442
  }
443
 
444
  .citation-panel-close:hover {
445
+ color: var(--text-color);
446
  }
447
 
448
  .citation-panel-body {
 
456
  .citation-input {
457
  width: 100%;
458
  padding: 0.55rem 0.75rem;
459
+ border: 1px solid var(--border-color);
460
  border-radius: 6px;
461
+ background: var(--code-bg);
462
+ color: var(--text-color);
463
  font-size: 0.8125rem;
464
  font-family: inherit;
465
  outline: none;
 
541
  }
542
 
543
  .citation-library-item:hover {
544
+ background: var(--code-bg);
545
  }
546
 
547
  .citation-library-info {
 
553
  }
554
 
555
  .citation-library-meta {
556
+ color: var(--muted-color);
557
  font-size: 0.75rem;
558
  font-weight: 500;
559
  }
560
 
561
  .citation-library-title {
562
+ color: var(--muted-color);
563
  font-size: 0.75rem;
564
  white-space: nowrap;
565
  overflow: hidden;
 
591
  .citation-library-insert:hover {
592
  background: var(--accent-bg-hover);
593
  }
594
+
595
+ /* ---- Hero editable elements ---- */
596
+
597
+ .editable-text {
598
+ border-radius: 4px;
599
+ transition: background 0.15s;
600
+ padding: 2px 4px;
601
+ }
602
+
603
+ /* Negative margin to visually align padding - but NOT on elements
604
+ that rely on margin: auto for centering (e.g. .hero-desc). */
605
+ .editable-text:not(.hero-desc):not(.hero-title) {
606
+ margin: -2px -4px;
607
+ }
608
+
609
+ .editable-text:hover {
610
+ background: var(--bg-hover);
611
+ }
612
+
613
+ .author-editable {
614
+ cursor: pointer;
615
+ border-radius: 4px;
616
+ transition: background 0.15s;
617
+ }
618
+
619
+ .author-editable:hover {
620
+ background: var(--bg-hover);
621
+ }
622
+
623
+ .author-add-btn {
624
+ display: inline-flex;
625
+ align-items: center;
626
+ margin-left: 4px;
627
+ }
frontend/src/styles/toc.css ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* -----------------------------------------------------------------------
2
+ Table of contents - Editor overrides
3
+
4
+ Base styles are in components/_toc.css (from template).
5
+ This file adds editor-specific adjustments (smaller TOC column,
6
+ tighter padding, collapse animation).
7
+ ----------------------------------------------------------------------- */
8
+
9
+ /* The template's _layout.css sets align-items:start on .content-grid,
10
+ which prevents grid cells from stretching. The TOC cell MUST stretch
11
+ to full row height so the sticky child has room to travel. */
12
+ .content-grid__toc {
13
+ align-self: stretch;
14
+ }
15
+
16
+ .content-grid__toc .table-of-contents--sticky {
17
+ position: sticky;
18
+ top: 0;
19
+ }
20
+
21
+ /* Override the template's own sticky (handled by --sticky wrapper instead) */
22
+ .content-grid__toc .table-of-contents {
23
+ position: static;
24
+ margin-top: 0;
25
+ padding: 24px 8px 0;
26
+ }
27
+
28
+ .content-grid__toc .table-of-contents .title {
29
+ font-size: 0.65rem;
30
+ text-transform: uppercase;
31
+ letter-spacing: 0.05em;
32
+ }
33
+
34
+ /* Collapse animation for React-driven expand/collapse */
35
+ .toc-children {
36
+ overflow: hidden;
37
+ transition: max-height 200ms ease, opacity 200ms ease;
38
+ }
39
+
40
+ /* Ensure TOC empty message inherits article font */
41
+ .toc-empty {
42
+ font-size: 12px;
43
+ color: var(--muted-color);
44
+ padding-top: 4px;
45
+ }
46
+
47
+ /* TOC title label */
48
+ .toc-title {
49
+ font-weight: 600;
50
+ font-size: 14px;
51
+ margin-bottom: 8px;
52
+ }
frontend/src/theme.ts CHANGED
@@ -1,36 +1,47 @@
1
  import { createTheme } from "@mui/material/styles";
2
 
3
- export const theme = createTheme({
4
- palette: {
5
- mode: "dark",
6
- background: {
7
- default: "#0f0f0f",
8
- paper: "#1a1a1a",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  },
10
- primary: {
11
- main: "#958DF1",
 
12
  },
13
- text: {
14
- primary: "#e0e0e0",
15
- secondary: "#888",
16
  },
17
- divider: "#2a2a2a",
18
- },
19
- typography: {
20
- fontFamily:
21
- '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
22
- },
23
- shape: {
24
- borderRadius: 8,
25
- },
26
- components: {
27
- MuiIconButton: {
28
- styleOverrides: {
29
- root: {
30
- borderRadius: 6,
31
- padding: 6,
32
  },
33
  },
34
  },
35
- },
36
- });
 
 
 
 
 
 
1
  import { createTheme } from "@mui/material/styles";
2
 
3
+ /**
4
+ * Build a MUI theme whose primary color matches the article's
5
+ * --primary-color CSS variable (stored in Yjs settings).
6
+ */
7
+ export function buildTheme(primaryColor: string) {
8
+ return createTheme({
9
+ palette: {
10
+ mode: "dark",
11
+ background: {
12
+ default: "#0f0f0f",
13
+ paper: "#1a1a1a",
14
+ },
15
+ primary: {
16
+ main: primaryColor,
17
+ },
18
+ text: {
19
+ primary: "#e0e0e0",
20
+ secondary: "#888",
21
+ },
22
+ divider: "#2a2a2a",
23
  },
24
+ typography: {
25
+ fontFamily:
26
+ '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
27
  },
28
+ shape: {
29
+ borderRadius: 8,
 
30
  },
31
+ components: {
32
+ MuiIconButton: {
33
+ styleOverrides: {
34
+ root: {
35
+ borderRadius: 6,
36
+ padding: 6,
37
+ },
 
 
 
 
 
 
 
 
38
  },
39
  },
40
  },
41
+ });
42
+ }
43
+
44
+ export const DEFAULT_HUE = 47;
45
+ export const DEFAULT_PRIMARY = "#c87533";
46
+
47
+ export const theme = buildTheme(DEFAULT_PRIMARY);