Merge branch 'master' into giscus

This commit is contained in:
radishzzz 2025-05-29 20:35:59 +01:00
commit 4b270755bf
356 changed files with 21152 additions and 3604 deletions

View file

@ -1,4 +1,6 @@
---
import LanguageSwitcherIcon from '@/assets/icons/language-switcher.svg'
import ThemeToggleIcon from '@/assets/icons/theme-toggle.svg'
import { moreLocales, themeConfig } from '@/config'
import { getNextGlobalLangPath, getNextSupportedLangPath } from '@/i18n/path'
import { isPostPage, isTagPage } from '@/utils/page'
@ -7,7 +9,10 @@ interface Props {
supportedLangs: string[]
}
const { light: { background: lightMode }, dark: { background: darkMode } } = themeConfig.color
const {
light: { background: lightMode },
dark: { background: darkMode },
} = themeConfig.color
const { supportedLangs } = Astro.props
const currentPath = Astro.url.pathname
@ -20,15 +25,14 @@ const showLanguageSwitcher = moreLocales.length > 0
const useSupportedLangs = isPost || (isTag && supportedLangs.length > 0)
// Choose a language switch list according to the page type
const nextUrl = useSupportedLangs
? getNextSupportedLangPath(currentPath, supportedLangs) // Switch between supported languages
: getNextGlobalLangPath(currentPath) // Switch between all languages
? getNextSupportedLangPath(currentPath, supportedLangs) // Switch between supported languages in post/tag page
: getNextGlobalLangPath(currentPath) // Switch between all languages in other pages
---
<div
class:list={[
'absolute flex gap-6 top-14.6 right-7.25vw min-[823px]:max-[1024px]:right-[calc(50vw-22rem)]',
'[@supports(-webkit-touch-callout:none)]:top-13.6', // fix position issue on ios
'lg:(fixed w-14rem top-auto bottom-47 right-[max(5.625rem,calc(50vw-34.375rem))])',
'absolute right-7.25vw top-14.6 flex gap-6 min-[823px]:max-[1024px]:right-[calc(50vw-22rem)]',
'lg:(fixed bottom-48 right-[max(5rem,calc(50vw-35rem))] top-auto w-14rem)',
]}
>
<!-- Language Switcher -->
@ -38,36 +42,31 @@ const nextUrl = useSupportedLangs
class="aspect-square w-4 c-secondary active:scale-90 hover:c-primary"
aria-label="Switch website language"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
<LanguageSwitcherIcon
aria-hidden="true"
class="h-full w-full"
fill="currentColor"
>
<path d="M19.1 21 12.4 2h-1.2L4.6 21l-2.5.2v.8h6.3v-.8L5.6 21l2-5.9h7.5l2 5.9-3.3.2v.8h8.1v-.8zM7.9 14.3l3.4-10.1 3.5 10.1z" />
</svg>
/>
</a>
)}
<!-- Theme Toggle -->
<button
id="theme-toggle-button"
aria-label="Switch light/dark theme"
class="button-theme-toggle aspect-square w-4.2 c-secondary active:scale-90 hover:c-primary"
class="aspect-square w-4 c-secondary active:scale-90 hover:c-primary"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
<ThemeToggleIcon
aria-hidden="true"
fill="currentColor"
>
<path d="M12 1C5.9 1 1 5.9 1 12s4.9 11 11 11 11-4.9 11-11S18.1 1 12 1m0 20c-5.8 0-10.5-4-10.5-9S6.2 3 12 3s10.5 4 10.5 9-4.7 9-10.5 9" />
</svg>
/>
</button>
</div>
<!-- Theme Toggle Script >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
<script is:inline define:vars={{ lightMode, darkMode }}>
<script
is:inline
define:vars={{ lightMode, darkMode }}
>
// Update theme
function updateTheme() {
// Toggle website theme
@ -76,7 +75,7 @@ function updateTheme() {
// Get current theme
const isDark = document.documentElement.classList.contains('dark')
// Update meta theme color
const metaThemeColor = document.querySelector('meta[name="theme-color"]')
const metaThemeColor = document.head.querySelector('meta[name="theme-color"]')
if (metaThemeColor) {
metaThemeColor.setAttribute('content', isDark ? darkMode : lightMode)
}
@ -89,29 +88,29 @@ function updateTheme() {
// Bind click event to the button
function setupThemeToggle() {
// Locate theme toggle button
const themeToggleButtons = document.querySelectorAll('.button-theme-toggle')
// Add click listener to each button
themeToggleButtons.forEach((button) => {
button.addEventListener('click', () => {
// If browser doesn't support View Transitions API, update theme directly
if (!document.startViewTransition) {
const themeToggleButton = document.getElementById('theme-toggle-button')
// Add click listener to the button
if (themeToggleButton) {
themeToggleButton.addEventListener('click', () => {
// If reduceMotion is enabled or browser doesn't support View Transitions API, update theme directly
if (document.documentElement.classList.contains('reduce-motion') || !document.startViewTransition) {
updateTheme()
return
}
// Temporarily add markers during animation to implement view transition and disable CSS transitions
document.documentElement.style.setProperty('view-transition-name', 'theme-transition')
document.documentElement.setAttribute('data-theme-transition', '')
document.documentElement.style.setProperty('view-transition-name', 'animation-theme-toggle')
document.documentElement.setAttribute('data-theme-changing', '')
// If browser supports View Transitions API, use it to update theme
const themeTransition = document.startViewTransition(updateTheme)
// Remove markers after animation
themeTransition.finished.then(() => {
document.documentElement.style.removeProperty('view-transition-name')
document.documentElement.removeAttribute('data-theme-transition')
document.documentElement.removeAttribute('data-theme-changing')
})
})
})
}
}
// Initialize click event (on first load or page transition)

View file

@ -2,66 +2,56 @@
import { defaultLocale, themeConfig } from '@/config'
import { walineLocaleMap } from '@/i18n/config'
const {
serverURL = '',
emoji = [],
search = false,
imageUploader = false,
} = themeConfig.comment?.waline ?? {}
// Get the language code of Waline
function getWalineLang(currentPath: string, defaultLocale: string): string {
// Extract language code from path
const pathLang = Object.keys(walineLocaleMap).find(code =>
currentPath.startsWith(`/${code}/`),
)
// Return found path language or default language
const lang = pathLang || defaultLocale
return walineLocaleMap[lang as keyof typeof walineLocaleMap]
}
// Get Waline language and generate configuration json
const walineLang = getWalineLang(Astro.url.pathname, defaultLocale)
const walineConfigJson = JSON.stringify({
serverURL,
lang: walineLang,
emoji,
search,
imageUploader,
})
const { waline: { serverURL = '', emoji = [], search = false, imageUploader = false } = {} } = themeConfig.comment ?? {}
---
<div
id="waline"
class="mt-16"
data-config={walineConfigJson}
>
</div>
></div>
<!-- Not use is:inline or define:vars >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
<script>
import { init } from '@waline/client'
import '@waline/client/style'
<!-- Waline Script >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
<script
is:inline
define:vars={{
serverURL,
emoji,
search,
imageUploader,
walineLocaleMap,
defaultLocale,
}}
type="module"
>
import { init } from '/vendors/waline/waline.js'
function initWaline() {
const walineEl = document.getElementById('waline')
const walineConfig = JSON.parse(walineEl?.dataset.config || '{}')
const currentPath = window.location.pathname
const pathLang = Object.keys(walineLocaleMap).find(code =>
currentPath.startsWith(`/${code}/`),
)
const lang = pathLang || defaultLocale
const currentWalineLang = walineLocaleMap[lang]
init({
el: '#waline',
path: window.location.pathname.replace(/^\/([a-z]{2}(-[a-z]{2})?)\//, '/'),
serverURL,
path: window.location.pathname.replace(/^\/([a-z]{2}(-[a-z]{2})?)\//, '/'), // Share comments on posts in different languages
lang: currentWalineLang,
emoji,
dark: 'html.dark',
requiredMeta: ['nick', 'mail'],
highlighter: false,
texRenderer: false,
imageUploader,
search,
noCopyright: true,
reaction: [],
...walineConfig,
})
}
initWaline()
document.addEventListener('astro:after-swap', initWaline)
document.addEventListener('astro:page-load', initWaline)
</script>
<!-- Custom CSS Styles >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
@ -69,141 +59,123 @@ document.addEventListener('astro:after-swap', initWaline)
#waline .wl-login-info {
--at-apply: 'mt-0 mr-3'
}
#waline .wl-avatar {
--at-apply: 'border-none'
}
#waline .wl-logout-btn {
--at-apply: 'z-99'
}
#waline .wl-login-nick:not(:has(img)) {
--at-apply: 'leading-3.6 mt-1.4';
}
#waline .wl-panel {
--at-apply: 'm-0 rounded-lg border-secondary/25'
}
#waline .wl-header {
--at-apply: 'p-0';
}
#waline .wl-header-item {
border-bottom: 1px solid var(--waline-border-color);
--at-apply: 'border-b border-solid border-primary/25';
}
#waline .wl-header label {
--at-apply: 'text-3';
}
#waline .wl-header input {
--at-apply: 'text-2.8';
}
#waline .wl-card,
#waline .wl-header.item3 {
--at-apply: 'border-b-0';
}
#waline .wl-card .wl-quote {
--at-apply: 'border-is-none mt-6';
--at-apply: 'border-is-none mt-4';
}
#waline .wl-editor {
--at-apply: 'min-h-24';
}
#waline .wl-editor::placeholder {
color: var(--waline-light-grey);
--at-apply: 'c-primary/25';
}
#waline .wl-footer {
--at-apply: 'm-2';
}
#waline .wl-text-number,
#waline .wl-action[title="Markdown Guide"],
#waline .wl-sort,
#waline .wl-emoji-popup .wl-tab-wrapper::-webkit-scrollbar,
#waline .wl-gallery::-webkit-scrollbar {
--at-apply: 'hidden';
}
#waline .wl-emoji-popup {
--at-apply: 'start-0 border-secondary/25';
--at-apply: 'start-0 border-secondary/25 max-w-532px';
}
#waline .wl-emoji-popup .wl-tab-wrapper,
#waline .wl-gallery {
--at-apply: 'scrollbar-hidden';
#waline .wl-emoji-popup .wl-tab-wrapper {
scrollbar-width: thin;
}
#waline .wl-gif-popup {
--at-apply: 'border-secondary/25';
}
#waline .wl-gif-popup input {
--at-apply: 'bg-background border-secondary/25';
}
#waline .wl-gif-popup input::placeholder {
--at-apply: 'c-secondary/30 text-3.5';
}
#waline .wl-gallery {
--at-apply: 'scrollbar-hidden';
}
#waline .wl-meta-head {
--at-apply: 'pt-3 pb-2 px-0';
}
#waline .wl-card-item {
--at-apply: 'px-0';
}
#waline .wl-user-avatar {
--at-apply: 'mt-1';
}
#waline .wl-content p {
--at-apply: 'leading-6 text-3.5';
}
#waline .wl-time {
color: oklch(var(--un-preset-theme-colors-primary) / 0.75);
--at-apply: 'c-primary/75';
}
#waline .wl-edit,
#waline .wl-delete {
--at-apply: 'mr-0.4';
}
#waline .wl-like {
--at-apply: 'mr-1.2';
}
</style>
<!-- Official CSS Variables >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
<!-- https://waline.js.org/reference/client/style.html -->
<style>
#waline {
/* Regular Colors */
--waline-white: var(--uno-colors-background);
--waline-light-grey: oklch(var(--un-preset-theme-colors-primary) / 0.25);
--waline-dark-grey: var(--uno-colors-secondary);
#waline {
/* Regular Colors */
--waline-white: oklch(var(--un-preset-theme-colors-background));
--waline-light-grey: oklch(var(--un-preset-theme-colors-primary) / 0.25);
--waline-dark-grey: oklch(var(--un-preset-theme-colors-secondary));
/* Theme Colors */
--waline-theme-color: var(--uno-colors-primary);
--waline-active-color: var(--uno-colors-primary);
/* Theme Colors */
--waline-theme-color: oklch(var(--un-preset-theme-colors-primary));
--waline-active-color: oklch(var(--un-preset-theme-colors-primary));
/* Layout Colors */
--waline-color: var(--uno-colors-secondary);
--waline-bg-color: var(--uno-colors-background);
--waline-bg-color-light: var(--uno-colors-background);
--waline-bg-color-hover: var(--uno-colors-background);
--waline-border-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
--waline-disable-bg-color: oklch(var(--un-preset-theme-colors-secondary) / 0.05);
--waline-disable-color: var(--uno-colors-primary);
/* Layout Colors */
--waline-color: oklch(var(--un-preset-theme-colors-secondary));
--waline-bg-color: oklch(var(--un-preset-theme-colors-background));
--waline-bg-color-light: oklch(var(--un-preset-theme-colors-background));
--waline-bg-color-hover: oklch(var(--un-preset-theme-colors-background));
--waline-border-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
--waline-disable-bg-color: oklch(var(--un-preset-theme-colors-secondary) / 0.05);
--waline-disable-color: oklch(var(--un-preset-theme-colors-primary));
/* Special Colors */
--waline-bq-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
/* Special Colors */
--waline-bq-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
/* Information */
--waline-info-bg-color: var(--uno-colors-background);
--waline-info-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
/* Information */
--waline-info-bg-color: oklch(var(--un-preset-theme-colors-background));
--waline-info-color: oklch(var(--un-preset-theme-colors-primary) / 0.25);
/* Rendering Options */
--waline-avatar-radius: 0.5rem;
}
</style>
/* Rendering Options */
--waline-avatar-radius: 0.5rem;
}
</style>

