# Sutra UI Usage Rules for LLMs

This document provides guidelines for AI assistants when working with the Sutra UI component library.

For application-level composition, start with `SKILL.md`, then use
`cheatsheets/components.cheatmd` for component examples and
`cheatsheets/forms.cheatmd` for form patterns. Use this file when editing
Sutra UI internals, adding components, or reviewing implementation quality.

## Overview

Sutra UI is a pure Phoenix LiveView UI component library with no external JavaScript/npm dependencies. Components use colocated JavaScript hooks where interactivity is needed; most are runtime hooks, with extracted exceptions documented in the hook guide.

**Requirements:**
- Phoenix 1.8+ (for colocated hooks)
- Phoenix LiveView 1.2+
- Tailwind CSS v4 (CSS-first configuration)

## Core Principles

### Reference Order

- Start with `SKILL.md` for Sutra's design philosophy and reference routing.
- Use `skill/patterns.md` for API selection and composition rules.
- Use `skill/recipes.md` for common full-interface compositions.
- Use `cheatsheets/components.cheatmd` and `cheatsheets/forms.cheatmd` for
  compact examples.
- Inspect `lib/sutra_ui/*.ex` before relying on exact attrs, slots, events, or
  helper function names.

### 1. CSS-First Styling

All component styling is defined in `priv/static/sutra_ui.css`. When modifying components:

- **DO**: Use CSS classes from `sutra_ui.css`
- **DO**: Add new CSS classes to `sutra_ui.css` when needed
- **DON'T**: Add inline Tailwind classes directly in component templates
- **DON'T**: Use helper functions to generate Tailwind class strings

**Pattern for class attributes:**
```elixir
# Good - uses CSS class with optional user override
class={["component-class", @class]}

# Avoid - inline Tailwind
class={["flex items-center gap-2", @class]}
```

### 2. Required ID Attributes

Components that require JavaScript hooks or stable DOM references expose an `id`
attribute. Hook-only components generally require it; progressive-enhancement
components may enable extra behavior only when an `id` is provided:

```elixir
# Good - explicit required ID
attr(:id, :string, required: true, doc: "Unique identifier for the component")

# Avoid - auto-generated IDs
attr(:id, :string, default: fn -> "component-#{System.unique_integer()}" end)
```

**Rationale:**
- Predictable IDs aid debugging
- Prevents LiveView diffing issues on reconnect
- User maintains control over their DOM

### 3. Event Naming Convention

Custom JavaScript events use the `sutra-ui:` namespace:

```javascript
// Good
this.el.dispatchEvent(new CustomEvent('sutra-ui:component-action', { detail: {...} }))

// Avoid
this.el.dispatchEvent(new CustomEvent('component-action', { detail: {...} }))
```

### 4. Attribute Types

Use appropriate attribute types:

| Attribute | Type | Notes |
|-----------|------|-------|
| `class` | `:string` or `:any` | Prefer `:string`; use `:any` when list-style class values are intentional |
| `id` | `:string` | Required when hook behavior needs a stable DOM target; optional only for progressive enhancements |
| `disabled` | `:boolean` | Boolean attributes |
| `value` | `:string` or `:any` | Depends on component needs |
| `errors` | `:list` or `:map` | Lists for field messages; maps for keyed workflows like steppers |

### 5. Global Attributes

Use `:global` for attributes passed through to rendered HTML. Phoenix already
accepts all `phx-*`, `data-*`, and `aria-*` attributes on `:global`; the
`include:` option only adds extra allowed attributes.

```elixir
attr(:rest, :global, include: ~w(form type name value), doc: "Additional HTML attributes")
```

```heex
<.button phx-click="select_page" phx-value-page="2" data-testid="next-page">
  Next
</.button>
```

Do not treat examples like `phx-value-id` as a whitelist. Any `phx-value-*`
payload supported by Phoenix LiveView can be passed through component globals.

### 6. Slot Conventions

Slots should follow consistent patterns:

```elixir
# Simple content slot
slot(:inner_block, doc: "Main content")

# Named slot with attributes
slot :item, doc: "List items" do
  attr(:value, :string, required: true)
  attr(:disabled, :boolean)
end
```

### 7. Icons

Sutra UI does not export a general icon component or bundled icon set. Library
components use inline SVG only where they own the icon.

- **DO** use a host application's existing icon helper if it has one.
- **DO** use inline SVG for local examples when no app helper exists.
- **DON'T** invent `SutraUI.Icon`, `icon/1`, `icon_name`, or a new icon package.
- **DON'T** copy the demo app's `<.icon name="lucide-...">` helper into generic
  library guidance unless the target app provides it.

