Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ description: End-to-end guide for adding a new component to the Apsara design sy
metadata:
author: raystack
version: "1.0"
internal: true
---

# Add New Component to Apsara
Expand Down
79 changes: 79 additions & 0 deletions .agents/skills/apsara/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
name: apsara
description: Helps consume the @raystack/apsara React component library correctly in an application. Use when installing or setting up Apsara, building UI with Apsara components (Button, Dialog, Select, Menu, DataTable, Form, Tabs, Sidebar, Toast, etc.), theming (light/dark, accent/gray colors, modern/traditional style), styling with design tokens, or troubleshooting Apsara component behavior. Covers install, the Theme provider, the `--rs-*` token system, compound-component composition, and common pitfalls.
license: ISC
metadata:
author: raystack
---

# Apsara

Apsara (`@raystack/apsara`) is an open-source React component library built on [Base UI](https://base-ui.com/) primitives. It targets enterprise, data-dense interfaces (data tables, navigation shells, forms, overlays) and ships ~70 accessible, typed components. Styling is **vanilla CSS driven by `--rs-*` design tokens** that react to `data-*` attributes set by the theme provider — there is no Tailwind, no runtime CSS-in-JS, and no per-component install step.

## What this skill is for

Use this skill to help a **consumer** of the published package:

- install and wire up Apsara in a new or existing app (Next.js, Vite, etc.)
- pick and correctly compose Apsara components for a UI task
- theme the app (light/dark, accent/gray color, modern/traditional style)
- style and customize components using design tokens and `data-*` attributes
- avoid common composition and SSR pitfalls

This is **not** the skill for contributing components to the Apsara repo itself — for that, use the `add-new-component` skill.

## Source of truth

The library evolves quickly. Per-component exact props/variants are documented on the live docs site, which serves machine-readable Markdown. **Fetch these instead of guessing an API:**

- Docs index for agents: `https://apsara.raystack.org/llms.txt`
- Full docs (all pages concatenated): `https://apsara.raystack.org/llms-full.txt`
- Any single component: `https://apsara.raystack.org/docs/components/<name>.mdx` (e.g. `.../button.mdx`, `.../select.mdx`, `.../alert-dialog.mdx`)
- Any token category: `https://apsara.raystack.org/tokens/<category>.mdx` (`colors`, `spacing`, `typography`, `effects`, `radius`)

When you need a prop you are unsure about, fetch the component's `.mdx` page rather than inventing it.

## Core facts (always true)

1. **One package, one CSS import.** `npm install @raystack/apsara`, then `import "@raystack/apsara/style.css"` once at the app root. That stylesheet contains every component's styles and all tokens.
2. **Wrap the app in `<Theme>`.** Theming, dark mode, and the no-flash hydration script all come from the `Theme` provider. Without it, `data-theme`/token resolution will not work.
3. **Components are compound, dot-notation.** Apsara exports a single name per component and hangs sub-parts off it: `Dialog.Content`, `Select.Trigger`, `Menu.Item`, `Tabs.Tab`. It does **not** export flat names like `DialogContent`. (Contrast with shadcn/Radix and coss.)
4. **Style with tokens, never hard-coded values.** Use `--rs-*` custom properties (`var(--rs-color-foreground-base-primary)`, `var(--rs-space-5)`) so styling follows the active theme.
5. **Built on Base UI.** Components expose Base UI `data-*` state attributes (`data-open`, `data-disabled`, `data-starting-style`, …) for state-driven CSS, and trigger-based overlays follow Base UI composition.

## Principles for output

1. Use existing Apsara components before inventing custom markup.
2. Verify component props against the live `.mdx` docs; do not fabricate APIs.
3. Use dot-notation sub-components exactly as exported.
4. Use `--rs-*` tokens for all colors, spacing, radius, typography, and effects.
5. Keep accessibility intact: visible/`aria-label` labels, explicit `type` on buttons, icon `aria-hidden` when decorative.
6. Match Apsara's own conventions: CSS Modules + `class-variance-authority` for any custom styled components.

## Usage workflow

1. Confirm setup exists (CSS import + `<Theme>` wrapper). If not, see `references/setup.md`.
2. Identify the component(s) for the task. Use `references/components.md` as the registry index.
3. Fetch the component's `.mdx` page for exact props/variants when unsure.
4. Compose with correct dot-notation sub-parts (`references/composition.md`).
5. Style only via tokens / `className` / `data-*` (`references/styling.md`, `references/tokens.md`).
6. Self-check the output checklist below.

## References (read on demand)

- `references/setup.md` — install, CSS import, `<Theme>` wiring for Next.js App Router & Vite, icons & hooks subpath exports
- `references/theming.md` — full `Theme` / `ThemeProvider` API, `useTheme`, dark mode, accent/gray/style options, scoped (nested) themes, `ThemeSwitcher`
- `references/tokens.md` — **complete `--rs-*` token reference**: semantic colors, color scales, spacing, radius, typography, effects, theme `data-*` attributes
- `references/styling.md` — customizing components via `className`, `style`, `data-*`, CSS Modules, and CVA
- `references/composition.md` — compound dot-notation pattern, Base UI trigger/overlay composition, `render` prop, and per-component composition gotchas
- `references/components.md` — registry index of all exported components grouped by category, with imports and the live docs URL for each

## Output checklist

Before returning Apsara code:

- Component imports come from `@raystack/apsara` (icons from `@raystack/apsara/icons`, hooks from `@raystack/apsara/hooks`).
- Sub-parts use dot-notation exactly as exported (`Dialog.Content`, not `DialogContent`).
- Props/variants are verified against the live `.mdx` docs, not assumed.
- Colors/spacing/radius/typography use `--rs-*` tokens — no hard-coded hex/px for themed values.
- Buttons have explicit `type`; icon-only controls have `aria-label`; decorative icons have `aria-hidden`.
140 changes: 140 additions & 0 deletions .agents/skills/apsara/references/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Component Registry

Index of everything exported from `@raystack/apsara`, grouped by purpose. Use it to pick the right component, then fetch its live docs page for exact props/variants:

`https://apsara.raystack.org/docs/components/<slug>.mdx`

All components import from the root: `import { Button, Dialog } from "@raystack/apsara"`. Sub-parts are dot-notation (`Dialog.Content`) — see `composition.md`.

## Layout

| Component | Purpose | slug |
|---|---|---|
| `Box` | Generic layout primitive (display/spacing props) | — |
| `Flex` | Flexbox container (`direction`, `gap`, `align`, `justify`) | `flex` |
| `Grid` | CSS grid container | `grid` |
| `Container` | Width-constrained responsive wrapper | `container` |
| `Separator` | Visual/semantic divider | `separator` |
| `ScrollArea` | Styled custom scroll container | `scroll-area` |

## Typography

| Component | Purpose | slug |
|---|---|---|
| `Text` | Body text with `size`/`weight` token mapping | `text` |
| `Headline` | Display/heading text | `headline` |
| `Label` | Accessible form label | `label` |
| `Link` | Styled anchor | `link` |
| `CodeBlock` | Syntax-highlighted code block | `code-block` |

## Actions

| Component | Purpose | slug |
|---|---|---|
| `Button` | Primary action trigger (`variant`, `color`, `size`, `loading`, leading/trailing icons) | `button` |
| `IconButton` | Icon-only button (needs `aria-label`) | `icon-button` |
| `CopyButton` | Copy-to-clipboard button | `copy-button` |
| `Toggle` | Two-state pressable button | `toggle` |
| `FloatingActions` | Floating action cluster | `floating-actions` |

## Forms & inputs

| Component | Purpose | slug |
|---|---|---|
| `Form` | Form submission/validation wrapper | `form` |
| `Field` | Label + description + error wiring | `field` |
| `Fieldset` | Grouped controls with legend | `fieldset` |
| `Input` | Single-line text input | `input` |
| `TextArea` | Multi-line text input | `textarea` |
| `Select` | Single/multi choice from a list | `select` |
| `Combobox` | Searchable selection | `combobox` |
| `Search` | Search input | `search` |
| `Checkbox` | Boolean checkbox | `checkbox` |
| `Radio` | Mutually exclusive choice group | `radio` |
| `Switch` | On/off toggle | `switch` |
| `Slider` | Numeric range control | `slider` |
| `NumberField` | Numeric input with steppers | `number-field` |
| `OTPField` | Segmented one-time-passcode input | `otp-field` |
| `Calendar` / `DatePicker` / `RangePicker` | Date & range selection | `calendar` |
| `ColorPicker` (`* from color-picker`) | Color selection | `color-picker` |

## Overlays & popups

| Component | Purpose | slug |
|---|---|---|
| `Dialog` | Centered modal | `dialog` |
| `AlertDialog` | Confirmation modal (no outside-click close) | `alert-dialog` |
| `Drawer` | Edge drawer with swipe-to-dismiss | `drawer` |
| `SidePanel` | Side panel surface | `sidepanel` |
| `Popover` | Anchored non-modal floating content | `popover` |
| `Tooltip` | Hover/focus hint | `tooltip` |
| `PreviewCard` | Hover-triggered rich preview | `preview-card` |
| `Menu` | Dropdown action menu (groups, submenus) | `menu` |
| `Menubar` | Horizontal app menu bar | `menubar` |
| `ContextMenu` | Right-click context menu | `context-menu` |
| `Command` | Command palette | `command` |

## Navigation

| Component | Purpose | slug |
|---|---|---|
| `Sidebar` | App-shell navigation panel | `sidebar` |
| `Navbar` | Top navigation bar | `navbar` |
| `Tabs` | Tabbed panels | `tabs` |
| `Breadcrumb` | Hierarchical trail | `breadcrumb` |
| `Toolbar` | Grouped command strip | `toolbar` |

## Data display

| Component | Purpose | slug |
|---|---|---|
| `DataTable` (+ `useDataTable`) | Full-featured data table (sort/filter/virtualize, TanStack-based) | `datatable` |
| `DataView` (+ `useDataView`) | Unified table/list data view | `dataview` |
| `Table` | Primitive table markup | `table` |
| `List` | Structured list | `list` |
| `Avatar` / `AvatarGroup` (+ `getAvatarColor`) | User/entity avatars | `avatar` |
| `Badge` | Status label | `badge` |
| `Chip` | Compact token/tag | `chip` |
| `Amount` | Formatted monetary value | `amount` |
| `Image` | Image with handling | `image` |
| `EmptyState` | Empty-state placeholder | `empty-state` |

## Feedback & status

| Component | Purpose | slug |
|---|---|---|
| `Toast` (+ `toastManager`, `useToastManager`) | Transient notifications (imperative `toastManager`) | `toast` |
| `Callout` | Inline attention message | `callout` |
| `Indicator` | Status dot/indicator | `indicator` |
| `Progress` | Determinate/indeterminate progress bar | `progress` |
| `Meter` | Bounded scalar measurement | `meter` |
| `Spinner` | Indeterminate loading spinner | `spinner` |
| `Skeleton` | Loading placeholder | `skeleton` |
| `AnnouncementBar` | Page-level announcement banner | `announcement-bar` |

## Disclosure

| Component | Purpose | slug |
|---|---|---|
| `Accordion` | Stacked collapsible sections | `accordion` |
| `Collapsible` | Single expand/collapse region | `collapsible` |

## Filtering

| Component | Purpose | slug |
|---|---|---|
| `FilterChip` | Filter token with value/edit affordance | `filter-chip` |

## Theming (not visual components)

| Export | Purpose |
|---|---|
| `Theme` (alias `ThemeProvider`, deprecated) | Theme provider — wrap the app. See `theming.md`. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Clarify the deprecation relationship between Theme and ThemeProvider.

The phrasing "Theme (alias ThemeProvider, deprecated)" is ambiguous. It's unclear whether:

  • Theme is the current name and ThemeProvider is a deprecated alias, or
  • Theme is an alias for the deprecated ThemeProvider

Since this documentation guides AI agents, precision is critical. Consider rewording to:

  • "Theme (current; ThemeProvider is a deprecated alias)" or
  • "Theme / ThemeProvider (deprecated alias)"

This will prevent agents from using or recommending the deprecated name.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.agents/skills/apsara/references/components.md at line 132, The entry for
Theme/ThemeProvider is ambiguous: update the table row that currently reads
"`Theme` (alias `ThemeProvider`, deprecated)" to clearly indicate which name is
current and which is deprecated—for example change it to "`Theme` (current;
`ThemeProvider` is a deprecated alias)" or "`Theme` / `ThemeProvider`
(`ThemeProvider` is deprecated)" so agents know to use Theme; target the table
cell referencing Theme and ThemeProvider in the components.md file and replace
the ambiguous phrase with one of the clearer alternatives.

