11 KiB
Inline-Friendly Section Conventions (Option A)
Active rewrite: every section in src/components/sections/** becomes a flat-bodied React component whose JSX can be lifted verbatim into a generated HomePage.tsx by the snippet AST inliner. Only the file body changes — the props interface stays exactly as it is so the snippet pipeline keeps working.
These rules SUPERSEDE components.md / transformation.md / animations.md for files in src/components/sections/**. The legacy docs still describe how the v4 showcase and shared helpers are wired, but a section file written today must not import any of those helpers.
What "inline-friendly" means
After the AST inliner expands <HeroSplit ... /> into HomePage, the resulting JSX must be:
- Flat — only browser-intrinsic tags (
section,div,h1,p,a,img,video,button,form,input,nav, etc.),motion.<tag>frommotion/react, lucide icons, and the five whitelisted open-source primitives. - Self-explanatory to an LLM — class names from the v4 fluid scale (
text-7xl,w-content-width,p-1–p-8,gap-1–gap-8) and shadcn aliases (bg-primary,text-muted-foreground,border-border) only. - Identifier-clean — every name referenced inside the JSX is either a destructured prop, a
constdeclared at the top of the function, or imported from one of the allowed sources below.
If any one of those three is broken, free-edit will misfire. No exceptions.
Allowed imports — exhaustive list
// Animation
import { motion } from "motion/react";
// Icons
import { Star, ArrowRight /* ... any lucide icon */ } from "lucide-react";
// Open-source primitives (curated five)
import TextAnimation from "@/components/ui/text-animation";
import HoverPattern from "@/components/ui/hover-pattern";
import ShimmerText from "@/components/ui/shimmer-text";
import Marquee from "@/components/ui/marquee";
import GlowCard from "@/components/ui/glow-card";
Anything else is forbidden, including (non-exhaustive):
@/components/ui/Button,@/components/ui/TextAnimation(capital T — the closed v4 one),@/components/ui/ScrollReveal,@/components/ui/ImageOrVideo,@/components/ui/HeroBackgroundSlot,@/components/ui/GridOrCarousel, any@/components/ui/*Background*, any@/components/ui/Navbar*, any@/components/ui/Button*,Card,Carousel,Modal,Sheet,Accordion,Tooltip,Dropdown,DropdownMenu,Tag,Tabs,Spinner,Switch,Checkbox,Input,Label,Textarea,MediaStack,OrbitingIcons,LoopCarousel,TiltedCarousel,TiltedStackCards,AnimatedBarChart,AvatarGroup,BorderGlow,AutoFillText,IconTextMarquee,ChatMarquee,InfoCardMarquee,ChecklistTimeline,Calendar,Separator,TextLink,Transition,StyleProvider,useStyle.@/components/shared/*— every shared helper.@/components/cardStack/*.@/components/text/*.@/hooks/*— sections must be stateless; if interactivity is needed, ship a tiny inlineuseStateBUT only inside one of the five open-source primitives, never at section level.gsap,framer-motion(usemotion/react),react-router-dom, anything else that isn't already inwebild-vitepackage.json.
Replacement table
| Closed v4 primitive | Inline replacement |
|---|---|
<Button text="Get started" href="/x" variant="primary" /> |
<a href="/x" className="primary-button rounded-theme px-6 h-9 inline-flex items-center justify-center text-primary-cta-text text-sm">Get started</a> |
<Button ... variant="secondary" /> |
same shape with secondary-button and text-secondary-cta-text |
<TextAnimation text={title} tag="h1" variant="fade" gradientText className="text-7xl" /> (closed v4) |
<motion.h1 initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-15%" }} transition={{ duration: 0.6, ease: "easeOut" }} className="text-7xl font-medium text-balance">{title}</motion.h1> |
<ScrollReveal variant="slide-up" delay={0.2}>...</ScrollReveal> |
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-10%" }} transition={{ duration: 0.6, delay: 0.2, ease: "easeOut" }}>...</motion.div> |
<ImageOrVideo imageSrc={src} /> |
<img src={imageSrc} alt={imageAlt ?? ""} className="w-full h-full object-cover" /> (drop the conditional video branch unless the section's prop type actually has videoSrc; if it does, render a ternary inline) |
<HeroBackgroundSlot /> |
omit (background is delivered by the wrapper <Layout> or by inline gradients on the section itself) |
<GridOrCarousel items={...}> |
inline <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> and .map(item => ...) directly. No carousel — generated sites don't need touch carousels. If the original section had a true mobile carousel, replace with a horizontal scroll snap container. |
<AvatarGroup avatars={...} /> |
inline <div className="flex -space-x-2">{avatars.map(...)}</div> |
<Tag text="..." /> |
<span className="card rounded-full px-3 py-1 text-sm">{text}</span> |
<MediaContent imageSrc={...} /> |
inline <img> (and <video> when applicable) |
For the animated text effect (used to be TextAnimation with variant="fade" / gradientText), use the new open-source @/components/ui/text-animation primitive: it supports per-word stagger but receives the text string as a child prop, so an LLM can edit the text by changing one literal.
Required structure
Every section file must conform to this skeleton:
import { motion } from "motion/react";
// ...allowed icons + primitives only
type FooSectionProps = {
// props identical to the existing v4 prop type — DO NOT change names or shapes.
// The snippet AST inliner reads these to substitute literals.
};
const FooSection = ({ title, description, items, ... }: FooSectionProps) => {
return (
<section
data-webild-section="FooSection"
aria-label="Foo section"
className="relative w-full py-hero-page-padding"
>
<div className="w-content-width mx-auto">
{/* flat JSX only */}
</div>
</section>
);
};
export default FooSection;
Hard requirements:
data-webild-section="<ComponentName>"on the outermost<section>— used by the selection bridge (Alt+click → postMessage).- Outermost
<section>carriesaria-label. Nested<section>blocks are forbidden inside a single section file. - Default export is the component, name matches file name (
HeroSplit.tsx→export default HeroSplit). - Props interface name =
<Component>Props. - No
useState,useEffect,useRef,useReducer,useCallback,useMemoat section level — sections are stateless. If the original section used a hook (useEmblaCarousel,useScrollProgress,useTransform), drop the dynamic behavior and replace with a static layout. - No
"use client"directive — Vite + React is single-mode.
Class vocabulary — what to use
The v4 fluid scale is the source of truth and is mapped through tailwind @theme inline. Both showcase and sandbox define the exact same variables, so any class below renders identically in both environments.
Layout
- Container:
w-content-width mx-auto(ormax-w-content mx-auto— same thing via alias) - Page padding:
py-hero-page-padding(top of hero),py-16 md:py-24(other sections — these resolve to fluid via padding scale) - Section wrapper:
relative w-full - Spacing:
gap-1–gap-8,p-1–p-8,m-1–m-8only. Do not inventgap-12orp-10.
Typography
text-base/text-lgbodytext-2xl–text-4xlfor sub-headingstext-6xl–text-8xlfor hero / section headingsfont-mediumfor headings,font-semiboldonly when the design clearly calls for ittext-balanceon H1,leading-tighton long paragraphs- Color:
text-foreground(default),text-muted-foreground(secondary),text-primary(accent),text-primary-cta-text/text-secondary-cta-texton filled buttons
Color & surface
- Page bg: omit —
<body>paints--background - Card bg:
cardutility class (already defined globally — gradient + border + shadow), withrounded-theme p-4–p-6 - Inverted bg sections:
bg-foreground text-background - Borders:
border border-border - Accent surfaces:
bg-primary/bg-secondary/bg-muted
Buttons
- Primary:
<a className="primary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-primary-cta-text">Label</a> - Secondary:
<a className="secondary-button rounded-theme h-9 px-6 inline-flex items-center justify-center text-sm text-secondary-cta-text">Label</a> - Buttons are always
<a>(with optional<button>for forms). Never<Button>.
Radius
rounded-theme(theme-aware, the new default)rounded-theme-capped(capped at xl)rounded-full(pills, avatars)- Don't use
rounded-2xl,rounded-3xl— they aren't in the fluid scale.
Animation
- One canonical fade-in motif:
<motion.div initial={{ opacity: 0, y: 20 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true, margin: "-15%" }} transition={{ duration: 0.6, ease: "easeOut" }} > - Stagger via incremental
delay: heading 0, sub 0.1, button row 0.2, media 0.3. - Hover: rely on Tailwind
hover:utilities, no JS.
Snippet pipeline contract
When the snippet AST inliner ingests <HeroSplit tag="X" title="Y" .../>, it:
- Reads
HeroSplit.tsx. - Substitutes destructured prop usages with the literals from the JSX call.
- Pastes the resulting JSX into
HomePage.tsx. - Hoists imports (deduped).
For step 2 to work, every prop must appear in the JSX exactly as {propName} or {propName.field} — no spreading ({...props}), no renaming (const t = title), no helper functions that consume props before render. Compute layout-only locals (e.g. const isPrimary = i === 0) is fine; transforming a prop value before render is not.
For step 4 to work, every import must be a top-level static import { ... } from "..." declaration. No dynamic import(), no conditional re-exports.
Workflow when rewriting a section
- Open
http://localhost:3000/components/sections/<category>/<name>in the browser the user has running. - Take a fullPage screenshot — that's the visual ground truth. The rewrite must reproduce the same layout, hierarchy, and tone. Faster = better, but never at the cost of visual parity.
- Read the existing
.tsxto capture the exact props interface — do not rename or drop props. - Replace the body with flat inline JSX following the rules above.
- Verify with
pnpm typecheckfromwebild-components-version-4/. - Reload the showcase page and compare to the baseline screenshot.