@inovus-medical/docs 0.5.0 → 0.8.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 +11 -1
- package/assets/themes/totum.css +112 -0
- package/bin/cli.js +7 -6
- package/package.json +1 -1
- package/src/build.js +39 -1
- package/src/config.js +4 -1
- package/src/init.js +4 -0
- package/src/layout.js +5 -2
- package/src/watch.js +2 -2
package/README.md
CHANGED
|
@@ -22,7 +22,17 @@ npx inovus-docs dev # build + serve + watch (http://localhost:8788)
|
|
|
22
22
|
npx inovus-docs serve # serve an existing dist/
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Flags: `--root <dir>` `--in <docs>` `--out <dist>` `--port <n>`.
|
|
25
|
+
Flags: `--root <dir>` `--in <docs>` `--out <dist>` `--port <n>` `--theme <name>`.
|
|
26
|
+
|
|
27
|
+
## Themes
|
|
28
|
+
|
|
29
|
+
Named themes each include light and dark variants. Built-ins: `default`, `totum`.
|
|
30
|
+
|
|
31
|
+
Selection (first match wins): `--theme` > `DOCS_THEME` env var > `"theme"` in
|
|
32
|
+
`docs.config.json` > `default`. On Cloudflare, set `DOCS_THEME` under Worker →
|
|
33
|
+
**Settings → Build → Variables** (build-time).
|
|
34
|
+
|
|
35
|
+
Local preview: `npx inovus-docs dev --theme totum`.
|
|
26
36
|
|
|
27
37
|
## What it does
|
|
28
38
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* Totum theme — light + dark (selected via DOCS_THEME / --theme / docs.config.json) */
|
|
2
|
+
|
|
3
|
+
:root {
|
|
4
|
+
--accent: #00bbff;
|
|
5
|
+
--accent-hover: #0099cc;
|
|
6
|
+
--bg: #f9fafb;
|
|
7
|
+
--bg-soft: #ffffff;
|
|
8
|
+
--bg-sidebar: #ffffff;
|
|
9
|
+
--text: #0f172a;
|
|
10
|
+
--text-soft: #64748b;
|
|
11
|
+
--border: #e2e8f0;
|
|
12
|
+
--code-bg: #f1f5f9;
|
|
13
|
+
--header-bg: #000000;
|
|
14
|
+
--header-text: #ffffff;
|
|
15
|
+
--header-muted: #d1d5db;
|
|
16
|
+
--radius: 12px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
[data-theme='dark'] {
|
|
20
|
+
--accent: #00bbff;
|
|
21
|
+
--accent-hover: #33ccff;
|
|
22
|
+
--bg: #0a0a0a;
|
|
23
|
+
--bg-soft: #141414;
|
|
24
|
+
--bg-sidebar: #141414;
|
|
25
|
+
--text: #f1f5f9;
|
|
26
|
+
--text-soft: #94a3b8;
|
|
27
|
+
--border: #262626;
|
|
28
|
+
--code-bg: #1a1a1a;
|
|
29
|
+
--header-bg: #000000;
|
|
30
|
+
--header-text: #ffffff;
|
|
31
|
+
--header-muted: #9ca3af;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* ---------- Header (black in both modes) ---------- */
|
|
35
|
+
[data-docs-theme='totum'] .site-header {
|
|
36
|
+
background: var(--header-bg);
|
|
37
|
+
border-bottom-color: #1f2937;
|
|
38
|
+
color: var(--header-text);
|
|
39
|
+
backdrop-filter: none;
|
|
40
|
+
}
|
|
41
|
+
[data-docs-theme='totum'] .site-header .brand {
|
|
42
|
+
color: var(--header-text);
|
|
43
|
+
}
|
|
44
|
+
[data-docs-theme='totum'] .site-header .header-link {
|
|
45
|
+
color: var(--header-muted);
|
|
46
|
+
}
|
|
47
|
+
[data-docs-theme='totum'] .site-header .header-link:hover {
|
|
48
|
+
color: var(--header-text);
|
|
49
|
+
}
|
|
50
|
+
[data-docs-theme='totum'] .site-header .vis-badge {
|
|
51
|
+
color: var(--header-muted);
|
|
52
|
+
border-color: #374151;
|
|
53
|
+
}
|
|
54
|
+
[data-docs-theme='totum'] .site-header .vis-private {
|
|
55
|
+
color: #fbbf24;
|
|
56
|
+
border-color: color-mix(in srgb, #fbbf24 45%, #374151);
|
|
57
|
+
}
|
|
58
|
+
[data-docs-theme='totum'] .site-header .vis-public {
|
|
59
|
+
color: #4ade80;
|
|
60
|
+
border-color: color-mix(in srgb, #4ade80 45%, #374151);
|
|
61
|
+
}
|
|
62
|
+
[data-docs-theme='totum'] .site-header .theme-toggle,
|
|
63
|
+
[data-docs-theme='totum'] .site-header .menu-toggle {
|
|
64
|
+
color: var(--header-text);
|
|
65
|
+
border-color: #374151;
|
|
66
|
+
}
|
|
67
|
+
[data-docs-theme='totum'] .site-header .theme-toggle:hover,
|
|
68
|
+
[data-docs-theme='totum'] .site-header .menu-toggle:hover {
|
|
69
|
+
background: #1f2937;
|
|
70
|
+
}
|
|
71
|
+
[data-docs-theme='totum'] .site-header #search-input {
|
|
72
|
+
background: #1f2937;
|
|
73
|
+
border-color: #374151;
|
|
74
|
+
color: var(--header-text);
|
|
75
|
+
}
|
|
76
|
+
[data-docs-theme='totum'] .site-header #search-input::placeholder {
|
|
77
|
+
color: #9ca3af;
|
|
78
|
+
}
|
|
79
|
+
[data-docs-theme='totum'] .site-header #search-input:focus {
|
|
80
|
+
outline: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
|
|
81
|
+
border-color: var(--accent);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ---------- Nav / surfaces ---------- */
|
|
85
|
+
[data-docs-theme='totum'] .sidebar {
|
|
86
|
+
border-right-color: var(--border);
|
|
87
|
+
}
|
|
88
|
+
[data-docs-theme='totum'] .nav-link {
|
|
89
|
+
border-radius: 999px;
|
|
90
|
+
}
|
|
91
|
+
[data-docs-theme='totum'] .nav-link.active {
|
|
92
|
+
color: var(--accent);
|
|
93
|
+
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
}
|
|
96
|
+
[data-docs-theme='totum'] .pager-link {
|
|
97
|
+
border-radius: var(--radius);
|
|
98
|
+
background: var(--bg-soft);
|
|
99
|
+
}
|
|
100
|
+
[data-docs-theme='totum'] .pager-link:hover {
|
|
101
|
+
border-color: var(--accent);
|
|
102
|
+
}
|
|
103
|
+
[data-docs-theme='totum'] .prose pre,
|
|
104
|
+
[data-docs-theme='totum'] .mermaid-wrap {
|
|
105
|
+
border-radius: var(--radius);
|
|
106
|
+
}
|
|
107
|
+
[data-docs-theme='totum'] .search-results {
|
|
108
|
+
border-radius: var(--radius);
|
|
109
|
+
}
|
|
110
|
+
[data-docs-theme='totum'] a:hover {
|
|
111
|
+
color: var(--accent-hover);
|
|
112
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ const root = path.resolve(flags.root || process.cwd())
|
|
|
31
31
|
const inDir = flags.in // undefined -> build() auto-detects (docs/ or repo root)
|
|
32
32
|
const outDir = flags.out || 'dist'
|
|
33
33
|
const port = Number(flags.port || 8788)
|
|
34
|
+
const theme = typeof flags.theme === 'string' ? flags.theme : undefined
|
|
34
35
|
|
|
35
36
|
async function run() {
|
|
36
37
|
if (cmd === 'init') {
|
|
@@ -50,18 +51,18 @@ async function run() {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
if (cmd === 'build') {
|
|
53
|
-
const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir })
|
|
54
|
+
const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir, theme })
|
|
54
55
|
console.log(`✓ Built ${pageCount} page(s) from ${contentDir === '.' ? 'repo root' : contentDir + '/'} -> ${path.relative(process.cwd(), outPath) || outPath}`)
|
|
55
56
|
return
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
if (cmd === 'dev') {
|
|
59
|
-
const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir })
|
|
60
|
+
const { pageCount, outPath, contentDir } = await build({ root, inDir, outDir, theme })
|
|
60
61
|
console.log(`✓ Built ${pageCount} page(s) from ${contentDir === '.' ? 'repo root' : contentDir + '/'}`)
|
|
61
62
|
const { port: actual } = await serve({ dir: outPath, port })
|
|
62
63
|
const busy = actual !== port ? ` (port ${port} was busy)` : ''
|
|
63
64
|
console.log(`→ Serving http://localhost:${actual}${busy} (watching ${contentDir === '.' ? 'repo root' : contentDir + '/'} for changes)`)
|
|
64
|
-
watchAndRebuild({ root, inDir: contentDir, outDir })
|
|
65
|
+
watchAndRebuild({ root, inDir: contentDir, outDir, theme })
|
|
65
66
|
return
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -76,9 +77,9 @@ async function run() {
|
|
|
76
77
|
console.error(
|
|
77
78
|
`Unknown command: ${cmd}\n` +
|
|
78
79
|
`Usage:\n` +
|
|
79
|
-
` inovus-docs init [--name <n>] [--title <t>] [--public] [--repo <url>] [--no-readme] [--force]\n` +
|
|
80
|
-
` inovus-docs build [--root .] [--in <dir>] [--out dist]\n` +
|
|
81
|
-
` inovus-docs dev [--port 8788]\n` +
|
|
80
|
+
` inovus-docs init [--name <n>] [--title <t>] [--content <dir>] [--public] [--repo <url>] [--no-readme] [--force]\n` +
|
|
81
|
+
` inovus-docs build [--root .] [--in <dir>] [--out dist] [--theme <name>]\n` +
|
|
82
|
+
` inovus-docs dev [--port 8788] [--theme <name>]\n` +
|
|
82
83
|
` inovus-docs serve [--port 8788]`
|
|
83
84
|
)
|
|
84
85
|
process.exit(1)
|
package/package.json
CHANGED
package/src/build.js
CHANGED
|
@@ -10,11 +10,43 @@ import { buildRouteMap, rewriteLinks } from './links.js'
|
|
|
10
10
|
|
|
11
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
12
|
const ASSET_SRC = path.join(__dirname, '..', 'assets')
|
|
13
|
+
const THEMES_DIR = path.join(ASSET_SRC, 'themes')
|
|
13
14
|
|
|
14
15
|
const DEFAULT_IGNORE_DIRS = new Set([
|
|
15
16
|
'node_modules', '.git', '.github', '.svn', '.hg', '.cache', '.vscode', '.idea', '.wrangler'
|
|
16
17
|
])
|
|
17
18
|
|
|
19
|
+
/** List named themes shipped as assets/themes/<name>.css (plus built-in "default"). */
|
|
20
|
+
async function listThemes() {
|
|
21
|
+
const names = new Set(['default'])
|
|
22
|
+
try {
|
|
23
|
+
const entries = await fs.readdir(THEMES_DIR)
|
|
24
|
+
for (const e of entries) {
|
|
25
|
+
if (e.toLowerCase().endsWith('.css')) names.add(e.slice(0, -4).toLowerCase())
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err.code !== 'ENOENT') throw err
|
|
29
|
+
}
|
|
30
|
+
return names
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve theme: CLI --theme > DOCS_THEME env > docs.config.json > "default".
|
|
35
|
+
* Unknown names warn and fall back to default.
|
|
36
|
+
*/
|
|
37
|
+
async function resolveTheme({ themeFlag, config }) {
|
|
38
|
+
const raw = themeFlag ?? process.env.DOCS_THEME ?? config.theme ?? 'default'
|
|
39
|
+
const name = String(raw).trim().toLowerCase() || 'default'
|
|
40
|
+
const available = await listThemes()
|
|
41
|
+
if (!available.has(name)) {
|
|
42
|
+
console.warn(
|
|
43
|
+
`⚠ Unknown theme "${name}" — falling back to default. Available: ${[...available].sort().join(', ')}`
|
|
44
|
+
)
|
|
45
|
+
return 'default'
|
|
46
|
+
}
|
|
47
|
+
return name
|
|
48
|
+
}
|
|
49
|
+
|
|
18
50
|
// Only these non-markdown files get copied into the site. Keeps a whole-repo scan from
|
|
19
51
|
// shipping source, lockfiles, or secrets (e.g. .env) to a public site.
|
|
20
52
|
const MEDIA_EXT = new Set([
|
|
@@ -87,8 +119,10 @@ function routeToFile(outPath, route) {
|
|
|
87
119
|
return path.join(outPath, rel, 'index.html')
|
|
88
120
|
}
|
|
89
121
|
|
|
90
|
-
export async function build({ root = process.cwd(), inDir, outDir = 'dist' } = {}) {
|
|
122
|
+
export async function build({ root = process.cwd(), inDir, outDir = 'dist', theme: themeFlag } = {}) {
|
|
91
123
|
const config = await loadConfig(root)
|
|
124
|
+
const theme = await resolveTheme({ themeFlag, config })
|
|
125
|
+
config.theme = theme
|
|
92
126
|
const outPath = path.join(root, outDir)
|
|
93
127
|
const ignore = makeIgnore(config, path.basename(outPath))
|
|
94
128
|
|
|
@@ -190,6 +224,10 @@ export async function build({ root = process.cwd(), inDir, outDir = 'dist' } = {
|
|
|
190
224
|
await fs.copyFile(path.join(ASSET_SRC, name), path.join(assetsOut, name))
|
|
191
225
|
}
|
|
192
226
|
|
|
227
|
+
if (theme !== 'default') {
|
|
228
|
+
await fs.copyFile(path.join(THEMES_DIR, `${theme}.css`), path.join(assetsOut, 'theme.css'))
|
|
229
|
+
}
|
|
230
|
+
|
|
193
231
|
for (const file of assetFiles) {
|
|
194
232
|
const rel = path.relative(contentRoot, file)
|
|
195
233
|
const dest = path.join(outPath, rel)
|
package/src/config.js
CHANGED
|
@@ -5,7 +5,10 @@ const DEFAULTS = {
|
|
|
5
5
|
title: 'Documentation',
|
|
6
6
|
description: '',
|
|
7
7
|
logo: null,
|
|
8
|
-
accent
|
|
8
|
+
// null = use the theme's accent (base styles.css sets --accent for default).
|
|
9
|
+
accent: null,
|
|
10
|
+
// Named theme (each has light + dark). Overridden by DOCS_THEME / --theme.
|
|
11
|
+
theme: 'default',
|
|
9
12
|
repo: null,
|
|
10
13
|
editBase: null,
|
|
11
14
|
visibility: 'private',
|
package/src/init.js
CHANGED
|
@@ -112,6 +112,9 @@ export async function init({ root = process.cwd(), flags = {} } = {}) {
|
|
|
112
112
|
const visibility = flags.public ? 'public' : 'private'
|
|
113
113
|
const repoUrl = flags.repo || remote.httpsUrl || null
|
|
114
114
|
const force = flags.force === true
|
|
115
|
+
// Content root: --content <dir>, else auto-set "docs" when docs/ exists, else omit (auto-detect at build).
|
|
116
|
+
let content = typeof flags.content === 'string' ? flags.content.trim() : null
|
|
117
|
+
if (!content && (await fileExists(path.join(root, 'docs')))) content = 'docs'
|
|
115
118
|
|
|
116
119
|
const created = []
|
|
117
120
|
const skipped = []
|
|
@@ -123,6 +126,7 @@ export async function init({ root = process.cwd(), flags = {} } = {}) {
|
|
|
123
126
|
} else {
|
|
124
127
|
const cfg = { title, description: '', visibility }
|
|
125
128
|
if (repoUrl) cfg.repo = repoUrl
|
|
129
|
+
if (content) cfg.content = content
|
|
126
130
|
await fs.writeFile(cfgPath, JSON.stringify(cfg, null, 2) + '\n')
|
|
127
131
|
created.push('docs.config.json')
|
|
128
132
|
}
|
package/src/layout.js
CHANGED
|
@@ -74,16 +74,19 @@ function header(config) {
|
|
|
74
74
|
|
|
75
75
|
function shell({ config, pageTitle, description, navHtml, main }) {
|
|
76
76
|
const accent = config.accent ? `<style>:root{--accent:${esc(config.accent)}}</style>` : ''
|
|
77
|
+
const docsTheme = (config.theme && config.theme !== 'default') ? config.theme : 'default'
|
|
78
|
+
const themeLink =
|
|
79
|
+
docsTheme !== 'default' ? '<link rel="stylesheet" href="/_assets/theme.css">\n' : ''
|
|
77
80
|
const desc = description || config.description || ''
|
|
78
81
|
return `<!doctype html>
|
|
79
|
-
<html lang="en" data-theme="light">
|
|
82
|
+
<html lang="en" data-theme="light" data-docs-theme="${esc(docsTheme)}">
|
|
80
83
|
<head>
|
|
81
84
|
<meta charset="utf-8">
|
|
82
85
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
83
86
|
<title>${esc(pageTitle)}</title>
|
|
84
87
|
${desc ? `<meta name="description" content="${esc(desc)}">` : ''}
|
|
85
88
|
<link rel="stylesheet" href="/_assets/styles.css">
|
|
86
|
-
${accent}
|
|
89
|
+
${themeLink}${accent}
|
|
87
90
|
<script>(function(){try{var t=localStorage.getItem('theme');if(!t)t=window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
|
|
88
91
|
</head>
|
|
89
92
|
<body>
|
package/src/watch.js
CHANGED
|
@@ -10,7 +10,7 @@ const IGNORED = /(^|[\\/])(node_modules|\.git|dist)([\\/]|$)/
|
|
|
10
10
|
* ignored so writing dist/ doesn't trigger an infinite rebuild loop (matters when the
|
|
11
11
|
* content root is the whole repo).
|
|
12
12
|
*/
|
|
13
|
-
export function watchAndRebuild({ root, inDir, outDir }) {
|
|
13
|
+
export function watchAndRebuild({ root, inDir, outDir, theme }) {
|
|
14
14
|
const watchPath = inDir === '.' ? root : path.join(root, inDir)
|
|
15
15
|
const outBase = path.basename(outDir)
|
|
16
16
|
let timer = null
|
|
@@ -23,7 +23,7 @@ export function watchAndRebuild({ root, inDir, outDir }) {
|
|
|
23
23
|
if (building) return
|
|
24
24
|
building = true
|
|
25
25
|
try {
|
|
26
|
-
const { pageCount } = await build({ root, inDir: inDir === '.' ? '.' : inDir, outDir })
|
|
26
|
+
const { pageCount } = await build({ root, inDir: inDir === '.' ? '.' : inDir, outDir, theme })
|
|
27
27
|
console.log(`↻ Rebuilt ${pageCount} page(s)`)
|
|
28
28
|
} catch (err) {
|
|
29
29
|
console.error('Build failed:', err.message)
|