Repository: woylie/doggo
Pull request #323 (2024-07-23 05:05:33)

input macro
- **turn input into macro**
- **remove obsolete components**
- **update input story**
- **pass gettext module as build option**
- **make classes configurable**



Patch:
diff --git a/README.md b/README.md
index 23c7b06..c337093 100644
--- a/README.md
+++ b/README.md
@@ -95,18 +95,6 @@ This button could be used with:
 Refer to the `Doggo.Components` module documentation for more information about
 the options and the individual components.
 
-### Gettext
-
-To allow Doggo to translate certain strings such as form field errors with
-Gettext, set your Gettext module in `config/config.exs`:
-
-```elixir
-config :doggo, gettext: MyApp.Gettext
-```
-
-This only affects the `input` component. If you prefer, you can pass the
-gettext module as an attribute to the component instead.
-
 ### Storybook
 
 Doggo can generate
diff --git a/demo/lib/demo_web/components/core_components.ex b/demo/lib/demo_web/components/core_components.ex
index dd3bc4e..2e997f4 100644
--- a/demo/lib/demo_web/components/core_components.ex
+++ b/demo/lib/demo_web/components/core_components.ex
@@ -32,6 +32,7 @@ defmodule DemoWeb.CoreComponents do
   build_icon()
   build_icon_sprite()
   build_image()
+  build_input()
   build_label()
   build_menu()
   build_menu_bar()
diff --git a/demo/storybook/form/input.story.exs b/demo/storybook/form/input.story.exs
new file mode 100644
index 0000000..94fef62
--- /dev/null
+++ b/demo/storybook/form/input.story.exs
@@ -0,0 +1,4 @@
+defmodule Storybook.Components.Input do
+  use PhoenixStorybook.Story, :component
+  use Doggo.Storybook, module: DemoWeb.CoreComponents, name: :input
+end
diff --git a/lib/doggo.ex b/lib/doggo.ex
index 4b35a7c..33f7390 100644
--- a/lib/doggo.ex
+++ b/lib/doggo.ex
@@ -101,559 +101,19 @@ defmodule Doggo do
     end)
   end
 
