# Country Club UI Kit — agent guidelines

These are the working rules for building with **`@countryclub/ui-react`**, the
React component library for the Country Club design system. Hand this file to
your AI assistant (as `CLAUDE.md`, `GEMINI.md`, `AGENTS.md`, etc.) so it
generates code that matches the kit.

## Project overview

A React UI kit built with:

- **React 19** with TypeScript
- **[Base UI](https://base-ui.com/) `1.1.0`** primitives as the accessibility and
  behavior foundation (pinned — the kit relies on its data attributes and APIs)
- **Tailwind CSS v4** for styling, with a shadcn-style semantic token system
- **[TanStack Form](https://tanstack.com/form)** for form state (optional entry point)
- **[lucide-react](https://lucide.dev/)** for icons

The kit is consumed as an **installed package** — you import components from
`@countryclub/ui-react`. You do not copy component source into your project.

## Key conventions

### Import from the package root

Always import components, the `cn` helper, hooks, and types from the package
entry point. Never reach into internal file paths.

```tsx
// ✅ Correct
import { Button, Card, Input, cn } from "@countryclub/ui-react";

// ❌ Incorrect — internal paths are not part of the public API
import { Button } from "@countryclub/ui-react/src/components/ui/button";
```

Form bindings live in a separate entry point so TanStack Form stays out of your
bundle unless you use it:

```tsx
import { useAppForm } from "@countryclub/ui-react/form";
```

### Polymorphism: use the `render` prop, not `asChild`

Components are built on Base UI, so they accept a **`render`** prop to change the
rendered element (Base UI's equivalent of Radix's `asChild`). Do not use
`asChild`.

```tsx
import Link from "next/link";

// Render a Button as a Next.js Link
<Button render={<Link href="/dashboard" />} variant="outline">
  View dashboard
</Button>

// Render a Button as an anchor
<Button render={<a href="https://country.club" />} variant="link">
  Visit site
</Button>
```

### `data-slot` attributes

Every component sets a `data-slot` attribute (e.g. `data-slot="button"`,
`data-slot="card"`). Use these for targeting in CSS or for structural selectors —
don't strip or override them.

### The `cn` helper

Merge class names with `cn` (clsx + tailwind-merge). Use it whenever you compose
classes so later utilities win conflicts.

```tsx
import { cn } from "@countryclub/ui-react";

<div className={cn("rounded-lg border", isActive && "border-brand", className)} />
```

### `"use client"`

Interactive components are client components. When you build wrappers around the
kit in a React Server Components environment (Next.js App Router), add the
`"use client"` directive to any file that uses hooks, state, or event handlers.

### File naming

Name all files in **kebab-case** (`tee-time-card.tsx`, `use-member.ts`), matching
the rest of the codebase.

## Styling & design tokens

### Always use semantic color classes

The design system exposes **semantic tokens** as Tailwind color utilities. Style
with these — never hard-code raw palette colors. Tokens automatically adapt
between light and dark mode.

```
❌ Bad                     ✅ Good
text-stone-900             text-foreground
text-stone-500             text-muted-foreground
bg-white                   bg-background  /  bg-card
border-black/10            border-border
bg-blue-600                bg-primary
```

The available tokens (use as `bg-*`, `text-*`, `border-*`, `ring-*`, `fill-*`, etc.):

| Token | Usage |
| :-- | :-- |
| `background` / `foreground` | Page background and default text. |
| `card` / `card-foreground` | Card surfaces and text on them. |
| `popover` / `popover-foreground` | Floating surfaces (menus, popovers, dropdowns). |
| `primary` / `primary-foreground` | Primary actions (the default Button). |
| `secondary` / `secondary-foreground` | Secondary, lower-emphasis surfaces. |
| `muted` / `muted-foreground` | Muted backgrounds and supporting/secondary text. |
| `accent` / `accent-foreground` | Hover/active surface tint (e.g. ghost buttons, menu items). |
| `destructive` / `destructive-foreground` | Dangerous actions and errors. |
| `success` / `warning` / `info` (+ `-foreground`) | Semantic status colors. |
| `border` | Default borders and dividers. |
| `input` | Border color for form controls. |
| `ring` | Focus ring color. |
| `brand` | The Country Club brand accent (warm gold). |
| `chart-1` … `chart-5` | Categorical chart series colors. |
| `sidebar`, `sidebar-foreground`, `sidebar-primary`, `sidebar-accent`, `sidebar-border`, `sidebar-ring` | Sidebar surface tokens. |

### Typography

The kit ships two typefaces: **DM Sans** (`font-sans`, body) and **Fraunces**
(`font-serif`, display). Headings sized **`text-xl` and larger render in Fraunces
automatically** — add `font-sans` to opt a large element back into DM Sans.

```tsx
<h1 className="text-4xl">Members lounge</h1>          {/* serif (Fraunces) */}
<p className="text-4xl font-sans">Override to sans</p> {/* sans (DM Sans) */}
```

### Radius

The radius scale derives from a single `--radius` (0.625rem). Use the Tailwind
radius utilities (`rounded-sm`, `rounded-md`, `rounded-lg`, `rounded-xl`,
`rounded-2xl`, …) rather than fixed pixel values.

### Dark mode

Dark mode is driven by the **`data-theme="dark"`** attribute on `<html>` (managed
by `ThemeProvider`, below). Because every component uses the semantic tokens
above, components adapt to dark mode with no extra work — do not write
`dark:` overrides for color unless you have a specific reason.

### Depth via inset shadows

Components create subtle depth with `before:`/`after:` pseudo-element shadows and
inset shadows rather than heavy borders. Follow that convention when extending
the kit; keep effects subtle.

## Icons

Use **lucide-react**. Import the named icon and render it inside a component or
standalone. Size with `size-*` utilities and color with semantic text tokens.

```tsx
import { CalendarIcon, ChevronDownIcon } from "lucide-react";

// Inside a button
<Button>
  <CalendarIcon /> Book a tee time
</Button>

// Standalone, sized and colored
<ChevronDownIcon className="size-4 text-muted-foreground" />
```

Buttons auto-size and dim child SVGs, so you usually don't need to set `size-*`
on icons placed inside a `Button`.

## Components

Import any of these from `@countryclub/ui-react`:

> Accordion, Alert, AlertDialog, AnimatedCounter, Autocomplete, Avatar, AvatarGroup, Badge, BadgeGroup,
> Breadcrumb, Button, Calendar, Card, Carousel, Chart, Chat, Checkbox, CheckboxGroup,
> Collapsible, Combobox, Command, Confetti, ConfettiButton, ContextMenu, CreditCard, DashboardTopbar,
> DataTablePaged, DatePicker, DateRangePicker, Dialog, Drawer, Empty, Fab,
> FeaturedIcon, Field, Fieldset, Form, Group, Input, InputGroup, InputOTP, Item,
> Kbd, Label, Markdown, Menu, Menubar, Meter, MultiSelect, NavigationMenu, NumberField,
> Pagination, Pipeline, Popover, PreviewCard, Progress, QRCode, RadioGroup, Resizable,
> ScrollArea, SectionHeader, SegmentedControl, Select, Separator, Sheet, Sidebar, Skeleton, Slider,
> SocialButton, Stat, StatGroup, Stepper, Switch, Table, Tabs, Tag, TextEditor,
> Textarea, Timeline, Toast,
> Toggle, ToggleGroup, Toolbar, Tooltip, VideoPlayer

### Chat / AI-agent primitives

For AI-assistant experiences the kit ships purpose-built parts:

- `Fab` — fixed round floating action button (`position="bottom-right"` default);
  composes `Button`, so all variants/sizes apply.
- `Chat`, `ChatMessages` (auto-scrolls while pinned to bottom), `ChatMessage` /
  `ChatMessageBubble` (`from="user" | "assistant"`), `ChatTypingIndicator`,
  `ChatToolCard` + `ChatToolCardHeader` + `ChatToolCardActions` (tool activity and
  write-approval prompts), and `ChatInput` (rounded composer card wrapping a
  controlled textarea + round send button; Enter submits, Shift+Enter for a newline).
- Header chrome: `ChatHeader` + `ChatHeaderAvatar` (gradient sparkle avatar with a
  "live" dot; pass a custom icon as children) + `ChatHeaderTitle` + `ChatHeaderActions`.
- Empty-state hero: `ChatEmptyState` + `ChatEmptyStateIcon` + `ChatEmptyStateTitle` +
  `ChatEmptyStateDescription`, with `ChatSuggestions` wrapping `ChatSuggestion`
  prompt buttons (icon chip + label + hover arrow; pass `index` to stagger the
  entrance animation, optional `icon` to override the sparkle).
- `ChatToolChip` — small inline pill for tool activity lines ("Using X…",
  "X finished"); pass the status icon via `icon`.
- `Markdown` — GitHub-flavored markdown renderer for model output (raw HTML skipped).
- `DataTablePaged` — client-side paginated + searchable table for tool/API result
  sets: `columns=[{key, label?}]`, `rows=[{...}]`, `pageSize`, optional `totalCount`.

```tsx
<Chat>
  <ChatMessages>
    <ChatMessage from="user"><ChatMessageBubble from="user">Hi!</ChatMessageBubble></ChatMessage>
    <ChatMessage from="assistant">
      <ChatMessageBubble from="assistant"><Markdown>{answer}</Markdown></ChatMessageBubble>
    </ChatMessage>
    {loading && <ChatTypingIndicator />}
  </ChatMessages>
  <ChatInput value={draft} onValueChange={setDraft} onSubmit={send} disabled={loading} />
</Chat>
```

Most complex components are **compound** (a root plus sub-components). Compose them
with the exported parts rather than passing large config objects.

### Button

```tsx
import { Button } from "@countryclub/ui-react";
```

- `variant`: `"default"` (primary) · `"secondary"` · `"outline"` · `"ghost"` ·
  `"link"` · `"link-muted"` · `"destructive"` · `"destructive-outline"` ·
  `"destructive-ghost"` — default `"default"`
- `size`: `"default"` · `"sm"` · `"lg"` · `"xl"` · `"xs"` · `"icon"` · `"icon-sm"` ·
  `"icon-lg"` · `"icon-xl"` — default `"default"`
- `render`: element to render as instead of `<button>` (see polymorphism above)

```tsx
<Button>Save changes</Button>
<Button variant="outline">Cancel</Button>
<Button variant="destructive">Delete</Button>
<Button size="icon" variant="ghost"><TrashIcon /></Button>
```

### Card

```tsx
import {
  Card,
  CardHeader,
  CardTitle,
  CardDescription,
  CardAction,
  CardContent, // alias of CardPanel
  CardFooter,
} from "@countryclub/ui-react";

<Card>
  <CardHeader>
    <CardTitle>Spring Invitational</CardTitle>
    <CardDescription>Three days of match play.</CardDescription>
    <CardAction>
      <Button size="sm" variant="ghost">Edit</Button>
    </CardAction>
  </CardHeader>
  <CardContent>{/* … */}</CardContent>
  <CardFooter>
    <Button>Register</Button>
  </CardFooter>
</Card>
```

### Input & fields

`Input` is a styled control; pair it with the `Field` parts for labels, hints,
and error messages.

```tsx
import { Field, FieldLabel, FieldDescription, FieldError, Input } from "@countryclub/ui-react";

<Field>
  <FieldLabel>Email</FieldLabel>
  <Input type="email" placeholder="august@country.club" />
  <FieldDescription>We'll send your booking confirmation here.</FieldDescription>
  <FieldError />
</Field>
```

- `Input` props: `size` (`"sm" | "default" | "lg"` or a number), plus all native
  input attributes. Use `unstyled` to drop the wrapper styles, `nativeInput` to
  render a plain `<input>` instead of the Base UI primitive.

### Badge

```tsx
import { Badge } from "@countryclub/ui-react";
```

- `variant`: `"default"` · `"secondary"` · `"outline"` · `"modern"` ·
  `"destructive"` · `"error"` · `"info"` · `"success"` · `"warning"`
- `size`: `"sm"` · `"default"` · `"lg"`
- `shape`: `"default"` · `"pill"`

```tsx
<Badge variant="success" shape="pill">Active</Badge>
<Badge variant="outline">Draft</Badge>
```

### Stat

```tsx
import {
  Stat,
  StatLabel,
  StatValue,
  StatDescription,
  StatTrend,
  StatGroup,
} from "@countryclub/ui-react";
```

A headline metric (KPI). Compose `Stat` from its parts; drop bare `Stat`s into a
`StatGroup` for a divided row, or use `variant="muted" | "card"` for a tile.

- `Stat` — `variant`: `"default"` (bare) · `"muted"` · `"card"`
- `StatValue` — sans + tabular by default so digits align; drop `font-sans` to
  render the number in the serif display face
- `StatTrend` — `direction`: `"up"` · `"down"` · `"neutral"` (colors itself
  success/destructive/muted and shows an arrow); `showIcon` defaults to `true`
- `StatGroup` — lays bare `Stat`s out as a divided card row (stacked on mobile)

```tsx
<StatGroup>
  <Stat>
    <StatLabel>Active members</StatLabel>
    <StatValue>1,284</StatValue>
    <StatDescription><StatTrend direction="up">+12.5%</StatTrend> vs. last month</StatDescription>
  </Stat>
</StatGroup>
```

### Dashboard Topbar

```tsx
import { DashboardTopbar } from "@countryclub/ui-react";
```

The shared dashboard top bar: a sidebar trigger, a search field styled as an
input, an optional theme toggle, an optional support link, and an account menu.
It renders `data-slot="dashboard-topbar"` and sits inside a `SidebarProvider`
(its sidebar trigger reads that context). It does **not** own the command
palette, the ⌘K key handler, or a settings modal — it only renders the search
affordance and calls back to you.

- `user`: `{ name; email?; avatarSrc?; initials? }` — shown in the account menu
  and avatar trigger (required)
- `onSearch?`: called when the search field is activated (open your palette)
- `searchPlaceholder?`: search field text — default `"Search"`
- `searchShortcut?`: trailing hint node — default `<KbdGroup><Kbd>⌘</Kbd><Kbd>K</Kbd></KbdGroup>`; pass `null` to hide
- `onSettings?` / `onLogout?`: account-menu item handlers — each item is omitted when its handler is undefined
- `supportHref?`: mailto/url; renders the support icon button when set
- `showThemeToggle?`: default `true`
- `actions?`: extra controls rendered left of the theme toggle
- `className?`

```tsx
<DashboardTopbar
  user={{ name: "August Whitmore", email: "august@country.club", initials: "AW" }}
  onSearch={() => setPaletteOpen(true)}
  onSettings={() => router.push("/settings")}
  onLogout={logout}
  supportHref="mailto:support@country.club"
/>
```

### Section Header

```tsx
import { SectionHeader } from "@countryclub/ui-react";
```

A titled section heading, one altitude below the page-level `DashboardHeader`.

- `title` / `description` — `ReactNode`
- `actions` — right-aligned node (buttons, counts, menus)
- `icon` — leading node (e.g. a `<FeaturedIcon />`)
- `size`: `"sm"` · `"default"` · `"lg"` — default `"default"`
- `border`: adds a bottom hairline + padding so it sits above a list/grid

```tsx
<SectionHeader
  title="Team members"
  description="People who can manage dinners."
  actions={<Button size="sm" variant="outline">Invite</Button>}
  border
/>
```

### Avatar Group

```tsx
import { AvatarGroup } from "@countryclub/ui-react";
```

A row of overlapping avatars with an overflow "+N" chip.

- `items`: `{ src?, initials?, alt? }[]` for the data-driven path — or omit it and
  compose `<Avatar>` children directly
- `max`: caps the visible avatars; the remainder collapse into a count
- `size`: matches Avatar sizes (`"xs"`–`"2xl"`) — default `"sm"`
- `overlap`: `"sm"` · `"md"` · `"lg"` — default `"md"`

```tsx
<AvatarGroup items={members} max={4} size="md" />
```

## Forms

Forms use **TanStack Form** through the `@countryclub/ui-react/form` entry point.
`useAppForm` returns a form bound to the kit's field components (`TextField`,
`SelectField`, `CheckboxGroupField`, `DateField`, `DateRangeField`,
`NumberField`) and `SubmitButton`. Validation runs on submit, then re-validates
on blur.

```tsx
"use client";

import { useAppForm } from "@countryclub/ui-react/form";

const tiers = [
  { label: "Social", value: "social" },
  { label: "Golf", value: "golf" },
  { label: "Full club", value: "full" },
];

export function JoinForm() {
  const form = useAppForm({
    defaultValues: { name: "", tier: "" },
    onSubmit: async ({ value }) => {
      // submit `value`
    },
  });

  return (
    <form
      noValidate
      onSubmit={(e) => {
        e.preventDefault();
        form.handleSubmit();
      }}
    >
      <form.AppField
        name="name"
        validators={{
          onDynamic: ({ value }) =>
            value.trim() ? undefined : "Member name is required",
        }}
      >
        {(field) => (
          <field.TextField field={field} label="Member name" placeholder="August Whitmore" />
        )}
      </form.AppField>

      <form.AppField
        name="tier"
        validators={{
          onDynamic: ({ value }) => (value ? undefined : "Choose a tier"),
        }}
      >
        {(field) => (
          <field.SelectField field={field} label="Membership tier" options={tiers} placeholder="Select a tier" />
        )}
      </form.AppField>

      <form.AppForm>
        <form.SubmitButton>Join the club</form.SubmitButton>
      </form.AppForm>
    </form>
  );
}
```

## Theming

Wrap your app once with `ThemeProvider`. It writes `data-theme` to `<html>` and
persists the choice; read or change the theme with `useTheme`.

```tsx
import { ThemeProvider, useTheme } from "@countryclub/ui-react";

// At the root
<ThemeProvider attribute="data-theme" defaultTheme="system" enableSystem disableTransitionOnChange>
  {children}
</ThemeProvider>;

// Anywhere below it
function ThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme();
  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
    >
      {resolvedTheme === "dark" ? <SunIcon /> : <MoonIcon />}
    </Button>
  );
}
```

## Best practices for AI assistance

1. Import from `@countryclub/ui-react` (and `/form` for forms) — never deep paths.
2. Style with semantic tokens (`bg-primary`, `text-muted-foreground`, `border-border`),
   never raw palette colors.
3. Use the `render` prop for polymorphism; never `asChild`.
4. Compose compound components from their exported parts.
5. Let large headings inherit Fraunces; reach for `font-sans` only to override.
6. Don't fight dark mode — tokens already adapt. Avoid manual `dark:` color classes.
7. Add `"use client"` to any file using hooks, state, or events in an RSC app.
8. Merge classes with `cn` so consumer overrides win.
9. Keep motion subtle and preserve `data-slot` attributes when extending components.
