183 lines
11 KiB
Markdown
183 lines
11 KiB
Markdown
# 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:
|
||
|
||
1. **Flat** — only browser-intrinsic tags (`section`, `div`, `h1`, `p`, `a`, `img`, `video`, `button`, `form`, `input`, `nav`, etc.), `motion.<tag>` from `motion/react`, lucide icons, and the five whitelisted open-source primitives.
|
||
2. **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.
|
||
3. **Identifier-clean** — every name referenced inside the JSX is either a destructured prop, a `const` declared 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
|
||
|
||
```ts
|
||
// 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 inline `useState` BUT only inside one of the five open-source primitives, never at section level.
|
||
- `gsap`, `framer-motion` (use `motion/react`), `react-router-dom`, anything else that isn't already in `webild-vite` `package.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:
|
||
|
||
```tsx
|
||
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>` carries `aria-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`, `useMemo` at 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` (or `max-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-8` only. Do **not** invent `gap-12` or `p-10`.
|
||
|
||
### Typography
|
||
- `text-base` / `text-lg` body
|
||
- `text-2xl`–`text-4xl` for sub-headings
|
||
- `text-6xl`–`text-8xl` for hero / section headings
|
||
- `font-medium` for headings, `font-semibold` only when the design clearly calls for it
|
||
- `text-balance` on H1, `leading-tight` on long paragraphs
|
||
- Color: `text-foreground` (default), `text-muted-foreground` (secondary), `text-primary` (accent), `text-primary-cta-text` / `text-secondary-cta-text` on filled buttons
|
||
|
||
### Color & surface
|
||
- Page bg: omit — `<body>` paints `--background`
|
||
- Card bg: `card` utility class (already defined globally — gradient + border + shadow), with `rounded-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:
|
||
```tsx
|
||
<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:
|
||
|
||
1. Reads `HeroSplit.tsx`.
|
||
2. Substitutes destructured prop usages with the literals from the JSX call.
|
||
3. Pastes the resulting JSX into `HomePage.tsx`.
|
||
4. 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
|
||
|
||
1. Open `http://localhost:3000/components/sections/<category>/<name>` in the browser the user has running.
|
||
2. 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.
|
||
3. Read the existing `.tsx` to capture the exact props interface — do not rename or drop props.
|
||
4. Replace the body with flat inline JSX following the rules above.
|
||
5. Verify with `pnpm typecheck` from `webild-components-version-4/`.
|
||
6. Reload the showcase page and compare to the baseline screenshot.
|