Restore distinct proposal designs and improve PDF typography and pagination
@ -11,7 +11,7 @@
|
||||
"serverReady": true,
|
||||
"workspaceAutoDiscovery": true,
|
||||
"invitesTemporarilyDisabled": false,
|
||||
"pwaCache": "v91-20260918-trial-demo",
|
||||
"pwaCache": "v92-20260918-proposal-quality",
|
||||
"fullOfferDescriptions": true,
|
||||
"dynamicOfferRows": true,
|
||||
"pdfOfferDescriptionFix": true,
|
||||
@ -60,7 +60,15 @@
|
||||
"solar-experience",
|
||||
"midnight-compact",
|
||||
"emerald-gold",
|
||||
"neon-emerald"
|
||||
"neon-emerald",
|
||||
"cream-elegance",
|
||||
"neon-menu",
|
||||
"emerald-circles",
|
||||
"midnight-checklist",
|
||||
"gourmet-hero",
|
||||
"diamond-gold",
|
||||
"premium-dark",
|
||||
"premium-emerald"
|
||||
],
|
||||
"addressCandidateSelection": true,
|
||||
"addressMapConfirmation": true,
|
||||
@ -334,7 +342,7 @@
|
||||
"indexCacheBustCurrent": true,
|
||||
"backgroundImageMutationBatching": true,
|
||||
"opsBootWaitBounded": true,
|
||||
"offerTemplates": 13,
|
||||
"offerTemplates": 21,
|
||||
"offerTemplatePerOrder": true,
|
||||
"offerTemplateStructuralPreviews": true,
|
||||
"offerPdfDistinctLayouts": true,
|
||||
@ -393,5 +401,8 @@
|
||||
"suppliers": 5,
|
||||
"autoNewTrial": true,
|
||||
"preservesSolnce": true
|
||||
}
|
||||
},
|
||||
"proposalLayout": "core/proposal-layout.js",
|
||||
"proposalLocalCyrillicFonts": true,
|
||||
"proposalRasterDpi": 290
|
||||
}
|
||||
|
||||
19
docs/releases/2026-09-18-PROPOSAL-QUALITY.md
Normal file
@ -0,0 +1,19 @@
|
||||
# Client proposal PDF quality
|
||||
|
||||
Classic template selections were routed through the signature fallback and lost their original covers. Global settings only exposed 8 templates; grouping could also remove the modern buttons when classic choices were present. The new renderer supports all 21 existing IDs, retaining the order's selected style.
|
||||
|
||||
The 10 covers from commit `a3616b7` were rendered and visually compared with the current application. Their editorial, letter, ticket, photographic and sunny compositions informed the restored designs. Each of the 21 covers now has its own composition. Menu pages use matching fonts and colors, with measured list, compact and two-column card layouts.
|
||||
|
||||
Local, licensed Cyrillic fonts (Manrope and Playfair Display, including italic) finish loading before layout. Name, composition, quantity, unit price and line total are retained. Long descriptions flow across pages. Prices share a right edge; page numbers, company identity and contacts have reserved space. Gallery photos fill remaining space rather than adding empty pages. Per-order uploaded gallery images are resolved before conversion, for every template.
|
||||
|
||||
Preview and download use the same A4 canvases, at approximately 290 dpi for ordinary offers and 242 dpi for catalogs above 24 lines to bound memory. The existing raster PDF writer is retained. The generic selector thumbnails contain fictional demo data and Caterium branding. Company names/logos in actual exports still come from the workspace.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm run check:deploy` passed.
|
||||
- Full desktop/mobile suite: 104 passed, 2 skipped; 10 focused proposal checks passed after adding the real preview/download test.
|
||||
- 21 final PDFs, 92 A4 pages, rendered for visual inspection. Earlier/current references are outside the repository under `../caterium-pdf-20260918/tmp/pdfs/`.
|
||||
- Tests exercise all 21 template routes, local fonts, long Cyrillic text, page boundaries, collisions, privacy switches, custom photos, picker groups and actual PDF downloads.
|
||||
- Reproducible fictional fixtures: `ops/pdf/audit-proposals.mjs`, `render-audit.py`, `build-showcase.py`.
|
||||
|
||||
PWA cache: `v92-20260918-proposal-quality`. No database migration or customer data changes.
|
||||
47
ops/pdf/audit-proposals.mjs
Normal file
@ -0,0 +1,47 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {execFileSync} from 'node:child_process';
|
||||
import {chromium} from '@playwright/test';
|
||||
import {boxes} from '../demo/trial-demo-data.mjs';
|
||||
|
||||
// Local, fictional fixtures only. Never authenticates or writes customer orders.
|
||||
const phase=process.argv[2]||'after';
|
||||
const root=path.resolve('..','caterium-pdf-20260918','tmp','pdfs',phase);
|
||||
await fs.mkdir(root,{recursive:true});
|
||||
const browser=await chromium.launch();
|
||||
const page=await browser.newPage();
|
||||
await page.route(/https:\/\/(?!127\.0\.0\.1)/,r=>r.abort());
|
||||
await page.goto('http://127.0.0.1:4173/index.html',{waitUntil:'domcontentloaded'});
|
||||
await page.waitForFunction(()=>Boolean(window.sunClientOfferDebugPdfPages));
|
||||
const image=async file=>'data:image/'+(file.endsWith('.png')?'png':'jpeg')+';base64,'+(await fs.readFile(path.resolve('public',file))).toString('base64');
|
||||
const items=await Promise.all(boxes.map(async(b,i)=>({...b,categoryId:0,categoryName:'Боксы',qty:i===0?2:1,unitPrice:b.price,sum:b.price*(i===0?2:1),photoData:await image(b.photo)})));
|
||||
const base=items.reduce((n,i)=>n+i.sum,0);
|
||||
const snapshot={brandName:'Солнце Кейтеринг',brandCity:'Санкт-Петербург',brandContacts:'Ваш менеджер · +7 (900) 000-00-00',logo:await image('sun-logo.png'),client:'Анна и Михаил',event:'Вечер в кругу друзей',date:'2026-10-24',time:'18:00',guests:20,items,pricing:{base,manual:1500,promo:0,discount:1500,itemsTotal:base-1500,delivery:1500,total:base},foodGrams:11000,foodPieces:192,controlLines:['Согласуем время и адрес доставки','Подготовим заказ к вашему мероприятию'],extraServices:['Сервировка стола','Обслуживание мероприятия'],finalGallery:[await image('offer-gallery/001.jpg'),await image('offer-gallery/002.jpg')]};
|
||||
if(phase==='thumbs'){snapshot.brandName='Caterium';snapshot.brandCity='';snapshot.logo='';snapshot.client='Анна и Михаил'}
|
||||
await page.evaluate(s=>window.__pdfFixture=s,snapshot);
|
||||
const ids=await page.evaluate(()=>[...SunClassicOfferPDFV1767.CLASSIC_IDS,...SunClassicOfferPDFV1767.ARCHIVE_IDS,...SunSignatureOfferPDFV18.SIGNATURE_IDS]);
|
||||
if(phase==='early'){
|
||||
const source=execFileSync('git',['show','a3616b7:public/core/classic-offer-pdf-v1767.js'],{encoding:'utf8'});
|
||||
await page.evaluate(()=>{delete window.SunClassicOfferPDFV1767});
|
||||
await page.addScriptTag({content:source});
|
||||
}
|
||||
const report=[];
|
||||
for(const id of phase==='early'?ids.slice(0,10):ids){
|
||||
const result=await page.evaluate(async({id,phase})=>{
|
||||
const s={...window.__pdfFixture,offerTemplateId:id};
|
||||
const pages=phase==='early'?[await SunClassicOfferPDFV1767.renderCover(s,id)]:await sunClientOfferDebugPdfPages(s);
|
||||
const jpg=pages.map(c=>({width:c.width,height:c.height,bytes:Uint8Array.from(atob(c.toDataURL('image/jpeg',.94).split(',')[1]),c=>c.charCodeAt(0))}));
|
||||
const blob=SunPdfEngine.fromJpegs(jpg);
|
||||
const pdf=await new Promise(resolve=>{const r=new FileReader();r.onload=()=>resolve(r.result.split(',')[1]);r.readAsDataURL(blob)});
|
||||
const thumb=document.createElement('canvas');thumb.width=320;thumb.height=452;thumb.getContext('2d').drawImage(pages[0],0,0,320,452);
|
||||
const thumbnail=thumb.toDataURL('image/jpeg',.88).split(',')[1];
|
||||
const info={id,pages:pages.length,width:pages[0].width,cover:pages[0].dataset.sunProposalTemplate||pages[0].dataset.sunClassicTemplate||pages[0].dataset.sunSignatureTemplate||null,layout:pages.map(p=>p.__proposalLayout||null)};
|
||||
pages.forEach(c=>{c.width=0;c.height=0});
|
||||
return {pdf,info,thumbnail};
|
||||
},{id,phase});
|
||||
if(phase==='thumbs')await fs.writeFile(path.resolve('public','offer-templates','quality-'+id+'.jpg'),Buffer.from(result.thumbnail,'base64'));
|
||||
else await fs.writeFile(path.join(root,id+'.pdf'),Buffer.from(result.pdf,'base64'));
|
||||
report.push(result.info);console.log(id,result.info.pages,result.info.cover);
|
||||
}
|
||||
await fs.writeFile(path.join(root,'report.json'),JSON.stringify(report,null,2));
|
||||
await browser.close();
|
||||
60
ops/pdf/build-showcase.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""Overview plus unchanged PDFs exported by the application. Fictional order data."""
|
||||
from pathlib import Path
|
||||
import json
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.colors import HexColor
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
root=Path(__file__).resolve().parents[3]/'caterium-pdf-20260918'
|
||||
audit=root/'tmp'/'pdfs'/'after'
|
||||
output=root/'output'/'pdf';output.mkdir(parents=True,exist_ok=True)
|
||||
pdfmetrics.registerFont(TTFont('Body','C:/Windows/Fonts/arial.ttf'))
|
||||
pdfmetrics.registerFont(TTFont('Title','C:/Windows/Fonts/georgia.ttf'))
|
||||
names=['Минимализм','Luxury Dark','Editorial Magazine','Warm Sun','Bento Cards','Event Story','Food First','Personal Letter','Event Ticket','Solar Experience','Ночной минимализм','Изумрудная классика','Изумрудный акцент','Кремовая классика','Неоновое меню','Изумрудные круги','Тёмный чек-лист','Гастро-витрина','Изумруд и золото','Премиум тёмный','Премиум изумрудный']
|
||||
rows=json.loads((audit/'report.json').read_text(encoding='utf8'))
|
||||
overview=root/'tmp'/'pdfs'/'overview.pdf'
|
||||
W,H=595.28,841.89
|
||||
c=canvas.Canvas(str(overview),pagesize=(W,H))
|
||||
c.setTitle('Caterium - обновлённые предложения клиенту')
|
||||
def background():
|
||||
c.setFillColor(HexColor('#faf7ef'));c.rect(0,0,W,H,fill=1,stroke=0)
|
||||
c.setFillColor(HexColor('#20362d'))
|
||||
def text(x,y,value,size=12,font='Body'):
|
||||
c.setFont(font,size);c.drawString(x,y,value)
|
||||
background()
|
||||
text(44,H-66,'Caterium',17)
|
||||
text(44,H-150,'Предложения,',38,'Title')
|
||||
text(44,H-198,'которые хочется',38,'Title')
|
||||
text(44,H-246,'отправить клиенту',38,'Title')
|
||||
text(44,H-304,'Обзор 21 стиля и три полных образца',15)
|
||||
c.setStrokeColor(HexColor('#b19464'));c.line(44,H-340,W-44,H-340)
|
||||
text(44,H-380,'Что изменилось',20,'Title')
|
||||
for i,line in enumerate(['Вернули композиции первых обложек и развели стили.', 'Выровняли поля, цены и карточки меню.', 'Сохранили полные названия, состав, вес и расчёт стоимости.', 'Шрифты загружаются вместе с приложением.', 'Предпросмотр и скачанный PDF используют одни страницы.']):
|
||||
text(44,H-418-i*26,line,11)
|
||||
text(44,190,'В этом файле',20,'Title')
|
||||
text(44,158,'Стр. 2-4: обзор всех вариантов оформления.',11)
|
||||
starts={};page_no=5
|
||||
selected=['light','editorial-grid','premium-dark']
|
||||
for id in selected:
|
||||
i=next(i for i,r in enumerate(rows) if r['id']==id);count=rows[i]['pages'];starts[id]=page_no
|
||||
text(44,130-selected.index(id)*24,f'Стр. {page_no}-{page_no+count-1}: {names[i]}.',11);page_no+=count
|
||||
text(44,39,'18 сентября 2026 · Учебный заказ, не клиентская заявка',9)
|
||||
c.showPage()
|
||||
for start in range(0,len(rows),9):
|
||||
background();text(34,H-44,'Коллекция оформлений',25,'Title');text(34,H-69,f'{start+1}-{min(start+9,len(rows))} из 21',11)
|
||||
for i,row in enumerate(rows[start:start+9]):
|
||||
col=i%3;line=i//3;x=34+col*178;y=H-106-line*235
|
||||
c.drawImage(str(audit/(row['id']+'-1.png')),x,y-209,width=148,height=209)
|
||||
text(x,y-224,f'{start+i+1:02d} {names[start+i]}',8.4)
|
||||
text(34,27,'Полные примеры далее. Любой стиль доступен в приложении.',9)
|
||||
c.showPage()
|
||||
c.save()
|
||||
writer=PdfWriter();writer.append(PdfReader(str(overview)))
|
||||
for id in selected:writer.append(PdfReader(str(audit/(id+'.pdf'))))
|
||||
writer.add_metadata({'/Title':'Caterium - коллекция предложений клиенту','/Author':'Caterium'})
|
||||
target=output/'Caterium-предложения-образцы.pdf'
|
||||
writer.write(str(target))
|
||||
print(target, 'pages:',len(writer.pages))
|
||||
29
ops/pdf/render-audit.py
Normal file
@ -0,0 +1,29 @@
|
||||
import sys, json
|
||||
from pathlib import Path
|
||||
import pypdfium2 as pdfium
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
base=Path(__file__).resolve().parents[3]/'caterium-pdf-20260918'/'tmp'/'pdfs'
|
||||
for phase in sys.argv[1:] or ['after']:
|
||||
root=base/phase
|
||||
rows=json.loads((root/'report.json').read_text(encoding='utf8'))
|
||||
covers=[]; pages=[]
|
||||
for row in rows:
|
||||
doc=pdfium.PdfDocument(str(root/(row['id']+'.pdf')))
|
||||
for n in range(len(doc)):
|
||||
im=doc[n].render(scale=1.25).to_pil().convert('RGB')
|
||||
im.save(root/f"{row['id']}-{n+1}.png")
|
||||
pages.append((row['id']+f' / {n+1}',im))
|
||||
if n==0: covers.append(pages[-1])
|
||||
def sheet(entries,name,cols=4):
|
||||
tw,th=250,380
|
||||
out=Image.new('RGB',(tw*cols,th*((len(entries)+cols-1)//cols)),'#dce1e5')
|
||||
d=ImageDraw.Draw(out)
|
||||
for i,(label,im) in enumerate(entries):
|
||||
tile=im.copy(); tile.thumbnail((tw-12,th-32))
|
||||
x=(i%cols)*tw; y=(i//cols)*th
|
||||
out.paste(tile,(x+6,y+25));d.text((x+6,y+6),label,fill='black')
|
||||
out.save(root/name)
|
||||
sheet(covers,'covers.png')
|
||||
for start in range(0,len(pages),12):sheet(pages[start:start+12],f'pages-{start//12+1}.png')
|
||||
print(phase,len(pages),'pages rendered')
|
||||
@ -4,7 +4,7 @@
|
||||
"version": "17.7.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js && node --check public/core/trial-demo.js",
|
||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/account-center-v1780.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/trial-promo-developer-v181.js && node --check public/core/order-enhancements-v1775.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/signature-offer-pdf-v18.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js && node --check public/core/brand-theme.js && node --check public/core/company-branding.js && node --check public/core/import-archive.js && node --check public/core/access-policy.js && node --check public/core/banquet-menu.js && node --check public/core/cloud-transport.js && node --check public/core/trial-demo.js && node --check public/core/proposal-layout.js",
|
||||
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs && node tests/html-integrity-v1774.mjs && node tests/edge-security-v1774.mjs && node tests/branding-v1774.mjs && node tests/order-enhancements-v1775.mjs",
|
||||
"check:release": "node tests/release-check.mjs",
|
||||
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release && node tests/backend-cutover.mjs && npm run test:db",
|
||||
|
||||
@ -977,6 +977,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
||||
}
|
||||
function sectionTitle(item){const s=String(item?.catalogSection||'').trim();return s||String(item?.categoryName||'Меню')}
|
||||
function canapePieces(item){
|
||||
if(item?.demo&&Number(item.pieces)===0)return 0;
|
||||
// v17.5.1: any food box may contain countable ready-to-serve pieces, not only sections named "canape".
|
||||
if(![0,5].includes(Number(item?.category??item?.categoryId??0)))return 0;
|
||||
const explicit=Math.max(0,num(item?.pieces));
|
||||
@ -1140,6 +1141,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
||||
}
|
||||
async function preparedPdfSnapshot(prepared){
|
||||
const copy=structuredClone(prepared),cache=new Map();
|
||||
if(copy.orderId&&window.SunOfferWorkspaceV1769?.galleryFor)copy.finalGallery=window.SunOfferWorkspaceV1769.galleryFor(copy.orderId);
|
||||
const resolve=src=>{const key=String(src||'');if(!key)return Promise.resolve('');if(!cache.has(key))cache.set(key,asDataUrl(key));return cache.get(key);};
|
||||
copy.logo=await resolve(copy.logo);
|
||||
copy.finalGallery=(await Promise.all((copy.finalGallery||[]).map(resolve))).filter(Boolean);
|
||||
@ -1661,8 +1663,10 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
||||
return canvases;
|
||||
}
|
||||
async function renderOfferPdfPages(s){
|
||||
const quality=window.CateriumProposalPDF;
|
||||
if(quality?.IDS?.includes(s.offerTemplateId||offerTemplateId()))return quality.renderPages({...s,offerTemplateId:s.offerTemplateId||offerTemplateId(),pdfSettings:offerSettingsForSnapshot(s)});
|
||||
const signature=window.SunSignatureOfferPDFV18,classic=window.SunClassicOfferPDFV1767;
|
||||
if(signature?.renderPages)return signature.renderPages(s,renderOfferPdfPagesBase);
|
||||
if(signature?.SIGNATURE_IDS?.includes(s.offerTemplateId)&&signature?.renderPages)return signature.renderPages(s,renderOfferPdfPagesBase);
|
||||
if(classic?.renderPages)return classic.renderPages(s,renderOfferPdfPagesBase);
|
||||
return renderOfferPdfPagesBase(s);
|
||||
}
|
||||
@ -4217,7 +4221,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
||||
|
||||
const KEY='sunOfferTemplateV1';
|
||||
const DEFAULT_ID='cream-elegance';
|
||||
const TEMPLATES=[
|
||||
const TEMPLATES=window.CateriumProposalPDF?.templates||[
|
||||
{id:'cream-elegance',name:'Кремовая классика',desc:'Светлый премиальный дизайн: крупная обложка, круглые иконки категорий и аккуратная сетка меню.',thumb:''},
|
||||
{id:'neon-menu',name:'Неоновое меню',desc:'Тёмный дизайн с неоновыми акцентами, нумерованной фотосеткой блюд и карточками категорий.',thumb:''},
|
||||
{id:'emerald-circles',name:'Изумрудные круги',desc:'Тёмно-изумрудная презентация с круглыми фото блюд и списком категорий.',thumb:''},
|
||||
@ -4267,6 +4271,7 @@ window.SUN_LEGACY_CATALOG_V175=[];
|
||||
document.head.appendChild(style);
|
||||
|
||||
function offerTemplateMini(id){
|
||||
if(window.CateriumProposalPDF?.IDS.includes(id))return `<img src="offer-templates/quality-${id}.jpg?v=20260918" alt="" loading="lazy" style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#eeece5;z-index:2">`;
|
||||
if(id==='editorial-grid')return '<i class="mini-head"></i><i class="mini-editorial-hero"></i><i class="mini-editorial-stack a"></i><i class="mini-editorial-stack b"></i><i class="mini-editorial-grid"></i>';
|
||||
if(id==='midnight-glass')return '<i class="mini-head"></i><i class="mini-midnight-hero"></i><i class="mini-midnight-total"></i><i class="mini-midnight-left"></i><i class="mini-midnight-right"></i>';
|
||||
if(id==='emerald-gold')return '<i class="mini-head"></i><i class="mini-emerald-hero"></i><i class="mini-emerald-gold"></i><i class="mini-emerald-info"></i><i class="mini-emerald-menu"></i>';
|
||||
|
||||
@ -112,7 +112,7 @@
|
||||
.sun-v1767-group-label{grid-column:1/-1;font-size:10px;font-weight:900;letter-spacing:.07em;text-transform:uppercase;color:#7d858c;padding:6px 2px 1px}
|
||||
#sunClientOfferPreview .sun-offer-page-canvas[data-sun-classic-cover="1"]{box-shadow:0 18px 50px rgba(31,35,36,.16)}
|
||||
`;document.head.appendChild(style)}
|
||||
function groupPicker(){const grid=document.querySelector('.sun-v1764-offer-template-grid');if(!grid||grid.dataset.sunV1767Grouped==='1')return;const classic=[...grid.querySelectorAll('button')].filter(b=>CLASSIC_SET.has(b.dataset.v1764OfferTemplate)),archive=[...grid.querySelectorAll('button')].filter(b=>ARCHIVE_SET.has(b.dataset.v1764OfferTemplate));if(!classic.length&&!archive.length)return;grid.dataset.sunV1767Grouped='1';const label=t=>{const d=document.createElement('div');d.className='sun-v1767-group-label';d.textContent=t;return d};grid.innerHTML='';if(classic.length){grid.appendChild(label('Классические — как раньше'));classic.forEach(b=>grid.appendChild(b))}if(archive.length){grid.appendChild(label('Архивные шаблоны'));archive.forEach(b=>grid.appendChild(b))}}
|
||||
function groupPicker(){const grid=document.querySelector('.sun-v1764-offer-template-grid');if(!grid||grid.dataset.sunV1767Grouped==='1')return;const classic=[...grid.querySelectorAll('button')].filter(b=>CLASSIC_SET.has(b.dataset.v1764OfferTemplate)),archive=[...grid.querySelectorAll('button')].filter(b=>ARCHIVE_SET.has(b.dataset.v1764OfferTemplate));if(!classic.length&&!archive.length)return;grid.dataset.sunV1767Grouped='1';const label=t=>{const d=document.createElement('div');d.className='sun-v1767-group-label';d.textContent=t;return d};const modern=[...grid.querySelectorAll('button')].filter(b=>!CLASSIC_SET.has(b.dataset.v1764OfferTemplate)&&!ARCHIVE_SET.has(b.dataset.v1764OfferTemplate));grid.innerHTML='';if(classic.length){grid.appendChild(label('Классические — как раньше'));classic.forEach(b=>grid.appendChild(b))}if(modern.length){grid.appendChild(label('Современная коллекция'));modern.forEach(b=>grid.appendChild(b))}if(archive.length){grid.appendChild(label('Дополнительные стили'));archive.forEach(b=>grid.appendChild(b))}}
|
||||
function maintain(){installPreviewStyles();groupPicker()}
|
||||
const observer=new MutationObserver(()=>setTimeout(maintain,0));if(document.documentElement)observer.observe(document.documentElement,{childList:true,subtree:true});if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',maintain,{once:true});else maintain();
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
(()=>{
|
||||
'use strict';
|
||||
const VERSION='17.7.3';
|
||||
const RELEASE='20260918-trial-demo';
|
||||
const RELEASE='20260918-proposal-quality';
|
||||
|
||||
const hasStoredSession=()=>{try{return Object.keys(localStorage).some(k=>/^sb-.*-auth-token$/i.test(k)&&String(localStorage.getItem(k)||'').length>20)}catch(_){return false}};
|
||||
function installAuthBoot(){
|
||||
|
||||
247
public/core/proposal-layout.js
Normal file
@ -0,0 +1,247 @@
|
||||
/* Client proposals. A4 layouts inspired by the original v17.6.7 covers.
|
||||
* Coordinates are shared by preview and download; text is measured after local fonts load.
|
||||
*/
|
||||
(()=>{
|
||||
'use strict';
|
||||
const W=1000,H=1414,M=70,BOTTOM=1310,SCALE=2.4;
|
||||
const SANS='"Caterium Manrope",Arial,sans-serif';
|
||||
const SERIF='"Caterium Playfair",Georgia,serif';
|
||||
const ITALIC='"Caterium Playfair Italic",Georgia,serif';
|
||||
const definitions=[
|
||||
['light','Минимализм','split','#fffdf8','#20362d','#98713b','serif','rows'],
|
||||
['midnight-glass','Luxury Dark','night','#111916','#f6f1e5','#d4b272','serif','rows'],
|
||||
['editorial-grid','Editorial Magazine','editorial','#f6f3ec','#183d30','#94743c','serif','cards'],
|
||||
['warm-sun','Warm Sun','sun','#fff4d8','#51361e','#9a5b1b','serif','rows'],
|
||||
['bento-cards','Bento Cards','bento','#f3f2eb','#27382b','#8b6022','sans','cards'],
|
||||
['event-story','Event Story','story','#f4ece0','#523a2b','#92643c','serif','rows'],
|
||||
['black-gold','Food First','photo','#10100e','#fff7e9','#d8b671','serif','cards'],
|
||||
['personal-letter','Personal Letter','letter','#f7f1e6','#473b2d','#8b673b','italic','rows'],
|
||||
['event-ticket','Event Ticket','ticket','#eef2f3','#203f53','#38677a','sans','rows'],
|
||||
['solar-experience','Solar Experience','solar','#ffd54f','#28392c','#77521f','sans','cards'],
|
||||
['midnight-compact','Ночной минимализм','compact','#122630','#edf4f4','#9cbec4','sans','compact'],
|
||||
['emerald-gold','Изумрудная классика','frame','#123b31','#f8f2df','#d1b578','serif','rows'],
|
||||
['neon-emerald','Изумрудный акцент','neon-vertical','#071f1d','#edfff6','#7debaf','sans','compact'],
|
||||
['cream-elegance','Кремовая классика','arch','#faf3e6','#35432e','#917445','serif','cards'],
|
||||
['neon-menu','Неоновое меню','neon','#111a15','#f1ffef','#a1ed92','sans','compact'],
|
||||
['emerald-circles','Изумрудные круги','circles','#102e2c','#f0f6ee','#d3b779','serif','cards'],
|
||||
['midnight-checklist','Тёмный чек-лист','checklist','#12241d','#f1f5ea','#d7c593','sans','rows'],
|
||||
['gourmet-hero','Гастро-витрина','gallery','#181611','#fff6e6','#dbb775','serif','cards'],
|
||||
['diamond-gold','Изумруд и золото','diamond','#133329','#fff6de','#dabe7b','serif','rows'],
|
||||
['premium-dark','Премиум тёмный','panorama','#141820','#f7f2e9','#cfb18a','sans','rows'],
|
||||
['premium-emerald','Премиум изумрудный','botanical','#0e382e','#f5f2df','#b8d19c','italic','cards']
|
||||
];
|
||||
const THEMES=Object.fromEntries(definitions.map(([id,name,cover,bg,ink,accent,type,menu])=>[id,{id,name,cover,bg,ink,accent,type,menu,dark:!['light','editorial-grid','warm-sun','bento-cards','event-story','personal-letter','event-ticket','solar-experience','cream-elegance'].includes(id)}]));
|
||||
const IDs=Object.keys(THEMES);
|
||||
const number=v=>Number.isFinite(Number(v))?Number(v):0;
|
||||
const money=v=>Math.round(number(v)).toLocaleString('ru-RU')+' ₽';
|
||||
const qty=v=>number(v).toLocaleString('ru-RU',{maximumFractionDigits:3});
|
||||
const date=v=>{if(!v)return '';const d=new Date(String(v)+'T12:00:00');return Number.isNaN(d.getTime())?String(v):d.toLocaleDateString('ru-RU',{day:'numeric',month:'long',year:'numeric'})};
|
||||
let fontsPromise;
|
||||
async function ready(){
|
||||
if(!fontsPromise)fontsPromise=(async()=>{
|
||||
if(!window.FontFace||!document.fonts)return;
|
||||
const defs=[['Caterium Manrope','Manrope.ttf','200 800'],['Caterium Playfair','PlayfairDisplay.ttf','400 900'],['Caterium Playfair Italic','PlayfairDisplay-Italic.ttf','400 900']];
|
||||
await Promise.all(defs.map(async([family,file,weight])=>{
|
||||
const face=new FontFace(family,`url("${new URL('fonts/'+file,document.baseURI)}")`,{weight});
|
||||
try{await Promise.race([face.load(),new Promise((_,reject)=>setTimeout(()=>reject(new Error('Font timeout')),8000))]);document.fonts.add(face)}catch(e){console.warn('Proposal font fallback:',family,e.message)}
|
||||
}));
|
||||
await document.fonts.ready;
|
||||
})();
|
||||
return fontsPromise;
|
||||
}
|
||||
function loadImage(src){return new Promise(resolve=>{
|
||||
if(!/^data:image\/(?:png|jpe?g|webp);base64,/i.test(String(src||'')))return resolve(null);
|
||||
const image=new Image();let done=false;
|
||||
const finish=v=>{if(done)return;done=true;clearTimeout(timer);resolve(v)};
|
||||
const timer=setTimeout(()=>finish(null),4000);image.onload=()=>finish(image);image.onerror=()=>finish(null);image.src=src;
|
||||
})}
|
||||
function split(ctx,value,width){
|
||||
const out=[];
|
||||
for(const para of String(value??'').split(/\r?\n/)){
|
||||
if(!para.trim()){out.push('');continue}
|
||||
let line='';
|
||||
for(const word of para.trim().split(/\s+/)){
|
||||
if(ctx.measureText((line?line+' ':'')+word).width<=width){line+=(line?' ':'')+word;continue}
|
||||
if(line){out.push(line);line=''}
|
||||
for(const char of word){if(line&&ctx.measureText(line+char).width>width){out.push(line);line=''}line+=char}
|
||||
}
|
||||
if(line)out.push(line);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
const defaults={showClientName:true,showDate:true,showGuests:true,showGallery:true,showPriceBreakdown:true,showAmountPerGuest:true,showBoxCount:true,showControl:true,showExtras:true,showFooterNote:true,showFinalPhoto:true,metricMode:'weight',texts:{}};
|
||||
function settings(s){return {...defaults,...s.pdfSettings,texts:{...defaults.texts,...s.pdfSettings?.texts,...s.offerOverrides?.texts}}}
|
||||
function painter(canvas,theme){
|
||||
const ctx=canvas.getContext('2d',{alpha:false});const scale=canvas.width/W;ctx.setTransform(scale,0,0,scale,0,0);
|
||||
const boxes=canvas.__proposalLayout||[];canvas.__proposalLayout=boxes;
|
||||
const body=(size=19,weight=400)=>`${weight} ${size}px ${SANS}`;
|
||||
const display=(size=54)=>`${theme.type==='sans'?700:500} ${size}px ${theme.type==='sans'?SANS:theme.type==='italic'?ITALIC:SERIF}`;
|
||||
function rect(x,y,w,h,color,r=0){ctx.fillStyle=color;ctx.beginPath();ctx.roundRect(x,y,w,h,r);ctx.fill()}
|
||||
function line(x,y,w,color=theme.accent){rect(x,y,w,1,color)}
|
||||
function lines(value,w,font){ctx.font=font;return split(ctx,value,w)}
|
||||
function write(value,x,y,w,{size=19,weight=400,color=theme.ink,font=body(size,weight),lh=size*1.42,align='left',role='text'}={}){
|
||||
const values=Array.isArray(value)?value:lines(value,w,font);ctx.save();ctx.font=font;ctx.textBaseline='top';ctx.textAlign=align;ctx.fillStyle=color;
|
||||
const xx=align==='right'?x+w:align==='center'?x+w/2:x;
|
||||
values.forEach((v,i)=>ctx.fillText(v,xx,y+i*lh));ctx.restore();
|
||||
boxes.push({x,y,w,h:values.length*lh,role,text:values.join('\n'),font});return values.length*lh;
|
||||
}
|
||||
function fit(value,x,y,w,h,{size=54,min=18,font,displayFont=true,...opts}={}){
|
||||
let f,ls,lh;
|
||||
do{f=font|| (displayFont?display(size):body(size,opts.weight||500));lh=size*1.27;ls=lines(value,w,f);if(ls.length*lh<=h)break;size-=1}while(size>=min);
|
||||
// Cover copy is repeated in the flowing details when unusually long.
|
||||
if(ls.length*lh>h){canvas.__proposalOverflow=canvas.__proposalOverflow||[];canvas.__proposalOverflow.push(String(value));return write('Подробности на следующих страницах',x,y,w,{size:18,lh:25,...opts})}
|
||||
return write(ls,x,y,w,{...opts,font:f,size,lh});
|
||||
}
|
||||
function photo(img,x,y,w,h,{radius=0,shape='rect',contain=false}={}){
|
||||
ctx.save();ctx.beginPath();if(shape==='circle')ctx.ellipse(x+w/2,y+h/2,w/2,h/2,0,0,Math.PI*2);else if(shape==='diamond'){ctx.moveTo(x+w/2,y);ctx.lineTo(x+w,y+h/2);ctx.lineTo(x+w/2,y+h);ctx.lineTo(x,y+h/2);ctx.closePath()}else ctx.roundRect(x,y,w,h,radius);ctx.clip();
|
||||
rect(x,y,w,h,theme.dark?'#263e34':'#e9e3d7');
|
||||
if(img){const scale=(contain?Math.min:Math.max)(w/img.width,h/img.height);ctx.drawImage(img,x+(w-img.width*scale)/2,y+(h-img.height*scale)/2,img.width*scale,img.height*scale)}
|
||||
ctx.restore();
|
||||
}
|
||||
return {ctx,body,display,rect,line,lines,write,fit,photo};
|
||||
}
|
||||
function page(t,kind){const c=document.createElement('canvas');c.width=W*(t.scale||SCALE);c.height=Math.round(H*(t.scale||SCALE));c.dataset.sunProposalTemplate=t.id;c.dataset.sunProposalPage=kind;const d=painter(c,t);d.rect(0,0,W,H,t.bg);return {c,...d}}
|
||||
function brand(d,s,t,logo,{x=M,y=60,w=860}={}){
|
||||
if(logo){d.rect(x,y-4,78,58,'#fffdf6',8);d.photo(logo,x+6,y,66,50,{contain:true});x+=94;w-=94}
|
||||
d.fit(s.brandName||'Моя компания',x,y+6,w,56,{size:24,min:15,displayFont:false,weight:700});
|
||||
}
|
||||
function facts(s,cfg){return [cfg.showDate?[date(s.date),s.time].filter(Boolean).join(' · '):'',cfg.showGuests&&number(s.guests)?`${qty(s.guests)} гостей`:''].filter(Boolean)}
|
||||
function cover(s,t,images,logo,cfg){
|
||||
const d=page(t,'cover'),hero=images.find(Boolean),hero2=images.filter(Boolean)[1]||hero,hero3=images.filter(Boolean)[2]||hero;
|
||||
const title=s.event||'Ваше мероприятие',client=cfg.showClientName?s.client||'':'',eyebrow=cfg.texts.proposalLabel||'Индивидуальное предложение';
|
||||
const ink=t.ink,accent=t.accent;
|
||||
const heading=(x,y,w,h=250,size=64)=>d.fit(title,x,y,w,h,{size});
|
||||
const sub=(x,y,w,h=94)=>d.fit(client,x,y,w,h,{size:25,displayFont:false});
|
||||
const label=(x,y,w=860)=>d.fit(eyebrow,x,y,w,54,{size:18,displayFont:false,color:accent});
|
||||
const fact=(x,y,w=860)=>d.fit(facts(s,cfg).join(' / '),x,y,w,65,{size:21,displayFont:false});
|
||||
const note=(x,y,w,h=140)=>d.fit(cfg.texts.heroNote||'Меню, подобранное для вашего события.',x,y,w,h,{size:22,displayFont:false,color:ink});
|
||||
const total=(x,y,w=400)=>{d.fit(cfg.texts.totalPriceTitle||'Итоговая стоимость',x,y,w,50,{size:17,displayFont:false,color:accent});d.fit(money(s.pricing?.total),x,y+50,w,100,{size:52})};
|
||||
if(!['night','photo'].includes(t.cover))brand(d,s,t,logo);
|
||||
switch(t.cover){
|
||||
case 'split':
|
||||
label(M,198,400);heading(M,265,414,290,62);sub(M,582,395);d.line(M,718,370);note(M,758,380);d.photo(hero,520,195,410,760,{radius:180});fact(M,1010);total(M,1120);break;
|
||||
case 'night':
|
||||
d.photo(hero,482,0,518,H);d.rect(0,0,482,H,t.bg);brand(d,s,t,logo,{w:340});label(M,235,350);heading(M,310,350,310,58);sub(M,650,345);fact(M,835,345);d.line(M,1000,340);total(M,1050,350);break;
|
||||
case 'editorial':
|
||||
label(M,180);heading(M,244,840,200,72);d.line(M,478,860);d.photo(hero,414,526,516,610);d.write('01',M,530,230,{size:54,font:d.display(54),color:accent});sub(M,638,280,145);fact(M,825,290);total(M,999,310);break;
|
||||
case 'sun':
|
||||
d.rect(758,180,172,172,'#f2c55b',86);label(M,200,550);heading(M,280,630,250,64);sub(M,562,780);d.photo(hero,320,698,610,390,{radius:180});fact(M,649,850);total(M,1140,530);break;
|
||||
case 'bento':
|
||||
label(M,180);heading(M,255,850,200,67);sub(M,480,800);d.photo(hero,M,610,556,430,{radius:26});d.photo(hero2,650,610,280,202,{radius:24});d.photo(hero3,650,838,280,202,{radius:24});fact(M,1080);total(M,1160,740);break;
|
||||
case 'story':
|
||||
label(M,182);heading(M,260,835,200,68);sub(M,485,820);d.line(M,635,860);['Меню','Подготовка','Ваше событие'].forEach((v,i)=>{const x=M+i*310;d.rect(x,627,16,16,accent,8);d.write(v,x,664,240,{size:18})});d.photo(hero,M,735,860,360);fact(M,1140,410);total(553,1130,377);break;
|
||||
case 'photo':{
|
||||
d.photo(hero,0,0,W,H);const g=d.ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'rgba(0,0,0,.82)');g.addColorStop(.48,'rgba(0,0,0,.24)');g.addColorStop(1,'rgba(0,0,0,.95)');d.ctx.fillStyle=g;d.ctx.fillRect(0,0,W,H);brand(d,s,t,logo);label(M,215);heading(M,290,800,270,74);sub(M,620,800);fact(M,1010);total(M,1120,820);break;}
|
||||
case 'letter':
|
||||
d.rect(44,162,912,1125,'#fffcf5',4);label(94,218,700);d.fit(client?`${client},`:'Дорогие гости,',94,308,760,165,{size:56});d.write('Предложение для вашего мероприятия',94,496,730,{size:22});heading(94,549,730,170,44);note(94,761,470,155);d.photo(hero,690,769,210,210,{shape:'circle'});fact(94,1010,790);total(94,1100,570);break;
|
||||
case 'ticket':
|
||||
d.rect(M,190,860,1080,'#fbfcfa',18);d.rect(740,190,190,1080,'#dbe8ea',18);d.ctx.save();d.ctx.setLineDash([7,9]);d.ctx.strokeStyle=accent;d.ctx.beginPath();d.ctx.moveTo(716,215);d.ctx.lineTo(716,1240);d.ctx.stroke();d.ctx.restore();label(106,246,570);heading(106,329,560,270,59);sub(106,632,550,115);fact(106,790,550);d.photo(hero,106,899,255,200,{radius:10});total(390,946,280);d.write('МЕНЮ',762,268,150,{size:17,weight:700});if(cfg.showDate){d.fit(date(s.date),765,341,140,180,{size:26,displayFont:false});d.fit(s.time||'',765,570,140,80,{size:34,displayFont:false})}break;
|
||||
case 'solar':
|
||||
label(M,205);heading(M,276,460,300,64);d.photo(hero,571,390,359,359,{shape:'circle'});sub(M,649,435,150);d.line(M,860,860);fact(M,913);d.rect(M,1040,860,238,'#fff1b5',30);total(106,1080,780);break;
|
||||
case 'compact':
|
||||
label(M,212);heading(M,300,850,250,70);sub(M,590,820);fact(M,760);d.line(M,867,860);d.photo(hero,550,935,380,330);total(M,980,420);break;
|
||||
case 'frame':
|
||||
d.ctx.strokeStyle=accent;d.ctx.lineWidth=1;d.ctx.strokeRect(38,158,924,1140);label(96,227,808);d.fit(title,96,313,808,255,{size:67,align:'center'});d.fit(client,96,604,808,90,{size:24,displayFont:false,align:'center'});d.photo(hero,240,750,520,300,{radius:160});fact(96,1103,810);total(96,1175,810);break;
|
||||
case 'neon-vertical':
|
||||
d.rect(M,218,12,910,accent);label(114,218,815);heading(114,300,800,240,72);sub(114,571,780);d.photo(hero,114,750,390,390,{radius:195});d.photo(hero2,536,750,390,390,{radius:195});fact(114,668,800);total(114,1170,790);break;
|
||||
case 'arch':
|
||||
label(M,189);d.fit(title,M,265,860,205,{size:65,align:'center'});d.fit(client,M,490,860,94,{size:24,displayFont:false,align:'center'});d.photo(hero,244,648,512,445,{radius:[250,250,10,10]});fact(M,1140,420);total(555,1136,375);break;
|
||||
case 'neon':
|
||||
d.rect(M,190,860,328,accent,24);d.fit(eyebrow,105,222,790,54,{size:18,displayFont:false,color:'#142019'});d.fit(title,105,290,790,200,{size:62,color:'#142019'});sub(M,558,800);fact(M,660);d.photo(hero,M,785,520,425,{radius:24});d.photo(hero2,616,785,314,205,{radius:24});total(616,1040,314);break;
|
||||
case 'circles':
|
||||
label(M,213);heading(M,291,820,225,68);sub(M,550,820);d.photo(hero,M,731,400,400,{shape:'circle'});d.photo(hero2,513,711,256,256,{shape:'circle'});d.photo(hero3,701,919,229,229,{shape:'circle'});fact(M,646);total(M,1165,800);break;
|
||||
case 'checklist':
|
||||
label(M,212);heading(M,294,850,240,67);sub(M,568,810);fact(M,680);d.line(M,786,860);(s.items||[]).slice(0,3).forEach((it,i)=>{const y=843+i*99;d.write(String(i+1).padStart(2,'0'),M,y,70,{size:24,color:accent});d.fit(it.name,170,y,750,77,{size:24,displayFont:false})});total(M,1170,810);break;
|
||||
case 'gallery':
|
||||
label(M,203);heading(M,280,840,210,68);sub(M,528,810);[hero,hero2,hero3].forEach((im,i)=>d.photo(im,M+i*294,707+i*37,272,355,{radius:136}));fact(M,626);total(M,1160,830);break;
|
||||
case 'diamond':
|
||||
label(M,200);heading(M,284,840,220,66);sub(M,537,810);fact(M,648);d.photo(hero,430,735,500,455,{shape:'diamond'});d.write('МЕНЮ',M,879,225,{size:18,color:accent});total(M,969,330);break;
|
||||
case 'panorama':
|
||||
d.photo(hero,0,176,W,467);label(M,701);heading(M,778,840,220,65);sub(M,1040,430,140);fact(M,1209,410);total(550,1135,380);break;
|
||||
case 'botanical':
|
||||
label(M,199,460);heading(M,284,470,350,64);d.photo(hero,590,205,340,590,{radius:170});sub(M,686,460,155);d.line(M,901,860);note(M,958,475,140);fact(M,1150,445);total(568,1119,362);break;
|
||||
}
|
||||
return d.c;
|
||||
}
|
||||
function composition(item){const values=item.compositionTotal?.length?item.compositionTotal:item.composition||[];return (Array.isArray(values)?values:[values]).map(v=>typeof v==='string'?v:v?.name||'').filter(Boolean).join(' · ')}
|
||||
async function renderPages(s){
|
||||
const cfg=settings(s),items=s.items||[],t={...(THEMES[s.offerTemplateId]||THEMES.light),scale:items.length>24?2:SCALE},cache=new Map();
|
||||
const img=src=>{if(!src)return Promise.resolve(null);if(!cache.has(src))cache.set(src,loadImage(src));return cache.get(src)};
|
||||
await ready();
|
||||
const [logo,images,gallery]=await Promise.all([img(s.logo),Promise.all(items.map(i=>img((i.photoData||i.photo)===s.logo?'':i.photoData||i.photo))),Promise.all((s.finalGallery||[]).map(img))]);
|
||||
const covers=[cover(s,t,images,logo,cfg)],pages=[...covers];let d,y;
|
||||
const muted=t.dark?'#c0cabf':'#686a5f',lineColor=t.dark?'#426055':'#d9d2c4',panel=t.dark?'#203b30':'#ffffff';
|
||||
// A sunny cover is paired with warm paper inside, making long menus easier to read.
|
||||
const inner={...t,bg:t.id==='solar-experience'?'#fff9e8':t.bg};
|
||||
function newPage(title){d=page(inner,'content');pages.push(d.c);brand(d,s,t,logo,{y:48});d.line(M,135,860,lineColor);y=172;if(title){y+=d.write(title,M,y,860,{size:36,font:d.display(36),lh:46});y+=28}}
|
||||
function ensure(h,title){if(!d||y+h>BOTTOM)newPage(title)}
|
||||
function flow(value,{size=19,color=t.ink,font,role='text',gap=20}={}){
|
||||
if(!String(value||'').trim())return;
|
||||
ensure(size*1.45+gap);const lines=d.lines(value,860,font||d.body(size));let pos=0;const lh=size*1.45;
|
||||
while(pos<lines.length){let count=Math.floor((BOTTOM-y-gap)/lh);if(count<1){newPage();count=Math.floor((BOTTOM-y-gap)/lh)}const part=lines.slice(pos,pos+count);y+=d.write(part,M,y,860,{font:font||d.body(size),size,lh,color,role});pos+=part.length;if(pos<lines.length)newPage()}
|
||||
y+=gap;
|
||||
}
|
||||
const title=cfg.texts.menuTitle||'Меню';newPage(title);
|
||||
for(const value of covers[0].__proposalOverflow||[])flow(value,{size:22});
|
||||
const meta=it=>[it.categoryName,it.weight?`Вес: ${it.weight}`:'',number(it.pieces)>0?`${qty(it.pieces)} шт. в порции`:''].filter(Boolean).join(' · ');
|
||||
function priceLine(it){return `${qty(it.qty)} × ${money(it.unitPrice)} = ${money(it.sum)}`}
|
||||
function measure(it,width,card){return d.lines(it.name||'Позиция меню',width,d.body(card?25:24,700)).length*(card?33:32)+d.lines(meta(it),width,d.body(16)).length*23+(composition(it)?d.lines(composition(it),width,d.body(18)).length*26+12:0)}
|
||||
function details(it,x,yy,width,card){
|
||||
yy+=d.write(it.name||'Позиция меню',x,yy,width,{size:card?25:24,weight:700,lh:card?33:32,role:'item-name'})+10;
|
||||
if(meta(it))yy+=d.write(meta(it),x,yy,width,{size:16,lh:23,color:muted,role:'item-meta'})+10;
|
||||
if(composition(it))yy+=d.write(composition(it),x,yy,width,{size:18,lh:26,color:muted,role:'composition'});
|
||||
return yy;
|
||||
}
|
||||
function tallItem(it,index){
|
||||
if(y>280)newPage(title+' · продолжение');
|
||||
flow(it.name||'Позиция меню',{size:25,font:d.body(25,700),role:'item-name'});flow(meta(it),{size:16,color:muted});flow(composition(it),{size:18,color:muted,role:'composition'});flow(priceLine(it),{size:21,role:'item-price'});d.line(M,y-4,860,lineColor);y+=24;
|
||||
}
|
||||
if(!items.length)flow('Позиции меню пока не добавлены.',{color:muted});
|
||||
if(t.menu==='cards'){
|
||||
const cw=415,gap=30,photoH=cfg.showGallery?160:0;
|
||||
for(let index=0;index<items.length;){
|
||||
const pair=items.slice(index,index+2),heights=pair.map(it=>measure(it,cw-40,true)+photoH+132);
|
||||
if(heights.some(h=>h>1080)){tallItem(items[index],index);index++;continue}
|
||||
const h=Math.max(...heights);ensure(h+24,title+' · продолжение');
|
||||
pair.forEach((it,k)=>{const x=M+k*(cw+gap);d.rect(x,y,cw,h,panel,12);if(photoH)d.photo(images[index+k],x,y,cw,photoH,{radius:[12,12,0,0]});details(it,x+20,y+photoH+24,cw-40,true);const priceY=y+h-75;d.line(x+20,priceY-15,cw-40,lineColor);d.write(`${qty(it.qty)} × ${money(it.unitPrice)}`,x+20,priceY,cw-40,{size:16,color:muted,role:'quantity-price'});d.write(money(it.sum),x+20,priceY+28,cw-40,{size:23,weight:700,align:'right',role:'item-price'})});y+=h+24;index+=pair.length;
|
||||
}
|
||||
}else{
|
||||
const photoW=cfg.showGallery?(t.menu==='compact'?98:145):0,tx=M+(photoW?photoW+25:0),tw=860-(tx-M)-185;
|
||||
for(let index=0;index<items.length;index++){
|
||||
const it=items[index],amountLines=d.lines(money(it.sum),176,d.body(23,700)),amountH=amountLines.length*32,unitLines=d.lines(`${qty(it.qty)} × ${money(it.unitPrice)}`,176,d.body(16)),h=Math.max(photoW,measure(it,tw,false)+40,amountH+12+unitLines.length*23)+42;
|
||||
if(h>1080){tallItem(it,index);continue}
|
||||
ensure(h,title+' · продолжение');if(photoW)d.photo(images[index],M,y,photoW,photoW,{radius:t.menu==='compact'?49:9});details(it,tx,y,tw,false);
|
||||
d.write(amountLines,754,y,176,{size:23,lh:32,weight:700,align:'right',role:'item-price'});
|
||||
d.write(unitLines,754,y+amountH+12,176,{size:16,lh:23,color:muted,align:'right',role:'quantity-price'});
|
||||
d.line(M,y+h-23,860,lineColor);y+=h;
|
||||
}
|
||||
}
|
||||
const p=s.pricing||{},text=cfg.texts;
|
||||
const rows=cfg.showPriceBreakdown?[[text.priceItemsLabel||'Стоимость позиций',p.base],...(number(p.manual)?[[text.discountLabel||'Скидка',-number(p.manual)]]:[]),...(number(p.promo)?[[`${text.promoLabel||'Промокод'}${s.promoCode?' '+s.promoCode:''}`,-number(p.promo)]]:[]),[text.menuAfterDiscountLabel||'Меню после скидок',p.itemsTotal],[text.deliveryLabel||'Доставка',p.delivery]]:[];
|
||||
ensure(200);
|
||||
y+=22;flow(text.pricingTitle||'Расчёт стоимости',{size:34,font:d.display(34),gap:24});
|
||||
for(const [label,value] of rows){
|
||||
const ls=d.lines(label,610,d.body(19)),rs=d.lines(money(value),215,d.body(21,700)),h=Math.max(ls.length*28,rs.length*29)+22;ensure(h+95);
|
||||
d.write(ls,M,y,610,{size:19,lh:28,color:muted,role:'pricing-label'});d.write(rs,715,y,215,{size:21,weight:700,lh:29,align:'right',role:'pricing-value'});y+=h;
|
||||
}
|
||||
ensure(124);d.line(M,y,860,lineColor);y+=28;const totalLabel=d.lines(text.summaryTitle||text.totalLabel||'Итого',380,d.display(29));d.write(totalLabel,M,y,380,{size:29,font:d.display(29),lh:39,role:'total-label'});const amount=d.lines(money(p.total),460,d.body(35,700));y+=Math.max(60,totalLabel.length*39,d.write(amount,470,y,460,{size:35,weight:700,lh:45,align:'right',role:'total-value'}))+26;
|
||||
const metrics=[];const guests=number(s.guests);
|
||||
if(cfg.showBoxCount)metrics.push(`${text.boxCountLabel||'Количество боксов'}: ${qty(items.filter(i=>[0,5].includes(number(i.categoryId))).reduce((n,i)=>n+number(i.qty),0))}`);
|
||||
if(cfg.showGuests&&guests){if(cfg.showAmountPerGuest)metrics.push(`${text.amountPerGuestLabel||'Сумма на гостя'}: ${money(number(p.itemsTotal)/guests)}`);if(cfg.metricMode==='pieces'){if(number(s.foodPieces))metrics.push(`${text.piecesMetricLabel||'Канапе на гостя'}: ${qty(Math.ceil(number(s.foodPieces)/guests))} шт.`)}else if(number(s.foodGrams))metrics.push(`${text.weightMetricLabel||'Вес еды на гостя'}: ${qty(Math.ceil(number(s.foodGrams)/guests/5)*5)} г`)}
|
||||
if(metrics.length)flow(metrics.join(' · '),{size:17,color:muted});
|
||||
if(cfg.showFooterNote&&text.footerNote)flow(text.footerNote,{size:17,color:muted,gap:28});
|
||||
if(text.salesTitle&&text.salesNote){ensure(155);flow(text.salesTitle,{size:27,font:d.display(27),gap:16});flow(text.salesNote,{size:19,gap:28})}
|
||||
const sections=[[cfg.showControl,text.controlTitle||s.controlTitle||'Организация мероприятия',cfg.controlLines||s.controlLines],[cfg.showExtras,text.extrasTitle||s.extrasTitle||'Дополнительно',cfg.extraServices||s.extraServices]].filter(([enabled,,values])=>enabled&&Array.isArray(values)&&values.length);
|
||||
const columnH=([,heading,values])=>d.lines(heading,405,d.display(27)).length*36+24+values.reduce((sum,v)=>sum+d.lines('• '+String(v),405,d.body(18)).length*26+12,0);
|
||||
if(sections.length===2&§ions.every(section=>columnH(section)<650)){
|
||||
const h=Math.max(...sections.map(columnH));ensure(h+35);sections.forEach(([,heading,values],index)=>{const x=M+index*455;let yy=y;d.line(x,yy,405,lineColor);yy+=18;yy+=d.write(heading,x,yy,405,{size:27,font:d.display(27),lh:36})+18;for(const value of values)yy+=d.write('• '+String(value),x,yy,405,{size:18,lh:26})+12});y+=h+35;
|
||||
}else for(const [,heading,values] of sections){ensure(135);flow(heading,{size:27,font:d.display(27),gap:16});for(const value of values)flow('• '+String(value),{size:18,gap:10});y+=18}
|
||||
const pics=gallery.filter(Boolean).slice(0,2);
|
||||
// Gallery fills existing space; it never creates a nearly empty extra page.
|
||||
if(cfg.showGallery&&cfg.showFinalPhoto&&pics.length&&BOTTOM-y>=260){const label=text.finalGalleryTitle||'Сервировка вашего события',h=d.lines(label,860,d.display(27)).length*38;const available=BOTTOM-y-h-18;if(available>=180){y+=d.write(label,M,y,860,{size:27,font:d.display(27),lh:38})+18;const w=(860-22*(pics.length-1))/pics.length;pics.forEach((im,i)=>d.photo(im,M+i*(w+22),y,w,Math.min(340,available),{radius:8}))}}
|
||||
pages.forEach((c,i)=>{const fd=painter(c,i?inner:t);const darkCover=i===0&&t.cover==='photo';if(i===0&&['night','photo'].includes(t.cover))fd.rect(0,1330,W,84,t.bg);fd.line(M,1343,860,darkCover?'#8b8b78':lineColor);fd.fit([s.brandName||'Моя компания',s.brandCity].filter(Boolean).join(' · '),M,s.brandContacts?1351:1361,720,26,{size:14,min:12,displayFont:false,color:darkCover?'#fff5e6':muted});if(s.brandContacts)fd.fit(s.brandContacts,M,1376,720,26,{size:12,min:10,displayFont:false,color:darkCover?'#fff5e6':muted});fd.write(`${i+1} / ${pages.length}`,850,1361,80,{size:14,align:'right',color:darkCover?'#fff5e6':muted,role:'page-number'});});
|
||||
return pages;
|
||||
}
|
||||
const descriptions={split:'Светлая обложка, вертикальное фото и спокойная типографика.',night:'Тёмная полоса с текстом и фотография на всю высоту.',editorial:'Журнальная сетка, выразительные заголовки и карточки меню.',sun:'Тёплая бумага, солнечный акцент и широкая фотография.',bento:'Модульная фотокомпозиция и меню в двух колонках.',story:'История события с этапами подготовки и панорамным фото.',photo:'Фотография на всю обложку и золотистые акценты.',letter:'Личное обращение на светлой бумаге и курсивные заголовки.',ticket:'Обложка в форме приглашения с отдельной полосой даты.',solar:'Яркая солнечная обложка и светлые внутренние страницы.',compact:'Лаконичная ночная палитра и компактный список меню.',frame:'Тонкая золотистая рамка и симметричная композиция.', 'neon-vertical':'Вертикальный акцент и пара круглых фотографий.',arch:'Кремовая бумага, арочное фото и классические заголовки.',neon:'Яркий заголовок на тёмной бумаге и модульные фото.',circles:'Круглые фотографии разных размеров и изумрудная палитра.',checklist:'Нумерованные акценты меню на строгой тёмной обложке.',gallery:'Три фотографии в арках и крупная журнальная типографика.',diamond:'Фотография в ромбе и изумрудно-золотая палитра.',panorama:'Широкая панорама и сдержанные современные заголовки.',botanical:'Высокая арка, курсивные заголовки и глубокий изумрудный фон.'};
|
||||
window.CateriumProposalPDF=Object.freeze({VERSION:'20260918-proposal-quality',IDS:IDs,templates:definitions.map(([id,name,style])=>({id,name,desc:descriptions[style],thumb:`offer-templates/quality-${id}.jpg`})),ready,renderPages});
|
||||
})();
|
||||
@ -46,7 +46,7 @@
|
||||
link.href='https://fonts.googleapis.com/css2?family=Playfair+Display:wght@500;600;700&family=Montserrat:wght@500;600;700;800&family=Unbounded:wght@600;700;800&family=Manrope:wght@500;600;700;800&display=swap';
|
||||
document.head.appendChild(link);
|
||||
}
|
||||
ensureFontLink();
|
||||
if(!window.CateriumProposalPDF)ensureFontLink();
|
||||
async function ensureFontsReady(id){
|
||||
const f=FONT_STACKS[id];if(!f||!document.fonts)return;
|
||||
try{
|
||||
|
||||
@ -161,13 +161,14 @@
|
||||
const id=globalTemplateId();persistOfferTemplate(activeOfferOrderId,id);return id;
|
||||
}
|
||||
function offerTemplateMini(id){
|
||||
if(window.CateriumProposalPDF?.IDS.includes(id))return `<img src="offer-templates/quality-${id}.jpg?v=20260918" alt="" loading="lazy" style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#eeece5;z-index:2">`;
|
||||
if(id==='editorial-grid')return '<i class="mini-head"></i><i class="mini-editorial-hero"></i><i class="mini-editorial-stack a"></i><i class="mini-editorial-stack b"></i><i class="mini-editorial-grid"></i>';
|
||||
if(id==='midnight-glass')return '<i class="mini-head"></i><i class="mini-midnight-hero"></i><i class="mini-midnight-total"></i><i class="mini-midnight-left"></i><i class="mini-midnight-right"></i>';
|
||||
if(id==='emerald-gold')return '<i class="mini-head"></i><i class="mini-emerald-hero"></i><i class="mini-emerald-gold"></i><i class="mini-emerald-info"></i><i class="mini-emerald-menu"></i>';
|
||||
return '<i class="mini-head"></i><i class="mini-light-hero"></i><i class="mini-light-row a"></i><i class="mini-light-row b"></i><i class="mini-light-row c"></i><i class="mini-light-total"></i>';
|
||||
}
|
||||
function pickerHtml(){
|
||||
const current=activeTemplateId();return `<div class="sun-v1764-offer-template-head"><div><b>Оформление PDF</b><small>Классические и архивные шаблоны. Предварительный просмотр совпадает со скачиваемым PDF.</small></div></div><div class="sun-v1764-offer-template-grid">${templateList().map(t=>`<button type="button" data-v1764-offer-template="${esc(t.id)}" class="${t.id===current?'on':''}"><div class="sun-offer-template-mini sun-offer-template-mini-${esc(t.id)}" data-template-mini="${esc(t.id)}" aria-hidden="true">${offerTemplateMini(t.id)}</div><span>${esc(t.name||t.id)}</span></button>`).join('')}</div>`;
|
||||
const current=activeTemplateId();return `<div class="sun-v1764-offer-template-head"><div><b>Оформление PDF</b><small>Классическая и современная коллекции. Предварительный просмотр совпадает со скачиваемым PDF.</small></div></div><div class="sun-v1764-offer-template-grid">${templateList().map(t=>`<button type="button" data-v1764-offer-template="${esc(t.id)}" class="${t.id===current?'on':''}"><div class="sun-offer-template-mini sun-offer-template-mini-${esc(t.id)}" data-template-mini="${esc(t.id)}" aria-hidden="true">${offerTemplateMini(t.id)}</div><span>${esc(t.name||t.id)}</span></button>`).join('')}</div>`;
|
||||
}
|
||||
function ensureOfferPicker(){
|
||||
patchOfferTemplateApi();const modal=$('sunClientOfferModal'),dialog=modal?.querySelector('.dialog');if(!dialog||!activeOfferOrderId)return false;
|
||||
|
||||
BIN
public/fonts/Manrope.ttf
Normal file
BIN
public/fonts/PlayfairDisplay-Italic.ttf
Normal file
BIN
public/fonts/PlayfairDisplay.ttf
Normal file
8
public/fonts/README.md
Normal file
@ -0,0 +1,8 @@
|
||||
# Proposal fonts
|
||||
|
||||
Original, unmodified variable font files from Google Fonts. All are under the SIL Open Font License included alongside them. Cyrillic and Latin glyphs are retained. No third-party font request is required while creating proposals.
|
||||
|
||||
- Manrope: https://github.com/google/fonts/tree/main/ofl/manrope
|
||||
- Playfair Display (regular and italic): https://github.com/google/fonts/tree/main/ofl/playfairdisplay
|
||||
|
||||
Downloaded 2026-09-18. CSS FontFace aliases in `core/proposal-layout.js` are internal names; the source font files and copyright notices are unchanged.
|
||||
93
public/fonts/manrope-OFL.txt
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright 2018 The Manrope Project Authors (https://github.com/googlefonts/manrope)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
93
public/fonts/playfairdisplay-OFL.txt
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display"
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
public/offer-templates/quality-bento-cards.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/offer-templates/quality-black-gold.jpg
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/offer-templates/quality-cream-elegance.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/offer-templates/quality-diamond-gold.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
public/offer-templates/quality-editorial-grid.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/offer-templates/quality-emerald-circles.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/offer-templates/quality-emerald-gold.jpg
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/offer-templates/quality-event-story.jpg
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
public/offer-templates/quality-event-ticket.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/offer-templates/quality-gourmet-hero.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/offer-templates/quality-light.jpg
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
public/offer-templates/quality-midnight-checklist.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
public/offer-templates/quality-midnight-compact.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/offer-templates/quality-midnight-glass.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
public/offer-templates/quality-neon-emerald.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
public/offer-templates/quality-neon-menu.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
public/offer-templates/quality-personal-letter.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/offer-templates/quality-premium-dark.jpg
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
public/offer-templates/quality-premium-emerald.jpg
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/offer-templates/quality-solar-experience.jpg
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
public/offer-templates/quality-warm-sun.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
@ -1,7 +1,7 @@
|
||||
const CACHE='sun-catering-pwa-v91-20260918-trial-demo';
|
||||
const VERSION='20260918-trial-demo';
|
||||
const CACHE='sun-catering-pwa-v92-20260918-proposal-quality';
|
||||
const VERSION='20260918-proposal-quality';
|
||||
const CORE=[
|
||||
'./','./index.html',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
||||
'./','./index.html',`./core/proposal-layout.js?v=${VERSION}`,'./fonts/Manrope.ttf','./fonts/PlayfairDisplay.ttf','./fonts/PlayfairDisplay-Italic.ttf',`./core/trial-demo.js?v=${VERSION}`,`./core/cloud-transport.js?v=${VERSION}`,`./core/banquet-menu.js?v=${VERSION}`,`./core/access-policy.js?v=${VERSION}`,`./core/import-archive.js?v=${VERSION}`,`./core/company-branding.js?v=${VERSION}`,`./core/signature-offer-pdf-v18.js?v=${VERSION}`,`./core/brand-theme.js?v=${VERSION}`,
|
||||
`./core/sun-safe.js?v=${VERSION}`,`./core/performance.js?v=${VERSION}`,`./core/account-center-v1780.js?v=${VERSION}`,`./core/login-signature-v1776.js?v=${VERSION}`,`./core/data-layer-v1773.js?v=${VERSION}`,`./core/server-automation-v1770.js?v=${VERSION}`,`./core/hotfix-v1763.js?v=${VERSION}`,`./core/ops-ux-v1762.js?v=${VERSION}`,`./core/ux-fixes-v1764.js?v=${VERSION}`,`./core/pdf-engine.js?v=${VERSION}`,`./core/classic-offer-pdf-v1767.js?v=${VERSION}`,`./core/developer-console-v1768.js?v=${VERSION}`,`./core/offer-workspace-v1769.js?v=${VERSION}`,`./core/auth-security-v1774.js?v=${VERSION}`,`./core/order-enhancements-v1775.js?v=${VERSION}`,`./legacy/bootstrap.js?v=${VERSION}`,`./app-runtime.js?v=${VERSION}`,
|
||||
'./offer-gallery/001.jpg','./offer-gallery/002.jpg',
|
||||
'./catalog/001.jpg','./catalog/002.jpg','./catalog/003.jpg',
|
||||
@ -9,7 +9,7 @@ const CORE=[
|
||||
'./offer-templates/thumb-light.jpg','./offer-templates/thumb-editorial-grid.jpg','./offer-templates/thumb-midnight-glass.jpg','./offer-templates/thumb-emerald-gold.jpg'
|
||||
];
|
||||
const CRITICAL_FRESH=new Set([
|
||||
'/core/cloud-transport.js','/core/trial-demo.js',
|
||||
'/core/cloud-transport.js','/core/trial-demo.js','/core/proposal-layout.js',
|
||||
'/core/banquet-menu.js','/core/access-policy.js','/core/import-archive.js','/core/company-branding.js','/core/brand-theme.js','/core/sun-safe.js','/core/performance.js','/core/account-center-v1780.js','/core/login-signature-v1776.js','/core/auth-security-v1774.js','/legacy/bootstrap.js','/app-runtime.js'
|
||||
]);
|
||||
self.addEventListener('install',event=>{
|
||||
|
||||
@ -2,7 +2,7 @@ import { defineConfig, devices } from '@playwright/test';
|
||||
import {fileURLToPath} from 'node:url';
|
||||
export default defineConfig({
|
||||
testDir:'.',
|
||||
testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','trial-demo.spec.mjs'],
|
||||
testMatch:['app.spec.mjs','theme-startup.spec.mjs','company-branding.spec.mjs','order-import.spec.mjs','account-access.spec.mjs','banquet-menu.spec.mjs','calendar-print.spec.mjs','login-recovery.spec.mjs','trial-demo.spec.mjs','proposal-quality.spec.mjs'],
|
||||
timeout:30000,
|
||||
use:{baseURL:'http://127.0.0.1:4173'},
|
||||
webServer:{command:'npx http-server public -p 4173 -c-1',cwd:fileURLToPath(new URL('../',import.meta.url)),port:4173,reuseExistingServer:true},
|
||||
|
||||
92
tests/proposal-quality.spec.mjs
Normal file
@ -0,0 +1,92 @@
|
||||
import {test,expect} from '@playwright/test';
|
||||
|
||||
async function boot(page){
|
||||
await page.route('https://**',r=>r.abort());
|
||||
await page.goto('/index.html',{waitUntil:'domcontentloaded'});
|
||||
await page.waitForFunction(()=>Boolean(window.sunClientOfferDebugPdfPages&&window.SunOfferTemplate));
|
||||
}
|
||||
const base={brandName:'Компания Лист',brandCity:'Казань',client:'Анна и Михаил',event:'Ужин для друзей',date:'2026-10-24',guests:20,foodGrams:8000,pricing:{base:24000,manual:2000,promo:0,discount:2000,itemsTotal:22000,delivery:1000,total:23000},items:[{id:'fixture',name:'Брускетта с печёными овощами, творожным сыром и зеленью',categoryId:0,categoryName:'Боксы',qty:2,unitPrice:12000,sum:24000,weight:'1200 г',composition:['Томаты, баклажаны, перец, сливочный сыр и зелень'],photoData:''}]};
|
||||
function validLayout(pages){
|
||||
return pages.flatMap((p,index)=>p.__proposalLayout.filter(b=>b.x<35||b.x+b.w>965||b.y<0||b.y+b.h>1400).map(b=>({page:index+1,...b})));
|
||||
}
|
||||
|
||||
test('all 21 selections render their actual distinct covers and local Cyrillic fonts',async({page})=>{
|
||||
test.setTimeout(90000);await boot(page);
|
||||
const result=await page.evaluate(async s=>{
|
||||
const out=[];
|
||||
for(const id of CateriumProposalPDF.IDS){
|
||||
const pages=await sunClientOfferDebugPdfPages({...s,offerTemplateId:id});
|
||||
const text=pages.flatMap(p=>p.__proposalLayout).map(x=>x.text).join('\n');
|
||||
out.push({id,cover:pages[0].dataset.sunProposalTemplate,width:pages[0].width,pages:pages.length,text,pixel:pages[0].toDataURL('image/jpeg',.1)});
|
||||
pages.forEach(c=>{c.width=0;c.height=0});
|
||||
}
|
||||
return {out,fonts:[...document.fonts].filter(f=>f.family.includes('Caterium')).map(f=>f.status),choices:SunOfferTemplate.list().map(t=>t.id)};
|
||||
},base);
|
||||
expect(result.out).toHaveLength(21);expect(result.choices).toHaveLength(21);
|
||||
expect(result.fonts).toEqual(['loaded','loaded','loaded']);
|
||||
expect(new Set(result.out.map(r=>r.pixel)).size).toBe(21);
|
||||
for(const row of result.out){expect(row.cover).toBe(row.id);expect(row.width).toBe(2400);expect(row.text.replaceAll('\n',' ')).toContain(base.items[0].name);expect(row.text).toContain('23');expect(row.text).not.toMatch(/Солнце|SUN \/ CATERING/)}
|
||||
});
|
||||
|
||||
test('flow preserves long Cyrillic names and compositions across pages without text collisions',async({page})=>{
|
||||
test.setTimeout(90000);await boot(page);
|
||||
const results=await page.evaluate(async({s,validator})=>{
|
||||
const valid=eval('('+validator+')'),out=[];
|
||||
const long='Очень длинное название блюда с запечённым баклажаном и сливочным сыром ';
|
||||
const composition='НачалоСостава '+('Томаты и печёный перец, сливочный сыр; '.repeat(130))+' КонецСостава';
|
||||
const sample={...s,event:long.repeat(3),client:long.repeat(2),items:[{...s.items[0],name:long.repeat(4),composition:[composition]}]};
|
||||
for(const id of ['light','editorial-grid','neon-menu','premium-emerald']){
|
||||
const pages=await sunClientOfferDebugPdfPages({...sample,offerTemplateId:id});
|
||||
const layout=pages.flatMap(p=>p.__proposalLayout),text=layout.map(b=>b.text).join(' ').replace(/\s+/g,' ');
|
||||
const collisions=[];
|
||||
pages.slice(1).forEach((p,index)=>{const list=p.__proposalLayout;for(let i=0;i<list.length;i++)for(let j=i+1;j<list.length;j++){const a=list[i],b=list[j];if(Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x)>2&&Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y)>2)collisions.push({page:index+2,a:a.role,b:b.role})}});
|
||||
out.push({id,bounds:valid(pages),collisions,text});pages.forEach(c=>{c.width=0;c.height=0});
|
||||
}return out;
|
||||
},{s:base,validator:validLayout.toString()});
|
||||
for(const r of results){expect(r.bounds).toEqual([]);expect(r.collisions).toEqual([]);expect(r.text).toContain('НачалоСостава');expect(r.text).toContain('КонецСостава');expect(r.text).not.toContain('…')}
|
||||
});
|
||||
|
||||
test('offer settings hide personal data and optional blocks; custom gallery reaches the PDF',async({page})=>{
|
||||
await boot(page);
|
||||
const result=await page.evaluate(async s=>{
|
||||
const c=document.createElement('canvas');c.width=40;c.height=40;const ctx=c.getContext('2d');ctx.fillStyle='#891552';ctx.fillRect(0,0,40,40);const gallery=c.toDataURL();
|
||||
window.SunOfferWorkspaceV1769={galleryFor:()=>[gallery]};
|
||||
window.SunClientOfferSettings={get:()=>({showClientName:false,showDate:false,showGuests:false,showGallery:true,showBoxCount:false,showPriceBreakdown:false,showAmountPerGuest:false,showControl:false,showExtras:false,showFooterNote:false,texts:{menuTitle:'Специальное меню'}})};
|
||||
const draws=[],draw=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(image,...a){draws.push(image.src);return draw.call(this,image,...a)};
|
||||
try{
|
||||
const pages=await sunClientOfferDebugPdfPages({...s,offerTemplateId:'light',orderId:'local-fixture'}),text=pages.flatMap(p=>p.__proposalLayout).map(b=>b.text).join(' ');
|
||||
return {text,gallery:draws.includes(gallery),bounds:pages.flatMap(p=>p.__proposalLayout.filter(b=>b.y+b.h>1400))};
|
||||
}finally{CanvasRenderingContext2D.prototype.drawImage=draw}
|
||||
},base);
|
||||
expect(result.text).toContain('Специальное меню');expect(result.text).not.toContain(base.client);expect(result.text).not.toContain('октября');expect(result.text).not.toContain('Скидка');expect(result.text).not.toContain('гостей');expect(result.gallery).toBe(true);expect(result.bounds).toEqual([]);
|
||||
});
|
||||
|
||||
test('per-order picker retains the classic, modern and additional collections together',async({page})=>{
|
||||
await boot(page);
|
||||
await page.evaluate(()=>{
|
||||
const grid=document.createElement('div');grid.className='sun-v1764-offer-template-grid';
|
||||
for(const id of CateriumProposalPDF.IDS){const b=document.createElement('button');b.dataset.v1764OfferTemplate=id;grid.appendChild(b)}document.body.appendChild(grid);
|
||||
});
|
||||
await expect(page.locator('.sun-v1764-offer-template-grid button')).toHaveCount(21);
|
||||
await expect(page.locator('.sun-v1767-group-label')).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('an actual offer downloads the same A4 pages shown in its preview',async({page})=>{
|
||||
test.setTimeout(60000);await boot(page);
|
||||
await page.evaluate(async s=>{
|
||||
boxes=[{id:'pdf-local',name:s.items[0].name,price:12000,category:0,weight:'1200 г',composition:s.items[0].composition,photo:''}];
|
||||
orders=[{id:'pdf-local-order',event:s.event,contact:s.client,date:s.date,time:'18:00',guestsCount:20,lines:[{id:'pdf-local',qty:2}],total:24000,status:'Новый',clientOfferTemplateId:'event-ticket'}];
|
||||
await window.sunOpenClientOffer('pdf-local-order');
|
||||
},base);
|
||||
await expect(page.locator('#sunClientOfferPreview canvas').first()).toHaveAttribute('data-sun-proposal-template','event-ticket');
|
||||
const pageCount=await page.locator('#sunClientOfferPreview canvas').count();
|
||||
const downloadPromise=page.waitForEvent('download');
|
||||
await page.evaluate(()=>window.sunClientOfferDownload());
|
||||
const download=await downloadPromise,stream=await download.createReadStream(),chunks=[];
|
||||
for await(const chunk of stream)chunks.push(chunk);
|
||||
const body=Buffer.concat(chunks).toString('latin1');
|
||||
expect(body.startsWith('%PDF-1.4')).toBe(true);
|
||||
expect((body.match(/\/Type \/Page\b/g)||[]).length).toBe(pageCount);
|
||||
expect((body.match(/\/MediaBox \[0 0 595\.28 841\.89\]/g)||[]).length).toBe(pageCount);
|
||||
expect(body).toContain('/Width 2400');
|
||||
});
|
||||
@ -14,8 +14,8 @@ check(!index.includes('offer-gallery-data.js'),'blocking Base64 gallery absent')
|
||||
check((runtime.match(/\/Type \/Catalog/g)||[]).length===0,'runtime contains no PDF binary writer');
|
||||
check(read('core/pdf-engine.js').includes('595.28')&&read('core/pdf-engine.js').includes('841.89'),'PDF engine uses A4 MediaBox');
|
||||
check([...index.matchAll(/@page\{([^}]*)\}/g)].every(m=>/size:A4/i.test(m[1])),'compact @page rules use A4');
|
||||
check(sw.includes('v91-20260918-trial-demo')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
||||
check(index.includes('20260918-trial-demo')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
||||
check(sw.includes('v92-20260918-proposal-quality')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js')&&sw.includes('offer-workspace-v1769.js'),'service worker cache is v17.7.3');
|
||||
check(index.includes('20260918-proposal-quality')&&index.includes('classic-offer-pdf-v1767.js')&&!index.includes('20260907-v17-6-0-stability-security'),'index cache-busting points to v17.7.3');
|
||||
check(performance.includes('SunAttachmentGuard')&&performance.includes('TARGET=2*1024*1024'),'chat photo auto-compression is versioned');
|
||||
check(performance.includes("rpc('sun_dev_dashboard')")&&performance.includes('server_size')&&performance.includes('storage_size'),'Developer Console server/storage counters are versioned');
|
||||
check(performance.includes('MEMORY_REFRESH_MS=30000')&&performance.includes('MEMORY_TIMEOUT_MS=8000')&&performance.includes('memoryPromise'),'Developer Console memory refresh is bounded');
|
||||
@ -39,7 +39,7 @@ check(!/sb_secret_[A-Za-z0-9_-]{20,}|service_role\s*[:=]\s*["'][A-Za-z0-9._-]{30
|
||||
check(lock.version===pkg.version&&lock.packages?.['']?.version===pkg.version,'package.json and package-lock.json versions match');
|
||||
check(releaseManifest.version===`v${pkg.version}`,'release manifest version matches package.json');
|
||||
check(releaseManifest.channel==='production','release manifest channel is production');
|
||||
check(String(releaseManifest.pwaCache||'').includes('v91-20260918-trial-demo'),'release manifest points to current PWA cache');
|
||||
check(String(releaseManifest.pwaCache||'').includes('v92-20260918-proposal-quality'),'release manifest points to current PWA cache');
|
||||
check(['17.6.2','17.6.3','17.6.4','17.6.5','17.6.6','17.6.7','17.6.8','17.6.9','17.7.0','17.7.1','17.7.2','17.7.3'].every(v=>fs.existsSync(path.join(root,`docs/releases/V${v}-CHANGES.txt`))),'release notes exist through v17.7.3');
|
||||
check(runtime.includes('CLOUD_RPC_TIMEOUT_MS=12000')&&runtime.includes('CLOUD_CONFLICT_MAX_RETRIES=4')&&runtime.includes('retryCount'),'cloud sync has timeout and capped exponential conflict retries');
|
||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes('ERROR_DEDUPE_MS=5*60*1000')&&runtime.includes('mirrorBusy=false')&&runtime.includes('backupBusy=false'),'stability logger uses current version, dedupe and single-flight guards');
|
||||
@ -55,7 +55,7 @@ check(runtime.includes("'warm-sun'")&&runtime.includes("'bento-cards'")&&runtime
|
||||
check(runtime.includes("'midnight-compact'")&&runtime.includes("'black-gold'")&&runtime.includes("'neon-emerald'"),'hidden archive proposal templates are restored');
|
||||
check(runtime.includes('SunClassicOfferPDFV1767')&&runtime.includes('renderOfferPdfPagesBase'),'classic renderer wraps the same PDF pages used by preview/download');
|
||||
check(classic.includes('data-sun-classic-cover')&&classic.includes('renderPages'),'classic renderer adds real A4 cover pages');
|
||||
check(releaseManifest.offerTemplates===13&&releaseManifest.classicOfferTemplates===10,'release manifest exposes 10 classic + 3 archive templates');
|
||||
check(releaseManifest.offerTemplates===21&&releaseManifest.classicOfferTemplates===10,'release manifest exposes all 21 proposal designs');
|
||||
check(developerUX.includes("const VERSION='17.6.8'")&&developerUX.includes('sun_dev_delete_company_v1768')&&developerUX.includes('sun_dev_error_groups_v1768'),'Developer Console v17.6.8 module is versioned');
|
||||
check(developerUX.includes('selectedAccountsWorkspace')&&developerUX.includes('sun-dev-plan-matrix'),'accounts are grouped by company and plans use compact matrix');
|
||||
check(developerUX.includes('KNOWN_DOM_RACE')&&developerUX.includes('prepareAsyncRoots'),'Developer Console async DOM race is guarded');
|
||||
@ -65,8 +65,8 @@ check(offerWorkspace.includes('PDF и предпросмотр')&&offerWorkspace
|
||||
check(offerWorkspace.includes('SunClassicOfferPDFV1767')&&offerWorkspace.includes('finalGallery=galleryFor'),'custom gallery is injected into PDF renderer');
|
||||
check(releaseManifest.offerWorkspaceTabs===true&&releaseManifest.offerTemplatesSeparateTab===true&&releaseManifest.offerTwoCustomGalleryPhotos===true,'release manifest records offer workspace changes');
|
||||
check(pkg.version==='17.7.3','package version is v17.7.3');
|
||||
check(index.includes('20260918-trial-demo'),'index cache bust is v17.7.3');
|
||||
check(sw.includes('v91-20260918-trial-demo')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
||||
check(index.includes('20260918-proposal-quality'),'index cache bust is v17.7.3');
|
||||
check(sw.includes('v92-20260918-proposal-quality')&&sw.includes('data-layer-v1773.js')&&sw.includes('server-automation-v1770.js'),'PWA caches v17.7.3 client foundation modules');
|
||||
check(fs.existsSync(path.join(root,'public/core/data-layer-v1773.js'))&&fs.existsSync(path.join(root,'public/core/server-automation-v1770.js')),'data layer and server automation modules exist');
|
||||
check(ux.includes('CateriumServerAutomationV1770?.enabled'),'cloud browser auto completion is disabled when server automation is active');
|
||||
check(runtime.includes("const VERSION = '17.7.3'")&&runtime.includes("v17.7.3 Clients Server Read"),'stability logger reports v17.7.3');
|
||||
|
||||
@ -28,8 +28,8 @@ if(current!==113)fail(`current catalog photo count ${current}, expected 113`);el
|
||||
if(legacyCount!==60)fail(`legacy catalog photo count ${legacyCount}, expected 60`);else ok('60 legacy catalog photos');
|
||||
const gallery=fs.readdirSync(path.join(pub,'offer-gallery')).filter(x=>/\.jpg$/i.test(x));
|
||||
if(gallery.length!==2)fail(`offer gallery contains ${gallery.length} jpg files, expected 2`);else ok('offer gallery trimmed');
|
||||
if(!sw.includes('20260918-trial-demo')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-trial-demo')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||
if(!sw.includes('20260918-proposal-quality')||!sw.includes('login-signature-v1776.js')||!sw.includes('data-layer-v1773.js')||!sw.includes('server-automation-v1770.js')||!sw.includes('offer-workspace-v1769.js')||sw.includes('offer-gallery-data.js'))fail('service worker cache is stale');else ok('PWA cache updated for login refresh');
|
||||
if(html.includes('20260907-v17-6-0-stability-security')||html.includes('20260909-v17-7-3-clients-server-read')||!html.includes('20260918-proposal-quality')||!html.includes('classic-offer-pdf-v1767.js'))fail('index still serves stale core asset version');else ok('index cache-busting is current');
|
||||
if(!performance.includes('SunAttachmentGuard')||!performance.includes('MAX_SIDE=2048'))fail('chat photo compression guard missing');else ok('chat photo compression guard present');
|
||||
if(!performance.includes("rpc('sun_dev_dashboard')")||!performance.includes('storage_size')||!performance.includes('server_size'))fail('Developer Console memory counters missing');else ok('Developer Console memory counters present');
|
||||
if(performance.includes('records.forEach(r=>r.addedNodes.forEach(n=>{if(n.nodeType===1)scan(n)}));enhanceDeveloperMemory()'))fail('Developer Console memory refresh is still coupled to MutationObserver');else ok('Developer Console memory refresh loop removed');
|
||||
|
||||