@tobq/boardclip 0.0.0-stage → 2.1.107

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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -2
  3. package/assets/boardclip-icon.png +0 -0
  4. package/bin/boardclip.js +43 -0
  5. package/editor-preload.js +71 -0
  6. package/editor.html +220 -0
  7. package/icon.png +0 -0
  8. package/icon@2x.png +0 -0
  9. package/iconTemplate.png +0 -0
  10. package/iconTemplate@2x.png +0 -0
  11. package/index.html +1339 -0
  12. package/lib/appearance.js +183 -0
  13. package/lib/auto-update.js +306 -0
  14. package/lib/backup-worker.js +46 -0
  15. package/lib/backup.js +275 -0
  16. package/lib/blob-field-store.js +275 -0
  17. package/lib/blob-store.js +68 -0
  18. package/lib/build-info.js +89 -0
  19. package/lib/clip-blob-store.js +161 -0
  20. package/lib/clip-revision.js +81 -0
  21. package/lib/clipboard-capture.js +95 -0
  22. package/lib/clipboard-follow.js +98 -0
  23. package/lib/clipboard-model.js +1235 -0
  24. package/lib/cloud-accounts.js +250 -0
  25. package/lib/conflict-model.js +186 -0
  26. package/lib/control-client.js +87 -0
  27. package/lib/control-server.js +149 -0
  28. package/lib/diagnostics.js +121 -0
  29. package/lib/drag-files.js +48 -0
  30. package/lib/ensure-directory.js +56 -0
  31. package/lib/fork-names.js +35 -0
  32. package/lib/fs-probe.js +132 -0
  33. package/lib/history-feed.js +48 -0
  34. package/lib/hmac-auth.js +78 -0
  35. package/lib/macos-hotkey.js +297 -0
  36. package/lib/macos-paste.js +112 -0
  37. package/lib/mcp-core.js +270 -0
  38. package/lib/mcp-installers.js +284 -0
  39. package/lib/mcp-paths.js +99 -0
  40. package/lib/p2p-crypto.js +54 -0
  41. package/lib/p2p-discovery.js +191 -0
  42. package/lib/quick-paste.js +187 -0
  43. package/lib/retention.js +43 -0
  44. package/lib/sync-delta.js +227 -0
  45. package/lib/sync-journal.js +181 -0
  46. package/lib/sync-paths.js +73 -0
  47. package/lib/tailscale.js +80 -0
  48. package/lib/text-blob-store.js +38 -0
  49. package/lib/windows-clipboard.js +213 -0
  50. package/lib/windows-console.js +63 -0
  51. package/lib/windows-dwm.js +183 -0
  52. package/lib/windows-hook-worker.js +236 -0
  53. package/lib/windows-hook.js +91 -0
  54. package/lib/windows-paste.js +185 -0
  55. package/main.js +7540 -0
  56. package/mcp/boardclip-mcp.js +255 -0
  57. package/mcp-approval-preload.js +22 -0
  58. package/mcp-approval.html +218 -0
  59. package/package.json +55 -4
  60. package/preload.js +106 -0
  61. package/site/index.html +1624 -0
  62. package/site/shared/clip-search.js +2330 -0
  63. package/site/shared/clipboard-popup.css +1046 -0
  64. package/site/shared/clipboard-tokens.css +326 -0
  65. package/site/shared/clipboard-ui-core.js +6921 -0
  66. package/site/shared/clipboard-window.css +36 -0
  67. package/site/shared/vendor/cm5/README.md +52 -0
  68. package/site/shared/vendor/cm5/codemirror.css +344 -0
  69. package/site/shared/vendor/cm5/codemirror.js +9884 -0
  70. package/site/shared/vendor/cm5/diff-match-patch.js +2228 -0
  71. package/site/shared/vendor/cm5/merge.css +119 -0
  72. package/site/shared/vendor/cm5/merge.js +1182 -0
  73. package/site/shared/vendor/fonts/README.md +19 -0
  74. package/site/shared/vendor/fonts/material-symbols-rounded.woff2 +0 -0
  75. package/viewer-preload.js +51 -0
  76. package/viewer.html +129 -0
