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 {
|
||||
|
||||
+113
-40
@@ -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; }
|
||||
Reference in new issue
Block a user