-  @doc """
-  Renders a form field including input, label, errors, and description.
-
-  A `Phoenix.HTML.FormField` may be passed as argument,
-  which is used to retrieve the input name, id, and values.
-  Otherwise all attributes may be passed explicitly.
-
-  ## Types
-
-  In addition to all HTML input types, the following type values are also
-  supported:
-
-  - `"select"` - For `<select>` elements.
-
-  ## Gettext
-
-  To translate field errors using Gettext, configure your Gettext module in
-  `config/config.exs`.
-
-      config :doggo, gettext: MyApp.Gettext
-
-  Alternatively, pass the Gettext module as an attribute:
-
-  ```heex
-  <Doggo.input field={@form[:name]} gettext={MyApp.Gettext} />
-  ```
-
-  ## Label positioning
-
-  The component does not provide an attribute to modify label positioning
-  directly. Instead, label positioning should be handled with CSS. If your
-  application requires different label positions, such as horizontal and
-  vertical layouts, it is recommended to add a modifier class to the form.
-
-  For example, the default style could position labels above inputs. To place
-  labels to the left of the inputs in a horizontal form layout, you can add an
-  `is-horizontal` class to the form:
-
-  ```heex
-  <.form class="is-horizontal">
-    <!-- inputs -->
-  </.form>
-  ```
-
-  Then, in your CSS, apply the necessary styles to the `.field` class within
-  forms having the `is-horizontal` class:
-
-  ```css
-  form.is-horizontal .field {
-    // styles to position label left of the input
-  }
-  ```
-
-  The component has a `hide_label` attribute to visually hide labels while still
-  making them accessible to screen readers. If all labels within a form need to
-  be visually hidden, it may be more convenient to define a
-  `.has-visually-hidden-labels` modifier class for the `<form>`.
-
-  ```heex
-  <.form class="has-visually-hidden-labels">
-    <!-- inputs -->
-  </.form>
-  ```
-
-  Ensure to take checkbox and radio labels into consideration when writing the
-  CSS styles.
-
-  ## Examples
-
-  ```heex
-  <Doggo.input field={@form[:name]} />
-  ```
-
-  ```heex
-  <Doggo.input field={@form[:email]} type="email" />
-  ```
-
-  ### Radio group and checkbox group
-
-  The `radio-group` and `checkbox-group` types allow you to easily render groups
-  of radio buttons or checkboxes with a single component invocation. The
-  `options` attribute is required for these types and has the same format as
-  the options for the `select` type, except that options may not be nested.
-
-  ```heex
-  <Doggo.input
-    field={@form[:email]}
-    type="checkbox-group"
-    label="Cuisine"
-    options={[
-      {"Mexican", "mexican"},
-      {"Japanese", "japanese"},
-      {"Libanese", "libanese"}
-    ]}
-  />
-  ```
-
-  Note that the `checkbox-group` type renders an additional hidden input with
-  an empty value before the checkboxes. This ensures that a value exists in case
-  all checkboxes are unchecked. Consequently, the resulting list value includes
-  an extra empty string. While `Ecto.Changeset.cast/3` filters out empty strings
-  in array fields by default, you may need to handle the additional empty string
-  manual in other contexts.
-  """
-  @doc type: :form
-  @doc since: "0.1.0"
-
-  attr :id, :any, default: nil
-  attr :name, :any
-
-  attr :label, :string,
-    default: nil,
-    doc: """
-    Required for all types except `"hidden"`.
-    """
-
-  attr :hide_label, :boolean,
-    default: false,
-    doc: """
-    Adds an "is-visually-hidden" class to the `<label>`. This option does not
-    apply to checkbox and radio inputs.
-    """
-
-  attr :value, :any
-
-  attr :type, :string,
-    default: "text",
-    values: ~w(checkbox checkbox-group color date datetime-local email file
-         hidden month number password range radio radio-group search select
-         switch tel text textarea time url week)
-
-  attr :field, Phoenix.HTML.FormField,
-    doc: "A form field struct, for example: @form[:name]"
-
-  attr :errors, :list
-
-  attr :validations, :list,
-    doc: """
-    A list of HTML input validation attributes (`required`, `minlength`,
-    `maxlength`, `min`, `max`, `pattern`). The attributes are derived
-    automatically from the form.
-    """
-
-  attr :checked_value, :string,
-    default: "true",
-    doc: "The value that is sent when the checkbox is checked."
-
-  attr :checked, :boolean, doc: "The checked attribute for checkboxes."
-
-  attr :on_text, :string,
-    default: "On",
-    doc: "The state text for a switch when on."
-
-  attr :off_text, :string,
-    default: "Off",
-    doc: "The state text for a switch when off."
-
-  attr :prompt, :string,
-    default: nil,
-    doc: "An optional prompt for select elements."
-
-  attr :options, :list,
-    default: nil,
-    doc: """
-    A list of options.
-
-    This attribute is supported for the following types:
-
-    - `"select"`
-    - `"radio-group"`
-    - `"checkbox-group"`
-    - other text types, date and time types, and the `"range"` type
-
-    If this attribute is set for types other than select, radio, and checkbox,
-    a [datalist](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist)
-    is rendered for the input.
-
-    See `Phoenix.HTML.Form.options_for_select/2` for the format. Note that only
-    the select supports nested options.
-    """
-
-  attr :multiple, :boolean,
-    default: false,
-    doc: """
-    Sets the `multiple` attribute on a select element to allow selecting
-    multiple options.
-    """
-
-  attr :rest, :global,
-    include:
-      ~w(accept autocomplete capture cols disabled form list max maxlength min
-         minlength multiple passwordrules pattern placeholder readonly required
-         rows size step)
-
-  attr :gettext, :atom,
-    doc: """
-    The Gettext module to use for translating error messages. This option can
-    also be set globally, see above.
-    """
-
-  attr :required_text, :atom,
-    doc: """
-    The presentational text or symbol to be added to labels of required inputs.
-
-    This option can also be set globally:
-
-        config :doggo, required_text: "required"
-    """
-
-  attr :required_title, :atom,
-    doc: """
-    The `title` attribute for the `required_text`.
-
-    This option can also be set globally:
-
-        config :doggo, required_title: "required"
-    """
-
-  slot :description, doc: "A field description to render underneath the input."
-
-  slot :addon_left,
-    doc: """
-    Can be used to render an icon left in the input. Only supported for
-    single-line inputs.
-    """
-
-  slot :addon_right,
-    doc: """
-    Can be used to render an icon left in the input. Only supported for
-    single-line inputs.
-    """
-
-  def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
-    errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
-
-    gettext_module =
-      Map.get(assigns, :gettext, Application.get_env(:doggo, :gettext))
-
-    assigns
-    |> assign(field: nil, id: assigns.id || field.id)
-    |> assign_new(
-      :errors,
-      fn -> Enum.map(errors, &translate_error(&1, gettext_module)) end
-    )
-    |> assign_new(
-      :required_text,
-      fn -> Application.get_env(:doggo, :required_text, "*") end
-    )
-    |> assign_new(
-      :required_title,
-      fn -> Application.get_env(:doggo, :required_title, "required") end
-    )
-    |> assign_new(:validations, fn ->
-      Form.input_validations(field.form, field.field)
-    end)
-    |> assign_new(:name, fn ->
-      if assigns.multiple, do: field.name <> "[]", else: field.name
-    end)
-    |> assign_new(:value, fn -> field.value end)
-    |> input()
-  end
-
-  def input(%{type: "checkbox"} = assigns) do
-    assigns =
-      assign_new(assigns, :checked, fn ->
-        Form.normalize_value("checkbox", assigns[:value])
-      end)
-
-    ~H"""
-    <div class={["field", field_error_class(@errors)]}>
-      <.label required={@validations[:required] || false} class="checkbox">
-        <input type="hidden" name={@name} value="false" />
-        <input
-          type="checkbox"
-          name={@name}
-          id={@id}
-          value={@checked_value}
-          checked={@checked}
-          aria-describedby={input_aria_describedby(@id, @description)}
-          aria-errormessage={input_aria_errormessage(@id, @errors)}
-          aria-invalid={@errors != [] && "true"}
-          {@validations}
-          {@rest}
-        />
-        <%= @label %>
-      </.label>
-      <.field_errors for={@id} errors={@errors} />
-      <.field_description :if={@description != []} for={@id}>
-        <%= render_slot(@description) %>
-      </.field_description>
-    </div>
-    """
-  end
-
-  def input(%{type: "checkbox-group"} = assigns) do
-    ~H"""
-    <div class={["field", field_error_class(@errors)]}>
-      <fieldset class="checkbox-group">
-        <legend>
-          <%= @label %>
-          <.required_mark
-            :if={@validations[:required]}
-            text={@required_text}
-            title={@required_title}
-          />
-        </legend>
-        <div>
-          <input type="hidden" name={@name <> "[]"} value="" />
-          <.checkbox
-            :for={option <- @options}
-            option={option}
-            name={@name}
-            id={@id}
-            value={@value}
-            errors={@errors}
-            description={@description}
-          />
-        </div>
-      </fieldset>
-      <.field_errors for={@id} errors={@errors} />
-      <.field_description :if={@description != []} for={@id}>
-        <%= render_slot(@description) %>
-      </.field_description>
-    </div>
-    """
-  end
-
-  def input(%{type: "radio-group"} = assigns) do
-    ~H"""
-    <div class={["field", field_error_class(@errors)]}>
-      <fieldset class="radio-group">
-        <legend>
-          <%= @label %>
-          <.required_mark
-            :if={@validations[:required]}
-            text={@required_text}
-            title={@required_title}
-          />
-        </legend>
-        <div>
-          <Doggo.Components.radio
-            :for={option <- @options}
-            option={option}
-            name={@name}
-            id={@id}
-            value={@value}
-            errors={@errors}
-            description={@description}
-          />
-        </div>
-      </fieldset>
-      <.field_errors for={@id} errors={@errors} />
-      <.field_description :if={@description != []} for={@id}>
-        <%= render_slot(@description) %>
-      </.field_description>
-    </div>
-    """
-  end
-
-  def input(%{type: "switch"} = assigns) do
-    assigns =
-      assign_new(assigns, :checked, fn ->
-        Form.normalize_value("checkbox", assigns[:value])
-      end)
-
-    ~H"""
-    <div class={["field", field_error_class(@errors)]}>
-      <.label required={@validations[:required] || false} class="switch">
-        <span class="switch-label"><%= @label %></span>
-        <input type="hidden" name={@name} value="false" />
-        <input
-          type="checkbox"
-          role="switch"
-          name={@name}
-          id={@id}
-          value={@checked_value}
-          checked={@checked}
-          aria-describedby={input_aria_describedby(@id, @description)}
-          aria-errormessage={input_aria_errormessage(@id, @errors)}
-          aria-invalid={@errors != [] && "true"}
-          {@validations}
-          {@rest}
-        />
-        <span class="switch-state">
-          <span
-            class={if @checked, do: "switch-state-on", else: "switch-state-off"}
-            aria-hidden="true"
-          >
-            <%= if @checked do %>
-              <%= @on_text %>
-            <% else %>
-              <%= @off_text %>
-            <% end %>
-          </span>
-        </span>
-      </.label>
-      <.field_errors for={@id} errors={@errors} />
-      <.field_description :if={@description != []} for={@id}>
-        <%= render_slot(@description) %>
-      </.field_description>
-    </div>
-    """
-  end
-
-  def input(%{type: "select"} = assigns) do
-    ~H"""
-    <div class={["field", field_error_class(@errors)]}>
-      <.label
-        for={@id}
-        required={@validations[:required] || false}
-        required_text={@required_text}
-        required_title={@required_title}
-        visually_hidden={@hide_label}
-      >
-        <%= @label %>
-      </.label>
-      <div class={["select", @multiple && "is-multiple"]}>
-        <select
-          name={@name}
-          id={@id}
-          multiple={@multiple}
-          aria-describedby={input_aria_describedby(@id, @description)}
-          aria-errormessage={input_aria_errormessage(@id, @errors)}
-          aria-invalid={@errors != [] && "true"}
-          {@validations}
-          {@rest}
-        >
-          <option :if={@prompt} value=""><%= @prompt %></option>
-          <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
-        </select>
-      </div>
-      <.field_errors for={@id} errors={@errors} />
-      <.field_description :if={@description != []} for={@id}>
-        <%= render_slot(@description) %>
-      </.field_description>
-    </div>
-    """
-  end
-
-  def input(%{type: "textarea"} = assigns) do
-    ~H"""
-    <div class={["field", field_error_class(@errors)]}>
-      <.label
-        for={@id}
-        required={@validations[:required] || false}
-        required_text={@required_text}
-        required_title={@required_title}
-        visually_hidden={@hide_label}
-      >
-        <%= @label %>
-      </.label>
-      <textarea
-        name={@name}
-        id={@id}
-        aria-describedby={input_aria_describedby(@id, @description)}
-        aria-errormessage={input_aria_errormessage(@id, @errors)}
-        aria-invalid={@errors != [] && "true"}
-        {@validations}
-        {@rest}
-      ><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
-      <.field_errors for={@id} errors={@errors} />
-      <.field_description :if={@description != []} for={@id}>
-        <%= render_slot(@description) %>
-      </.field_description>
-    </div>
-    """
-  end
-
-  def input(%{type: "hidden", value: values} = assigns) when is_list(values) do
-    ~H"""
-    <input :for={value <- @value} type="hidden" name={@name <> "[]"} value={value} />
-    """
-  end
-
-  def input(%{type: "hidden"} = assigns) do
-    ~H"""
-    <input type="hidden" name={@name} value={@value} />
-    """
-  end
-
-  def input(assigns) do
-    ~H"""
-    <div class={["field", field_error_class(@errors)]}>
-      <.label
-        for={@id}
-        required={@validations[:required] || false}
-        required_text={@required_text}
-        required_title={@required_title}
-        visually_hidden={@hide_label}
-      >
-        <%= @label %>
-      </.label>
-      <div class={[
-        "input-wrapper",
-        @addon_left != [] && "has-addon-left",
-        @addon_right != [] && "has-addon-right"
-      ]}>
-        <input
-          name={@name}
-          id={@id}
-          list={@options && "#{@id}_datalist"}
-          type={@type}
-          value={normalize_value(@type, @value)}
-          aria-describedby={input_aria_describedby(@id, @description)}
-          aria-errormessage={input_aria_errormessage(@id, @errors)}
-          aria-invalid={@errors != [] && "true"}
-          {@validations}
-          {@rest}
-        />
-        <div :if={@addon_left != []} class="input-addon-left">
-          <%= render_slot(@addon_left) %>
-        </div>
-        <div :if={@addon_right != []} class="input-addon-right">
-          <%= render_slot(@addon_right) %>
-        </div>
-      </div>
-      <datalist :if={@options} id={"#{@id}_datalist"}>
-        <.option :for={option <- @options} option={option} />
-      </datalist>
-      <.field_errors for={@id} errors={@errors} />
-      <.field_description :if={@description != []} for={@id}>
-        <%= render_slot(@description) %>
-      </.field_description>
-    </div>
-    """
-  end
-
-  defp option(%{option: {label, value}} = assigns) do
-    assigns = assign(assigns, label: label, value: value)
-
-    ~H"""
-    <option value={@value}><%= @label %></option>
-    """
-  end
-
-  defp option(%{option: _} = assigns) do
-    ~H"""
-    <option value={@option}><%= @option %></option>
-    """
-  end
-
-  defp normalize_value("date", %struct{} = value)
-       when struct in [Date, NaiveDateTime, DateTime] do
+  @doc false
+  def normalize_value("date", %struct{} = value)
+      when struct in [Date, NaiveDateTime, DateTime] do
     <<date::10-binary, _::binary>> = struct.to_string(value)
     {:safe, date}
   end
 
-  defp normalize_value("date", <<date::10-binary, _::binary>>) do
+  def normalize_value("date", <<date::10-binary, _::binary>>) do
     {:safe, date}
   end
 
-  defp normalize_value("date", _), do: ""
-  defp normalize_value(type, value), do: Form.normalize_value(type, value)
+  def normalize_value("date", _), do: ""
+  def normalize_value(type, value), do: Form.normalize_value(type, value)
 
   @doc false
   def input_aria_describedby(_, []), do: nil
