Setting the file. One moment. Classifiers · Wix Headless Replatform · wix/skills · Skills Docs28.10
Workflow
scripts/lib/layout-blueprint/classifiers.mjs
JavaScript·141 lines·8 KB
??
Number
(rect.width)
>=
Number
(viewport.width)
*
0.88
;
7 let heightMode = "content";
8 if (scene?.implementation?.primitive === "scroll-scene") heightMode = "scroll-driven";
9 else if (Number(rect.height) >= Number(viewport.height) * 0.82 && Number(rect.height) <= Number(viewport.height) * 1.35) heightMode = "viewport";
10 else if (section.layoutEvidence?.canvas?.minHeight?.includes?.("vh")) heightMode = "viewport";
11 return {
12 widthMode: fullWidth ? "full-bleed" : "bounded",
13 heightMode,
14 sourceRect: rect,
15 viewport,
16 positioningContext: section.layoutEvidence?.canvas?.position || "static",
17 overflow: {
18 x: section.layoutEvidence?.canvas?.overflowX || "visible",
19 y: section.layoutEvidence?.canvas?.overflowY || "visible",
20 },
21 pinned: scene?.implementation?.primitive === "scroll-scene",
22 };
23}
24
25export function classifyBackground(section, scene) {
26 const color = section.background?.color || section.layoutEvidence?.canvas?.background?.color || "";
27 const cssImage = section.background?.image || section.layoutEvidence?.canvas?.background?.image || "";
28 const media = (scene?.media || []).find((item) => item.role === "background") || null;
29 const measuredMedia = (section.layoutEvidence?.layers || []).find((layer) =>
30 ["video", "image"].includes(layer.kind)
31 && layer.position === "absolute"
32 && Number(layer.normalizedRect?.width) >= 0.75
33 && Number(layer.normalizedRect?.height) >= 0.25
34 ) || null;
35 const isGradient = /gradient\(/i.test(cssImage);
36 const hasColor = Boolean(color && !/transparent|rgba\(0,\s*0,\s*0,\s*0\)/i.test(color));
37 const hasImage = Boolean((cssImage && cssImage !== "none") || (!media && measuredMedia?.kind === "image"));
38 const hasVideo = Boolean((media && ["video", "iframe"].includes(media.tag)) || (!media && measuredMedia?.kind === "video"));
39 const signalCount = [hasColor, hasImage || Boolean(media?.fallback?.src), hasVideo].filter(Boolean).length;
40 const kind = signalCount > 1 ? "composite" : hasVideo ? "video" : isGradient ? "gradient" : hasImage ? "image" : hasColor ? "color" : "none";
41 const layers = [];
42 if (hasColor) layers.push({ role: "base", kind: "color", color });
43 if (hasVideo) layers.push({
44 role: "media",
45 kind: "video",
46 src: media?.src || measuredMedia?.src || "",
47 provider: media?.provider || media?.tag || "native",
48 fit: measuredMedia?.objectFit || "cover",
49 position: "center",
50 playback: media?.playback || {},
51 });
52 if (!hasVideo && hasImage) layers.push({ role: "media", kind: isGradient ? "gradient" : "image", src: cssImage || measuredMedia?.src || "", fit: measuredMedia?.objectFit || "cover", position: "center" });
53 if (media?.fallback?.src) layers.push({
54 role: "media",
55 kind: "image",
56 src: media.fallback.src,
57 fit: "cover",
58 position: "center",
59 policy: media.fallback.policy || "fallback-only",
60 });
61 const scrollStates = (scene?.states || []).find((state) => state.kind === "scroll-state")?.states || [];
62 const observedSceneLayers = scrollStates.flatMap((state) => state.sceneLayers || []);
63 for (const layer of section.layoutEvidence?.layers || []) {
64 const layerClass = (layer.classTokens || []).join(" ");
65 const canvasLayer = layer.kind === "pseudo-layer"
66 || (Number(layer.normalizedRect?.width) >= 0.75 && Number(layer.normalizedRect?.height) >= 0.25);
67 if (canvasLayer && (layer.kind === "pseudo-layer" || /overlay|curtain|mask/i.test(layerClass))) {
68 if (/curtain/i.test(layerClass) && observedSceneLayers.some((candidate) => candidate.role === "curtain")) continue;
69 const scrim = layer.kind === "pseudo-layer" || /overlay|mask/i.test(layerClass);
70 layers.push({ role: scrim ? "scrim" : "decoration", kind: layer.kind, ...layer });
71 }
72 }
73 const dynamicLayerKeys = new Set();
74 for (const layer of observedSceneLayers) {
75 if (!['curtain', 'background'].includes(layer.role)) continue;
76 const key = `${layer.role}:${layer.className || layer.tag}`;
77 if (dynamicLayerKeys.has(key)) continue;
78 dynamicLayerKeys.add(key);
79 const peers = observedSceneLayers.filter((candidate) => `${candidate.role}:${candidate.className || candidate.tag}` === key);
80 layers.push({
81 role: layer.role === "curtain" ? "decoration" : "scrim",
82 kind: layer.role === "curtain" ? "curtain" : "scene-layer",
83 position: layer.position,
84 color: layer.backgroundColor,
85 motion: "scroll-driven",
86 observedWidthPx: {
87 minimum: Math.min(...peers.map((candidate) => Number(candidate.width) || 0)),
88 maximum: Math.max(...peers.map((candidate) => Number(candidate.width) || 0)),
89 },
90 className: layer.className || "",
91 });
92 }
93 const resolvedKind = new Set(layers.map((layer) => `${layer.role}:${layer.kind}`)).size > 1 ? "composite" : kind;
94 return { kind: resolvedKind, color: hasColor ? color : null, layers };
95}
96
97export function buildRegions(section) {
98 return (section.layoutEvidence?.regions || []).map((region, index) => ({
99 id: `${section.id}-region-${String(index + 1).padStart(2, "0")}`,
100 role: region.role || "content",
101 text: region.text || "",
102 placement: placementFor(region.normalizedRect),
103 normalizedRect: region.normalizedRect,
104 sourceRect: region.rect,
105 positioning: region.position || "static",
106 alignment: region.textAlign || "start",
107 zIndex: region.zIndex || "auto",
108 textGeometry: region.textGeometry || null,
109 domRef: region.domRef || null,
110 }));
111}
112
113export function classifyComposition({ section, scene, background, regions }) {
114 const primitive = scene?.implementation?.primitive;
115 if (primitive === "active-card-rail" || section.capabilities?.isCarouselLike) return "rail";
116 if (primitive === "scroll-scene") return "scroll-narrative";
117 if (section.kind === "card-collection") return "grid";
118 const mediaIsBackground = background.layers.some((layer) => layer.role === "media" && ["video", "image", "gradient"].includes(layer.kind));
119 const contentRegions = regions.filter((region) => !["media", "navigation"].includes(region.role));
120 const mediaRegions = regions.filter((region) => region.role === "media");
121 if (mediaIsBackground && (contentRegions.length || section.heading || section.text)) return "layered-overlay";
122 if (mediaRegions.length && contentRegions.length) {
123 const pairs = mediaRegions.flatMap((media) => contentRegions.map((content) => horizontalRelation(media.normalizedRect, content.normalizedRect)));
124 if (pairs.some((relation) => relation === "before" || relation === "after")) return "split";
125 if (mediaRegions.some((media) => contentRegions.some((content) => overlapRatio(media.normalizedRect, content.normalizedRect) >= 0.2))) return "layered-overlay";
126 }
127 return regions.length ? "stack" : "freeform";
128}
129
130export function relationshipsFor(composition, regions) {
131 const relationships = [];
132 const headings = regions.filter((region) => region.role === "heading");
133 const body = regions.filter((region) => region.role === "body-copy");
134 const actions = regions.filter((region) => region.role === "action");
135 const tabs = regions.filter((region) => region.role === "tabs");
136 if (headings.length && body.length) relationships.push("heading-before-body");
137 if (actions.length && body.length) relationships.push(horizontalRelation(body[0].normalizedRect, actions[0].normalizedRect) === "before" ? "action-right-of-copy" : "action-near-copy");
138 if (tabs.some((region) => region.placement.startsWith("bottom"))) relationships.push("tabs-bottom-anchored");
139 if (["layered-overlay", "scroll-narrative"].includes(composition)) relationships.push("content-over-background-media");
140 return [...new Set(relationships)];
141}