# Agent Development Guide

> This file contains @ably/ui package-specific guidance. For universal code style and git workflow, see the [root AGENTS.md](../../AGENTS.md).

## Build & Test Commands

- `pnpm build` - Build the library (prebuild, icons, swc, tsc, cleanup)
- `pnpm test` - Run all tests with Vitest
- `pnpm test -- src/core/insights/index.test.ts` - Run a single test file
- `pnpm lint` - Run ESLint on all files
- `pnpm format:check` - Check formatting with Prettier
- `pnpm format:write` - Auto-format all files with Prettier
- `pnpm storybook` - Start Storybook dev server on port 3200
- `pnpm start` - Start Vite dev server on port 5000

## Code Style

- **React**: Use functional components with hooks; React 18.x
- **Imports**: Default export for main component, named exports for types/utils
- **Naming**: PascalCase for components/types, camelCase for functions/variables,
  kebab-case for files
- **Types**: Define prop types as `ComponentNameProps`, use `PropsWithChildren<T>`
  when needed
- **Utility**: Use `cn()` from `./src/core/utils/cn` for className merging (clsx
  & tailwind-merge)
- **Formatting**: Prettier defaults (no config = defaults), 2-space indent
- **Error Handling**: Wrap external service calls in try-catch, log with logger module
- **Comments**: JSDoc for props, brief inline comments only for genuinely complex logic — keep them terse per the [root AGENTS.md](../../AGENTS.md) comment rule

### Custom Hooks

- **Naming**: `use` prefix with descriptive name (e.g., `useContentHeight`, `useThemedScrollpoints`)
- **JSDoc**: Always include for custom hooks, especially performance-related ones — document `@param` with types/defaults and `@returns` with type and semantic meaning
- **Performance rationale**: Note the "why" in JSDoc when optimizing — a short phrase, not a paragraph (e.g., "eliminates forced reflows")
- **Cleanup**: Always return cleanup function to prevent memory leaks
- **Shared constants**: Import from `src/core/utils/heights.ts` instead of duplicating

## Development

- Run `pnpm lint` & `pnpm format:write` on files after making changes, we lint
  files in CI and don't want preventable failures. `pnpm lint:fix` should also
  apply our formatting rules while trying to fix most things for you
- Run tests with `pnpm test` after making file changes
- When testing with Storybook, use Chrome DevTools Performance tab to verify no forced reflows
- For performance-related changes, compare before/after metrics and include in commit/PR

### Chrome DevTools MCP (Optional)

See the [root AGENTS.md](../../AGENTS.md) for the tool decision table. Use Chrome DevTools MCP for performance profiling and visual verification. Storybook URL: `http://localhost:3200` (port varies by worktree index).

## Styling Guide

### Color Palettes

The design system uses semantic color palettes defined in `src/core/styles/properties.css`
and configured for Tailwind in `tailwind.config.js`. Each palette has a different
number of color values:

- **Neutral**: 000, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300 (14 values)
- **Orange**: 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100 (11 values)
- **Yellow**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
- **Green**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
- **Blue**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
- **Violet**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)
- **Pink**: 100, 200, 300, 400, 500, 600, 700, 800, 900 (9 values)

### Interactive Element Styling Patterns

When developing components with @ably/ui, **always** use Tailwind classes following
these established patterns to ensure consistent interactive behavior across light
and dark modes:

#### Dark Mode Mirroring

For any given color, add a dark mode class that mirrors it across the palette.
Lower values (lighter colors) in light mode should map to higher values (darker
colors) in dark mode, and vice versa.

The sum of mirrored color numbers should equal the total palette range:

- Neutral (000-1300): `light + dark = 1300` (e.g., `bg-neutral-200` / `dark:bg-neutral-1100`, `text-neutral-1300` / `dark:text-neutral-000`)
- Orange (100-1100): `light + dark = 1200` (e.g., `bg-orange-200` pairs with `dark:bg-orange-1000`)
- Secondary colors (100-900): `light + dark = 1000` (e.g., `bg-blue-300` pairs with `dark:bg-blue-700`)

The mirroring formula applies to all property prefixes (`bg-`, `text-`, `border-`, etc.), not just backgrounds.

#### Hover and Active States

- **Hover**: Use the next color value along the palette (e.g., `bg-neutral-200` → `hover:bg-neutral-300`)
- **Active**: Use two color values along (e.g., `bg-neutral-200` → `active:bg-neutral-400`)
- Apply the same stepping in dark mode, moving in the opposite direction (towards lower values)
- For Radix stateful components, apply the same hover/active stepping to state variants (e.g., `data-[state=checked]:bg-orange-600 data-[state=checked]:hover:bg-orange-700`)