@@ -663,42 +123,9 @@ defmodule Doggo do
   def input_aria_errormessage(_, []), do: nil
   def input_aria_errormessage(id, _), do: field_errors_id(id)
 
-  defp field_error_class([]), do: nil
-  defp field_error_class(_), do: "has-errors"
-
-  defp checkbox(%{option_value: _} = assigns) do
-    ~H"""
-    <.label class="checkbox">
-      <input
-        type="checkbox"
-        name={@name <> "[]"}
-        id={@id <> "_#{@option_value}"}
-        value={@option_value}
-        checked={checked?(@option_value, @value)}
-        aria-describedby={input_aria_describedby(@id, @description)}
-        aria-errormessage={input_aria_errormessage(@id, @errors)}
-        aria-invalid={@errors != [] && "true"}
-      />
-      <%= @label %>
-    </.label>
-    """
-  end
-
-  defp checkbox(%{option: {option_label, option_value}} = assigns) do
-    assigns
-    |> assign(label: option_label, option_value: option_value, option: nil)
-    |> checkbox()
-  end
-
-  defp checkbox(%{option: option_value} = assigns) do
-    assigns
-    |> assign(
-      label: humanize(option_value),
-      option_value: option_value,
-      option: nil
-    )
-    |> checkbox()
-  end
+  @doc false
+  def field_error_class([]), do: nil
+  def field_error_class(_), do: "has-errors"
 
   @doc false
   def checked?(option, value) when is_list(value) do
@@ -712,146 +139,21 @@ defmodule Doggo do
     Phoenix.HTML.html_escape(option) == Phoenix.HTML.html_escape(value)
   end
 
-  @doc """
-  Renders the label for an input.
-
-  ## Example
-
-  ```heex
-  <Doggo.label for="name" required>
-    Name
-  </Doggo.label>
-  ```
-  """
-  @doc type: :form
-  @doc since: "0.1.0"
-
-  attr :for, :string, default: nil, doc: "The ID of the input."
-
-  attr :required, :boolean,
-    default: false,
-    doc: "If set to `true`, a 'required' mark is rendered."
-
-  attr :required_text, :any,
-    default: "*",
-    doc: """
-    Sets the presentational text or symbol to mark an input as required.
-    """
-
-  attr :required_title, :any,
-    default: "required",
-    doc: """
-    Sets the `title` attribute of the required mark.
-    """
-
-  attr :visually_hidden, :boolean,
-    default: false,
-    doc: """
-    Adds an "is-visually-hidden" class to the `<label>`.
-    """
-
-  attr :class, :any,
-    default: [],
-    doc: "Additional CSS classes. Can be a string or a list of strings."
-
-  attr :rest, :global, doc: "Any additional HTML attributes."
-
-  slot :inner_block, required: true
-
-  def label(assigns) do
-    ~H"""
-    <label
-      for={@for}
-      class={[@visually_hidden && "is-visually-hidden" | List.wrap(@class)]}
-      {@rest}
-    >
-      <%= render_slot(@inner_block) %>
-      <.required_mark :if={@required} title={@required_title} text={@required_text} />
-    </label>
-    """
-  end
-
-  attr :text, :string, default: "*"
-  attr :title, :string, default: "required"
-
-  # inputs are announced as required by screen readers if the `required`
-  # attribute is set. This makes this mark purely visual. `aria-hidden="true"`
-  # is added so that screen readers don't announce redundant information. The
-  # title attribute has poor accessibility characteristics, but since this is
-  # purely presentational, this is acceptable.
-  # It is good practice to add a sentence explaining that fields marked with an
-  # asterisk (*) are required to the form.
-  # Alternatively, the word `required` might be used instead of an asterisk.
-  defp required_mark(assigns) do
-    ~H"""
-    <span :if={@text} class="label-required" aria-hidden="true" title={@title}>
-      <%= @text %>
-    </span>
-    """
-  end
-
-  @doc """
-  Renders the errors for an input.
-
-  ## Example
-
-  ```heex
-  <Doggo.field_errors for="name" errors={["too many characters"]} />
-  ```
-  """
-  @doc type: :form
-  @doc since: "0.1.0"
-
-  attr :for, :string, required: true, doc: "The ID of the input."
-  attr :errors, :list, required: true, doc: "A list of errors as strings."
-
-  def field_errors(assigns) do
-    ~H"""
-    <ul :if={@errors != []} id={field_errors_id(@for)} class="field-errors">
-      <li :for={error <- @errors}><%= error %></li>
-    </ul>
-    """
-  end
-
   @doc false
   def field_errors_id(id) when is_binary(id), do: "#{id}_errors"
 
-  @doc """
-  Renders the description of an input.
-
-  ## Example
-
-  ```heex
-  <Doggo.field_description for="name">
-    max. 100 characters
-  </Doggo.field_description>
-  ```
-  """
-  @doc type: :form
-  @doc since: "0.1.0"
-
-  attr :for, :string, required: true, doc: "The ID of the input."
-  slot :inner_block, required: true
-
-  def field_description(assigns) do
-    ~H"""
-    <div id={field_description_id(@for)} class="field-description">
-      <%= render_slot(@inner_block) %>
-    </div>
-    """
-  end
-
   @doc false
   def field_description_id(id) when is_binary(id), do: "#{id}_description"
 
-  defp translate_error({msg, opts}, nil) do
+  @doc false
+  def translate_error({msg, opts}, nil) do
     Enum.reduce(opts, msg, fn {key, value}, acc ->
       String.replace(acc, "%{#{key}}", fn _ -> to_string(value) end)
     end)
   end
 
