# `PhiaUi.Components.Utilities`
[🔗](https://github.com/charlenopires/PhiaUI/blob/v0.1.17/lib/phia_ui/components/display/utilities.ex#L1)

General-purpose utility display components for PhiaUI.

15 components covering accessibility wrappers, text display helpers,
time formatting, print/screen visibility, and client-side focus management.

All components are tier `:primitive` except `focus_trap` (`:interactive`).

## Components

| Component          | Purpose                                                    |
|--------------------|------------------------------------------------------------|
| `visually_hidden`  | `sr-only` wrapper for accessibility text                   |
| `line_clamp`       | Truncate text with a read-more toggle (no server round-trip)|
| `highlight_text`   | Highlight query matches in text                            |
| `relative_time`    | Format a `DateTime` as "X minutes ago"                    |
| `number_format`    | Format numbers with compact, prefix, suffix                |
| `reading_time`     | Estimate reading time from text                            |
| `label_with_tooltip` | Label + help icon with tooltip                           |
| `print_only`       | Content visible only when printing                         |
| `screen_only`      | Content hidden when printing                               |
| `color_mode_value` | Render different slots in light vs dark mode               |
| `diff_display`     | Word-level diff with added/removed markup                  |
| `stat_unit`        | Value + unit display pair                                  |
| `word_count`       | Display word count with label                              |
| `focus_trap`       | Focus trap wrapper (requires `PhiaFocusTrap` JS hook)      |
| `sticky_wrapper`   | Sticky-positioned wrapper with configurable top offset     |

# `color_mode_value`

Renders different content in light mode vs dark mode.

Uses CSS `dark:hidden` / `dark:block` — no JS needed. The `:light` slot
is hidden in dark mode; the `:dark` slot is hidden in light mode.

## Example

    <.color_mode_value>
      <:light><img src="/logo-light.png" alt="Logo" /></:light>
      <:dark><img src="/logo-dark.png" alt="Logo" /></:dark>
    </.color_mode_value>

## Attributes

* `class` (`:string`) - Defaults to `nil`.
## Slots

* `light` (required) - Content rendered in light mode only.
* `dark` (required) - Content rendered in dark mode only.

# `diff_display`

Renders a word-level diff between two strings.

Removed words are rendered in `<del>` with a red background.
Added words are rendered in `<ins>` with a green background.
Unchanged words are rendered as plain text.

## Example

    <.diff_display before="The quick brown fox" after="The slow green fox" />

## Attributes

* `before` (`:string`) (required) - Original text (words removed will be highlighted).
* `after` (`:string`) (required) - New text (words added will be highlighted).
* `class` (`:string`) - Defaults to `nil`.

# `focus_trap`

Wraps content in a focus trap managed by the `PhiaFocusTrap` JS hook.

When enabled, Tab/Shift+Tab cycle only within focusable elements inside
this wrapper. Pressing Escape fires a `"focus-trap-escape"` push event
to the parent LiveView.

Requires the `PhiaFocusTrap` JS hook to be installed.

## Example

    <.focus_trap id="modal-trap" enabled={@modal_open}>
      <form>...</form>
      <button phx-click="close">Close</button>
    </.focus_trap>

## LiveView handler

    def handle_event("focus-trap-escape", _params, socket) do
      {:noreply, assign(socket, modal_open: false)}
    end

## Attributes

* `id` (`:string`) (required) - Unique element ID (required by phx-hook).
* `enabled` (`:boolean`) - When false, the trap is disabled. Defaults to `true`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `highlight_text`

Renders text with query matches highlighted in `<mark>` elements.

XSS-safe: splits in Elixir and renders via HEEx — no `raw/1` used.
Case-insensitive matching. Renders plain text when query is empty.

## Example

    <.highlight_text text="Hello World" query="world" />

## Attributes

* `text` (`:string`) (required) - Full text to render.
* `query` (`:string`) - Search query to highlight. Defaults to `""`.
* `mark_class` (`:string`) - CSS classes for highlighted `<mark>` elements. Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.

# `label_with_tooltip`

Renders a form label with an attached help icon and tooltip on hover.

The tooltip is implemented with CSS `:hover`/`:focus-visible` using a
`group` parent — no JS hook needed.

## Example

    <.label_with_tooltip
      label="API Key"
      tooltip="Your secret API key. Never share this."
      for="api-key-input"
    />

## Attributes

* `label` (`:string`) (required) - Label text.
* `tooltip` (`:string`) (required) - Tooltip text shown on hover/focus.
* `for` (`:string`) - HTML `for` attribute for the label. Defaults to `nil`.
* `required` (`:boolean`) - Show required asterisk. Defaults to `false`.
* `class` (`:string`) - Defaults to `nil`.

# `line_clamp`

Clamps text to a number of lines with a client-side read-more/less toggle.

Uses `JS.toggle_class` to flip the clamp — no server round-trip needed.
The clamped state uses CSS `line-clamp-{n}` utilities.

## Example

    <.line_clamp lines={3}>
      A very long article excerpt that goes on for many lines...
    </.line_clamp>

## Attributes

* `lines` (`:integer`) - Number of lines to clamp to initially. Defaults to `3`.
* `read_more_label` (`:string`) - Defaults to `"Read more"`.
* `read_less_label` (`:string`) - Defaults to `"Read less"`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `number_format`

Renders a formatted number with optional compact notation, prefix, and suffix.

Compact notation: 1000 → "1K", 1_000_000 → "1M", 1_000_000_000 → "1B".

## Example

    <.number_format value={1234567} compact={true} prefix="$" />
    <%!-- Renders: $1.2M --%>

    <.number_format value={3.14159} decimals={2} suffix="%" />
    <%!-- Renders: 3.14% --%>

## Attributes

* `value` (`:any`) (required) - Numeric value to format.
* `compact` (`:boolean`) - Use compact notation: 1K, 1M, 1B. Defaults to `false`.
* `decimals` (`:integer`) - Decimal places (non-compact mode). Defaults to `0`.
* `prefix` (`:string`) - Prefix string (e.g. '$'). Defaults to `nil`.
* `suffix` (`:string`) - Suffix string (e.g. '%'). Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.

# `print_only`

Wraps content that should only be visible when printing.

Hidden on screen (`hidden`), shown when printing (`print:block`).

## Example

    <.print_only>
      <h1>Invoice #1234</h1>
      <p>Printed at: 2026-01-01</p>
    </.print_only>

## Attributes

* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `reading_time`

Estimates and displays reading time for a block of text.

Uses `ceil(word_count / wpm)` rounding up to the nearest minute.
Default WPM is 238, which is the average adult silent reading speed.

## Example

    <.reading_time text={@article.body} />
    <%!-- Renders: "5 min read" --%>

    <.reading_time text={@post.content} wpm={200} label="minute read" />

## Attributes

* `text` (`:string`) (required) - Plain text content to estimate reading time for.
* `wpm` (`:integer`) - Words per minute reading speed (default: 238). Defaults to `238`.
* `label` (`:string`) - Label appended after the minute count. Defaults to `"min read"`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.

# `relative_time`

Renders a `DateTime` as a relative human-readable string.

Returns values like "just now", "5 minutes ago", "2 hours ago", "3 days ago",
"4 months ago", "2 years ago". Negative differences show "in X minutes", etc.

## Example

    <.relative_time datetime={@post.inserted_at} />

## Attributes

* `datetime` (`:any`) (required) - A `DateTime` or `NaiveDateTime` struct.
* `now` (`:any`) - Override 'now' (for testing). Defaults to `DateTime.utc_now()`. Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.

# `screen_only`

Wraps content that should be hidden when printing.

Visible on screen, hidden when printing (`print:hidden`).

## Example

    <.screen_only>
      <nav>...</nav>
      <button phx-click="action">Click me</button>
    </.screen_only>

## Attributes

* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `stat_unit`

Renders a value+unit pair for metric or stat displays.

The value is bold; the unit is muted and slightly smaller.

## Example

    <.stat_unit value="42" unit="requests/sec" />
    <.stat_unit value="99.9" unit="%" value_class="text-3xl" />

## Attributes

* `value` (`:string`) (required) - The primary numeric or text value.
* `unit` (`:string`) (required) - The unit label displayed after the value.
* `value_class` (`:string`) - Additional classes for the value element. Defaults to `nil`.
* `unit_class` (`:string`) - Additional classes for the unit element. Defaults to `nil`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.

# `sticky_wrapper`

Renders a sticky-positioned wrapper at a configurable top offset.

Use for headers, toolbars, or sidebars that should remain visible
during scrolling within a scrollable container.

## Example

    <.sticky_wrapper offset_top={64}>
      <.data_grid_toolbar>...</.data_grid_toolbar>
    </.sticky_wrapper>

## Attributes

* `offset_top` (`:integer`) - Top offset in pixels for the sticky position. Defaults to `0`.
* `z_index` (`:integer`) - z-index for the sticky element. Defaults to `10`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `visually_hidden`

Renders content that is visually hidden but accessible to screen readers.

Uses the `sr-only` utility class. Use this for labels, descriptions, and
context text that sighted users can infer visually but screen reader users need.

## Example

    <.visually_hidden>Current step: 3 of 5</.visually_hidden>

## Attributes

* `as` (`:string`) - HTML element to render: 'span' or 'div'. Defaults to `"span"`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.
## Slots

* `inner_block` (required)

# `word_count`

Displays the word count of a string.

Splits on whitespace and counts non-empty tokens.

## Example

    <.word_count text={@body} />
    <%!-- Renders: "247 words" --%>

    <.word_count text={@body} label="word count" />

## Attributes

* `text` (`:string`) (required) - Text to count words in.
* `label` (`:string`) - Label appended after the word count. Defaults to `"words"`.
* `class` (`:string`) - Defaults to `nil`.
* Global attributes are accepted.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
