methanol 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +58 -0
  3. package/banner.txt +6 -0
  4. package/bin/methanol.js +24 -0
  5. package/index.js +22 -0
  6. package/package.json +51 -9
  7. package/src/assets.js +30 -0
  8. package/src/build-system.js +200 -0
  9. package/src/components.js +145 -0
  10. package/src/config.js +396 -0
  11. package/src/dev-server.js +632 -0
  12. package/src/main.js +133 -0
  13. package/src/mdx.js +406 -0
  14. package/src/node-loader.js +88 -0
  15. package/src/pagefind.js +107 -0
  16. package/src/pages.js +771 -0
  17. package/src/preview-server.js +58 -0
  18. package/src/public-assets.js +73 -0
  19. package/src/register-loader.js +29 -0
  20. package/src/rehype-plugins/link-resolve.js +116 -0
  21. package/src/rehype-plugins/methanol-ctx.js +89 -0
  22. package/src/renderer.js +25 -0
  23. package/src/rewind.js +117 -0
  24. package/src/stage-logger.js +59 -0
  25. package/src/state.js +179 -0
  26. package/src/virtual-module/inject.js +30 -0
  27. package/src/virtual-module/loader.js +116 -0
  28. package/src/virtual-module/pagefind.js +108 -0
  29. package/src/vite-plugins.js +173 -0
  30. package/themes/default/components/ThemeAccentSwitch.client.jsx +95 -0
  31. package/themes/default/components/ThemeAccentSwitch.static.jsx +23 -0
  32. package/themes/default/components/ThemeColorSwitch.client.jsx +95 -0
  33. package/themes/default/components/ThemeColorSwitch.static.jsx +23 -0
  34. package/themes/default/components/ThemeSearchBox.client.jsx +324 -0
  35. package/themes/default/components/ThemeSearchBox.static.jsx +40 -0
  36. package/themes/default/components/ThemeToCContainer.client.jsx +154 -0
  37. package/themes/default/components/ThemeToCContainer.static.jsx +61 -0
  38. package/themes/default/components/pre.client.jsx +84 -0
  39. package/themes/default/components/pre.static.jsx +27 -0
  40. package/themes/default/heading.jsx +35 -0
  41. package/themes/default/index.js +41 -0
  42. package/themes/default/page.jsx +303 -0
  43. package/themes/default/pages/404.mdx +8 -0
  44. package/themes/default/pages/index.mdx +31 -0
  45. package/themes/default/public/favicon.png +0 -0
  46. package/themes/default/public/logo.png +0 -0
  47. package/themes/default/sources/prefetch.js +49 -0
  48. package/themes/default/sources/style.css +1660 -0