-  defp translate_error({msg, opts}, gettext_module)
-       when is_atom(gettext_module) do
+  def translate_error({msg, opts}, gettext_module)
+      when is_atom(gettext_module) do
     if count = opts[:count] do
       # credo:disable-for-next-line
       apply(Gettext, :dngettext, [
diff --git a/lib/doggo/components.ex b/lib/doggo/components.ex
index e4196d7..22c6b68 100644
--- a/lib/doggo/components.ex
+++ b/lib/doggo/components.ex
@@ -2887,6 +2887,639 @@ defmodule Doggo.Components do
       end
   )
 
+  component(
+    :input,
+    base_class: "field",
+    modifiers: [],
+    extra: [
+      gettext_module: nil,
+      addon_left_class: "has-addon-left",
+      addon_right_class: "has-addon-right"
+    ],
+    doc: """
+    Renders a form field including input, label, errors, and description.
+
+    A `Phoenix.HTML.FormField` may be passed as argument,
+    which is used to retrieve the input name, id, and values.
+    Otherwise all attributes may be passed explicitly.
+    """,
+    usage: """
+    ### Types
+
+    In addition to all HTML input types, the following type values are also
+    supported:
+
+    - `"select"`
+    - `"checkbox-group"`
+    - `"radio-group"`
+    - `"switch"`
+
+    ### Class and Global Attribute
+
+    Note that the `class` attribute is applied to the outer container, while
+    the `rest` global attribute is applied to the `<input>` element.
+
+    ### Gettext
+
+    To translate field errors using Gettext, set the `gettext_module` option
+    when building the component:
+
+        build_input(gettext_module: MyApp.Gettext)
+
+    ### Label positioning
+
+    The component does not provide an attribute to modify label positioning
+    directly. Instead, label positioning should be handled with CSS. If your
+    application requires different label positions, such as horizontal and
+    vertical layouts, it is recommended to add a modifier class to the form.
+
+    For example, the default style could position labels above inputs. To place
+    labels to the left of the inputs in a horizontal form layout, you can add an
+    `is-horizontal` class to the form:
+
+    ```heex
+    <.form class="is-horizontal">
+      <!-- inputs -->
+    </.form>
+    ```
+
+    Then, in your CSS, apply the necessary styles to the `.field` class within
+    forms having the `is-horizontal` class:
+
+    ```css
+    form.is-horizontal .field {
+      // styles to position label left of the input
+    }
+    ```
+
+    The component has a `hide_label` attribute to visually hide labels while still
+    making them accessible to screen readers. If all labels within a form need to
+    be visually hidden, it may be more convenient to define a
+    `.has-visually-hidden-labels` modifier class for the `<form>`.
+
+    ```heex
+    <.form class="has-visually-hidden-labels">
+      <!-- inputs -->
+    </.form>
+    ```
+
+    Ensure to take checkbox and radio labels into consideration when writing the
+    CSS styles.
+
+    ### Examples
+
+    ```heex
+    <.input field={@form[:name]} />
+    ```
+
+    ```heex
+    <.input field={@form[:email]} type="email" />
+    ```
+
+    #### Radio group and checkbox group
+
+    The `radio-group` and `checkbox-group` types allow you to easily render groups
+    of radio buttons or checkboxes with a single component invocation. The
+    `options` attribute is required for these types and has the same format as
+    the options for the `select` type, except that options may not be nested.
+
+    ```heex
+    <.input
+      field={@form[:email]}
+      type="checkbox-group"
+      label="Cuisine"
+      options={[
+        {"Mexican", "mexican"},
+        {"Japanese", "japanese"},
+        {"Libanese", "libanese"}
+      ]}
+    />
+    ```
+
+    Note that the `checkbox-group` type renders an additional hidden input with
+    an empty value before the checkboxes. This ensures that a value exists in case
+    all checkboxes are unchecked. Consequently, the resulting list value includes
+    an extra empty string. While `Ecto.Changeset.cast/3` filters out empty strings
+    in array fields by default, you may need to handle the additional empty string
+    manual in other contexts.
+    """,
+    type: :form,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
+        attr :id, :any, default: nil
+        attr :name, :any
+
+        attr :label, :string,
+          default: nil,
+          doc: """
+          Required for all types except `"hidden"`.
+          """
+
+        attr :hide_label, :boolean,
+          default: false,
+          doc: """
+          Adds an "is-visually-hidden" class to the `<label>`. This option does not
+          apply to checkbox and radio inputs.
+          """
+
+        attr :value, :any
+
+        attr :type, :string,
+          default: "text",
+          values:
+            ~w(checkbox checkbox-group color date datetime-local email file
+         hidden month number password range radio radio-group search select
+         switch tel text textarea time url week)
+
+        attr :field, Phoenix.HTML.FormField,
+          doc: "A form field struct, for example: @form[:name]"
+
+        attr :errors, :list
+
+        attr :validations, :list,
+          doc: """
+          A list of HTML input validation attributes (`required`, `minlength`,
+          `maxlength`, `min`, `max`, `pattern`). The attributes are derived
+          automatically from the form.
+          """
+
+        attr :checked_value, :string,
+          default: "true",
+          doc: "The value that is sent when the checkbox is checked."
+
+        attr :checked, :boolean, doc: "The checked attribute for checkboxes."
+
+        attr :on_text, :string,
+          default: "On",
+          doc: "The state text for a switch when on."
+
+        attr :off_text, :string,
+          default: "Off",
+          doc: "The state text for a switch when off."
+
+        attr :prompt, :string,
+          default: nil,
+          doc: "An optional prompt for select elements."
+
+        attr :options, :list,
+          default: nil,
+          doc: """
+          A list of options.
+
+          This attribute is supported for the following types:
+
+          - `"select"`
+          - `"radio-group"`
+          - `"checkbox-group"`
+          - other text types, date and time types, and the `"range"` type
+
+          If this attribute is set for types other than select, radio, and checkbox,
+          a [datalist](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist)
+          is rendered for the input.
+
+          See `Phoenix.HTML.Form.options_for_select/2` for the format. Note that only
+          the select supports nested options.
+          """
+
+        attr :multiple, :boolean,
+          default: false,
+          doc: """
+          Sets the `multiple` attribute on a select element to allow selecting
+          multiple options.
+          """
+
+        attr :rest, :global,
+          include:
+            ~w(accept autocomplete capture cols disabled form list max maxlength min
+         minlength multiple passwordrules pattern placeholder readonly required
+         rows size step)
+
+        attr :gettext, :atom,
+          doc: """
+          The Gettext module to use for translating error messages. This option can
+          also be set globally, see above.
+          """
+
+        attr :required_text, :atom,
+          doc: """
+          The presentational text or symbol to be added to labels of required inputs.
+
+          This option can also be set globally:
+
+              config :doggo, required_text: "required"
+          """
+
+        attr :required_title, :atom,
+          doc: """
+          The `title` attribute for the `required_text`.
+
+          This option can also be set globally:
+
+              config :doggo, required_title: "required"
+          """
+
+        slot :description,
+          doc: "A field description to render underneath the input."
+
+        slot :addon_left,
+          doc: """
+          Can be used to render an icon left in the input. Only supported for
+          single-line inputs.
+          """
+
+        slot :addon_right,
+          doc: """
+          Can be used to render an icon left in the input. Only supported for
+          single-line inputs.
+          """
+      end,
+    component_function: fn opts, extra ->
+      base_class = Keyword.fetch!(opts, :base_class)
+      addon_left_class = Keyword.fetch!(extra, :addon_left_class)
+      addon_right_class = Keyword.fetch!(extra, :addon_right_class)
+      modifier_names = opts |> Keyword.fetch!(:modifiers) |> Keyword.keys()
+      class_name_fun = Keyword.fetch!(opts, :class_name_fun)
+      gettext_module = Keyword.get(extra, :gettext_module)
+
+      # credo:disable-for-next-line
+      quote do
+        def input(%{field: %Phoenix.HTML.FormField{} = field} = var!(assigns)) do
+          unquote(
+            prepare_class(
+              base_class,
+              modifier_names,
+              class_name_fun
+            )
+          )
+
+          errors =
+            if Phoenix.Component.used_input?(field), do: field.errors, else: []
+
+          assigns
+          |> var!()
+          |> assign(field: nil, id: var!(assigns).id || field.id)
+          |> assign_new(
+            :errors,
+            fn ->
+              Enum.map(
+                errors,
+                &Doggo.translate_error(&1, unquote(gettext_module))
+              )
+            end
+          )
+          |> assign_new(
+            :required_text,
+            fn -> Application.get_env(:doggo, :required_text, "*") end
+          )
+          |> assign_new(
+            :required_title,
+            fn -> Application.get_env(:doggo, :required_title, "required") end
+          )
+          |> assign_new(:validations, fn ->
+            Phoenix.HTML.Form.input_validations(field.form, field.field)
+          end)
+          |> assign_new(:name, fn ->
+            if var!(assigns).multiple, do: field.name <> "[]", else: field.name
+          end)
+          |> assign_new(:value, fn -> field.value end)
+          |> input()
+        end
+
+        def input(%{type: "checkbox"} = var!(assigns)) do
+          var!(assigns) =
+            assign_new(var!(assigns), :checked, fn ->
+              Phoenix.HTML.Form.normalize_value(
+                "checkbox",
+                var!(assigns)[:value]
+              )
+            end)
+
+          ~H"""
+          <div class={@class ++ [Doggo.field_error_class(@errors)]}>
+            <.label required={@validations[:required] || false} class="checkbox">
+              <input type="hidden" name={@name} value="false" />
+              <input
+                type="checkbox"
+                name={@name}
+                id={@id}
+                value={@checked_value}
+                checked={@checked}
+                aria-describedby={Doggo.input_aria_describedby(@id, @description)}
+                aria-errormessage={Doggo.input_aria_errormessage(@id, @errors)}
+                aria-invalid={@errors != [] && "true"}
+                {@validations}
+                {@rest}
+              />
+              <%= @label %>
+            </.label>
+            <.field_errors for={@id} errors={@errors} />
+            <.field_description :if={@description != []} for={@id}>
+              <%= render_slot(@description) %>
+            </.field_description>
+          </div>
+          """
+        end
+
+        def input(%{type: "checkbox-group"} = var!(assigns)) do
+          ~H"""
+          <div class={@class ++ [Doggo.field_error_class(@errors)]}>
+            <fieldset class="checkbox-group">
+              <legend>
+                <%= @label %>
+                <Doggo.Components.required_mark
+                  :if={@validations[:required]}
+                  text={@required_text}
+                  title={@required_title}
+                />
+              </legend>
+              <div>
+                <input type="hidden" name={@name <> "[]"} value="" />
+                <Doggo.Components.checkbox
+                  :for={option <- @options}
+                  option={option}
+                  name={@name}
+                  id={@id}
+                  value={@value}
+                  errors={@errors}
+                  description={@description}
+                />
+              </div>
+            </fieldset>
+            <.field_errors for={@id} errors={@errors} />
+            <.field_description :if={@description != []} for={@id}>
+              <%= render_slot(@description) %>
+            </.field_description>
+          </div>
+          """
+        end
+
+        def input(%{type: "hidden", value: values} = var!(assigns))
+            when is_list(values) do
+          ~H"""
+          <input :for={value <- @value} type="hidden" name={@name <> "[]"} value={value} />
+          """
+        end
+
+        def input(%{type: "hidden"} = var!(assigns)) do
+          ~H"""
+          <input type="hidden" name={@name} value={@value} />
+          """
+        end
+
+        def input(%{type: "radio-group"} = var!(assigns)) do
+          ~H"""
+          <div class={@class ++ [Doggo.field_error_class(@errors)]}>
+            <fieldset class="radio-group">
+              <legend>
+                <%= @label %>
+                <Doggo.Components.required_mark
+                  :if={@validations[:required]}
+                  text={@required_text}
+                  title={@required_title}
+                />
+              </legend>
+              <div>
+                <Doggo.Components.radio
+                  :for={option <- @options}
+                  option={option}
+                  name={@name}
+                  id={@id}
+                  value={@value}
+                  errors={@errors}
+                  description={@description}
+                />
+              </div>
+            </fieldset>
+            <.field_errors for={@id} errors={@errors} />
+            <.field_description :if={@description != []} for={@id}>
+              <%= render_slot(@description) %>
+            </.field_description>
+          </div>
+          """
+        end
+
+        def input(%{type: "select"} = var!(assigns)) do
+          ~H"""
+          <div class={@class ++ [Doggo.field_error_class(@errors)]}>
+            <.label
+              for={@id}
+              required={@validations[:required] || false}
+              required_text={@required_text}
+              required_title={@required_title}
+              visually_hidden={@hide_label}
+            >
+              <%= @label %>
+            </.label>
+            <div class={["select", @multiple && "is-multiple"]}>
+              <select
+                name={@name}
+                id={@id}
+                multiple={@multiple}
+                aria-describedby={Doggo.input_aria_describedby(@id, @description)}
+                aria-errormessage={Doggo.input_aria_errormessage(@id, @errors)}
+                aria-invalid={@errors != [] && "true"}
+                {@validations}
+                {@rest}
+              >
+                <option :if={@prompt} value=""><%= @prompt %></option>
+                <%= Phoenix.HTML.Form.options_for_select(@options, @value) %>
+              </select>
+            </div>
+            <.field_errors for={@id} errors={@errors} />
+            <.field_description :if={@description != []} for={@id}>
+              <%= render_slot(@description) %>
+            </.field_description>
+          </div>
+          """
+        end
+
+        def input(%{type: "switch"} = var!(assigns)) do
+          var!(assigns) =
+            assign_new(var!(assigns), :checked, fn ->
+              Phoenix.HTML.Form.normalize_value(
+                "checkbox",
+                var!(assigns)[:value]
+              )
+            end)
+
+          ~H"""
+          <div class={@class ++ [Doggo.field_error_class(@errors)]}>
+            <.label required={@validations[:required] || false} class="switch">
+              <span class="switch-label"><%= @label %></span>
+              <input type="hidden" name={@name} value="false" />
+              <input
+                type="checkbox"
+                role="switch"
+                name={@name}
+                id={@id}
+                value={@checked_value}
+                checked={@checked}
+                aria-describedby={Doggo.input_aria_describedby(@id, @description)}
+                aria-errormessage={Doggo.input_aria_errormessage(@id, @errors)}
+                aria-invalid={@errors != [] && "true"}
+                {@validations}
+                {@rest}
+              />
+              <span class="switch-state">
+                <span
+                  class={if @checked, do: "switch-state-on", else: "switch-state-off"}
+                  aria-hidden="true"
+                >
+                  <%= if @checked do %>
+                    <%= @on_text %>
+                  <% else %>
+                    <%= @off_text %>
+                  <% end %>
+                </span>
+              </span>
+            </.label>
+            <.field_errors for={@id} errors={@errors} />
+            <.field_description :if={@description != []} for={@id}>
+              <%= render_slot(@description) %>
+            </.field_description>
+          </div>
+          """
+        end
+
+        def input(%{type: "textarea"} = var!(assigns)) do
+          ~H"""
+          <div class={@class ++ [Doggo.field_error_class(@errors)]}>
+            <.label
+              for={@id}
+              required={@validations[:required] || false}
+              required_text={@required_text}
+              required_title={@required_title}
+              visually_hidden={@hide_label}
+            >
+              <%= @label %>
+            </.label>
+            <textarea
+              name={@name}
+              id={@id}
+              aria-describedby={Doggo.input_aria_describedby(@id, @description)}
+              aria-errormessage={Doggo.input_aria_errormessage(@id, @errors)}
+              aria-invalid={@errors != [] && "true"}
+              {@validations}
+              {@rest}
+            ><%= Phoenix.HTML.Form.normalize_value("textarea", @value) %></textarea>
+            <.field_errors for={@id} errors={@errors} />
+            <.field_description :if={@description != []} for={@id}>
+              <%= render_slot(@description) %>
+            </.field_description>
+          </div>
+          """
+        end
+
+        def input(var!(assigns)) do
+          var!(assigns) =
+            assign(
+              var!(assigns),
+              addon_left_class: unquote(addon_left_class),
+              addon_right_class: unquote(addon_right_class),
+              base_class: unquote(base_class)
+            )
+
+          ~H"""
+          <div class={@class ++ [Doggo.field_error_class(@errors)]}>
+            <.label
+              for={@id}
+              required={@validations[:required] || false}
+              required_text={@required_text}
+              required_title={@required_title}
+              visually_hidden={@hide_label}
+            >
+              <%= @label %>
+            </.label>
+            <div class={[
+              "#{@base_class}-input-wrapper",
+              @addon_left != [] && @addon_left_class,
+              @addon_right != [] && @addon_right_class
+            ]}>
+              <input
+                name={@name}
+                id={@id}
+                list={@options && "#{@id}_datalist"}
+                type={@type}
+                value={Doggo.normalize_value(@type, @value)}
+                aria-describedby={Doggo.input_aria_describedby(@id, @description)}
+                aria-errormessage={Doggo.input_aria_errormessage(@id, @errors)}
+                aria-invalid={@errors != [] && "true"}
+                {@validations}
+                {@rest}
+              />
+              <div :if={@addon_left != []} class={"#{@base_class}-input-addon-left"}>
+                <%= render_slot(@addon_left) %>
+              </div>
+              <div :if={@addon_right != []} class={"#{@base_class}-input-addon-right"}>
+                <%= render_slot(@addon_right) %>
+              </div>
+            </div>
+            <datalist :if={@options} id={"#{@id}_datalist"}>
+              <Doggo.Components.option :for={option <- @options} option={option} />
+            </datalist>
+            <.field_errors for={@id} errors={@errors} />
+            <.field_description :if={@description != []} for={@id}>
+              <%= render_slot(@description) %>
+            </.field_description>
+          </div>
+          """
+        end
+      end
+    end
+  )
+
+  @doc false
+  def option(%{option: {label, value}} = assigns) do
+    assigns = assign(assigns, label: label, value: value)
+
+    ~H"""
+    <option value={@value}><%= @label %></option>
+    """
+  end
+
+  def option(%{option: _} = assigns) do
+    ~H"""
+    <option value={@option}><%= @option %></option>
+    """
+  end
+
+  @doc false
+  def checkbox(%{option_value: _} = assigns) do
+    ~H"""
+    <label class="checkbox">
+      <input
+        type="checkbox"
+        name={@name <> "[]"}
+        id={@id <> "_#{@option_value}"}
+        value={@option_value}
+        checked={Doggo.checked?(@option_value, @value)}
+        aria-describedby={Doggo.input_aria_describedby(@id, @description)}
+        aria-errormessage={Doggo.input_aria_errormessage(@id, @errors)}
+        aria-invalid={@errors != [] && "true"}
+      />
+      <%= @label %>
+    </label>
+    """
+  end
+
+  def checkbox(%{option: {option_label, option_value}} = assigns) do
+    assigns
+    |> assign(label: option_label, option_value: option_value, option: nil)
+    |> checkbox()
+  end
+
+  def checkbox(%{option: option_value} = assigns) do
+    assigns
+    |> assign(
+      label: Doggo.humanize(option_value),
+      option_value: option_value,
+      option: nil
+    )
+    |> checkbox()
+  end
+
   component(
     :label,
     name: :label,
@@ -4144,7 +4777,7 @@ defmodule Doggo.Components do
       )
 
     ~H"""
-    <Doggo.label>
+    <label>
       <input
         type="radio"
         name={@name}
@@ -4156,7 +4789,7 @@ defmodule Doggo.Components do
         aria-invalid={@invalid}
       />
       <%= @label %>
-    </Doggo.label>
+    </label>
     """
   end
 
diff --git a/lib/doggo/macros.ex b/lib/doggo/macros.ex
index a936c3f..08a160e 100644
--- a/lib/doggo/macros.ex
+++ b/lib/doggo/macros.ex
@@ -7,6 +7,7 @@ defmodule Doggo.Macros do
       Keyword.validate!(opts, [
         :attrs_and_slots,
         :base_class,
+        :component_function,
         :doc,
         :extra,
         :heex,
@@ -45,7 +46,8 @@ defmodule Doggo.Macros do
     type = Keyword.fetch!(opts, :type)
     since = Keyword.fetch!(opts, :since)
     attrs_and_slots = Keyword.fetch!(opts, :attrs_and_slots)
-    heex = Keyword.fetch!(opts, :heex)
+    heex = Keyword.get(opts, :heex)
+    component_function = Keyword.get(opts, :component_function)
 
     maturity_info = build_maturity_info(maturity, maturity_note)
 
@@ -92,64 +94,95 @@ defmodule Doggo.Macros do
         modifier_names = Keyword.keys(modifiers)
         class_name_fun = Keyword.fetch!(opts, :class_name_fun)
         heex = Doggo.Macros.unpack_heex(unquote(heex), extra)
+
+        component_function =
+          unpack_component_function(unquote(component_function), opts, extra)
+
         attrs_and_slots = unquote(attrs_and_slots)
         usage = unquote(usage)
         doc = unquote(doc)
 
-        quote do
-          @dog_components unquote(component_info)
+        preamble =
+          quote do
+            @dog_components unquote(component_info)
 
-          @doc """
-          #{unquote(doc)}
+            @doc """
+            #{unquote(doc)}
 
-          ## Usage
+            ## Usage
 
-          #{unquote(usage)}
-          """
+            #{unquote(usage)}
+            """
 
-          for {name, modifier_opts} <- unquote(modifiers) do
-            attr name, :string, modifier_opts
-          end
+            for {name, modifier_opts} <- unquote(modifiers) do
+              attr name, :string, modifier_opts
+            end
 
-          attr :class, :any,
-            default: nil,
-            doc: """
-            Any additional classes to be added.
+            attr :class, :any,
+              default: nil,
+              doc: """
+              Any additional classes to be added.
 
-            Variations of the component should be expressed via modifier
-            attributes, and it is preferable to use styles on the parent
-            container to arrange components on the page, but if you have to,
-            you can use this attribute to pass additional utility classes to
-            the component.
+              Variations of the component should be expressed via modifier
+              attributes, and it is preferable to use styles on the parent
+              container to arrange components on the page, but if you have to,
+              you can use this attribute to pass additional utility classes to
+              the component.
 
-            The value can be a string or a list of strings.
-            """
+              The value can be a string or a list of strings.
+              """
+
+            unquote(attrs_and_slots)
+          end
 
-          unquote(attrs_and_slots)
-
-          def unquote(name)(var!(assigns)) do
-            var!(assigns) =
-              assign(var!(assigns),
-                base_class: unquote(base_class),
-                class:
-                  Doggo.build_class(
-                    unquote(base_class),
-                    unquote(modifier_names),
-                    unquote(class_name_fun),
-                    var!(assigns)
+        function =
+          if heex do
+            quote do
+              def unquote(name)(var!(assigns)) do
+                unquote(
+                  prepare_class(
+                    base_class,
+                    Keyword.keys(modifiers),
+                    class_name_fun
                   )
-              )
+                )
 
-            unquote(heex)
+                unquote(heex)
+              end
+            end
+          else
+            component_function
           end
-        end
+
+        [preamble, function]
       end
     end
   end
 
+  def prepare_class(base_class, modifier_names, class_name_fun) do
+    quote do
+      var!(assigns) =
+        assign(var!(assigns),
+          base_class: unquote(base_class),
+          class:
+            Doggo.build_class(
+              unquote(base_class),
+              unquote(modifier_names),
+              unquote(class_name_fun),
+              var!(assigns)
+            )
+        )
+    end
+  end
+
   def unpack_heex(heex, extra) when is_function(heex), do: heex.(extra)
   def unpack_heex(heex, _), do: heex
 
+  def unpack_component_function(fun, opts, extra) when is_function(fun),
+    do: fun.(opts, extra)
+
+  def unpack_component_function(fun, _, _), do: fun
+
   defp build_maturity_info(maturity, nil) do
     """
     > #### Maturity: #{maturity_to_string(maturity)} {: .info}
diff --git a/priv/storybook/form/input.story.exs b/lib/doggo/storybook/input.ex
similarity index 85%
rename from priv/storybook/form/input.story.exs
rename to lib/doggo/storybook/input.ex
index ebd0bd7..b573b78 100644
--- a/priv/storybook/form/input.story.exs
+++ b/lib/doggo/storybook/input.ex
@@ -1,8 +1,12 @@
-defmodule Storybook.Components.Input do
-  use PhoenixStorybook.Story, :component
+defmodule Doggo.Storybook.Input do
+  @moduledoc false
 
-  def function, do: &Doggo.input/1
-  # def imports, do: [{Phoenix.Component, [simple_form: 1]}]
+  import Doggo.Storybook.Shared
+
+  alias PhoenixStorybook.Stories.Variation
+  alias PhoenixStorybook.Stories.VariationGroup
+
+  def dependent_components, do: [:icon]
 
   def template do
     """
@@ -12,7 +16,9 @@ defmodule Storybook.Components.Input do
     """
   end
 
-  def variations do
+  def variations(opts) do
+    dependent_components = opts[:dependent_components]
+
     [
       %VariationGroup{
         id: :basic_inputs,
@@ -276,9 +282,7 @@ defmodule Storybook.Components.Input do
             slots: [
               """
               <:addon_left>
-                <Doggo.icon>
-                  #{mail_icon()}
-                </Doggo.icon>
+                #{icon(:mail, dependent_components)}
               </:addon_left>
               """
             ]
@@ -293,9 +297,7 @@ defmodule Storybook.Components.Input do
             slots: [
               """
               <:addon_right>
-                <Doggo.icon>
-                  #{mail_icon()}
-                </Doggo.icon>
+                #{icon(:mail, dependent_components)}
               </:addon_right>
               """
             ]
@@ -310,14 +312,10 @@ defmodule Storybook.Components.Input do
             slots: [
               """
               <:addon_left>
-                <Doggo.icon>
-                  #{mail_icon()}
-                </Doggo.icon>
+                #{icon(:mail, dependent_components)}
               </:addon_left>
               <:addon_right>
-                <Doggo.icon>
-                  #{check_icon()}
-                </Doggo.icon>
+                #{icon(:check, dependent_components)}
               </:addon_right>
               """
             ]
@@ -327,41 +325,13 @@ defmodule Storybook.Components.Input do
     ]
   end
 
-  defp mail_icon do
-    """
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      viewBox="0 0 24 24"
-      fill="none"
-      stroke="currentColor"
-      stroke-width="2"
-      stroke-linecap="round"
-      stroke-linejoin="round"
-      class="lucide lucide-mail"
-    >
-      <rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
-    </svg>
-    """
-  end
-
-  defp check_icon do
-    """
-    <svg
-      xmlns="http://www.w3.org/2000/svg"
-      width="24"
-      height="24"
-      viewBox="0 0 24 24"
-      fill="none"
-      stroke="currentColor"
-      stroke-width="2"
-      stroke-linecap="round"
-      stroke-linejoin="round"
-      class="lucide lucide-check"
-    >
-      <path d="M20 6 9 17l-5-5" />
-    </svg>
-    """
+  def modifier_variation_base(_id, _name, value, _opts) do
+    %{
+      attributes: %{
+        type: "text",
+        label: "Text",
+        placeholder: value || "nil"
+      }
+    }
   end
 end
diff --git a/lib/doggo/storybook/shared.ex b/lib/doggo/storybook/shared.ex
index 0248569..3c4a542 100644
--- a/lib/doggo/storybook/shared.ex
+++ b/lib/doggo/storybook/shared.ex
@@ -89,6 +89,25 @@ defmodule Doggo.Storybook.Shared do
     """
   end
 
+  defp icon_svg(:check) do
+    """
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+      stroke="currentColor"
+      stroke-width="2"
+      stroke-linecap="round"
+      stroke-linejoin="round"
+      class="lucide lucide-check"
+    >
+      <path d="M20 6 9 17l-5-5" />
+    </svg>
+    """
+  end
+
   defp icon_svg(:chevron_left) do
     """
     <svg
@@ -254,6 +273,25 @@ defmodule Doggo.Storybook.Shared do
     """
   end
 
+  defp icon_svg(:mail) do
+    """
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      width="24"
+      height="24"
+      viewBox="0 0 24 24"
+      fill="none"
+      stroke="currentColor"
+      stroke-width="2"
+      stroke-linecap="round"
+      stroke-linejoin="round"
+      class="lucide lucide-mail"
+    >
+      <rect width="20" height="16" x="2" y="4" rx="2" /><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
+    </svg>
+    """
+  end
+
   defp icon_svg(:mails) do
     """
     <svg


Test patch:
diff --git a/test/doggo/components_test.exs b/test/doggo/components_test.exs
index b78d2d2..62c46ce 100644
--- a/test/doggo/components_test.exs
+++ b/test/doggo/components_test.exs
@@ -45,6 +45,7 @@ defmodule Doggo.ComponentsTest do
     build_icon()
     build_icon_sprite()
     build_image()
+    build_input(gettext_module: Doggo.Gettext)
     build_label()
     build_menu()
     build_menu_bar()
@@ -2678,6 +2679,505 @@ defmodule Doggo.ComponentsTest do
     end
   end
 
+  describe "input/1" do
+    test "with text input" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={@form[:age]} label="Age" />
+        </.form>
+        """)
+
+      input = find_one(html, "input")
+      assert attribute(input, "id") == "age"
+      assert attribute(input, "name") == "age"
+      assert attribute(input, "type") == "text"
+      assert attribute(input, "aria-describedby") == nil
+      assert attribute(input, "aria-invalid") == nil
+      assert attribute(input, "aria-errormessage") == nil
+
+      assert attribute(html, "label", "for") == "age"
+      assert text(html, "label") == "Age"
+    end
+
+    test "with text input and description" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={@form[:age]} label="Age">
+            <:description>How old?</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert text(html, ".field-description") == "How old?"
+      assert attribute(html, ".field-description", "id") == "age_description"
+      assert attribute(html, "input", "aria-describedby") == "age_description"
+    end
+
+    test "with checkbox" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:subscribe]}
+            label="Subscribe"
+            type="checkbox"
+          >
+            <:description>Please do.</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert attribute(html, "label", "class") == "label checkbox"
+      assert attribute(html, "input[type='hidden']", "value") == "false"
+
+      assert text(html, ".field-description") == "Please do."
+
+      assert attribute(html, ".field-description", "id") ==
+               "subscribe_description"
+
+      assert attribute(html, "input[type='checkbox']", "aria-describedby") ==
+               "subscribe_description"
+
+      assert attribute(html, "input[type='checkbox']", "value") == "true"
+    end
+
+    test "with checkbox and checked value" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:subscribe]}
+            label="Subscribe"
+            type="checkbox"
+            checked_value="yes"
+          >
+            <:description>Please do.</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert attribute(html, "input[type='checkbox']", "value") == "yes"
+    end
+
+    test "with checkbox group" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:animals]}
+            label="Animals"
+            type="checkbox-group"
+            options={[{"Dog", "dog"}, "cat", "rabbit_id", :elk]}
+            value={["dog", "elk"]}
+          >
+            <:description>Which animals?</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert text(html, "fieldset > legend") == "Animals"
+      assert attribute(html, "input[type='hidden']", "name") == "animals[]"
+      assert attribute(html, "input[type='hidden']", "value") == ""
+
+      input = find_one(html, "input[id='animals_dog']")
+      assert attribute(input, "value") == "dog"
+
+      input = find_one(html, "input[id='animals_cat']")
+      assert attribute(input, "value") == "cat"
+    end
+
+    test "with radio group" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:animals]}
+            label="Animals"
+            type="radio-group"
+            options={[{"Dog", "dog"}, "cat", "rabbit_id", :elk]}
+          >
+            <:description>Which animals?</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert text(html, "fieldset > legend") == "Animals"
+
+      input = find_one(html, "input[id='animals_dog']")
+      assert attribute(input, "value") == "dog"
+
+      input = find_one(html, "input[id='animals_cat']")
+      assert attribute(input, "value") == "cat"
+    end
+
+    test "with switch off" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:subscribe]}
+            label="Subscribe"
+            type="switch"
+          >
+            <:description>Subscribe?</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert text(html, ".switch-state-off") == "Off"
+    end
+
+    test "with switch on" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:subscribe]}
+            label="Subscribe"
+            type="switch"
+            checked
+          >
+            <:description>Subscribe?</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert text(html, ".switch-state-on") == "On"
+    end
+
+    test "with select" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:animals]}
+            label="Animals"
+            type="select"
+            options={[{"Dog", "dog"}, {"Cat", "cat"}]}
+          >
+            <:description>Which animals?</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert attribute(html, "option:first-child", "value") == "dog"
+      assert attribute(html, "option:last-child", "value") == "cat"
+    end
+
+    test "with multiple select" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:animals]}
+            label="Animals"
+            type="select"
+            options={[{"Dog", "dog"}, {"Cat", "cat"}]}
+            multiple
+          >
+            <:description>Which animals?</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert attribute(html, "select", "multiple") == "multiple"
+    end
+
+    test "with textarea" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={@form[:bio]} label="Bio" type="textarea">
+            <:description>Tell us more about you.</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      textarea = find_one(html, "textarea")
+      assert attribute(textarea, "id") == "bio"
+      assert attribute(textarea, "name") == "bio"
+      assert attribute(textarea, "aria-describedby") == "bio_description"
+
+      assert attribute(html, "label", "for") == "bio"
+      assert text(html, "label") == "Bio"
+    end
+
+    test "with hidden input" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={@form[:sentiment]} type="hidden" value="jaja" />
+        </.form>
+        """)
+
+      assert attribute(html, "input", "type") == "hidden"
+      assert attribute(html, "input", "name") == "sentiment"
+      assert attribute(html, "input", "value") == "jaja"
+    end
+
+    test "with hidden input and list value" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:sentiment]}
+            type="hidden"
+            value={["ja", "ne"]}
+          />
+        </.form>
+        """)
+
+      assert attribute(html, "input:first-child", "type") == "hidden"
+      assert attribute(html, "input:last-child", "type") == "hidden"
+      assert attribute(html, "input:first-child", "name") == "sentiment[]"
+      assert attribute(html, "input:last-child", "name") == "sentiment[]"
+      assert attribute(html, "input:first-child", "value") == "ja"
+      assert attribute(html, "input:last-child", "value") == "ne"
+    end
+
+    test "with add-ons" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={@form[:addons]} type="text">
+            <:addon_left>left</:addon_left>
+            <:addon_right>right</:addon_right>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert text(html, ".field-input-wrapper > .field-input-addon-left") ==
+               "left"
+
+      assert text(html, ".field-input-wrapper > .field-input-addon-right") ==
+               "right"
+    end
+
+    test "with datalist" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:species]}
+            type="text"
+            options={["option_a", {"Option B", "option_b"}]}
+          />
+        </.form>
+        """)
+
+      assert attribute(html, "input", "list") == "species_datalist"
+      assert attribute(html, "datalist", "id") == "species_datalist"
+
+      assert attribute(html, "datalist > option:first-child", "value") ==
+               "option_a"
+
+      assert attribute(html, "datalist > option:last-child", "value") ==
+               "option_b"
+
+      assert text(html, "datalist > option:first-child") == "option_a"
+      assert text(html, "datalist > option:last-child") == "Option B"
+    end
+
+    test "with errors" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={@form[:species]} type="text" errors={["wrong"]} />
+        </.form>
+        """)
+
+      assert attribute(html, "input", "aria-invalid") == "true"
+      assert attribute(html, "input", "aria-errormessage") == "species_errors"
+      assert attribute(html, "ul", "id") == "species_errors"
+      assert attribute(html, "ul", "class") == "field-errors"
+      assert text(html, ".field-errors > li") == "wrong"
+    end
+
+    test "with errors and description" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={@form[:species]} type="text" errors={["wrong"]}>
+            <:description>What are you?</:description>
+          </TestComponents.input>
+        </.form>
+        """)
+
+      assert attribute(html, "input", "aria-invalid") == "true"
+      assert attribute(html, "input", "aria-errormessage") == "species_errors"
+
+      assert attribute(html, "input", "aria-describedby") ==
+               "species_description"
+
+      assert attribute(html, "ul", "id") == "species_errors"
+      assert attribute(html, "ul", "class") == "field-errors"
+      assert text(html, ".field-errors > li") == "wrong"
+
+      assert text(html, ".field-description") == "What are you?"
+
+      assert attribute(html, ".field-description", "id") ==
+               "species_description"
+    end
+
+    test "converts datetime to date string for date input" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:when]}
+            type="date"
+            value={~U[1900-01-01T12:00:00Z]}
+          />
+        </.form>
+        """)
+
+      assert attribute(html, "input", "value") == "1900-01-01"
+    end
+
+    test "converts datetime string to date string for date input" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={@form[:when]}
+            type="date"
+            value="1900-01-01T12:00:00Z"
+          />
+        </.form>
+        """)
+
+      assert attribute(html, "input", "value") == "1900-01-01"
+    end
+
+    test "removes other invalid date values" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={@form[:when]} type="date" value="1900-01" />
+        </.form>
+        """)
+
+      assert attribute(html, "input", "value") == ""
+    end
+
+    test "hides errors if field is unused" do
+      assigns = %{form: to_form(%{})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={%{@form[:what] | errors: [{"weird", []}]}} />
+        </.form>
+        """)
+
+      assert Floki.find(html, ".field-errors") == []
+
+      assigns = %{form: to_form(%{"what" => "what", "_unused_what" => ""})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={%{@form[:what] | errors: [{"weird", []}]}} />
+        </.form>
+        """)
+
+      assert Floki.find(html, ".field-errors") == []
+    end
+
+    test "inserts gettext variables in errors without gettext module" do
+      assigns = %{form: to_form(%{"what" => "what"})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={
+            %{@form[:what] | errors: [{"weird %{animal}", [animal: "dog"]}]}
+          } />
+        </.form>
+        """)
+
+      assert text(html, ".field-errors > li") == "weird dog"
+    end
+
+    test "translates errors with gettext" do
+      assigns = %{form: to_form(%{"what" => "what"})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input field={%{@form[:what] | errors: [{"weird dog", []}]}} />
+        </.form>
+        """)
+
+      assert text(html, ".field-errors > li") == "chien bizarre"
+    end
+
+    test "translates errors with numbers with gettext" do
+      assigns = %{form: to_form(%{"what" => "what"})}
+
+      html =
+        parse_heex(~H"""
+        <.form for={@form}>
+          <TestComponents.input
+            field={
+              %{
+                @form[:what]
+                | errors: [
+                    {"only %{count} dog(s) allowed", [count: 5]}
+                  ]
+              }
+            }
+            gettext={Doggo.Gettext}
+          />
+        </.form>
+        """)
+
+      assert text(html, ".field-errors > li") == "seulement 5 chiens autorisés"
+    end
+  end
+
   describe "label/1" do
     test "default" do
       assigns = %{}
