0.1.1 (2026-10-25)

  • The README's banner follows the light or dark theme on hex.pm and HexDocs too, not only on GitHub.

0.1.0 (2026-10-25)

The first release. The repository also has examples/, a runnable Phoenix app with one page per feature, including a file-based routing example that uses every kind of folder.

A guide, "Icons, notifications and languages", and four examples show everyday Phoenix with Xenia: Iconify icon sets as Tailwind classes, success and error messages with put_flash/3, and English and German with Gettext, reaching JavaScript islands as props. They use Phoenix's own tools; Xenia gains no API for them.

Components

  • createHooks({ Name: adapter }) turns adapters into LiveView hooks. An adapter has mount, destroy, and optionally props, events, commands and consumes.
  • <.xenia> renders the container for any adapter: props and events as JSON, with class, style, aria-* and bindings passing through.
  • use Xenia.Components declares typed tags: component :chart, adapter: "Chart" do prop ...; event ... end generates <.chart>, checked at compile time, with on_<event> attributes. Formatter rules via import_deps: [:xenia].
  • Props are compared on every render, and only changed ones reach the adapter. Props without a handler are assigned to the instance, so web components need no prop code.
  • ctx.remount() rebuilds an instance with the latest props, for libraries that can't change a setting in place.
  • Async mount (lazy-loaded libraries): props and commands wait for it.

Loading and errors

  • <:loading> on <.xenia>, every declared tag and <.monaco>: rendered by the server, shown from the first paint until the island's first mount finishes. The island's element stays full size underneath, so libraries that measure themselves while mounting work.
  • <:error>: shown if the adapter throws in mount, a prop handler, a command or a remount. An element inside it with data-xenia-retry starts the island again.
  • on_crash="event" sends %{"id", "phase", "message"} to the LiveView when an island crashes. Neither feature needs adapter code.

Co-located adapters

  • An adapter only one page uses can live in that page's module, in LiveView's <script :type={Phoenix.LiveView.ColocatedHook} name=".Chart">, exporting adapterHook({...}) from "xenia". No change to app.js per adapter.
  • <.xenia adapter={colocated(".Chart")}> and component ..., adapter: ".Chart" resolve the name to the module's hook at compile time, so two pages can each define .Chart.
  • The console check for unregistered adapters also knows co-located ones, and says what to check when one is missing.

Events and commands

  • ctx.emit(name, payload), mapped by the page to a LiveView event, with per-event debounce. Pending events are sent when a navigation starts.
  • Xenia.push_to(socket, id, command, args) sends a command to one component.
  • Events are sent from an element of Xenia's own, so overlapping events can't make LiveView restore stale props on the container.

Calls

  • ctx.call(name, payload) sends an event and resolves with the server's reply: what handle_event returns as {:reply, map, socket}. Declared and bound like events; pending events go first. Sent from a hidden element of Xenia's (the built-in XeniaCall hook), so a pending reply never locks the container.
  • Tom Select's remote search uses it: each query's results come from its own reply.

Keyboard shortcuts

  • shortcuts={%{"mod+s" => "save"}} on any component sends LiveView events for key combinations while focus is inside it. mod is Cmd on a Mac and Ctrl elsewhere. Pending events are sent first; browser shortcuts are refused (Xenia.Shortcuts).

Forms and files

  • field={@form[:name]} on <.xenia>, <.monaco>, and declared tags with form_value: :prop. The component is submitted with the form, fires its phx-change, survives a reconnect, and shows errors like <.input>.
  • ctx.setFormValue(value, { notify }) keeps the form input equal to what the component shows.
  • ctx.upload(name, files) sends files through a LiveView upload.

App-wide values

  • consumes: ["theme"] delivers app-wide values as ordinary props. An explicit prop wins.
  • Publish from the server with <Xenia.Provider.provider name value>, or from the browser with setProvider. Read them from any JavaScript with readProvider and subscribeProvider.

