Guidelines for building minimal, professional, and concise frontend components in the /web project using TypeScript, React Router v7, Tailwind CSS, and the CVA pattern.
This skill provides guidance for developing frontend components in the /web project for CRAN/E, a PWA that searches CRAN packages and authors.
@remixicon/react) for iconsThe design follows a clean, minimal aesthetic with:
All UI components follow these patterns:
PropsWithChildren when accepting children, combine with VariantProps<typeof twBase> from CVAcva() for reusable, type-safe class compositionComponent.displayName for debuggingHeader (app/modules/header.tsx):
PageContent (app/modules/page-content.tsx):
full-width class for spanning viewport widthPageContentSection (app/modules/page-content-section.tsx):
InfoPill (app/modules/info-pill.tsx):
InfoCard (app/modules/info-card.tsx):
Tag (app/modules/tag.tsx):
Separator (app/modules/separator.tsx):
<hr> with opacity-20 for subtle dividersProse (app/modules/prose.tsx):
Anchors & AnchorLink (app/modules/anchors.tsx):
Use Radix Colors with semantic variants:
Text colors:
text-gray-normal: Body texttext-gray-dim: Secondary/muted texttext-gray-12/text-gray-4: Prose (dark/light mode)Reference pages follow this structure:
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>
_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 generationUse 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>().
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` },
];
});
dark: variantsmd: breakpoint for desktop adjustmentsImport from @remixicon/react:
import { RiArrowRightSLine, RiExternalLinkLine } from "@remixicon/react";
Size prop for consistency (typically 16 or 20).
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";
npm run lint from /webnpm run typecheck from /webnpm run build from /web/web/app/routes/*.tsx/web/app/modules/*.tsx/web/app/data/*.ts/web/app/data/*.shape.ts (Zod schemas), *.types.generated.ts (Supabase).nvmrc)legacy-peer-deps = true in .npmrc).env.exampleSearch for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer