methanol 0.0.1 → 0.0.3

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/src/state.js CHANGED
@@ -101,6 +101,19 @@ const withCommonOptions = (y) =>
101
101
  type: 'boolean',
102
102
  default: false
103
103
  })
104
+ .option('code-highlighting', {
105
+ describe: 'Enable or disable code highlighting',
106
+ type: 'string',
107
+ coerce: (value) => {
108
+ if (value == null) return null
109
+ if (value === true || value === '') return true
110
+ if (typeof value === 'boolean') return value
111
+ const normalized = String(value).trim().toLowerCase()
112
+ if (normalized === 'true') return true
113
+ if (normalized === 'false') return false
114
+ return null
115
+ }
116
+ })
104
117
 
105
118
  const parser = yargs(hideBin(process.argv))
106
119
  .scriptName('methanol')
@@ -127,7 +140,8 @@ export const cli = {
127
140
  CLI_ASSETS_DIR: argv.assets || null,
128
141
  CLI_OUTPUT_DIR: argv.output || null,
129
142
  CLI_CONFIG_PATH: argv.config || null,
130
- CLI_SITE_NAME: argv['site-name'] || null
143
+ CLI_SITE_NAME: argv['site-name'] || null,
144
+ CLI_CODE_HIGHLIGHTING: typeof argv['code-highlighting'] === 'boolean' ? argv['code-highlighting'] : null
131
145
  }
132
146
 
