Commit 7b628e7

HPCesia <me@hpcesia.com>
2025-01-21 14:01:02
feat: toc card
1 parent 5272a07
src/components/widgets/AuthorInfoCard.astro
src/components/widgets/Pagination.astro
@@ -133,7 +133,6 @@ else {
         const [page, url] = p.split(':', 2);
         return { page: Number(page), url: url };
       });
-      console.log(specialPages);
       return specialPages?.find((p) => p.page === page)?.url || `${baseUrl}/${page}`;
     }
 
src/components/widgets/TOC.astro
@@ -0,0 +1,45 @@
+---
+import type { MarkdownHeading } from 'astro';
+
+interface Props {
+  class?: string;
+  headings: MarkdownHeading[];
+}
+
+const { headings = [], class: className } = Astro.props;
+const minDepth = Math.min(...headings.map((h) => h.depth));
+const maxLevel = 3;
+---
+
+<div class:list={['theme-card-bg theme-border border-2 rounded-xl p-2', className]}>
+  <div></div>
+  {
+    headings
+      .filter((heading) => heading.depth < minDepth + maxLevel)
+      .map((heading) => (
+        <a class:list={[`level-${heading.depth - minDepth + 1}`]} href={`#${heading.slug}`}>
+          {heading.text}
+        </a>
+      ))
+  }
+</div>
+
+<style lang="scss">
+  $max-level: 3;
+
+  a {
+    display: block;
+    padding: 0.5rem 1rem;
+
+    @apply duration-200 ease-in-out;
+    @apply hover:scale-105;
+    @apply active:scale-95;
+
+    @for $i from 1 through $max-level {
+      &.level-#{$i} {
+        padding-left: #{($i - 1) * 0.5 + 1}rem;
+        font-size: #{1 - ($i - 1) * 0.1}rem;
+      }
+    }
+  }
+</style>
src/layouts/GridLayout.astro
@@ -10,10 +10,13 @@ const { title, description, lang } = Astro.props;
 ---
 
 <MainLayout title={title} description={description} lang={lang}>
-  <div id="main-content" class="my-4 w-full mx-4">
+  <div id="main-content" class="my-4 w-full">
     <slot />
   </div>
-  <div id="aside-content" class="max-xl:hidden my-4 mx-2 w-96">
-    <slot name="aside" slot="aside" />
+  <div id="aside-content" class="max-xl:hidden my-4 w-96 flex flex-col gap-4">
+    <slot name="aside-fixed" slot="aside-fixed" />
+    <div class="sticky flex flex-col gap-4 top-20">
+      <slot name="aside-sticky" slot="aside-sticky" />
+    </div>
   </div>
 </MainLayout>
src/pages/posts/[article].astro
@@ -3,6 +3,7 @@ import { getCollection, render } from 'astro:content';
 import GridLayout from '@layouts/GridLayout.astro';
 import ProfileCard from '@components/widgets/ProfileCard.astro';
 import '@/styles/article.scss';
+import TOC from '@components/widgets/TOC.astro';
 
 export async function getStaticPaths() {
   const articles = await getCollection('posts');
@@ -13,7 +14,7 @@ export async function getStaticPaths() {
 }
 
 const { article } = Astro.props;
-const { Content } = await render(article);
+const { Content, headings } = await render(article);
 ---
 
 <GridLayout>
@@ -22,7 +23,10 @@ const { Content } = await render(article);
       <Content />
     </article>
   </div>
-  <Fragment slot="aside">
+  <Fragment slot="aside-fixed">
     <ProfileCard />
   </Fragment>
+  <Fragment slot="aside-sticky">
+    <TOC headings={headings} />
+  </Fragment>
 </GridLayout>
src/styles/article.scss
@@ -1,27 +1,34 @@
 article {
   h1 {
-    @apply text-2xl font-bold;
-    @apply mt-4 mb-2;
+    @apply text-2xl;
   }
 
   h2 {
-    @apply text-xl font-bold;
-    @apply mt-4 mb-2;
+    @apply text-xl;
   }
 
   h3 {
-    @apply text-lg font-bold;
-    @apply mt-4 mb-2;
+    @apply text-lg;
   }
 
   h4 {
-    @apply text-base font-bold;
-    @apply mt-4 mb-2;
+    @apply text-base;
   }
 
   h5 {
-    @apply text-base font-bold;
-    @apply mt-4 mb-2;
+    @apply text-base;
+  }
+
+  h1,
+  h2,
+  h3,
+  h4,
+  h5 {
+    display: inline;
+    width: 100%;
+    scroll-margin-top: 4rem;
+    font-weight: bold;
+    margin: 1rem 0 0.5rem;
   }
 
   p {