diff --git a/public/app-runtime.js b/public/app-runtime.js index 9697609..be58118 100644 --- a/public/app-runtime.js +++ b/public/app-runtime.js @@ -2064,6 +2064,7 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс const DEFAULT_SUPABASE_URL = 'https://cksuehzcimitsxmeloes.supabase.co'; const DEFAULT_SUPABASE_KEY = 'sb_publishable_v8Z3hEBnu7zsDwAb5KCWcg_T96IejsM'; const SUPABASE_API_PROXY = 'https://api.caterium.ru'; + const PROXY_FETCH_TIMEOUT_MS = 7000; function supabaseProxyFetch(input, init) { try { const url = typeof input === 'string' ? input : input?.url; @@ -2072,7 +2073,14 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс input = typeof input === 'string' ? proxied : new Request(proxied, input); } } catch (_) {} - return fetch(input, init); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), PROXY_FETCH_TIMEOUT_MS); + const externalSignal = init?.signal; + if (externalSignal) { + if (externalSignal.aborted) controller.abort(); + else externalSignal.addEventListener('abort', () => controller.abort(), {once:true}); + } + return fetch(input, {...init, signal: controller.signal}).finally(() => clearTimeout(timer)); } let config = loadConfig(); @@ -4215,7 +4223,11 @@ window.SUN_LEGACY_CATALOG_V175=[{"id":"1","name":"Фуршетный бокс const DEFAULT_ID='cream-elegance'; const TEMPLATES=[ {id:'cream-elegance',name:'Кремовая классика',desc:'Светлый премиальный дизайн: крупная обложка, круглые иконки категорий и аккуратная сетка меню.',thumb:''}, - {id:'neon-menu',name:'Неоновое меню',desc:'Тёмный дизайн с неоновыми акцентами, нумерованной фотосеткой блюд и карточками категорий.',thumb:''} + {id:'neon-menu',name:'Неоновое меню',desc:'Тёмный дизайн с неоновыми акцентами, нумерованной фотосеткой блюд и карточками категорий.',thumb:''}, + {id:'emerald-circles',name:'Изумрудные круги',desc:'Тёмно-изумрудная презентация с круглыми фото блюд и списком категорий.',thumb:''}, + {id:'midnight-checklist',name:'Тёмный чек-лист',desc:'Компактный тёмно-зелёный макет: нумерованное меню по категориям и фотополоса блюд.',thumb:''}, + {id:'gourmet-hero',name:'Гастро-витрина',desc:'Крупный фото-хиро в духе лендинга, вкладки категорий и карточки блюд.',thumb:''}, + {id:'diamond-gold',name:'Изумруд и золото',desc:'Тёмно-зелёный дизайн с золотыми акцентами, нумерованной фотосеткой и блоком скидки.',thumb:''} ]; const esc=window.SunSafe.escapeHTML; diff --git a/public/core/signature-offer-pdf-v18.js b/public/core/signature-offer-pdf-v18.js index e0197ad..fd85013 100644 --- a/public/core/signature-offer-pdf-v18.js +++ b/public/core/signature-offer-pdf-v18.js @@ -27,7 +27,11 @@ // Distinctive Cyrillic-friendly type pairing per polished template (falls back to Georgia/Arial elsewhere). const FONT_STACKS={ 'cream-elegance':{display:'"Playfair Display",Georgia,serif',label:'"Montserrat",Arial,sans-serif'}, - 'neon-menu':{display:'"Unbounded",Arial,sans-serif',label:'"Manrope",Arial,sans-serif'} + 'neon-menu':{display:'"Unbounded",Arial,sans-serif',label:'"Manrope",Arial,sans-serif'}, + 'emerald-circles':{display:'"Playfair Display",Georgia,serif',label:'"Montserrat",Arial,sans-serif'}, + 'midnight-checklist':{display:'"Unbounded",Arial,sans-serif',label:'"Manrope",Arial,sans-serif'}, + 'gourmet-hero':{display:'"Unbounded",Arial,sans-serif',label:'"Manrope",Arial,sans-serif'}, + 'diamond-gold':{display:'"Playfair Display",Georgia,serif',label:'"Montserrat",Arial,sans-serif'} }; function fontsFor(id){return FONT_STACKS[id]||{display:'Georgia,serif',label:'Arial,sans-serif'}} function ensureFontLink(){ @@ -70,7 +74,21 @@ function photoGridSquare(ctx,s,p,x,y,w,h,cols,rows,{numbered=true,fonts}={}){const lf=fonts?.label||'Arial';const items=(s.items||[]).slice(0,cols*rows);const gap=7,cw=(w-gap*(cols-1))/cols,ch=(h-gap*(rows-1))/rows;return (async()=>{const imgs=await Promise.all(items.map(it=>loadImage(it.photoData||'')));imgs.forEach((img,i)=>{const col=i%cols,row=Math.floor(i/cols),xx=x+col*(cw+gap),yy=y+row*(ch+gap);roundRect(ctx,xx,yy,cw,ch,10,p.deep,null);if(img)coverImage(ctx,img,xx,yy,cw,ch,10);if(numbered){roundRect(ctx,xx+6,yy+6,22,22,11,'rgba(6,10,8,.72)',null);text(ctx,String(i+1),xx+17,yy+10,22,16,{font:`700 10px ${lf}`,color:'#fff',align:'center',maxLines:1})}})})()} function photoGridCircles(ctx,s,p,x,y,w,cols,r,{numbered=true,fonts}={}){const lf=fonts?.label||'Arial';const items=(s.items||[]).slice(0,cols*2);const gapX=(w-cols*r*2)/(cols-1||1);return (async()=>{const imgs=await Promise.all(items.map(it=>loadImage(it.photoData||'')));imgs.forEach((img,i)=>{const col=i%cols,row=Math.floor(i/cols),cx=x+r+col*(r*2+gapX),cy=y+r+row*(r*2+22);ctx.save();ctx.beginPath();ctx.arc(cx,cy,r+3,0,Math.PI*2);ctx.strokeStyle=p.accent;ctx.lineWidth=2;ctx.stroke();ctx.restore();if(img)circleImage(ctx,img,cx,cy,r);else{ctx.save();ctx.beginPath();ctx.arc(cx,cy,r,0,Math.PI*2);ctx.fillStyle=p.deep;ctx.fill();ctx.restore()}if(numbered){roundRect(ctx,cx-13,cy+r-8,26,22,11,p.accent,null);text(ctx,String(i+1),cx,cy+r-4,26,16,{font:`700 10px ${lf}`,color:p.deep,align:'center',maxLines:1})}})})()} function statCards(ctx,s,p,x,y,w,h,cards,{dark=true,fonts}={}){const lf=fonts?.label||'Arial';const gap=10,cw=(w-gap*(cards.length-1))/cards.length;cards.forEach((c,i)=>{const xx=x+i*(cw+gap);roundRect(ctx,xx,y,cw,h,14,dark?'rgba(255,255,255,.05)':p.paper,dark?'rgba(255,255,255,.16)':p.line);text(ctx,c[1],xx+16,y+14,cw-32,17,{font:`700 10px ${lf}`,color:p.muted,maxLines:1});text(ctx,String(c[0]),xx+16,y+38,cw-32,32,{font:`800 24px ${lf}`,color:dark?'#fff':p.deep,maxLines:1})})} - function priceBand(ctx,s,p,x,y,w,h,{dark=true,fonts}={}){const df=fonts?.display||'Arial',lf=fonts?.label||'Arial';roundRect(ctx,x,y,w,h,16,p.accent,null);text(ctx,'ИТОГОВАЯ СТОИМОСТЬ',x+22,y+16,w-44,16,{font:`700 11px ${lf}`,color:p.deep,maxLines:1});text(ctx,money(s.pricing?.total),x+22,y+38,w-44,44,{font:`700 32px ${df}`,color:p.deep,maxLines:1});if(s.guests)text(ctx,'на гостя: '+money((Number(s.pricing?.itemsTotal||s.pricing?.total||0))/Math.max(1,Number(s.guests))),x+22,y+h-26,w-44,18,{font:`500 12px ${lf}`,color:p.deep,maxLines:1})} + function priceBand(ctx,s,p,x,y,w,h,{dark=true,fonts}={}){ + const df=fonts?.display||'Arial',lf=fonts?.label||'Arial'; + const perGuest=s.guests?money((Number(s.pricing?.itemsTotal||s.pricing?.total||0))/Math.max(1,Number(s.guests))):''; + if(dark){ + roundRect(ctx,x,y,w,h,16,p.accent,null); + text(ctx,'ИТОГОВАЯ СТОИМОСТЬ',x+22,y+16,w-44,16,{font:`700 11px ${lf}`,color:p.deep,maxLines:1}); + text(ctx,money(s.pricing?.total),x+22,y+38,w-44,44,{font:`700 32px ${df}`,color:p.deep,maxLines:1}); + if(perGuest)text(ctx,'на гостя: '+perGuest,x+22,y+h-26,w-44,18,{font:`500 12px ${lf}`,color:p.deep,maxLines:1}); + }else{ + roundRect(ctx,x,y,w,h,16,p.paper,p.accent,2); + text(ctx,'ИТОГОВАЯ СТОИМОСТЬ',x+22,y+18,w-44,16,{font:`700 11px ${lf}`,color:p.accent2,maxLines:1}); + text(ctx,money(s.pricing?.total),x+22,y+38,w-44,46,{font:`700 34px ${df}`,color:p.deep,maxLines:1}); + if(perGuest)text(ctx,'на гостя: '+perGuest,x+22,y+h-25,w-44,18,{font:`500 12px ${lf}`,color:p.muted,maxLines:1}); + } + } function checklistCard(ctx,p,x,y,w,h,title,lines,{dark=true,fonts}={}){const lf=fonts?.label||'Arial';roundRect(ctx,x,y,w,h,16,dark?'rgba(255,255,255,.04)':p.paper,dark?'rgba(255,255,255,.14)':p.line);text(ctx,title,x+18,y+16,w-36,18,{font:`700 13px ${lf}`,color:p.accent,maxLines:1});const cols=2,rowH=22,colW=(w-36)/cols;lines.forEach((t,i)=>{const col=i%cols,row=Math.floor(i/cols),xx=x+18+col*colW,yy=y+46+row*rowH;roundRect(ctx,xx,yy+2,14,14,7,p.accent,null);text(ctx,'✓',xx+7,yy+3,10,12,{font:`700 9px ${lf}`,color:p.deep,align:'center',maxLines:1});text(ctx,t,xx+20,yy,colW-24,14,{font:`500 10px ${lf}`,color:dark?'#eef':p.ink,maxLines:1})})} function addonRow(ctx,p,x,y,w,h,items,{dark=true,fonts}={}){const lf=fonts?.label||'Arial';const gap=9,cw=(w-gap*(items.length-1))/items.length;items.forEach((t,i)=>{const xx=x+i*(cw+gap);roundRect(ctx,xx,y,cw,h,13,dark?'rgba(255,255,255,.04)':p.paper,dark?'rgba(255,255,255,.14)':p.line);roundRect(ctx,xx+cw/2-11,y+12,22,22,11,p.accent,null);text(ctx,'+',xx+cw/2,y+14,22,18,{font:`700 14px ${lf}`,color:p.deep,align:'center',maxLines:1});text(ctx,t,xx+cw/2,y+44,cw-16,14,{font:`600 10px ${lf}`,color:dark?'#eef':p.ink,align:'center',maxLines:2})})} @@ -117,35 +135,35 @@ checklistCard(ctx,p,M,1160,470,150,'ВСЁ ПОД КОНТРОЛЕМ',CHECK_LINES,{dark:true,fonts:f}); addonRow(ctx,p,540,1160,406,150,ADDON_LINES,{dark:true,fonts:f}); }else if(id==='emerald-circles'){ - logoOrBrand(ctx,logo,p,{x:M,y:52,dark:true},s.brandName);roundRect(ctx,M,140,260,34,17,'rgba(201,162,74,.12)',p.accent,1);text(ctx,'ИНДИВИДУАЛЬНОЕ ПРЕДЛОЖЕНИЕ',M+14,150,240,14,{font:'700 9px Arial',color:p.accent,maxLines:1});text(ctx,title,M,192,430,58,{font:'500 38px Georgia',color:'#fff',maxLines:3});text(ctx,'Составим идеальное гастрономическое решение для вашего мероприятия.',M,350,410,22,{font:'13px Arial',color:p.muted,maxLines:3}); - const feats=['Разнообразие блюд','Свежие ингредиенты','Премиальное качество'];feats.forEach((t,i)=>{const xx=M+i*145;roundRect(ctx,xx,430,132,48,13,'rgba(255,255,255,.05)',p.line);text(ctx,t,xx+10,446,112,14,{font:'10px Arial',color:p.accent2,align:'center',maxLines:2})}); - menuList(ctx,s,p,M,510,470,66,5,{dark:true});await photoGridCircles(ctx,s,p,565,520,380,3,58,{numbered:true}); - statCards(ctx,s,p,M,940,W-2*M,78,[[Math.round(classicBoxCount(s))||0,'КОРОБОК'],[(s.items||[]).length,'ПОЗИЦИЙ ',],[s.guests||'—','ГОСТЕЙ']],{dark:true});priceBand(ctx,s,p,M,1032,W-2*M,90,{dark:true}); - checklistCard(ctx,p,M,1140,470,170,'ВСЁ ПОД КОНТРОЛЕМ КОМАНДЫ СОЛНЦА',CHECK_LINES,{dark:true});addonRow(ctx,p,540,1140,406,170,ADDON_LINES,{dark:true}); + logoOrBrand(ctx,logo,p,{x:M,y:52,dark:true,fonts:f},s.brandName);roundRect(ctx,M,140,260,34,17,'rgba(201,162,74,.12)',p.accent,1);text(ctx,'ИНДИВИДУАЛЬНОЕ ПРЕДЛОЖЕНИЕ',M+14,150,240,14,{font:`700 9px ${f.label}`,color:p.accent,maxLines:1});text(ctx,title,M,192,430,58,{font:`600 38px ${f.display}`,color:'#fff',maxLines:3});text(ctx,'Составим идеальное гастрономическое решение для вашего мероприятия.',M,350,410,22,{font:`500 13px ${f.label}`,color:p.muted,maxLines:3}); + const feats=['Разнообразие блюд','Свежие ингредиенты','Премиальное качество'];feats.forEach((t,i)=>{const xx=M+i*145;roundRect(ctx,xx,430,132,48,13,'rgba(255,255,255,.05)',p.line);text(ctx,t,xx+66,446,112,14,{font:`600 10px ${f.label}`,color:p.accent2,align:'center',maxLines:2})}); + menuList(ctx,s,p,M,510,470,66,5,{dark:true,fonts:f});await photoGridCircles(ctx,s,p,565,520,380,3,58,{numbered:true,fonts:f}); + statCards(ctx,s,p,M,940,W-2*M,78,[[Math.round(classicBoxCount(s))||0,'КОРОБОК'],[(s.items||[]).length,'ПОЗИЦИЙ ',],[s.guests||'—','ГОСТЕЙ']],{dark:true,fonts:f});priceBand(ctx,s,p,M,1032,W-2*M,90,{dark:true,fonts:f}); + checklistCard(ctx,p,M,1140,470,170,'ВСЁ ПОД КОНТРОЛЕМ КОМАНДЫ СОЛНЦА',CHECK_LINES,{dark:true,fonts:f});addonRow(ctx,p,540,1140,406,170,ADDON_LINES,{dark:true,fonts:f}); }else if(id==='midnight-checklist'){ - logoOrBrand(ctx,logo,p,{x:M,y:48,dark:true},s.brandName);if(s.client)text(ctx,String(s.client),W-M,58,300,20,{font:'700 15px Arial',color:'#fff',align:'right',maxLines:1});const metaLine=[dateText(s.date),s.guests?String(s.guests)+' гостей':''].filter(Boolean).join(' · ');if(metaLine)text(ctx,metaLine,W-M,84,300,16,{font:'11px Arial',color:p.muted,align:'right',maxLines:1}); - roundRect(ctx,M,120,W-2*M,60,14,'rgba(255,255,255,.04)',p.line);text(ctx,'Меню составлено из расчёта количества гостей. Все позиции приедут готовыми к подаче на стол.',M+20,138,W-2*M-40,20,{font:'12px Arial',color:p.muted,maxLines:2}); - text(ctx,'МЕНЮ',M,215,300,24,{font:'700 20px Arial',color:'#fff',maxLines:1});menuList(ctx,s,p,M,255,W-2*M,58,8,{dark:true}); - await photoGridSquare(ctx,s,p,M,860,W-2*M,190,5,2,{numbered:false}); - statCards(ctx,s,p,M,1075,470,74,[[Math.round(classicBoxCount(s))||0,'КОРОБОК'],[(s.items||[]).length,'ПОЗИЦИЙ']],{dark:true});priceBand(ctx,s,p,540,1075,406,74,{dark:true}); - checklistCard(ctx,p,M,1170,470,150,'ВСЁ ПОД КОНТРОЛЕМ КОМАНДЫ СОЛНЦА',CHECK_LINES,{dark:true});addonRow(ctx,p,540,1170,406,150,ADDON_LINES,{dark:true}); + logoOrBrand(ctx,logo,p,{x:M,y:48,dark:true,fonts:f},s.brandName);if(s.client)text(ctx,String(s.client),W-M,58,300,20,{font:`700 15px ${f.display}`,color:'#fff',align:'right',maxLines:1});const metaLine=[dateText(s.date),s.guests?String(s.guests)+' гостей':''].filter(Boolean).join(' · ');if(metaLine)text(ctx,metaLine,W-M,84,300,16,{font:`500 11px ${f.label}`,color:p.muted,align:'right',maxLines:1}); + roundRect(ctx,M,120,W-2*M,60,14,'rgba(255,255,255,.04)',p.line);text(ctx,'Меню составлено из расчёта количества гостей. Все позиции приедут готовыми к подаче на стол.',M+20,138,W-2*M-40,20,{font:`500 12px ${f.label}`,color:p.muted,maxLines:2}); + text(ctx,'МЕНЮ',M,215,300,24,{font:`700 20px ${f.display}`,color:'#fff',maxLines:1});menuList(ctx,s,p,M,255,W-2*M,58,8,{dark:true,fonts:f}); + await photoGridSquare(ctx,s,p,M,860,W-2*M,190,5,2,{numbered:false,fonts:f}); + statCards(ctx,s,p,M,1075,470,74,[[Math.round(classicBoxCount(s))||0,'КОРОБОК'],[(s.items||[]).length,'ПОЗИЦИЙ']],{dark:true,fonts:f});priceBand(ctx,s,p,540,1075,406,74,{dark:true,fonts:f}); + checklistCard(ctx,p,M,1170,470,150,'ВСЁ ПОД КОНТРОЛЕМ КОМАНДЫ СОЛНЦА',CHECK_LINES,{dark:true,fonts:f});addonRow(ctx,p,540,1170,406,150,ADDON_LINES,{dark:true,fonts:f}); }else if(id==='gourmet-hero'){ - logoOrBrand(ctx,logo,p,{x:W-M-190,y:44,dark:true},s.brandName);text(ctx,'КЕЙТЕРИНГ ДЛЯ',M,90,420,60,{font:'700 46px Arial',color:'#fff',maxLines:1});text(ctx,'ВАШЕГО СОБЫТИЯ',M,150,420,60,{font:'700 46px Arial',color:'#fff',maxLines:1});text(ctx,'Индивидуальное меню, безупречная подача и сервис, о котором будут говорить ваши гости.',M,235,400,24,{font:'14px Arial',color:p.muted,maxLines:3});if(hero)coverImage(ctx,hero,520,60,426,340,18);roundRect(ctx,520,420,240,42,12,p.paper,p.line);text(ctx,String((s.items||[]).length)+' блюд в меню',540,432,220,18,{font:'700 12px Arial',color:p.accent,maxLines:1}); - const tabs=['РЫБНЫЕ','МЯСНЫЕ','ВЕГЕТАРИАНСКИЕ','САЛАТЫ','ДЕСЕРТЫ'];let tx=M;tabs.forEach((t,i)=>{text(ctx,t,tx,505,200,16,{font:'700 11px Arial',color:i===0?'#fff':p.muted,maxLines:1});if(i===0)line(ctx,tx,528,tx+70,528,p.accent,2);tx+=90}); + logoOrBrand(ctx,logo,p,{x:W-M-190,y:44,dark:true,fonts:f},s.brandName);text(ctx,'КЕЙТЕРИНГ ДЛЯ',M,96,460,44,{font:`700 32px ${f.display}`,color:'#fff',maxLines:1});text(ctx,'ВАШЕГО СОБЫТИЯ',M,140,460,44,{font:`700 32px ${f.display}`,color:'#fff',maxLines:1});text(ctx,'Индивидуальное меню, безупречная подача и сервис, о котором будут говорить ваши гости.',M,235,400,24,{font:`500 14px ${f.label}`,color:p.muted,maxLines:3});if(hero)coverImage(ctx,hero,520,60,426,340,18);roundRect(ctx,520,420,240,42,12,p.paper,p.line);text(ctx,String((s.items||[]).length)+' блюд в меню',540,432,220,18,{font:`700 12px ${f.label}`,color:p.accent,maxLines:1}); + const tabs=['РЫБНЫЕ','МЯСНЫЕ','ВЕГЕТАРИАНСКИЕ','САЛАТЫ','ДЕСЕРТЫ'];let tx=M;tabs.forEach((t,i)=>{text(ctx,t,tx,505,200,16,{font:`700 11px ${f.label}`,color:i===0?'#fff':p.muted,maxLines:1});if(i===0)line(ctx,tx,528,tx+70,528,p.accent,2);tx+=90}); const featured=(s.items||[]).slice(0,3);const cardW=(W-2*M-2*14)/3; - {const imgs=await Promise.all(featured.map(it=>loadImage(it.photoData||'')));featured.forEach((it,i)=>{const xx=M+i*(cardW+14);roundRect(ctx,xx,555,cardW,230,16,p.paper,p.line);if(imgs[i])coverImage(ctx,imgs[i],xx,555,cardW,150,16);text(ctx,String(i+1).padStart(2,'0'),xx+14,715,40,16,{font:'700 12px Arial',color:p.accent,maxLines:1});text(ctx,it.name||'Позиция меню',xx+14,735,cardW-28,20,{font:'600 13px Arial',color:'#fff',maxLines:2});if(it.weight)text(ctx,String(it.weight),xx+14,765,cardW-28,14,{font:'10px Arial',color:p.muted,maxLines:1})})} - roundRect(ctx,M,822,W-2*M,52,14,null,p.accent,1.4);text(ctx,'СМОТРЕТЬ ПОЛНОЕ МЕНЮ →',0,838,W,18,{font:'700 12px Arial',color:p.accent,align:'center',maxLines:1}); - statCards(ctx,s,p,M,910,306,100,[[Math.round(classicBoxCount(s))||0,'КОРОБОК'],[(s.items||[]).length,'ПОЗИЦИЙ']],{dark:true});priceBand(ctx,s,p,376,910,306,100,{dark:true});checklistCard(ctx,p,698,910,248,100,'ДОСТАВКА',CHECK_LINES.slice(0,2),{dark:true}); - addonRow(ctx,p,M,1045,W-2*M,150,ADDON_LINES,{dark:true}); + {const imgs=await Promise.all(featured.map(it=>loadImage(it.photoData||'')));featured.forEach((it,i)=>{const xx=M+i*(cardW+14);roundRect(ctx,xx,555,cardW,230,16,p.paper,p.line);if(imgs[i])coverImage(ctx,imgs[i],xx,555,cardW,150,16);text(ctx,String(i+1).padStart(2,'0'),xx+14,715,40,16,{font:`700 12px ${f.label}`,color:p.accent,maxLines:1});text(ctx,it.name||'Позиция меню',xx+14,735,cardW-28,20,{font:`600 13px ${f.display}`,color:'#fff',maxLines:2});if(it.weight)text(ctx,String(it.weight),xx+14,765,cardW-28,14,{font:`500 10px ${f.label}`,color:p.muted,maxLines:1})})} + roundRect(ctx,M,822,W-2*M,52,14,null,p.accent,1.4);text(ctx,'СМОТРЕТЬ ПОЛНОЕ МЕНЮ →',W/2,838,W,18,{font:`700 12px ${f.label}`,color:p.accent,align:'center',maxLines:1}); + statCards(ctx,s,p,M,910,306,100,[[Math.round(classicBoxCount(s))||0,'КОРОБОК'],[(s.items||[]).length,'ПОЗИЦИЙ']],{dark:true,fonts:f});priceBand(ctx,s,p,376,910,306,100,{dark:true,fonts:f});checklistCard(ctx,p,698,910,248,100,'ДОСТАВКА',CHECK_LINES.slice(0,2),{dark:true,fonts:f}); + addonRow(ctx,p,M,1045,W-2*M,150,ADDON_LINES,{dark:true,fonts:f}); }else if(id==='diamond-gold'){ - logoOrBrand(ctx,logo,p,{x:M,y:50,dark:true},s.brandName);if(s.guests){roundRect(ctx,W-M-140,50,140,34,17,'rgba(255,255,255,.06)',p.line);text(ctx,String(s.guests)+' гостей',W-M-140,58,140,16,{font:'700 11px Arial',color:'#fff',align:'center',maxLines:1})} - text(ctx,'ИНДИВИДУАЛЬНОЕ МЕНЮ',M,150,420,60,{font:'700 34px Arial',color:'#fff',maxLines:2});text(ctx,'для вашего мероприятия',M,196,420,26,{font:'400 20px Georgia',color:p.accent,maxLines:1});text(ctx,'Мы создаём гастрономические впечатления, которые запоминаются.',M,240,400,24,{font:'13px Arial',color:p.muted,maxLines:3}); - const feats=['Индивидуальный подход','Только свежие ингредиенты','Разнообразие вкусов','Сделано с любовью'];feats.forEach((t,i)=>{const xx=M+(i%2)*220,yy=330+Math.floor(i/2)*46;roundRect(ctx,xx,yy,205,38,11,'rgba(255,255,255,.04)',p.line);text(ctx,t,xx+10,yy+12,185,14,{font:'10px Arial',color:p.accent2,maxLines:1})}); - menuList(ctx,s,p,M,440,470,56,5,{dark:true}); - text(ctx,'ПРИМЕРЫ ЗАКУСОК',540,428,406,16,{font:'700 12px Arial',color:p.accent,maxLines:1});await photoGridSquare(ctx,s,p,540,455,406,300,3,2,{numbered:true}); - statCards(ctx,s,p,M,900,306,90,[[Math.round(classicBoxCount(s))||0,'КОРОБОК'],[(s.items||[]).length,'ПОЗИЦИЙ']],{dark:true});priceBand(ctx,s,p,376,900,306,90,{dark:true}); - const dgx=698,dgy=900;roundRect(ctx,dgx,dgy,248,90,16,'rgba(255,255,255,.04)',p.line);ctx.save();ctx.translate(dgx+124,dgy+45);ctx.rotate(Math.PI/4);ctx.fillStyle=p.accent;ctx.fillRect(-16,-16,32,32);ctx.restore();text(ctx,'СКИДКА ВКЛЮЧЕНА',dgx,dgy+70,248,14,{font:'700 9px Arial',color:p.muted,align:'center',maxLines:1}); - checklistCard(ctx,p,M,1010,470,170,'ДОСТАВКА И КОНТРОЛЬ КАЧЕСТВА',CHECK_LINES,{dark:true});addonRow(ctx,p,540,1010,406,170,ADDON_LINES.concat(['Посуда','Мебель']),{dark:true}); + logoOrBrand(ctx,logo,p,{x:M,y:50,dark:true,fonts:f},s.brandName);if(s.guests){roundRect(ctx,W-M-140,50,140,34,17,'rgba(255,255,255,.06)',p.line);text(ctx,String(s.guests)+' гостей',W-M-70,58,140,16,{font:`700 11px ${f.label}`,color:'#fff',align:'center',maxLines:1})} + text(ctx,'ИНДИВИДУАЛЬНОЕ МЕНЮ',M,150,440,34,{font:`700 28px ${f.display}`,color:'#fff',maxLines:1});text(ctx,'для вашего мероприятия',M,186,440,24,{font:`500 18px ${f.display}`,color:p.accent,maxLines:1});text(ctx,'Мы создаём гастрономические впечатления, которые запоминаются.',M,222,400,20,{font:`500 13px ${f.label}`,color:p.muted,maxLines:3}); + const feats=['Индивидуальный подход','Только свежие ингредиенты','Разнообразие вкусов','Сделано с любовью'];feats.forEach((t,i)=>{const xx=M+(i%2)*220,yy=330+Math.floor(i/2)*46;roundRect(ctx,xx,yy,205,38,11,'rgba(255,255,255,.04)',p.line);text(ctx,t,xx+10,yy+12,185,14,{font:`500 10px ${f.label}`,color:p.accent2,maxLines:1})}); + menuList(ctx,s,p,M,440,470,56,5,{dark:true,fonts:f}); + text(ctx,'ПРИМЕРЫ ЗАКУСОК',540,428,406,16,{font:`700 12px ${f.label}`,color:p.accent,maxLines:1});await photoGridSquare(ctx,s,p,540,455,406,300,3,2,{numbered:true,fonts:f}); + statCards(ctx,s,p,M,900,306,90,[[Math.round(classicBoxCount(s))||0,'КОРОБОК'],[(s.items||[]).length,'ПОЗИЦИЙ']],{dark:true,fonts:f});priceBand(ctx,s,p,376,900,306,90,{dark:true,fonts:f}); + const dgx=698,dgy=900;roundRect(ctx,dgx,dgy,248,90,16,'rgba(255,255,255,.04)',p.line);ctx.save();ctx.translate(dgx+124,dgy+45);ctx.rotate(Math.PI/4);ctx.fillStyle=p.accent;ctx.fillRect(-16,-16,32,32);ctx.restore();text(ctx,'СКИДКА ВКЛЮЧЕНА',dgx+124,dgy+70,248,14,{font:`700 9px ${f.label}`,color:p.muted,align:'center',maxLines:1}); + checklistCard(ctx,p,M,1010,470,170,'ДОСТАВКА И КОНТРОЛЬ КАЧЕСТВА',CHECK_LINES,{dark:true,fonts:f});addonRow(ctx,p,540,1010,406,170,ADDON_LINES.concat(['Посуда','Мебель']),{dark:true,fonts:f}); } canvas.dataset.sunSignatureTemplate=id;canvas.dataset.sunSignatureCover='1';return canvas; } @@ -194,7 +212,7 @@ } async function renderContentPages(s,id){ await ensureFontsReady(id); - const p=PALETTES[id],f=fontsFor(id),dark=id==='neon-menu'; + const p=PALETTES[id],f=fontsFor(id),dark=id!=='cream-elegance'; const items=s.items||[],top=120,bottom=H-90,rowH=dark?96:104,gap=dark?12:14; const perPage=Math.max(1,Math.floor((bottom-top+gap)/(rowH+gap))); const pageCount=Math.max(1,Math.ceil(items.length/perPage)); @@ -216,17 +234,11 @@ if(typeof baseRenderer!=='function')throw new Error('Base PDF renderer is unavailable'); const id=String(snapshot?.offerTemplateId||''); if(!SIGNATURE_SET.has(id))return baseRenderer(snapshot); - const selfContained=id==='cream-elegance'||id==='neon-menu'; - const innerId=INNER_THEME[id]||'light',inner={...snapshot,offerTemplateId:innerId}; - const [cover,innerPages]=await Promise.all([renderCover(snapshot,id),selfContained?renderContentPages(snapshot,id):baseRenderer(inner)]); + const [cover,innerPages]=await Promise.all([renderCover(snapshot,id),renderContentPages(snapshot,id)]); const pages=[cover,...(innerPages||[])],total=pages.length,p=PALETTES[id]||PALETTES['cream-elegance']; try{const ctx=cover.getContext('2d');ctx.save();ctx.setTransform(SCALE,0,0,SCALE,0,0);footer(ctx,p,1,total,snapshot?.brandName,fontsFor(id));ctx.restore()}catch(_){} - if(selfContained){ - const f=fontsFor(id); - (innerPages||[]).forEach((canvas,i)=>{try{const scale=canvas.width/W,ctx=canvas.getContext('2d');ctx.save();ctx.setTransform(scale,0,0,scale,0,0);footer(ctx,p,2+i,total,snapshot?.brandName,f);ctx.restore()}catch(_){}}); - }else{ - correctPageNumbers(innerPages||[],2,total,innerId); - } + const f=fontsFor(id); + (innerPages||[]).forEach((canvas,i)=>{try{const scale=canvas.width/W,ctx=canvas.getContext('2d');ctx.save();ctx.setTransform(scale,0,0,scale,0,0);footer(ctx,p,2+i,total,snapshot?.brandName,f);ctx.restore()}catch(_){}}); return pages; }