Initial scaffold for kestrove-desktop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
+106
@@ -0,0 +1,106 @@
|
||||
// Minimal desktop UI. All vault logic runs in Rust (src-tauri → kv-ffi); this layer only renders.
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
interface ItemSummary { id: string; title: string; username: string | null; urls: string[]; favorite: boolean }
|
||||
interface ItemDto { id: string; title: string; username: string | null; password: string | null; urls: string[]; totp: string | null; notes: string | null }
|
||||
|
||||
const app = document.getElementById("app")!;
|
||||
|
||||
function el<K extends keyof HTMLElementTagNameMap>(tag: K, props: Record<string, unknown> = {}, ...children: (Node | string)[]): HTMLElementTagNameMap[K] {
|
||||
const e: HTMLElementTagNameMap[K] = Object.assign(document.createElement(tag), props);
|
||||
e.append(...children);
|
||||
return e;
|
||||
}
|
||||
|
||||
async function render() {
|
||||
const state = await invoke<{ has_account: boolean; locked: boolean }>("state");
|
||||
app.replaceChildren();
|
||||
if (!state.has_account) return renderCreate();
|
||||
if (state.locked) return renderUnlock();
|
||||
return renderVault();
|
||||
}
|
||||
|
||||
function renderCreate() {
|
||||
const pw = el("input", { type: "password", placeholder: "Master password (10+ chars)" });
|
||||
const err = el("div", { className: "err" });
|
||||
const btn = el("button", { textContent: "Create vault" });
|
||||
const card = el("div", { className: "card" }, el("h2", { textContent: "Create your Kestrove vault" }), pw, btn, err);
|
||||
btn.onclick = async () => {
|
||||
try {
|
||||
const secretKey = await invoke<string>("create_account", { masterPassword: pw.value });
|
||||
card.replaceChildren(
|
||||
el("h2", { textContent: "Your Secret Key" }),
|
||||
el("p", { className: "mono", textContent: secretKey }),
|
||||
el("p", { className: "muted", textContent: "Write this down in your Emergency Kit. You need it to add devices." }),
|
||||
el("button", { textContent: "Continue", onclick: render }),
|
||||
);
|
||||
} catch (e) {
|
||||
err.textContent = String(e);
|
||||
}
|
||||
};
|
||||
app.append(el("div", { className: "center" }, card));
|
||||
}
|
||||
|
||||
function renderUnlock() {
|
||||
const pw = el("input", { type: "password", placeholder: "Master password" });
|
||||
const sk = el("input", { placeholder: "Secret Key (K1-…)", className: "mono" });
|
||||
const err = el("div", { className: "err" });
|
||||
const btn = el("button", { textContent: "Unlock" });
|
||||
btn.onclick = async () => {
|
||||
try {
|
||||
await invoke("unlock", { masterPassword: pw.value, secretKey: sk.value });
|
||||
pw.value = "";
|
||||
await render();
|
||||
} catch (e) {
|
||||
err.textContent = String(e);
|
||||
}
|
||||
};
|
||||
app.append(el("div", { className: "center" }, el("div", { className: "card" }, el("h2", { textContent: "Kestrove is locked" }), pw, sk, btn, err)));
|
||||
}
|
||||
|
||||
async function renderVault() {
|
||||
const items = await invoke<ItemSummary[]>("list_items");
|
||||
const detail = el("section");
|
||||
const list = el("ul");
|
||||
for (const it of items) {
|
||||
const li = el("li", {}, el("div", { textContent: it.title }), el("div", { className: "muted", textContent: it.username ?? "" }));
|
||||
li.onclick = async () => {
|
||||
list.querySelectorAll("li").forEach((n) => n.classList.remove("sel"));
|
||||
li.classList.add("sel");
|
||||
const item = await invoke<ItemDto>("get_item", { id: it.id });
|
||||
const pwSpan = el("span", { className: "mono", textContent: "••••••••" });
|
||||
detail.replaceChildren(
|
||||
el("h2", { textContent: item.title }),
|
||||
el("p", {}, "Username: ", el("span", { className: "mono", textContent: item.username ?? "" })),
|
||||
el("p", {}, "Password: ", pwSpan, " ",
|
||||
el("button", { textContent: "Reveal", onclick: () => (pwSpan.textContent = item.password ?? "") }),
|
||||
" ",
|
||||
el("button", { textContent: "Copy", onclick: () => invoke("copy_secret", { id: item.id }) })),
|
||||
el("p", { className: "muted", textContent: item.urls.join(", ") }),
|
||||
);
|
||||
};
|
||||
list.append(li);
|
||||
}
|
||||
const add = el("button", { textContent: "+ New login" });
|
||||
add.onclick = () => renderEditor(detail);
|
||||
const lock = el("button", { textContent: "Lock", onclick: async () => { await invoke("lock"); await render(); } });
|
||||
app.append(el("aside", {}, el("div", { className: "toolbar" }, add, lock), list), detail);
|
||||
}
|
||||
|
||||
function renderEditor(target: HTMLElement) {
|
||||
const title = el("input", { placeholder: "Title" });
|
||||
const user = el("input", { placeholder: "Username" });
|
||||
const pass = el("input", { placeholder: "Password", className: "mono" });
|
||||
const url = el("input", { placeholder: "https://example.com" });
|
||||
const gen = el("button", { textContent: "Generate", onclick: async () => (pass.value = await invoke<string>("generate_password", { length: 24 })) });
|
||||
const save = el("button", { textContent: "Save" });
|
||||
save.onclick = async () => {
|
||||
await invoke("save_item", {
|
||||
item: { title: title.value, username: user.value || null, password: pass.value || null, urls: url.value ? [url.value] : [] },
|
||||
});
|
||||
await render();
|
||||
};
|
||||
target.replaceChildren(el("div", { className: "card" }, el("h2", { textContent: "New login" }), title, user, pass, gen, url, save));
|
||||
}
|
||||
|
||||
void render();
|
||||
@@ -0,0 +1,16 @@
|
||||
:root { color-scheme: light dark; font: 14px system-ui, sans-serif; }
|
||||
body { margin: 0; }
|
||||
main { display: grid; grid-template-columns: 300px 1fr; height: 100vh; }
|
||||
.center { display: grid; place-items: center; height: 100vh; grid-column: 1 / -1; }
|
||||
.card { display: grid; gap: 8px; width: 340px; }
|
||||
input, button { font: inherit; padding: 8px; border-radius: 6px; border: 1px solid #8886; }
|
||||
button { cursor: pointer; }
|
||||
aside { border-right: 1px solid #8884; overflow: auto; }
|
||||
aside li { list-style: none; padding: 8px 12px; cursor: pointer; }
|
||||
aside li:hover, aside li.sel { background: #8882; }
|
||||
aside ul { margin: 0; padding: 0; }
|
||||
section { padding: 16px; }
|
||||
.muted { opacity: .7; }
|
||||
.err { color: #d33; }
|
||||
.mono { font-family: ui-monospace, monospace; }
|
||||
.toolbar { display: flex; gap: 8px; padding: 8px; }
|
||||
Reference in New Issue
Block a user