methanol 0.0.14 → 0.0.16
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/index.js +1 -0
- package/package.json +1 -1
- package/src/build-system.js +1 -0
- package/src/client/virtual-module/assets.js +7 -6
- package/src/config.js +33 -3
- package/src/dev-server.js +68 -26
- package/src/error-page.jsx +49 -0
- package/src/mdx.js +235 -8
- package/src/pages-index.js +42 -0
- package/src/pages.js +1 -0
- package/src/reframe.js +1 -1
- package/src/state.js +8 -0
- package/src/text-utils.js +60 -0
- package/src/vite-plugins.js +12 -27
- package/src/workers/build-pool.js +1 -0
- package/themes/blog/README.md +26 -0
- package/themes/blog/components/CategoryView.client.jsx +164 -0
- package/themes/blog/components/CategoryView.static.jsx +35 -0
- package/themes/blog/components/CollectionView.client.jsx +151 -0
- package/themes/blog/components/CollectionView.static.jsx +37 -0
- package/themes/blog/components/PostList.client.jsx +92 -0
- package/themes/blog/components/PostList.static.jsx +36 -0
- package/themes/blog/components/ThemeSearchBox.client.jsx +427 -0
- package/themes/blog/components/ThemeSearchBox.static.jsx +40 -0
- package/themes/blog/index.js +40 -0
- package/themes/blog/pages/404.mdx +12 -0
- package/themes/blog/pages/about.mdx +14 -0
- package/themes/blog/pages/categories.mdx +6 -0
- package/themes/blog/pages/collections.mdx +6 -0
- package/themes/blog/pages/index.mdx +16 -0
- package/themes/blog/pages/offline.mdx +11 -0
- package/themes/blog/sources/style.css +579 -0
- package/themes/blog/src/date-utils.js +28 -0
- package/themes/blog/src/heading.jsx +37 -0
- package/themes/blog/src/layout-categories.jsx +65 -0
- package/themes/blog/src/layout-collections.jsx +64 -0
- package/themes/blog/src/layout-home.jsx +65 -0
- package/themes/blog/src/layout-post.jsx +40 -0
- package/themes/blog/src/page.jsx +152 -0
- package/themes/blog/src/post-utils.js +83 -0
- package/themes/default/src/page.jsx +2 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { signal, $, For, If } from 'refui'
|
|
22
|
+
import pages from 'methanol:pages'
|
|
23
|
+
import { formatDate } from '../src/date-utils.js'
|
|
24
|
+
|
|
25
|
+
const rawPages = Array.isArray(pages) ? pages : []
|
|
26
|
+
const isPostPage = (p) => {
|
|
27
|
+
if (!p?.routeHref) return false
|
|
28
|
+
if (p.routeHref === '/' || p.routeHref === '/404' || p.routeHref === '/offline') return false
|
|
29
|
+
if (p.routeHref.startsWith('/.methanol')) return false
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
const resolvePosts = () => {
|
|
33
|
+
const list = rawPages.filter(isPostPage)
|
|
34
|
+
list.sort((a, b) => {
|
|
35
|
+
const dateA = new Date(a.frontmatter?.date || a.stats?.createdAt || 0)
|
|
36
|
+
const dateB = new Date(b.frontmatter?.date || b.stats?.createdAt || 0)
|
|
37
|
+
return dateB - dateA
|
|
38
|
+
})
|
|
39
|
+
return list.map((p) => ({
|
|
40
|
+
title: p.title,
|
|
41
|
+
routeHref: p.routeHref,
|
|
42
|
+
frontmatter: p.frontmatter || {},
|
|
43
|
+
stats: p.stats || {},
|
|
44
|
+
excerpt: p.excerpt || p.frontmatter?.excerpt || ''
|
|
45
|
+
}))
|
|
46
|
+
}
|
|
47
|
+
const allPosts = resolvePosts()
|
|
48
|
+
|
|
49
|
+
export default function ({ initialCount = 10 } = {}) {
|
|
50
|
+
const visibleCount = signal(initialCount)
|
|
51
|
+
const visiblePosts = $(() => allPosts.slice(0, visibleCount.value))
|
|
52
|
+
const hasMore = $(() => visibleCount.value < allPosts.length)
|
|
53
|
+
const showEmpty = $(() => allPosts.length === 0)
|
|
54
|
+
|
|
55
|
+
const loadMore = () => {
|
|
56
|
+
visibleCount.value += initialCount
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div class="post-list-container">
|
|
61
|
+
<div class="post-list">
|
|
62
|
+
<For entries={visiblePosts}>
|
|
63
|
+
{({ item: p }) => {
|
|
64
|
+
const dateStr = formatDate(p.frontmatter?.date || p.stats?.createdAt)
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<article class="post-item">
|
|
68
|
+
<div class="post-meta">{dateStr && <span class="post-date">{dateStr}</span>}</div>
|
|
69
|
+
<h2 class="post-item-title">
|
|
70
|
+
<a href={p.routeHref}>{p.title || 'Untitled'}</a>
|
|
71
|
+
</h2>
|
|
72
|
+
<div class="post-excerpt">{p.excerpt || p.frontmatter.excerpt || 'No excerpt available.'}</div>
|
|
73
|
+
</article>
|
|
74
|
+
)
|
|
75
|
+
}}
|
|
76
|
+
</For>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<If condition={showEmpty}>{() => <p>No posts found.</p>}</If>
|
|
80
|
+
|
|
81
|
+
<If condition={hasMore}>
|
|
82
|
+
{() => (
|
|
83
|
+
<div class="pagination-container">
|
|
84
|
+
<button class="load-more-btn" on:click={loadMore}>
|
|
85
|
+
Load More
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
</If>
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export default function ({ hasMore = false } = {}, ...children) {
|
|
22
|
+
if (!children.length) return null
|
|
23
|
+
return (
|
|
24
|
+
<div class="post-list-container">
|
|
25
|
+
<div class="post-list">{...children}</div>
|
|
26
|
+
|
|
27
|
+
{hasMore && (
|
|
28
|
+
<div class="pagination-container">
|
|
29
|
+
<button class="load-more-btn" disabled>
|
|
30
|
+
Load More
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { signal, $, t, If, For, onCondition } from 'refui'
|
|
22
|
+
import { createPortal } from 'refui/extras'
|
|
23
|
+
|
|
24
|
+
let pagefindModule = null
|
|
25
|
+
const loadPagefindModule = async () => {
|
|
26
|
+
if (pagefindModule) return pagefindModule
|
|
27
|
+
pagefindModule = import('methanol:pagefind-loader')
|
|
28
|
+
return pagefindModule
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let keybindReady = false
|
|
32
|
+
let cachedPagefind = null
|
|
33
|
+
const PAGE_SIZE = 10
|
|
34
|
+
|
|
35
|
+
const resolveShortcutLabel = () => {
|
|
36
|
+
if (typeof navigator === 'undefined') return 'Ctrl+K'
|
|
37
|
+
const platform = navigator.platform || ''
|
|
38
|
+
const agent = navigator.userAgent || ''
|
|
39
|
+
const isMac = /Mac|iPhone|iPad|iPod/.test(platform) || /Mac OS X/.test(agent)
|
|
40
|
+
return isMac ? '⌘K' : 'Ctrl+K'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ensurePagefind = async (options) => {
|
|
44
|
+
if (cachedPagefind) return cachedPagefind
|
|
45
|
+
const module = await loadPagefindModule()
|
|
46
|
+
const pagefind = await module?.loadPagefind?.()
|
|
47
|
+
if (!pagefind) return null
|
|
48
|
+
if (pagefind.options) {
|
|
49
|
+
const nextOptions = { excerptLength: 30, ...(options || {}) }
|
|
50
|
+
await pagefind.options(nextOptions)
|
|
51
|
+
}
|
|
52
|
+
if (pagefind.init) {
|
|
53
|
+
await pagefind.init()
|
|
54
|
+
}
|
|
55
|
+
cachedPagefind = pagefind
|
|
56
|
+
return pagefind
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default function ({ options } = {}) {
|
|
60
|
+
const isOpen = signal(false)
|
|
61
|
+
const query = signal('')
|
|
62
|
+
const results = signal([])
|
|
63
|
+
const isLoading = signal(false)
|
|
64
|
+
const isLoadingMore = signal(false)
|
|
65
|
+
const hasMore = signal(false)
|
|
66
|
+
const activeIndex = signal(-1)
|
|
67
|
+
const loadError = signal('')
|
|
68
|
+
|
|
69
|
+
const buttonRef = signal()
|
|
70
|
+
const inputRef = signal()
|
|
71
|
+
const resultsRef = signal()
|
|
72
|
+
const loadingMoreRef = signal()
|
|
73
|
+
const resultIdPrefix = `search-result-${Math.random().toString(36).slice(2)}`
|
|
74
|
+
const activeMatch = onCondition(activeIndex)
|
|
75
|
+
|
|
76
|
+
let debounceTimer = null
|
|
77
|
+
let resultHandles = []
|
|
78
|
+
let resultOffset = 0
|
|
79
|
+
let latestSearchId = 0
|
|
80
|
+
const shortcutLabel = resolveShortcutLabel()
|
|
81
|
+
const [Inlet, Outlet] = createPortal()
|
|
82
|
+
|
|
83
|
+
const resetSearchState = () => {
|
|
84
|
+
resultHandles = []
|
|
85
|
+
resultOffset = 0
|
|
86
|
+
hasMore.value = false
|
|
87
|
+
isLoadingMore.value = false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const loadMore = async (initial = false) => {
|
|
91
|
+
const searchId = latestSearchId
|
|
92
|
+
if (!initial) {
|
|
93
|
+
if (isLoadingMore.value || !hasMore.value) return
|
|
94
|
+
isLoadingMore.value = true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const slice = resultHandles.slice(resultOffset, resultOffset + PAGE_SIZE)
|
|
98
|
+
if (!slice.length) {
|
|
99
|
+
hasMore.value = false
|
|
100
|
+
if (!initial) isLoadingMore.value = false
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const data = await Promise.all(slice.map((r) => r.data()))
|
|
106
|
+
if (searchId !== latestSearchId) return
|
|
107
|
+
|
|
108
|
+
results.value = results.value.concat(data.map((value) => ({ value, el: signal() })))
|
|
109
|
+
resultOffset += slice.length
|
|
110
|
+
hasMore.value = resultOffset < resultHandles.length
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (searchId !== latestSearchId) return
|
|
113
|
+
loadError.value = 'Search is unavailable. Please refresh and try again.'
|
|
114
|
+
console.error('Search error:', err)
|
|
115
|
+
} finally {
|
|
116
|
+
if (!initial && searchId === latestSearchId) isLoadingMore.value = false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const search = async (q) => {
|
|
121
|
+
const searchId = ++latestSearchId
|
|
122
|
+
isLoading.value = true
|
|
123
|
+
results.value = []
|
|
124
|
+
activeIndex.value = -1
|
|
125
|
+
resetSearchState()
|
|
126
|
+
|
|
127
|
+
const pagefind = await ensurePagefind(options)
|
|
128
|
+
if (searchId !== latestSearchId) return
|
|
129
|
+
|
|
130
|
+
if (!pagefind) {
|
|
131
|
+
isLoading.value = false
|
|
132
|
+
loadError.value = 'Search is unavailable. Please refresh and try again.'
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
loadError.value = ''
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const searchResult = await pagefind.search(q)
|
|
139
|
+
if (searchId !== latestSearchId) return
|
|
140
|
+
|
|
141
|
+
resultHandles = searchResult?.results || []
|
|
142
|
+
resultOffset = 0
|
|
143
|
+
hasMore.value = resultHandles.length > 0
|
|
144
|
+
await loadMore(true)
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (searchId !== latestSearchId) return
|
|
147
|
+
loadError.value = 'Search is unavailable. Please refresh and try again.'
|
|
148
|
+
console.error('Search error:', err)
|
|
149
|
+
} finally {
|
|
150
|
+
if (searchId === latestSearchId) isLoading.value = false
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const onInput = (event) => {
|
|
155
|
+
const value = event.target.value
|
|
156
|
+
query.value = value
|
|
157
|
+
loadError.value = ''
|
|
158
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
159
|
+
|
|
160
|
+
if (!value.trim()) {
|
|
161
|
+
latestSearchId++
|
|
162
|
+
isLoading.value = false
|
|
163
|
+
results.value = []
|
|
164
|
+
resetSearchState()
|
|
165
|
+
activeIndex.value = -1
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
debounceTimer = setTimeout(() => {
|
|
170
|
+
search(value)
|
|
171
|
+
}, 300)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const focusInput = () => {
|
|
175
|
+
if (inputRef.value) inputRef.value.focus()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const open = async () => {
|
|
179
|
+
isOpen.value = true
|
|
180
|
+
setTimeout(focusInput, 50)
|
|
181
|
+
const pagefind = await ensurePagefind(options)
|
|
182
|
+
if (!pagefind) {
|
|
183
|
+
loadError.value = 'Search is unavailable. Please refresh and try again.'
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const close = () => {
|
|
188
|
+
isOpen.value = false
|
|
189
|
+
query.value = ''
|
|
190
|
+
results.value = []
|
|
191
|
+
loadError.value = ''
|
|
192
|
+
resetSearchState()
|
|
193
|
+
activeIndex.value = -1
|
|
194
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
195
|
+
if (inputRef.value) inputRef.value.blur()
|
|
196
|
+
if (buttonRef.value) buttonRef.value.focus()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const scrollActiveIntoView = () => {
|
|
200
|
+
setTimeout(() => {
|
|
201
|
+
const activeEl = results.value[activeIndex.value]?.el.value
|
|
202
|
+
if (activeEl) {
|
|
203
|
+
activeEl.scrollIntoView({ block: 'nearest' })
|
|
204
|
+
}
|
|
205
|
+
}, 0)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const onKeyDown = (event) => {
|
|
209
|
+
if (event.key === 'Escape') {
|
|
210
|
+
event.preventDefault()
|
|
211
|
+
close()
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (event.key === 'ArrowDown') {
|
|
216
|
+
event.preventDefault()
|
|
217
|
+
if (results.value.length > 0) {
|
|
218
|
+
if (hasMore.value && activeIndex.value === results.value.length - 1) {
|
|
219
|
+
loadMore(false)
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
loadingMoreRef.value?.scrollIntoView({ block: 'nearest' })
|
|
222
|
+
}, 10)
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
const nextIndex = activeIndex.value >= 0 ? (activeIndex.value + 1) % results.value.length : 0
|
|
226
|
+
activeIndex.value = nextIndex
|
|
227
|
+
scrollActiveIntoView()
|
|
228
|
+
}
|
|
229
|
+
} else if (event.key === 'ArrowUp') {
|
|
230
|
+
event.preventDefault()
|
|
231
|
+
if (results.value.length > 0) {
|
|
232
|
+
const nextIndex = activeIndex.value > 0 ? activeIndex.value - 1 : results.value.length - 1
|
|
233
|
+
activeIndex.value = nextIndex
|
|
234
|
+
scrollActiveIntoView()
|
|
235
|
+
}
|
|
236
|
+
} else if (event.key === 'Enter') {
|
|
237
|
+
event.preventDefault()
|
|
238
|
+
const selected = results.value[activeIndex.value]?.value
|
|
239
|
+
const fallback = results.value[0]?.value
|
|
240
|
+
const target = selected || fallback
|
|
241
|
+
if (target?.url) {
|
|
242
|
+
window.location.href = target.url
|
|
243
|
+
close()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const onResultKeyDown = (event, indexValue) => {
|
|
249
|
+
if (event.key === 'Escape') {
|
|
250
|
+
event.preventDefault()
|
|
251
|
+
close()
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
if (event.key === 'ArrowDown') {
|
|
255
|
+
event.preventDefault()
|
|
256
|
+
if (hasMore.value && indexValue === results.value.length - 1) {
|
|
257
|
+
loadMore(false)
|
|
258
|
+
setTimeout(() => {
|
|
259
|
+
loadingMoreRef.value?.scrollIntoView({ block: 'nearest' })
|
|
260
|
+
}, 10)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
const nextIndex = (indexValue + 1) % results.value.length
|
|
264
|
+
activeIndex.value = nextIndex
|
|
265
|
+
scrollActiveIntoView()
|
|
266
|
+
} else if (event.key === 'ArrowUp') {
|
|
267
|
+
event.preventDefault()
|
|
268
|
+
if (indexValue === 0) {
|
|
269
|
+
activeIndex.value = -1
|
|
270
|
+
focusInput()
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
const nextIndex = indexValue - 1
|
|
274
|
+
activeIndex.value = nextIndex
|
|
275
|
+
scrollActiveIntoView()
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const onResultsScroll = (event) => {
|
|
280
|
+
const el = event.currentTarget
|
|
281
|
+
if (!el) return
|
|
282
|
+
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 24
|
|
283
|
+
if (nearBottom) loadMore(false)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const showEmpty = $(() => !query.value)
|
|
287
|
+
const showNoResults = $(() => {
|
|
288
|
+
const _query = query.value
|
|
289
|
+
const _isLoading = isLoading.value
|
|
290
|
+
const _length = results.value.length
|
|
291
|
+
return _query && !_isLoading && _length === 0
|
|
292
|
+
})
|
|
293
|
+
const showError = $(() => loadError.value)
|
|
294
|
+
const showStatus = $(() => !loadError.value)
|
|
295
|
+
const showLoadingMore = $(() => isLoadingMore.value && !isLoading.value)
|
|
296
|
+
|
|
297
|
+
if (typeof window !== 'undefined') {
|
|
298
|
+
window.__methanolSearchOpen = open
|
|
299
|
+
window.__methanolSearchClose = close
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (typeof window !== 'undefined' && !keybindReady) {
|
|
303
|
+
keybindReady = true
|
|
304
|
+
window.addEventListener('keydown', (event) => {
|
|
305
|
+
const key = event.key?.toLowerCase?.()
|
|
306
|
+
if ((event.metaKey || event.ctrlKey) && key === 'k') {
|
|
307
|
+
event.preventDefault()
|
|
308
|
+
if (isOpen.value) {
|
|
309
|
+
close()
|
|
310
|
+
} else {
|
|
311
|
+
open()
|
|
312
|
+
}
|
|
313
|
+
} else if (key === 'escape' && isOpen.value) {
|
|
314
|
+
close()
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return (R) => {
|
|
320
|
+
R.render(document.body, Outlet)
|
|
321
|
+
return (
|
|
322
|
+
<button class="search-box" type="button" on:click={open} attr:aria-label="Open search" $ref={buttonRef}>
|
|
323
|
+
<svg
|
|
324
|
+
attr:width="16"
|
|
325
|
+
attr:height="16"
|
|
326
|
+
attr:viewBox="0 0 24 24"
|
|
327
|
+
attr:fill="none"
|
|
328
|
+
attr:stroke="currentColor"
|
|
329
|
+
attr:stroke-width="2"
|
|
330
|
+
attr:stroke-linecap="round"
|
|
331
|
+
attr:stroke-linejoin="round"
|
|
332
|
+
>
|
|
333
|
+
<circle attr:cx="11" attr:cy="11" attr:r="8"></circle>
|
|
334
|
+
<path attr:d="m21 21-4.3-4.3"></path>
|
|
335
|
+
</svg>
|
|
336
|
+
<span>Search</span>
|
|
337
|
+
<kbd>{shortcutLabel}</kbd>
|
|
338
|
+
<Inlet>
|
|
339
|
+
<div class="search-modal" class:open={isOpen} attr:inert={$(() => (isOpen.value ? null : ''))}>
|
|
340
|
+
<div class="search-modal__scrim" on:click={close}></div>
|
|
341
|
+
<div class="search-modal__panel">
|
|
342
|
+
<div class="search-input-wrapper">
|
|
343
|
+
<svg
|
|
344
|
+
attr:width="20"
|
|
345
|
+
attr:height="20"
|
|
346
|
+
attr:viewBox="0 0 24 24"
|
|
347
|
+
attr:fill="none"
|
|
348
|
+
attr:stroke="currentColor"
|
|
349
|
+
attr:stroke-width="2"
|
|
350
|
+
attr:stroke-linecap="round"
|
|
351
|
+
attr:stroke-linejoin="round"
|
|
352
|
+
>
|
|
353
|
+
<circle attr:cx="11" attr:cy="11" attr:r="8"></circle>
|
|
354
|
+
<path attr:d="m21 21-4.3-4.3"></path>
|
|
355
|
+
</svg>
|
|
356
|
+
<input
|
|
357
|
+
class="search-input"
|
|
358
|
+
type="text"
|
|
359
|
+
placeholder="Search posts..."
|
|
360
|
+
value={query}
|
|
361
|
+
on:input={onInput}
|
|
362
|
+
on:keydown={onKeyDown}
|
|
363
|
+
attr:aria-activedescendant={$(() =>
|
|
364
|
+
activeIndex.value >= 0 ? `${resultIdPrefix}-${activeIndex.value}` : null
|
|
365
|
+
)}
|
|
366
|
+
attr:autocomplete="off"
|
|
367
|
+
attr:autocorrect="off"
|
|
368
|
+
attr:spellcheck="false"
|
|
369
|
+
$ref={inputRef}
|
|
370
|
+
/>
|
|
371
|
+
</div>
|
|
372
|
+
<div class="search-results" on:scroll={onResultsScroll} $ref={resultsRef}>
|
|
373
|
+
<If condition={showError}>{() => <div class="search-status">{loadError}</div>}</If>
|
|
374
|
+
<If condition={showStatus}>
|
|
375
|
+
{() => (
|
|
376
|
+
<>
|
|
377
|
+
<If condition={showEmpty}>{() => <div class="search-status">Type to start searching...</div>}</If>
|
|
378
|
+
<If condition={showNoResults}>
|
|
379
|
+
{() => <div class="search-status">No results found for "{query}"</div>}
|
|
380
|
+
</If>
|
|
381
|
+
<If condition={isLoading}>{() => <div class="search-status">Searching...</div>}</If>
|
|
382
|
+
</>
|
|
383
|
+
)}
|
|
384
|
+
</If>
|
|
385
|
+
<For entries={results} indexed>
|
|
386
|
+
{({ item: { value, el }, index }) => (
|
|
387
|
+
<a
|
|
388
|
+
class="search-result-item"
|
|
389
|
+
class:active={activeMatch(index)}
|
|
390
|
+
href={value.url}
|
|
391
|
+
on:click={close}
|
|
392
|
+
on:keydown={(event) => onResultKeyDown(event, index.value)}
|
|
393
|
+
on:focus={() => {
|
|
394
|
+
activeIndex.value = index.value
|
|
395
|
+
}}
|
|
396
|
+
attr:aria-selected={$(() => (activeIndex.value === index.value ? 'true' : 'false'))}
|
|
397
|
+
attr:id={t`${resultIdPrefix}-${index.value}`}
|
|
398
|
+
$ref={el}
|
|
399
|
+
>
|
|
400
|
+
<div class="search-result-title">
|
|
401
|
+
{value?.meta?.title || value?.title || value?.url}
|
|
402
|
+
</div>
|
|
403
|
+
<div class="search-result-excerpt" innerHTML={value.excerpt || ''}></div>
|
|
404
|
+
</a>
|
|
405
|
+
)}
|
|
406
|
+
</For>
|
|
407
|
+
<If condition={showLoadingMore}>
|
|
408
|
+
{() => (
|
|
409
|
+
<div
|
|
410
|
+
class="search-status"
|
|
411
|
+
$ref={(el) => {
|
|
412
|
+
loadingMoreRef.value = el
|
|
413
|
+
el.scrollIntoView({ block: 'nearest' })
|
|
414
|
+
}}
|
|
415
|
+
>
|
|
416
|
+
Loading more results...
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</If>{' '}
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</Inlet>
|
|
424
|
+
</button>
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export default function () {
|
|
22
|
+
return (
|
|
23
|
+
<button class="search-box" type="button" aria-label="Open search">
|
|
24
|
+
<svg
|
|
25
|
+
width="16"
|
|
26
|
+
height="16"
|
|
27
|
+
viewBox="0 0 24 24"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
stroke-width="2"
|
|
31
|
+
stroke-linecap="round"
|
|
32
|
+
stroke-linejoin="round"
|
|
33
|
+
>
|
|
34
|
+
<circle cx="11" cy="11" r="8"></circle>
|
|
35
|
+
<path d="m21 21-4.3-4.3"></path>
|
|
36
|
+
</svg>
|
|
37
|
+
<span>Search</span>
|
|
38
|
+
</button>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* Copyright Yukino Song, SudoMaker Ltd.
|
|
2
|
+
*
|
|
3
|
+
* Licensed to the Apache Software Foundation (ASF) under one
|
|
4
|
+
* or more contributor license agreements. See the NOTICE file
|
|
5
|
+
* distributed with this work for additional information
|
|
6
|
+
* regarding copyright ownership. The ASF licenses this file
|
|
7
|
+
* to you under the Apache License, Version 2.0 (the
|
|
8
|
+
* "License"); you may not use this file except in compliance
|
|
9
|
+
* with the License. You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing,
|
|
14
|
+
* software distributed under the License is distributed on an
|
|
15
|
+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
16
|
+
* KIND, either express or implied. See the License for the
|
|
17
|
+
* specific language governing permissions and limitations
|
|
18
|
+
* under the License.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { fileURLToPath } from 'url'
|
|
22
|
+
import { dirname } from 'path'
|
|
23
|
+
import PAGE_TEMPLATE from './src/page.jsx'
|
|
24
|
+
import { createHeadings } from './src/heading.jsx'
|
|
25
|
+
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
27
|
+
const __dirname = dirname(__filename)
|
|
28
|
+
|
|
29
|
+
export default () => {
|
|
30
|
+
return {
|
|
31
|
+
root: __dirname,
|
|
32
|
+
sources: {
|
|
33
|
+
'/.methanol_theme_blog': './sources'
|
|
34
|
+
},
|
|
35
|
+
components: {
|
|
36
|
+
...createHeadings()
|
|
37
|
+
},
|
|
38
|
+
template: PAGE_TEMPLATE
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: About
|
|
3
|
+
hidden: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## About Me
|
|
7
|
+
|
|
8
|
+
Welcome to my blog! I'm a developer using **Methanol** to build static sites.
|
|
9
|
+
|
|
10
|
+
This is the default about page. You can customize it by overriding it in your project's `pages/about.mdx`.
|
|
11
|
+
|
|
12
|
+
### Contact
|
|
13
|
+
|
|
14
|
+
Feel free to reach out via [GitHub](https://github.com/SudoMaker/Methanol).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Home
|
|
3
|
+
excerpt: A simple blog theme for Methanol.
|
|
4
|
+
hidden: true
|
|
5
|
+
navLinks:
|
|
6
|
+
- label: Home
|
|
7
|
+
href: /
|
|
8
|
+
- label: About
|
|
9
|
+
href: /about
|
|
10
|
+
- label: Categories
|
|
11
|
+
href: /categories
|
|
12
|
+
- label: Collections
|
|
13
|
+
href: /collections
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Welcome to my blog. Here are my latest posts.
|