JobyKit.CoreComponents (JobyKit v0.3.2)

Copy Markdown View Source

Core wrapper components shipped by JobyKit. Each component:

  • Carries data-component="JobyKit.CoreComponents.<name>" on its root element.
  • Declares every prop with attr (variant/size enums via values:).
  • Accepts attr :rest, :global so callers can pass id/class/aria-/ phx- through.
  • Composes daisyUI primitives + theme tokens internally; the class attr (where exposed) is additive, layered on top of the wrapper's identity classes.

Hosts register these against JobyKit.CoreComponents in their manifest:

component JobyKit.CoreComponents, :button,
  category: :core,
  daisy_basis: "btn",
  summary: "Standard text button.",
  preview: &MyAppWeb.DesignPreviews.button_preview/1

And expose them by importing this module into their core_components / html_helpers (or _web.ex) so call sites can use the <.button> form.

Components

  • flash/1, flash_group/1 — toast-style flashes
  • badge/1 — status chip with a semantic tone
  • button/1 — text/link button with variant, size, and shape
  • card/1 — content surface with eyebrow/title/actions slots
  • eyebrow/1 — small uppercase label
  • header/1 — page or section header
  • icon/1 — Heroicon span
  • input/1 — form input (text, email, select, textarea, checkbox…)
  • list/1 — generic list
  • modal/1 — server-driven dialog
  • table/1 — table with col/action slots, stream-aware
  • theme_toggle/1 — system / light / dark control

Plus the JS helpers show/2, hide/2, and the i18n-free translate_error/1.

Summary

Functions

Status chip with a semantic tone.

Standard text button. Renders as <button> by default, or <.link> when href/navigate/patch is passed via :rest.

daisyUI card with optional eyebrow, title, and actions slots.

Small uppercase label that sits above a heading or leads a data pair.

Renders a single flash notice as a daisyUI alert.

Renders the standard flash group: :info and :error flashes plus the disconnected/server-error toasts wired to phx-disconnected / phx-connected. Hosts call this from their root layout.

Page or section header, with optional eyebrow, subtitle, and actions.

JS command for fading an element out.

Renders a Heroicon.

Renders a form input with label and error messages.

Generic data list, one row per :item slot.

Dialog whose visibility is driven by the server.

JS command for fading an element in.

Generic table with :col slots and an optional :action slot.

Segmented system / light / dark theme control.

Translate an Ecto error tuple {msg, opts} into a plain string by interpolating %{key} placeholders. No Gettext dependency — hosts that need i18n should override this function (or wrap <.input>).

Functions

badge(assigns)

Status chip with a semantic tone.

<.badge tone="ok">Healthy</.badge>
<.badge tone="danger" variant="solid">Failed</.badge>

tone names the state, not a colour, so the palette stays consistent across apps and themes. It deliberately shares neutral and danger with button/1 — the same word means the same thing wherever it appears.

This exists because it was missing: one app maintained five separate tone-to-class functions mapping the same ok/warn/critical/neutral set to border+bg+text triples, and one of them had been copied verbatim into a second file — the exact drift the kit's own guidance warns about.

Attributes

  • tone (:string) - The state being reported, not a colour. Defaults to "neutral". Must be one of "neutral", "ok", "warn", "danger", or "info".
  • variant (:string) - Defaults to "soft". Must be one of "soft", "solid", or "outline".
  • size (:string) - Defaults to "sm". Must be one of "xs", "sm", "md", or "lg".
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

button(assigns)

Standard text button. Renders as <button> by default, or <.link> when href/navigate/patch is passed via :rest.

<.button>Send</.button>
<.button variant="primary" size="sm">Save</.button>
<.button navigate={~p"/dashboard"}>Home</.button>

type passes through, so a non-submitting button inside a form is <.button type="button">. Omitted, the browser default applies — submit inside a form.

Tone

variant carries meaning, not just colour — danger is how a destructive action reads as destructive:

