Initial scaffold for kestrove-desktop

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-15 18:30:14 +09:00
co-authored by Claude Opus 5.5
commit d2cbd56ff7
19 changed files with 1802 additions and 0 deletions
+106
View File
@@ -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();
+16
View File
@@ -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; }