style: redesign PDF as full-page cyberpunk dashboard
CI and Deploy / test-build-deploy (push) Successful in 4s

This commit is contained in:
Hermes Agent
2026-08-03 14:44:34 +00:00
parent 9f8e130d41
commit 07e7bf25c6
2 changed files with 153 additions and 41 deletions
+140 -37
View File
@@ -1217,51 +1217,154 @@ function wrapPdfLines(lines, width=92){
return out;
}
function parsePdfRecipe(lines){
const source=lines.map(line=>String(line));
const title=source[0]||"PIZZATEIG";
const overview=[];
const sections=[];
let current=null, link="";
source.slice(1).forEach(raw=>{
const line=raw.trim();
if(!line) return;
if(/^https?:\/\//.test(line)){ link=line; return; }
if(/^(ZUTATEN|HAUPTTEIG|ZEITPLAN|VORTEIG\b)/.test(line)){
current={title:line,items:[]};
sections.push(current);
return;
}
if(current) current.items.push(line);
else overview.push(...line.split(" · ").map(part=>part.trim()).filter(Boolean));
});
if(overview.length>4) sections.unshift({title:"WEITERE DATEN",items:overview.slice(4)});
return {title,overview:overview.slice(0,4),sections,link};
}
function buildPdfBytes(lines){
const encoder=new TextEncoder();
const title=String(lines[0]||"PIZZATEIG");
let body=wrapPdfLines(lines.slice(1),88);
const maxRows=62;
if(body.length>maxRows) body=[...body.slice(0,maxRows-1),"[... weitere Angaben gekürzt]"];
const data=parsePdfRecipe(lines);
const ingredients=data.sections.filter(section=>section.title!=="ZEITPLAN");
const timeline=data.sections.find(section=>section.title==="ZEITPLAN")||{title:"ZEITPLAN",items:[]};
const capItems=(items,max)=>items.length>max?[...items.slice(0,max-1),"... weitere Angaben gekürzt"]:items;
const units=body.reduce((sum,line)=>sum+(line ? 1 : .55),0)||1;
const lineHeight=Math.max(8.5,Math.min(13,610/units));
const bodySize=Math.max(6.8,Math.min(9.5,lineHeight-1.8));
let stream="";
const rect=(x,y,w,h,fill,stroke="",lineWidth=.7)=>{
if(fill) stream+=`${fill} rg ${x} ${y} ${w} ${h} re f\n`;
if(stroke) stream+=`${stroke} RG ${lineWidth} w ${x} ${y} ${w} ${h} re S\n`;
};
const line=(x1,y1,x2,y2,color,width=.7)=>{ stream+=`${color} RG ${width} w ${x1} ${y1} m ${x2} ${y2} l S\n`; };
const text=(value,x,y,size,font="F1",color="0.886 0.925 0.961")=>{
stream+=`BT\n/${font} ${size} Tf\n${color} rg\n${x} ${y} Td\n<${winAnsiHex(value)}> Tj\nET\n`;
};
const wrapped=(value,width)=>wrapPdfLines([String(value)],width);
let stream="q\n";
// Dunkle Grundfläche und dezentes Terminal-Raster.
stream+="0.027 0.031 0.043 rg 0 0 595 842 re f\n";
stream+="0.055 0.082 0.114 RG 0.3 w\n";
for(let x=19;x<595;x+=32) stream+=`${x} 0 m ${x} 842 l S\n`;
for(let y=10;y<842;y+=32) stream+=`0 ${y} m 595 ${y} l S\n`;
// Panel, Neon-Kanten und asymmetrische Akzentleisten.
stream+="0.063 0.082 0.122 rg 28 58 539 704 re f\n";
stream+="0 0.941 1 RG 0.9 w 28 58 539 704 re S\n";
stream+="0 0.941 1 rg 28 786 539 3 re f\n";
stream+="1 0.176 0.471 rg 28 776 128 5 re f\n";
stream+="1 0.176 0.471 rg 552 58 15 15 re f\nQ\n";
// Vollflächige Cyberpunk-Bühne mit Raster, Scanlines und Neonrahmen.
rect(0,0,595,842,"0.027 0.031 0.043");
rect(0,704,595,138,"0.039 0.051 0.078");
stream+="0.047 0.071 0.102 RG 0.25 w\n";
for(let x=18;x<595;x+=24) stream+=`${x} 0 m ${x} 842 l S\n`;
for(let y=8;y<842;y+=24) stream+=`0 ${y} m 595 ${y} l S\n`;
stream+="0.075 0.094 0.133 RG 0.18 w\n";
for(let y=3;y<842;y+=6) stream+=`0 ${y} m 595 ${y} l S\n`;
rect(18,18,559,806,"","0.12 0.23 0.30",.6);
line(18,823,577,823,"0 0.941 1",2.2);
line(18,816,170,816,"1 0.176 0.471",4.5);
line(424,26,577,26,"1 0.176 0.471",3);
// Kopfzeile im Stil der Anwendung.
stream+=`BT\n/F2 22 Tf\n0 0.941 1 rg\n36 805 Td\n<${winAnsiHex("TEIG//TERMINAL")}> Tj\nET\n`;
stream+=`BT\n/F1 7.5 Tf\n1 0.176 0.471 rg\n37 793 Td\n<${winAnsiHex("RECIPE EXPORT // OFFLINE GENERATED // SYSTEM READY")}> Tj\nET\n`;
stream+=`BT\n/F2 14 Tf\n0.922 0.965 1 rg\n42 741 Td\n<${winAnsiHex(title)}> Tj\nET\n`;
stream+="0 0.941 1 RG 0.7 w 42 729 m 553 729 l S\n";
// Kopf: doppelte Neon-Typografie, Statusblock und technische Marker.
text("TEIG//TERMINAL",35,778,29,"F2","0.23 0.02 0.19");
text("TEIG//TERMINAL",33,780,29,"F2","0 0.941 1");
text("PIZZA FORMULATION SYSTEM",35,765,7.5,"F1","1 0.176 0.471");
rect(414,770,145,28,"0.055 0.071 0.106","1 0.176 0.471",.8);
rect(425,781,5,5,"0.545 1 0.231");
text("RECIPE // READY",438,780,8.2,"F2","0.922 0.965 1");
text("06",507,716,42,"F2","0.10 0.19 0.24");
text(data.title,34,724,16,"F2","0.922 0.965 1");
text("FORMULA EXPORT // DIN A4 // ONE PAGE",35,709,6.8,"F1","0.53 0.64 0.74");
let y=711;
body.forEach(line=>{
if(!line){ y-=lineHeight*.55; return; }
const section=/^[A-ZÄÖÜ][A-ZÄÖÜ0-9 /()%-]{2,}$/.test(line.trim()) && line.trim().length<42;
const isLink=/^https?:\/\//.test(line.trim());
const font=section?"F2":"F1";
const size=section?Math.min(10.5,bodySize+1):isLink?Math.max(6.5,bodySize-1):bodySize;
const color=section?"1 0.176 0.471":isLink?"0 0.941 1":"0.886 0.925 0.961";
if(section) stream+=`0 0.941 1 rg 42 ${y-1.5} 5 5 re f\n`;
stream+=`BT\n/${font} ${size.toFixed(1)} Tf\n${color} rg\n${section?53:42} ${y.toFixed(1)} Td\n<${winAnsiHex(line.trimEnd())}> Tj\nET\n`;
y-=lineHeight;
// Vier Dashboard-Kacheln nutzen die gesamte Breite für die Kerndaten.
const cards=[34,167,300,433];
cards.forEach((x,index)=>{
rect(x,634,126,60,index%2?"0.055 0.071 0.106":"0.063 0.082 0.122",index===3?"1 0.176 0.471":"0 0.941 1",.65);
text(`DATA // 0${index+1}`,x+9,678,6.3,"F2",index===3?"1 0.176 0.471":"0 0.941 1");
const value=data.overview[index]||"";
const parts=wrapped(value,23).slice(0,2);
parts.forEach((part,row)=>text(part,x+9,659-row*11,8.2,"F2","0.922 0.965 1"));
rect(x+113,637,10,3,index===3?"1 0.176 0.471":"0 0.941 1");
});
stream+="0 0.941 1 RG 0.5 w 42 47 m 553 47 l S\n";
stream+=`BT\n/F1 7 Tf\n0 0.941 1 rg\n42 35 Td\n<${winAnsiHex("TEIG//TERMINAL // LOCAL PDF // A4 // 1 OF 1")}> Tj\nET\n`;
// Zwei große Module: Zutaten/Formel links, Prozess-Timeline rechts.
rect(34,112,238,500,"0.043 0.059 0.086","0 0.941 1",.75);
rect(286,112,273,500,"0.043 0.059 0.086","1 0.176 0.471",.75);
rect(34,568,238,44,"0.00 0.22 0.26");
rect(286,568,273,44,"0.25 0.035 0.15");
text("01",47,580,17,"F2","0 0.941 1");
text("FORMULA // ZUTATEN",79,584,10.5,"F2","0.922 0.965 1");
text("02",299,580,17,"F2","1 0.176 0.471");
text("PROCESS // ZEITPLAN",332,584,10.5,"F2","0.922 0.965 1");
let leftY=545, itemNo=1;
const ingredientRows=[];
ingredients.forEach(section=>{
ingredientRows.push({heading:section.title});
capItems(section.items,12).forEach(item=>ingredientRows.push({item}));
});
if(!ingredientRows.length) ingredientRows.push({heading:"ZUTATEN"},{item:"Keine Angaben"});
const ingredientCount=Math.max(1,ingredientRows.filter(row=>row.item).length);
const headingCount=ingredientRows.filter(row=>row.heading).length;
const leftStep=Math.max(28,Math.min(62,(390-headingCount*24)/ingredientCount));
ingredientRows.slice(0,15).forEach((row,index)=>{
if(row.heading){
text(row.heading,48,leftY,8.8,"F2","1 0.176 0.471");
line(48,leftY-7,255,leftY-7,"0.18 0.30 0.36",.45);
leftY-=24;
return;
}
const rowH=Math.max(25,leftStep-4);
rect(46,leftY-rowH+7,214,rowH,index%2?"0.055 0.071 0.106":"0.063 0.082 0.122");
rect(46,leftY-rowH+7,4,rowH,itemNo%2?"0 0.941 1":"1 0.176 0.471");
text(String(itemNo).padStart(2,"0"),57,leftY-8,7,"F2","0.35 0.52 0.61");
const amount=row.item.match(/^(.*\S)\s+([0-9][0-9.,]*\s*(?:g|%|ml))$/i);
if(amount){
const labelLines=wrapped(amount[1],20).slice(0,2);
labelLines.forEach((part,i)=>text(part,82,leftY-6-i*10,8.2,"F1","0.922 0.965 1"));
const amountX=Math.max(184,248-amount[2].length*6.1);
text(amount[2],amountX,leftY-7,10,"F2",itemNo%2?"0 0.941 1":"1 0.176 0.471");
}else{
const itemLines=wrapped(row.item,34).slice(0,2);
itemLines.forEach((part,i)=>text(part,78,leftY-5-i*10,8.3,"F1","0.922 0.965 1"));
}
leftY-=leftStep; itemNo++;
});
text("BAKER'S PERCENTAGE // MASS CONTROL",48,130,6.5,"F1","0.36 0.49 0.58");
for(let x=48;x<255;x+=9) rect(x,121,(x%27===0)?4:2,5,"0 0.941 1");
const steps=capItems(timeline.items,10);
const timelineTop=546, timelineBottom=150;
const slot=(timelineTop-timelineBottom)/Math.max(1,steps.length);
line(309,timelineBottom,309,timelineTop,"0 0.941 1",1.1);
steps.forEach((raw,index)=>{
const y=timelineTop-index*slot;
const parts=raw.split(/\s+—\s+/);
const when=parts.length>1?parts.shift():String(index+1).padStart(2,"0");
const action=parts.length?parts.join(" — "):raw;
rect(304,y-2,10,10,"0.043 0.059 0.086","0 0.941 1",1);
rect(307,y+1,4,4,index===steps.length-1?"1 0.176 0.471":"0 0.941 1");
text(when,324,y+1,8.6,"F2",index===steps.length-1?"1 0.176 0.471":"0 0.941 1");
const actionLines=wrapped(action,38).slice(0,2);
actionLines.forEach((part,row)=>text(part,324,y-11-row*10,7.8,"F1","0.922 0.965 1"));
if(index<steps.length-1) line(324,y-slot+9,545,y-slot+9,"0.12 0.19 0.24",.35);
});
if(!steps.length) text("Kein Zeitplan vorhanden",324,530,8.5,"F1","0.53 0.64 0.74");
text("FERMENTATION SEQUENCE // REVERSE SCHEDULE",304,130,6.5,"F1","0.45 0.39 0.57");
// Link-Zeile und Footer schließen die Seite wie ein System-Dashboard ab.
rect(34,61,525,35,"0.039 0.051 0.078","0.17 0.31 0.37",.5);
text("RECIPE LINK",46,82,6.3,"F2","1 0.176 0.471");
const linkLines=wrapped(data.link||"Lokales Rezept ohne Link",102).slice(0,2);
linkLines.forEach((part,index)=>text(part,111,80-index*8,5.7,"F1","0 0.941 1"));
text("SYS.ONLINE",34,38,6.5,"F2","0.545 1 0.231");
text("NO CLOUD // NO TRACKING // GENERATED LOCALLY",112,38,6.5,"F1","0.53 0.64 0.74");
text("A4 // 1 OF 1",484,38,6.5,"F2","1 0.176 0.471");
const objects=[];
objects[1]="<< /Type /Catalog /Pages 2 0 R >>";