| `ThemeSwitcher` | Prebuilt light/dark toggle button |
| `useTheme` | Hook to read/set theme (also at `@raystack/apsara/hooks`) |

## Notes

- A `—` slug means there's no standalone docs page (e.g. `Box`); use the component's TypeScript types.
- `DataTable` and `DataView` are large, opinionated components — always fetch their `.mdx` (and the `useDataTable`/`useDataView` hooks) before wiring data, sorting, filtering, or virtualization.
- Icons import from `@raystack/apsara/icons`, not the root.
160 changes: 160 additions & 0 deletions .agents/skills/apsara/references/composition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Composition Patterns

Use this when assembling multi-part Apsara components (overlays, menus, selects, tabs, forms) so the structure and props are correct. Apsara wraps Base UI, so its composition model follows Base UI conventions.

## The compound dot-notation pattern

Apsara exports **one name per component** and hangs every sub-part off it as a property. Import the single root; reach sub-parts with dots:

```tsx
import { Dialog, Button } from "@raystack/apsara";

<Dialog>
<Dialog.Trigger render={<Button />}>Open</Dialog.Trigger>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>Title</Dialog.Title>
</Dialog.Header>
<Dialog.Body>
<Dialog.Description>Body copy</Dialog.Description>
</Dialog.Body>
<Dialog.Footer>
<Dialog.Close render={<Button variant="outline" color="neutral">Cancel</Button>} />
<Button>OK</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
```

