New Website in VueJS #6
2 changed files with 340 additions and 179 deletions
|
|
@ -14,12 +14,12 @@
|
|||
<span class="text-red-500">Error loading projects</span>
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-6">
|
||||
<div v-else class="flex flex-col md:grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<RouterLink
|
||||
v-for="project in items"
|
||||
:key="project.path"
|
||||
:to="`/projects/${project.slug}`"
|
||||
class="bg-base-200 p-6 rounded border border-base-300 hover:border-primary transition-all block"
|
||||
class="bg-base-200 p-6 rounded-2xl border border-base-300 hover:border-primary transition-all block"
|
||||
>
|
||||
<img
|
||||
v-if="project.frontmatter.cover"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,61 @@
|
|||
<template>
|
||||
<div class="min-h-screen bg-base-100 text-base-content">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
|
||||
<div class="mb-4 md:hidden">
|
||||
<button
|
||||
class="btn h-auto w-full justify-between border-base-300 bg-base-200 px-4 py-3 text-left normal-case shadow-sm hover:bg-base-300"
|
||||
type="button"
|
||||
@click="toggleMobileSidebar"
|
||||
:aria-expanded="isMobileSidebarOpen"
|
||||
aria-controls="wiki-sidebar-panel"
|
||||
>
|
||||
<span class="flex flex-col items-start">
|
||||
<span class="text-sm font-semibold text-base-content">Sommario e Indice pagina</span>
|
||||
<span class="text-xs text-base-content/70">{{ mobileSidebarSummary }}</span>
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
class="h-4 w-4 text-base-content/70 transition-transform"
|
||||
:class="{ 'rotate-180': isMobileSidebarOpen }"
|
||||
>
|
||||
<path
|
||||
d="M6 9l6 6 6-6"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-12 md:gap-8">
|
||||
<div
|
||||
v-if="isMobileSidebarOpen"
|
||||
class="fixed inset-0 z-40 bg-black/40 md:hidden"
|
||||
@click="closeMobileSidebar"
|
||||
></div>
|
||||
|
||||
<!-- Sidebar con navigazione pagine wiki -->
|
||||
<aside class="lg:col-span-1 sticky top-4">
|
||||
<aside
|
||||
id="wiki-sidebar-panel"
|
||||
:class="sidebarPanelClasses"
|
||||
aria-label="Wiki sidebar"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between md:hidden">
|
||||
<span class="text-sm font-semibold uppercase tracking-wide text-primary"
|
||||
>Wiki menu</span
|
||||
>
|
||||
<button
|
||||
class="btn btn-xs btn-ghost"
|
||||
type="button"
|
||||
@click="closeMobileSidebar"
|
||||
>
|
||||
X
|
||||
|
|
||||
</button>
|
||||
</div>
|
||||
<nav class="rounded-lg">
|
||||
<!-- Lista pagine dalla sidebar -->
|
||||
<div class="space-y-2 text-sm">
|
||||
|
|
@ -22,7 +74,7 @@
|
|||
:target="item.isExternal ? '_blank' : null"
|
||||
:rel="item.isExternal ? 'noopener noreferrer' : null"
|
||||
@click="onSidebarLinkClick($event, item)"
|
||||
class="block py-1 px-2 rounded-lg hover:bg-base-300/20 transition truncate"
|
||||
class="block min-h-11 rounded-lg px-3 py-2.5 transition hover:bg-base-300/20 md:min-h-0 md:py-1"
|
||||
:class="{
|
||||
'bg-base-300/20 text-primary font-bold':
|
||||
!item.isExternal && currentPage === item.slug,
|
||||
|
|
@ -39,16 +91,16 @@
|
|||
<!-- TOC (Table of Contents) - Outline minimal terminal style -->
|
||||
<nav
|
||||
v-if="currentPage === item.slug && outline.length > 0"
|
||||
class="sticky top-4"
|
||||
class="mt-2"
|
||||
>
|
||||
<ul
|
||||
class="space-y-0 text-xs font-mono leading-snug border-l border-base-300 pl-3"
|
||||
class="space-y-0 text-xs font-mono leading-snug border-l border-base-300 pl-2"
|
||||
>
|
||||
<li v-for="heading in outline" :key="heading.id">
|
||||
<a
|
||||
:href="`#${heading.id}`"
|
||||
@click.prevent="scrollToHeading(heading.id)"
|
||||
class="block py-0.5 hover:text-base-content rounded-lg transition truncate"
|
||||
class="block rounded-md py-1.5 pr-2 transition hover:text-base-content"
|
||||
:style="{
|
||||
paddingLeft: `${(heading.level - 1) * 0.5}rem`,
|
||||
}"
|
||||
|
|
@ -70,7 +122,7 @@
|
|||
</aside>
|
||||
|
||||
<!-- Contenuto principale -->
|
||||
<main class="lg:col-span-4">
|
||||
<main class="md:col-span-8 lg:col-span-9 xl:col-span-10">
|
||||
<div v-if="loading" class="text-center py-12">
|
||||
<span class="text-primary"
|
||||
>Loading<span class="blinking-cursor"></span
|
||||
|
|
@ -79,13 +131,13 @@
|
|||
|
||||
<article
|
||||
v-else
|
||||
class="bg-base-200 rounded-lg p-8 border border-base-300"
|
||||
class="bg-base-200 rounded-lg border border-base-300 p-4 sm:p-6 lg:p-8"
|
||||
>
|
||||
<h1 class="text-4xl font-bold text-primary mb-6">
|
||||
<h1 class="mb-5 text-2xl font-bold text-primary sm:text-3xl lg:mb-6 lg:text-4xl">
|
||||
{{ pageTitle }}<span class="blinking-cursor"></span>
|
||||
</h1>
|
||||
<div
|
||||
class="markdown-content prose prose-invert max-w-none"
|
||||
class="markdown-content prose prose-invert max-w-none text-sm md:text-base"
|
||||
v-html="pageContent"
|
||||
></div>
|
||||
</article>
|
||||
|
|
@ -93,7 +145,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-circle btn-secondary bottom-4 right-4 fixed font-bold text-3xl"
|
||||
class="btn btn-circle btn-secondary fixed bottom-4 right-4 z-30 text-3xl font-bold sm:bottom-6 sm:right-6"
|
||||
:class="{ hidden: !showScrollTopButton }"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
|
|
@ -103,13 +155,19 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
||||
import { marked } from "marked";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const MOBILE_BREAKPOINT_QUERY = "(max-width: 767px)";
|
||||
const MOBILE_HEADING_THRESHOLD = 84;
|
||||
const DESKTOP_HEADING_THRESHOLD = 100;
|
||||
const WIKI_CONTENT_BASE_PATH = "/content/wiki/adm.wiki";
|
||||
const WIKI_SIDEBAR_PATH = `${WIKI_CONTENT_BASE_PATH}/_Sidebar.md?raw`;
|
||||
|
||||
const loading = ref(true);
|
||||
const pageContent = ref("");
|
||||
const pageTitle = ref("Wiki ADM");
|
||||
|
|
@ -118,147 +176,118 @@ const sidebarItems = ref([]);
|
|||
const outline = ref([]);
|
||||
const activeHeading = ref("");
|
||||
const showScrollTopButton = ref(false);
|
||||
const isMobileSidebarOpen = ref(false);
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
showScrollTopButton.value = false;
|
||||
const sidebarPageCount = computed(
|
||||
() =>
|
||||
sidebarItems.value.filter(
|
||||
(item) => item.type === "link" && !item.isExternal,
|
||||
).length,
|
||||
);
|
||||
|
||||
const mobileSidebarSummary = computed(() => {
|
||||
const pages = sidebarPageCount.value;
|
||||
const sections = outline.value.length;
|
||||
return `${pages} pagine · ${sections} sezioni`;
|
||||
});
|
||||
|
||||
const sidebarPanelClasses = computed(() => [
|
||||
|
samu
commented
We only use this once in the html, why use a compute? We only use this once in the html, why use a compute?
|
||||
"md:col-span-4 lg:col-span-3 xl:col-span-2 md:sticky md:top-4 md:self-start",
|
||||
"fixed left-4 right-4 top-4 z-50 max-h-[calc(100vh-2rem)] overflow-y-auto rounded-lg border border-base-300 bg-base-100/95 p-4 shadow-lg backdrop-blur transition-transform duration-200 ease-out md:static md:z-auto md:max-h-none md:overflow-visible md:rounded-none md:border-0 md:bg-transparent md:p-0 md:shadow-none md:backdrop-blur-none",
|
||||
isMobileSidebarOpen.value
|
||||
? "translate-x-0"
|
||||
: "-translate-x-[110%] md:translate-x-0",
|
||||
]);
|
||||
|
||||
function toHeadingId(text) {
|
||||
return text.toLowerCase().replace(/[^\w]+/g, "-");
|
||||
}
|
||||
|
||||
function onSidebarLinkClick(event, item) {
|
||||
if (item.isExternal) return;
|
||||
event.preventDefault();
|
||||
loadPage(item.slug);
|
||||
function toWikiSlug(value) {
|
||||
return value.replace(/^\.\//, "").replace(/\/$/, "").toLowerCase();
|
||||
}
|
||||
|
||||
// Carica il contenuto della sidebar
|
||||
async function loadSidebar() {
|
||||
try {
|
||||
const sidebarModule =
|
||||
await import("/content/wiki/adm.wiki/_Sidebar.md?raw");
|
||||
const sidebarContent = sidebarModule.default;
|
||||
function toTitleCaseSlug(value) {
|
||||
return value
|
||||
.split("-")
|
||||
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
||||
.join("-");
|
||||
}
|
||||
|
||||
// Parsa la sidebar markdown
|
||||
const lines = sidebarContent.split("\n").filter((line) => line.trim());
|
||||
const items = [];
|
||||
function buildPossibleWikiPaths(slug) {
|
||||
return [
|
||||
`${WIKI_CONTENT_BASE_PATH}/${slug}.md`,
|
||||
`${WIKI_CONTENT_BASE_PATH}/${slug.charAt(0).toUpperCase() + slug.slice(1)}.md`,
|
||||
`${WIKI_CONTENT_BASE_PATH}/${toTitleCaseSlug(slug)}.md`,
|
||||
];
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
// Ignora commenti
|
||||
if (line.includes("<!-- NOT INCLUDE -->") || line.includes("<!--"))
|
||||
continue;
|
||||
|
||||
// Heading (###)
|
||||
if (line.startsWith("###") || line.startsWith("- ###")) {
|
||||
items.push({
|
||||
type: "heading",
|
||||
text: line.replace(/^###\s*/, "").trim(),
|
||||
});
|
||||
}
|
||||
// Link standard [text](url)
|
||||
else if (line.includes("](")) {
|
||||
const match = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
|
||||
|
||||
if (match) {
|
||||
const text = match[1];
|
||||
const url = match[2];
|
||||
const isExternal = /^https?:\/\//i.test(url);
|
||||
// Estrai lo slug dal URL
|
||||
const slug = url
|
||||
.replace(/^\.\//, "")
|
||||
.replace(/\/$/, "")
|
||||
.toLowerCase();
|
||||
items.push({
|
||||
type: "link",
|
||||
text,
|
||||
slug,
|
||||
isExternal,
|
||||
url,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Link wiki-style [[text]]
|
||||
else if (line.includes("[[")) {
|
||||
const match = line.match(/\[\[([^\]]+)\]\]/);
|
||||
if (match) {
|
||||
const text = match[1];
|
||||
items.push({
|
||||
type: "link",
|
||||
text,
|
||||
slug: text.toLowerCase(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sidebarItems.value = items;
|
||||
} catch (error) {
|
||||
console.error("Error loading sidebar:", error);
|
||||
function parseSidebarLine(line) {
|
||||
if (line.includes("<!-- NOT INCLUDE -->") || line.includes("<!--")) {
|
||||
|
samu
commented
What's the point of this? The exclusion should be server-side with the python script, not client side What's the point of this? The exclusion should be server-side with the python script, not client side
|
||||
return null;
|
||||
}
|
||||
|
||||
if (line.startsWith("###") || line.startsWith("- ###")) {
|
||||
return {
|
||||
type: "heading",
|
||||
text: line.replace(/^-\s*/, "").replace(/^###\s*/, "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
if (line.includes("](")) {
|
||||
const match = line.match(/\[([^\]]+)\]\(([^)]+)\)/);
|
||||
if (!match) return null;
|
||||
|
||||
const text = match[1];
|
||||
const url = match[2];
|
||||
const isExternal = /^https?:\/\//i.test(url);
|
||||
|
||||
return {
|
||||
type: "link",
|
||||
text,
|
||||
slug: toWikiSlug(url),
|
||||
isExternal,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
if (line.includes("[[")) {
|
||||
const match = line.match(/\[\[([^\]]+)\]\]/);
|
||||
if (!match) return null;
|
||||
|
||||
return {
|
||||
type: "link",
|
||||
text: match[1],
|
||||
slug: toWikiSlug(match[1]),
|
||||
isExternal: false,
|
||||
url: "#",
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Carica una pagina wiki
|
||||
async function loadPage(slug) {
|
||||
loading.value = true;
|
||||
currentPage.value = slug;
|
||||
function createMarkdownRenderer() {
|
||||
|
samu
commented
All of this is very ugly tbh. Can we do it at compile time like the og photos? All of this is very ugly tbh. Can we do it at compile time like the og photos?
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
// Aggiorna URL senza ricaricare
|
||||
router.push({ query: { page: slug } });
|
||||
renderer.heading = function (text, level) {
|
||||
const id = toHeadingId(text);
|
||||
return `<h${level} id="${id}" href="#${id}" >${text}</h${level}>`;
|
||||
};
|
||||
|
||||
try {
|
||||
// Prova vari formati di filename
|
||||
const possiblePaths = [
|
||||
`/content/wiki/adm.wiki/${slug}.md`,
|
||||
`/content/wiki/adm.wiki/${slug.charAt(0).toUpperCase() + slug.slice(1)}.md`,
|
||||
`/content/wiki/adm.wiki/${slug
|
||||
.split("-")
|
||||
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||
.join("-")}.md`,
|
||||
];
|
||||
|
||||
let content = null;
|
||||
let foundPath = null;
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
try {
|
||||
const module = await import(/* @vite-ignore */ path + "?raw");
|
||||
content = module.default;
|
||||
foundPath = path;
|
||||
break;
|
||||
} catch (err) {
|
||||
// Continua a provare
|
||||
}
|
||||
renderer.image = function (href, title, text) {
|
||||
let src = href;
|
||||
if (!href.startsWith("http") && !href.startsWith("/")) {
|
||||
src = `${WIKI_CONTENT_BASE_PATH}/${href}`;
|
||||
}
|
||||
const titleAttr = title ? ` title="${title}"` : "";
|
||||
return `<img src="${src}" alt="${text}"${titleAttr} class="max-w-full h-auto" />`;
|
||||
};
|
||||
|
||||
if (!content) {
|
||||
throw new Error(`Page not found: ${slug}`);
|
||||
}
|
||||
// Estrai titolo
|
||||
const titleMatch = content.match(/^#\s+(.+)$/m);
|
||||
pageTitle.value = titleMatch ? titleMatch[1] : slug.replace(/-/g, " ");
|
||||
|
||||
// Rimuovi solo il primo H1 iniziale (se presente)
|
||||
const contentWithoutMainTitle = content.replace(/^\s*#\s+.+(?:\r?\n)+/, "");
|
||||
|
||||
// Converti markdown con custom renderers
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
renderer.heading = function (text, level) {
|
||||
const id = text.toLowerCase().replace(/[^\w]+/g, "-");
|
||||
return `<h${level} id="${id}" href="#${id}" >${text}</h${level}>`;
|
||||
};
|
||||
|
||||
renderer.image = function (href, title, text) {
|
||||
let src = href;
|
||||
if (!href.startsWith("http") && !href.startsWith("/")) {
|
||||
src = `/content/wiki/adm.wiki/${href}`;
|
||||
}
|
||||
const titleAttr = title ? ` title="${title}"` : "";
|
||||
return `<img src="${src}" alt="${text}"${titleAttr} class="max-w-full h-auto" />`;
|
||||
};
|
||||
|
||||
renderer.code = function (code, language) {
|
||||
const lang = language || "text";
|
||||
const escapedCode = code.replace(/</g, "<").replace(/>/g, ">");
|
||||
return `<div class="code-block-wrapper">
|
||||
renderer.code = function (code, language) {
|
||||
const lang = language || "text";
|
||||
const escapedCode = code.replace(/</g, "<").replace(/>/g, ">");
|
||||
return `<div class="code-block-wrapper">
|
||||
<div class="code-block-header">
|
||||
<span class="code-block-lang">${lang}</span>
|
||||
<button class="code-block-copy" onclick="copyCodeToClipboard(this)" title="Copy code">
|
||||
|
|
@ -270,15 +299,119 @@ async function loadPage(slug) {
|
|||
</div>
|
||||
<pre><code class="language-${lang}">${escapedCode}</code></pre>
|
||||
</div>`;
|
||||
};
|
||||
};
|
||||
|
||||
// Render e outline coerenti, entrambi senza H1 iniziale
|
||||
return renderer;
|
||||
}
|
||||
|
||||
function copyCodeToClipboard(button) {
|
||||
const wrapper = button?.closest(".code-block-wrapper");
|
||||
const codeElement = wrapper?.querySelector("code");
|
||||
if (!codeElement) return;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(codeElement.textContent)
|
||||
.then(() => {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML =
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
||||
button.style.color = "var(--adm-primary)";
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
button.style.color = "";
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to copy:", err);
|
||||
});
|
||||
}
|
||||
|
||||
function isMobileViewport() {
|
||||
return window.matchMedia(MOBILE_BREAKPOINT_QUERY).matches;
|
||||
}
|
||||
|
||||
function closeMobileSidebar() {
|
||||
isMobileSidebarOpen.value = false;
|
||||
}
|
||||
|
||||
function toggleMobileSidebar() {
|
||||
isMobileSidebarOpen.value = !isMobileSidebarOpen.value;
|
||||
}
|
||||
|
||||
function updateBodyScrollLock() {
|
||||
if (!isMobileViewport()) {
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.classList.toggle("overflow-hidden", isMobileSidebarOpen.value);
|
||||
}
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||
showScrollTopButton.value = false;
|
||||
}
|
||||
|
||||
function onSidebarLinkClick(event, item) {
|
||||
if (item.isExternal) return;
|
||||
event.preventDefault();
|
||||
closeMobileSidebar();
|
||||
loadPage(item.slug);
|
||||
}
|
||||
|
||||
// Carica il contenuto della sidebar
|
||||
async function loadSidebar() {
|
||||
try {
|
||||
const sidebarModule = await import(WIKI_SIDEBAR_PATH);
|
||||
const sidebarContent = sidebarModule.default;
|
||||
const lines = sidebarContent.split("\n").filter((line) => line.trim());
|
||||
sidebarItems.value = lines.map(parseSidebarLine).filter(Boolean);
|
||||
} catch (error) {
|
||||
console.error("Error loading sidebar:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Carica una pagina wiki
|
||||
async function loadPage(slug) {
|
||||
const normalizedSlug = toWikiSlug(slug);
|
||||
loading.value = true;
|
||||
currentPage.value = normalizedSlug;
|
||||
|
||||
// Aggiorna URL senza ricaricare
|
||||
router.push({ query: { page: normalizedSlug } });
|
||||
|
||||
try {
|
||||
const possiblePaths = buildPossibleWikiPaths(normalizedSlug);
|
||||
let content = null;
|
||||
|
||||
for (const path of possiblePaths) {
|
||||
try {
|
||||
const module = await import(/* @vite-ignore */ path + "?raw");
|
||||
content = module.default;
|
||||
break;
|
||||
} catch (err) {
|
||||
// Continua a provare
|
||||
}
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
throw new Error(`Page not found: ${normalizedSlug}`);
|
||||
}
|
||||
// Estrai titolo
|
||||
const titleMatch = content.match(/^#\s+(.+)$/m);
|
||||
pageTitle.value =
|
||||
titleMatch?.[1] || normalizedSlug.replace(/-/g, " ");
|
||||
|
||||
// Rimuovi solo il primo H1 iniziale (se presente)
|
||||
const contentWithoutMainTitle = content.replace(/^\s*#\s+.+(?:\r?\n)+/, "");
|
||||
|
||||
const renderer = createMarkdownRenderer();
|
||||
pageContent.value = marked(contentWithoutMainTitle, { renderer });
|
||||
// TODO: togliere parti di codice per elaborare l'outline
|
||||
extractOutline(contentWithoutMainTitle);
|
||||
} catch (error) {
|
||||
console.error("Error loading page:", error);
|
||||
pageContent.value = `<p class="text-red-500">Page not found: ${slug}</p>`;
|
||||
pageContent.value = `<p class="text-red-500">Page not found: ${normalizedSlug}</p>`;
|
||||
pageTitle.value = "Error";
|
||||
outline.value = [];
|
||||
} finally {
|
||||
|
|
@ -296,7 +429,7 @@ function extractOutline(markdown) {
|
|||
if (match) {
|
||||
const level = match[1].length;
|
||||
const text = match[2];
|
||||
const id = text.toLowerCase().replace(/[^\w]+/g, "-");
|
||||
const id = toHeadingId(text);
|
||||
|
||||
headings.push({ level, text, id });
|
||||
}
|
||||
|
|
@ -311,18 +444,29 @@ function scrollToHeading(id) {
|
|||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
closeMobileSidebar();
|
||||
}
|
||||
}
|
||||
|
||||
// Rileva heading attivo durante lo scroll
|
||||
function handleScroll() {
|
||||
if (outline.value.length === 0) return;
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
showScrollTopButton.value = scrollTop > 300;
|
||||
|
||||
if (outline.value.length === 0) {
|
||||
activeHeading.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const threshold = isMobileViewport()
|
||||
? MOBILE_HEADING_THRESHOLD
|
||||
: DESKTOP_HEADING_THRESHOLD;
|
||||
|
||||
for (const heading of outline.value) {
|
||||
const element = document.getElementById(heading.id);
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
if (rect.top <= 100 && rect.bottom >= 100) {
|
||||
if (rect.top <= threshold && rect.bottom >= threshold) {
|
||||
activeHeading.value = heading.id;
|
||||
break;
|
||||
}
|
||||
|
|
@ -331,6 +475,8 @@ function handleScroll() {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.copyCodeToClipboard = copyCodeToClipboard;
|
||||
|
||||
await loadSidebar();
|
||||
|
||||
// Carica la pagina dall'URL o Home come default
|
||||
|
|
@ -338,11 +484,31 @@ onMounted(async () => {
|
|||
await loadPage(page);
|
||||
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
window.addEventListener("resize", updateBodyScrollLock);
|
||||
handleScroll();
|
||||
updateBodyScrollLock();
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", () => {
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
||||
showScrollTopButton.value = scrollTop > 300;
|
||||
});
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
window.removeEventListener("resize", updateBodyScrollLock);
|
||||
document.body.classList.remove("overflow-hidden");
|
||||
delete window.copyCodeToClipboard;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.query.page,
|
||||
async (page) => {
|
||||
const normalizedPage = typeof page === "string" ? page : "home";
|
||||
if (normalizedPage !== currentPage.value) {
|
||||
await loadPage(normalizedPage);
|
||||
}
|
||||
closeMobileSidebar();
|
||||
},
|
||||
);
|
||||
|
||||
watch(isMobileSidebarOpen, () => {
|
||||
updateBodyScrollLock();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -350,7 +516,15 @@ onMounted(async () => {
|
|||
.prose :deep(h2),
|
||||
.prose :deep(h3),
|
||||
.prose :deep(h4) {
|
||||
scroll-margin-top: 5rem;
|
||||
scroll-margin-top: 4rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.prose :deep(h2),
|
||||
.prose :deep(h3),
|
||||
.prose :deep(h4) {
|
||||
scroll-margin-top: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Code blocks styling */
|
||||
|
|
@ -385,7 +559,7 @@ onMounted(async () => {
|
|||
border: none;
|
||||
color: var(--adm-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 4px;
|
||||
|
|
@ -410,29 +584,16 @@ onMounted(async () => {
|
|||
line-height: 1.6;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.markdown-content :deep(.code-block-header) {
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.markdown-content :deep(table) {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Funzione globale per copiare il codice
|
||||
window.copyCodeToClipboard = function (button) {
|
||||
const wrapper = button.closest(".code-block-wrapper");
|
||||
const code = wrapper.querySelector("code").textContent;
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(code)
|
||||
.then(() => {
|
||||
const originalHTML = button.innerHTML;
|
||||
button.innerHTML =
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
||||
button.style.color = "var(--adm-primary)";
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalHTML;
|
||||
button.style.color = "";
|
||||
}, 2000);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Failed to copy:", err);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in a new issue
An icon would look better :)