@x-withu/page-withu 1.1.0 → 1.1.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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p align="center">
4
4
  <strong>
5
- 一款轻量、优雅的个人主页生成器
5
+ 一款轻量的个人主页生成器
6
6
  </strong>
7
7
  </p>
8
8
 
@@ -41,15 +41,14 @@
41
41
  # 安装 page-withu
42
42
  npm install -g @x-withu/page-withu
43
43
 
44
- # 创建项目,默认配置会写入 config.js,后续可自行编辑
44
+ # 创建项目
45
45
  page-withu new my-homepage
46
-
47
46
  cd my-homepage
48
47
 
49
48
  # 本地预览
50
49
  page-withu serve
51
50
 
52
- # 构建分发产物
51
+ # 生成静态网站
53
52
  page-withu build
54
53
  ```
55
54
 
package/bin/page-withu.js CHANGED
@@ -1,31 +1,58 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import { constants as fsConstants } from 'node:fs';
3
5
  import { createRequire } from 'node:module';
4
- import { Command } from 'commander';
5
- import fs from 'fs-extra';
6
6
  import path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import chalk from 'chalk';
9
- import ora from 'ora';
10
8
 
11
- const pkg = await fs.readJson(new URL('../package.json', import.meta.url));
9
+ const pkg = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url), 'utf8'));
12
10
 
13
11
  const __filename = fileURLToPath(import.meta.url);
14
12
  const __dirname = path.dirname(__filename);
15
13
  const packageRoot = path.resolve(__dirname, '..');
16
- const templateDir = path.join(packageRoot, 'template');
17
- const viteConfigPath = path.join(templateDir, 'vite.config.js');
18
- const require = createRequire(import.meta.url);
19
- const vitePackageDir = path.dirname(require.resolve('vite/package.json'));
14
+ const sourceDir = path.join(packageRoot, 'src');
15
+ const defaultsDir = path.join(sourceDir, 'defaults');
16
+ const viteConfigPath = path.join(sourceDir, 'vite.config.js');
17
+ const packageRequire = createRequire(import.meta.url);
18
+ const vitePackageDir = path.dirname(packageRequire.resolve('vite/package.json'));
20
19
  const viteBin = path.join(vitePackageDir, 'bin/vite.js');
21
- const program = new Command();
20
+
21
+ const cyan = (text) => `\x1b[36m${text}\x1b[0m`;
22
+ const green = (text) => `\x1b[32m${text}\x1b[0m`;
23
+ const red = (text) => `\x1b[31m${text}\x1b[0m`;
24
+ const yellow = (text) => `\x1b[33m${text}\x1b[0m`;
25
+
26
+ async function exists(filePath) {
27
+ try {
28
+ await fs.access(filePath, fsConstants.F_OK);
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ function run(command, args, options = {}) {
36
+ return new Promise((resolve, reject) => {
37
+ const child = spawn(command, args, { stdio: 'inherit', ...options });
38
+ child.on('error', reject);
39
+ child.on('exit', (code, signal) => {
40
+ if (signal) {
41
+ process.kill(process.pid, signal);
42
+ return;
43
+ }
44
+ if (code === 0) resolve();
45
+ else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
46
+ });
47
+ });
48
+ }
22
49
 
23
50
  async function detectProject(projectDir) {
24
51
  const required = ['config.js', 'content/index.md', 'content/domains.md'];
25
52
  const missing = [];
26
53
 
27
54
  for (const item of required) {
28
- if (!await fs.pathExists(path.join(projectDir, item))) missing.push(item);
55
+ if (!await exists(path.join(projectDir, item))) missing.push(item);
29
56
  }
30
57
 
31
58
  return missing;
@@ -35,26 +62,26 @@ async function createProject(projectName = 'my-homepage') {
35
62
  const targetDir = projectName.trim() || 'my-homepage';
36
63
  const fullPath = path.resolve(process.cwd(), targetDir);
37
64
 
38
- if (await fs.pathExists(fullPath)) {
39
- console.error(chalk.red(`\nError: Directory ${targetDir} already exists.`));
65
+ if (await exists(fullPath)) {
66
+ process.stderr.write(red(`\nError: Directory ${targetDir} already exists.\n`));
40
67
  process.exit(1);
41
68
  }
42
69
 
43
- const spinner = ora('Creating project...').start();
70
+ process.stdout.write('Creating project...\n');
44
71
 
45
72
  try {
46
- await fs.ensureDir(fullPath);
47
- await fs.copy(path.join(templateDir, 'config.js'), path.join(fullPath, 'config.js'));
48
- await fs.copy(path.join(templateDir, 'content'), path.join(fullPath, 'content'));
49
- await fs.ensureDir(path.join(fullPath, 'content/blog'));
50
-
51
- spinner.succeed(chalk.green(`Successfully created PageWithU project in ${targetDir}!`));
52
- console.log('\nNext steps:');
53
- console.log(chalk.cyan(` cd ${targetDir}`));
54
- console.log(chalk.cyan(' page-withu serve'));
55
- console.log(chalk.cyan(' page-withu build\n'));
73
+ await fs.mkdir(fullPath, { recursive: true });
74
+ await fs.copyFile(path.join(defaultsDir, 'config.js'), path.join(fullPath, 'config.js'));
75
+ await fs.cp(path.join(defaultsDir, 'content'), path.join(fullPath, 'content'), { recursive: true });
76
+ await fs.mkdir(path.join(fullPath, 'content/blog'), { recursive: true });
77
+
78
+ process.stdout.write(`${green(`Successfully created PageWithU project in ${targetDir}!`)}\n`);
79
+ process.stdout.write('\nNext steps:\n');
80
+ process.stdout.write(cyan(` cd ${targetDir}\n`));
81
+ process.stdout.write(cyan(' page-withu serve\n'));
82
+ process.stdout.write(cyan(' page-withu build\n\n'));
56
83
  } catch (err) {
57
- spinner.fail(chalk.red('Failed to create project.'));
84
+ process.stderr.write(`${red('Failed to create project.')}\n`);
58
85
  console.error(err);
59
86
  process.exit(1);
60
87
  }
@@ -64,52 +91,107 @@ async function runVite(command, args) {
64
91
  const projectRoot = process.cwd();
65
92
  const missing = await detectProject(projectRoot);
66
93
  if (missing.length) {
67
- console.error(chalk.red('Current directory does not look like a PageWithU project.'));
68
- console.error(chalk.yellow(`Missing: ${missing.join(', ')}`));
94
+ process.stderr.write(`${red('Current directory does not look like a PageWithU project.')}\n`);
95
+ process.stderr.write(`${yellow(`Missing: ${missing.join(', ')}`)}\n`);
69
96
  process.exit(1);
70
97
  }
71
98
 
72
99
  const viteArgs = command === 'serve'
73
100
  ? ['--config', viteConfigPath, ...args]
74
101
  : ['build', '--config', viteConfigPath, ...args];
75
- const child = spawn(process.execPath, [viteBin, ...viteArgs], {
76
- cwd: projectRoot,
77
- stdio: 'inherit',
78
- env: {
79
- ...process.env,
80
- PAGE_WITHU_PROJECT_ROOT: projectRoot,
81
- },
82
- });
83
102
 
84
- child.on('exit', (code, signal) => {
85
- if (signal) {
86
- process.kill(process.pid, signal);
87
- return;
88
- }
89
- process.exit(code ?? 1);
90
- });
103
+ try {
104
+ await run(process.execPath, [viteBin, ...viteArgs], {
105
+ cwd: projectRoot,
106
+ env: {
107
+ ...process.env,
108
+ PAGE_WITHU_PROJECT_ROOT: projectRoot,
109
+ },
110
+ });
111
+ } catch (err) {
112
+ console.error(err.message);
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ function printMainHelp() {
118
+ console.log(`Usage: page-withu [options] [command]
119
+
120
+ CLI to scaffold, preview, and build a lightweight personal homepage
121
+
122
+ Options:
123
+ -V, --version output the version number
124
+ -h, --help display help for command
125
+
126
+ Commands:
127
+ new [project-name] create a new PageWithU project with the default content and config
128
+ serve [args...] start local preview for the current PageWithU project
129
+ build [args...] build dist assets for the current PageWithU project
130
+ help [command] display help for command`);
131
+ }
132
+
133
+ function printCommandHelp(command) {
134
+ if (command === 'new') {
135
+ console.log(`Usage: page-withu new [options] [project-name]
136
+
137
+ create a new PageWithU project with the default content and config
138
+
139
+ Options:
140
+ -h, --help display help for command`);
141
+ return;
142
+ }
143
+ if (command === 'serve') {
144
+ console.log(`Usage: page-withu serve [options] [args...]
145
+
146
+ start local preview for the current PageWithU project
147
+
148
+ Options:
149
+ -h, --help display help for command`);
150
+ return;
151
+ }
152
+ if (command === 'build') {
153
+ console.log(`Usage: page-withu build [options] [args...]
154
+
155
+ build dist assets for the current PageWithU project
156
+
157
+ Options:
158
+ -h, --help display help for command`);
159
+ return;
160
+ }
161
+ printMainHelp();
162
+ }
163
+
164
+ async function main(argv) {
165
+ const [command, ...args] = argv;
166
+
167
+ if (!command || command === '--help' || command === '-h') {
168
+ printMainHelp();
169
+ return;
170
+ }
171
+ if (command === '--version' || command === '-V') {
172
+ console.log(pkg.version);
173
+ return;
174
+ }
175
+ if (command === 'help') {
176
+ printCommandHelp(args[0]);
177
+ return;
178
+ }
179
+ if (args.includes('--help') || args.includes('-h')) {
180
+ printCommandHelp(command);
181
+ return;
182
+ }
183
+ if (command === 'new') {
184
+ await createProject(args[0]);
185
+ return;
186
+ }
187
+ if (command === 'serve' || command === 'build') {
188
+ await runVite(command, args);
189
+ return;
190
+ }
191
+
192
+ process.stderr.write(`Unknown command: ${command}\n\n`);
193
+ printMainHelp();
194
+ process.exit(1);
91
195
  }
92
196
 
93
- program
94
- .name('page-withu')
95
- .description('CLI to scaffold, preview, and build a lightweight personal homepage')
96
- .version(pkg.version);
97
-
98
- program
99
- .command('new [project-name]')
100
- .description('create a new PageWithU project with the default content and config')
101
- .action(createProject);
102
-
103
- program
104
- .command('serve [args...]')
105
- .description('start local preview for the current PageWithU project')
106
- .allowUnknownOption(true)
107
- .action((args = []) => runVite('serve', args));
108
-
109
- program
110
- .command('build [args...]')
111
- .description('build dist assets for the current PageWithU project')
112
- .allowUnknownOption(true)
113
- .action((args = []) => runVite('build', args));
114
-
115
- program.parse();
197
+ await main(process.argv.slice(2));
package/docs/develop.md CHANGED
@@ -9,23 +9,26 @@ Page With U 的目标是保持轻量、清晰、容易定制。开发时建议
9
9
  - 优先修改已有文件,不要为了小功能引入复杂架构。
10
10
  - 模板默认配置要适合大多数用户开箱即用。
11
11
  - 文档中的用户流程要始终和 CLI 行为保持一致。
12
- - 修改 `template/` 后,需要用模板本身或新生成项目执行一次 `page-withu build`。
13
- - 不要引入大型 UI 框架,样式优先维护在 `template/src/styles/main.css`。
14
- - Markdown 能力优先在 `template/vite.config.js` 中集中处理,避免分散到多个组件。
12
+ - 修改包内运行时或默认内容后,需要用源码目录或新生成项目执行一次 `page-withu build`。
13
+ - 不要引入大型 UI 框架,样式优先维护在 `src/app/src/styles/main.css`。
14
+ - Markdown 能力优先在 `src/vite.config.js` 中集中处理,避免分散到多个组件。
15
15
 
16
16
  项目结构:
17
17
 
18
18
  ```text
19
19
  page-withu/
20
20
  ├── bin/
21
- │ └── page-withu.js # CLI 入口
22
- ├── docs/ # 使用、部署、开发文档
23
- ├── template/
24
- │ ├── config.js # 新项目默认配置
25
- │ ├── content/ # 新项目默认内容
26
- ├── src/ # 包内 Vue 运行时源码
27
- └── vite.config.js # 包内 Markdown 渲染与静态路由生成
28
- ├── package.json # page-withu CLI 与运行时依赖配置
21
+ │ └── page-withu.js # CLI 入口
22
+ ├── docs/ # 使用、部署、开发文档
23
+ ├── src/
24
+ │ ├── app/ # 包内 Vue 运行时应用
25
+ ├── index.html
26
+ │ └── src/
27
+ ├── defaults/ # 新项目默认配置和内容
28
+ │ │ ├── config.js
29
+ │ │ └── content/
30
+ │ └── vite.config.js # 包内 Markdown 渲染与静态路由生成
31
+ ├── package.json # page-withu CLI 与运行时依赖配置
29
32
  └── README.md
30
33
  ```
31
34
 
@@ -70,8 +73,8 @@ page-withu new my-homepage
70
73
  调整 CLI 行为时,通常需要同步修改:
71
74
 
72
75
  - `bin/page-withu.js` 中的 commander command。
73
- - 必要时修改 `template/config.js` 的默认字段。
74
- - 必要时修改模板中读取配置的 Vue 组件。
76
+ - 必要时修改 `src/defaults/config.js` 的默认字段。
77
+ - 必要时修改包内运行时中读取配置的 Vue 组件。
75
78
  - 必要时更新 `README.md` 和 `docs/setup.md`。
76
79
 
77
80
  测试脚手架生成流程:
@@ -86,30 +89,36 @@ node /path/to/page-withu/bin/page-withu.js build
86
89
 
87
90
  如果构建成功,说明内容型项目可以通过包内运行时正常生成。
88
91
 
89
- ## 开发 Template
92
+ ## 开发包内运行时与默认内容
90
93
 
91
- 模板应用位于:
94
+ 包内 Vue 运行时应用位于:
92
95
 
93
96
  ```text
94
- template/
97
+ src/app/
95
98
  ```
96
99
 
97
- 启动模板开发服务:
100
+ 新项目默认配置和内容位于:
101
+
102
+ ```text
103
+ src/defaults/
104
+ ```
105
+
106
+ 启动运行时开发服务:
98
107
 
99
108
  ```bash
100
- cd template
101
- node ../bin/page-withu.js serve
109
+ cd src/defaults
110
+ node ../../bin/page-withu.js serve
102
111
  ```
103
112
 
104
113
  常见开发位置:
105
114
 
106
- - `template/src/app.vue`:主页面路由、布局、浏览器标签页标题和 favicon。
107
- - `template/src/components/blog-list.vue`:博客列表、分页、搜索、标签和时间线。
108
- - `template/src/components/blog-detail.vue`:文章详情、ToC、图片放大、Mermaid 渲染。
109
- - `template/src/styles/main.css`:全局样式、亮暗模式、响应式布局、Markdown 样式。
110
- - `template/vite.config.js`:Markdown 插件、callout、代码高亮、静态 `.html` 路由生成。
111
- - `template/config.js`:站点默认配置。
112
- - `template/content/`:默认内容样例。
115
+ - `src/app/src/app.vue`:主页面路由、布局、浏览器标签页标题和 favicon。
116
+ - `src/app/src/components/blog_list.vue`:博客列表、分页、搜索、标签和时间线。
117
+ - `src/app/src/components/blog_detail.vue`:文章详情、ToC、图片放大、Mermaid 渲染。
118
+ - `src/app/src/styles/main.css`:全局样式、亮暗模式、响应式布局、Markdown 样式。
119
+ - `src/vite.config.js`:Markdown 插件、callout、代码高亮、静态 `.html` 路由生成。
120
+ - `src/defaults/config.js`:站点默认配置。
121
+ - `src/defaults/content/`:默认内容样例。
113
122
 
114
123
  修改 Markdown 渲染逻辑后,建议检查这些能力:
115
124
 
@@ -118,11 +127,11 @@ node ../bin/page-withu.js serve
118
127
  - Mermaid、LaTeX、脚注、Emoji 是否能正常渲染。
119
128
  - callout 是否在亮色和暗色模式下都可读。
120
129
 
121
- 构建模板:
130
+ 构建默认内容项目:
122
131
 
123
132
  ```bash
124
- cd template
125
- node ../bin/page-withu.js build
133
+ cd src/defaults
134
+ node ../../bin/page-withu.js build
126
135
  ```
127
136
 
128
137
  如果改动影响脚手架生成后的项目,还需要用 CLI 重新生成一个临时项目并构建。
@@ -150,7 +159,10 @@ npm run pack:check
150
159
  确认包中至少包含:
151
160
 
152
161
  - `bin/page-withu.js`
153
- - `template/`
162
+ - `src/app/`
163
+ - `src/defaults/config.js`
164
+ - `src/defaults/content/`
165
+ - `src/vite.config.js`
154
166
  - `docs/`
155
167
  - `package.json`
156
168
  - `README.md`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@x-withu/page-withu",
3
- "version": "1.1.0",
4
- "description": "A lightweight, elegant personal homepage generator.",
3
+ "version": "1.1.2",
4
+ "description": "A lightweight personal homepage generator.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/Explorer-Dong/page-withu"
@@ -12,32 +12,19 @@
12
12
  },
13
13
  "files": [
14
14
  "bin/",
15
- "template/config.js",
16
- "template/content/",
17
- "template/src/",
18
- "template/index.html",
19
- "template/vite.config.js",
20
15
  "docs/",
16
+ "src/app/",
17
+ "src/defaults/config.js",
18
+ "src/defaults/content/",
19
+ "src/vite.config.js",
21
20
  "README.md"
22
21
  ],
23
22
  "scripts": {
24
23
  "pack:check": "npm pack --dry-run"
25
24
  },
26
- "keywords": [
27
- "homepage",
28
- "blog",
29
- "generator",
30
- "vue",
31
- "vite"
32
- ],
33
- "author": "Explorer-Dong",
34
- "license": "MIT",
35
25
  "dependencies": {
36
26
  "@traptitech/markdown-it-katex": "^3.6.0",
37
27
  "@vitejs/plugin-vue": "^5.2.3",
38
- "chalk": "^5.3.0",
39
- "commander": "^12.1.0",
40
- "fs-extra": "^11.2.0",
41
28
  "gray-matter": "^4.0.3",
42
29
  "highlight.js": "^11.11.1",
43
30
  "katex": "^0.16.45",
@@ -45,8 +32,15 @@
45
32
  "markdown-it-emoji": "^3.0.0",
46
33
  "markdown-it-footnote": "^4.0.0",
47
34
  "mermaid": "^11.15.0",
48
- "ora": "^8.0.1",
49
35
  "vite": "^6.3.1",
50
36
  "vue": "^3.5.13"
51
- }
37
+ },
38
+ "keywords": [
39
+ "static site generator",
40
+ "lightweight",
41
+ "vue",
42
+ "vite"
43
+ ],
44
+ "author": "Explorer-Dong",
45
+ "license": "MIT"
52
46
  }
@@ -4,7 +4,7 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>WithU</title>
7
+ <title>PageWithU</title>
8
8
  <link rel="icon" type="image/svg" href="/src/assets/bulb.svg">
9
9
  <script>
10
10
  (function () {
@@ -41,12 +41,18 @@
41
41
 
42
42
 
43
43
  <footer>
44
- <p>&copy; {{ userConfig.year }} {{ userConfig.author }}</p>
45
- <div class="footer-links">
46
- <template v-for="(link, index) in userConfig.footerLinks" :key="link.label">
47
- <a :href="link.url">{{ link.label }}</a>
48
- <span v-if="index < userConfig.footerLinks.length - 1" class="separator">&middot;</span>
49
- </template>
44
+ <div class="footer-content">
45
+ <div class="footer-meta">
46
+ <p>&copy; {{ userConfig.year }} {{ userConfig.author }}</p>
47
+ <div class="footer-links">
48
+ <template v-for="(link, index) in userConfig.footerLinks" :key="link.label">
49
+ <a :href="link.url">{{ link.label }}</a>
50
+ <span v-if="index < userConfig.footerLinks.length - 1" class="separator">&middot;</span>
51
+ </template>
52
+ </div>
53
+ </div>
54
+ <p class="made-with-page-withu">Made with <a href="https://github.com/Explorer-Dong/page-withu" target="_blank"
55
+ rel="noreferrer">PageWithU</a></p>
50
56
  </div>
51
57
  </footer>
52
58
  </main>
@@ -57,8 +63,8 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
57
63
  import userConfig from '@page-withu/user-config'
58
64
  import ThemeToggle from './components/theme_toggle.vue'
59
65
  import SiteCard from './components/site_card.vue'
60
- import BlogList from './components/blog-list.vue'
61
- import BlogDetail from './components/blog-detail.vue'
66
+ import BlogList from './components/blog_list.vue'
67
+ import BlogDetail from './components/blog_detail.vue'
62
68
  import { frontmatter as domainsFrontmatter } from '@page-withu/user-content/domains.md'
63
69
  import { sections as aboutSections } from '@page-withu/user-content/index.md'
64
70
  import defaultFavicon from './assets/bulb.svg'
@@ -67,13 +73,14 @@ const blogModules = import.meta.glob('@page-withu/user-content/blog/*.md', { eag
67
73
  const validPages = ['about', 'domains', 'blog']
68
74
  const basePath = import.meta.env.BASE_URL.replace(/\/$/, '')
69
75
  const navItems = [
70
- { page: 'domains', label: 'Domains' },
71
- { page: 'blog', label: 'Blog' },
76
+ { page: 'about', label: '主页' },
77
+ { page: 'domains', label: '项目' },
78
+ { page: 'blog', label: '博客' },
72
79
  ]
73
80
 
74
81
  const domainsTitle = domainsFrontmatter.title || 'Domains'
75
82
  const sites = domainsFrontmatter.sites || []
76
- const tabTitle = userConfig.tabTitle || userConfig.title || 'WithU'
83
+ const tabTitle = userConfig.tabTitle || userConfig.title || 'PageWithU'
77
84
  const favicon = userConfig.favicon === '/src/assets/bulb.svg' ? defaultFavicon : userConfig.favicon || defaultFavicon
78
85
 
79
86
  function applyDocumentMeta() {
@@ -3,13 +3,13 @@
3
3
  <a class="blog-back" :href="blogHref" @click.prevent="$emit('back')">← Back to all posts</a>
4
4
  <div :class="['blog-article-layout', { 'without-toc': !toc.length }]">
5
5
  <div class="blog-article-main">
6
- <header class="blog-article-header">
6
+ <div class="blog-article-header">
7
7
  <h1 class="markdown-body">{{ post.title }}</h1>
8
8
  <div class="blog-meta">
9
9
  <time :datetime="post.date">{{ formatDate(post.date) }}</time>
10
10
  <span v-for="tag in post.tags" :key="tag" class="tag">{{ tag }}</span>
11
11
  </div>
12
- </header>
12
+ </div>
13
13
 
14
14
  <div ref="contentRef" class="markdown-body" @click="openImage" v-html="post.html"></div>
15
15
 
@@ -35,10 +35,12 @@
35
35
  <button class="blog-toc-close" type="button" aria-label="Close table of contents"
36
36
  @click="tocOpen = false">×</button>
37
37
  </div>
38
- <a v-for="item in toc" :key="item.id" :href="`#${item.id}`"
39
- :class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]" @click="tocOpen = false">
40
- {{ item.title }}
41
- </a>
38
+ <div class="blog-toc-links">
39
+ <a v-for="item in toc" :key="item.id" :href="`#${item.id}`"
40
+ :class="[`toc-level-${item.level}`, { active: activeHeading === item.id }]" @click="tocOpen = false">
41
+ {{ item.title }}
42
+ </a>
43
+ </div>
42
44
  </aside>
43
45
  </div>
44
46
 
@@ -165,63 +165,115 @@ main {
165
165
  }
166
166
 
167
167
  /* ===== Header ===== */
168
- header {
168
+ main > header {
169
+ position: sticky;
170
+ top: 16px;
171
+ z-index: 80;
169
172
  display: flex;
170
173
  align-items: center;
171
174
  justify-content: space-between;
172
- gap: 16px;
175
+ gap: 24px;
176
+ width: min(100%, 720px);
177
+ margin: 0 auto;
178
+ padding: 6px 8px 6px 20px;
179
+ border: 1px solid var(--color-card-border);
180
+ border-radius: 24px;
181
+ background: rgba(255, 255, 255, 0.85);
182
+ box-shadow: 0 14px 38px rgba(0, 0, 0, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.6);
183
+ backdrop-filter: blur(18px) saturate(1.35);
184
+ -webkit-backdrop-filter: blur(18px) saturate(1.35);
185
+ }
186
+
187
+ [data-theme="dark"] main > header,
188
+ :root:not([data-theme="light"]) main > header {
189
+ border: 1px solid rgba(255, 255, 255, 0.1);
190
+ background: rgba(18, 18, 18, 0.78);
191
+ box-shadow: 0 14px 38px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.08);
192
+ }
193
+
194
+ main > header::before {
195
+ content: none;
173
196
  }
174
197
 
175
198
  .site-title {
176
- font-size: 1.45rem;
177
- font-weight: 750;
178
- color: var(--color-heading);
199
+ flex: 0 0 auto;
200
+ max-width: 180px;
201
+ overflow: hidden;
202
+ color: var(--color-text);
203
+ text-overflow: ellipsis;
204
+ white-space: nowrap;
205
+ font-size: 0.95rem;
206
+ font-weight: 800;
179
207
  text-decoration: none;
180
208
  margin: 0;
181
209
  padding: 0;
182
210
  background: none;
183
211
  }
184
212
 
213
+ [data-theme="dark"] .site-title,
214
+ :root:not([data-theme="light"]) .site-title {
215
+ color: #fff;
216
+ }
217
+
185
218
  .site-title:hover {
186
- color: var(--color-heading);
219
+ color: var(--color-link);
187
220
  background: none;
188
221
  }
189
222
 
223
+ [data-theme="dark"] .site-title:hover,
224
+ :root:not([data-theme="light"]) .site-title:hover {
225
+ color: #fff;
226
+ }
227
+
190
228
  .header-actions {
191
229
  display: flex;
192
230
  align-items: center;
193
- gap: 12px;
231
+ gap: 24px;
232
+ min-width: 0;
194
233
  }
195
234
 
196
235
  /* ===== Navigation Bar ===== */
197
236
  .nav-bar {
198
237
  display: flex;
199
- gap: 4px;
200
- padding: 4px;
201
- border: 1px solid var(--color-card-border);
202
- border-radius: 999px;
203
- background: var(--color-card-bg);
238
+ align-items: center;
239
+ gap: 24px;
240
+ min-width: 0;
241
+ padding: 0;
242
+ border: none;
243
+ border-radius: 0;
244
+ background: transparent;
204
245
  }
205
246
 
206
247
  .nav-item {
207
- padding: 5px 12px;
248
+ padding: 6px 0;
208
249
  font-size: 0.9rem;
209
- font-weight: 500;
250
+ font-weight: 650;
210
251
  color: var(--color-subtext);
211
252
  text-decoration: none;
212
- border-radius: 999px;
213
- transition: color 0.2s ease, background-color 0.2s ease;
253
+ border-radius: 0;
254
+ transition: color 0.2s ease, text-shadow 0.2s ease;
214
255
  background: none;
256
+ white-space: nowrap;
215
257
  }
216
258
 
217
- .nav-item:hover {
218
- color: var(--color-text);
219
- background: var(--color-link-hover-bg);
259
+ [data-theme="dark"] .nav-item,
260
+ :root:not([data-theme="light"]) .nav-item {
261
+ color: rgba(255, 255, 255, 0.68);
220
262
  }
221
263
 
264
+ .nav-item:hover,
222
265
  .nav-item.active {
223
- color: var(--color-link);
224
- background: var(--color-link-active-bg);
266
+ color: var(--color-text);
267
+ background: none;
268
+ text-shadow: none;
269
+ }
270
+
271
+ [data-theme="dark"] .nav-item:hover,
272
+ [data-theme="dark"] .nav-item.active,
273
+ :root:not([data-theme="light"]) .nav-item:hover,
274
+ :root:not([data-theme="light"]) .nav-item.active {
275
+ color: #fff;
276
+ text-shadow: 0 0 18px rgba(255, 255, 255, 0.28);
225
277
  }
226
278
 
227
279
  /* ===== Section Titles ===== */
@@ -398,6 +450,17 @@ footer p {
398
450
  margin-bottom: 6px;
399
451
  }
400
452
 
453
+ .footer-content {
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: space-between;
457
+ gap: 16px;
458
+ }
459
+
460
+ .footer-meta {
461
+ min-width: 0;
462
+ }
463
+
401
464
  .footer-links {
402
465
  display: flex;
403
466
  align-items: center;
@@ -419,40 +482,75 @@ footer p {
419
482
  color: var(--color-separator);
420
483
  }
421
484
 
485
+ .made-with-page-withu {
486
+ flex-shrink: 0;
487
+ margin: 0;
488
+ color: var(--color-footer-text);
489
+ font-size: 0.85rem;
490
+ text-align: right;
491
+ white-space: nowrap;
492
+ }
493
+
494
+ .made-with-page-withu a {
495
+ color: var(--color-footer-link);
496
+ font-weight: 600;
497
+ }
498
+
499
+ .made-with-page-withu a:hover {
500
+ color: var(--color-footer-link-hover);
501
+ background: var(--color-footer-link-hover-bg);
502
+ }
503
+
422
504
  /* ===== Theme Toggle ===== */
423
505
  .theme-toggle {
424
- width: 38px;
425
- height: 38px;
426
- border: 1px solid var(--color-card-border);
506
+ width: 34px;
507
+ height: 34px;
508
+ border: none;
427
509
  border-radius: 50%;
428
- background: var(--color-card-bg);
510
+ background: transparent;
429
511
  cursor: pointer;
430
512
  display: flex;
431
513
  align-items: center;
432
514
  justify-content: center;
433
- transition: background-color 0.3s ease, border-color 0.3s ease, transform 0.15s ease;
434
- box-shadow: 0 1px 4px var(--color-card-active-shadow);
515
+ transition: background-color 0.2s ease, transform 0.15s ease, color 0.2s ease;
516
+ color: var(--color-subtext);
517
+ }
518
+
519
+ [data-theme="dark"] .theme-toggle,
520
+ :root:not([data-theme="light"]) .theme-toggle {
521
+ color: rgba(255, 255, 255, 0.68);
435
522
  }
436
523
 
437
524
  .theme-toggle:hover {
438
- border-color: var(--color-card-hover-border);
439
- box-shadow: 0 2px 8px var(--color-card-hover-shadow);
440
- transform: scale(1.05);
525
+ background: var(--color-link-hover-bg);
526
+ color: var(--color-text);
527
+ text-shadow: none;
528
+ }
529
+
530
+ [data-theme="dark"] .theme-toggle:hover,
531
+ :root:not([data-theme="light"]) .theme-toggle:hover {
532
+ background: rgba(255, 255, 255, 0.1);
533
+ color: #fff;
534
+ text-shadow: 0 0 18px rgba(255, 255, 255, 0.28);
441
535
  }
442
536
 
443
537
  .theme-toggle:active {
444
- transform: scale(0.95);
538
+ transform: scale(0.92);
445
539
  }
446
540
 
447
541
  .theme-toggle svg {
448
- width: 19px;
449
- height: 19px;
542
+ width: 18px;
543
+ height: 18px;
450
544
  fill: none;
451
- stroke: var(--color-subtext);
545
+ stroke: currentColor;
452
546
  stroke-width: 2;
453
547
  stroke-linecap: round;
454
548
  stroke-linejoin: round;
455
- transition: stroke 0.3s ease;
549
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
550
+ }
551
+
552
+ .theme-toggle:hover svg {
553
+ transform: rotate(15deg);
456
554
  }
457
555
 
458
556
  .theme-toggle .icon-sun {
@@ -553,6 +651,39 @@ footer p {
553
651
  padding: 16px;
554
652
  overflow-x: auto;
555
653
  margin-bottom: 1em;
654
+ scrollbar-width: thin;
655
+ scrollbar-color: transparent transparent;
656
+ }
657
+
658
+ .markdown-body pre:hover {
659
+ scrollbar-color: color-mix(in srgb, var(--color-subtext) 45%, transparent) transparent;
660
+ }
661
+
662
+ .markdown-body pre::-webkit-scrollbar {
663
+ height: 4px;
664
+ }
665
+
666
+ .markdown-body pre::-webkit-scrollbar-button {
667
+ display: none;
668
+ width: 0;
669
+ height: 0;
670
+ }
671
+
672
+ .markdown-body pre::-webkit-scrollbar-track {
673
+ background: transparent;
674
+ }
675
+
676
+ .markdown-body pre::-webkit-scrollbar-thumb {
677
+ background: transparent;
678
+ border-radius: 999px;
679
+ }
680
+
681
+ .markdown-body pre:hover::-webkit-scrollbar-thumb {
682
+ background: color-mix(in srgb, var(--color-subtext) 42%, transparent);
683
+ }
684
+
685
+ .markdown-body pre::-webkit-scrollbar-thumb:hover {
686
+ background: color-mix(in srgb, var(--color-subtext) 62%, transparent);
556
687
  }
557
688
 
558
689
  .markdown-body pre code {
@@ -1007,12 +1138,53 @@ footer p {
1007
1138
  flex-direction: column;
1008
1139
  gap: 8px;
1009
1140
  max-height: calc(100vh - 48px);
1010
- padding: 0 0 16px 16px;
1141
+ padding: 0 4px 16px 16px;
1011
1142
  border-left: 1px solid var(--color-footer-border);
1012
- overflow-y: auto;
1143
+ overflow: hidden;
1013
1144
  font-size: 0.84rem;
1014
1145
  }
1015
1146
 
1147
+ .blog-toc-links {
1148
+ display: flex;
1149
+ flex-direction: column;
1150
+ gap: 8px;
1151
+ min-height: 0;
1152
+ overflow-y: auto;
1153
+ scrollbar-width: thin;
1154
+ scrollbar-color: transparent transparent;
1155
+ }
1156
+
1157
+ .blog-toc-links:hover {
1158
+ scrollbar-color: color-mix(in srgb, var(--color-subtext) 45%, transparent) transparent;
1159
+ }
1160
+
1161
+ .blog-toc-links::-webkit-scrollbar {
1162
+ width: 4px;
1163
+ }
1164
+
1165
+ .blog-toc-links::-webkit-scrollbar-button {
1166
+ display: none;
1167
+ width: 0;
1168
+ height: 0;
1169
+ }
1170
+
1171
+ .blog-toc-links::-webkit-scrollbar-track {
1172
+ background: transparent;
1173
+ }
1174
+
1175
+ .blog-toc-links::-webkit-scrollbar-thumb {
1176
+ background: transparent;
1177
+ border-radius: 999px;
1178
+ }
1179
+
1180
+ .blog-toc-links:hover::-webkit-scrollbar-thumb {
1181
+ background: color-mix(in srgb, var(--color-subtext) 42%, transparent);
1182
+ }
1183
+
1184
+ .blog-toc-links::-webkit-scrollbar-thumb:hover {
1185
+ background: color-mix(in srgb, var(--color-subtext) 62%, transparent);
1186
+ }
1187
+
1016
1188
  .blog-toc-header {
1017
1189
  display: flex;
1018
1190
  align-items: center;
@@ -1204,14 +1376,34 @@ footer p {
1204
1376
  gap: 28px;
1205
1377
  }
1206
1378
 
1207
- header {
1208
- align-items: flex-start;
1209
- flex-direction: column;
1379
+ main > header {
1380
+ top: 12px;
1381
+ gap: 18px;
1382
+ width: 100%;
1383
+ padding: 8px 10px 8px 18px;
1210
1384
  }
1211
1385
 
1212
1386
  .header-actions {
1213
- width: 100%;
1214
- justify-content: space-between;
1387
+ flex: 1;
1388
+ width: auto;
1389
+ justify-content: flex-end;
1390
+ gap: 18px;
1391
+ min-width: 0;
1392
+ }
1393
+
1394
+ .nav-bar {
1395
+ gap: 18px;
1396
+ overflow-x: auto;
1397
+ scrollbar-width: none;
1398
+ }
1399
+
1400
+ .nav-bar::-webkit-scrollbar {
1401
+ display: none;
1402
+ }
1403
+
1404
+ .theme-toggle {
1405
+ width: 38px;
1406
+ height: 38px;
1215
1407
  }
1216
1408
  }
1217
1409
 
@@ -1227,12 +1419,13 @@ footer p {
1227
1419
  }
1228
1420
 
1229
1421
  .site-title {
1230
- font-size: 1.3rem;
1422
+ max-width: 120px;
1423
+ font-size: 0.9rem;
1231
1424
  }
1232
1425
 
1233
1426
  .nav-item {
1234
- padding: 5px 10px;
1235
- font-size: 0.84rem;
1427
+ padding: 7px 0;
1428
+ font-size: 0.86rem;
1236
1429
  }
1237
1430
 
1238
1431
  .about-section {
@@ -1322,6 +1515,7 @@ footer p {
1322
1515
  box-shadow: var(--shadow-card);
1323
1516
  transform: translate(-50%, -50%);
1324
1517
  display: none;
1518
+ overflow: hidden;
1325
1519
  }
1326
1520
 
1327
1521
  .blog-toc.open {
@@ -1329,12 +1523,11 @@ footer p {
1329
1523
  }
1330
1524
 
1331
1525
  .blog-toc-header {
1332
- position: sticky;
1333
- top: -16px;
1334
1526
  margin: -16px -16px 4px;
1335
1527
  padding: 16px;
1336
1528
  background: var(--color-card-bg);
1337
1529
  border-bottom: 1px solid var(--color-footer-border);
1530
+ flex-shrink: 0;
1338
1531
  }
1339
1532
 
1340
1533
  .blog-toc-close {
@@ -1377,3 +1570,27 @@ footer p {
1377
1570
  height: 36px;
1378
1571
  }
1379
1572
  }
1573
+
1574
+ @media (max-width: 520px) {
1575
+ main > header {
1576
+ padding-left: 14px;
1577
+ gap: 14px;
1578
+ }
1579
+
1580
+ .site-title {
1581
+ max-width: 80px;
1582
+ }
1583
+
1584
+ .header-actions {
1585
+ gap: 14px;
1586
+ }
1587
+
1588
+ .nav-bar {
1589
+ gap: 14px;
1590
+ }
1591
+
1592
+ .theme-toggle {
1593
+ width: 32px;
1594
+ height: 32px;
1595
+ }
1596
+ }
@@ -1,19 +1,23 @@
1
1
  import { createHash } from 'node:crypto'
2
+ import { createRequire } from 'node:module'
2
3
  import { tmpdir } from 'node:os'
3
- import { defineConfig } from 'vite'
4
- import vue from '@vitejs/plugin-vue'
5
- import MarkdownIt from 'markdown-it'
6
- import mk from '@traptitech/markdown-it-katex'
7
- import footnote from 'markdown-it-footnote'
8
- import { full as emoji } from 'markdown-it-emoji'
9
- import matter from 'gray-matter'
10
- import hljs from 'highlight.js'
11
4
  import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
12
5
  import { dirname, join, resolve } from 'node:path'
13
6
  import { fileURLToPath, pathToFileURL } from 'node:url'
14
7
 
15
- const runtimeRoot = dirname(fileURLToPath(import.meta.url))
8
+ const sourceRoot = dirname(fileURLToPath(import.meta.url))
9
+ const appRoot = resolve(sourceRoot, 'app')
16
10
  const projectRoot = resolve(process.env.PAGE_WITHU_PROJECT_ROOT || process.cwd())
11
+ const packageRoot = resolve(sourceRoot, '..')
12
+ const dependencyRoot = resolve(packageRoot, 'node_modules')
13
+ const runtimeRequire = createRequire(import.meta.url)
14
+ const loadRuntimeModule = async (specifier) => import(pathToFileURL(runtimeRequire.resolve(specifier)).href)
15
+ const runtimeModuleAlias = Object.fromEntries([
16
+ 'vue',
17
+ 'katex/dist/katex.min.css',
18
+ 'highlight.js/styles/github.css',
19
+ 'mermaid',
20
+ ].map((specifier) => [specifier, runtimeRequire.resolve(specifier)]))
17
21
  const userConfigFile = resolve(projectRoot, 'config.js')
18
22
  const userContentDir = resolve(projectRoot, 'content')
19
23
  const cacheKey = createHash('sha256').update(projectRoot).digest('hex').slice(0, 12)
@@ -73,7 +77,7 @@ function callouts(md) {
73
77
  })
74
78
  }
75
79
 
76
- function createMarkdownRenderer() {
80
+ function createMarkdownRenderer({ MarkdownIt, mk, footnote, emoji, hljs }) {
77
81
  const toc = []
78
82
  let md
79
83
  md = new MarkdownIt({
@@ -127,8 +131,9 @@ function renderMarkdown(md, content) {
127
131
  return { html, toc: env.toc }
128
132
  }
129
133
 
130
- function markdown() {
131
- const { md } = createMarkdownRenderer()
134
+ function markdown(deps) {
135
+ const { md } = createMarkdownRenderer(deps)
136
+ const { matter } = deps
132
137
  return {
133
138
  name: 'vite-plugin-markdown',
134
139
  transform(code, id) {
@@ -187,24 +192,44 @@ function staticHtmlRoutes(userConfig) {
187
192
  }
188
193
  }
189
194
 
195
+ const { defineConfig } = await loadRuntimeModule('vite')
196
+
190
197
  export default defineConfig(async () => {
198
+ const [vueModule, MarkdownItModule, mkModule, footnoteModule, emojiModule, matterModule, hljsModule] = await Promise.all([
199
+ loadRuntimeModule('@vitejs/plugin-vue'),
200
+ loadRuntimeModule('markdown-it'),
201
+ loadRuntimeModule('@traptitech/markdown-it-katex'),
202
+ loadRuntimeModule('markdown-it-footnote'),
203
+ loadRuntimeModule('markdown-it-emoji'),
204
+ loadRuntimeModule('gray-matter'),
205
+ loadRuntimeModule('highlight.js'),
206
+ ])
207
+ const deps = {
208
+ MarkdownIt: MarkdownItModule.default,
209
+ mk: mkModule.default,
210
+ footnote: footnoteModule.default,
211
+ emoji: emojiModule.full,
212
+ matter: matterModule.default,
213
+ hljs: hljsModule.default,
214
+ }
191
215
  const userConfig = (await import(`${pathToFileURL(userConfigFile).href}?t=${Date.now()}`)).default
192
216
 
193
217
  return {
194
- root: runtimeRoot,
218
+ root: appRoot,
195
219
  base: basePath,
196
220
  cacheDir,
197
221
  resolve: {
198
222
  alias: {
223
+ ...runtimeModuleAlias,
199
224
  '@page-withu/user-config': userConfigFile,
200
225
  '@page-withu/user-content': userContentDir,
201
226
  },
202
227
  },
203
- plugins: [vue(), markdown(), staticHtmlRoutes(userConfig)],
228
+ plugins: [vueModule.default(), markdown(deps), staticHtmlRoutes(userConfig)],
204
229
  server: {
205
230
  port: 5500,
206
231
  fs: {
207
- allow: [runtimeRoot, projectRoot],
232
+ allow: [appRoot, projectRoot, dependencyRoot],
208
233
  },
209
234
  },
210
235
  build: {
File without changes
File without changes
File without changes
File without changes
File without changes