mirror of
https://github.com/reonokiy/blog.nokiy.net.git
synced 2025-06-16 19:51:07 +02:00
Merge branch 'master' into giscus
This commit is contained in:
commit
4b270755bf
356 changed files with 21152 additions and 3604 deletions
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> {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>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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>
|
||||
|
|
37
src/components/Widgets/BackButton.astro
Normal file
37
src/components/Widgets/BackButton.astro
Normal 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>
|
|
@ -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>
|
||||
|
|
77
src/components/Widgets/CodeCopyButton.astro
Normal file
77
src/components/Widgets/CodeCopyButton.astro
Normal 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>
|
71
src/components/Widgets/GithubCard.astro
Normal file
71
src/components/Widgets/GithubCard.astro
Normal 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>
|
|
@ -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>
|
111
src/components/Widgets/GsapAnimation.astro
Normal file
111
src/components/Widgets/GsapAnimation.astro
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue