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; }
min-height: 52px;
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 {
font-size: var(--fs-xl);
font-weight: 800;
......
......@@ -98,9 +98,9 @@
/* Boards are printed on card stock in the same inks. */
--board-light: #EFEADF;
--board-dark: #3A4A9C;
--board-highlight-light: #F2D58A;
--board-highlight-dark: #C9A24A;
--board-dark: #7684BF;
--board-highlight-light: #F3DC98;
--board-highlight-dark: #C7A65A;
--board-check: #C41E2E;
--board-frame: #0B1035;
......
......@@ -52,7 +52,7 @@ const strings = {
'reaction.oops': 'أوبس',
'reaction.sorry': 'آسف!',
'reaction.thanks': 'شكرًا',
'reaction.hurry_please': 'دورك ⏱️',
'reaction.hurry_please': 'دورك',
'reaction.rematch': 'نعيدها؟',
'game.thinking': 'يفكر...',
'game.bot_unavailable': 'الخصم الآلي لا يستجيب. هل تريد إنهاء هذه المباراة؟',
......@@ -148,12 +148,12 @@ const strings = {
'common.back': 'رجوع',
'common.send': 'إرسال',
'common.sending': 'جاري الإرسال...',
'common.sent': '✓ تم الإرسال',
'common.sent': 'تم الإرسال',
'common.failed': 'فشل',
'common.already_sent': 'مرسل سابقاً',
'common.already_friends': 'صديق بالفعل',
'common.accepted': '✓ تم القبول',
'common.done': '✓ تم',
'common.accepted': 'تم القبول',
'common.done': 'تم',
'common.error_load': 'فشل التحميل',
'common.now': 'الآن',
'common.minutes_ago': 'منذ {n} دقيقة',
......@@ -170,8 +170,8 @@ const strings = {
'game.points': '+{n} نقطة',
'game.choose_placement': 'اختر مكان الوضع',
'game.opponent_plays': 'الخصم يلعب...',
'game.one_piece_warning': '⚠️ قطعة واحدة!',
'game.two_piece_warning': '⚠️ قطعتين!',
'game.one_piece_warning': 'قطعة واحدة!',
'game.two_piece_warning': 'قطعتين!',
'game.resign_confirm': 'هل تريد الاستسلام؟',
'game.resign_yes': 'نعم، استسلم',
'game.resign_no': 'تراجع',
......@@ -181,7 +181,7 @@ const strings = {
'game.vs_friend': 'تحدي صديق',
'game.roll_dice': 'ارمِ النرد',
'game.choose_piece': 'اختر قطعة',
'game.speed_mode': '⚡ وضع السرعة!',
'game.speed_mode': 'وضع السرعة!',
'game.turn_of': 'دور {name}',
'game.bot_thinking': '{name} يفكر...',
'game.rating': 'التصنيف',
......@@ -215,19 +215,19 @@ const strings = {
'match.disconnected_waiting': 'في انتظار عودته...',
'match.disconnected_auto_win': 'ستفوز تلقائياً إذا لم يعد خلال الوقت المحدد',
'match.claim_early_win': 'فوز مبكر',
'match.opponent_returned': '✅ الخصم عاد — استمروا!',
'match.opponent_returned': 'الخصم عاد — استمروا!',
'match.you_won': 'فزت!',
'match.opponent_left': 'الخصم غادر المباراة',
'match.return': 'العودة',
'match.connection_lost': 'انقطع الاتصال',
'match.reconnecting': 'جاري إعادة الاتصال...',
'match.reconnected': '✅ تم استعادة الاتصال',
'match.reconnected': 'تم استعادة الاتصال',
'match.restoring': 'جاري استعادة المباراة...',
'match.your_turn_warning': '⏱️ دورك! العب قبل انتهاء الوقت',
'match.your_turn_warning': 'دورك! العب قبل انتهاء الوقت',
'match.opponent_thinking': 'الخصم يفكر...',
'match.bot_replacement': '🤖 الخصم غادر — بوت يكمل بدلاً منه',
'match.your_time_expired': '⏱️ انتهى وقتك! تم التمرير تلقائياً',
'match.opponent_time_expired': '⏱️ انتهى وقت الخصم — دورك!',
'match.bot_replacement': 'الخصم غادر — بوت يكمل بدلاً منه',
'match.your_time_expired': 'انتهى وقتك! تم التمرير تلقائياً',
'match.opponent_time_expired': 'انتهى وقت الخصم — دورك!',
'tournament.round_ready': 'جولة {n} جاهزة!',
'tournament.tap_to_play': 'اضغط للعب مباراتك',
'tournament.play': 'العب',
......@@ -235,7 +235,7 @@ const strings = {
'mp.profile': 'الملف الشخصي',
'mp.add_friend': 'إضافة صديق',
'mp.report': 'إبلاغ',
'mp.reported': '✓ تم الإبلاغ',
'mp.reported': 'تم الإبلاغ',
'mp.block': 'حظر',
'social.friends_tab': 'الأصدقاء',
'social.pending_tab': 'الطلبات',
......@@ -285,8 +285,8 @@ const strings = {
'challenge.offline': 'غير متصلين',
'challenge.select_game': 'اختر اللعبة ونوع الوقت',
'challenge.send': 'أرسل التحدي',
'challenge.sending': '⏳ جاري الإرسال...',
'challenge.sent': '✓ تم إرسال التحدي!',
'challenge.sending': 'جاري الإرسال...',
'challenge.sent': 'تم إرسال التحدي!',
'challenge.failed_create': 'فشل إنشاء المباراة',
'challenge.failed_retry': 'فشل — حاول مرة أخرى',
'challenge.sent_msg': 'أرسل تحدي {game}',
......@@ -439,7 +439,7 @@ const strings = {
'review.brilliant': '!! رائعة',
'review.great': '! ممتازة',
'review.best': 'أفضل نقلة',
'review.good': '✓ جيدة',
'review.good': 'جيدة',
'review.book': 'نظرية',
'review.inaccuracy': '?! عدم دقة',
'review.mistake': '? خطأ',
......@@ -458,7 +458,7 @@ const strings = {
'history.draw': 'تعادل',
'history.insufficient_data': 'بيانات غير كافية',
'history.matches_count': '{n} مباراة',
'result.copied': '✓ تم النسخ',
'result.copied': 'تم النسخ',
'result.copy_pgn': 'نسخ',
'result.share_pgn': 'مشاركة PGN',
'result.analyze': 'تحليل المباراة',
......@@ -502,8 +502,8 @@ const strings = {
'profile.org_apply': 'الانضمام لمنظمة',
'profile.logout': 'خروج',
'profile.view_title': 'الملف الشخصي',
'profile.add_friend': '➕ إضافة صديق',
'profile.friend_sent': '✓ تم إرسال الطلب',
'profile.add_friend': 'إضافة صديق',
'profile.friend_sent': 'تم إرسال الطلب',
'profile.pending_request': 'طلب صداقة معلق',
'profile.challenge': 'تحدي',
'profile.message': 'مراسلة',
......@@ -571,7 +571,7 @@ const strings = {
'group.play_invite': 'دعوة لعب',
'group.players': 'لاعبين',
'group.join': 'انضم',
'group.accepted': '✓ قبلت',
'group.accepted': 'قبلت',
'group.invite_sent': 'تم إرسال دعوة اللعب!',
'group.create_title': 'إنشاء مجموعة',
'group.name_label': 'اسم المجموعة',
......@@ -661,7 +661,7 @@ const strings = {
'tournament.prizes': 'الجوائز',
'tournament.register_now': 'سجّل الآن',
'tournament.registering': 'جاري التسجيل...',
'tournament.registered': '✅ تم التسجيل',
'tournament.registered': 'تم التسجيل',
'tournament.player_col': 'اللاعب',
'tournament.points_col': 'نقاط',
'tournament.buchholz_col': 'بوخ',
......@@ -795,7 +795,7 @@ const strings = {
'reaction.oops': 'Oops',
'reaction.sorry': 'Sorry!',
'reaction.thanks': 'Thanks',
'reaction.hurry_please': 'Your turn ⏱️',
'reaction.hurry_please': 'Your turn',
'reaction.rematch': 'Rematch?',
'game.thinking': 'Thinking...',
'game.bot_unavailable': 'The computer opponent is not responding. End this game?',
......@@ -891,12 +891,12 @@ const strings = {
'common.back': 'Back',
'common.send': 'Send',
'common.sending': 'Sending...',
'common.sent': '✓ Sent',
'common.sent': 'Sent',
'common.failed': 'Failed',
'common.already_sent': 'Already sent',
'common.already_friends': 'Already friends',
'common.accepted': '✓ Accepted',
'common.done': '✓ Done',
'common.accepted': 'Accepted',
'common.done': 'Done',
'common.error_load': 'Failed to load',
'common.now': 'Now',
'common.minutes_ago': '{n}m ago',
......@@ -913,8 +913,8 @@ const strings = {
'game.points': '+{n} points',
'game.choose_placement': 'Choose placement',
'game.opponent_plays': 'Opponent playing...',
'game.one_piece_warning': '⚠️ One piece left!',
'game.two_piece_warning': '⚠️ Two pieces left!',
'game.one_piece_warning': 'One piece left!',
'game.two_piece_warning': 'Two pieces left!',
'game.resign_confirm': 'Do you want to resign?',
'game.resign_yes': 'Yes, resign',
'game.resign_no': 'Cancel',
......@@ -924,7 +924,7 @@ const strings = {
'game.vs_friend': 'Challenge Friend',
'game.roll_dice': 'Roll Dice',
'game.choose_piece': 'Choose piece',
'game.speed_mode': '⚡ Speed Mode!',
'game.speed_mode': 'Speed Mode!',
'game.turn_of': "{name}'s turn",
'game.bot_thinking': '{name} thinking...',
'game.rating': 'Rating',
......@@ -958,19 +958,19 @@ const strings = {
'match.disconnected_waiting': 'Waiting for them to return...',
'match.disconnected_auto_win': 'You will win automatically if they don\'t return in time',
'match.claim_early_win': 'Claim Win',
'match.opponent_returned': '✅ Opponent returned — continue!',
'match.opponent_returned': 'Opponent returned — continue!',
'match.you_won': 'You Won!',
'match.opponent_left': 'Opponent left the match',
'match.return': 'Return',
'match.connection_lost': 'Connection Lost',
'match.reconnecting': 'Reconnecting...',
'match.reconnected': '✅ Connection restored',
'match.reconnected': 'Connection restored',
'match.restoring': 'Restoring match...',
'match.your_turn_warning': '⏱️ Your turn! Play before time runs out',
'match.your_turn_warning': 'Your turn! Play before time runs out',
'match.opponent_thinking': 'Opponent thinking...',
'match.bot_replacement': '🤖 Opponent left — bot taking over',
'match.your_time_expired': '⏱️ Time expired! Auto-passed',
'match.opponent_time_expired': '⏱️ Opponent\'s time expired — your turn!',
'match.bot_replacement': 'Opponent left — bot taking over',
'match.your_time_expired': 'Time expired! Auto-passed',
'match.opponent_time_expired': 'Opponent\'s time expired — your turn!',
'tournament.round_ready': 'Round {n} ready!',
'tournament.tap_to_play': 'Tap to play your match',
'tournament.play': 'Play',
......@@ -978,7 +978,7 @@ const strings = {
'mp.profile': 'Profile',
'mp.add_friend': 'Add Friend',
'mp.report': 'Report',
'mp.reported': '✓ Reported',
'mp.reported': 'Reported',
'mp.block': 'Block',
'social.friends_tab': 'Friends',
'social.pending_tab': 'Requests',
......@@ -1028,8 +1028,8 @@ const strings = {
'challenge.offline': 'Offline',
'challenge.select_game': 'Choose game and time control',
'challenge.send': 'Send Challenge',
'challenge.sending': '⏳ Sending...',
'challenge.sent': '✓ Challenge sent!',
'challenge.sending': 'Sending...',
'challenge.sent': 'Challenge sent!',
'challenge.failed_create': 'Failed to create match',
'challenge.failed_retry': 'Failed — try again',
'challenge.sent_msg': 'Sent {game} challenge',
......@@ -1182,7 +1182,7 @@ const strings = {
'review.brilliant': '!! Brilliant',
'review.great': '! Great',
'review.best': 'Best move',
'review.good': '✓ Good',
'review.good': 'Good',
'review.book': 'Book',
'review.inaccuracy': '?! Inaccuracy',
'review.mistake': '? Mistake',
......@@ -1201,7 +1201,7 @@ const strings = {
'history.draw': 'Draw',
'history.insufficient_data': 'Insufficient data',
'history.matches_count': '{n} matches',
'result.copied': '✓ Copied',
'result.copied': 'Copied',
'result.copy_pgn': 'Copy',
'result.share_pgn': 'Share PGN',
'result.analyze': 'Analyze Game',
......@@ -1245,8 +1245,8 @@ const strings = {
'profile.org_apply': 'Join Organization',
'profile.logout': 'Logout',
'profile.view_title': 'Profile',
'profile.add_friend': '➕ Add Friend',
'profile.friend_sent': '✓ Request sent',
'profile.add_friend': 'Add Friend',
'profile.friend_sent': 'Request sent',
'profile.pending_request': 'Friend request pending',
'profile.challenge': 'Challenge',
'profile.message': 'Message',
......@@ -1314,7 +1314,7 @@ const strings = {
'group.play_invite': 'Play invite',
'group.players': 'Players',
'group.join': 'Join',
'group.accepted': '✓ Accepted',
'group.accepted': 'Accepted',
'group.invite_sent': 'Play invite sent!',
'group.create_title': 'Create Group',
'group.name_label': 'Group Name',
......@@ -1404,7 +1404,7 @@ const strings = {
'tournament.prizes': 'Prizes',
'tournament.register_now': 'Register Now',
'tournament.registering': 'Registering...',
'tournament.registered': '✅ Registered',
'tournament.registered': 'Registered',
'tournament.player_col': 'Player',
'tournament.points_col': 'Points',
'tournament.buchholz_col': 'Buch.',
......
// 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 @@
*
* node tools/shoot.mjs --out <dir> [--base <url>] [--width 390 --height 844]
* [--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
* 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);
const ONLY = (argOf('--only', '') || '').split(',').filter(Boolean);
const GUEST = args.includes('--guest');
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 SK = process.env.SUPABASE_SERVICE_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4';
......@@ -143,8 +148,9 @@ try {
await page.goto(BASE + '/', { waitUntil: 'networkidle2', timeout: 60000 });
await new Promise(r => setTimeout(r, 2000));
for (const s of SCENES) {
if (ONLY.length && !ONLY.includes(s.id)) continue;
const list = PUSH ? [{ id: NAME, push: [PUSH, JSON.parse(PARAMS)], settle: SETTLE }] : SCENES;
for (const s of list) {
if (!PUSH && ONLY.length && !ONLY.includes(s.id)) continue;
try {
await goTo(page, s);
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