methanol 0.0.5 → 0.0.7
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/package.json +1 -1
- package/src/build-system.js +6 -6
- package/src/config.js +30 -8
- package/src/dev-server.js +31 -6
- package/src/main.js +2 -0
- package/src/pages.js +6 -1
- package/src/public-assets.js +105 -34
- package/src/state.js +1 -0
- package/themes/default/components/ButtonGroup.jsx +38 -0
- package/themes/default/components/LinkButton.jsx +37 -0
- package/themes/default/page.jsx +9 -22
- package/themes/default/{sources/prefetch.js → public/theme-prepare.js} +13 -1
- package/themes/default/sources/style.css +108 -23
package/package.json
CHANGED
package/src/build-system.js
CHANGED
|
@@ -28,7 +28,7 @@ import { renderHtml } from './mdx.js'
|
|
|
28
28
|
import { buildComponentRegistry } from './components.js'
|
|
29
29
|
import { methanolVirtualHtmlPlugin, methanolResolverPlugin } from './vite-plugins.js'
|
|
30
30
|
import { createStageLogger } from './stage-logger.js'
|
|
31
|
-
import {
|
|
31
|
+
import { preparePublicAssets } from './public-assets.js'
|
|
32
32
|
|
|
33
33
|
const ensureDir = async (dir) => {
|
|
34
34
|
await mkdir(dir, { recursive: true })
|
|
@@ -163,11 +163,11 @@ export const buildHtmlEntries = async () => {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
export const runViteBuild = async (entry, htmlCache) => {
|
|
166
|
-
if (state.STATIC_DIR !== false) {
|
|
167
|
-
await
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
if (state.STATIC_DIR !== false && state.MERGED_ASSETS_DIR) {
|
|
167
|
+
await preparePublicAssets({
|
|
168
|
+
themeDir: state.THEME_ASSETS_DIR,
|
|
169
|
+
userDir: state.USER_ASSETS_DIR,
|
|
170
|
+
targetDir: state.MERGED_ASSETS_DIR
|
|
171
171
|
})
|
|
172
172
|
}
|
|
173
173
|
const copyPublicDirEnabled = state.STATIC_DIR !== false
|
package/src/config.js
CHANGED
|
@@ -216,6 +216,7 @@ export const applyConfig = async (config, mode) => {
|
|
|
216
216
|
state.ROOT_DIR = root
|
|
217
217
|
const configSiteName = cli.CLI_SITE_NAME ?? config.site?.name ?? null
|
|
218
218
|
state.SITE_NAME = configSiteName || basename(root) || 'Methanol Site'
|
|
219
|
+
state.USER_SITE = config.site && typeof config.site === 'object' ? { ...config.site } : null
|
|
219
220
|
if (mode) {
|
|
220
221
|
state.CURRENT_MODE = mode
|
|
221
222
|
}
|
|
@@ -289,15 +290,36 @@ export const applyConfig = async (config, mode) => {
|
|
|
289
290
|
if (hasOwn(state.USER_THEME, 'publicDir') && state.THEME_PUBLIC_DIR && !existsSync(state.THEME_PUBLIC_DIR)) {
|
|
290
291
|
throw new Error(`Theme public directory not found: ${state.THEME_PUBLIC_DIR}`)
|
|
291
292
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
)
|
|
299
|
-
|
|
293
|
+
|
|
294
|
+
// Asset Merging Logic
|
|
295
|
+
const userAssetsDir = userSpecifiedPublicDir
|
|
296
|
+
? resolveFromRoot(root, publicDirValue, 'public')
|
|
297
|
+
: resolveFromRoot(root, 'public', 'public')
|
|
298
|
+
|
|
299
|
+
const hasUserAssets = existsSync(userAssetsDir)
|
|
300
|
+
state.USER_ASSETS_DIR = hasUserAssets ? userAssetsDir : null
|
|
301
|
+
state.THEME_ASSETS_DIR = state.THEME_PUBLIC_DIR && existsSync(state.THEME_PUBLIC_DIR) ? state.THEME_PUBLIC_DIR : null
|
|
302
|
+
|
|
303
|
+
if (state.STATIC_DIR !== false) {
|
|
304
|
+
if (!hasUserAssets) {
|
|
305
|
+
// Optimization: No user assets, just use theme assets directly
|
|
306
|
+
state.STATIC_DIR = state.THEME_ASSETS_DIR
|
|
307
|
+
state.MERGED_ASSETS_DIR = null
|
|
308
|
+
} else {
|
|
309
|
+
// We need to merge
|
|
310
|
+
const nodeModulesPath = resolve(root, 'node_modules')
|
|
311
|
+
if (existsSync(nodeModulesPath)) {
|
|
312
|
+
state.MERGED_ASSETS_DIR = resolve(nodeModulesPath, '.methanol/assets')
|
|
313
|
+
} else {
|
|
314
|
+
state.MERGED_ASSETS_DIR = resolve(state.PAGES_DIR || resolve(root, 'pages'), '.methanol/assets')
|
|
315
|
+
}
|
|
316
|
+
state.STATIC_DIR = state.MERGED_ASSETS_DIR
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
state.STATIC_DIR = false
|
|
320
|
+
state.MERGED_ASSETS_DIR = null
|
|
300
321
|
}
|
|
322
|
+
|
|
301
323
|
state.SOURCES = normalizeSources(state.USER_THEME.sources, themeRoot)
|
|
302
324
|
state.USER_VITE_CONFIG = config.vite || null
|
|
303
325
|
state.USER_MDX_CONFIG = config.mdx || null
|
package/src/dev-server.js
CHANGED
|
@@ -39,10 +39,13 @@ import {
|
|
|
39
39
|
import { buildPagesContext, buildPageEntry, routePathFromFile } from './pages.js'
|
|
40
40
|
import { compilePageMdx, renderHtml } from './mdx.js'
|
|
41
41
|
import { methanolResolverPlugin } from './vite-plugins.js'
|
|
42
|
-
import {
|
|
42
|
+
import { preparePublicAssets, updateAsset } from './public-assets.js'
|
|
43
43
|
|
|
44
44
|
export const runViteDev = async () => {
|
|
45
45
|
const baseFsAllow = [state.ROOT_DIR, state.USER_THEME.root].filter(Boolean)
|
|
46
|
+
if (state.MERGED_ASSETS_DIR) {
|
|
47
|
+
baseFsAllow.push(state.MERGED_ASSETS_DIR)
|
|
48
|
+
}
|
|
46
49
|
const baseConfig = {
|
|
47
50
|
configFile: false,
|
|
48
51
|
root: state.PAGES_DIR,
|
|
@@ -63,11 +66,11 @@ export const runViteDev = async () => {
|
|
|
63
66
|
}
|
|
64
67
|
const userConfig = await resolveUserViteConfig('serve')
|
|
65
68
|
const finalConfig = userConfig ? mergeConfig(baseConfig, userConfig) : baseConfig
|
|
66
|
-
if (state.STATIC_DIR !== false) {
|
|
67
|
-
await
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
if (state.STATIC_DIR !== false && state.MERGED_ASSETS_DIR) {
|
|
70
|
+
await preparePublicAssets({
|
|
71
|
+
themeDir: state.THEME_ASSETS_DIR,
|
|
72
|
+
userDir: state.USER_ASSETS_DIR,
|
|
73
|
+
targetDir: state.MERGED_ASSETS_DIR
|
|
71
74
|
})
|
|
72
75
|
}
|
|
73
76
|
if (cli.CLI_PORT != null) {
|
|
@@ -94,6 +97,28 @@ export const runViteDev = async () => {
|
|
|
94
97
|
}
|
|
95
98
|
const server = await createServer(finalConfig)
|
|
96
99
|
|
|
100
|
+
if (state.MERGED_ASSETS_DIR && state.USER_ASSETS_DIR) {
|
|
101
|
+
const assetWatcher = chokidar.watch(state.USER_ASSETS_DIR, {
|
|
102
|
+
ignoreInitial: true
|
|
103
|
+
})
|
|
104
|
+
const handleAssetUpdate = (type, filePath) => {
|
|
105
|
+
const relPath = relative(state.USER_ASSETS_DIR, filePath)
|
|
106
|
+
enqueue(async () => {
|
|
107
|
+
await updateAsset({
|
|
108
|
+
type,
|
|
109
|
+
filePath,
|
|
110
|
+
relPath,
|
|
111
|
+
themeDir: state.THEME_ASSETS_DIR,
|
|
112
|
+
userDir: state.USER_ASSETS_DIR,
|
|
113
|
+
targetDir: state.MERGED_ASSETS_DIR
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
assetWatcher.on('add', (filePath) => handleAssetUpdate('add', filePath))
|
|
118
|
+
assetWatcher.on('change', (filePath) => handleAssetUpdate('change', filePath))
|
|
119
|
+
assetWatcher.on('unlink', (filePath) => handleAssetUpdate('unlink', filePath))
|
|
120
|
+
}
|
|
121
|
+
|
|
97
122
|
const themeComponentsDir = state.THEME_COMPONENTS_DIR
|
|
98
123
|
const themeEnv = state.THEME_ENV
|
|
99
124
|
const themeRegistry = themeComponentsDir
|
package/src/main.js
CHANGED
|
@@ -62,6 +62,7 @@ const main = async () => {
|
|
|
62
62
|
const mode = isDev ? 'development' : 'production'
|
|
63
63
|
const config = await loadUserConfig(mode, cli.CLI_CONFIG_PATH)
|
|
64
64
|
await applyConfig(config, mode)
|
|
65
|
+
const userSite = state.USER_SITE || {}
|
|
65
66
|
const hookContext = {
|
|
66
67
|
mode,
|
|
67
68
|
root: state.ROOT_DIR,
|
|
@@ -71,6 +72,7 @@ const main = async () => {
|
|
|
71
72
|
isPreview,
|
|
72
73
|
HTMLRenderer,
|
|
73
74
|
site: {
|
|
75
|
+
...userSite,
|
|
74
76
|
name: state.SITE_NAME,
|
|
75
77
|
root: state.ROOT_DIR,
|
|
76
78
|
pagesDir: state.PAGES_DIR,
|
package/src/pages.js
CHANGED
|
@@ -273,7 +273,10 @@ const buildPagesTree = (pages, options = {}) => {
|
|
|
273
273
|
const shouldExposeHidden =
|
|
274
274
|
!isHidden404 &&
|
|
275
275
|
page.hiddenByFrontmatter === true &&
|
|
276
|
-
(
|
|
276
|
+
(
|
|
277
|
+
page.routePath === currentRoutePath ||
|
|
278
|
+
(page.isIndex && page.dir && isUnderExposedHiddenDir(page.dir))
|
|
279
|
+
)
|
|
277
280
|
if (!shouldExposeHidden) {
|
|
278
281
|
continue
|
|
279
282
|
}
|
|
@@ -679,7 +682,9 @@ export const buildPagesContext = async ({ compileAll = true } = {}) => {
|
|
|
679
682
|
let pagesTree = getPagesTree('/')
|
|
680
683
|
const notFound = pagesByRoute.get('/404') || null
|
|
681
684
|
const languages = collectLanguagesFromPages(pages)
|
|
685
|
+
const userSite = state.USER_SITE || {}
|
|
682
686
|
const site = {
|
|
687
|
+
...userSite,
|
|
683
688
|
name: state.SITE_NAME,
|
|
684
689
|
root: state.ROOT_DIR,
|
|
685
690
|
pagesDir: state.PAGES_DIR,
|
package/src/public-assets.js
CHANGED
|
@@ -18,56 +18,127 @@
|
|
|
18
18
|
* under the License.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
import { readdir, stat, copyFile, mkdir } from 'fs/promises'
|
|
21
|
+
import { readdir, stat, lstat, copyFile, mkdir, symlink, unlink, rm, link } from 'fs/promises'
|
|
22
22
|
import { existsSync } from 'fs'
|
|
23
|
-
import { resolve, dirname, relative } from 'path'
|
|
23
|
+
import { resolve, dirname, relative, parse } from 'path'
|
|
24
24
|
|
|
25
25
|
const ensureDir = async (dir) => {
|
|
26
26
|
await mkdir(dir, { recursive: true })
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const isWindows = process.platform === 'win32'
|
|
30
|
+
|
|
31
|
+
const linkOrCopyFile = async (src, dest) => {
|
|
32
|
+
try {
|
|
33
|
+
try {
|
|
34
|
+
await lstat(dest)
|
|
35
|
+
await unlink(dest)
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e.code !== 'ENOENT') {
|
|
38
|
+
try {
|
|
39
|
+
await rm(dest, { recursive: true, force: true })
|
|
40
|
+
} catch (e2) {
|
|
41
|
+
console.error(`Methanol: Failed to clean destination ${dest}`, e2)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error(`Methanol: Failed to remove existing file at ${dest}`, err)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await ensureDir(dirname(dest))
|
|
51
|
+
|
|
52
|
+
if (isWindows) {
|
|
53
|
+
// Windows: Check for different drives first
|
|
54
|
+
if (parse(src).root.toLowerCase() !== parse(dest).root.toLowerCase()) {
|
|
55
|
+
await copyFile(src, dest)
|
|
56
|
+
return 'copied'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Try hard link (no admin required)
|
|
60
|
+
try {
|
|
61
|
+
await link(src, dest)
|
|
62
|
+
return 'hardlinked'
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Fallback to copy
|
|
65
|
+
// console.warn(`Methanol: Hardlink failed for ${src} -> ${dest}. Falling back to copy.`, err.message)
|
|
66
|
+
await copyFile(src, dest)
|
|
67
|
+
return 'copied (fallback)'
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// macOS/Linux: Symlink
|
|
71
|
+
await symlink(src, dest)
|
|
72
|
+
return 'symlinked'
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error(`Methanol: Failed to link ${src} to ${dest}`, err)
|
|
76
|
+
return 'failed'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const processDir = async (sourceDir, targetDir, accumulated = new Set()) => {
|
|
81
|
+
if (!existsSync(sourceDir)) return
|
|
31
82
|
const entries = await readdir(sourceDir)
|
|
32
83
|
for (const entry of entries) {
|
|
33
|
-
if (entry.startsWith('.'))
|
|
34
|
-
continue
|
|
35
|
-
}
|
|
84
|
+
if (entry.startsWith('.')) continue
|
|
36
85
|
const sourcePath = resolve(sourceDir, entry)
|
|
37
86
|
const targetPath = resolve(targetDir, entry)
|
|
38
87
|
const stats = await stat(sourcePath)
|
|
88
|
+
|
|
39
89
|
if (stats.isDirectory()) {
|
|
40
|
-
await
|
|
90
|
+
await processDir(sourcePath, targetPath, accumulated)
|
|
41
91
|
} else {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
onFile(sourcePath, targetPath, { skipped: true })
|
|
45
|
-
}
|
|
46
|
-
continue
|
|
47
|
-
}
|
|
48
|
-
await ensureDir(dirname(targetPath))
|
|
49
|
-
await copyFile(sourcePath, targetPath)
|
|
50
|
-
if (onFile) {
|
|
51
|
-
onFile(sourcePath, targetPath, { skipped: false })
|
|
52
|
-
}
|
|
92
|
+
await linkOrCopyFile(sourcePath, targetPath)
|
|
93
|
+
accumulated.add(relative(targetDir, targetPath))
|
|
53
94
|
}
|
|
54
95
|
}
|
|
55
96
|
}
|
|
56
97
|
|
|
57
|
-
export const
|
|
58
|
-
if (
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
98
|
+
export const preparePublicAssets = async ({ themeDir, userDir, targetDir }) => {
|
|
99
|
+
if (existsSync(targetDir)) {
|
|
100
|
+
await rm(targetDir, { recursive: true, force: true })
|
|
101
|
+
}
|
|
102
|
+
await ensureDir(targetDir)
|
|
103
|
+
|
|
104
|
+
if (themeDir) {
|
|
105
|
+
await processDir(themeDir, targetDir)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (userDir) {
|
|
109
|
+
await processDir(userDir, targetDir)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const updateAsset = async ({ type, filePath, themeDir, userDir, targetDir, relPath }) => {
|
|
114
|
+
const targetPath = resolve(targetDir, relPath)
|
|
115
|
+
|
|
116
|
+
if (type === 'unlink') {
|
|
117
|
+
try {
|
|
118
|
+
try {
|
|
119
|
+
await unlink(targetPath)
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (e.code !== 'ENOENT') {
|
|
122
|
+
await rm(targetPath, { recursive: true, force: true })
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (themeDir) {
|
|
127
|
+
const themePath = resolve(themeDir, relPath)
|
|
128
|
+
if (existsSync(themePath)) {
|
|
129
|
+
await linkOrCopyFile(themePath, targetPath)
|
|
130
|
+
return 'restored theme asset'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(`Methanol: Error updating asset ${relPath}`, err)
|
|
135
|
+
}
|
|
136
|
+
return 'removed'
|
|
137
|
+
} else {
|
|
138
|
+
const sourcePath = userDir ? resolve(userDir, relPath) : null
|
|
139
|
+
if (sourcePath && existsSync(sourcePath)) {
|
|
140
|
+
await linkOrCopyFile(sourcePath, targetPath)
|
|
141
|
+
return 'updated'
|
|
142
|
+
}
|
|
67
143
|
}
|
|
68
|
-
await copyDir(resolvedSource, resolvedTarget, (sourcePath, targetPath, info) => {
|
|
69
|
-
const rel = relative(resolvedSource, sourcePath).replace(/\\/g, '/')
|
|
70
|
-
if (info?.skipped) return
|
|
71
|
-
console.log(`Methanol: copied ${label}/${rel}`)
|
|
72
|
-
})
|
|
73
144
|
}
|
package/src/state.js
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
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 ButtonGroup({
|
|
22
|
+
children,
|
|
23
|
+
className = '',
|
|
24
|
+
align = 'left', // left, center, right
|
|
25
|
+
...props
|
|
26
|
+
}) {
|
|
27
|
+
const classes = [
|
|
28
|
+
'button-group',
|
|
29
|
+
`button-group--${align}`,
|
|
30
|
+
className
|
|
31
|
+
].filter(Boolean).join(' ')
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div class={classes} {...props}>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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 LinkButton({
|
|
22
|
+
children,
|
|
23
|
+
variant = 'primary', // primary, secondary, outline, ghost
|
|
24
|
+
size = 'md', // sm, md, lg
|
|
25
|
+
class: className = '',
|
|
26
|
+
...props
|
|
27
|
+
}) {
|
|
28
|
+
const classes = ['link-button', `link-button--${variant}`, `link-button--${size}`, className]
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
.join(' ')
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<a class={classes} {...props}>
|
|
34
|
+
{children}
|
|
35
|
+
</a>
|
|
36
|
+
)
|
|
37
|
+
}
|
package/themes/default/page.jsx
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
import { HTMLRenderer as R } from 'methanol'
|
|
22
22
|
import { renderToc } from './components/ThemeToCContainer.static.jsx'
|
|
23
23
|
|
|
24
|
+
const DOCHTML = R.rawHTML`<!DOCTYPE html>`
|
|
25
|
+
|
|
24
26
|
const renderPageTree = (nodes = [], currentRoute, depth = 0) => {
|
|
25
27
|
const items = []
|
|
26
28
|
let hasActive = false
|
|
@@ -87,8 +89,9 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
87
89
|
const logo = pageFrontmatter.logo ?? rootFrontmatter.logo ?? ctx.site?.logo ?? themeLogo
|
|
88
90
|
const favicon = pageFrontmatter.favicon ?? rootFrontmatter.favicon ?? ctx.site?.favicon ?? themeFavIcon
|
|
89
91
|
const excerpt = pageFrontmatter.excerpt ?? `${title} | ${siteName} - Powered by Methanol`
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
+
const _ogTitle = pageFrontmatter.ogTitle ?? title ?? null
|
|
93
|
+
const ogTitle = _ogTitle ? `${_ogTitle} | ${siteName}` : null
|
|
94
|
+
const ogDescription = pageFrontmatter.ogDescription ?? excerpt ?? null
|
|
92
95
|
const ogImage = pageFrontmatter.ogImage ?? null
|
|
93
96
|
const ogUrl = pageFrontmatter.ogUrl ?? null
|
|
94
97
|
const twitterTitle = pageFrontmatter.twitterTitle ?? ogTitle
|
|
@@ -142,7 +145,7 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
142
145
|
) : null
|
|
143
146
|
return (
|
|
144
147
|
<>
|
|
145
|
-
{
|
|
148
|
+
{DOCHTML}
|
|
146
149
|
<html lang={htmlLang}>
|
|
147
150
|
<head>
|
|
148
151
|
<meta charset="UTF-8" />
|
|
@@ -151,7 +154,7 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
151
154
|
{title} | {siteName}
|
|
152
155
|
</title>
|
|
153
156
|
{baseHref ? <base href={baseHref} /> : null}
|
|
154
|
-
<link rel="icon" href={favicon} />
|
|
157
|
+
{favicon ? <link rel="icon" href={favicon} /> : null}
|
|
155
158
|
<meta name="description" content={excerpt} />
|
|
156
159
|
{ogTitle ? <meta property="og:title" content={ogTitle} /> : null}
|
|
157
160
|
{ogDescription ? <meta property="og:description" content={ogDescription} /> : null}
|
|
@@ -163,23 +166,7 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
163
166
|
{twitterImage ? <meta name="twitter:image" content={twitterImage} /> : null}
|
|
164
167
|
<ExtraHead />
|
|
165
168
|
<link rel="preload stylesheet" as="style" href="/.methanol_theme_default/style.css" />
|
|
166
|
-
|
|
167
|
-
<script>
|
|
168
|
-
(function() {
|
|
169
|
-
const savedTheme = localStorage.getItem('methanol-theme');
|
|
170
|
-
const systemTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
171
|
-
const theme = savedTheme || systemTheme;
|
|
172
|
-
document.documentElement.classList.toggle('light', theme === 'light');
|
|
173
|
-
document.documentElement.classList.toggle('dark', theme === 'dark');
|
|
174
|
-
|
|
175
|
-
const savedAccent = localStorage.getItem('methanol-accent');
|
|
176
|
-
if (savedAccent && savedAccent !== 'default') {
|
|
177
|
-
document.documentElement.classList.add('accent-' + savedAccent);
|
|
178
|
-
}
|
|
179
|
-
})();
|
|
180
|
-
</script>
|
|
181
|
-
`}
|
|
182
|
-
<script type="module" src="/.methanol_theme_default/prefetch.js" defer></script>
|
|
169
|
+
<script src="/theme-prepare.js"></script>
|
|
183
170
|
</head>
|
|
184
171
|
<body>
|
|
185
172
|
<input type="checkbox" id="nav-toggle" class="nav-toggle" />
|
|
@@ -244,7 +231,7 @@ const PAGE_TEMPLATE = ({ Page, ExtraHead, components, ctx }) => {
|
|
|
244
231
|
<aside class="sidebar">
|
|
245
232
|
<div class="sidebar-header">
|
|
246
233
|
<div class="logo">
|
|
247
|
-
<img src={logo} alt="logo" fetchpriority="high"/>
|
|
234
|
+
{logo ? <img src={logo} alt="logo" fetchpriority="high"/> : null}
|
|
248
235
|
<span>{siteName}</span>
|
|
249
236
|
</div>
|
|
250
237
|
{pagefindEnabled ? <ThemeSearchBox options={pagefindOptions} /> : null}
|
|
@@ -18,7 +18,19 @@
|
|
|
18
18
|
* under the License.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
(()
|
|
21
|
+
;(function initThemeColor() {
|
|
22
|
+
const savedTheme = localStorage.getItem('methanol-theme')
|
|
23
|
+
const systemTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
|
|
24
|
+
const theme = savedTheme || systemTheme
|
|
25
|
+
document.documentElement.classList.toggle('light', theme === 'light')
|
|
26
|
+
document.documentElement.classList.toggle('dark', theme === 'dark')
|
|
27
|
+
|
|
28
|
+
const savedAccent = localStorage.getItem('methanol-accent')
|
|
29
|
+
if (savedAccent && savedAccent !== 'default') {
|
|
30
|
+
document.documentElement.classList.add('accent-' + savedAccent)
|
|
31
|
+
}
|
|
32
|
+
})()
|
|
33
|
+
;(function initPrefetch() {
|
|
22
34
|
const prefetched = new Set()
|
|
23
35
|
const canPrefetch = (anchor) => {
|
|
24
36
|
if (!anchor || !anchor.href) return false
|
|
@@ -270,7 +270,7 @@ a {
|
|
|
270
270
|
.sidebar {
|
|
271
271
|
position: sticky;
|
|
272
272
|
top: 0;
|
|
273
|
-
height:
|
|
273
|
+
height: 100dvh;
|
|
274
274
|
overflow: visible;
|
|
275
275
|
padding: 2rem 0;
|
|
276
276
|
display: flex;
|
|
@@ -1420,6 +1420,7 @@ a {
|
|
|
1420
1420
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1421
1421
|
position: relative;
|
|
1422
1422
|
overflow: hidden;
|
|
1423
|
+
-webkit-tap-highlight-color: transparent;
|
|
1423
1424
|
|
|
1424
1425
|
&:hover {
|
|
1425
1426
|
border-color: var(--accent);
|
|
@@ -1473,7 +1474,7 @@ a {
|
|
|
1473
1474
|
|
|
1474
1475
|
.page-nav-card.next {
|
|
1475
1476
|
text-align: right;
|
|
1476
|
-
align-items:
|
|
1477
|
+
align-items: stretch;
|
|
1477
1478
|
|
|
1478
1479
|
.page-nav-label {
|
|
1479
1480
|
justify-content: flex-end;
|
|
@@ -1493,26 +1494,6 @@ a {
|
|
|
1493
1494
|
grid-template-columns: 1fr;
|
|
1494
1495
|
gap: 1rem;
|
|
1495
1496
|
}
|
|
1496
|
-
|
|
1497
|
-
.page-nav-card.next {
|
|
1498
|
-
text-align: left;
|
|
1499
|
-
align-items: flex-start;
|
|
1500
|
-
|
|
1501
|
-
.page-nav-label {
|
|
1502
|
-
flex-direction: row;
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
.page-nav-label::after {
|
|
1506
|
-
display: none;
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
.page-nav-label::before {
|
|
1510
|
-
content: '→';
|
|
1511
|
-
font-family: system-ui;
|
|
1512
|
-
margin-right: 0.1rem;
|
|
1513
|
-
transition: transform 0.2s ease;
|
|
1514
|
-
}
|
|
1515
|
-
}
|
|
1516
1497
|
}
|
|
1517
1498
|
|
|
1518
1499
|
/* --- Mobile / Toggles --- */
|
|
@@ -1566,7 +1547,7 @@ a {
|
|
|
1566
1547
|
.toc-panel {
|
|
1567
1548
|
position: fixed;
|
|
1568
1549
|
top: 0;
|
|
1569
|
-
|
|
1550
|
+
height: 100dvh;
|
|
1570
1551
|
width: 280px;
|
|
1571
1552
|
background: var(--surface);
|
|
1572
1553
|
border: 1px solid var(--border);
|
|
@@ -1646,6 +1627,110 @@ a {
|
|
|
1646
1627
|
}
|
|
1647
1628
|
}
|
|
1648
1629
|
|
|
1630
|
+
/* --- Components --- */
|
|
1631
|
+
|
|
1632
|
+
.link-button {
|
|
1633
|
+
display: inline-flex;
|
|
1634
|
+
align-items: center;
|
|
1635
|
+
justify-content: center;
|
|
1636
|
+
font-weight: 600;
|
|
1637
|
+
border-radius: 999px;
|
|
1638
|
+
text-decoration: none !important;
|
|
1639
|
+
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
1640
|
+
cursor: pointer;
|
|
1641
|
+
line-height: 1;
|
|
1642
|
+
gap: 0.5rem;
|
|
1643
|
+
white-space: nowrap;
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
.link-button:active {
|
|
1647
|
+
transform: scale(0.96);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/* Variants */
|
|
1651
|
+
.link-button--primary {
|
|
1652
|
+
background-color: var(--accent);
|
|
1653
|
+
color: var(--accent-foreground);
|
|
1654
|
+
border: 1px solid transparent;
|
|
1655
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
1656
|
+
|
|
1657
|
+
&:hover {
|
|
1658
|
+
opacity: 0.9;
|
|
1659
|
+
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
1660
|
+
transform: translateY(-1px);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
.link-button--secondary {
|
|
1665
|
+
background-color: var(--surface-elevated);
|
|
1666
|
+
color: var(--text);
|
|
1667
|
+
border: 1px solid var(--border);
|
|
1668
|
+
|
|
1669
|
+
&:hover {
|
|
1670
|
+
background-color: var(--surface-muted);
|
|
1671
|
+
border-color: var(--muted);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
.link-button--outline {
|
|
1676
|
+
background-color: transparent;
|
|
1677
|
+
color: var(--text);
|
|
1678
|
+
border: 1px solid var(--border);
|
|
1679
|
+
|
|
1680
|
+
&:hover {
|
|
1681
|
+
border-color: var(--accent);
|
|
1682
|
+
color: var(--accent);
|
|
1683
|
+
background-color: var(--accent-soft);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
.link-button--ghost {
|
|
1688
|
+
background-color: transparent;
|
|
1689
|
+
color: var(--muted);
|
|
1690
|
+
border: 1px solid transparent;
|
|
1691
|
+
|
|
1692
|
+
&:hover {
|
|
1693
|
+
color: var(--accent);
|
|
1694
|
+
background-color: var(--accent-soft);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/* Sizes */
|
|
1699
|
+
.link-button--sm {
|
|
1700
|
+
padding: 0.375rem 0.75rem;
|
|
1701
|
+
font-size: 0.85rem;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
.link-button--md {
|
|
1705
|
+
padding: 0.625rem 1.25rem;
|
|
1706
|
+
font-size: 0.95rem;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
.link-button--lg {
|
|
1710
|
+
padding: 0.875rem 1.75rem;
|
|
1711
|
+
font-size: 1.1rem;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
.button-group {
|
|
1715
|
+
display: flex;
|
|
1716
|
+
flex-wrap: wrap;
|
|
1717
|
+
gap: 1rem;
|
|
1718
|
+
margin: 1.5rem 0;
|
|
1719
|
+
align-items: center;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
.button-group--left {
|
|
1723
|
+
justify-content: flex-start;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
.button-group--center {
|
|
1727
|
+
justify-content: center;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
.button-group--right {
|
|
1731
|
+
justify-content: flex-end;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1649
1734
|
/* --- View Transitions --- */
|
|
1650
1735
|
|
|
1651
1736
|
@view-transition {
|