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/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 theme: tell the UA + Dark Reader to leave colors alone -->
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="#b4befe" stroke-width="1.8"/>
20
- <line x1="8.5" y1="4.5" x2="8.5" y2="19.5" stroke="#b4befe" stroke-width="1.8"/>
21
- <circle cx="13.4" cy="9.4" r="1.25" fill="#cba6f7"/>
22
- <circle cx="18" cy="9.4" r="1.25" fill="#cba6f7"/>
23
- <circle cx="15.7" cy="14.6" r="1.25" fill="#cba6f7"/>
24
- <path d="M13.4 9.4 15.7 14.6 18 9.4" stroke="#cba6f7" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
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">🔎</span><span class="op-text">search…</span><kbd>⌘K</kbd>
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 (/\.(md|mmd)$/i.test(target)) openFile(target, frag);
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 === state.currentPath) { pulse(); openFile(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
- export async function runMermaid() {
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
- const src = node.textContent.trim();
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.addEventListener('click', () => openZoom(node.querySelector('svg')?.outerHTML || ''));
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 { SVG_FILE, SVG_DIAGRAM } from './icons.js';
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 // can this client create/edit comments? (loopback only)
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
  };