Icon-only buttons and controls must have an accessible name such as
`aria-label`.

## Component Patterns

### Basic Component Structure

```elixir
defmodule SutraUI.ComponentName do
  @moduledoc """
  Brief description of the component.

  ## Examples

      <.component_name id="my-component" required_attr="value">
        Content here
      </.component_name>

  ## Accessibility

  - List ARIA attributes used
  - Keyboard navigation support
  - Screen reader considerations
  """

  use Phoenix.Component

  @doc """
  Renders the component.
  """
  attr(:id, :string, required: true, doc: "Unique identifier")
  attr(:class, :string, default: nil, doc: "Additional CSS classes")
  attr(:rest, :global, doc: "Additional HTML attributes")

  slot(:inner_block, required: true, doc: "Main content")

  def component_name(assigns) do
    ~H"""
    <div id={@id} class={["component-class", @class]} {@rest}>
      {render_slot(@inner_block)}
    </div>
    """
  end
end
```

### Hook-Based Component Structure

```elixir
defmodule SutraUI.InteractiveComponent do
  use Phoenix.Component
  alias Phoenix.LiveView.ColocatedHook

  attr(:id, :string, required: true, doc: "Required for JavaScript hook")
  # ... other attrs

  def interactive_component(assigns) do
    ~H"""
    <div id={@id} class="component-class" phx-hook=".ComponentHook" data-option={@option}>
      {render_slot(@inner_block)}
    </div>

    <script :type={ColocatedHook} name=".ComponentHook" runtime>
      {
        mounted() {
          // Initialize component
        },
        updated() {
          // Handle LiveView updates
        },
        destroyed() {
          // Cleanup
        }
      }
    </script>
    """
  end
end
```

Most Sutra UI hooks are runtime colocated hooks. Extracted hook exceptions must
document the generated `phoenix-colocated/sutra_ui` LiveSocket import.

## Available Components

### Foundation
- `button/1` - Buttons with variants: primary, secondary, outline, ghost, link, destructive
- `badge/1` - Status badges
- `spinner/1` - Loading indicators
- `kbd/1` - Keyboard shortcut display

### Form Controls
- `label/1` - Form labels
- `input/1` - Unified form fields with label, description, native select, and error support
- `textarea/1` - Multi-line text input
- `checkbox/1` - Checkbox input
- `switch/1` - Toggle switch
- `radio_group/1`, `radio/1` - Radio button groups
- `simple_form/1` - Styled plain form shell; use Phoenix `<.form class="form">` for changeset-backed LiveView forms
- `select/1` - Custom select dropdown (JS hook)
- `slider/1` - Range slider (JS hook)
- `range_slider/1` - Dual-handle range slider (JS hook)
- `SutraUI.LiveSelect` - LiveComponent async searchable select; render with `<.live_component module={SutraUI.LiveSelect} ... />`
- `input_otp/1` - One-time password input (JS hook)
- `file_upload/1` - LiveView upload dropzone configured with `allow_upload/3`

### Layout & Data Display
- `card/1` - Card container with header/content/footer slots
- `header/1` - Page header with title/subtitle/actions
- `table/1` - Raw styled table wrapper
- `data_table/1` - Data table with column definitions
- `skeleton/1` - Loading placeholder
- `empty/1` - Empty state display
- `alert/1` - Alert/callout messages
- `flash/1` - Phoenix flash messages
- `progress/1` - Progress bar
- `stepper/1` - Multi-step progress indicator
- `stepper_wizard/1` - Multi-step wizard shell for large forms and flows
- `timeline/1` - Chronological event list with item slots
- `tree_view/1`, `tree_item/1` - Hierarchical tree navigation (`id` enables keyboard hook)

### Navigation & Interactive
- `breadcrumb/1` - Breadcrumb navigation
- `pagination/1` - Page navigation
- `accordion/1` - Collapsible content sections
- `tabs/1` - Tab panels (JS hook)
- `dropdown_menu/1` - Dropdown menu (JS hook)
- `context_menu/1` - Right-click menu (JS hook)
- `toast_container/1`, `toast/1` - Toast notifications (JS hook)

### Advanced UI
- `avatar/1` - User avatars with fallback
- `tooltip/1` - Hover and focus tooltip (JS hook)
- `hover_card/1` - Rich hover preview (JS hook)
- `dialog/1` - Modal dialogs
- `popover/1` - Click-triggered popups
- `command/1` - Command palette with search (JS hook)
- `carousel/1` - CSS scroll-snap carousel
- `calendar/1` - Monthly calendar grid
- `marquee/1` - CSS-only scrolling content banner
- `separator/1` - Visual or semantic divider

