cover/Elixir.DaProductAppWeb.CoreComponents.html

1 99 defmodule DaProductAppWeb.CoreComponents do
2 @moduledoc """
3 Provides core UI components.
4
5 At first glance, this module may seem daunting, but its goal is to provide
6 core building blocks for your application, such as modals, tables, and
7 forms. The components consist mostly of markup and are well-documented
8 with doc strings and declarative assigns. You may customize and style
9 them in any way you want, based on your application growth and needs.
10
11 The default components use Tailwind CSS, a utility-first CSS framework.
12 See the [Tailwind CSS documentation](https://tailwindcss.com) to learn
13 how to customize them or feel free to swap in another framework altogether.
14
15 Icons are provided by [heroicons](https://heroicons.com). See `icon/1` for usage.
16 """
17 use Phoenix.Component
18 use Gettext, backend: DaProductAppWeb.Gettext
19
20 alias Phoenix.LiveView.JS
21
22 @doc """
23 Renders a modal.
24
25 ## Examples
26
27 <.modal id="confirm-modal">
28 This is a modal.
29 </.modal>
30
31 JS commands may be passed to the `:on_cancel` to configure
32 the closing/cancel event, for example:
33
34 <.modal id="confirm" on_cancel={JS.navigate(~p"/posts")}>
35 This is another modal.
36 </.modal>
37
38 """
39 attr :id, :string, required: true
40 attr :show, :boolean, default: false
41 attr :on_cancel, JS, default: %JS{}
42 slot :inner_block, required: true
43
44 def modal(assigns) do
45
:-(
~H"""
46
:-(
<div
47
:-(
id={@id}
48
:-(
phx-mounted={@show && show_modal(@id)}
49
:-(
phx-remove={hide_modal(@id)}
50
:-(
data-cancel={JS.exec(@on_cancel, "phx-remove")}
51 class="relative z-50 hidden"
52 >
53
:-(
<div id={"#{@id}-bg"} class="bg-zinc-50/90 fixed inset-0 transition-opacity" aria-hidden="true" />
54 <div
55 class="fixed inset-0 overflow-y-auto"
56
:-(
aria-labelledby={"#{@id}-title"}
57
:-(
aria-describedby={"#{@id}-description"}
58 role="dialog"
59 aria-modal="true"
60 tabindex="0"
61 >
62 <div class="flex min-h-full items-center justify-center">
63 <div class="w-full max-w-3xl p-4 sm:p-6 lg:py-8">
64
:-(
<.focus_wrap
65
:-(
id={"#{@id}-container"}
66
:-(
phx-window-keydown={JS.exec("data-cancel", to: "##{@id}")}
67 phx-key="escape"
68
:-(
phx-click-away={JS.exec("data-cancel", to: "##{@id}")}
69 class="shadow-zinc-700/10 ring-zinc-700/10 relative hidden rounded-2xl bg-white p-14 shadow-lg ring-1 transition"
70 >
71 <div class="absolute top-6 right-5">
72
:-(
<button
73
:-(
phx-click={JS.exec("data-cancel", to: "##{@id}")}
74 type="button"
75 class="-m-3 flex-none p-3 opacity-20 hover:opacity-40"
76 aria-label={gettext("close")}
77 >
78
:-(
<.icon name="hero-x-mark-solid" class="h-5 w-5" />
79 </button>
80 </div>
81
:-(
<div id={"#{@id}-content"}>
82
:-(
{render_slot(@inner_block)}
83 </div>
84 </.focus_wrap>
85 </div>
86 </div>
87 </div>
88 </div>
89 """
90 end
91
92 @doc """
93 Renders flash notices.
94
95 ## Examples
96
97 <.flash kind={:info} flash={@flash} />
98 <.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>
99 """
100 attr :id, :string, doc: "the optional id of flash container"
101 attr :flash, :map, default: %{}, doc: "the map of flash messages to display"
102 attr :title, :string, default: nil
103 attr :kind, :atom, values: [:info, :error], doc: "used for styling and flash lookup"
104 attr :rest, :global, doc: "the arbitrary HTML attributes to add to the flash container"
105
106 slot :inner_block, doc: "the optional inner block that renders the flash message"
107
108 def flash(assigns) do
109 36 assigns = assign_new(assigns, :id, fn -> "flash-#{assigns.kind}" end)
110
111 36 ~H"""
112 18 <div
113 36 :if={msg = render_slot(@inner_block) || Phoenix.Flash.get(@flash, @kind)}
114 18 id={@id}
115 18 phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")}
116 role="alert"
117 class={[
118 "fixed top-2 right-2 mr-2 w-80 sm:w-96 z-50 rounded-lg p-3 ring-1",
119 18 @kind == :info && "bg-emerald-50 text-emerald-800 ring-emerald-500 fill-cyan-900",
120 18 @kind == :error && "bg-rose-50 text-rose-900 shadow-md ring-rose-500 fill-rose-900"
121 ]}
122 18 {@rest}
123 >
124 18 <p :if={@title} class="flex items-center gap-1.5 text-sm font-semibold leading-6">
125 18 <.icon :if={@kind == :info} name="hero-information-circle-mini" class="h-4 w-4" />
126 18 <.icon :if={@kind == :error} name="hero-exclamation-circle-mini" class="h-4 w-4" />
127 18 {@title}
128 </p>
129 <p class="mt-2 text-sm leading-5">{msg}</p>
130 18 <button type="button" class="group absolute top-1 right-1 p-2" aria-label={gettext("close")}>
131 18 <.icon name="hero-x-mark-solid" class="h-5 w-5 opacity-40 group-hover:opacity-70" />
132 </button>
133 </div>
134 """
135 end
136
137 @doc """
138 Shows the flash group with standard titles and content.
139
140 ## Examples
141
142 <.flash_group flash={@flash} />
143 """
144 attr :flash, :map, required: true, doc: "the map of flash messages"
145 attr :id, :string, default: "flash-group", doc: "the optional id of flash container"
146
147 def flash_group(assigns) do
148 9 ~H"""
149 9 <div id={@id}>
150 9 <.flash kind={:info} title={gettext("Success!")} flash={@flash} />
151 9 <.flash kind={:error} title={gettext("Error!")} flash={@flash} />
152 9 <.flash
153 id="client-error"
154 kind={:error}
155 title={gettext("We can't find the internet")}
156 phx-disconnected={show(".phx-client-error #client-error")}
157 phx-connected={hide("#client-error")}
158 hidden
159 >
160 9 {gettext("Attempting to reconnect")}
161 9 <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
162 </.flash>
163
164 9 <.flash
165 id="server-error"
166 kind={:error}
167 title={gettext("Something went wrong!")}
168 phx-disconnected={show(".phx-server-error #server-error")}
169 phx-connected={hide("#server-error")}
170 hidden
171 >
172 9 {gettext("Hang in there while we get back on track")}
173 9 <.icon name="hero-arrow-path" class="ml-1 h-3 w-3 animate-spin" />
174 </.flash>
175 </div>
176 """
177 end
178
179 @doc """
180 Renders a simple form.
181
182 ## Examples
183
184 <.simple_form for={@form} phx-change="validate" phx-submit="save">
185 <.input field={@form[:email]} label="Email"/>
186 <.input field={@form[:username]} label="Username" />
187 <:actions>
188 <.button>Save</.button>
189 </:actions>
190 </.simple_form>
191 """
192 attr :for, :any, required: true, doc: "the data structure for the form"
193 attr :as, :any, default: nil, doc: "the server side parameter to collect all input under"
194
195 attr :rest, :global,
196 include: ~w(autocomplete name rel action enctype method novalidate target multipart),
197 doc: "the arbitrary HTML attributes to apply to the form tag"
198
199 slot :inner_block, required: true
200 slot :actions, doc: "the slot for form actions, such as a submit button"
201
202 def simple_form(assigns) do
203
:-(
~H"""
204
:-(
<.form :let={f} for={@for} as={@as} {@rest}>
205 <div class="mt-10 space-y-8 bg-white">
206
:-(
{render_slot(@inner_block, f)}
207
:-(
<div :for={action <- @actions} class="mt-2 flex items-center justify-between gap-6">
208 {render_slot(action, f)}
209 </div>
210 </div>
211 </.form>
212 """
213 end
214
215 @doc """
216 Renders a button.
217
218 ## Examples
219
220 <.button>Send!</.button>
221 <.button phx-click="go" class="ml-2">Send!</.button>
222 """
223 attr :type, :string, default: nil
224 attr :class, :string, default: nil
225 attr :rest, :global, include: ~w(disabled form name value)
226
227 slot :inner_block, required: true
228
229 def button(assigns) do
230
:-(
~H"""
231
:-(
<button
232
:-(
type={@type}
233 class={[
234 "phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
235 "text-sm font-semibold leading-6 text-white active:text-white/80",
236
:-(
@class
237 ]}
238
:-(
{@rest}
239 >
240
:-(
{render_slot(@inner_block)}
241 </button>
242 """
243 end
244
245 @doc """
246 Renders an input with label and error messages.
247
248 A `Phoenix.HTML.FormField` may be passed as argument,
249 which is used to retrieve the input name, id, and values.
250 Otherwise all attributes may be passed explicitly.
251
252 ## Types
253
254 This function accepts all HTML input types, considering that:
255
256 * You may also set `type="select"` to render a `<select>` tag
257
258 * `type="checkbox"` is used exclusively to render boolean values
259
260 * For live file uploads, see `Phoenix.Component.live_file_input/1`
261
262 See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input
263 for more information. Unsupported types, such as hidden and radio,
264 are best written directly in your templates.
265
266 ## Examples
267
268 <.input field={@form[:email]} type="email" />
269 <.input name="my-input" errors={["oh no!"]} />
270 """
271 attr :id, :any, default: nil
272 attr :name, :any
273 attr :label, :string, default: nil
274 attr :value, :any
275
276 attr :type, :string,
277 default: "text",
278 values: ~w(checkbox color date datetime-local email file month number password
279 range search select tel text textarea time url week)
280
281 attr :field, Phoenix.HTML.FormField,
282 doc: "a form field struct retrieved from the form, for example: @form[:email]"
283
284 attr :errors, :list, default: []
285 attr :checked, :boolean, doc: "the checked flag for checkbox inputs"
286 attr :prompt, :string, default: nil, doc: "the prompt for select inputs"
287 attr :options, :list, doc: "the options to pass to Phoenix.HTML.Form.options_for_select/2"
288 attr :multiple, :boolean, default: false, doc: "the multiple flag for select inputs"
289
290 attr :rest, :global,
291 include: ~w(accept autocomplete capture cols disabled form list max maxlength min minlength
292 multiple pattern placeholder readonly required rows size step)
293
294 def input(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
295
:-(
errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []
296
297 assigns
298
:-(
|> assign(field: nil, id: assigns.id || field.id)
299 |> assign(:errors, Enum.map(errors, &translate_error(&1)))
300
:-(
|> assign_new(:name, fn -> if assigns.multiple, do: field.name <> "[]", else: field.name end)
301
:-(
|> assign_new(:value, fn -> field.value end)
302
:-(
|> input()
303 end
304
305 def input(%{type: "checkbox"} = assigns) do
306
:-(
assigns =
307 assign_new(assigns, :checked, fn ->
308
:-(
Phoenix.HTML.Form.normalize_value("checkbox", assigns[:value])
309 end)
310
311
:-(
~H"""
312 <div>
313 <label class="flex items-center gap-4 text-sm leading-6 text-zinc-600">
314
:-(
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
315
:-(
<input
316 type="checkbox"
317
:-(
id={@id}
318
:-(
name={@name}
319 value="true"
320
:-(
checked={@checked}
321 class="rounded border-zinc-300 text-zinc-900 focus:ring-0"
322
:-(
{@rest}
323 />
324
:-(
{@label}
325 </label>
326
:-(
<.error :for={msg <- @errors}>{msg}</.error>
327 </div>
328 """
329 end
330
331 def input(%{type: "select"} = assigns) do
332
:-(
~H"""
333 <div>
334
:-(
<.label for={@id}>{@label}</.label>
335
:-(
<select
336
:-(
id={@id}
337
:-(
name={@name}
338 class="mt-2 block w-full rounded-md border border-gray-300 bg-white shadow-sm focus:border-zinc-400 focus:ring-0 sm:text-sm"
339
:-(
multiple={@multiple}
340
:-(
{@rest}
341 >
342
:-(
<option :if={@prompt} value="">{@prompt}</option>
343
:-(
{Phoenix.HTML.Form.options_for_select(@options, @value)}
344 </select>
345
:-(
<.error :for={msg <- @errors}>{msg}</.error>
346 </div>
347 """
348 end
349
350 def input(%{type: "textarea"} = assigns) do
351
:-(
~H"""
352 <div>
353
:-(
<.label for={@id}>{@label}</.label>
354
:-(
<textarea
355
:-(
id={@id}
356
:-(
name={@name}
357 class={[
358 "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6 min-h-[6rem]",
359
:-(
@errors == [] && "border-zinc-300 focus:border-zinc-400",
360
:-(
@errors != [] && "border-rose-400 focus:border-rose-400"
361 ]}
362
:-(
{@rest}
363
:-(
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
364
:-(
<.error :for={msg <- @errors}>{msg}</.error>
365 </div>
366 """
367 end
368
369 # All other inputs text, datetime-local, url, password, etc. are handled here...
370 def input(assigns) do
371
:-(
~H"""
372 <div>
373
:-(
<.label for={@id}>{@label}</.label>
374
:-(
<input
375
:-(
type={@type}
376
:-(
name={@name}
377
:-(
id={@id}
378
:-(
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
379 class={[
380 "mt-2 block w-full rounded-lg text-zinc-900 focus:ring-0 sm:text-sm sm:leading-6",
381
:-(
@errors == [] && "border-zinc-300 focus:border-zinc-400",
382
:-(
@errors != [] && "border-rose-400 focus:border-rose-400"
383 ]}
384
:-(
{@rest}
385 />
386
:-(
<.error :for={msg <- @errors}>{msg}</.error>
387 </div>
388 """
389 end
390
391 @doc """
392 Renders a label.
393 """
394 attr :for, :string, default: nil
395 slot :inner_block, required: true
396
397 def label(assigns) do
398
:-(
~H"""
399
:-(
<label for={@for} class="block text-sm font-semibold leading-6 text-zinc-800">
400
:-(
{render_slot(@inner_block)}
401 </label>
402 """
403 end
404
405 @doc """
406 Generates a generic error message.
407 """
408 slot :inner_block, required: true
409
410 def error(assigns) do
411
:-(
~H"""
412 <p class="mt-3 flex gap-3 text-sm leading-6 text-rose-600">
413
:-(
<.icon name="hero-exclamation-circle-mini" class="mt-0.5 h-5 w-5 flex-none" />
414
:-(
{render_slot(@inner_block)}
415 </p>
416 """
417 end
418
419 @doc """
420 Renders a header with title.
421 """
422 attr :class, :string, default: nil
423
424 slot :inner_block, required: true
425 slot :subtitle
426 slot :actions
427
428 def header(assigns) do
429
:-(
~H"""
430
:-(
<header class={[@actions != [] && "flex items-center justify-between gap-6", @class]}>
431 <div>
432 <h1 class="text-lg font-semibold leading-8 text-zinc-800">
433
:-(
{render_slot(@inner_block)}
434 </h1>
435
:-(
<p :if={@subtitle != []} class="mt-2 text-sm leading-6 text-zinc-600">
436
:-(
{render_slot(@subtitle)}
437 </p>
438 </div>
439
:-(
<div class="flex-none">{render_slot(@actions)}</div>
440 </header>
441 """
442 end
443
444 @doc ~S"""
445 Renders a table with generic styling.
446
447 ## Examples
448
449 <.table id="users" rows={@users}>
450 <:col :let={user} label="id">{user.id}</:col>
451 <:col :let={user} label="username">{user.username}</:col>
452 </.table>
453 """
454 attr :id, :string, required: true
455 attr :rows, :list, required: true
456 attr :row_id, :any, default: nil, doc: "the function for generating the row id"
457 attr :row_click, :any, default: nil, doc: "the function for handling phx-click on each row"
458
459 attr :row_item, :any,
460 default: &Function.identity/1,
461 doc: "the function for mapping each row before calling the :col and :action slots"
462
463 slot :col, required: true do
464 attr :label, :string
465 end
466
467 slot :action, doc: "the slot for showing user actions in the last table column"
468
469 def table(assigns) do
470
:-(
assigns =
471
:-(
with %{rows: %Phoenix.LiveView.LiveStream{}} <- assigns do
472
:-(
assign(assigns, row_id: assigns.row_id || fn {id, _item} -> id end)
473 end
474
475
:-(
~H"""
476 <div class="overflow-y-auto px-4 sm:overflow-visible sm:px-0">
477 <table class="w-[40rem] mt-11 sm:w-full">
478 <thead class="text-sm text-left leading-6 text-zinc-500">
479 <tr>
480
:-(
<th :for={col <- @col} class="p-0 pb-4 pr-6 font-normal">{col[:label]}</th>
481
:-(
<th :if={@action != []} class="relative p-0 pb-4">
482
:-(
<span class="sr-only">{gettext("Actions")}</span>
483 </th>
484 </tr>
485 </thead>
486
:-(
<tbody
487
:-(
id={@id}
488
:-(
phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
489 class="relative divide-y divide-zinc-100 border-t border-zinc-200 text-sm leading-6 text-zinc-700"
490 >
491
:-(
<tr :for={row <- @rows} id={@row_id && @row_id.(row)} class="group hover:bg-zinc-50">
492
:-(
<td
493
:-(
:for={{col, i} <- Enum.with_index(@col)}
494
:-(
phx-click={@row_click && @row_click.(row)}
495
:-(
class={["relative p-0", @row_click && "hover:cursor-pointer"]}
496 >
497 <div class="block py-4 pr-6">
498 <span class="absolute -inset-y-px right-0 -left-4 group-hover:bg-zinc-50 sm:rounded-l-xl" />
499
:-(
<span class={["relative", i == 0 && "font-semibold text-zinc-900"]}>
500
:-(
{render_slot(col, @row_item.(row))}
501 </span>
502 </div>
503 </td>
504
:-(
<td :if={@action != []} class="relative w-14 p-0">
505 <div class="relative whitespace-nowrap py-4 text-right text-sm font-medium">
506 <span class="absolute -inset-y-px -right-4 left-0 group-hover:bg-zinc-50 sm:rounded-r-xl" />
507 <span
508
:-(
:for={action <- @action}
509 class="relative ml-4 font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
510 >
511
:-(
{render_slot(action, @row_item.(row))}
512 </span>
513 </div>
514 </td>
515 </tr>
516 </tbody>
517 </table>
518 </div>
519 """
520 end
521
522 @doc """
523 Renders a data list.
524
525 ## Examples
526
527 <.list>
528 <:item title="Title">{@post.title}</:item>
529 <:item title="Views">{@post.views}</:item>
530 </.list>
531 """
532 slot :item, required: true do
533 attr :title, :string, required: true
534 end
535
536 def list(assigns) do
537
:-(
~H"""
538 <div class="mt-14">
539 <dl class="-my-4 divide-y divide-zinc-100">
540
:-(
<div :for={item <- @item} class="flex gap-4 py-4 text-sm leading-6 sm:gap-8">
541
:-(
<dt class="w-1/4 flex-none text-zinc-500">{item.title}</dt>
542
:-(
<dd class="text-zinc-700">{render_slot(item)}</dd>
543 </div>
544 </dl>
545 </div>
546 """
547 end
548
549 @doc """
550 Renders a back navigation link.
551
552 ## Examples
553
554 <.back navigate={~p"/posts"}>Back to posts</.back>
555 """
556 attr :navigate, :any, required: true
557 slot :inner_block, required: true
558
559 def back(assigns) do
560
:-(
~H"""
561 <div class="mt-16">
562
:-(
<.link
563
:-(
navigate={@navigate}
564 class="text-sm font-semibold leading-6 text-zinc-900 hover:text-zinc-700"
565 >
566
:-(
<.icon name="hero-arrow-left-solid" class="h-3 w-3" />
567
:-(
{render_slot(@inner_block)}
568 </.link>
569 </div>
570 """
571 end
572
573 @doc """
574 Renders a [Heroicon](https://heroicons.com).
575
576 Heroicons come in three styles – outline, solid, and mini.
577 By default, the outline style is used, but solid and mini may
578 be applied by using the `-solid` and `-mini` suffix.
579
580 You can customize the size and colors of the icons by setting
581 width, height, and background color classes.
582
583 Icons are extracted from the `deps/heroicons` directory and bundled within
584 your compiled app.css by the plugin in your `assets/tailwind.config.js`.
585
586 ## Examples
587
588 <.icon name="hero-x-mark-solid" />
589 <.icon name="hero-arrow-path" class="ml-1 w-3 h-3 animate-spin" />
590 """
591 attr :name, :string, required: true
592 attr :class, :string, default: nil
593
594 def icon(%{name: "hero-" <> _} = assigns) do
595 54 ~H"""
596 54 <span class={[@name, @class]} />
597 """
598 end
599
600 ## JS Commands
601
602 18 def show(js \\ %JS{}, selector) do
603 18 JS.show(js,
604 to: selector,
605 time: 300,
606 transition:
607 {"transition-all transform ease-out duration-300",
608 "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
609 "opacity-100 translate-y-0 sm:scale-100"}
610 )
611 end
612
613 18 def hide(js \\ %JS{}, selector) do
614 36 JS.hide(js,
615 to: selector,
616 time: 200,
617 transition:
618 {"transition-all transform ease-in duration-200",
619 "opacity-100 translate-y-0 sm:scale-100",
620 "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"}
621 )
622 end
623
624
:-(
def show_modal(js \\ %JS{}, id) when is_binary(id) do
625 js
626
:-(
|> JS.show(to: "##{id}")
627 |> JS.show(
628
:-(
to: "##{id}-bg",
629 time: 300,
630 transition: {"transition-all transform ease-out duration-300", "opacity-0", "opacity-100"}
631 )
632
:-(
|> show("##{id}-container")
633 |> JS.add_class("overflow-hidden", to: "body")
634
:-(
|> JS.focus_first(to: "##{id}-content")
635 end
636
637
:-(
def hide_modal(js \\ %JS{}, id) do
638 js
639 |> JS.hide(
640
:-(
to: "##{id}-bg",
641 transition: {"transition-all transform ease-in duration-200", "opacity-100", "opacity-0"}
642 )
643
:-(
|> hide("##{id}-container")
644
:-(
|> JS.hide(to: "##{id}", transition: {"block", "block", "hidden"})
645 |> JS.remove_class("overflow-hidden", to: "body")
646
:-(
|> JS.pop_focus()
647 end
648
649 @doc """
650 Translates an error message using gettext.
651 """
652 def translate_error({msg, opts}) do
653 # When using gettext, we typically pass the strings we want
654 # to translate as a static argument:
655 #
656 # # Translate the number of files with plural rules
657 # dngettext("errors", "1 file", "%{count} files", count)
658 #
659 # However the error messages in our forms and APIs are generated
660 # dynamically, so we need to translate them by calling Gettext
661 # with our gettext backend as first argument. Translations are
662 # available in the errors.po file (as we use the "errors" domain).
663
:-(
if count = opts[:count] do
664
:-(
Gettext.dngettext(DaProductAppWeb.Gettext, "errors", msg, msg, count, opts)
665 else
666
:-(
Gettext.dgettext(DaProductAppWeb.Gettext, "errors", msg, opts)
667 end
668 end
669
670 @doc """
671 Translates the errors for a field from a keyword list of errors.
672 """
673 def translate_errors(errors, field) when is_list(errors) do
674
:-(
for {^field, {msg, opts}} <- errors, do: translate_error({msg, opts})
675 end
676
677 @doc """
678 Renders a container with flexible max-width options.
679
680 ## Examples
681
682 <.container>
683 Content goes here
684 </.container>
685
686 <.container max_width="sm">
687 Small container
688 </.container>
689
690 <.container max_width="full">
691 Full width container
692 </.container>
693
694 """
695 attr :max_width, :string, default: "7xl", values: ["sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", "6xl", "7xl", "full"]
696 attr :class, :string, default: ""
697 slot :inner_block, required: true
698
699 def container(assigns) do
700
:-(
~H"""
701 <div class={[
702 "mx-auto px-4 sm:px-6 lg:px-8",
703
:-(
container_width_class(@max_width),
704
:-(
@class
705 ]}>
706
:-(
<%= render_slot(@inner_block) %>
707 </div>
708 """
709 end
710
711
:-(
defp container_width_class("sm"), do: "max-w-sm"
712
:-(
defp container_width_class("md"), do: "max-w-md"
713
:-(
defp container_width_class("lg"), do: "max-w-lg"
714
:-(
defp container_width_class("xl"), do: "max-w-xl"
715
:-(
defp container_width_class("2xl"), do: "max-w-2xl"
716
:-(
defp container_width_class("3xl"), do: "max-w-3xl"
717
:-(
defp container_width_class("4xl"), do: "max-w-4xl"
718
:-(
defp container_width_class("5xl"), do: "max-w-5xl"
719
:-(
defp container_width_class("6xl"), do: "max-w-6xl"
720
:-(
defp container_width_class("7xl"), do: "max-w-7xl"
721
:-(
defp container_width_class("full"), do: "max-w-none"
722 end
Line Hits Source