GFS Telephone
Greenstone Frontend Starter
v0.1.0
Next.js 15
TypeScript 6
Production ready

Build fast. Ship right.

A production-ready Next.js 15 starter. Radix UI, Tailwind v4, Jest, and Playwright — pre-wired so your team can focus on the product from day one.

pnpm install && pnpm dev
http://localhost:3007
Next.js 15.5
React 19
TypeScript 6
Tailwind v4
Radix UI
Jest 29
Playwright
ESLint v9

Quick start

Three commands to go from zero to running.

01

Clone

git clone https://github.com/gfs-engineering/gfs-telephone.git

Then cd gfs-telephone && cp .env.example .env.local

02

Install

pnpm install

Requires pnpm ≥ 10. Run npm install -g pnpm@10 if not installed.

03

Develop

pnpm dev

Next.js 15 + Turbopack. Hot reload in ~2s → http://localhost:3007

Component library

Radix UI primitives styled with Tailwind v4. Import from @/components/ui.

Button5 variants · 3 sizes · forwardRef
import { Button } from '@/components/ui/button';

<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
Badge7 semantic variants for status and labels
Default
Secondary
Outline
Active
Pending
Failed
Info
import { Badge } from '@/components/ui/badge';

<Badge>Default</Badge>
<Badge variant="success">Active</Badge>
<Badge variant="warning">Pending</Badge>
<Badge variant="destructive">Failed</Badge>
Alert5 variants with icon slot — default, info, success, warning, destructive
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/alert';

<Alert variant="success">
  <CheckCircle className="h-4 w-4" />
  <AlertTitle>Payment processed</AlertTitle>
  <AlertDescription>$45.00 collected successfully.</AlertDescription>
</Alert>
CardComposable — CardHeader, CardTitle, CardDescription, CardContent, CardFooter

Life Cover

Real Insurance

Active
Sum insured$500,000
Monthly premium$45.00
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';

<Card>
  <CardHeader>
    <CardTitle>Life Cover</CardTitle>
    <CardDescription>Real Insurance</CardDescription>
  </CardHeader>
  <CardContent>…</CardContent>
</Card>

Interactive components

Client-side Radix primitives — click the <> Code button on each card to see the usage.

TabsRadix UI — keyboard navigable, accessible by default
DialogFocus-managed modal with accessible overlay
Tooltip + AvatarHover or focus triggers — never blocks keyboard navigation
ACSLMK
Form inputsLabel, Input, Checkbox and Switch — all accessible and keyboard-friendly
Team cardComposing Card, Avatar, Badge and Button together
AC

Alex Chen

Senior Developer

Active
Platform
SL

Sarah Liu

Product Manager

On leave
Growth

Styling

Everything comes from Tailwind v4 utility classes. No CSS files per component, no CSS-in-JS, no styled-components.

Tailwind utility classesApply styles directly as className strings
LabelSupporting text
col 1
col 2
col 3
// Layout, spacing, typography — all as className
<div className="flex items-center gap-4 rounded-xl border bg-white p-6 shadow-sm">
  <span className="text-sm font-medium text-gray-900">Label</span>
  <span className="text-xs text-gray-500">Supporting text</span>
</div>

// Responsive — sm: md: lg: xl: 2xl:
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">

// State variants — hover: focus: active: disabled: group-hover:
<button className="bg-brand-600 hover:bg-brand-700 disabled:opacity-50
                   focus-visible:ring-2 focus-visible:ring-brand-500">
cn() — composing classes safelyUse cn() from @/lib/utils for conditional and merged classes
Default state
Active state
Error state
import { cn } from '@/lib/utils';

// Conditional classes — no string interpolation bugs
<div className={cn(
  'rounded-lg border p-4',           // always applied
  isActive  && 'border-brand-500 bg-brand-50',
  isError   && 'border-red-500 bg-red-50',
  className,                         // forwarded className prop
)}>

// CVA for variant components (used in all UI components here)
import { cva } from 'class-variance-authority';
const variants = cva('base-classes', {
  variants: {
    size: { sm: 'h-8 px-3 text-xs', md: 'h-10 px-4 text-sm' },
  },
});

Brand color palette

Defined in globals.css @theme — use as bg-brand-*, text-brand-*, border-brand-*, ring-brand-*

50
100
200
300
400
500
600
700
800
900
Primary actionbg-brand-600
Hover statebg-brand-700
Subtle tintbg-brand-50
Focus ringring-brand-500

Never write CSS files

All component styling lives in className strings. If you reach for a .css file, check the Tailwind docs — there is almost certainly a utility for it.

No style="" for layout

Use Tailwind classes for all spacing, sizing, colours, and layout. Only use inline style for truly dynamic values (e.g. a progress bar width from JS).

Extend via @theme

Add new design tokens in globals.css inside the @theme block. A token like --color-surface becomes the class bg-surface in Tailwind v4.

Project structure

Conventions baked in so your team starts with good habits from day one.

./
├── src/
│   ├── app/
│   │   ├── _components/       # Client / reference components for this route
│   │   │   ├── demos.tsx      # Interactive UI demos (use client)
│   │   │   └── intro.tsx      # Reference page — delete import in page.tsx to start fresh
│   │   ├── globals.css        # Tailwind v4 @theme tokens
│   │   ├── layout.tsx         # Root layout — metadata, global styles
│   │   └── page.tsx           # Your app starts here
│   ├── components/
│   │   └── ui/                # Radix + CVA component primitives
│   │       ├── index.ts       # Barrel — import from '@/components/ui'
│   │       ├── alert.tsx
│   │       ├── avatar.tsx
│   │       ├── badge.tsx
│   │       ├── button.tsx
│   │       ├── card.tsx
│   │       ├── checkbox.tsx
│   │       ├── dialog.tsx
│   │       ├── input.tsx
│   │       ├── label.tsx
│   │       ├── separator.tsx
│   │       ├── switch.tsx
│   │       ├── tabs.tsx
│   │       ├── textarea.tsx
│   │       └── tooltip.tsx
│   ├── lib/
│   │   └── utils.ts           # cn() — clsx + tailwind-merge
│   └── types/
│       └── global.d.ts        # CSS module ambient declarations
├── e2e/                       # Playwright E2E tests
├── .env.example               # Required environment variables
├── CLAUDE.md                  # AI assistant guide for this project
├── jest.config.ts
├── next.config.ts
├── playwright.config.ts
└── tsconfig.json
src/app/

Next.js App Router. Each route is a folder. Server Components by default. Add "use client" only for browser APIs or state.

src/app/_components/

Client components scoped to a route. The underscore prefix keeps them out of the URL routing. One file = one concern.

src/components/ui/

Unstyled Radix primitives + Tailwind v4 + CVA. No business logic. Import everything from the barrel: import { Button, Badge } from "@/components/ui".

src/lib/utils.ts

cn() merges Tailwind classes with clsx + tailwind-merge. Use it everywhere to safely compose class strings without conflicts.

e2e/

Playwright specs. webServer block auto-starts the app. Specs should test real user flows, not implementation details.