> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.ekanos.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Runtime Notes

> Client/server boundaries, next-intl integration, and other implementation details.

## Client/server boundaries

### Directive preservation

Each client entry ships with `'use client'` at the top of its bundle:

| Entry | Directive |
| - | - |
| `ai-prompt-input`, `dialog`, `dropdown-menu`, `form`, `icon`, `label`, `page`, `select`, `separator`, `switch`, `tooltip`, `trans` | `'use client'` |
| `alert`, `badge`, `button`, `card`, `if`, `input`, `skeleton`, `spinner`, `stat`, `status-badge`, `textarea`, `utils` | None (server-safe) |

So `cn()`, `If`, `Spinner`, and the other directive-free entries can be used from Server Components:

```tsx theme={null}
// In a server component
import { cn } from '@ekanos/ui/utils';
import { Spinner } from '@ekanos/ui/spinner';

export async function Loading() {
  return (
    <div className={cn('p-4', 'text-center')}>
      <Spinner />
    </div>
  );
}
```

Because client entries carry the directive themselves, a Server Component can render them directly. Your own code needs its own `'use client'` only when it uses hooks or event handlers — for example, a form built with `useForm()`:

```tsx theme={null}
'use client';

import { Form } from '@ekanos/ui/form';
import { useForm } from 'react-hook-form';

export function MyForm() {
  const form = useForm();
  // ...
}
```

## Trans (i18n with next-intl)

`Trans` is a client component that reads next-intl's context, so it must render under a `NextIntlClientProvider`. The Ekanos host mounts one at its root, and `@ekanos/harness`'s `RootLayout` mounts one for you:

```tsx theme={null}
import { NextIntlClientProvider } from 'next-intl';
import { Trans } from '@ekanos/ui/trans';

<NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
  <Trans
    i18nKey="ns:key"
    defaults="fallback {{var}}"
    values={{ var: 'value' }}
  />
</NextIntlClientProvider>;
```

### Fallback chain

A key (`ns:key` shape) resolves to the host's message when it exists. Otherwise `Trans` falls back in this order:

1. **`defaults`** (interpolated with `values`, with `components`)
2. **`children`**
3. **The key itself**

With no provider at all, `Trans` throws. Anywhere else (a Storybook, a bare unit test), wrap the tree in a `NextIntlClientProvider`; empty messages are enough, because a missing key renders `defaults`:

```tsx theme={null}
<NextIntlClientProvider locale="en" messages={{}} timeZone="UTC">
  <Trans i18nKey="any:key" defaults="Default message" />
</NextIntlClientProvider>
```

### Interpolation

`defaults` strings use `{{variable}}` interpolation:

```tsx theme={null}
<Trans
  i18nKey="greeting"
  defaults="Hello {{name}}, welcome to {{app}}"
  values={{ name: 'Alice', app: 'Ekanos' }}
/>
```

### Components

A `components` map renders tags in the string as elements:

```tsx theme={null}
<Trans
  i18nKey="terms"
  defaults="I agree to the <link>terms</link>"
  components={{
    link: <a href="/terms" />,
  }}
/>
```

## Spinner

`Spinner` draws an inline SVG (a faint full ring under a quarter-turn arc), so unlike `Icon` it needs no icon font — only the Tailwind utilities it emits (`animate-spin`, `size-*`, `text-*`):

```tsx theme={null}
import { Spinner } from '@ekanos/ui/spinner';

<Spinner />                                  {/* Default: size-6, text-muted-foreground */}
<Spinner className="size-4 text-primary" />  {/* className lands on the SVG */}
```

It renders as `<span role="status" aria-label="Loading">`; other props land on that span. It honours `prefers-reduced-motion` via `motion-reduce:animate-none`, freezing the arc into a static busy glyph. It is hook-free, so it renders from a Server Component or a Client Component.

## Base UI (not Radix)

Popup-shaped components (`Select`, `DropdownMenu`, `Dialog`, `Tooltip`) are built on `@base-ui/react`, not Radix. The main difference you'll notice:

### No `asChild` prop

Instead, pass a `render` prop to the trigger:

```tsx theme={null}
// Radix (not supported)
<SelectTrigger asChild>
  <CustomButton>Open</CustomButton>
</SelectTrigger>

// Base UI (correct)
<SelectTrigger render={<CustomButton />}>
  Open
</SelectTrigger>
```