### AI Primitives
- `response/1` - Text responses with optional reveal styles, or streamed Markdown
- `activity/1` - Safe user-facing agent progress with slot-owned row content

### Layout Helpers
- `filter_bar/1` - Filter bar for index pages
- `input_group/1` - Input with prefix/suffix
- `item/1` - Versatile list item
- `loading_state/1` - Loading indicator with message

### Navigation
- `drawer/1` - Collapsible drawer navigation
- `tab_nav/1` - Responsive server-side routed tab navigation
- `theme_switcher/1` - Light/dark theme event button

## CSS Class Naming

CSS classes in `sutra_ui.css` follow these conventions:

```css
/* Component base */
.component-name { }

/* Variants */
.component-name-variant { }

/* States */
.component-name-disabled { }
.component-name-active { }

/* Sub-elements */
.component-name-header { }
.component-name-content { }
.component-name-footer { }
```

## Testing Components

Tests are in `test/sutra_ui/`. Most existing tests use `ExUnit.Case` with
`Phoenix.LiveViewTest.rendered_to_string/1`; `SutraUI.ComponentCase` is
available when its helpers are useful:

```elixir
defmodule SutraUI.ComponentNameTest do
  use SutraUI.ComponentCase, async: true

  import SutraUI.ComponentName

  describe "component_name/1" do
    test "renders with required attributes" do
      html = render_component(&component_name/1, id: "test", required: "value")
      assert html =~ ~s(id="test")
    end

    test "applies custom class" do
      html = render_component(&component_name/1, id: "test", class: "custom")
      assert html =~ "custom"
    end
  end
end
```

## Common Mistakes to Avoid

1. **Missing required `id`** on hook-based components
2. **Inline Tailwind** instead of CSS classes
3. **Auto-generating IDs** instead of requiring them
4. **Missing accessibility attributes** (ARIA, roles, keyboard support)
5. **Not updating moduledoc examples** when changing required attrs
6. **Forgetting to add new components** to `lib/sutra_ui.ex` imports

## File Structure

```
lib/
  sutra_ui/
    component_name.ex    # Component module
  sutra_ui.ex            # Main module with imports
priv/
  static/
    sutra_ui.css         # All component styles
test/
  sutra_ui/
    component_name_test.exs
  support/
    component_case.ex    # Test helpers
```

## Adding a New Component

1. Create `lib/sutra_ui/component_name.ex` with moduledoc and examples
2. Add CSS classes to `priv/static/sutra_ui.css`
3. Add import to `lib/sutra_ui.ex` in `__using__` macro
4. Update component list in `lib/sutra_ui.ex` moduledoc
5. Create tests in `test/sutra_ui/component_name_test.exs`
6. Update this file if the component introduces new patterns

## Common Recipes

### Modal Dialog with Form

```heex
<%!-- Server-controlled dialog (recommended) --%>
<.dialog id="edit-user-dialog" show={@show_edit_dialog} on_cancel="close_edit_dialog">
  <:title>Edit User</:title>
  <:description>Update user information below.</:description>

  <.form for={@form} class="form" phx-submit="save_user">
    <.input field={@form[:name]} label="Name" />
    <.input field={@form[:email]} label="Email" type="email" />

    <div class="flex justify-end gap-2">
      <.button variant="outline" phx-click="close_edit_dialog">
        Cancel
      </.button>
      <.button type="submit">Save Changes</.button>
    </div>
  </.form>
</.dialog>

<.button phx-click="open_edit_dialog">
  Edit User
</.button>
```

```elixir
# In your LiveView
def handle_event("open_edit_dialog", _, socket) do
  {:noreply, assign(socket, show_edit_dialog: true)}
end

def handle_event("close_edit_dialog", _, socket) do
  {:noreply, assign(socket, show_edit_dialog: false)}
end

def handle_event("save_user", params, socket) do
  case save_user(params) do
    {:ok, _user} ->
      {:noreply, assign(socket, show_edit_dialog: false)}
    {:error, changeset} ->
      {:noreply, assign(socket, form: to_form(changeset))}
  end
end
```

### Data Table with Pagination

