| name | web-frontend |
| description | Guidelines for building minimal, professional, and concise frontend components in the /web project using TypeScript, React Router v7, Tailwind CSS, and the CVA pattern. |
Web Frontend Development Skill
This skill provides guidance for developing frontend components in the /web project for CRAN/E, a PWA that searches CRAN packages and authors.
Tech Stack
- TypeScript for type safety
- React Router v7 (Vite build) with Remix-style file-based routes
- Tailwind CSS with Radix Colors for styling
- CVA (Class Variance Authority) for component variants
- Remix Icons (
@remixicon/react) for icons - Supabase for data fetching via typed client
Design Principles
Minimal & Professional
The design follows a clean, minimal aesthetic with:
- Gradient accents (iris, ruby, jade, bronze, sand, amethyst, opal)
- Subtle hover effects with slow transitions (duration-700)
- Rounded corners (rounded-full for pills, rounded-xl for cards)
- Consistent spacing using Tailwind's gap utilities
- Dark mode support via Radix Colors
Component Structure
All UI components follow these patterns:
- Props Definition: Use
PropsWithChildrenwhen accepting children, combine withVariantProps<typeof twBase>from CVA - CVA Styling: Define variants using
cva()for reusable, type-safe class composition - Display Name: Always set
Component.displayNamefor debugging - Type Safety: Leverage TypeScript for props, Zod schemas for validation
Core Component Patterns
Layout Components
Header (app/modules/header.tsx):
- Gradient backgrounds with variants
- Headline + optional subline structure
- Optional ornament slot for additional content
PageContent (app/modules/page-content.tsx):
- Full-width wrapper with consistent padding
full-widthclass for spanning viewport width- Inner flex column with gap-16 for section spacing
PageContentSection (app/modules/page-content-section.tsx):
- Headline, subline, and fragment ID for anchor navigation
- Prose variant for long-form text styling
- Gap-16 for consistent vertical rhythm
Interactive Components
InfoPill (app/modules/info-pill.tsx):
- Rounded-full border with gradient hover effects
- Optional label slot (left-aligned, gray-dim)
- Size variants: xs, sm, md, lg
- Gradient variants matching design system
- Hover opacity transition on gradient overlay
InfoCard (app/modules/info-card.tsx):
- Rounded-xl border with gradient hover effects
- Min height for consistent card sizing
- Icon indicators (external/internal) with wiggle animation on hover
- Group hover states for coordinated animations
Tag (app/modules/tag.tsx):
- Small, uppercase, rounded-full labels
- Border gradients (iris, jade) for semantic meaning
- Size variants: xs, sm
Utility Components
Separator (app/modules/separator.tsx):
- Simple
<hr>with opacity-20 for subtle dividers
Prose (app/modules/prose.tsx):
- Typography component for long-form content
- Supports dangerouslySetInnerHTML for rendered HTML
- Text-xl, font-light, leading-normal baseline
Anchors & AnchorLink (app/modules/anchors.tsx):
- Navigation for in-page sections
- Fragment-based routing
Color System
Use Radix Colors with semantic variants:
- iris: Primary/action color
- ruby: Error/destructive actions
- jade: Success/positive states
- bronze: Neutral/informational
- sand/gold: Warm highlights
- amethyst: Secondary accent (plum + iris mix)
- opal: Cool accent (iris + sky mix)
Text colors:
text-gray-normal: Body texttext-gray-dim: Secondary/muted texttext-gray-12/text-gray-4: Prose (dark/light mode)
Page Structure Pattern
Reference pages follow this structure:
- Header with gradient and title
- Anchors navigation (if multiple sections)
- PageContent wrapper
- PageContentSection for each major section
- Separator between sections
Example from _page.privacy._index.tsx:
<Header gradient="sand" headline="Privacy" subline="..." />
<Anchors anchorIds={...}>...</Anchors>
<PageContent>
<PageContentSection headline="General" fragment="general">
<p>...</p>
</PageContentSection>
<Separator />
<PageContentSection headline="..." fragment="...">
...
</PageContentSection>
</PageContent>
Route Patterns
_page.*files: Public pages with layout wrapper_page.package.$packageName.tsx: Package detail pages_page.author.$authorName.tsx: Author profile pages_page.privacy._index.tsx: Static content pagesapi.*: API endpoints (search, MCP)*.og.*: Open Graph image generation
Data Loading
Use React Router's loader pattern:
export const loader: LoaderFunction = async ({ params }) => {
const data = await Service.fetchData(params.id);
if (!data) throw data(null, { status: 404 });
return data(data);
};
Access via useLoaderData<LoaderData>().
Meta Tags
Use mergeMeta() helper for combining meta tags:
export const meta = mergeMeta(({ data }) => {
const title = `${data.name} | CRAN/E`;
return [
{ title },
{ name: "description", content: data.description },
{ property: "og:title", content: title },
{ property: "og:image", content: `${url}/og` },
];
});
Styling Best Practices
- Use CVA for variants: Don't hardcode conditional classes
- Prefer Tailwind utilities: Avoid custom CSS when possible
- Dark mode: Use Radix Colors with
dark:variants - Transitions: Use duration-700 for smooth, professional animations
- Responsive: Use
md:breakpoint for desktop adjustments - Spacing: Use gap utilities for flex/grid spacing instead of margins
Accessibility
- Use semantic HTML elements
- Provide meaningful alt text for images
- Ensure keyboard navigation works
- Test with screen readers when adding interactive elements
Icon Usage
Import from @remixicon/react:
import { RiArrowRightSLine, RiExternalLinkLine } from "@remixicon/react";
Size prop for consistency (typically 16 or 20).
Component File Structure
import { cva, VariantProps } from "cva";
import { PropsWithChildren, ReactNode } from "react";
type Props = PropsWithChildren<
VariantProps<typeof twBase> & {
// Custom props
}
>;
const twBase = cva({
base: "...",
variants: {
size: { ... },
},
defaultVariants: { ... },
});
export function Component(props: Props) {
const { children, ...rest } = props;
return <div className={twBase(rest)}>{children}</div>;
}
Component.displayName = "Component";
Testing Strategy
- Lint:
npm run lintfrom/web - Typecheck:
npm run typecheckfrom/web - Build:
npm run buildfrom/web - No unit tests; validation relies on lint/typecheck/build
Common Patterns to Follow
Example: Package Detail Page (_page.package.$packageName.tsx)
- Header with package name and gradient
- Anchors for section navigation (Synopsis, Documentation, Team, Insights, Binaries, Dependencies)
- Multiple PageContentSection components
- InfoPill for metadata (version, license, etc.)
- InfoCard for related packages/authors
- ExternalLink components for CRAN URLs
- Lazy-loaded components for performance (e.g., PackageDependencySearch)
Example: Author Page (_page.author.$authorName.tsx)
- Header with author name
- List of packages by author using InfoCard grid
- Team section showing collaborators
- Similar structure to package page
File Organization
- Routes:
/web/app/routes/*.tsx - Components:
/web/app/modules/*.tsx - Data services:
/web/app/data/*.ts - Types:
/web/app/data/*.shape.ts(Zod schemas),*.types.generated.ts(Supabase)
Environment
- Node >= 20 (v23.3.0 recommended via
.nvmrc) - npm (with
legacy-peer-deps = truein.npmrc) - Required env vars: See
.env.example