If you don't provide `render`, `SelectTrigger` renders a `<button>` by default.

### Polymorphic components

Some components (triggers, items) accept a `render` prop to change the element:

```tsx theme={null}
<DropdownMenuTrigger render={<a href="#" />}>Menu Link</DropdownMenuTrigger>
```

## Form with react-hook-form

Forms use `react-hook-form` and `@hookform/resolvers` for validation:

```tsx theme={null}
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormControl,
  FormMessage,
} from '@ekanos/ui/form';
import { Input } from '@ekanos/ui/input';
import { Button } from '@ekanos/ui/button';

const schema = z.object({
  email: z.string().email(),
});

export function LoginForm() {
  const form = useForm({
    resolver: zodResolver(schema),
    defaultValues: { email: '' },
  });
  const onSubmit = (values: z.infer<typeof schema>) => console.log(values);

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input type="email" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Sign in</Button>
      </form>
    </Form>
  );
}
```

`FormMessage` displays the field's validation error. `zod` here is your own dependency; `@ekanos/ui` does not ship it.

## Type imports

Some entries export named prop types (`ButtonProps` from `button`, `BadgeProps` from `badge`, the `PromptInput*Props` types from `ai-prompt-input`). For the rest, use `React.ComponentProps<typeof Component>`:

```ts theme={null}
import type { ComponentProps } from 'react';

import type { ButtonProps } from '@ekanos/ui/button';
import type { Input } from '@ekanos/ui/input';

type InputProps = ComponentProps<typeof Input>;
```

Props extend the underlying element's props, so standard attributes work:

```tsx theme={null}
<Button type="submit" className="custom" disabled />
<Input aria-label="Search" placeholder="..." />
```

## Peer dependencies

You provide these (`react-hook-form` and `@hookform/resolvers` for form usage, `next-intl` for `Trans`):

```json theme={null}
{
  "react": "^19.2.8",
  "react-dom": "^19.2.8",
  "react-hook-form": "^7.68.0",
  "@hookform/resolvers": "^5.2.2",
  "next-intl": "^4.14.7"
}
```

The package declares:

* `@base-ui/react` — the primitives (Base UI, not Radix)
* `ai` — used for types only (`ChatStatus`, `FileUIPart` in `PromptInput`'s props); no `ai` code is imported at runtime
* `class-variance-authority` — variant management
* `clsx` + `tailwind-merge` — class merging
* `cmdk` — command palette/combobox
* `nanoid` — unique IDs

Everything else the components need at runtime is inlined into the package's `dist/`.

## `'use client'` in Next.js App Router

Client entries keep their directive in the published bundle (the build re-applies it per entry), so Next.js treats them as client components:

```tsx theme={null}
// app/page.tsx (server component)
import { ClientWidget } from './client-widget';

export default function Home() {
  return <ClientWidget />;
}
```

```tsx theme={null}
// app/client-widget.tsx
'use client';

import { useState } from 'react';

import { Button } from '@ekanos/ui/button';

export function ClientWidget() {
  const [count, setCount] = useState(0);
  return <Button onClick={() => setCount(count + 1)}>{count}</Button>;
}
```

## Per-component entries

Each component is its own subpath with its own bundle, and the package declares `"sideEffects": false`, so importing `@ekanos/ui/button` does not pull in `@ekanos/ui/select`. There is no root barrel export. Entries that render icons (for example `select` and `dropdown-menu`) share the one `icon` bundle, so a single `IconStyleOverrideProvider` reaches every icon.

### Base UI patch divergence (minor)

The Ekanos monorepo patches `@base-ui/react` to widen a trigger drag-boundary offset in menu, select, and combobox triggers. The published package resolves the unpatched package from npm, so partner-side menus keep upstream's slightly tighter press-and-drag selection boundary. Cosmetic only.

## CSS-in-JS vs. Tailwind

All components emit static Tailwind class strings, not CSS-in-JS: no styles are generated or injected at render time. The tradeoff is that you must load either `styles.css` (pre-compiled) or `theme.css` in your Tailwind build so the utilities exist. See [Styling](/ui/styling).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.