There is **no** `DialogContent`, `SelectTrigger`, etc. as flat exports. If migrating from shadcn/Radix/coss, convert flat names to dot-notation (`DialogContent` → `Dialog.Content`).

## Composing a custom element into a part: `render` (not `asChild`)

Base UI uses a `render` prop to merge a part's behavior/props onto your element. Use it on triggers, closes, and any part you want to render as a different element.

```tsx
// Render the dialog trigger AS an Apsara Button:
<Dialog.Trigger render={<Button variant="outline" />}>Open</Dialog.Trigger>

// Close button rendered as a Button:
<Dialog.Close render={<Button color="neutral">Cancel</Button>} />
```

- `render` replaces the old Radix/shadcn `asChild` mental model. Don't pass `asChild`.
- `render` can also take a function for full control: `render={(props) => <a {...props} />}`.

## Trigger + content overlays (Dialog, Popover, Tooltip, Menu, …)

Common shape: a `Root` holds a `Trigger` and a portaled content part. The trigger must live inside the root.

```tsx
import { Popover, Button } from "@raystack/apsara";

<Popover>
<Popover.Trigger render={<Button variant="outline" />}>Details</Popover.Trigger>
<Popover.Content>…</Popover.Content>
</Popover>
```

Controlled state (for cross-component flows — e.g. a menu item opening a dialog) uses `open` + `onOpenChange`:

