PrimerLive.Theme (PrimerLive v0.1.15)

Primer CSS contains styles for light/dark color modes and themes, with support for color blindness.

PrimerLive provides components and functions to work with themes:

persistency

Persistency

There is no easy way to save persistent session data in LiveView because LiveView's state is stored in a process that ends when the page is left. The solution is to use an Ajax request to our Phoenix app, which updates the session.

This setup involves 5 steps, but the provided helper functions make it a bit easier.

1-create-a-sessioncontroller

1. Create a SessionController

Create file controllers/session_controller.ex:

defmodule MyAppWeb.SessionController do
  use MyAppWeb, :controller
  use PrimerLive.ThemeSessionController
end

PrimerLive.ThemeSessionController will add the received theme request data to the session.

2-add-sessioncontroller-to-the-router-s-api

2. Add SessionController to the router's api

scope "/api", MyAppWeb do
  pipe_through :api

  post PrimerLive.Theme.session_route(), SessionController, :set
end

Optionally set the max_age in endpoint.ex:

@session_options [
  ...
  # Over 300 years.
  max_age: 9_999_999_999
]

3-add-thememenu-hook

3. Add ThemeMenu hook

In app.js, import ThemeMenu and add it to the liveSocket hooks:

import { Prompt, ThemeMenu } from 'primer-live';

const hooks = {};
hooks.Prompt = Prompt;
hooks.ThemeMenu = ThemeMenu;

let liveSocket = new LiveSocket('/live', Socket, {
  params: { _csrf_token: csrfToken },
  hooks,
});

You may the hook anywhere, but only once for the entire application. If you have a single theme menu, add it there:

<.action_menu phx-hook="ThemeMenu" id="theme_menu">
...
</.action_menu>

4-initialise-the-theme-from-session-data

4. Initialise the theme from session data

In the LiveView's mount, call add_to_socket:

def mount(_params, session, socket) do
  socket =
    socket
    |> PrimerLive.Theme.add_to_socket(session)

  {:ok, socket}
end

This reads the theme state and adds it to the socket assigns.

Optionally set the default theme in the seconds argument:

PrimerLive.Theme.add_to_socket(session, %{
  color_mode: "light",
  light_theme: "light",
  dark_theme: "dark"
})

5-handle-update-events

5. Handle update events

In the LiveView where the action menu resides, add:

use PrimerLive.ThemeEvent

