Files
3df875bd-3de5-46d4-bbca-c04…/.claude/rules/inline-sections.md
2026-05-05 11:48:58 +03:00

11 KiB
Raw Blame History

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-1p-8, gap-1gap-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

// 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:

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.tsxexport 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-1gap-8, p-1p-8, m-1m-8 only. Do not invent gap-12 or p-10.

Typography

  • text-base / text-lg body
  • text-2xltext-4xl for sub-headings
  • text-6xltext-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-4p-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:

  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.