README.md

# AvenUI

A free, open-source UI component library for **Phoenix LiveView** — the missing shadcn/ui for the Elixir ecosystem.

> **Free alternative to Petal.** MIT licensed. You own your components.

---

## Why AvenUI?

- **Zero JS by default** — every component works with pure LiveView server rendering
- **shadcn-style installer** — `mix aven_ui.add button badge` copies code into your project so you own it
- **Full dark mode** — CSS variable tokens, no Tailwind class flicker
- **LiveView streams** — Table and lists support `phx-update="stream"` out of the box
- **Form-native** — Input and Select integrate with `Phoenix.HTML.FormField` + Ecto changesets
- **Accessible** — ARIA roles, keyboard navigation, focus management built in
- **Tailwind v3 + v4** compatible

---

## Installation

### 1. Add dep

```elixir
# mix.exs
def deps do
  [
    {:aven_ui, "~> 0.1", github: "khemmanat/aven_ui"}
  ]
end
```

### 2. Install components

```bash
mix deps.get

# Add specific components
mix aven_ui.add button badge alert card input tabs modal

# Or add everything
mix aven_ui.add --all

# Dry run — see what would be copied
mix aven_ui.add --all --dry-run
```

Components are copied into `lib/my_app_web/components/ui/` with the namespace
rewritten to `MyAppWeb.UI.*`. You own the code.

### 3. Import in web.ex

```elixir
# lib/my_app_web.ex
defp html_helpers do
  quote do
    use AvenUI, :components
    # or cherry-pick:
    # import MyAppWeb.UI.Button
    # import MyAppWeb.UI.Badge
  end
end
```

### 4. Add CSS

```css
/* assets/css/app.css */
@import "./avenui.css";
```

### 5. Add JS hooks

```js
// assets/js/app.js
import { AvenUIHooks } from "./hooks/aven_ui";

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { ...AvenUIHooks, ...YourHooks },
});
```

### 6. Add Tailwind preset

```js
// assets/tailwind.config.js
const avenUIPreset = require("../../deps/aven_ui/assets/tailwind.config.js");

module.exports = {
  presets: [avenUIPreset],
  content: ["./lib/**/*.{ex,heex}", "./assets/js/**/*.js"],
};
```

### 7. Add flash toasts to root layout

```heex
<%!-- lib/my_app_web/components/layouts/root.html.heex --%>
<body>
  <%= @inner_content %>
  <.flash_group flash={@flash} />
</body>
```

---

## Project structure

```
aven_ui/
├── mix.exs
├── lib/
│   ├── aven_ui.ex                        ← use AvenUI, :components macro
│   └── aven_ui/
│       ├── helpers.ex                    ← shared classes/1 utility
│       ├── components/
│       │   ├── accordion.ex
│       │   ├── alert.ex
│       │   ├── avatar.ex
│       │   ├── badge.ex
│       │   ├── button.ex
│       │   ├── card.ex
│       │   ├── code_block.ex
│       │   ├── dropdown.ex
│       │   ├── empty_state.ex
│       │   ├── input.ex
│       │   ├── kbd.ex
│       │   ├── modal.ex
│       │   ├── progress.ex
│       │   ├── separator.ex
│       │   ├── skeleton.ex
│       │   ├── spinner.ex
│       │   ├── stat.ex
│       │   ├── table.ex
│       │   ├── tabs.ex
│       │   ├── toast.ex
│       │   └── toggle.ex
│       └── mix/tasks/
│           └── add.ex                    ← mix aven_ui.add
├── assets/
│   ├── css/avenui.css                    ← --avn- CSS design tokens
│   ├── js/hooks/index.js                 ← AvenUIHooks
│   └── tailwind.config.js
└── storybook/                            ← live component docs (Phoenix app)
```

---

## Components

### Button

```heex
<.button>Deploy</.button>
<.button variant="secondary" size="sm">Cancel</.button>
<.button variant="danger" phx-click="delete" phx-value-id={@id}>Delete</.button>
<.button loading={@saving}>Saving…</.button>
```

**Variants:** `primary` `secondary` `ghost` `danger` `outline` `link`  
**Sizes:** `xs` `sm` `md` `lg` `xl`

---

### Badge

```heex
<.badge>Default</.badge>
<.badge variant="success"><.badge_dot /> Online</.badge>
<.badge variant="danger">Expired</.badge>
```

**Variants:** `default` `primary` `success` `warning` `danger` `info` `outline`

---

### Alert

```heex
<.alert variant="success" title="Deployed!">Version 2.4.1 is live.</.alert>
<.alert variant="warning" title="High memory" dismissible phx-click="dismiss">
  Node #3 is at 87%.
</.alert>
```

---

### Input + Select

```heex
<.input field={@form[:email]} type="email" label="Email" hint="We'll never share this." />

<.input field={@form[:amount]} label="Amount">
  <:prefix>฿</:prefix>
</.input>

<.select field={@form[:region]} label="Region"
  options={["Bangkok", "Singapore", "Tokyo"]} prompt="Choose…" />
```

---

### Card

```heex
<.card>
  <:header>
    <.card_title>Server #3</.card_title>
    <.card_description>Last ping 2s ago</.card_description>
  </:header>
  <:body>
    <.progress value={72} label="CPU" show_value />
  </:body>
  <:footer>
    <.button size="sm">Restart</.button>
  </:footer>
</.card>
```

---

