Skip to content

Latest commit

 

History

History
429 lines (333 loc) · 12.7 KB

File metadata and controls

429 lines (333 loc) · 12.7 KB

React Starter Kit

An opinionated template for building React apps on Cloudflare Workers. TanStack Start for SSR, Panda CSS for styling, BaseUI for accessible primitives, Bun for everything.

This is a GitHub template repo. Click "Use this template" to create a new project, or clone it directly.

Quick Start

# From GitHub template
gh repo create my-app --template anulman/react-starter-kit --clone
cd my-app

# Or with bun
bun create anulman/react-starter-kit my-app

# Or clone directly
git clone https://github.com/anulman/react-starter-kit.git my-app
cd my-app
rm -rf .git && git init

# Install + codegen (Panda CSS generates styled-system/ automatically)
bun install

# Start dev server on http://localhost:3000
bun run dev

Post-Clone Checklist

After cloning, replace these placeholders with your project's details:

File What to change
package.json "name" — your package name
wrangler.jsonc "name" — your Cloudflare Worker name
.env.schema Add VITE_APP_NAME with your app name (used in page titles via makeHead())
src/routes/_app/index.tsx Replace landing page heading and description
README.md Replace this README with your own
CLAUDE.md Update project-specific conventions as you go

For local dev, create a .dev.vars file:

VITE_APP_NAME=My App

Stack

Layer Choice Why
Framework TanStack Start File-based routing, SSR, server functions
Runtime Bun Fast installs, native TypeScript
Styling Panda CSS Near-zero-runtime — styles generated at build time, with minimal JS for class name resolution
Components BaseUI Headless accessible primitives
Deployment Cloudflare Workers Edge SSR via @cloudflare/vite-plugin
Testing Vitest Vite-native, fast
Storybook Storybook 10 Component explorer with a11y addon

What's Included

24 Core Components

All follow the same pattern: BaseUI primitive → Panda CSS cva() recipe → typed props → ref as a regular prop (React 19).

import * as ui from "@/components/ui";

// Buttons with variants
<ui.Button variant="primary" size="md" loading={isSubmitting}>
  Save
</ui.Button>

// Form inputs
<ui.Input type="email" placeholder="you@example.com" />
<ui.TextArea rows={4} placeholder="Write something..." />
<ui.Select
  options={[
    { label: "Option A", value: "a" },
    { label: "Option B", value: "b" },
  ]}
  value={selected}
  onChange={setSelected}
/>
<ui.Checkbox checked={agreed} onChange={setAgreed}>
  I agree to the terms
</ui.Checkbox>

// Dialogs
<ui.Modal open={isOpen} onClose={() => setIsOpen(false)} title="Edit Item">
  <p>Modal content here</p>
</ui.Modal>

<ui.ConfirmDialog
  open={showConfirm}
  onConfirm={handleDelete}
  onCancel={() => setShowConfirm(false)}
  title="Delete item?"
  confirmLabel="Delete"
  variant="danger"
/>

// Feedback
<ui.LoadingSpinner size="md" />
<ui.Skeleton width="100%" height="20px" />
<ui.Badge variant="success">Active</ui.Badge>
<ui.EmptyState
  title="No items yet"
  description="Create your first item to get started."
/>

Toasts via provider + hook:

// ToastProvider is already in __root.tsx
import { useToast } from "@/components/ui";

function MyComponent() {
  const { toast } = useToast();

  const handleSave = async () => {
    try {
      await save();
      toast({ message: "Saved!", variant: "success" });
    } catch {
      toast({ message: "Something went wrong", variant: "error" });
    }
  };
}

Layout Utilities

Re-exported from Panda CSS JSX -- composable flex/grid primitives:

import { Flex, Grid, HStack, VStack, Box, Center } from "@/components/layout";

<Flex gap="md" align="center" justify="space-between">
  <HStack gap="sm">
    <ui.Badge>New</ui.Badge>
    <span>Item title</span>
  </HStack>
  <ui.Button size="sm" variant="ghost">Edit</ui.Button>