#### Focus and Transitions

- Add the `focus-base` class (defined in `src/core/styles/utils.css`) to all interactive elements for consistent accessible focus styling
- Add `transition-colors` to interactive elements unless a higher-specificity `transition` class is already present

### Complete Example

A button demonstrating all patterns — dark mode mirroring, hover/active stepping, focus, and transitions:

```tsx
<button
  className={cn(
    "px-4 py-2 rounded",
    "bg-neutral-200 hover:bg-neutral-300 active:bg-neutral-400",
    "dark:bg-neutral-1100 dark:hover:bg-neutral-1000 dark:active:bg-neutral-900",
    "text-neutral-1300 dark:text-neutral-000",
    "focus-base transition-colors",
  )}
>
  Click me
</button>
```

## Performance Optimization Guidelines

### When to Optimize

Optimize when Chrome DevTools Performance profiling shows:

- Forced reflows/layouts in event handlers (scroll, resize, input)
- Long tasks blocking the main thread (>50ms)
- CPU throttling causing device overheating (especially iOS)

Common anti-patterns causing forced reflows:

- `getBoundingClientRect()` in scroll/resize handlers
- `clientHeight/scrollHeight/offsetHeight` reads during interactions
- Synchronous layout queries followed by style changes
- DOM queries inside throttled/debounced callbacks

### Observer API Patterns

Prefer `IntersectionObserver` and `ResizeObserver` over synchronous layout reads.

**IntersectionObserver** (scroll position detection, viewport triggers):

- Observer only reports state _changes_, not all currently intersecting elements — maintain a `Map` to track the full set of intersecting elements
- Batch state updates inside `requestAnimationFrame()`
- Perform a manual initial state check with `setTimeout(..., 0)` since callbacks don't fire on first `observe()`
- Track the previous value and skip redundant `setState` calls
- When multiple elements have equal distances, use array order as a tiebreaker (earlier wins) — this prevents nondeterministic behaviour when elements share the same position (e.g., Voltaire's meganav and main theme at position 0)
- Clean up: disconnect observer, clear the Map, cancel any pending timeout

**ResizeObserver** (element dimension tracking):

- Store the `requestAnimationFrame` ID and cancel it on cleanup to prevent stale updates
- Use an `isMounted` flag to guard `setState` calls after unmount
- Cancel any pending RAF before scheduling a new one
- Unlike IntersectionObserver, ResizeObserver fires immediately on `observe()` — no manual initial check needed

### Testing Observer Hooks

See `src/core/hooks/use-themed-scrollpoints.test.ts` for a reference implementation of these patterns:

- Save original globals (`IntersectionObserver`, `requestAnimationFrame`) _before_ mocking and restore them in `afterEach` to prevent test pollution
- Mock `getBoundingClientRect` on test elements and provide `boundingClientRect` in `IntersectionObserverEntry` mocks
- Wrap timer advances and observer callback invocations in `act()`
- Test both initial state and subsequent observer-driven updates

### Common Pitfalls Checklist

When writing performance-optimized hooks:

- [ ] RAF cleanup: Store ID, cancel in cleanup
- [ ] isMounted guard: Prevent setState after unmount
- [ ] Initial state check: Manual check for IntersectionObserver
- [ ] Previous value tracking: Skip redundant setState
- [ ] Map/Set cleanup: Clear in cleanup function
- [ ] Test mock restoration: Save originals, restore in afterEach
- [ ] Console warnings: For missing DOM elements (not errors)
- [ ] Tiebreaker logic: When multiple candidates have equal scores

## Knowledge Base

`knowledge/` contains patterns, conventions, and gotchas learned from past work (one file per pattern). Consult these when working in this package.

## Storybook Development

- Use `http://localhost:3200` when developing/testing components
- Create stories that simulate production patterns (e.g., overlapping scrollpoints like Voltaire) — simple sequential layouts may not expose bugs like the tiebreaker issue where elements share the same position
- Test edge cases in stories (empty arrays, missing DOM elements, rapid state changes)
- **Performance testing:** Open Chrome DevTools Performance tab, record while interacting with the component, then check for:
  - Forced reflow indicators: `getBoundingClientRect`, `clientHeight`/`scrollHeight`/`offsetHeight`, "Forced reflow" warnings
  - Target: <5ms layout/reflow time during interactions

## Git workflow

See the [root AGENTS.md](../../AGENTS.md) for universal git workflow guidance.

**UI package-specific notes:**

- Run `pnpm lint` and `pnpm test` before pushing
- For performance-related changes, compare before/after metrics and include in commit/PR