Pages from folders

  • xenia_routes "lib/my_app_web/app" generates Phoenix live routes from a folder:

    • page.ex makes its folder a route
    • (group) folders stay out of the URL
    • [id] and [...path] folders are parameters
    • _private folders are never routes

    Routes update when pages are added, renamed or removed, and conflicting paths are compile errors.

  • use Xenia.Page: a page drawn inside the layout.ex files above it.

  • use Xenia.Layout: a frame/1 around the pages below, with on_mount: for hooks such as authentication.

  • persistent.ex next to a layout.ex with use Xenia.Persistent: the layout's persistent part, a LiveView that stays alive across navigation between its pages, told about navigation through handle_navigation/2.

  • live.ex with use Xenia.Live: one LiveView for the pages below it, switched by patch.

  • mix xenia.routes lists the generated routes with their layouts.

  • A catch-all ([...path]) also matches its folder's own URL, with an empty list. [[...path]] is an error that says to write [...path].

Metadata and the sitemap

  • metadata/1 on pages and layouts: title (with a layout's title_template), description, canonical, robots, Open Graph and Twitter tags, merged from the outermost layout to the page. Computed after the page's data is loaded; the title becomes @page_title. <Xenia.Metadata.tags> renders the tags in root.html.heex.
  • Xenia.Sitemap: get "/sitemap.xml", Xenia.Sitemap, [] lists the app folder's pages. Pages with parameters list the values their sitemap/0 returns; use Xenia.Layout, sitemap: false leaves pages out.
  • mix xenia.install adds the tags to root.html.heex, and its layout and home page use metadata/1.

Generators

  • mix xenia.gen.component name prop:type --events a,b: the adapter in assets/js/ (a working placeholder that shows its props) and its typed tag, plus the line to add to app.js. --colocated prints a version to paste into a page instead.
  • mix xenia.gen.page path and mix xenia.gen.layout path [--persistent] [--on-mount Mod:arg] write app-folder files, with module names that mirror the folders. They refuse conflicting paths, misplaced files and existing files (unless --force).

Official adapters

  • createChartjs({ load: () => import("chart.js/auto") }) from "xenia/chartjs", a separate entry point, and the <.chartjs> tag (Xenia.Adapters.Chartjs). Every Chart.js type and option; light and dark colors from the app's theme; point, legend and hover events; highlight, toggle, reset and image commands.
  • Configure an official adapter with factory options (Chart.js: plugins, options, palette, setup); extend it by spreading the plain adapter object. Libraries are loaded once per factory, and plugins registered once.
  • Fourteen more, each with its own entry point and tag, all set up the same way (create<Name>({ load })):
    • editors: xenia/monaco-diff (<.monaco_diff>), xenia/tiptap (<.tiptap>, rich text with a toolbar)
    • data: xenia/d3 (<.d3_chart>, built-in kinds plus your own through charts:), xenia/tabulator (<.tabulator>), xenia/fullcalendar (<.full_calendar>), xenia/leaflet (<.leaflet_map>)
    • inputs: xenia/flatpickr, xenia/tom-select, xenia/shoelace-rating, xenia/sortablejs (<.sortable_list>)
    • media and tools: xenia/xterm, xenia/uppy (uploads through LiveView, XHR or tus), xenia/wavesurfer, xenia/wavesurfer-multitrack
  • Monaco diff, Tiptap, flatpickr, Tom Select and the Shoelace rating are form fields (field={@form[:x]}). Chart.js, xterm and Uppy follow the app's theme.
  • Xenia's bundles import no library: apps install only what they use.

Starter template

  • mix xenia.install turns a new Phoenix 1.8 app into a Xenia app: a plain light and dark theme in Xenia's purple and gold, with a faint grid; a layout and a home page in the app folder, routed by xenia_routes; Xenia's hooks in app.js, with the theme published for components that follow it. Files that don't look like Phoenix's are left alone, with the step to do by hand, and a second run changes nothing.
  • It also gives the app better flash messages: a :success kind, stacked newest first, closing by themselves after 5 seconds with a countdown line around their border (hovering pauses it; errors stay), with shape (rounded, sharp, pill) and duration on <.flash_group>. It replaces flash/1 only when it is exactly what Phoenix generated.

Monaco

  • <.monaco> and createMonaco({ load: () => import("monaco-editor") }) from "xenia/monaco", a separate entry point, so apps without Monaco never load or resolve it.
  • Server updates keep cursors and undo history, and land while read-only. The editor tells echoes of its own typing apart from real updates (version), and revision forces the server's value.
  • Follows the app's theme, with themes for your own light and dark Monaco themes, and theme to pin one.