Packages
petal_components
4.4.0
4.7.0
4.6.2
4.6.1
4.6.0
4.5.0
4.4.0
4.3.0
4.2.1
4.2.0
4.1.2
4.1.1
4.1.0
4.0.12
4.0.11
4.0.10
4.0.6
4.0.5
4.0.4
4.0.3
4.0.1
3.2.2
3.2.1
3.2.0
3.1.0
3.0.2
3.0.1
3.0.0
2.9.3
2.9.2
retired
2.9.1
retired
2.9.0
retired
2.8.4
2.8.3
2.8.2
2.8.1
2.8.0
2.7.4
2.7.3
2.7.2
2.7.1
2.7.0
2.6.1
2.6.0
2.5.2
2.5.1
2.5.0
2.4.3
2.4.2
2.4.1
2.4.0
2.3.0
2.2.1
2.2.0
2.1.2
2.1.1
2.1.0
2.0.6
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.9.3
1.9.2
1.9.1
1.9.0
1.8.0
1.7.1
1.7.0
1.6.2
1.6.1
1.6.0
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.0
1.2.14
1.2.13
1.2.12
1.2.11
1.2.10
1.2.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.19.10
0.19.9
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.0
0.14.1
0.14.0
0.13.7
0.13.6
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.8
0.10.7
0.10.6
0.10.5
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.0
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.0
0.3.2
0.3.1
0.3.0
0.2.2
0.2.1
0.2.0
0.1.0
Shadcn-style Phoenix LiveView components that AI assistants can actually use. Pair with the MCP server so AI coding tools can inspect the real component API.
Current section
Files
Jump to
Current section
Files
lib/petal_components/input_otp.ex
defmodule PetalComponents.InputOtp do
use Phoenix.Component
@doc """
A segmented one-time-code input.
Under the hood there is exactly ONE real (invisible) input stretched across
the painted segments, which is what makes everything work the way users
expect: native paste, one form field posted, and SMS / password-manager
autofill via `autocomplete="one-time-code"`. The `PetalInputOTP` hook
(shipped in the petal_components JS bundle) paints the segments, the fake
caret and the active ring.
Fires a `petal:otp-complete` DOM event (bubbling, with `detail.value`) when
all segments are filled; `phx-change` on the surrounding form works as
normal.
## Examples
<.input_otp name="code" />
<.input_otp name="code" length={6} group_size={3} />
<.input_otp name="pin" length={4} pattern="alphanumeric" />
"""
attr :id, :string, default: nil
attr :name, :string, required: true
attr :value, :string, default: ""
attr :length, :integer, default: 6
attr :group_size, :integer, default: nil, doc: "splits segments into groups with a separator"
attr :pattern, :string,
default: "numeric",
values: ["numeric", "alphanumeric"],
doc: "which characters are accepted"
attr :disabled, :boolean, default: false
attr :class, :any, default: nil
attr :rest, :global
def input_otp(assigns) do
assigns =
assigns
|> assign(:id, assigns.id || "pc-otp-#{assigns.name}")
|> assign(:groups, slot_groups(assigns.length, assigns.group_size))
~H"""
<div
class={["pc-otp", @class]}
id={@id}
phx-hook="PetalInputOTP"
data-pattern={@pattern}
{@rest}
>
<input
type="text"
class="pc-otp__input"
name={@name}
id={@id <> "-input"}
value={@value}
maxlength={@length}
inputmode={if @pattern == "numeric", do: "numeric", else: "text"}
autocomplete="one-time-code"
spellcheck="false"
autocapitalize="off"
disabled={@disabled}
data-pc-otp-input
/>
<div class="pc-otp__slots" aria-hidden="true">
<%= for {group, index} <- Enum.with_index(@groups) do %>
<div :if={index > 0} class="pc-otp__separator">-</div>
<div class="pc-otp__group">
<div :for={_slot <- group} class="pc-otp__slot" data-pc-otp-slot></div>
</div>
<% end %>
</div>
</div>
"""
end
defp slot_groups(length, nil), do: [Enum.to_list(1..length)]
defp slot_groups(length, group_size), do: Enum.chunk_every(Enum.to_list(1..length), group_size)
end