feat: redesign the Montagem page, with thumbnails, check chips and a DPI price scale
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m31s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m0s
All checks were successful
Build and deploy / Validate source (push) Successful in 6s
Build and deploy / Integration suite on a real stack (push) Successful in 2m31s
Build and deploy / Secret scan and release gate (push) Successful in 6s
Build and deploy / Publish images (push) Successful in 1m0s
The product page is rebuilt around mockup 1. The two ways to send are
cards under the title; the PNG/CDR route choice is gone (it never changed
the price, and the picker now takes every allowed format). Each file card
has a thumbnail, its size, check chips with a plain sentence for any
problem, a "Ver detalhes da conferência" link and a copies stepper; a CDR
file says "Conferência manual · preço cheio" and asks for the length. The
buy box shows the price scale in DPI with the customer's step, the metre
and 10 cm length, the total and what the resolution saved, and "Sai em N
partes" only above 20 m. Folha já montada gets "Ampliar" instead of zoom.
Pricing, grade and checks are unchanged.
Fixes on top: the card thumbnail is a small image made once, not the full
preview re-parsed on every repaint; sizes from 1 GB up read in GB; a failed
upload is shown with "Tentar de novo" instead of retrying silently on every
cart change. The browser suites wait for the product and the reloaded page
instead of racing them, and the artwork suite delays imagemDaArte, which
the packing now uses, fixing the CI failure of f4aacb5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
160
web/site-pdf.js
160
web/site-pdf.js
@@ -356,6 +356,15 @@ function textoProgresso(x){
|
||||
if(x.med && x.med.grande) return 'gerando a prévia e conferindo a arte… '+x.pct+'%';
|
||||
return x.pct<50?'lendo o arquivo…':x.pct<70?'medindo a folha…':'conferindo a arte…';
|
||||
}
|
||||
// The file card's thumbnail (52 x 96 px, drawn at twice that for sharp screens).
|
||||
function miniatura(fonte){
|
||||
const w=fonte.naturalWidth||fonte.width, h=fonte.naturalHeight||fonte.height;
|
||||
if(!(w>0 && h>0)) return null;
|
||||
const s=Math.min(1,104/w,192/h), cv=document.createElement('canvas');
|
||||
cv.width=Math.max(1,Math.round(w*s)); cv.height=Math.max(1,Math.round(h*s));
|
||||
cv.getContext('2d').drawImage(fonte,0,0,cv.width,cv.height);
|
||||
return cv.toDataURL('image/webp',0.8);
|
||||
}
|
||||
// Moves one sheet's progress bar in place: repainting the whole list and the
|
||||
// preview on every step made "Sua folha" flicker while a large file was read.
|
||||
function pintaProgressoFolha(x){
|
||||
@@ -364,66 +373,79 @@ function pintaProgressoFolha(x){
|
||||
if(!barra){ pintaFolha(); return; }
|
||||
barra.style.width=x.pct+'%'; if(texto) texto.textContent=textoProgresso(x);
|
||||
}
|
||||
// What the check found, as chips: ok (verde), av (atenção), er (resolva você),
|
||||
// fix (arrumamos de graça), nt (a equipe confere). Details stay one click away.
|
||||
function chipsDaFolha(a){
|
||||
const chip=(k,t)=>'<li class="chip '+k+'">'+t+'</li>';
|
||||
const dpi = a.vetor ? chip('ok','Arte vetorial')
|
||||
: chip(a.dpi>=DPI_AVISA?'ok':a.dpi>=DPI_RECUSA?'av':'er', a.dpi+' DPI');
|
||||
if(a.grande) return dpi+chip('nt','Peças, resíduos e fundo: a equipe confere');
|
||||
return dpi+
|
||||
(a.fundoChapado? chip('er','Fundo chapado') : chip('ok','Fundo transparente'))+
|
||||
(a.residuos? chip('er',a.residuos+' resíduo'+(a.residuos>1?'s':'')) : chip('ok','Sem resíduos'))+
|
||||
(a.encostadas? chip('fix',a.encostadas+' peças coladas · separamos grátis') : chip('ok','Peças separadas'));
|
||||
}
|
||||
function avisosDaFolha(a){
|
||||
if(a.grande) return '';
|
||||
return (a.fundoChapado? '<p class="avisoF"><b>O fundo não é transparente.</b> '+
|
||||
'Branco parecendo transparente sai branco no filme. Exporte com fundo transparente.</p>' : '')+
|
||||
(a.residuos? '<p class="avisoF"><b>'+a.residuos+' pontinho'+(a.residuos>1?'s soltos serão impressos':' solto será impresso')+
|
||||
'.</b> A impressora imprime qualquer resíduo de recorte. Apague no arquivo e envie de novo.</p>' : '');
|
||||
}
|
||||
function detalhesDaFolha(a,i,aberto){
|
||||
const linhas = a.grande
|
||||
? ['Resolução '+a.dpi+' DPI, lida do arquivo',
|
||||
'Arquivo grande: peças, resíduos e fundo são conferidos pela equipe']
|
||||
: [a.artes+' peça'+(a.artes===1?'':'s')+' · '+a.aproveitamento+'% da folha com arte',
|
||||
'Resolução '+a.dpi+' DPI · '+(a.fonteDpi==='imagens'? 'medida imagem por imagem dentro do PDF'
|
||||
: a.fonteDpi==='vetor'? 'arte vetorial, sem imagem embutida' : 'lida do arquivo'),
|
||||
'Conferido numa grade de '+n1(a.grade)+' mm'];
|
||||
return '<details class="confDet" data-det="'+i+'"'+(aberto?' open':'')+'>'+
|
||||
'<summary>Ver detalhes da conferência</summary>'+
|
||||
linhas.map(l=>'<span>'+l+'</span>').join('')+'</details>';
|
||||
}
|
||||
function pintaFolha(){
|
||||
invalidaItemAtual();
|
||||
const L=larguraFilme();
|
||||
pintaTipoEnvio(); // trava o seletor enquanto houver folha
|
||||
$('zona').classList.toggle('compacta', folhas.length>0);
|
||||
if(!folhas.length){ $('lista').innerHTML=''; agendaAvaliacao(); previaAoVivo(); return; }
|
||||
|
||||
$('lista').innerHTML = folhas.map((x,i)=>{
|
||||
const tot=folhaTotalM();
|
||||
$('lista').innerHTML = '<div class="listaCab"><h4>Seus arquivos</h4><span>'+
|
||||
folhas.length+' folha'+(folhas.length>1?'s':'')+(tot>0? ' · '+fmtM(tot)+' m' : '')+'</span></div>'+
|
||||
folhas.map((x,i)=>{
|
||||
const lido=!!x.med, larga=lido && x.med.larg>L, suspeito=lido && x.med.foraDoPadrao;
|
||||
const sub=(x.m||0)*(x.rep||1);
|
||||
let medida='';
|
||||
const sub=(x.m||0)*(x.rep||1), a=lido && x.an;
|
||||
let medida='', manual='';
|
||||
if(lido){
|
||||
const a=x.an;
|
||||
const conferido = a && a.grande
|
||||
? '<div class="conf"><b>Arquivo grande · '+a.dpi+' DPI</b>'+
|
||||
'<span>nota pela resolução · peças, resíduos e fundo conferidos pela equipe</span></div>'
|
||||
: a
|
||||
? '<div class="conf"><b>Conferido de verdade</b>'+
|
||||
'<span>'+a.artes+' peça'+(a.artes===1?'':'s')+' · '+a.aproveitamento+
|
||||
'% da folha virou arte · '+
|
||||
(a.residuos? a.residuos+' resíduo'+(a.residuos>1?'s':'') : 'sem resíduo')+' · '+
|
||||
(a.fundoChapado? 'fundo chapado' : 'fundo transparente')+'</span>'+
|
||||
'<span>resolução '+a.dpi+' DPI · '+
|
||||
(a.fonteDpi==='imagens'? 'medida imagem por imagem dentro do PDF'
|
||||
: a.fonteDpi==='vetor'? 'arte vetorial, sem imagem embutida'
|
||||
: 'lida do arquivo')+
|
||||
' · grade de '+n1(a.grade)+' mm</span></div>'
|
||||
: '';
|
||||
medida='<div class="med'+(larga||suspeito?' alerta':'')+'">'+
|
||||
'<b>'+n1(x.med.larg)+' × '+n1(x.med.alt)+' cm</b>'+
|
||||
'<span>'+x.med.fonte+' · '+fmtM(x.m)+' m de folha</span>'+
|
||||
conferido+
|
||||
(suspeito? '<em><b>Este PDF passa do tamanho que o formato comporta.</b> '+
|
||||
medida='<div class="repMed"><b>'+n1(x.med.larg)+' × '+n1(x.med.alt)+' cm</b>'+
|
||||
'<span>'+escapeHTML(extDe(x.f.name))+' · '+fmt(x.f.size)+'</span></div>'+
|
||||
(a? '<ul class="chips">'+chipsDaFolha(a)+'</ul>'+avisosDaFolha(a)+detalhesDaFolha(a,i,x.detAberto) : '')+
|
||||
(suspeito? '<p class="avisoF"><b>Este PDF passa do tamanho que o formato comporta.</b> '+
|
||||
'A especificação para em 508 cm e o seu tem '+n1(x.med.alt)+' cm. '+
|
||||
'Ele abre certo só em quem o gerou — aqui vai precisar de tratamento. '+
|
||||
'Manda em <b>PNG ou TIFF</b>, ou divide em partes de até 5 m, que sai na hora.</em>':'')+
|
||||
(larga? '<em><b>Esta folha não roda neste filme.</b> Ela tem '+n1(x.med.larg)+
|
||||
'Manda em <b>PNG ou TIFF</b>, ou divide em partes de até 5 m, que sai na hora.</p>':'')+
|
||||
(larga? '<p class="avisoF"><b>Esta folha não roda neste filme.</b> Ela tem '+n1(x.med.larg)+
|
||||
' cm de largura e o filme aqui tem '+n1(L)+' cm. Não dá para reduzir: '+
|
||||
'encolher a folha encolheria cada arte junto.'+
|
||||
(x.med.larg<=57.5
|
||||
? ' Ela cabe no <b>DTF têxtil de 57 cm</b> — troque de produto ali em cima.'
|
||||
: ' Remonte a folha em '+n1(L)+' cm e suba de novo.')+'</em>':'')+
|
||||
'</div>';
|
||||
: ' Remonte a folha em '+n1(L)+' cm e suba de novo.')+'</p>':'');
|
||||
}else if(x.measurementError){
|
||||
medida='<div class="med alerta"><b>Este PDF não pode ser orçado</b><span>'+
|
||||
escapeHTML(x.measurementError)+'</span></div>';
|
||||
medida='<span class="repPeso">'+escapeHTML(extDe(x.f.name))+' · '+fmt(x.f.size)+'</span>'+
|
||||
'<p class="avisoF"><b>Este PDF não pode ser orçado.</b> '+escapeHTML(x.measurementError)+'</p>';
|
||||
}else{
|
||||
const teto=TABELA[modo], piso=pisoEscada();
|
||||
const dif=(x.m>0)? (teto-piso)*cobrar(x.m*(x.rep||1)) : 0;
|
||||
medida='<div class="med pede">'+
|
||||
'<b>'+escapeHTML(extDe(x.f.name))+' não abre no navegador</b>'+
|
||||
'<span>Sem conseguir conferir o arquivo, não temos como dar nota — e sem nota '+
|
||||
'o metro sai pela tabela, <b>'+rs(teto)+'</b>. Alguém vai abrir esse arquivo na mão '+
|
||||
'antes de imprimir.</span>'+
|
||||
'<div class="troca">Exportando a mesma folha em <b>PNG ou PDF</b>, a conferência é '+
|
||||
'automática e o metro pode cair até <b>'+rs(piso)+'</b>'+
|
||||
(dif>0.5? ' — <b>'+rs(dif)+' a menos</b> só nesta folha' : '')+'.</div>'+
|
||||
'<span style="margin-top:9px">Informe o comprimento para seguir:</span>'+
|
||||
'<input type="number" min="0.5" max="60" step="0.01" data-comp="'+i+'" '+
|
||||
'placeholder="metros" value="'+(x.m? x.m.toFixed(2):'')+'">'+
|
||||
'</div>';
|
||||
medida='<span class="chipSo">Conferência manual · preço cheio</span>'+
|
||||
'<p class="repTxt">O navegador não abre '+escapeHTML(extDe(x.f.name))+', então alguém da equipe '+
|
||||
'confere antes de imprimir e o metro sai pela tabela, <b>'+rs(teto)+'</b>. '+
|
||||
'Exportando em <b>PNG ou PDF</b>, o metro pode cair até <b>'+rs(piso)+'</b>'+
|
||||
(dif>0.5? ' — <b>'+rs(dif)+' a menos</b> só nesta folha' : '')+'.</p>';
|
||||
manual='<div class="campo"><label for="comp'+i+'">Comprimento</label>'+
|
||||
'<span class="un"><input type="number" id="comp'+i+'" min="0.5" max="60" step="0.01" data-comp="'+i+'" '+
|
||||
'placeholder="0,00" value="'+(x.m? x.m.toFixed(2):'')+'"><i>m</i></span></div>';
|
||||
}
|
||||
const barra = x.pct!=null
|
||||
? '<div class="prog"><i style="width:'+x.pct+'%"></i></div>'+
|
||||
@@ -433,36 +455,41 @@ function pintaFolha(){
|
||||
'. Seguimos com o preço de tabela — se quiser a nota e o desconto, exporte a mesma '+
|
||||
'folha em <b>PNG</b>, que a conferência é bem mais leve.</div>'
|
||||
: '';
|
||||
return '<div class="rep" data-folha="'+i+'"><div class="l1"><b>'+
|
||||
(folhas.length>1? (i+1)+'. ':'')+escapeHTML(x.f.name)+'</b>'+
|
||||
'<span style="color:var(--fraco);font-size:10.5px">'+fmt(x.f.size)+'</span>'+
|
||||
'<button data-rmf="'+i+'">×</button></div>'+
|
||||
barra+medida+
|
||||
(x.m>0 ?
|
||||
'<div class="cps" style="margin-top:10px">'+
|
||||
'<div><label>quantas vezes repetir</label>'+
|
||||
'<input type="number" min="1" max="200" data-repf="'+i+'" value="'+x.rep+'"></div>'+
|
||||
'<div class="conta"><b>'+fmtM(sub)+' m</b>'+
|
||||
(x.rep>1? ' · '+x.rep+' × '+fmtM(x.m)+' m':'')+'</div>'+
|
||||
'</div>' : '')+
|
||||
// A small thumbnail made once: the list is rebuilt on every change, and the
|
||||
// full preview of a file up to 150 MB would be re-parsed each time.
|
||||
const mini = x.miniSrc
|
||||
? '<img src="'+escapeHTML(x.miniSrc)+'" alt="">'
|
||||
: '<span>'+escapeHTML(extDe(x.f.name))+'</span>';
|
||||
const copias = x.m>0 || manual ?
|
||||
'<div class="cps">'+manual+
|
||||
(x.m>0 ?
|
||||
'<div class="campo"><label for="repf'+i+'">Cópias</label>'+
|
||||
'<span class="passo"><button type="button" data-passo="-1" data-i="'+i+'" aria-label="Menos uma cópia">−</button>'+
|
||||
'<input type="number" id="repf'+i+'" min="1" max="200" data-repf="'+i+'" value="'+x.rep+'">'+
|
||||
'<button type="button" data-passo="1" data-i="'+i+'" aria-label="Mais uma cópia">+</button></span></div>'+
|
||||
'<div class="conta">'+(x.rep>1? x.rep+' × '+fmtM(x.m)+' m = ' : '')+'<b>'+fmtM(sub)+' m</b></div>' : '')+
|
||||
'</div>' : '';
|
||||
return '<div class="rep" data-folha="'+i+'">'+
|
||||
'<div class="repTopo"><div class="miniF">'+mini+'</div><div class="repInfo">'+
|
||||
'<div class="l1"><b>'+(folhas.length>1? (i+1)+'. ':'')+escapeHTML(x.f.name)+'</b>'+
|
||||
'<button data-rmf="'+i+'" aria-label="Remover '+escapeHTML(x.f.name)+'">×</button></div>'+
|
||||
(lido || x.measurementError ? '' : '<span class="repPeso">'+escapeHTML(extDe(x.f.name))+' · '+fmt(x.f.size)+'</span>')+
|
||||
barra+medida+
|
||||
'</div></div>'+copias+
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
// total do pedido, quando há mais de uma folha
|
||||
const tot=folhaTotalM(), partes=Math.ceil(tot/20);
|
||||
if(folhas.length>1 && tot>0){
|
||||
$('lista').innerHTML += '<div class="somaF">'+
|
||||
'<span>'+folhas.length+' folhas neste pedido</span>'+
|
||||
'<b>'+fmtM(tot)+' m no total</b>'+
|
||||
(partes>1? '<em>vai em '+partes+' partes de até 20 m</em>':'')+'</div>';
|
||||
}else if(partes>1 && tot>0){
|
||||
$('lista').innerHTML += '<div class="somaF"><span>folha longa</span>'+
|
||||
'<b>'+fmtM(tot)+' m</b><em>vai em '+partes+' partes de até 20 m</em></div>';
|
||||
}
|
||||
|
||||
$('lista').querySelectorAll('[data-rmf]').forEach(b=>b.addEventListener('click',()=>{
|
||||
folhas.splice(+b.dataset.rmf,1); pintaFolha();
|
||||
}));
|
||||
$('lista').querySelectorAll('[data-det]').forEach(d=>d.addEventListener('toggle',()=>{
|
||||
folhas[+d.dataset.det].detAberto=d.open;
|
||||
}));
|
||||
$('lista').querySelectorAll('[data-passo]').forEach(b=>b.addEventListener('click',()=>{
|
||||
const el=$('repf'+b.dataset.i);
|
||||
el.value=Math.max(1,Math.min(200,(+el.value||1)+(+b.dataset.passo)));
|
||||
el.dispatchEvent(new Event('change'));
|
||||
}));
|
||||
$('lista').querySelectorAll('[data-repf]').forEach(el=>el.addEventListener('change',e=>{
|
||||
folhas[+el.dataset.repf].rep=Math.max(1,Math.min(200,+e.target.value||1)); pintaFolha();
|
||||
}));
|
||||
@@ -501,6 +528,7 @@ function pintaProgressoArte(a){
|
||||
}
|
||||
function pintaArtes(){
|
||||
invalidaItemAtual();
|
||||
$('zona').classList.toggle('compacta', artes.length>0);
|
||||
$('lista').innerHTML=artes.map((a,i)=>{
|
||||
let barra='', dica='', calc='<span>informe a largura para ver a qualidade</span>';
|
||||
if(a.lendo!=null){
|
||||
|
||||
Reference in New Issue
Block a user