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.
Enter on a focused data-lantern-list-item activates that item. Enter
elsewhere inside the list (a group-band collapse button) is left alone.
Group collapse
data-lantern-collapse="<key>" on the band's control (a <button>). Click,
Enter, or Space toggles data-collapsed on the closest .lui-group-band and
the hidden attribute on every sibling in the same parent that carries
data-lantern-group="<key>". aria-expanded tracks the open state.
group_band with group= (and no navigate/patch/href) wires the
button. list_row with group= sets data-lantern-group. Any sibling may
carry that attribute — it is not list-row-only.
<div>
<.group_band name="In progress" count={2} group="tickets:in_progress">
<:glyph><.status_glyph status={:in_progress} /></:glyph>
</.group_band>
<.list_row group="tickets:in_progress" title={ticket.title} navigate={ticket.href} />
</div>After a LiveView morph, restoreAll re-hides siblings from the band's
data-collapsed (so a newly patched-in row under a still-collapsed band
disappears). To keep the collapsed set when the server rewrites the band,
and across a full reload, put data-lantern-persist on the band with
the same key:
<.group_band
name="In progress"
group="tickets:in_progress"
data-lantern-persist="tickets:in_progress"
>That writes lantern:persist:tickets:in_progress as open / closed. Do
not also put data-lantern-persist-toggle on the collapse button — collapse
is the writer for this key. Namespace keys per page (tickets:done vs
runs:done) so two lists do not share a collapsed set.
The chevron stays the server-rendered down icon; CSS rotates
.lui-group-band[data-collapsed] .lui-group-band-main[data-lantern-collapse] .lui-group-band-chevron
so a client toggle does not need a re-render.
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 as group collapse; a group band
uses persist with the group key, and should not also carry
data-lantern-persist-toggle.
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.