@mattli/dotmd 0.1.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.
Files changed (78) hide show
  1. package/README.md +77 -0
  2. package/dist/cli/commands/init.d.ts +4 -0
  3. package/dist/cli/commands/init.d.ts.map +1 -0
  4. package/dist/cli/commands/init.js +23 -0
  5. package/dist/cli/commands/init.js.map +1 -0
  6. package/dist/cli/commands/install-hook.d.ts +2 -0
  7. package/dist/cli/commands/install-hook.d.ts.map +1 -0
  8. package/dist/cli/commands/install-hook.js +31 -0
  9. package/dist/cli/commands/install-hook.js.map +1 -0
  10. package/dist/cli/commands/scan.d.ts +5 -0
  11. package/dist/cli/commands/scan.d.ts.map +1 -0
  12. package/dist/cli/commands/scan.js +75 -0
  13. package/dist/cli/commands/scan.js.map +1 -0
  14. package/dist/cli/commands/serve.d.ts +4 -0
  15. package/dist/cli/commands/serve.d.ts.map +1 -0
  16. package/dist/cli/commands/serve.js +8 -0
  17. package/dist/cli/commands/serve.js.map +1 -0
  18. package/dist/cli/commands/status.d.ts +2 -0
  19. package/dist/cli/commands/status.d.ts.map +1 -0
  20. package/dist/cli/commands/status.js +70 -0
  21. package/dist/cli/commands/status.js.map +1 -0
  22. package/dist/cli/index.d.ts +3 -0
  23. package/dist/cli/index.d.ts.map +1 -0
  24. package/dist/cli/index.js +38 -0
  25. package/dist/cli/index.js.map +1 -0
  26. package/dist/config/defaults.d.ts +13 -0
  27. package/dist/config/defaults.d.ts.map +1 -0
  28. package/dist/config/defaults.js +36 -0
  29. package/dist/config/defaults.js.map +1 -0
  30. package/dist/config/loader.d.ts +4 -0
  31. package/dist/config/loader.d.ts.map +1 -0
  32. package/dist/config/loader.js +28 -0
  33. package/dist/config/loader.js.map +1 -0
  34. package/dist/dashboard/layout.d.ts +6 -0
  35. package/dist/dashboard/layout.d.ts.map +1 -0
  36. package/dist/dashboard/layout.js +47 -0
  37. package/dist/dashboard/layout.js.map +1 -0
  38. package/dist/dashboard/server.d.ts +5 -0
  39. package/dist/dashboard/server.d.ts.map +1 -0
  40. package/dist/dashboard/server.js +305 -0
  41. package/dist/dashboard/server.js.map +1 -0
  42. package/dist/dashboard/settings-client.d.ts +2 -0
  43. package/dist/dashboard/settings-client.d.ts.map +1 -0
  44. package/dist/dashboard/settings-client.js +310 -0
  45. package/dist/dashboard/settings-client.js.map +1 -0
  46. package/dist/dashboard/settings.d.ts +3 -0
  47. package/dist/dashboard/settings.d.ts.map +1 -0
  48. package/dist/dashboard/settings.js +99 -0
  49. package/dist/dashboard/settings.js.map +1 -0
  50. package/dist/dashboard/views.d.ts +8 -0
  51. package/dist/dashboard/views.d.ts.map +1 -0
  52. package/dist/dashboard/views.js +694 -0
  53. package/dist/dashboard/views.js.map +1 -0
  54. package/dist/dashboard/wizard-client.d.ts +2 -0
  55. package/dist/dashboard/wizard-client.d.ts.map +1 -0
  56. package/dist/dashboard/wizard-client.js +266 -0
  57. package/dist/dashboard/wizard-client.js.map +1 -0
  58. package/dist/dashboard/wizard.d.ts +9 -0
  59. package/dist/dashboard/wizard.d.ts.map +1 -0
  60. package/dist/dashboard/wizard.js +236 -0
  61. package/dist/dashboard/wizard.js.map +1 -0
  62. package/dist/scanner/git.d.ts +8 -0
  63. package/dist/scanner/git.d.ts.map +1 -0
  64. package/dist/scanner/git.js +34 -0
  65. package/dist/scanner/git.js.map +1 -0
  66. package/dist/scanner/index.d.ts +10 -0
  67. package/dist/scanner/index.d.ts.map +1 -0
  68. package/dist/scanner/index.js +60 -0
  69. package/dist/scanner/index.js.map +1 -0
  70. package/dist/storage/db.d.ts +3 -0
  71. package/dist/storage/db.d.ts.map +1 -0
  72. package/dist/storage/db.js +52 -0
  73. package/dist/storage/db.js.map +1 -0
  74. package/dist/storage/snapshots.d.ts +43 -0
  75. package/dist/storage/snapshots.d.ts.map +1 -0
  76. package/dist/storage/snapshots.js +102 -0
  77. package/dist/storage/snapshots.js.map +1 -0
  78. package/package.json +60 -0
