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,80 @@
1
+ ---
2
+ import type { ThemeConfig } from '@/types'
3
+ import { themeConfig } from '@/config'
4
+ import { isJournalPage, isNotePage, isPostPage } from '@/utils/page'
5
+
6
+ type DateFormat = ThemeConfig['global']['dateFormat']
7
+
8
+ interface Props {
9
+ date: Date
10
+ updatedDate?: Date
11
+ minutes: number
12
+ }
13
+
14
+ const { date, updatedDate, minutes } = Astro.props
15
+ const { dateFormat: format } = themeConfig.global
16
+ const isArticle = isPostPage(Astro.url.pathname) || isNotePage(Astro.url.pathname) || isJournalPage(Astro.url.pathname)
17
+ const timeSpacingClass = isArticle ? 'ml-1.75' : 'ml-1.5'
18
+
19
+ function formatDate(date: Date, format: DateFormat) {
20
+ if (format === 'YYYY-MM-DD') {
21
+ return date.toISOString().split('T')[0]
22
+ }
23
+
24
+ const options: Intl.DateTimeFormatOptions = {
25
+ year: 'numeric',
26
+ month: format === 'MMM D YYYY' || format === 'D MMM YYYY' ? 'short' : '2-digit',
27
+ day: format === 'MMM D YYYY' || format === 'D MMM YYYY' ? 'numeric' : '2-digit',
28
+ }
29
+
30
+ switch (format) {
31
+ // US date format: 04-13-2025
32
+ case 'MM-DD-YYYY':
33
+ return date.toLocaleDateString('en-US', options).replace(/\//g, '-')
34
+
35
+ // European date format: 13-04-2025
36
+ case 'DD-MM-YYYY':
37
+ return date.toLocaleDateString('en-GB', options).replace(/\//g, '-')
38
+
39
+ // US month text format: Apr 13 2025
40
+ case 'MMM D YYYY':
41
+ return date.toLocaleDateString('en-US', {
42
+ year: 'numeric',
43
+ month: 'short',
44
+ day: 'numeric',
45
+ }).replace(',', '')
46
+
47
+ // British month text format: 13 Apr 2025
48
+ case 'D MMM YYYY':
49
+ return date.toLocaleDateString('en-GB', {
50
+ year: 'numeric',
51
+ month: 'short',
52
+ day: 'numeric',
53
+ }).replace(',', '')
54
+
55
+ // Default to ISO format
56
+ default:
57
+ return date.toISOString().split('T')[0]
58
+ }
59
+ }
60
+ ---
61
+
62
+ <!-- published date -->
63
+ <time datetime={date.toISOString().split('T')[0]}>
64
+ {formatDate(date, format)}
65
+ </time>
66
+
67
+ <!-- updated date -->
68
+ {updatedDate && (
69
+ <time
70
+ datetime={updatedDate.toISOString().split('T')[0]}
71
+ class={timeSpacingClass}
72
+ >
73
+ updated {formatDate(updatedDate, format)}
74
+ </time>
75
+ )}
76
+
77
+ <!-- reading time -->
78
+ <span class={timeSpacingClass}>
79
+ {minutes} min
80
+ </span>
@@ -0,0 +1,87 @@
1
+ ---
2
+ import type { Language } from '@/i18n/config'
3
+ import type { Post } from '@/types'
4
+ import PinIcon from '@/assets/icons/pin-icon.svg'
5
+ import PostDate from '@/components/PostDate.astro'
6
+ import { getPostPath } from '@/i18n/path'
7
+ import { getPostDescription } from '@/utils/description'
8
+ import { getPostSlug } from '@/utils/content'
9
+ import { isHomePage } from '@/utils/page'
10
+
11
+ export interface Props {
12
+ posts: Post[]
13
+ lang: Language
14
+ pinned?: boolean
15
+ }
16
+
17
+ const { posts, lang, pinned = false } = Astro.props
18
+ const isHome = isHomePage(Astro.url.pathname)
19
+ ---
20
+
21
+ <ul>
22
+ {posts.map((post) => {
23
+ const slug = getPostSlug(post)
24
+
25
+ return (
26
+ <li
27
+ class:list={[
28
+ 'mb-5.5',
29
+ isHome ? 'lg:mb-10' : '',
30
+ ]}
31
+ >
32
+ {/* post title */}
33
+ <h3 class="inline transition-colors hover:c-primary">
34
+ <a
35
+ class:list={[
36
+ 'cjk:tracking-wide',
37
+ isHome ? 'lg:font-medium lg:text-4.5' : '',
38
+ ]}
39
+ href={getPostPath(slug, lang)}
40
+ transition:name={`post-${slug}${lang ? `-${lang}` : ''}`}
41
+ data-disable-theme-toggle-transition
42
+ >
43
+ {post.data.title}
44
+ </a>
45
+ {/* pinned icon */}
46
+ {pinned && (
47
+ <PinIcon
48
+ aria-hidden="true"
49
+ class="ml-0.25em inline-block aspect-square w-0.98em translate-y--0.1em lg:(w-1.05em translate-y--0.15em)"
50
+ fill="currentColor"
51
+ />
52
+ )}
53
+ </h3>
54
+
55
+ {/* mobile post time */}
56
+ <div
57
+ class="py-0.8 text-3.5 font-time lg:hidden"
58
+ transition:name={`time-${slug}${lang ? `-${lang}` : ''}`}
59
+ data-disable-theme-toggle-transition
60
+ >
61
+ <PostDate
62
+ date={post.data.published}
63
+ minutes={post.remarkPluginFrontmatter.minutes}
64
+ />
65
+ </div>
66
+
67
+ {/* desktop post time */}
68
+ <div class="hidden text-3.65 font-time lg:(ml-2.5 inline)">
69
+ <PostDate
70
+ date={post.data.published}
71
+ minutes={post.remarkPluginFrontmatter.minutes}
72
+ />
73
+ </div>
74
+
75
+ {/* desktop post description */}
76
+ {isHome && (
77
+ <div
78
+ class="heti hidden"
79
+ lg="mt-2.25 block"
80
+ >
81
+ <p>{getPostDescription(post, 'list')}</p>
82
+ </div>
83
+ )}
84
+ </li>
85
+ )
86
+ })}
87
+ </ul>
@@ -0,0 +1,340 @@
1
+ ---
2
+ import { ui } from '@/i18n/ui'
3
+ import { getPageInfo } from '@/utils/page'
4
+
5
+ const { currentLang } = getPageInfo(Astro.url.pathname)
6
+ const currentUI = ui[currentLang as keyof typeof ui] ?? {}
7
+ ---
8
+
9
+ <dialog id="search-modal" class="search-modal">
10
+ <div class="modal-content">
11
+ <div class="modal-header">
12
+ <h2 class="modal-title">{currentUI.search || 'Search'}</h2>
13
+ <button id="close-search-modal" class="close-btn" aria-label="Close">
14
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
15
+ <path d="M18 6L6 18M6 6l12 12" />
16
+ </svg>
17
+ </button>
18
+ </div>
19
+ <div id="search-container" data-placeholder={currentUI.searchPlaceholder} data-no-results={currentUI.searchNoResults} data-results-found={currentUI.searchResultsFound}></div>
20
+ </div>
21
+ </dialog>
22
+
23
+ <link href="/pagefind/pagefind-ui.css" rel="stylesheet" />
24
+ <script is:inline src="/pagefind/pagefind-ui.js" type="text/javascript"></script>
25
+
26
+ <script>
27
+ let pagefindInitialized = false;
28
+
29
+ function initPagefind() {
30
+ if (pagefindInitialized) return;
31
+ if (typeof PagefindUI === 'undefined') return;
32
+
33
+ const container = document.getElementById('search-container');
34
+ const placeholder = container?.dataset.placeholder || 'Type to search...';
35
+
36
+ new PagefindUI({
37
+ element: '#search-container',
38
+ showImages: false,
39
+ showSubResults: true
40
+ });
41
+
42
+ // Set placeholder after initialization
43
+ setTimeout(() => {
44
+ const input = document.querySelector('.pagefind-ui__search-input') as HTMLInputElement;
45
+ if (input) input.placeholder = placeholder;
46
+ }, 50);
47
+
48
+ pagefindInitialized = true;
49
+ }
50
+
51
+ function openSearchModal() {
52
+ const modal = document.getElementById('search-modal') as HTMLDialogElement;
53
+ if (modal) {
54
+ modal.showModal();
55
+ initPagefind();
56
+ setTimeout(() => {
57
+ const input = modal.querySelector('.pagefind-ui__search-input') as HTMLInputElement;
58
+ input?.focus();
59
+ }, 100);
60
+ }
61
+ }
62
+
63
+ function closeSearchModal() {
64
+ const modal = document.getElementById('search-modal') as HTMLDialogElement;
65
+ modal?.close();
66
+ }
67
+
68
+ document.addEventListener('click', (e) => {
69
+ const target = e.target as Element;
70
+ if (target.closest('#search-button')) {
71
+ e.preventDefault();
72
+ openSearchModal();
73
+ }
74
+ if (target.closest('#close-search-modal')) {
75
+ closeSearchModal();
76
+ }
77
+ });
78
+
79
+ document.getElementById('search-modal')?.addEventListener('click', (e) => {
80
+ const modal = e.currentTarget as HTMLDialogElement;
81
+ const rect = modal.getBoundingClientRect();
82
+ if (
83
+ e.clientX < rect.left ||
84
+ e.clientX > rect.right ||
85
+ e.clientY < rect.top ||
86
+ e.clientY > rect.bottom
87
+ ) {
88
+ closeSearchModal();
89
+ }
90
+ });
91
+
92
+ (window as any).openSearchModal = openSearchModal;
93
+ </script>
94
+
95
+ <style is:global>
96
+ /* Modal Base */
97
+ .search-modal {
98
+ padding: 0;
99
+ border: none;
100
+ border-radius: 12px;
101
+ max-width: 600px;
102
+ width: 90vw;
103
+ background: transparent;
104
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
105
+ }
106
+
107
+ .search-modal::backdrop {
108
+ background: rgba(0, 0, 0, 0.5);
109
+ backdrop-filter: blur(4px);
110
+ }
111
+
112
+ .modal-content {
113
+ background: var(--c-bg);
114
+ border-radius: 12px;
115
+ overflow: hidden;
116
+ }
117
+
118
+ /* Header */
119
+ .modal-header {
120
+ display: flex;
121
+ justify-content: space-between;
122
+ align-items: center;
123
+ padding: 1rem 1.25rem;
124
+ border-bottom: 1px solid var(--c-divider, rgba(128, 128, 128, 0.2));
125
+ }
126
+
127
+ .modal-title {
128
+ font-size: 1rem;
129
+ font-weight: 600;
130
+ color: var(--c-text-1);
131
+ margin: 0;
132
+ }
133
+
134
+ .close-btn {
135
+ padding: 0.25rem;
136
+ background: transparent;
137
+ border: none;
138
+ color: var(--c-text-2);
139
+ cursor: pointer;
140
+ border-radius: 4px;
141
+ display: flex;
142
+ align-items: center;
143
+ justify-content: center;
144
+ transition: color 0.2s, background 0.2s;
145
+ }
146
+
147
+ .close-btn:hover {
148
+ color: var(--c-text-1);
149
+ background: rgba(128, 128, 128, 0.1);
150
+ }
151
+
152
+ /* Pagefind Container */
153
+ #search-container {
154
+ padding: 0;
155
+ }
156
+
157
+ /* Pagefind UI Overrides */
158
+ .search-modal .pagefind-ui {
159
+ --pagefind-ui-scale: 1;
160
+ --pagefind-ui-primary: var(--c-primary, #3b82f6);
161
+ --pagefind-ui-text: var(--c-text-1);
162
+ --pagefind-ui-background: var(--c-bg);
163
+ --pagefind-ui-border: var(--c-divider, rgba(128, 128, 128, 0.2));
164
+ --pagefind-ui-border-width: 1px;
165
+ --pagefind-ui-border-radius: 8px;
166
+ --pagefind-ui-font: inherit;
167
+ }
168
+
169
+ .search-modal .pagefind-ui__form {
170
+ padding: 1rem 1.25rem;
171
+ border-bottom: 1px solid var(--c-divider, rgba(128, 128, 128, 0.2));
172
+ }
173
+
174
+ .search-modal .pagefind-ui__search-input {
175
+ font-size: 0.95rem !important;
176
+ padding: 0.625rem 1rem !important;
177
+ height: auto !important;
178
+ background: var(--c-bg-soft, rgba(128, 128, 128, 0.05)) !important;
179
+ border: 1px solid var(--c-divider, rgba(128, 128, 128, 0.2)) !important;
180
+ border-radius: 8px !important;
181
+ color: var(--c-text-1) !important;
182
+ transition: border-color 0.2s, box-shadow 0.2s !important;
183
+ }
184
+
185
+ .search-modal .pagefind-ui__search-input:focus {
186
+ outline: none !important;
187
+ border-color: var(--c-primary, #3b82f6) !important;
188
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1) !important;
189
+ }
190
+
191
+ .search-modal .pagefind-ui__search-input::placeholder {
192
+ color: var(--c-text-3, rgba(128, 128, 128, 0.6)) !important;
193
+ }
194
+
195
+ .search-modal .pagefind-ui__search-clear {
196
+ padding: 0.5rem !important;
197
+ right: 0.5rem !important;
198
+ color: var(--c-text-3) !important;
199
+ background: transparent !important;
200
+ }
201
+
202
+ .search-modal .pagefind-ui__search-clear:hover {
203
+ color: var(--c-text-1) !important;
204
+ }
205
+
206
+ /* Results Area */
207
+ .search-modal .pagefind-ui__results-area {
208
+ padding: 0 !important;
209
+ margin: 0 !important;
210
+ }
211
+
212
+ .search-modal .pagefind-ui__message {
213
+ padding: 1rem 1.25rem !important;
214
+ font-size: 0.875rem !important;
215
+ color: var(--c-text-2) !important;
216
+ border-bottom: 1px solid var(--c-divider, rgba(128, 128, 128, 0.2)) !important;
217
+ }
218
+
219
+ .search-modal .pagefind-ui__results {
220
+ max-height: 50vh;
221
+ overflow-y: auto;
222
+ padding: 0 !important;
223
+ }
224
+
225
+ /* Individual Result */
226
+ .search-modal .pagefind-ui__result {
227
+ padding: 1rem 1.25rem !important;
228
+ border-bottom: 1px solid var(--c-divider, rgba(128, 128, 128, 0.1)) !important;
229
+ transition: background 0.15s !important;
230
+ }
231
+
232
+ .search-modal .pagefind-ui__result:hover {
233
+ background: var(--c-bg-soft, rgba(128, 128, 128, 0.05)) !important;
234
+ }
235
+
236
+ .search-modal .pagefind-ui__result:last-child {
237
+ border-bottom: none !important;
238
+ }
239
+
240
+ .search-modal .pagefind-ui__result-inner {
241
+ padding: 0 !important;
242
+ }
243
+
244
+ .search-modal .pagefind-ui__result-title {
245
+ margin-bottom: 0.375rem !important;
246
+ }
247
+
248
+ .search-modal .pagefind-ui__result-link {
249
+ color: var(--c-text-1) !important;
250
+ font-weight: 600 !important;
251
+ font-size: 0.95rem !important;
252
+ text-decoration: none !important;
253
+ transition: color 0.15s !important;
254
+ }
255
+
256
+ .search-modal .pagefind-ui__result-link:hover {
257
+ color: var(--c-primary, #3b82f6) !important;
258
+ }
259
+
260
+ .search-modal .pagefind-ui__result-link::before {
261
+ content: "▸ " !important;
262
+ color: var(--c-text-3) !important;
263
+ }
264
+
265
+ .search-modal .pagefind-ui__result-excerpt {
266
+ font-size: 0.85rem !important;
267
+ line-height: 1.5 !important;
268
+ color: var(--c-text-2) !important;
269
+ margin-top: 0.25rem !important;
270
+ }
271
+
272
+ /* Highlight */
273
+ .search-modal .pagefind-ui__result-excerpt mark,
274
+ .search-modal mark {
275
+ background: rgba(250, 204, 21, 0.4) !important;
276
+ color: inherit !important;
277
+ padding: 0.1em 0.2em !important;
278
+ border-radius: 2px !important;
279
+ }
280
+
281
+ /* Sub Results */
282
+ .search-modal .pagefind-ui__result-nested {
283
+ margin-left: 1rem !important;
284
+ padding-left: 0.75rem !important;
285
+ border-left: 2px solid var(--c-divider, rgba(128, 128, 128, 0.2)) !important;
286
+ margin-top: 0.5rem !important;
287
+ }
288
+
289
+ .search-modal .pagefind-ui__result-nested .pagefind-ui__result-link::before {
290
+ content: "⤷ " !important;
291
+ }
292
+
293
+ /* Button */
294
+ .search-modal .pagefind-ui__button {
295
+ margin: 1rem 1.25rem !important;
296
+ padding: 0.625rem 1rem !important;
297
+ background: var(--c-bg-soft, rgba(128, 128, 128, 0.05)) !important;
298
+ border: 1px solid var(--c-divider, rgba(128, 128, 128, 0.2)) !important;
299
+ border-radius: 8px !important;
300
+ color: var(--c-text-2) !important;
301
+ font-size: 0.875rem !important;
302
+ cursor: pointer !important;
303
+ transition: all 0.15s !important;
304
+ }
305
+
306
+ .search-modal .pagefind-ui__button:hover {
307
+ background: var(--c-bg-mute, rgba(128, 128, 128, 0.1)) !important;
308
+ color: var(--c-text-1) !important;
309
+ }
310
+
311
+ /* Hide default drawer toggle */
312
+ .search-modal .pagefind-ui__drawer {
313
+ display: none !important;
314
+ }
315
+
316
+ /* Loading state */
317
+ .search-modal .pagefind-ui__loading {
318
+ padding: 2rem 1.25rem !important;
319
+ text-align: center !important;
320
+ color: var(--c-text-3) !important;
321
+ }
322
+
323
+ /* Scrollbar styling */
324
+ .search-modal .pagefind-ui__results::-webkit-scrollbar {
325
+ width: 6px;
326
+ }
327
+
328
+ .search-modal .pagefind-ui__results::-webkit-scrollbar-track {
329
+ background: transparent;
330
+ }
331
+
332
+ .search-modal .pagefind-ui__results::-webkit-scrollbar-thumb {
333
+ background: var(--c-divider, rgba(128, 128, 128, 0.3));
334
+ border-radius: 3px;
335
+ }
336
+
337
+ .search-modal .pagefind-ui__results::-webkit-scrollbar-thumb:hover {
338
+ background: var(--c-text-3, rgba(128, 128, 128, 0.5));
339
+ }
340
+ </style>
@@ -0,0 +1,135 @@
1
+ ---
2
+ import type { Language } from '@/i18n/config'
3
+ import { getTagPath } from '@/i18n/path'
4
+
5
+ interface TagWithCount {
6
+ name: string
7
+ count: number
8
+ }
9
+
10
+ interface Props {
11
+ tags: string[] | TagWithCount[]
12
+ currentTag?: string
13
+ lang: Language
14
+ cloudStyle?: boolean
15
+ }
16
+
17
+ const { tags, currentTag = '', lang, cloudStyle = false } = Astro.props
18
+
19
+ // Normalize tags to TagWithCount format
20
+ const normalizedTags: TagWithCount[] = tags.map(tag =>
21
+ typeof tag === 'string' ? { name: tag, count: 1 } : tag
22
+ )
23
+
24
+ // Calculate min/max for scaling
25
+ const counts = normalizedTags.map(t => t.count)
26
+ const maxCount = Math.max(...counts)
27
+ const minCount = Math.min(...counts)
28
+ const countRange = maxCount - minCount || 1
29
+
30
+ // Shuffle tags for cloud style (using seeded random for SSR consistency)
31
+ function seededShuffle<T>(array: T[], seed: number): T[] {
32
+ const result = [...array]
33
+ let currentSeed = seed
34
+ const random = () => {
35
+ currentSeed = (currentSeed * 9301 + 49297) % 233280
36
+ return currentSeed / 233280
37
+ }
38
+ for (let i = result.length - 1; i > 0; i--) {
39
+ const j = Math.floor(random() * (i + 1))
40
+ ;[result[i], result[j]] = [result[j], result[i]]
41
+ }
42
+ return result
43
+ }
44
+
45
+ const displayTags = cloudStyle
46
+ ? seededShuffle(normalizedTags, 42)
47
+ : normalizedTags
48
+
49
+ // Helper to calculate styles based on count ratio
50
+ function getTagStyles(count: number) {
51
+ const ratio = (count - minCount) / countRange
52
+ const fontSize = 0.85 + ratio * 0.5 // 0.85rem to 1.35rem
53
+ const lightness = 55 - ratio * 30 // 55% to 25% (lighter to darker)
54
+ return { fontSize, lightness, ratio }
55
+ }
56
+ ---
57
+
58
+ {cloudStyle ? (
59
+ <div class="no-heti flex flex-wrap gap-3 justify-center items-center">
60
+ {displayTags.map(tag => {
61
+ const { fontSize, lightness } = getTagStyles(tag.count)
62
+ const isActive = currentTag === tag.name
63
+ return (
64
+ <a
65
+ href={getTagPath(tag.name, lang)}
66
+ class="tag-cloud-item"
67
+ style={`--tag-size: ${fontSize}rem; --tag-lightness: ${lightness}%;`}
68
+ data-active={isActive ? 'true' : undefined}
69
+ >
70
+ {tag.name}
71
+ </a>
72
+ )
73
+ })}
74
+ </div>
75
+ ) : (
76
+ <div class="no-heti flex flex-wrap gap-x-3 gap-y-3.2">
77
+ {displayTags.map(tag => (
78
+ <a
79
+ href={getTagPath(tag.name, lang)}
80
+ class:list={[
81
+ 'relative inline-block border border-secondary/25 rounded-full px-3.2 py-0.7 ring-secondary/80',
82
+ 'transition-[colors,box-shadow] ease-out hover:(border-secondary/80 c-primary font-medium ring-0.2)',
83
+ currentTag === tag.name
84
+ ? 'border-secondary/80 c-primary font-medium ring-0.2'
85
+ : 'ring-0',
86
+ ]}
87
+ >
88
+ <span class="absolute inset-0 flex items-center justify-center whitespace-nowrap transition-font-weight">
89
+ {tag.name}
90
+ </span>
91
+ <span
92
+ class="inline-block font-medium opacity-0"
93
+ aria-hidden="true"
94
+ >
95
+ {tag.name}
96
+ </span>
97
+ </a>
98
+ ))}
99
+ </div>
100
+ )}
101
+
102
+ <style>
103
+ .tag-cloud-item {
104
+ display: inline-block;
105
+ padding: 0.5em 1em;
106
+ font-size: var(--tag-size);
107
+ color: oklch(var(--tag-lightness) 0.005 298);
108
+ border: 1px solid oklch(var(--tag-lightness) 0.005 298 / 0.4);
109
+ border-radius: 1.5em;
110
+ text-decoration: none;
111
+ transition: all 0.2s ease;
112
+ white-space: nowrap;
113
+ }
114
+
115
+ .tag-cloud-item:hover {
116
+ transform: scale(1.08);
117
+ box-shadow: 0 2px 8px oklch(25% 0.005 298 / 0.15);
118
+ border-color: oklch(var(--tag-lightness) 0.005 298 / 0.8);
119
+ }
120
+
121
+ .tag-cloud-item[data-active="true"] {
122
+ border-color: oklch(var(--tag-lightness) 0.005 298 / 0.8);
123
+ font-weight: 500;
124
+ }
125
+
126
+ :global(.dark) .tag-cloud-item {
127
+ color: oklch(calc(100% - var(--tag-lightness) + 15%) 0.005 298);
128
+ border-color: oklch(calc(100% - var(--tag-lightness) + 15%) 0.005 298 / 0.4);
129
+ }
130
+
131
+ :global(.dark) .tag-cloud-item:hover {
132
+ box-shadow: 0 2px 8px oklch(92% 0.005 298 / 0.15);
133
+ border-color: oklch(calc(100% - var(--tag-lightness) + 15%) 0.005 298 / 0.8);
134
+ }
135
+ </style>
@@ -0,0 +1,43 @@
1
+ ---
2
+ import GoBackIcon from '@/assets/icons/go-back.svg';
3
+ ---
4
+
5
+ <button
6
+ id="back-button"
7
+ class="hidden"
8
+ lg="absolute left--10 top-3.8 block aspect-square w-4.5 c-secondary/60 transition-colors ease-out hover:c-primary active:scale-90!"
9
+ type="button"
10
+ aria-label="Go back"
11
+ >
12
+ <GoBackIcon
13
+ aria-hidden="true"
14
+ fill="currentColor"
15
+ />
16
+ </button>
17
+
18
+ <!-- Go Back Script >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> -->
19
+ <script>
20
+ function handleBackButtonClick(e: MouseEvent) {
21
+ if (!(e.target instanceof Element)) {
22
+ return
23
+ }
24
+
25
+ if (!e.target.closest('#back-button')) {
26
+ return
27
+ }
28
+
29
+ // Navigate back if history exists
30
+ if (window.history.length > 1) {
31
+ window.history.back()
32
+ return
33
+ }
34
+
35
+ // Fallback to homepage
36
+ const siteTitleLink = document.getElementById('site-title-link')
37
+ if (siteTitleLink) {
38
+ siteTitleLink.click()
39
+ }
40
+ }
41
+
42
+ document.addEventListener('click', handleBackButtonClick)
43
+ </script>