Repository: woylie/doggo
Pull request #326 (2024-07-23 05:21:34)

rename input
- **rename input to field**
- **add note about dependencies**



Patch:
diff --git a/demo/lib/demo_web/components/core_components.ex b/demo/lib/demo_web/components/core_components.ex
index 2e997f4..0c28909 100644
--- a/demo/lib/demo_web/components/core_components.ex
+++ b/demo/lib/demo_web/components/core_components.ex
@@ -25,6 +25,7 @@ defmodule DemoWeb.CoreComponents do
   build_drawer()
   build_fab()
   build_fallback()
+  build_field()
   build_field_description()
   build_field_errors()
   build_field_group()
@@ -32,7 +33,6 @@ 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/field.story.exs b/demo/storybook/form/field.story.exs
new file mode 100644
index 0000000..026c929
--- /dev/null
+++ b/demo/storybook/form/field.story.exs
@@ -0,0 +1,4 @@
+defmodule Storybook.Components.Field do
+  use PhoenixStorybook.Story, :component
+  use Doggo.Storybook, module: DemoWeb.CoreComponents, name: :field
+end
diff --git a/demo/storybook/form/input.story.exs b/demo/storybook/form/input.story.exs
deleted file mode 100644
index 94fef62..0000000
--- a/demo/storybook/form/input.story.exs
+++ /dev/null
@@ -1,4 +0,0 @@
-defmodule Storybook.Components.Input do
-  use PhoenixStorybook.Story, :component
-  use Doggo.Storybook, module: DemoWeb.CoreComponents, name: :input
-end
diff --git a/lib/doggo/components.ex b/lib/doggo/components.ex
index 22c6b68..cfd3421 100644
--- a/lib/doggo/components.ex
+++ b/lib/doggo/components.ex
@@ -2410,1064 +2410,1072 @@ defmodule Doggo.Components do
   )
 
   component(
-    :field_description,
-    name: :field_description,
-    base_class: "field-description",
+    :field,
     modifiers: [],
+    extra: [
+      gettext_module: nil,
+      addon_left_class: "has-addon-left",
+      addon_right_class: "has-addon-right"
+    ],
     doc: """
-    Renders the description of an input.
+    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: """
-    ```heex
-    <.field_description for="name">
-      max. 100 characters
-    </.field_description>
-    ```
-    """,
-    type: :form,
-    since: "0.6.0",
-    maturity: :developing,
-    attrs_and_slots:
-      quote do
-        attr :for, :string, required: true, doc: "The ID of the input."
-        attr :rest, :global, doc: "Any additional HTML attributes."
+    ### Dependencies
 
-        slot :inner_block, required: true
-      end,
-    heex:
-      quote do
-        %{for: for} = var!(assigns)
+    This component requires the `field_description`, `field_errors`,
+    `field_group`, and `label` components to be compiled with their default
+    names in the same module.
 
-        var!(assigns) =
-          assign(var!(assigns), :id, Doggo.field_description_id(for))
+    ### Types
 
-        ~H"""
-        <div id={@id} class={@class}>
-          <%= render_slot(@inner_block) %>
-        </div>
-        """
-      end
-  )
+    In addition to all HTML input types, the following type values are also
+    supported:
 
-  component(
-    :field_errors,
-    name: :field_errors,
-    base_class: "field-errors",
-    modifiers: [],
-    doc: """
-    Renders the errors for an input.
-    """,
-    usage: """
-    ```heex
-    <.field_errors for="name" errors={["too many characters"]} />
-    ```
-    """,
-    type: :form,
-    since: "0.6.0",
-    maturity: :developing,
-    attrs_and_slots:
-      quote do
-        attr :for, :string, required: true, doc: "The ID of the input."
-        attr :errors, :list, required: true, doc: "A list of errors as strings."
-        attr :rest, :global, doc: "Any additional HTML attributes."
-      end,
-    heex:
-      quote do
-        %{for: for} = var!(assigns)
+    - `"select"`
+    - `"checkbox-group"`
+    - `"radio-group"`
+    - `"switch"`
 
-        var!(assigns) =
-          assign(var!(assigns), :id, Doggo.field_errors_id(for))
+    ### Class and Global Attribute
 
-        ~H"""
-        <ul :if={@errors != []} id={@id} class={@class} {@rest}>
-          <li :for={error <- @errors}><%= error %></li>
-        </ul>
-        """
-      end
-  )
+    Note that the `class` attribute is applied to the outer container, while
+    the `rest` global attribute is applied to the `<input>` element.
 
-  component(
-    :field_group,
-    name: :field_group,
-    base_class: "field-group",
-    modifiers: [],
-    doc: """
-    Use the field group component to visually group multiple inputs in a form.
+    ### Gettext
 
-    This component is intended for styling purposes and does not provide semantic
-    grouping. For semantic grouping of related form elements, use the `<fieldset>`
-    and `<legend>` HTML elements instead.
-    """,
-    usage: """
-    Visual grouping of inputs:
+    To translate field errors using Gettext, set the `gettext_module` option
+    when building the component:
+
+        build_field(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
-    <.field_group>
-      <.input field={@form[:given_name]} label="Given name" />
-      <.input field={@form[:family_name]} label="Family name"/>
-    </.field_group>
+    <.form class="is-horizontal">
+      <!-- inputs -->
+    </.form>
     ```
 
-    Semantic grouping (for reference):
+    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
-    <fieldset>
-      <legend>Personal Information</legend>
-      <.input field={@form[:given_name]} label="Given name" />
-      <.input field={@form[:family_name]} label="Family name"/>
-    </fieldset>
+    <.form class="has-visually-hidden-labels">
+      <!-- inputs -->
+    </.form>
     ```
-    """,
-    type: :form,
-    since: "0.6.0",
-    maturity: :developing,
-    attrs_and_slots:
-      quote do
-        attr :rest, :global, doc: "Any additional HTML attributes."
 
-        slot :inner_block, required: true
-      end,
-    heex:
-      quote do
-        ~H"""
-        <div class={@class} {@rest}>
-          <%= render_slot(@inner_block) %>
-        </div>
-        """
-      end
-  )
+    Ensure to take checkbox and radio labels into consideration when writing the
+    CSS styles.
 
-  component(
-    :frame,
-    name: :frame,
-    base_class: "frame",
-    modifiers: [
-      ratio: [
-        values: [
-          nil,
-          "1-by-1",
-          "3-by-2",
-          "2-by-3",
-          "4-by-3",
-          "3-by-4",
-          "5-by-4",
-          "4-by-5",
-          "16-by-9",
-          "9-by-16"
-        ],
-        default: nil
-      ],
-      shape: [values: [nil, "circle"], default: nil]
-    ],
-    doc: """
-    Renders a frame with an aspect ratio for images or videos.
+    ### Examples
 
-    This component is used within the `image/1` component.
-    """,
-    usage: """
-    Rendering an image with the aspect ratio 4:3.
+    ```heex
+    <.field field={@form[:name]} />
+    ```
 
     ```heex
-    <.frame ratio={{4, 3}}>
-      <img src="image.png" alt="An example image illustrating the usage." />
-    </.frame>
+    <.field field={@form[:email]} type="email" />
     ```
 
-    Rendering an image as a circle.
+    #### 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
-    <.frame circle>
-      <img src="image.png" alt="An example image illustrating the usage." />
-    </.frame>
+    <.field
+      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: :media,
+    type: :form,
     since: "0.6.0",
     maturity: :developing,
     attrs_and_slots:
       quote do
-        attr :rest, :global, doc: "Any additional HTML attributes."
-        slot :inner_block
-      end,
-    heex:
-      quote do
-        ~H"""
-        <div class={@class} {@rest}>
-          <%= render_slot(@inner_block) %>
-        </div>
-        """
-      end
-  )
-
-  component(
-    :icon,
-    modifiers: [
-      size: [values: ["small", "normal", "medium", "large"], default: "normal"]
-    ],
-    extra: [
-      visually_hidden_class: "is-visually-hidden"
-    ],
-    doc: """
-    Renders a customizable icon using a slot for SVG content.
+        attr :id, :any, default: nil
+        attr :name, :any
 
-    This component does not bind you to a specific set of icons. Instead, it
-    provides a slot for inserting SVG content from any icon library you choose.
-    """,
-    usage: """
-    Render an icon with text as `aria-label` using the `heroicons` library:
+        attr :label, :string,
+          default: nil,
+          doc: """
+          Required for all types except `"hidden"`.
+          """
 
-    ```heex
-    <.icon label="report bug"><Heroicons.bug_ant /></.icon>
-    ```
+        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.
+          """
 
-    To display the text visibly:
+        attr :value, :any
 
-    ```heex
-    <.icon label="report bug" label_placement={:right}>
-      <Heroicons.bug_ant />
-    </.icon>
-    ```
+        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)
 
-    > #### aria-hidden {: .info}
-    >
-    > Not all icon libraries set the `aria-hidden` attribute by default. Always
-    > make sure that it is set on the `<svg>` element that the library renders.
-    """,
-    type: :media,
-    since: "0.6.0",
-    maturity: :developing,
-    attrs_and_slots:
-      quote do
-        slot :inner_block, doc: "Slot for the SVG element.", required: true
+        attr :field, Phoenix.HTML.FormField,
+          doc: "A form field struct, for example: @form[:name]"
 
-        attr :label, :string,
+        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: """
-          Text that describes the icon. If `label_placement` is set to `:hidden`,
-          this text is set as `aria-label` attribute.
+          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 :label_placement, :atom,
-          default: :hidden,
-          values: [:left, :right, :hidden],
+        attr :multiple, :boolean,
+          default: false,
           doc: """
-          Position of the label relative to the icon. If set to `:hidden`, the
-          `label` text is used as `aria-label` attribute.
+          Sets the `multiple` attribute on a select element to allow selecting
+          multiple options.
           """
 
-        attr :rest, :global, doc: "Any additional HTML attributes."
-      end,
-    heex: fn extra ->
-      visually_hidden_class = Keyword.fetch!(extra, :visually_hidden_class)
+        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)
 
