These ship in the hooks bundle and install themselves on import. No page-local phx-hook and no extra Hooks entry. Markup is enough.

import LanternHooks from "../../deps/lantern_ui/priv/static/lantern_ui_hooks.js"

Rebuild the bundle after editing assets/js/ with npm run build and commit priv/static/lantern_ui_hooks.js.

List keyboard nav

data-lantern-list-nav on the list, data-lantern-list-item on each row. While focus is inside the list: j / ArrowDown and k / ArrowUp move a roving tabindex, Home / End jump, Enter activates the item's own link (or clicks the item). The focused row gets a visible ring.

<ul data-lantern-list-nav>
  <li :for={row <- @rows} data-lantern-list-item>
    <.link navigate={~p"/tickets/#{row}"}>{row.title}</.link>
  </li>
</ul>

Do not bind j/k on a typing target (input, textarea, select, contenteditable) — those keys stay characters there.

Persist open/collapsed

data-lantern-persist="<key>" stores open / closed in localStorage (lantern:persist:<key>). Failures (quota, private mode) are ignored.

On a <details>:

<details data-lantern-persist="hub:filters">
  <summary>Filters</summary>
  ...
</details>

On any other element, add a toggle (and optional panel):

<div data-lantern-persist="hub:side" data-open="true">
  <button type="button" data-lantern-persist-toggle aria-expanded="true">
    Inspector
  </button>
  <div data-lantern-persist-panel>
    ...
  </div>
</div>

side_panel in a consuming app can keep a temporary page-local hook until it switches to this attribute. Same key space; do not write both to the same key.

Overlay placement

Popover, dropdown, select, menu, date picker, and autocomplete panels are positioned with @floating-ui/dom (flip + shift), bundled into the hooks file. Consumers do not add that dependency themselves.