@tobq/boardclip 0.0.0-stage → 2.1.108

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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tobi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,171 @@
1
- # Temporary Holding Version
1
+ # BoardClip
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Fast cross-platform clipboard history for desktop. BoardClip runs as a tray app, opens with a global shortcut, and keeps your useful text and image clips searchable, pinnable, and quick to paste.
4
+
5
+ ## Install
6
+
7
+ Everyday desktop builds are available from the website:
8
+
9
+ [Download BoardClip](https://boardclip.app)
10
+
11
+ With a package manager:
12
+
13
+ ```sh
14
+ # macOS (Homebrew): the app opens with no security prompt
15
+ brew install --cask tobq/tap/boardclip
16
+
17
+ # Windows (Scoop)
18
+ scoop bucket add tobq https://github.com/tobq/homebrew-tap
19
+ scoop install tobq/boardclip
20
+
21
+ # Any OS with Node.js 18+ (then run `boardclip`; update with `npm update -g @tobq/boardclip`)
22
+ npm i -g @tobq/boardclip
23
+ ```
24
+
25
+ Or try it without installing: `npx @tobq/boardclip`.
26
+
27
+ Terminal install keeps a source checkout on your machine and reuses the same local lifecycle scripts for future updates. This path requires Git, and install/dependency updates also require npm.
28
+ It also registers a normal launcher: `BoardClip` in the Windows Start Menu or `~/Applications/BoardClip.app` on macOS.
29
+
30
+ macOS / Linux:
31
+
32
+ ```sh
33
+ curl -fsSL https://boardclip.sh/install.sh | sh
34
+ ```
35
+
36
+ Windows PowerShell:
37
+
38
+ ```powershell
39
+ irm https://boardclip.sh/install.ps1 | iex
40
+ ```
41
+
42
+ Prefer to inspect first? Open the script URL in your browser, or download it before running:
43
+
44
+ ```sh
45
+ curl -fsSLO https://boardclip.sh/install.sh
46
+ sh install.sh
47
+ ```
48
+
49
+ ## Update
50
+
51
+ macOS / Linux:
52
+
53
+ ```sh
54
+ curl -fsSL https://boardclip.sh/update.sh | sh
55
+ ```
56
+
57
+ Windows PowerShell:
58
+
59
+ ```powershell
60
+ irm https://boardclip.sh/update.ps1 | iex
61
+ ```
62
+
63
+ Local checkout update scripts are also available:
64
+
65
+ ```sh
66
+ ./update.sh
67
+ ```
68
+
69
+ ```bat
70
+ update.bat
71
+ ```
72
+
73
+ Local update scripts are production-safe by default: they fast-forward only and
74
+ refuse to run if tracked app files have local edits. Runtime data files are
75
+ ignored and do not block updates. The public update commands above set
76
+ `BOARDCLIP_UPDATE_ALLOW_DIRTY=1`, so normal installs can repair a dirty checkout
77
+ with Git autostash. For a developer checkout, set that variable yourself only
78
+ when you intentionally want Git to autostash local code changes.
79
+
80
+ ## Features
81
+
82
+ - Hotkey popup: Cmd+Shift+V on macOS, Win+V on Windows.
83
+ - Text and image history with automatic deduplication.
84
+ - Numpad quick-paste slots 1-9 for snippets you reuse constantly.
85
+ - Pins and groups for keeping durable clips organized, with include/exclude filters.
86
+ - Native cloud sync through Google Drive, OneDrive, and iCloud folders.
87
+ - Regex search, external editor support, and image export.
88
+ - Local-first data: no hosted clipboard database or BoardClip account.
89
+
90
+ ## Keyboard Shortcuts
91
+
92
+ | Shortcut | Action |
93
+ | --- | --- |
94
+ | Cmd+Shift+V / Win+V | Toggle popup |
95
+ | Cmd+Numpad 1-9 / Win+Numpad 1-9 | Quick-paste slot |
96
+ | 1-9 in empty search | Quick-paste slot |
97
+ | Arrow keys | Navigate items |
98
+ | Enter | Paste selected item |
99
+ | Escape | Close popup |
100
+
101
+ ## Development
102
+
103
+ Requirements:
104
+
105
+ - Git
106
+ - Node.js and npm
107
+
108
+ Run from a checkout:
109
+
110
+ ```sh
111
+ npm install
112
+ npm test
113
+ npm start
114
+ ```
115
+
116
+ Build desktop binaries locally:
117
+
118
+ ```sh
119
+ npm run dist
120
+ ```
121
+
122
+ Platform-specific package scripts are available as `npm run dist:win`, `npm run dist:mac`, and `npm run dist:linux`. Tagged releases matching `v*` are built by GitHub Actions and attached to the GitHub release.
123
+
124
+ Regenerate the shared app, installer, and site icons:
125
+
126
+ ```sh
127
+ npm run sync:icons
128
+ ```
129
+
130
+ Useful local scripts:
131
+
132
+ ```sh
133
+ ./start.sh
134
+ ./update.sh
135
+ ./kill.sh
136
+ ```
137
+
138
+ ```bat
139
+ start.bat
140
+ update.bat
141
+ kill.bat
142
+ ```
143
+
144
+ ## Project Structure
145
+
146
+ - `main.js`: Electron main process, tray, shortcuts, clipboard polling, sync, IPC.
147
+ - `index.html`: single-file renderer UI.
148
+ - `preload.js`: safe renderer API bridge.
149
+ - `lib/clipboard-model.js`: shared pure history/pin/group merge logic.
150
+ - `lib/cloud-accounts.js`: Google Drive, OneDrive, and iCloud folder discovery.
151
+ - `lib/windows-hook*`: Windows low-level keyboard hook for Win+V / Win+Numpad.
152
+ - `site/`: static landing page and remote install/update scripts.
153
+
154
+ ## Data
155
+
156
+ Source installs store data in the app checkout. Packaged installs store data in Electron's per-user app data directory:
157
+
158
+ - `clipboard-history.json`
159
+ - `clipboard-settings.json`
160
+ - `clipboard-images/`
161
+ - `clipboard-text/` for large text snippets
162
+
163
+ Cloud sync writes the same files into detected provider folders under `clipboard-tray/`. That folder name is retained for compatibility with earlier installs.
164
+
165
+ ## Contributing
166
+
167
+ Issues and pull requests are welcome. Please run `npm test` before opening a PR and keep changes focused; most behavior should go through shared helpers in `lib/clipboard-model.js` instead of duplicating merge logic in UI or IPC code.
168
+
169
+ ## License
170
+
171
+ MIT
Binary file
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ // `boardclip` command of the npm package (npm i -g @tobq/boardclip, npx @tobq/boardclip):
3
+ // starts the tray app with the Electron that npm installed beside it, detached
4
+ // so the terminal is free again and closing it does not take the app along.
5
+ // A second start only brings the running app's popup up (single-instance lock).
6
+ 'use strict';
7
+
8
+ const { spawn } = require('child_process');
9
+ const path = require('path');
10
+
11
+ const appDir = path.join(__dirname, '..');
12
+ const args = process.argv.slice(2);
13
+
14
+ if (args.includes('--version') || args.includes('-v')) {
15
+ console.log(require('../package.json').version);
16
+ process.exit(0);
17
+ }
18
+
19
+ let electron;
20
+ try {
21
+ electron = require('electron');
22
+ } catch (error) {
23
+ console.error('BoardClip needs its Electron download. Reinstall with: npm i -g @tobq/boardclip');
24
+ process.exit(1);
25
+ }
26
+
27
+ const env = { ...process.env };
28
+ // Set by some shells and tools; it would start Electron as plain Node.
29
+ delete env.ELECTRON_RUN_AS_NODE;
30
+
31
+ const child = spawn(electron, [appDir, ...args], {
32
+ cwd: appDir,
33
+ detached: true,
34
+ stdio: 'ignore',
35
+ windowsHide: true,
36
+ env,
37
+ });
38
+ child.on('error', (error) => {
39
+ console.error(`Could not start BoardClip: ${error.message}`);
40
+ process.exit(1);
41
+ });
42
+ child.unref();
43
+ console.log('BoardClip is running in your menu bar / system tray.');
@@ -0,0 +1,71 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+
3
+ // Bridge for the built-in editor window (editor.html). Main pushes the initial
4
+ // text via 'editor-init'; the renderer streams drafts (every keystroke, for
5
+ // crash-safety) and commits (idle/save/close, to write the clip), keyed by the
6
+ // session id main assigned at open.
7
+ let sessionId = null;
8
+
9
+ contextBridge.exposeInMainWorld('editorApi', {
10
+ onInit: (callback) => {
11
+ ipcRenderer.on('editor-init', (_, init) => {
12
+ sessionId = init && init.sessionId;
13
+ callback(init || {});
14
+ });
15
+ },
16
+ onFind: (callback) => {
17
+ const listener = (_, find) => callback(find || {});
18
+ ipcRenderer.on('editor-find', listener);
19
+ return () => ipcRenderer.removeListener('editor-find', listener);
20
+ },
21
+ onConflict: (callback) => {
22
+ const listener = (_, conflict) => callback(conflict || {});
23
+ ipcRenderer.on('editor-conflict', listener);
24
+ return () => ipcRenderer.removeListener('editor-conflict', listener);
25
+ },
26
+ draft: (payload) => ipcRenderer.send('editor-draft', sessionId, payload),
27
+ // invoke (not send): the promise resolves AFTER the clip write, with the
28
+ // session's current (content-addressed) id — the title-bar tag strip's
29
+ // commit-on-add awaits it before opening the group picker.
30
+ commit: (payload) => ipcRenderer.invoke('editor-commit', sessionId, payload),
31
+ resolveConflict: (payload) => ipcRenderer.invoke('resolve-conflict', payload),
32
+ unifyStep: (payload) => ipcRenderer.invoke('unify-step', sessionId, payload),
33
+ close: () => ipcRenderer.send('editor-close', sessionId),
34
+ // The find bar's mode + match case, remembered per device.
35
+ saveFindPrefs: (prefs) => ipcRenderer.invoke('save-settings', { find_regex: !!(prefs && prefs.regex), find_case: !!(prefs && prefs.caseSensitive) }),
36
+ // A drag on the idle title moves this window (main's window-drag, sender only).
37
+ windowDrag: (phase, dx, dy) => ipcRenderer.send('window-drag', phase, dx, dy),
38
+ // The bar's height + resolved colours, for the native window controls.
39
+ windowChrome: (chrome) => ipcRenderer.send('window-chrome', chrome),
40
+ // Clipboard follow: while the clipboard holds this note, saves update it.
41
+ focused: () => ipcRenderer.send('editor-focus', sessionId),
42
+ copyToClipboard: (payload) => ipcRenderer.invoke('editor-copy', sessionId, payload),
43
+ onClipboardState: (callback) => {
44
+ const listener = (_, state) => callback(state || {});
45
+ ipcRenderer.on('editor-clipboard', listener);
46
+ return () => ipcRenderer.removeListener('editor-clipboard', listener);
47
+ },
48
+ // Clip menu (shared with the popup + viewer). The clip id changes on every
49
+ // committed edit (content-addressed), so the menu asks for the CURRENT id.
50
+ currentId: () => ipcRenderer.invoke('editor-current-id', sessionId),
51
+ state: (id) => ipcRenderer.invoke('clip-window-state', id),
52
+ pin: (id, rev) => ipcRenderer.invoke('pin', id, rev),
53
+ groupCreate: (name) => ipcRenderer.invoke('group-create', name),
54
+ groupAssign: (id, group, rev) => ipcRenderer.invoke('group-assign', id, group, rev),
55
+ numpadAssign: (id, slot, rev) => ipcRenderer.invoke('numpad-assign', id, slot, rev),
56
+ numpadUnassign: (slot) => ipcRenderer.invoke('numpad-unassign', slot),
57
+ deleteSelf: (rev) => ipcRenderer.invoke('editor-delete-clip', sessionId, rev),
58
+ getColorScheme: () => ipcRenderer.invoke('get-color-scheme'),
59
+ onColorSchemeChanged: (callback) => {
60
+ const listener = (_, scheme) => callback(scheme);
61
+ ipcRenderer.on('color-scheme-changed', listener);
62
+ return () => ipcRenderer.removeListener('color-scheme-changed', listener);
63
+ },
64
+ // Appearance (accent, density, corners, theme mode, this window's surface):
65
+ // one event whenever any of it changes - a setting, another device, the OS accent.
66
+ onAppearanceChanged: (callback) => {
67
+ const listener = (_, look) => callback(look || {});
68
+ ipcRenderer.on('appearance-changed', listener);
69
+ return () => ipcRenderer.removeListener('appearance-changed', listener);
70
+ },
71
+ });
package/editor.html ADDED
@@ -0,0 +1,220 @@
1
+ <!DOCTYPE html>
2
+ <html><head><meta charset="utf-8"><title>Editor</title>
3
+ <script>
4
+ document.documentElement.dataset.theme = matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
5
+ </script>
6
+ <!-- Vendored CodeMirror 5 merge view (offline, shared with the website demo);
7
+ powers the shared reconciliation/unify UI. Load before clipboard-ui-core. -->
8
+ <link rel="stylesheet" href="site/shared/vendor/cm5/codemirror.css">
9
+ <link rel="stylesheet" href="site/shared/vendor/cm5/merge.css">
10
+ <script src="site/shared/vendor/cm5/codemirror.js"></script>
11
+ <script src="site/shared/vendor/cm5/diff-match-patch.js"></script>
12
+ <script src="site/shared/vendor/cm5/merge.js"></script>
13
+ <link rel="stylesheet" href="site/shared/clipboard-popup.css">
14
+ <link rel="stylesheet" href="site/shared/clipboard-window.css">
15
+ <style>
16
+ /* Editor window shell only; the editor itself + tokens come from the shared
17
+ clipboard-popup.css + Core.createEditor (single source with the demo), the
18
+ window base (reset, font, background, glass scrim) from clipboard-window.css. */
19
+ #editorHost { height:100%; }
20
+ </style></head><body>
21
+ <div id="editorHost"></div>
22
+ <div class="toast" id="toast">Copied</div>
23
+ <script src="site/shared/clip-search.js"></script>
24
+ <script src="site/shared/clipboard-ui-core.js"></script>
25
+ <script>
26
+ const Core = window.BoardClipCore;
27
+ const host = document.getElementById('editorHost');
28
+
29
+ // Theme: follow the app's theme_mode setting + live system changes, exactly
30
+ // like the popup (Core.applyTheme on <html>).
31
+ let themeMode = 'system';
32
+ let systemDark = matchMedia('(prefers-color-scheme: dark)').matches;
33
+ function refreshTheme() { Core.applyTheme(document.documentElement, themeMode, systemDark); }
34
+ if (window.editorApi.getColorScheme) window.editorApi.getColorScheme().then((s) => { systemDark = s !== 'light'; refreshTheme(); }).catch(() => {});
35
+ if (window.editorApi.onColorSchemeChanged) window.editorApi.onColorSchemeChanged((s) => { systemDark = s !== 'light'; refreshTheme(); });
36
+ // Later appearance changes (a setting here or on another device, the OS
37
+ // accent, the glass scope) arrive as the same payload as init.
38
+ if (window.editorApi.onAppearanceChanged) window.editorApi.onAppearanceChanged((look) => {
39
+ if (look.themeMode) { themeMode = (look.themeMode === 'light' || look.themeMode === 'dark') ? look.themeMode : 'system'; refreshTheme(); }
40
+ Core.applyAppearance(document.documentElement, look);
41
+ });
42
+
43
+ let editor = null;
44
+ let reconcileView = null;
45
+ let windowControls = null; // Core.attachWindowControls, set up on init
46
+ let toastTimer = null;
47
+ function showToast(message) {
48
+ const toast = document.getElementById('toast');
49
+ if (!toast) return;
50
+ toast.textContent = message || 'Copied';
51
+ toast.classList.add('show');
52
+ clearTimeout(toastTimer);
53
+ toastTimer = setTimeout(() => toast.classList.remove('show'), 1200);
54
+ }
55
+
56
+ // --- Shared clip menu (pin/group/numpad/title/delete), same as the popup rows
57
+ // and the image viewer. Backed by a light state snapshot fetched right before
58
+ // the menu opens (the clip id changes on every committed edit, so the CURRENT
59
+ // id comes from main at open time too). context 'editor' hides "Open in editor".
60
+ let menuState = { item: null, items: [], groups: [], aiGroup: 'AI' };
61
+ let menuClipId = null;
62
+ // Title-bar tag strip: re-render from the CURRENT clip (the id is content-
63
+ // addressed, so it changes on every committed text edit — always re-query).
64
+ // A new note that hasn't committed yet shows just the + (commit-on-add).
65
+ async function refreshTags() {
66
+ if (!editor) return;
67
+ const id = await window.editorApi.currentId();
68
+ if (!id) { editor.setTags(null, { allowAdd: true }); return; }
69
+ menuClipId = id;
70
+ menuState = await window.editorApi.state(id);
71
+ editor.setTags(menuState.item, { allowAdd: true });
72
+ }
73
+ // Menu mutations send the rev from the state snapshot fetched when the menu
74
+ // opened; a stale one is refused. (Text commits keep their own base-text
75
+ // conflict handling, which forks rather than losing typed work.)
76
+ const clipRevOf = (id) => { const it = menuState.items.find((x) => Core.itemId(x) === id); return it ? it.rev : undefined; };
77
+ const revGuard = (run, fallback) => Core.guardRevision(run, { toast: showToast, refresh: () => refreshTags(), fallback });
78
+ const menuAdapter = {
79
+ menuContext: 'editor',
80
+ itemById: (id) => menuState.items.find((it) => Core.itemId(it) === id) || null,
81
+ allItems: () => menuState.items,
82
+ groupNames: () => menuState.groups,
83
+ numpadMap: () => Core.numpadMap(menuState.items),
84
+ protectedGroups: () => new Set([menuState.aiGroup]),
85
+ pin: (id) => revGuard(() => window.editorApi.pin(id, clipRevOf(id)), false),
86
+ toggleGroup: (id, group) => revGuard(() => window.editorApi.groupAssign(id, group, clipRevOf(id)), false),
87
+ createGroup: (name) => window.editorApi.groupCreate(name),
88
+ numpadAssign: (id, slot) => revGuard(() => window.editorApi.numpadAssign(id, slot, clipRevOf(id)), false),
89
+ numpadUnassign: (slot) => window.editorApi.numpadUnassign(slot),
90
+ // The editor's title INPUT is authoritative (it rides every commit); a menu
91
+ // rename would be reverted by the next commit if it only touched the clip.
92
+ // Set the input + commit through the session instead — one write path.
93
+ setClipTitle: (_id, title) => { if (editor) { editor.setTitle(title); editor.commit(); } },
94
+ // The clip menu's "Revert to original": the editor's own revert (one path).
95
+ revertClip: () => { if (editor) editor.revert(); },
96
+ // Delete closes the window; main suppresses the close-commit so the draft
97
+ // can't resurrect the clip.
98
+ deleteClips: async (ids) => revGuard(async () => { await window.editorApi.deleteSelf(clipRevOf((ids && ids[0]) || menuClipId)); return []; }, []),
99
+ // Commit-on-add for the tag strip's + on a new note: force-commit the draft
100
+ // through the ONE existing write path, then re-query the fresh clip id.
101
+ ensureClipId: async () => {
102
+ if (editor) await Promise.resolve(editor.commit());
103
+ const id = await window.editorApi.currentId();
104
+ if (id) await refreshTags(); // picker needs the fresh item in menuState
105
+ return id || null;
106
+ },
107
+ toast: (message) => showToast(message),
108
+ render: () => {},
109
+ refresh: async () => { await refreshTags(); },
110
+ };
111
+ const menuController = Core.createClipController(menuAdapter);
112
+ document.addEventListener('click', (e) => menuController.onClick(e));
113
+ async function openClipMenu(x, y) {
114
+ menuClipId = await window.editorApi.currentId();
115
+ if (!menuClipId) return; // new note not committed yet — no clip to act on
116
+ menuState = await window.editorApi.state(menuClipId);
117
+ menuController.openClipMenu(menuClipId, x, y);
118
+ }
119
+
120
+ window.editorApi.onInit((init) => {
121
+ themeMode = (init.themeMode === 'light' || init.themeMode === 'dark') ? init.themeMode : 'system';
122
+ refreshTheme();
123
+ // The appearance payload (this window's surface, the accent colour, density,
124
+ // corners), through the one applier every window uses.
125
+ Core.applyAppearance(document.documentElement, init);
126
+ // The OS's own window controls: the bar reserves their room and reports its
127
+ // colours, so the caption buttons match it (main's setTitleBarOverlay).
128
+ if (!windowControls) windowControls = Core.attachWindowControls(init.windowControls, window.editorApi.windowChrome);
129
+ host.innerHTML = '';
130
+ reconcileView = null;
131
+ editor = Core.createEditor({
132
+ initialText: init.text || '',
133
+ initialTitle: init.noteTitle || '',
134
+ initialFind: init.find || '',
135
+ initialFindRegex: init.findRegex,
136
+ findPrefs: init.findPrefs,
137
+ saveFindPrefs: (prefs) => window.editorApi.saveFindPrefs && window.editorApi.saveFindPrefs(prefs),
138
+ initialFocusTitle: !!init.focusTitle,
139
+ nativeControls: true,
140
+ windowDrag: window.editorApi.windowDrag,
141
+ toastEl: document.getElementById('toast'), // Revert to original offers Undo here
142
+ onInput: (payload) => window.editorApi.draft(payload), // live crash-safe draft, every keystroke
143
+ // idle / Ctrl+S / close -> write the clip. The commit changes the content-
144
+ // addressed id, so refresh the title-bar tag strip once the write lands
145
+ // (returned promise also lets the strip's commit-on-add await the write).
146
+ onCommit: (payload) => window.editorApi.commit(payload).then((r) => { refreshTags(); return r; }),
147
+ onClose: () => window.editorApi.close(),
148
+ // While the clipboard holds this note, every save also updates it (main's
149
+ // clipboard follower); the footer shows that, or a Copy button to start it.
150
+ clipboard: window.editorApi.copyToClipboard ? {
151
+ following: !!init.clipboardFollowing,
152
+ onCopy: (payload) => window.editorApi.copyToClipboard(payload),
153
+ } : null,
154
+ // "..." in the editor chrome -> the shared clip menu (existing clips only;
155
+ // a brand-new note has no clip until its first commit).
156
+ ...(init.isNew ? {} : { onMenu: (x, y) => openClipMenu(x, y) }),
157
+ });
158
+ host.appendChild(editor.el);
159
+ windowControls.refresh();
160
+ editor.focus();
161
+ refreshTags();
162
+ });
163
+
164
+ // Clipboard follow: main pushes start/stop; regaining focus asks main to
165
+ // re-check (the user may have copied this note, or something else, meanwhile).
166
+ if (window.editorApi.onClipboardState) window.editorApi.onClipboardState((state) => { if (editor) editor.setClipboardState(state); });
167
+ window.addEventListener('focus', () => { if (editor && window.editorApi.focused) window.editorApi.focused(); });
168
+ // Right-click in the editor chrome (bar/foot — not the text area, where the
169
+ // native text menu belongs) opens the same clip menu.
170
+ document.addEventListener('contextmenu', (e) => {
171
+ if (e.target.closest('.bc-editor-area, .bc-bar-title, .bc-find-input, input, textarea')) return;
172
+ if (!editor || reconcileView) return;
173
+ e.preventDefault();
174
+ openClipMenu(e.clientX, e.clientY);
175
+ });
176
+
177
+ if (window.editorApi.onFind) {
178
+ window.editorApi.onFind((find) => {
179
+ if (!editor || !find) return;
180
+ if (find.focusTitle && editor.focusTitle) editor.focusTitle();
181
+ else if (find.query) editor.openFind(find.query, find.findRegex);
182
+ });
183
+ }
184
+
185
+ if (window.editorApi.onConflict) {
186
+ // Mounts the SHARED reconciliation view for both a sync conflict (resolve +
187
+ // close) and a Unify session (record.unify: fold each step, only close on the
188
+ // final merge). One view, two drivers.
189
+ function mountReconcile(record) {
190
+ host.innerHTML = '';
191
+ editor = null;
192
+ reconcileView = Core.createReconciliationView({
193
+ record,
194
+ windowDrag: window.editorApi.windowDrag,
195
+ onResolve: async (resolution) => {
196
+ if (record.unify && window.editorApi.unifyStep) {
197
+ const next = await window.editorApi.unifyStep(resolution);
198
+ if (next && next.done === false && next.nextRecord) { mountReconcile(next.nextRecord); return; }
199
+ window.editorApi.close();
200
+ return;
201
+ }
202
+ await window.editorApi.resolveConflict(resolution);
203
+ window.editorApi.close();
204
+ },
205
+ onClose: () => window.editorApi.close(),
206
+ nativeControls: true,
207
+ });
208
+ host.appendChild(reconcileView.el);
209
+ if (windowControls) windowControls.refresh();
210
+ }
211
+ window.editorApi.onConflict((record) => {
212
+ if (!record || !record.id) return;
213
+ mountReconcile(record);
214
+ });
215
+ }
216
+
217
+ // Commit any pending edit if the window is closing out from under us.
218
+ window.addEventListener('beforeunload', () => { if (editor) editor.commit(); });
219
+ </script>
220
+ </body></html>
package/icon.png ADDED
Binary file
package/icon@2x.png ADDED
Binary file
Binary file
Binary file