-      quote do
-        var!(assigns) =
-          assigns
-          |> var!()
-          |> Map.update!(
-            :class,
-            &(&1 ++
-                [Doggo.label_placement_class(var!(assigns).label_placement)])
-          )
-          |> assign(:visually_hidden_class, unquote(visually_hidden_class))
+        attr :gettext, :atom,
+          doc: """
+          The Gettext module to use for translating error messages. This option can
+          also be set globally, see above.
+          """
 
-        ~H"""
-        <span class={@class} {@rest}>
-          <%= render_slot(@inner_block) %>
-          <span
-            :if={@label}
-            class={@label_placement == :hidden && @visually_hidden_class}
-          >
-            <%= @label %>
-          </span>
-        </span>
-        """
-      end
-    end
-  )
+        attr :required_text, :atom,
+          doc: """
+          The presentational text or symbol to be added to labels of required inputs.
 
-  component(
-    :icon_sprite,
-    base_class: "icon",
-    maturity: :developing,
-    modifiers: [
-      size: [values: ["small", "normal", "medium", "large"], default: "normal"]
-    ],
-    extra: [
-      visually_hidden_class: "is-visually-hidden"
-    ],
-    doc: """
-    Renders an icon using an SVG sprite.
-    """,
-    usage: """
-    Render an icon with text as `aria-label`:
+          This option can also be set globally:
 
