# `Raxol.LiveView.TerminalBridge`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.7.0/lib/raxol/live_view/terminal_bridge.ex#L1)

Converts Raxol.Core buffers to HTML for Phoenix LiveView rendering.

This module provides efficient buffer-to-HTML conversion with performance
optimizations for 60fps rendering (< 16ms per frame).

## Features

- Virtual DOM-style diffing (only update changed cells)
- Character and style caching for performance
- CSS class generation for theming
- Inline style support for custom colors
- Accessibility features (ARIA labels)

## Performance

Target: < 16ms per frame for 60fps
Actual: ~2-5ms for typical 80x24 buffers

## Examples

    # Basic conversion
    buffer = Raxol.Core.Buffer.create_blank_buffer(80, 24)
    buffer = Raxol.Core.Buffer.write_at(buffer, 5, 3, "Hello, World!")
    html = Raxol.LiveView.TerminalBridge.buffer_to_html(buffer)

    # With theme
    html = Raxol.LiveView.TerminalBridge.buffer_to_html(buffer, theme: :nord)

    # With custom CSS classes
    html = Raxol.LiveView.TerminalBridge.buffer_to_html(buffer,
      css_prefix: "terminal",
      use_inline_styles: false
    )

## Integration with LiveView

    defmodule MyAppWeb.TerminalLive do
      use MyAppWeb, :live_view
      alias Raxol.LiveView.TerminalBridge

      def mount(_params, _session, socket) do
        buffer = Raxol.Core.Buffer.create_blank_buffer(80, 24)
        {:ok, assign(socket, buffer: buffer)}
      end

      def render(assigns) do
        ~H"""
        <div class="terminal-container">
          <%= raw(TerminalBridge.buffer_to_html(@buffer, theme: :dracula)) %>
        </div>
        """
      end
    end

# `aria_mode`

```elixir
@type aria_mode() :: :log | :application
```

# `html_opts`

```elixir
@type html_opts() :: [
  theme: theme(),
  css_prefix: String.t(),
  use_inline_styles: boolean(),
  show_cursor: boolean(),
  cursor_position: {non_neg_integer(), non_neg_integer()} | nil,
  cursor_style: :block | :underline | :bar,
  aria_mode: aria_mode(),
  a11y_map: %{optional(String.t()) =&gt; map()}
]
```

# `row`

```elixir
@type row() :: %{id: String.t(), y: non_neg_integer(), html: String.t()}
```

# `theme`

```elixir
@type theme() ::
  :nord
  | :dracula
  | :solarized_dark
  | :solarized_light
  | :monokai
  | :synthwave84
  | :gruvbox_dark
  | :one_dark
  | :tokyo_night
  | :catppuccin
  | :default
```

# `animation_css`

```elixir
@spec animation_css([map()]) :: String.t()
```

Generates CSS transition rules from animation hints on positioned elements.

Takes a list of positioned element maps (output of LayoutEngine) and
extracts animation hints from elements that have an `:id` field. Returns
a `<style>` block with CSS `transition` rules targeting `[data-raxol-id]`
selectors, plus a `prefers-reduced-motion` media query.

Also emits `overflow-anchor` rules for elements carrying an
`:overflow_anchor` field (`:auto` or `:none`) -- the browser-native
counterpart to the terminal `Viewport` component's `overflow_anchor`
scroll-pinning behavior. `:auto` lets the browser hold scroll position as
content grows above the fold (mirrors the terminal's follow-mode / hold
semantics); `:none` opts an element out entirely.

Returns an empty string if no elements carry animation hints or an
overflow-anchor setting.

## Examples

    elements = [
      %{id: "panel", type: :box, animation_hints: [
        %{property: :opacity, duration_ms: 300, easing: :ease_out_cubic, delay_ms: 0}
      ]}
    ]
    css = animation_css(elements)
    # => "<style>[data-raxol-id=\"panel\"] { transition: opacity 300ms cubic-bezier(...) 0ms; }\n..."

    elements = [%{id: "log", type: :box, overflow_anchor: :auto}]
    css = animation_css(elements)
    # => "<style>[data-raxol-id=\"log\"] { overflow-anchor: auto; }\n...</style>"

# `aria_mode_of`

```elixir
@spec aria_mode_of(String.t()) :: aria_mode()
```

The `:aria_mode` a screen rendered by `buffer_to_html/2` was rendered in.

A caller that owns the `<pre>` itself (see `buffer_to_rows/2`) strips the one
`buffer_to_html/2` wrote, and the container semantics with it. Reading the
mode back off the markup keeps the two in step without a second channel:
`Raxol.LiveView.TEALive` receives the screen as a bare string and has nothing
else to learn it from. It previously hardcoded `:log`, so an app rendering in
`:application` mode had that silently reversed and its screen became a
whole-screen live region again -- which is the exact thing `:application`
exists to opt out of, and the failure is audible only to a screen-reader user.

Defaults to `:log`, matching `buffer_to_html/2`'s own default.

