UI: unified design system, light/dark themes, redesigned pages and player UI
Tokens and components in styles.css, shared Ui (dialogs, toasts), icons, bottom tab bar on mobile, keyboard/gamepad navigation, library shelf with filters and conversion badges, hero game page, restyled player screens. Games list now returns per-game version counts. Adds docs/UI_DESIGN.md. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
51a7e77e56
commit
9ee3e1ff6e
27 files changed
+2399
-508
No files matched your search
@@ -1149,6 +1149,18 @@ export class Engine {
|
||||
this.notifyEl = el("div", "pg-notify pg-hidden");
|
||||
stage.append(this.nvlEl, this.textbox, this.centeredEl, this.menuEl, this.screen, this.notifyEl);
|
||||
|
||||
// touch-friendly menu button outside the scaled stage
|
||||
const hud = el("button", "pg-hud-btn", "☰");
|
||||
hud.title = "Меню";
|
||||
hud.setAttribute("aria-label", "Меню");
|
||||
hud.onclick = (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (this.screen.classList.contains("pg-main")) return;
|
||||
if (this.screen.classList.contains("pg-hidden")) this.openGameMenu();
|
||||
else this.dismissScreen();
|
||||
};
|
||||
this.container.append(hud);
|
||||
|
||||
stage.addEventListener("click", (ev) => {
|
||||
if (!this.screen.classList.contains("pg-hidden")) return;
|
||||
if ((ev.target as HTMLElement).closest(".pg-menu, .pg-quick")) return;
|
||||
@@ -1169,6 +1181,37 @@ export class Engine {
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
/** In-player confirmation (the native confirm() breaks fullscreen and gamepads). */
|
||||
private confirmAsk(text: string, okLabel = "Ок"): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
const back = el("div", "pg-dialog-back");
|
||||
const box = el("div", "pg-dialog");
|
||||
box.setAttribute("role", "dialog");
|
||||
box.append(el("p", "", text));
|
||||
const row = el("div", "pg-dialog-row");
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key !== "Escape") return;
|
||||
e.stopImmediatePropagation();
|
||||
done(false);
|
||||
};
|
||||
const done = (v: boolean) => {
|
||||
window.removeEventListener("keydown", onKey, true);
|
||||
back.remove();
|
||||
resolve(v);
|
||||
};
|
||||
const no = el("button", "", "Отмена");
|
||||
no.onclick = () => done(false);
|
||||
const yes = el("button", "pg-primary", okLabel);
|
||||
yes.onclick = () => done(true);
|
||||
window.addEventListener("keydown", onKey, true);
|
||||
row.append(no, yes);
|
||||
box.append(row);
|
||||
back.append(box);
|
||||
this.container.append(back);
|
||||
yes.focus();
|
||||
});
|
||||
}
|
||||
|
||||
private advanceInput() {
|
||||
if (this.skip) {
|
||||
this.setSkip(false);
|
||||
@@ -1212,6 +1255,7 @@ export class Engine {
|
||||
private openScreen(title: string, build: (body: HTMLElement) => void, onClose?: () => void) {
|
||||
this.screen.textContent = "";
|
||||
this.screen.classList.remove("pg-main");
|
||||
this.container.classList.remove("pg-in-main");
|
||||
this.screen.style.backgroundImage = "";
|
||||
this.screenOnClose = onClose ?? null;
|
||||
const head = el("div", "pg-screen-head");
|
||||
@@ -1235,6 +1279,7 @@ export class Engine {
|
||||
this.screenOnClose = null;
|
||||
this.screen.classList.add("pg-hidden");
|
||||
this.screen.classList.remove("pg-main");
|
||||
this.container.classList.remove("pg-in-main");
|
||||
this.screen.style.backgroundImage = "";
|
||||
}
|
||||
|
||||
@@ -1248,6 +1293,7 @@ export class Engine {
|
||||
this.clearNvl();
|
||||
this.screen.textContent = "";
|
||||
this.screen.classList.add("pg-main");
|
||||
this.container.classList.add("pg-in-main");
|
||||
const bg = this.game.ui.mainMenu ? this.fileUrl(this.game.ui.mainMenu) : null;
|
||||
if (bg) this.screen.style.backgroundImage = `url("${bg}")`;
|
||||
const panel = el("div", "pg-main-panel");
|
||||
@@ -1302,11 +1348,15 @@ export class Engine {
|
||||
list.append(b);
|
||||
};
|
||||
add("Вернуться", () => this.closeScreen());
|
||||
add("История", () => this.openHistory());
|
||||
add(this.skip ? "Выключить пропуск" : "Пропуск", () => { this.closeScreen(); this.setSkip(!this.skip); });
|
||||
add(this.auto ? "Выключить авто" : "Автопрочтение", () => { this.closeScreen(); this.setAuto(!this.auto); });
|
||||
add("Сохранить", () => this.openSaves("save"));
|
||||
add("Загрузить", () => this.openSaves("load"));
|
||||
add("Настройки", () => this.openSettings());
|
||||
add("Главное меню", () => {
|
||||
if (confirm("Выйти в главное меню? Несохранённый прогресс будет потерян.")) this.showMainMenu();
|
||||
void this.confirmAsk("Выйти в главное меню? Несохранённый прогресс будет потерян.", "Выйти")
|
||||
.then((yes) => yes && this.showMainMenu());
|
||||
});
|
||||
add("В библиотеку", () => this.onExit());
|
||||
body.append(list);
|
||||
@@ -1346,7 +1396,7 @@ export class Engine {
|
||||
card.disabled = mode === "load" && !meta;
|
||||
card.onclick = async () => {
|
||||
if (mode === "save") {
|
||||
if (meta && !confirm("Перезаписать сохранение?")) return;
|
||||
if (meta && !(await this.confirmAsk("Перезаписать сохранение?", "Перезаписать"))) return;
|
||||
await this.save(slot);
|
||||
render(await this.saves.list());
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user