View file

@ -10,39 +10,46 @@ const year = Number(startYear) === currentYear
? startYear
: `${startYear}-${currentYear}`
// i18n RSS Path
// i18n RSS Feed Path
const currentLang = getLangFromPath(Astro.url.pathname)
const links = socialLinks.map((link) => {
if (link.name === 'RSS') {
return {
...link,
url: currentLang === defaultLocale ? link.url : `/${currentLang}${link.url}`,
url: currentLang === defaultLocale
? link.url
: `/${currentLang}${link.url}`,
}
}
if (link.name === 'Email') {
return {
...link,
url: `mailto:${link.url}`,
}
}
return link
})
const footerLinkClass = 'highlight-hover py-0.8 transition-colors after:bottom-0.35em hover:c-primary'
---
<footer
class="text-3 leading-4.75 font-navbar lg:text-3.5"
class="text-3 leading-1.25em font-navbar lg:text-3.5"
lg="uno-desktop-column bottom-20"
>
<p>
{links.map((link, index) => (
<>
<a class="transition-colors hover:c-primary" href={link.url}>
{link.name}
</a>
{index < links.length - 1 && ' / '}
<a class={footerLinkClass} href={link.url} target="_blank" rel="noopener noreferrer">{link.name}</a>&nbsp;{index < links.length - 1 && '/'}
</>
))}
</p>
<p>
Powered by <a class="transition-colors hover:c-primary" href="https://astro.build/">Astro</a> and <a class="transition-colors hover:c-primary" href="https://github.com/radishzzz/astro-theme-retypeset">Retypeset</a>
© {year} {author}
</p>
<p>
© {year} {author}
Powered by <a class={footerLinkClass} href="https://astro.build/" target="_blank" rel="noopener noreferrer">Astro</a> and <a class={footerLinkClass} href="https://github.com/radishzzz/astro-theme-retypeset" target="_blank" rel="noopener noreferrer">Retypeset</a>
</p>
</footer>