<.button variant="danger">Delete workspace</.button>
<.button variant="ghost">Dismiss</.button>

The default (nil) is the soft-primary treatment, also nameable as soft when the value is computed at runtime.

Icon-only buttons

shape sizes the button to a single glyph. Always give it an accessible name, since there is no text to read:

<.button shape="circle" variant="ghost" aria-label="Close">
  <.icon name="hero-x-mark" class="size-4" />
</.button>

Attributes

  • class (:any) - Defaults to nil.
  • variant (:string) - Tone. Defaults to the soft-primary treatment; danger for destructive actions. Must be one of "soft", "primary", "neutral", "ghost", or "danger".
  • size (:string) - Control height. Defaults to "md". Must be one of "xs", "sm", "md", or "lg".
  • shape (:string) - Square off an icon-only button. Give it an aria-label. Must be one of "circle", or "square".
  • Global attributes are accepted. Supports all globals plus: ["href", "navigate", "patch", "method", "download", "name", "value", "disabled", "type", "form"].

Slots

  • inner_block (required)

card(assigns)

daisyUI card with optional eyebrow, title, and actions slots.

<.card>
  <:eyebrow>/design</:eyebrow>
  <:title>Kit-curated wrappers</:title>
  Body content goes here.
  <:actions><.button>Open</.button></:actions>
</.card>

Body content

The body renders as direct children of card-body, so the card's own gap spaces whatever you put there — no inner wrapper to fight.

The card takes no opinion on body typography. prose={true} opts into the muted small-text treatment for cards that really are prose; body_class sets it yourself.

<.card prose>Explanatory copy.</.card>
<.card body_class="text-base">Dense data, styled by the caller.</.card>

Attributes

  • class (:any) - Defaults to nil.
  • body_class (:any) - Utilities for the card-body element. Defaults to nil.
  • variant (:string) - Surface treatment. Defaults to "bordered". Must be one of "bordered", "ghost", or "elevated".
  • prose (:boolean) - Apply the muted small-text treatment to body content. Defaults to false.
  • Global attributes are accepted.

Slots

  • eyebrow
  • title
  • actions
  • inner_block (required)

eyebrow(assigns)

Small uppercase label that sits above a heading or leads a data pair.

<.eyebrow>Workspace</.eyebrow>

This is the most-duplicated string in the fleet: one app carried 326 hand-typed instances of the same mono-uppercase treatment, with letter spacing drifting across nine values and six font sizes because every one was written by hand. One definition, one look.

card/1 and header/1 render their :eyebrow slots through this, so the three stay identical by construction.

Attributes

  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

flash(assigns)

Renders a single flash notice as a daisyUI alert.

Positioning belongs to the container, not the notice: flash_group/1 supplies the single toast that stacks every notice. Rendered on its own, flash/1 sits inline wherever you put it.

<.flash kind={:info} flash={@flash} />
<.flash kind={:error} title="Heads up">Something happened</.flash>

Attributes

  • id (:string) - Defaults to nil.
  • flash (:map) - Defaults to %{}.
  • title (:string) - Defaults to nil.
  • kind (:atom) (required) - Which flash key to read and how to style it. Must be one of :info, :success, :warning, or :error.
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block

flash_group(assigns)

Renders the standard flash group: :info and :error flashes plus the disconnected/server-error toasts wired to phx-disconnected / phx-connected. Hosts call this from their root layout.

This is the single toast container for the page — every notice stacks inside it. The container is click-through (pointer-events-none) so the empty corner never intercepts clicks; each notice re-enables pointer events for its own dismiss handler.

Attributes

  • id (:string) - Defaults to "flash-group".
  • flash (:map) (required)
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

header(assigns)

Page or section header, with optional eyebrow, subtitle, and actions.

<.header>
  Team settings
  <:eyebrow>Workspace</:eyebrow>
  <:subtitle>Manage members and their permissions.</:subtitle>
  <:actions><.button variant="primary">Invite</.button></:actions>
</.header>

