mirror of
https://github.com/anotherhadi/blog.git
synced 2026-05-20 05:32:32 +02:00
Edit sidebar
Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
@@ -1,115 +0,0 @@
|
||||
---
|
||||
import { getCategory, extractInlineHashtags } from "../utils/notes";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
interface Props {
|
||||
notes: CollectionEntry<"notes">[];
|
||||
currentEntry: CollectionEntry<"notes">;
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
const { notes, currentEntry, categories } = Astro.props;
|
||||
---
|
||||
|
||||
<style>
|
||||
/*
|
||||
* DaisyUI's menu items use display:grid with grid-auto-columns:minmax(auto,max-content)
|
||||
* which expands to fit content. Override to block so text-overflow:ellipsis works directly.
|
||||
* Inner <ul> has no w-full so DaisyUI's margin-inline-start/padding-inline-start don't overflow.
|
||||
*/
|
||||
.nav-item {
|
||||
display: block !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<aside
|
||||
class="w-56 flex flex-col border-r border-base-300/60 h-[calc(100vh-3rem)]"
|
||||
style="background: oklch(4% 0 0);"
|
||||
>
|
||||
<div class="px-3 py-3 border-b border-base-300/40">
|
||||
<label
|
||||
class="input input-sm w-full font-mono text-xs border-base-300/40 bg-base-200/50"
|
||||
>
|
||||
<span class="text-base-content/30">›</span>
|
||||
<input
|
||||
data-search
|
||||
type="text"
|
||||
placeholder="search..."
|
||||
class="text-base-content/70 placeholder:text-base-content/25"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<ul
|
||||
class="nav-sidebar menu menu-xs flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 bg-transparent"
|
||||
>
|
||||
{
|
||||
categories.map((cat) => (
|
||||
<li class="w-full">
|
||||
<details open={cat === getCategory(currentEntry)}>
|
||||
<summary class="font-bold tracking-tight text-sm">
|
||||
<span class="text-primary/50 font-mono">/</span>
|
||||
{cat}
|
||||
</summary>
|
||||
<ul>
|
||||
{notes
|
||||
.filter((n) => getCategory(n) === cat)
|
||||
.map((n) => (
|
||||
<li>
|
||||
<a
|
||||
href={`/notes/${n.id}`}
|
||||
class:list={[
|
||||
"nav-item font-mono text-xs tooltip tooltip-right",
|
||||
n.id === currentEntry.id ? "active" : "",
|
||||
]}
|
||||
data-tip={n.data.title}
|
||||
data-title={n.data.title.toLowerCase()}
|
||||
data-tags={[
|
||||
...n.data.tags,
|
||||
...extractInlineHashtags(n.body ?? ""),
|
||||
].join(",")}
|
||||
>
|
||||
{n.data.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<script>
|
||||
document.addEventListener("astro:page-load", () => {
|
||||
const navItems = document.querySelectorAll<HTMLElement>(".nav-item");
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>("[data-search]")
|
||||
.forEach((input) => {
|
||||
input.addEventListener("input", (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const raw = target.value.toLowerCase().trim();
|
||||
document
|
||||
.querySelectorAll<HTMLInputElement>("[data-search]")
|
||||
.forEach((o) => {
|
||||
if (o !== target) o.value = target.value;
|
||||
});
|
||||
const isTag = raw.startsWith("#");
|
||||
const search = isTag ? raw.slice(1) : raw;
|
||||
navItems.forEach((item) => {
|
||||
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";
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,158 @@
|
||||
<script lang="ts">
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
interface Note {
|
||||
id: string;
|
||||
data: { title: string; tags: string[]; category?: string };
|
||||
body?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
notes: Note[];
|
||||
currentEntry: Note;
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
const { notes, currentEntry, categories }: Props = $props();
|
||||
|
||||
let search = $state("");
|
||||
|
||||
function getCategory(n: Note): string {
|
||||
if (n.data.category) return n.data.category;
|
||||
const parts = n.id.split("/");
|
||||
return parts.length > 1 ? parts[0] : "General";
|
||||
}
|
||||
|
||||
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 matchesSearch(note: Note): boolean {
|
||||
const raw = search.toLowerCase().trim();
|
||||
if (!raw) return true;
|
||||
const isTag = raw.startsWith("#");
|
||||
const term = isTag ? raw.slice(1) : raw;
|
||||
const title = note.data.title.toLowerCase();
|
||||
const tags = [
|
||||
...note.data.tags,
|
||||
...extractInlineHashtags(note.body ?? ""),
|
||||
].map((t) => t.toLowerCase());
|
||||
return isTag
|
||||
? tags.some((t) => t.includes(term))
|
||||
: title.includes(term) || tags.join(",").includes(term);
|
||||
}
|
||||
|
||||
let openCategories = $state<string[]>(
|
||||
categories.filter((c) => c === getCategory(currentEntry)),
|
||||
);
|
||||
|
||||
function toggle(cat: string) {
|
||||
if (openCategories.includes(cat)) {
|
||||
openCategories = openCategories.filter((c) => c !== cat);
|
||||
} else {
|
||||
openCategories = [...openCategories, cat];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="w-56 shrink-0 flex flex-col border-r border-base-300/60 h-[calc(100vh-3rem)]"
|
||||
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="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"
|
||||
>
|
||||
<span class="text-base-content/30 font-mono text-xs shrink-0">›</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="search..."
|
||||
bind:value={search}
|
||||
class="flex-1 min-w-0 bg-transparent text-xs font-mono text-base-content/70 placeholder:text-base-content/25 outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nav -->
|
||||
<nav class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden px-2 py-2 space-y-px">
|
||||
{#each categories as cat}
|
||||
{@const catNotes = notes.filter(
|
||||
(n) => getCategory(n) === cat && matchesSearch(n),
|
||||
)}
|
||||
{#if catNotes.length > 0 || !search}
|
||||
<div>
|
||||
<!-- Category header -->
|
||||
<button
|
||||
onclick={() => toggle(cat)}
|
||||
class="w-full flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-base-200/40 transition-colors duration-150 group"
|
||||
>
|
||||
<svg
|
||||
class="w-3 h-3 text-base-content/35 shrink-0 transition-transform duration-200"
|
||||
class:rotate-90={openCategories.includes(cat)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
<span class="text-primary/50 font-mono text-xs shrink-0">/</span>
|
||||
<span
|
||||
class="font-bold tracking-tight text-sm truncate text-base-content/80"
|
||||
>
|
||||
{cat}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- Notes list -->
|
||||
{#if openCategories.includes(cat)}
|
||||
<ul
|
||||
class="ml-4 mt-0.5 pb-1 space-y-px"
|
||||
transition:slide={{ duration: 180 }}
|
||||
>
|
||||
{#each catNotes as note}
|
||||
<li>
|
||||
<a
|
||||
href={`/notes/${note.id}`}
|
||||
title={note.data.title}
|
||||
class="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-mono truncate transition-colors duration-150
|
||||
{note.id === currentEntry.id
|
||||
? 'text-primary/90 bg-primary/10'
|
||||
: 'text-base-content/45 hover:text-base-content/80 hover:bg-base-200/40'}"
|
||||
>
|
||||
<span class="shrink-0 font-mono text-base-content/20">
|
||||
{note.id === currentEntry.id ? "▸" : "–"}
|
||||
</span>
|
||||
<span class="truncate">{note.data.title}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -3,7 +3,7 @@ import { getCollection, render } from "astro:content";
|
||||
import Layout from "../../layouts/Layout.astro";
|
||||
import { List, PanelRight } from "@lucide/astro";
|
||||
import NoteTOC from "../../components/NoteTOC.astro";
|
||||
import NoteNavSidebar from "../../components/NoteNavSidebar.astro";
|
||||
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
||||
import NoteGraphSidebar from "../../components/NoteGraphSidebar.astro";
|
||||
import NoteVars from "../../components/NoteVars.svelte";
|
||||
import {
|
||||
@@ -199,6 +199,7 @@ const headings = extractHeadings(entry.body ?? "");
|
||||
class="drawer-overlay"
|
||||
></label>
|
||||
<NoteNavSidebar
|
||||
client:load
|
||||
notes={sortedNotes}
|
||||
currentEntry={entry}
|
||||
categories={categories}
|
||||
|
||||
Reference in New Issue
Block a user