```heex
<.data_table rows={@users}>
  <:col :let={user} label="Name">{user.name}</:col>
  <:col :let={user} label="Email">{user.email}</:col>
  <:col :let={user} label="Status">
    <.badge variant={status_variant(user.status)}>{user.status}</.badge>
  </:col>
  <:action :let={user}>
    <.dropdown_menu id={"user-actions-#{user.id}"}>
      <:trigger>Actions</:trigger>
      <.dropdown_item><.link navigate={~p"/users/#{user.id}"}>View</.link></.dropdown_item>
      <.dropdown_item><.link navigate={~p"/users/#{user.id}/edit"}>Edit</.link></.dropdown_item>
      <.dropdown_separator />
      <.dropdown_item variant="destructive" phx-click="delete_user" phx-value-id={user.id}>
        Delete
      </.dropdown_item>
    </.dropdown_menu>
  </:action>
</.data_table>

<.pagination
  page={@page}
  total_pages={@total_pages}
  on_page_change="page_changed"
/>
```

### Form with Validation Errors

```heex
<.form for={@form} class="form" phx-change="validate" phx-submit="save">
  <.input field={@form[:name]} label="Name" required />

  <.input field={@form[:email]} type="email" label="Email" required />

  <.input
    field={@form[:role]}
    type="select"
    label="Role"
    options={[{"Administrator", "admin"}, {"Standard User", "user"}, {"Viewer", "viewer"}]}
  />

  <.button type="submit" loading={@saving}>Save</.button>
</.form>
```

### Confirmation Dialog Pattern

```heex
<.dialog
  id="confirm-delete-dialog"
  show={@show_delete_dialog}
  on_cancel="cancel_delete"
>
  <:title>Delete Item</:title>
  <:description>This action cannot be undone. Are you sure?</:description>
  <:footer>
    <.button variant="outline" phx-click="cancel_delete">
      Cancel
    </.button>
    <.button variant="destructive" phx-click="delete_confirmed">
      Delete
    </.button>
  </:footer>
</.dialog>
```

```elixir
# In your LiveView
def handle_event("confirm_delete", %{"id" => id}, socket) do
  {:noreply, assign(socket, delete_target_id: id, show_delete_dialog: true)}
end

def handle_event("cancel_delete", _, socket) do
  {:noreply, assign(socket, show_delete_dialog: false)}
end

def handle_event("delete_confirmed", _, socket) do
  MyApp.delete_item(socket.assigns.delete_target_id)

  {:noreply,
   socket
   |> assign(show_delete_dialog: false)
   |> put_flash(:info, "Item deleted")
   |> push_navigate(to: ~p"/items")}
end
```

### Tabs with Dynamic Content

```heex
<.tabs id="content-tabs" default_value="overview">
  <:tab value="overview">Overview</:tab>
  <:tab value="activity">Activity</:tab>
  <:tab value="settings">Settings</:tab>
  
  <:panel value="overview">
    <.card>
      <:header>
        <h2>Overview</h2>
      </:header>
      <:content>
        <p>Overview content here...</p>
      </:content>
    </.card>
  </:panel>
  
  <:panel value="activity">
    <.card>
      <:header>
        <h2>Recent Activity</h2>
      </:header>
      <:content>
        <ul>
          <.item :for={activity <- @activities}>
            <:title>{activity.description}</:title>
          </.item>
        </ul>
      </:content>
    </.card>
  </:panel>
  
  <:panel value="settings">
    <.form for={@settings_form} class="form" phx-submit="save_settings">
      <.input field={@settings_form[:notifications]} type="switch" label="Enable notifications" />
      <.input field={@settings_form[:dark_mode]} type="switch" label="Dark mode" />
    </.form>
  </:panel>
</.tabs>
```

### Toast Notifications from LiveView

```elixir
# Show toast on successful action
def handle_event("save", params, socket) do
  case save_data(params) do
    {:ok, _record} ->
      {:noreply,
       socket
       |> put_flash(:info, "Changes saved successfully")}
    
    {:error, changeset} ->
      {:noreply, assign(socket, :form, to_form(changeset))}
  end
end

# Or use push_event for more control
def handle_event("export_complete", _, socket) do
  {:noreply,
   push_event(socket, "toast", %{
     variant: "success",
     title: "Export Complete",
     description: "Your data has been exported to CSV.",
     duration: 5000
   })}
end
```

## Troubleshooting

### Component hooks not working

**Symptoms:** Hook-based components (Select, Dialog, Tabs, etc.) don't respond to interactions.

**Solutions:**

1. **Check Phoenix version** - Colocated hooks require Phoenix 1.8+:
   ```elixir
   # mix.exs
   {:phoenix, "~> 1.8"}
   ```

2. **Check component ID** - many hook-based components require unique IDs:
   ```heex
   <%# Wrong - missing ID %>
   <.select name="country">
     <.select_option value="us" label="United States" />
   </.select>
   
   <%# Correct %>
   <.select id="country-select" name="country">
     <.select_option value="us" label="United States" />
   </.select>
   ```

