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:
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user