version-history-widget 0.1.2 → 0.1.4

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
@@ -28,8 +28,13 @@ npx vh init # sets up .versions/, injects the widget, saves vers
28
28
  npx vh record "Title" -d "what changed and why"
29
29
  npx vh restore 4 # safety-snapshots current state, then restores version 4
30
30
  npx vh list [query] # list versions, optionally filtered
31
+ npx vh update # refresh .versions/*.js from the currently installed package
31
32
  ```
32
33
 
34
+ `vh init` only copies `widget.js` (and friends) into `.versions/` once. If
35
+ you upgrade the package later and want an already-initialized site to pick
36
+ up widget changes (new styling, bug fixes, etc.), run `vh update`.
37
+
33
38
  Serve it so the restore button works:
34
39
 
35
40
  ```bash
@@ -64,6 +69,20 @@ npx vh-singlefile list page.html [query]
64
69
  Restoring is handled entirely in the browser by the injected widget — no
65
70
  server needed.
66
71
 
72
+ ## Claude Code skill
73
+
74
+ If you use Claude Code, this installs a skill so you can drive it with
75
+ `/version-history-widget` (or by just describing what you want) instead
76
+ of typing the CLI commands yourself:
77
+
78
+ ```bash
79
+ npx version-history-widget skill
80
+ ```
81
+
82
+ This copies a `SKILL.md` into `~/.claude/skills/version-history-widget/`
83
+ on your machine. It's per-machine — anyone else who wants the slash
84
+ command needs to run this once on their own machine too.
85
+
67
86
  ## How it works
68
87
 
69
88
  - No runtime dependencies — only Node's built-in `fs`, `path`, `http`, and
@@ -72,6 +91,10 @@ server needed.
72
91
  are exact and history is human-readable.
73
92
  - The widget (`widget.js`) is vanilla JS + inline CSS, scoped under `vh-`
74
93
  class names so it never collides with your site's styles.
94
+ - The "Versions" pill is draggable — click and drag it anywhere on screen
95
+ (works with touch too), and its position is remembered per-browser via
96
+ `localStorage`. The panel it opens repositions itself next to wherever
97
+ the pill currently is.
75
98
 
76
99
  ## Customizing the widget UI
77
100
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "version-history-widget",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Zero-dependency version history for web projects: snapshots every change and adds a floating widget to browse, search, and restore any version.",
5
5
  "keywords": ["version history", "undo", "snapshot", "restore", "dev-tool", "widget"],
6
6
  "license": "MIT",
@@ -17,7 +17,8 @@
17
17
  "widget.js",
18
18
  "middleware.js",
19
19
  "serve.js",
20
- "singlefile.js"
20
+ "singlefile.js",
21
+ "skill/SKILL.md"
21
22
  ],