-    ```heex
-    <.icon name="arrow-left" label="Go back" />
-    ```
+              config :doggo, required_text: "required"
+          """
 
-    To display the text visibly:
+        attr :required_title, :atom,
+          doc: """
+          The `title` attribute for the `required_text`.
 
-    ```heex
-    <.icon name="arrow-left" label="Go back" label_placement={:right} />
-    ```
-    """,
-    type: :media,
-    since: "0.6.0",
-    attrs_and_slots:
-      quote do
-        attr :name, :string,
-          required: true,
-          doc: "Icon name as used in the sprite."
+          This option can also be set globally:
 
-        attr :sprite_url, :string,
-          default: "/assets/icons/sprite.svg",
-          doc: "The URL of the SVG sprite."
+              config :doggo, required_title: "required"
+          """
 
-        attr :label, :string,
-          default: nil,
+        slot :description,
+          doc: "A field description to render underneath the input."
+
+        slot :addon_left,
           doc: """
-          Text that describes the icon. If `label_placement` is set to `:hidden`, this
-          text is set as `aria-label` attribute.
+          Can be used to render an icon left in the input. Only supported for
+          single-line inputs.
           """
 
-        attr :label_placement, :atom,
-          default: :hidden,
-          values: [:left, :right, :hidden],
+        slot :addon_right,
           doc: """
-          Position of the label relative to the icon. If set to `:hidden`, the
-          `label` text is used as `aria-label` attribute.
+          Can be used to render an icon left in the input. Only supported for
+          single-line inputs.
           """
-
-        attr :rest, :global, doc: "Any additional HTML attributes."
       end,