package/src/state.js ADDED
@@ -0,0 +1,179 @@
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 { resolve } from 'path'
22
+ import yargs from 'yargs/yargs'
23
+ import { hideBin } from 'yargs/helpers'
24
+
25
+ const PROJECT_ROOT = resolve('.')
26
+
27
+ const withCommonOptions = (y) =>
28
+ y
29
+ .positional('input', {
30
+ describe: 'Pages directory',
31
+ type: 'string',
32
+ nargs: 1
33
+ })
34
+ .positional('output', {
35
+ describe: 'Output directory',
36
+ type: 'string',
37
+ nargs: 1
38
+ })
39
+ .option('input', {
40
+ alias: 'i',
41
+ describe: 'Pages directory',
42
+ type: 'string',
43
+ requiresArg: true,
44
+ nargs: 1
45
+ })
46
+ .option('components', {
47
+ describe: 'Components directory',
48
+ type: 'string',
49
+ requiresArg: true,
50
+ nargs: 1
51
+ })
52
+ .option('assets', {
53
+ describe: 'Assets/public directory',
54
+ type: 'string',
55
+ requiresArg: true,
56
+ nargs: 1
57
+ })
58
+ .option('output', {
59
+ alias: 'o',
60
+ describe: 'Output directory',
61
+ type: 'string',
62
+ requiresArg: true,
63
+ nargs: 1
64
+ })
65
+ .option('config', {
66
+ alias: 'c',
67
+ describe: 'Config file path',
68
+ type: 'string',
69
+ requiresArg: true,
70
+ nargs: 1
71
+ })
72
+ .option('site-name', {
73
+ describe: 'Site name override',
74
+ type: 'string',
75
+ requiresArg: true,
76
+ nargs: 1
77
+ })
78
+ .option('port', {
79
+ describe: 'Port for dev/preview',
80
+ type: 'number',
81
+ requiresArg: true,
82
+ nargs: 1
83
+ })
84
+ .option('host', {
85
+ describe: 'Host for dev/preview',
86
+ type: 'string',
87
+ coerce: (value) => {
88
+ if (value == null) return null
89
+ if (value === true || value === '' || value === 'true') return true
90
+ return value
91
+ }
92
+ })
93
+ .option('intermediate-dir', {
94
+ describe: 'Write intermediate HTML output to a directory',
95
+ type: 'string',
96
+ requiresArg: true,
97
+ nargs: 1
98
+ })
99
+ .option('emit-intermediate', {
100
+ describe: 'Emit intermediate HTML output to the default build dir',
101
+ type: 'boolean',
102
+ default: false
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
+ })
117
+
118
+ const parser = yargs(hideBin(process.argv))
119
+ .scriptName('methanol')
120
+ .usage('Usage: $0 <command> [options]')
121
+ .command('dev [input]', 'Start the Methanol dev server', withCommonOptions)
122
+ .command('build [input] [output]', 'Build the static site', withCommonOptions)
123
+ .command('serve [input] [output]', 'Serve the production build', withCommonOptions)
124
+ .command('preview [input] [output]', false, withCommonOptions)
125
+ .help()
126
+ .wrap(null)
127
+
128
+ const argv = parser.parseSync()
129
+
130
+ export const cli = {
131
+ argv,
132
+ command: argv._[0] ? String(argv._[0]) : null,
133
+ showHelp: () => parser.showHelp(),
134
+ CLI_INTERMEDIATE_DIR: argv['intermediate-dir'] || null,
135
+ CLI_EMIT_INTERMEDIATE: Boolean(argv['emit-intermediate']),
136
+ CLI_HOST: argv.host ?? null,
137
+ CLI_PORT: typeof argv.port === 'number' ? argv.port : null,
138
+ CLI_PAGES_DIR: argv.input || null,
139
+ CLI_COMPONENTS_DIR: argv.components || null,
140
+ CLI_ASSETS_DIR: argv.assets || null,
141
+ CLI_OUTPUT_DIR: argv.output || null,
142
+ CLI_CONFIG_PATH: argv.config || null,
143
+ CLI_SITE_NAME: argv['site-name'] || null,
144
+ CLI_CODE_HIGHLIGHTING: typeof argv['code-highlighting'] === 'boolean' ? argv['code-highlighting'] : null
145
+ }
146
+
147
+ export const state = {
148
+ PROJECT_ROOT,
149
+ ROOT_DIR: PROJECT_ROOT,
150
+ SITE_NAME: 'Methanol Site',
151
+ PAGES_DIR: resolve(PROJECT_ROOT, 'pages'),
152
+ COMPONENTS_DIR: resolve(PROJECT_ROOT, 'components'),
153
+ STATIC_DIR: resolve(PROJECT_ROOT, 'public'),
154
+ BUILD_DIR: resolve(PROJECT_ROOT, 'build'),
155
+ DIST_DIR: resolve(PROJECT_ROOT, 'dist'),
156
+ VIRTUAL_HTML_OUTPUT_ROOT: PROJECT_ROOT,
157
+ INTERMEDIATE_DIR: null,
158
+ THEME_COMPONENTS_DIR: null,
159
+ THEME_PAGES_DIR: null,
160
+ THEME_PUBLIC_DIR: null,
161
+ THEME_ENV: null,
162
+ USER_THEME: null,
163
+ USER_VITE_CONFIG: null,
164
+ USER_MDX_CONFIG: null,
165
+ USER_PUBLIC_OVERRIDE: false,
166
+ SOURCES: [],
167
+ PAGEFIND_ENABLED: false,
168
+ PAGEFIND_OPTIONS: null,
169
+ PAGEFIND_BUILD: null,
170
+ USER_PRE_BUILD_HOOKS: [],
171
+ USER_POST_BUILD_HOOKS: [],
172
+ THEME_PRE_BUILD_HOOKS: [],
173
+ THEME_POST_BUILD_HOOKS: [],
174
+ STARRY_NIGHT_ENABLED: false,
175
+ STARRY_NIGHT_OPTIONS: null,
176
+ CURRENT_MODE: 'production',
177
+ RESOLVED_MDX_CONFIG: undefined,
178
+ RESOLVED_VITE_CONFIG: undefined
179
+ }
@@ -0,0 +1,30 @@
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 { createDOMRenderer } from 'refui/dom'
22
+ import { defaults } from 'refui/browser'
23
+ import { lazy } from 'refui'
24
+ import { init } from '/.methanol_virtual_module/loader.js'
25
+
26
+ const reg = import('/.methanol_virtual_module/registry.js')
27
+
28
+ const R = createDOMRenderer(defaults)
29
+
30
+ reg.then((m) => init(m.registry, R))
@@ -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.SOURCES.length) {
150
+ const { pathname, search } = new URL(id, 'http://methanol')
151
+ for (const entry of state.SOURCES) {
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 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
+ }