diff --git a/apps/studio/src/components/Ref17Cover.tsx b/apps/studio/src/components/Ref17Cover.tsx index 5f88822..37556a4 100644 --- a/apps/studio/src/components/Ref17Cover.tsx +++ b/apps/studio/src/components/Ref17Cover.tsx @@ -11,7 +11,7 @@ export function Ref17Cover({template,slide,masthead=template.masthead,guides=fal const bodySize=Math.min(32,...body.map(s=>sW/Math.max(1,Array.from(s).reduce((n,c)=>n+(/[\u3400-\u9fff]/.test(c)?1:.5),0)))); const accent=slide.accent||'#d8ddd4'; return - {slide.label||slide.heading} · 模板试版源自 ref-17 的九个固定构件。图片位使用色块,占位文案可替换,暂无具体工具内容。 + {(slide.label||slide.heading)+' · 模板试版'}源自 ref-17 的九个固定构件。图片位使用色块,占位文案可替换,暂无具体工具内容。 diff --git a/apps/studio/src/lib/native-layout.ts b/apps/studio/src/lib/native-layout.ts index fe174a4..ea19c4f 100644 --- a/apps/studio/src/lib/native-layout.ts +++ b/apps/studio/src/lib/native-layout.ts @@ -9,28 +9,29 @@ export const designUrl=(layoutId:string,id:string)=>`/studio/layouts/${layoutId} export function nativeLayout(layout:Layout):Layout { return {id:layout.id,name:layout.name,width:layout.width,height:layout.height,elements:layout.elements.map(({id,kind,role,box,direction,text,reading})=>({id,kind,role,box,direction,text,reading}))}; } -// Four coordinated inks per composition: paper, ink, image field and accent. +// Each composition has its own paper and inks; layered artwork can add a middle tone. const palettes=[ ['#f3eee4','#293b42','#7c9a9e','#c4d5cb'],['#f5f2e9','#344335','#879782','#d7c39f'],['#efede7','#383d4a','#8996a6','#c5d0d7'], - ['#647c7a','#f5f0e5','#b6c8be','#d9ded0'],['#f5eddf','#5c3b32','#b47d68','#d9bba0'],['#eceee8','#263d43','#688a94','#b4c7c7'], + ['#5e7472','#f5f0e5','#354e4a','#d9ded0'],['#f5eddf','#5c3b32','#b47d68','#d9bba0'],['#eceee8','#263d43','#688a94','#b4c7c7'], ['#f3eddf','#4c4a36','#a1a185','#d8cba9'],['#edf0ef','#23424b','#73929a','#b6cbd2'],['#f5f1e9','#2f3e38','#7f9688','#d1d7ba'], ['#eeece4','#344b57','#526e80','#afc4c9'],['#f4eee8','#4e393b','#a58a90','#d8bdb3'],['#f1f0e9','#424b36','#8c997b','#d1c7aa'], ['#f3ebe5','#5b3e39','#b48e7e','#dec6ab'],['#eef0e7','#384c42','#789485','#c4d1b5'],['#e9edee','#2e4555','#839ba8','#c6d3d4'], ['#f4f0e7','#50483a','#ada18a','#d9d1c0'],['#eeeee5','#394d42','#7e9785','#c8d5bc'],['#f2ede6','#3e424e','#8a92a3','#d0c7c0'], ['#f5eede','#443d31','#a79b7a','#dbca9f'],['#eeefe8','#34463f','#809b89','#c5d3ba'],['#f0ebe5','#51413d','#a48b7f','#d5bfb3'], - ['#64797d','#f8f2e8','#9fb3b5','#cbd4c9'],['#f0efe9','#2f414d','#8199a7','#c5d2d6'],['#f4eee5','#5a443b','#ae927b','#dfc8ac'], + ['#64797d','#142a2e','#b5c7c7','#3d565b','#7c9497'],['#f0efe9','#2f414d','#8199a7','#c5d2d6'],['#f4eee5','#5a443b','#ae927b','#dfc8ac'], ['#ebeee8','#344b41','#829c8b','#c7d5bb'],['#edece8','#3c4354','#8b93aa','#c5cbd7'],['#f2ece2','#3b4e4b','#6f918c','#b8cdc3'] ]; export function layoutPalette(id:string){return palettes[Number(id.slice(4))-1]||['#ffe5df','#303d43','#a9bec0','#f0bca6']} export function paperColor(id:string){return layoutPalette(id)[0]} -export function textColor(id:string,slot:Slot){return (id==='ref-04'&&['T01','T02'].includes(slot.id))||(id==='ref-22'&&slot.id==='T01')||(id==='ref-27'&&slot.id==='T01')||(id==='ref-10'&&slot.id==='T03')?'#f9faf5':layoutPalette(id)[1]} +export function textColor(id:string,slot:Slot){return (id==='ref-04'&&['T01','T02'].includes(slot.id))||(id==='ref-22'&&['T01','T03','T04'].includes(slot.id))||(id==='ref-27'&&slot.id==='T01')||(id==='ref-10'&&slot.id==='T03')?'#f9faf5':layoutPalette(id)[1]} // The screenshot's illegible small print is replaced with editable editorial copy. export function defaultText(layout:Layout,slot:Slot):string { if(slot.text)return slot.text; if(slot.direction==='curved')return 'BULUBLUE DESIGN · FORM AND SPACE · EDITORIAL DESIGN ·'; if(slot.role.includes('日期'))return '2025 / 03 / 25'; + if(slot.role.includes('微字'))return slot.box[2]>60?'FORM / SPACE / DESIGN / 2026':'FORM / '+layout.id.slice(-2); if(slot.role.includes('英文')||slot.role.includes('品牌')||slot.role.includes('标识'))return 'BULUBLUE DESIGN'; - if(slot.role.includes('图注')||slot.role.includes('标签')||slot.box[3]<10)return 'FORM / SPACE / DESIGN'; + if(slot.role.includes('图注')||slot.role.includes('标签')||slot.box[3]<10)return slot.box[2]<25?'FORM / '+layout.id.slice(-2):'FORM / SPACE / DESIGN'; if(slot.direction==='vertical')return '设计灵感 · 日常观察'; return slot.box[2]<25?'留白之间\n观察日常\n记录灵感':'让文字与形状相遇。\n在留白之间,寻找新的秩序。'; } @@ -42,14 +43,16 @@ export function textSize(layout:Layout,slot:Slot,text:string):number { const length=(s:string)=>Array.from(s).reduce((n,c)=>n+(/[\u3400-\u9fff]/.test(c)?1:/[A-Z]/.test(c)?.8:.6),0); if(slot.direction==='vertical')return Math.min(w*.9,h/Math.max(1,Array.from(text.replaceAll('\n','')).length)*.88); if(slot.direction==='rotated')return Math.min(w*.85,h/Math.max(1,length(text))); - if(slot.direction==='curved')return Math.min(2.2,w/Math.max(1,length(text))); - return Math.min(h/(lines.length*1.15),w/Math.max(1,...lines.map(length)))*.92; + if(slot.direction==='curved')return Math.min(2.2,Math.max(w,h)*1.1/Math.max(1,length(text))); + const fitted=Math.min(h/(lines.length*1.15),w/Math.max(1,...lines.map(length)))*.92; + return /微字|脚注|图注|标签/.test(slot.role)?Math.min(fitted,2.2):fitted; } export function imageShape(layout:Layout,slot:Slot):CSSProperties { const n=Number(layout.id.slice(4)),s:CSSProperties={backgroundColor:layoutPalette(layout.id)[slot.id==='I01'?2:3],backgroundImage:'linear-gradient(145deg,#ffffff12,transparent 65%)'}; if([4,5,7,13,19,20].includes(n)||(n===17&&slot.id==='I02'))s.borderRadius='50%'; if(n===4)s.transform='rotate(-9deg)'; if(n===15&&slot.id==='I01')s.borderRadius='100% 0 0 0'; + if(n===22)s.backgroundColor=layoutPalette(layout.id)[slot.id==='I02'?3:slot.id==='I03'?4:2]; if(n===22)s.clipPath=slot.id==='I01'?'polygon(0 0,100% 0,100% 48%,0 100%)':slot.id==='I02'?'polygon(0 30%,100% 0,100% 70%,0 100%)':'polygon(0 50%,100% 0,100% 100%,0 100%)'; if(n===27)s.clipPath=slot.id==='I01'?'polygon(0 0,100% 35%,100% 100%,0 68%)':'polygon(0 40%,100% 0,100% 100%,0 100%)'; if(n===10)s.backgroundColor=layoutPalette(layout.id)[2]; diff --git a/apps/studio/src/styles/globals.css b/apps/studio/src/styles/globals.css index 044a073..943c137 100644 --- a/apps/studio/src/styles/globals.css +++ b/apps/studio/src/styles/globals.css @@ -84,3 +84,5 @@ /* Keep the cross-axis line box inside narrow vertical title slots. */ .native-copy.direction-vertical,.native-copy.direction-rotated{line-height:1} + +.ref-19 .native-text:is([data-element=T01],[data-element=T02],[data-element=T03],[data-element=T04]) .native-copy{font-weight:700;letter-spacing:-.06em} diff --git a/scripts/verify-studio-visual.cjs b/scripts/verify-studio-visual.cjs index b0562cb..810681b 100644 --- a/scripts/verify-studio-visual.cjs +++ b/scripts/verify-studio-visual.cjs @@ -16,13 +16,13 @@ const assert=require('node:assert/strict'); page.on('response',async response=>{if(response.request().resourceType()==='image'){const url=new URL(response.url());images.push({path:url.pathname+url.search,status:response.status(),type:response.headers()['content-type'],bytes:(await response.body().catch(()=>Buffer.alloc(0))).length})}}); const metrics=process.argv.includes('--after-layouts')?JSON.parse(await readFile(dir+'/metrics.json','utf8')).metrics:[]; for(let i=1;i<=27&&!process.argv.includes('--after-layouts');i++){ - const id='ref-'+String(i).padStart(2,'0');await page.goto(base+'/studio/layouts/'+id,{waitUntil:'networkidle'});await page.locator('article.native-layout').screenshot({path:dir+'/'+id+'.png'}); + const id='ref-'+String(i).padStart(2,'0');await page.goto(base+'/studio/layouts/'+id,{waitUntil:'networkidle'});await page.locator('article.native-layout').screenshot({path:dir+'/'+id+'.png',style:'.page-tools{visibility:hidden}'}); const m=await page.locator('article.native-layout').evaluate(el=>({id:el.dataset.layoutId,elements:el.querySelectorAll('[data-element]').length,width:el.clientWidth,height:el.clientHeight,overflow:[...el.querySelectorAll('.native-copy')].filter(e=>e.scrollHeight>e.clientHeight+2||e.scrollWidth>e.clientWidth+2).map(e=>e.closest('[data-element]').dataset.element)}));metrics.push(m);await writeFile(dir+'/metrics.json',JSON.stringify({metrics,images,errors},null,2)); } if(!process.argv.includes('--after-layouts')&&!process.argv.includes('--diagnose'))assert.deepEqual(metrics.filter(m=>m.overflow.length),[],'Default copy must fit every native slot'); await page.goto(base+'/studio',{waitUntil:'networkidle'});assert.equal(await page.locator('.library-item').count(),27);await page.screenshot({path:dir+'/catalog-desktop.png',fullPage:true}); const verify=JSON.parse(await readFile('/tmp/studio-library/verification.json','utf8')); - for(const d of verify.results){await page.goto(base+d.url,{waitUntil:'networkidle'});assert.equal(await page.locator('img').count(),1);const current=await page.locator('img').evaluate(img=>({src:img.currentSrc,complete:img.complete,width:img.naturalWidth}));assert.ok(current.complete&¤t.width>0);assert.equal(new URL(current.src).pathname,'/api/preview');await page.locator('article.native-layout').screenshot({path:dir+'/'+d.layoutId+'-saved.png'}); + for(const d of verify.results){await page.goto(base+d.url,{waitUntil:'networkidle'});assert.equal(await page.locator('img').count(),1);const current=await page.locator('img').evaluate(img=>({src:img.currentSrc,complete:img.complete,width:img.naturalWidth}));assert.ok(current.complete&¤t.width>0);assert.equal(new URL(current.src).pathname,'/api/preview');await page.locator('article.native-layout').screenshot({path:dir+'/'+d.layoutId+'-saved.png',style:'.page-tools{visibility:hidden}'}); await page.getByRole('button',{name:'编辑页面',exact:true}).click();assert.equal(await page.locator('.workbench-top').count(),1);await page.screenshot({path:dir+'/'+d.layoutId+'-editor.png'}); } for(const slug of ['learning-skills-template','layout-selection']){await page.goto(base+'/studio/ideas/'+slug,{waitUntil:'networkidle'});await page.screenshot({path:dir+'/tool-'+slug+'.png',fullPage:true})}