Commit 969b2048 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat(proposal): show the real app and portal, and drop the phases

The Sayd proposal carried a hand-built imitation of the app and a mocked
portal. Both are replaced with screenshots of the real thing: 17 screens
captured from the Flutter app (sayd-mobile-app-main) driving the live demo
API on an emulator, and 19 portal screens (sayd-mobile-portal-main) wearing
the club's own branding.

The phase framing is gone — no "first/second phase", no week-by-week plan.
Scope now lists what the system actually does (installments, fines and
appeals, credit balance, transfer proofs, medical certificates, attendance
and evaluations, events, audit log). Captions reuse the proposal's own
Arabic and the Arabic user guide's wording.

The PDF export prints both screenshot sets as captioned grids.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent f4f4b150
This diff is collapsed.
<!DOCTYPE html>
<html lang="ar" dir="rtl" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<title>نادي الصيد المصري — نموذج تطبيق الأعضاء</title>
<meta name="theme-color" content="#070F1E">
<link rel="icon" href="../assets/club-logo.png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cairo:wght@700;800;900&family=IBM+Plex+Sans+Arabic:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="css/app.css">
<style>
.ico.flip{ width:16px;height:16px;color:var(--txt-3);transform:scaleX(-1) }
.ico.tiny{ width:12px;height:12px;stroke-width:3 }
.ico.big{ width:30px;height:30px }
.flagsvg{ width:22px;height:15px;stroke:none }
</style>
</head>
<body>
<svg class="sprite" aria-hidden="true"><defs>
<symbol id="i-home" viewBox="0 0 24 24"><path d="M3.8 10.4 12 3.8l8.2 6.6"/><path d="M6 9.6V20.2h12V9.6"/><path d="M9.9 20.2v-5h4.2v5"/></symbol>
<symbol id="i-activity" viewBox="0 0 24 24"><path d="M4 9v6M7.2 6.8v10.4M16.8 6.8v10.4M20 9v6M7.2 12h9.6"/></symbol>
<symbol id="i-gate" viewBox="0 0 24 24"><rect x="3.5" y="3.5" width="7" height="7" rx="1.3"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.3"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.3"/><path d="M13.5 14.2h3v3h-3zM19.4 14.2h1.1M13.5 20.4h3M19.6 18.6v1.8"/></symbol>
<symbol id="i-user" viewBox="0 0 24 24"><circle cx="12" cy="8" r="3.6"/><path d="M4.6 20.2c0-3.9 3.4-6 7.4-6s7.4 2.1 7.4 6"/></symbol>
<symbol id="i-bell" viewBox="0 0 24 24"><path d="M6.2 9.4a5.8 5.8 0 0 1 11.6 0c0 3.9 1.5 5.4 1.5 5.4H4.7s1.5-1.5 1.5-5.4Z"/><path d="M10 18.1a2.2 2.2 0 0 0 4 0"/></symbol>
<symbol id="i-mail" viewBox="0 0 24 24"><rect x="2.8" y="5.4" width="18.4" height="13.2" rx="2.2"/><path d="m3.8 7.1 7.3 5.3a1.6 1.6 0 0 0 1.8 0l7.3-5.3"/></symbol>
<symbol id="i-receipt" viewBox="0 0 24 24"><path d="M6 3.6h12v16.8l-2.4-1.6-2.4 1.6-2.4-1.6-2.4 1.6Z"/><path d="M9.4 8.6h5.2M9.4 12.6h5.2"/></symbol>
<symbol id="i-card" viewBox="0 0 24 24"><rect x="2.6" y="5" width="18.8" height="14" rx="2.4"/><path d="M2.6 9.8h18.8"/><path d="M6.2 14.6h3.6"/></symbol>
<symbol id="i-wallet" viewBox="0 0 24 24"><rect x="6.6" y="2.6" width="10.8" height="18.8" rx="2.4"/><path d="M10.6 5.7h2.8M11 18.4h2"/></symbol>
<symbol id="i-bank" viewBox="0 0 24 24"><path d="M3.6 9.6 12 4.2l8.4 5.4"/><path d="M5.8 10.8v6.8M10 10.8v6.8M14 10.8v6.8M18.2 10.8v6.8"/><path d="M3.6 20.2h16.8"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="m5 12.5 4.6 4.5L19 7.5"/></symbol>
<symbol id="i-chev" viewBox="0 0 24 24"><path d="m9 6 6 6-6 6"/></symbol>
<symbol id="i-back" viewBox="0 0 24 24"><path d="M20 12H8.5"/><path d="m12.5 7-5 5 5 5"/><path d="M4 7v10"/></symbol>
<symbol id="i-download" viewBox="0 0 24 24"><path d="M12 4.4v11.8"/><path d="m7.6 11.8 4.4 4.4 4.4-4.4"/><path d="M4.6 15v3.4a2 2 0 0 0 2 2h10.8a2 2 0 0 0 2-2V15"/></symbol>
<symbol id="i-clock" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="M12 6.9v5.4l3.4 2"/></symbol>
<symbol id="i-refresh" viewBox="0 0 24 24"><path d="M20.1 12a8.1 8.1 0 1 1-2.6-6"/><path d="M20.3 4.3v4.4h-4.4"/></symbol>
<symbol id="i-news" viewBox="0 0 24 24"><path d="M17.5 20.4H5.2a2 2 0 0 1-2-2V5.6a2 2 0 0 1 2-2h12.3v14.8a2 2 0 0 0 2 2 2 2 0 0 0 2-2V9.4h-4"/><path d="M6.6 7.6h7.4M6.6 11.4h7.4M6.6 15.2h4.6"/></symbol>
<symbol id="i-image" viewBox="0 0 24 24"><rect x="3.2" y="4.6" width="17.6" height="14.8" rx="2.2"/><circle cx="8.6" cy="9.8" r="1.7"/><path d="m3.8 17.2 4.9-4.4a1.8 1.8 0 0 1 2.4 0l4.2 3.8"/><path d="m13.6 13.4 2.2-1.9a1.8 1.8 0 0 1 2.4 0l2.4 2.1"/></symbol>
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M20.5 14.3A8.6 8.6 0 0 1 9.7 3.5a8.6 8.6 0 1 0 10.8 10.8Z"/></symbol>
<symbol id="i-swim" viewBox="0 0 24 24"><circle cx="15.6" cy="6.4" r="2"/><path d="M4.6 13.2 9.2 10l4 3.1"/><path d="M2.6 17.9c1.7 0 1.7 1.5 3.5 1.5s1.7-1.5 3.5-1.5 1.7 1.5 3.5 1.5 1.7-1.5 3.5-1.5 1.7 1.5 3.4 1.5"/></symbol>
<symbol id="i-football" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="m12 7.6 3.5 2.5-1.3 4.1H9.8l-1.3-4.1Z"/><path d="M12 3.6v4M19.6 9.7l-3.8.4M17 18.9l-2.8-4.7M7 18.9l2.8-4.7M4.4 9.7l3.8.4"/></symbol>
<symbol id="i-karate" viewBox="0 0 24 24"><path d="M3.2 9.4h17.6v4H3.2z"/><path d="m10 13.6-1.4 6.2 3.4-2.2 3.4 2.2-1.4-6.2"/></symbol>
<symbol id="i-tennis" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.4"/><path d="M6.1 5.6c2.5 2 3.8 4.6 3.8 7.9 0 1.6-.3 3.2-1 4.7"/><path d="M17.9 5.6c-2.5 2-3.8 4.6-3.8 7.9 0 1.6.3 3.2 1 4.7"/></symbol>
<symbol id="i-gym" viewBox="0 0 24 24"><circle cx="12" cy="4.8" r="2.1"/><path d="M12 7.3v6.3"/><path d="m6.6 9.6 5.4 1.6 5.4-1.6"/><path d="m9.1 20.4 2.9-6.8 2.9 6.8"/></symbol>
<symbol id="i-flag-eg" viewBox="0 0 24 16">
<rect x="0" y="0" width="24" height="5.33" fill="#CE1126"/>
<rect x="0" y="5.33" width="24" height="5.34" fill="#F5F5F5"/>
<rect x="0" y="10.67" width="24" height="5.33" fill="#0B0B0B"/>
</symbol>
</defs></svg>
<div id="app"></div>
<script src="js/data.js"></script>
<script src="js/app.js"></script>
</body>
</html>
This diff is collapsed.
/* Mock dataset for the prototype. Everything the screens read and mutate lives
here, so a demo session behaves like a real account: paying clears dues and
creates a receipt, subscribing adds an invoice, issuing an invite decrements
the balance. Reset from the profile screen. */
window.DB = {
member: {
id: 10245,
name: 'محمد أحمد سيد إبراهيم',
short: 'محمد أحمد',
type: 'عضو عامل',
validUntil: '2026/12/31',
phone: '100 123 4567',
kids: [
{ id: 'k1', name: 'يوسف محمد' },
{ id: 'k2', name: 'لُجين محمد' }
]
},
dues: [
{ id:'d1', title:'قسط تجديد العضوية', sub:'القسط ٢ من ٣ — فبراير 2026', due:'2026/02/28',
amount:1500, state:'late', kind:'renewal' },
{ id:'d2', title:'اشتراك سباحة مبتدئين', sub:'يوسف محمد — مارس 2026', due:'2026/03/05',
amount:900, state:'soon', kind:'activity' },
{ id:'d3', title:'غرامة تأخير سداد', sub:'عن تأخر قسط يناير', due:'2026/02/28',
amount:350, state:'late', kind:'fine' }
],
receipts: [
{ id:'r0', title:'اشتراك جمباز فني — لُجين', amount:1000, date:'2026/02/03', method:'بطاقة بنكية' },
{ id:'r1', title:'قسط تجديد العضوية — القسط ١', amount:1500, date:'2026/01/08', method:'محفظة إلكترونية' }
],
invites: { free: 4, used: 1, bought: 0, priceAdult: 150, priceChild: 75, maxBuy: 10 },
issued: [
{ id:'i1', name:'أحمد سامي', date:'15/03', state:'دخل' }
],
activities: [
{ id:'SW-01', name:'سباحة مبتدئين', cat:'سباحة', icon:'i-swim', age:'٦ - ٩ سنوات', sess:12, price:900, join:300, cap:24, taken:18, coach:'كابتن هشام صلاح' },
{ id:'SW-02', name:'سباحة متقدم', cat:'سباحة', icon:'i-swim', age:'١٠ - ١٤ سنة', sess:16, price:1200, join:300, cap:20, taken:14, coach:'كابتن هشام صلاح' },
{ id:'FB-01', name:'كرة القدم — الموهبة', cat:'كرة', icon:'i-football', age:'٨ - ١١ سنة', sess:12, price:1100, join:400, cap:28, taken:26, coach:'كابتن طارق منير' },
{ id:'KR-01', name:'كاراتيه مبتدئين', cat:'فردي', icon:'i-karate', age:'٦ - ١٢ سنة', sess:8, price:700, join:250, cap:30, taken:22, coach:'كابتن أمير فؤاد' },
{ id:'TN-01', name:'تنس أرضي — أطفال', cat:'فردي', icon:'i-tennis', age:'٧ - ١٢ سنة', sess:8, price:1400, join:350, cap:12, taken:12, coach:'كابتن نهى سامي' },
{ id:'GY-01', name:'جمباز فني — سيدات', cat:'فردي', icon:'i-gym', age:'١٢ - ١٨ سنة', sess:12, price:1000, join:300, cap:18, taken:11, coach:'كابتن دينا عادل' }
],
subs: [
{ id:'s1', actId:'SW-01', kid:'يوسف محمد', group:'مجموعة أ — صباحي', state:'نشط' },
{ id:'s2', actId:'GY-01', kid:'لُجين محمد', group:'مجموعة ب — مسائي', state:'تُجدَّد خلال ٥ أيام' }
],
schedule: {
'SW-01': { group:'مجموعة أ — صباحي', coach:'كابتن هشام صلاح', place:'حمام السباحة الأولمبي',
days:[ {d:'السبت',f:'09:00',t:'10:00'}, {d:'الاثنين',f:'09:00',t:'10:00'}, {d:'الأربعاء',f:'09:00',t:'10:00', note:'حصة قياس مستوى'} ],
changes:['أُلغيت حصة الاثنين 10/03 لصيانة حمام السباحة.','حصة تعويضية الجمعة 14/03 الساعة 10:00.'] },
'GY-01': { group:'مجموعة ب — مسائي', coach:'كابتن دينا عادل', place:'صالة الجمباز',
days:[ {d:'الأحد',f:'17:00',t:'18:30'}, {d:'الثلاثاء',f:'17:00',t:'18:30'} ], changes:[] }
},
entries: [
{ gate:'البوابة الرئيسية', when:'اليوم 08:14' },
{ gate:'بوابة حمام السباحة', when:'أمس 17:02' },
{ gate:'البوابة الرئيسية — ومعه ضيف', when:'أمس 16:58' }
],
/* Club blog. Images are described by a tone key rather than a file: the
prototype paints a branded placeholder, so the demo never ships stock
photos the club has not approved. */
news: [
{ id:'a1', cat:'بطولات', tone:'gold', date:'11 مارس 2026', title:'فريق السباحة يحصد ٧ ميداليات في بطولة الجمهورية',
lead:'حقق فريق النادي للسباحة نتائج هي الأفضل منذ خمس سنوات في بطولة الجمهورية للناشئين.',
body:'اختتمت أمس منافسات بطولة الجمهورية للناشئين، وحصد فريق نادي الصيد المصري سبع ميداليات: ثلاث ذهبيات وميداليتين فضيتين وبرونزيتين، ليحتل المركز الثاني في الترتيب العام.\n\nوجّه مجلس الإدارة الشكر إلى الجهاز الفني بقيادة كابتن هشام صلاح، وإلى أولياء الأمور على دعمهم المتواصل. وتبدأ استعدادات الفريق للبطولة العربية مطلع الشهر القادم.', pinned:true },
{ id:'a2', cat:'فعاليات', tone:'mint', date:'9 مارس 2026', title:'فتح باب التسجيل في معسكر الصيف للأطفال',
lead:'يبدأ المعسكر أول يوليو ويستمر ستة أسابيع، ويشمل السباحة وكرة القدم والتنس.',
body:'أعلنت إدارة الأنشطة الرياضية فتح باب التسجيل في معسكر الصيف للأطفال من سن ٦ إلى ١٤ سنة.\n\nيشمل المعسكر تدريبات يومية في السباحة وكرة القدم والتنس، بالإضافة إلى أنشطة ترفيهية ووجبة غداء. الأماكن محدودة والتسجيل من التطبيق مباشرة عبر قسم الأنشطة.' },
{ id:'a3', cat:'إعلانات', tone:'sky', date:'6 مارس 2026', title:'صيانة حمام السباحة الأولمبي الأسبوع القادم',
lead:'يغلق الحمام الأولمبي من الاثنين إلى الأربعاء لأعمال الصيانة الدورية.',
body:'تُجرى أعمال الصيانة الدورية لحمام السباحة الأولمبي خلال الفترة من الاثنين ١٠ مارس إلى الأربعاء ١٢ مارس.\n\nتُنقل حصص السباحة خلال هذه الأيام إلى الحمام المغطى، وستصل رسالة بالمواعيد الجديدة لكل مجموعة. نعتذر عن أي إزعاج.' },
{ id:'a4', cat:'أخبار المجلس', tone:'rose', date:'2 مارس 2026', title:'مجلس الإدارة يعتمد خطة تطوير الملاعب',
lead:'اعتمد المجلس في اجتماعه الأخير خطة تطوير ملاعب التنس وإنشاء صالة جديدة متعددة الأغراض.',
body:'اعتمد مجلس إدارة النادي خطة تطوير المنشآت الرياضية للعام الجاري، وتشمل إعادة تأهيل ملاعب التنس الأربعة وإنشاء صالة متعددة الأغراض.\n\nمن المقرر أن تبدأ الأعمال مطلع الشهر القادم وتنتهي قبل بداية الموسم الصيفي.' }
],
notifications: [
{ id:'n1', title:'قسط مستحق منذ يومين', body:'قسط تجديد العضوية ٢ من ٣ بقيمة 1,500 ج.م.', when:'اليوم 09:10', kind:'due', unread:true },
{ id:'n2', title:'حصة تعويضية', body:'أُضيفت حصة سباحة الجمعة 14/03 الساعة 10:00.', when:'أمس 18:40', kind:'sched', unread:true },
{ id:'n3', title:'تجديد اشتراك لُجين', body:'اشتراك الجمباز يُجدَّد خلال ٥ أيام.', when:'أمس 09:00', kind:'act', unread:true },
{ id:'n4', title:'تم استلام دفعتك', body:'وصل سداد 1,000 ج.م — إيصال رقم 20260203.', when:'03/02', kind:'ok', unread:false }
]
};
......@@ -561,24 +561,35 @@ footer > *{ position:relative; z-index:1; }
inside a .dark section inherited navy-on-navy headings. Raise specificity. */
.dark .card h4{ color:#fff; }
/* ================= LIVE PROTOTYPE =================
The phone now hosts the real prototype in an iframe rather than a stack of
static divs, so the screens in the proposal are the screens that ship. */
.phone iframe.screen{ width:100%; border:0; display:block; background:#070F1E; }
/* ================= REAL SCREENSHOTS =================
The phone and the browser frame now hold captures of the shipped app and
portal, not a hand-built imitation of them. */
.phone .shot{ display:block; width:100%; height:auto; border-radius:34px; background:#F4F6FA; }
/* A real capture has no status bar to hide behind, so the drawn notch would
sit on top of the screen title. */
.shot-phone::before{ display:none; }
.proto-tools{ display:flex; gap:8px; flex-wrap:wrap; margin-top:14px; }
.proto-tools .pbtn{ cursor:pointer; }
.proto-nav{ max-height:none; }
@media (max-width:1020px){
.proto-tools{ justify-content:center; }
}
/* .tbl clips rather than scrolls and its cells are nowrap, so a long Arabic
headline in a narrow column loses the status badge off the edge. Let the
title column wrap in the news table only. */
.news-tbl td:first-child{ white-space:normal; min-width:150px; line-height:1.55; }
@media (max-width:1020px){
#portal .grid[style*="1.3fr"]{ grid-template-columns:1fr !important; }
.shot-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(132px,1fr)); gap:16px; }
.shot-grid button{
display:flex; flex-direction:column; align-items:center; gap:8px; padding:8px;
background:#fff; border:1px solid var(--line); border-radius:var(--r-m);
font:inherit; cursor:pointer; transition:.18s;
}
.shot-grid button:hover{ border-color:var(--line-2); box-shadow:var(--sh-s); transform:translateY(-2px); }
.shot-grid button.on{ border-color:var(--navy); box-shadow:0 0 0 3px rgba(18,53,102,.12); }
.shot-grid button:focus-visible{ outline:2px solid var(--gold); outline-offset:2px; }
.shot-grid img{ display:block; width:100%; height:auto; border-radius:14px; }
.shot-grid span{ font-family:var(--f-head); font-weight:700; font-size:.76rem; color:var(--navy); line-height:1.4; text-align:center; }
.bshot{ display:block; width:100%; height:auto; }
.shot-note{ margin-top:16px; background:#fff; border:1px solid var(--line); border-radius:var(--r-m); padding:18px 20px; }
.shot-note h4{ color:var(--navy); font-size:1rem; margin-bottom:6px; }
.shot-note p{ font-size:.88rem; color:var(--txt-2); line-height:1.85; }
/* ================= PRINT / PDF =================
The page is built for scrolling: reveals start invisible, the nav is fixed,
......@@ -596,7 +607,7 @@ footer > *{ position:relative; z-index:1; }
/* Never split an atomic block across a page boundary. */
.card,.pain,.tmpl,.pay-step,.tl-i,.faq,.kpi,.pline,.note,.drop,
.price-hero,.phone,.mcard,.stat,.fstep,.tbl,.scope-list li{ break-inside:avoid; }
.price-hero,.phone,.mcard,.stat,.fstep,.tbl,.scope-list li,.shot-grid button,figure{ break-inside:avoid; }
.faq{ break-inside:avoid; }
.faq p{ display:block !important; } /* <details> print collapsed otherwise */
......
/**
* Renders the proposal microsite to PDF.
*
* The site is built for scrolling, so two things need undoing before printing:
* the phone shows one prototype screen at a time, and the portal shows one
* admin screen at a time. On paper that would lose almost the whole prototype,
* so both are expanded into labelled grids first.
* The site is built for scrolling: the phone shows one app screenshot at a
* time and the browser frame one portal screenshot at a time. On paper that
* would hide almost every screen, so both viewers are replaced by labelled
* grids of all their screenshots before printing.
*
* node generate-pdf.mjs # from the working tree
* node generate-pdf.mjs --live # from the deployed site
......@@ -20,40 +20,12 @@ const SRC = LIVE
: 'file://' + path.join(__dirname, 'index.html');
const OUT = path.join(__dirname, 'عرض-تطبيق-الأعضاء-نادي-الصيد-المصري.pdf');
/* Screens worth a page in the printed prototype, in reading order. */
const APP_SCREENS = [
['home', 'الرئيسية وبطاقة العضوية'],
['dues', 'المستحقات والأقساط'],
['pay', 'السداد الإلكتروني'],
['qr', 'كود دخول البوابة'],
['invites', 'الدعوات وشراء المزيد'],
['activities', 'الأنشطة الرياضية'],
['news', 'أخبار النادي'],
['article/a1', 'صفحة الخبر'],
['schedule', 'جدول التدريب'],
];
const PORTAL_LABELS = {
dash: 'لوحة التحصيل',
import: 'رفع بيانات الأعضاء',
members: 'الأعضاء والمستحقات',
catalog: 'تسعير الأنشطة',
invites: 'سياسة الدعوات',
sched: 'جداول المدربين',
news: 'أخبار النادي',
};
const wait = ms => new Promise(r => setTimeout(r, ms));
(async () => {
const browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--allow-file-access-from-files', // the phone frames are file:// iframes
'--font-render-hinting=none',
],
args: ['--no-sandbox', '--disable-setuid-sandbox', '--allow-file-access-from-files', '--font-render-hinting=none'],
});
const page = await browser.newPage();
......@@ -63,75 +35,61 @@ const wait = ms => new Promise(r => setTimeout(r, ms));
await page.goto(SRC, { waitUntil: 'networkidle0', timeout: 90000 });
await page.emulateMediaType('print');
console.log('· expanding prototypes');
await page.evaluate(({ screens, labels }) => {
console.log('· expanding screenshot viewers');
await page.evaluate(() => {
document.querySelectorAll('.reveal').forEach(el => el.classList.add('in'));
const nav = document.getElementById('nav'); if (nav) nav.remove();
const tools = document.querySelector('.proto-tools'); if (tools) tools.remove();
/* --- mobile prototype: one live frame -> a grid of labelled frames --- */
const stage = document.querySelector('.phone-stage');
const protoNav = document.querySelector('.proto-nav');
const proto = document.querySelector('.proto');
if (stage && proto) {
const desc = protoNav ? protoNav.querySelector('.pn-desc') : null;
const intro = desc ? desc.outerHTML : '';
proto.style.display = 'block';
if (protoNav) protoNav.remove();
stage.outerHTML = `
<div class="print-intro">${intro}</div>
<div class="print-phones">
${screens.map(([route, label]) => `
<figure class="print-phone">
<div class="phone"><iframe class="screen" data-route="${route}"
src="app/index.html#/${route}" loading="eager"></iframe></div>
<figcaption>${label}</figcaption>
/* --- app: the single-phone viewer goes; the all-screens grid stays and
becomes the printed gallery, with its images loaded eagerly. --- */
const proto = document.querySelector('#app .proto');
if (proto) proto.remove();
document.querySelectorAll('#shot-grid img').forEach(img => { img.loading = 'eager'; });
/* --- portal: button row + single frame -> every screen, captioned --- */
const btns = [...document.querySelectorAll('.pn-item[data-pshot]')];
const shots = btns.map(b => ({ k: b.dataset.pshot, t: b.textContent.replace(/^\s*\S+\s+/, '').trim() }));
const row = btns.length ? btns[0].parentElement : null;
const frame = document.querySelector('#portal .browser');
const note = document.getElementById('pshot-note');
if (frame) {
frame.insertAdjacentHTML('beforebegin', `
<div class="print-portal">
${shots.map(s => `
<figure>
<img src="screenshots/portal/${s.k}.jpg" alt="">
<figcaption>${s.t}</figcaption>
</figure>`).join('')}
</div>`;
</div>`);
frame.remove();
}
if (row) row.remove();
if (note) note.remove();
/* --- portal: one visible screen -> all of them, stacked ---
Each .pscr already opens with its own .ptop heading, so no caption is
injected here; adding one printed the screen name twice. */
document.querySelectorAll('.pscr').forEach(scr => { scr.style.display = 'block'; });
const pnav = document.querySelector('[data-pscr]');
if (pnav && pnav.parentElement) pnav.parentElement.remove();
const side = document.querySelector('.side'); if (side) side.style.display = 'none';
const bbar = document.querySelector('.bbar'); if (bbar) bbar.style.display = 'none';
/* <details> print collapsed unless forced open */
document.querySelectorAll('details').forEach(d => d.open = true);
const css = document.createElement('style');
css.textContent = `
.print-intro{ max-width:760px; margin:0 0 26px; font-size:.9rem; color:var(--txt-2); line-height:1.85; }
.print-intro .pn-desc{ border:0; padding:0; margin:0; }
.print-phones{ display:grid; grid-template-columns:repeat(3,1fr); gap:24px 18px; }
/* 600px keeps two rows inside one 1754px page; at 660 the second row
overflowed by a few pixels and half a page was left blank. */
.print-phone{ margin:0; break-inside:avoid; text-align:center; }
.print-phone .phone{ width:100%; }
.print-phone .screen{ width:100%; height:600px; border:0; }
.print-phone figcaption{ margin-top:9px; font-family:var(--f-head); font-weight:700;
font-size:.82rem; color:var(--navy); }
.pscr{ break-inside:avoid; margin-bottom:8px; }
.bwin{ display:block; } .pmain{ padding:18px 4px; }
/* Six across keeps all seventeen screens on one page; at five the last
row spilled onto a page of its own. */
.shot-grid{ grid-template-columns:repeat(6,1fr); gap:14px 12px; }
.shot-grid button{ border:0; padding:0; box-shadow:none; }
.shot-grid img{ border:1px solid var(--line); }
.print-portal{ display:grid; grid-template-columns:repeat(2,1fr); gap:22px 18px; margin-top:26px; }
.print-portal figure{ margin:0; break-inside:avoid; }
.print-portal img{ display:block; width:100%; height:auto; border:1px solid var(--line); border-radius:10px; }
.print-portal figcaption{ margin-top:7px; font-family:var(--f-head); font-weight:700; font-size:.82rem; color:var(--navy); }
`;
document.head.appendChild(css);
}, { screens: APP_SCREENS, labels: PORTAL_LABELS });
console.log('· waiting for prototype frames');
await page.waitForFunction(() => {
const frames = [...document.querySelectorAll('.print-phone iframe')];
if (!frames.length) return false;
return frames.every(f => {
try { return !!f.contentDocument?.querySelector('#app .screen'); }
catch { return false; }
});
}, { timeout: 60000 });
console.log('· waiting for images');
await page.waitForFunction(
() => [...document.images].every(img => img.complete && img.naturalWidth > 0),
{ timeout: 60000 },
);
await page.evaluate(() => document.fonts.ready);
await wait(2500); // let the in-frame reveal/transition settle
await wait(1200);
console.log('· printing');
await page.pdf({
......
This diff is collapsed.
This diff is collapsed.
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