mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 05:32:32 +02:00
edit sidebars
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -10,9 +10,10 @@ interface Props {
|
|||||||
graphEdges: { from: string; to: string }[];
|
graphEdges: { from: string; to: string }[];
|
||||||
forwardLinks: CollectionEntry<"notes">[];
|
forwardLinks: CollectionEntry<"notes">[];
|
||||||
backlinks: CollectionEntry<"notes">[];
|
backlinks: CollectionEntry<"notes">[];
|
||||||
|
externalLinks: { url: string; label: string }[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
|
const { entry, graphNodes, graphEdges, forwardLinks, backlinks, externalLinks } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
@@ -26,7 +27,7 @@ const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
|
|||||||
>
|
>
|
||||||
graph
|
graph
|
||||||
</p>
|
</p>
|
||||||
<NoteGraph client:visible nodes={graphNodes} edges={graphEdges} />
|
<NoteGraph client:load nodes={graphNodes} edges={graphEdges} />
|
||||||
{
|
{
|
||||||
graphNodes.length < 2 && (
|
graphNodes.length < 2 && (
|
||||||
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">
|
<p class="font-mono text-[9px] text-base-content/20 text-center py-2">
|
||||||
@@ -90,6 +91,31 @@ const { entry, graphNodes, graphEdges, forwardLinks, backlinks } = Astro.props;
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
externalLinks.length > 0 && (
|
||||||
|
<div class="p-3 border-t border-base-300/40">
|
||||||
|
<p class="font-mono text-[10px] text-base-content/25 uppercase tracking-widest mb-2">
|
||||||
|
external
|
||||||
|
</p>
|
||||||
|
<ul class="space-y-1">
|
||||||
|
{externalLinks.map(({ url, label }) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="font-mono text-xs text-base-content/45 hover:text-primary/80 transition-colors block truncate"
|
||||||
|
title={url}
|
||||||
|
>
|
||||||
|
↗ {label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
<div class="px-4 pt-4 pb-1 border-t border-base-300/40 mt-auto">
|
<div class="px-4 pt-4 pb-1 border-t border-base-300/40 mt-auto">
|
||||||
<time
|
<time
|
||||||
datetime={entry.data.publishDate.toISOString()}
|
datetime={entry.data.publishDate.toISOString()}
|
||||||
|
|||||||
@@ -19,36 +19,6 @@
|
|||||||
|
|
||||||
let search = $state("");
|
let search = $state("");
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const navDrawer = document.getElementById("nav-drawer") as HTMLInputElement | null;
|
|
||||||
if (!navDrawer) return;
|
|
||||||
|
|
||||||
const lgQuery = window.matchMedia("(min-width: 1024px)");
|
|
||||||
|
|
||||||
const overlay = document.createElement("div");
|
|
||||||
overlay.style.cssText =
|
|
||||||
"position:fixed;inset:0 0 0 14rem;background:oklch(0% 0 0/.4);z-index:51;display:none;cursor:pointer";
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
function update() {
|
|
||||||
overlay.style.display =
|
|
||||||
navDrawer.checked && !lgQuery.matches ? "block" : "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
navDrawer.addEventListener("change", update);
|
|
||||||
lgQuery.addEventListener("change", update);
|
|
||||||
overlay.addEventListener("click", () => {
|
|
||||||
navDrawer.checked = false;
|
|
||||||
navDrawer.dispatchEvent(new Event("change"));
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
navDrawer.removeEventListener("change", update);
|
|
||||||
lgQuery.removeEventListener("change", update);
|
|
||||||
overlay.remove();
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
function getCategory(n: Note): string {
|
function getCategory(n: Note): string {
|
||||||
if (n.data.category) return n.data.category;
|
if (n.data.category) return n.data.category;
|
||||||
const parts = n.id.split("/");
|
const parts = n.id.split("/");
|
||||||
@@ -95,25 +65,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<aside
|
<div class="flex flex-col flex-1 min-h-0">
|
||||||
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-full pt-2 lg:pt-0"
|
|
||||||
style="background: oklch(4% 0 0);"
|
|
||||||
>
|
|
||||||
<!-- Mobile close bar -->
|
|
||||||
<div class="lg:hidden flex items-center justify-between px-3 py-2 border-b border-base-300/40 shrink-0">
|
|
||||||
<span class="font-mono text-[10px] text-base-content/30 uppercase tracking-widest">nav</span>
|
|
||||||
<label
|
|
||||||
for="nav-drawer"
|
|
||||||
class="cursor-pointer text-base-content/30 hover:text-base-content/70 transition-colors p-1"
|
|
||||||
aria-label="close sidebar"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<path d="M18 6 6 18M6 6l12 12"/>
|
|
||||||
</svg>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search -->
|
|
||||||
<div class="px-3 py-3 border-b border-base-300/40 shrink-0">
|
<div class="px-3 py-3 border-b border-base-300/40 shrink-0">
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-base-200/50 border border-base-300/40 focus-within:border-base-300/70 transition-colors"
|
class="flex items-center gap-1.5 px-2 py-1.5 rounded-md bg-base-200/50 border border-base-300/40 focus-within:border-base-300/70 transition-colors"
|
||||||
@@ -128,7 +80,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nav -->
|
|
||||||
<nav class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 space-y-px">
|
<nav class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 space-y-px">
|
||||||
{#each categories as cat}
|
{#each categories as cat}
|
||||||
{@const catNotes = notes.filter(
|
{@const catNotes = notes.filter(
|
||||||
@@ -137,7 +88,6 @@
|
|||||||
{#if catNotes.length > 0 || !search}
|
{#if catNotes.length > 0 || !search}
|
||||||
{@const isFolder = notes.some((n) => n.id.includes("/") && getCategory(n) === cat)}
|
{@const isFolder = notes.some((n) => n.id.includes("/") && getCategory(n) === cat)}
|
||||||
<div>
|
<div>
|
||||||
<!-- Category header -->
|
|
||||||
<div class="flex items-center w-full">
|
<div class="flex items-center w-full">
|
||||||
<button
|
<button
|
||||||
onclick={() => toggle(cat)}
|
onclick={() => toggle(cat)}
|
||||||
@@ -172,7 +122,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notes list -->
|
|
||||||
{#if openCategories.includes(cat)}
|
{#if openCategories.includes(cat)}
|
||||||
<ul
|
<ul
|
||||||
class="ml-4 mt-0.5 pb-1 space-y-px"
|
class="ml-4 mt-0.5 pb-1 space-y-px"
|
||||||
@@ -200,4 +149,4 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</div>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
import NoteNavContent from "./NoteNavContent.svelte";
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
notes: CollectionEntry<"notes">[];
|
||||||
|
currentEntry?: CollectionEntry<"notes">;
|
||||||
|
currentCategory?: string;
|
||||||
|
categories: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { notes, currentEntry, currentCategory, categories } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<aside
|
||||||
|
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-full"
|
||||||
|
style="background: oklch(4% 0 0);"
|
||||||
|
>
|
||||||
|
<NoteNavContent
|
||||||
|
client:load
|
||||||
|
notes={notes}
|
||||||
|
currentEntry={currentEntry}
|
||||||
|
currentCategory={currentCategory}
|
||||||
|
categories={categories}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
@@ -3,10 +3,10 @@ import { getCollection, render } from "astro:content";
|
|||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import { List, PanelRight } from "@lucide/astro";
|
import { List, PanelRight } from "@lucide/astro";
|
||||||
import NoteTOC from "../../components/NoteTOC.astro";
|
import NoteTOC from "../../components/NoteTOC.astro";
|
||||||
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
|
||||||
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
|
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
|
||||||
import NoteVars from "../../components/NoteVars.svelte";
|
import NoteVars from "../../components/NoteVars.svelte";
|
||||||
import { getCategory, extractLinks, extractHeadings } from "../../utils/notes";
|
import { getCategory, extractLinks, extractExternalLinks, extractHeadings } from "../../utils/notes";
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const notes = await getCollection("notes");
|
const notes = await getCollection("notes");
|
||||||
@@ -61,11 +61,17 @@ const noteVars = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const headings = extractHeadings(entry.body ?? "");
|
const headings = extractHeadings(entry.body ?? "");
|
||||||
|
const externalLinks = extractExternalLinks(entry.body ?? "");
|
||||||
---
|
---
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 768px) {
|
||||||
.drawer.lg\:drawer-open > .drawer-side,
|
.drawer.md\:drawer-open > .drawer-side {
|
||||||
|
top: 3rem;
|
||||||
|
height: calc(100vh - 3rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (min-width: 1280px) {
|
||||||
.drawer.xl\:drawer-open > .drawer-side {
|
.drawer.xl\:drawer-open > .drawer-side {
|
||||||
top: 3rem;
|
top: 3rem;
|
||||||
height: calc(100vh - 3rem);
|
height: calc(100vh - 3rem);
|
||||||
@@ -78,11 +84,11 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
description={entry.data.description}
|
description={entry.data.description}
|
||||||
>
|
>
|
||||||
<main class="max-w-screen-2xl mx-auto">
|
<main class="max-w-screen-2xl mx-auto">
|
||||||
<div class="drawer lg:drawer-open min-h-[calc(100vh-3rem)]">
|
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
|
||||||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
<div class="drawer-content flex min-h-[calc(100vh-3rem)] min-w-0">
|
<div class="drawer-content flex min-h-[calc(100vh-3rem)] min-w-0">
|
||||||
<div class="drawer drawer-end xl:drawer-open w-full">
|
<div class="drawer drawer-end xl:drawer-open w-full" id="right-drawer">
|
||||||
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
<input id="graph-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
<div class="drawer-content flex flex-col min-w-0">
|
<div class="drawer-content flex flex-col min-w-0">
|
||||||
@@ -117,7 +123,7 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label
|
<label
|
||||||
for="nav-drawer"
|
for="nav-drawer"
|
||||||
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||||
>
|
>
|
||||||
<List size={11} />
|
<List size={11} />
|
||||||
nav
|
nav
|
||||||
@@ -126,7 +132,7 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
<label
|
<label
|
||||||
for="graph-drawer"
|
for="graph-drawer"
|
||||||
id="graph-toggle"
|
id="graph-toggle"
|
||||||
class="btn btn-ghost btn-xs font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
class="btn btn-ghost btn-xs xl:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||||
title="Toggle graph"
|
title="Toggle graph"
|
||||||
>
|
>
|
||||||
<PanelRight size={11} />
|
<PanelRight size={11} />
|
||||||
@@ -206,25 +212,26 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-side z-40">
|
<div class="drawer-side z-[60]">
|
||||||
<label
|
<label
|
||||||
for="graph-drawer"
|
for="graph-drawer"
|
||||||
aria-label="close sidebar"
|
aria-label="close sidebar"
|
||||||
class="drawer-overlay xl:hidden"></label>
|
class="drawer-overlay"></label>
|
||||||
<NoteGraphSidebar
|
<NoteGraphSidebar
|
||||||
entry={entry}
|
entry={entry}
|
||||||
graphNodes={graphNodes}
|
graphNodes={graphNodes}
|
||||||
graphEdges={graphEdges}
|
graphEdges={graphEdges}
|
||||||
forwardLinks={forwardLinks}
|
forwardLinks={forwardLinks}
|
||||||
backlinks={backlinks}
|
backlinks={backlinks}
|
||||||
|
externalLinks={externalLinks}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-side z-50">
|
<div class="drawer-side z-[70]">
|
||||||
|
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
<NoteNavSidebar
|
<NoteNavSidebar
|
||||||
client:load
|
|
||||||
notes={sortedNotes}
|
notes={sortedNotes}
|
||||||
currentEntry={entry}
|
currentEntry={entry}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
@@ -288,51 +295,7 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initXlGraphToggle() {
|
|
||||||
const graphDrawer = document.getElementById(
|
|
||||||
"graph-drawer",
|
|
||||||
) as HTMLInputElement | null;
|
|
||||||
if (!graphDrawer) return;
|
|
||||||
|
|
||||||
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
|
|
||||||
const xlQuery = window.matchMedia("(min-width: 1280px)");
|
|
||||||
const STORAGE_KEY = "notes-graph-sidebar";
|
|
||||||
|
|
||||||
function setXlSidebar(open: boolean) {
|
|
||||||
if (!outerDrawer) return;
|
|
||||||
if (open) {
|
|
||||||
outerDrawer.classList.add("xl:drawer-open");
|
|
||||||
} else {
|
|
||||||
outerDrawer.classList.remove("xl:drawer-open");
|
|
||||||
}
|
|
||||||
localStorage.setItem(STORAGE_KEY, open ? "1" : "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
const graphToggle = document.getElementById("graph-toggle");
|
|
||||||
graphToggle?.addEventListener("click", (e) => {
|
|
||||||
if (!xlQuery.matches) return;
|
|
||||||
e.preventDefault();
|
|
||||||
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (xlQuery.matches) {
|
|
||||||
const saved = localStorage.getItem(STORAGE_KEY);
|
|
||||||
// Open by default unless user explicitly closed it
|
|
||||||
setXlSidebar(saved !== "0");
|
|
||||||
}
|
|
||||||
|
|
||||||
xlQuery.addEventListener("change", (e) => {
|
|
||||||
if (!e.matches) {
|
|
||||||
outerDrawer?.classList.remove("xl:drawer-open");
|
|
||||||
} else {
|
|
||||||
const saved = localStorage.getItem(STORAGE_KEY);
|
|
||||||
setXlSidebar(saved !== "0");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("astro:page-load", () => {
|
document.addEventListener("astro:page-load", () => {
|
||||||
injectHeadingAnchors();
|
injectHeadingAnchors();
|
||||||
initXlGraphToggle();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from "astro:content";
|
import { getCollection } from "astro:content";
|
||||||
import Layout from "../../layouts/Layout.astro";
|
import Layout from "../../layouts/Layout.astro";
|
||||||
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
|
||||||
import { getCategory } from "../../utils/notes";
|
import { getCategory } from "../../utils/notes";
|
||||||
import { List } from "@lucide/astro";
|
import { List } from "@lucide/astro";
|
||||||
|
|
||||||
@@ -41,8 +41,8 @@ if (!categoryNotes) {
|
|||||||
---
|
---
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 768px) {
|
||||||
.drawer.lg\:drawer-open > .drawer-side {
|
.drawer.md\:drawer-open > .drawer-side {
|
||||||
top: 3rem;
|
top: 3rem;
|
||||||
height: calc(100vh - 3rem);
|
height: calc(100vh - 3rem);
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ if (!categoryNotes) {
|
|||||||
description={`Notes on ${category}.`}
|
description={`Notes on ${category}.`}
|
||||||
>
|
>
|
||||||
<main class="max-w-screen-2xl mx-auto">
|
<main class="max-w-screen-2xl mx-auto">
|
||||||
<div class="drawer lg:drawer-open min-h-[calc(100vh-3rem)]">
|
<div class="drawer md:drawer-open min-h-[calc(100vh-3rem)]">
|
||||||
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
<input id="nav-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
|
|
||||||
<div class="drawer-content flex flex-col min-w-0">
|
<div class="drawer-content flex flex-col min-w-0">
|
||||||
@@ -74,7 +74,7 @@ if (!categoryNotes) {
|
|||||||
</div>
|
</div>
|
||||||
<label
|
<label
|
||||||
for="nav-drawer"
|
for="nav-drawer"
|
||||||
class="btn btn-ghost btn-xs lg:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
class="btn btn-ghost btn-xs md:hidden font-mono text-base-content/40 hover:text-base-content/70 border border-base-300/50"
|
||||||
>
|
>
|
||||||
<List size={11} />
|
<List size={11} />
|
||||||
nav
|
nav
|
||||||
@@ -163,9 +163,9 @@ if (!categoryNotes) {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="drawer-side z-50">
|
<div class="drawer-side z-[70]">
|
||||||
|
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
<NoteNavSidebar
|
<NoteNavSidebar
|
||||||
client:load
|
|
||||||
notes={allNotes}
|
notes={allNotes}
|
||||||
currentCategory={category}
|
currentCategory={category}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
|
|||||||
+13
-2
@@ -36,6 +36,13 @@
|
|||||||
--noise: 0;
|
--noise: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drawer-side > aside > astro-island {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.btn:not(.btn-circle):not(.btn-square) {
|
.btn:not(.btn-circle):not(.btn-square) {
|
||||||
@apply rounded-lg;
|
@apply rounded-lg;
|
||||||
}
|
}
|
||||||
@@ -50,7 +57,9 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--color-base-content);
|
color: var(--color-base-content);
|
||||||
text-decoration: none !important;
|
text-decoration: none !important;
|
||||||
transition: background 0.15s ease, border-color 0.15s ease;
|
transition:
|
||||||
|
background 0.15s ease,
|
||||||
|
border-color 0.15s ease;
|
||||||
margin-block: 0.25rem;
|
margin-block: 0.25rem;
|
||||||
}
|
}
|
||||||
.link-card::after {
|
.link-card::after {
|
||||||
@@ -60,7 +69,9 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
transition:
|
||||||
|
opacity 0.15s ease,
|
||||||
|
transform 0.15s ease;
|
||||||
transform: translate(-4px, 4px);
|
transform: translate(-4px, 4px);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,34 @@ function slugify(text: string): string {
|
|||||||
.replace(/ +/g, "-");
|
.replace(/ +/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractExternalLinks(body: string): { url: string; label: string }[] {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
const links: { url: string; label: string }[] = [];
|
||||||
|
|
||||||
|
// Markdown: [label](https://...)
|
||||||
|
const mdRe = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g;
|
||||||
|
let m;
|
||||||
|
while ((m = mdRe.exec(body)) !== null) {
|
||||||
|
if (!seen.has(m[2])) {
|
||||||
|
seen.add(m[2]);
|
||||||
|
links.push({ url: m[2], label: m[1] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTML: <a href="https://...">...</a> — use h4 content as label if present, else href host
|
||||||
|
const htmlRe = /<a\s[^>]*href="(https?:\/\/[^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
||||||
|
while ((m = htmlRe.exec(body)) !== null) {
|
||||||
|
const url = m[1];
|
||||||
|
if (seen.has(url)) continue;
|
||||||
|
seen.add(url);
|
||||||
|
const h4 = m[2].match(/<h4[^>]*>([\s\S]*?)<\/h4>/);
|
||||||
|
const label = h4 ? h4[1].trim() : new URL(url).hostname;
|
||||||
|
links.push({ url, label });
|
||||||
|
}
|
||||||
|
|
||||||
|
return links;
|
||||||
|
}
|
||||||
|
|
||||||
export function extractLinks(body: string): string[] {
|
export function extractLinks(body: string): string[] {
|
||||||
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
|
const re = /\(\/notes\/([^)#\s]+)(?:#[^)\s]*)?\)/g;
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user