@hewliyang/headless-spreadjs 0.0.7 → 0.0.9

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 (66) hide show
  1. package/README.md +118 -0
  2. package/dist/cli/client.d.ts +2 -2
  3. package/dist/cli/client.d.ts.map +1 -1
  4. package/dist/cli/client.js +7 -7
  5. package/dist/cli/client.js.map +1 -1
  6. package/dist/cli/commands/clear.d.ts.map +1 -1
  7. package/dist/cli/commands/clear.js +8 -1
  8. package/dist/cli/commands/clear.js.map +1 -1
  9. package/dist/cli/commands/copy.d.ts.map +1 -1
  10. package/dist/cli/commands/copy.js +10 -1
  11. package/dist/cli/commands/copy.js.map +1 -1
  12. package/dist/cli/commands/diff.d.ts.map +1 -1
  13. package/dist/cli/commands/diff.js +61 -0
  14. package/dist/cli/commands/diff.js.map +1 -1
  15. package/dist/cli/commands/eval.d.ts.map +1 -1
  16. package/dist/cli/commands/eval.js +148 -6
  17. package/dist/cli/commands/eval.js.map +1 -1
  18. package/dist/cli/commands/search.d.ts.map +1 -1
  19. package/dist/cli/commands/search.js +5 -2
  20. package/dist/cli/commands/search.js.map +1 -1
  21. package/dist/cli/commands/set.d.ts +1 -0
  22. package/dist/cli/commands/set.d.ts.map +1 -1
  23. package/dist/cli/commands/set.js +123 -13
  24. package/dist/cli/commands/set.js.map +1 -1
  25. package/dist/cli/commands/watch.d.ts +24 -0
  26. package/dist/cli/commands/watch.d.ts.map +1 -0
  27. package/dist/cli/commands/watch.js +223 -0
  28. package/dist/cli/commands/watch.js.map +1 -0
  29. package/dist/cli/context.d.ts +2 -0
  30. package/dist/cli/context.d.ts.map +1 -1
  31. package/dist/cli/context.js +72 -23
  32. package/dist/cli/context.js.map +1 -1
  33. package/dist/cli/daemon.d.ts.map +1 -1
  34. package/dist/cli/daemon.js +232 -67
  35. package/dist/cli/daemon.js.map +1 -1
  36. package/dist/cli/dispatch.d.ts +1 -1
  37. package/dist/cli/dispatch.d.ts.map +1 -1
  38. package/dist/cli/dispatch.js +32 -7
  39. package/dist/cli/dispatch.js.map +1 -1
  40. package/dist/cli/file-cache.d.ts +11 -0
  41. package/dist/cli/file-cache.d.ts.map +1 -1
  42. package/dist/cli/file-cache.js +22 -1
  43. package/dist/cli/file-cache.js.map +1 -1
  44. package/dist/cli/main.d.ts.map +1 -1
  45. package/dist/cli/main.js +78 -8
  46. package/dist/cli/main.js.map +1 -1
  47. package/dist/cli/sheet-size.d.ts +14 -0
  48. package/dist/cli/sheet-size.d.ts.map +1 -0
  49. package/dist/cli/sheet-size.js +20 -0
  50. package/dist/cli/sheet-size.js.map +1 -0
  51. package/dist/cli/styles.d.ts +11 -0
  52. package/dist/cli/styles.d.ts.map +1 -1
  53. package/dist/cli/styles.js +75 -0
  54. package/dist/cli/styles.js.map +1 -1
  55. package/dist/cli/watch-server.d.ts +34 -0
  56. package/dist/cli/watch-server.d.ts.map +1 -0
  57. package/dist/cli/watch-server.js +792 -0
  58. package/dist/cli/watch-server.js.map +1 -0
  59. package/dist/excel-file.d.ts.map +1 -1
  60. package/dist/excel-file.js +0 -21
  61. package/dist/excel-file.js.map +1 -1
  62. package/dist/hooks.d.ts +159 -0
  63. package/dist/hooks.d.ts.map +1 -0
  64. package/dist/hooks.js +300 -0
  65. package/dist/hooks.js.map +1 -0
  66. package/package.json +9 -4
