Files
probably-rendering-one-more…/apps/wiki/public/layout-reference/ReferenceLayout.tsx
T
codex-bot a8515e575a
wiki CI and deployment / verify-and-deploy (push) Successful in 16m40s
Replace approximate recipes with 27 source-matched component layouts
2026-10-26 16:02:03 +08:00

40 lines
3.8 KiB
TypeScript

import React from 'react';
import type {CSSProperties} from 'react';
import layouts from './manifest.json';
export const layoutIds = ['ref-01','ref-02','ref-03','ref-04','ref-05','ref-06','ref-07','ref-08','ref-09','ref-10','ref-11','ref-12','ref-13','ref-14','ref-15','ref-16','ref-17','ref-18','ref-19','ref-20','ref-21','ref-22','ref-23','ref-24','ref-25','ref-26','ref-27'] as const;
export type LayoutId=typeof layoutIds[number];
export type Replacements={text?:Record<string,string>;images?:Record<string,string>};
export function getLayout(id:string){return layouts.find(layout=>layout.id===id)}
export function validateReplacements(id:string,replacements:Replacements){
const layout=getLayout(id);if(!layout)throw Error(`Unknown layout ID: ${id}`);
for(const[kind,values]of [['text',replacements.text],['image',replacements.images]] as const)for(const key of Object.keys(values||{})){
if(!layout.elements.some(e=>key===id+'/'+e.id&&e.kind===kind))throw Error(`Component does not belong to ${id} or has the wrong kind: ${key}`);
}return layout;
}
function textStyle(text:string,box:number[],width:number,direction:string):CSSProperties{
const vertical=direction==='vertical'||direction==='rotated',lines=text.split('\n'),units=Math.max(...lines.map(line=>Array.from(line).reduce((n,c)=>n+(/[\u3400-\u9fff]/.test(c)?1:.56),0)),1);
const size=vertical?Math.min(box[2],box[3]/Math.max(text.length,1)):Math.min(box[3]/(lines.length*1.1),box[2]/units);
return {fontSize:`${size/width*100}cqw`,lineHeight:1.1,whiteSpace:'pre-wrap',writingMode:vertical?'vertical-rl':undefined,textOrientation:direction==='rotated'?'sideways':undefined,transform:direction==='angled'?'rotate(-12deg)':undefined,transformOrigin:'left center',color:'#222',fontFamily:'"Songti SC", "Noto Serif CJK SC", serif'};
}
/** Untouched mode reconstructs typed source layers; replacement mode keeps the exact preset geometry. */
export default function ReferenceLayout({id,text={},images={},assetBase=''}:{id:LayoutId;text?:Record<string,string>;images?:Record<string,string>;assetBase?:string}){
const layout=validateReplacements(id,{text,images}),src=(url:string)=>assetBase+url;
return <figure data-layout-id={id} aria-label={`${id} ${layout.name}`} style={{margin:0,width:'min(100%, 672px)',containerType:'inline-size'}}>
<div style={{position:'relative',width:'100%',aspectRatio:`${layout.width}/${layout.height}`,isolation:'isolate',background:'#f4f3ee'}}>
<img src={src(layout.background)} alt="" width={layout.width} height={layout.height} style={{position:'absolute',inset:0,width:'100%',height:'100%'}}/>
{layout.elements.map(e=>{
const[x,y,w,h]=e.box,key=id+'/'+e.id,position:CSSProperties={position:'absolute',left:x/layout.width*100+'%',top:y/layout.height*100+'%',width:w/layout.width*100+'%',height:h/layout.height*100+'%',zIndex:e.kind==='text'?3:e.kind==='decoration'?2:1};
if(e.kind==='text'&&Object.hasOwn(text,key)){
if(e.direction==='curved')throw Error(`Curved text requires the source-specific SVG path: ${key}`);
return <React.Fragment key={key}>
{'underlay' in e&&<img src={src(String(e.underlay))} alt="" style={position}/>}
<span data-component={key} data-kind="text" style={{...position,...textStyle(text[key],e.box,layout.width,e.direction)}}>{text[key]}</span>
</React.Fragment>;
}
if(e.kind==='image'&&Object.hasOwn(images,key))return <img key={key} data-component={key} data-kind="image" src={images[key]} alt={e.role} style={{...position,objectFit:'cover',maskImage:'mask' in e?`url("${src(String(e.mask))}")`:undefined,maskSize:'100% 100%'}}/>;
return <img key={key} data-component={key} data-kind={e.kind} src={src(e.asset)} alt={e.kind==='text'?e.text||`${e.role}:原字形保真,内容未辨识`:''} width={w} height={h} style={position}/>;
})}
</div>
</figure>;
}