### Modal

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

<.modal :if={@show_modal} id="confirm-modal" on_close="close_modal">
  <:title>Delete project?</:title>
  <:description>This cannot be undone.</:description>
  <p>All data will be permanently deleted.</p>
  <:footer>
    <.button variant="ghost" phx-click="close_modal">Cancel</.button>
    <.button variant="danger" phx-click="confirm_delete">Delete</.button>
  </:footer>
</.modal>
```

In LiveView:

```elixir
def handle_event("open_modal",  _, socket), do: {:noreply, assign(socket, show_modal: true)}
def handle_event("close_modal", _, socket), do: {:noreply, assign(socket, show_modal: false)}
```

---

### Dropdown

```heex
<.dropdown id="actions-menu">
  <:trigger>
    <.button variant="secondary" size="sm">Options <.dropdown_chevron /></.button>
  </:trigger>
  <.dropdown_label>Actions</.dropdown_label>
  <.dropdown_item phx-click="edit">Edit</.dropdown_item>
  <.dropdown_separator />
  <.dropdown_item variant="danger" phx-click="delete">Delete</.dropdown_item>
</.dropdown>
```

---

### Tabs

```heex
<.tabs active={@tab} patch="/dashboard" param="tab">
  <:tab id="overview">Overview</:tab>
  <:tab id="settings">Settings</:tab>
  <:panel id="overview"><.overview /></:panel>
  <:panel id="settings"><.settings_form /></:panel>
</.tabs>
```

**Variants:** `underline` `pills` `boxed`

---

### Table

```heex
<.table rows={@deployments} sort_field={@sort_by} sort_dir={@sort_dir}>
  <:col :let={row} label="Commit" field="sha" sortable>
    <code class="font-mono text-xs"><%= row.sha %></code>
  </:col>
  <:col :let={row} label="Status">
    <.badge variant={status_color(row.status)}><%= row.status %></.badge>
  </:col>
  <:action :let={row}>
    <.button size="xs" variant="ghost" phx-click="restart" phx-value-id={row.id}>
      Restart
    </.button>
  </:action>
</.table>

<.pagination page={@page} total_pages={@total_pages} phx-click="paginate" />
```

---

### Toast / Flash

```heex
<%!-- In root.html.heex — renders all @flash messages as toasts --%>
<.flash_group flash={@flash} />
```

From LiveView:

```elixir
socket |> put_flash(:success, "Deployment complete!")
socket |> put_flash(:error,   "Connection failed.")
```

---

### Accordion

```heex
<.accordion id="faq">
  <:item title="Is AvenUI free?">Yes. MIT licensed.</:item>
  <:item title="Does it support dark mode?" open>
    Yes — via CSS variables, no Tailwind class flicker.
  </:item>
</.accordion>
```

---

### Avatar

```heex
<.avatar initials="KN" />
<.avatar initials="KN" size="lg" color="green" />
<.avatar src="https://..." alt="Khemmanat" />

<.avatar_group>
  <.avatar initials="KN" />
  <.avatar initials="AB" color="amber" />
  <.avatar initials="+3" color="gray" />
</.avatar_group>
```

---

### Stat

```heex
<div class="grid grid-cols-3 gap-4">
  <.stat label="Deploys today" value="24"    change="+8"    trend="up" />
  <.stat label="Avg response"  value="142"   suffix="ms"    change="+12ms" trend="down" />
  <.stat label="Uptime (30d)"  value="99.97" suffix="%" />
</div>
```

---

### Utility components

```heex
<.progress value={72} label="Storage" show_value color="blue" />
<.skeleton class="h-4 w-48" />
<.spinner size="lg" class="text-avn-purple" />
<.separator label="or continue with" />
<.kbd>⌘</.kbd><.kbd>K</.kbd>
<.empty_state title="No results" description="Try a different search." />
<.code_block lang="elixir" copyable>def hello, do: "world"</.code_block>
```

---

## JS Hooks

| Hook              | Purpose                           |
| ----------------- | --------------------------------- |
| `Dropdown`        | Keyboard nav, outside-click close |
| `Modal`           | Focus trap, scroll lock, Escape   |
| `Tooltip`         | Position-aware tooltip            |
| `Flash`           | Auto-dismiss with pause-on-hover  |
| `AutoResize`      | Growing textarea                  |
| `CopyToClipboard` | Clipboard API with feedback       |
| `InfiniteScroll`  | Load-more on sentinel visible     |
| `ScrollTop`       | Smooth scroll on LiveView patch   |

```heex
<%!-- Tooltip usage --%>
<button phx-hook="Tooltip" id="info-btn" data-tooltip="More info">?</button>

<%!-- Dropdown usage --%>
<div phx-hook="Dropdown" id="my-menu">
  <button data-avn-dropdown-trigger>Open</button>
  <div data-avn-dropdown-menu hidden>
    <button data-avn-dropdown-item>Edit</button>
  </div>
</div>
```

---

## Storybook

Run the interactive component doc site locally:

```bash
cd storybook
mix deps.get
mix phx.server
# Open http://localhost:4000
```

---

## Roadmap

- [ ] Command Palette / Combobox
- [ ] Drawer / Slideout panel
- [ ] Date Picker
- [ ] Chart hooks (VegaLite + Chart.js)
- [ ] Multi-select
- [ ] File upload
- [ ] Publish to Hex.pm

---

## License

MIT — free to use, modify, and distribute.