Files
2026-05-05 11:59:18 +03:00

183 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.