This implements function handle_event for "update_theme" (which is called by clicks on the menu's action_list items). The function updates the socket and sends the event that is picked by by JavaScript (via the ThemeMenu hook).

Link to this section Summary

Functions

Adds theme_state and default_theme_state to socket.assigns.

Configures menu options from supplied params

Default label for a theme menu.

Default options for a theme menu.

Initial theme state.

Creates HTML (data) attributes from the supplied theme state to set a theme on a component or element directly

Compares the supplied state with the supplied default state.

Internal use. Reset link identifier to distinguish in update.

Generic key used for

Route for session api calls.

Theme data stored in the session.

Returns an updated theme state by putting the supplied data in the theme state. If key is the reset key, returns the default theme state.

Internal use. Update link identifier.

Link to this section Functions

Link to this function

add_to_socket(socket, session)

See add_to_socket/3.

Link to this function

add_to_socket(socket, session, default_theme_state)

Adds theme_state and default_theme_state to socket.assigns.

Link to this function

create_menu_items(theme, menu_options, menu_labels)

Configures menu options from supplied params:

  • theme: the current theme state (used to define the selected menu items)
  • menu_options: which menu options will be displayed
  • menu_labels: overrides of default text labels

Returns a list with 3 menu elements:

  • color_mode
  • dark_theme
  • light_theme

Each list item is a map that contains display attributes:

  • group label
  • option labels
  • the selected item

tests

Tests

iex> PrimerLive.Theme.create_menu_items(
...> %{
...>   color_mode: "light",
...>   light_theme: "light_high_contrast",
...>   dark_theme: "dark_high_contrast"
...> },
...> PrimerLive.Theme.default_menu_options(),
...> PrimerLive.Theme.default_menu_labels()
...> )
[color_mode: %{labeled_options: [{"light", "Light"}, {"dark", "Dark"}, {"auto", "System"}], options: ["light", "dark", "auto"], selected: "light", title: "Theme"}, dark_theme: %{labeled_options: [{"dark", "Dark"}, {"dark_dimmed", "Dark dimmed"}, {"dark_high_contrast", "Dark high contrast"}, {"dark_colorblind", "Dark colorblind"}, {"dark_tritanopia", "Dark Tritanopia"}], options: ["dark", "dark_dimmed", "dark_high_contrast", "dark_colorblind", "dark_tritanopia"], selected: "dark_high_contrast", title: "Dark tone"}, light_theme: %{labeled_options: [{"light", "Light"}, {"light_high_contrast", "Light high contrast"}, {"light_colorblind", "Light colorblind"}, {"light_tritanopia", "Light Tritanopia"}], options: ["light", "light_high_contrast", "light_colorblind", "light_tritanopia"], selected: "light_high_contrast", title: "Light tone"}]

iex> PrimerLive.Theme.create_menu_items(
...> %{
...>   color_mode: "light",
...>   light_theme: "light_high_contrast",
...>   dark_theme: "dark_high_contrast"
...> },
...> %{
...>   color_mode: ~w(light dark)
...> },
...> %{
...>   color_mode: %{
...>     light: "Light theme"
...>   },
...>   reset: "Reset"
...> })
[color_mode: %{labeled_options: [{"light", "Light theme"}, {"dark", "Dark"}], options: ["light", "dark"], selected: "light", title: "Theme"}]
Link to this function

default_menu_labels()

Default label for a theme menu.

Link to this function

default_menu_options()

Default options for a theme menu.

Link to this function

default_theme_state()

Initial theme state.

Link to this function

html_attributes(theme_state)

See html_attributes/2.

Link to this function

html_attributes(theme_state, default_theme_state)

Creates HTML (data) attributes from the supplied theme state to set a theme on a component or element directly:

<.button
  {PrimerLive.Theme.html_attributes([color_mode: "dark", dark_theme: "dark_high_contrast"])}
>Button</.button>

<.octicon name="sun-24"
  {PrimerLive.Theme.html_attributes(%{color_mode: "dark", dark_theme: "dark_dimmed"})}
/>

tests

Tests

iex> PrimerLive.Theme.html_attributes(
...> %{
...>   color_mode: "light",
...>   light_theme: "light_high_contrast",
...>   dark_theme: "dark_high_contrast"
...> }
...> )
[data_color_mode: "light", data_light_theme: "light_high_contrast", data_dark_theme: "dark_high_contrast"]

iex> PrimerLive.Theme.html_attributes(
...> %{
...> },
...> %{
...>   color_mode: "auto",
...>   light_theme: "light",
...>   dark_theme: "dark"
...> }
...> )
[data_color_mode: "auto", data_light_theme: "light", data_dark_theme: "dark"]

iex> PrimerLive.Theme.html_attributes(
...> %{
...>   light_theme: "light_high_contrast",
...> },
...> %{
...>   color_mode: "auto",
...>   light_theme: "light",
...>   dark_theme: "dark"
...> }
...> )
[data_color_mode: "auto", data_light_theme: "light_high_contrast", data_dark_theme: "dark"]

iex> PrimerLive.Theme.html_attributes(
...> %{
...> },
...> %{
...>   color_mode: "auto",
...> }
...> )
[data_color_mode: "auto"]
Link to this function

is_default_theme(theme, default_theme_state)

Compares the supplied state with the supplied default state.

tests

Tests

iex> PrimerLive.Theme.is_default_theme(
...> %{
...>   color_mode: "auto",
...>   light_theme: "light",
...>   dark_theme: "dark"
...> },
...> PrimerLive.Theme.default_theme_state()
...> )
true

iex> PrimerLive.Theme.is_default_theme(
...> %{
...>   color_mode: "light",
...>   light_theme: "light_high_contrast",
...>   dark_theme: "dark_high_contrast"
...> },
...> PrimerLive.Theme.default_theme_state()
...> )
false

Internal use. Reset link identifier to distinguish in update.

Generic key used for:

  • session route
  • JS event name ("phx:" prefix is assigned automatically)
Link to this function

session_route()

Route for session api calls.

For example:

scope "/api", MyAppWeb do
  pipe_through :api

  post PrimerLive.Theme.session_route(), SessionController, :set
end
Link to this function

session_theme_key()

Theme data stored in the session.

Link to this function

update(theme_state, map, default_theme_state)

Returns an updated theme state by putting the supplied data in the theme state. If key is the reset key, returns the default theme state.

Link to this function

update_theme_event_key()

Internal use. Update link identifier.