Add dark mode with toggle and localStorage persistence

Uses Tailwind v4 class-based dark mode with a Stimulus controller for
toggling. Respects prefers-color-scheme as default, prevents FOUC with
an inline script, and persists the user's choice in localStorage. All
views updated with dark: variants for backgrounds, text, borders,
badges, buttons, and form inputs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dan Milne
2026-03-22 00:37:58 +11:00
parent 43958f50ce
commit 3d98261a51
38 changed files with 744 additions and 636 deletions

View File

@@ -1,8 +1,8 @@
<div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">
<h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
Welcome, <%= @user.email_address %>
</h1>
<p class="mt-2 text-gray-600">
<p class="mt-2 text-gray-600 dark:text-gray-400">
<% if @user.admin? %>
Administrator
<% else %>
@@ -13,34 +13,34 @@
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<!-- Active Sessions Card -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Active Sessions
</dt>
<dd class="text-lg font-semibold text-gray-900">
<dd class="text-lg font-semibold text-gray-900 dark:text-gray-100">
<%= @user.sessions.active.count %>
</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "View all sessions", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div>
</div>
<% if @user.totp_enabled? %>
<!-- 2FA Status Card -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
@@ -50,7 +50,7 @@
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Two-Factor Authentication
</dt>
<dd class="text-lg font-semibold text-green-600">
@@ -60,13 +60,13 @@
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Manage 2FA", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div>
</div>
<% else %>
<!-- 2FA Disabled Card -->
<div class="bg-white overflow-hidden shadow rounded-lg border-2 border-yellow-200">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg border-2 border-yellow-200">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
@@ -76,7 +76,7 @@
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Two-Factor Authentication
</dt>
<dd class="text-lg font-semibold text-yellow-600">
@@ -86,34 +86,34 @@
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Enable 2FA", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div>
</div>
<% end %>
<!-- API Keys Card -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
</svg>
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
API Keys
</dt>
<dd class="text-lg font-semibold text-gray-900">
<dd class="text-lg font-semibold text-gray-900 dark:text-gray-100">
<%= @user.api_keys.active.count %>
</dd>
</dl>
</div>
</div>
</div>
<div class="bg-gray-50 px-5 py-3">
<div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Manage API Keys", api_keys_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div>
</div>
@@ -121,39 +121,39 @@
<!-- Your Applications Section -->
<div class="mt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Your Applications</h2>
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Your Applications</h2>
<% if @applications.any? %>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<% @applications.each do |app| %>
<div class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition">
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md transition">
<div class="p-6">
<div class="flex items-start gap-3 mb-4">
<% if app.icon.attached? %>
<%= image_tag app.icon, class: "h-12 w-12 rounded-lg object-cover border border-gray-200 shrink-0", alt: "#{app.name} icon" %>
<%= image_tag app.icon, class: "h-12 w-12 rounded-lg object-cover border border-gray-200 dark:border-gray-700 shrink-0", alt: "#{app.name} icon" %>
<% else %>
<div class="h-12 w-12 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="h-12 w-12 rounded-lg bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-700 flex items-center justify-center shrink-0">
<svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<% end %>
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between">
<h3 class="text-lg font-semibold text-gray-900 truncate">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 truncate">
<%= app.name %>
</h3>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium shrink-0
<% if app.oidc? %>
bg-blue-100 text-blue-800
bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200
<% else %>
bg-green-100 text-green-800
bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200
<% end %>">
<%= app.app_type.humanize %>
</span>
</div>
<% if app.description.present? %>
<p class="text-sm text-gray-600 mt-1 line-clamp-2">
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
<%= app.description %>
</p>
<% end %>
@@ -165,30 +165,40 @@
<%= link_to "Open Application", app.landing_url,
target: "_blank",
rel: "noopener noreferrer",
class: "w-full flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition" %>
class: "w-full flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-blue-500 transition" %>
<% else %>
<div class="text-sm text-gray-500 italic">
<div class="text-sm text-gray-500 dark:text-gray-400 italic">
No landing URL configured
</div>
<% end %>
<% if app.user_has_active_session?(@user) %>
<%= button_to "Require Re-Auth", logout_from_app_active_sessions_path(application_id: app.id), method: :delete,
class: "w-full flex justify-center items-center px-4 py-2 border border-orange-300 text-sm font-medium rounded-md text-orange-700 bg-white hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 transition",
class: "w-full flex justify-center items-center px-4 py-2 border border-orange-300 text-sm font-medium rounded-md text-orange-700 bg-white dark:bg-gray-700 dark:ring-gray-600 dark:text-gray-200 hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-orange-500 transition",
form: { data: { turbo_confirm: "This will revoke #{app.name}'s access tokens. The next time #{app.name} needs to authenticate, you'll sign in again (no re-authorization needed). Continue?" } } %>
<% end %>
<% if @user.admin? %>
<div class="flex gap-2 mt-1">
<%= link_to "View", admin_application_path(app),
class: "text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-blue-600 transition" %>
<span class="text-gray-300 dark:text-gray-600">|</span>
<%= link_to "Edit", edit_admin_application_path(app),
class: "text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-blue-600 transition" %>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="bg-gray-50 rounded-lg border border-gray-200 p-8 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-8 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No applications available</h3>
<p class="mt-2 text-sm text-gray-500">
<h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">No applications available</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
You don't have access to any applications yet. Contact your administrator if you think this is an error.
</p>
</div>
@@ -197,21 +207,21 @@
<% if @user.admin? %>
<div class="mt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Admin Quick Actions</h2>
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Admin Quick Actions</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<%= link_to admin_users_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Manage Users</h3>
<p class="text-sm text-gray-600">View, edit, and invite users</p>
<%= link_to admin_users_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Manage Users</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">View, edit, and invite users</p>
<% end %>
<%= link_to admin_applications_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Manage Applications</h3>
<p class="text-sm text-gray-600">Register and configure applications</p>
<%= link_to admin_applications_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Manage Applications</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Register and configure applications</p>
<% end %>
<%= link_to admin_groups_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Manage Groups</h3>
<p class="text-sm text-gray-600">Create and organize user groups</p>
<%= link_to admin_groups_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Manage Groups</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">Create and organize user groups</p>
<% end %>
</div>
</div>