```tsx
const [open, setOpen] = useState(false);
<Dialog open={open} onOpenChange={setOpen}>
<Dialog.Content>…</Dialog.Content>
</Dialog>
```

Several overlay roots (`Dialog`, `Menu`, `Popover`, `Tooltip`) also expose `createHandle` for advanced imperative open/close from outside the subtree — use only when a trigger can't live in the same tree.

## Select

`Select.Value` (with `placeholder`) goes inside `Select.Trigger`; options are `Select.Item` children inside `Select.Content`. Items are children-based — no items array required.

```tsx
import { Select } from "@raystack/apsara";

<Select>
<Select.Trigger width={200}>
<Select.Value placeholder="Select a fruit" />
</Select.Trigger>
<Select.Content>
<Select.Group>
<Select.Label>Fruits</Select.Label>
<Select.Item value="apple">Apple</Select.Item>
<Select.Item value="banana">Banana</Select.Item>
</Select.Group>
</Select.Content>
</Select>
```

- Put `placeholder` on `Select.Value`, not the trigger.
- `Select.Item` supports `leadingIcon`; `disabled` marks unavailable rows.
- Sub-parts: `Select.Trigger`, `Select.Value`, `Select.Content`, `Select.Item`, `Select.Group`, `Select.Label`, `Select.Separator`, `Select.List`.

