Add user onboarding, keyboard overlay, and DB support

Introduce first-time user onboarding and a navigable on-screen keyboard, plus persistent user storage via a bundled rusqlite database and Tauri API. Backend: add rusqlite dependency, DB initialization in tauri setup, schema migration/backfill for users, and two Tauri commands (get_first_user, create_user) with input sanitization and structured UserRecord. Frontend: add core/users.js (invoke + localStorage fallback), integrate user state initialization, add user setup view/styles and keyboard overlay (JS/CSS), wire views and navigation to show onboarding when needed, and update lock view behavior to coordinate onboarding and passkey flow. Also add @tauri-apps/api dependency and update package/Cargo lock files accordingly.
This commit is contained in:
2026-04-08 21:39:18 +12:00
parent c890636f03
commit f41768c6a9
15 changed files with 1022 additions and 13 deletions
+175
View File
@@ -0,0 +1,175 @@
const KEY_ROWS = [
["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"],
["K", "L", "M", "N", "O", "P", "Q", "R", "S", "T"],
["U", "V", "W", "X", "Y", "Z", "-", "'", ".", ","],
["SPACE", "BACK", "CLR", "DONE"],
];
const KEYBOARD_TEMPLATE = `
<section class="overlay-keyboard" data-overlay-keyboard hidden>
<div class="overlay-keyboard-inner">
<div class="overlay-keyboard-grid" data-overlay-keyboard-grid></div>
</div>
</section>
`;
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
export const createKeyboardOverlay = ({ mountRoot }) => {
mountRoot.innerHTML = KEYBOARD_TEMPLATE;
const root = mountRoot.querySelector("[data-overlay-keyboard]");
const grid = mountRoot.querySelector("[data-overlay-keyboard-grid]");
const keyButtons = [];
let openState = false;
let selectedRow = 0;
let selectedCol = 0;
let handlers = null;
const applySelection = () => {
keyButtons.forEach((button) => button.classList.remove("is-focused"));
const selected = keyButtons.find(
(button) =>
Number(button.dataset.row) === selectedRow && Number(button.dataset.col) === selectedCol,
);
selected?.classList.add("is-focused");
};
const currentKey = () => {
const selected = keyButtons.find(
(button) =>
Number(button.dataset.row) === selectedRow && Number(button.dataset.col) === selectedCol,
);
return selected?.dataset.key ?? null;
};
const build = () => {
if (!grid) {
return;
}
grid.innerHTML = "";
keyButtons.length = 0;
KEY_ROWS.forEach((row, rowIndex) => {
row.forEach((key, colIndex) => {
const button = document.createElement("button");
button.type = "button";
button.className = "overlay-keyboard-key";
button.dataset.row = String(rowIndex);
button.dataset.col = String(colIndex);
button.dataset.key = key;
button.textContent = key === "SPACE" ? "Space" : key;
if (key === "DONE") {
button.classList.add("is-done");
}
if (key === "SPACE") {
button.classList.add("is-wide");
}
grid.append(button);
keyButtons.push(button);
});
});
};
build();
const open = (nextHandlers = {}) => {
handlers = nextHandlers;
openState = true;
selectedRow = 0;
selectedCol = 0;
root.hidden = false;
applySelection();
};
const close = () => {
openState = false;
root.hidden = true;
handlers = null;
};
const move = (direction) => {
const rowWidth = KEY_ROWS[selectedRow]?.length ?? 0;
if (direction === "left") {
selectedCol = clamp(selectedCol - 1, 0, Math.max(0, rowWidth - 1));
return;
}
if (direction === "right") {
selectedCol = clamp(selectedCol + 1, 0, Math.max(0, rowWidth - 1));
return;
}
if (direction === "up") {
selectedRow = clamp(selectedRow - 1, 0, KEY_ROWS.length - 1);
const nextWidth = KEY_ROWS[selectedRow]?.length ?? 0;
selectedCol = clamp(selectedCol, 0, Math.max(0, nextWidth - 1));
return;
}
if (direction === "down") {
selectedRow = clamp(selectedRow + 1, 0, KEY_ROWS.length - 1);
const nextWidth = KEY_ROWS[selectedRow]?.length ?? 0;
selectedCol = clamp(selectedCol, 0, Math.max(0, nextWidth - 1));
}
};
const handleAction = (action) => {
if (!openState) {
return false;
}
if (action === "up" || action === "down" || action === "left" || action === "right") {
move(action);
applySelection();
return true;
}
if (action === "accept") {
const key = currentKey();
if (!key) {
return true;
}
if (key === "DONE") {
handlers?.onSubmit?.();
return true;
}
if (key === "BACK") {
handlers?.onBackspace?.();
return true;
}
if (key === "CLR") {
handlers?.onClear?.();
return true;
}
handlers?.onKey?.(key === "SPACE" ? " " : key);
return true;
}
if (action === "back") {
handlers?.onBackspace?.();
return true;
}
if (action === "menu") {
handlers?.onSubmit?.();
return true;
}
if (action === "l1") {
handlers?.onPrevField?.();
return true;
}
if (action === "r1") {
handlers?.onNextField?.();
return true;
}
return false;
};
return {
open,
close,
isOpen: () => openState,
handleAction,
};
};