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,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>
|