View file

@ -5,24 +5,11 @@ import { getPageInfo } from '@/utils/page'
const { currentLang, getLocalizedPath, isPost } = getPageInfo(Astro.url.pathname)
const { title, subtitle, i18nTitle } = themeConfig.site
const { titleGap } = themeConfig.global
const currentUI = ui[currentLang as keyof typeof ui]
const headerTitle = i18nTitle ? currentUI.title : title
const headerSubtitle = i18nTitle ? currentUI.subtitle : subtitle
const marginBottom = {
1: 'mb-0.9',
2: 'mb-1.8',
3: 'mb-2.7',
}[titleGap] || 'mb-1.8 '
const postMarginBottom = {
1: 'mb-1.9 lg:mb-0.9',
2: 'mb-2.8 lg:mb-1.8',
3: 'mb-3.7 lg:mb-2.7',
}[titleGap] || 'mb-2.8 lg:mb-1.8'
const TitleTag = isPost ? 'h2' : 'h1'
const SubtitleTag = isPost ? 'div' : 'h2'
---
@ -30,14 +17,14 @@ const SubtitleTag = isPost ? 'div' : 'h2'
<header
class:list={[
isPost ? 'mb-10.8' : 'mb-10.5',
'lg:(uno-desktop-column top-20)',
'lg:(uno-desktop-column top-20) cjk:tracking-0.02em',
]}
>
<TitleTag
class:list={[
isPost
? `${postMarginBottom} mt-3.2 text-5.375 c-secondary lg:(mt-0 text-9 c-primary)`
: `${marginBottom} text-8 w-75% c-primary lg:(text-9 w-full)`,
? `mb-2.8 mt-3 text-5.375 c-secondary lg:(mb-1.8 mt-0 text-9 c-primary)`
: `mb-1.8 w-75% text-8 c-primary lg:(w-full text-9)`,
'font-bold font-title',
]}
>
@ -45,7 +32,7 @@ const SubtitleTag = isPost ? 'div' : 'h2'
<div
class="box-content inline-block pr-1"
transition:name={`site-title-${currentLang}`}
data-disable-transition-on-theme
data-disable-theme-transition
>
<a
id="site-title-link"
@ -60,9 +47,9 @@ const SubtitleTag = isPost ? 'div' : 'h2'
<SubtitleTag
class:list={[
isPost
? `opacity-0 lg:opacity-100`
? `op-0 lg:op-100`
: 'w-75% lg:w-full',
'c-secondary font-navbar text-3.5 lg:text-4',
'text-3.5 c-secondary font-navbar lg:text-4',
]}
aria-hidden={isPost}
>