level picks the heading element so a section header doesn't emit a second <h1>; size picks the type scale independently, and title_class overrides it outright when the app has its own display face. The header carries no outer spacing — the parent owns that.

Attributes

  • class (:any) - Defaults to nil.
  • title_class (:any) - Replaces the heading's type scale. Defaults to nil.
  • level (:string) - Defaults to "h1". Must be one of "h1", "h2", or "h3".
  • size (:string) - Defaults to "section". Must be one of "page", or "section".
  • Global attributes are accepted.

Slots

  • inner_block (required)
  • eyebrow
  • subtitle
  • actions

hide(js \\ %JS{}, selector)

JS command for fading an element out.

icon(assigns)

Renders a Heroicon.

Heroicons come in three styles — outline, solid, and mini. Default is outline; pass name="hero-foo-solid" or name="hero-foo-mini" for the others. The host must have the Heroicons CSS plugin installed (Phoenix ships this by default in assets/vendor/heroicons.js).

<.icon name="hero-x-mark" />
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />

Attributes

  • name (:string) (required)
  • class (:any) - Defaults to "size-4".
  • Global attributes are accepted.

input(assigns)

Renders a form input with label and error messages.

Pass field={@form[:foo]} for the common case; the input pulls id, name, value, and errors from the form field. Otherwise pass name, value, errors explicitly.

Supported types: text, email, password, number, search, tel, url, date, datetime-local, month, time, week, color, file, hidden, checkbox, select, textarea.

<.input field={@form[:email]} type="email" label="Email" />
<.input field={@form[:role]} type="select" options={["Admin": "admin"]} />

Class composition

class and any global attributes land on the root <fieldset>, the same as every other kit wrapper — use it for layout (grid placement, width, margins). input_class adds utilities to the control itself, on top of the daisyUI class set the wrapper composes (input / select / textarea / checkbox).

<.input field={@form[:email]} class="col-span-2" input_class="font-mono" />

The wrapper carries no outer margin. Spacing belongs to the parent — reach for space-y-* or gap-* on the container.

Width works through the root: the control is always w-full, so constrain the root and the control follows.

<.input field={@form[:code]} class="w-32" />

Attributes

  • id (:any) - Defaults to nil.
  • name (:any)
  • label (:string) - Defaults to nil.
  • value (:any)
  • type (:string) - Defaults to "text". Must be one of "checkbox", "color", "date", "datetime-local", "email", "file", "month", "number", "password", "search", "select", "tel", "text", "textarea", "time", "url", "week", or "hidden".
  • field (Phoenix.HTML.FormField)
  • errors (:list) - Defaults to [].
  • checked (:boolean)
  • prompt (:string) - Defaults to nil.
  • options (:list)
  • multiple (:boolean) - Defaults to false.
  • class (:any) - Utilities for the root fieldset — layout, width, spacing. Defaults to nil.
  • input_class (:any) - Utilities for the control itself. Defaults to nil.
  • Global attributes are accepted. Supports all globals plus: ["accept", "autocomplete", "capture", "cols", "disabled", "form", "list", "max", "maxlength", "min", "minlength", "multiple", "pattern", "placeholder", "readonly", "required", "rows", "size", "step"].

list(assigns)

Generic data list, one row per :item slot.

<.list>
  <:item title="Title">{@post.title}</:item>
  <:item title="Views">{@post.views}</:item>
</.list>

Attributes

  • class (:any) - Defaults to nil.
  • title_class (:any) - Replaces the title's type treatment. The default is font-bold. Defaults to nil.
  • Global attributes are accepted.

Slots

  • item (required) - Accepts attributes:
    • title (:string) (required)

modal(assigns)

Dialog whose visibility is driven by the server.

<.modal id="confirm" show={@confirming?} on_cancel={JS.push("cancel")}>
  <:title>Delete workspace</:title>
  This removes every peer and session in it. It cannot be undone.
  <:actions>
    <.button variant="ghost" phx-click={JS.push("cancel")}>Keep it</.button>
    <.button variant="danger" phx-click={JS.push("delete")}>Delete</.button>
  </:actions>
