Skip to content

Commit e0ae6f5

Browse files
authored
feat: Light theme (#179)
Add a new light theme and a switcher to toggle between it and the existing dark theme.
1 parent b0b85f0 commit e0ae6f5

10 files changed

Lines changed: 334 additions & 44 deletions

File tree

assets/css/app.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,20 @@
1212
@apply bg-gray-500;
1313
border-radius: 4px;
1414
}
15+
16+
/* ===== Light Theme ===== */
17+
body.light-theme {
18+
background-color: rgb(255 255 255);
19+
color: rgb(17 24 39);
20+
}
21+
22+
/* Theme toggle icon visibility */
23+
.theme-icon-light {
24+
display: none;
25+
}
26+
body.light-theme .theme-icon-dark {
27+
display: none;
28+
}
29+
body.light-theme .theme-icon-light {
30+
display: block;
31+
}

assets/js/app.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,27 @@ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("
55
let livePath = document.querySelector("meta[name='live-path']").getAttribute("content");
66
let liveTransport = document .querySelector("meta[name='live-transport']") .getAttribute("content");
77

8+
// Theme management
9+
const Theme = {
10+
STORAGE_KEY: "error-tracker-theme",
11+
12+
init() {
13+
const saved = localStorage.getItem(this.STORAGE_KEY);
14+
if (saved === "light") {
15+
document.body.classList.add("light-theme");
16+
}
17+
},
18+
19+
toggle() {
20+
const isLight = document.body.classList.toggle("light-theme");
21+
localStorage.setItem(this.STORAGE_KEY, isLight ? "light" : "dark");
22+
},
23+
24+
isLight() {
25+
return document.body.classList.contains("light-theme");
26+
}
27+
};
28+
829
const Hooks = {
930
JsonPrettyPrint: {
1031
mounted() {
@@ -26,6 +47,11 @@ const Hooks = {
2647
// Keep the original content if there's an error
2748
}
2849
}
50+
},
51+
ThemeInit: {
52+
mounted() {
53+
Theme.init();
54+
}
2955
}
3056
};
3157

@@ -41,6 +67,14 @@ topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" });
4167
window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300));
4268
window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide());
4369

70+
// Set up theme toggle via event delegation (CSP-compliant, avoids inline onclick)
71+
document.addEventListener("click", function(e) {
72+
var toggle = e.target.closest("[data-theme-toggle]");
73+
if (toggle) {
74+
Theme.toggle();
75+
}
76+
});
77+
4478
// connect if there are any LiveViews on the page
4579
liveSocket.connect();
4680
window.liveSocket = liveSocket;

assets/tailwind.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ module.exports = {
1717
plugin(({addVariant}) => addVariant('phx-no-feedback', ['&.phx-no-feedback', '.phx-no-feedback &'])),
1818
plugin(({addVariant}) => addVariant('phx-click-loading', ['&.phx-click-loading', '.phx-click-loading &'])),
1919
plugin(({addVariant}) => addVariant('phx-submit-loading', ['&.phx-submit-loading', '.phx-submit-loading &'])),
20-
plugin(({addVariant}) => addVariant('phx-change-loading', ['&.phx-change-loading', '.phx-change-loading &']))
20+
plugin(({addVariant}) => addVariant('phx-change-loading', ['&.phx-change-loading', '.phx-change-loading &'])),
21+
plugin(({addVariant}) => addVariant('light', 'body.light-theme &'))
2122
]
2223
}

