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:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 03:43:53 +03:00
1 parent 51a7e77e56
commit 9ee3e1ff6e
27 files changed
+2399 -508

No files matched your search

+52 -2
View File
@@ -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 {
+113 -40
View File
@@ -69,22 +69,33 @@
.pg-quick {
position: absolute;
bottom: 0.4em;
bottom: 0.35em;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 0.2em;
gap: 0.15em;
padding: 0.1em 0.3em;
border-radius: 99em;
background: rgba(8, 10, 20, 0.45);
backdrop-filter: blur(6px);
opacity: 0.55;
transition: opacity 0.2s;
}
.pg-quick:hover, .pg-quick:focus-within { opacity: 1; }
.pg-quick button {
background: none;
border: 0;
color: rgba(255, 255, 255, 0.55);
border-radius: 99em;
color: rgba(255, 255, 255, 0.8);
font: inherit;
font-size: calc(var(--pg-font) * 0.55);
padding: 0.2em 0.6em;
padding: 0.25em 0.75em;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.pg-quick button:hover, .pg-quick button.pg-on { color: var(--pg-accent); }
.pg-quick button:hover { background: rgba(255, 255, 255, 0.12); color: #fff; }
.pg-quick button.pg-on { color: #1a1205; background: var(--pg-accent); }
.pg-quick button:focus-visible { outline: 2px solid var(--pg-accent); }
.pg-menu {
position: absolute;
@@ -147,37 +158,50 @@
.pg-screen {
position: absolute;
inset: 0;
background: rgba(8, 10, 20, 0.94) center / cover;
background: rgba(8, 10, 20, 0.88) center / cover;
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
padding: 3% 6%;
font-size: calc(var(--pg-font) * 0.85);
z-index: 10;
animation: pg-fade 0.15s ease-out;
}
.pg-screen-head { display: flex; justify-content: space-between; align-items: center; }
.pg-screen-head h2 { margin: 0 0 0.6em; color: var(--pg-accent); font-weight: 600; }
.pg-screen button {
@keyframes pg-fade { from { opacity: 0; } }
.pg-screen-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.4em; }
.pg-screen-head h2 { margin: 0 0 0.3em; color: #fff; font-weight: 650; font-size: 1.3em; letter-spacing: -0.01em; }
.pg-screen-head h2::before { content: ""; display: inline-block; width: 0.2em; height: 0.9em; margin-right: 0.5em; border-radius: 99em; background: var(--pg-accent); vertical-align: -0.1em; }
.pg-screen button, .pg-dialog button, .pg-screen select {
font: inherit;
color: #fff;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 0.3em;
padding: 0.4em 1em;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 0.5em;
padding: 0.5em 1.1em;
cursor: pointer;
transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.pg-screen button:hover:not(:disabled) { border-color: var(--pg-accent); color: var(--pg-accent); }
.pg-screen button:disabled { opacity: 0.4; cursor: default; }
.pg-close { background: none !important; border: 0 !important; font-size: 1.3em !important; }
.pg-screen-body { flex: 1; overflow: auto; }
.pg-menu-list { display: flex; flex-direction: column; gap: 0.5em; max-width: 30%; }
.pg-history { display: flex; flex-direction: column; gap: 0.6em; max-height: 100%; overflow: auto; }
.pg-screen button:hover:not(:disabled), .pg-dialog button:hover { background: rgba(255, 255, 255, 0.15); border-color: var(--pg-accent); }
.pg-screen button:focus-visible, .pg-dialog button:focus-visible, .pg-screen select:focus-visible, .pg-hud-btn:focus-visible {
outline: 3px solid var(--pg-accent);
outline-offset: 2px;
}
.pg-screen button:disabled { opacity: 0.35; cursor: default; }
.pg-screen option { color: #111; }
.pg-close { background: rgba(255, 255, 255, 0.08) !important; width: 2.2em; height: 2.2em; padding: 0 !important; border-radius: 50% !important; font-size: 1em !important; }
.pg-screen-body { flex: 1; overflow: auto; padding: 0.2em; }
.pg-menu-list { display: flex; flex-direction: column; gap: 0.5em; max-width: min(24em, 100%); }
.pg-menu-list button { text-align: left; padding: 0.7em 1.2em; }
.pg-history { display: flex; flex-direction: column; gap: 0.8em; max-height: 100%; overflow: auto; max-width: 52em; }
.pg-history-row { padding: 0.5em 0.8em; border-left: 3px solid rgba(255, 255, 255, 0.12); }
.pg-history-row b { color: var(--pg-accent); margin-right: 0.6em; }
.pg-slots { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.6em; }
.pg-slot { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25em; text-align: left; min-height: 5em; }
.pg-slot-title { font-size: 0.8em; opacity: 0.85; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.pg-slot small { opacity: 0.5; font-size: 0.65em; }
.pg-setting { display: grid; grid-template-columns: 30% 1fr 9em; align-items: center; gap: 1em; margin-bottom: 0.7em; }
.pg-setting input { width: 100%; accent-color: var(--pg-accent); }
.pg-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(11em, 1fr)); gap: 0.7em; }
.pg-slot { display: flex; flex-direction: column; align-items: flex-start; gap: 0.25em; text-align: left; min-height: 6em; }
.pg-slot b { color: var(--pg-accent); font-weight: 600; }
.pg-slot-title { font-size: 0.85em; opacity: 0.9; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.pg-slot small { opacity: 0.55; font-size: 0.7em; margin-top: auto; }
.pg-setting { display: grid; grid-template-columns: minmax(8em, 30%) 1fr 9em; align-items: center; gap: 1em; margin-bottom: 0.8em; }
.pg-setting input { width: 100%; accent-color: var(--pg-accent); min-height: 1.6em; }
.pg-setting-val { opacity: 0.7; }
.pg-wide { margin-top: 0.8em; margin-right: 0.6em; }
@@ -185,22 +209,71 @@
.pg-main-panel {
display: flex;
flex-direction: column;
gap: 0.5em;
width: 32%;
gap: 0.55em;
width: min(26em, 100%);
padding: 2em;
background: rgba(0, 0, 0, 0.6);
border-radius: 0.5em;
background: rgba(8, 10, 20, 0.72);
backdrop-filter: blur(14px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 1em;
box-shadow: 0 1em 3em rgba(0, 0, 0, 0.5);
}
.pg-main-panel h1 { margin: 0 0 0.5em; font-size: 1.6em; color: var(--pg-accent); }
.pg-main-panel button { text-align: left; }
.pg-select {
font: inherit;
font-size: 0.85em;
.pg-main-panel h1 { margin: 0 0 0.5em; font-size: 1.6em; color: #fff; letter-spacing: -0.01em; }
.pg-main-panel button { text-align: left; padding: 0.7em 1.2em; }
.pg-main-panel button:first-of-type { background: var(--pg-accent); border-color: var(--pg-accent); color: #1a1205; font-weight: 650; }
.pg-main-panel button:first-of-type:hover { filter: brightness(1.1); }
/* menu button outside the scaled stage: stays tappable on phones */
.pg-hud-btn {
position: absolute;
top: max(10px, env(safe-area-inset-top));
right: max(10px, env(safe-area-inset-right));
z-index: 20;
width: 44px;
height: 44px;
display: grid;
place-items: center;
font-size: 22px;
line-height: 1;
color: #fff;
background: rgba(255, 255, 255, 0.07);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 0.3em;
padding: 0.3em 0.6em;
background: rgba(8, 10, 20, 0.55);
backdrop-filter: blur(6px);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 50%;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s, background 0.15s;
}
.pg-select option { background: #151827; }
.pg-lang { margin-top: 0.6em; }
.pg-root:hover .pg-hud-btn, .pg-hud-btn:focus-visible { opacity: 0.8; }
.pg-hud-btn:hover { opacity: 1 !important; background: rgba(8, 10, 20, 0.85); }
.pg-root.pg-in-main .pg-hud-btn { display: none; }
@media (pointer: coarse) {
.pg-hud-btn { opacity: 0.6; }
}
/* in-player confirmation */
.pg-dialog-back {
position: absolute;
inset: 0;
z-index: 30;
display: grid;
place-items: center;
padding: 16px;
background: rgba(5, 7, 12, 0.7);
backdrop-filter: blur(4px);
font-family: "Segoe UI", system-ui, sans-serif;
}
.pg-dialog {
width: min(420px, 100%);
padding: 22px;
color: #eceef5;
background: #171b25;
border: 1px solid #2a3042;
border-radius: 14px;
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.55);
font-size: 16px;
}
.pg-dialog p { margin: 0 0 18px; line-height: 1.45; }
.pg-dialog-row { display: flex; justify-content: flex-end; gap: 10px; }
.pg-dialog button { font-size: 15px; padding: 9px 18px; }
.pg-dialog .pg-primary { background: #ff9f43; border-color: #ff9f43; color: #1a1205; font-weight: 650; }