@@ -0,0 +1,792 @@
1
+ import { createServer, } from "node:http";
2
+ import { basename } from "node:path";
3
+ import { getRegistry } from "../hooks.js";
4
+ // ---------------------------------------------------------------------------
5
+ // SSE helpers
6
+ // ---------------------------------------------------------------------------
7
+ function sseConnect(res, clients) {
8
+ res.writeHead(200, {
9
+ "Content-Type": "text/event-stream",
10
+ "Cache-Control": "no-cache",
11
+ Connection: "keep-alive",
12
+ "Access-Control-Allow-Origin": "*",
13
+ });
14
+ res.write(": connected\n\n");
15
+ clients.add(res);
16
+ res.on("close", () => clients.delete(res));
17
+ }
18
+ function sseBroadcast(clients, data) {
19
+ const frame = `data: ${data}\n\n`;
20
+ for (const c of clients) {
21
+ try {
22
+ c.write(frame);
23
+ }
24
+ catch { }
25
+ }
26
+ }
27
+ // ---------------------------------------------------------------------------
28
+ // Hooks summary helper
29
+ // ---------------------------------------------------------------------------
30
+ function getHookName(fn) {
31
+ if (typeof fn === "function" && fn.name)
32
+ return fn.name;
33
+ return "(anonymous)";
34
+ }
35
+ async function getHooksSummary() {
36
+ const registry = await getRegistry();
37
+ const hooks = [];
38
+ for (const event of [
39
+ "preCommand",
40
+ "postCommand",
41
+ "onOpen",
42
+ "preSave",
43
+ "postSave",
44
+ ]) {
45
+ for (const entry of registry[event]) {
46
+ hooks.push({ event, name: getHookName(entry.fn) });
47
+ }
48
+ }
49
+ return { hooks, total: hooks.length };
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Viewer HTML
53
+ // ---------------------------------------------------------------------------
54
+ function buildViewerHTML(filenames) {
55
+ const fileListJSON = JSON.stringify(filenames);
56
+ return `<!DOCTYPE html>
57
+ <html>
58
+ <head>
59
+ <meta charset="utf-8">
60
+ <title>hsx watch</title>
61
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@grapecity/spread-sheets/styles/gc.spread.sheets.excel2016colorful.min.css">
62
+ <style>
63
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
64
+
65
+ :root {
66
+ --bg-base: #ffffff;
67
+ --bg-surface: #f8f8f8;
68
+ --bg-raised: #f0f0f0;
69
+ --bg-overlay: #e8e8e8;
70
+ --border-subtle: rgba(0,0,0,0.08);
71
+ --border-medium: rgba(0,0,0,0.14);
72
+ --text-primary: #1a1a1a;
73
+ --text-secondary: #6b6b6b;
74
+ --text-tertiary: #999999;
75
+ --accent: #1a8a5c;
76
+ --accent-dim: rgba(26,138,92,0.08);
77
+ --accent-glow: rgba(26,138,92,0.2);
78
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
79
+ --font-mono: 'SF Mono', 'JetBrains Mono', Consolas, monospace;
80
+ --radius-sm: 4px;
81
+ --radius-md: 8px;
82
+ --radius-lg: 12px;
83
+ --shadow-popup: 0 8px 32px rgba(0,0,0,0.12), 0 0 0 1px var(--border-subtle);
84
+ --transition-fast: 0.12s cubic-bezier(0.4,0,0.2,1);
85
+ --transition-normal: 0.2s cubic-bezier(0.4,0,0.2,1);
86
+ }
87
+
88
+ * { box-sizing: border-box; margin: 0; padding: 0; }
89
+ html, body { height: 100%; }
90
+ body { font-family: var(--font-sans); background: var(--bg-base); -webkit-font-smoothing: antialiased; color: var(--text-primary); }
91
+
92
+ /* ---- Toolbar ---- */
93
+ #toolbar {
94
+ display: flex; align-items: stretch;
95
+ background: var(--bg-surface);
96
+ color: var(--text-secondary); font-size: 13px;
97
+ height: 34px;
98
+ border-bottom: 1px solid var(--border-subtle);
99
+ overflow: hidden;
100
+ position: relative;
101
+ z-index: 10;
102
+ }
103
+
104
+ #file-tabs {
105
+ display: flex; align-items: stretch;
106
+ min-width: 0; gap: 1px;
107
+ padding-left: 2px;
108
+ overflow-x: auto; overflow-y: hidden;
109
+ scrollbar-width: none;
110
+ }
111
+ #file-tabs::-webkit-scrollbar { display: none; }
112
+
113
+ .file-tab {
114
+ display: flex; align-items: center; gap: 8px;
115
+ padding: 0 18px; cursor: pointer; color: var(--text-tertiary);
116
+ background: transparent;
117
+ white-space: nowrap; user-select: none;
118
+ font-family: var(--font-mono);
119
+ font-size: 12px; font-weight: 400;
120
+ letter-spacing: 0.01em;
121
+ transition: color var(--transition-fast), background var(--transition-fast);
122
+ position: relative;
123
+ outline: none;
124
+ border: none;
125
+ border-bottom: 2px solid transparent;
126
+ }
127
+ .file-tab::after {
128
+ content: ''; position: absolute; bottom: -1px; left: 18px; right: 18px;
129
+ height: 2px; background: var(--accent); border-radius: 2px 2px 0 0;
130
+ transform: scaleX(0); transition: transform var(--transition-normal);
131
+ }
132
+ .file-tab:hover { color: var(--text-primary); background: var(--bg-raised); }
133
+ .file-tab:focus-visible { color: var(--text-primary); background: var(--bg-raised); }
134
+ .file-tab:focus-visible::after { transform: scaleX(1); }
135
+ .file-tab.active {
136
+ color: var(--text-primary); background: var(--bg-base);
137
+ }
138
+ .file-tab.active::after { transform: scaleX(1); }
139
+ .file-tab .dot {
140
+ width: 7px; height: 7px; border-radius: 50%;
141
+ background: var(--accent); flex-shrink: 0;
142
+ box-shadow: 0 0 6px var(--accent-glow);
143
+ transition: opacity var(--transition-fast);
144
+ }
145
+ .file-tab:not(.active) .dot { opacity: 0; }
146
+
147
+ .file-tab .kbd {
148
+ font-family: var(--font-mono);
149
+ font-size: 9px; letter-spacing: 0.04em;
150
+ color: var(--text-tertiary); background: var(--bg-overlay);
151
+ padding: 2px 6px; border-radius: var(--radius-sm);
152
+ border: 1px solid var(--border-subtle);
153
+ transition: color var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast);
154
+ }
155
+ .file-tab:hover .kbd { color: var(--text-secondary); background: rgba(255,255,255,0.06); }
156
+ .file-tab.active .kbd { color: var(--accent); background: var(--accent-dim); border-color: rgba(86,212,160,0.15); }
157
+
158
+ .spacer { flex: 1; }
159
+
160
+ /* ---- Hooks badge ---- */
161
+ #hooks-badge {
162
+ display: flex; align-items: center; gap: 6px;
163
+ padding: 0 14px;
164
+ font-family: var(--font-mono);
165
+ font-size: 11px; color: var(--text-tertiary); cursor: pointer;
166
+ border-left: 1px solid var(--border-subtle);
167
+ flex-shrink: 0;
168
+ transition: color var(--transition-fast), background var(--transition-fast);
169
+ position: relative; z-index: 200;
170
+ }
171
+ #hooks-badge:hover { color: var(--text-secondary); background: var(--bg-raised); }
172
+ #hooks-badge .hooks-icon { display: flex; align-items: center; opacity: 0.5; }
173
+ #hooks-badge:hover .hooks-icon { opacity: 0.8; }
174
+ #hooks-badge .hooks-count {
175
+ font-family: var(--font-mono);
176
+ font-size: 10px; font-weight: 500;
177
+ background: var(--bg-overlay); color: var(--text-secondary);
178
+ padding: 2px 7px; border-radius: 10px;
179
+ border: 1px solid var(--border-subtle);
180
+ min-width: 20px; text-align: center;
181
+ }
182
+
183
+ /* ---- Hooks tooltip ---- */
184
+ #hooks-tooltip {
185
+ display: none;
186
+ position: fixed;
187
+ top: 42px; right: 12px;
188
+ background: var(--bg-raised);
189
+ border: 1px solid var(--border-medium);
190
+ border-radius: var(--radius-lg); padding: 6px 0;
191
+ min-width: 260px; z-index: 99999;
192
+ box-shadow: var(--shadow-popup);
193
+ font-size: 12px;
194
+ backdrop-filter: blur(12px);
195
+ animation: tooltipIn 0.15s ease-out;
196
+ }
197
+ @keyframes tooltipIn {
198
+ from { opacity: 0; transform: translateY(-4px); }
199
+ to { opacity: 1; transform: translateY(0); }
200
+ }
201
+ #hooks-tooltip.visible { display: block; }
202
+ #hooks-tooltip .hook-row {
203
+ display: flex; align-items: center;
204
+ padding: 6px 14px; gap: 10px;
205
+ transition: background var(--transition-fast);
206
+ }
207
+ #hooks-tooltip .hook-row:hover { background: rgba(255,255,255,0.03); }
208
+ #hooks-tooltip .hook-event {
209
+ font-family: var(--font-mono);
210
+ font-size: 10px; font-weight: 500;
211
+ color: var(--accent); background: var(--accent-dim);
212
+ padding: 2px 7px; border-radius: var(--radius-sm); white-space: nowrap;
213
+ }
214
+ #hooks-tooltip .hook-name {
215
+ font-family: var(--font-mono);
216
+ color: var(--text-secondary); white-space: nowrap; font-size: 12px;
217
+ }
218
+ #hooks-tooltip .hook-none {
219
+ font-family: var(--font-sans);
220
+ padding: 8px 14px; color: var(--text-tertiary); font-style: italic; font-size: 12px;
221
+ }
222
+
223
+ /* ---- Status ---- */
224
+ #status {
225
+ display: flex; align-items: center; gap: 7px;
226
+ padding: 0 16px;
227
+ font-family: var(--font-mono);
228
+ font-size: 11px; color: var(--accent); white-space: nowrap;
229
+ letter-spacing: 0.02em;
230
+ flex-shrink: 0;
231
+ }
232
+ #status .status-dot {
233
+ width: 6px; height: 6px; border-radius: 50%;
234
+ background: var(--accent);
235
+ box-shadow: 0 0 8px var(--accent-glow);
236
+ animation: pulse 2s ease-in-out infinite;
237
+ }
238
+ @keyframes pulse {
239
+ 0%, 100% { opacity: 1; transform: scale(1); }
240
+ 50% { opacity: 0.5; transform: scale(0.85); }
241
+ }
242
+ #status.error { color: #f47067; }
243
+ #status.error .status-dot { background: #f47067; box-shadow: 0 0 8px rgba(244,112,103,0.3); animation: none; }
244
+ #status.loading { color: var(--text-tertiary); }
245
+ #status.loading .status-dot { background: var(--text-tertiary); box-shadow: none; animation: pulse 1s ease-in-out infinite; }
246
+
247
+ /* ---- Formula bar ---- */
248
+ #formula-bar {
249
+ display: flex; align-items: center;
250
+ background: var(--bg-surface);
251
+ border-bottom: 1px solid var(--border-subtle);
252
+ height: 26px;
253
+ }
254
+ #cell-ref {
255
+ width: 88px; padding: 4px 10px;
256
+ border-right: 1px solid var(--border-subtle);
257
+ font-size: 12px; font-family: var(--font-mono);
258
+ color: var(--accent); background: var(--bg-raised); text-align: center;
259
+ font-weight: 500; letter-spacing: 0.02em;
260
+ }
261
+ #fx-label {
262
+ padding: 0 10px; color: var(--text-tertiary);
263
+ font-style: italic; font-size: 12px;
264
+ font-family: var(--font-mono);
265
+ }
266
+ #formula-input {
267
+ flex: 1; height: 100%; border: none; outline: none;
268
+ padding: 0 10px; font-size: 12px;
269
+ font-family: var(--font-mono);
270
+ color: var(--text-primary);
271
+ background: var(--bg-base);
272
+ }
273
+
274
+ #ss { width: 100%; }
275
+
276
+ /* ---- Empty state ---- */
277
+ #empty-state {
278
+ display: flex; flex-direction: column; align-items: center;
279
+ justify-content: center; height: calc(100vh - 34px);
280
+ color: var(--text-tertiary); font-size: 14px; gap: 6px;
281
+ animation: fadeIn 0.4s ease-out;
282
+ }
283
+ @keyframes fadeIn {
284
+ from { opacity: 0; transform: translateY(8px); }
285
+ to { opacity: 1; transform: translateY(0); }
286
+ }
287
+ #empty-state .empty-icon {
288
+ width: 64px; height: 64px; margin-bottom: 12px;
289
+ border-radius: 16px;
290
+ background: var(--bg-raised);
291
+ border: 1px solid var(--border-subtle);
292
+ display: flex; align-items: center; justify-content: center;
293
+ }
294
+ #empty-state .empty-icon svg { width: 28px; height: 28px; stroke: var(--text-tertiary); }
295
+ #empty-state .empty-title {
296
+ font-family: var(--font-mono);
297
+ font-size: 15px; font-weight: 500;
298
+ color: var(--text-secondary);
299
+ margin-top: 4px;
300
+ }
301
+ #empty-state .empty-desc {
302
+ font-family: var(--font-mono);
303
+ font-size: 13px; color: var(--text-tertiary);
304
+ margin-bottom: 4px;
305
+ }
306
+ #empty-state code {
307
+ font-family: var(--font-mono);
308
+ color: var(--accent); font-size: 12px;
309
+ background: var(--accent-dim);
310
+ padding: 6px 14px; border-radius: var(--radius-md);
311
+ border: 1px solid rgba(86,212,160,0.12);
312
+ letter-spacing: 0.02em;
313
+ }
314
+ </style>
315
+ </head>
316
+ <body>
317
+
318
+ <div id="toolbar">
319
+ <div id="file-tabs"></div>
320
+ <div class="spacer"></div>
321
+ <div id="hooks-badge">
322
+ <span class="hooks-icon"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></span>
323
+ <span>hooks</span>&nbsp;
324
+ <span class="hooks-count" id="hooks-count">\u2026</span>
325
+ </div>
326
+ <span id="status">loading\u2026</span>
327
+ </div>
328
+
329
+ <div id="formula-bar" style="display:none">
330
+ <div id="cell-ref">A1</div>
331
+ <span id="fx-label">fx</span>
332
+ <input type="text" id="formula-input" readonly>
333
+ </div>
334
+
335
+ <div id="ss" style="display:none"></div>
336
+
337
+ <div id="hooks-tooltip"></div>
338
+
339
+ <div id="empty-state">
340
+ <div class="empty-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 16V12"/><path d="M12 16V8"/><path d="M16 16v-2"/></svg></div>
341
+ <div>Waiting for files\u2026</div>
342
+ <div>Open a workbook in another terminal:</div>
343
+ <code>hsx get myfile.xlsx A1</code>
344
+ </div>
345
+
346
+ <script src="https://cdn.jsdelivr.net/npm/@grapecity/spread-sheets/dist/gc.spread.sheets.all.min.js"></script>
347
+ <script src="https://cdn.jsdelivr.net/npm/@grapecity/spread-excelio/dist/gc.spread.excelio.min.js"></script>
348
+ <script>
349
+ // --- Watermark suppression ---
350
+ (function(){
351
+ var orig = CanvasRenderingContext2D.prototype.fillText;
352
+ CanvasRenderingContext2D.prototype.fillText = function(text){
353
+ if (text && typeof text === 'string' && (
354
+ text.includes('GrapeCity') || text.includes('MESCIUS') ||
355
+ text.includes('EVALUATION') || text.includes('Powered') ||
356
+ text.includes('deployment') || text.includes('grapecity.com') ||
357
+ text.includes('mescius.com') || text.includes('Email us') ||
358
+ text === 'Evaluation Version'
359
+ )) return;
360
+ return orig.apply(this, arguments);
361
+ };
362
+ })();
363
+
364
+ var FILES = ${fileListJSON};
365
+ var activeFile = FILES[0] || null;
366
+ var spread = null;
367
+ var viewStates = {};
368
+ var statusEl = document.getElementById('status');
369
+ var cellRefEl = document.getElementById('cell-ref');
370
+ var formulaInputEl = document.getElementById('formula-input');
371
+ var tabsEl = document.getElementById('file-tabs');
372
+ var formulaBarEl = document.getElementById('formula-bar');
373
+ var ssEl = document.getElementById('ss');
374
+ var emptyEl = document.getElementById('empty-state');
375
+
376
+ function showSpread() {
377
+ emptyEl.style.display = 'none';
378
+ formulaBarEl.style.display = 'flex';
379
+ ssEl.style.display = 'block';
380
+ if (!spread) initSpread();
381
+ else { layoutSpread(); spread.refresh(); }
382
+ }
383
+
384
+ function showEmpty() {
385
+ emptyEl.style.display = 'flex';
386
+ formulaBarEl.style.display = 'none';
387
+ ssEl.style.display = 'none';
388
+ }
389
+
390
+ function layoutSpread() {
391
+ ssEl.style.height = (window.innerHeight - 34 - 26) + 'px';
392
+ }
393
+
394
+ // --- File tabs ---
395
+ function renderTabs() {
396
+ tabsEl.innerHTML = '';
397
+ FILES.forEach(function(name, i) {
398
+ var tab = document.createElement('div');
399
+ tab.className = 'file-tab' + (name === activeFile ? ' active' : '');
400
+ tab.setAttribute('tabindex', name === activeFile ? '0' : '-1');
401
+ tab.setAttribute('role', 'tab');
402
+ tab.setAttribute('aria-selected', name === activeFile ? 'true' : 'false');
403
+ tab.setAttribute('data-index', i);
404
+ tab.setAttribute('data-file', name);
405
+
406
+ var dot = document.createElement('span');
407
+ dot.className = 'dot';
408
+ tab.appendChild(dot);
409
+ tab.appendChild(document.createTextNode(name));
410
+
411
+ // Keyboard shortcut badge
412
+ if (i < 9) {
413
+ var kbd = document.createElement('span');
414
+ kbd.className = 'kbd';
415
+ kbd.textContent = '\\u2303' + (i + 1);
416
+ tab.appendChild(kbd);
417
+ }
418
+
419
+ tab.onclick = function() { switchFile(name); };
420
+
421
+ // Arrow key navigation within tablist
422
+ tab.onkeydown = function(e) {
423
+ var tabs = tabsEl.querySelectorAll('.file-tab');
424
+ var idx = Number(tab.getAttribute('data-index'));
425
+ if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
426
+ e.preventDefault();
427
+ var next = tabs[(idx + 1) % tabs.length];
428
+ next.focus();
429
+ } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
430
+ e.preventDefault();
431
+ var prev = tabs[(idx - 1 + tabs.length) % tabs.length];
432
+ prev.focus();
433
+ } else if (e.key === 'Enter' || e.key === ' ') {
434
+ e.preventDefault();
435
+ switchFile(tab.getAttribute('data-file'));
436
+ } else if (e.key === 'Home') {
437
+ e.preventDefault();
438
+ tabs[0].focus();
439
+ } else if (e.key === 'End') {
440
+ e.preventDefault();
441
+ tabs[tabs.length - 1].focus();
442
+ }
443
+ };
444
+
445
+ tabsEl.appendChild(tab);
446
+ });
447
+ }
448
+
449
+ function switchFile(name) {
450
+ if (name === activeFile) return;
451
+ saveViewState();
452
+ activeFile = name;
453
+ renderTabs();
454
+ loadFile(name);
455
+ }
456
+
457
+ // --- Ctrl+N keyboard shortcuts ---
458
+ document.addEventListener('keydown', function(e) {
459
+ if ((e.ctrlKey || e.metaKey) && e.key >= '1' && e.key <= '9') {
460
+ var idx = parseInt(e.key, 10) - 1;
461
+ if (idx < FILES.length) {
462
+ e.preventDefault();
463
+ switchFile(FILES[idx]);
464
+ var tabs = tabsEl.querySelectorAll('.file-tab');
465
+ if (tabs[idx]) tabs[idx].focus();
466
+ }
467
+ }
468
+ });
469
+
470
+ // --- Col helper ---
471
+ function colLetter(col) {
472
+ var s = '';
473
+ while (col >= 0) { s = String.fromCharCode((col % 26) + 65) + s; col = Math.floor(col / 26) - 1; }
474
+ return s;
475
+ }
476
+
477
+ // --- Formula bar ---
478
+ function updateFormulaBar() {
479
+ if (!spread) return;
480
+ var sheet = spread.getActiveSheet();
481
+ var r = sheet.getActiveRowIndex(), c = sheet.getActiveColumnIndex();
482
+ cellRefEl.textContent = colLetter(c) + (r + 1);
483
+ var f = sheet.getFormula(r, c);
484
+ formulaInputEl.value = f ? '=' + f : (sheet.getValue(r, c) ?? '');
485
+ }
486
+
487
+ // --- View state ---
488
+ function saveViewState() {
489
+ if (!spread || !activeFile) return;
490
+ var sheet = spread.getActiveSheet();
491
+ viewStates[activeFile] = {
492
+ sheetName: sheet.name(),
493
+ sheetIndex: spread.getActiveSheetIndex(),
494
+ row: sheet.getActiveRowIndex(),
495
+ col: sheet.getActiveColumnIndex(),
496
+ topRow: sheet.getViewportTopRow(1),
497
+ leftCol: sheet.getViewportLeftColumn(1),
498
+ sels: sheet.getSelections().map(function(s){
499
+ return { row: s.row, col: s.col, rowCount: s.rowCount, colCount: s.colCount };
500
+ })
501
+ };
502
+ }
503
+
504
+ function restoreViewState(state) {
505
+ if (!spread || !state) return;
506
+ var idx = spread.getSheetIndex(state.sheetName);
507
+ if (idx === -1) idx = Math.min(state.sheetIndex, spread.getSheetCount() - 1);
508
+ spread.setActiveSheetIndex(idx);
509
+ var sheet = spread.getActiveSheet();
510
+ try { sheet.showRow(state.topRow, GC.Spread.Sheets.VerticalPosition.top); } catch(e){}
511
+ try { sheet.showColumn(state.leftCol, GC.Spread.Sheets.HorizontalPosition.left); } catch(e){}
512
+ if (state.sels && state.sels.length) {
513
+ sheet.clearSelection();
514
+ state.sels.forEach(function(sel, i){
515
+ if (i === 0) sheet.setSelection(sel.row, sel.col, sel.rowCount, sel.colCount);
516
+ else sheet.addSelection(sel.row, sel.col, sel.rowCount, sel.colCount);
517
+ });
518
+ }
519
+ sheet.setActiveCell(state.row, state.col);
520
+ }
521
+
522
+ // --- SpreadJS init ---
523
+ function initSpread() {
524
+ layoutSpread();
525
+ spread = new GC.Spread.Sheets.Workbook(ssEl);
526
+ spread.bind(GC.Spread.Sheets.Events.SelectionChanged, updateFormulaBar);
527
+ spread.bind(GC.Spread.Sheets.Events.ActiveSheetChanged, updateFormulaBar);
528
+ window.addEventListener('resize', function(){
529
+ layoutSpread();
530
+ spread.refresh();
531
+ });
532
+ }
533
+
534
+ // --- Load file ---
535
+ function loadFile(name) {
536
+ name = name || activeFile;
537
+ if (!name) return;
538
+ statusEl.textContent = 'loading\u2026';
539
+ saveViewState();
540
+
541
+ fetch('/file/' + encodeURIComponent(name) + '?t=' + Date.now())
542
+ .then(function(r){ if (!r.ok) throw new Error(r.status); return r.blob(); })
543
+ .then(function(blob){
544
+ showSpread();
545
+ var io = new GC.Spread.Excel.IO();
546
+ io.open(blob, function(json){
547
+ if (json.sheets) {
548
+ Object.values(json.sheets).forEach(function(s){
549
+ s.rowCount = Math.max(s.rowCount || 0, 10000);
550
+ s.columnCount = Math.max(s.columnCount || 0, 500);
551
+ });
552
+ }
553
+ spread.fromJSON(json);
554
+ restoreViewState(viewStates[name]);
555
+ spread.refresh();
556
+ updateFormulaBar();
557
+ statusEl.textContent = new Date().toLocaleTimeString();
558
+ }, function(err){
559
+ statusEl.textContent = 'error: ' + err;
560
+ });
561
+ })
562
+ .catch(function(e){ statusEl.textContent = 'error: ' + e.message; });
563
+ }
564
+
565
+ // --- Hooks info ---
566
+ function loadHooksInfo() {
567
+ fetch('/hooks')
568
+ .then(function(r){ return r.json(); })
569
+ .then(function(data){
570
+ document.getElementById('hooks-count').textContent = data.total;
571
+ var tooltip = document.getElementById('hooks-tooltip');
572
+ if (!data.hooks.length) {
573
+ tooltip.innerHTML = '<div class="hook-none">No hooks registered</div>';
574
+ return;
575
+ }
576
+ tooltip.innerHTML = data.hooks.map(function(h){
577
+ return '<div class="hook-row">' +
578
+ '<span class="hook-event">' + h.event + '</span>' +
579
+ '<span class="hook-name">' + h.name + '</span>' +
580
+ '</div>';
581
+ }).join('');
582
+ })
583
+ .catch(function(){ });
584
+ }
585
+
586
+ // --- Hooks tooltip toggle ---
587
+ (function(){
588
+ var badge = document.getElementById('hooks-badge');
589
+ var tooltip = document.getElementById('hooks-tooltip');
590
+ badge.addEventListener('click', function(e){
591
+ e.stopPropagation();
592
+ tooltip.classList.toggle('visible');
593
+ });
594
+ document.addEventListener('click', function(){
595
+ tooltip.classList.remove('visible');
596
+ });
597
+ tooltip.addEventListener('click', function(e){ e.stopPropagation(); });
598
+ })();
599
+
600
+ // --- SSE for live reload ---
601
+ function connectSSE() {
602
+ var es = new EventSource('/events');
603
+ es.onopen = function() { statusEl.textContent = FILES.length ? 'watching' : 'waiting\u2026'; };
604
+ es.onmessage = function(e) {
605
+ try {
606
+ var msg = JSON.parse(e.data);
607
+ if (msg.type === 'reload') {
608
+ if (!msg.file || msg.file === activeFile) loadFile();
609
+ }
610
+ if (msg.type === 'files') {
611
+ FILES = msg.files;
612
+ renderTabs();
613
+ // If we had no files and now we do, load the first
614
+ if (!activeFile && FILES.length) {
615
+ activeFile = FILES[0];
616
+ renderTabs();
617
+ loadFile();
618
+ }
619
+ }
620
+ } catch(err) {}
621
+ };
622
+ es.onerror = function() {
623
+ statusEl.textContent = 'reconnecting\u2026';
624
+ };
625
+ }
626
+
627
+ // --- Boot ---
628
+ requestAnimationFrame(function(){
629
+ renderTabs();
630
+ loadHooksInfo();
631
+ if (FILES.length) {
632
+ loadFile();
633
+ } else {
634
+ showEmpty();
635
+ }
636
+ connectSSE();
637
+ });
638
+ </script>
639
+ </body>
640
+ </html>`;
641
+ }
642
+ // ---------------------------------------------------------------------------
643
+ // Watch server
644
+ // ---------------------------------------------------------------------------
645
+ export class WatchServer {
646
+ server;
647
+ sseClients = new Set();
648
+ fileNames = []; // display names
649
+ fileMap = new Map(); // displayName → absPath
650
+ reverseMap = new Map(); // absPath → displayName
651
+ provider;
652
+ unsub = null;
653
+ constructor(provider) {
654
+ this.provider = provider;
655
+ this.syncFiles(provider.listFiles());
656
+ this.server = createServer(this.handleHttp.bind(this));
657
+ }
658
+ displayName(absPath) {
659
+ return basename(absPath);
660
+ }
661
+ syncFiles(absPaths) {
662
+ // Deduplicate display names
663
+ const counts = new Map();
664
+ for (const p of absPaths) {
665
+ const n = this.displayName(p);
666
+ counts.set(n, (counts.get(n) ?? 0) + 1);
667
+ }
668
+ const newNames = [];
669
+ const newFileMap = new Map();
670
+ const newReverseMap = new Map();
671
+ for (const p of absPaths) {
672
+ let name = this.displayName(p);
673
+ if ((counts.get(name) ?? 0) > 1)
674
+ name = p;
675
+ newNames.push(name);
676
+ newFileMap.set(name, p);
677
+ newReverseMap.set(p, name);
678
+ }
679
+ const changed = JSON.stringify(newNames) !== JSON.stringify(this.fileNames);
680
+ this.fileNames = newNames;
681
+ this.fileMap = newFileMap;
682
+ this.reverseMap = newReverseMap;
683
+ return changed;
684
+ }
685
+ async start(port) {
686
+ // Subscribe to file events
687
+ this.unsub = this.provider.subscribe((event) => {
688
+ if (event.type === "opened") {
689
+ const files = this.provider.listFiles();
690
+ if (this.syncFiles(files)) {
691
+ sseBroadcast(this.sseClients, JSON.stringify({ type: "files", files: this.fileNames }));
692
+ }
693
+ }
694
+ else if (event.type === "changed") {
695
+ const name = this.reverseMap.get(event.absPath);
696
+ if (name) {
697
+ sseBroadcast(this.sseClients, JSON.stringify({ type: "reload", file: name }));
698
+ }
699
+ }
700
+ });
701
+ return new Promise((resolve, reject) => {
702
+ this.server.on("error", reject);
703
+ this.server.listen(port, () => {
704
+ const addr = this.server.address();
705
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
706
+ resolve(actualPort);
707
+ });
708
+ });
709
+ }
710
+ stop() {
711
+ if (this.unsub) {
712
+ this.unsub();
713
+ this.unsub = null;
714
+ }
715
+ for (const c of this.sseClients) {
716
+ try {
717
+ c.end();
718
+ }
719
+ catch { }
720
+ }
721
+ this.sseClients.clear();
722
+ this.provider.stop();
723
+ this.server.close();
724
+ }
725
+ handleHttp(req, res) {
726
+ const url = req.url ?? "/";
727
+ if (url === "/events") {
728
+ sseConnect(res, this.sseClients);
729
+ return;
730
+ }
731
+ if (url === "/hooks") {
732
+ getHooksSummary()
733
+ .then((summary) => {
734
+ const body = Buffer.from(JSON.stringify(summary), "utf-8");
735
+ res.writeHead(200, {
736
+ "Content-Type": "application/json",
737
+ "Content-Length": body.length,
738
+ "Cache-Control": "no-cache",
739
+ });
740
+ res.end(body);
741
+ })
742
+ .catch(() => {
743
+ res.writeHead(500);
744
+ res.end("Internal error");
745
+ });
746
+ return;
747
+ }
748
+ if (url === "/" || url === "/index.html") {
749
+ const html = buildViewerHTML(this.fileNames);
750
+ const body = Buffer.from(html, "utf-8");
751
+ res.writeHead(200, {
752
+ "Content-Type": "text/html; charset=utf-8",
753
+ "Content-Length": body.length,
754
+ });
755
+ res.end(body);
756
+ return;
757
+ }
758
+ if (url.startsWith("/file/")) {
759
+ const reqName = decodeURIComponent(url.slice(6).split("?")[0]);
760
+ const absPath = this.fileMap.get(reqName);
761
+ if (absPath) {
762
+ this.provider
763
+ .getBuffer(absPath)
764
+ .then((buf) => {
765
+ if (buf) {
766
+ res.writeHead(200, {
767
+ "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
768
+ "Content-Length": buf.length,
769
+ "Cache-Control": "no-cache",
770
+ });
771
+ res.end(buf);
772
+ }
773
+ else {
774
+ res.writeHead(404);
775
+ res.end("Not found");
776
+ }
777
+ })
778
+ .catch(() => {
779
+ res.writeHead(500);
780
+ res.end("Internal error");
781
+ });
782
+ return;
783
+ }
784
+ res.writeHead(404);
785
+ res.end("Not found");
786
+ return;
787
+ }
788
+ res.writeHead(404);
789
+ res.end("Not found");
790
+ }
791
+ }
792
+ //# sourceMappingURL=watch-server.js.map