Skip to content

Commit 8c327ec

Browse files
committed
Rework theme toggle into a dropdown menu
Now it is possible to reset back to the system theme, rather than toggling between light and dark without any ability to revert.
1 parent 8e2c5f6 commit 8c327ec

File tree

12 files changed

+218
-31
lines changed

12 files changed

+218
-31
lines changed

.formatter.exs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
:ecto_sql,
66
:phoenix
77
],
8-
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
8+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs,heex}"],
99
export: [
1010
locals_without_parens: [oban_dashboard: 1, oban_dashboard: 2]
1111
],
12-
locals_without_parens: [oban_dashboard: 1, oban_dashboard: 2]
12+
locals_without_parens: [oban_dashboard: 1, oban_dashboard: 2],
13+
plugins: [Phoenix.LiveView.HTMLFormatter]
1314
]

assets/js/app.js

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,38 @@ Hooks.ToggleRefresh = {
3636
}
3737
}
3838

39-
Hooks.ToggleDarkMode = {
40-
setMode() {
41-
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
39+
Hooks.RestoreTheme = {
40+
mounted() {
41+
this.pushEventTo("#theme-selector", "restore", {
42+
theme: localStorage.getItem("theme")
43+
})
44+
}
45+
}
46+
47+
Hooks.ChangeTheme = {
48+
applyTheme() {
49+
const wantsDark = window.matchMedia("(prefers-color-scheme: dark)").matches
50+
const noPreference = !("theme" in localStorage)
51+
52+
if ((localStorage.theme === "dark") || (localStorage.theme === "system" && wantsDark) || (noPreference && wantsDark)) {
4253
document.documentElement.classList.add("dark")
4354
} else {
4455
document.documentElement.classList.remove("dark")
4556
}
4657
},
4758

4859
mounted() {
60+
let elem = this;
61+
4962
this.el.addEventListener("click", _event => {
50-
localStorage.theme = localStorage.theme === "dark" ? "light" : "dark";
63+
const theme = this.el.getAttribute("value")
5164

52-
this.setMode();
53-
})
65+
localStorage.theme = theme
5466

55-
const content = this.el.getAttribute("data-title");
67+
this.applyTheme()
5668

57-
tippy(this.el, { arrow: roundArrow, content: content, delay: [500, null] });
69+
elem.pushEventTo("#theme-selector", "restore", {theme: theme})
70+
})
5871
}
5972
}
6073