# `buffer_diff_to_html`

```elixir
@spec buffer_diff_to_html(Raxol.Core.Buffer.t(), Raxol.Core.Buffer.t(), html_opts()) ::
  String.t()
```

Converts a buffer to HTML with diff highlighting (for debugging).

Useful for visualizing which cells changed between frames.

## Examples

    old_buffer = Raxol.Core.Buffer.create_blank_buffer(80, 24)
    new_buffer = Raxol.Core.Buffer.write_at(old_buffer, 5, 3, "Changed")
    html = buffer_diff_to_html(old_buffer, new_buffer)
    # Changed cells will have "raxol-diff-changed" class

# `buffer_to_html`

```elixir
@spec buffer_to_html(Raxol.Core.Buffer.t(), html_opts()) :: String.t()
```

Converts a buffer to HTML string.

## Options

  - `:theme` - Color theme to use (default: :default)
  - `:css_prefix` - CSS class prefix (default: "raxol")
  - `:use_inline_styles` - Use inline styles instead of classes (default: false)
  - `:show_cursor` - Show cursor indicator (default: false)
  - `:cursor_position` - Cursor position {x, y} (default: nil)
  - `:cursor_style` - Cursor style (default: :block)
  - `:aria_mode` - Coarse container semantics (default: `:log`). `:log` wraps
    the terminal in `<pre role="log" aria-live="polite">` so screen readers
    announce output as it changes. `:application` emits `<pre
    role="application">` with no live region, so per-element ARIA and a
    dedicated announcement region drive announcements instead of a
    whole-screen re-read.
  - `:a11y_map` - `id -> accessibility_node` map (from
    `Raxol.Core.Accessibility.Projection.by_id/1`). Spans whose
    `data-raxol-id` matches an entry get per-element ARIA
    (role/aria-label/aria-disabled/aria-checked/aria-selected/aria-expanded/aria-required).

## Examples

    buffer = Raxol.Core.Buffer.create_blank_buffer(80, 24)
    html = buffer_to_html(buffer)
    # => "<pre class=\"raxol-terminal\">...</pre>"

    html = buffer_to_html(buffer, theme: :nord, show_cursor: true, cursor_position: {5, 3})
    # => "<pre class=\"raxol-terminal raxol-theme-nord\">...</pre>"

# `buffer_to_rows`

```elixir
@spec buffer_to_rows(Raxol.Core.Buffer.t(), html_opts()) :: [row()]
```

Converts a buffer to a list of addressable rows.

Takes the same options as `buffer_to_html/2` and produces the same per-row
markup: joining the `:html` values with `"\n"` inside that function's
`<pre>` wrapper reproduces its output byte for byte.

Rows exist so a caller can give each one its own DOM node. With the screen
as a single string, a LiveView sees one changed dynamic and resends every
row on every frame, even when one cell moved.

Ids are `"<css_prefix>-row-<y>"`: stable for a given row across frames, so a
DOM patch strategy can key on them, and prefixed so two terminals on one
page do not collide.

## Examples

    buffer = Raxol.Core.Buffer.create_blank_buffer(3, 2)
    buffer_to_rows(buffer)
    # => [
    #      %{id: "raxol-row-0", y: 0, html: "   "},
    #      %{id: "raxol-row-1", y: 1, html: "   "}
    #    ]

# `container_attrs`

```elixir
@spec container_attrs(aria_mode()) :: keyword()
```

Container ARIA attributes for an `:aria_mode`, as a keyword list.

Public because a caller that renders rows itself owns the `<pre>` wrapper
(see `buffer_to_rows/2`) and has to emit the same container semantics. Two
hand-written copies drift, and the failure mode is a nested or duplicated
live region that re-reads the whole screen.

# `html_to_rows`

```elixir
@spec html_to_rows(String.t(), html_opts()) :: [row()]
```

Splits a screen rendered by `buffer_to_html/2` back into `buffer_to_rows/2`
rows.

The LiveView render backend broadcasts the screen as one already-rendered
string, so a LiveView that wants per-row DOM nodes has no buffer to call
`buffer_to_rows/2` on. This is the exact inverse of the `"\n"` join, and it
leans on the same property that join already leans on: a row's html never
contains a newline, because cells hold single graphemes.

Pass the same `:css_prefix` used to render, or the ids will not match.

# `style_to_classes`

```elixir
@spec style_to_classes(map(), String.t()) :: String.t()
```

Converts a style map to CSS classes.

## Examples

    iex> style = %{bold: true, fg_color: :blue}
    iex> style_to_classes(style, "raxol")
    "raxol-bold raxol-fg-blue"

# `style_to_inline`

```elixir
@spec style_to_inline(map()) :: String.t()
```

Converts a style map to inline CSS styles.

## Examples

    iex> style = %{bold: true, fg_color: {255, 0, 0}}
    iex> style_to_inline(style)
    "font-weight: bold; color: rgb(255, 0, 0);"

---

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