3. **Check rendered hook markup** - Runtime hooks are emitted with the
   component. If the hook script is missing, recompile the app:
   ```bash
   mix compile --force
   ```

### CSS styles not applied

**Symptoms:** Components render but look unstyled or broken.

**Solutions:**

1. **Check Tailwind v4 setup** - Verify `@source` directive in `app.css`:
   ```css
   @import "tailwindcss";
   @source "../../deps/sutra_ui/lib";
   @import "../../deps/sutra_ui/priv/static/sutra_ui.css";
   ```

2. **Check import order** - Sutra UI CSS must come after Tailwind:
   ```css
   @import "tailwindcss";
   @import "../../deps/sutra_ui/priv/static/sutra_ui.css";
   /* Your overrides come last */
   ```

3. **Clear cache** - After changing CSS config:
   ```bash
   mix assets.clean
   mix phx.server
   ```

### Form values not updating

**Symptoms:** Select or other form controls don't update the form value.

**Solutions:**

1. **Check name attribute** - Ensure `name` matches the form field:
   ```heex
   <.select id="role-select" name={@form[:role].name} value={@form[:role].value}>
     <.select_option value="admin" label="Administrator" />
   </.select>
   ```

2. **Check phx-change** - Form needs `phx-change` for live validation:
   ```heex
   <.form for={@form} class="form" phx-change="validate" phx-submit="save">
   ```

### Dialog not opening/closing

**Symptoms:** Dialog doesn't show or hide properly.

**Solutions:**

1. **Use server-controlled pattern (recommended)**:
   ```heex
   <%!-- Control via show attribute and on_cancel event --%>
   <.dialog id="my-dialog" show={@show_dialog} on_cancel="close_dialog">
     ...
   </.dialog>

   <.button phx-click="open_dialog">Open</.button>
   ```

   ```elixir
   def handle_event("open_dialog", _, socket) do
     {:noreply, assign(socket, show_dialog: true)}
   end

   def handle_event("close_dialog", _, socket) do
     {:noreply, assign(socket, show_dialog: false)}
   end
   ```

2. **Or use JS commands** - If using `show_dialog`/`hide_dialog`:
   ```heex
   <%!-- Use correct module path --%>
   <.button phx-click={SutraUI.Dialog.show_dialog("my-dialog")}>

   <%!-- ID must match exactly --%>
   <.dialog id="my-dialog" on_cancel="close">
   ```

3. **Check on_cancel handler** - Close button only renders when `on_cancel` is set

### LiveView disconnects on component interaction

**Symptoms:** Page refreshes or socket disconnects when clicking components.

**Solutions:**

1. **Check event handlers** - Ensure `phx-click` handlers exist in LiveView:
   ```elixir
   def handle_event("my_action", params, socket) do
     {:noreply, socket}
   end
   ```

2. **Prevent default on links** - Use `phx-click` instead of `onclick` for LiveView events.

### Theme variables not working

**Symptoms:** Custom CSS variables are ignored.

**Solutions:**

1. **Check variable syntax** - Sutra UI defaults use OKLCH; other valid CSS
   color formats also work if they give sufficient contrast:
   ```css
   /* Valid, but less consistent with the default palette */
   --primary: #3b82f6;
   
   /* Preferred for Sutra UI themes */
   --primary: oklch(0.623 0.214 259.815);
   ```

2. **Check override order** - Custom variables must come after Sutra UI CSS:
   ```css
   @import "../../deps/sutra_ui/priv/static/sutra_ui.css";
   
   :root {
     --primary: oklch(0.65 0.20 145); /* This overrides */
   }
   ```

### Component tests failing

**Symptoms:** Tests can't find components or hooks.

**Solutions:**

1. **Import correctly** - Test files need explicit imports:
   ```elixir
   defmodule SutraUI.ButtonTest do
     use SutraUI.ComponentCase, async: true
     import SutraUI.Button
   ```

2. **Use render_component** - Not `render`:
   ```elixir
   html = render_component(&button/1, variant: "primary")
   ```

## Migration Notes

### From Phoenix 1.7 to 1.8+

1. Update dependencies in `mix.exs`
2. Remove manual hook registrations from old `hooks.js` files
3. Keep generated `phoenix-colocated/sutra_ui` imports when using extracted hooks

### From Tailwind v3 to v4

1. Replace `tailwind.config.js` with `@source` directives in CSS
2. Update color values from HSL to OKLCH if customizing theme
3. Update any custom CSS using Tailwind's `@apply` with new syntax
