@inovus-medical/docs 0.5.0 → 0.6.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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inovus-medical/docs",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Turn a folder of markdown into a themed, Cloudflare-ready docs site.",
5
5
  "type": "module",
6
6
  "bin": {
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: '#2563eb',
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)