Tests/temp.md
... ...
@@ -0,0 +1,305 @@
1
+<!doctype html>
2
+<html lang="en">
3
+<head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Browser Git + Markdown Hierarchy (vanilla)</title>
7
+ <link rel="manifest" href="manifest.json" />
8
+ <style>
9
+ :root{--bg:#0f1720;--panel:#0b1220;--muted:#9aa4b2;--accent:#4f46e5;--card:#0b1226}
10
+ html,body{height:100%;margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,"Helvetica Neue",Arial;background:linear-gradient(180deg,#071025 0%,#071827 100%);color:#e6eef6}
11
+ .app{display:grid;grid-template-columns:260px 1fr 420px;gap:12px;padding:12px;height:100vh;box-sizing:border-box}
12
+ .panel{background:var(--panel);border-radius:8px;padding:10px;box-shadow:0 6px 18px rgba(2,6,23,.6);overflow:auto}
13
+ header{display:flex;gap:12px;align-items:center;margin-bottom:8px}
14
+ header h1{font-size:14px;margin:0}
15
+ .tree{font-size:13px}
16
+ .node{padding:6px;border-radius:6px;cursor:pointer;color:var(--muted)}
17
+ .node:hover{background:#0f1a2b;color:#fff}
18
+ .node.active{background:linear-gradient(90deg,var(--accent),#2dd4bf);color:#021124}
19
+ .toolbar{display:flex;gap:6px;margin-bottom:8px}
20
+ button{background:#0b1226;border:1px solid rgba(255,255,255,.04);color:#dbeafe;padding:6px 8px;border-radius:6px;cursor:pointer}
21
+ textarea{width:100%;height:60vh;background:#071226;color:#e6eef6;border:1px solid rgba(255,255,255,.04);padding:8px;border-radius:6px;font-family:monospace;font-size:13px}
22
+ .preview{background:#071226;padding:10px;border-radius:6px;height:60vh;overflow:auto}
23
+ .meta{display:flex;gap:8px;align-items:center;margin-bottom:8px}
24
+ input[type=text]{padding:6px;border-radius:6px;border:1px solid rgba(255,255,255,.04);background:#071226;color:#e6eef6}
25
+ .small{font-size:12px;color:var(--muted)}
26
+ footer{margin-top:8px;font-size:12px;color:var(--muted)}
27
+ </style>
28
+</head>
29
+<body>
30
+ <div class="app">
31
+ <div class="panel" id="left">
32
+ <header>
33
+ <h1>Repo</h1>
34
+ <div class="small" id="status">init...</div>
35
+ </header>
36
+ <div class="toolbar">
37
+ <button id="newFile">New file</button>
38
+ <button id="newFolder">New folder</button>
39
+ <button id="refreshTree">Refresh</button>
40
+ </div>
41
+ <div class="tree" id="tree"></div>
42
+ <footer class="small">Files stored in browser IndexedDB via LightningFS</footer>
43
+ </div>
44
+
45
+ <div class="panel" id="center">
46
+ <div class="meta">
47
+ <input id="filepath" type="text" placeholder="objects/example.md" style="flex:1" />
48
+ <input id="commitMsg" type="text" placeholder="Commit message" style="width:260px" />
49
+ <button id="saveCommit">Save & Commit</button>
50
+ </div>
51
+ <textarea id="editor" placeholder="File content (Markdown or YAML/JSON)"></textarea>
52
+ <div style="display:flex;gap:8px;margin-top:8px">
53
+ <button id="initRepo">Init Repo</button>
54
+ <button id="logBtn">Log</button>
55
+ <button id="setRemote">Set Remote</button>
56
+ <button id="pushBtn">Push</button>
57
+ <button id="pullBtn">Pull</button>
58
+ </div>
59
+ <pre id="log" class="small" style="margin-top:8px;white-space:pre-wrap"></pre>
60
+ </div>
61
+
62
+ <div class="panel" id="right">
63
+ <header><h1>Markdown Preview</h1></header>
64
+ <div class="preview" id="preview">Nothing selected</div>
65
+ </div>
66
+ </div>
67
+
68
+ <script type="module">
69
+ import LightningFS from 'https://unpkg.com/@isomorphic-git/lightning-fs@4.0.1?module';
70
+ import * as git from 'https://unpkg.com/isomorphic-git@1.20.0?module';
71
+ import { marked } from 'https://cdn.jsdelivr.net/npm/marked@5.1.1/+esm';
72
+
73
+ // Basic globals
74
+ const repoDir = '/repo';
75
+ const fs = new LightningFS('browser-fs', { wipe: false });
76
+ git.plugins.set('fs', fs);
77
+
78
+ const pfs = fs.promises;
79
+ const treeEl = document.getElementById('tree');
80
+ const editor = document.getElementById('editor');
81
+ const preview = document.getElementById('preview');
82
+ const filepathInput = document.getElementById('filepath');
83
+ const commitMsgInput = document.getElementById('commitMsg');
84
+ const statusEl = document.getElementById('status');
85
+ const logEl = document.getElementById('log');
86
+
87
+ // Utility: ensure repo dir exists
88
+ async function ensureRepoDir() {
89
+ try { await pfs.stat(repoDir); } catch (e) { await pfs.mkdir(repoDir); }
90
+ }
91
+
92
+ // Initialize git repo if not present
93
+ async function initRepo() {
94
+ status('initializing repo...');
95
+ await ensureRepoDir();
96
+ try {
97
+ const isRepo = await git.isRepo({ fs, dir: repoDir });
98
+ if (!isRepo) {
99
+ await git.init({ fs, dir: repoDir, defaultBranch: 'main' });
100
+ status('repo initialized');
101
+ } else {
102
+ status('repo already initialized');
103
+ }
104
+ } catch (e) {
105
+ console.error(e);
106
+ status('init error: ' + e.message);
107
+ }
108
+ await refreshTree();
109
+ }
110
+
111
+ // Read directory recursively and build tree
112
+ async function readTree(dir = repoDir) {
113
+ const items = [];
114
+ try {
115
+ const names = await pfs.readdir(dir);
116
+ for (const name of names) {
117
+ const path = dir + '/' + name;
118
+ const st = await pfs.stat(path);
119
+ if (st && st.type === 'directory') {
120
+ items.push({ name, path, type: 'dir', children: await readTree(path) });
121
+ } else {
122
+ items.push({ name, path, type: 'file' });
123
+ }
124
+ }
125
+ } catch (e) {
126
+ // ignore
127
+ }
128
+ return items;
129
+ }
130
+
131
+ function renderTree(nodes, container) {
132
+ container.innerHTML = '';
133
+ for (const node of nodes) {
134
+ const div = document.createElement('div');
135
+ div.className = 'node';
136
+ div.textContent = node.name;
137
+ div.title = node.path;
138
+ div.onclick = async (ev) => {
139
+ ev.stopPropagation();
140
+ document.querySelectorAll('.node').forEach(n=>n.classList.remove('active'));
141
+ div.classList.add('active');
142
+ if (node.type === 'file') {
143
+ const rel = node.path.replace(repoDir + '/', '');
144
+ filepathInput.value = rel;
145
+ const data = await pfs.readFile(node.path, { encoding: 'utf8' }).catch(()=>'');
146
+ editor.value = data;
147
+ preview.innerHTML = marked.parse(data || '');
148
+ } else {
149
+ // toggle expand: show children inline
150
+ if (div.nextSibling && div.nextSibling.classList && div.nextSibling.classList.contains('children')) {
151
+ div.nextSibling.remove();
152
+ } else {
153
+ const childWrap = document.createElement('div');
154
+ childWrap.className = 'children';
155
+ childWrap.style.paddingLeft = '12px';
156
+ renderTree(node.children, childWrap);
157
+ div.after(childWrap);
158
+ }
159
+ }
160
+ };
161
+ container.appendChild(div);
162
+ }
163
+ }
164
+
165
+ async function refreshTree() {
166
+ const nodes = await readTree();
167
+ renderTree(nodes, treeEl);
168
+ status('tree refreshed');
169
+ }
170
+
171
+ // Save file to fs and stage/commit
172
+ async function saveAndCommit() {
173
+ const rel = filepathInput.value.trim();
174
+ if (!rel) { alert('Enter a filepath'); return; }
175
+ const full = repoDir + '/' + rel;
176
+ // ensure parent dirs
177
+ const parts = full.split('/');
178
+ for (let i = 2; i <= parts.length - 1; i++) {
179
+ const p = parts.slice(0, i).join('/');
180
+ try { await pfs.stat(p); } catch (e) { await pfs.mkdir(p); }
181
+ }
182
+ await pfs.writeFile(full, editor.value, { encoding: 'utf8' });
183
+ await git.add({ fs, dir: repoDir, filepath: rel });
184
+ const message = commitMsgInput.value.trim() || 'Edit ' + rel;
185
+ try {
186
+ const sha = await git.commit({
187
+ fs, dir: repoDir,
188
+ author: { name: 'Browser User', email: 'user@local' },
189
+ message
190
+ });
191
+ status('committed ' + sha.slice(0,7));
192
+ await refreshTree();
193
+ log('Committed: ' + sha);
194
+ } catch (e) {
195
+ console.error(e);
196
+ status('commit error: ' + e.message);
197
+ }
198
+ }
199
+
200
+ // Show log
201
+ async function showLog() {
202
+ try {
203
+ const commits = await git.log({ fs, dir: repoDir, depth: 20 });
204
+ logEl.textContent = commits.map(c => `${c.oid.slice(0,7)} ${c.commit.message} — ${c.commit.author.name}`).join('\n\n');
205
+ } catch (e) {
206
+ logEl.textContent = 'Log error: ' + e.message;
207
+ }
208
+ }
209
+
210
+ // Set remote (store in localStorage)
211
+ function setRemote() {
212
+ const url = prompt('Remote URL (HTTPS, e.g. https://github.com/you/repo.git)');
213
+ if (url) {
214
+ localStorage.setItem('remoteUrl', url);
215
+ status('remote set');
216
+ }
217
+ }
218
+
219
+ // Push (prompts for token)
220
+ async function push() {
221
+ const remote = localStorage.getItem('remoteUrl');
222
+ if (!remote) { alert('Set remote first'); return; }
223
+ const token = prompt('Personal access token (will not be stored). For GitHub use token as password and username "x-access-token" or "token".');
224
+ if (!token) return;
225
+ status('pushing...');
226
+ try {
227
+ await git.addRemote({ fs, dir: repoDir, remote: 'origin', url: remote }).catch(()=>{});
228
+ await git.push({
229
+ fs, http: git.http, dir: repoDir,
230
+ remote: 'origin', ref: 'main',
231
+ onAuth: () => ({ username: 'x-access-token', password: token })
232
+ });
233
+ status('push complete');
234
+ } catch (e) {
235
+ console.error(e);
236
+ status('push failed: ' + e.message);
237
+ }
238
+ }
239
+
240
+ // Pull (prompts for token)
241
+ async function pull() {
242
+ const remote = localStorage.getItem('remoteUrl');
243
+ if (!remote) { alert('Set remote first'); return; }
244
+ const token = prompt('Personal access token (will not be stored).');
245
+ if (!token) return;
246
+ status('pulling...');
247
+ try {
248
+ await git.addRemote({ fs, dir: repoDir, remote: 'origin', url: remote }).catch(()=>{});
249
+ await git.pull({
250
+ fs, http: git.http, dir: repoDir,
251
+ remote: 'origin', ref: 'main',
252
+ singleBranch: true,
253
+ author: { name: 'Browser User', email: 'user@local' },
254
+ onAuth: () => ({ username: 'x-access-token', password: token })
255
+ });
256
+ status('pull complete');
257
+ await refreshTree();
258
+ } catch (e) {
259
+ console.error(e);
260
+ status('pull failed: ' + e.message);
261
+ }
262
+ }
263
+
264
+ // Helpers
265
+ function status(text) { statusEl.textContent = text; }
266
+ function log(text) { logEl.textContent = (new Date()).toLocaleTimeString() + ' — ' + text + '\n' + logEl.textContent; }
267
+
268
+ // Wire UI
269
+ document.getElementById('initRepo').onclick = initRepo;
270
+ document.getElementById('refreshTree').onclick = refreshTree;
271
+ document.getElementById('saveCommit').onclick = saveAndCommit;
272
+ document.getElementById('logBtn').onclick = showLog;
273
+ document.getElementById('newFile').onclick = async () => {
274
+ const name = prompt('New file path (e.g. objects/note.md)');
275
+ if (!name) return;
276
+ filepathInput.value = name;
277
+ editor.value = '# ' + name.split('/').pop();
278
+ };
279
+ document.getElementById('newFolder').onclick = async () => {
280
+ const name = prompt('New folder path (e.g. objects/notes)');
281
+ if (!name) return;
282
+ const full = repoDir + '/' + name;
283
+ try { await pfs.mkdir(full, { recursive: true }); status('folder created'); await refreshTree(); } catch(e){status('mkdir error')}
284
+ };
285
+ document.getElementById('setRemote').onclick = setRemote;
286
+ document.getElementById('pushBtn').onclick = push;
287
+ document.getElementById('pullBtn').onclick = pull;
288
+
289
+ // Live preview
290
+ editor.addEventListener('input', () => {
291
+ preview.innerHTML = marked.parse(editor.value || '');
292
+ });
293
+
294
+ // Register service worker
295
+ if ('serviceWorker' in navigator) {
296
+ navigator.serviceWorker.register('sw.js').then(()=>console.log('sw registered')).catch(()=>console.warn('sw failed'));
297
+ }
298
+
299
+ // Auto init on load
300
+ await initRepo();
301
+ await refreshTree();
302
+ status('ready');
303
+ </script>
304
+</body>
305
+</html>