</Flex>

<Grid columns={{ base: 1, md: 2, lg: 3 }} gap="md">
  {items.map(item => <Card key={item.id} {...item} />)}
</Grid>

Route Patterns

src/routes/
├── __root.tsx          # HTML shell, providers (QueryClient, ToastProvider)
├── _app.tsx            # Layout route (add header/sidebar here)
├── _app/
│   └── index.tsx       # Landing page (/)
└── api/
    ├── health.ts       # GET /api/health
    └── liveness.ts     # GET /api/liveness

Adding a protected layout route:

// src/routes/_authed.tsx
import { createFileRoute, redirect, Outlet } from "@tanstack/react-router";

export const Route = createFileRoute("/_authed")({
  async beforeLoad({ context }) {
    if (!context.user) {
      throw redirect({ to: "/login" });
    }
  },
  component: () => <Outlet />,
});

// src/routes/_authed/dashboard.tsx -> /dashboard (protected)

Server functions:

import { createServerFn } from "@tanstack/react-start";
import { z } from "zod";

export const getItems = createServerFn({ method: "GET" })
  .handler(async () => {
    // Runs on the server (CF Worker)
    // Access server env via ENV from varlock, NOT process.env
    const { API_KEY } = ENV;
    return fetch("https://api.example.com/items", {
      headers: { Authorization: `Bearer ${API_KEY}` },
    }).then(r => r.json());
  });

Storybook

Storybook 10 is pre-configured with Panda CSS support and the a11y addon. The config filters out TanStack Start and Cloudflare plugins that don't apply in the Storybook context.

bun run storybook  # Opens on http://localhost:6006

Add stories next to your components:

// src/components/ui/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react-vite";
import { Button } from "./Button";

const meta = {
  component: Button,
  args: { children: "Click me" },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = { args: { variant: "primary" } };
export const Ghost: Story = { args: { variant: "ghost" } };
export const Loading: Story = { args: { loading: true } };

For deployment options (CF Pages, subdirectory), see recipes/storybook-deploy/.

Environment Variables

Environment validation is handled by varlock via .env.schema. The included schema is scaffolded — expand for your project. Add @required, @type, @sensitive decorators to define your schema. Varlock validates on load and fails fast with clear errors.

// Client-side (must be prefixed with VITE_)
// Use import.meta.env directly — varlock validates on load via .env.schema
const appName = import.meta.env.VITE_APP_NAME;

// Server-side (inside request handlers only  --  not at module top level)
import { ENV } from "@/lib/serverEnv";
const { SECRET_KEY } = ENV;

For local dev, create .dev.vars:

SECRET_KEY=dev-secret
API_TOKEN=dev-token

Recipes

Self-contained patterns in recipes/ you can copy into your project when needed. Each has its own README with setup instructions.

recipes/auth/ -- OTP Authentication

Passwordless auth with verification codes (email/SMS). Dual-token session pattern: long-lived session cookie (3 months) + short-lived JWT (5 minutes).

// After copying recipe files into src/:
import { OtpInput } from "@/features/auth/OtpInput";
import { authMiddleware } from "@/features/auth/middleware";

// OTP input with auto-advance and paste support
<OtpInput
  length={6}
  onComplete={(code) => verify(code)}
/>

// Protect server functions with auth middleware
export const getProfile = createServerFn({ method: "GET" })
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    const user = context.user; // guaranteed by middleware
    return db.getUser(user.id);
  });

Includes: OtpInput component, session management (Lucia Auth-inspired), auth middleware, JWKS endpoint pattern.

recipes/authoring/ -- Rich Text & Markdown

Markdown rendering and TipTap-based rich text editing.

import { Markdown } from "@/features/authoring/Markdown";
import { MarkdownEditor } from "@/features/authoring/MarkdownEditor";

// Render markdown content
<Markdown content={post.body} />

// Rich text editor with toolbar
<MarkdownEditor
  initialContent={draft}
  onChange={(markdown) => saveDraft(markdown)}
  placeholder="Start writing..."
