methanol 0.0.0 → 0.0.1
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/.editorconfig +19 -0
- package/.prettierrc +10 -0
- package/LICENSE +203 -0
- package/banner.txt +6 -0
- package/bin/methanol.js +24 -0
- package/index.js +22 -0
- package/package.json +42 -9
- package/src/assets.js +30 -0
- package/src/build-system.js +200 -0
- package/src/components.js +145 -0
- package/src/config.js +355 -0
- package/src/dev-server.js +559 -0
- package/src/main.js +87 -0
- package/src/mdx.js +254 -0
- package/src/node-loader.js +88 -0
- package/src/pagefind.js +99 -0
- package/src/pages.js +638 -0
- package/src/preview-server.js +58 -0
- package/src/public-assets.js +73 -0
- package/src/register-loader.js +29 -0
- package/src/rehype-plugins/link-resolve.js +89 -0
- package/src/rehype-plugins/methanol-ctx.js +89 -0
- package/src/renderer.js +25 -0
- package/src/rewind.js +117 -0
- package/src/stage-logger.js +59 -0
- package/src/state.js +159 -0
- package/src/virtual-module/inject.js +30 -0
- package/src/virtual-module/loader.js +116 -0
- package/src/virtual-module/pagefind.js +108 -0
- package/src/vite-plugins.js +173 -0
- package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
- package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
- package/themes/default/components/ThemeSearchBox.client.jsx +287 -0
- package/themes/default/components/ThemeSearchBox.static.jsx +41 -0
- package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
- package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
- package/themes/default/components/pre.client.jsx +84 -0
- package/themes/default/components/pre.jsx +27 -0
- package/themes/default/heading.jsx +35 -0
- package/themes/default/index.js +50 -0
- package/themes/default/page.jsx +249 -0
- package/themes/default/pages/404.mdx +8 -0
- package/themes/default/pages/index.mdx +9 -0
- package/themes/default/public/logo.png +0 -0
- package/themes/default/resources/style.css +1089 -0
|
@@ -0,0 +1,116 @@
|
|
|
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 function init(registry, R) {
|
|
22
|
+
if (!registry) return
|
|
23
|
+
|
|
24
|
+
async function $$rwnd(key, id, props, target = document.currentScript) {
|
|
25
|
+
const loader = registry[key]
|
|
26
|
+
|
|
27
|
+
if (!loader) {
|
|
28
|
+
target.remove()
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const renderer = (await loader()).default
|
|
33
|
+
|
|
34
|
+
if (!renderer) {
|
|
35
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
36
|
+
throw new Error(`[REWiND] Hydration failed! Component '${key}' does not export \`default\`!`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const findRewindAnchor = (node, token) => {
|
|
43
|
+
const marker = `{${token}}`
|
|
44
|
+
let current = node
|
|
45
|
+
while (current) {
|
|
46
|
+
let cursor = current.previousSibling
|
|
47
|
+
while (cursor) {
|
|
48
|
+
if (cursor.nodeType === 8 && cursor.nodeValue === marker) {
|
|
49
|
+
return cursor
|
|
50
|
+
}
|
|
51
|
+
cursor = cursor.previousSibling
|
|
52
|
+
}
|
|
53
|
+
current = current.parentNode
|
|
54
|
+
}
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const collectChildren = (fragment, token) => {
|
|
59
|
+
const startMarker = `[${token}[`
|
|
60
|
+
const endMarker = `]${token}]`
|
|
61
|
+
const walker = document.createTreeWalker(
|
|
62
|
+
fragment,
|
|
63
|
+
NodeFilter.SHOW_COMMENT,
|
|
64
|
+
null,
|
|
65
|
+
)
|
|
66
|
+
let start = null
|
|
67
|
+
let end = null
|
|
68
|
+
while (walker.nextNode()) {
|
|
69
|
+
const value = walker.currentNode.nodeValue
|
|
70
|
+
if (!start && value === startMarker) {
|
|
71
|
+
start = walker.currentNode
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
if (start && value === endMarker) {
|
|
75
|
+
end = walker.currentNode
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (!start || !end) return []
|
|
80
|
+
const range = document.createRange()
|
|
81
|
+
range.setStartAfter(start)
|
|
82
|
+
range.setEndBefore(end)
|
|
83
|
+
const childrenFragment = range.extractContents()
|
|
84
|
+
return Array.from(childrenFragment.childNodes)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const idStr = id.toString(16)
|
|
88
|
+
|
|
89
|
+
const anchor = findRewindAnchor(target, idStr)
|
|
90
|
+
if (!anchor || !anchor.parentNode) {
|
|
91
|
+
target.replaceWith(R.c(renderer, props))
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const range = document.createRange()
|
|
96
|
+
range.setStartAfter(anchor)
|
|
97
|
+
range.setEndBefore(target)
|
|
98
|
+
const between = range.extractContents()
|
|
99
|
+
const children = collectChildren(between, idStr)
|
|
100
|
+
const rendered = R.c(renderer, props, ...children)
|
|
101
|
+
target.replaceWith(rendered)
|
|
102
|
+
anchor.remove()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let loaded = []
|
|
106
|
+
if (window.$$rwnd) {
|
|
107
|
+
loaded = window.$$rwnd.$$loaded
|
|
108
|
+
}
|
|
109
|
+
window.$$rwnd = $$rwnd
|
|
110
|
+
|
|
111
|
+
if (loaded) {
|
|
112
|
+
for (let i = 0; i < loaded.length; i++) {
|
|
113
|
+
$$rwnd.apply(null, loaded[i])
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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
|
+
let pagefindInit = null
|
|
22
|
+
let pagefindUiInit = null
|
|
23
|
+
let pagefindUiReady = false
|
|
24
|
+
|
|
25
|
+
const dynamicImport = (path) => {
|
|
26
|
+
try {
|
|
27
|
+
const importer = new Function('p', 'return import(p)')
|
|
28
|
+
return importer(path)
|
|
29
|
+
} catch {
|
|
30
|
+
return import(/* @vite-ignore */path)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const loadPagefind = async () => {
|
|
35
|
+
if (pagefindInit) return pagefindInit
|
|
36
|
+
pagefindInit = new Promise((resolve) => {
|
|
37
|
+
if (typeof window === 'undefined') {
|
|
38
|
+
resolve(null)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
dynamicImport('/pagefind/pagefind.js')
|
|
42
|
+
.then((mod) => {
|
|
43
|
+
if (!mod) return resolve(null)
|
|
44
|
+
if (mod.search) return resolve(mod)
|
|
45
|
+
if (mod.default?.search) return resolve(mod.default)
|
|
46
|
+
return resolve(mod.default || mod)
|
|
47
|
+
})
|
|
48
|
+
.catch(() => resolve(null))
|
|
49
|
+
})
|
|
50
|
+
return pagefindInit
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const defaultUiOptions = {
|
|
54
|
+
element: '#pagefind-ui',
|
|
55
|
+
showImages: false,
|
|
56
|
+
resetStyles: false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const resolveTarget = (element) => {
|
|
60
|
+
if (!element) return null
|
|
61
|
+
if (typeof element === 'string') {
|
|
62
|
+
return document.querySelector(element)
|
|
63
|
+
}
|
|
64
|
+
return element
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const initPagefindUI = (options) => {
|
|
68
|
+
const PagefindUI = window.PagefindUI
|
|
69
|
+
if (!PagefindUI) return false
|
|
70
|
+
const merged = { ...defaultUiOptions, ...(options || {}) }
|
|
71
|
+
const target = resolveTarget(merged.element)
|
|
72
|
+
if (!target) return false
|
|
73
|
+
new PagefindUI(merged)
|
|
74
|
+
pagefindUiReady = true
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const loadPagefindUI = async (options = {}) => {
|
|
79
|
+
if (pagefindUiReady) return true
|
|
80
|
+
if (pagefindUiInit) return pagefindUiInit
|
|
81
|
+
pagefindUiInit = new Promise((resolve) => {
|
|
82
|
+
if (typeof window === 'undefined') {
|
|
83
|
+
resolve(false)
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const done = (value) => resolve(Boolean(value))
|
|
87
|
+
if (window.PagefindUI) {
|
|
88
|
+
done(initPagefindUI(options))
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
const script = document.createElement('script')
|
|
92
|
+
script.src = '/pagefind/pagefind-ui.js'
|
|
93
|
+
script.async = true
|
|
94
|
+
script.onload = () => done(initPagefindUI(options))
|
|
95
|
+
script.onerror = () => done(false)
|
|
96
|
+
document.head.appendChild(script)
|
|
97
|
+
})
|
|
98
|
+
return pagefindUiInit
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const focusPagefindInput = () => {
|
|
102
|
+
if (typeof document === 'undefined') return
|
|
103
|
+
const input =
|
|
104
|
+
document.querySelector('#pagefind-ui input[type="search"]') || document.querySelector('#pagefind-ui input')
|
|
105
|
+
if (input) {
|
|
106
|
+
input.focus()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
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 { createRequire } from 'node:module'
|
|
22
|
+
import { readFile } from 'node:fs/promises'
|
|
23
|
+
import { existsSync } from 'node:fs'
|
|
24
|
+
import { resolve } from 'node:path'
|
|
25
|
+
import { normalizePath } from 'vite'
|
|
26
|
+
import { state } from './state.js'
|
|
27
|
+
import { genRegistryScript } from './components.js'
|
|
28
|
+
import { INJECT_SCRIPT, LOADER_SCRIPT, PAGEFIND_SCRIPT } from './assets.js'
|
|
29
|
+
import { projectRequire } from './node-loader.js'
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url)
|
|
32
|
+
|
|
33
|
+
export const methanolVirtualHtmlPlugin = (htmlCache) => {
|
|
34
|
+
const prefix = normalizePath(state.VIRTUAL_HTML_OUTPUT_ROOT + '/')
|
|
35
|
+
return {
|
|
36
|
+
name: 'methanol-virtual-html',
|
|
37
|
+
resolveId(id) {
|
|
38
|
+
const normalized = normalizePath(id)
|
|
39
|
+
if (normalized.startsWith(prefix) && htmlCache.has(normalized)) {
|
|
40
|
+
return normalized
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
load(id) {
|
|
44
|
+
const normalized = normalizePath(id)
|
|
45
|
+
if (normalized.startsWith(prefix) && htmlCache.has(normalized)) {
|
|
46
|
+
return htmlCache.get(normalized) ?? null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const methanolPreviewRoutingPlugin = (distDir, notFoundPath) => ({
|
|
53
|
+
name: 'methanol-preview-routing',
|
|
54
|
+
configurePreviewServer(server) {
|
|
55
|
+
return () => {
|
|
56
|
+
let cachedHtml = null
|
|
57
|
+
const loadNotFoundHtml = async () => {
|
|
58
|
+
if (!existsSync(notFoundPath)) return null
|
|
59
|
+
if (cachedHtml != null) return cachedHtml
|
|
60
|
+
cachedHtml = await readFile(notFoundPath, 'utf-8')
|
|
61
|
+
return cachedHtml
|
|
62
|
+
}
|
|
63
|
+
const handler = async (req, res, next) => {
|
|
64
|
+
if (!req.url || req.method !== 'GET') {
|
|
65
|
+
return next()
|
|
66
|
+
}
|
|
67
|
+
const accept = req.headers.accept || ''
|
|
68
|
+
let pathname = req.url
|
|
69
|
+
try {
|
|
70
|
+
pathname = new URL(req.url, 'http://methanol').pathname
|
|
71
|
+
pathname = decodeURIComponent(pathname)
|
|
72
|
+
} catch {}
|
|
73
|
+
const hasTrailingSlash = pathname.endsWith('/') && pathname !== '/'
|
|
74
|
+
if (pathname.includes('.') && !pathname.endsWith('.html')) {
|
|
75
|
+
return next()
|
|
76
|
+
}
|
|
77
|
+
if (!pathname.endsWith('.html') && !accept.includes('text/html')) {
|
|
78
|
+
return next()
|
|
79
|
+
}
|
|
80
|
+
const resolveHtmlPath = (value) => resolve(distDir, value.replace(/^\//, ''))
|
|
81
|
+
const candidates = []
|
|
82
|
+
if (pathname === '/' || pathname === '') {
|
|
83
|
+
candidates.push(resolveHtmlPath('/index.html'))
|
|
84
|
+
} else if (pathname.endsWith('.html')) {
|
|
85
|
+
candidates.push(resolveHtmlPath(pathname))
|
|
86
|
+
} else {
|
|
87
|
+
candidates.push(resolveHtmlPath(`${pathname}.html`))
|
|
88
|
+
candidates.push(resolveHtmlPath(`${pathname}/index.html`))
|
|
89
|
+
}
|
|
90
|
+
if (candidates.some((candidate) => existsSync(candidate))) {
|
|
91
|
+
return next()
|
|
92
|
+
}
|
|
93
|
+
const html = await loadNotFoundHtml()
|
|
94
|
+
if (!html) {
|
|
95
|
+
return next()
|
|
96
|
+
}
|
|
97
|
+
res.statusCode = 404
|
|
98
|
+
res.setHeader('Content-Type', 'text/html')
|
|
99
|
+
res.end(html)
|
|
100
|
+
}
|
|
101
|
+
if (Array.isArray(server.middlewares.stack)) {
|
|
102
|
+
server.middlewares.stack.unshift({ route: '', handle: handler })
|
|
103
|
+
} else {
|
|
104
|
+
server.middlewares.use(handler)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const virtualModulePrefix = '/.methanol_virtual_module/'
|
|
111
|
+
const resolvedVirtualModulePrefix = '\0' + virtualModulePrefix
|
|
112
|
+
|
|
113
|
+
const virtualModuleMap = {
|
|
114
|
+
get 'registry.js'() {
|
|
115
|
+
return `export const registry = ${genRegistryScript()}`
|
|
116
|
+
},
|
|
117
|
+
'loader.js': LOADER_SCRIPT,
|
|
118
|
+
'inject.js': INJECT_SCRIPT,
|
|
119
|
+
'pagefind.js': PAGEFIND_SCRIPT
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const getModuleIdSegment = (id, start) => {
|
|
123
|
+
return new URL(id.slice(start), 'http://methanol').pathname.slice(1)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const methanolResolverPlugin = () => {
|
|
127
|
+
return {
|
|
128
|
+
name: 'methanol-resolver',
|
|
129
|
+
resolveId(id) {
|
|
130
|
+
if (id === 'refui' || id.startsWith('refui/')) {
|
|
131
|
+
try {
|
|
132
|
+
return projectRequire.resolve(id)
|
|
133
|
+
} catch {
|
|
134
|
+
return require.resolve(id)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (id === 'methanol' || id.startsWith('methanol/')) {
|
|
139
|
+
return require.resolve(id)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (id.startsWith(virtualModulePrefix)) {
|
|
143
|
+
const _moduleId = getModuleIdSegment(id, virtualModulePrefix.length)
|
|
144
|
+
if (Object.prototype.hasOwnProperty.call(virtualModuleMap, _moduleId)) {
|
|
145
|
+
return '\0' + id
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (state.RESOURCES.length) {
|
|
150
|
+
const { pathname, search } = new URL(id, 'http://methanol')
|
|
151
|
+
for (const entry of state.RESOURCES) {
|
|
152
|
+
const { find, replacement } = entry
|
|
153
|
+
if (!find || !replacement) continue
|
|
154
|
+
if (typeof find === 'string') {
|
|
155
|
+
if (pathname === find || pathname.startsWith(`${find}/`)) {
|
|
156
|
+
return `${replacement}${pathname.slice(find.length)}${search}`
|
|
157
|
+
}
|
|
158
|
+
continue
|
|
159
|
+
}
|
|
160
|
+
if (find instanceof RegExp && find.test(pathname)) {
|
|
161
|
+
return `${pathname.replace(find, replacement)}${search}`
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
load(id) {
|
|
167
|
+
if (id.startsWith(resolvedVirtualModulePrefix)) {
|
|
168
|
+
const _moduleId = getModuleIdSegment(id, resolvedVirtualModulePrefix.length)
|
|
169
|
+
return virtualModuleMap[_moduleId]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -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 LightIcon = () => (
|
|
24
|
+
<svg
|
|
25
|
+
attr:width="18"
|
|
26
|
+
attr:height="18"
|
|
27
|
+
attr:viewBox="0 0 24 24"
|
|
28
|
+
attr:fill="none"
|
|
29
|
+
attr:stroke="currentColor"
|
|
30
|
+
attr:stroke-width="2"
|
|
31
|
+
attr:stroke-linecap="round"
|
|
32
|
+
attr:stroke-linejoin="round"
|
|
33
|
+
>
|
|
34
|
+
<circle attr:cx="12" attr:cy="12" attr:r="5"></circle>
|
|
35
|
+
<line attr:x1="12" attr:y1="1" attr:x2="12" attr:y2="3"></line>
|
|
36
|
+
<line attr:x1="12" attr:y1="21" attr:x2="12" attr:y2="23"></line>
|
|
37
|
+
<line attr:x1="4.22" attr:y1="4.22" attr:x2="5.64" attr:y2="5.64"></line>
|
|
38
|
+
<line attr:x1="18.36" attr:y1="18.36" attr:x2="19.78" attr:y2="19.78"></line>
|
|
39
|
+
<line attr:x1="1" attr:y1="12" attr:x2="3" attr:y2="12"></line>
|
|
40
|
+
<line attr:x1="21" attr:y1="12" attr:x2="23" attr:y2="12"></line>
|
|
41
|
+
<line attr:x1="4.22" attr:y1="19.78" attr:x2="5.64" attr:y2="18.36"></line>
|
|
42
|
+
<line attr:x1="18.36" attr:y1="5.64" attr:x2="19.78" attr:y2="4.22"></line>
|
|
43
|
+
</svg>
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const DarkIcon = () => (
|
|
47
|
+
<svg
|
|
48
|
+
attr:width="18"
|
|
49
|
+
attr:height="18"
|
|
50
|
+
attr:viewBox="0 0 24 24"
|
|
51
|
+
attr:fill="none"
|
|
52
|
+
attr:stroke="currentColor"
|
|
53
|
+
attr:stroke-width="2"
|
|
54
|
+
attr:stroke-linecap="round"
|
|
55
|
+
attr:stroke-linejoin="round"
|
|
56
|
+
>
|
|
57
|
+
<path attr:d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
58
|
+
</svg>
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
export default function () {
|
|
62
|
+
const theme = signal('dark')
|
|
63
|
+
|
|
64
|
+
// Initialize theme from localStorage or system preference
|
|
65
|
+
if (typeof window !== 'undefined') {
|
|
66
|
+
const savedTheme = localStorage.getItem('methanol-theme')
|
|
67
|
+
const systemTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
|
|
68
|
+
theme.value = savedTheme || systemTheme
|
|
69
|
+
document.documentElement.classList.toggle('light', theme.value === 'light')
|
|
70
|
+
document.documentElement.classList.toggle('dark', theme.value === 'dark')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const toggle = () => {
|
|
74
|
+
theme.value = theme.value === 'light' ? 'dark' : 'light'
|
|
75
|
+
localStorage.setItem('methanol-theme', theme.value)
|
|
76
|
+
document.documentElement.classList.toggle('light', theme.value === 'light')
|
|
77
|
+
document.documentElement.classList.toggle('dark', theme.value === 'dark')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const CurrentIcon = $(() => {
|
|
81
|
+
if (theme.value === 'light') {
|
|
82
|
+
return LightIcon
|
|
83
|
+
} else {
|
|
84
|
+
return DarkIcon
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div class="theme-switch-container">
|
|
90
|
+
<button class="theme-switch-btn" on:click={toggle} aria-label="Toggle theme">
|
|
91
|
+
<CurrentIcon />
|
|
92
|
+
</button>
|
|
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
|
+
}
|