-    heex: fn extra ->
-      visually_hidden_class = Keyword.fetch!(extra, :visually_hidden_class)
+    component_function: fn opts, extra ->
+      name = Keyword.fetch!(opts, :name)
+      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
-        var!(assigns) =
+        def unquote(name)(
+              %{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!()
-          |> Map.update!(
-            :class,
-            &(&1 ++
-                [Doggo.label_placement_class(var!(assigns).label_placement)])
+          |> 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(:visually_hidden_class, unquote(visually_hidden_class))
+          |> 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)
+          |> unquote(name)()
+        end
 
-        ~H"""
-        <span class={@class} {@rest}>
-          <svg aria-hidden="true"><use href={"#{@sprite_url}##{@name}"} /></svg>
-          <span
-            :if={@label}
-            class={@label_placement == :hidden && @visually_hidden_class}
-          >
-            <%= @label %>
-          </span>
-        </span>
-        """
-      end
+        def unquote(name)(%{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 unquote(name)(%{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 unquote(name)(%{type: "hidden", value: values} = var!(assigns))
+            when is_list(values) do
+          ~H"""
+          <input :for={value <- @value} type="hidden" name={@name <> "[]"} value={value} />
+          """
+        end
+
+        def unquote(name)(%{type: "hidden"} = var!(assigns)) do
+          ~H"""
+          <input type="hidden" name={@name} value={@value} />
+          """
+        end
+
+        def unquote(name)(%{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 unquote(name)(%{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 unquote(name)(%{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 unquote(name)(%{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 unquote(name)(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
   )
 
   component(
-    :image,
-    modifiers: [
-      ratio: [
-        values: [
-          nil,
-          "1-by-1",
-          "3-by-2",
-          "2-by-3",
-          "4-by-3",
-          "3-by-4",
-          "5-by-4",
-          "4-by-5",
-          "16-by-9",
-          "9-by-16"
-        ],
-        default: nil
-      ]
-    ],
+    :field_description,
+    name: :field_description,
+    base_class: "field-description",
+    modifiers: [],
     doc: """
-    Renders an image with an optional caption.
-
-    Note that this component relies on the frame component being compiled in the
-    same module with `frame/1` with the default name (`:frame`). If you
-    override the default ratios, ensure that both the `image` component and
-    the `frame` component are generated with the same values.
+    Renders the description of an input.
     """,
     usage: """
     ```heex
-    <.image
-      src="https://github.com/woylie/doggo/blob/main/assets/dog_poncho.jpg?raw=true"
-      alt="A dog wearing a colorful poncho walks down a fashion show runway."
-      ratio={{16, 9}}
-    >
-      <:caption>
-        Spotlight on canine couture: A dog fashion show where four-legged models
-        dazzle the runway with the latest in pet apparel.
-      </:caption>
-    </.image>
+    <.field_description for="name">
+      max. 100 characters
+    </.field_description>
     ```
     """,
-    type: :media,
+    type: :form,
     since: "0.6.0",
     maturity: :developing,
     attrs_and_slots:
       quote do
-        attr :src, :string,
-          required: true,
-          doc: "The URL of the image to render."
+        attr :for, :string, required: true, doc: "The ID of the input."
+        attr :rest, :global, doc: "Any additional HTML attributes."
 
-        attr :srcset, :any,
-          default: nil,
-          doc: """
-          A set of image URLs in different sizes. Can be passed as a string or a map.
+        slot :inner_block, required: true
+      end,
+    heex:
+      quote do
+        %{for: for} = var!(assigns)
 
-          For example, this map:
+        var!(assigns) =
+          assign(var!(assigns), :id, Doggo.field_description_id(for))
 
-              %{
-                "1x" => "images/image-1x.jpg",
-                "2x" => "images/image-2x.jpg"
-              }
+        ~H"""
+        <div id={@id} class={@class}>
+          <%= render_slot(@inner_block) %>
+        </div>
+        """
+      end
+  )
 
-          Will result in this `srcset`:
+  component(
+    :field_errors,
+    name: :field_errors,
+    base_class: "field-errors",
+    modifiers: [],
+    doc: """
+    Renders the errors for an input.
+    """,
+    usage: """
+    ```heex
+    <.field_errors for="name" errors={["too many characters"]} />
+    ```
+    """,
+    type: :form,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
+        attr :for, :string, required: true, doc: "The ID of the input."
+        attr :errors, :list, required: true, doc: "A list of errors as strings."
+        attr :rest, :global, doc: "Any additional HTML attributes."
+      end,
+    heex:
+      quote do
+        %{for: for} = var!(assigns)
 
-              "images/image-1x.jpg 1x, images/image-2x.jpg 2x"
+        var!(assigns) =
+          assign(var!(assigns), :id, Doggo.field_errors_id(for))
 
-          See https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset.
-          """
+        ~H"""
+        <ul :if={@errors != []} id={@id} class={@class} {@rest}>
+          <li :for={error <- @errors}><%= error %></li>
+        </ul>
+        """
+      end
+  )
 
-        attr :sizes, :string,
-          default: nil,
-          doc: """
-          Specifies media conditions for the image widths, if the `srcset` attribute
-          uses intrinsic widths.
+  component(
+    :field_group,
+    name: :field_group,
+    base_class: "field-group",
+    modifiers: [],
+    doc: """
+    Use the field group component to visually group multiple inputs in a form.
 
-          See https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes.
-          """
+    This component is intended for styling purposes and does not provide semantic
+    grouping. For semantic grouping of related form elements, use the `<fieldset>`
+    and `<legend>` HTML elements instead.
+    """,
+    usage: """
+    Visual grouping of inputs:
 
-        attr :alt, :string,
-          required: true,
-          doc: """
-          A text description of the image for screen reader users and those with slow
-          internet. Effective alt text should concisely capture the image's essence
-          and function, considering its context within the content. Aim for clarity
-          and inclusivity without repeating information already conveyed by
-          surrounding text, and avoid starting with "Image of" as screen readers
-          automatically announce image presence.
-          """
+    ```heex
+    <.field_group>
+      <.field field={@form[:given_name]} label="Given name" />
+      <.field field={@form[:family_name]} label="Family name"/>
+    </.field_group>
+    ```
 
-        attr :width, :integer, default: nil
-        attr :height, :integer, default: nil
-        attr :loading, :string, values: ["eager", "lazy"], default: "lazy"
+    Semantic grouping (for reference):
+
+    ```heex
+    <fieldset>
+      <legend>Personal Information</legend>
+      <.field field={@form[:given_name]} label="Given name" />
+      <.field field={@form[:family_name]} label="Family name"/>
+    </fieldset>
+    ```
+    """,
+    type: :form,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
         attr :rest, :global, doc: "Any additional HTML attributes."
 
-        slot :caption
+        slot :inner_block, required: true
       end,
     heex:
       quote do
         ~H"""
-        <figure class={@class} {@rest}>
-          <.frame ratio={@ratio}>
-            <img
-              src={@src}
-              width={@width}
-              height={@height}
-              alt={@alt}
-              loading={@loading}
-              srcset={Doggo.build_srcset(@srcset)}
-              sizes={@sizes}
-            />
-          </.frame>
-          <figcaption :if={@caption != []}><%= render_slot(@caption) %></figcaption>
-        </figure>
+        <div class={@class} {@rest}>
+          <%= render_slot(@inner_block) %>
+        </div>
         """
       end
   )
 
   component(
-    :input,
-    base_class: "field",
-    modifiers: [],
-    extra: [
-      gettext_module: nil,
-      addon_left_class: "has-addon-left",
-      addon_right_class: "has-addon-right"
+    :frame,
+    name: :frame,
+    base_class: "frame",
+    modifiers: [
+      ratio: [
+        values: [
+          nil,
+          "1-by-1",
+          "3-by-2",
+          "2-by-3",
+          "4-by-3",
+          "3-by-4",
+          "5-by-4",
+          "4-by-5",
+          "16-by-9",
+          "9-by-16"
+        ],
+        default: nil
+      ],
+      shape: [values: [nil, "circle"], default: nil]
     ],
     doc: """
-    Renders a form field including input, label, errors, and description.
+    Renders a frame with an aspect ratio for images or videos.
 
-    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.
+    This component is used within the `image/1` component.
     """,
     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:
+    Rendering an image with the aspect ratio 4:3.
 
     ```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
-    }
+    <.frame ratio={{4, 3}}>
+      <img src="image.png" alt="An example image illustrating the usage." />
+    </.frame>
     ```
 
-    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>`.
+    Rendering an image as a circle.
 
     ```heex
-    <.form class="has-visually-hidden-labels">
-      <!-- inputs -->
-    </.form>
+    <.frame circle>
+      <img src="image.png" alt="An example image illustrating the usage." />
+    </.frame>
     ```
+    """,
+    type: :media,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
+        attr :rest, :global, doc: "Any additional HTML attributes."
+        slot :inner_block
+      end,
+    heex:
+      quote do
+        ~H"""
+        <div class={@class} {@rest}>
+          <%= render_slot(@inner_block) %>
+        </div>
+        """
+      end
+  )
 
-    Ensure to take checkbox and radio labels into consideration when writing the
-    CSS styles.
-
-    ### Examples
+  component(
+    :icon,
+    modifiers: [
+      size: [values: ["small", "normal", "medium", "large"], default: "normal"]
+    ],
+    extra: [
+      visually_hidden_class: "is-visually-hidden"
+    ],
+    doc: """
+    Renders a customizable icon using a slot for SVG content.
 
-    ```heex
-    <.input field={@form[:name]} />
-    ```
+    This component does not bind you to a specific set of icons. Instead, it
+    provides a slot for inserting SVG content from any icon library you choose.
+    """,
+    usage: """
+    Render an icon with text as `aria-label` using the `heroicons` library:
 
     ```heex
-    <.input field={@form[:email]} type="email" />
+    <.icon label="report bug"><Heroicons.bug_ant /></.icon>
     ```
 
-    #### 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.
+    To display the text visibly:
 
     ```heex
-    <.input
-      field={@form[:email]}
-      type="checkbox-group"
-      label="Cuisine"
-      options={[
-        {"Mexican", "mexican"},
-        {"Japanese", "japanese"},
-        {"Libanese", "libanese"}
-      ]}
-    />
+    <.icon label="report bug" label_placement={:right}>
+      <Heroicons.bug_ant />
+    </.icon>
     ```
 
-    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.
+    > #### aria-hidden {: .info}
+    >
+    > Not all icon libraries set the `aria-hidden` attribute by default. Always
+    > make sure that it is set on the `<svg>` element that the library renders.
     """,
-    type: :form,
+    type: :media,
     since: "0.6.0",
     maturity: :developing,
     attrs_and_slots:
       quote do
-        attr :id, :any, default: nil
-        attr :name, :any
+        slot :inner_block, doc: "Slot for the SVG element.", required: true
 
         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.
+          Text that describes the icon. If `label_placement` is set to `:hidden`,
+          this text is set as `aria-label` attribute.
           """
 
-        attr :multiple, :boolean,
-          default: false,
+        attr :label_placement, :atom,
+          default: :hidden,
+          values: [:left, :right, :hidden],
           doc: """
-          Sets the `multiple` attribute on a select element to allow selecting
-          multiple options.
+          Position of the label relative to the icon. If set to `:hidden`, the
+          `label` text is used as `aria-label` attribute.
           """
 
-        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 :rest, :global, doc: "Any additional HTML attributes."
+      end,
+    heex: fn extra ->
+      visually_hidden_class = Keyword.fetch!(extra, :visually_hidden_class)
 
-        attr :required_text, :atom,
-          doc: """
-          The presentational text or symbol to be added to labels of required inputs.
+      quote do
+        var!(assigns) =
+          assigns
+          |> var!()
+          |> Map.update!(
+            :class,
+            &(&1 ++
+                [Doggo.label_placement_class(var!(assigns).label_placement)])
+          )
+          |> assign(:visually_hidden_class, unquote(visually_hidden_class))
 
-          This option can also be set globally:
+        ~H"""
+        <span class={@class} {@rest}>
+          <%= render_slot(@inner_block) %>
+          <span
+            :if={@label}
+            class={@label_placement == :hidden && @visually_hidden_class}
+          >
+            <%= @label %>
+          </span>
+        </span>
+        """
+      end
+    end
+  )
 
-              config :doggo, required_text: "required"
-          """
+  component(
+    :icon_sprite,
+    base_class: "icon",
+    maturity: :developing,
+    modifiers: [
+      size: [values: ["small", "normal", "medium", "large"], default: "normal"]
+    ],
+    extra: [
+      visually_hidden_class: "is-visually-hidden"
+    ],
+    doc: """
+    Renders an icon using an SVG sprite.
+    """,
+    usage: """
+    Render an icon with text as `aria-label`:
 
-        attr :required_title, :atom,
-          doc: """
-          The `title` attribute for the `required_text`.
+    ```heex
+    <.icon name="arrow-left" label="Go back" />
+    ```
 
-          This option can also be set globally:
+    To display the text visibly:
 
-              config :doggo, required_title: "required"
-          """
+    ```heex
+    <.icon name="arrow-left" label="Go back" label_placement={:right} />
+    ```
+    """,
+    type: :media,
+    since: "0.6.0",
+    attrs_and_slots:
+      quote do
+        attr :name, :string,
+          required: true,
+          doc: "Icon name as used in the sprite."
 
-        slot :description,
-          doc: "A field description to render underneath the input."
+        attr :sprite_url, :string,
+          default: "/assets/icons/sprite.svg",
+          doc: "The URL of the SVG sprite."
 
-        slot :addon_left,
+        attr :label, :string,
+          default: nil,
           doc: """
-          Can be used to render an icon left in the input. Only supported for
-          single-line inputs.
+          Text that describes the icon. If `label_placement` is set to `:hidden`, this
+          text is set as `aria-label` attribute.
           """
 
-        slot :addon_right,
+        attr :label_placement, :atom,
+          default: :hidden,
+          values: [:left, :right, :hidden],
           doc: """
-          Can be used to render an icon left in the input. Only supported for
-          single-line inputs.
+          Position of the label relative to the icon. If set to `:hidden`, the
+          `label` text is used as `aria-label` attribute.
           """
+
+        attr :rest, :global, doc: "Any additional HTML attributes."
       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)
+    heex: fn extra ->
+      visually_hidden_class = Keyword.fetch!(extra, :visually_hidden_class)
 
-      # 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: []
-
+        var!(assigns) =
           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
+          |> Map.update!(
+            :class,
+            &(&1 ++
+                [Doggo.label_placement_class(var!(assigns).label_placement)])
           )
-          |> 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
+          |> assign(:visually_hidden_class, unquote(visually_hidden_class))
 
-        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"""
+        <span class={@class} {@rest}>
+          <svg aria-hidden="true"><use href={"#{@sprite_url}##{@name}"} /></svg>
+          <span
+            :if={@label}
+            class={@label_placement == :hidden && @visually_hidden_class}
+          >
+            <%= @label %>
+          </span>
+        </span>
+        """
+      end
+    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
+  component(
+    :image,
+    modifiers: [
+      ratio: [
+        values: [
+          nil,
+          "1-by-1",
+          "3-by-2",
+          "2-by-3",
+          "4-by-3",
+          "3-by-4",
+          "5-by-4",
+          "4-by-5",
+          "16-by-9",
+          "9-by-16"
+        ],
+        default: nil
+      ]
+    ],
+    doc: """
+    Renders an image with an optional caption.
 
-        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
+    Note that this component relies on the frame component being compiled in the
+    same module with `frame/1` with the default name (`:frame`). If you
+    override the default ratios, ensure that both the `image` component and
+    the `frame` component are generated with the same values.
+    """,
+    usage: """
+    ```heex
+    <.image
+      src="https://github.com/woylie/doggo/blob/main/assets/dog_poncho.jpg?raw=true"
+      alt="A dog wearing a colorful poncho walks down a fashion show runway."
+      ratio={{16, 9}}
+    >
+      <:caption>
+        Spotlight on canine couture: A dog fashion show where four-legged models
+        dazzle the runway with the latest in pet apparel.
+      </:caption>
+    </.image>
+    ```
+    """,
+    type: :media,
+    since: "0.6.0",
+    maturity: :developing,
+    attrs_and_slots:
+      quote do
+        attr :src, :string,
+          required: true,
+          doc: "The URL of the image to render."
 
-        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
+        attr :srcset, :any,
+          default: nil,
+          doc: """
+          A set of image URLs in different sizes. Can be passed as a string or a map.
 
-        def input(%{type: "hidden"} = var!(assigns)) do
-          ~H"""
-          <input type="hidden" name={@name} value={@value} />
-          """
-        end
+          For example, this map:
 
-        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
+              %{
+                "1x" => "images/image-1x.jpg",
+                "2x" => "images/image-2x.jpg"
+              }
 
-        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>
+          Will result in this `srcset`:
+
+              "images/image-1x.jpg 1x, images/image-2x.jpg 2x"
+
+          See https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/srcset.
           """
-        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)
+        attr :sizes, :string,
+          default: nil,
+          doc: """
+          Specifies media conditions for the image widths, if the `srcset` attribute
+          uses intrinsic widths.
 
-          ~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>
+          See https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/sizes.
           """
-        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>
+        attr :alt, :string,
+          required: true,
+          doc: """
+          A text description of the image for screen reader users and those with slow
+          internet. Effective alt text should concisely capture the image's essence
+          and function, considering its context within the content. Aim for clarity
+          and inclusivity without repeating information already conveyed by
+          surrounding text, and avoid starting with "Image of" as screen readers
+          automatically announce image presence.
           """
-        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)
-            )
+        attr :width, :integer, default: nil
+        attr :height, :integer, default: nil
+        attr :loading, :string, values: ["eager", "lazy"], default: "lazy"
+        attr :rest, :global, doc: "Any additional HTML attributes."
 
-          ~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
+        slot :caption
+      end,
+    heex:
+      quote do
+        ~H"""
+        <figure class={@class} {@rest}>
+          <.frame ratio={@ratio}>
+            <img
+              src={@src}
+              width={@width}
+              height={@height}
+              alt={@alt}
+              loading={@loading}
+              srcset={Doggo.build_srcset(@srcset)}
+              sizes={@sizes}
+            />
+          </.frame>
+          <figcaption :if={@caption != []}><%= render_slot(@caption) %></figcaption>
+        </figure>
+        """
       end
-    end
   )
 
   @doc false
diff --git a/lib/doggo/storybook/input.ex b/lib/doggo/storybook/field.ex
similarity index 99%
rename from lib/doggo/storybook/input.ex
rename to lib/doggo/storybook/field.ex
index b573b78..accfc2d 100644
--- a/lib/doggo/storybook/input.ex
+++ b/lib/doggo/storybook/field.ex
@@ -1,4 +1,4 @@
-defmodule Doggo.Storybook.Input do
+defmodule Doggo.Storybook.Field do
   @moduledoc false
 
   import Doggo.Storybook.Shared
diff --git a/lib/doggo/storybook/field_description.ex b/lib/doggo/storybook/field_description.ex
index 29d90a1..b4ee355 100644
--- a/lib/doggo/storybook/field_description.ex
+++ b/lib/doggo/storybook/field_description.ex
@@ -2,7 +2,7 @@ defmodule Doggo.Storybook.FieldDescription do
   @moduledoc false
   alias PhoenixStorybook.Stories.Variation
 
-  def dependent_components, do: [:input]
+  def dependent_components, do: [:field]
 
   def variations(opts) do
     [
diff --git a/lib/doggo/storybook/field_errors.ex b/lib/doggo/storybook/field_errors.ex
index 26043ca..574892b 100644
--- a/lib/doggo/storybook/field_errors.ex
+++ b/lib/doggo/storybook/field_errors.ex
@@ -2,7 +2,7 @@ defmodule Doggo.Storybook.FieldErrors do
   @moduledoc false
   alias PhoenixStorybook.Stories.Variation
 
-  def dependent_components, do: [:input]
+  def dependent_components, do: [:field]
 
   def variations(_opts) do
     [
diff --git a/lib/doggo/storybook/field_group.ex b/lib/doggo/storybook/field_group.ex
index 1dbe9ba..c180ba5 100644
--- a/lib/doggo/storybook/field_group.ex
+++ b/lib/doggo/storybook/field_group.ex
@@ -2,7 +2,7 @@ defmodule Doggo.Storybook.FieldGroup do
   @moduledoc false
   alias PhoenixStorybook.Stories.Variation
 
-  def dependent_components, do: [:input]
+  def dependent_components, do: [:field]
 
   def template do
     """
@@ -30,19 +30,19 @@ defmodule Doggo.Storybook.FieldGroup do
 
   defp slots(opts) do
     dependent_components = opts[:dependent_components]
-    input_fun = dependent_components[:input]
+    field_fun = dependent_components[:field]
 
-    if input_fun do
+    if field_fun do
       [
         """
-        <.#{input_fun} type="text" field={f[:given_name]} label="Given name" />
-        <.#{input_fun} type="text" field={f[:family_name]} label="Family name" />
+        <.#{field_fun} type="text" field={f[:given_name]} label="Given name" />
+        <.#{field_fun} type="text" field={f[:family_name]} label="Family name" />
         """
       ]
     else
       [
         """
-        <p>Please compile the <code>input</code> component for a complete preview.</p>
+        <p>Please compile the <code>field</code> component for a complete preview.</p>
         """
       ]
     end
diff --git a/lib/doggo/storybook/label.ex b/lib/doggo/storybook/label.ex
index 69a0814..99fdf22 100644
--- a/lib/doggo/storybook/label.ex
+++ b/lib/doggo/storybook/label.ex
@@ -2,7 +2,7 @@ defmodule Doggo.Storybook.Label do
   @moduledoc false
   alias PhoenixStorybook.Stories.Variation
 
-  def dependent_components, do: [:input]
+  def dependent_components, do: [:field]
 
   def variations(_opts) do
     [


Test patch:
diff --git a/test/doggo/components_test.exs b/test/doggo/components_test.exs
index 62c46ce..dea3dae 100644
--- a/test/doggo/components_test.exs
+++ b/test/doggo/components_test.exs
@@ -38,6 +38,7 @@ defmodule Doggo.ComponentsTest do
     build_drawer()
     build_fab()
     build_fallback()
+    build_field(gettext_module: Doggo.Gettext)
     build_field_description()
     build_field_errors()
     build_field_group()
@@ -45,7 +46,6 @@ defmodule Doggo.ComponentsTest do
     build_icon()
     build_icon_sprite()
     build_image()
-    build_input(gettext_module: Doggo.Gettext)
     build_label()
     build_menu()
     build_menu_bar()
@@ -2686,7 +2686,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={@form[:age]} label="Age" />
+          <TestComponents.field field={@form[:age]} label="Age" />
         </.form>
         """)
 
@@ -2708,9 +2708,9 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={@form[:age]} label="Age">
+          <TestComponents.field field={@form[:age]} label="Age">
             <:description>How old?</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2725,13 +2725,13 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:subscribe]}
             label="Subscribe"
             type="checkbox"
           >
             <:description>Please do.</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2755,14 +2755,14 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:subscribe]}
             label="Subscribe"
             type="checkbox"
             checked_value="yes"
           >
             <:description>Please do.</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2775,7 +2775,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:animals]}
             label="Animals"
             type="checkbox-group"
@@ -2783,7 +2783,7 @@ defmodule Doggo.ComponentsTest do
             value={["dog", "elk"]}
           >
             <:description>Which animals?</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2804,14 +2804,14 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:animals]}
             label="Animals"
             type="radio-group"
             options={[{"Dog", "dog"}, "cat", "rabbit_id", :elk]}
           >
             <:description>Which animals?</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2830,13 +2830,13 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:subscribe]}
             label="Subscribe"
             type="switch"
           >
             <:description>Subscribe?</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2849,14 +2849,14 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:subscribe]}
             label="Subscribe"
             type="switch"
             checked
           >
             <:description>Subscribe?</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2869,14 +2869,14 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:animals]}
             label="Animals"
             type="select"
             options={[{"Dog", "dog"}, {"Cat", "cat"}]}
           >
             <:description>Which animals?</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2890,7 +2890,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:animals]}
             label="Animals"
             type="select"
@@ -2898,7 +2898,7 @@ defmodule Doggo.ComponentsTest do
             multiple
           >
             <:description>Which animals?</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2911,9 +2911,9 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={@form[:bio]} label="Bio" type="textarea">
+          <TestComponents.field field={@form[:bio]} label="Bio" type="textarea">
             <:description>Tell us more about you.</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2932,7 +2932,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={@form[:sentiment]} type="hidden" value="jaja" />
+          <TestComponents.field field={@form[:sentiment]} type="hidden" value="jaja" />
         </.form>
         """)
 
@@ -2947,7 +2947,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:sentiment]}
             type="hidden"
             value={["ja", "ne"]}
@@ -2969,10 +2969,10 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={@form[:addons]} type="text">
+          <TestComponents.field field={@form[:addons]} type="text">
             <:addon_left>left</:addon_left>
             <:addon_right>right</:addon_right>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -2989,7 +2989,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:species]}
             type="text"
             options={["option_a", {"Option B", "option_b"}]}
@@ -3016,7 +3016,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={@form[:species]} type="text" errors={["wrong"]} />
+          <TestComponents.field field={@form[:species]} type="text" errors={["wrong"]} />
         </.form>
         """)
 
@@ -3033,9 +3033,9 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={@form[:species]} type="text" errors={["wrong"]}>
+          <TestComponents.field field={@form[:species]} type="text" errors={["wrong"]}>
             <:description>What are you?</:description>
-          </TestComponents.input>
+          </TestComponents.field>
         </.form>
         """)
 
@@ -3061,7 +3061,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:when]}
             type="date"
             value={~U[1900-01-01T12:00:00Z]}
@@ -3078,7 +3078,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={@form[:when]}
             type="date"
             value="1900-01-01T12:00:00Z"
@@ -3095,7 +3095,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={@form[:when]} type="date" value="1900-01" />
+          <TestComponents.field field={@form[:when]} type="date" value="1900-01" />
         </.form>
         """)
 
@@ -3108,7 +3108,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={%{@form[:what] | errors: [{"weird", []}]}} />
+          <TestComponents.field field={%{@form[:what] | errors: [{"weird", []}]}} />
         </.form>
         """)
 
@@ -3119,7 +3119,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={%{@form[:what] | errors: [{"weird", []}]}} />
+          <TestComponents.field field={%{@form[:what] | errors: [{"weird", []}]}} />
         </.form>
         """)
 
@@ -3132,7 +3132,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={
+          <TestComponents.field field={
             %{@form[:what] | errors: [{"weird %{animal}", [animal: "dog"]}]}
           } />
         </.form>
@@ -3147,7 +3147,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input field={%{@form[:what] | errors: [{"weird dog", []}]}} />
+          <TestComponents.field field={%{@form[:what] | errors: [{"weird dog", []}]}} />
         </.form>
         """)
 
@@ -3160,7 +3160,7 @@ defmodule Doggo.ComponentsTest do
       html =
         parse_heex(~H"""
         <.form for={@form}>
-          <TestComponents.input
+          <TestComponents.field
             field={
               %{
                 @form[:what]
diff --git a/test/doggo/storybook_test.exs b/test/doggo/storybook_test.exs
index dcc7ac6..e295eb0 100644
--- a/test/doggo/storybook_test.exs
+++ b/test/doggo/storybook_test.exs
@@ -49,6 +49,7 @@ defmodule Doggo.StorybookTest do
     build_drawer(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     build_fab(modifiers: [variant: [values: [nil, "yes"], default: nil]])
     build_fallback(modifiers: [variant: [values: [nil, "yes"], default: nil]])
+    build_field()
 
     build_field_description(
       modifiers: [variant: [values: [nil, "yes"], default: nil]]
@@ -65,7 +66,6 @@ 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]])