md-lv 1.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [2.0.0] - 2026-01-25
9
+
10
+ ### Added
11
+
12
+ - **ESM Support**: Full ECMAScript Modules support with `"type": "module"`
13
+ - **Express 5.x**: Upgraded from Connect to Express 5.x framework
14
+ - **Mermaid Diagrams**: Render flowcharts, sequence diagrams, and more
15
+ - **MathJax Support**: LaTeX math formula rendering with `$...$` and `$$...$$`
16
+ - **Dark Mode**: Automatic theme switching based on system preferences
17
+ - **Search Functionality**: Client-side file search with real-time results
18
+ - **Keyboard Navigation**: Vim-style navigation (j/k) and shortcuts
19
+ - **Security Headers**: CSP, X-Content-Type-Options, X-Frame-Options, etc.
20
+ - **Path Traversal Protection**: Enhanced security against directory traversal attacks
21
+ - **Modern UI**: Improved styling with CSS custom properties
22
+ - **README Command**: `mdv readme` to find and open nearest README.md
23
+ - **Port Auto-Selection**: Automatically find available port if specified port is in use
24
+ - **Integration Tests**: Comprehensive test suite with Jest and Supertest
25
+ - **GitHub Actions CI**: Automated testing on Node.js 18, 20, and 22
26
+
27
+ ### Changed
28
+
29
+ - **CLI Parser**: Migrated from meow to Commander.js
30
+ - **Directory Structure**: Reorganized to `src/`, `routes/`, `middleware/`, `utils/`
31
+ - **Template System**: Simplified HTML templating with mustache-style variables
32
+ - **Logging**: Unified logger with configurable log levels
33
+ - **Error Handling**: Centralized error handling with custom error pages
34
+ - **Node.js Requirement**: Minimum version is now Node.js 18.0.0
35
+
36
+ ### Removed
37
+
38
+ - **CommonJS Support**: No longer supports `require()` imports
39
+ - **Connect Framework**: Replaced with Express 5.x
40
+ - **Bluebird**: Using native Promises
41
+ - **meow**: Replaced with Commander.js
42
+ - **markdown-it**: Moved to client-side marked.js rendering
43
+ - **LiveReload**: Removed in favor of manual refresh (may be re-added)
44
+
45
+ ### Security
46
+
47
+ - Path traversal prevention with symlink resolution
48
+ - Null byte injection protection
49
+ - Security headers (CSP, X-Frame-Options, etc.)
50
+ - Input validation and HTML escaping
51
+
52
+ ### Fixed
53
+
54
+ - Unicode file name handling
55
+ - URL encoding in breadcrumbs
56
+ - Memory leaks in template caching
57
+
58
+ ## [1.x.x] - Previous Versions
59
+
60
+ See the [archived markserv changelog](archived-markserv/CHANGELOG.md) for previous version history.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 markdown-viewer contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # markdown-viewer
2
+
3
+ > Serve Markdown files as HTML with live features
4
+
5
+ A lightweight local server that renders Markdown files as beautiful HTML pages, with support for GitHub Flavored Markdown, syntax highlighting, Mermaid diagrams, and MathJax formulas.
6
+
7
+ ## Features
8
+
9
+ - **GitHub-style Markdown** - Full GFM support with tables, task lists, and more
10
+ - **Syntax Highlighting** - Code blocks with automatic language detection
11
+ - **Mermaid Diagrams** - Flowcharts, sequence diagrams, and more
12
+ - **MathJax Support** - LaTeX math formulas (`$...$` and `$$...$$`)
13
+ - **Directory Browsing** - Navigate through your files with ease
14
+ - **Dark Mode** - Automatic theme switching based on system preferences
15
+ - **Search** - Find files quickly with built-in search
16
+ - **Keyboard Navigation** - Navigate with vim-style keys (j/k)
17
+ - **Security** - Path traversal protection and security headers
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install -g markdown-viewer
23
+ ```
24
+
25
+ Or use npx:
26
+ ```bash
27
+ npx markdown-viewer
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### Basic Usage
33
+
34
+ ```bash
35
+ # Serve current directory
36
+ mdv
37
+
38
+ # Serve specific directory
39
+ mdv --dir /path/to/docs
40
+
41
+ # Serve on custom port
42
+ mdv --port 8080
43
+
44
+ # Open README.md automatically
45
+ mdv readme
46
+ ```
47
+
48
+ ### CLI Options
49
+
50
+ | Option | Short | Default | Description |
51
+ |--------|-------|---------|-------------|
52
+ | `--port` | `-p` | `3000` | Server port |
53
+ | `--host` | `-H` | `localhost` | Bind address |
54
+ | `--dir` | `-d` | `.` | Document root |
55
+ | `--no-watch` | | `false` | Disable file watching |
56
+ | `--quiet` | `-q` | `false` | Suppress output |
57
+ | `--debug` | | `false` | Enable debug mode |
58
+
59
+ ### Subcommands
60
+
61
+ #### `mdv readme`
62
+
63
+ Find and display the nearest README.md file:
64
+
65
+ ```bash
66
+ cd /path/to/project
67
+ mdv readme
68
+ ```
69
+
70
+ This command searches up the directory tree to find README.md and opens it in your browser.
71
+
72
+ ## Supported Content
73
+
74
+ ### Markdown Features
75
+
76
+ - Headings, paragraphs, lists
77
+ - Tables (GFM)
78
+ - Task lists
79
+ - Code blocks with syntax highlighting
80
+ - Blockquotes
81
+ - Links and images
82
+ - Horizontal rules
83
+
84
+ ### Mermaid Diagrams
85
+
86
+ ````markdown
87
+ ```mermaid
88
+ flowchart TD
89
+ A[Start] --> B{Decision}
90
+ B -->|Yes| C[OK]
91
+ B -->|No| D[Cancel]
92
+ ```
93
+ ````
94
+
95
+ ### Math Formulas
96
+
97
+ Inline: `$E = mc^2$`
98
+
99
+ Block:
100
+ ```markdown
101
+ $$
102
+ \sum_{i=1}^n i = \frac{n(n+1)}{2}
103
+ $$
104
+ ```
105
+
106
+ ## Keyboard Shortcuts
107
+
108
+ | Shortcut | Action |
109
+ |----------|--------|
110
+ | `Alt + ←` | Go to parent directory |
111
+ | `Alt + Home` | Go to root |
112
+ | `j` / `↓` | Next item (in directory listing) |
113
+ | `k` / `↑` | Previous item |
114
+ | `Enter` | Open selected item |
115
+ | `/` | Focus search |
116
+ | `Escape` | Close search results |
117
+
118
+ ## Requirements
119
+
120
+ - Node.js 18.0.0 or higher
121
+
122
+ ## Development
123
+
124
+ ```bash
125
+ # Clone repository
126
+ git clone https://github.com/your-username/markdown-viewer.git
127
+ cd markdown-viewer
128
+
129
+ # Install dependencies
130
+ npm install
131
+
132
+ # Run tests
133
+ npm test
134
+
135
+ # Start development server
136
+ npm start
137
+ ```
138
+
139
+ ## Contributing
140
+
141
+ Contributions are welcome! Please read our [Contributing Guide](CONTRIBUTING.md) for details.
142
+
143
+ ## License
144
+
145
+ MIT License - see [LICENSE](LICENSE) for details.
146
+
147
+ ## Acknowledgments
148
+
149
+ - [marked](https://marked.js.org/) - Markdown parser
150
+ - [highlight.js](https://highlightjs.org/) - Syntax highlighting
151
+ - [Mermaid](https://mermaid.js.org/) - Diagram rendering
152
+ - [MathJax](https://www.mathjax.org/) - Math formulas
package/bin/mdv.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from '../src/cli.js';
4
+
5
+ run(process.argv);
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "md-lv",
3
+ "version": "1.0.0",
4
+ "description": "Serve Markdown files as HTML with live features - syntax highlighting, Mermaid diagrams, and MathJax formulas",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18.0.0"
8
+ },
9
+ "main": "src/index.js",
10
+ "bin": {
11
+ "mdv": "bin/mdv.js"
12
+ },
13
+ "files": [
14
+ "bin/",
15
+ "src/",
16
+ "templates/",
17
+ "public/",
18
+ "README.md",
19
+ "LICENSE",
20
+ "CHANGELOG.md"
21
+ ],
22
+ "scripts": {
23
+ "start": "node bin/mdv.js",
24
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
25
+ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/unit",
26
+ "test:integration": "node --experimental-vm-modules node_modules/jest/bin/jest.js tests/integration",
27
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage",
28
+ "prepublishOnly": "npm test"
29
+ },
30
+ "keywords": [
31
+ "markdown",
32
+ "server",
33
+ "html",
34
+ "viewer",
35
+ "mermaid",
36
+ "mathjax",
37
+ "syntax-highlighting",
38
+ "gfm",
39
+ "github-flavored-markdown",
40
+ "documentation",
41
+ "docs",
42
+ "cli"
43
+ ],
44
+ "author": "Watanabe Riku",
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/no-problem-dev/markdown-viewer.git"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/no-problem-dev/markdown-viewer/issues"
52
+ },
53
+ "homepage": "https://github.com/no-problem-dev/markdown-viewer#readme",
54
+ "dependencies": {
55
+ "express": "^5.0.0",
56
+ "commander": "^12.0.0",
57
+ "chokidar": "^3.5.0",
58
+ "open": "^10.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "jest": "^29.0.0",
62
+ "supertest": "^6.0.0"
63
+ }
64
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
2
+ <rect width="100" height="100" rx="15" fill="#0366d6"/>
3
+ <text x="50" y="70" font-family="system-ui, sans-serif" font-size="60" font-weight="bold" text-anchor="middle" fill="white">M</text>
4
+ </svg>
File without changes
@@ -0,0 +1,174 @@
1
+ /**
2
+ * markdown-viewer Client-side JavaScript
3
+ */
4
+
5
+ document.addEventListener('DOMContentLoaded', async () => {
6
+ // Markdown レンダリング
7
+ renderMarkdown();
8
+
9
+ // シンタックスハイライト
10
+ highlightCode();
11
+
12
+ // Mermaid 図のレンダリング
13
+ await renderMermaid();
14
+
15
+ // MathJax レンダリング(Mermaid の後)
16
+ await renderMath();
17
+ });
18
+
19
+ /**
20
+ * Markdown をレンダリング
21
+ */
22
+ function renderMarkdown() {
23
+ const source = document.getElementById('markdown-source');
24
+ const rendered = document.getElementById('markdown-rendered');
25
+
26
+ if (source && rendered && typeof marked !== 'undefined') {
27
+ const markdown = source.textContent;
28
+
29
+ // カスタムレンダラーで Mermaid コードブロックを処理
30
+ let mermaidCounter = 0;
31
+ const renderer = {
32
+ code({ text, lang }) {
33
+ if (lang === 'mermaid') {
34
+ const id = `mermaid-diagram-${mermaidCounter++}`;
35
+ return `<div class="mermaid" id="${id}">${escapeHtmlForMermaid(text)}</div>`;
36
+ }
37
+ // デフォルトのコードブロックレンダリング
38
+ const escaped = escapeHtmlForMermaid(text);
39
+ const langClass = lang ? ` class="language-${lang}"` : '';
40
+ return `<pre><code${langClass}>${escaped}</code></pre>`;
41
+ }
42
+ };
43
+
44
+ // marked.js の設定とカスタムレンダラーを適用
45
+ marked.use({
46
+ gfm: true,
47
+ breaks: false,
48
+ pedantic: false,
49
+ renderer
50
+ });
51
+
52
+ // Markdown をレンダリング
53
+ rendered.innerHTML = marked.parse(markdown);
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Mermaid 図をレンダリング
59
+ */
60
+ async function renderMermaid() {
61
+ const mermaidDivs = document.querySelectorAll('.mermaid');
62
+
63
+ if (mermaidDivs.length === 0) return;
64
+
65
+ // Mermaid がロードされるまで待機(ESM モジュールは window.mermaid に公開される)
66
+ if (typeof window.mermaid === 'undefined') {
67
+ // Mermaid は ESM モジュールとして読み込まれるため、ポーリングで待機
68
+ let attempts = 0;
69
+ while (typeof window.mermaid === 'undefined' && attempts < 30) {
70
+ await new Promise(resolve => setTimeout(resolve, 100));
71
+ attempts++;
72
+ }
73
+
74
+ // 再度チェック
75
+ if (typeof window.mermaid === 'undefined') {
76
+ console.warn('Mermaid library not loaded');
77
+ mermaidDivs.forEach(div => {
78
+ div.innerHTML = `<pre class="mermaid-error">Mermaid library not loaded</pre>`;
79
+ });
80
+ return;
81
+ }
82
+ }
83
+
84
+ try {
85
+ // Mermaid を初期化(まだ初期化されていない場合)
86
+ window.mermaid.initialize({
87
+ startOnLoad: false,
88
+ theme: 'default',
89
+ securityLevel: 'loose',
90
+ flowchart: {
91
+ useMaxWidth: true,
92
+ htmlLabels: true
93
+ }
94
+ });
95
+
96
+ // 各 Mermaid ブロックをレンダリング
97
+ for (const div of mermaidDivs) {
98
+ const code = div.textContent;
99
+ const id = div.id || `mermaid-${Date.now()}`;
100
+
101
+ try {
102
+ const { svg } = await window.mermaid.render(id + '-svg', code);
103
+ div.innerHTML = svg;
104
+ div.classList.add('mermaid-rendered');
105
+ } catch (err) {
106
+ console.error('Mermaid rendering error:', err);
107
+ div.innerHTML = `<pre class="mermaid-error">Mermaid Error: ${escapeHtmlForMermaid(err.message || 'Unknown error')}</pre>`;
108
+ div.classList.add('mermaid-error');
109
+ }
110
+ }
111
+ } catch (err) {
112
+ console.error('Mermaid initialization error:', err);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * シンタックスハイライトを適用
118
+ */
119
+ function highlightCode() {
120
+ if (typeof hljs !== 'undefined') {
121
+ document.querySelectorAll('pre code').forEach((block) => {
122
+ hljs.highlightElement(block);
123
+ });
124
+ }
125
+ }
126
+
127
+ /**
128
+ * MathJax で数式をレンダリング
129
+ * @param {Element} container - レンダリング対象のコンテナ要素(省略時は content 全体)
130
+ */
131
+ async function renderMath(container) {
132
+ const target = container || document.getElementById('markdown-rendered') || document.getElementById('content');
133
+
134
+ if (!target) return;
135
+
136
+ // MathJax がロードされるまで待機(最大2秒)
137
+ if (!window.MathJax || !window.MathJax.typesetPromise) {
138
+ let attempts = 0;
139
+ while ((!window.MathJax || !window.MathJax.typesetPromise) && attempts < 20) {
140
+ await new Promise(resolve => setTimeout(resolve, 100));
141
+ attempts++;
142
+ }
143
+
144
+ if (!window.MathJax || !window.MathJax.typesetPromise) {
145
+ console.warn('MathJax library not loaded after 2 seconds');
146
+ return;
147
+ }
148
+ }
149
+
150
+ try {
151
+ await window.MathJax.typesetPromise([target]);
152
+ } catch (err) {
153
+ console.error('MathJax rendering error:', err);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Mermaid 用の HTML エスケープ(最小限)
159
+ */
160
+ function escapeHtmlForMermaid(str) {
161
+ if (typeof str !== 'string') return '';
162
+ return str
163
+ .replace(/&/g, '&amp;')
164
+ .replace(/</g, '&lt;')
165
+ .replace(/>/g, '&gt;');
166
+ }
167
+
168
+ // グローバルに公開
169
+ window.mdv = {
170
+ renderMarkdown,
171
+ highlightCode,
172
+ renderMermaid,
173
+ renderMath
174
+ };
@@ -0,0 +1,201 @@
1
+ /**
2
+ * markdown-viewer Navigation Enhancement
3
+ * Keyboard navigation and UI improvements
4
+ */
5
+
6
+ document.addEventListener('DOMContentLoaded', () => {
7
+ initKeyboardNavigation();
8
+ initDirectoryListNavigation();
9
+ initBackToTop();
10
+ });
11
+
12
+ /**
13
+ * キーボードナビゲーションを初期化
14
+ */
15
+ function initKeyboardNavigation() {
16
+ document.addEventListener('keydown', (e) => {
17
+ // Alt + Left Arrow: 親ディレクトリへ
18
+ if (e.altKey && e.key === 'ArrowLeft') {
19
+ e.preventDefault();
20
+ navigateToParent();
21
+ }
22
+
23
+ // Alt + Home: ルートへ
24
+ if (e.altKey && e.key === 'Home') {
25
+ e.preventDefault();
26
+ navigateToRoot();
27
+ }
28
+
29
+ // Alt + Up Arrow: ページ先頭へ
30
+ if (e.altKey && e.key === 'ArrowUp') {
31
+ e.preventDefault();
32
+ window.scrollTo({ top: 0, behavior: 'smooth' });
33
+ }
34
+
35
+ // Alt + Down Arrow: ページ末尾へ
36
+ if (e.altKey && e.key === 'ArrowDown') {
37
+ e.preventDefault();
38
+ window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
39
+ }
40
+
41
+ // "/" キー: 検索フォーカス(検索機能がある場合)
42
+ if (e.key === '/' && !isInputFocused()) {
43
+ const searchInput = document.getElementById('search-input');
44
+ if (searchInput) {
45
+ e.preventDefault();
46
+ searchInput.focus();
47
+ }
48
+ }
49
+
50
+ // Escape: フォーカス解除
51
+ if (e.key === 'Escape') {
52
+ document.activeElement.blur();
53
+ }
54
+ });
55
+ }
56
+
57
+ /**
58
+ * 親ディレクトリへナビゲート
59
+ */
60
+ function navigateToParent() {
61
+ const breadcrumbs = document.querySelectorAll('#breadcrumbs a');
62
+ if (breadcrumbs.length >= 1) {
63
+ // 最後から2番目のリンク(親ディレクトリ)
64
+ const parentLink = breadcrumbs[breadcrumbs.length - 1];
65
+ if (parentLink) {
66
+ window.location.href = parentLink.href;
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * ルートディレクトリへナビゲート
73
+ */
74
+ function navigateToRoot() {
75
+ window.location.href = '/';
76
+ }
77
+
78
+ /**
79
+ * ディレクトリ一覧のキーボードナビゲーション
80
+ */
81
+ function initDirectoryListNavigation() {
82
+ const directoryList = document.querySelector('.directory-listing');
83
+ if (!directoryList) return;
84
+
85
+ const items = directoryList.querySelectorAll('li a');
86
+ let currentIndex = -1;
87
+
88
+ document.addEventListener('keydown', (e) => {
89
+ if (isInputFocused()) return;
90
+
91
+ // j/k または ArrowDown/ArrowUp でリスト内を移動
92
+ if (e.key === 'j' || e.key === 'ArrowDown') {
93
+ e.preventDefault();
94
+ currentIndex = Math.min(currentIndex + 1, items.length - 1);
95
+ focusItem(currentIndex);
96
+ }
97
+
98
+ if (e.key === 'k' || e.key === 'ArrowUp') {
99
+ e.preventDefault();
100
+ currentIndex = Math.max(currentIndex - 1, 0);
101
+ focusItem(currentIndex);
102
+ }
103
+
104
+ // Enter で選択したアイテムを開く
105
+ if (e.key === 'Enter' && currentIndex >= 0) {
106
+ items[currentIndex].click();
107
+ }
108
+
109
+ // g で先頭へ
110
+ if (e.key === 'g' && !e.ctrlKey && !e.metaKey) {
111
+ currentIndex = 0;
112
+ focusItem(currentIndex);
113
+ }
114
+
115
+ // G (Shift + g) で末尾へ
116
+ if (e.key === 'G') {
117
+ currentIndex = items.length - 1;
118
+ focusItem(currentIndex);
119
+ }
120
+ });
121
+
122
+ function focusItem(index) {
123
+ // 前のフォーカスを解除
124
+ items.forEach(item => item.parentElement.classList.remove('keyboard-focus'));
125
+
126
+ if (index >= 0 && index < items.length) {
127
+ items[index].parentElement.classList.add('keyboard-focus');
128
+ items[index].focus();
129
+ items[index].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 「トップへ戻る」ボタンを初期化
136
+ */
137
+ function initBackToTop() {
138
+ // ボタンを作成
139
+ const button = document.createElement('button');
140
+ button.id = 'back-to-top';
141
+ button.innerHTML = '↑';
142
+ button.title = 'Back to top (Alt + ↑)';
143
+ button.setAttribute('aria-label', 'Back to top');
144
+
145
+ // スタイル設定
146
+ Object.assign(button.style, {
147
+ position: 'fixed',
148
+ bottom: '20px',
149
+ right: '20px',
150
+ width: '40px',
151
+ height: '40px',
152
+ borderRadius: '50%',
153
+ border: 'none',
154
+ backgroundColor: 'var(--color-link, #0366d6)',
155
+ color: 'white',
156
+ fontSize: '20px',
157
+ cursor: 'pointer',
158
+ opacity: '0',
159
+ visibility: 'hidden',
160
+ transition: 'opacity 0.3s, visibility 0.3s',
161
+ zIndex: '1000',
162
+ boxShadow: 'var(--shadow-md, 0 4px 6px rgba(0, 0, 0, 0.1))'
163
+ });
164
+
165
+ document.body.appendChild(button);
166
+
167
+ // スクロール時の表示制御
168
+ window.addEventListener('scroll', () => {
169
+ if (window.scrollY > 300) {
170
+ button.style.opacity = '1';
171
+ button.style.visibility = 'visible';
172
+ } else {
173
+ button.style.opacity = '0';
174
+ button.style.visibility = 'hidden';
175
+ }
176
+ });
177
+
178
+ // クリックでトップへ
179
+ button.addEventListener('click', () => {
180
+ window.scrollTo({ top: 0, behavior: 'smooth' });
181
+ });
182
+ }
183
+
184
+ /**
185
+ * 入力フィールドにフォーカスがあるかチェック
186
+ */
187
+ function isInputFocused() {
188
+ const active = document.activeElement;
189
+ return active && (
190
+ active.tagName === 'INPUT' ||
191
+ active.tagName === 'TEXTAREA' ||
192
+ active.isContentEditable
193
+ );
194
+ }
195
+
196
+ // グローバルに公開
197
+ window.mdvNav = {
198
+ navigateToParent,
199
+ navigateToRoot,
200
+ isInputFocused
201
+ };