## Menu / ContextMenu

```tsx
import { Menu, Button } from "@raystack/apsara";

<Menu>
<Menu.Trigger render={<Button variant="outline" />}>Actions</Menu.Trigger>
<Menu.Content>
<Menu.Item onClick={onEdit}>Edit</Menu.Item>
<Menu.Separator />
<Menu.Submenu>
<Menu.SubmenuTrigger>More</Menu.SubmenuTrigger>
<Menu.SubmenuContent>
<Menu.Item onClick={onArchive}>Archive</Menu.Item>
</Menu.SubmenuContent>
</Menu.Submenu>
</Menu.Content>
</Menu>
```

- Menu actions use `onClick` (not Radix's `onSelect`).
- Sub-parts: `Menu.Trigger`, `Menu.Content`, `Menu.Item`, `Menu.Group`, `Menu.Label`, `Menu.Separator`, `Menu.EmptyState`, `Menu.Submenu`, `Menu.SubmenuTrigger`, `Menu.SubmenuContent`.

## Tabs

```tsx
import { Tabs } from "@raystack/apsara";

<Tabs defaultValue="overview">
<Tabs.List>
<Tabs.Tab value="overview">Overview</Tabs.Tab>
<Tabs.Tab value="activity">Activity</Tabs.Tab>
</Tabs.List>
<Tabs.Content value="overview">…</Tabs.Content>
<Tabs.Content value="activity">…</Tabs.Content>
</Tabs>
```

## Forms (`Form` + `Field`)

Wrap form-bound controls in `Field` so label, description, and error stay wired and accessible. `Form` handles submission/validation.

```tsx
import { Form, Field, Input, Button } from "@raystack/apsara";

<Form onSubmit={handleSubmit}>
<Field>
<Field.Label>Email</Field.Label>
<Input type="email" name="email" />
<Field.Error />
</Field>
<Button type="submit">Save</Button>
</Form>
```

- Always set `type` on inputs (`type="email"`) and buttons (`submit`/`button`/`reset`).
- Provide a visible label or `aria-label` for every control.
- Fetch `form.mdx` / `field.mdx` from the docs for the exact sub-parts and validation props.

## Composition rules & anti-patterns

- **Do** use dot-notation sub-parts exactly as exported; **don't** invent flat names.
- **Do** compose custom elements with `render`; **don't** use `asChild`.
- **Do** keep required structural parts (title/description in dialogs, value in select triggers) for accessibility.
- **Don't** mix one component's API onto another (e.g. Select patterns inside Menu).
- **Don't** assume a shadcn/Radix prop exists — verify against the component's `.mdx` page (`https://apsara.raystack.org/docs/components/<name>.mdx`).
Loading
Loading