mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-15 19:22:52 +02:00
🚀 refactor: toc component with responsive design
- Implement scroll-sync highlight tracking - Add auto-scroll functionality for active items - Remove animations during view transitions
This commit is contained in:
parent
4c8dff619e
commit
7c6172e0ae
5 changed files with 449 additions and 309 deletions
|
@ -6,6 +6,7 @@ import robotsTxt from 'astro-robots-txt'
|
|||
import { defineConfig } from 'astro/config'
|
||||
import rehypeExternalLinks from 'rehype-external-links'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import rehypeSlug from 'rehype-slug'
|
||||
import remarkMath from 'remark-math'
|
||||
import UnoCSS from 'unocss/astro'
|
||||
import { themeConfig } from './src/config'
|
||||
|
@ -56,6 +57,7 @@ export default defineConfig({
|
|||
remarkReadingTime,
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeSlug,
|
||||
rehypeKatex,
|
||||
[
|
||||
rehypeExternalLinks,
|
||||
|
|
10
package.json
10
package.json
|
@ -22,7 +22,7 @@
|
|||
"astro-compress": "^2.3.7",
|
||||
"astro-og-canvas": "^0.7.0",
|
||||
"astro-robots-txt": "^1.0.0",
|
||||
"canvaskit-wasm": "^0.39.1",
|
||||
"canvaskit-wasm": "^0.40.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"overlayscrollbars": "^2.11.1",
|
||||
"photoswipe": "^5.4.4",
|
||||
|
@ -38,9 +38,9 @@
|
|||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/node": "^22.13.14",
|
||||
"@types/sanitize-html": "^2.15.0",
|
||||
"@unocss/eslint-plugin": "66.1.0-beta.7",
|
||||
"@unocss/preset-attributify": "66.1.0-beta.7",
|
||||
"@unocss/reset": "66.1.0-beta.7",
|
||||
"@unocss/eslint-plugin": "66.1.0-beta.8",
|
||||
"@unocss/preset-attributify": "66.1.0-beta.8",
|
||||
"@unocss/reset": "66.1.0-beta.8",
|
||||
"astro-eslint-parser": "^1.2.2",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-plugin-astro": "^1.3.1",
|
||||
|
@ -49,7 +49,7 @@
|
|||
"reading-time": "^1.5.0",
|
||||
"sharp": "^0.33.5",
|
||||
"typescript": "~5.8.2",
|
||||
"unocss": "66.1.0-beta.7",
|
||||
"unocss": "66.1.0-beta.8",
|
||||
"unocss-preset-theme": "^0.14.1"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
|
565
pnpm-lock.yaml
generated
565
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -93,4 +93,10 @@ document.addEventListener('astro:after-swap', setupScrollbar)
|
|||
--os-handle-bg-active: oklch(var(--un-preset-theme-colors-secondary) / 0.35);
|
||||
--os-handle-min-size: 12%;
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
#toc-content .os-scrollbar {
|
||||
--at-apply: 'hidden';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,27 +20,34 @@ const filteredHeadings = headings.filter(heading =>
|
|||
|
||||
{filteredHeadings.length > 0 && (
|
||||
<div
|
||||
class="relative mb-6 bg-secondary/5"
|
||||
class="mb-6 bg-secondary/5 2xl:(fixed left-0 top-53 max-w-[min(calc(50vw-38rem),13rem)] border-none bg-transparent)"
|
||||
border="~ secondary/5 rounded solid"
|
||||
transition="~ duration-300 ease-in-out"
|
||||
>
|
||||
{/* Accordion toggle for expandable TOC */}
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toc-toggle"
|
||||
class="accordion-toggle"
|
||||
hidden
|
||||
/>
|
||||
<label
|
||||
for="toc-toggle"
|
||||
class="absolute inset-0 z-99 cursor-pointer"
|
||||
></label>
|
||||
<div class="relative h-12 w-full bg-transparent">
|
||||
<label
|
||||
for="toc-toggle"
|
||||
class="absolute inset-0 flex cursor-pointer items-center 2xl:(static h-max w-max flex c-secondary/60 transition-colors ease-in hover:c-secondary)"
|
||||
>
|
||||
<span class="toc-title">
|
||||
{currentUI.toc}
|
||||
</span>
|
||||
|
||||
{/* TOC title bar */}
|
||||
<div class="h-12 w-full flex items-center bg-secondary/0">
|
||||
<span class="toc-title">
|
||||
{currentUI.toc}
|
||||
</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
class="ml-4 hidden aspect-square w-4.2 2xl:(mt-4 block origin-center active:scale-90)"
|
||||
>
|
||||
<path d="m12 1c-6.1 0-11 4.9-11 11s4.9 11 11 11 11-4.9 11-11-4.9-11-11-11m0 20c-5.8 0-10.5-4-10.5-9s4.7-9 10.5-9 10.5 4 10.5 9-4.7 9-10.5 9" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Expandable content wrapper */}
|
||||
|
@ -77,36 +84,154 @@ const filteredHeadings = headings.filter(heading =>
|
|||
</div>
|
||||
)}
|
||||
|
||||
<!-- Override heti default styles -->
|
||||
<!-- Override heti default styles >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
|
||||
<style>
|
||||
.toc-title {
|
||||
--at-apply: 'font-semibold pl-4 px-4 py-3';
|
||||
--at-apply: 'font-semibold ml-4 select-none 2xl:hidden';
|
||||
}
|
||||
|
||||
.toc-list {
|
||||
--at-apply: 'list-none pl-0 space-y-2 mt-1 mb-4';
|
||||
--at-apply: 'list-none pl-0 space-y-2 mt-1 mb-4 2xl:space-y-1.2';
|
||||
}
|
||||
.toc-link-h2 {
|
||||
--at-apply: 'text-sm no-underline font-semibold';
|
||||
}
|
||||
.toc-link-h3 {
|
||||
--at-apply: 'text-sm no-underline font-normal';
|
||||
}
|
||||
.toc-link-h4 {
|
||||
--at-apply: 'text-sm no-underline font-normal';
|
||||
|
||||
.toc-link-h2, .toc-link-h3, .toc-link-h4 {
|
||||
--at-apply: 'text-sm no-underline font-normal text-balance select-none 2xl:(text-3.2 c-secondary/60 transition-colors ease-in hover:(c-secondary font-medium))';
|
||||
}
|
||||
|
||||
/* Initial collapsed state with zero height grid row */
|
||||
.accordion-wrapper {
|
||||
--at-apply: 'grid rows-[0fr] duration-300 ease-in-out';
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
--at-apply: 'overflow-hidden max-h-66 lg:max-h-82 z-99 pl-4';
|
||||
}
|
||||
--at-apply: 'overflow-hidden max-h-66 lg:max-h-82 2xl:(max-h-[calc(100vh-21.5rem)]) px-4';
|
||||
}
|
||||
|
||||
/* When toggle is checked, expand the wrapper to show content */
|
||||
.accordion-toggle:checked ~ .accordion-wrapper {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.accordion-toggle:checked ~ .accordion-wrapper .accordion-content {
|
||||
--at-apply: 'overflow-y-auto';
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.accordion-wrapper {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.accordion-toggle:checked ~ .accordion-wrapper {
|
||||
grid-template-rows: 0fr;
|
||||
}
|
||||
|
||||
.accordion-content {
|
||||
--at-apply: 'overflow-y-auto';
|
||||
}
|
||||
|
||||
.accordion-toggle:checked ~ .accordion-wrapper .accordion-content {
|
||||
--at-apply: 'overflow-hidden';
|
||||
}
|
||||
|
||||
.toc-link-active {
|
||||
--at-apply: 'c-secondary font-medium';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- TOC Highlight Script >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
|
||||
<script>
|
||||
function setupTOCHighlight() {
|
||||
const tocContent = document.getElementById('toc-content')
|
||||
if (!tocContent)
|
||||
return
|
||||
|
||||
const tocLinks = tocContent.querySelectorAll('a')
|
||||
if (tocLinks.length === 0)
|
||||
return
|
||||
|
||||
// Check if in large screen (2xl) mode
|
||||
let is2xl = window.innerWidth >= 1536
|
||||
|
||||
// Build mapping from heading IDs to TOC links
|
||||
const headingMap = new Map<string, HTMLAnchorElement>()
|
||||
tocLinks.forEach((link) => {
|
||||
const id = link.getAttribute('href')?.substring(1)
|
||||
if (id)
|
||||
headingMap.set(id, link as HTMLAnchorElement)
|
||||
})
|
||||
|
||||
let currentActiveLink: HTMLAnchorElement | null = null
|
||||
|
||||
function highlightLink(link: HTMLAnchorElement) {
|
||||
if (link === currentActiveLink)
|
||||
return
|
||||
|
||||
if (currentActiveLink) {
|
||||
currentActiveLink.classList.remove('toc-link-active')
|
||||
}
|
||||
|
||||
link.classList.add('toc-link-active')
|
||||
currentActiveLink = link
|
||||
|
||||
// Only scroll the TOC in large screen (2xl) mode
|
||||
if (is2xl) {
|
||||
scrollLinkToCenter(link)
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll the link to center position
|
||||
function scrollLinkToCenter(link: HTMLAnchorElement) {
|
||||
link.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
})
|
||||
}
|
||||
|
||||
// Intersection observer callback
|
||||
const intersectionCallback = (entries: IntersectionObserverEntry[]) => {
|
||||
const visibleHeading = entries.find(entry => entry.isIntersecting)?.target
|
||||
|
||||
if (visibleHeading && visibleHeading.id) {
|
||||
const link = headingMap.get(visibleHeading.id)
|
||||
if (link)
|
||||
highlightLink(link)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the intersection observer
|
||||
const headingObserver = new IntersectionObserver(intersectionCallback, {
|
||||
rootMargin: '-5% 0% -85% 0%',
|
||||
threshold: [0.4],
|
||||
})
|
||||
|
||||
// Observe all heading elements
|
||||
document.querySelectorAll('h2, h3, h4').forEach((heading) => {
|
||||
if (heading.id && heading.id !== 'footnotes') {
|
||||
headingObserver.observe(heading)
|
||||
}
|
||||
})
|
||||
|
||||
// Highlight the first TOC item by default
|
||||
if (tocLinks.length > 0) {
|
||||
highlightLink(tocLinks[0] as HTMLAnchorElement)
|
||||
}
|
||||
|
||||
// Listen for window resize events
|
||||
window.addEventListener('resize', () => {
|
||||
const newIs2xl = window.innerWidth >= 1536
|
||||
if (is2xl !== newIs2xl) {
|
||||
// Update screen size flag
|
||||
is2xl = newIs2xl
|
||||
|
||||
if (currentActiveLink && is2xl) {
|
||||
scrollLinkToCenter(currentActiveLink)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setupTOCHighlight()
|
||||
document.addEventListener('astro:after-swap', setupTOCHighlight)
|
||||
</script>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue