feat: bring the other 4 signature templates up to the same quality bar

- Fix the price band on cream-elegance: it was a solid muddy olive
  fill that didn't read as "this is your total" (reported directly by
  the user). It's now a white card with a gold border, matching the
  rest of that template's light card language; the dark templates'
  solid-fill price band is unchanged since that already reads well
  against a dark background.
- Give emerald-circles, midnight-checklist, gourmet-hero and
  diamond-gold the same treatment as the first 2: real fonts
  (Playfair Display/Montserrat or Unbounded/Manrope depending on
  aesthetic) instead of Arial/Georgia, the same center-alignment fixes
  applied everywhere text used align:'center', and their own
  fully-styled inner pages (menu + pricing) via the shared
  renderContentPages engine instead of the old generic base renderer.
- Fix two real regressions the font swap introduced and caught by
  rendering each template locally: gourmet-hero's headline was
  overflowing width and getting ellipsis-truncated ("КЕЙТЕРИНГ..."),
  and diamond-gold's heading was wrapping to a second line that
  collided with the subtitle below it. Both fixed with size/width
  adjustments verified by direct canvas measurement.
- All 6 signature templates are selectable in Settings again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
pavlov346346-source 2026-09-16 10:14:25 +03:00
parent 51e2fa4c33
commit d78ca14baf
2 changed files with 63 additions and 39 deletions

View File

@ -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;

View File

@ -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);
}
return pages;
}