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.
Files changed (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +168 -0
  3. package/astro.config.ts +18 -0
  4. package/default-config.yaml +136 -0
  5. package/discover-collections.ts +160 -0
  6. package/integration.ts +394 -0
  7. package/package.json +105 -0
  8. package/patches/@qwik.dev__partytown@0.11.2.patch +98 -0
  9. package/public/_redirects +819 -0
  10. package/public/feeds/atom-style.xsl +105 -0
  11. package/public/feeds/rss-style.xsl +105 -0
  12. package/public/fonts/EarlySummer-VF-Split/00785494587e3487ac63a0e7e4fa30f0.woff2 +0 -0
  13. package/public/fonts/EarlySummer-VF-Split/08e5d941a4c76fad7b68e7a937ebb21f.woff2 +0 -0
  14. package/public/fonts/EarlySummer-VF-Split/1268e5072156188d601f1eeb4473655d.woff2 +0 -0
  15. package/public/fonts/EarlySummer-VF-Split/12a385475353c815d7a5add53ee51e37.woff2 +0 -0
  16. package/public/fonts/EarlySummer-VF-Split/12b11ca08223c65a21fc731d59dcfc11.woff2 +0 -0
  17. package/public/fonts/EarlySummer-VF-Split/16d6676d3cb645c520ee6df8a1f89afd.woff2 +0 -0
  18. package/public/fonts/EarlySummer-VF-Split/2912a75ffef95e7a5ae9e2b2311ad61d.woff2 +0 -0
  19. package/public/fonts/EarlySummer-VF-Split/298d96ea561e419a4104bc9fc18499ce.woff2 +0 -0
  20. package/public/fonts/EarlySummer-VF-Split/2a2c71acc17ec39f6780835899e53096.woff2 +0 -0
  21. package/public/fonts/EarlySummer-VF-Split/2a7e2d0e59d3f638074c50fab39fdef1.woff2 +0 -0
  22. package/public/fonts/EarlySummer-VF-Split/36931fc4370e1670ed76af5d3feccba2.woff2 +0 -0
  23. package/public/fonts/EarlySummer-VF-Split/3a68fdc792e4a9e0399a04e32d0cc2e3.woff2 +0 -0
  24. package/public/fonts/EarlySummer-VF-Split/4054d6a4d6b37719b51e0f71da6e7cd9.woff2 +0 -0
  25. package/public/fonts/EarlySummer-VF-Split/429cb25f825c3cbde6bfac5b36ae9675.woff2 +0 -0
  26. package/public/fonts/EarlySummer-VF-Split/42a9efc11298368ecdc1b85ab46f0b4f.woff2 +0 -0
  27. package/public/fonts/EarlySummer-VF-Split/432018d2bdc9df92a7662056eb2b1261.woff2 +0 -0
  28. package/public/fonts/EarlySummer-VF-Split/44a6fb782f2a01560faa0f95248b60ef.woff2 +0 -0
  29. package/public/fonts/EarlySummer-VF-Split/45367b060e8ba0aa2507e6b91b86620b.woff2 +0 -0
  30. package/public/fonts/EarlySummer-VF-Split/571db7564bda7c1a93542881b8976f4b.woff2 +0 -0
  31. package/public/fonts/EarlySummer-VF-Split/58d55eeef4cf455e86a1142b1f3110d3.woff2 +0 -0
  32. package/public/fonts/EarlySummer-VF-Split/59ea41e77309160a0f63cdc76a010202.woff2 +0 -0
  33. package/public/fonts/EarlySummer-VF-Split/5d19d9174e568db4755981aa2e4ab380.woff2 +0 -0
  34. package/public/fonts/EarlySummer-VF-Split/5e811eb3b4175ee93d7ec000bf4631c2.woff2 +0 -0
  35. package/public/fonts/EarlySummer-VF-Split/6268e0cd5d66d6fe05b331f259e7b9e4.woff2 +0 -0
  36. package/public/fonts/EarlySummer-VF-Split/6549844aa3d833ca06a68a8e839db465.woff2 +0 -0
  37. package/public/fonts/EarlySummer-VF-Split/714b459658a7321ceeb1e1386ce165c2.woff2 +0 -0
  38. package/public/fonts/EarlySummer-VF-Split/7511d97a469915013683eae06cb21cd9.woff2 +0 -0
  39. package/public/fonts/EarlySummer-VF-Split/7784b4ebe543d13f62f6f6e05beb0b2e.woff2 +0 -0
  40. package/public/fonts/EarlySummer-VF-Split/77c9bea70b3c6ab24e1497d5468c825b.woff2 +0 -0
  41. package/public/fonts/EarlySummer-VF-Split/789ebea9e81df623e930b86de98fbfab.woff2 +0 -0
  42. package/public/fonts/EarlySummer-VF-Split/885bb7ab0717e8a47fc17f953adcdbf1.woff2 +0 -0
  43. package/public/fonts/EarlySummer-VF-Split/896c58aff69a9a857764cee0663bc56d.woff2 +0 -0
  44. package/public/fonts/EarlySummer-VF-Split/8fb6fc01c59d1e3ad1910b58dec7f5e7.woff2 +0 -0
  45. package/public/fonts/EarlySummer-VF-Split/95be5462b91b9a0458797cdc89d94cb5.woff2 +0 -0
  46. package/public/fonts/EarlySummer-VF-Split/9a5b2724f983ca0fc0d5ff8d10c41396.woff2 +0 -0
  47. package/public/fonts/EarlySummer-VF-Split/9ffe17f9c0e4cc4356cb3f08ffdb9c6d.woff2 +0 -0
  48. package/public/fonts/EarlySummer-VF-Split/EarlySummer-VF-Subset.woff2 +0 -0
  49. package/public/fonts/EarlySummer-VF-Split/EarlySummerSerif License.txt +91 -0
  50. package/public/fonts/EarlySummer-VF-Split/a097ef49be62cd2565aca45600e1e3ac.woff2 +0 -0
  51. package/public/fonts/EarlySummer-VF-Split/a17a1ae6063088e5b3a48c06b816929a.woff2 +0 -0
  52. package/public/fonts/EarlySummer-VF-Split/a83fdcfc5ecf2f6996704b0c02758689.woff2 +0 -0
  53. package/public/fonts/EarlySummer-VF-Split/a8cf15ff9b71e59407d8406866ff6f99.woff2 +0 -0
  54. package/public/fonts/EarlySummer-VF-Split/af530ed51dd519e4456f8a5e259e908b.woff2 +0 -0
  55. package/public/fonts/EarlySummer-VF-Split/b195a8924915deec4aa9c3ec777cc93f.woff2 +0 -0
  56. package/public/fonts/EarlySummer-VF-Split/b4b6bb5df9239dd67b52ca858fd2a506.woff2 +0 -0
  57. package/public/fonts/EarlySummer-VF-Split/b7592e1e027923f19e0e55dfdac69668.woff2 +0 -0
  58. package/public/fonts/EarlySummer-VF-Split/b965859f69d8ccceaf0e2d6292afbcfb.woff2 +0 -0
  59. package/public/fonts/EarlySummer-VF-Split/bbe9333f1ff242bd96ecb23ff9e723b1.woff2 +0 -0
  60. package/public/fonts/EarlySummer-VF-Split/be758580e295339ea98f0240b9869f24.woff2 +0 -0
  61. package/public/fonts/EarlySummer-VF-Split/c07099e1d025617f6d40966986e1941b.woff2 +0 -0
  62. package/public/fonts/EarlySummer-VF-Split/c1b593dda62fdeb7dde3af02016da282.woff2 +0 -0
  63. package/public/fonts/EarlySummer-VF-Split/c89f0335910a68a0958f2846108370e8.woff2 +0 -0
  64. package/public/fonts/EarlySummer-VF-Split/ca49aa409fdedd3f2f894cd20a16640a.woff2 +0 -0
  65. package/public/fonts/EarlySummer-VF-Split/ccd4a28d2f63797e0183c87792e20b75.woff2 +0 -0
  66. package/public/fonts/EarlySummer-VF-Split/d2718da923fce8e7ea229d65e306e92c.woff2 +0 -0
  67. package/public/fonts/EarlySummer-VF-Split/d893e9b307d96041e9cfcbd03761b9f4.woff2 +0 -0
  68. package/public/fonts/EarlySummer-VF-Split/dafaedaee41b75e21479d4ff324b6a34.woff2 +0 -0
  69. package/public/fonts/EarlySummer-VF-Split/db392af65f1867e5fd580eed2195df99.woff2 +0 -0
  70. package/public/fonts/EarlySummer-VF-Split/dc7c73a9e5577143ccd11e05ab55cb39.woff2 +0 -0
  71. package/public/fonts/EarlySummer-VF-Split/de396881189f747eba67685298363242.woff2 +0 -0
  72. package/public/fonts/EarlySummer-VF-Split/df625b213228bba22a7733d4eff8f148.woff2 +0 -0
  73. package/public/fonts/EarlySummer-VF-Split/e6e60b384f220b893ef31a926ece829a.woff2 +0 -0
  74. package/public/fonts/EarlySummer-VF-Split/e6e8ce2c5972ab665630bb705383d0fb.woff2 +0 -0
  75. package/public/fonts/EarlySummer-VF-Split/e963c7ed7104c2d6d68fcb5f952fe2f5.woff2 +0 -0
  76. package/public/fonts/EarlySummer-VF-Split/e966b23b4cd7783f43e31032d41784f4.woff2 +0 -0
  77. package/public/fonts/EarlySummer-VF-Split/edaac57c3856ec13128f4c6c3e00975c.woff2 +0 -0
  78. package/public/fonts/EarlySummer-VF-Split/ee54e0d86edf068c6c9cbddb76a856fe.woff2 +0 -0
  79. package/public/fonts/EarlySummer-VF-Split/f612c78a5544ff2dd3e8296ac3e58344.woff2 +0 -0
  80. package/public/fonts/EarlySummer-VF-Split/f9e539bd9b7bf999c3da82f5403ec3b6.woff2 +0 -0
  81. package/public/fonts/EarlySummer-VF-Split/fa5863b923ac15993c52a619f699ee63.woff2 +0 -0
  82. package/public/fonts/EarlySummer-VF-Split/fc759e56ec6f6e6d3d4cb163d62fb557.woff2 +0 -0
  83. package/public/fonts/Font Subset List/CJK Common Characters.txt +7534 -0
  84. package/public/fonts/Font Subset List/EarlySummer Subset.txt +3 -0
  85. package/public/fonts/Font Subset List/Japanese Kana + Korean Letters.txt +6123 -0
  86. package/public/fonts/Font Subset List/Latin + Cyrillic + Greek + Arabic Glyphs.txt +121 -0
  87. package/public/fonts/Font Subset List/unicode_range.py +49 -0
  88. package/public/fonts/NotoSansSC-Bold.otf +0 -0
  89. package/public/fonts/NotoSansSC-Regular.otf +0 -0
  90. package/public/fonts/STIX-Italic-VF.woff2 +0 -0
  91. package/public/fonts/STIX-VF.woff2 +0 -0
  92. package/public/fonts/Snell-Black-SF.woff2 +0 -0
  93. package/public/fonts/Snell-Bold-SF.woff2 +0 -0
  94. package/public/giscus/theme-dark.css +208 -0
  95. package/public/giscus/theme-light.css +208 -0
  96. package/public/icons/favicon.svg +4 -0
  97. package/public/icons/og-logo.png +0 -0
  98. package/public/robots.txt +4 -0
  99. package/public/sounds/tap_01.wav +0 -0
  100. package/public/sounds/tap_02.wav +0 -0
  101. package/public/sounds/tap_03.wav +0 -0
  102. package/public/sounds/tap_04.wav +0 -0
  103. package/public/sounds/tap_05.wav +0 -0
  104. package/public/sounds/type_01.wav +0 -0
  105. package/public/sounds/type_02.wav +0 -0
  106. package/public/sounds/type_03.wav +0 -0
  107. package/public/sounds/type_04.wav +0 -0
  108. package/public/sounds/type_05.wav +0 -0
  109. package/scripts/apply-lqip.ts +276 -0
  110. package/scripts/format-posts.ts +105 -0
  111. package/scripts/migration/README.md +52 -0
  112. package/scripts/migration/migrate-hexo.ts +185 -0
  113. package/scripts/migration/validate-abbrlinks.ts +161 -0
  114. package/scripts/new-post.ts +52 -0
  115. package/scripts/seo/generate-legacy-redirects.ts +407 -0
  116. package/scripts/update-theme.ts +46 -0
  117. package/src/assets/icons/copy-check.svg +3 -0
  118. package/src/assets/icons/copy-icon.svg +4 -0
  119. package/src/assets/icons/go-back.svg +3 -0
  120. package/src/assets/icons/heading-anchor.svg +4 -0
  121. package/src/assets/icons/lang-en.svg +3 -0
  122. package/src/assets/icons/lang-ja.svg +5 -0
  123. package/src/assets/icons/lang-zh.svg +5 -0
  124. package/src/assets/icons/language-switcher.svg +3 -0
  125. package/src/assets/icons/pin-icon.svg +3 -0
  126. package/src/assets/icons/search-icon.svg +3 -0
  127. package/src/assets/icons/theme-toggle.svg +3 -0
  128. package/src/assets/icons/toc-icon.svg +3 -0
  129. package/src/assets/icons/top-icon.svg +3 -0
  130. package/src/assets/lqip-map.json +10 -0
  131. package/src/components/Button.astro +152 -0
  132. package/src/components/CategoryList.astro +66 -0
  133. package/src/components/Comment/Giscus.astro +119 -0
  134. package/src/components/Comment/Index.astro +30 -0
  135. package/src/components/Comment/Twikoo.astro +114 -0
  136. package/src/components/Comment/Waline.astro +149 -0
  137. package/src/components/FloatingButtons.astro +101 -0
  138. package/src/components/Footer.astro +74 -0
  139. package/src/components/Header.astro +62 -0
  140. package/src/components/JournalList.astro +56 -0
  141. package/src/components/Navbar.astro +69 -0
  142. package/src/components/NoteList.astro +56 -0
  143. package/src/components/Pagination.astro +267 -0
  144. package/src/components/PostDate.astro +80 -0
  145. package/src/components/PostList.astro +87 -0
  146. package/src/components/SearchModal.astro +340 -0
  147. package/src/components/TagList.astro +135 -0
  148. package/src/components/Widgets/BackButton.astro +43 -0
  149. package/src/components/Widgets/CodeCopyButton.astro +47 -0
  150. package/src/components/Widgets/GithubCard.astro +110 -0
  151. package/src/components/Widgets/ImageZoom.astro +135 -0
  152. package/src/components/Widgets/MediaEmbed.astro +127 -0
  153. package/src/components/Widgets/SoundEffect.astro +179 -0
  154. package/src/components/Widgets/TOC.astro +198 -0
  155. package/src/config-schema.ts +164 -0
  156. package/src/config.ts +127 -0
  157. package/src/config.ts.example +205 -0
  158. package/src/content/about/_example-about-en.md +6 -0
  159. package/src/content/about/about-en.md +21 -0
  160. package/src/content/about/about-ja.md +21 -0
  161. package/src/content/about/about-zh.md +24 -0
  162. package/src/content.config.ts +247 -0
  163. package/src/env.d.ts +25 -0
  164. package/src/i18n/config.ts +65 -0
  165. package/src/i18n/lang.ts +70 -0
  166. package/src/i18n/path.ts +160 -0
  167. package/src/i18n/ui.ts +214 -0
  168. package/src/layouts/Head.astro +203 -0
  169. package/src/layouts/Layout.astro +69 -0
  170. package/src/pages/404.astro +20 -0
  171. package/src/pages/[...lang]/[...page].astro +48 -0
  172. package/src/pages/[...lang]/about.astro +28 -0
  173. package/src/pages/[...lang]/atom.xml.ts +14 -0
  174. package/src/pages/[...lang]/categories/index.astro +35 -0
  175. package/src/pages/[...lang]/journals/[slug].astro +89 -0
  176. package/src/pages/[...lang]/journals/index.astro +55 -0
  177. package/src/pages/[...lang]/journals/page/[page].astro +66 -0
  178. package/src/pages/[...lang]/notes/[slug].astro +88 -0
  179. package/src/pages/[...lang]/notes/index.astro +55 -0
  180. package/src/pages/[...lang]/notes/page/[page].astro +66 -0
  181. package/src/pages/[...lang]/posts/[slug].astro +101 -0
  182. package/src/pages/[...lang]/rss.xml.ts +14 -0
  183. package/src/pages/[...lang]/search.astro +65 -0
  184. package/src/pages/[...lang]/tags/[tag].astro +53 -0
  185. package/src/pages/[...lang]/tags/index.astro +36 -0
  186. package/src/pages/_dynamic/list.astro +101 -0
  187. package/src/pages/_dynamic/slug.astro +100 -0
  188. package/src/pages/og/[...image].ts +114 -0
  189. package/src/pages/robots.txt.ts +20 -0
  190. package/src/plugins/rehype-code-copy-button.mjs +82 -0
  191. package/src/plugins/rehype-external-links.mjs +18 -0
  192. package/src/plugins/rehype-heading-anchor.mjs +55 -0
  193. package/src/plugins/rehype-image-processor.mjs +77 -0
  194. package/src/plugins/remark-container-directives.mjs +135 -0
  195. package/src/plugins/remark-leaf-directives.mjs +184 -0
  196. package/src/plugins/remark-reading-time.mjs +11 -0
  197. package/src/styles/comment.css +205 -0
  198. package/src/styles/extension.css +180 -0
  199. package/src/styles/font.css +111 -0
  200. package/src/styles/global.css +91 -0
  201. package/src/styles/lqip.css +71 -0
  202. package/src/styles/markdown.css +276 -0
  203. package/src/styles/transition.css +173 -0
  204. package/src/types/global.d.ts +22 -0
  205. package/src/types/index.d.ts +111 -0
  206. package/src/utils/cache.ts +32 -0
  207. package/src/utils/content.ts +819 -0
  208. package/src/utils/description.ts +147 -0
  209. package/src/utils/dynamic-collections.ts +155 -0
  210. package/src/utils/feed.ts +238 -0
  211. package/src/utils/page.ts +107 -0
  212. package/tsconfig.json +13 -0
  213. 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>