lib/error_tracker/web/components/core_components.ex

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ defmodule ErrorTracker.Web.CoreComponents do
2121
<.link
2222
class={[
2323
"phx-submit-loading:opacity-75 py-[11.5px]",
24-
"text-sm font-semibold text-sky-500 hover:text-white/80",
24+
"text-sm font-semibold text-sky-500 light:text-sky-600 hover:text-white/80 light:hover:text-gray-900/80",
2525
@class
2626
]}
2727
{@rest}
@@ -63,14 +63,29 @@ defmodule ErrorTracker.Web.CoreComponents do
6363
def badge(assigns) do
6464
color_class =
6565
case assigns.color do
66-
:blue -> "bg-blue-900 text-blue-300"
67-
:gray -> "bg-gray-700 text-gray-300"
68-
:red -> "bg-red-400/10 text-red-300 ring-red-400/20"
69-
:green -> "bg-emerald-400/10 text-emerald-300 ring-emerald-400/20"
70-
:yellow -> "bg-yellow-900 text-yellow-300"
71-
:indigo -> "bg-indigo-900 text-indigo-300"
72-
:purple -> "bg-purple-900 text-purple-300"
73-
:pink -> "bg-pink-900 text-pink-300"
66+
:blue ->
67+
"bg-blue-900 light:bg-blue-100 text-blue-300 light:text-blue-800"
68+
69+
:gray ->
70+
"bg-gray-700 light:bg-gray-200 text-gray-300 light:text-gray-700"
71+
72+
:red ->
73+
"bg-red-400/10 light:bg-red-100 text-red-300 light:text-red-800 ring-red-400/20 light:ring-red-400/30"
74+
75+
:green ->
76+
"bg-emerald-400/10 light:bg-emerald-100 text-emerald-300 light:text-emerald-800 ring-emerald-400/20 light:ring-emerald-400/30"
77+
78+
:yellow ->
79+
"bg-yellow-900 light:bg-yellow-100 text-yellow-300 light:text-yellow-800"
80+
81+
:indigo ->
82+
"bg-indigo-900 light:bg-indigo-100 text-indigo-300 light:text-indigo-800"
83+
84+
:purple ->
85+
"bg-purple-900 light:bg-purple-100 text-purple-300 light:text-purple-800"
86+
87+
:pink ->
88+
"bg-pink-900 light:bg-pink-100 text-pink-300 light:text-pink-800"
7489
end
7590

7691
assigns = Map.put(assigns, :color_class, color_class)
@@ -95,14 +110,14 @@ defmodule ErrorTracker.Web.CoreComponents do
95110
<div class="mt-10 w-full flex">
96111
<button
97112
:if={@page > 1}
98-
class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 bg-gray-900 border border-gray-400 rounded-lg hover:bg-gray-800 hover:text-white"
113+
class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 light:text-gray-500 bg-gray-900 light:bg-gray-100 border border-gray-400 light:border-gray-300 rounded-lg hover:bg-gray-800 light:hover:bg-gray-200 hover:text-white light:hover:text-gray-900"
99114
phx-click={@event_previous}
100115
>
101116
Previous page
102117
</button>
103118
<button
104119
:if={@page < @total_pages}
105-
class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 bg-gray-900 border border-gray-400 rounded-lg hover:bg-gray-800 hover:text-white"
120+
class="flex items-center justify-center px-4 h-10 text-base font-medium text-gray-400 light:text-gray-500 bg-gray-900 light:bg-gray-100 border border-gray-400 light:border-gray-300 rounded-lg hover:bg-gray-800 light:hover:bg-gray-200 hover:text-white light:hover:text-gray-900"
106121
phx-click={@event_next}
107122
>
108123
Next page
@@ -122,7 +137,10 @@ defmodule ErrorTracker.Web.CoreComponents do
122137
<div>
123138
<h2
124139
:if={assigns[:title]}
125-
class={["text-sm font-semibold mb-2 uppercase text-gray-400", @title_class]}
140+
class={[
141+
"text-sm font-semibold mb-2 uppercase text-gray-400 light:text-gray-500",
142+
@title_class
143+
]}
126144
>
127145
{@title}
128146
</h2>

lib/error_tracker/web/components/layouts.ex

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,17 @@ defmodule ErrorTracker.Web.Layouts do
3535

