projview 1.0.0 → 1.2.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 +7 -4
- package/docs/README.md +3 -0
- package/docs/data/minified.json +1 -0
- package/docs/data/not-strict.jsonc.json +4 -0
- package/docs/data/values.yaml +36 -0
- package/docs/reference/api.md +32 -1
- package/package.json +1 -1
- package/public/comments.js +4 -4
- package/public/editor.js +306 -0
- package/public/history.js +162 -0
- package/public/icons.js +7 -1
- package/public/index.html +20 -10
- package/public/links.js +3 -2
- package/public/live.js +7 -1
- package/public/main.js +7 -1
- package/public/mermaid.js +17 -10
- package/public/search.js +1 -3
- package/public/store.js +3 -1
- package/public/style.css +350 -122
- package/public/theme.js +52 -0
- package/public/toc.js +13 -5
- package/public/tree.js +4 -3
- package/public/viewer.js +14 -3
- package/src/render.js +161 -1
- package/src/search.js +17 -4
- package/src/server.js +131 -19
- package/src/store.js +88 -3
- package/src/walk.js +11 -1
package/public/index.html
CHANGED
|
@@ -3,9 +3,19 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<!-- we ship our own dark
|
|
7
|
-
<meta name="color-scheme" content="dark" />
|
|
6
|
+
<!-- we ship our own light + dark themes: tell the UA + Dark Reader to leave colors alone -->
|
|
7
|
+
<meta name="color-scheme" content="dark light" />
|
|
8
8
|
<meta name="darkreader-lock" />
|
|
9
|
+
<!-- resolve the saved theme before first paint (theme.js owns it after load;
|
|
10
|
+
the 'pv-theme' key and fallback must match theme.js) -->
|
|
11
|
+
<script>
|
|
12
|
+
(function () {
|
|
13
|
+
var p = 'system';
|
|
14
|
+
try { p = localStorage.getItem('pv-theme') || 'system'; } catch (e) {}
|
|
15
|
+
var light = p === 'light' || (p !== 'dark' && matchMedia('(prefers-color-scheme: light)').matches);
|
|
16
|
+
document.documentElement.dataset.theme = light ? 'light' : 'dark';
|
|
17
|
+
})();
|
|
18
|
+
</script>
|
|
9
19
|
<title>projview</title>
|
|
10
20
|
<link rel="stylesheet" href="/vendor/hljs.css" />
|
|
11
21
|
<link rel="stylesheet" href="/style.css" />
|
|
@@ -16,21 +26,22 @@
|
|
|
16
26
|
<header id="brand">
|
|
17
27
|
<span class="logo" aria-hidden="true">
|
|
18
28
|
<svg viewBox="0 0 24 24" width="26" height="26" fill="none">
|
|
19
|
-
<rect x="2" y="4.5" width="20" height="15" rx="4" stroke="
|
|
20
|
-
<line x1="8.5" y1="4.5" x2="8.5" y2="19.5" stroke="
|
|
21
|
-
<circle cx="13.4" cy="9.4" r="1.25" fill="
|
|
22
|
-
<circle cx="18" cy="9.4" r="1.25" fill="
|
|
23
|
-
<circle cx="15.7" cy="14.6" r="1.25" fill="
|
|
24
|
-
<path d="M13.4 9.4 15.7 14.6 18 9.4" stroke="
|
|
29
|
+
<rect x="2" y="4.5" width="20" height="15" rx="4" stroke="currentColor" stroke-width="1.8"/>
|
|
30
|
+
<line x1="8.5" y1="4.5" x2="8.5" y2="19.5" stroke="currentColor" stroke-width="1.8"/>
|
|
31
|
+
<circle cx="13.4" cy="9.4" r="1.25" fill="currentColor"/>
|
|
32
|
+
<circle cx="18" cy="9.4" r="1.25" fill="currentColor"/>
|
|
33
|
+
<circle cx="15.7" cy="14.6" r="1.25" fill="currentColor"/>
|
|
34
|
+
<path d="M13.4 9.4 15.7 14.6 18 9.4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
|
|
25
35
|
</svg>
|
|
26
36
|
</span>
|
|
27
37
|
<div class="brand-text">
|
|
28
38
|
<strong class="wordmark"><span class="wm-proj">proj</span><span class="wm-view">view</span></strong>
|
|
29
39
|
<small id="version" class="version"></small>
|
|
30
40
|
</div>
|
|
41
|
+
<button id="theme-toggle" class="theme-toggle" title="theme"></button>
|
|
31
42
|
</header>
|
|
32
43
|
<button id="open-palette" class="open-palette" title="search (⌘K)">
|
|
33
|
-
<span class="op-ico"
|
|
44
|
+
<span class="op-ico"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/></svg></span><span class="op-text">search…</span><kbd>⌘K</kbd>
|
|
34
45
|
</button>
|
|
35
46
|
<div id="tree" class="tree"></div>
|
|
36
47
|
<footer id="side-foot"><span id="file-count">0</span> files · ctrl-c to stop</footer>
|
|
@@ -40,7 +51,6 @@
|
|
|
40
51
|
<nav id="breadcrumb" hidden></nav>
|
|
41
52
|
<div id="content" class="markdown-body">
|
|
42
53
|
<div class="empty-state">
|
|
43
|
-
<div class="empty-emoji">👈</div>
|
|
44
54
|
<p>pick a file from the tree to preview it</p>
|
|
45
55
|
</div>
|
|
46
56
|
</div>
|
package/public/links.js
CHANGED
|
@@ -5,6 +5,7 @@ import { state } from './store.js';
|
|
|
5
5
|
import { resolveRelative } from './paths.js';
|
|
6
6
|
import { openFile, scrollToFragment } from './viewer.js';
|
|
7
7
|
import { toast } from './toast.js';
|
|
8
|
+
import { fileKind } from './icons.js';
|
|
8
9
|
|
|
9
10
|
const contentEl = $('content');
|
|
10
11
|
|
|
@@ -26,6 +27,6 @@ contentEl.addEventListener('click', (e) => {
|
|
|
26
27
|
|
|
27
28
|
const [pathPart, frag] = href.split('#');
|
|
28
29
|
const target = resolveRelative(state.currentPath, pathPart);
|
|
29
|
-
if (
|
|
30
|
-
else toast(`can't preview "${pathPart}" - only .md / .mmd for now`);
|
|
30
|
+
if (fileKind(target)) openFile(target, frag);
|
|
31
|
+
else toast(`can't preview "${pathPart}" - only .md / .mmd / .json / .yaml for now`);
|
|
31
32
|
});
|
package/public/live.js
CHANGED
|
@@ -5,6 +5,8 @@ import { state } from './store.js';
|
|
|
5
5
|
import { loadTree } from './tree.js';
|
|
6
6
|
import { openFile } from './viewer.js';
|
|
7
7
|
import { refreshComments } from './comments.js';
|
|
8
|
+
import { isEditing, onDiskChange } from './editor.js';
|
|
9
|
+
import { isHistory, refreshHistory } from './history.js';
|
|
8
10
|
|
|
9
11
|
const versionEl = $('version');
|
|
10
12
|
|
|
@@ -12,7 +14,11 @@ export function connectEvents() {
|
|
|
12
14
|
const es = new EventSource('/api/events');
|
|
13
15
|
es.addEventListener('change', (e) => {
|
|
14
16
|
const { path } = JSON.parse(e.data);
|
|
15
|
-
if (path
|
|
17
|
+
if (path !== state.currentPath) return;
|
|
18
|
+
pulse();
|
|
19
|
+
if (isEditing(path)) onDiskChange(); // never re-render over an open editor
|
|
20
|
+
else if (isHistory(path)) setTimeout(refreshHistory, 300); // the new version lands just after the change event
|
|
21
|
+
else openFile(path);
|
|
16
22
|
});
|
|
17
23
|
es.addEventListener('tree', async () => { pulse(); await loadTree(); });
|
|
18
24
|
es.addEventListener('comments', (e) => {
|
package/public/main.js
CHANGED
|
@@ -8,9 +8,11 @@ import { connectEvents } from './live.js';
|
|
|
8
8
|
// side-effect modules: each attaches its own listeners on import
|
|
9
9
|
import './links.js';
|
|
10
10
|
import './toc.js';
|
|
11
|
-
import './mermaid.js';
|
|
11
|
+
import { runMermaid } from './mermaid.js';
|
|
12
|
+
import { onThemeChange } from './theme.js';
|
|
12
13
|
import './search.js';
|
|
13
14
|
import './comments.js';
|
|
15
|
+
import './editor.js';
|
|
14
16
|
|
|
15
17
|
// browser back / forward
|
|
16
18
|
window.addEventListener('hashchange', () => {
|
|
@@ -18,6 +20,10 @@ window.addEventListener('hashchange', () => {
|
|
|
18
20
|
if (p && p !== state.currentPath) openFile(p); // guard prevents self-trigger loop
|
|
19
21
|
});
|
|
20
22
|
|
|
23
|
+
// light/dark flip: CSS follows on its own; diagrams are baked SVG, so redraw them
|
|
24
|
+
// in place (no re-fetch, no scroll jump, an open editor is left alone)
|
|
25
|
+
onThemeChange(() => runMermaid({ rerender: true }));
|
|
26
|
+
|
|
21
27
|
async function boot() {
|
|
22
28
|
await loadTree();
|
|
23
29
|
const hash = decodeURIComponent(location.hash.slice(1));
|
package/public/mermaid.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* mermaid rendering (per-diagram, with error cards) + zoom overlay */
|
|
2
2
|
|
|
3
3
|
import { $, esc } from './dom.js';
|
|
4
|
+
import { currentTheme } from './theme.js';
|
|
4
5
|
|
|
5
6
|
const mermaid = window.mermaid; // UMD global, loaded via classic <script> before this module
|
|
6
7
|
const contentEl = $('content');
|
|
@@ -9,27 +10,33 @@ const zoomInnerEl = $('zoom-inner');
|
|
|
9
10
|
|
|
10
11
|
let mermaidSeq = 0; // unique id source for mermaid.render
|
|
11
12
|
|
|
12
|
-
mermaid.initialize({
|
|
13
|
-
startOnLoad: false,
|
|
14
|
-
theme: 'dark',
|
|
15
|
-
securityLevel: 'loose',
|
|
16
|
-
suppressErrorRendering: true // we render our own error card; don't inject the default bomb
|
|
17
|
-
});
|
|
18
|
-
|
|
19
13
|
// Render each .mermaid block individually so one bad diagram can't kill the rest.
|
|
20
|
-
|
|
14
|
+
// A block keeps its source in data-src, so it can be re-rendered in place (theme
|
|
15
|
+
// flip) - otherwise blocks that are already rendered are left alone.
|
|
16
|
+
export async function runMermaid({ rerender = false } = {}) {
|
|
17
|
+
mermaid.initialize({
|
|
18
|
+
startOnLoad: false,
|
|
19
|
+
theme: currentTheme() === 'light' ? 'neutral' : 'dark', // read per render: themes can flip mid-session
|
|
20
|
+
securityLevel: 'loose',
|
|
21
|
+
suppressErrorRendering: true // we render our own error card; don't inject the default bomb
|
|
22
|
+
});
|
|
21
23
|
const nodes = [...contentEl.querySelectorAll('.mermaid')];
|
|
22
24
|
for (const node of nodes) {
|
|
23
|
-
|
|
25
|
+
if (!node.dataset.src) node.dataset.src = node.textContent.trim();
|
|
26
|
+
else if (!rerender) continue;
|
|
27
|
+
const src = node.dataset.src;
|
|
28
|
+
node.classList.remove('rendered', 'mermaid-error');
|
|
24
29
|
try {
|
|
25
30
|
const { svg, bindFunctions } = await mermaid.render(`pv-mmd-${++mermaidSeq}`, src);
|
|
26
31
|
node.innerHTML = svg;
|
|
27
32
|
bindFunctions?.(node);
|
|
28
33
|
node.classList.add('rendered');
|
|
29
34
|
node.title = 'click to zoom';
|
|
30
|
-
node.
|
|
35
|
+
node.onclick = () => openZoom(node.querySelector('svg')?.outerHTML || '');
|
|
31
36
|
} catch (err) {
|
|
32
37
|
node.classList.add('mermaid-error');
|
|
38
|
+
node.onclick = null;
|
|
39
|
+
node.title = '';
|
|
33
40
|
node.innerHTML =
|
|
34
41
|
`<div class="diagram-error"><strong>⚠ diagram failed to render</strong>` +
|
|
35
42
|
`<pre class="err-msg">${esc(String(err && err.message ? err.message : err))}</pre>` +
|
package/public/search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* projview cmd+K search palette - fuzzy filenames + full-text contents */
|
|
2
2
|
|
|
3
3
|
import { $, esc } from './dom.js';
|
|
4
|
-
import {
|
|
4
|
+
import { fileIcon as icon } from './icons.js';
|
|
5
5
|
import { openFile } from './viewer.js';
|
|
6
6
|
|
|
7
7
|
const paletteEl = $('palette');
|
|
@@ -9,8 +9,6 @@ const inputEl = $('palette-input');
|
|
|
9
9
|
const resultsEl = $('palette-results');
|
|
10
10
|
const openBtn = $('open-palette');
|
|
11
11
|
|
|
12
|
-
const icon = (name) => (name.endsWith('.mmd') ? SVG_DIAGRAM : SVG_FILE);
|
|
13
|
-
|
|
14
12
|
let selectable = []; // flat list of openable results
|
|
15
13
|
let selected = 0;
|
|
16
14
|
let debounce;
|
package/public/store.js
CHANGED
|
@@ -5,5 +5,7 @@ export const state = {
|
|
|
5
5
|
expanded: new Set(), // dir paths the user has opened
|
|
6
6
|
treeData: [], // last tree payload from the server
|
|
7
7
|
rootName: 'projview', // indexed root's display name
|
|
8
|
-
writable: false
|
|
8
|
+
writable: false, // can this client create/edit comments + files? (loopback only)
|
|
9
|
+
view: 'rendered', // md / mmd: 'rendered' or 'raw' - sticks while browsing
|
|
10
|
+
persistent: false // does the store keep comments + versions across runs?
|
|
9
11
|
};
|