22
23
  "repository": {
23
24
  "type": "git",
package/skill/SKILL.md ADDED
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: version-history-widget
3
+ description: Zero-dependency version history for web projects (the `vh` / version-history-widget CLI). Use when the user wants to save a checkpoint or snapshot of their site, record a titled version of recent changes, list past versions, or restore/roll back to an earlier version. Covers both project mode (multi-file sites, snapshots in .versions/) and single-file mode (one HTML file, snapshots embedded in the file).
4
+ ---
5
+
6
+ # Version History Widget
7
+
8
+ CLI for saving titled, restorable snapshots of a web project, plus a
9
+ floating "Versions" widget injected into the live site so anyone can
10
+ browse, search, and restore with one click. No runtime dependencies.
11
+
12
+ Two modes — pick based on what the user is working on:
13
+
14
+ - **Project mode**: a normal multi-file site/app. History lives in `.versions/`.
15
+ - **Single-file mode**: one standalone `.html` file. History lives inside that file.
16
+
17
+ ## Project mode
18
+
19
+ Run these with Bash from the project root (`npx` needs no local install):
20
+
21
+ - First time only — sets up `.versions/`, injects the widget into the
22
+ site's entry HTML, saves version 1:
23
+ `npx version-history-widget init`
24
+ - After the user has made changes and wants a checkpoint:
25
+ `npx version-history-widget record "Title" -d "what changed and why"`
26
+ - List versions, optionally filtered by title/details text:
27
+ `npx version-history-widget list [query]`
28
+ - Restore an earlier version (this automatically snapshots the
29
+ current state first, so it's safe):
30
+ `npx version-history-widget restore <id>`
31
+ - To serve the site so the widget's in-browser restore button works:
32
+ `node .versions/serve.js` (or mount `.versions/middleware.js` in
33
+ their existing dev server — see `.versions/middleware.js`'s header
34
+ comment for Express/Vite examples).
35
+
36
+ If `.versions/` doesn't exist yet in the project, run `init` first.
37
+
38
+ ## Single-file mode
39
+
40
+ Use when the user names one `.html` file rather than a project:
41
+
42
+ - Init: `npx -p version-history-widget vh-singlefile init page.html`
43
+ - Record: `npx -p version-history-widget vh-singlefile record page.html "Title" -d "details"`
44
+ - List: `npx -p version-history-widget vh-singlefile list page.html [query]`
45
+
46
+ Restoring in single-file mode happens entirely in the browser via the
47
+ widget embedded in the file — no server or CLI restore command needed.
48
+
49
+ ## Notes
50
+
51
+ - If the user has it installed as a devDependency already
52
+ (`npm install --save-dev version-history-widget`), use `npx vh ...`
53
+ and `npx vh-singlefile ...` directly instead of the longer `-p` form.
54
+ - Always ask for or infer a short, human title when recording — it's
55
+ what shows up in the widget's list and search.
56
+ - Don't run `restore` without the user confirming which version id
57
+ they want — list versions first if it's ambiguous.
package/vh.js CHANGED
@@ -4,8 +4,12 @@
4
4
  node vh.js record "Title" [-d "details" | -f details.txt] — snapshot changed files as a new version
5
5
  node vh.js restore <id> — snapshot current state, then restore version <id>
6
6
  node vh.js list [query] — list versions (optionally filtered by title/details)
7
+ node vh.js update — refresh the vendored widget.js/vh.js/serve.js/middleware.js
8
+ in .versions/ from the installed package (run after upgrading)
9
+ node vh.js skill — install the Claude Code skill (~/.claude/skills) so
10
+ /version-history-widget works in Claude Code sessions
7
11
  Env: VH_ROOT (project root, default cwd) */
8
- const fs = require('fs'), path = require('path');
12
+ const fs = require('fs'), path = require('path'), os = require('os');
9
13
  const ROOT = path.resolve(process.env.VH_ROOT || process.cwd());
10
14
  const VDIR = path.join(ROOT, '.versions'), SNAP = path.join(VDIR, 'snapshots'), MAN = path.join(VDIR, 'manifest.json');
11
15
  const IGNORE = new Set(['node_modules', '.git', '.versions', 'dist', 'build', '.next', '.nuxt', 'coverage', '.cache']);
@@ -82,10 +86,15 @@ function injectWidget() {
82
86
  return null;
83
87
  }
84
88
 
89
+ function vendorFiles() {
90
+ fs.mkdirSync(VDIR, { recursive: true });
91
+ for (const f of ['widget.js', 'vh.js', 'serve.js', 'middleware.js']) { const src = path.join(__dirname, f); if (fs.existsSync(src) && src !== path.join(VDIR, f)) fs.copyFileSync(src, path.join(VDIR, f)); }
92
+ }
93
+
85
94
  const cmd = process.argv[2], args = process.argv.slice(3);
86
95
  if (cmd === 'init') {
87
96
  fs.mkdirSync(SNAP, { recursive: true });
88
- for (const f of ['widget.js', 'vh.js', 'serve.js', 'middleware.js']) { const src = path.join(__dirname, f); if (fs.existsSync(src) && src !== path.join(VDIR, f)) fs.copyFileSync(src, path.join(VDIR, f)); }
97
+ vendorFiles();
89
98
  if (!fs.existsSync(MAN)) writeMan([]);
90
99
  const gi = path.join(ROOT, '.gitignore'); const line = '.versions/snapshots/';
91
100
  if (!fs.existsSync(gi) || !fs.readFileSync(gi, 'utf8').includes(line)) fs.appendFileSync(gi, '\n' + line + '\n');
@@ -115,5 +124,14 @@ if (cmd === 'init') {
115
124
  } else if (cmd === 'list') {
116
125
  const q = (args[0] || '').toLowerCase();
117
126
  for (const v of readMan()) if (!q || v.title.toLowerCase().includes(q) || (v.details || '').toLowerCase().includes(q)) console.log(String(v.id).padStart(3) + ' ' + v.time.slice(0, 16).replace('T', ' ') + ' ' + v.title);
127
+ } else if (cmd === 'update') {
128
+ if (!fs.existsSync(VDIR)) { console.error('No .versions/ here — run `vh init` first.'); process.exit(1); }
129
+ vendorFiles();
130
+ console.log('Updated .versions/{widget.js,vh.js,serve.js,middleware.js} from version-history-widget@' + require('./package.json').version + '. Reload the site to pick up widget changes.');
131
+ } else if (cmd === 'skill') {
132
+ const dest = path.join(os.homedir(), '.claude', 'skills', 'version-history-widget');
133
+ fs.mkdirSync(dest, { recursive: true });
134
+ fs.copyFileSync(path.join(__dirname, 'skill', 'SKILL.md'), path.join(dest, 'SKILL.md'));
135
+ console.log('Installed Claude Code skill to ' + dest + '. Restart Claude Code (or start a new session) and try /version-history-widget.');
118
136
  } else { console.log(fs.readFileSync(__filename, 'utf8').split('*/')[0].split('\n').slice(1).join('\n')); }
119
137
  module.exports = { restore: id => { const { execFileSync } = require('child_process'); return execFileSync(process.execPath, [__filename, 'restore', String(id)], { cwd: ROOT, env: process.env }).toString(); } };
package/widget.js CHANGED
@@ -3,7 +3,8 @@
3
3
  if (window.__vhLoaded) return; window.__vhLoaded = true;
4
4
  var Z = 2147483647, MODE = document.getElementById('vh-store') ? 'single' : 'project';
5
5
  var css = '\
6
- .vh-pill{position:fixed;right:16px;top:16px;z-index:' + Z + ';font:13px/1 system-ui,sans-serif;background:#111;color:#fff;border:0;border-radius:999px;padding:10px 14px;cursor:pointer;box-shadow:0 6px 20px rgba(0,0,0,.35)}\
6
+ .vh-pill{position:fixed;right:16px;top:16px;z-index:' + Z + ';font:13px/1 system-ui,sans-serif;background:#111;color:#fff;border:0;border-radius:999px;padding:10px 14px;cursor:grab;box-shadow:0 6px 20px rgba(0,0,0,.35);touch-action:none;user-select:none}\
7
+ .vh-pill.vh-dragging{cursor:grabbing}\
7
8
  .vh-panel{position:fixed;right:16px;top:56px;width:440px;max-width:calc(100vw - 32px);max-height:70vh;z-index:' + Z + ';background:#fff;color:#111;border:1px solid #ddd;border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.35);font:13px/1.4 system-ui,sans-serif;display:flex;flex-direction:column;overflow:hidden}\
8
9
  .vh-head{display:flex;gap:8px;padding:10px;border-bottom:1px solid #eee;align-items:center}\
9
10
  .vh-search{flex:1;padding:8px 10px;border:1px solid #ccc;border-radius:8px;font:inherit;outline:none}.vh-search:focus{border-color:#111}\
@@ -69,16 +70,65 @@
69
70
  });
70
71
  }
71
72
 
73
+ var POS_KEY = 'vh-pill-pos';
74
+ function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
75
+ function loadPos() { try { var p = JSON.parse(localStorage.getItem(POS_KEY)); if (p && isFinite(p.left) && isFinite(p.top)) return p; } catch (e) { } return null; }
76
+ function savePos(p) { try { localStorage.setItem(POS_KEY, JSON.stringify(p)); } catch (e) { } }
77
+ function placePill(left, top) {
78
+ var r = pill.getBoundingClientRect();
79
+ left = clamp(left, 8, window.innerWidth - r.width - 8);
80
+ top = clamp(top, 8, window.innerHeight - r.height - 8);
81
+ pill.style.left = left + 'px'; pill.style.top = top + 'px'; pill.style.right = 'auto'; pill.style.bottom = 'auto';
82
+ return { left: left, top: top };
83
+ }
84
+ function positionPanel() {
85
+ var r = pill.getBoundingClientRect(), vw = window.innerWidth, vh = window.innerHeight;
86
+ var w = Math.min(440, vw - 16), h = Math.min(vh * 0.7, panel.scrollHeight || vh * 0.7);
87
+ var top = (vh - r.bottom >= h + 8 || vh - r.bottom >= r.top) ? r.bottom + 8 : r.top - h - 8;
88
+ top = clamp(top, 8, vh - h - 8);
89
+ var left = clamp(r.right - w, 8, vw - w - 8);
90
+ panel.style.top = top + 'px'; panel.style.left = left + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; panel.style.width = w + 'px';
91
+ }
92
+ function initDrag() {
93
+ var saved = loadPos(); if (saved) placePill(saved.left, saved.top);
94
+ var dragging = false, moved = false, startX, startY, baseLeft, baseTop;
95
+ function start(x, y) {
96
+ var r = pill.getBoundingClientRect();
97
+ dragging = true; moved = false; startX = x; startY = y; baseLeft = r.left; baseTop = r.top;
98
+ pill.classList.add('vh-dragging');
99
+ }
100
+ function moveTo(x, y) {
101
+ if (!dragging) return;
102
+ if (Math.abs(x - startX) > 3 || Math.abs(y - startY) > 3) moved = true;
103
+ if (!moved) return;
104
+ placePill(baseLeft + (x - startX), baseTop + (y - startY));
105
+ if (open) positionPanel();
106
+ }
107
+ function end() {
108
+ if (!dragging) return;
109
+ dragging = false; pill.classList.remove('vh-dragging');
110
+ if (moved) { var r = pill.getBoundingClientRect(); savePos({ left: r.left, top: r.top }); }
111
+ }
112
+ pill.addEventListener('mousedown', function (e) { start(e.clientX, e.clientY); e.preventDefault(); });
113
+ document.addEventListener('mousemove', function (e) { moveTo(e.clientX, e.clientY); });
114
+ document.addEventListener('mouseup', end);
115
+ pill.addEventListener('touchstart', function (e) { var t = e.touches[0]; start(t.clientX, t.clientY); }, { passive: true });
116
+ document.addEventListener('touchmove', function (e) { if (!dragging) return; var t = e.touches[0]; moveTo(t.clientX, t.clientY); }, { passive: true });
117
+ document.addEventListener('touchend', end);
118
+ pill.onclick = function () { if (moved) { moved = false; return; } toggle(); };
119
+ window.addEventListener('resize', function () { var r = pill.getBoundingClientRect(); placePill(r.left, r.top); if (open) positionPanel(); });
120
+ }
72
121
  function build() {
73
- pill = document.createElement('button'); pill.className = 'vh-pill'; pill.textContent = 'Versions'; pill.onclick = toggle; document.body.appendChild(pill);
122
+ pill = document.createElement('button'); pill.className = 'vh-pill'; pill.textContent = 'Versions'; document.body.appendChild(pill);
74
123
  panel = document.createElement('div'); panel.className = 'vh-panel'; panel.style.display = 'none';
75
124
  panel.innerHTML = '<div class="vh-head"><input class="vh-search" placeholder="Search versions\u2026"><button class="vh-clear" title="Clear">\u00D7</button></div><div class="vh-list"></div><div class="vh-note"></div>';
76
125
  var inp = panel.querySelector('.vh-search'); inp.oninput = function () { query = inp.value; render(); };
77
126
  panel.querySelector('.vh-clear').onclick = function () { inp.value = ''; query = ''; render(); inp.focus(); };
78
127
  document.addEventListener('keydown', function (e) { if (e.key !== 'Escape' || !open) return; if (query) { inp.value = ''; query = ''; render(); } else toggle(); });
79
128
  document.body.appendChild(panel);
129
+ initDrag();
80
130
  }
81
- function toggle() { open = !open; if (open) load(function () { panel.style.display = 'flex'; render(); panel.querySelector('.vh-search').focus(); }); else panel.style.display = 'none'; }
131
+ function toggle() { open = !open; if (open) load(function () { panel.style.display = 'flex'; positionPanel(); render(); panel.querySelector('.vh-search').focus(); }); else panel.style.display = 'none'; }
82
132
  function init() { build(); load(render); }
83
133
  if (document.body) init(); else document.addEventListener('DOMContentLoaded', init);
84
134
  })();