change notes sidebars behavior

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-04-24 23:53:02 +02:00
parent 97bdfd9a6e
commit 294c4e3acd
5 changed files with 87 additions and 25 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ const username = "anotherhadi"
const bio = "Infosec engineer." const bio = "Infosec engineer."
--- ---
<div class="flex gap-3 justify-start"> <div class="flex flex-wrap gap-3 justify-start">
<div <div
class="ring-base-300 ring-offset-base-100 rounded-full ring-2 ring-offset-2 flex justify-center items-center" class="ring-base-300 ring-offset-base-100 rounded-full ring-2 ring-offset-2 flex justify-center items-center"
> >
+2 -2
View File
@@ -15,7 +15,7 @@ function isActive(href: string) {
--- ---
<header <header
class="fixed top-0 left-0 right-0 z-50 h-12 flex items-center px-5" class="fixed top-0 left-0 right-0 z-[60] h-12 flex items-center px-5"
style="background: oklch(0% 0 0 / 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid oklch(22% 0 0);" style="background: oklch(0% 0 0 / 0.85); backdrop-filter: blur(12px); border-bottom: 1px solid oklch(22% 0 0);"
> >
<div class="flex items-center justify-between w-full max-w-screen-xl mx-auto"> <div class="flex items-center justify-between w-full max-w-screen-xl mx-auto">
@@ -67,7 +67,7 @@ function isActive(href: string) {
<div <div
id="mobile-menu" id="mobile-menu"
class="hidden fixed inset-x-0 top-12 z-40 md:hidden border-b border-base-300/60 py-2" class="hidden fixed inset-x-0 top-12 z-[59] md:hidden border-b border-base-300/60 py-2"
style="background: oklch(2% 0 0 / 0.97); backdrop-filter: blur(12px);" style="background: oklch(2% 0 0 / 0.97); backdrop-filter: blur(12px);"
> >
{ {
+1 -1
View File
@@ -19,7 +19,7 @@ const notes = defineCollection({
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
description: z.string(), description: z.string(),
category: z.string(), category: z.string().optional(),
tags: z.array(z.string()).default([]), tags: z.array(z.string()).default([]),
publishDate: z.coerce.date(), publishDate: z.coerce.date(),
}), }),
+74 -18
View File
@@ -2,6 +2,7 @@
import { getCollection, render } from "astro:content"; import { getCollection, render } from "astro:content";
import Layout from "../../layouts/Layout.astro"; import Layout from "../../layouts/Layout.astro";
import { Shield, ChevronLeft, List, PanelRight } from "@lucide/astro"; import { Shield, ChevronLeft, List, PanelRight } from "@lucide/astro";
import Author from "../../components/Author.astro";
export async function getStaticPaths() { export async function getStaticPaths() {
const notes = await getCollection("notes"); const notes = await getCollection("notes");
@@ -16,7 +17,13 @@ const { Content } = await render(entry);
const allNotes = await getCollection("notes"); const allNotes = await getCollection("notes");
const sortedNotes = allNotes.sort((a, b) => a.data.title.localeCompare(b.data.title)); const sortedNotes = allNotes.sort((a, b) => a.data.title.localeCompare(b.data.title));
const categories = [...new Set(allNotes.map((n) => n.data.category))].sort(); function getCategory(n: { id: string; data: { category?: string } }): string {
if (n.data.category) return n.data.category;
const parts = n.id.split("/");
return parts.length > 1 ? parts[0] : "General";
}
const categories = [...new Set(allNotes.map(getCategory))].sort();
function formatDate(date: Date) { function formatDate(date: Date) {
return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" }); return date.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
@@ -50,6 +57,14 @@ const graphEdges = [
...backlinks.map((n) => ({ from: n.id, to: entry.id })), ...backlinks.map((n) => ({ from: n.id, to: entry.id })),
]; ];
function extractInlineHashtags(body: string): string[] {
const re = /#(\w+)/g;
const tags: string[] = [];
let m;
while ((m = re.exec(body)) !== null) tags.push(m[1].toLowerCase());
return [...new Set(tags)];
}
function slugify(text: string) { function slugify(text: string) {
return text.toLowerCase().replace(/`[^`]*`/g, "").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-"); return text.toLowerCase().replace(/`[^`]*`/g, "").replace(/[^\w\s-]/g, "").trim().replace(/[\s_]+/g, "-");
} }
@@ -62,12 +77,21 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
} }
--- ---
<style>
/* Both sidebars sit below the navbar when in drawer-open mode */
.drawer.lg\:drawer-open > .drawer-side,
.drawer.xl\:drawer-open > .drawer-side {
top: 3rem;
height: calc(100vh - 3rem);
}
</style>
<Layout <Layout
title={`${entry.data.title} — Security Notes`} title={`${entry.data.title} — Security Notes`}
description={entry.data.description} description={entry.data.description}
> >
<div class="drawer drawer-end min-h-[calc(100vh-3rem)]"> <div class="drawer drawer-end xl:drawer-open min-h-[calc(100vh-3rem)]">
<input id="graph-drawer" type="checkbox" class="drawer-toggle" /> <input id="graph-drawer" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex min-h-[calc(100vh-3rem)]"> <div class="drawer-content flex min-h-[calc(100vh-3rem)]">
@@ -103,10 +127,10 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
</div> </div>
</div> </div>
<header class="mb-10"> <header class="mb-8">
<div class="flex items-center gap-3 mb-5"> <div class="flex items-center gap-3 mb-5">
<span class="text-xl font-bold tracking-tight"> <span class="text-xl font-bold tracking-tight">
<span class="text-primary/50 font-mono mr-0.5">/</span>{entry.data.category} <span class="text-primary/50 font-mono mr-0.5">/</span>{getCategory(entry)}
</span> </span>
<span class="text-base-content/20 text-xs">·</span> <span class="text-base-content/20 text-xs">·</span>
<time datetime={entry.data.publishDate.toISOString()} class="text-xs text-base-content/35"> <time datetime={entry.data.publishDate.toISOString()} class="text-xs text-base-content/35">
@@ -116,7 +140,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
<h1 class="text-4xl sm:text-5xl font-bold tracking-tight mb-3">{entry.data.title}</h1> <h1 class="text-4xl sm:text-5xl font-bold tracking-tight mb-3">{entry.data.title}</h1>
<p class="text-base-content/50 mb-4">{entry.data.description}</p> <p class="text-base-content/50 mb-4">{entry.data.description}</p>
{entry.data.tags.length > 0 && ( {entry.data.tags.length > 0 && (
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1 mb-4">
{entry.data.tags.map((tag) => ( {entry.data.tags.map((tag) => (
<a href={`/notes?tag=${tag}`} <a href={`/notes?tag=${tag}`}
class="font-mono text-[10px] px-1.5 py-0.5 border border-base-300/40 text-base-content/25 hover:text-primary/70 hover:border-primary/40 transition-colors"> class="font-mono text-[10px] px-1.5 py-0.5 border border-base-300/40 text-base-content/25 hover:text-primary/70 hover:border-primary/40 transition-colors">
@@ -181,7 +205,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
<div class="drawer-side z-50"> <div class="drawer-side z-50">
<label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label> <label for="nav-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<aside <aside
class="w-56 min-h-full flex flex-col border-r border-base-300/60" class="w-56 flex flex-col border-r border-base-300/60 h-full"
style="background: oklch(4% 0 0);" style="background: oklch(4% 0 0);"
> >
<div class="px-4 py-4 border-b border-base-300/40"> <div class="px-4 py-4 border-b border-base-300/40">
@@ -208,7 +232,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
</span> </span>
</div> </div>
<ul class="ml-3 space-y-0.5 border-l border-base-300/30 pl-2"> <ul class="ml-3 space-y-0.5 border-l border-base-300/30 pl-2">
{sortedNotes.filter((n) => n.data.category === cat).map((n) => ( {sortedNotes.filter((n) => getCategory(n) === cat).map((n) => (
<li> <li>
<a <a
href={`/notes/${n.id}`} href={`/notes/${n.id}`}
@@ -219,7 +243,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
: "text-base-content/45 hover:text-base-content/80 hover:bg-base-200/30", : "text-base-content/45 hover:text-base-content/80 hover:bg-base-200/30",
]} ]}
data-title={n.data.title.toLowerCase()} data-title={n.data.title.toLowerCase()}
data-tags={n.data.tags.join(",")} data-tags={[...n.data.tags, ...extractInlineHashtags(n.body ?? "")].join(",")}
> >
{n.id === entry.id ? "▶ " : ""}{n.data.title} {n.id === entry.id ? "▶ " : ""}{n.data.title}
</a> </a>
@@ -239,7 +263,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
<label for="graph-drawer" aria-label="close sidebar" class="drawer-overlay xl:hidden"></label> <label for="graph-drawer" aria-label="close sidebar" class="drawer-overlay xl:hidden"></label>
<aside <aside
id="right-sidebar" id="right-sidebar"
class="w-52 min-h-full flex flex-col border-l border-base-300/60" class="w-52 flex flex-col border-l border-base-300/60 h-full overflow-y-auto"
style="background: oklch(4% 0 0);" style="background: oklch(4% 0 0);"
> >
<div class="border-b border-base-300/40"> <div class="border-b border-base-300/40">
@@ -291,6 +315,10 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
<p class="font-mono text-[9px] text-base-content/20">no linked notes</p> <p class="font-mono text-[9px] text-base-content/20">no linked notes</p>
</div> </div>
)} )}
<div class="px-4 py-6">
<Author />
</div>
</aside> </aside>
</div> </div>
</div> </div>
@@ -462,6 +490,7 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
const graphDrawer = document.getElementById("graph-drawer") as HTMLInputElement | null; const graphDrawer = document.getElementById("graph-drawer") as HTMLInputElement | null;
if (!graphDrawer) return; if (!graphDrawer) return;
// On non-xl: let DaisyUI overlay work via checkbox
function onGraphDrawerChange() { function onGraphDrawerChange() {
if (graphDrawer!.checked) { if (graphDrawer!.checked) {
requestAnimationFrame(() => { stopGraph = startGraph() ?? null; }); requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
@@ -471,16 +500,35 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
} }
graphDrawer.addEventListener("change", onGraphDrawerChange); graphDrawer.addEventListener("change", onGraphDrawerChange);
const outerDrawer = graphDrawer.closest<HTMLElement>(".drawer.drawer-end");
const xlQuery = window.matchMedia("(min-width: 1280px)"); const xlQuery = window.matchMedia("(min-width: 1280px)");
if (xlQuery.matches && !graphDrawer.checked) {
graphDrawer.checked = true; function setXlSidebar(open: boolean) {
onGraphDrawerChange(); if (!outerDrawer) return;
} if (open) {
xlQuery.addEventListener("change", (e) => { outerDrawer.classList.add("xl:drawer-open");
if (!e.matches && graphDrawer.checked) { requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
graphDrawer.checked = false; } else {
outerDrawer.classList.remove("xl:drawer-open");
if (stopGraph) { stopGraph(); stopGraph = null; } if (stopGraph) { stopGraph(); stopGraph = null; }
} }
}
// On xl: toggle the class instead of the checkbox (avoids DaisyUI overlay + scroll lock)
const graphToggle = document.getElementById("graph-toggle");
graphToggle?.addEventListener("click", (e) => {
if (!xlQuery.matches) return;
e.preventDefault();
setXlSidebar(!outerDrawer?.classList.contains("xl:drawer-open"));
});
// Auto-open on xl, close when leaving xl
if (xlQuery.matches) {
outerDrawer?.classList.add("xl:drawer-open");
requestAnimationFrame(() => { stopGraph = startGraph() ?? null; });
}
xlQuery.addEventListener("change", (e) => {
if (!e.matches) setXlSidebar(false);
}); });
if (!document.getElementById("heading-anchor-styles")) { if (!document.getElementById("heading-anchor-styles")) {
@@ -533,12 +581,20 @@ while ((hm = headingRe.exec(entry.body ?? "")) !== null) {
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((input) => { document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((input) => {
input.addEventListener("input", (e) => { input.addEventListener("input", (e) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
const search = target.value.toLowerCase().trim(); const raw = target.value.toLowerCase().trim();
document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((o) => { document.querySelectorAll<HTMLInputElement>("[data-search]").forEach((o) => {
if (o !== target) o.value = target.value; if (o !== target) o.value = target.value;
}); });
const isTag = raw.startsWith("#");
const search = isTag ? raw.slice(1) : raw;
navItems.forEach((item) => { navItems.forEach((item) => {
const match = !search || (item.dataset.title ?? "").includes(search) || (item.dataset.tags ?? "").includes(search); const title = item.dataset.title ?? "";
const tags = item.dataset.tags ? item.dataset.tags.split(",") : [];
const match = !search || (
isTag
? tags.some((t) => t.includes(search))
: title.includes(search) || tags.join(",").includes(search)
);
item.style.display = match ? "" : "none"; item.style.display = match ? "" : "none";
}); });
}); });
+9 -3
View File
@@ -8,7 +8,13 @@ const sortedNotes = notes.sort(
(a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime() (a, b) => b.data.publishDate.getTime() - a.data.publishDate.getTime()
); );
const categories = [...new Set(notes.map((n) => n.data.category))].sort(); function getCategory(n: { id: string; data: { category?: string } }): string {
if (n.data.category) return n.data.category;
const parts = n.id.split("/");
return parts.length > 1 ? parts[0] : "General";
}
const categories = [...new Set(notes.map(getCategory))].sort();
const searchIndex = Object.fromEntries( const searchIndex = Object.fromEntries(
sortedNotes.map((n) => [ sortedNotes.map((n) => [
@@ -53,7 +59,7 @@ const searchIndex = Object.fromEntries(
<div id="notes-container" class="space-y-12"> <div id="notes-container" class="space-y-12">
{ {
categories.map((cat) => { categories.map((cat) => {
const catNotes = sortedNotes.filter((n) => n.data.category === cat); const catNotes = sortedNotes.filter((n) => getCategory(n) === cat);
return ( return (
<section data-category={cat.toLowerCase()}> <section data-category={cat.toLowerCase()}>
<div class="flex items-baseline gap-3 mb-4"> <div class="flex items-baseline gap-3 mb-4">
@@ -136,7 +142,7 @@ const searchIndex = Object.fromEntries(
const tags = card.dataset.tags ? card.dataset.tags.split(",") : []; const tags = card.dataset.tags ? card.dataset.tags.split(",") : [];
const show = !query || ( const show = !query || (
isTag isTag
? tags.some((t) => t.includes(query)) ? tags.some((t) => t.includes(query)) || (searchIndex[id] ?? "").includes(`#${query}`)
: (searchIndex[id] ?? "").includes(query) : (searchIndex[id] ?? "").includes(query)
); );
card.style.display = show ? "" : "none"; card.style.display = show ? "" : "none";