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,101 @@
1
+ ---
2
+ import type { Language } from '@/i18n/config'
3
+ import LanguageSwitcherIcon from '@/assets/icons/language-switcher.svg'
4
+ import SearchIcon from '@/assets/icons/search-icon.svg'
5
+ import ThemeToggleIcon from '@/assets/icons/theme-toggle.svg'
6
+ import { moreLocales, themeConfig } from '@/config'
7
+ import { getNextGlobalLangPath, getNextSupportedLangPath } from '@/i18n/path'
8
+ import { ui } from '@/i18n/ui'
9
+ import { getPageInfo } from '@/utils/page'
10
+
11
+ interface Props {
12
+ supportedLangs?: Language[]
13
+ }
14
+
15
+ const {
16
+ light: { background: lightMode },
17
+ dark: { background: darkMode },
18
+ } = themeConfig.color
19
+
20
+ const { supportedLangs = [] } = Astro.props
21
+ const currentPath = Astro.url.pathname
22
+ const { currentLang } = getPageInfo(currentPath)
23
+ const currentUI = ui[currentLang as keyof typeof ui] ?? {}
24
+
25
+ const showLanguageSwitcher = moreLocales.length > 0
26
+ const nextUrl = supportedLangs.length > 0
27
+ ? getNextSupportedLangPath(currentPath, supportedLangs)
28
+ : getNextGlobalLangPath(currentPath)
29
+ ---
30
+
31
+ <div class="floating-container">
32
+ <!-- Action Buttons -->
33
+ <div class="floating-actions">
34
+ <button
35
+ id="search-button"
36
+ class="floating-btn"
37
+ aria-label={currentUI.search || 'Search'}
38
+ >
39
+ <SearchIcon aria-hidden="true" fill="currentColor" />
40
+ </button>
41
+
42
+ {showLanguageSwitcher && (
43
+ <a
44
+ id="language-switcher"
45
+ href={nextUrl}
46
+ class="floating-btn"
47
+ aria-label="Switch language"
48
+ >
49
+ <LanguageSwitcherIcon aria-hidden="true" fill="currentColor" />
50
+ </a>
51
+ )}
52
+
53
+ <button
54
+ id="theme-toggle-button"
55
+ class="floating-btn"
56
+ data-light-mode={lightMode}
57
+ data-dark-mode={darkMode}
58
+ aria-label="Toggle theme"
59
+ >
60
+ <ThemeToggleIcon aria-hidden="true" fill="currentColor" />
61
+ </button>
62
+ </div>
63
+ </div>
64
+
65
+ <style>
66
+ .floating-container {
67
+ /* Mobile: absolute position at top right */
68
+ position: absolute;
69
+ right: 7.25vw;
70
+ top: 14.6rem;
71
+ display: flex;
72
+ flex-direction: column;
73
+ gap: 1.5rem;
74
+ z-index: 50;
75
+ }
76
+
77
+ /* Desktop: fixed position on right side */
78
+ @media (min-width: 1024px) {
79
+ .floating-container {
80
+ position: fixed;
81
+ bottom: min(10.27rem + 1.92vw, 12rem);
82
+ right: max(5rem, calc(50vw - 35rem));
83
+ top: auto;
84
+ }
85
+ }
86
+
87
+ .floating-actions {
88
+ --at-apply: 'flex flex-col gap-2';
89
+ }
90
+
91
+ .floating-btn {
92
+ --at-apply: 'w-10 h-10 flex items-center justify-center';
93
+ --at-apply: 'bg-background/80 backdrop-blur-sm rounded-lg shadow-sm';
94
+ --at-apply: 'border border-secondary/10';
95
+ --at-apply: 'c-secondary hover:c-primary transition-colors cursor-pointer';
96
+ }
97
+
98
+ .floating-btn svg {
99
+ --at-apply: 'w-5 h-5';
100
+ }
101
+ </style>
@@ -0,0 +1,74 @@
1
+ ---
2
+ import { themeConfig } from '@/config'
3
+ import { getLangFromPath } from '@/i18n/lang'
4
+ import { getLocalizedPath } from '@/i18n/path'
5
+
6
+ const { author } = themeConfig.site
7
+ const { links, startYear } = themeConfig.footer
8
+
9
+ const currentYear = new Date().getFullYear()
10
+ const year = Number(startYear) === currentYear
11
+ ? startYear
12
+ : `${startYear}-${currentYear}`
13
+
14
+ const currentLang = getLangFromPath(Astro.url.pathname)
15
+ const socialLinks = links.map(({ name, url }) => {
16
+ const isRSS = url === '/atom.xml' || url === '/rss.xml'
17
+ const isEmail = /^[\w.-]+@[\w.-]+\.[a-z]{2,}$/i.test(url)
18
+
19
+ if (isRSS) {
20
+ const localizedPath = getLocalizedPath(url, currentLang)
21
+ const href = import.meta.env.DEV
22
+ ? localizedPath
23
+ : localizedPath.slice(0, -1)
24
+
25
+ return {
26
+ name,
27
+ href,
28
+ target: '_blank',
29
+ rel: 'noopener noreferrer',
30
+ }
31
+ }
32
+
33
+ if (isEmail) {
34
+ const href = `mailto:${url}`
35
+ return {
36
+ name,
37
+ href,
38
+ 'data-umami-event': 'outbound-link-click',
39
+ 'data-umami-event-url': href,
40
+ }
41
+ }
42
+
43
+ return {
44
+ name,
45
+ 'href': url,
46
+ 'target': '_blank',
47
+ 'rel': 'noopener noreferrer',
48
+ 'data-umami-event': 'outbound-link-click',
49
+ 'data-umami-event-url': url,
50
+ }
51
+ })
52
+ ---
53
+
54
+ <footer
55
+ class="text-3 leading-1.25em font-navbar"
56
+ lg="uno-desktop-column bottom-20 text-3.5"
57
+ >
58
+ <p>
59
+ {socialLinks.map((link, index) => (
60
+ <>
61
+ <a class="highlight-hover py-0.8 transition-colors after:bottom-0.35em hover:c-primary" {...link}>{link.name}</a>
62
+ {index < socialLinks.length - 1 && '/'}
63
+ </>
64
+ ))}
65
+ </p>
66
+
67
+ <p>
68
+ © {year} {author}
69
+ </p>
70
+
71
+ <p>
72
+ Powered by <a class="highlight-hover py-0.8 transition-colors after:bottom-0.35em hover:c-primary" href="https://astro.build/" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://astro.build/">Astro</a> and <a class="highlight-hover py-0.8 transition-colors after:bottom-0.35em hover:c-primary" href="https://github.com/radishzzz/astro-theme-retypeset" target="_blank" rel="noopener noreferrer" data-umami-event="outbound-link-click" data-umami-event-url="https://github.com/radishzzz/astro-theme-retypeset">Retypeset</a>
73
+ </p>
74
+ </footer>
@@ -0,0 +1,62 @@
1
+ ---
2
+ import { themeConfig } from '@/config'
3
+ import { ui } from '@/i18n/ui'
4
+ import { getPageInfo } from '@/utils/page'
5
+
6
+ const { currentLang, getLocalizedPath, isPost, isNote, isJournal } = getPageInfo(Astro.url.pathname)
7
+ const isArticle = isPost || isNote || isJournal
8
+ const { title, subtitle, i18nTitle } = themeConfig.site
9
+
10
+ const currentUI = ui[currentLang as keyof typeof ui] ?? {}
11
+ const headerTitle = i18nTitle ? currentUI.title : title
12
+ const headerSubtitle = i18nTitle ? currentUI.subtitle : subtitle
13
+
14
+ const TitleTag = isArticle ? 'h2' : 'h1'
15
+ const SubtitleTag = isArticle ? 'div' : 'h2'
16
+ ---
17
+
18
+ <header
19
+ class:list={[
20
+ 'lg:(uno-desktop-column top-20) cjk:tracking-wide',
21
+ isArticle
22
+ ? 'mb-10.8'
23
+ : 'mb-10.5',
24
+ ]}
25
+ >
26
+ <TitleTag
27
+ class:list={[
28
+ 'font-bold font-title',
29
+ isArticle
30
+ ? `mb-2.8 mt-3 text-5.375 c-secondary lg:(mb-1.8 mt-0 text-9 c-primary)`
31
+ : `mb-1.8 w-75% text-8 c-primary lg:(w-full text-9)`,
32
+ ]}
33
+ >
34
+ <!-- Fix text cropping issues during view transition on iOS by adding a div tag -->
35
+ <div
36
+ class="box-content inline-block pr-1"
37
+ transition:name={`site-title-${currentLang}`}
38
+ data-disable-theme-toggle-transition
39
+ >
40
+ <a
41
+ id="site-title-link"
42
+ href={getLocalizedPath('/')}
43
+ >
44
+ {headerTitle}
45
+ </a>
46
+ </div>
47
+ </TitleTag>
48
+
49
+ {headerSubtitle && (
50
+ <SubtitleTag
51
+ class:list={[
52
+ 'text-3.5 c-secondary font-navbar lg:text-4',
53
+ isArticle
54
+ ? `op-0 lg:op-100`
55
+ : 'w-75% lg:w-full',
56
+ ]}
57
+ aria-hidden={isArticle}
58
+ >
59
+ {headerSubtitle}
60
+ </SubtitleTag>
61
+ )}
62
+ </header>
@@ -0,0 +1,56 @@
1
+ ---
2
+ import type { Language } from '@/i18n/config'
3
+ import type { Journal } from '@/types'
4
+ import PostDate from '@/components/PostDate.astro'
5
+ import { getJournalPath } from '@/i18n/path'
6
+ import { getJournalDescription } from '@/utils/description'
7
+ import { getJournalSlug } from '@/utils/content'
8
+
9
+ export interface Props {
10
+ journals: Journal[]
11
+ lang: Language
12
+ }
13
+
14
+ const { journals, lang } = Astro.props
15
+ ---
16
+
17
+ <ul>
18
+ {journals.map((journal) => {
19
+ const slug = getJournalSlug(journal)
20
+
21
+ return (
22
+ <li class="mb-5.5 lg:mb-7.5">
23
+ {/* journal title */}
24
+ <h3 class="inline transition-colors hover:c-primary">
25
+ <a
26
+ class="cjk:tracking-wide lg:font-medium lg:text-4.25"
27
+ href={getJournalPath(slug, lang)}
28
+ transition:name={`journal-${slug}${lang ? `-${lang}` : ''}`}
29
+ data-disable-theme-toggle-transition
30
+ >
31
+ {journal.data.title}
32
+ </a>
33
+ </h3>
34
+
35
+ {/* journal time */}
36
+ <div
37
+ class="py-0.8 text-3.5 font-time"
38
+ transition:name={`journal-time-${slug}${lang ? `-${lang}` : ''}`}
39
+ data-disable-theme-toggle-transition
40
+ >
41
+ <PostDate
42
+ date={journal.data.published}
43
+ updatedDate={journal.data.updated}
44
+ minutes={journal.remarkPluginFrontmatter.minutes}
45
+ />
46
+ </div>
47
+
48
+ {/* journal description */}
49
+ <div class="heti hidden lg:block">
50
+ <p>{getJournalDescription(journal, 'list')}</p>
51
+ </div>
52
+ </li>
53
+ )
54
+ })}
55
+ </ul>
56
+
@@ -0,0 +1,69 @@
1
+ ---
2
+ import { ui } from '@/i18n/ui'
3
+ import { getPageInfo } from '@/utils/page'
4
+
5
+ const { currentLang, isHome, isPost, isNote, isJournal, isTag, isAbout, getLocalizedPath } = getPageInfo(Astro.url.pathname)
6
+ const currentUI = ui[currentLang as keyof typeof ui] ?? {}
7
+
8
+ const isPostActive = isHome || isPost
9
+ const isNoteActive = isNote
10
+ const isJournalActive = isJournal
11
+ const isTagActive = isTag
12
+ const isAboutActive = isAbout
13
+
14
+ function getNavItemClass(isActive: boolean) {
15
+ return isActive
16
+ ? 'highlight-static c-primary font-bold after:bottom-0.7em'
17
+ : 'highlight-hover transition-[colors,font-weight] after:bottom-0.7em hover:(c-primary font-bold)'
18
+ }
19
+
20
+ const navItems = [
21
+ {
22
+ href: '/',
23
+ label: currentUI.posts,
24
+ className: getNavItemClass(isPostActive),
25
+ },
26
+ {
27
+ href: '/notes/',
28
+ label: currentUI.notes ?? 'Notes',
29
+ className: getNavItemClass(isNoteActive),
30
+ },
31
+ {
32
+ href: '/journals/',
33
+ label: currentUI.journals ?? 'Journal',
34
+ className: getNavItemClass(isJournalActive),
35
+ },
36
+ {
37
+ href: '/tags/',
38
+ label: currentUI.tags,
39
+ className: getNavItemClass(isTagActive),
40
+ },
41
+ {
42
+ href: '/about/',
43
+ label: currentUI.about,
44
+ className: getNavItemClass(isAboutActive),
45
+ },
46
+ ]
47
+ ---
48
+
49
+ <nav
50
+ aria-label="Site Navigation"
51
+ class:list={[
52
+ 'mb-10.5 text-3.6 font-semibold leading-2.45em font-navbar',
53
+ 'lg:(uno-desktop-column text-4 bottom-[min(9.04rem+3.85vw,12.5rem)]) cjk:tracking-wide',
54
+ (isPost || isNote || isJournal) ? 'hidden lg:block' : '',
55
+ ]}
56
+ >
57
+ <ul>
58
+ {navItems.map(item => (
59
+ <li>
60
+ <a
61
+ href={getLocalizedPath(item.href)}
62
+ class={item.className}
63
+ >
64
+ {item.label}
65
+ </a>
66
+ </li>
67
+ ))}
68
+ </ul>
69
+ </nav>
@@ -0,0 +1,56 @@
1
+ ---
2
+ import type { Language } from '@/i18n/config'
3
+ import type { Note } from '@/types'
4
+ import PostDate from '@/components/PostDate.astro'
5
+ import { getNotePath } from '@/i18n/path'
6
+ import { getNoteDescription } from '@/utils/description'
7
+ import { getNoteSlug } from '@/utils/content'
8
+
9
+ export interface Props {
10
+ notes: Note[]
11
+ lang: Language
12
+ }
13
+
14
+ const { notes, lang } = Astro.props
15
+ ---
16
+
17
+ <ul>
18
+ {notes.map((note) => {
19
+ const slug = getNoteSlug(note)
20
+
21
+ return (
22
+ <li class="mb-5.5 lg:mb-7.5">
23
+ {/* note title */}
24
+ <h3 class="inline transition-colors hover:c-primary">
25
+ <a
26
+ class="cjk:tracking-wide lg:font-medium lg:text-4.25"
27
+ href={getNotePath(slug, lang)}
28
+ transition:name={`note-${slug}${lang ? `-${lang}` : ''}`}
29
+ data-disable-theme-toggle-transition
30
+ >
31
+ {note.data.title}
32
+ </a>
33
+ </h3>
34
+
35
+ {/* note time */}
36
+ <div
37
+ class="py-0.8 text-3.5 font-time"
38
+ transition:name={`note-time-${slug}${lang ? `-${lang}` : ''}`}
39
+ data-disable-theme-toggle-transition
40
+ >
41
+ <PostDate
42
+ date={note.data.published}
43
+ updatedDate={note.data.updated}
44
+ minutes={note.remarkPluginFrontmatter.minutes}
45
+ />
46
+ </div>
47
+
48
+ {/* note description */}
49
+ <div class="heti hidden lg:block">
50
+ <p>{getNoteDescription(note, 'list')}</p>
51
+ </div>
52
+ </li>
53
+ )
54
+ })}
55
+ </ul>
56
+
@@ -0,0 +1,267 @@
1
+ ---
2
+ interface Props {
3
+ currentPage: number
4
+ totalPages: number
5
+ baseUrl: string
6
+ pagePrefix?: string
7
+ }
8
+
9
+ const { currentPage, totalPages, baseUrl, pagePrefix } = Astro.props
10
+
11
+ // Generate page URL - page 1 has no suffix, others have /2, /3, etc.
12
+ function getPageUrl(page: number): string {
13
+ if (page === 1) {
14
+ return baseUrl
15
+ }
16
+ // Remove trailing slash from baseUrl if present, then add page number
17
+ const cleanBase = baseUrl.replace(/\/$/, '')
18
+ return pagePrefix
19
+ ? `${cleanBase}/${pagePrefix}/${page}`
20
+ : `${cleanBase}/${page}`
21
+ }
22
+
23
+ const hasPrev = currentPage > 1
24
+ const hasNext = currentPage < totalPages
25
+ const prevUrl = hasPrev ? getPageUrl(currentPage - 1) : null
26
+ const nextUrl = hasNext ? getPageUrl(currentPage + 1) : null
27
+
28
+ // Generate visible page numbers (show max 5 pages around current)
29
+ function getVisiblePages(): number[] {
30
+ const pages: number[] = []
31
+ const maxVisible = 5
32
+ let start = Math.max(1, currentPage - Math.floor(maxVisible / 2))
33
+ let end = Math.min(totalPages, start + maxVisible - 1)
34
+
35
+ // Adjust start if we're near the end
36
+ if (end - start + 1 < maxVisible) {
37
+ start = Math.max(1, end - maxVisible + 1)
38
+ }
39
+
40
+ for (let i = start; i <= end; i++) {
41
+ pages.push(i)
42
+ }
43
+ return pages
44
+ }
45
+
46
+ const visiblePages = getVisiblePages()
47
+ ---
48
+
49
+ {totalPages > 1 && (
50
+ <nav
51
+ class="pagination"
52
+ aria-label="Pagination"
53
+ data-base-url={baseUrl}
54
+ data-total-pages={totalPages}
55
+ data-page-prefix={pagePrefix}
56
+ >
57
+ <div class="pagination-inner">
58
+ {/* Previous button */}
59
+ {hasPrev ? (
60
+ <a href={prevUrl} class="pagination-btn" aria-label="Previous page">
61
+ <span class="pagination-arrow">‹</span>
62
+ <span class="pagination-text">Prev</span>
63
+ </a>
64
+ ) : (
65
+ <span class="pagination-btn disabled" aria-disabled="true">
66
+ <span class="pagination-arrow">‹</span>
67
+ <span class="pagination-text">Prev</span>
68
+ </span>
69
+ )}
70
+
71
+ {/* Page numbers */}
72
+ <div class="pagination-pages">
73
+ {visiblePages[0] > 1 && (
74
+ <>
75
+ <a href={getPageUrl(1)} class="pagination-page">1</a>
76
+ {visiblePages[0] > 2 && (
77
+ <div class="pagination-ellipsis-wrapper">
78
+ <button type="button" class="pagination-ellipsis" aria-label="Jump to page">•••</button>
79
+ <input
80
+ type="number"
81
+ class="pagination-input"
82
+ min="1"
83
+ max={totalPages}
84
+ placeholder="Go"
85
+ aria-label="Enter page number"
86
+ />
87
+ </div>
88
+ )}
89
+ </>
90
+ )}
91
+
92
+ {visiblePages.map(page => (
93
+ page === currentPage ? (
94
+ <span class="pagination-page current" aria-current="page">{page}</span>
95
+ ) : (
96
+ <a href={getPageUrl(page)} class="pagination-page">{page}</a>
97
+ )
98
+ ))}
99
+
100
+ {visiblePages[visiblePages.length - 1] < totalPages && (
101
+ <>
102
+ {visiblePages[visiblePages.length - 1] < totalPages - 1 && (
103
+ <div class="pagination-ellipsis-wrapper">
104
+ <button type="button" class="pagination-ellipsis" aria-label="Jump to page">•••</button>
105
+ <input
106
+ type="number"
107
+ class="pagination-input"
108
+ min="1"
109
+ max={totalPages}
110
+ placeholder="Go"
111
+ aria-label="Enter page number"
112
+ />
113
+ </div>
114
+ )}
115
+ <a href={getPageUrl(totalPages)} class="pagination-page">{totalPages}</a>
116
+ </>
117
+ )}
118
+ </div>
119
+
120
+ {/* Next button */}
121
+ {hasNext ? (
122
+ <a href={nextUrl} class="pagination-btn" aria-label="Next page">
123
+ <span class="pagination-text">Next</span>
124
+ <span class="pagination-arrow">›</span>
125
+ </a>
126
+ ) : (
127
+ <span class="pagination-btn disabled" aria-disabled="true">
128
+ <span class="pagination-text">Next</span>
129
+ <span class="pagination-arrow">›</span>
130
+ </span>
131
+ )}
132
+ </div>
133
+ </nav>
134
+ )}
135
+
136
+ <style>
137
+ .pagination {
138
+ --at-apply: 'mt-10 mb-6';
139
+ }
140
+
141
+ .pagination-inner {
142
+ --at-apply: 'flex items-center justify-center gap-0';
143
+ }
144
+
145
+ .pagination-btn {
146
+ --at-apply: 'flex items-center gap-1 px-3 py-2 text-sm c-secondary hover:c-primary transition-colors duration-200';
147
+ letter-spacing: 0.05em;
148
+ }
149
+
150
+ .pagination-btn.disabled {
151
+ --at-apply: 'c-tertiary/35 cursor-not-allowed hover:c-tertiary/35';
152
+ }
153
+
154
+ .pagination-arrow {
155
+ --at-apply: 'text-base';
156
+ }
157
+
158
+ .pagination-text {
159
+ --at-apply: 'hidden sm:inline';
160
+ }
161
+
162
+ .pagination-pages {
163
+ --at-apply: 'flex items-center gap-0 border-l border-r border-secondary/30';
164
+ }
165
+
166
+ .pagination-page {
167
+ --at-apply: 'min-w-9 h-9 flex items-center justify-center text-sm c-secondary hover:c-primary transition-colors duration-200 font-serif';
168
+ }
169
+
170
+ .pagination-page.current {
171
+ --at-apply: 'c-primary font-semibold';
172
+ text-decoration: underline;
173
+ text-underline-offset: 4px;
174
+ text-decoration-thickness: 1.5px;
175
+ }
176
+
177
+ .pagination-ellipsis-wrapper {
178
+ --at-apply: 'relative';
179
+ }
180
+
181
+ .pagination-ellipsis {
182
+ --at-apply: 'min-w-8 h-9 flex items-center justify-center text-sm c-tertiary hover:c-primary cursor-pointer transition-colors duration-200 border-none bg-transparent';
183
+ letter-spacing: 0.1em;
184
+ }
185
+
186
+ .pagination-ellipsis.hidden {
187
+ --at-apply: 'invisible w-0 min-w-0 overflow-hidden';
188
+ }
189
+
190
+ .pagination-input {
191
+ --at-apply: 'absolute top-0 left-0 w-12 h-9 text-sm text-center rounded c-primary bg-secondary/10 border border-secondary/30 outline-none transition-all duration-200 opacity-0 invisible font-serif';
192
+ }
193
+
194
+ .pagination-input:focus {
195
+ --at-apply: 'border-primary/60';
196
+ }
197
+
198
+ .pagination-input.active {
199
+ --at-apply: 'opacity-100 visible';
200
+ }
201
+
202
+ /* Hide number input spinners */
203
+ .pagination-input::-webkit-outer-spin-button,
204
+ .pagination-input::-webkit-inner-spin-button {
205
+ -webkit-appearance: none;
206
+ margin: 0;
207
+ }
208
+
209
+ .pagination-input[type=number] {
210
+ -moz-appearance: textfield;
211
+ }
212
+ </style>
213
+
214
+ <script>
215
+ document.addEventListener('astro:page-load', initPagination);
216
+ document.addEventListener('DOMContentLoaded', initPagination);
217
+
218
+ function initPagination() {
219
+ const nav = document.querySelector('.pagination') as HTMLElement | null;
220
+ if (!nav) return;
221
+
222
+ const baseUrl = nav.dataset.baseUrl || '';
223
+ const totalPages = parseInt(nav.dataset.totalPages || '1', 10);
224
+ const pagePrefix = nav.dataset.pagePrefix || '';
225
+
226
+ document.querySelectorAll('.pagination-ellipsis').forEach(btn => {
227
+ btn.addEventListener('click', () => {
228
+ const wrapper = btn.closest('.pagination-ellipsis-wrapper');
229
+ const input = wrapper?.querySelector('.pagination-input') as HTMLInputElement | null;
230
+ if (!input) return;
231
+
232
+ btn.classList.add('hidden');
233
+ input.classList.add('active');
234
+ input.focus();
235
+ });
236
+ });
237
+
238
+ document.querySelectorAll<HTMLInputElement>('.pagination-input').forEach(input => {
239
+ const wrapper = input.closest('.pagination-ellipsis-wrapper');
240
+ const btn = wrapper?.querySelector('.pagination-ellipsis');
241
+
242
+ input.addEventListener('blur', () => {
243
+ input.classList.remove('active');
244
+ input.value = '';
245
+ btn?.classList.remove('hidden');
246
+ });
247
+
248
+ input.addEventListener('keydown', (e: KeyboardEvent) => {
249
+ if (e.key === 'Enter') {
250
+ e.preventDefault();
251
+ const page = parseInt(input.value, 10);
252
+ if (page >= 1 && page <= totalPages) {
253
+ const cleanBase = baseUrl.replace(/\/$/, '');
254
+ const url = page === 1
255
+ ? baseUrl
256
+ : pagePrefix
257
+ ? `${cleanBase}/${pagePrefix}/${page}`
258
+ : `${cleanBase}/${page}`;
259
+ window.location.href = url;
260
+ }
261
+ } else if (e.key === 'Escape') {
262
+ input.blur();
263
+ }
264
+ });
265
+ });
266
+ }
267
+ </script>