/* Site DTF — Product tables, commercial ladders, shared state and field validation. Extracted verbatim from the single inline script in web/index.html. Loaded as classic scripts in the order listed there: they share one global scope and run top to bottom, exactly as the original did. */ const $=id=>document.getElementById(id); const rs=v=>'R$ '+v.toFixed(2).replace('.',','); // Ícones (Lucide, licença ISC) desenhados no próprio HTML: nada a carregar, e a // cor vem do texto em volta. Os que ficam no HTML estático são os mesmos. const ICONES={ busca:'', conta:'', sacola:'', enviar:'', girar:'', espelhar:'', fechar:'', adicionar:'', cadeado:'', lixeira:'', local:'', caminhao:'', cartao:'', qrcode:'', copiar:'', relogio:'', ok:'', okCirculo:'', alerta:'', refazer:'', }; const icone=n=>''; const fmt=b=>b<1048576?(b/1024).toFixed(0)+' KB' :b<1073741824?(b/1048576).toFixed(1).replace('.',',')+' MB':(b/1073741824).toFixed(1).replace('.',',')+' GB'; // ── modos const MODOS={ file: {tit:'Arquivo por metro', lg:'DTF têxtil · 57 cm', larg:57, multi:true, folha:true, zt:'Arraste sua folha montada aqui', zs:'PNG, JPG ou PDF · 57 cm de largura'}, avulsa:{tit:'Artes avulsas', lg:'DTF têxtil · 57 cm', larg:57, multi:true, zt:'Arraste suas artes aqui', zs:'PNG ou JPG com fundo transparente · informe a largura de cada uma'}, uvfile:{tit:'Arquivo por metro · UV', lg:'Adesivo UV · 28,5 cm', larg:28.5, multi:true, folha:true, zt:'Arraste sua folha de UV aqui', zs:'PNG, JPG ou PDF · 28,5 cm de largura'}, uv: {tit:'Artes avulsas · UV', lg:'Adesivo UV · 28,5 cm', larg:28.5, multi:true, zt:'Arraste suas artes de UV', zs:'PNG ou JPG com fundo transparente · informe a largura de cada arte'} }; // Quatro escadas. A de artes avulsas e a da folha pronta mais a montagem: // R$ 10,00 no têxtil, R$ 14,00 no UV, constante em todas as faixas. const FAIXAS={ file: [[90,14.90],[75,16.20],[60,17.50],[40,18.70],[0,19.90]], avulsa:[[90,24.90],[75,26.20],[60,27.50],[40,28.70],[0,29.90]], uvfile:[[90,69.90],[75,73.90],[60,77.90],[40,81.90],[0,85.90]], uv: [[90,83.90],[75,87.90],[60,91.90],[40,95.90],[0,99.90]] }; const TABELA={file:19.90, avulsa:29.90, uvfile:85.90, uv:99.90}; // teto de cada escada const MONTAGEM={avulsa:10.00, uv:14.00}; // o que a montagem soma sobre a folha pronta const ehFolha=m=>!!MODOS[m||modo].folha; const pisoEscada=m=>FAIXAS[m||modo][0][1]; const MINIMO_M=1, GAP_CM=0.5, RODAPE_CM=5; const RENDERIZA=/[.](png|jpe?g|webp|gif|bmp)$/i; // Folha pronta: dá para medir PNG, JPG e PDF. PSD, AI, TIFF e CDR entram, mas // o navegador não abre — o cliente informa o comprimento e o arquivo passa por // tratamento antes de imprimir. const ACEITA={ folha: /[.](png|jpe?g|tiff?|pdf|psd|psb|ai|cdr)$/i, avulsa: /[.](png|jpe?g|webp)$/i }; const MEDE=/[.](png|jpe?g|webp|gif|bmp|pdf)$/i; const DPI_RECUSA=96, DPI_AVISA=300; // régua dos designers · 02/09/2026 // A especificação do PDF limita a página a 14.400 unidades — 200 pol, 508 cm. // Passar disso só é válido com UserUnit (PDF 1.6+), que a maioria dos programas // não escreve: o arquivo sai fora do padrão e só o gerador lê direito. const PDF_MAX_PT=14400, PT_CM=2.54/72; const extDe=n=>(n.split('.').pop()||'').toUpperCase(); let CLIENTE='SEU NOME AQUI', PEDIDO='000000'; let modo=null, artes=[], recemChegada=null, metros=0, nota=0, reencaixado=false; let folhas=[]; // [{f, med, rep, m}] · o cliente pode subir várias const folhaTotalM=()=>folhas.reduce((t,x)=>t+(x.m||0)*(x.rep||1),0); // O cliente declara o que está mandando escolhendo o produto, em vez de o site // adivinhar pelo formato do arquivo. A declaração de folha montada é conferida na // medição: uma imagem sem pixel para ocupar a largura do filme não é uma folha. // Cada modo por metro tem o seu par de artes avulsas, e o seletor anda nos dois sentidos. const PAR={file:'avulsa', avulsa:'file', uvfile:'uv', uv:'uvfile'}; let montagemCm=0; // altura da folha montada pelo motor, em cm let montagemLayout=null; // posições aprovadas de cada cópia, em cm const ZOOMS=[1,1.5,2,3,4]; let zoomI=0; // ampliação da montagem ao vivo let pedido=[]; // itens já fechados · o pagamento é um só // A escolha de entrega vale para o pedido inteiro, não por item. No Tiny, retirada // grava forma_envio X, transportador DropStar e frete por conta do remetente — igual // ao que a expedição já recebe hoje, sem criar processo novo. let entrega={tipo:'retira', cep:'', valor:0, cotado:true, end:{}}; let cliente={cnpj:'', zap:'', mail:''}; // CNPJ com os dois dígitos verificadores · máscara sem conta não vale nada function cnpjOk(v){ const d=(v||'').replace(/\D/g,''); if(d.length!==14 || /^(\d)\1{13}$/.test(d)) return false; const calc=(base,pesos)=>{ const soma=base.split('').reduce((t,n,i)=>t+ +n*pesos[i],0); const r=soma%11; return r<2? 0 : 11-r; }; const p1=[5,4,3,2,9,8,7,6,5,4,3,2], p2=[6,...p1]; return calc(d.slice(0,12),p1)===+d[12] && calc(d.slice(0,13),p2)===+d[13]; } const zapOk=v=>{ const d=(v||'').replace(/\D/g,''); return d.length===10||d.length===11; }; const mailOk=v=>/^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test((v||'').trim()); const clienteOk=()=>cnpjOk(cliente.cnpj)&&zapOk(cliente.zap)&&mailOk(cliente.mail); // Frete cotado pelo CEP não entrega nada sozinho: a transportadora e o Tiny // precisam do endereço completo antes do pagamento. const UFS='AC AL AP AM BA CE DF ES GO MA MT MS MG PA PB PR PE PI RJ RN RS RO RR SC SP SE TO'.split(' '); const enderecoOk=()=>{ if(entrega.tipo!=='frete') return true; const e=entrega.end||{}, t=v=>(v||'').trim(); return t(e.nome).length>=2 && t(e.rua).length>=2 && t(e.num).length>=1 && t(e.bairro).length>=2 && t(e.cidade).length>=2 && UFS.includes(t(e.uf).toUpperCase()); }; const destinoApi=()=>entrega.tipo!=='frete' ? null : { recipient:entrega.end.nome.trim(), street:entrega.end.rua.trim(), number:entrega.end.num.trim(), complement:(entrega.end.comp||'').trim(), district:entrega.end.bairro.trim(), city:entrega.end.cidade.trim(), state:entrega.end.uf.trim().toUpperCase(), postal_code:entrega.cep}; // Tempo de envio antes de medir a internet de quem compra: uma faixa entre um // upload lento (10 Mbps) e um rápido (150 Mbps). O arquivo vai direto para o // armazenamento, então é o upload do cliente que manda. const ENVIO_LENTO=10e6/8, ENVIO_RAPIDO=150e6/8; const duracao=s=>{ const min=Math.ceil(s/60); return s<60 ? 'menos de 1 min' : min<60 ? min+' min' : Math.floor(min/60)+' h'+(min%60? ' '+min%60+' min' : ''); }; const faixaEnvio=bytes=>bytes/ENVIO_RAPIDO<60 ? 'até '+duracao(bytes/ENVIO_LENTO) : 'de '+duracao(bytes/ENVIO_RAPIDO)+' a '+duracao(bytes/ENVIO_LENTO); // Quanto falta para ler um arquivo grande no navegador, pelo ritmo até aqui. // A leitura é quase linear, então o tempo gasto diz o que resta. function faltaLeitura(l){ if(!l || !(l.p>0.02)) return 'calculando o tempo…'; const s=(Date.now()-l.inicio)/1000*(1-l.p)/l.p; return s<10 ? 'faltam '+Math.max(1,Math.ceil(s))+' s' : s<60 ? 'faltam cerca de '+Math.ceil(s/5)*5+' s' : 'faltam cerca de '+duracao(s); } const larguraFilme=()=>MODOS[modo].larg; // cobrança proporcional: arredonda para cima em 10 cm · 2,75 m vira 2,80 m const cobrar=m=>Math.max(MINIMO_M, Math.ceil(m*10)/10); const fmtM=m=>m.toFixed(2).replace('.',','); const precoBase=n=> (FAIXAS[modo].find(f=>n>=f[0])||FAIXAS[modo][FAIXAS[modo].length-1])[1]; const descontoMetragem=()=>0; // sem progressão por quantidade const cls=n=>n>=85?'ok':n>=50?'av':'er'; const dpiDe=a=>a.cm>0? (a.giro?a.py:a.px)/(a.cm/2.54) : 0; // girar 90° inverte a proporção · espelhar não muda medida, só o desenho const propDe=a=>{ const p=a.prop||1; return a.giro? 1/p : p; }; const TAMANHOS={57:[10,15,20,25,28.2], 28.5:[5,8,10,14,28.2]};