retypeset-odyssey 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +168 -0
- package/astro.config.ts +18 -0
- package/default-config.yaml +136 -0
- package/discover-collections.ts +160 -0
- package/integration.ts +394 -0
- package/package.json +105 -0
- package/patches/@qwik.dev__partytown@0.11.2.patch +98 -0
- package/public/_redirects +819 -0
- package/public/feeds/atom-style.xsl +105 -0
- package/public/feeds/rss-style.xsl +105 -0
- package/public/fonts/EarlySummer-VF-Split/00785494587e3487ac63a0e7e4fa30f0.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/08e5d941a4c76fad7b68e7a937ebb21f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/1268e5072156188d601f1eeb4473655d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/12a385475353c815d7a5add53ee51e37.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/12b11ca08223c65a21fc731d59dcfc11.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/16d6676d3cb645c520ee6df8a1f89afd.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2912a75ffef95e7a5ae9e2b2311ad61d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/298d96ea561e419a4104bc9fc18499ce.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2a2c71acc17ec39f6780835899e53096.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/2a7e2d0e59d3f638074c50fab39fdef1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/36931fc4370e1670ed76af5d3feccba2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/3a68fdc792e4a9e0399a04e32d0cc2e3.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/4054d6a4d6b37719b51e0f71da6e7cd9.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/429cb25f825c3cbde6bfac5b36ae9675.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/42a9efc11298368ecdc1b85ab46f0b4f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/432018d2bdc9df92a7662056eb2b1261.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/44a6fb782f2a01560faa0f95248b60ef.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/45367b060e8ba0aa2507e6b91b86620b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/571db7564bda7c1a93542881b8976f4b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/58d55eeef4cf455e86a1142b1f3110d3.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/59ea41e77309160a0f63cdc76a010202.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/5d19d9174e568db4755981aa2e4ab380.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/5e811eb3b4175ee93d7ec000bf4631c2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/6268e0cd5d66d6fe05b331f259e7b9e4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/6549844aa3d833ca06a68a8e839db465.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/714b459658a7321ceeb1e1386ce165c2.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/7511d97a469915013683eae06cb21cd9.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/7784b4ebe543d13f62f6f6e05beb0b2e.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/77c9bea70b3c6ab24e1497d5468c825b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/789ebea9e81df623e930b86de98fbfab.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/885bb7ab0717e8a47fc17f953adcdbf1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/896c58aff69a9a857764cee0663bc56d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/8fb6fc01c59d1e3ad1910b58dec7f5e7.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/95be5462b91b9a0458797cdc89d94cb5.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/9a5b2724f983ca0fc0d5ff8d10c41396.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/9ffe17f9c0e4cc4356cb3f08ffdb9c6d.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/EarlySummer-VF-Subset.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/EarlySummerSerif License.txt +91 -0
- package/public/fonts/EarlySummer-VF-Split/a097ef49be62cd2565aca45600e1e3ac.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a17a1ae6063088e5b3a48c06b816929a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a83fdcfc5ecf2f6996704b0c02758689.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/a8cf15ff9b71e59407d8406866ff6f99.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/af530ed51dd519e4456f8a5e259e908b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b195a8924915deec4aa9c3ec777cc93f.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b4b6bb5df9239dd67b52ca858fd2a506.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b7592e1e027923f19e0e55dfdac69668.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/b965859f69d8ccceaf0e2d6292afbcfb.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/bbe9333f1ff242bd96ecb23ff9e723b1.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/be758580e295339ea98f0240b9869f24.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c07099e1d025617f6d40966986e1941b.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c1b593dda62fdeb7dde3af02016da282.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/c89f0335910a68a0958f2846108370e8.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ca49aa409fdedd3f2f894cd20a16640a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ccd4a28d2f63797e0183c87792e20b75.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/d2718da923fce8e7ea229d65e306e92c.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/d893e9b307d96041e9cfcbd03761b9f4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/dafaedaee41b75e21479d4ff324b6a34.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/db392af65f1867e5fd580eed2195df99.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/dc7c73a9e5577143ccd11e05ab55cb39.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/de396881189f747eba67685298363242.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/df625b213228bba22a7733d4eff8f148.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e6e60b384f220b893ef31a926ece829a.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e6e8ce2c5972ab665630bb705383d0fb.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e963c7ed7104c2d6d68fcb5f952fe2f5.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/e966b23b4cd7783f43e31032d41784f4.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/edaac57c3856ec13128f4c6c3e00975c.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/ee54e0d86edf068c6c9cbddb76a856fe.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/f612c78a5544ff2dd3e8296ac3e58344.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/f9e539bd9b7bf999c3da82f5403ec3b6.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/fa5863b923ac15993c52a619f699ee63.woff2 +0 -0
- package/public/fonts/EarlySummer-VF-Split/fc759e56ec6f6e6d3d4cb163d62fb557.woff2 +0 -0
- package/public/fonts/Font Subset List/CJK Common Characters.txt +7534 -0
- package/public/fonts/Font Subset List/EarlySummer Subset.txt +3 -0
- package/public/fonts/Font Subset List/Japanese Kana + Korean Letters.txt +6123 -0
- package/public/fonts/Font Subset List/Latin + Cyrillic + Greek + Arabic Glyphs.txt +121 -0
- package/public/fonts/Font Subset List/unicode_range.py +49 -0
- package/public/fonts/NotoSansSC-Bold.otf +0 -0
- package/public/fonts/NotoSansSC-Regular.otf +0 -0
- package/public/fonts/STIX-Italic-VF.woff2 +0 -0
- package/public/fonts/STIX-VF.woff2 +0 -0
- package/public/fonts/Snell-Black-SF.woff2 +0 -0
- package/public/fonts/Snell-Bold-SF.woff2 +0 -0
- package/public/giscus/theme-dark.css +208 -0
- package/public/giscus/theme-light.css +208 -0
- package/public/icons/favicon.svg +4 -0
- package/public/icons/og-logo.png +0 -0
- package/public/robots.txt +4 -0
- package/public/sounds/tap_01.wav +0 -0
- package/public/sounds/tap_02.wav +0 -0
- package/public/sounds/tap_03.wav +0 -0
- package/public/sounds/tap_04.wav +0 -0
- package/public/sounds/tap_05.wav +0 -0
- package/public/sounds/type_01.wav +0 -0
- package/public/sounds/type_02.wav +0 -0
- package/public/sounds/type_03.wav +0 -0
- package/public/sounds/type_04.wav +0 -0
- package/public/sounds/type_05.wav +0 -0
- package/scripts/apply-lqip.ts +276 -0
- package/scripts/format-posts.ts +105 -0
- package/scripts/migration/README.md +52 -0
- package/scripts/migration/migrate-hexo.ts +185 -0
- package/scripts/migration/validate-abbrlinks.ts +161 -0
- package/scripts/new-post.ts +52 -0
- package/scripts/seo/generate-legacy-redirects.ts +407 -0
- package/scripts/update-theme.ts +46 -0
- package/src/assets/icons/copy-check.svg +3 -0
- package/src/assets/icons/copy-icon.svg +4 -0
- package/src/assets/icons/go-back.svg +3 -0
- package/src/assets/icons/heading-anchor.svg +4 -0
- package/src/assets/icons/lang-en.svg +3 -0
- package/src/assets/icons/lang-ja.svg +5 -0
- package/src/assets/icons/lang-zh.svg +5 -0
- package/src/assets/icons/language-switcher.svg +3 -0
- package/src/assets/icons/pin-icon.svg +3 -0
- package/src/assets/icons/search-icon.svg +3 -0
- package/src/assets/icons/theme-toggle.svg +3 -0
- package/src/assets/icons/toc-icon.svg +3 -0
- package/src/assets/icons/top-icon.svg +3 -0
- package/src/assets/lqip-map.json +10 -0
- package/src/components/Button.astro +152 -0
- package/src/components/CategoryList.astro +66 -0
- package/src/components/Comment/Giscus.astro +119 -0
- package/src/components/Comment/Index.astro +30 -0
- package/src/components/Comment/Twikoo.astro +114 -0
- package/src/components/Comment/Waline.astro +149 -0
- package/src/components/FloatingButtons.astro +101 -0
- package/src/components/Footer.astro +74 -0
- package/src/components/Header.astro +62 -0
- package/src/components/JournalList.astro +56 -0
- package/src/components/Navbar.astro +69 -0
- package/src/components/NoteList.astro +56 -0
- package/src/components/Pagination.astro +267 -0
- package/src/components/PostDate.astro +80 -0
- package/src/components/PostList.astro +87 -0
- package/src/components/SearchModal.astro +340 -0
- package/src/components/TagList.astro +135 -0
- package/src/components/Widgets/BackButton.astro +43 -0
- package/src/components/Widgets/CodeCopyButton.astro +47 -0
- package/src/components/Widgets/GithubCard.astro +110 -0
- package/src/components/Widgets/ImageZoom.astro +135 -0
- package/src/components/Widgets/MediaEmbed.astro +127 -0
- package/src/components/Widgets/SoundEffect.astro +179 -0
- package/src/components/Widgets/TOC.astro +198 -0
- package/src/config-schema.ts +164 -0
- package/src/config.ts +127 -0
- package/src/config.ts.example +205 -0
- package/src/content/about/_example-about-en.md +6 -0
- package/src/content/about/about-en.md +21 -0
- package/src/content/about/about-ja.md +21 -0
- package/src/content/about/about-zh.md +24 -0
- package/src/content.config.ts +247 -0
- package/src/env.d.ts +25 -0
- package/src/i18n/config.ts +65 -0
- package/src/i18n/lang.ts +70 -0
- package/src/i18n/path.ts +160 -0
- package/src/i18n/ui.ts +214 -0
- package/src/layouts/Head.astro +203 -0
- package/src/layouts/Layout.astro +69 -0
- package/src/pages/404.astro +20 -0
- package/src/pages/[...lang]/[...page].astro +48 -0
- package/src/pages/[...lang]/about.astro +28 -0
- package/src/pages/[...lang]/atom.xml.ts +14 -0
- package/src/pages/[...lang]/categories/index.astro +35 -0
- package/src/pages/[...lang]/journals/[slug].astro +89 -0
- package/src/pages/[...lang]/journals/index.astro +55 -0
- package/src/pages/[...lang]/journals/page/[page].astro +66 -0
- package/src/pages/[...lang]/notes/[slug].astro +88 -0
- package/src/pages/[...lang]/notes/index.astro +55 -0
- package/src/pages/[...lang]/notes/page/[page].astro +66 -0
- package/src/pages/[...lang]/posts/[slug].astro +101 -0
- package/src/pages/[...lang]/rss.xml.ts +14 -0
- package/src/pages/[...lang]/search.astro +65 -0
- package/src/pages/[...lang]/tags/[tag].astro +53 -0
- package/src/pages/[...lang]/tags/index.astro +36 -0
- package/src/pages/_dynamic/list.astro +101 -0
- package/src/pages/_dynamic/slug.astro +100 -0
- package/src/pages/og/[...image].ts +114 -0
- package/src/pages/robots.txt.ts +20 -0
- package/src/plugins/rehype-code-copy-button.mjs +82 -0
- package/src/plugins/rehype-external-links.mjs +18 -0
- package/src/plugins/rehype-heading-anchor.mjs +55 -0
- package/src/plugins/rehype-image-processor.mjs +77 -0
- package/src/plugins/remark-container-directives.mjs +135 -0
- package/src/plugins/remark-leaf-directives.mjs +184 -0
- package/src/plugins/remark-reading-time.mjs +11 -0
- package/src/styles/comment.css +205 -0
- package/src/styles/extension.css +180 -0
- package/src/styles/font.css +111 -0
- package/src/styles/global.css +91 -0
- package/src/styles/lqip.css +71 -0
- package/src/styles/markdown.css +276 -0
- package/src/styles/transition.css +173 -0
- package/src/types/global.d.ts +22 -0
- package/src/types/index.d.ts +111 -0
- package/src/utils/cache.ts +32 -0
- package/src/utils/content.ts +819 -0
- package/src/utils/description.ts +147 -0
- package/src/utils/dynamic-collections.ts +155 -0
- package/src/utils/feed.ts +238 -0
- package/src/utils/page.ts +107 -0
- package/tsconfig.json +13 -0
- package/uno.config.ts +75 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
const timeoutMap = new WeakMap<Element, number>()
|
|
3
|
+
|
|
4
|
+
async function handleCodeCopyClick(e: MouseEvent) {
|
|
5
|
+
if (!(e.target instanceof Element)) {
|
|
6
|
+
return
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const button = e.target.closest('.code-copy-button')
|
|
10
|
+
if (!button) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const text = button.parentElement?.querySelector('pre > code')?.textContent
|
|
15
|
+
if (!text) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Copy text to clipboard
|
|
20
|
+
try {
|
|
21
|
+
await navigator.clipboard.writeText(text)
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error('[CodeCopyButton] Failed to copy:', error)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Add copied state
|
|
29
|
+
button.classList.add('copied')
|
|
30
|
+
|
|
31
|
+
// Clear existing timeout to prevent state flickering
|
|
32
|
+
const existingTimeout = timeoutMap.get(button)
|
|
33
|
+
if (existingTimeout) {
|
|
34
|
+
clearTimeout(existingTimeout)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Reset state after 1.5s
|
|
38
|
+
const timeoutId = window.setTimeout(() => {
|
|
39
|
+
button.classList.remove('copied')
|
|
40
|
+
timeoutMap.delete(button)
|
|
41
|
+
}, 1500)
|
|
42
|
+
|
|
43
|
+
timeoutMap.set(button, timeoutId)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
document.addEventListener('click', handleCodeCopyClick)
|
|
47
|
+
</script>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
interface GithubRepoData {
|
|
3
|
+
owner: { avatar_url: string }
|
|
4
|
+
description: string | null
|
|
5
|
+
stargazers_count: number
|
|
6
|
+
forks_count: number
|
|
7
|
+
license: { spdx_id: string } | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const compactNumberFormat = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 })
|
|
11
|
+
|
|
12
|
+
// Fetch repository data from GitHub API with caching
|
|
13
|
+
async function fetchRepoData(repo: string): Promise<GithubRepoData | null> {
|
|
14
|
+
const cacheKey = `github-repo-${repo}`
|
|
15
|
+
|
|
16
|
+
// Check session storage for cached data
|
|
17
|
+
try {
|
|
18
|
+
const cachedData = sessionStorage.getItem(cacheKey)
|
|
19
|
+
if (cachedData) {
|
|
20
|
+
return JSON.parse(cachedData) as GithubRepoData
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
try {
|
|
25
|
+
sessionStorage.removeItem(cacheKey)
|
|
26
|
+
}
|
|
27
|
+
catch {}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fetch from API if not cached
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`https://api.github.com/repos/${repo}`)
|
|
33
|
+
if (!response.ok) {
|
|
34
|
+
console.warn(`[GithubCard] Failed to fetch ${repo}: ${response.status} ${response.statusText}`)
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const raw = await response.json()
|
|
39
|
+
const data: GithubRepoData = {
|
|
40
|
+
owner: { avatar_url: raw.owner?.avatar_url },
|
|
41
|
+
description: raw.description,
|
|
42
|
+
stargazers_count: raw.stargazers_count,
|
|
43
|
+
forks_count: raw.forks_count,
|
|
44
|
+
license: raw.license ? { spdx_id: raw.license.spdx_id } : null,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Cache the successful response
|
|
48
|
+
try {
|
|
49
|
+
sessionStorage.setItem(cacheKey, JSON.stringify(data))
|
|
50
|
+
}
|
|
51
|
+
catch {}
|
|
52
|
+
|
|
53
|
+
return data
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error(`[GithubCard] Failed to fetch ${repo}:`, error)
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Update card UI with repository data
|
|
62
|
+
function updateCardUI(card: HTMLElement, data: GithubRepoData | null) {
|
|
63
|
+
const setText = (selector: string, text: string) => {
|
|
64
|
+
const el = card.querySelector<HTMLElement>(selector)
|
|
65
|
+
if (el) {
|
|
66
|
+
el.textContent = text
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!data) {
|
|
71
|
+
setText('.gc-repo-description', 'Failed to load data')
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const avatar = card.querySelector<HTMLElement>('.gc-owner-avatar')
|
|
76
|
+
if (avatar && data.owner?.avatar_url) {
|
|
77
|
+
avatar.style.backgroundImage = `url(${data.owner.avatar_url})`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
setText('.gc-repo-description', data.description ?? 'No description')
|
|
81
|
+
setText('.gc-stars-count', compactNumberFormat.format(data.stargazers_count ?? 0))
|
|
82
|
+
setText('.gc-forks-count', compactNumberFormat.format(data.forks_count ?? 0))
|
|
83
|
+
setText('.gc-license-info', data.license?.spdx_id ?? 'No License')
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Load data for a specific card element
|
|
87
|
+
async function loadRepoData(card: HTMLElement) {
|
|
88
|
+
const repo = card.dataset.repo
|
|
89
|
+
if (!repo) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = await fetchRepoData(repo)
|
|
94
|
+
updateCardUI(card, data)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Initialize all GitHub cards on the page
|
|
98
|
+
function setupGithubCards() {
|
|
99
|
+
const cards = document.querySelectorAll<HTMLElement>('.gc-container')
|
|
100
|
+
if (cards.length === 0) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
cards.forEach((card) => {
|
|
105
|
+
loadRepoData(card)
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
document.addEventListener('astro:page-load', setupGithubCards)
|
|
110
|
+
</script>
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
let overlay: HTMLDivElement | null = null
|
|
3
|
+
let zoomedImg: HTMLImageElement | null = null
|
|
4
|
+
let originalImg: HTMLImageElement | null = null
|
|
5
|
+
|
|
6
|
+
// Setup overlay element
|
|
7
|
+
function setupOverlay() {
|
|
8
|
+
overlay = document.createElement('div')
|
|
9
|
+
overlay.className = 'zoom-overlay'
|
|
10
|
+
overlay.setAttribute('role', 'dialog')
|
|
11
|
+
overlay.setAttribute('aria-modal', 'true')
|
|
12
|
+
overlay.setAttribute('aria-label', 'Image Viewer')
|
|
13
|
+
overlay.setAttribute('tabindex', '-1')
|
|
14
|
+
|
|
15
|
+
document.body.appendChild(overlay)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Clean up zoom state
|
|
19
|
+
function cleanupZoom() {
|
|
20
|
+
overlay = null
|
|
21
|
+
zoomedImg = null
|
|
22
|
+
originalImg = null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Zoom in the image
|
|
26
|
+
function zoomIn(img: HTMLImageElement) {
|
|
27
|
+
if (!overlay) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Disable scrolling and get position
|
|
32
|
+
document.body.style.overflow = 'hidden'
|
|
33
|
+
const rect = img.getBoundingClientRect()
|
|
34
|
+
originalImg = img
|
|
35
|
+
|
|
36
|
+
// Clone and setup image
|
|
37
|
+
zoomedImg = img.cloneNode() as HTMLImageElement
|
|
38
|
+
zoomedImg.className = 'zoom-img'
|
|
39
|
+
zoomedImg.removeAttribute('id')
|
|
40
|
+
zoomedImg.removeAttribute('loading')
|
|
41
|
+
|
|
42
|
+
zoomedImg.style.top = `${rect.top}px`
|
|
43
|
+
zoomedImg.style.left = `${rect.left}px`
|
|
44
|
+
zoomedImg.style.width = `${rect.width}px`
|
|
45
|
+
zoomedImg.style.height = `${rect.height}px`
|
|
46
|
+
|
|
47
|
+
// Add to DOM and show
|
|
48
|
+
document.body.appendChild(zoomedImg)
|
|
49
|
+
overlay.style.display = 'block'
|
|
50
|
+
overlay.focus()
|
|
51
|
+
|
|
52
|
+
// Calculate scale and position
|
|
53
|
+
const viewportWidth = window.innerWidth
|
|
54
|
+
const viewportHeight = window.innerHeight
|
|
55
|
+
const scaleFactor = window.innerWidth < 768 ? 1 : 0.8
|
|
56
|
+
const scale = Math.min(
|
|
57
|
+
(viewportWidth * scaleFactor) / rect.width,
|
|
58
|
+
(viewportHeight * scaleFactor) / rect.height,
|
|
59
|
+
)
|
|
60
|
+
const translateX = (-rect.left + (viewportWidth - rect.width) / 2) / scale
|
|
61
|
+
const translateY = (-rect.top + (viewportHeight - rect.height) / 2) / scale
|
|
62
|
+
|
|
63
|
+
// Start animation
|
|
64
|
+
requestAnimationFrame(() => {
|
|
65
|
+
if (overlay) {
|
|
66
|
+
overlay.style.opacity = '1'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (zoomedImg) {
|
|
70
|
+
zoomedImg.style.transform = `scale(${scale}) translate3d(${translateX}px, ${translateY}px, 0)`
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Zoom out the image
|
|
76
|
+
function zoomOut() {
|
|
77
|
+
if (!overlay || !zoomedImg || !originalImg) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Start closing animation
|
|
82
|
+
zoomedImg.style.transform = ''
|
|
83
|
+
overlay.style.opacity = '0'
|
|
84
|
+
document.body.style.overflow = ''
|
|
85
|
+
|
|
86
|
+
// Define cleanup logic
|
|
87
|
+
const cleanup = () => {
|
|
88
|
+
if (!zoomedImg) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Remove zoomed image
|
|
93
|
+
zoomedImg?.remove()
|
|
94
|
+
zoomedImg = null
|
|
95
|
+
|
|
96
|
+
// Hide overlay
|
|
97
|
+
if (overlay) {
|
|
98
|
+
overlay.style.display = 'none'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Restore focus
|
|
102
|
+
originalImg?.focus()
|
|
103
|
+
originalImg = null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Listen for transition end to cleanup
|
|
107
|
+
zoomedImg.addEventListener('transitionend', cleanup, { once: true })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Handle click events
|
|
111
|
+
function handleClick(event: MouseEvent) {
|
|
112
|
+
if (zoomedImg) {
|
|
113
|
+
zoomOut()
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const target = event.target
|
|
118
|
+
if (!(target instanceof HTMLImageElement)) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Ignore small or incomplete images
|
|
123
|
+
if (!target.complete || target.width < 100 || target.height < 100) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
event.preventDefault()
|
|
128
|
+
zoomIn(target)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
document.addEventListener('astro:page-load', setupOverlay)
|
|
132
|
+
document.addEventListener('astro:before-swap', cleanupZoom)
|
|
133
|
+
document.addEventListener('click', handleClick)
|
|
134
|
+
window.addEventListener('resize', zoomOut)
|
|
135
|
+
</script>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
import 'lite-youtube-embed/src/lite-yt-embed.css'
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<script>
|
|
6
|
+
// Initialize Mermaid diagrams
|
|
7
|
+
async function setupMermaidEmbeds() {
|
|
8
|
+
const mermaids = document.querySelectorAll('pre.mermaid')
|
|
9
|
+
if (mermaids.length === 0) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Store original code for re-rendering
|
|
14
|
+
mermaids.forEach((element) => {
|
|
15
|
+
const mermaid = element as HTMLElement
|
|
16
|
+
if (!mermaid.dataset.mermaidCode) {
|
|
17
|
+
mermaid.dataset.mermaidCode = mermaid.textContent?.trim() ?? ''
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Load and run Mermaid
|
|
22
|
+
try {
|
|
23
|
+
const { default: mermaid } = await import('mermaid')
|
|
24
|
+
mermaid.initialize({
|
|
25
|
+
startOnLoad: false,
|
|
26
|
+
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'default',
|
|
27
|
+
})
|
|
28
|
+
mermaid.run()
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error('[MediaEmbed] Failed to load Mermaid:', error)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Load YouTube embed script
|
|
36
|
+
async function setupYouTubeEmbeds() {
|
|
37
|
+
const youtube = document.querySelectorAll('lite-youtube')
|
|
38
|
+
if (youtube.length === 0) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
// @ts-expect-error - No type definitions available for lite-youtube-embed
|
|
44
|
+
await import('lite-youtube-embed')
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error('[MediaEmbed] Failed to load YouTube embed:', error)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Initialize Twitter widgets
|
|
52
|
+
async function setupTweetEmbeds() {
|
|
53
|
+
const tweets = document.querySelectorAll('.twitter-tweet')
|
|
54
|
+
if (tweets.length === 0) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
tweets.forEach((tweet) => {
|
|
59
|
+
tweet.setAttribute('data-theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Inject Twitter script
|
|
63
|
+
const script = document.createElement('script')
|
|
64
|
+
script.src = 'https://platform.twitter.com/widgets.js'
|
|
65
|
+
script.async = true
|
|
66
|
+
document.head.appendChild(script)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Initialize all media embeds
|
|
70
|
+
async function setupMediaEmbeds() {
|
|
71
|
+
await Promise.allSettled([
|
|
72
|
+
setupYouTubeEmbeds(),
|
|
73
|
+
setupTweetEmbeds(),
|
|
74
|
+
setupMermaidEmbeds(),
|
|
75
|
+
])
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Re-render Mermaid diagrams on theme change
|
|
79
|
+
function updateMermaidTheme() {
|
|
80
|
+
const mermaids = document.querySelectorAll('pre.mermaid[data-mermaid-code]')
|
|
81
|
+
if (mermaids.length === 0) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const isDark = document.documentElement.classList.contains('dark')
|
|
86
|
+
|
|
87
|
+
import('mermaid').then(({ default: mermaid }) => {
|
|
88
|
+
mermaid.initialize({
|
|
89
|
+
startOnLoad: false,
|
|
90
|
+
theme: isDark ? 'dark' : 'default',
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Reset content and re-run Mermaid
|
|
94
|
+
mermaids.forEach((element) => {
|
|
95
|
+
const mermaid = element as HTMLElement
|
|
96
|
+
mermaid.innerHTML = mermaid.dataset.mermaidCode ?? ''
|
|
97
|
+
mermaid.removeAttribute('data-processed')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
mermaid.run()
|
|
101
|
+
}).catch((error) => {
|
|
102
|
+
console.error('[MediaEmbed] Failed to update Mermaid theme:', error)
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Handle horizontal scrolling for gallery
|
|
107
|
+
function handleGalleryWheel(e: WheelEvent) {
|
|
108
|
+
const container = (e.target as Element)?.closest('.gallery-container') as HTMLElement
|
|
109
|
+
if (!container) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Convert vertical scroll to horizontal
|
|
114
|
+
const previousScrollLeft = container.scrollLeft
|
|
115
|
+
container.scrollLeft += e.deltaY
|
|
116
|
+
if (container.scrollLeft === previousScrollLeft) {
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Prevent default scroll only if horizontal scroll occurred
|
|
121
|
+
e.preventDefault()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
document.addEventListener('astro:page-load', setupMediaEmbeds)
|
|
125
|
+
document.addEventListener('theme-changed', updateMermaidTheme)
|
|
126
|
+
document.addEventListener('wheel', handleGalleryWheel, { passive: false })
|
|
127
|
+
</script>
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
const soundTypes = {
|
|
3
|
+
click: 'tap',
|
|
4
|
+
typing: 'type',
|
|
5
|
+
} as const
|
|
6
|
+
|
|
7
|
+
type SoundType = typeof soundTypes[keyof typeof soundTypes]
|
|
8
|
+
|
|
9
|
+
const volumeSettings: Record<SoundType, number> = {
|
|
10
|
+
[soundTypes.click]: 0.8,
|
|
11
|
+
[soundTypes.typing]: 0.4,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const clickTargets = [
|
|
15
|
+
'#language-switcher',
|
|
16
|
+
'#theme-toggle-button',
|
|
17
|
+
] as const
|
|
18
|
+
|
|
19
|
+
const typingTargets = [
|
|
20
|
+
// Twikoo
|
|
21
|
+
'.el-input__inner',
|
|
22
|
+
'.el-textarea__inner',
|
|
23
|
+
// Waline
|
|
24
|
+
'#wl-nick',
|
|
25
|
+
'#wl-mail',
|
|
26
|
+
'#wl-link',
|
|
27
|
+
'#wl-edit',
|
|
28
|
+
] as const
|
|
29
|
+
|
|
30
|
+
const ignoredKeys = new Set([
|
|
31
|
+
'Shift',
|
|
32
|
+
'Control',
|
|
33
|
+
'Alt',
|
|
34
|
+
'Meta',
|
|
35
|
+
'Tab',
|
|
36
|
+
'Escape',
|
|
37
|
+
'CapsLock',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
const clickSelector = clickTargets.join(',')
|
|
41
|
+
const typingSelector = typingTargets.join(',')
|
|
42
|
+
|
|
43
|
+
function isMobileDevice(): boolean {
|
|
44
|
+
return window.matchMedia('(max-width: 1023px)').matches
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getBasePath(): string {
|
|
48
|
+
const sitemap = document.head.querySelector('link[rel="sitemap"]')
|
|
49
|
+
const href = sitemap?.getAttribute('href')
|
|
50
|
+
return href?.replace('/sitemap-index.xml', '') || ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
class SoundEffectManager {
|
|
54
|
+
private audioContext: AudioContext | null = null
|
|
55
|
+
private audioBuffers: Record<SoundType, AudioBuffer[]> = {
|
|
56
|
+
[soundTypes.click]: [],
|
|
57
|
+
[soundTypes.typing]: [],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private initPromise: Promise<void> | null = null
|
|
61
|
+
|
|
62
|
+
// Preload all sound files during browser idle time
|
|
63
|
+
constructor() {
|
|
64
|
+
if (!isMobileDevice() && 'requestIdleCallback' in window) {
|
|
65
|
+
requestIdleCallback(() => this.initialize())
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Fetch, decode and cache a single sound file
|
|
70
|
+
private async fetchAndCacheSound(type: SoundType, index: number): Promise<void> {
|
|
71
|
+
if (!this.audioContext) {
|
|
72
|
+
throw new Error('Audio context not initialized')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const soundId = `${type}_0${index + 1}`
|
|
76
|
+
const response = await fetch(`${getBasePath()}/sounds/${soundId}.wav`)
|
|
77
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
78
|
+
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer)
|
|
79
|
+
this.audioBuffers[type].push(audioBuffer)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Preload all sound variants
|
|
83
|
+
private async preloadAllSounds(): Promise<void> {
|
|
84
|
+
const soundTypeValues = Object.values(soundTypes) as SoundType[]
|
|
85
|
+
const allPromises = soundTypeValues.flatMap(type =>
|
|
86
|
+
Array.from({ length: 5 }, (_, i) => this.fetchAndCacheSound(type, i)),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
await Promise.allSettled(allPromises)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Initialize audio context and preload sound files (executes only once)
|
|
93
|
+
private async initialize(): Promise<void> {
|
|
94
|
+
return this.initPromise ??= (async () => {
|
|
95
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext
|
|
96
|
+
this.audioContext = new AudioContextClass()
|
|
97
|
+
await this.preloadAllSounds()
|
|
98
|
+
})()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Play a random sound of the specified type
|
|
102
|
+
public async playSound(soundType: SoundType): Promise<void> {
|
|
103
|
+
try {
|
|
104
|
+
await this.initialize()
|
|
105
|
+
|
|
106
|
+
if (!this.audioContext) {
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Resume audio context if suspended
|
|
111
|
+
if (this.audioContext.state === 'suspended') {
|
|
112
|
+
await this.audioContext.resume()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Get matching sound buffers for the requested type
|
|
116
|
+
const buffers = this.audioBuffers[soundType]
|
|
117
|
+
if (buffers.length === 0) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Select random sound from available buffers
|
|
122
|
+
const source = this.audioContext.createBufferSource()
|
|
123
|
+
source.buffer = buffers[Math.floor(Math.random() * buffers.length)]
|
|
124
|
+
|
|
125
|
+
// Create and configure audio nodes
|
|
126
|
+
const gainNode = this.audioContext.createGain()
|
|
127
|
+
gainNode.gain.value = volumeSettings[soundType]
|
|
128
|
+
|
|
129
|
+
// Connect nodes and play sound
|
|
130
|
+
source.connect(gainNode).connect(this.audioContext.destination)
|
|
131
|
+
source.start(0)
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
console.warn('Sound playback failed:', error)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const soundManager = new SoundEffectManager()
|
|
140
|
+
|
|
141
|
+
// Handle click events on interactive elements
|
|
142
|
+
function handleGlobalClick(event: MouseEvent) {
|
|
143
|
+
if (isMobileDevice()) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const target = event.target as Element | null
|
|
148
|
+
if (!target?.closest(clickSelector)) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
soundManager.playSound(soundTypes.click)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle keyboard events for typing sounds
|
|
156
|
+
function handleGlobalKeydown(event: KeyboardEvent) {
|
|
157
|
+
if (isMobileDevice()) {
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (event.ctrlKey || event.altKey || event.metaKey) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (ignoredKeys.has(event.key)) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const target = event.target as Element | null
|
|
170
|
+
if (!target?.closest(typingSelector)) {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
soundManager.playSound(soundTypes.typing)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
document.addEventListener('click', handleGlobalClick)
|
|
178
|
+
document.addEventListener('keydown', handleGlobalKeydown)
|
|
179
|
+
</script>
|