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 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.astro";
|
import NoteNavSidebar from "../../components/NoteNavSidebar.svelte";
|
||||||
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 {
|
import {
|
||||||
@@ -199,6 +199,7 @@ const headings = extractHeadings(entry.body ?? "");
|
|||||||
class="drawer-overlay"
|
class="drawer-overlay"
|
||||||
></label>
|
></label>
|
||||||
<NoteNavSidebar
|
<NoteNavSidebar
|
||||||
|
client:load
|
||||||
notes={sortedNotes}
|
notes={sortedNotes}
|
||||||
currentEntry={entry}
|
currentEntry={entry}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
- Fix left sidebar overflow/minwidth issue
|
|
||||||
- The cat just TP
|
|
||||||
Reference in New Issue
Block a user