</.modal>

show is a plain assign rather than client-side state, so the dialog can't disagree with the LiveView that owns it, and reconnecting restores the right thing.

Dismissal

on_cancel runs for all three ways out — the close button, the backdrop, and Escape — so there is one path to handle instead of the separate close/dismiss handlers apps ended up writing. The keydown listener is only attached while the dialog is open, so closed dialogs on the page cost nothing and can't swallow the key.

Set dismissable={false} for a dialog that must be resolved through its actions; that drops the close button, the backdrop handler, and the Escape binding together.

Previews

.modal is position: fixed, so rendering one inside a preview card would cover the page. static renders just the box, in flow, for design pages and documentation.

Attributes

  • id (:string) (required)
  • show (:boolean) - Defaults to false.
  • on_cancel (:any) - JS command or event run on any dismissal. Defaults to nil.
  • dismissable (:boolean) - Defaults to true.
  • static (:boolean) - Render in flow instead of as an overlay. Defaults to false.
  • class (:any) - Defaults to nil.
  • box_class (:any) - Utilities for the modal box. Defaults to nil.
  • Global attributes are accepted.

Slots

  • title
  • inner_block (required)
  • actions

show(js \\ %JS{}, selector)

JS command for fading an element in.

table(assigns)

Generic table with :col slots and an optional :action slot.

:rows takes a plain list or a LiveView stream (see Phoenix.LiveView.stream/4).

<.table id="users" rows={@users}>
  <:col :let={user} label="id">{user.id}</:col>
  <:col :let={user} label="username">{user.username}</:col>
  <:empty>No users yet.</:empty>
</.table>

Streams enumerate as {dom_id, item}

A stream yields tuples, so destructure in the slot — or hand the unwrapping to row_item:

<:col :let={{_id, user}} label="id">{user.id}</:col>
<.table id="users" rows={@stream} row_item={fn {_id, u} -> u end}>

Styling hooks

zebra={false} drops the striping, size sets density, and the action cell carries data-table-actions so a host override can target it precisely instead of guessing with :last-child.

Attributes

  • id (:string) (required) - Identifies the row container. Streams require it; it lands on <tbody>.
  • table_id (:string) - Optional id for the <table> element itself, since id is taken. Defaults to nil.
  • rows (:any) (required) - A list, or a LiveView stream from Phoenix.LiveView.stream/4 (which enumerates as {dom_id, item}).
  • class (:any) - Defaults to nil.
  • zebra (:boolean) - Row striping. Turn off for dense data. Defaults to true.
  • size (:string) - Defaults to "md". Must be one of "xs", "sm", "md", or "lg".
  • row_id (:any) - Defaults to nil.
  • row_click (:any) - Defaults to nil.
  • row_item (:any) - Defaults to &Function.identity/1.
  • Global attributes are accepted.

Slots

  • col (required) - Accepts attributes:
    • label (:string)
  • action
  • empty - Rendered in place of the body when there are no rows.

theme_toggle(assigns)

Segmented system / light / dark theme control.

<.theme_toggle />

Pairs with the theme script Phoenix puts in root.html.heex, which applies the stored choice before first paint and listens for the phx:set-theme event these buttons dispatch. Selection state is read straight off <html data-theme> / data-theme-source in CSS, so the control stays correct without round-tripping through the server.

Generated apps get this in their layout. If you are installing into an existing app, make sure root.html.heex carries that script — without it the buttons dispatch into nothing.

Attributes

  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

translate_error(msg)

Translate an Ecto error tuple {msg, opts} into a plain string by interpolating %{key} placeholders. No Gettext dependency — hosts that need i18n should override this function (or wrap <.input>).

Opts whose placeholder isn't present in the message are never stringified, so non-String.Chars values (type: {:array, :string}, validation: :cast, and friends — which Ecto attaches to every cast error) pass through harmlessly.