@@ -0,0 +1,19 @@
1
+ # Vendored icon font: Material Symbols Rounded
2
+
3
+ - `material-symbols-rounded.woff2` (561,004 bytes, sha256
4
+ `8fbf735bcf81756bdc32d66e4f04b189b3fa49fd30c50b06faa89834ca94c51a`), Apache-2.0
5
+ (https://github.com/google/material-design-icons/blob/master/LICENSE).
6
+ - Source (2026-10-07): the Google Fonts css2 API, fetched with a Chrome 132 User-Agent so it
7
+ answers with one woff2:
8
+ `https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20,400,0..1,0&display=block`
9
+ -> `https://fonts.gstatic.com/s/materialsymbolsrounded/v376/syl7-zNym6YjUruM-QrEh7-nyTnjDwKNJ_190FjpZIvLgyidOK7BDB_Qb9vUdV6_gjDK-P2puF_Zs-obHph2-jOcZTKPqw.woff2`
10
+ - Axes: only what the canon pins on `.mi` (clipboard-popup.css): opsz 20, wght 400, GRAD 0
11
+ fixed; FILL 0..1 kept variable (FILL 1 is the state signal, `--icon-fill`).
12
+
13
+ Declared ONCE, by the `@font-face` at the top of `site/shared/clipboard-popup.css`, which every
14
+ app window and the website load. `font-display: block`, so a window never shows the ligature
15
+ words ("settings", "close") while the font loads, and nothing waits on Google Fonts.
16
+
17
+ Re-vendor: fetch the same css2 URL with a current Chrome User-Agent (a different axis set needs
18
+ the `.mi` rule changed too), download the woff2 it names over this file, update the size, hash
19
+ and date above.
@@ -0,0 +1,51 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ // Bridge for the in-app image viewer window (viewer.html). Main pushes the
4
+ // image + theme via 'viewer-init'; the clip menu reuses the SAME mutation IPC
5
+ // channels the popup uses (pin/group-assign/numpad-assign/...), so the two
6
+ // surfaces cannot drift.
7
+ let clipId = null;
8
+
9
+ contextBridge.exposeInMainWorld('viewerApi', {
10
+ onInit: (callback) => {
11
+ ipcRenderer.on('viewer-init', (_, init) => {
12
+ clipId = init && init.id;
13
+ callback(init || {});
14
+ });
15
+ },
16
+ // Light snapshot (items/groups/numpad map) for the shared clip menu.
17
+ state: () => ipcRenderer.invoke('clip-window-state', clipId),
18
+ pin: (id, rev) => ipcRenderer.invoke('pin', id, rev),
19
+ groupCreate: (name) => ipcRenderer.invoke('group-create', name),
20
+ groupAssign: (id, group, rev) => ipcRenderer.invoke('group-assign', id, group, rev),
21
+ numpadAssign: (id, slot, rev) => ipcRenderer.invoke('numpad-assign', id, slot, rev),
22
+ numpadUnassign: (slot) => ipcRenderer.invoke('numpad-unassign', slot),
23
+ setClipTitle: (id, title, rev) => ipcRenderer.invoke('set-clip-title', id, title, rev),
24
+ deleteItems: (targets) => ipcRenderer.invoke('delete-items', targets), // [{ id, rev }]
25
+ copyImagePath: (id) => ipcRenderer.invoke('copy-image-path', id),
26
+ openImageExternal: (id) => ipcRenderer.invoke('open-image-external', id),
27
+ // Drag this image out as a file (native drag of a temporary copy).
28
+ startDrag: () => ipcRenderer.send('start-drag', [clipId]),
29
+ close: () => ipcRenderer.send('viewer-close'),
30
+ // The bar's height + resolved colours, for the native window controls.
31
+ windowChrome: (chrome) => ipcRenderer.send('window-chrome', chrome),
32
+ // Ctrl/Cmd+= / - / 0, claimed in main before the app menu: zoom the image.
33
+ onZoomKey: (callback) => {
34
+ const listener = (_, act) => callback(act);
35
+ ipcRenderer.on('image-zoom-key', listener);
36
+ return () => ipcRenderer.removeListener('image-zoom-key', listener);
37
+ },
38
+ getColorScheme: () => ipcRenderer.invoke('get-color-scheme'),
39
+ onColorSchemeChanged: (callback) => {
40
+ const listener = (_, scheme) => callback(scheme);
41
+ ipcRenderer.on('color-scheme-changed', listener);
42
+ return () => ipcRenderer.removeListener('color-scheme-changed', listener);
43
+ },
44
+ // Appearance (accent, density, corners, theme mode, this window's surface):
45
+ // one event whenever any of it changes - a setting, another device, the OS accent.
46
+ onAppearanceChanged: (callback) => {
47
+ const listener = (_, look) => callback(look || {});
48
+ ipcRenderer.on('appearance-changed', listener);
49
+ return () => ipcRenderer.removeListener('appearance-changed', listener);
50
+ },
51
+ });
package/viewer.html ADDED
@@ -0,0 +1,129 @@
1
+ <!DOCTYPE html>
2
+ <html><head><meta charset="utf-8"><title>Image</title>
3
+ <script>
4
+ document.documentElement.dataset.theme = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
5
+ </script>
6
+ <link rel="stylesheet" href="site/shared/clipboard-popup.css">
7
+ <link rel="stylesheet" href="site/shared/clipboard-window.css">
8
+ <style>
9
+ /* Viewer window shell only; the viewer itself + tokens come from the shared
10
+ clipboard-popup.css + Core.createImageViewer (single source), the window
11
+ base (reset, font, background, glass scrim) from clipboard-window.css. */
12
+ #viewerHost { height:100%; }
13
+ </style></head><body>
14
+ <div id="viewerHost"></div>
15
+ <div class="toast" id="toast">Copied</div>
16
+ <script src="site/shared/clip-search.js"></script>
17
+ <script src="site/shared/clipboard-ui-core.js"></script>
18
+ <script>
19
+ const Core = window.BoardClipCore;
20
+ const host = document.getElementById('viewerHost');
21
+ const toast = document.getElementById('toast');
22
+ let toastTimer = null;
23
+ function showToast(msg) { toast.textContent = msg || 'Copied'; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 1200); }
24
+
25
+ // Theme: follow the app's theme_mode setting + live system changes (same as editor.html).
26
+ let themeMode = 'system';
27
+ let systemDark = matchMedia('(prefers-color-scheme: dark)').matches;
28
+ function refreshTheme() { Core.applyTheme(document.documentElement, themeMode, systemDark); }
29
+ if (window.viewerApi.getColorScheme) window.viewerApi.getColorScheme().then((s) => { systemDark = s !== 'light'; refreshTheme(); }).catch(() => {});
30
+ if (window.viewerApi.onColorSchemeChanged) window.viewerApi.onColorSchemeChanged((s) => { systemDark = s !== 'light'; refreshTheme(); });
31
+ // Later appearance changes (a setting here or on another device, the OS
32
+ // accent, the glass scope) arrive as the same payload as init.
33
+ if (window.viewerApi.onAppearanceChanged) window.viewerApi.onAppearanceChanged((look) => {
34
+ if (look.themeMode) { themeMode = (look.themeMode === 'light' || look.themeMode === 'dark') ? look.themeMode : 'system'; refreshTheme(); }
35
+ Core.applyAppearance(document.documentElement, look);
36
+ });
37
+
38
+ let viewer = null;
39
+ let clipId = null;
40
+ // Light state snapshot backing the shared clip menu (items for numpad previews,
41
+ // groups tree, pin state). Refetched right before the menu opens + after every
42
+ // mutation, so it is always current without a live push channel.
43
+ let state = { item: null, items: [], groups: [], aiGroup: 'AI' };
44
+ async function refreshState() {
45
+ state = await window.viewerApi.state();
46
+ if (state && state.item && viewer) viewer.setTitle(Core.titleOf(state.item) || 'Image');
47
+ // Title-bar tag strip: the clip's groups + the "+" picker (images always
48
+ // exist as clips, so no commit-on-add here; a null item hides the strip).
49
+ if (viewer) viewer.setTags(state ? state.item : null);
50
+ }
51
+
52
+ // The SAME shared controller the popup uses drives the menu dispatch (pin/
53
+ // group/numpad/rename/delete/save/open-external), against an adapter backed by
54
+ // the viewer IPC. context 'viewer' swaps "Open image" for "Open externally".
55
+ // Mutations send the rev of the clip this window shows; a stale one is refused.
56
+ const clipRevOf = (id) => { const it = state.items.find((x) => Core.itemId(x) === id); return it ? it.rev : undefined; };
57
+ const revGuard = (run, fallback) => Core.guardRevision(run, { toast: showToast, refresh: () => refreshState(), fallback });
58
+ const adapter = {
59
+ menuContext: 'viewer',
60
+ itemById: (id) => state.items.find((it) => Core.itemId(it) === id) || null,
61
+ allItems: () => state.items,
62
+ groupNames: () => state.groups,
63
+ numpadMap: () => Core.numpadMap(state.items),
64
+ protectedGroups: () => new Set([state.aiGroup]),
65
+ pin: (id) => revGuard(() => window.viewerApi.pin(id, clipRevOf(id)), false),
66
+ toggleGroup: (id, group) => revGuard(() => window.viewerApi.groupAssign(id, group, clipRevOf(id)), false),
67
+ createGroup: (name) => window.viewerApi.groupCreate(name),
68
+ numpadAssign: (id, slot) => revGuard(() => window.viewerApi.numpadAssign(id, slot, clipRevOf(id)), false),
69
+ numpadUnassign: (slot) => window.viewerApi.numpadUnassign(slot),
70
+ setClipTitle: (id, title) => revGuard(() => window.viewerApi.setClipTitle(id, title, clipRevOf(id)), false),
71
+ // Delete closes the window (the popup shows the Undo toast on its side).
72
+ deleteClips: async (ids) => {
73
+ const done = await revGuard(async () => { await window.viewerApi.deleteItems(Core.revisionTargets(ids, clipRevOf)); return true; }, false);
74
+ if (done) window.viewerApi.close();
75
+ return [];
76
+ },
77
+ openImageExternal: (item) => window.viewerApi.openImageExternal(Core.itemId(item)),
78
+ saveImage: async (item) => { const d = await window.viewerApi.copyImagePath(Core.itemId(item)); return d && d.path ? 'Path copied' : null; },
79
+ toast: (msg) => showToast(msg),
80
+ render: () => {},
81
+ refresh: () => refreshState(),
82
+ };
83
+ const controller = Core.createClipController(adapter);
84
+ document.addEventListener('click', (e) => controller.onClick(e));
85
+
86
+ async function openMenu(x, y) {
87
+ await refreshState();
88
+ if (!state.item) return;
89
+ controller.openClipMenu(Core.itemId(state.item), x, y);
90
+ }
91
+
92
+ window.viewerApi.onInit(async (init) => {
93
+ themeMode = (init.themeMode === 'light' || init.themeMode === 'dark') ? init.themeMode : 'system';
94
+ refreshTheme();
95
+ Core.applyAppearance(document.documentElement, init);
96
+ clipId = init.id;
97
+ host.innerHTML = '';
98
+ viewer = Core.createImageViewer({
99
+ src: init.src,
100
+ title: init.title || 'Image',
101
+ nativeControls: true,
102
+ onMenu: (x, y) => openMenu(x, y),
103
+ onClose: () => window.viewerApi.close(),
104
+ // Drag the image out (title-bar handle; the image itself at fit size or
105
+ // with Alt): a native file drag of a temporary copy, done by main.
106
+ onDragOut: window.viewerApi.startDrag ? () => window.viewerApi.startDrag() : null,
107
+ });
108
+ host.appendChild(viewer.el);
109
+ // The OS's own window controls: the bar reserves their room and reports its
110
+ // colours, so the caption buttons match it (main's setTitleBarOverlay).
111
+ Core.attachWindowControls(init.windowControls, window.viewerApi.windowChrome);
112
+ viewer.focus();
113
+ refreshState();
114
+ });
115
+ // Ctrl/Cmd+= / - / 0 zoom the image (main claims them before the app menu).
116
+ if (window.viewerApi.onZoomKey) window.viewerApi.onZoomKey((act) => { if (viewer) viewer.zoomKey(act); });
117
+
118
+ // Right-click anywhere → the same clip menu (parity with popup rows).
119
+ document.addEventListener('contextmenu', (e) => {
120
+ e.preventDefault();
121
+ openMenu(e.clientX, e.clientY);
122
+ });
123
+ // Escape falls through the viewer root (its handler covers the stage); catch
124
+ // stray focus targets here too. Dialogs/menu capture their own Escape first.
125
+ document.addEventListener('keydown', (e) => {
126
+ if (e.key === 'Escape') { e.preventDefault(); window.viewerApi.close(); }
127
+ });
128
+ </script>
129
+ </body></html>