View file

@ -10,7 +10,9 @@ const isTagActive = isTag
const isAboutActive = isAbout
function getNavItemClass(isActive: boolean) {
return isActive ? 'font-bold c-primary' : 'hover:(c-primary font-bold) transition-all'
return isActive
? 'highlight-static c-primary font-bold after:bottom-0.7em'
: 'highlight-hover transition-colors transition-font-weight after:bottom-0.7em hover:(c-primary font-bold)'
}
const navItems = [
@ -36,8 +38,8 @@ const navItems = [
aria-label="Site Navigation"
class:list={[
isPost ? 'hidden lg:block' : '',
'mb-10.5 text-3.6 font-semibold leading-8.75 font-navbar',
'lg:(uno-desktop-column text-4 leading-9.72 bottom-50)',
'mb-10.5 text-3.6 font-semibold leading-2.45em font-navbar',
'lg:(uno-desktop-column text-4 bottom-50) cjk:tracking-0.02em',
]}
>
<ul>

View file

@ -21,19 +21,19 @@ function formatDate(date: Date, format: 'YYYY-MM-DD' | 'MM-DD-YYYY' | 'DD-MM-YYY
}
switch (format) {
// ISO format: 2024-03-04
// ISO format: 2025-04-13
case 'YYYY-MM-DD':
return date.toISOString().split('T')[0]
// US date format: 03-04-2024
// US date format: 04-13-2025
case 'MM-DD-YYYY':
return date.toLocaleDateString('en-US', options).replace(/\//g, '-')
// European date format: 04-03-2024
// European date format: 13-04-2025
case 'DD-MM-YYYY':
return date.toLocaleDateString('en-GB', options).replace(/\//g, '-')
// US month text format: Mar 4 2024
// US month text format: Apr 13 2025
case 'MONTH DAY YYYY':
return date.toLocaleDateString('en-US', {
year: 'numeric',
@ -41,7 +41,7 @@ function formatDate(date: Date, format: 'YYYY-MM-DD' | 'MM-DD-YYYY' | 'DD-MM-YYY
day: 'numeric',
}).replace(',', '')
// British month text format: 4 Mar 2024
// British month text format: 13 Apr 2025
case 'DAY MONTH YYYY':
return date.toLocaleDateString('en-GB', {
year: 'numeric',

View file

@ -1,9 +1,10 @@
---
import type { CollectionEntry } from 'astro:content'
import PinIcon from '@/assets/icons/pin-icon.svg'
import PostDate from '@/components/PostDate.astro'
import { defaultLocale } from '@/config'
import { generateDescription } from '@/utils/description'
import { isTagPage } from '@/utils/page'
import { isHomePage } from '@/utils/page'
type Post = CollectionEntry<'posts'> & {
remarkPluginFrontmatter: {
@ -12,7 +13,7 @@ type Post = CollectionEntry<'posts'> & {
}
const { posts, lang, pinned = false } = Astro.props
const isTag = isTagPage(Astro.url.pathname)
const isHome = isHomePage(Astro.url.pathname)
export interface Props {
posts: Post[]
@ -32,40 +33,36 @@ function getPostPath(post: Post) {
{posts.map(post => (
<li
class="mb-5.5"
lg={isTag ? '' : 'mb-10'}
lg={isHome ? 'mb-10' : ''}
>
{/* post title */}
<h3 class="inline">
<h3 class="inline transition-colors hover:c-primary">
<a
class="hover:c-primary"
lg={isTag ? '' : 'font-medium text-4.5'}
class="cjk:tracking-0.02em"
lg={isHome ? 'font-medium text-4.5' : ''}
href={getPostPath(post)}
transition:name={`post-${post.data.abbrlink || post.id}-${lang}`}
data-disable-transition-on-theme
data-disable-theme-transition
>
{post.data.title}
</a>
{/* pinned icon */}
{pinned && (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
<PinIcon
aria-hidden="true"
class="ml-1 inline-block aspect-square w-3.7 shrink-0 translate-y--0.45 lg:(ml-1.2 w-4.1)"
class="ml-0.25em inline-block aspect-square w-0.98em translate-y--0.1em lg:(w-1.05em translate-y--0.15em)"
fill="currentColor"
>
<path d="M16.5 23.6c.6-6.1 1.1-8.6 7.2-15.5L15.9.4C9 6.5 6.5 7 .4 7.5l7.4 7.4-6.4 7 .7.7 7-6.4zm-.8-21.3 6 6c-5 6.1-5.7 8.1-6.2 12.2L3.4 8.5C7.5 8 9.5 7.3 15.6 2.3Z" />
</svg>
/>
)}
</h3>
{/* mobile post time */}
<div
class="text-3.5 leading-6.875 font-time lg:hidden"
class="py-0.8 text-3.5 font-time lg:hidden"
transition:name={`time-${post.data.abbrlink || post.id}-${lang}`}
data-disable-transition-on-theme
data-disable-theme-transition
>
<PostDate
date={post.data.published}
@ -74,7 +71,7 @@ function getPostPath(post: Post) {
</div>
{/* desktop post time */}
<div class="hidden text-3.65 leading-6.875 font-time lg:(ml-2.5 inline)">
<div class="hidden text-3.65 font-time lg:(ml-2.5 inline)">
<PostDate
date={post.data.published}
minutes={post.remarkPluginFrontmatter.minutes}
@ -82,10 +79,10 @@ function getPostPath(post: Post) {
</div>
{/* desktop post description */}
{!isTag && (
{isHome && (
<div
class="heti hidden"
lg="mt-2 block"
lg="mt-2.25 block"
>
<p>{generateDescription(post, 'list')}</p>
</div>

View file

@ -0,0 +1,37 @@
---
import GoBackIcon from '@/assets/icons/go-back.svg';
---
<button
id="back-button"
class="hidden"
lg="absolute left--10 top-3.8 block aspect-square w-4.5 c-secondary/40 transition-colors ease-out hover:c-secondary/80 active:scale-90!"
aria-label="Go back"
>
<GoBackIcon
aria-hidden="true"
fill="currentColor"
/>
</button>
<!-- Go Back Script >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
<script>
function setupBackButton() {
document.getElementById('back-button')?.addEventListener('click', () => {
// Navigate back if history exists
if (window.history.length > 1) {
window.history.back()
}
else {
// Fallback to homepage
const siteTitleLink = document.getElementById('site-title-link')
if (siteTitleLink) {
siteTitleLink.click()
}
}
})
}
setupBackButton()
document.addEventListener('astro:after-swap', setupBackButton)
</script>

View file

@ -8,7 +8,7 @@
<button
id="back-to-top-button"
aria-label="Back to top"
class="fixed bottom-8 right-8 h-10 w-10 rounded-full bg-background transition-all duration-300 ease-out"
class="fixed bottom-8 right-8 h-10 w-10 rounded-full bg-background transition-all ease-out"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -36,10 +36,10 @@ function initBackToTop() {
observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
backToTopButton?.classList.add('opacity-0', 'pointer-events-none', 'translate-y-4')
backToTopButton?.classList.add('op-0', 'pointer-events-none', 'translate-y-4')
}
else {
backToTopButton?.classList.remove('opacity-0', 'pointer-events-none', 'translate-y-4')
backToTopButton?.classList.remove('op-0', 'pointer-events-none', 'translate-y-4')
}
},
{
@ -60,18 +60,6 @@ function initBackToTop() {
})
}
function cleanup() {
// Cleanup observer
if (observer) {
observer.disconnect()
observer = null
}
// Remove event listeners
backToTopButton = null
}
// Handle page transitions
document.addEventListener('astro:page-load', initBackToTop)
document.addEventListener('astro:before-swap', cleanup)
document.addEventListener('astro:after-swap', initBackToTop)
</script>

View file

@ -0,0 +1,77 @@
<script>
const copyIcons = {
copy:
`<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M6.9.8v18h14.5V.8zm12.8 16h-11v-14h11z"/>
<path d="M4.3 21.2V5.6l-1.7.5v17.1h14.3l.6-2z"/>
</svg>`,
success:
`<svg
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="m23.1 6.4-1.3-1.3L9.4 16.6l-6.3-5.4-1.2 1.2L9.4 20z"/>
</svg>`,
}
// Track timeout references for each button to manage icon state transitions
const activeTimeouts = new WeakMap<HTMLButtonElement, ReturnType<typeof setTimeout>>()
async function handleCopy(button: HTMLButtonElement) {
const codeElement = button.parentElement?.querySelector('pre code')
const code = codeElement?.textContent || ''
try {
await navigator.clipboard.writeText(code)
// Clear existing timeout to prevent icon state conflicts on multiple clicks
const existingTimeout = activeTimeouts.get(button)
if (existingTimeout) {
clearTimeout(existingTimeout)
}
button.innerHTML = copyIcons.success
button.classList.add('copy-success')
// Set timeout to revert to copy icon after 1.5s
const timeoutId = setTimeout(() => {
button.innerHTML = copyIcons.copy
button.classList.remove('copy-success')
activeTimeouts.delete(button)
}, 1500)
activeTimeouts.set(button, timeoutId)
}
catch {
}
}
// Initialize copy buttons with icons and mark them to prevent duplicate initialization
function setupCodeCopyButtons() {
document
.querySelectorAll<HTMLButtonElement>('.code-copy-button:not([data-initialized])')
.forEach((button) => {
button.innerHTML = copyIcons.copy
button.setAttribute('data-initialized', 'true')
})
}
// Use event delegation for better performance
document.addEventListener('click', (e) => {
if (!(e.target instanceof HTMLElement))
return
// Find closest button element if clicked on button or child element
const button = e.target.closest('.code-copy-button') as HTMLButtonElement | null
if (button) {
handleCopy(button)
}
}, { passive: true })
setupCodeCopyButtons()
document.addEventListener('astro:page-load', setupCodeCopyButtons)
</script>

View file

@ -0,0 +1,71 @@
<script>
function setupGithubCards() {
const githubCards = document.getElementsByClassName('gc-container')
if (githubCards.length === 0)
return
// Create an intersection observer to lazy load GitHub repo data when cards enter viewport
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
loadCardData(entry.target as HTMLElement)
observer.unobserve(entry.target)
}
})
}, { rootMargin: '500px' })
Array.from(githubCards).forEach(card => observer.observe(card))
}
async function loadCardData(card: HTMLElement) {
const repo = card.dataset.repo
if (!repo)
return
const avatarEl = card.getElementsByClassName('gc-owner-avatar')[0] as HTMLElement
const descEl = card.getElementsByClassName('gc-repo-description')[0] as HTMLElement
const starsEl = card.getElementsByClassName('gc-stars-count')[0] as HTMLElement
const forksEl = card.getElementsByClassName('gc-forks-count')[0] as HTMLElement
const licenseEl = card.getElementsByClassName('gc-license-info')[0] as HTMLElement
try {
const response = await fetch(`https://api.github.com/repos/${repo}`)
if (response.ok) {
const data = await response.json()
if (avatarEl && data.owner?.avatar_url)
avatarEl.style.backgroundImage = `url(${data.owner.avatar_url})`
if (descEl && data.description)
descEl.textContent = data.description
if (starsEl) {
starsEl.textContent = new Intl.NumberFormat('en', {
notation: 'compact',
maximumFractionDigits: 1,
}).format(data.stargazers_count)
}
if (forksEl) {
forksEl.textContent = new Intl.NumberFormat('en', {
notation: 'compact',
maximumFractionDigits: 1,
}).format(data.forks_count)
}
if (licenseEl)
licenseEl.textContent = data.license?.spdx_id || 'No License'
}
else {
if (descEl)
descEl.textContent = 'Loading failed.'
}
}
catch {
console.error(`Failed to fetch ${repo}`)
}
}
setupGithubCards()
document.addEventListener('astro:page-load', setupGithubCards)
</script>

View file

@ -1,36 +0,0 @@
<button
id="back-button"
class="hidden"
lg="block absolute c-secondary/25 left--10 top-1/2 aspect-square w-4.5 translate-y--1/2 transition-colors ease-out c-secondary active:scale-90 hover:c-primary/60"
aria-label="Back to home"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
fill="currentColor"
>
<path d="M17.9.6 4 12l13.8 11.4.7-.9L7.2 12 18.5 1.5l-.7-.9Z"></path>
</svg>
</button>
<!-- Go Back Script >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
<script>
function setupBackButton() {
document.getElementById('back-button')?.addEventListener('click', () => {
if (window.history.length > 1) {
window.history.back()
}
else {
// Click site title link to trigger view transition when no history
const titleLink = document.getElementById('site-title-link') as HTMLAnchorElement
if (titleLink) {
titleLink.click()
}
}
})
}
setupBackButton()
document.addEventListener('astro:after-swap', setupBackButton)
</script>

View file

@ -0,0 +1,111 @@
<script>
import { gsap } from 'gsap'
function setupPostPageAnimation() {
// Animated Elements
const postContent = document.getElementById('gsap-post-page-content')
const postContentChildren = postContent ? Array.from(postContent.children) : []
const tocContainer = document.getElementById('toc-container')
const tocIcon = document.getElementById('toc-icon')
const tocList = document.getElementById('toc-list')
const tocListChildren = tocList ? Array.from(tocList.children) : []
const backButton = document.getElementById('back-button')
const postDate = document.getElementById('gsap-post-page-date')
// Screen Size Check
const isLargeScreen = window.matchMedia('(min-width: 1024px)').matches
const isSmallScreen = window.matchMedia('(max-width: 1535px)').matches
if (isLargeScreen) {
// First 14 elements of post content
gsap.to(postContentChildren.slice(0, 14), {
opacity: 1,
y: 0,
duration: 0.5,
delay: 0.2,
ease: 'power2.out',
stagger: 0.05,
})
// Rest elements of post content as the 15th element
if (postContentChildren.length > 14) {
gsap.to(postContentChildren.slice(14), {
opacity: 1,
y: 0,
duration: 0.5,
delay: 0.2 + 0.05 * 14,
ease: 'power2.out',
})
}
// Post Date
if (postDate) {
gsap.to(postDate, {
opacity: 1,
y: 0,
duration: 0.5,
delay: 0.15,
ease: 'power2.out',
})
}
// TOC Icon
if (tocIcon) {
gsap.to(tocIcon, {
opacity: 1,
y: 0,
duration: 0.5,
delay: 0.25,
ease: 'power2.out',
})
}
// Toc List
if (tocListChildren.length > 0) {
gsap.to(tocListChildren, {
opacity: 1,
y: 0,
duration: 0.5,
delay: 0.25,
ease: 'power2.out',
stagger: 0.025,
})
}
// Back Button
if (backButton) {
gsap.to(backButton, {
opacity: 1,
x: 0,
duration: 0.5,
delay: 0.2,
ease: 'power2.out',
})
}
}
else {
// First 7 elements of post content
gsap.to(postContentChildren.slice(0, 7), {
opacity: 1,
y: 0,
duration: 0.5,
delay: 0.2,
ease: 'power2.out',
stagger: 0.05,
})
}
// TOC Container
if (isSmallScreen && tocContainer) {
gsap.to(tocContainer, {
opacity: 1,
y: 0,
duration: 0.5,
delay: 0.15,
ease: 'power2.out',
})
}
}
setupPostPageAnimation()
document.addEventListener('astro:after-swap', setupPostPageAnimation)
</script>

View file

@ -1,75 +1,67 @@
<script>
import PhotoSwipeLightbox from 'photoswipe/lightbox'
import 'photoswipe/style.css'
import PhotoSwipeLightbox from 'photoswipe/lightbox'
let lightbox: PhotoSwipeLightbox | null = null
const pswp = import('photoswipe')
let lightbox: PhotoSwipeLightbox
const pswp = import('photoswipe')
function cleanup() {
if (lightbox) {
lightbox.destroy()
lightbox = null
function setupPhotoSwipe() {
const bodyElement = document.body
if (bodyElement.hasAttribute('data-photoswipe-initialized'))
return
const article = document.querySelector('article.heti')
const images = article ? article.getElementsByTagName('img') : []
if (images.length === 0)
return
lightbox = new PhotoSwipeLightbox({
gallery: 'article.heti img',
pswpModule: () => pswp,
bgOpacity: 0.9,
padding: {
top: window.innerHeight * 0.1,
bottom: window.innerHeight * 0.1,
left: window.innerWidth * 0.073,
right: window.innerWidth * 0.073,
},
zoom: false,
close: false,
wheelToZoom: true,
imageClickAction: 'close',
tapAction: 'close',
})
// Set image dimensions
lightbox.addFilter('domItemData', (itemData, element) => {
if (element instanceof HTMLImageElement) {
// Set image source
itemData.src = element.src
// Set dimensions with fallback to window size
itemData.w = Number(element.naturalWidth || window.innerWidth)
itemData.h = Number(element.naturalHeight || window.innerHeight)
// Set thumbnail source
itemData.msrc = element.src
}
document.removeEventListener('astro:page-load', createPhotoSwipe)
document.removeEventListener('astro:before-swap', cleanup)
return itemData
})
lightbox.init()
bodyElement.setAttribute('data-photoswipe-initialized', 'true')
}
function lazySetupPhotoSwipe() {
if (typeof window.requestIdleCallback === 'function') {
window.requestIdleCallback(() => setupPhotoSwipe(), { timeout: 1000 })
}
function createPhotoSwipe() {
// Clean up existing instance if any
cleanup()
lightbox = new PhotoSwipeLightbox({
gallery: 'article img',
pswpModule: () => pswp,
closeSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="2.4rem" viewBox="0 -960 960 960" width="3.8rem" fill="#A0A09F"><path d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/></svg>',
zoomSVG: '<svg xmlns="http://www.w3.org/2000/svg" height="2.4rem" viewBox="0 -960 960 960" width="3.8rem" fill="#A0A09F"><path d="M340-540h-40q-17 0-28.5-11.5T260-580q0-17 11.5-28.5T300-620h40v-40q0-17 11.5-28.5T380-700q17 0 28.5 11.5T420-660v40h40q17 0 28.5 11.5T500-580q0 17-11.5 28.5T460-540h-40v40q0 17-11.5 28.5T380-460q-17 0-28.5-11.5T340-500v-40Zm40 220q-109 0-184.5-75.5T120-580q0-109 75.5-184.5T380-840q109 0 184.5 75.5T640-580q0 44-14 83t-38 69l224 224q11 11 11 28t-11 28q-11 11-28 11t-28-11L532-372q-30 24-69 38t-83 14Zm0-80q75 0 127.5-52.5T560-580q0-75-52.5-127.5T380-760q-75 0-127.5 52.5T200-580q0 75 52.5 127.5T380-400Z"/></svg>',
padding: { top: window.innerHeight * 0.1, bottom: window.innerHeight * 0.1, left: window.innerWidth * 0.06, right: window.innerWidth * 0.06 },
wheelToZoom: true,
arrowPrev: false,
arrowNext: false,
imageClickAction: 'close',
tapAction: 'close',
doubleTapAction: 'zoom',
else {
requestAnimationFrame(() => {
setupPhotoSwipe()
})
// Automatically add image dimensions
lightbox.addFilter('domItemData', (itemData: any, element: Element) => {
if (element instanceof HTMLImageElement) {
itemData.src = element.src
itemData.w = Number(element.naturalWidth || window.innerWidth)
itemData.h = Number(element.naturalHeight || window.innerHeight)
itemData.msrc = element.src
}
return itemData
})
lightbox.init()
}
}
document.addEventListener('astro:page-load', createPhotoSwipe)
document.addEventListener('astro:before-swap', cleanup)
lazySetupPhotoSwipe()
document.addEventListener('astro:page-load', lazySetupPhotoSwipe)
</script>
<style is:global>
.pswp__button {
--at-apply: 'flex items-center justify-center transition';
}
.pswp__button--zoom,
.pswp__button--close {
--at-apply: 'mt-2 lg:mt-4 active:scale-90';
}
.pswp__button--zoom svg:hover,
.pswp__button--close svg:hover {
fill: #BEBEBE;
}
.pswp__button--close {
--at-apply: 'mr-4 lg:mr-8';
}
.pswp__button--zoom {
--at-apply: 'mr--6 lg:mr-0';
}
</style>

View file

@ -1,111 +0,0 @@
<script>
import { OverlayScrollbars } from 'overlayscrollbars'
function setupScrollbar() {
// Add scrollbar to body
const bodyElement = document.body
if (!bodyElement.hasAttribute('data-scrollbar-initialized')) {
OverlayScrollbars({
target: bodyElement,
cancel: {
nativeScrollbarsOverlaid: true,
},
}, {
scrollbars: {
theme: 'scrollbar-body',
autoHide: 'scroll',
autoHideDelay: 800,
},
overflow: {
x: 'hidden',
},
})
bodyElement.setAttribute('data-scrollbar-initialized', 'true')
}
// Add scrollbar to code blocks
const preElements = document.querySelectorAll('pre')
preElements.forEach((pre) => {
if (!pre.hasAttribute('data-scrollbar-initialized')) {
OverlayScrollbars({
target: pre,
}, {
scrollbars: {
theme: 'scrollbar-widget',
autoHide: 'leave',
autoHideDelay: 500,
},
overflow: {
y: 'hidden',
},
})
pre.setAttribute('data-scrollbar-initialized', 'true')
}
})
// Add scrollbar to TOC content
const tocElement = document.getElementById('toc-content')
if (tocElement && !tocElement.hasAttribute('data-scrollbar-initialized')) {
OverlayScrollbars({
target: tocElement,
}, {
scrollbars: {
theme: 'scrollbar-widget',
autoHide: 'leave',
autoHideDelay: 500,
},
overflow: {
x: 'hidden',
},
})
tocElement.setAttribute('data-scrollbar-initialized', 'true')
}
}
setupScrollbar()
document.addEventListener('astro:after-swap', setupScrollbar)
</script>
<style is:global>
@import 'overlayscrollbars/overlayscrollbars.css';
.scrollbar-body {
--os-size: 0.8rem;
--os-padding-perpendicular: 0.15rem;
--os-padding-axis: 0.2rem;
--os-handle-border-radius: 99rem;
--os-handle-perpendicular-size: 75%;
--os-handle-perpendicular-size-hover: 100%;
--os-handle-perpendicular-size-active: 100%;
--os-handle-interactive-area-offset: 0.2rem;
--os-handle-bg: oklch(var(--un-preset-theme-colors-secondary) / 0.25);
--os-handle-bg-hover: oklch(var(--un-preset-theme-colors-secondary) / 0.40);
--os-handle-bg-active: oklch(var(--un-preset-theme-colors-secondary) / 0.40);
--os-handle-max-size: 80%;
--os-handle-min-size: 12%;
}
.scrollbar-widget {
--os-size: 0.35rem;
--os-padding-perpendicular: 0;
--os-padding-axis: 0;
--os-handle-border-radius: 99rem;
--os-handle-perpendicular-size: 75%;
--os-handle-perpendicular-size-hover: 100%;
--os-handle-perpendicular-size-active: 100%;
--os-handle-interactive-area-offset: 0.1rem;
--os-handle-bg: oklch(var(--un-preset-theme-colors-secondary) / 0.15);
--os-handle-bg-hover: oklch(var(--un-preset-theme-colors-secondary) / 0.25);
--os-handle-bg-active: oklch(var(--un-preset-theme-colors-secondary) / 0.25);
--os-handle-min-size: 12%;
}
@media (min-width: 1536px) {
#toc-content .os-scrollbar {
--at-apply: 'hidden';
}
}
</style>

View file

@ -1,5 +1,6 @@
---
import type { MarkdownHeading } from 'astro'
import TocIcon from '@/assets/icons/toc-icon.svg'
import { ui } from '@/i18n/ui'
import { getPageInfo } from '@/utils/page'
@ -13,51 +14,56 @@ const currentUI = ui[currentLang as keyof typeof ui]
const { headings = [] } = Astro.props
const filteredHeadings = headings.filter(heading =>
heading.depth >= 2
&& heading.depth <= 4
&& heading.text !== 'Footnotes',
&& heading.depth <= 4,
)
---
{filteredHeadings.length > 0 && (
// TOC Container
<div
class="mb-6 bg-secondary/5 2xl:(fixed left-0 top-43.5 max-w-[min(calc(50vw-38rem),13rem)] border-none bg-transparent)"
border="~ secondary/5 rounded solid"
id="toc-container"
class="mb-6 uno-round-border bg-secondary/5 2xl:(fixed left-0 top-44.5 max-w-[min(calc(50vw-38rem),13rem)] border-none bg-secondary/0)"
>
{/* Hidden Checkbox */}
<input
type="checkbox"
id="toc-toggle"
class="accordion-toggle"
hidden
/>
<div class="relative h-12 w-full bg-transparent">
{/* TOC Toggle */}
<div class="relative h-12 w-full">
<label
for="toc-toggle"
class="absolute inset-0 flex cursor-pointer items-center 2xl:(static h-max w-max flex c-secondary/25 transition-colors ease-out hover:c-secondary/60)"
class="absolute inset-0 flex cursor-pointer items-center 2xl:(static flex c-secondary/40 transition-colors ease-out hover:c-secondary/80)"
>
{/* Title on Mobile */}
<span class="toc-title">
{currentUI.toc}
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
{/* Icon on Desktop */}
<TocIcon
id="toc-icon"
aria-hidden="true"
class="ml-1 hidden aspect-square w-4.2 2xl:(mt-4 block origin-center active:scale-90!)"
fill="currentColor"
class="ml-4 hidden aspect-square w-4.2 2xl:(mt-4 block origin-center active:scale-90)"
>
<path d="M22.2 2.3H1.8v1.6h19.8zM1.8 21.7h19.8l.6-1.6H1.8zM15.5 11.2H1.8v1.6h13.1z" />
</svg>
/>
</label>
</div>
{/* Expandable content wrapper */}
{/* Expandable Content Wrapper with Accordion Animation */}
<div class="accordion-wrapper">
<nav
id="toc-content"
class="accordion-content"
aria-label="Table of Contents"
>
<ul class="toc-list">
{/* TOC List */}
<ul
id="toc-list"
class="toc-list"
>
{filteredHeadings.map(heading => (
<li
class:list={{
@ -87,55 +93,57 @@ const filteredHeadings = headings.filter(heading =>
<!-- Override heti default styles >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
<style>
.toc-title {
--at-apply: 'font-semibold ml-4 select-none 2xl:hidden';
--at-apply: 'ml-4 font-semibold 2xl:hidden';
}
.toc-list {
--at-apply: 'list-none pl-0 space-y-2 mt-1 mb-4 2xl:space-y-1.2';
--at-apply: 'mb-2.5 mt-1 list-none pl-0 space-y-1.1 2xl:(mb-1 space-y-1)';
}
.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))';
--at-apply: 'text-balance text-sm font-normal no-underline 2xl:(text-3.2 c-secondary/60 transition-colors transition-font-weight ease-out hover:c-secondary hover:font-medium)';
}
/* Initial collapsed state with zero height grid row */
.accordion-wrapper {
--at-apply: 'grid rows-[0fr] duration-300 ease-in-out';
--at-apply: 'grid rows-[0fr] transition-all duration-350 ease-in-out';
}
.accordion-content {
--at-apply: 'overflow-hidden max-h-66 lg:max-h-82 2xl:(max-h-[calc(100vh-21.5rem)]) px-4';
--at-apply: 'max-h-59.3 overflow-hidden pl-4 pr-6 2xl:(max-h-[calc(100vh-26.75rem)] pl-1)';
}
/* 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';
}
#toc-content {
scrollbar-width: thin;
scrollbar-color: oklch(var(--un-preset-theme-colors-secondary) / 0.15) transparent;
}
@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';
}
#toc-content {
--at-apply: 'scrollbar-hidden';
}
#toc-content::-webkit-scrollbar {
display: none;
}
}
</style>
@ -146,7 +154,7 @@ function setupTOCHighlight() {
if (!tocContent)
return
const tocLinks = tocContent.querySelectorAll('a')
const tocLinks = tocContent.getElementsByTagName('a')
if (tocLinks.length === 0)
return
@ -155,7 +163,7 @@ function setupTOCHighlight() {
// Build mapping from heading IDs to TOC links
const headingMap = new Map<string, HTMLAnchorElement>()
tocLinks.forEach((link) => {
Array.from(tocLinks).forEach((link) => {
const id = link.getAttribute('href')?.substring(1)
if (id)
headingMap.set(id, link as HTMLAnchorElement)