diff --git a/.claude/skills/ui-framework/SKILL.md b/.claude/skills/ui-framework/SKILL.md
new file mode 100644
index 00000000..48cb12ee
--- /dev/null
+++ b/.claude/skills/ui-framework/SKILL.md
@@ -0,0 +1,58 @@
+---
+name: ui-framework
+description: "Use this skill when working on the web layer and UI components using the Phoenix framework"
+metadata:
+ managed-by: usage-rules
+---
+
+
+## Additional References
+
+- [ecto](references/ecto.md)
+- [elixir](references/elixir.md)
+- [html](references/html.md)
+- [liveview](references/liveview.md)
+- [phoenix](references/phoenix.md)
+- [phoenix_ecto](references/phoenix_ecto.md)
+- [phoenix_html](references/phoenix_html.md)
+- [phoenix_html_helpers](references/phoenix_html_helpers.md)
+- [phoenix_live_dashboard](references/phoenix_live_dashboard.md)
+- [phoenix_live_reload](references/phoenix_live_reload.md)
+- [phoenix_live_view](references/phoenix_live_view.md)
+- [fluxon](references/fluxon.md)
+
+## Searching Documentation
+
+```sh
+mix usage_rules.search_docs "search term" -p phoenix -p phoenix_ecto -p phoenix_html -p phoenix_html_helpers -p phoenix_live_dashboard -p phoenix_live_reload -p phoenix_live_view -p fluxon
+```
+
+## Available Mix Tasks
+
+- `mix compile.phoenix`
+- `mix phx` - Prints Phoenix help information
+- `mix phx.digest` - Digests and compresses static files
+- `mix phx.digest.clean` - Removes old versions of static assets.
+- `mix phx.gen` - Lists all available Phoenix generators
+- `mix phx.gen.auth` - Generates authentication logic for a resource
+- `mix phx.gen.auth.hashing_library`
+- `mix phx.gen.auth.injector`
+- `mix phx.gen.auth.migration`
+- `mix phx.gen.cert` - Generates a self-signed certificate for HTTPS testing
+- `mix phx.gen.channel` - Generates a Phoenix channel
+- `mix phx.gen.context` - Generates a context with functions around an Ecto schema
+- `mix phx.gen.embedded` - Generates an embedded Ecto schema file
+- `mix phx.gen.html` - Generates context and controller for an HTML resource
+- `mix phx.gen.json` - Generates context and controller for a JSON resource
+- `mix phx.gen.live` - Generates LiveView, templates, and context for a resource
+- `mix phx.gen.notifier` - Generates a notifier that delivers emails by default
+- `mix phx.gen.presence` - Generates a Presence tracker
+- `mix phx.gen.release` - Generates release files and optional Dockerfile for release-based deployments
+- `mix phx.gen.schema` - Generates an Ecto schema and migration file
+- `mix phx.gen.secret` - Generates a secret
+- `mix phx.gen.socket` - Generates a Phoenix socket handler
+- `mix phx.routes` - Prints all routes
+- `mix phx.server` - Starts applications and their servers
+- `mix compile.phoenix_live_view`
+- `mix phoenix_live_view.upgrade`
+
diff --git a/.claude/skills/ui-framework/references/ecto.md b/.claude/skills/ui-framework/references/ecto.md
new file mode 100644
index 00000000..3d7fd39f
--- /dev/null
+++ b/.claude/skills/ui-framework/references/ecto.md
@@ -0,0 +1,9 @@
+## Ecto Guidelines
+
+- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email`
+- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs`
+- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string`
+- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed
+- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields
+- Fields which are set programmatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct
+- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied
diff --git a/.claude/skills/ui-framework/references/elixir.md b/.claude/skills/ui-framework/references/elixir.md
new file mode 100644
index 00000000..79c40822
--- /dev/null
+++ b/.claude/skills/ui-framework/references/elixir.md
@@ -0,0 +1,54 @@
+## Elixir guidelines
+
+- Elixir lists **do not support index based access via the access syntax**
+
+ **Never do this (invalid)**:
+
+ i = 0
+ mylist = ["blue", "green"]
+ mylist[i]
+
+ Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie:
+
+ i = 0
+ mylist = ["blue", "green"]
+ Enum.at(mylist, i)
+
+- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc
+ you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:
+
+ # INVALID: we are rebinding inside the `if` and the result never gets assigned
+ if connected?(socket) do
+ socket = assign(socket, :val, val)
+ end
+
+ # VALID: we rebind the result of the `if` to a new variable
+ socket =
+ if connected?(socket) do
+ assign(socket, :val, val)
+ end
+
+- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
+- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets
+- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package)
+- Don't use `String.to_atom/1` on user input (memory leak risk)
+- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
+- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)`
+- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
+
+## Mix guidelines
+
+- Read the docs and options before using tasks (by using `mix help task_name`)
+- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
+- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
+
+## Test guidelines
+
+- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests
+- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests
+ - Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message:
+
+ ref = Process.monitor(pid)
+ assert_receive {:DOWN, ^ref, :process, ^pid, :normal}
+
+ - Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages
diff --git a/.claude/skills/ui-framework/references/fluxon.md b/.claude/skills/ui-framework/references/fluxon.md
new file mode 100644
index 00000000..9b082638
--- /dev/null
+++ b/.claude/skills/ui-framework/references/fluxon.md
@@ -0,0 +1,2437 @@
+# Fluxon UI Components Usage Rules
+
+This document provides a guide for Large Language Models (LLMs) on how to use Fluxon UI components. It covers component attributes, slots, and provides usage examples for different scenarios.
+
+## Accordion
+
+The Accordion component provides collapsible content sections with headers that toggle panel visibility.
+
+### Components
+- `accordion`: Main container that manages state and accessibility
+- `accordion_item`: Individual collapsible sections
+
+### Attributes
+
+#### accordion
+- `id` (string, optional): Unique identifier for the accordion container
+- `class` (any, optional): Additional CSS classes for the accordion container
+- `multiple` (boolean, default: false): Allow multiple items to be expanded simultaneously
+- `prevent_all_closed` (boolean, default: false): Prevent all items from being closed at once
+- `animation_duration` (integer, default: 300): Duration of expand/collapse animation in milliseconds
+- `rest`: Additional HTML attributes
+
+#### accordion_item
+- `id` (string, optional): Unique identifier for the accordion item
+- `class` (any, optional): Additional CSS classes for the accordion item container
+- `expanded` (boolean, default: false): Initial expanded state of the item
+- `icon` (boolean, default: true): Show/hide the chevron icon
+- `rest`: Additional HTML attributes
+
+### Slots
+
+#### accordion
+- `inner_block` (required): Contains one or more accordion_item components
+
+#### accordion_item
+- `header` (required): Always-visible clickable area that toggles the panel
+ - `class` (optional): Additional CSS classes for the header button
+- `panel` (required): Expandable content area
+ - `class` (optional): Additional CSS classes for the panel content
+
+### Usage Examples
+
+#### Basic Accordion
+```heex
+<.accordion>
+ <.accordion_item>
+ <:header>What is Fluxon?
+ <:panel>
+ Fluxon is a powerful UI component library for Phoenix LiveView applications.
+
+
+
+ <.accordion_item>
+ <:header>How do I get started?
+ <:panel>
+ Add Fluxon to your dependencies and follow the installation guide.
+
+
+
+```
+
+#### Multiple Sections and Rich Headers
+```heex
+<.accordion multiple>
+ <.accordion_item>
+ <:header class="flex items-center gap-3">
+ <.icon name="document" class="size-5 text-zinc-400" />
+
+
Documentation
+
View the complete documentation
+
+ <.badge class="ml-auto">New
+
+ <:panel>Detailed documentation content...
+
+
+```
+
+#### Custom Animation and No Icon
+```heex
+<.accordion animation_duration={500}>
+ <.accordion_item icon={false}>
+ <:header>Custom Header Without Chevron
+ <:panel>No chevron icon is shown for this item
+
+
+```
+
+## Alert
+
+The Alert component displays status messages, notifications, and interactive feedback with various visual styles and interactive elements.
+
+### Attributes
+- `id` (string, optional): Unique identifier for the alert element
+- `class` (any, optional): Additional CSS classes for the alert element
+- `title` (string, optional): Main heading text of the alert
+- `subtitle` (string, optional): Secondary text displayed alongside the title
+- `color` (string, default: "default"): Visual style color - "default", "primary", "danger", "success", "info", "warning"
+- `hide_icon` (boolean, default: false): Hide the alert's status icon
+- `hide_close` (boolean, default: false): Hide the alert's close button
+- `on_close` (JS, default: %JS{}): LiveView JS commands to execute when closed
+
+### Slots
+- `inner_block` (optional): Main content displayed in the alert body
+- `icon` (optional): Custom icon to replace the default status icon
+
+### Usage Examples
+
+#### Basic Alert with Colors
+```heex
+<.alert>Simple alert message
+
+<.alert color="info" title="Update Available">
+ A new version of the application is ready to install.
+
+
+<.alert color="success" title="Order Confirmed">
+ Your order has been successfully processed.
+
+
+<.alert color="warning" title="Session Expiring">
+ Your session will expire in 5 minutes.
+
+
+<.alert color="danger" title="Connection Lost">
+ Unable to connect to the server.
+
+```
+
+#### Alert with Actions
+```heex
+<.alert
+ color="warning"
+ title="Unsaved Changes"
+ on_close={JS.push("dismiss_warning")}>
+ You have unsaved changes that will be lost.
+
+
+ <.button size="xs">Save Changes
+ <.button size="xs" color="ghost">Discard
+
+
+```
+
+#### Non-dismissible and Custom Icon
+```heex
+<.alert hide_close>
+ This alert cannot be dismissed
+
+
+<.alert>
+ <:icon>
+ <.icon name="hero-bell" class="size-5" />
+
+ Custom notification icon
+
+```
+
+## Autocomplete
+
+The Autocomplete component provides a text input that filters a list of options as the user types, with keyboard navigation and accessibility features. It supports both client-side and server-side search capabilities.
+
+### Attributes
+- `id` (any, optional): Unique identifier for the autocomplete component
+- `name` (any): Form name for the autocomplete (required when not using `field`)
+- `field` (Phoenix.HTML.FormField, optional): Form field to bind to
+- `class` (any, optional): Additional CSS classes for the field wrapper element
+- `label` (string, optional): Primary label for the autocomplete
+- `sublabel` (string, optional): Additional context displayed beside the main label
+- `help_text` (string, optional): Help text displayed below the autocomplete
+- `description` (string, optional): Description displayed below the label
+- `placeholder` (string, optional): Text to display when input is empty
+- `autofocus` (boolean, default: false): Whether input should have autofocus
+- `disabled` (boolean, default: false): Disable the autocomplete component
+- `size` (string, default: "md"): Size variant - "xs", "sm", "md", "lg", "xl"
+- `search_threshold` (integer, default: 0): Minimum characters before showing suggestions
+- `no_results_text` (string, default: "No results found for \"%{query}\"."): Text for no results
+- `on_search` (string, optional): LiveView event name for server-side search
+- `debounce` (integer, default: 200): Debounce time in milliseconds for server-side searches
+- `search_mode` (string, default: "contains"): Search mode - "contains", "starts-with", "exact"
+- `open_on_focus` (boolean, default: false): Open listbox when input is focused
+- `value` (any, optional): Current selected value
+- `errors` (list, default: []): List of error messages
+- `options` (list, required): List of options in various formats
+- `clearable` (boolean, default: false): Show clear button to remove selection
+- `rest`: Additional HTML attributes
+
+### Slots
+- `inner_prefix` (optional): Content inside input field before text
+- `outer_prefix` (optional): Content outside and before input field
+- `inner_suffix` (optional): Content inside input field after text
+- `outer_suffix` (optional): Content outside and after input field
+- `option` (optional): Custom option rendering slot
+- `empty_state` (optional): Custom empty state rendering
+- `header` (optional): Custom header content for listbox
+- `footer` (optional): Custom footer content for listbox
+
+### Usage Examples
+
+#### Basic and Form Integration
+```heex
+<.autocomplete
+ name="country"
+ options={[{"United States", "us"}, {"Canada", "ca"}]}
+ placeholder="Search countries..."
+/>
+
+<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
+ <.autocomplete
+ field={f[:user_id]}
+ label="Assigned To"
+ options={@users}
+ />
+
+```
+
+#### Different Option Formats
+```heex
+
+<.autocomplete
+ name="fruits"
+ options={["Apple", "Banana", "Cherry"]}
+ placeholder="Search fruits..."
+/>
+
+
+<.autocomplete
+ name="products"
+ options={[
+ {"Fruits", [{"Apple", "apple"}, {"Banana", "banana"}]},
+ {"Vegetables", [{"Carrot", "carrot"}, {"Broccoli", "broccoli"}]}
+ ]}
+ placeholder="Search products..."
+/>
+```
+
+#### Size Variants and Labels
+```heex
+<.autocomplete name="sm" size="sm" placeholder="Small" options={@options} />
+<.autocomplete name="lg" size="lg" placeholder="Large" options={@options} />
+
+<.autocomplete
+ name="language"
+ label="Programming Language"
+ sublabel="Required"
+ description="Choose the web framework for your project"
+ help_text="Choose from our supported platforms"
+ placeholder="Select your favorite..."
+ options={["Elixir", "Phoenix", "LiveView"]}
+/>
+```
+
+#### Affixes and Search Features
+```heex
+
+<.autocomplete name="user_search" options={@users} placeholder="Search users...">
+ <:inner_prefix>
+ <.icon name="hero-magnifying-glass" class="size-4" />
+
+
+
+
+<.autocomplete
+ name="movie"
+ options={@movies}
+ on_search="search_movies"
+ search_threshold={2}
+ clearable
+ placeholder="Type to search movies..."
+/>
+```
+
+#### Custom Option Rendering
+```heex
+<.autocomplete
+ name="users"
+ placeholder="Search team members..."
+ options={[{"John Doe", "john"}, {"Jane Smith", "jane"}]}
+>
+ <:option :let={{label, value}}>
+
+
+ {String.first(label)}
+
+
+
+
+
+```
+
+## Badge
+
+The Badge component provides versatile visual markers for status indicators, categories, and notification counts with semantic colors, size variants, and visual styles.
+
+### Attributes
+- `class` (any, optional): Additional CSS classes for the badge element
+- `color` (string, default: "primary"): Semantic color - "primary", "info", "success", "warning", "danger"
+- `size` (string, default: "md"): Size variant - "xs", "sm", "md", "lg", "xl"
+- `variant` (string, default: "surface"): Visual style - "solid", "soft", "surface", "outline", "dashed", "ghost"
+- `rest`: Additional HTML attributes
+
+### Slots
+- `inner_block` (required): Content displayed within the badge (text, icons, or both)
+
+### Usage Examples
+
+#### Colors and Variants
+```heex
+<.badge>Default Badge
+<.badge color="success">Active
+<.badge color="danger" size="lg">Error
+<.badge variant="ghost" color="info">Draft
+
+<.badge variant="solid" color="success">High Emphasis
+<.badge variant="soft" color="info">Medium Emphasis
+<.badge variant="outline" color="primary">Outline
+```
+
+#### Size Variants
+```heex
+<.badge size="xs">Extra Small
+<.badge size="sm">Small
+<.badge size="lg">Large
+```
+
+#### With Icons and Interactive
+```heex
+<.badge color="success">
+ <.icon name="hero-check-circle" class="icon" /> Verified
+
+
+
+<.badge
+ color={if @selected, do: "primary", else: "info"}
+ variant={if @selected, do: "solid", else: "dashed"}
+ phx-click="toggle_selection"
+ class="cursor-pointer"
+>
+ <.icon :if={@selected} name="hero-check" class="icon" />
+ Category
+
+```
+
+## Button
+
+The Button component provides consistent, accessible, and visually appealing interactive elements for actions and navigation. It automatically renders either a `` or `` tag based on provided attributes.
+
+### Components
+- `button`: Interactive button/link element
+- `button_group`: Container to group multiple buttons visually
+
+### Attributes
+
+#### button
+- `color` (string, default: "primary"): Semantic color - "primary", "danger", "warning", "success", "info"
+- `size` (string, default: "md"): Size variant - "xs", "sm", "md", "lg", "xl", "icon-xs", "icon-sm", "icon-md", "icon", "icon-lg", "icon-xl"
+- `variant` (string, default: "outline"): Visual style - "solid", "soft", "surface", "outline", "dashed", "ghost"
+- `disabled` (boolean, default: false): Whether the button is disabled
+- `class` (any, optional): Additional CSS classes for the button
+- `rest`: Additional HTML attributes (supports both button and anchor attributes)
+
+#### button_group
+- `class` (any, optional): Additional CSS classes for the group container
+- `rest`: Additional HTML attributes for the group container
+
+### Slots
+
+#### button
+- `inner_block` (required): Content displayed within the button (text, icons, etc.)
+
+#### button_group
+- `inner_block` (required): Buttons to be grouped together
+
+### Usage Examples
+
+#### Basic Buttons and Variants
+```heex
+<.button>Outline Button (Default)
+<.button variant="solid" color="primary">Solid Button
+<.button variant="soft" color="primary">Soft Button
+<.button variant="ghost">Ghost Button
+```
+
+#### Colors and Sizes
+```heex
+<.button color="primary" variant="solid">Primary
+<.button color="danger" variant="soft">Danger
+<.button color="success" variant="surface">Success
+
+<.button size="xs">Extra Small
+<.button size="lg">Large
+<.button size="xl">Extra Large
+```
+
+#### Icon Buttons
+```heex
+<.button size="icon-sm">
+ <.icon name="hero-pencil" class="icon" />
+
+
+<.button size="icon" variant="solid" color="primary">
+ <.icon name="hero-magnifying-glass" class="icon" />
+
+
+<.button size="lg" variant="solid" color="success">
+ <.icon name="hero-check-circle" class="icon" /> Order Confirmed
+
+```
+
+#### Automatic Link Rendering
+```heex
+
+<.button navigate={~p"/dashboard"} variant="solid" color="primary">
+ Go to Dashboard
+
+
+<.button href="https://example.com" target="_blank" variant="soft">
+ External Link
+
+```
+
+#### Interactive Examples
+```heex
+
+<.button variant="solid" color="danger" phx-click="delete_item" phx-value-id={@item.id} phx-confirm="Are you sure?">
+ <.icon name="hero-trash" class="icon" />
+ Delete Item
+
+
+
+<.button size="icon" variant="ghost" phx-click="show-details" phx-value-id={@user.id} aria-label="View user details">
+ <.icon name="hero-eye" class="icon" />
+
+```
+
+#### Button Group
+```heex
+<.button_group>
+ <.button>Year
+ <.button>Month
+ <.button>Week
+
+```
+
+## Checkbox
+
+The Checkbox component provides versatile checkbox inputs for capturing single and multiple selections. It offers both standard and card variants with comprehensive form integration and accessibility features.
+
+### Components
+- `checkbox`: Single checkbox input for boolean or single-value selections
+- `checkbox_group`: Container for multiple related checkbox selections
+
+### Attributes
+
+#### checkbox
+- `id` (any, optional): Unique identifier for the checkbox
+- `name` (any): Form name for the checkbox (required when not using `field`)
+- `checked` (boolean, optional): Whether the checkbox is checked
+- `value` (any, optional): Value associated with the checkbox
+- `errors` (list, default: []): List of error messages
+- `label` (string, optional): Primary label for the checkbox
+- `sublabel` (string, optional): Additional context beside the main label
+- `description` (string, optional): Detailed description below the label
+- `class` (any, optional): Additional CSS classes for the checkbox
+- `field` (Phoenix.HTML.FormField, optional): Form field to bind to
+- `variant` (string, optional): Visual variant - `nil` (default), `"card"`
+- `control` (string, optional): Position of checkbox in card variant - "left", "right"
+- `rest`: Additional HTML attributes
+
+#### checkbox_group
+- `id` (any, optional): Unique identifier for the checkbox group
+- `name` (string): Form name for the checkbox group (required when not using `field`)
+- `value` (any, optional): Current value(s) of the checkbox group (list of selected values)
+- `label` (string, optional): Primary label for the checkbox group
+- `sublabel` (string, optional): Additional context beside the main label
+- `description` (string, optional): Detailed description of the checkbox group
+- `errors` (list, default: []): List of error messages
+- `class` (any, optional): Additional CSS classes for the group container
+- `field` (Phoenix.HTML.FormField, optional): Form field to bind to
+- `disabled` (boolean, default: false): Disable all checkboxes in the group
+- `variant` (string, optional): Visual variant - `nil` (default), `"card"`
+- `control` (string, optional): Position of checkbox in card variant - "left", "right"
+- `rest`: Additional HTML attributes
+
+### Slots
+
+#### checkbox
+- `inner_block` (optional): Custom content for the checkbox (replaces standard label structure in card variants)
+
+#### checkbox_group
+- `checkbox` (required): Individual checkboxes within the group
+
+### Usage Examples
+
+#### Basic Checkbox and Form Integration
+```heex
+<.checkbox
+ name="terms"
+ label="I agree to the terms and conditions"
+ value="accepted"
+/>
+
+<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
+ <.checkbox
+ field={f[:marketing_emails]}
+ label="Marketing emails"
+ sublabel="Receive updates about new features and promotions"
+ />
+
+```
+
+#### Card Variant
+```heex
+<.checkbox
+ control="left"
+ field={f[:notifications]}
+ variant="card"
+ label="Push Notifications"
+ sublabel="Stay informed"
+ description="Get real-time updates for messages and activity"
+ value="enabled"
+/>
+```
+
+#### Checkbox Group
+```heex
+<.checkbox_group
+ name="preferences"
+ label="Notification Preferences"
+ description="Choose when you want to receive notifications"
+>
+ <:checkbox value="email" label="Email notifications" />
+ <:checkbox value="push" label="Push notifications" />
+ <:checkbox value="sms" label="SMS notifications" />
+
+
+<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
+ <.checkbox_group
+ field={f[:notification_preferences]}
+ label="Notification Preferences"
+ description="Choose how you want to be notified"
+ >
+ <:checkbox value="email" label="Email" sublabel="Get notified via email" />
+ <:checkbox value="push" label="Push" sublabel="Receive push notifications" />
+
+
+```
+
+#### Custom Card Content
+```heex
+<.checkbox variant="card" name="feature" value="advanced">
+
+
+ <.icon name="hero-star" class="size-5 text-blue-600" />
+
+
+
Advanced Features
+
Access premium functionality
+
+
+
+```
+
+## DatePicker
+
+The DatePicker component provides calendar-based date selection with support for single dates, multiple dates, date ranges, and time selection. It includes configurable sizing and affix slots for UI customization.
+
+### Components
+- `date_picker`: Single date selection with optional multiple date support
+- `date_time_picker`: Date and time selection
+- `date_range_picker`: Date range selection with start and end dates
+
+### Attributes
+
+#### date_picker
+- `id` (any, optional): Unique identifier for the date picker
+- `name` (any): Form name for the date picker (required when not using `field`)
+- `field` (Phoenix.HTML.FormField, optional): Form field to bind to
+- `value` (any, optional): Current selected value
+- `multiple` (boolean, default: false): Allow selecting multiple dates
+- `label` (string, optional): Primary label for the date picker
+- `sublabel` (string, optional): Additional context beside the main label
+- `description` (string, optional): Detailed description below the label
+- `help_text` (string, optional): Help text displayed below the component
+- `placeholder` (string, optional): Text to display when no date is selected
+- `size` (string, default: "md"): Size variant - "xs", "sm", "md", "lg", "xl"
+- `min` (Date/string, optional): Minimum selectable date
+- `max` (Date/string, optional): Maximum selectable date
+- `disabled` (boolean, default: false): Disable the date picker
+- `errors` (list, default: []): List of error messages
+- `display_format` (string, default: "%b %-d, %Y"): Format for displaying selected dates
+- `week_start` (integer, default: 0): Day of week to start calendar (0=Sunday, 1=Monday, etc.)
+- `rest`: Additional HTML attributes
+
+#### date_time_picker
+- All attributes from `date_picker` plus:
+- `time_format` (string, default: "24"): Time format - "12" (12-hour) or "24" (24-hour)
+
+#### date_range_picker
+- `id` (any, optional): Unique identifier for the date range picker
+- `start_name` (string): Form name for the start date (required when not using fields)
+- `end_name` (string): Form name for the end date (required when not using fields)
+- `start_field` (Phoenix.HTML.FormField, optional): Start date form field to bind to
+- `end_field` (Phoenix.HTML.FormField, optional): End date form field to bind to
+- `start_value` (any, optional): Current start date value
+- `end_value` (any, optional): Current end date value
+- Plus other shared attributes
+
+### Slots (All Components)
+- `inner_prefix` (optional): Content inside the field border, before the date display
+- `outer_prefix` (optional): Content outside and before the field
+- `inner_suffix` (optional): Content inside the field border, after the date display
+- `outer_suffix` (optional): Content outside and after the field
+
+### Usage Examples
+
+#### Basic Date Pickers
+```heex
+<.date_picker
+ name="appointment"
+ label="Appointment Date"
+ placeholder="Select a date"
+/>
+
+<.date_time_picker
+ name="meeting"
+ label="Meeting Time"
+ time_format="12"
+ placeholder="Select date and time"
+/>
+
+<.date_range_picker
+ start_name="check_in"
+ end_name="check_out"
+ label="Stay Period"
+ placeholder="Select date range"
+/>
+```
+
+#### Size Variants and Labels
+```heex
+<.date_picker name="sm" size="sm" label="Small Date" placeholder="Small" />
+<.date_picker name="lg" size="lg" label="Large Date" placeholder="Large" />
+
+<.date_picker
+ name="event_date"
+ label="Event Date"
+ sublabel="Required"
+ description="Choose when your event will take place"
+ help_text="Events can be scheduled up to 6 months in advance"
+ placeholder="Select event date"
+/>
+```
+
+#### Date Constraints and Multiple Selection
+```heex
+
+<.date_picker
+ name="appointment"
+ label="Appointment Date"
+ min={Date.utc_today()}
+ max={Date.add(Date.utc_today(), 60)}
+ placeholder="Select your appointment date"
+/>
+
+<.date_picker
+ name="available_dates"
+ label="Available Dates"
+ multiple
+ placeholder="Select multiple dates"
+/>
+```
+
+#### Form Integration with Affixes
+```heex
+<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
+ <.date_picker
+ field={f[:appointment_date]}
+ label="Appointment Date"
+ placeholder="Choose date"
+ />
+
+ <.date_range_picker
+ start_field={f[:start_date]}
+ end_field={f[:end_date]}
+ label="Date Range"
+ placeholder="Choose date range"
+ />
+
+
+
+<.date_picker name="appointment" label="Appointment Date" placeholder="Select date">
+ <:inner_prefix>
+ <.icon name="hero-calendar-days" class="icon" />
+
+
+```
+
+## Dropdown
+
+The Dropdown component provides a comprehensive system for creating accessible, interactive menus and selection interfaces with proper keyboard navigation and focus management.
+
+### Components
+- `dropdown`: Main dropdown container
+- `dropdown_link`: Link menu item for navigation
+- `dropdown_button`: Button menu item for actions
+- `dropdown_header`: Section header for organizing menu items
+- `dropdown_separator`: Visual separator between menu sections
+- `dropdown_custom`: Custom content container for rich menu items
+
+### Attributes
+
+#### dropdown
+- `id` (string, optional): Unique identifier for the dropdown component
+- `label` (string, default: "Menu"): Text label for the default toggle button
+- `class` (any, optional): Additional CSS classes for the dropdown container
+- `placement` (string, default: "bottom-start"): Menu position relative to toggle
+- `open_on_hover` (boolean, default: false): Enable hover-based opening
+- `hover_open_delay` (integer, default: 100): Delay before opening on hover (ms)
+- `hover_close_delay` (integer, default: 300): Delay before closing when leaving hover (ms)
+- `rest`: Additional HTML attributes
+
+#### Other dropdown components
+- `class` (any, optional): Additional CSS classes for the element
+- `rest`: Additional HTML attributes
+
+### Slots
+
+#### dropdown
+- `toggle` (optional): Custom toggle element to replace the default button
+- `inner_block` (required): Menu items and content
+
+#### All other dropdown components
+- `inner_block` (required): Content for the menu item/element
+
+### Usage Examples
+
+#### Basic Dropdown
+```heex
+<.dropdown>
+ <.dropdown_link navigate={~p"/profile"}>Profile
+ <.dropdown_link navigate={~p"/settings"}>Settings
+ <.dropdown_separator />
+ <.dropdown_link href={~p"/logout"} method="delete">Sign Out
+
+```
+
+#### Custom Toggle and Rich Content
+```heex
+<.dropdown>
+ <:toggle>
+
+
+ John Doe
+ <.icon name="hero-chevron-down" class="size-4" />
+
+
+ <.dropdown_button>Profile
+ <.dropdown_button>Settings
+
+
+<.dropdown class="w-64">
+ <.dropdown_custom class="flex items-center p-2">
+
+
+ Emma Johnson
+ emma@acme.com
+
+
+
+ <.dropdown_separator />
+
+ <.dropdown_header>Account
+ <.dropdown_link navigate={~p"/profile"}>Profile
+ <.dropdown_link navigate={~p"/billing"}>Billing
+
+```
+
+#### Hover Interaction and Button Actions
+```heex
+<.dropdown open_on_hover hover_open_delay={200} hover_close_delay={300}>
+ <.dropdown_link navigate={~p"/profile"}>Profile
+ <.dropdown_link navigate={~p"/settings"}>Settings
+
+
+<.dropdown>
+ <.dropdown_button phx-click="export_data">Export Data
+ <.dropdown_button phx-click="import_data">Import Data
+ <.dropdown_separator />
+ <.dropdown_button phx-click="delete_all" phx-confirm="Are you sure?">
+ Delete All
+
+
+```
+
+## Input
+
+The Input component provides versatile text input fields with support for various types, sizes, form integration, and customizable prefix/suffix content both inside and outside the input field.
+
+### Components
+- `input`: Main text input component
+- `input_group`: Container for visually grouping multiple related inputs
+
+### Attributes
+
+#### input
+- `id` (any, optional): HTML id attribute for the input element
+- `name` (any): Form name for the input (required when not using `field`)
+- `field` (Phoenix.HTML.FormField, optional): Form field to bind to
+- `value` (any, optional): Current input value
+- `type` (string, default: "text"): HTML input type
+- `label` (string, optional): Primary label for the input
+- `sublabel` (string, optional): Additional context beside the main label
+- `description` (string, optional): Detailed description below the label
+- `help_text` (string, optional): Help text displayed below the input
+- `placeholder` (string, optional): Placeholder text
+- `size` (string, default: "md"): Size variant - "xs", "sm", "md", "lg", "xl"
+- `disabled` (boolean, default: false): Disable the input
+- `readonly` (boolean, optional): Make input readonly
+- `errors` (list, default: []): List of error messages
+- `class` (any, optional): Additional CSS classes
+- `rest`: Additional HTML attributes
+
+#### input_group
+- `class` (any, optional): Additional CSS classes for the group container
+- `label` (string, optional): Primary label for the input group
+- `sublabel` (string, optional): Additional context beside the main label
+- `description` (string, optional): Detailed description of the input group
+- `help_text` (string, optional): Help text displayed below the group
+- `rest`: Additional HTML attributes
+
+### Slots
+
+#### input
+- `inner_prefix` (optional): Content inside the input border, before the text
+- `outer_prefix` (optional): Content outside and before the input field
+- `inner_suffix` (optional): Content inside the input border, after the text
+- `outer_suffix` (optional): Content outside and after the input field
+
+#### input_group
+- `inner_block` (required): Input elements and controls to be grouped
+
+### Usage Examples
+
+#### Basic Input and Size Variants
+```heex
+<.input name="username" label="Username" placeholder="Enter username..." />
+<.input name="password" type="password" label="Password" />
+<.input name="email" type="email" label="Email" />
+
+<.input name="input_sm" size="sm" placeholder="Small" />
+<.input name="input_lg" size="lg" placeholder="Large" />
+<.input name="input_xl" size="xl" placeholder="Extra Large" />
+```
+
+#### Complete Labeling and Form Integration
+```heex
+<.input
+ name="full_example"
+ label="Email Address"
+ sublabel="(required)"
+ description="We'll send a confirmation link here."
+ help_text="We never share your email."
+ placeholder="you@example.com"
+/>
+
+<.form :let={f} for={@form}>
+ <.input field={f[:email]} type="email" label="Email" />
+ <.input field={f[:password]} type="password" label="Password" />
+
+```
+
+#### Input States and Types
+```heex
+
+<.input name="disabled_input" value="Cannot change" disabled />
+<.input name="readonly_input" value="Readonly value" readonly />
+
+
+<.input type="date" name="date_input" label="Appointment Date" />
+<.input type="number" name="count" label="Quantity" min="1" max="10" />
+<.input type="search" name="site_search" placeholder="Search site..." />
+```
+
+#### Inner Affixes
+```heex
+
+<.input name="email_icon" placeholder="user@example.com">
+ <:inner_prefix>
+ <.icon name="hero-at-symbol" class="icon" />
+
+
+
+
+<.input name="password_toggle" type="password" value="secretpassword">
+ <:inner_suffix>
+ <.button variant="ghost" size="icon-sm" title="Show password">
+ <.icon name="hero-eye" class="icon" />
+
+
+
+
+
+<.input name="url_prefix" placeholder="yourdomain.com">
+ <:inner_prefix class="pointer-events-none text-zinc-500">https://www.
+
+```
+
+#### Outer Affixes
+```heex
+
+<.input name="invite_user" placeholder="user@example.com">
+ <:inner_prefix>
+ <.icon name="hero-at-symbol" class="icon" />
+
+ <:outer_suffix>
+ <.button variant="solid" color="primary">
+ <.icon name="hero-paper-airplane" class="icon" /> Invite
+
+
+
+
+
+<.input name="website_url" placeholder="mysite">
+ <:outer_prefix class="px-2 text-zinc-500">https://
+ <:outer_suffix class="px-2 text-zinc-500">.example.com
+
+```
+
+#### Input Groups
+```heex
+
+<.input_group label="Full Name">
+ <.input name="first_name" placeholder="First Name" />
+ <.input name="last_name" placeholder="Last Name" />
+
+
+
+<.input_group label="Price Range">
+ <.input name="min_price" placeholder="Min price">
+ <:inner_prefix>$
+
+
+ to
+
+ <.input name="max_price" placeholder="Max price">
+ <:inner_prefix>$
+
+
+```
+
+## Loading
+
+The Loading component provides versatile animated loading indicators for displaying loading states with multiple animation styles and customization options.
+
+### Attributes
+- `class` (any, optional): Additional CSS classes for customizing size and color (default: size-5, text-zinc-600)
+- `duration` (integer, default: 600): Duration of one complete animation cycle in milliseconds
+- `variant` (string, default: "ring"): Animation style - "ring", "ring-bg", "dots-bounce", "dots-fade", "dots-scale"
+- `rest`: Additional HTML attributes
+
+### Usage Examples
+
+#### Animation Variants
+```heex
+<.loading />
+<.loading variant="ring-bg" />
+<.loading variant="dots-bounce" />
+<.loading variant="dots-fade" />
+<.loading variant="dots-scale" />
+```
+
+#### Size and Color Variations
+```heex
+<.loading class="size-4" />
+<.loading class="size-8" />
+<.loading class="size-12" />
+
+<.loading class="text-blue-500" />
+<.loading class="text-green-500" />
+<.loading class="text-red-500" />
+```
+
+#### Button Loading States
+```heex
+<.button disabled>
+ <.loading class="size-4" /> Loading...
+
+
+<.button variant="solid" color="primary" disabled>
+ <.loading class="size-4 text-white" /> Processing
+
+```
+
+#### Page and Section Loading
+```heex
+
+
+ <.loading class="size-8" />
+
+
+
+
+
+ <.loading class="size-6" />
+
+
+
+
+
+Processing your request <.loading class="size-4 inline" variant="dots-bounce" />
+```
+
+## Modal
+
+The Modal component provides a powerful and accessible modal overlay for displaying focused content with LiveView integration, supporting both client-side and server-side control.
+
+### Attributes
+- `id` (string, required): Unique identifier for the modal component
+- `open` (boolean, default: false): Whether the modal is initially open
+- `on_close` (JS, optional): JavaScript commands to execute when the modal closes
+- `on_open` (JS, optional): JavaScript commands to execute when the modal opens
+- `class` (any, optional): Additional CSS classes for the modal content container
+- `container_class` (any, optional): CSS classes for the outer modal container
+- `close_on_esc` (boolean, default: true): Whether pressing ESC closes the modal
+- `close_on_outside_click` (boolean, default: true): Whether clicking outside closes the modal
+- `prevent_closing` (boolean, default: false): Prevent all client-side closing behaviors
+- `hide_close_button` (boolean, default: false): Hide the default close button
+- `animation` (string, default: "transition duration-300 ease-out"): Animation classes
+- `animation_enter` (string, default: "opacity-100 scale-100"): Enter animation classes
+- `animation_leave` (string, default: "opacity-0 scale-95"): Leave animation classes
+- `backdrop_class` (string, optional): Custom classes for the modal backdrop
+- `placement` (string, default: "center"): Modal positioning on screen
+- `rest`: Additional HTML attributes
+
+### Slots
+- `inner_block` (required): Content displayed within the modal
+
+### Usage Examples
+
+#### Basic Modal
+```heex
+<.button phx-click={Fluxon.open_dialog("basic-modal")}>Open Modal
+
+<.modal id="basic-modal">
+
+
Modal Title
+
This is the modal content.
+
+
+ <.button phx-click={Fluxon.close_dialog("basic-modal")}>Close
+
+
+
+```
+
+#### Server-Side Control
+```heex
+<.button phx-click="show_modal">Open Server Modal
+
+<.modal id="server-modal" open={@show_modal} on_close={JS.push("hide_modal")}>
+
+
Server Controlled Modal
+
This modal is controlled by server state.
+ <.button phx-click="hide_modal" class="mt-4">Close
+
+
+
+
+<.modal id="secure-modal" open={@show_secure_modal} prevent_closing>
+
+
Critical Operation
+
This modal can only be closed through server commands.
+ <.button phx-click="hide_secure_modal" color="danger" class="mt-4">
+ Complete Action
+
+
+
+```
+
+#### Custom Styling and Form Modal
+```heex
+
+<.modal
+ id="large-modal"
+ class="w-full max-w-4xl max-h-[80vh] overflow-auto"
+ container_class="p-4"
+>
+
+
Large Modal
+
This modal has custom sizing and scrolling behavior.
+
+
+
+
+<.modal id="form-modal" on_close={JS.push("reset_form")}>
+
+
Create User
+
+ <.form :let={f} for={@changeset} phx-change="validate" phx-submit="create_user">
+ <.input field={f[:name]} label="Full Name" />
+ <.input field={f[:email]} type="email" label="Email" />
+
+
+ <.button type="submit" disabled={!@changeset.valid?}>
+ Create User
+
+ <.button
+ type="button"
+ variant="outline"
+ phx-click={Fluxon.close_dialog("form-modal")}
+ >
+ Cancel
+
+
+
+
+
+```
+
+#### Confirmation Modal
+```heex
+<.modal id="confirm-delete" class="w-96">
+
+
+ <.icon name="hero-exclamation-triangle" class="size-6 text-red-600" />
+
+
+
Delete Item
+
+ Are you sure you want to delete this item? This action cannot be undone.
+
+
+
+ <.button
+ color="danger"
+ phx-click="confirm_delete"
+ phx-value-id={@delete_item_id}
+ >
+ Delete
+
+ <.button
+ variant="outline"
+ phx-click={Fluxon.close_dialog("confirm-delete")}
+ >
+ Cancel
+
+
+
+
+```
+
+## Navlist
+
+The Navlist component provides a comprehensive navigation system for building structured, accessible navigation menus with support for sections, headings, and interactive links.
+
+### Components
+- `navlist`: Main navigation container that provides structure and spacing
+- `navheading`: Optional section headers for organizing navigation groups
+- `navlink`: Interactive navigation items with LiveView integration
+
+### Attributes
+
+#### navlist
+- `heading` (string, optional): Primary heading for the navigation section
+- `class` (any, optional): Additional CSS classes for the navigation container
+- `rest`: Additional HTML attributes for the nav container
+
+#### navheading
+- `class` (any, optional): Additional CSS classes for the heading element
+- `rest`: Additional HTML attributes
+
+#### navlink
+- `class` (any, optional): Additional CSS classes for the navigation link
+- `active` (boolean, default: false): Whether this navigation item is currently active
+- `rest`: Additional HTML attributes (supports navigation attributes like `navigate`, `patch`, `href`, etc.)
+
+### Slots
+
+#### navlist
+- `inner_block` (required): Navigation items and headings
+
+#### navheading
+- `inner_block` (required): Heading content
+
+#### navlink
+- `inner_block` (required): Link content (text, icons, badges, etc.)
+
+### Usage Examples
+
+#### Basic Navigation
+```heex
+<.navlist heading="Main Navigation">
+ <.navlink navigate={~p"/dashboard"} active>
+ <.icon name="hero-home" class="size-5" /> Dashboard
+
+ <.navlink navigate={~p"/projects"}>
+ <.icon name="hero-folder" class="size-5" /> Projects
+
+ <.navlink navigate={~p"/settings"}>
+ <.icon name="hero-cog-6-tooth" class="size-5" /> Settings
+
+
+```
+
+#### Multiple Sections
+```heex
+<.navlist heading="Main">
+ <.navlink navigate={~p"/dashboard"} active>
+ <.icon name="hero-home" class="size-5" /> Dashboard
+
+ <.navlink navigate={~p"/projects"}>
+ <.icon name="hero-folder" class="size-5" /> Projects
+
+
+
+<.navlist heading="Settings">
+ <.navlink navigate={~p"/profile"}>
+ <.icon name="hero-user" class="size-5" /> Profile
+
+ <.navlink navigate={~p"/preferences"}>
+ <.icon name="hero-cog-6-tooth" class="size-5" /> Preferences
+
+
+```
+
+#### Navigation with Badges
+```heex
+<.navlist heading="Inbox">
+ <.navlink navigate={~p"/inbox/unread"}>
+ <.icon name="hero-envelope" class="size-5" />
+ Unread
+ <.badge variant="solid" color="danger" class="ml-auto">23
+
+
+ <.navlink navigate={~p"/inbox/starred"}>
+ <.icon name="hero-star" class="size-5" />
+ Starred
+ <.badge variant="soft" color="warning" class="ml-auto">5
+
+
+```
+
+#### External Links and Actions
+```heex
+<.navlist heading="Resources">
+ <.navlink href="https://docs.example.com" target="_blank">
+ <.icon name="hero-document-text" class="size-5" />
+ Documentation
+ <.icon name="hero-arrow-top-right-on-square" class="size-4 ml-auto text-gray-400" />
+
+
+
+<.navlist heading="Actions">
+ <.navlink phx-click="export_data">
+ <.icon name="hero-arrow-down-tray" class="size-5" />
+ Export Data
+
+
+ <.navlink phx-click="refresh_data">
+ <.icon name="hero-arrow-path" class="size-5" />
+ Refresh
+
+
+```
+
+## Popover
+
+A powerful and accessible popover component that displays floating content anchored to a trigger element.
+
+### Attributes
+- `id` (string, default: auto-generated): Optional unique identifier for the popover
+- `target` (string, default: nil): CSS selector of an external element to use as positioning reference
+- `class` (any, default: nil): Additional CSS classes for the popover content container
+- `open_on_hover` (boolean, default: false): Opens popover on hover for tooltip-like behavior
+- `open_on_focus` (boolean, default: false): Opens popover when trigger receives focus
+- `placement` (string, default: "top"): Popover position - "top", "top-start", "top-end", "right", "right-start", "right-end", "left", "left-start", "left-end", "bottom", "bottom-start", "bottom-end"
+
+### Slots
+- `inner_block`: The trigger element that will open the popover (optional when using target)
+- `content` (required): The content to display in the popover
+
+### Usage Examples
+
+#### Basic Tooltip
+```heex
+<.popover open_on_hover>
+ <.icon name="hero-information-circle" class="text-zinc-400" />
+
+ <:content>
+ The invoice will be generated at the end of the month.
+
+
+```
+
+#### Interactive Menu and Form Help
+```heex
+<.popover placement="bottom-end" class="w-64">
+ <.button variant="ghost">
+ <.icon name="hero-cog-6-tooth" /> Settings
+
+
+ <:content>
+
+
+ Dark Mode
+ <.switch name="dark_mode" checked />
+
+ <.button size="sm" class="w-full">
+ Reset Preferences
+
+
+
+
+
+<.input name="api-key" label="API Key" value="sk_test_..." class="font-mono">
+ <:inner_suffix>
+ <.popover open_on_hover placement="right">
+ <.icon name="hero-question-mark-circle" class="text-zinc-400" />
+
+ <:content>
+
+
About API Keys
+
+ Your API key is used to authenticate requests. Keep it secure.
+
+
+
+
+
+
+```
+
+#### Search Suggestions
+```heex
+<.popover open_on_focus placement="bottom-start" class="w-80">
+ <.input type="search" placeholder="Search users..." phx-debounce="300" />
+
+ <:content>
+
+ <.loading />
+
+
+
{user.name}
+
{user.email}
+
+
+
+```
+
+## Radio
+
+A versatile radio component for building single-selection interfaces with rich styling options.
+
+### Attributes
+- `id` (any, default: nil): The unique identifier for the radio group
+- `name` (string, required when not using field): The form name for the radio group
+- `value` (any): The current selected value of the radio group
+- `label` (string, default: nil): The primary label for the radio group
+- `sublabel` (string, default: nil): Additional context displayed beside the main label
+- `description` (string, default: nil): Detailed description below the label
+- `errors` (list, default: []): List of error messages
+- `class` (any, default: nil): Additional CSS classes for the radio group container
+- `field` (Phoenix.HTML.FormField): The form field to bind to
+- `disabled` (boolean, default: false): Disables all radio buttons in the group
+- `variant` (string, default: nil): Visual variant - nil (default) or "card"
+- `control` (string): Controls radio position in card variants - "left" or "right"
+- `rest` (global): Additional attributes
+
+### Slots
+- `radio` (required): Defines individual radio buttons with attributes: `value`, `label`, `sublabel`, `description`, `disabled`, `class`, `checked`
+
+### Usage Examples
+
+#### Basic Radio Group
+```heex
+<.radio_group name="system" value="debian" label="Operating System">
+ <:radio value="ubuntu" label="Ubuntu" />
+ <:radio value="debian" label="Debian" />
+ <:radio value="fedora" label="Fedora" />
+
+```
+
+#### With Context and Form Integration
+```heex
+<.radio_group
+ name="system"
+ label="Operating System"
+ sublabel="Choose your preferred OS"
+ description="Select the operating system that best suits your needs"
+>
+ <:radio
+ value="ubuntu"
+ label="Ubuntu"
+ sublabel="Popular and user-friendly"
+ description="Ubuntu is a Debian-based Linux operating system"
+ />
+ <:radio
+ value="debian"
+ label="Debian"
+ sublabel="Stable and reliable"
+ description="Debian is composed of free and open-source software"
+ />
+
+
+<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
+ <.radio_group
+ field={f[:subscription]}
+ label="Subscription Plan"
+ description="Choose your preferred subscription plan"
+ >
+ <:radio value="basic" label="Basic Plan" sublabel="$10/month" />
+ <:radio value="pro" label="Pro Plan" sublabel="$20/month" />
+ <:radio value="enterprise" label="Enterprise Plan" sublabel="$50/month" />
+
+
+```
+
+#### Card Variant
+```heex
+<.radio_group
+ name="plan"
+ label="Choose a plan"
+ description="Choose the plan that best suits your needs."
+ variant="card"
+ control="left"
+ class="gap-0"
+>
+ <:radio value="basic" label="Basic" sublabel="Perfect for small projects" class="rounded-none -my-px rounded-t-lg" />
+ <:radio value="pro" label="Professional" checked sublabel="Most popular for growing teams" class="rounded-none -my-px" />
+ <:radio value="business" label="Business" sublabel="Advanced features for larger teams" class="rounded-none -my-px rounded-b-lg" />
+
+
+<.radio_group name="category" label="Category" variant="card" class="grid grid-cols-3">
+ <:radio value="web-design" class="flex-1 group has-checked:border-blue-500 has-checked:bg-blue-50">
+
+ <.icon name="hero-computer-desktop" class="size-6 text-zinc-500 group-has-checked:text-blue-500" />
+ Web Design
+
+
+ <:radio value="ui-ux" class="flex-1 group has-checked:border-blue-500 has-checked:bg-blue-50">
+
+ <.icon name="hero-pencil" class="size-6 text-zinc-500 group-has-checked:text-blue-500" />
+ UI/UX Design
+
+
+
+```
+
+## Select
+
+A select component that implements a modern, accessible selection interface with support for single and multiple selections.
+
+### Attributes
+- `id` (any, default: nil): The unique identifier for the select component
+- `name` (any, required when not using field): The form name for the select
+- `field` (Phoenix.HTML.FormField): The form field to bind to
+- `native` (boolean, default: false): Renders a native HTML select element instead of custom select
+- `class` (any, default: nil): Additional CSS classes for the select component
+- `label` (string, default: nil): The primary label for the select
+- `sublabel` (string, default: nil): Additional context beside the main label
+- `help_text` (string, default: nil): Help text displayed below the select
+- `description` (string, default: nil): Description below the label but above select
+- `placeholder` (string, default: nil): Text displayed when no option is selected
+- `searchable` (boolean, default: false): Adds search input to filter options (custom select only)
+- `disabled` (boolean, default: false): Disables the select component
+- `size` (string, default: "md"): Controls select size - "xs", "sm", "md", "lg", "xl"
+- `search_input_placeholder` (string, default: "Search..."): Placeholder for search input
+- `search_no_results_text` (string, default: "No results found for %{query}."): No results text
+- `search_threshold` (integer, default: 0): Minimum characters before filtering
+- `debounce` (integer, default: 300): Debounce time for server searches
+- `on_search` (string, default: nil): LiveView event name for server searches
+- `multiple` (boolean, default: false): Allows selecting multiple options (not with native)
+- `value` (any): Current selected value(s)
+- `errors` (list, default: []): List of error messages
+- `options` (list, required): List of options for the select
+- `max` (integer, default: nil): Maximum selections when multiple
+- `clearable` (boolean, default: false): Shows clear button
+- `rest` (global): Additional attributes
+
+### Slots
+- `option`: Optional slot for custom option rendering
+- `toggle`: Optional slot for custom toggle rendering
+- `header`: Optional slot for custom header content
+- `footer`: Optional slot for custom footer content
+- `inner_prefix`: Content inside select field before value display
+- `outer_prefix`: Content outside and before select field
+- `inner_suffix`: Content inside select field after value display
+- `outer_suffix`: Content outside and after select field
+
+### Usage Examples
+
+#### Basic and Full-featured Select
+```heex
+<.select
+ name="country"
+ options={[{"United States", "US"}, {"Canada", "CA"}]}
+/>
+
+<.select
+ name="payment_method"
+ label="Payment Method"
+ sublabel="Select payment type"
+ description="Choose your preferred payment method"
+ help_text="We securely process all payment information"
+ placeholder="Select payment method"
+ options={[
+ {"Credit Card", "credit_card"},
+ {"PayPal", "paypal"},
+ {"Bank Transfer", "bank_transfer"}
+ ]}
+/>
+```
+
+#### Native, Searchable and Multiple
+```heex
+<.select
+ name="country"
+ native
+ options={[{"United States", "US"}, {"Canada", "CA"}]}
+/>
+
+<.select
+ name="country"
+ searchable
+ search_input_placeholder="Search for a country"
+ search_no_results_text="No countries found for %{query}"
+ options={@countries}
+/>
+
+<.select
+ name="countries"
+ multiple
+ max={3}
+ clearable
+ options={[{"United States", "US"},{"Canada", "CA"},{"Mexico", "MX"}]}
+/>
+```
+
+#### Form Integration and Affixes
+```heex
+<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
+ <.select
+ field={f[:country]}
+ label="Country"
+ options={@countries}
+ />
+
+
+<.select name="category" options={@categories} placeholder="Select category">
+ <:inner_prefix>
+ <.icon name="hero-folder" class="icon" />
+
+ <:outer_suffix>
+ <.button size="md">Apply
+
+
+```
+
+#### Custom Option Rendering
+```heex
+<.select
+ name="role"
+ placeholder="Select role"
+ options={[{"Admin", "admin"}, {"Editor", "editor"}, {"Viewer", "viewer"}]}
+>
+ <:option :let={{label, value}}>
+
+
+
{label}
+
+ {case value do
+ "admin" -> "Full access to all features"
+ "editor" -> "Can create and modify content"
+ "viewer" -> "Read-only access to content"
+ end}
+
+
+ <.icon :if={value == "admin"} name="hero-shield-check" class="size-4 text-blue-500" />
+
+
+
+```
+
+## Separator
+
+A versatile separator component for creating visual boundaries between content sections.
+
+### Attributes
+- `text` (string, default: nil): Optional text to display in the center of the separator
+- `vertical` (boolean, default: false): Renders a vertical separator instead of horizontal
+- `class` (any, default: nil): Additional CSS classes
+
+### Usage Examples
+
+#### Basic Separators
+```heex
+Content above
+<.separator />
+Content below
+
+
+ Left
+ <.separator vertical />
+ Right
+
+
+<.separator text="or" />
+<.separator text="Section" class="my-6" />
+```
+
+#### Form and Navigation Usage
+```heex
+
+
+
+ Profile
+ <.separator vertical />
+ Settings
+ <.separator vertical />
+ Logout
+
+```
+
+## Sheet
+
+A powerful and accessible sheet component that provides a sliding panel interface for displaying content from screen edges.
+
+### Attributes
+- `id` (string, required): The unique identifier for the sheet component
+- `open` (boolean, default: false): Whether the sheet is initially open
+- `on_close` (JS, default: %JS{}): JavaScript commands to execute when sheet closes
+- `on_open` (JS, default: %JS{}): JavaScript commands to execute when sheet opens
+- `class` (any, default: ""): Additional CSS classes for sheet content container
+- `close_on_esc` (boolean, default: true): Whether to close sheet when Escape is pressed
+- `close_on_outside_click` (boolean, default: true): Whether to close when clicking outside
+- `prevent_closing` (boolean, default: false): Prevents sheet from being closed through standard interactions
+- `hide_close_button` (boolean, default: false): Whether to hide the close button
+- `animation` (string, default: "transition duration-200 ease-in-out"): Base animation classes
+- `animation_enter` (string): Classes applied when sheet enters (auto-set based on placement)
+- `animation_leave` (string): Classes applied when sheet leaves (auto-set based on placement)
+- `backdrop_class` (string, default: ""): Additional CSS classes for backdrop overlay
+- `placement` (string, default: "left"): Edge the sheet slides from - "left", "right", "top", "bottom"
+
+### Slots
+- `inner_block` (required): The content of the sheet
+
+### Usage Examples
+
+#### Basic Client-Side Control
+```heex
+<.button phx-click={Fluxon.open_dialog("filters-sheet")}>Filters
+
+<.sheet id="filters-sheet">
+ Filters
+
+
+
+
+
+ <.button phx-click={Fluxon.close_dialog("filters-sheet")}>Apply
+
+```
+
+#### Server-Side Control
+```heex
+<.button phx-click="show-sheet">Open Filters
+
+<.sheet id="filters-sheet" open={@show_filters}>
+ Server-controlled Sheet
+
+
+
+<.sheet
+ id="filters-sheet"
+ open={@show_filters}
+ on_close={JS.push("hide_filters")}
+>
+ Filters with State Sync
+
+```
+
+#### Different Placements
+```heex
+
+<.sheet id="nav-sheet" placement="left" class="w-80">
+
+ <.navlink href="/dashboard">Dashboard
+ <.navlink href="/projects">Projects
+
+
+
+
+<.sheet id="details-sheet" placement="right" class="w-96">
+ Details
+ Detailed information goes here.
+
+
+
+<.sheet id="actions-sheet" placement="bottom" class="h-96">
+
+ <.button class="w-full">Share
+ <.button class="w-full">Copy Link
+ <.button class="w-full">Delete
+
+
+```
+
+#### Form in Sheet
+```heex
+<.sheet id="new-user-sheet" placement="right" class="w-96">
+ <.form for={@form} phx-submit="save_user">
+
+
+
+ <.input field={@form[:name]} label="Name" />
+ <.input field={@form[:email]} type="email" label="Email" />
+
+
+ <.button phx-click={Fluxon.close_dialog("new-user-sheet")}>
+ Cancel
+
+ <.button type="submit" phx-disable-with="Creating...">
+ Create User
+
+
+
+
+
+```
+
+## Switch
+
+A toggle switch component for binary choices and settings with immediate effect.
+
+### Attributes
+- `id` (any, default: nil): The unique identifier for the switch
+- `name` (string, required when not using field): The form name for the switch
+- `class` (any, default: nil): Additional CSS classes for the switch wrapper
+- `checked` (boolean): Whether the switch is in the on position
+- `disabled` (boolean, default: false): Disables the switch
+- `value` (any): The value associated with the switch
+- `label` (string, default: nil): The primary label for the switch
+- `sublabel` (string, default: nil): Additional context beside the main label
+- `description` (string, default: nil): Detailed description below the label
+- `size` (string, default: "md"): Controls switch size - "sm", "md", "lg"
+- `color` (string, default: "primary"): Color theme when on - "primary", "danger", "success", "warning", "info"
+- `field` (Phoenix.HTML.FormField): The form field to bind to
+- `rest` (global): Additional attributes
+
+### Usage Examples
+
+#### Basic Switch and Size Variants
+```heex
+<.switch
+ name="notifications"
+ label="Enable Notifications"
+ checked={@settings.notifications}
+ phx-click="toggle_setting"
+ phx-value-setting="notifications"
+/>
+
+<.switch name="small_switch" label="Small Switch" size="sm" checked={@settings.compact_mode} />
+<.switch name="large_switch" label="Large Switch" size="lg" checked={@settings.important_setting} />
+```
+
+#### Labels and Context
+```heex
+<.switch name="simple" label="Enable Feature" checked={@feature_enabled} />
+
+<.switch
+ name="with_sublabel"
+ label="Auto Save"
+ sublabel="Recommended"
+ checked={@settings.auto_save}
+/>
+
+<.switch
+ name="comprehensive"
+ label="Advanced Analytics"
+ sublabel="Beta"
+ description="Share detailed usage patterns to help improve the product experience"
+ checked={@settings.analytics_enabled}
+/>
+```
+
+#### Color Variants and Form Integration
+```heex
+<.switch name="primary" label="Default Setting" color="primary" checked={@settings.default} />
+<.switch name="success" label="Enable Backup" color="success" checked={@settings.backup_enabled} />
+<.switch name="danger" label="Public Profile" color="danger" checked={@settings.public_profile} />
+
+<.form :let={f} for={@changeset} phx-change="validate" phx-submit="save">
+ <.switch
+ field={f[:notifications_enabled]}
+ label="Push Notifications"
+ sublabel="Real-time updates"
+ description="Receive notifications about important account activity"
+ />
+
+ <.switch
+ field={f[:email_marketing]}
+ label="Email Marketing"
+ sublabel="Optional"
+ description="Receive promotional emails about new features"
+ />
+
+```
+
+#### Disabled States
+```heex
+<.switch
+ name="disabled_off"
+ label="Unavailable Feature"
+ description="This feature is currently unavailable"
+ disabled
+ checked={false}
+/>
+
+<.switch
+ name="disabled_on"
+ label="Premium Feature"
+ sublabel="Upgrade required"
+ description="This feature requires a premium subscription"
+ disabled
+ checked={true}
+/>
+```
+
+## Table
+
+A comprehensive table system for displaying structured data with rich customization options.
+
+### Components
+- `table`: The main container providing structure and responsive behavior
+- `table_head`: Header section with column definitions
+- `table_body`: Content section containing rows of data
+- `table_row`: Individual data rows with cell content
+
+### Attributes
+- All components have `class` (optional) and `rest` attributes for customization
+
+### Slots
+
+#### table
+- `inner_block` (required): Usually contains table_head and table_body components
+
+#### table_head
+- `col` (required): Defines table columns rendered as th elements
+
+#### table_body
+- `inner_block` (required): Usually contains table_row components
+
+#### table_row
+- `cell` (required): Defines table cells with custom content and styling
+
+### Usage Examples
+
+#### Basic Table
+```heex
+<.table>
+ <.table_head>
+ <:col>Name
+ <:col>Status
+ <:col>Email
+
+
+ <.table_body>
+ <.table_row>
+ <:cell>Alice Smith
+ <:cell>New
+ <:cell>alice@example.com
+
+ <.table_row>
+ <:cell>Bob Johnson
+ <:cell>In Progress
+ <:cell>bob@example.com
+
+
+
+```
+
+#### Rich Content Table
+```heex
+<.table>
+ <.table_head>
+ <:col>Lead
+ <:col>Stage
+ <:col>Contact
+ <:col>
+
+ <.table_body>
+ <.table_row>
+ <:cell class="w-full flex items-center gap-2">
+
+
+ Sarah Johnson
+ Product Manager
+
+
+ <:cell>
+ <.badge color="green">Active
+
+ <:cell>sarah.j@example.com
+ <:cell>
+ <.icon name="hero-ellipsis-horizontal" class="size-5" />
+
+
+
+
+```
+
+#### Sortable Columns and Clickable Rows
+```heex
+<.table>
+ <.table_head>
+ <:col phx-click="sort" phx-value-column="name">
+
+ Lead <.icon name="hero-chevron-up-down" class="size-4 text-zinc-500" />
+
+
+ <:col phx-click="sort" phx-value-column="stage">
+
+ Stage <.icon name="hero-chevron-up-down" class="size-4 text-zinc-500" />
+
+
+
+ <.table_body>
+ <.table_row>
+ <:cell>John Smith
+ <:cell>New Lead
+
+
+
+
+<.table>
+ <.table_head>
+ <:col>Customer
+ <:col>Status
+ <:col>Last Order
+
+
+ <.table_body>
+ <.table_row
+ :for={customer <- @customers}
+ class="cursor-pointer hover:bg-zinc-50"
+ phx-click="show_customer"
+ phx-value-id={customer.id}
+ >
+ <:cell>
+
+
+
{customer.name}
+
+
+ <:cell><.badge color="green">Active
+ <:cell>{customer.last_order_date}
+
+
+
+```
+
+#### Data Table with Actions
+```heex
+<.table>
+ <.table_head>
+ <:col>User
+ <:col>Role
+ <:col>Status
+ <:col>Actions
+
+
+ <.table_body>
+ <.table_row :for={user <- @users}>
+ <:cell>
+
+
+
+
{user.name}
+
{user.email}
+
+
+
+ <:cell>
+
+ {user.role}
+
+
+ <:cell>
+ <.badge color={if user.active, do: "green", else: "red"}>
+ {if user.active, do: "Active", else: "Inactive"}
+
+
+ <:cell>
+
+ <.button size="sm" variant="ghost" phx-click="edit_user" phx-value-id={user.id}>
+ Edit
+
+ <.button size="sm" variant="ghost" color="danger" phx-click="delete_user" phx-value-id={user.id}>
+ Delete
+
+
+
+
+
+
+```
+
+#### Empty State Table
+```heex
+<.table>
+ <.table_head>
+ <:col>Name
+ <:col>Email
+ <:col>Status
+
+
+ <.table_body>
+ <.table_row :if={Enum.empty?(@users)}>
+ <:cell colspan="3" class="text-center py-12 text-zinc-500">
+
+ <.icon name="hero-users" class="size-12 text-zinc-300" />
+
+
No users found
+
Get started by creating your first user.
+
+ <.button size="sm" phx-click="new_user">Add User
+
+
+
+
+ <.table_row :for={user <- @users}>
+ <:cell>{user.name}
+ <:cell>{user.email}
+ <:cell>{user.status}
+
+
+
+```
+
+## Tabs
+
+A tabs system for creating accessible, interactive tabbed interfaces with keyboard navigation support.
+
+### Components
+- `tabs`: The main container providing structure and JavaScript functionality
+- `tabs_list`: Navigation container holding the interactive tab buttons
+- `tabs_panel`: Content panels associated with each tab, displayed one at a time
+
+### Attributes
+
+#### tabs
+- `id` (string): A unique identifier for the tabs container
+- `class` (any, default: nil): Additional CSS classes for the tabs container
+- `rest` (global): Additional HTML attributes
+
+#### tabs_list
+- `class` (any, default: nil): Additional CSS classes for the tablist container
+- `active_tab` (string): The name of the tab that should be initially active
+- `variant` (string, default: "default"): The visual style variant - "default", "segmented", "ghost"
+- `size` (string, default: "md"): The size of the tabs container - "xs", "sm", "md"
+
+#### tabs_panel
+- `name` (string, required): The unique identifier for this panel
+- `class` (any, default: nil): Additional CSS classes for the panel element
+- `active` (boolean, default: false): Controls the visibility of the panel
+- `rest` (global): Additional HTML attributes
+
+### Slots
+
+#### tabs
+- `inner_block` (required): Typically contains one tabs_list and one or more tabs_panel components
+
+#### tabs_list
+- `tab` (required): Defines an individual interactive tab button with required name attribute
+- `inner_block` (required): Main content area containing the tab slots
+
+#### tabs_panel
+- `inner_block` (required): The content displayed when corresponding tab is active
+
+### Usage Examples
+
+#### Basic Static Tabs
+```heex
+<.tabs id="my-tabs">
+ <.tabs_list active_tab="settings">
+ <:tab name="profile">Profile
+ <:tab name="settings">Settings
+ <:tab name="notifications">Notifications
+
+
+ <.tabs_panel name="profile">
+ Profile content here...
+
+
+ <.tabs_panel name="settings" active>
+ Settings content here...
+
+
+ <.tabs_panel name="notifications">
+ Notifications content here...
+
+
+```
+
+#### Visual Variants and Sizes
+```heex
+
+<.tabs_list variant="default">
+ <:tab name="tab1">Default Tab
+
+
+
+<.tabs_list variant="segmented">
+ <:tab name="tab1">Segmented Tab
+
+
+
+<.tabs_list variant="ghost">
+ <:tab name="tab1">Ghost Tab
+
+
+
+<.tabs_list size="xs">
+ <:tab name="tab1">Extra Small Tab
+
+
+<.tabs_list size="sm">
+ <:tab name="tab1">Small Tab
+
+```
+
+#### Rich Tab Content
+```heex
+<.tabs_list>
+ <:tab name="messages">
+ <.icon name="hero-envelope" class="icon" />
+ Messages
+ <.badge class="ml-2">3
+
+
+ <:tab name="settings">
+ <.icon name="hero-cog-6-tooth" class="icon" />
+ Settings
+
+
+```
+
+#### LiveView Integration
+```heex
+<.tabs id="lv-sync-tabs">
+ <.tabs_list active_tab={@active_tab}>
+ <:tab name="profile" phx-click={JS.push("set_active_tab", value: %{tab: "profile"})}>
+ Profile
+
+ <:tab name="settings" phx-click={JS.push("set_active_tab", value: %{tab: "settings"})}>
+ Settings
+
+
+
+ <.tabs_panel name="profile" active={@active_tab == "profile"}>
+ Profile content...
+
+ <.tabs_panel name="settings" active={@active_tab == "settings"}>
+ Settings content...
+
+
+```
+
+#### Complex Form Tabs
+```heex
+<.tabs id="user-form-tabs">
+ <.tabs_list active_tab={@active_tab} variant="segmented">
+ <:tab name="basic" phx-click={JS.push("set_tab", value: %{tab: "basic"})}>
+ <.icon name="hero-user" class="icon" />
+ Basic Info
+
+ <:tab name="contact" phx-click={JS.push("set_tab", value: %{tab: "contact"})}>
+ <.icon name="hero-envelope" class="icon" />
+ Contact
+
+
+
+ <.tabs_panel name="basic" active={@active_tab == "basic"} class="space-y-4">
+ <.input field={@form[:name]} label="Full Name" />
+ <.input field={@form[:username]} label="Username" />
+
+
+ <.tabs_panel name="contact" active={@active_tab == "contact"} class="space-y-4">
+ <.input field={@form[:email]} type="email" label="Email" />
+ <.input field={@form[:phone]} label="Phone Number" />
+
+
+```
+
+## Textarea
+
+A versatile textarea component for capturing multi-line text input with form integration and size variants.
+
+### Attributes
+- `id` (string, default: nil): The unique identifier for the textarea
+- `field` (Phoenix.HTML.FormField): The form field to bind to
+- `class` (any, default: nil): Additional CSS classes for the textarea
+- `help_text` (string, default: nil): Optional help text displayed below the textarea
+- `label` (string, default: nil): The primary label for the textarea
+- `sublabel` (string, default: nil): Additional context beside the main label
+- `description` (string, default: nil): Detailed description below the label
+- `value` (string): The current value of the textarea
+- `errors` (list, default: []): List of error messages
+- `name` (string, required when not using field): The form name for the textarea
+- `rows` (integer, default: 3): The number of visible text lines
+- `disabled` (boolean, default: false): Disables the textarea
+- `size` (string, default: "md"): Controls textarea size - "sm", "md", "lg", "xl"
+- `rest` (global): Additional HTML attributes
+
+### Usage Examples
+
+#### Basic Textarea and Size Variants
+```heex
+<.textarea
+ name="description"
+ label="Description"
+ placeholder="Enter description..."
+/>
+
+<.textarea name="description" label="Small" size="sm" placeholder="A compact textarea" />
+<.textarea name="description" label="Large" size="lg" placeholder="Larger textarea" />
+<.textarea name="description" label="Extra Large" size="xl" placeholder="Maximum emphasis" />
+```
+
+#### With Labels and Form Integration
+```heex
+<.textarea
+ name="bio"
+ label="Biography"
+ help_text="Tell us about yourself"
+ rows={5}
+/>
+
+<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
+ <.textarea
+ field={f[:description]}
+ label="Description"
+ help_text="Provide a detailed description of your project"
+ />
+
+
+<.form :let={f} for={@form} phx-change="validate">
+ <.textarea
+ field={f[:description]}
+ label="Project Description"
+ sublabel="Optional"
+ description="Provide details about your project's goals and scope"
+ help_text="Be specific and concise"
+ rows={6}
+ />
+
+```
+
+#### Custom Styling and States
+```heex
+<.textarea
+ name="notes"
+ label="Meeting Notes"
+ size="lg"
+ class="font-mono"
+ rows={10}
+/>
+
+<.textarea
+ name="readonly_field"
+ label="Read Only Field"
+ value="This content cannot be edited"
+ disabled
+/>
+
+<.textarea
+ name="content"
+ label="Article Content"
+ rows={15}
+ maxlength={5000}
+ placeholder="Write your article content here..."
+ spellcheck="true"
+/>
+```
+
+#### Rich Form Example
+```heex
+<.form :let={f} for={@form} phx-change="validate" phx-submit="save">
+
+ <.textarea
+ field={f[:summary]}
+ label="Summary"
+ sublabel="Required"
+ rows={3}
+ maxlength={500}
+ help_text="Brief overview of the content"
+ />
+
+ <.textarea
+ field={f[:content]}
+ label="Full Content"
+ description="The main body of your article or post"
+ rows={12}
+ help_text="Use markdown formatting if needed"
+ />
+
+ <.textarea
+ field={f[:notes]}
+ label="Internal Notes"
+ sublabel="Optional"
+ size="sm"
+ rows={2}
+ help_text="Private notes for internal use"
+ />
+
+
+ <.button type="submit">Save Article
+
+```
+
+## Tooltip
+
+A lightweight and accessible tooltip component for displaying informative content on hover or focus.
+
+### Attributes
+- `id` (string, optional): Unique identifier for the tooltip
+- `value` (string, optional): Text content for simple tooltips
+- `class` (any, optional): Additional CSS classes for the tooltip container
+- `arrow` (boolean, default: true): Whether to show the pointing arrow indicator
+- `placement` (string, default: "top"): Tooltip position - "bottom", "left", "top", "right"
+- `delay` (integer, default: 0): Delay in milliseconds before showing tooltip
+
+### Slots
+- `inner_block` (required): The trigger element that shows tooltip on hover/focus
+- `content` (optional): Rich content slot for complex tooltip content
+
+### Usage Examples
+
+#### Basic and Rich Content
+```heex
+<.tooltip value="Opens in a new window">
+ <.button>Open
+
+
+<.tooltip>
+ <.button>View details
+
+ <:content>
+
+
+
Preview of the document layout and structure.
+
+
+
+```
+
+#### Different Placements and Icon Tooltips
+```heex
+
+ <.tooltip value="Top placement" placement="top">
+ <.button>Top
+
+
+ <.tooltip value="Right placement" placement="right">
+ <.button>Right
+
+
+
+
+ <.tooltip value="Share">
+ <.button variant="ghost"><.icon name="hero-share" />
+
+
+ <.tooltip value="Add to favorites">
+ <.button variant="ghost"><.icon name="hero-star" />
+
+
+```
+
+#### Form Field Help and Custom Styling
+```heex
+
+ <.input type="text" name="api_key" value="">
+ <:inner_suffix>
+ <.tooltip value="Your API key can be found in the developer settings">
+ <.icon name="hero-question-mark-circle" class="text-zinc-400" />
+
+
+
+
+
+<.tooltip
+ value="Draft saved"
+ class="bg-green-600 text-white"
+ arrow={false}
+>
+ <.badge>Draft
+
+
+<.tooltip value="Archived items are hidden from the main view" delay={300}>
+ <.icon name="hero-archive" class="text-zinc-400" />
+
+```
+
+#### User Profile Preview
+```heex
+<.tooltip class="max-w-xs">
+ <.link navigate={"/users/#{@user.id}"}>
+ {@user.name}
+
+
+ <:content>
+
+
{@user.name}
+
{@user.title}
+
{@user.department}
+
+
+
+```
diff --git a/.claude/skills/ui-framework/references/html.md b/.claude/skills/ui-framework/references/html.md
new file mode 100644
index 00000000..e47e7964
--- /dev/null
+++ b/.claude/skills/ui-framework/references/html.md
@@ -0,0 +1,76 @@
+## Phoenix HTML guidelines
+
+- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E`
+- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated
+- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]`
+- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`)
+- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name)
+
+- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`**. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals.
+
+ **Never do this (invalid)**:
+
+ <%= if condition do %>
+ ...
+ <% else if other_condition %>
+ ...
+ <% end %>
+
+ Instead **always** do this:
+
+ <%= cond do %>
+ <% condition -> %>
+ ...
+ <% condition2 -> %>
+ ...
+ <% true -> %>
+ ...
+ <% end %>
+
+- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `` or `` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
+
+
+ let obj = {key: "val"}
+
+
+ Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
+
+- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
+
+ Text
+
+ and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
+
+ and **never** do this, since it's invalid (note the missing `[` and `]`):
+
+ ...
+ => Raises compile syntax error on invalid HEEx attr syntax
+
+- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
+- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
+- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
+
+ **Always** do this:
+
+
+ {@my_assign}
+ <%= if @some_block_condition do %>
+ {@another_assign}
+ <% end %>
+
+
+ and **Never** do this – the program will terminate with a syntax error:
+
+ <%!-- THIS IS INVALID NEVER EVER DO THIS --%>
+
+ {if @invalid_block_construct do}
+ {end}
+
diff --git a/.claude/skills/ui-framework/references/liveview.md b/.claude/skills/ui-framework/references/liveview.md
new file mode 100644
index 00000000..96e83b7d
--- /dev/null
+++ b/.claude/skills/ui-framework/references/liveview.md
@@ -0,0 +1,231 @@
+## Phoenix LiveView guidelines
+
+- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews
+- **Avoid LiveComponent's** unless you have a strong, specific need for them
+- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive`
+
+### LiveView streams
+
+- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations:
+ - basic append of N items - `stream(socket, :messages, [new_msg])`
+ - resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items)
+ - prepend to stream - `stream(socket, :messages, [new_msg], at: -1)`
+ - deleting items - `stream_delete(socket, :messages, msg)`
+
+- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be:
+
+
+
+- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**:
+
+ def handle_event("filter", %{"filter" => filter}, socket) do
+ # re-fetch the messages based on the filter
+ messages = list_messages(filter)
+
+ {:noreply,
+ socket
+ |> assign(:messages_empty?, messages == [])
+ # reset the stream with the new messages
+ |> stream(:messages, messages, reset: true)}
+ end
+
+- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes:
+
+
+
No tasks yet
+
+ {task.name}
+
+
+
+ The above only works if the empty state is the only HTML block alongside the stream for-comprehension.
+
+- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items
+ along with the updated assign:
+
+ def handle_event("edit_message", %{"message_id" => message_id}, socket) do
+ message = Chat.get_message!(message_id)
+ edit_form = to_form(Chat.change_message(message, %{content: message.content}))
+
+ # re-insert message so @editing_message_id toggle logic takes effect for that stream item
+ {:noreply,
+ socket
+ |> stream_insert(:messages, message)
+ |> assign(:editing_message_id, String.to_integer(message_id))
+ |> assign(:edit_form, edit_form)}
+ end
+
+ And in the template:
+
+
+
+ {message.username}
+ <%= if @editing_message_id == message.id do %>
+ <%!-- Edit mode --%>
+ <.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit">
+ ...
+
+ <% end %>
+
+
+
+- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections
+
+### LiveView JavaScript interop
+
+- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute
+- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised
+
+LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx,
+and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor.
+
+#### Inline colocated js hooks
+
+**Never** write raw embedded `
+
+- colocated hooks are automatically integrated into the app.js bundle
+- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber`
+
+#### External phx-hook
+
+External JS hooks (``) must be placed in `assets/js/` and passed to the
+LiveSocket constructor:
+
+ const MyHook = {
+ mounted() { ... }
+ }
+ let liveSocket = new LiveSocket("/live", Socket, {
+ hooks: { MyHook }
+ });
+
+#### Pushing events between client and server
+
+Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle.
+**Always** return or rebind the socket on `push_event/3` when pushing events:
+
+ # re-bind socket so we maintain event state to be pushed
+ socket = push_event(socket, "my_event", %{...})
+
+ # or return the modified socket directly:
+ def handle_event("some_event", _, socket) do
+ {:noreply, push_event(socket, "my_event", %{...})}
+ end
+
+Pushed events can then be picked up in a JS hook with `this.handleEvent`:
+
+ mounted() {
+ this.handleEvent("my_event", data => console.log("from server:", data));
+ }
+
+Clients can also push an event to the server and receive a reply with `this.pushEvent`:
+
+ mounted() {
+ this.el.addEventListener("click", e => {
+ this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply));
+ })
+ }
+
+Where the server handled it via:
+
+ def handle_event("my_event", %{"one" => 1}, socket) do
+ {:reply, %{two: 2}, socket}
+ end
+
+### LiveView tests
+
+- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions
+- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions
+- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests
+- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc
+- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")`
+- Instead of relying on testing text content, which can change, favor testing for the presence of key elements
+- Focus on testing outcomes rather than implementation details
+- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be
+- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie:
+
+ html = render(view)
+ document = LazyHTML.from_fragment(html)
+ matches = LazyHTML.filter(document, "your-complex-selector")
+ IO.inspect(matches, label: "Matches")
+
+### Form handling
+
+#### Creating a form from params
+
+If you want to create a form based on `handle_event` params:
+
+ def handle_event("submitted", params, socket) do
+ {:noreply, assign(socket, form: to_form(params))}
+ end
+
+When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys.
+
+You can also specify a name to nest the params:
+
+ def handle_event("submitted", %{"user" => user_params}, socket) do
+ {:noreply, assign(socket, form: to_form(user_params, as: :user))}
+ end
+
+#### Creating a form from changesets
+
+When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema:
+
+ defmodule MyApp.Users.User do
+ use Ecto.Schema
+ ...
+ end
+
+And then you create a changeset that you pass to `to_form`:
+
+ %MyApp.Users.User{}
+ |> Ecto.Changeset.change()
+ |> to_form()
+
+Once the form is submitted, the params will be available under `%{"user" => user_params}`.
+
+In the template, the form form assign can be passed to the `<.form>` function component:
+
+ <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save">
+ <.input field={@form[:field]} type="text" />
+
+
+Always give the form an explicit, unique DOM ID, like `id="todo-form"`.
+
+#### Avoiding form errors
+
+**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**:
+
+ <%!-- ALWAYS do this (valid) --%>
+ <.form for={@form} id="my-form">
+ <.input field={@form[:field]} type="text" />
+
+
+And **never** do this:
+
+ <%!-- NEVER do this (invalid) --%>
+ <.form for={@changeset} id="my-form">
+ <.input field={@changeset[:field]} type="text" />
+
+
+- You are FORBIDDEN from accessing the changeset in the template as it will cause errors
+- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset
diff --git a/.claude/skills/ui-framework/references/phoenix.md b/.claude/skills/ui-framework/references/phoenix.md
new file mode 100644
index 00000000..79c2c495
--- /dev/null
+++ b/.claude/skills/ui-framework/references/phoenix.md
@@ -0,0 +1,15 @@
+## Phoenix guidelines
+
+- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
+
+- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie:
+
+ scope "/admin", AppWeb.Admin do
+ pipe_through :browser
+
+ live "/users", UserLive, :index
+ end
+
+ the UserLive route would point to the `AppWeb.Admin.UserLive` module
+
+- `Phoenix.View` no longer is needed or included with Phoenix, don't use it
diff --git a/AGENTS.md b/AGENTS.md
index e4781f5e..d9c490e6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -113,55 +113,6 @@ mix usage_rules.search_docs "Enum.zip" --query-by title
- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console.
-
-## usage_rules usage
-_A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills from dependencies_
-
-## Using Usage Rules
-
-Many packages have usage rules, which you should *thoroughly* consult before taking any
-action. These usage rules contain guidelines and rules *directly from the package authors*.
-They are your best source of knowledge for making decisions.
-
-## Modules & functions in the current app and dependencies
-
-When looking for docs for modules & functions that are dependencies of the current project,
-or for Elixir itself, use `mix usage_rules.docs`
-
-```
-# Search a whole module
-mix usage_rules.docs Enum
-
-# Search a specific function
-mix usage_rules.docs Enum.zip
-
-# Search a specific function & arity
-mix usage_rules.docs Enum.zip/1
-```
-
-
-## Searching Documentation
-
-You should also consult the documentation of any tools you are using, early and often. The best
-way to accomplish this is to use the `usage_rules.search_docs` mix task. Once you have
-found what you are looking for, use the links in the search results to get more detail. For example:
-
-```
-# Search docs for all packages in the current application, including Elixir
-mix usage_rules.search_docs Enum.zip
-
-# Search docs for specific packages
-mix usage_rules.search_docs Req.get -p req
-
-# Search docs for multi-word queries
-mix usage_rules.search_docs "making requests" -p req
-
-# Search only in titles (useful for finding specific functions/modules)
-mix usage_rules.search_docs "Enum.zip" --query-by title
-```
-
-
-
## usage_rules:otp usage
# OTP Usage Rules
@@ -189,682 +140,4 @@ mix usage_rules.search_docs "Enum.zip" --query-by title
- Use `Task.async_stream/3` for concurrent enumeration with back-pressure
-
-## phoenix:ecto usage
-## Ecto Guidelines
-
-- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email`
-- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs`
-- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string`
-- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed
-- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields
-- Fields which are set programmatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct
-- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied
-
-
-
-## phoenix:elixir usage
-## Elixir guidelines
-
-- Elixir lists **do not support index based access via the access syntax**
-
- **Never do this (invalid)**:
-
- i = 0
- mylist = ["blue", "green"]
- mylist[i]
-
- Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie:
-
- i = 0
- mylist = ["blue", "green"]
- Enum.at(mylist, i)
-
-- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc
- you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:
-
- # INVALID: we are rebinding inside the `if` and the result never gets assigned
- if connected?(socket) do
- socket = assign(socket, :val, val)
- end
-
- # VALID: we rebind the result of the `if` to a new variable
- socket =
- if connected?(socket) do
- assign(socket, :val, val)
- end
-
-- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
-- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets
-- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package)
-- Don't use `String.to_atom/1` on user input (memory leak risk)
-- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
-- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)`
-- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
-
-## Mix guidelines
-
-- Read the docs and options before using tasks (by using `mix help task_name`)
-- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
-- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
-
-## Test guidelines
-
-- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests
-- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests
- - Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message:
-
- ref = Process.monitor(pid)
- assert_receive {:DOWN, ^ref, :process, ^pid, :normal}
-
- - Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages
-
-
-
-
-## phoenix:html usage
-## Phoenix HTML guidelines
-
-- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E`
-- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated
-- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]`
-- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`)
-- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name)
-
-- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`**. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals.
-
- **Never do this (invalid)**:
-
- <%= if condition do %>
- ...
- <% else if other_condition %>
- ...
- <% end %>
-
- Instead **always** do this:
-
- <%= cond do %>
- <% condition -> %>
- ...
- <% condition2 -> %>
- ...
- <% true -> %>
- ...
- <% end %>
-
-- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `
` or `` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
-
-
- let obj = {key: "val"}
-
-
- Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
-
-- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
-
- Text
-
- and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
-
- and **never** do this, since it's invalid (note the missing `[` and `]`):
-
- ...
- => Raises compile syntax error on invalid HEEx attr syntax
-
-- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
-- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
-- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
-
- **Always** do this:
-
-
- {@my_assign}
- <%= if @some_block_condition do %>
- {@another_assign}
- <% end %>
-
-
- and **Never** do this – the program will terminate with a syntax error:
-
- <%!-- THIS IS INVALID NEVER EVER DO THIS --%>
-
- {if @invalid_block_construct do}
- {end}
-
-
-
-
-## phoenix:liveview usage
-## Phoenix LiveView guidelines
-
-- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews
-- **Avoid LiveComponent's** unless you have a strong, specific need for them
-- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive`
-
-### LiveView streams
-
-- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations:
- - basic append of N items - `stream(socket, :messages, [new_msg])`
- - resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items)
- - prepend to stream - `stream(socket, :messages, [new_msg], at: -1)`
- - deleting items - `stream_delete(socket, :messages, msg)`
-
-- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be:
-
-
-
-- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**:
-
- def handle_event("filter", %{"filter" => filter}, socket) do
- # re-fetch the messages based on the filter
- messages = list_messages(filter)
-
- {:noreply,
- socket
- |> assign(:messages_empty?, messages == [])
- # reset the stream with the new messages
- |> stream(:messages, messages, reset: true)}
- end
-
-- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes:
-
-
-
No tasks yet
-
- {task.name}
-
-
-
- The above only works if the empty state is the only HTML block alongside the stream for-comprehension.
-
-- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items
- along with the updated assign:
-
- def handle_event("edit_message", %{"message_id" => message_id}, socket) do
- message = Chat.get_message!(message_id)
- edit_form = to_form(Chat.change_message(message, %{content: message.content}))
-
- # re-insert message so @editing_message_id toggle logic takes effect for that stream item
- {:noreply,
- socket
- |> stream_insert(:messages, message)
- |> assign(:editing_message_id, String.to_integer(message_id))
- |> assign(:edit_form, edit_form)}
- end
-
- And in the template:
-
-
-
- {message.username}
- <%= if @editing_message_id == message.id do %>
- <%!-- Edit mode --%>
- <.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit">
- ...
-
- <% end %>
-
-
-
-- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections
-
-### LiveView JavaScript interop
-
-- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute
-- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised
-
-LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx,
-and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor.
-
-#### Inline colocated js hooks
-
-**Never** write raw embedded `
-
-- colocated hooks are automatically integrated into the app.js bundle
-- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber`
-
-#### External phx-hook
-
-External JS hooks (``) must be placed in `assets/js/` and passed to the
-LiveSocket constructor:
-
- const MyHook = {
- mounted() { ... }
- }
- let liveSocket = new LiveSocket("/live", Socket, {
- hooks: { MyHook }
- });
-
-#### Pushing events between client and server
-
-Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle.
-**Always** return or rebind the socket on `push_event/3` when pushing events:
-
- # re-bind socket so we maintain event state to be pushed
- socket = push_event(socket, "my_event", %{...})
-
- # or return the modified socket directly:
- def handle_event("some_event", _, socket) do
- {:noreply, push_event(socket, "my_event", %{...})}
- end
-
-Pushed events can then be picked up in a JS hook with `this.handleEvent`:
-
- mounted() {
- this.handleEvent("my_event", data => console.log("from server:", data));
- }
-
-Clients can also push an event to the server and receive a reply with `this.pushEvent`:
-
- mounted() {
- this.el.addEventListener("click", e => {
- this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply));
- })
- }
-
-Where the server handled it via:
-
- def handle_event("my_event", %{"one" => 1}, socket) do
- {:reply, %{two: 2}, socket}
- end
-
-### LiveView tests
-
-- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions
-- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions
-- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests
-- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc
-- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")`
-- Instead of relying on testing text content, which can change, favor testing for the presence of key elements
-- Focus on testing outcomes rather than implementation details
-- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be
-- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie:
-
- html = render(view)
- document = LazyHTML.from_fragment(html)
- matches = LazyHTML.filter(document, "your-complex-selector")
- IO.inspect(matches, label: "Matches")
-
-### Form handling
-
-#### Creating a form from params
-
-If you want to create a form based on `handle_event` params:
-
- def handle_event("submitted", params, socket) do
- {:noreply, assign(socket, form: to_form(params))}
- end
-
-When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys.
-
-You can also specify a name to nest the params:
-
- def handle_event("submitted", %{"user" => user_params}, socket) do
- {:noreply, assign(socket, form: to_form(user_params, as: :user))}
- end
-
-#### Creating a form from changesets
-
-When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema:
-
- defmodule MyApp.Users.User do
- use Ecto.Schema
- ...
- end
-
-And then you create a changeset that you pass to `to_form`:
-
- %MyApp.Users.User{}
- |> Ecto.Changeset.change()
- |> to_form()
-
-Once the form is submitted, the params will be available under `%{"user" => user_params}`.
-
-In the template, the form form assign can be passed to the `<.form>` function component:
-
- <.form for={@form} id="todo-form" phx-change="validate" phx-submit="save">
- <.input field={@form[:field]} type="text" />
-
-
-Always give the form an explicit, unique DOM ID, like `id="todo-form"`.
-
-#### Avoiding form errors
-
-**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**:
-
- <%!-- ALWAYS do this (valid) --%>
- <.form for={@form} id="my-form">
- <.input field={@form[:field]} type="text" />
-
-
-And **never** do this:
-
- <%!-- NEVER do this (invalid) --%>
- <.form for={@changeset} id="my-form">
- <.input field={@changeset[:field]} type="text" />
-
-
-- You are FORBIDDEN from accessing the changeset in the template as it will cause errors
-- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset
-
-
-
-## phoenix:phoenix usage
-## Phoenix guidelines
-
-- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
-
-- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie:
-
- scope "/admin", AppWeb.Admin do
- pipe_through :browser
-
- live "/users", UserLive, :index
- end
-
- the UserLive route would point to the `AppWeb.Admin.UserLive` module
-
-- `Phoenix.View` no longer is needed or included with Phoenix, don't use it
-
-
-
-## usage_rules usage
-_A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills from dependencies_
-
-## Using Usage Rules
-
-Many packages have usage rules, which you should *thoroughly* consult before taking any
-action. These usage rules contain guidelines and rules *directly from the package authors*.
-They are your best source of knowledge for making decisions.
-
-## Modules & functions in the current app and dependencies
-
-When looking for docs for modules & functions that are dependencies of the current project,
-or for Elixir itself, use `mix usage_rules.docs`
-
-```
-# Search a whole module
-mix usage_rules.docs Enum
-
-# Search a specific function
-mix usage_rules.docs Enum.zip
-
-# Search a specific function & arity
-mix usage_rules.docs Enum.zip/1
-```
-
-
-## Searching Documentation
-
-You should also consult the documentation of any tools you are using, early and often. The best
-way to accomplish this is to use the `usage_rules.search_docs` mix task. Once you have
-found what you are looking for, use the links in the search results to get more detail. For example:
-
-```
-# Search docs for all packages in the current application, including Elixir
-mix usage_rules.search_docs Enum.zip
-
-# Search docs for specific packages
-mix usage_rules.search_docs Req.get -p req
-
-# Search docs for multi-word queries
-mix usage_rules.search_docs "making requests" -p req
-
-# Search only in titles (useful for finding specific functions/modules)
-mix usage_rules.search_docs "Enum.zip" --query-by title
-```
-
-
-
-
-## usage_rules:elixir usage
-# Elixir Core Usage Rules
-
-## Pattern Matching
-- Use pattern matching over conditional logic when possible
-- Prefer to match on function heads instead of using `if`/`else` or `case` in function bodies
-- `%{}` matches ANY map, not just empty maps. Use `map_size(map) == 0` guard to check for truly empty maps
-
-## Error Handling
-- Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail
-- Avoid raising exceptions for control flow
-- Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}`
-
-## Common Mistakes to Avoid
-- Elixir has no `return` statement, nor early returns. The last expression in a block is always returned.
-- Don't use `Enum` functions on large collections when `Stream` is more appropriate
-- Avoid nested `case` statements - refactor to a single `case`, `with` or separate functions
-- Don't use `String.to_atom/1` on user input (memory leak risk)
-- Lists and enumerables cannot be indexed with brackets. Use pattern matching or `Enum` functions
-- Prefer `Enum` functions like `Enum.reduce` over recursion
-- When recursion is necessary, prefer to use pattern matching in function heads for base case detection
-- Using the process dictionary is typically a sign of unidiomatic code
-- Only use macros if explicitly requested
-- There are many useful standard library functions, prefer to use them where possible
-
-## Function Design
-- Use guard clauses: `when is_binary(name) and byte_size(name) > 0`
-- Prefer multiple function clauses over complex conditional logic
-- Name functions descriptively: `calculate_total_price/2` not `calc/2`
-- Predicate function names should not start with `is` and should end in a question mark.
-- Names like `is_thing` should be reserved for guards
-
-## Data Structures
-- Use structs over maps when the shape is known: `defstruct [:name, :age]`
-- Prefer keyword lists for options: `[timeout: 5000, retries: 3]`
-- Use maps for dynamic key-value data
-- Prefer to prepend to lists `[new | list]` not `list ++ [new]`
-
-## Mix Tasks
-
-- Use `mix help` to list available mix tasks
-- Use `mix help task_name` to get docs for an individual task
-- Read the docs and options fully before using tasks
-
-## Testing
-- Run tests in a specific file with `mix test test/my_test.exs` and a specific test with the line number `mix test path/to/test.exs:123`
-- Limit the number of failed tests with `mix test --max-failures n`
-- Use `@tag` to tag specific tests, and `mix test --only tag` to run only those tests
-- Use `assert_raise` for testing expected exceptions: `assert_raise ArgumentError, fn -> invalid_function() end`
-- Use `mix help test` to for full documentation on running tests
-
-## Debugging
-
-- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console.
-
-
-
-## usage_rules:otp usage
-# OTP Usage Rules
-
-## GenServer Best Practices
-- Keep state simple and serializable
-- Handle all expected messages explicitly
-- Use `handle_continue/2` for post-init work
-- Implement proper cleanup in `terminate/2` when necessary
-
-## Process Communication
-- Use `GenServer.call/3` for synchronous requests expecting replies
-- Use `GenServer.cast/2` for fire-and-forget messages.
-- When in doubt, use `call` over `cast`, to ensure back-pressure
-- Set appropriate timeouts for `call/3` operations
-
-## Fault Tolerance
-- Set up processes such that they can handle crashing and being restarted by supervisors
-- Use `:max_restarts` and `:max_seconds` to prevent restart loops
-
-## Task and Async
-- Use `Task.Supervisor` for better fault tolerance
-- Handle task failures with `Task.yield/2` or `Task.shutdown/2`
-- Set appropriate task timeouts
-- Use `Task.async_stream/3` for concurrent enumeration with back-pressure
-
-
-
-## usage_rules usage
-_A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills from dependencies_
-
-## Using Usage Rules
-
-Many packages have usage rules, which you should *thoroughly* consult before taking any
-action. These usage rules contain guidelines and rules *directly from the package authors*.
-They are your best source of knowledge for making decisions.
-
-## Modules & functions in the current app and dependencies
-
-When looking for docs for modules & functions that are dependencies of the current project,
-or for Elixir itself, use `mix usage_rules.docs`
-
-```
-# Search a whole module
-mix usage_rules.docs Enum
-
-# Search a specific function
-mix usage_rules.docs Enum.zip
-
-# Search a specific function & arity
-mix usage_rules.docs Enum.zip/1
-```
-
-
-## Searching Documentation
-
-You should also consult the documentation of any tools you are using, early and often. The best
-way to accomplish this is to use the `usage_rules.search_docs` mix task. Once you have
-found what you are looking for, use the links in the search results to get more detail. For example:
-
-```
-# Search docs for all packages in the current application, including Elixir
-mix usage_rules.search_docs Enum.zip
-
-# Search docs for specific packages
-mix usage_rules.search_docs Req.get -p req
-
-# Search docs for multi-word queries
-mix usage_rules.search_docs "making requests" -p req
-
-# Search only in titles (useful for finding specific functions/modules)
-mix usage_rules.search_docs "Enum.zip" --query-by title
-```
-
-
-
-
-## usage_rules:elixir usage
-# Elixir Core Usage Rules
-
-## Pattern Matching
-- Use pattern matching over conditional logic when possible
-- Prefer to match on function heads instead of using `if`/`else` or `case` in function bodies
-- `%{}` matches ANY map, not just empty maps. Use `map_size(map) == 0` guard to check for truly empty maps
-
-## Error Handling
-- Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail
-- Avoid raising exceptions for control flow
-- Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}`
-
-## Common Mistakes to Avoid
-- Elixir has no `return` statement, nor early returns. The last expression in a block is always returned.
-- Don't use `Enum` functions on large collections when `Stream` is more appropriate
-- Avoid nested `case` statements - refactor to a single `case`, `with` or separate functions
-- Don't use `String.to_atom/1` on user input (memory leak risk)
-- Lists and enumerables cannot be indexed with brackets. Use pattern matching or `Enum` functions
-- Prefer `Enum` functions like `Enum.reduce` over recursion
-- When recursion is necessary, prefer to use pattern matching in function heads for base case detection
-- Using the process dictionary is typically a sign of unidiomatic code
-- Only use macros if explicitly requested
-- There are many useful standard library functions, prefer to use them where possible
-
-## Function Design
-- Use guard clauses: `when is_binary(name) and byte_size(name) > 0`
-- Prefer multiple function clauses over complex conditional logic
-- Name functions descriptively: `calculate_total_price/2` not `calc/2`
-- Predicate function names should not start with `is` and should end in a question mark.
-- Names like `is_thing` should be reserved for guards
-
-## Data Structures
-- Use structs over maps when the shape is known: `defstruct [:name, :age]`
-- Prefer keyword lists for options: `[timeout: 5000, retries: 3]`
-- Use maps for dynamic key-value data
-- Prefer to prepend to lists `[new | list]` not `list ++ [new]`
-
-## Mix Tasks
-
-- Use `mix help` to list available mix tasks
-- Use `mix help task_name` to get docs for an individual task
-- Read the docs and options fully before using tasks
-
-## Testing
-- Run tests in a specific file with `mix test test/my_test.exs` and a specific test with the line number `mix test path/to/test.exs:123`
-- Limit the number of failed tests with `mix test --max-failures n`
-- Use `@tag` to tag specific tests, and `mix test --only tag` to run only those tests
-- Use `assert_raise` for testing expected exceptions: `assert_raise ArgumentError, fn -> invalid_function() end`
-- Use `mix help test` to for full documentation on running tests
-
-## Debugging
-
-- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console.
-
-
-
-## usage_rules:otp usage
-# OTP Usage Rules
-
-## GenServer Best Practices
-- Keep state simple and serializable
-- Handle all expected messages explicitly
-- Use `handle_continue/2` for post-init work
-- Implement proper cleanup in `terminate/2` when necessary
-
-## Process Communication
-- Use `GenServer.call/3` for synchronous requests expecting replies
-- Use `GenServer.cast/2` for fire-and-forget messages.
-- When in doubt, use `call` over `cast`, to ensure back-pressure
-- Set appropriate timeouts for `call/3` operations
-
-## Fault Tolerance
-- Set up processes such that they can handle crashing and being restarted by supervisors
-- Use `:max_restarts` and `:max_seconds` to prevent restart loops
-
-## Task and Async
-- Use `Task.Supervisor` for better fault tolerance
-- Handle task failures with `Task.yield/2` or `Task.shutdown/2`
-- Set appropriate task timeouts
-- Use `Task.async_stream/3` for concurrent enumeration with back-pressure
-
-
-
-## fluxon usage
-_Fluxon UI Components_
-
-@deps/fluxon/usage-rules.md
-
diff --git a/mix.exs b/mix.exs
index d0a4daab..d44674c4 100644
--- a/mix.exs
+++ b/mix.exs
@@ -179,13 +179,21 @@ defmodule MusicLibrary.MixProject do
[
file: "AGENTS.md",
usage_rules: [
- :elixir,
- :otp,
- "phoenix:all",
- :usage_rules,
- "usage_rules:all",
- # link fluxon as it's HUGE
- {:fluxon, link: :at}
+ "usage_rules:all"
+ ],
+ skills: [
+ location: ".claude/skills",
+ build: [
+ "ui-framework": [
+ description:
+ "Use this skill when working on the web layer and UI components using the Phoenix framework",
+ usage_rules: [
+ :phoenix,
+ ~r/^phoenix_/,
+ :fluxon
+ ]
+ ]
+ ]
]
]
end