/>

Extra deps: react-markdown, remark-gfm, @tiptap/react, @tiptap/starter-kit

recipes/convex/ -- Real-time Database

Convex integration with TanStack Query bridge for unified caching + real-time subscriptions.

import { useQuery } from "@tanstack/react-query";
import { convexQuery } from "@convex-dev/react-query";
import { api } from "@/convex/_generated/api";

// Reactive query  --  auto-updates when data changes
const { data: items } = useQuery(
  convexQuery(api.items.list, { projectId })
);

// Conditional subscription
const { data: details } = useQuery({
  ...convexQuery(api.items.get, { itemId }),
  enabled: isExpanded,
});

Extra deps: convex, @convex-dev/react-query

recipes/analytics/ -- PostHog

Client + server PostHog setup with page tracking and feature flags.

import { usePostHog } from "@/features/analytics/posthog";

function CheckoutButton() {
  const posthog = usePostHog();

  return (
    <ui.Button onClick={() => {
      posthog.capture("checkout_started", { items: cart.length });
    }}>
      Checkout
    </ui.Button>
  );
}

Extra deps: posthog-js, posthog-node

Commands

bun run dev              # Start dev server (port 3000)
bun run build            # Production build
bun run preview          # Preview production build locally
bun run test             # Run tests (Vitest)
bun run typecheck        # TypeScript check
bun run storybook        # Storybook dev server (port 6006)
bun run build:storybook  # Build static Storybook
bun run deploy           # Deploy to Cloudflare Workers

Project Structure

├── src/
│   ├── components/
│   │   ├── ui/           # Design system (Button, Input, Modal, etc.)
│   │   ├── layout/       # Flex, Grid, HStack, VStack, Box, Center
│   │   └── icons/        # 7 icons from Untitled UI (thin React wrappers)
│   ├── lib/
│   │   ├── env.ts        # Client env flags (isProduction, isDevelopment)
│   │   └── serverEnv.ts  # Server env (re-exports varlock ENV)
│   ├── routes/           # File-based routing (TanStack Start)
│   ├── styles.css        # Global styles + Panda CSS layers
│   ├── router.ts         # Router config + context type
│   ├── start.ts          # SSR entry
│   └── client.tsx        # Client entry
├── .storybook/           # Storybook config (pre-wired for Panda CSS + BaseUI)
├── recipes/              # Opt-in patterns (auth, authoring, convex, analytics, pickers, storybook-deploy)
├── docs/                 # Architecture decisions, component API, deployment
├── styled-system/        # Generated by Panda CSS (gitignored)
├── panda.config.ts       # Design tokens + theme
├── vite.config.ts        # TanStack Start + CF Workers + React
├── wrangler.jsonc        # Cloudflare Workers config
├── CLAUDE.md             # AI development guide
└── AGENTS.md             # Agent conventions

Styling

Uses Panda CSS with ~20 design tokens. Two APIs:

import { css, cva } from "styled-system/css";

// One-off styles
const header = css({
  p: "md",
  bg: "surface",
  borderBottom: "1px solid token(colors.border)",
});

// Component recipes with variants
const cardRecipe = cva({
  base: { borderRadius: "md", border: "1px solid token(colors.border)" },
  variants: {
    elevated: {
      true: { boxShadow: "sm" },
      false: { boxShadow: "none" },
    },
  },
  defaultVariants: { elevated: false },
});

Rules:

  • css() / cva() only -- no styled() API
  • z-index: only -1, 0, or 1 -- never arbitrary values
  • Mobile-first responsive: { base: "sm", md: "md", lg: "lg" }
  • Never nest <Button> inside <Link> (invalid HTML)

AI-Assisted Development

This template includes CLAUDE.md and AGENTS.md for AI coding agents. If you use Claude Code, Codex, or similar tools, they'll pick up the project conventions automatically.

Recommended tools:

  • qmd -- fast file reading for large codebases
  • ast-grep -- structural code search and refactoring

License

MIT