version-history-widget 0.1.3 → 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 +9 -0
- package/package.json +1 -1
- package/vh.js +12 -1
- package/widget.js +53 -3
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
|
|
@@ -86,6 +91,10 @@ command needs to run this once on their own machine too.
|
|
|
86
91
|
are exact and history is human-readable.
|
|
87
92
|
- The widget (`widget.js`) is vanilla JS + inline CSS, scoped under `vh-`
|
|
88
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.
|
|
89
98
|
|
|
90
99
|
## Customizing the widget UI
|
|
91
100
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "version-history-widget",
|
|
3
|
-
"version": "0.1.
|
|
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",
|
package/vh.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
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)
|
|
7
9
|
node vh.js skill — install the Claude Code skill (~/.claude/skills) so
|
|
8
10
|
/version-history-widget works in Claude Code sessions
|
|
9
11
|
Env: VH_ROOT (project root, default cwd) */
|
|
@@ -84,10 +86,15 @@ function injectWidget() {
|
|
|
84
86
|
return null;
|
|
85
87
|
}
|
|
86
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
|
+
|
|
87
94
|
const cmd = process.argv[2], args = process.argv.slice(3);
|
|
88
95
|
if (cmd === 'init') {
|
|
89
96
|
fs.mkdirSync(SNAP, { recursive: true });
|
|
90
|
-
|
|
97
|
+
vendorFiles();
|
|
91
98
|
if (!fs.existsSync(MAN)) writeMan([]);
|
|
92
99
|
const gi = path.join(ROOT, '.gitignore'); const line = '.versions/snapshots/';
|
|
93
100
|
if (!fs.existsSync(gi) || !fs.readFileSync(gi, 'utf8').includes(line)) fs.appendFileSync(gi, '\n' + line + '\n');
|
|
@@ -117,6 +124,10 @@ if (cmd === 'init') {
|
|
|
117
124
|
} else if (cmd === 'list') {
|
|
118
125
|
const q = (args[0] || '').toLowerCase();
|
|
119
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.');
|
|
120
131
|
} else if (cmd === 'skill') {
|
|
121
132
|
const dest = path.join(os.homedir(), '.claude', 'skills', 'version-history-widget');
|
|
122
133
|
fs.mkdirSync(dest, { recursive: true });
|
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:
|
|
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';
|
|
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
|
})();
|