Commit 6b2507b8 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(design): shared screen parts, emoji-free strings, printed chess board

- core/ui.js: screenHead/wireBack, emptyState, loadingRows, errorState, avatar,
  esc, so every redesigned screen is built from the same furniture.
- i18n: 44 strings carried emoji or check-mark glyphs; they now read as words.
- Chess board dark squares move to a mid-tone printed indigo so both piece
  colours read on them.
- tools/shoot.mjs can push any scene with params.
Co-Authored-By: 's avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 7119c142
...@@ -334,6 +334,8 @@ input, button, textarea, select { font-family: inherit; } ...@@ -334,6 +334,8 @@ input, button, textarea, select { font-family: inherit; }
min-height: 52px; min-height: 52px;
margin-bottom: var(--s-4); margin-bottom: var(--s-4);
} }
.screen-head-text { flex: 1; min-width: 0; }
.screen-head-actions { display: flex; align-items: center; gap: var(--s-2); margin-inline-start: auto; }
.screen-title { .screen-title {
font-size: var(--fs-xl); font-size: var(--fs-xl);
font-weight: 800; font-weight: 800;
......
...@@ -98,9 +98,9 @@ ...@@ -98,9 +98,9 @@
/* Boards are printed on card stock in the same inks. */ /* Boards are printed on card stock in the same inks. */
--board-light: #EFEADF; --board-light: #EFEADF;
--board-dark: #3A4A9C; --board-dark: #7684BF;
--board-highlight-light: #F2D58A; --board-highlight-light: #F3DC98;
--board-highlight-dark: #C9A24A; --board-highlight-dark: #C7A65A;
--board-check: #C41E2E; --board-check: #C41E2E;
--board-frame: #0B1035; --board-frame: #0B1035;
......
This diff is collapsed.
// Shared screen furniture for The Cafe Deck. Scenes build from these so every
// header, empty table and loading state reads the same way.
import * as scene from './scene.js';
import * as audio from './audio.js';
import { t } from './i18n.js';
import { icon } from './icons.js';
/** Escape text before it goes into a template string. */
export function esc(s) {
return String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
/**
* A screen header. The back button sits on the reading side and points back
* the way you came; actions sit on the far side.
*/
export function screenHead(title, { back = true, sub = '', actions = '' } = {}) {
return `
<header class="screen-head">
${back ? `<button class="icon-btn" data-back aria-label="${t('common.back')}">${icon('back', 20)}</button>` : ''}
<div class="screen-head-text">
<h1 class="screen-title">${title}</h1>
${sub ? `<p class="screen-sub">${sub}</p>` : ''}
</div>
${actions ? `<div class="screen-head-actions">${actions}</div>` : ''}
</header>`;
}
/** Wire every [data-back] inside root to pop the scene (or run onBack). */
export function wireBack(root, onBack) {
root.querySelectorAll('[data-back]').forEach(b => b.addEventListener('click', () => {
audio.play('click');
if (onBack) onBack(); else scene.pop();
}));
}
/** An empty table: a face-down card and one honest line. */
export function emptyState({ mark = 'cards', title, sub = '', action = '' } = {}) {
return `
<div class="empty">
<div class="deck-back">${icon(mark, 26)}</div>
${title ? `<div class="empty-title">${title}</div>` : ''}
${sub ? `<div class="empty-sub">${sub}</div>` : ''}
${action}
</div>`;
}
/** Placeholder rows while a list loads. */
export function loadingRows(n = 4, height = 60) {
return `<div class="rows" aria-busy="true" aria-label="${t('common.loading')}">${
Array.from({ length: n }, () => `<div class="skeleton" style="height:${height}px;margin-bottom:8px;"></div>`).join('')
}</div>`;
}
/** An error line with a retry button. */
export function errorState(message, retryId = 'retry-btn') {
return `
<div class="empty">
<div class="deck-back">${icon('alert', 24)}</div>
<div class="empty-title">${message || t('common.error_load')}</div>
<button class="btn btn-secondary" id="${retryId}">${icon('refresh', 18)}${t('common.retry')}</button>
</div>`;
}
/** A player's avatar: their picture, or their initial printed on the back. */
export function avatar(player, size = 40) {
const name = player?.display_name || player?.username || '';
const inner = player?.avatar_url
? `<img src="${esc(player.avatar_url)}" alt="">`
: (name ? esc(name.trim().charAt(0)) : icon('user', Math.round(size * 0.5)));
return `<span class="avatar" style="width:${size}px;height:${size}px;font-size:${Math.round(size * 0.42)}px;">${inner}</span>`;
}
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
* *
* node tools/shoot.mjs --out <dir> [--base <url>] [--width 390 --height 844] * node tools/shoot.mjs --out <dir> [--base <url>] [--width 390 --height 844]
* [--only play,rank] [--guest] [--raw-theme] * [--only play,rank] [--guest] [--raw-theme]
* node tools/shoot.mjs --out <dir> --push chess-result --params '{"result":"win"}' [--name file]
* *
* Creates one account through the real auth endpoint, gives it a name and a * Creates one account through the real auth endpoint, gives it a name and a
* few ratings so screens have something to lay out, and deletes it at the end * few ratings so screens have something to lay out, and deletes it at the end
...@@ -25,6 +26,10 @@ const W = +argOf('--width', 390), H = +argOf('--height', 844); ...@@ -25,6 +26,10 @@ const W = +argOf('--width', 390), H = +argOf('--height', 844);
const ONLY = (argOf('--only', '') || '').split(',').filter(Boolean); const ONLY = (argOf('--only', '') || '').split(',').filter(Boolean);
const GUEST = args.includes('--guest'); const GUEST = args.includes('--guest');
const RAW_THEME = args.includes('--raw-theme'); const RAW_THEME = args.includes('--raw-theme');
const PUSH = argOf('--push', '');
const PARAMS = argOf('--params', '{}');
const NAME = argOf('--name', PUSH);
const SETTLE = +argOf('--settle', 1500);
const SB = 'https://safe-supabase-kong.caprover.al-arcade.com'; const SB = 'https://safe-supabase-kong.caprover.al-arcade.com';
const SK = process.env.SUPABASE_SERVICE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4'; const SK = process.env.SUPABASE_SERVICE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4';
...@@ -143,8 +148,9 @@ try { ...@@ -143,8 +148,9 @@ try {
await page.goto(BASE + '/', { waitUntil: 'networkidle2', timeout: 60000 }); await page.goto(BASE + '/', { waitUntil: 'networkidle2', timeout: 60000 });
await new Promise(r => setTimeout(r, 2000)); await new Promise(r => setTimeout(r, 2000));
for (const s of SCENES) { const list = PUSH ? [{ id: NAME, push: [PUSH, JSON.parse(PARAMS)], settle: SETTLE }] : SCENES;
if (ONLY.length && !ONLY.includes(s.id)) continue; for (const s of list) {
if (!PUSH && ONLY.length && !ONLY.includes(s.id)) continue;
try { try {
await goTo(page, s); await goTo(page, s);
await page.screenshot({ path: path.join(OUT, `${s.id}.png`) }); await page.screenshot({ path: path.join(OUT, `${s.id}.png`) });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment