Setting the file. One moment.
Generate Layout Blueprint · Wix Headless Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page 28.10
Workflow
scripts/ generate-layout-blueprint.mjs
JavaScript · 140 lines · 9 KB
parseArgs
();
9 const outputDir = resolveOutputDir (args._[ 0 ] || args.url, args.out);
10 const blueprint = await generateLayoutBlueprint ({ outputDir });
11 if (args.json) process.stdout. write ( `${ JSON . stringify ( blueprint , null , 2 ) } \n ` );
12 }
13
14 export async function generateLayoutBlueprint ({ outputDir , pages : suppliedPages , interactionMap : suppliedInteractionMap , sceneContract : suppliedSceneContract } = {}) {
15 const docs = docsDir (outputDir);
16 const [ sourceMap , interactionMap , sceneContract ] = await Promise . all ([
17 suppliedPages ? null : readJson (path. join (docs, "source-map.json" )),
18 suppliedInteractionMap || readJson (path. join (docs, "interaction-map.json" )),
19 suppliedSceneContract || readJson (path. join (docs, "scene-contract.json" )),
20 ]);
21 const pages = suppliedPages || sourceMap?.pages || interactionMap?.pages || [];
22 const pageBlueprints = pages. map (( page ) => ({
23 url: page.url,
24 area: page.area || "page" ,
25 sections: (page.sections || []). map (( section ) => buildSectionBlueprint ({ page, section, sceneContract })),
26 }));
27 const blueprint = {
28 schemaVersion: 1 ,
29 vocabularyVersion: LAYOUT_VOCABULARY_VERSION ,
30 generatedAt: new Date (). toISOString (),
31 sourceUrl: interactionMap?.sourceUrl || pages[ 0 ]?.url || "" ,
32 vocabulary: VOCABULARY ,
33 implementationOrder: [ "canvas" , "background-layers" , "container" , "regions" , "relationships" , "responsive-reflow" , "ui-normalization" ],
34 pages: pageBlueprints,
35 };
36 await writeJson (path. join (docs, "layout-blueprint.json" ), blueprint);
37 await writeText (path. join (docs, "layout-blueprint.md" ), renderLayoutBlueprintMarkdown (blueprint));
38 return blueprint;
39 }
40
41 export function buildSectionBlueprint ({ page , section , sceneContract = {} }) {
42 const pagePrefix = `${ page . area || "page"}-` ;
43 const scene = (sceneContract.scenes || []). find (( candidate ) => candidate.sectionId === section.id && candidate.id. startsWith (pagePrefix));
44 const canvas = classifyCanvas (section, scene);
45 const background = classifyBackground (section, scene);
46 const regions = buildRegions (section). map (( region ) => ({
47 ... region,
48 ... (region.text ? { responsiveTextGeometry: responsiveTextSamples (page, region.text) } : {}),
49 }));
50 const composition = classifyComposition ({ section, scene, background, regions });
51 const relationships = relationshipsFor (composition, regions);
52 const confidence = section.layoutEvidence ? "measured" : scene ? "scene-inferred" : "heuristic" ;
53 return {
54 sectionId: section.id,
55 sourceClassification: { kind: section.kind || "section" , variant: section.variant || "generic" },
56 composition,
57 canvas,
58 background,
59 container: inferContainer (canvas, regions),
60 regions,
61 relationships,
62 responsive: {
63 strategy: composition === "rail" ? "retain-rail-with-touch-overflow" : composition === "split" ? "stack-at-narrow-width" : "preserve-composition" ,
64 observedViewports: section.layoutEvidence?.viewport ? [section.layoutEvidence.viewport] : [],
65 inferred: true ,
66 },
67 identityLocks: {
68 composition,
69 backgroundKind: background.kind,
70 mediaRole: background.layers. some (( layer ) => layer.role === "media" ) ? "background" : section.capabilities?.hasMedia ? "content" : "none" ,
71 relationships,
72 textGeometry: regions. filter (( region ) => region.textGeometry). map (( region ) => ({ role: region.role, text: region.text, ... region.textGeometry, responsive: region.responsiveTextGeometry || [] })),
73 forbiddenConversions: forbiddenConversions (composition),
74 },
75 implementation: implementationRecipe (composition, canvas, background, regions),
76 evidence: {
77 confidence,
78 sourceDomRef: section.domRef || null ,
79 sceneId: scene?.id || null ,
80 sourceScreenshot: scene?.sourceScreenshot || null ,
81 warnings: section.layoutEvidence ? [] : [ "layout-evidence-unavailable; rerun extraction for measured region geometry" ],
82 },
83 };
84 }
85
86 function responsiveTextSamples ( page , text ) {
87 const target = String (text || "" ). replace ( / \s + / g , " " ). trim ();
88 return Object. entries (page.responsiveTextGeometry || {}). map (([ viewport , samples ]) => {
89 const match = (samples || []). find (( sample ) => String (sample.text || "" ). replace ( / \s + / g , " " ). trim () === target);
90 return match ? { viewport, ... match } : null ;
91 }). filter (Boolean);
92 }
93
94 function inferContainer ( canvas , regions ) {
95 const content = regions. filter (( region ) => region.role !== "media" && region.normalizedRect);
96 if ( ! content. length ) return { widthMode: canvas.widthMode === "bounded" ? "bounded" : "full" , maxWidthPx: null , inlineInsetRatio: null };
97 const left = Math. min ( ... content. map (( region ) => region.normalizedRect.x));
98 const right = Math. max ( ... content. map (( region ) => region.normalizedRect.x + region.normalizedRect.width));
99 return {
100 widthMode: left > 0.04 || right < 0.96 ? "bounded" : "full" ,
101 maxWidthPx: null ,
102 inlineInsetRatio: { start: Number (left. toFixed ( 3 )), end: Number (( 1 - right). toFixed ( 3 )) },
103 };
104 }
105
106 function forbiddenConversions ( composition ) {
107 const map = {
108 "layered-overlay" : [ "split" , "stack" ],
109 "scroll-narrative" : [ "split" , "static-stack" ],
110 rail: [ "static-grid" ],
111 split: [ "layered-overlay" ],
112 };
113 return map[composition] || [];
114 }
115
116 function implementationRecipe ( composition , canvas , background , regions ) {
117 const steps = [
118 `Create one section root with ${ canvas . widthMode } width and ${ canvas . heightMode } height behavior.` ,
119 `Build background layers in declared order: ${ background . layers . map (( layer ) => `${ layer . role }:${ layer . kind }` ). join ( " -> " ) || "none"}.` ,
120 ];
121 if ([ "layered-overlay" , "scroll-narrative" ]. includes (composition)) steps. push ( "Use one positioning context; keep background layers full-bleed and place foreground regions above them. Do not create media and copy as peer columns." );
122 if (composition === "split" ) steps. push ( "Use peer media and content regions with their captured left/right relationship; stack only at the declared narrow reflow." );
123 if (composition === "rail" ) steps. push ( "Use a clipped viewport and max-content horizontal track; preserve shared item geometry and active-state behavior." );
124 if (regions. length ) steps. push ( "Place semantic regions from normalized rectangles and placement anchors before fine typography or motion polish." );
125 if (regions. some (( region ) => region.textGeometry)) steps. push ( "Apply each important text region's measured inline size, max-width, line count, and wrap policy. A captured single-line desktop heading must stay one line at the canonical desktop viewport." );
126 if (background.layers. some (( layer ) => layer.policy === "fallback-only" )) steps. push ( "Render fallback-only posters only until primary media is ready; never leave them visibly stacked behind playing media." );
127 return { steps };
128 }
129
130 function renderLayoutBlueprintMarkdown ( contract ) {
131 const pages = contract.pages. map (( page ) => `# ${ page . url } \n\n ${ page . sections . map (( section ) => `## ${ section . sectionId }: ${ section . composition } \n\n - Canvas: \` ${ section . canvas . widthMode }/${ section . canvas . heightMode } \` ${ section . canvas . pinned ? ", pinned" : ""} \n - Background: \` ${ section . background . kind } \` (${ section . background . layers . map (( layer ) => `${ layer . role }:${ layer . kind }` ). join ( " -> " ) || "none"}) \n - Container: \` ${ section . container . widthMode } \`\n - Identity locks: ${ section . identityLocks . relationships . join ( ", " ) || "composition and media role"} \n - Forbidden conversions: ${ section . identityLocks . forbiddenConversions . map (( item ) => ` \` ${ item } \` ` ). join ( ", " ) || "none"} \n\n Regions: \n ${ section . regions . map (( region ) => `- \` ${ region . role } \` at \` ${ region . placement } \` : ${ JSON . stringify ( region . normalizedRect ) }${ region . textGeometry ? `; text geometry ${ JSON . stringify ( region . textGeometry ) }` : ""}${ region . text ? ` — ${ region . text }` : ""}` ). join ( " \n " ) || "- no measured regions"} \n\n Build order: \n ${ section . implementation . steps . map (( step , index ) => `${ index + 1 }. ${ step }` ). join ( " \n " ) } \n\n Evidence: \` ${ section . evidence . confidence } \` ${ section . evidence . sourceScreenshot ? `; screenshot \` ${ section . evidence . sourceScreenshot } \` ` : ""}` ). join ( " \n\n " ) }` ). join ( " \n\n " );
132 return `# Section layout blueprints \n\n Controlled vocabulary version: ${ contract . vocabularyVersion }. Implement each section independently. Composition and layer order are identity locks; UI normalization comes last. \n\n ${ pages } \n ` ;
133 }
134
135 if ( import . meta .url === `file://${ process . argv [ 1 ] }` ) {
136 main (). catch (( error ) => {
137 console. error (error.stack || error.message);
138 process. exit ( 1 );
139 });
140 }