133
147
  export const state = {
@@ -149,10 +163,20 @@ export const state = {
149
163
  USER_VITE_CONFIG: null,
150
164
  USER_MDX_CONFIG: null,
151
165
  USER_PUBLIC_OVERRIDE: false,
152
- RESOURCES: [],
166
+ SOURCES: [],
153
167
  PAGEFIND_ENABLED: false,
154
168
  PAGEFIND_OPTIONS: null,
155
- PAGEFIND_BUILD_OPTIONS: null,
169
+ PAGEFIND_BUILD: null,
170
+ USER_PRE_BUILD_HOOKS: [],
171
+ USER_POST_BUILD_HOOKS: [],
172
+ USER_PRE_BUNDLE_HOOKS: [],
173
+ USER_POST_BUNDLE_HOOKS: [],
174
+ THEME_PRE_BUILD_HOOKS: [],
175
+ THEME_POST_BUILD_HOOKS: [],
176
+ THEME_PRE_BUNDLE_HOOKS: [],
177
+ THEME_POST_BUNDLE_HOOKS: [],
178
+ STARRY_NIGHT_ENABLED: false,
179
+ STARRY_NIGHT_OPTIONS: null,
156
180
  CURRENT_MODE: 'production',
157
181
  RESOLVED_MDX_CONFIG: undefined,
158
182
  RESOLVED_VITE_CONFIG: undefined
@@ -146,9 +146,9 @@ export const methanolResolverPlugin = () => {
146
146
  }
147
147
  }
148
148
 
149
- if (state.RESOURCES.length) {
149
+ if (state.SOURCES.length) {
150
150
  const { pathname, search } = new URL(id, 'http://methanol')
151
- for (const entry of state.RESOURCES) {
151
+ for (const entry of state.SOURCES) {
152
152
  const { find, replacement } = entry
153
153
  if (!find || !replacement) continue
154
154
  if (typeof find === 'string') {
@@ -0,0 +1,95 @@
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, $ } from 'refui'
22
+
23
+ const ACCENTS = [
24
+ { id: 'default', label: 'Amber', color: '#ffa000' },
25
+ { id: 'rose', label: 'Rose', color: '#f43f5e' },
26
+ { id: 'blue', label: 'Indigo', color: '#818cf8' },
27
+ { id: 'green', label: 'Teal', color: '#2dd4bf' },
28
+ { id: 'purple', label: 'Violet', color: '#a78bfa' }
29
+ ]
30
+
31
+ export default function () {
32
+ const currentAccent = signal('default')
33
+ const isOpen = signal(false)
34
+
35
+ // Initialize theme from localStorage
36
+ if (typeof window !== 'undefined') {
37
+ const saved = localStorage.getItem('methanol-accent')
38
+ if (saved && ACCENTS.some((a) => a.id === saved)) {
39
+ currentAccent.value = saved
40
+ if (saved !== 'default') {
41
+ document.documentElement.classList.add(`accent-${saved}`)
42
+ }
43
+ }
44
+
45
+ // Close popup when clicking outside
46
+ document.addEventListener('click', (e) => {
47
+ if (!e.target.closest('.theme-switch-wrapper')) {
48
+ isOpen.value = false
49
+ }
50
+ })
51
+ }
52
+
53
+ const setAccent = (id) => {
54
+ const oldId = currentAccent.value
55
+
56
+ // Remove old
57
+ if (oldId !== 'default') {
58
+ document.documentElement.classList.remove(`accent-${oldId}`)
59
+ }
60
+
61
+ // Add new
62
+ if (id !== 'default') {
63
+ document.documentElement.classList.add(`accent-${id}`)
64
+ }
65
+
66
+ currentAccent.value = id
67
+ localStorage.setItem('methanol-accent', id)
68
+ isOpen.value = false
69
+ }
70
+
71
+ const togglePopup = () => {
72
+ isOpen.value = !isOpen.value
73
+ }
74
+
75
+ return (
76
+ <div class="theme-switch-container">
77
+ <div class="theme-switch-wrapper">
78
+ <div class={$(() => `accent-popup ${isOpen.value ? 'open' : ''}`)}>
79
+ {ACCENTS.map((accent) => (
80
+ <button
81
+ class={$(() => `accent-option ${currentAccent.value === accent.id ? 'active' : ''}`)}
82
+ on:click={() => setAccent(accent.id)}
83
+ >
84
+ <span class="option-circle" style={`background-color: ${accent.color}`}></span>
85
+ {accent.label}
86
+ </button>
87
+ ))}
88
+ </div>
89
+ <button class="theme-switch-btn" on:click={togglePopup} attr:aria-label="Select accent color">
90
+ <div class="accent-circle"></div>
91
+ </button>
92
+ </div>
93
+ </div>
94
+ )
95
+ }
@@ -0,0 +1,23 @@
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
+ // render nothing on server side
23
+ }
@@ -87,7 +87,7 @@ export default function () {
87
87
 
88
88
  return (
89
89
  <div class="theme-switch-container">
90
- <button class="theme-switch-btn" on:click={toggle} aria-label="Toggle theme">
90
+ <button class="theme-switch-btn" on:click={toggle} attr:aria-label="Toggle theme">
91
91
  <CurrentIcon />
92
92
  </button>
93
93
  </div>
@@ -18,7 +18,7 @@
18
18
  * under the License.
19
19
  */
20
20
 
21
- import { signal, $, If, For } from 'refui'
21
+ import { signal, $, t, If, For, onCondition } from 'refui'
22
22
  import { createPortal } from 'refui/extras'
23
23
  import { loadPagefind } from '/.methanol_virtual_module/pagefind.js'
24
24
 
@@ -55,7 +55,10 @@ export default function ({ options } = {}) {
55
55
  const isLoading = signal(false)
56
56
  const activeIndex = signal(-1)
57
57
 
58
+ const buttonRef = signal()
58
59
  const inputRef = signal()
60
+ const resultIdPrefix = `search-result-${Math.random().toString(36).slice(2)}`
61
+ const activeMatch = onCondition(activeIndex)
59
62
 
60
63
  let debounceTimer = null
61
64
  const shortcutLabel = resolveShortcutLabel()
@@ -76,7 +79,7 @@ export default function ({ options } = {}) {
76
79
  const searchResult = await pagefind.search(q)
77
80
  if (searchResult?.results?.length) {
78
81
  const data = await Promise.all(searchResult.results.slice(0, 10).map((r) => r.data()))
79
- results.value = data
82
+ results.value = data.map((value) => ({ value, el: signal() }))
80
83
  }
81
84
  } catch (err) {
82
85
  console.error('Search error:', err)
@@ -117,11 +120,13 @@ export default function ({ options } = {}) {
117
120
  results.value = []
118
121
  activeIndex.value = -1
119
122
  if (debounceTimer) clearTimeout(debounceTimer)
123
+ if (inputRef.value) inputRef.value.blur()
124
+ if (buttonRef.value) buttonRef.value.focus()
120
125
  }
121
126
 
122
127
  const scrollActiveIntoView = () => {
123
128
  setTimeout(() => {
124
- const activeEl = document.querySelector('.search-result-item[aria-selected="true"]')
129
+ const activeEl = results.value[activeIndex.value]?.el.value
125
130
  if (activeEl) {
126
131
  activeEl.scrollIntoView({ block: 'nearest' })
127
132
  }
@@ -138,19 +143,21 @@ export default function ({ options } = {}) {
138
143
  if (event.key === 'ArrowDown') {
139
144
  event.preventDefault()
140
145
  if (results.value.length > 0) {
141
- activeIndex.value = (activeIndex.value + 1) % results.value.length
146
+ const nextIndex = activeIndex.value >= 0 ? (activeIndex.value + 1) % results.value.length : 0
147
+ activeIndex.value = nextIndex
142
148
  scrollActiveIntoView()
143
149
  }
144
150
  } else if (event.key === 'ArrowUp') {
145
151
  event.preventDefault()
146
152
  if (results.value.length > 0) {
147
- activeIndex.value = (activeIndex.value - 1 + results.value.length) % results.value.length
153
+ const nextIndex = activeIndex.value > 0 ? activeIndex.value - 1 : results.value.length - 1
154
+ activeIndex.value = nextIndex
148
155
  scrollActiveIntoView()
149
156
  }
150
157
  } else if (event.key === 'Enter') {
151
158
  event.preventDefault()
152
- const selected = results.value[activeIndex.value]
153
- const fallback = results.value[0]
159
+ const selected = results.value[activeIndex.value]?.value
160
+ const fallback = results.value[0]?.value
154
161
  const target = selected || fallback
155
162
  if (target?.url) {
156
163
  window.location.href = target.url
@@ -159,6 +166,30 @@ export default function ({ options } = {}) {
159
166
  }
160
167
  }
161
168
 
169
+ const onResultKeyDown = (event, indexValue) => {
170
+ if (event.key === 'Escape') {
171
+ event.preventDefault()
172
+ close()
173
+ return
174
+ }
175
+ if (event.key === 'ArrowDown') {
176
+ event.preventDefault()
177
+ const nextIndex = (indexValue + 1) % results.value.length
178
+ activeIndex.value = nextIndex
179
+ scrollActiveIntoView()
180
+ } else if (event.key === 'ArrowUp') {
181
+ event.preventDefault()
182
+ if (indexValue === 0) {
183
+ activeIndex.value = -1
184
+ focusInput()
185
+ return
186
+ }
187
+ const nextIndex = indexValue - 1
188
+ activeIndex.value = nextIndex
189
+ scrollActiveIntoView()
190
+ }
191
+ }
192
+
162
193
  const showEmpty = $(() => !query.value)
163
194
  const showNoResults = $(() => {
164
195
  const _query = query.value
@@ -192,28 +223,24 @@ export default function ({ options } = {}) {
192
223
  return (R) => {
193
224
  R.render(document.body, Outlet)
194
225
  return (
195
- <button class="search-box" type="button" on:click={open} aria-label="Open search">
196
- <svg
197
- attr:width="16"
198
- attr:height="16"
199
- attr:viewBox="0 0 24 24"
200
- attr:fill="none"
201
- attr:stroke="currentColor"
202
- attr:stroke-width="2"
203
- attr:stroke-linecap="round"
204
- attr:stroke-linejoin="round"
205
- >
206
- <circle attr:cx="11" attr:cy="11" attr:r="8"></circle>
207
- <path attr:d="m21 21-4.3-4.3"></path>
208
- </svg>
209
- <span>Search</span>
210
- <kbd>{shortcutLabel}</kbd>
226
+ <button class="search-box" type="button" on:click={open} attr:aria-label="Open search" $ref={buttonRef}>
227
+ <svg
228
+ attr:width="16"
229
+ attr:height="16"
230
+ attr:viewBox="0 0 24 24"
231
+ attr:fill="none"
232
+ attr:stroke="currentColor"
233
+ attr:stroke-width="2"
234
+ attr:stroke-linecap="round"
235
+ attr:stroke-linejoin="round"
236
+ >
237
+ <circle attr:cx="11" attr:cy="11" attr:r="8"></circle>
238
+ <path attr:d="m21 21-4.3-4.3"></path>
239
+ </svg>
240
+ <span>Search</span>
241
+ <kbd>{shortcutLabel}</kbd>
211
242
  <Inlet>
212
- <div
213
- class="search-modal"
214
- class:open={isOpen}
215
- attr:aria-hidden={$(() => (isOpen.value ? null : 'true'))}
216
- >
243
+ <div class="search-modal" class:open={isOpen} attr:inert={$(() => (isOpen.value ? null : ''))}>
217
244
  <div class="search-modal__scrim" on:click={close}></div>
218
245
  <div class="search-modal__panel">
219
246
  <div class="search-input-wrapper">
@@ -237,6 +264,9 @@ export default function ({ options } = {}) {
237
264
  value={query}
238
265
  on:input={onInput}
239
266
  on:keydown={onKeyDown}
267
+ attr:aria-activedescendant={$(() =>
268
+ activeIndex.value >= 0 ? `${resultIdPrefix}-${activeIndex.value}` : null
269
+ )}
240
270
  attr:autocomplete="off"
241
271
  attr:autocorrect="off"
242
272
  attr:spellcheck="false"
@@ -249,13 +279,20 @@ export default function ({ options } = {}) {
249
279
  {() => <div class="search-status">No results found for "{query}"</div>}
250
280
  </If>
251
281
  <If condition={isLoading}>{() => <div class="search-status">Searching...</div>}</If>
252
- <For entries={results} track="url" indexed={true}>
253
- {({ item, index }) => (
282
+ <For entries={results} indexed>
283
+ {({ item: { value, el }, index }) => (
254
284
  <a
255
285
  class="search-result-item"
256
- href={item.url}
286
+ class:active={activeMatch(index)}
287
+ href={value.url}
257
288
  on:click={close}
289
+ on:keydown={(event) => onResultKeyDown(event, index.value)}
290
+ on:focus={() => {
291
+ activeIndex.value = index.value
292
+ }}
258
293
  attr:aria-selected={$(() => (activeIndex.value === index.value ? 'true' : 'false'))}
294
+ attr:id={t`${resultIdPrefix}-${index.value}`}
295
+ $ref={el}
259
296
  >
260
297
  <div class="search-result-title">
261
298
  <svg
@@ -271,9 +308,9 @@ export default function ({ options } = {}) {
271
308
  <path attr:d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
272
309
  <polyline attr:points="14 2 14 8 20 8"></polyline>
273
310
  </svg>
274
- {item?.meta?.title || item?.title || item?.url}
311
+ {value?.meta?.title || value?.title || value?.url}
275
312
  </div>
276
- <div class="search-result-excerpt" innerHTML={item.excerpt || ''}></div>
313
+ <div class="search-result-excerpt" innerHTML={value.excerpt || ''}></div>
277
314
  </a>
278
315
  )}
279
316
  </For>
@@ -281,7 +318,7 @@ export default function ({ options } = {}) {
281
318
  </div>
282
319
  </div>
283
320
  </Inlet>
284
- </button>
321
+ </button>
285
322
  )
286
323
  }
287
324
  }
@@ -35,7 +35,6 @@ export default function () {
35
35
  <path d="m21 21-4.3-4.3"></path>
36
36
  </svg>
37
37
  <span>Search</span>
38
- <kbd>Ctrl+K</kbd>
39
38
  </button>
40
39
  )
41
40
  }
@@ -73,7 +73,7 @@ export default function (props, ...children) {
73
73
 
74
74
  return (
75
75
  <div class="code-block-container">
76
- <button class="copy-btn" on:click={copy} aria-label="Copy code">
76
+ <button class="copy-btn" on:click={copy} attr:aria-label="Copy code">
77
77
  <Btn />
78
78
  </button>
79
79
  <pre {...props} $ref={el}>
@@ -24,4 +24,4 @@ export default function (props, ...children) {
24
24
  <pre {...props}>{...children}</pre>
25
25
  </div>
26
26
  )
27
- }
27
+ }
@@ -21,30 +21,21 @@
21
21
  import { readFileSync } from 'fs'
22
22
  import { fileURLToPath } from 'url'
23
23
  import { dirname, resolve } from 'path'
24
- import rehypeStarryNight from 'rehype-starry-night'
25
-
26
24
  import PAGE_TEMPLATE from './page.jsx'
27
25
  import { createHeadings } from './heading.jsx'
28
26
 
29
27
  const __filename = fileURLToPath(import.meta.url)
30
28
  const __dirname = dirname(__filename)
31
29
 
32
- export default ({ starryNight = true, starryNightOptions } = {}) => {
30
+ export default () => {
33
31
  return {
34
32
  root: __dirname,
35
- // componentsDir: './components',
36
- // pagesDir: './pages',
37
- resources: {
38
- '/.methanol_theme_default': './resources'
33
+ sources: {
34
+ '/.methanol_theme_default': './sources'
39
35
  },
40
36
  components: {
41
37
  ...createHeadings()
42
38
  },
43
- template: PAGE_TEMPLATE,
44
- mdx: {
45
- rehypePlugins: [
46
- starryNight && [rehypeStarryNight, starryNightOptions]
47
- ].filter(Boolean)
48
- }
39
+ template: PAGE_TEMPLATE
49
40
  }
50
41
  }
@@ -78,13 +78,14 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
78
78
  const hasToc = Boolean(toc)
79
79
  const layoutClass = hasToc ? 'layout-container' : 'layout-container no-toc'
80
80
  const tree = renderPageTree(pagesTree, currentRoute, 0)
81
- const { ThemeSearchBox, ThemeColorSwitch, ThemeToCContainer } = components
81
+ const { ThemeSearchBox, ThemeColorSwitch, ThemeAccentSwitch, ThemeToCContainer } = components
82
82
  const rootPage = pagesByRoute?.get?.('/') || pages.find((entry) => entry.routePath === '/')
83
83
  const pageFrontmatter = page?.frontmatter || {}
84
84
  const rootFrontmatter = rootPage?.frontmatter || {}
85
85
  const themeLogo = '/logo.png'
86
+ const themeFavIcon = '/favicon.png'
86
87
  const logo = pageFrontmatter.logo ?? rootFrontmatter.logo ?? ctx.site?.logo ?? themeLogo
87
- const favicon = pageFrontmatter.favicon ?? rootFrontmatter.favicon ?? ctx.site?.favicon ?? logo ?? themeLogo
88
+ const favicon = pageFrontmatter.favicon ?? rootFrontmatter.favicon ?? ctx.site?.favicon ?? themeFavIcon
88
89
  const excerpt = pageFrontmatter.excerpt ?? null
89
90
  const ogTitle = pageFrontmatter.ogTitle ?? null
90
91
  const ogDescription = pageFrontmatter.ogDescription ?? null
@@ -94,8 +95,14 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
94
95
  const twitterDescription = pageFrontmatter.twitterDescription ?? ogDescription ?? excerpt
95
96
  const twitterImage = pageFrontmatter.twitterImage ?? ogImage
96
97
  const twitterCard = pageFrontmatter.twitterCard ?? (twitterImage ? 'summary_large_image' : null)
98
+ const siblings = typeof page?.getSiblings === 'function' ? page.getSiblings() : null
99
+ const prevPage = siblings?.prev || null
100
+ const nextPage = siblings?.next || null
97
101
  const languages = Array.isArray(ctx.languages) ? ctx.languages : []
98
102
  const currentLanguageHref = ctx.language?.href || ctx.language?.routePath || null
103
+ const languageCode =
104
+ pageFrontmatter.langCode ?? rootFrontmatter.langCode ?? ctx.language?.code ?? 'en'
105
+ const htmlLang = typeof languageCode === 'string' && languageCode.trim() ? languageCode : 'en'
99
106
  const pagefindEnabled = ctx.site?.pagefind?.enabled !== false
100
107
  const pagefindOptions = ctx.site?.pagefind?.options || null
101
108
  const languageSelector = languages.length ? (
@@ -137,7 +144,7 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
137
144
  return (
138
145
  <>
139
146
  {R.rawHTML`<!DOCTYPE html>`}
140
- <html lang="en">
147
+ <html lang={htmlLang}>
141
148
  <head>
142
149
  <meta charset="UTF-8" />
143
150
  <meta name="viewport" content="width=device-width" />
@@ -155,6 +162,7 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
155
162
  {twitterTitle ? <meta name="twitter:title" content={twitterTitle} /> : null}
156
163
  {twitterDescription ? <meta name="twitter:description" content={twitterDescription} /> : null}
157
164
  {twitterImage ? <meta name="twitter:image" content={twitterImage} /> : null}
165
+ <ExtraHead />
158
166
  <link rel="preload stylesheet" as="style" href="/.methanol_theme_default/style.css" />
159
167
  {R.rawHTML`
160
168
  <script>
@@ -164,10 +172,15 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
164
172
  const theme = savedTheme || systemTheme;
165
173
  document.documentElement.classList.toggle('light', theme === 'light');
166
174
  document.documentElement.classList.toggle('dark', theme === 'dark');
175
+
176
+ const savedAccent = localStorage.getItem('methanol-accent');
177
+ if (savedAccent && savedAccent !== 'default') {
178
+ document.documentElement.classList.add('accent-' + savedAccent);
179
+ }
167
180
  })();
168
181
  </script>
169
182
  `}
170
- <ExtraHead />
183
+ <script type="module" src="/.methanol_theme_default/prefetch.js" defer></script>
171
184
  </head>
172
185
  <body>
173
186
  <input type="checkbox" id="nav-toggle" class="nav-toggle" />
@@ -187,6 +200,27 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
187
200
  <line x1="3" y1="18" x2="21" y2="18"></line>
188
201
  </svg>
189
202
  </label>
203
+ {pagefindEnabled ? (
204
+ <button
205
+ class="search-toggle-label"
206
+ aria-label="Open search"
207
+ onclick="window.__methanolSearchOpen()"
208
+ >
209
+ <svg
210
+ width="24"
211
+ height="24"
212
+ viewBox="0 0 24 24"
213
+ fill="none"
214
+ stroke="currentColor"
215
+ stroke-width="2"
216
+ stroke-linecap="round"
217
+ stroke-linejoin="round"
218
+ >
219
+ <circle cx="11" cy="11" r="8"></circle>
220
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
221
+ </svg>
222
+ </button>
223
+ ) : null}
190
224
  {hasToc ? (
191
225
  <>
192
226
  <input type="checkbox" id="toc-toggle" class="toc-toggle" />
@@ -226,15 +260,35 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
226
260
  <div class="sidebar-footer">
227
261
  {languageSelector}
228
262
  <ThemeColorSwitch />
263
+ <ThemeAccentSwitch />
229
264
  </div>
230
265
  </aside>
231
266
  <main class="main-content" data-pagefind-body={pagefindEnabled ? '' : null}>
232
267
  <Page />
268
+ {prevPage || nextPage ? (
269
+ <nav class="page-nav">
270
+ {prevPage ? (
271
+ <a class="page-nav-card prev" href={prevPage.routeHref || prevPage.routePath}>
272
+ <span class="page-nav-label">Previous</span>
273
+ <span class="page-nav-title">{prevPage.title || prevPage.routePath}</span>
274
+ </a>
275
+ ) : <div class="page-nav-spacer"></div>}
276
+ {nextPage ? (
277
+ <a class="page-nav-card next" href={nextPage.routeHref || nextPage.routePath}>
278
+ <span class="page-nav-label">Next</span>
279
+ <span class="page-nav-title">{nextPage.title || nextPage.routePath}</span>
280
+ </a>
281
+ ) : null}
282
+ </nav>
283
+ ) : null}
233
284
  {page ? (
234
285
  <footer class="page-meta">
235
- Updated: {page.updatedAt || '-'}
236
- <br />
237
- Powered by Methanol
286
+ <div class="page-meta-item">
287
+ Updated: {page.updatedAt || '-'}
288
+ </div>
289
+ <div class="page-meta-item">
290
+ Powered by <a href="https://github.com/SudoMaker/Methanol" target="_blank" rel="noopener noreferrer" class="methanol-link">Methanol</a>
291
+ </div>
238
292
  </footer>
239
293
  ) : null}
240
294
  </main>
@@ -4,6 +4,28 @@ title: Welcome
4
4
 
5
5
  # Welcome to Methanol
6
6
 
7
- Start by adding docs under `pages/` (or `docs/`) as `.mdx` files, and place shared UI in `components/`.
7
+ Methanol turns your content folder into a static site with rEFui and MDX, file-based routing, and a fast dev server.
8
8
 
9
- Add your own `pages/index.md` or `pages/index.mdx` to replace this page.
9
+ ## Quick Start
10
+
11
+ ```bash
12
+ npx methanol dev
13
+ ```
14
+
15
+ Open `http://localhost:5173`.
16
+
17
+ ## Your Content Folder
18
+
19
+ Pages are currently read from the configured `pagesDir`:
20
+
21
+ <p><code>{ctx.site?.pagesDir || 'pages/'}</code></p>
22
+
23
+ You can change this with `pagesDir` in your config or `--input` on the CLI. Create an `index.mdx` in that folder to replace this page.
24
+
25
+ ## Common Folders
26
+
27
+ ```
28
+ components/ # JSX/TSX components for MDX
29
+ public/ # static assets copied as-is
30
+ dist/ # production output
31
+ ```
@@ -0,0 +1,49 @@
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
+ (() => {
22
+ const prefetched = new Set()
23
+ const canPrefetch = (anchor) => {
24
+ if (!anchor || !anchor.href) return false
25
+ if (anchor.dataset && anchor.dataset.prefetch === 'false') return false
26
+ if (anchor.hasAttribute('download')) return false
27
+ if (anchor.target && anchor.target !== '_self') return false
28
+ const url = new URL(anchor.href, window.location.href)
29
+ if (url.origin !== window.location.origin) return false
30
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return false
31
+ if (url.pathname === window.location.pathname && url.search === window.location.search) {
32
+ return false
33
+ }
34
+ return true
35
+ }
36
+ const onHover = (event) => {
37
+ const anchor = event.target && event.target.closest ? event.target.closest('a') : null
38
+ if (!canPrefetch(anchor)) return
39
+ const href = anchor.href
40
+ if (prefetched.has(href)) return
41
+ prefetched.add(href)
42
+ const link = document.createElement('link')
43
+ link.rel = 'prefetch'
44
+ link.as = 'document'
45
+ link.href = href
46
+ document.head.appendChild(link)
47
+ }
48
+ document.addEventListener('pointerover', onHover, { capture: true, passive: true })
49
+ })()