@@ -0,0 +1,694 @@
1
+ import os from "os";
2
+ function escapeHtml(str) {
3
+ return str
4
+ .replace(/&/g, "&")
5
+ .replace(/</g, "&lt;")
6
+ .replace(/>/g, "&gt;")
7
+ .replace(/"/g, "&quot;");
8
+ }
9
+ function displayPath(p) {
10
+ return p.replace(os.homedir(), "~");
11
+ }
12
+ function timeAgo(dateStr) {
13
+ const now = Date.now();
14
+ const then = new Date(dateStr + "Z").getTime();
15
+ const diff = now - then;
16
+ const minutes = Math.floor(diff / 60000);
17
+ if (minutes < 1)
18
+ return "just now";
19
+ if (minutes < 60)
20
+ return `${minutes}m ago`;
21
+ const hours = Math.floor(minutes / 60);
22
+ if (hours < 24)
23
+ return `${hours}h ago`;
24
+ const days = Math.floor(hours / 24);
25
+ return `${days}d ago`;
26
+ }
27
+ function renderDiffHtml(diff, maxLines) {
28
+ const allLines = diff.split("\n").filter((l) => !l.startsWith("Index:") && !l.startsWith("====") && !l.startsWith("---") && !l.startsWith("+++") && !l.startsWith("@@") && !l.startsWith("\\ No newline"));
29
+ const lines = maxLines ? allLines.slice(0, maxLines) : allLines;
30
+ let html = `<pre class="text-xs font-mono overflow-x-auto">`;
31
+ for (const line of lines) {
32
+ if (line.startsWith("+")) {
33
+ html += `<div class="diff-add">${escapeHtml(line)}</div>`;
34
+ }
35
+ else if (line.startsWith("-")) {
36
+ html += `<div class="diff-del">${escapeHtml(line)}</div>`;
37
+ }
38
+ else {
39
+ html += `<div>${escapeHtml(line)}</div>`;
40
+ }
41
+ }
42
+ if (maxLines && allLines.length > maxLines) {
43
+ html += `<div class="text-gray-400">... truncated</div>`;
44
+ }
45
+ html += `</pre>`;
46
+ return html;
47
+ }
48
+ const fixedCategoryLabels = {
49
+ global: "Claude Code Global",
50
+ memory: "Claude Code Memory",
51
+ custom: "Other",
52
+ };
53
+ function getCategoryLabel(cat) {
54
+ if (fixedCategoryLabels[cat])
55
+ return fixedCategoryLabels[cat];
56
+ if (cat.startsWith("project:"))
57
+ return cat.slice("project:".length);
58
+ return cat;
59
+ }
60
+ function getCategorySortKey(cat) {
61
+ if (cat === "global")
62
+ return 0;
63
+ if (cat === "memory")
64
+ return 1;
65
+ if (cat.startsWith("project:"))
66
+ return 2;
67
+ return 3;
68
+ }
69
+ function buildTree(files) {
70
+ const root = { name: "~", fullPath: "~", children: new Map() };
71
+ for (const file of files) {
72
+ const display = displayPath(file.path);
73
+ // Split "~/.claude/CLAUDE.md" into ["~", ".claude", "CLAUDE.md"]
74
+ const parts = display.split("/").filter(Boolean);
75
+ let current = root;
76
+ for (let i = 0; i < parts.length; i++) {
77
+ const part = parts[i];
78
+ const isFile = i === parts.length - 1;
79
+ const pathSoFar = parts.slice(0, i + 1).join("/");
80
+ if (!current.children.has(part)) {
81
+ current.children.set(part, {
82
+ name: part,
83
+ fullPath: pathSoFar,
84
+ children: new Map(),
85
+ });
86
+ }
87
+ const node = current.children.get(part);
88
+ if (isFile) {
89
+ node.file = file;
90
+ }
91
+ current = node;
92
+ }
93
+ }
94
+ return compressTree(root);
95
+ }
96
+ function compressTree(node) {
97
+ // Collapse single-child directory chains: a/ -> b/ -> file becomes a/b/ -> file
98
+ for (const [key, child] of node.children) {
99
+ const compressed = compressTree(child);
100
+ node.children.set(key, compressed);
101
+ }
102
+ // If this node is a directory (no file) with exactly one child, merge them.
103
+ // Handles both dir-to-dir chains and dir-with-single-file.
104
+ // "development" + "dotmd" becomes "development/dotmd"
105
+ // "briefing" + "CLAUDE.md" becomes "briefing/CLAUDE.md" (displayed as a file link)
106
+ if (!node.file && node.children.size === 1) {
107
+ const [, only] = [...node.children.entries()][0];
108
+ if (only.file && only.children.size === 0) {
109
+ // Single file child — merge into this node as a file
110
+ node.name = node.name + "/" + only.name;
111
+ node.fullPath = only.fullPath;
112
+ node.file = only.file;
113
+ node.children = new Map();
114
+ }
115
+ else if (!only.file && only.children.size > 0) {
116
+ // Single directory child — merge names
117
+ node.name = node.name + "/" + only.name;
118
+ node.fullPath = only.fullPath;
119
+ node.children = only.children;
120
+ }
121
+ }
122
+ return node;
123
+ }
124
+ function renderTree(node, lastUpdated, unseenChanges, depth = 0) {
125
+ let html = "";
126
+ // Sort: files first, then directories
127
+ const entries = [...node.children.values()].sort((a, b) => {
128
+ const aIsDir = a.children.size > 0 && !a.file;
129
+ const bIsDir = b.children.size > 0 && !b.file;
130
+ if (aIsDir && !bIsDir)
131
+ return 1;
132
+ if (!aIsDir && bIsDir)
133
+ return -1;
134
+ return a.name.localeCompare(b.name);
135
+ });
136
+ for (const child of entries) {
137
+ const isDir = child.children.size > 0;
138
+ const indent = depth * 20;
139
+ if (child.file) {
140
+ // File node
141
+ const updateTime = lastUpdated.get(child.file.id);
142
+ const isUnseen = unseenChanges.has(child.file.id);
143
+ const dot = isUnseen
144
+ ? `<span class="inline-block w-2 h-2 bg-yellow-400 rounded-full"></span>`
145
+ : "";
146
+ const timestamp = updateTime
147
+ ? `<span class="text-xs text-gray-400">${timeAgo(updateTime)}</span>`
148
+ : "";
149
+ const rightSide = (timestamp || dot)
150
+ ? `<div class="flex items-center gap-2">${timestamp}${dot}</div>`
151
+ : "";
152
+ html += `
153
+ <a href="/files/${child.file.id}" class="flex items-center justify-between px-4 py-2 rounded-lg hover:bg-blue-50 transition-colors" style="padding-left: ${indent + 16}px">
154
+ <span class="font-mono text-sm">${escapeHtml(child.name)}</span>
155
+ ${rightSide}
156
+ </a>`;
157
+ }
158
+ else if (isDir) {
159
+ // Directory node
160
+ const childCount = countFiles(child);
161
+ html += `
162
+ <div data-tree-dir data-path="${escapeHtml(child.fullPath)}">
163
+ <button type="button" data-tree-toggle class="w-full flex items-center gap-2 px-4 py-2 rounded-lg hover:bg-gray-100 transition-colors text-left" style="padding-left: ${indent + 16}px">
164
+ <span data-tree-arrow class="text-gray-400 text-xs transition-transform">&#9660;</span>
165
+ <span class="font-mono text-sm font-medium text-gray-700">${escapeHtml(child.name)}/</span>
166
+ <span class="text-xs text-gray-400">${childCount} file${childCount === 1 ? "" : "s"}</span>
167
+ </button>
168
+ <div data-tree-children>
169
+ ${renderTree(child, lastUpdated, unseenChanges, depth + 1)}
170
+ </div>
171
+ </div>`;
172
+ }
173
+ }
174
+ return html;
175
+ }
176
+ function countFiles(node) {
177
+ let count = 0;
178
+ if (node.file)
179
+ count++;
180
+ for (const child of node.children.values()) {
181
+ count += countFiles(child);
182
+ }
183
+ return count;
184
+ }
185
+ function renderCategoryView(files, lastUpdated, unseenChanges) {
186
+ const groups = {};
187
+ for (const file of files) {
188
+ if (!groups[file.category])
189
+ groups[file.category] = [];
190
+ groups[file.category].push(file);
191
+ }
192
+ const sortedCategories = Object.keys(groups).sort((a, b) => getCategorySortKey(a) - getCategorySortKey(b) || a.localeCompare(b));
193
+ let html = "";
194
+ for (const cat of sortedCategories) {
195
+ const catFiles = groups[cat];
196
+ if (!catFiles || catFiles.length === 0)
197
+ continue;
198
+ html += `<div class="mb-8">`;
199
+ html += `<h2 class="text-sm font-semibold text-gray-500 uppercase tracking-wide mb-3">${escapeHtml(getCategoryLabel(cat))}</h2>`;
200
+ html += `<div class="space-y-2">`;
201
+ for (const file of catFiles) {
202
+ const updateTime = lastUpdated.get(file.id);
203
+ const isUnseen = unseenChanges.has(file.id);
204
+ const dot = isUnseen
205
+ ? `<span class="inline-block w-2 h-2 bg-yellow-400 rounded-full"></span>`
206
+ : "";
207
+ const timestamp = updateTime
208
+ ? `<span class="text-xs text-gray-400">${timeAgo(updateTime)}</span>`
209
+ : "";
210
+ const rightSide = (timestamp || dot)
211
+ ? `<div class="flex items-center gap-2">${timestamp}${dot}</div>`
212
+ : "";
213
+ html += `
214
+ <a href="/files/${file.id}" class="block bg-white rounded-lg border border-gray-200 px-4 py-3 hover:border-blue-300 transition-colors">
215
+ <div class="flex items-center justify-between">
216
+ <span class="font-mono text-sm">${escapeHtml(displayPath(file.path))}</span>
217
+ ${rightSide}
218
+ </div>
219
+ </a>`;
220
+ }
221
+ html += `</div></div>`;
222
+ }
223
+ return html;
224
+ }
225
+ export function fileListPage(files, lastUpdated, unseenChanges) {
226
+ if (files.length === 0) {
227
+ return `
228
+ <div class="text-center py-16">
229
+ <h2 class="text-xl font-semibold mb-2">No tracked files</h2>
230
+ <p class="text-gray-500">Run <code class="bg-gray-100 px-2 py-1 rounded">dotmd init</code> to get started.</p>
231
+ </div>`;
232
+ }
233
+ const tree = buildTree(files);
234
+ const treeHtml = renderTree(tree, lastUpdated, unseenChanges);
235
+ const categoryHtml = renderCategoryView(files, lastUpdated, unseenChanges);
236
+ return `
237
+ <div class="flex items-center justify-between mb-6">
238
+ <h1 class="text-2xl font-bold">Tracked Files</h1>
239
+ <div class="flex bg-gray-100 rounded-lg p-0.5">
240
+ <button id="view-tree" type="button" class="px-3 py-1 rounded-md text-sm font-medium bg-white shadow-sm text-gray-900">Tree</button>
241
+ <button id="view-category" type="button" class="px-3 py-1 rounded-md text-sm font-medium text-gray-500">Group</button>
242
+ </div>
243
+ </div>
244
+ <div id="tree-view" class="bg-white border border-gray-200 rounded-lg py-2">
245
+ ${treeHtml}
246
+ </div>
247
+ <div id="category-view" class="hidden">
248
+ ${categoryHtml}
249
+ </div>
250
+ <script>
251
+ (function() {
252
+ var treeBtn = document.getElementById('view-tree');
253
+ var catBtn = document.getElementById('view-category');
254
+ var treeView = document.getElementById('tree-view');
255
+ var catView = document.getElementById('category-view');
256
+
257
+ // --- Collapsed state ---
258
+ var COLLAPSED_KEY = 'dotmd-collapsed';
259
+ var VIEW_KEY = 'dotmd-view';
260
+
261
+ function getCollapsed() {
262
+ try { return JSON.parse(localStorage.getItem(COLLAPSED_KEY) || '[]'); }
263
+ catch(e) { return []; }
264
+ }
265
+ function saveCollapsed(arr) {
266
+ localStorage.setItem(COLLAPSED_KEY, JSON.stringify(arr));
267
+ }
268
+
269
+ function showTree() {
270
+ treeView.classList.remove('hidden');
271
+ catView.classList.add('hidden');
272
+ treeBtn.classList.add('bg-white', 'shadow-sm', 'text-gray-900');
273
+ treeBtn.classList.remove('text-gray-500');
274
+ catBtn.classList.remove('bg-white', 'shadow-sm', 'text-gray-900');
275
+ catBtn.classList.add('text-gray-500');
276
+ localStorage.setItem(VIEW_KEY, 'tree');
277
+ }
278
+ function showCategory() {
279
+ catView.classList.remove('hidden');
280
+ treeView.classList.add('hidden');
281
+ catBtn.classList.add('bg-white', 'shadow-sm', 'text-gray-900');
282
+ catBtn.classList.remove('text-gray-500');
283
+ treeBtn.classList.remove('bg-white', 'shadow-sm', 'text-gray-900');
284
+ treeBtn.classList.add('text-gray-500');
285
+ localStorage.setItem(VIEW_KEY, 'category');
286
+ }
287
+
288
+ treeBtn.addEventListener('click', showTree);
289
+ catBtn.addEventListener('click', showCategory);
290
+
291
+ // Restore view preference
292
+ if (localStorage.getItem(VIEW_KEY) === 'category') {
293
+ showCategory();
294
+ }
295
+
296
+ // Collapsible tree directories
297
+ var collapsed = getCollapsed();
298
+
299
+ document.querySelectorAll('[data-tree-toggle]').forEach(function(btn) {
300
+ var dir = btn.closest('[data-tree-dir]');
301
+ var dirPath = dir.getAttribute('data-path');
302
+ var children = dir.querySelector('[data-tree-children]');
303
+ var arrow = btn.querySelector('[data-tree-arrow]');
304
+
305
+ // Restore collapsed state
306
+ if (collapsed.indexOf(dirPath) !== -1) {
307
+ children.classList.add('hidden');
308
+ arrow.style.transform = 'rotate(-90deg)';
309
+ }
310
+
311
+ btn.addEventListener('click', function() {
312
+ var isHidden = children.classList.contains('hidden');
313
+ var current = getCollapsed();
314
+ if (isHidden) {
315
+ children.classList.remove('hidden');
316
+ arrow.style.transform = '';
317
+ current = current.filter(function(p) { return p !== dirPath; });
318
+ } else {
319
+ children.classList.add('hidden');
320
+ arrow.style.transform = 'rotate(-90deg)';
321
+ current.push(dirPath);
322
+ }
323
+ saveCollapsed(current);
324
+ });
325
+ });
326
+ })();
327
+ </script>`;
328
+ }
329
+ export function fileDetailPage(file, snapshots, hasMore = false) {
330
+ const latest = snapshots[0];
331
+ let html = `
332
+ <div class="mb-6">
333
+ <a href="javascript:history.back()" class="text-blue-600 hover:underline text-sm">&larr; Back</a>
334
+ </div>
335
+ <h1 class="text-xl font-bold mb-1 font-mono">${escapeHtml(displayPath(file.path))}</h1>
336
+ <p class="text-sm text-gray-500 mb-6">${file.category} &middot; tracked since ${file.first_seen_at}</p>`;
337
+ // Current content
338
+ if (latest) {
339
+ html += `
340
+ <div class="mb-8">
341
+ <div class="flex items-center justify-between mb-3">
342
+ <h2 class="text-lg font-semibold">Current Content</h2>
343
+ <button
344
+ onclick="fetch('/api/open/${file.id}', { method: 'POST' }).then(r => r.json()).then(d => { if (d.error) alert(d.error); })"
345
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
346
+ >
347
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
348
+ Open in Editor
349
+ </button>
350
+ </div>
351
+ <div class="bg-white border border-gray-200 rounded-lg p-4 overflow-x-auto">
352
+ <pre class="text-sm font-mono whitespace-pre-wrap">${escapeHtml(latest.content)}</pre>
353
+ </div>
354
+ </div>`;
355
+ }
356
+ // Change history
357
+ if (snapshots.length > 0) {
358
+ html += `<h2 class="text-lg font-semibold mb-3">Change History</h2>`;
359
+ html += `<div class="space-y-4">`;
360
+ for (const snap of snapshots) {
361
+ if (snap.diff) {
362
+ const lines = snap.diff.split("\n");
363
+ const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
364
+ const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
365
+ const stats = [];
366
+ if (added)
367
+ stats.push(`<span class="text-green-600">+${added}</span>`);
368
+ if (removed)
369
+ stats.push(`<span class="text-red-500">-${removed}</span>`);
370
+ html += `
371
+ <div data-diff-card class="bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:bg-gray-50 transition-colors">
372
+ <div class="flex items-center justify-between">
373
+ <div class="flex items-center gap-3">
374
+ <span class="text-sm">${stats.join(" / ")}</span>
375
+ <span data-diff-toggle class="text-xs text-gray-400">Show diff</span>
376
+ </div>
377
+ <span class="text-sm text-gray-500">${timeAgo(snap.created_at)}</span>
378
+ </div>
379
+ <div data-diff-content class="hidden mt-3">
380
+ ${renderDiffHtml(snap.diff)}
381
+ </div>
382
+ </div>`;
383
+ }
384
+ else {
385
+ html += `
386
+ <div class="bg-white border border-gray-200 rounded-lg p-4">
387
+ <div class="flex items-center justify-between">
388
+ <span class="text-sm text-gray-400 italic">Initial snapshot</span>
389
+ <span class="text-sm text-gray-500">${timeAgo(snap.created_at)}</span>
390
+ </div>
391
+ </div>`;
392
+ }
393
+ }
394
+ html += `</div>`;
395
+ if (hasMore) {
396
+ html += `
397
+ <div id="load-more-wrap" class="text-center mt-6">
398
+ <button id="load-more" class="px-4 py-2 text-sm text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">Load More</button>
399
+ </div>`;
400
+ }
401
+ html += `<script>
402
+ function initDiffCards(root) {
403
+ root.querySelectorAll('[data-diff-card]').forEach(function(card) {
404
+ if (card._diffInit) return;
405
+ card._diffInit = true;
406
+ card.addEventListener('click', function(e) {
407
+ if (e.target.closest('a')) return;
408
+ var content = card.querySelector('[data-diff-content]');
409
+ var label = card.querySelector('[data-diff-toggle]');
410
+ var isHidden = content.classList.contains('hidden');
411
+ content.classList.toggle('hidden');
412
+ label.textContent = isHidden ? 'Hide diff' : 'Show diff';
413
+ });
414
+ });
415
+ }
416
+ initDiffCards(document);
417
+
418
+ (function() {
419
+ var PAGE_SIZE = 50;
420
+ var FILE_ID = ${file.id};
421
+ var offset = PAGE_SIZE;
422
+ var btn = document.getElementById('load-more');
423
+ var wrap = document.getElementById('load-more-wrap');
424
+ var container = document.querySelector('.space-y-4');
425
+ if (!btn) return;
426
+
427
+ btn.addEventListener('click', function() {
428
+ btn.textContent = 'Loading...';
429
+ btn.disabled = true;
430
+ fetch('/api/files/' + FILE_ID + '?limit=' + (PAGE_SIZE + 1) + '&offset=' + offset)
431
+ .then(function(r) { return r.json(); })
432
+ .then(function(data) {
433
+ var snapshots = data.snapshots;
434
+ var hasMore = snapshots.length > PAGE_SIZE;
435
+ if (hasMore) snapshots.pop();
436
+
437
+ snapshots.forEach(function(snap) {
438
+ var div = document.createElement('div');
439
+ if (snap.diff) {
440
+ var lines = snap.diff.split('\\n');
441
+ var added = lines.filter(function(l) { return l.charAt(0) === '+' && !l.startsWith('+++'); }).length;
442
+ var removed = lines.filter(function(l) { return l.charAt(0) === '-' && !l.startsWith('---'); }).length;
443
+ var stats = [];
444
+ if (added) stats.push('<span class="text-green-600">+' + added + '</span>');
445
+ if (removed) stats.push('<span class="text-red-500">-' + removed + '</span>');
446
+
447
+ div.setAttribute('data-diff-card', '');
448
+ div.className = 'bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:bg-gray-50 transition-colors';
449
+ div.innerHTML = '<div class="flex items-center justify-between">' +
450
+ '<div class="flex items-center gap-3">' +
451
+ '<span class="text-sm">' + stats.join(' / ') + '</span>' +
452
+ '<span data-diff-toggle class="text-xs text-gray-400">Show diff</span>' +
453
+ '</div>' +
454
+ '<span class="text-xs text-gray-400">' + timeAgo(snap.created_at) + '</span>' +
455
+ '</div>' +
456
+ '<div data-diff-content class="hidden mt-3"><pre class="text-xs font-mono overflow-x-auto">' + renderDiff(snap.diff) + '</pre></div>';
457
+ } else {
458
+ div.className = 'bg-white border border-gray-200 rounded-lg p-4';
459
+ div.innerHTML = '<div class="flex items-center justify-between">' +
460
+ '<span class="text-sm text-gray-400 italic">Initial snapshot</span>' +
461
+ '<span class="text-xs text-gray-400">' + timeAgo(snap.created_at) + '</span>' +
462
+ '</div>';
463
+ }
464
+ container.appendChild(div);
465
+ });
466
+
467
+ initDiffCards(container);
468
+ offset += PAGE_SIZE;
469
+
470
+ if (hasMore) {
471
+ btn.textContent = 'Load More';
472
+ btn.disabled = false;
473
+ } else {
474
+ wrap.remove();
475
+ }
476
+ });
477
+ });
478
+
479
+ function escapeHtml(s) {
480
+ var d = document.createElement('div');
481
+ d.textContent = s;
482
+ return d.innerHTML;
483
+ }
484
+
485
+ function timeAgo(dateStr) {
486
+ var now = Date.now();
487
+ var then = new Date(dateStr + 'Z').getTime();
488
+ var diff = now - then;
489
+ var minutes = Math.floor(diff / 60000);
490
+ if (minutes < 1) return 'just now';
491
+ if (minutes < 60) return minutes + 'm ago';
492
+ var hours = Math.floor(minutes / 60);
493
+ if (hours < 24) return hours + 'h ago';
494
+ var days = Math.floor(hours / 24);
495
+ return days + 'd ago';
496
+ }
497
+
498
+ function renderDiff(diff) {
499
+ return diff.split('\\n')
500
+ .filter(function(l) { return l.indexOf('Index:') !== 0 && l.indexOf('====') !== 0 && l.indexOf('---') !== 0 && l.indexOf('+++') !== 0 && l.indexOf('@@') !== 0 && l.indexOf('\\ No newline') !== 0; })
501
+ .map(function(l) {
502
+ var escaped = escapeHtml(l);
503
+ if (l.charAt(0) === '+') return '<div class="diff-add">' + escaped + '</div>';
504
+ if (l.charAt(0) === '-') return '<div class="diff-del">' + escaped + '</div>';
505
+ return '<div>' + escaped + '</div>';
506
+ }).join('');
507
+ }
508
+ })();
509
+ </script>`;
510
+ }
511
+ return html;
512
+ }
513
+ export function timelinePage(changes, unseenChanges = new Set(), hasMore = false) {
514
+ if (changes.length === 0) {
515
+ return `
516
+ <div class="text-center py-16">
517
+ <h2 class="text-xl font-semibold mb-2">No changes recorded</h2>
518
+ <p class="text-gray-500">Changes will appear here after your next <code class="bg-gray-100 px-2 py-1 rounded">dotmd scan</code>.</p>
519
+ </div>`;
520
+ }
521
+ let html = `<h1 class="text-2xl font-bold mb-6">Timeline</h1>`;
522
+ html += `<div class="space-y-4">`;
523
+ for (const change of changes) {
524
+ if (change.diff) {
525
+ const lines = change.diff.split("\n");
526
+ const added = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).length;
527
+ const removed = lines.filter((l) => l.startsWith("-") && !l.startsWith("---")).length;
528
+ const stats = [];
529
+ if (added)
530
+ stats.push(`<span class="text-green-600">+${added}</span>`);
531
+ if (removed)
532
+ stats.push(`<span class="text-red-500">-${removed}</span>`);
533
+ html += `
534
+ <div data-diff-card class="bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:bg-gray-50 transition-colors">
535
+ <div class="flex items-center justify-between">
536
+ <div class="flex items-center gap-3 flex-wrap">
537
+ <a href="/files/${change.file_id}" class="font-mono text-sm text-blue-600 hover:underline">${escapeHtml(displayPath(change.path))}</a>
538
+ <span class="text-sm">${stats.join(" / ")}</span>
539
+ <span data-diff-toggle class="text-xs text-gray-400">Show diff</span>
540
+ </div>
541
+ <div class="flex items-center gap-2 flex-shrink-0">
542
+ <span class="text-xs text-gray-400">${timeAgo(change.created_at)}</span>
543
+ ${unseenChanges.has(change.id) ? `<span class="inline-block w-2 h-2 bg-yellow-400 rounded-full"></span>` : ""}
544
+ </div>
545
+ </div>
546
+ <div data-diff-content class="hidden mt-3">
547
+ ${renderDiffHtml(change.diff)}
548
+ </div>
549
+ </div>`;
550
+ }
551
+ else {
552
+ html += `
553
+ <div class="bg-white border border-gray-200 rounded-lg p-4">
554
+ <div class="flex items-center justify-between">
555
+ <div class="flex items-center gap-3 flex-wrap">
556
+ <a href="/files/${change.file_id}" class="font-mono text-sm text-blue-600 hover:underline">${escapeHtml(displayPath(change.path))}</a>
557
+ <span class="text-sm text-gray-400 italic">Initial snapshot</span>
558
+ </div>
559
+ <div class="flex items-center gap-2 flex-shrink-0">
560
+ <span class="text-xs text-gray-400">${timeAgo(change.created_at)}</span>
561
+ ${unseenChanges.has(change.id) ? `<span class="inline-block w-2 h-2 bg-yellow-400 rounded-full"></span>` : ""}
562
+ </div>
563
+ </div>
564
+ </div>`;
565
+ }
566
+ }
567
+ html += `</div>`;
568
+ if (hasMore) {
569
+ html += `
570
+ <div id="load-more-wrap" class="text-center mt-6">
571
+ <button id="load-more" class="px-4 py-2 text-sm text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">Load More</button>
572
+ </div>`;
573
+ }
574
+ html += `<script>
575
+ function initDiffCards(root) {
576
+ root.querySelectorAll('[data-diff-card]').forEach(function(card) {
577
+ if (card._diffInit) return;
578
+ card._diffInit = true;
579
+ card.addEventListener('click', function(e) {
580
+ if (e.target.closest('a')) return;
581
+ var content = card.querySelector('[data-diff-content]');
582
+ var label = card.querySelector('[data-diff-toggle]');
583
+ var isHidden = content.classList.contains('hidden');
584
+ content.classList.toggle('hidden');
585
+ label.textContent = isHidden ? 'Hide diff' : 'Show diff';
586
+ });
587
+ });
588
+ }
589
+ initDiffCards(document);
590
+
591
+ (function() {
592
+ var PAGE_SIZE = 50;
593
+ var offset = PAGE_SIZE;
594
+ var btn = document.getElementById('load-more');
595
+ var wrap = document.getElementById('load-more-wrap');
596
+ var container = document.querySelector('.space-y-4');
597
+ if (!btn) return;
598
+
599
+ btn.addEventListener('click', function() {
600
+ btn.textContent = 'Loading...';
601
+ btn.disabled = true;
602
+ fetch('/api/timeline?limit=' + (PAGE_SIZE + 1) + '&offset=' + offset)
603
+ .then(function(r) { return r.json(); })
604
+ .then(function(data) {
605
+ var changes = data.changes;
606
+ var unseenIds = new Set(data.unseenIds || []);
607
+ var hasMore = changes.length > PAGE_SIZE;
608
+ if (hasMore) changes.pop();
609
+
610
+ changes.forEach(function(change) {
611
+ var div = document.createElement('div');
612
+ var dot = unseenIds.has(change.id) ? '<span class="inline-block w-2 h-2 bg-yellow-400 rounded-full"></span>' : '';
613
+ if (change.diff) {
614
+ var lines = change.diff.split('\\n');
615
+ var added = lines.filter(function(l) { return l.charAt(0) === '+' && !l.startsWith('+++'); }).length;
616
+ var removed = lines.filter(function(l) { return l.charAt(0) === '-' && !l.startsWith('---'); }).length;
617
+ var stats = [];
618
+ if (added) stats.push('<span class="text-green-600">+' + added + '</span>');
619
+ if (removed) stats.push('<span class="text-red-500">-' + removed + '</span>');
620
+
621
+ div.setAttribute('data-diff-card', '');
622
+ div.className = 'bg-white border border-gray-200 rounded-lg p-4 cursor-pointer hover:bg-gray-50 transition-colors';
623
+ div.innerHTML = '<div class="flex items-center justify-between">' +
624
+ '<div class="flex items-center gap-3 flex-wrap">' +
625
+ '<a href="/files/' + change.file_id + '" class="font-mono text-sm text-blue-600 hover:underline">' + escapeHtml(change.path) + '</a>' +
626
+ '<span class="text-sm">' + stats.join(' / ') + '</span>' +
627
+ '<span data-diff-toggle class="text-xs text-gray-400">Show diff</span>' +
628
+ '</div>' +
629
+ '<div class="flex items-center gap-2 flex-shrink-0">' +
630
+ '<span class="text-xs text-gray-400">' + timeAgo(change.created_at) + '</span>' +
631
+ dot +
632
+ '</div></div>' +
633
+ '<div data-diff-content class="hidden mt-3"><pre class="text-xs font-mono overflow-x-auto">' + renderDiff(change.diff) + '</pre></div>';
634
+ } else {
635
+ div.className = 'bg-white border border-gray-200 rounded-lg p-4';
636
+ div.innerHTML = '<div class="flex items-center justify-between">' +
637
+ '<div class="flex items-center gap-3 flex-wrap">' +
638
+ '<a href="/files/' + change.file_id + '" class="font-mono text-sm text-blue-600 hover:underline">' + escapeHtml(change.path) + '</a>' +
639
+ '<span class="text-sm text-gray-400 italic">Initial snapshot</span>' +
640
+ '</div>' +
641
+ '<div class="flex items-center gap-2 flex-shrink-0">' +
642
+ '<span class="text-xs text-gray-400">' + timeAgo(change.created_at) + '</span>' +
643
+ dot +
644
+ '</div></div>';
645
+ }
646
+ container.appendChild(div);
647
+ });
648
+
649
+ initDiffCards(container);
650
+ offset += PAGE_SIZE;
651
+
652
+ if (hasMore) {
653
+ btn.textContent = 'Load More';
654
+ btn.disabled = false;
655
+ } else {
656
+ wrap.remove();
657
+ }
658
+ });
659
+ });
660
+
661
+ function escapeHtml(s) {
662
+ var d = document.createElement('div');
663
+ d.textContent = s;
664
+ return d.innerHTML;
665
+ }
666
+
667
+ function timeAgo(dateStr) {
668
+ var now = Date.now();
669
+ var then = new Date(dateStr + 'Z').getTime();
670
+ var diff = now - then;
671
+ var minutes = Math.floor(diff / 60000);
672
+ if (minutes < 1) return 'just now';
673
+ if (minutes < 60) return minutes + 'm ago';
674
+ var hours = Math.floor(minutes / 60);
675
+ if (hours < 24) return hours + 'h ago';
676
+ var days = Math.floor(hours / 24);
677
+ return days + 'd ago';
678
+ }
679
+
680
+ function renderDiff(diff) {
681
+ return diff.split('\\n')
682
+ .filter(function(l) { return l.indexOf('Index:') !== 0 && l.indexOf('====') !== 0 && l.indexOf('---') !== 0 && l.indexOf('+++') !== 0 && l.indexOf('@@') !== 0 && l.indexOf('\\ No newline') !== 0; })
683
+ .map(function(l) {
684
+ var escaped = escapeHtml(l);
685
+ if (l.charAt(0) === '+') return '<div class="diff-add">' + escaped + '</div>';
686
+ if (l.charAt(0) === '-') return '<div class="diff-del">' + escaped + '</div>';
687
+ return '<div>' + escaped + '</div>';
688
+ }).join('');
689
+ }
690
+ })();
691
+ </script>`;
692
+ return html;
693
+ }
694
+ //# sourceMappingURL=views.js.map