3636
def navbar(assigns) do
3737
~H"""
38-
<nav class="border-gray-400 bg-gray-900">
38+
<nav class="border-gray-400 light:border-gray-300 bg-gray-900 light:bg-gray-100">
3939
<div class="container flex flex-wrap items-center justify-between mx-auto p-4">
4040
<.link
4141
href={dashboard_path(@socket)}
42-
class="self-center text-2xl font-semibold whitespace-nowrap text-white"
42+
class="self-center text-2xl font-semibold whitespace-nowrap text-white light:text-gray-900"
4343
>
4444
<span class="mr-2">🐛</span>ErrorTracker
4545
</.link>
4646
<button
4747
type="button"
48-
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded -lg md:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-500"
48+
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded -lg md:hidden focus:outline-none focus:ring-2 text-gray-400 light:text-gray-500 hover:bg-gray-700 light:hover:bg-gray-200 focus:ring-gray-500"
4949
aria-controls="navbar-main"
5050
aria-expanded="false"
5151
phx-click={JS.toggle(to: "#navbar-main")}
@@ -68,7 +68,7 @@ defmodule ErrorTracker.Web.Layouts do
6868
</svg>
6969
</button>
7070
<div class="hidden w-full md:block md:w-auto" id="navbar-main">
71-
<ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-400 bg-gray-900 rounded-lg md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-gray-800">
71+
<ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-400 light:border-gray-300 bg-gray-900 light:bg-gray-100 rounded-lg md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-gray-800 light:md:bg-gray-50">
7272
<.navbar_item to="https://github.com/elixir-error-tracker/error-tracker" target="_blank">
7373
<svg
7474
width="18"
@@ -86,6 +86,37 @@ defmodule ErrorTracker.Web.Layouts do
8686
</svg>
8787
GitHub
8888
</.navbar_item>
89+
<li>
90+
<button
91+
data-theme-toggle
92+
class="block py-2 px-3 rounded-lg text-white light:text-gray-900 hover:text-white light:hover:text-gray-900 hover:bg-gray-700 light:hover:bg-gray-200 md:hover:bg-transparent md:border-0 md:hover:text-sky-500"
93+
aria-label="Toggle theme"
94+
title="Toggle theme"
95+
>
96+
<!-- Moon icon (shown in dark mode) -->
97+
<svg
98+
xmlns="http://www.w3.org/2000/svg"
99+
viewBox="0 0 20 20"
100+
fill="currentColor"
101+
class="w-5 h-5 theme-icon-dark"
102+
>
103+
<path
104+
fill-rule="evenodd"
105+
d="M7.455 2.004a.75.75 0 0 1 .26.77 7 7 0 0 0 9.958 7.967.75.75 0 0 1 1.067.853A8.5 8.5 0 1 1 6.647 1.921a.75.75 0 0 1 .808.083Z"
106+
clip-rule="evenodd"
107+
/>
108+
</svg>
109+
<!-- Sun icon (shown in light mode) -->
110+
<svg
111+
xmlns="http://www.w3.org/2000/svg"
112+
viewBox="0 0 20 20"
113+
fill="currentColor"
114+
class="w-5 h-5 theme-icon-light"
115+
>
116+
<path d="M10 2a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 2ZM10 15a.75.75 0 0 1 .75.75v1.5a.75.75 0 0 1-1.5 0v-1.5A.75.75 0 0 1 10 15ZM10 7a3 3 0 1 0 0 6 3 3 0 0 0 0-6ZM15.657 5.404a.75.75 0 1 0-1.06-1.06l-1.061 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM6.464 14.596a.75.75 0 1 0-1.06-1.06l-1.06 1.06a.75.75 0 0 0 1.06 1.06l1.06-1.06ZM18 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 18 10ZM5 10a.75.75 0 0 1-.75.75h-1.5a.75.75 0 0 1 0-1.5h1.5A.75.75 0 0 1 5 10ZM14.596 15.657a.75.75 0 0 0 1.06-1.06l-1.06-1.061a.75.75 0 1 0-1.06 1.06l1.06 1.06ZM5.404 6.464a.75.75 0 0 0 1.06-1.06l-1.06-1.06a.75.75 0 1 0-1.061 1.06l1.06 1.06Z" />
117+
</svg>
118+
</button>
119+
</li>
89120
</ul>
90121
</div>
91122
</div>
@@ -103,7 +134,7 @@ defmodule ErrorTracker.Web.Layouts do
103134
<li>
104135
<a
105136
href={@to}
106-
class="whitespace-nowrap flex-0 block py-2 px-3 rounded-lg text-white hover:text-white hover:bg-gray-700 md:hover:bg-transparent md:border-0 md:hover:text-sky-500"
137+
class="whitespace-nowrap flex-0 block py-2 px-3 rounded-lg text-white light:text-gray-900 hover:text-white light:hover:text-gray-900 hover:bg-gray-700 light:hover:bg-gray-200 md:hover:bg-transparent md:border-0 md:hover:text-sky-500"
107138
{@rest}
108139
>
109140
{render_slot(@inner_block)}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
</script>
2626
</head>
2727

28-
<body class="bg-gray-800 text-white">
28+
<body id="body" class="bg-gray-800 text-white" phx-hook="ThemeInit">
2929
{@inner_content}
3030
</body>
3131
</html>

lib/error_tracker/web/live/dashboard.html.heex

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,28 @@
99
value={@search_form[:reason].value}
1010
type="text"
1111
placeholder="Error"
12-
class="border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
12+
class="border text-sm rounded-lg block p-2.5 bg-gray-700 light:bg-white border-gray-600 light:border-gray-300 placeholder-gray-400 text-white light:text-gray-900 focus:ring-blue-500 focus:border-blue-500"
1313
phx-debounce
1414
/>
1515
<input
1616
name={@search_form[:source_line].name}
1717
value={@search_form[:source_line].value}
1818
type="text"
1919
placeholder="Source line"
20-
class="border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
20+
class="border text-sm rounded-lg block p-2.5 bg-gray-700 light:bg-white border-gray-600 light:border-gray-300 placeholder-gray-400 text-white light:text-gray-900 focus:ring-blue-500 focus:border-blue-500"
2121
phx-debounce
2222
/>
2323
<input
2424
name={@search_form[:source_function].name}
2525
value={@search_form[:source_function].value}
2626
type="text"
2727
placeholder="Source function"
28-
class="border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
28+
class="border text-sm rounded-lg block p-2.5 bg-gray-700 light:bg-white border-gray-600 light:border-gray-300 placeholder-gray-400 text-white light:text-gray-900 focus:ring-blue-500 focus:border-blue-500"
2929
phx-debounce
3030
/>
3131
<select
3232
name={@search_form[:status].name}
33-
class="border text-sm rounded-lg block p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
33+
class="border text-sm rounded-lg block p-2.5 bg-gray-700 light:bg-white border-gray-600 light:border-gray-300 placeholder-gray-400 text-white light:text-gray-900 focus:ring-blue-500 focus:border-blue-500"
3434
>
3535
<option value="" selected={@search_form[:status].value == ""}>All</option>
3636
<option value="unresolved" selected={@search_form[:status].value == "unresolved"}>
@@ -42,9 +42,9 @@
4242
</select>
4343
</.form>
4444

45-
<div class="relative overflow-x-auto shadow-md sm:rounded-lg ring-1 ring-gray-900">
46-
<table class="w-full text-sm text-left rtl:text-right text-gray-400 table-fixed">
47-
<thead class="text-xs uppercase bg-gray-900">
45+
<div class="relative overflow-x-auto shadow-md sm:rounded-lg ring-1 ring-gray-900 light:ring-gray-200">
46+
<table class="w-full text-sm text-left rtl:text-right text-gray-400 light:text-gray-500 table-fixed">
47+
<thead class="text-xs uppercase bg-gray-900 light:bg-gray-100">
4848
<tr>
4949
<th scope="col" class="px-4 pr-2 w-72">Error</th>
5050
<th scope="col" class="px-4 py-3 w-72">Occurrences</th>
@@ -60,9 +60,9 @@
6060
</tr>
6161
<tr
6262
:for={error <- @errors}
63-
class="border-b bg-gray-400/10 border-y border-gray-900 hover:bg-gray-800/60 last-of-type:border-b-0"
63+
class="border-b bg-gray-400/10 light:bg-gray-50 border-y border-gray-900 light:border-gray-200 hover:bg-gray-800/60 light:hover:bg-gray-100 last-of-type:border-b-0"
6464
>
65-
<td scope="row" class="px-4 py-4 font-medium text-white relative">
65+
<td scope="row" class="px-4 py-4 font-medium text-white light:text-gray-900 relative">
6666
<.link navigate={error_path(@socket, error, @search)} class="absolute inset-1">
6767
<span class="sr-only">({sanitize_module(error.kind)}) {error.reason}</span>
6868
</.link>
@@ -71,7 +71,7 @@
7171
</p>
7272
<p
7373
:if={ErrorTracker.Error.has_source_info?(error)}
74-
class="whitespace-nowrap text-ellipsis overflow-hidden font-normal text-gray-400"
74+
class="whitespace-nowrap text-ellipsis overflow-hidden font-normal text-gray-400 light:text-gray-500"
7575
>
7676
{sanitize_module(error.source_function)}
7777
<br />

0 commit comments

Comments
 (0)