living-documentation 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/README.md +178 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +38 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/src/frontend/admin.html +268 -0
- package/dist/src/frontend/index.html +508 -0
- package/dist/src/lib/config.d.ts +11 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +43 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/parser.d.ts +10 -0
- package/dist/src/lib/parser.d.ts.map +1 -0
- package/dist/src/lib/parser.js +63 -0
- package/dist/src/lib/parser.js.map +1 -0
- package/dist/src/routes/config.d.ts +3 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +42 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/documents.d.ts +3 -0
- package/dist/src/routes/documents.d.ts.map +1 -0
- package/dist/src/routes/documents.js +106 -0
- package/dist/src/routes/documents.js.map +1 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +54 -0
- package/dist/src/server.js.map +1 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# Living Documentation
|
|
2
|
+
|
|
3
|
+
A CLI tool that serves a local Markdown documentation viewer in your browser.
|
|
4
|
+
|
|
5
|
+
No cloud, no database, no build step — just point it at a folder of `.md` files.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Sidebar** grouped by category, sorted by date (newest first)
|
|
16
|
+
- **Full-text search** — instant filter + server-side content search
|
|
17
|
+
- **Dark mode** — follows system preference, manually toggleable
|
|
18
|
+
- **Export to PDF** — print-friendly layout via `window.print()`
|
|
19
|
+
- **Deep links** — share a direct URL to any document (`?doc=…`)
|
|
20
|
+
- **Admin panel** — configure title, theme, filename pattern in the browser
|
|
21
|
+
- **Zero frontend build** — Tailwind and highlight.js loaded from CDN
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx living-documentation ./path/to/docs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then open [http://localhost:4321](http://localhost:4321).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### npx (no install)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npx living-documentation ./docs
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Global install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g living-documentation
|
|
47
|
+
living-documentation ./docs
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Local development
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
git clone <repo>
|
|
54
|
+
cd living-documentation
|
|
55
|
+
npm install
|
|
56
|
+
npm run dev -- ./docs # runs via ts-node, no build needed
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
living-documentation [folder] [options]
|
|
65
|
+
|
|
66
|
+
Arguments:
|
|
67
|
+
folder Path to the documentation folder (default: ".")
|
|
68
|
+
|
|
69
|
+
Options:
|
|
70
|
+
-p, --port <number> Port to listen on (default: 4321)
|
|
71
|
+
-o, --open Open browser automatically
|
|
72
|
+
-V, --version Print version
|
|
73
|
+
-h, --help Show help
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Examples:**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
living-documentation ./docs
|
|
80
|
+
living-documentation ./docs --port 4000 --open # override port
|
|
81
|
+
living-documentation . # current folder
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Filename convention
|
|
87
|
+
|
|
88
|
+
Documents are parsed using this default pattern:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
YYYY_MM_DD_[Category]_title_words.md
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Part | Example | Parsed as |
|
|
95
|
+
|------|---------|-----------|
|
|
96
|
+
| `2024_01_15` | `2024_01_15` | Date → Jan 15, 2024 |
|
|
97
|
+
| `[DevOps]` | `[DevOps]` | Category → DevOps |
|
|
98
|
+
| `deploy_pipeline` | `deploy_pipeline` | Title → Deploy Pipeline |
|
|
99
|
+
|
|
100
|
+
**Full example:**
|
|
101
|
+
```
|
|
102
|
+
2024_01_15_[DevOps]_deploy_pipeline.md
|
|
103
|
+
2024_03_20_[Frontend]_react_hooks_guide.md
|
|
104
|
+
2023_11_03_[Backend]_api_versioning_strategy.md
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Files that don't match the pattern are still shown — they appear under **Uncategorized** with the filename as the title.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Config file
|
|
112
|
+
|
|
113
|
+
A `.living-doc.json` file is created automatically in your docs folder on first run:
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"docsFolder": "/absolute/path/to/docs",
|
|
118
|
+
"filenamePattern": "YYYY_MM_DD_[Category]_title",
|
|
119
|
+
"title": "Living Documentation",
|
|
120
|
+
"theme": "system",
|
|
121
|
+
"port": 4321
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
You can edit it manually or use the **Admin panel** at [http://localhost:4321/admin](http://localhost:4321/admin).
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## Project structure
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
living-documentation/
|
|
133
|
+
├── bin/
|
|
134
|
+
│ └── cli.ts CLI entry point
|
|
135
|
+
├── src/
|
|
136
|
+
│ ├── server.ts Express app
|
|
137
|
+
│ ├── routes/
|
|
138
|
+
│ │ ├── documents.ts Documents API
|
|
139
|
+
│ │ └── config.ts Config API
|
|
140
|
+
│ ├── lib/
|
|
141
|
+
│ │ ├── parser.ts Filename parser
|
|
142
|
+
│ │ └── config.ts Config management
|
|
143
|
+
│ └── frontend/
|
|
144
|
+
│ ├── index.html Main viewer
|
|
145
|
+
│ └── admin.html Admin panel
|
|
146
|
+
├── scripts/
|
|
147
|
+
│ └── copy-assets.js Build helper (copies HTML to dist/)
|
|
148
|
+
├── package.json
|
|
149
|
+
└── tsconfig.json
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## API reference
|
|
155
|
+
|
|
156
|
+
| Method | Endpoint | Description |
|
|
157
|
+
|--------|----------|-------------|
|
|
158
|
+
| `GET` | `/api/documents` | List all documents with metadata |
|
|
159
|
+
| `GET` | `/api/documents/:id` | Get document content + rendered HTML |
|
|
160
|
+
| `GET` | `/api/documents/search?q=` | Full-text search |
|
|
161
|
+
| `GET` | `/api/config` | Read config |
|
|
162
|
+
| `PUT` | `/api/config` | Update config (`title`, `theme`, `filenamePattern`) |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Build
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
npm run build # compiles TypeScript → dist/ and copies HTML assets
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The compiled package is self-contained inside `dist/`. Only `dist/` is included in the npm publish.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":""}
|
package/dist/bin/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const server_1 = require("../src/server");
|
|
11
|
+
const program = new commander_1.Command();
|
|
12
|
+
program
|
|
13
|
+
.name('living-documentation')
|
|
14
|
+
.description('Serve a local Markdown documentation viewer')
|
|
15
|
+
.version('1.0.0')
|
|
16
|
+
.argument('[folder]', 'Path to documentation folder', '.')
|
|
17
|
+
.option('-p, --port <number>', 'Port to listen on', '4321')
|
|
18
|
+
.option('-o, --open', 'Open browser automatically')
|
|
19
|
+
.action(async (folder, options) => {
|
|
20
|
+
const docsPath = path_1.default.resolve(process.cwd(), folder);
|
|
21
|
+
if (!fs_1.default.existsSync(docsPath)) {
|
|
22
|
+
console.error(`\nError: Folder not found: ${docsPath}\n`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const stat = fs_1.default.statSync(docsPath);
|
|
26
|
+
if (!stat.isDirectory()) {
|
|
27
|
+
console.error(`\nError: Not a directory: ${docsPath}\n`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
const port = parseInt(options.port, 10);
|
|
31
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
32
|
+
console.error('\nError: Invalid port number\n');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
await (0, server_1.startServer)({ docsPath, port, openBrowser: options.open ?? false });
|
|
36
|
+
});
|
|
37
|
+
program.parse();
|
|
38
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":";;;;;;AAEA,yCAAoC;AACpC,gDAAwB;AACxB,4CAAoB;AACpB,0CAA4C;AAE5C,MAAM,OAAO,GAAG,IAAI,mBAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,sBAAsB,CAAC;KAC5B,WAAW,CAAC,6CAA6C,CAAC;KAC1D,OAAO,CAAC,OAAO,CAAC;KAChB,QAAQ,CAAC,UAAU,EAAE,8BAA8B,EAAE,GAAG,CAAC;KACzD,MAAM,CAAC,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,CAAC;KAC1D,MAAM,CAAC,YAAY,EAAE,4BAA4B,CAAC;KAClD,MAAM,CAAC,KAAK,EAAE,MAAc,EAAE,OAAwC,EAAE,EAAE;IACzE,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IAErD,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,KAAK,CAAC,8BAA8B,QAAQ,IAAI,CAAC,CAAC;QAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,YAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,6BAA6B,QAAQ,IAAI,CAAC,CAAC;QACzD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;QAC5C,OAAO,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QAChD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,IAAA,oBAAW,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,CAAC,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;AAC5E,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" class="h-full">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Admin — Living Documentation</title>
|
|
7
|
+
|
|
8
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
|
+
|
|
10
|
+
<script>
|
|
11
|
+
tailwind.config = { darkMode: 'class', theme: { extend: {} } };
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<style>
|
|
15
|
+
.field-label { @apply block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1; }
|
|
16
|
+
.field-input {
|
|
17
|
+
@apply w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700
|
|
18
|
+
bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
|
|
19
|
+
placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm;
|
|
20
|
+
}
|
|
21
|
+
.field-hint { @apply mt-1 text-xs text-gray-400 dark:text-gray-500; }
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
|
|
25
|
+
<body class="h-full bg-gray-50 dark:bg-gray-950 text-gray-900 dark:text-gray-100">
|
|
26
|
+
|
|
27
|
+
<!-- ── Header ── -->
|
|
28
|
+
<header class="flex items-center justify-between px-6 h-14 border-b border-gray-200 dark:border-gray-800
|
|
29
|
+
bg-white dark:bg-gray-900 shadow-sm">
|
|
30
|
+
<div class="flex items-center gap-3">
|
|
31
|
+
<a href="/" class="text-blue-600 dark:text-blue-400 hover:underline text-sm">
|
|
32
|
+
← Back to docs
|
|
33
|
+
</a>
|
|
34
|
+
<span class="text-gray-300 dark:text-gray-700">|</span>
|
|
35
|
+
<h1 class="text-sm font-semibold">Admin Panel</h1>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<button id="dark-toggle" title="Toggle dark mode"
|
|
39
|
+
class="p-2 rounded-lg text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors">
|
|
40
|
+
<span id="dark-icon" class="text-lg leading-none">☾</span>
|
|
41
|
+
</button>
|
|
42
|
+
</header>
|
|
43
|
+
|
|
44
|
+
<!-- ── Page ── -->
|
|
45
|
+
<main class="max-w-2xl mx-auto px-6 py-10">
|
|
46
|
+
|
|
47
|
+
<!-- Title -->
|
|
48
|
+
<div class="mb-8">
|
|
49
|
+
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-50">Configuration</h2>
|
|
50
|
+
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
51
|
+
Settings are saved to <code class="text-xs bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">.living-doc.json</code>
|
|
52
|
+
in your docs folder.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<!-- ── Form ── -->
|
|
57
|
+
<form id="config-form" class="space-y-6" novalidate>
|
|
58
|
+
|
|
59
|
+
<!-- Read-only info card -->
|
|
60
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5">
|
|
61
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-4">Server Info</h3>
|
|
62
|
+
<dl class="grid grid-cols-2 gap-x-6 gap-y-3 text-sm">
|
|
63
|
+
<div>
|
|
64
|
+
<dt class="text-xs text-gray-400 uppercase tracking-wide mb-0.5">Docs Folder</dt>
|
|
65
|
+
<dd id="info-folder" class="font-mono text-xs text-gray-700 dark:text-gray-300 break-all">—</dd>
|
|
66
|
+
</div>
|
|
67
|
+
<div>
|
|
68
|
+
<dt class="text-xs text-gray-400 uppercase tracking-wide mb-0.5">Port</dt>
|
|
69
|
+
<dd id="info-port" class="font-mono text-xs text-gray-700 dark:text-gray-300">—</dd>
|
|
70
|
+
</div>
|
|
71
|
+
</dl>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- Editable settings -->
|
|
75
|
+
<div class="rounded-xl border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 p-5 space-y-5">
|
|
76
|
+
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Appearance & Metadata</h3>
|
|
77
|
+
|
|
78
|
+
<!-- Site Title -->
|
|
79
|
+
<div>
|
|
80
|
+
<label class="field-label" for="field-title">Site Title</label>
|
|
81
|
+
<input id="field-title" name="title" type="text" class="field-input"
|
|
82
|
+
placeholder="Living Documentation" />
|
|
83
|
+
<p class="field-hint">Displayed in the browser tab and sidebar header.</p>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<!-- Theme -->
|
|
87
|
+
<div>
|
|
88
|
+
<label class="field-label" for="field-theme">Default Theme</label>
|
|
89
|
+
<select id="field-theme" name="theme" class="field-input">
|
|
90
|
+
<option value="system">System (follow OS preference)</option>
|
|
91
|
+
<option value="light">Light</option>
|
|
92
|
+
<option value="dark">Dark</option>
|
|
93
|
+
</select>
|
|
94
|
+
<p class="field-hint">Users can always override this with the toggle button.</p>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- Filename Pattern -->
|
|
98
|
+
<div>
|
|
99
|
+
<label class="field-label" for="field-pattern">Filename Pattern</label>
|
|
100
|
+
<input id="field-pattern" name="filenamePattern" type="text" class="field-input"
|
|
101
|
+
placeholder="YYYY_MM_DD_[Category]_title" />
|
|
102
|
+
<p class="field-hint">
|
|
103
|
+
Documents matching this pattern are parsed for date, category, and title.
|
|
104
|
+
<span class="font-mono bg-gray-100 dark:bg-gray-800 px-1 rounded">YYYY_MM_DD_[Category]_title.md</span>
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Pattern preview -->
|
|
110
|
+
<div id="pattern-preview"
|
|
111
|
+
class="rounded-xl border border-blue-100 dark:border-blue-900 bg-blue-50 dark:bg-blue-950/30 p-5">
|
|
112
|
+
<h3 class="text-sm font-semibold text-blue-700 dark:text-blue-400 mb-3">Pattern Preview</h3>
|
|
113
|
+
<p class="text-xs text-gray-500 dark:text-gray-400 mb-3">How your pattern parses example filenames:</p>
|
|
114
|
+
<div id="preview-rows" class="space-y-2 text-sm"></div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<!-- Submit -->
|
|
118
|
+
<div class="flex items-center justify-between">
|
|
119
|
+
<div id="save-msg" class="text-sm"></div>
|
|
120
|
+
<button type="submit"
|
|
121
|
+
class="px-5 py-2 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold text-sm
|
|
122
|
+
transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
|
|
123
|
+
Save changes
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</form>
|
|
127
|
+
|
|
128
|
+
</main>
|
|
129
|
+
|
|
130
|
+
<script>
|
|
131
|
+
// ── Boot ───────────────────────────────────────────────────
|
|
132
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
133
|
+
applyDarkMode(loadDarkPref());
|
|
134
|
+
setupDarkToggle();
|
|
135
|
+
await loadConfig();
|
|
136
|
+
setupPatternPreview();
|
|
137
|
+
document.getElementById('config-form').addEventListener('submit', saveConfig);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ── Dark mode ──────────────────────────────────────────────
|
|
141
|
+
function loadDarkPref() {
|
|
142
|
+
const saved = localStorage.getItem('ld-dark');
|
|
143
|
+
if (saved !== null) return saved === 'true';
|
|
144
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
145
|
+
}
|
|
146
|
+
function applyDarkMode(dark) {
|
|
147
|
+
document.documentElement.classList.toggle('dark', dark);
|
|
148
|
+
document.getElementById('dark-icon').textContent = dark ? '☀' : '☾';
|
|
149
|
+
}
|
|
150
|
+
function setupDarkToggle() {
|
|
151
|
+
document.getElementById('dark-toggle').addEventListener('click', () => {
|
|
152
|
+
const isDark = document.documentElement.classList.toggle('dark');
|
|
153
|
+
localStorage.setItem('ld-dark', isDark);
|
|
154
|
+
document.getElementById('dark-icon').textContent = isDark ? '☀' : '☾';
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Config ─────────────────────────────────────────────────
|
|
159
|
+
async function loadConfig() {
|
|
160
|
+
try {
|
|
161
|
+
const cfg = await fetch('/api/config').then(r => r.json());
|
|
162
|
+
document.getElementById('info-folder').textContent = cfg.docsFolder || '—';
|
|
163
|
+
document.getElementById('info-port').textContent = cfg.port || '—';
|
|
164
|
+
document.getElementById('field-title').value = cfg.title || '';
|
|
165
|
+
document.getElementById('field-theme').value = cfg.theme || 'system';
|
|
166
|
+
document.getElementById('field-pattern').value = cfg.filenamePattern || '';
|
|
167
|
+
updatePreview(cfg.filenamePattern);
|
|
168
|
+
} catch {
|
|
169
|
+
showMsg('Failed to load config.', 'error');
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function saveConfig(e) {
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
const payload = {
|
|
176
|
+
title: document.getElementById('field-title').value.trim(),
|
|
177
|
+
theme: document.getElementById('field-theme').value,
|
|
178
|
+
filenamePattern: document.getElementById('field-pattern').value.trim(),
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const res = await fetch('/api/config', {
|
|
183
|
+
method: 'PUT',
|
|
184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
185
|
+
body: JSON.stringify(payload),
|
|
186
|
+
});
|
|
187
|
+
if (!res.ok) throw new Error(await res.text());
|
|
188
|
+
showMsg('Saved!', 'ok');
|
|
189
|
+
} catch (err) {
|
|
190
|
+
showMsg('Save failed: ' + err.message, 'error');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function showMsg(text, type) {
|
|
195
|
+
const el = document.getElementById('save-msg');
|
|
196
|
+
el.textContent = text;
|
|
197
|
+
el.className = 'text-sm ' + (type === 'ok'
|
|
198
|
+
? 'text-green-600 dark:text-green-400'
|
|
199
|
+
: 'text-red-600 dark:text-red-400');
|
|
200
|
+
if (type === 'ok') setTimeout(() => { el.textContent = ''; }, 3000);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Pattern preview ────────────────────────────────────────
|
|
204
|
+
const EXAMPLES = [
|
|
205
|
+
'2024_01_15_[DevOps]_deploy_pipeline.md',
|
|
206
|
+
'2023_11_03_[Frontend]_react_hooks_guide.md',
|
|
207
|
+
'2025_06_20_meeting_notes.md',
|
|
208
|
+
'readme.md',
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
// Parse like server does
|
|
212
|
+
const FULL_PAT = /^(\d{4}_\d{2}_\d{2})_\[([^\]]+)\]_(.+)\.md$/i;
|
|
213
|
+
const DATE_ONLY = /^(\d{4}_\d{2}_\d{2})_(.+)\.md$/i;
|
|
214
|
+
|
|
215
|
+
function parsePreview(filename) {
|
|
216
|
+
const full = filename.match(FULL_PAT);
|
|
217
|
+
if (full) {
|
|
218
|
+
const [, d, cat, t] = full;
|
|
219
|
+
return { date: d.replace(/_/g,'-'), category: cat, title: titleCase(t), match: true };
|
|
220
|
+
}
|
|
221
|
+
const d = filename.match(DATE_ONLY);
|
|
222
|
+
if (d) {
|
|
223
|
+
return { date: d[1].replace(/_/g,'-'), category: 'Uncategorized', title: titleCase(d[2]), match: true };
|
|
224
|
+
}
|
|
225
|
+
return { date: null, category: 'Uncategorized', title: filename.replace('.md',''), match: false };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function titleCase(s) {
|
|
229
|
+
return s.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function updatePreview() {
|
|
233
|
+
const rows = document.getElementById('preview-rows');
|
|
234
|
+
rows.innerHTML = EXAMPLES.map(f => {
|
|
235
|
+
const p = parsePreview(f);
|
|
236
|
+
return `
|
|
237
|
+
<div class="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3">
|
|
238
|
+
<p class="font-mono text-xs text-gray-500 dark:text-gray-400 mb-2 break-all">${esc(f)}</p>
|
|
239
|
+
<div class="grid grid-cols-3 gap-2 text-xs">
|
|
240
|
+
<div>
|
|
241
|
+
<span class="text-gray-400 block mb-0.5">Date</span>
|
|
242
|
+
<span class="${p.date ? 'text-green-600 dark:text-green-400' : 'text-gray-400'}">${p.date || 'none'}</span>
|
|
243
|
+
</div>
|
|
244
|
+
<div>
|
|
245
|
+
<span class="text-gray-400 block mb-0.5">Category</span>
|
|
246
|
+
<span class="text-blue-600 dark:text-blue-400">${esc(p.category)}</span>
|
|
247
|
+
</div>
|
|
248
|
+
<div>
|
|
249
|
+
<span class="text-gray-400 block mb-0.5">Title</span>
|
|
250
|
+
<span class="text-gray-700 dark:text-gray-300 truncate block">${esc(p.title)}</span>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>`;
|
|
254
|
+
}).join('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function setupPatternPreview() {
|
|
258
|
+
document.getElementById('field-pattern').addEventListener('input', updatePreview);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function esc(s) {
|
|
262
|
+
return String(s)
|
|
263
|
+
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')
|
|
264
|
+
.replace(/"/g,'"').replace(/'/g,''');
|
|
265
|
+
}
|
|
266
|
+
</script>
|
|
267
|
+
</body>
|
|
268
|
+
</html>
|