diff --git a/test/doggo/storybook_test.exs b/test/doggo/storybook_test.exs
index 43ad247..dcc7ac6 100644
--- a/test/doggo/storybook_test.exs
+++ b/test/doggo/storybook_test.exs
@@ -65,6 +65,7 @@ defmodule Doggo.StorybookTest do
     build_frame()
     build_icon(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     build_image(modifiers: [variant: [values: [nil, "yes"], default: nil]])
+    build_input()
 
     build_label(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     build_menu(modifiers: [variant: [values: [nil, "yes"], default: nil]])
diff --git a/test/doggo_test.exs b/test/doggo_test.exs
index 481dcd9..e490df9 100644
--- a/test/doggo_test.exs
+++ b/test/doggo_test.exs
@@ -2,486 +2,6 @@ defmodule DoggoTest do
   use ExUnit.Case
   use Phoenix.Component
 
-  import Doggo.TestHelpers
-
-  describe "input/1" do
-    test "with text input" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:age]} label="Age" />
-        </.form>
-        """)
-
-      input = find_one(html, "input")
-      assert attribute(input, "id") == "age"
-      assert attribute(input, "name") == "age"
-      assert attribute(input, "type") == "text"
-      assert attribute(input, "aria-describedby") == nil
-      assert attribute(input, "aria-invalid") == nil
-      assert attribute(input, "aria-errormessage") == nil
-
-      assert attribute(html, "label", "for") == "age"
-      assert text(html, "label") == "Age"
-    end
-
-    test "with text input and description" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:age]} label="Age">
-            <:description>How old?</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert text(html, ".field-description") == "How old?"
-      assert attribute(html, ".field-description", "id") == "age_description"
-      assert attribute(html, "input", "aria-describedby") == "age_description"
-    end
-
-    test "with checkbox" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:subscribe]} label="Subscribe" type="checkbox">
-            <:description>Please do.</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert attribute(html, "label", "class") == "checkbox"
-      assert attribute(html, "input[type='hidden']", "value") == "false"
-
-      assert text(html, ".field-description") == "Please do."
-
-      assert attribute(html, ".field-description", "id") ==
-               "subscribe_description"
-
-      assert attribute(html, "input[type='checkbox']", "aria-describedby") ==
-               "subscribe_description"
-
-      assert attribute(html, "input[type='checkbox']", "value") == "true"
-    end
-
-    test "with checkbox and checked value" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={@form[:subscribe]}
-            label="Subscribe"
-            type="checkbox"
-            checked_value="yes"
-          >
-            <:description>Please do.</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert attribute(html, "input[type='checkbox']", "value") == "yes"
-    end
-
-    test "with checkbox group" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={@form[:animals]}
-            label="Animals"
-            type="checkbox-group"
-            options={[{"Dog", "dog"}, "cat", "rabbit_id", :elk]}
-            value={["dog", "elk"]}
-          >
-            <:description>Which animals?</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert text(html, "fieldset > legend") == "Animals"
-      assert attribute(html, "input[type='hidden']", "name") == "animals[]"
-      assert attribute(html, "input[type='hidden']", "value") == ""
-
-      input = find_one(html, "input[id='animals_dog']")
-      assert attribute(input, "value") == "dog"
-
-      input = find_one(html, "input[id='animals_cat']")
-      assert attribute(input, "value") == "cat"
-    end
-
-    test "with radio group" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={@form[:animals]}
-            label="Animals"
-            type="radio-group"
-            options={[{"Dog", "dog"}, "cat", "rabbit_id", :elk]}
-          >
-            <:description>Which animals?</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert text(html, "fieldset > legend") == "Animals"
-
-      input = find_one(html, "input[id='animals_dog']")
-      assert attribute(input, "value") == "dog"
-
-      input = find_one(html, "input[id='animals_cat']")
-      assert attribute(input, "value") == "cat"
-    end
-
-    test "with switch off" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:subscribe]} label="Subscribe" type="switch">
-            <:description>Subscribe?</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert text(html, ".switch-state-off") == "Off"
-    end
-
-    test "with switch on" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:subscribe]} label="Subscribe" type="switch" checked>
-            <:description>Subscribe?</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert text(html, ".switch-state-on") == "On"
-    end
-
-    test "with select" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={@form[:animals]}
-            label="Animals"
-            type="select"
-            options={[{"Dog", "dog"}, {"Cat", "cat"}]}
-          >
-            <:description>Which animals?</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert attribute(html, "option:first-child", "value") == "dog"
-      assert attribute(html, "option:last-child", "value") == "cat"
-    end
-
-    test "with multiple select" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={@form[:animals]}
-            label="Animals"
-            type="select"
-            options={[{"Dog", "dog"}, {"Cat", "cat"}]}
-            multiple
-          >
-            <:description>Which animals?</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert attribute(html, "select", "multiple") == "multiple"
-    end
-
-    test "with textarea" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:bio]} label="Bio" type="textarea">
-            <:description>Tell us more about you.</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      textarea = find_one(html, "textarea")
-      assert attribute(textarea, "id") == "bio"
-      assert attribute(textarea, "name") == "bio"
-      assert attribute(textarea, "aria-describedby") == "bio_description"
-
-      assert attribute(html, "label", "for") == "bio"
-      assert text(html, "label") == "Bio"
-    end
-
-    test "with hidden input" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:sentiment]} type="hidden" value="jaja" />
-        </.form>
-        """)
-
-      assert attribute(html, "input", "type") == "hidden"
-      assert attribute(html, "input", "name") == "sentiment"
-      assert attribute(html, "input", "value") == "jaja"
-    end
-
-    test "with hidden input and list value" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:sentiment]} type="hidden" value={["ja", "ne"]} />
-        </.form>
-        """)
-
-      assert attribute(html, "input:first-child", "type") == "hidden"
-      assert attribute(html, "input:last-child", "type") == "hidden"
-      assert attribute(html, "input:first-child", "name") == "sentiment[]"
-      assert attribute(html, "input:last-child", "name") == "sentiment[]"
-      assert attribute(html, "input:first-child", "value") == "ja"
-      assert attribute(html, "input:last-child", "value") == "ne"
-    end
-
-    test "with add-ons" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:addons]} type="text">
-            <:addon_left>left</:addon_left>
-            <:addon_right>right</:addon_right>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert text(html, ".input-wrapper > .input-addon-left") == "left"
-      assert text(html, ".input-wrapper > .input-addon-right") == "right"
-    end
-
-    test "with datalist" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={@form[:species]}
-            type="text"
-            options={["option_a", {"Option B", "option_b"}]}
-          />
-        </.form>
-        """)
-
-      assert attribute(html, "input", "list") == "species_datalist"
-      assert attribute(html, "datalist", "id") == "species_datalist"
-
-      assert attribute(html, "datalist > option:first-child", "value") ==
-               "option_a"
-
-      assert attribute(html, "datalist > option:last-child", "value") ==
-               "option_b"
-
-      assert text(html, "datalist > option:first-child") == "option_a"
-      assert text(html, "datalist > option:last-child") == "Option B"
-    end
-
-    test "with errors" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:species]} type="text" errors={["wrong"]} />
-        </.form>
-        """)
-
-      assert attribute(html, "input", "aria-invalid") == "true"
-      assert attribute(html, "input", "aria-errormessage") == "species_errors"
-      assert attribute(html, "ul", "id") == "species_errors"
-      assert attribute(html, "ul", "class") == "field-errors"
-      assert text(html, ".field-errors > li") == "wrong"
-    end
-
-    test "with errors and description" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:species]} type="text" errors={["wrong"]}>
-            <:description>What are you?</:description>
-          </Doggo.input>
-        </.form>
-        """)
-
-      assert attribute(html, "input", "aria-invalid") == "true"
-      assert attribute(html, "input", "aria-errormessage") == "species_errors"
-
-      assert attribute(html, "input", "aria-describedby") ==
-               "species_description"
-
-      assert attribute(html, "ul", "id") == "species_errors"
-      assert attribute(html, "ul", "class") == "field-errors"
-      assert text(html, ".field-errors > li") == "wrong"
-
-      assert text(html, ".field-description") == "What are you?"
-
-      assert attribute(html, ".field-description", "id") ==
-               "species_description"
-    end
-
-    test "converts datetime to date string for date input" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={@form[:when]}
-            type="date"
-            value={~U[1900-01-01T12:00:00Z]}
-          />
-        </.form>
-        """)
-
-      assert attribute(html, "input", "value") == "1900-01-01"
-    end
-
-    test "converts datetime string to date string for date input" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:when]} type="date" value="1900-01-01T12:00:00Z" />
-        </.form>
-        """)
-
-      assert attribute(html, "input", "value") == "1900-01-01"
-    end
-
-    test "removes other invalid date values" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={@form[:when]} type="date" value="1900-01" />
-        </.form>
-        """)
-
-      assert attribute(html, "input", "value") == ""
-    end
-
-    test "hides errors if field is unused" do
-      assigns = %{form: to_form(%{})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={%{@form[:what] | errors: [{"weird", []}]}} />
-        </.form>
-        """)
-
-      assert Floki.find(html, ".field-errors") == []
-
-      assigns = %{form: to_form(%{"what" => "what", "_unused_what" => ""})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={%{@form[:what] | errors: [{"weird", []}]}} />
-        </.form>
-        """)
-
-      assert Floki.find(html, ".field-errors") == []
-    end
-
-    test "inserts gettext variables in errors without gettext module" do
-      assigns = %{form: to_form(%{"what" => "what"})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input field={
-            %{@form[:what] | errors: [{"weird %{animal}", [animal: "dog"]}]}
-          } />
-        </.form>
-        """)
-
-      assert text(html, ".field-errors > li") == "weird dog"
-    end
-
-    test "translates errors with gettext" do
-      assigns = %{form: to_form(%{"what" => "what"})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={%{@form[:what] | errors: [{"weird dog", []}]}}
-            gettext={Doggo.Gettext}
-          />
-        </.form>
-        """)
-
-      assert text(html, ".field-errors > li") == "chien bizarre"
-    end
-
-    test "translates errors with numbers with gettext" do
-      assigns = %{form: to_form(%{"what" => "what"})}
-
-      html =
-        parse_heex(~H"""
-        <.form for={@form}>
-          <Doggo.input
-            field={
-              %{
-                @form[:what]
-                | errors: [
-                    {"only %{count} dog(s) allowed", [count: 5]}
-                  ]
-              }
-            }
-            gettext={Doggo.Gettext}
-          />
-        </.form>
-        """)
-
-      assert text(html, ".field-errors > li") == "seulement 5 chiens autorisés"
-    end
-  end
-
   defmodule TestComponents do
     @moduledoc """
     Generates components for tests.