New subsystem for extending Theseus with folders on disk. Each add-on
lives at <userData>/addons/<id>/ with an addon.json manifest and a
CommonJS entry that exports activate(api). Nothing about a private
add-on ships in the public installer - drop the folder, restart, it's
live. Bundled reference add-ons ride in the packaged app under
resources/bundled-addons/ and are seeded into <userData>/addons/ on
first boot; the framework treats seeded and drop-in add-ons the same.
Files:
- addons-host.js Loader + api.registerSidebarPanel() + per-
addon storage on <userData>/addons-data/.
Kept at the CommonJS-scoped top level (lib/
is ESM-scoped via its own package.json).
- sidebar-preload.js Runs in every sidebar panel. Exposes
window.silentmode.storage.{get,set,all} +
onVisibility. Main-side handlers derive the
add-on id from the sender file:// URL, so a
panel can only touch its own store.
- bundled-addons/notepad/ Reference add-on: addon.json, index.js,
note.html. Autosaving textarea with char /
word count.
main.js:
- Extension point: sidebar-panel. One right-anchored WebContentsView
(SIDEBAR_W=340) hosts the current panel; layout() shrinks the tab
views by the sidebar width when visible. First registered panel
wins for MVP; picker for multiple panels lands later.
- initAddons() at app.whenReady(): seedBundledAddons, then
AddonHost.discoverAndActivate.
- IPC surface: sidebar-toggle / sidebar-open / sidebar-close /
sidebar-state, addons-list / addons-set-enabled / addons-reveal /
addons-open-dir / addons-reload, and origin-gated
addon-storage-get/set/all.
- Settings gains `disabledAddons: []` — off-toggled ids persist and
the loader honours them without a restart (discoverAndActivate
runs again on toggle).
chrome.html: toolbar sidebar-toggle button, hidden until at least one
add-on has registered a sidebar panel.
settings.html: new "Add-ons" section under privacy. Lists installed
add-ons with icon / name / version / description / capabilities;
per-add-on enable/disable toggle + Show folder button; page-level
Reload and Open add-ons folder buttons; warning note about the trust
model.
package.json: build.files gains sidebar-preload.js + addons-host.js.
extraResources gains bundled-addons/ so the packaged app carries the
reference notepad for the first-boot seed.
Verified: `npm start` boots, addons-host discovers the notepad,
activates it, registers one sidebar panel. Log confirms
"1 installed, 1 enabled, 1 sidebar panels". Actual sidebar rendering
+ notepad UI need clicked-through validation on a real install.
Not shipped yet - deploy still blocked on the fail2ban VPS SSH ban.
Ships as 0.2.0 once SSH clears (this is a new subsystem, not a fix).
100 lines
3.6 KiB
HTML
100 lines
3.6 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Notepad</title>
|
|
<style>
|
|
:root { color-scheme: light dark;
|
|
--bg:#0e131c; --panel:#141a24; --line:rgba(255,255,255,.09);
|
|
--ink:#e7eaf1; --mut:#8b98a9; --dim:#5e6678; --acid:#d6ff3d; }
|
|
@media (prefers-color-scheme: light) {
|
|
:root { --bg:#f8faff; --panel:#ffffff; --line:rgba(0,0,0,.10);
|
|
--ink:#1a1f2b; --mut:#5c6577; --dim:#8a93a5; }
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; height: 100%; }
|
|
body { background: var(--bg); color: var(--ink);
|
|
font: 14px/1.55 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
|
display: flex; flex-direction: column; }
|
|
header { display: flex; align-items: center; justify-content: space-between;
|
|
padding: 10px 14px; border-bottom: 1px solid var(--line);
|
|
background: var(--panel); }
|
|
header .t { font-weight: 600; letter-spacing: .2px; display: flex; gap: 8px; align-items: center; }
|
|
header .t .em { font-size: 15px; }
|
|
header .m { color: var(--dim); font-size: 11.5px; font-variant-numeric: tabular-nums; }
|
|
textarea {
|
|
flex: 1; width: 100%; padding: 14px 16px; border: none; outline: none; resize: none;
|
|
background: transparent; color: var(--ink);
|
|
font: 13.5px/1.65 ui-monospace, "Cascadia Code", Consolas, monospace;
|
|
}
|
|
textarea::placeholder { color: var(--dim); }
|
|
footer { padding: 6px 14px 10px; border-top: 1px solid var(--line);
|
|
color: var(--dim); font-size: 11px; display: flex; justify-content: space-between; }
|
|
footer .l { display: flex; gap: 10px; }
|
|
footer .l b { color: var(--mut); font-weight: 500; }
|
|
footer button { border: none; background: transparent; color: var(--mut);
|
|
cursor: pointer; font: inherit; padding: 0; }
|
|
footer button:hover { color: var(--acid); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<div class="t"><span class="em">📝</span> <span>Notepad</span></div>
|
|
<div class="m" id="status">saved</div>
|
|
</header>
|
|
<textarea id="pad" placeholder="Notes. Autosaves as you type."
|
|
spellcheck="false" autofocus></textarea>
|
|
<footer>
|
|
<div class="l"><b><span id="cchars">0</span></b> chars · <b><span id="cwords">0</span></b> words</div>
|
|
<button id="clear" title="Clear all notes">clear</button>
|
|
</footer>
|
|
|
|
<script>
|
|
const pad = document.getElementById("pad");
|
|
const status = document.getElementById("status");
|
|
const cchars = document.getElementById("cchars");
|
|
const cwords = document.getElementById("cwords");
|
|
|
|
function updateCounts() {
|
|
const v = pad.value;
|
|
cchars.textContent = v.length;
|
|
cwords.textContent = v.trim() ? v.trim().split(/\s+/).length : 0;
|
|
}
|
|
|
|
// Load previous text on open.
|
|
(async () => {
|
|
try {
|
|
const text = await window.silentmode.storage.get("text", "");
|
|
pad.value = text || "";
|
|
updateCounts();
|
|
} catch (e) { console.warn("load failed:", e); }
|
|
})();
|
|
|
|
// Debounced autosave.
|
|
let saveTimer = null;
|
|
pad.addEventListener("input", () => {
|
|
updateCounts();
|
|
status.textContent = "saving…";
|
|
clearTimeout(saveTimer);
|
|
saveTimer = setTimeout(async () => {
|
|
try {
|
|
await window.silentmode.storage.set("text", pad.value);
|
|
status.textContent = "saved";
|
|
} catch (e) {
|
|
status.textContent = "save failed";
|
|
console.warn("save failed:", e);
|
|
}
|
|
}, 400);
|
|
});
|
|
|
|
document.getElementById("clear").addEventListener("click", async () => {
|
|
if (!pad.value) return;
|
|
if (!confirm("Clear all notes?")) return;
|
|
pad.value = "";
|
|
updateCounts();
|
|
try { await window.silentmode.storage.set("text", ""); status.textContent = "saved"; } catch {}
|
|
pad.focus();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|