assets/tailwind.config.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ module.exports = {
2222
colors: {
2323
transparent: "transparent",
2424
current: "currentColor",
25-
black: "#000",
26-
white: "#fff",
25+
black: colors.black,
26+
white: colors.white,
2727
blue: colors.sky,
2828
cyan: colors.cyan,
2929
gray: colors.gray,
@@ -34,7 +34,8 @@ module.exports = {
3434
red: colors.red,
3535
teal: colors.teal,
3636
violet: colors.violet,
37-
yellow: colors.amber
37+
yellow: colors.amber,
38+
slate: colors.slate
3839
}
3940
},
4041
content: ["../lib/**/*.*ex"],

lib/oban/web.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ defmodule Oban.Web do
4545
import Phoenix.LiveView.Helpers
4646

4747
alias Phoenix.LiveView.JS
48+
alias Oban.Web.Components.Icons
4849
end
4950
end
5051

lib/oban/web/components/icons.ex

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
defmodule Oban.Web.Components.Icons do
2+
use Oban.Web, :live_component
3+
4+
attr :rest, :global, default: %{
5+
"stroke-width": "1.5",
6+
class: "w-6 h-6",
7+
fill: "none",
8+
stroke: "currentColor",
9+
viewBox: "0 0 24 24"
10+
}
11+
12+
slot :inner_block, required: true
13+
14+
defp svg_outline(assigns) do
15+
~H"""
16+
<svg xmlns="http://www.w3.org/2000/svg" {@rest}>
17+
<%= render_slot(@inner_block) %>
18+
</svg>
19+
"""
20+
end
21+
22+
attr :rest, :global, default: %{
23+
"aria-hidden": "true",
24+
class: "w-6 h-6",
25+
fill: "currentColor",
26+
viewBox: "0 0 24 24"
27+
}
28+
29+
slot :inner_block, required: true
30+
31+
defp svg_solid(assigns) do
32+
~H"""
33+
<svg xmlns="http://www.w3.org/2000/svg" {@rest}>
34+
<%= render_slot(@inner_block) %>
35+
</svg>
36+
"""
37+
end
38+
39+
attr :rest, :global
40+
41+
def arrow_path(assigns) do
42+
~H"""
43+
<.svg_outline {@rest}>
44+
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
45+
</.svg_outline>
46+
"""
47+
end
48+
49+
attr :rest, :global
50+
51+
def computer_desktop(assigns) do
52+
~H"""
53+
<.svg_outline {@rest}>
54+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
55+
</.svg_outline>
56+
"""
57+
end
58+
59+
attr :rest, :global
60+
61+
def moon(assigns) do
62+
~H"""
63+
<.svg_outline {@rest}>
64+
<path d="M17.715 15.15A6.5 6.5 0 0 1 9 6.035C6.106 6.922 4 9.645 4 12.867c0 3.94 3.153 7.136 7.042 7.136 3.101 0 5.734-2.032 6.673-4.853Z"></path>
65+
</.svg_outline>
66+
"""
67+
end
68+
69+
attr :rest, :global
70+
71+
def sun(assigns) do
72+
~H"""
73+
<.svg_outline {@rest}>
74+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
75+
</.svg_outline>
76+
"""
77+
end
78+
end

lib/oban/web/components/layouts.ex

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ defmodule Oban.Web.Layouts do
33

44
import Oban.Web.Helpers
55

6-
alias Oban.Web.RefreshComponent
6+
alias Oban.Web.Components.{Refresh, Theme}
77

88
js_path = Path.join(__DIR__, "../../../../priv/static/app.js")
99
css_path = Path.join(__DIR__, "../../../../priv/static/app.css")
@@ -86,10 +86,10 @@ defmodule Oban.Web.Layouts do
8686
~H"""
8787
<button
8888
id="dark-toggle"
89-
class="ml-3 p-2 relative text-blue-800 dark:text-gray-50 bg-blue-200 dark:bg-blue-300 dark:bg-opacity-25 rounded-full focus:outline-none hover:text-blue-600 dark:hover:text-gray-300 hidden md:block"
89+
class="ml-3 relative text-slate-500 dark:text-slate-400 rounded-full focus:outline-none hover:text-slate-600 dark:hover:text-slate-300 hidden md:block"
9090
data-title="Toggle dark mode"
9191
phx-hook="ToggleDarkMode">
92-
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"></path></svg>
92+
<Icons.moon />
9393
</button>
9494
"""
9595
end
@@ -118,8 +118,7 @@ defmodule Oban.Web.Layouts do
118118
end
119119

120120
defp link_class(page, page) do
121-
"font-medium text-sm rounded-md px-3 py-2 bg-blue-200 text-blue-800 dark:text-gray-200 dark:bg-blue-300 dark:bg-opacity-25"
122-
end
121+
"font-medium text-sm rounded-md px-3 py-2 bg-blue-200 text-blue-800 dark:text-gray-200 dark:bg-blue-300 dark:bg-opacity-25" end
123122

124123
defp link_class(_pag, _exp) do
125124
"font-medium text-sm rounded-md px-3 py-2 text-gray-600 dark:text-gray-100 hover:text-gray-800 dark:hover:text-gray-300"

lib/oban/web/components/layouts/live.html.heex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
<header class="flex items-center">
1010
<.logo />
1111
<.tabs socket={@socket} page={@page.name} />
12-
<.live_component module={RefreshComponent} id="refresh" refresh={@refresh} />
13-
<.dark_toggle />
12+
<.live_component module={Refresh} id="refresh" refresh={@refresh} />
13+
<.live_component module={Theme} id="theme" />
1414
</header>
1515

1616
<%= @inner_content %>

lib/oban/web/components/layouts/root.html.heex

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@
1616
<style phx-track-static nonce={@csp_nonces.style}><%= raw(render("app.css")) %></style>
1717

1818
<script nonce={@csp_nonces.script}>
19-
if (localStorage.theme === "dark" || (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)) {
19+
const wantsDark = window.matchMedia("(prefers-color-scheme: dark)").matches
20+
const noPreference = !("theme" in localStorage)
21+
22+
if ((localStorage.theme === "dark") || (localStorage.theme === "system" && wantsDark) || (noPreference && wantsDark)) {
2023
document.documentElement.classList.add("dark")
2124
} else {
2225
document.documentElement.classList.remove("dark")
@@ -28,5 +31,7 @@
2831
<%= @inner_content %>
2932
</body>
3033

31-
<script defer phx-track-static type="text/javascript" nonce={@csp_nonces.script}><%= raw(render("app.js")) %></script>
34+
<script phx-track-static type="text/javascript" nonce={@csp_nonces.script}>
35+
<%= raw(render("app.js")) %>
36+
</script>
3237
</html>

lib/oban/web/components/refresh_component.ex renamed to lib/oban/web/components/refresh.ex

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
defmodule Oban.Web.RefreshComponent do
1+
defmodule Oban.Web.Components.Refresh do
22
use Oban.Web, :live_component
33

4-
alias Phoenix.LiveView.JS
5-
64
@refresh_options %{1 => "1s", 2 => "2s", 5 => "5s", 15 => "15s", 60 => "1m", -1 => "Off"}
75

86
@impl Phoenix.LiveComponent

lib/oban/web/components/theme.ex

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
defmodule Oban.Web.Components.Theme do
2+
use Oban.Web, :live_component
3+
4+
@impl Phoenix.LiveComponent
5+
def mount(socket) do
6+
{:ok, assign(socket, :theme, "unknown")}
7+
end
8+
9+
@impl Phoenix.LiveComponent
10+
def render(assigns) do
11+
~H"""
12+
<div class="ml-3 relative" id="theme-selector" phx-hook="RestoreTheme">
13+
<button
14+
id="dark-toggle"
15+
class="text-slate-500 dark:text-slate-400 focus:outline-none hover:text-slate-600 dark:hover:text-slate-300 hidden md:block"
16+
aria-haspopup="listbox"
17+
aria-expanded="true"
18+
aria-labelledby="listbox-label"
19+
data-title="Toggle theme"
20+
phx-hook="Tippy"
21+
phx-click={JS.toggle(to: "#theme-menu")}
22+
type="button"
23+
>
24+
<%= case @theme do %>
25+
<% "light" -> %>
26+
<Icons.sun />
27+
<% "dark" -> %>
28+
<Icons.moon />
29+
<% "system" -> %>
30+
<Icons.computer_desktop />
31+
<% _ -> %>
32+
<span class="block w-6 h-6"></span>
33+
<% end %>
34+
</button>
35+
36+
<ul
37+
id="theme-menu"
38+
class="hidden absolute z-50 top-full right-0 mt-4 overflow-hidden rounded-lg shadow-lg w-36 py-1 text-sm font-semibold bg-white dark:bg-slate-800"
39+
tabindex="-1"
40+
role="listbox"
41+
>
42+
<.option value="light" current={@theme}>
43+
<Icons.sun />
44+
</.option>
45+
46+
<.option value="dark" current={@theme}>
47+
<Icons.moon />
48+
</.option>
49+
50+
<.option value="system" current={@theme}>
51+
<Icons.computer_desktop />
52+
</.option>
53+
</ul>
54+
</div>
55+
"""
56+
end
57+
58+
attr :current, :string, required: true
59+
attr :value, :string, required: true
60+
slot :inner_block, required: true
61+
62+
defp option(assigns) do
63+
class =
64+
if assigns.current == assigns.value do
65+
"text-blue-500 dark:text-blue-400"
66+
else
67+
"text-slate-500 dark:text-slate-400 "
68+
end
69+
70+
assigns = assign(assigns, :class, class)
71+
72+
~H"""
73+
<li
74+
class={"block w-full py-1 px-2 flex items-center cursor-pointer space-x-2 hover:bg-slate-50 hover:dark:bg-slate-600/30 #{@class}"}
75+
id={"select-theme-#{@value}"}
76+
phx-click-away={JS.hide(to: "#theme-menu")}
77+
phx-hook="ChangeTheme"
78+
role="option"
79+
value={@value}
80+
>
81+
<%= render_slot(@inner_block) %>
82+
<span class="capitalize text-slate-800 dark:text-slate-200"><%= @value %></span>
83+
</li>
84+
"""
85+
end
86+
87+
@impl Phoenix.LiveComponent
88+
def handle_event("restore", %{"theme" => theme}, socket) do
89+
{:noreply, assign(socket, :theme, theme)}
90+
end
91+
end

priv/static/app.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

priv/static/app.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)