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:
@@ -2,51 +2,51 @@
|
||||
<%= render "shared/form_errors", form: form %>
|
||||
|
||||
<div>
|
||||
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "developers" %>
|
||||
<p class="mt-1 text-sm text-gray-500">Group names are automatically normalized to lowercase.</p>
|
||||
<%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "developers" %>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Group names are automatically normalized to lowercase.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Optional description of this group" %>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Optional description of this group" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :user_ids, "Group Members", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-2 space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-md p-3">
|
||||
<%= form.label :user_ids, "Group Members", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<div class="mt-2 space-y-2 max-h-64 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-md p-3">
|
||||
<% if @available_users.any? %>
|
||||
<% @available_users.each do |user| %>
|
||||
<div class="flex items-center">
|
||||
<%= check_box_tag "group[user_ids][]", user.id, group.users.include?(user), class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= label_tag "group_user_ids_#{user.id}", user.email_address, class: "ml-2 text-sm text-gray-900" %>
|
||||
<%= check_box_tag "group[user_ids][]", user.id, group.users.include?(user), class: "h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= label_tag "group_user_ids_#{user.id}", user.email_address, class: "ml-2 text-sm text-gray-900 dark:text-gray-100" %>
|
||||
<% if user.admin? %>
|
||||
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">Admin</span>
|
||||
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300">Admin</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-500">No users available.</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No users available.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">Select which users should be members of this group.</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Select which users should be members of this group.</p>
|
||||
</div>
|
||||
|
||||
<div data-controller="json-validator" data-json-validator-valid-class="border-green-500 focus:border-green-500 focus:ring-green-500" data-json-validator-invalid-class="border-red-500 focus:border-red-500 focus:ring-red-500" data-json-validator-valid-status-class="text-green-600" data-json-validator-invalid-status-class="text-red-600">
|
||||
<%= form.label :custom_claims, "Custom Claims (JSON)", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.label :custom_claims, "Custom Claims (JSON)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<%= form.text_area :custom_claims, value: (group.custom_claims.present? ? JSON.pretty_generate(group.custom_claims) : ""), rows: 8,
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono",
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono",
|
||||
placeholder: '{"roles": ["admin", "editor"]}',
|
||||
data: {
|
||||
action: "input->json-validator#validate blur->json-validator#format",
|
||||
json_validator_target: "textarea"
|
||||
} %>
|
||||
<div class="mt-2 text-sm text-gray-600 space-y-1">
|
||||
<div class="mt-2 text-sm text-gray-600 dark:text-gray-400 space-y-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<p>Optional: Custom claims to add to OIDC tokens for all members. These will be merged with user-level claims.</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<button type="button" data-action="json-validator#format" class="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded">Format JSON</button>
|
||||
<button type="button" data-action="json-validator#insertSample" data-json-sample='{"roles": ["admin", "editor"], "permissions": ["read", "write"], "team": "backend"}' class="text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 px-2 py-1 rounded">Insert Example</button>
|
||||
<button type="button" data-action="json-validator#format" class="text-xs bg-gray-100 dark:bg-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 px-2 py-1 rounded">Format JSON</button>
|
||||
<button type="button" data-action="json-validator#insertSample" data-json-sample='{"roles": ["admin", "editor"], "permissions": ["read", "write"], "team": "backend"}' class="text-xs bg-blue-100 dark:bg-blue-900/50 hover:bg-blue-200 dark:hover:bg-blue-900 text-blue-700 dark:text-blue-300 px-2 py-1 rounded">Insert Example</button>
|
||||
</div>
|
||||
</div>
|
||||
<div data-json-validator-target="status" class="text-xs font-medium"></div>
|
||||
@@ -55,6 +55,6 @@
|
||||
|
||||
<div class="flex gap-3">
|
||||
<%= form.submit group.persisted? ? "Update Group" : "Create Group", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
<%= link_to "Cancel", admin_groups_path, class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
||||
<%= link_to "Cancel", admin_groups_path, class: "rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Group</h1>
|
||||
<p class="text-sm text-gray-600 mb-6">Editing: <%= @group.name %></p>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-6">Edit Group</h1>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Editing: <%= @group.name %></p>
|
||||
<%= render "form", group: @group %>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Groups</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">Organize users into groups for application access control.</p>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Groups</h1>
|
||||
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">Organize users into groups for application access control.</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<%= link_to "New Group", new_admin_group_path, class: "block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
@@ -11,31 +11,31 @@
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Name</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Description</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Members</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Applications</th>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-0">Name</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Description</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Members</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Applications</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<% @groups.each do |group| %>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-0">
|
||||
<%= link_to group.name, admin_group_path(group), class: "text-blue-600 hover:text-blue-900" %>
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-gray-500">
|
||||
<%= truncate(group.description, length: 80) || content_tag(:span, "No description", class: "text-gray-400") %>
|
||||
<td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= truncate(group.description, length: 80) || content_tag(:span, "No description", class: "text-gray-400 dark:text-gray-500") %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= pluralize(group.users.count, "member") %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= pluralize(group.applications.count, "app") %>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-6">New Group</h1>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-6">New Group</h1>
|
||||
<%= render "form", group: @group %>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<div class="mb-6">
|
||||
<div class="sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900"><%= @group.name %></h1>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100"><%= @group.name %></h1>
|
||||
<% if @group.description.present? %>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= @group.description %></p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= @group.description %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 flex gap-3">
|
||||
<%= link_to "Edit", edit_admin_group_path(@group), class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
||||
<%= link_to "Edit", edit_admin_group_path(@group), class: "rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600" %>
|
||||
<%= button_to "Delete", admin_group_path(@group), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500" %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -15,25 +15,25 @@
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Members -->
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100 mb-4">
|
||||
Members (<%= @members.count %>)
|
||||
</h3>
|
||||
<% if @members.any? %>
|
||||
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-md">
|
||||
<% @members.each do |user| %>
|
||||
<li class="px-4 py-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900"><%= user.email_address %></p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= user.email_address %></p>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<% if user.admin? %>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">Admin</span>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300">Admin</span>
|
||||
<% end %>
|
||||
<% if user.totp_enabled? %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">2FA</span>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300">2FA</span>
|
||||
<% end %>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700"><%= user.status.titleize %></span>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300"><%= user.status.titleize %></span>
|
||||
</div>
|
||||
</div>
|
||||
<%= link_to "View", admin_user_path(user), class: "text-blue-600 hover:text-blue-900 text-sm" %>
|
||||
@@ -41,36 +41,36 @@
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<div class="rounded-md bg-gray-50 p-4">
|
||||
<p class="text-sm text-gray-500">No members in this group yet.</p>
|
||||
<div class="rounded-md bg-gray-50 dark:bg-gray-700 p-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">No members in this group yet.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Applications -->
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100 mb-4">
|
||||
Assigned Applications (<%= @applications.count %>)
|
||||
</h3>
|
||||
<% if @applications.any? %>
|
||||
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md">
|
||||
<ul class="divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-md">
|
||||
<% @applications.each do |app| %>
|
||||
<li class="px-4 py-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900"><%= app.name %></p>
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= app.name %></p>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<% case app.app_type %>
|
||||
<% when "oidc" %>
|
||||
<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700">OIDC</span>
|
||||
<span class="inline-flex items-center rounded-full bg-purple-100 dark:bg-purple-900/50 px-2 py-0.5 text-xs font-medium text-purple-700 dark:text-purple-300">OIDC</span>
|
||||
<% when "trusted_header" %>
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-medium text-indigo-700">ForwardAuth</span>
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 dark:bg-indigo-900/50 px-2 py-0.5 text-xs font-medium text-indigo-700 dark:text-indigo-300">ForwardAuth</span>
|
||||
<% end %>
|
||||
<% if app.active? %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">Active</span>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300">Active</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700">Inactive</span>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300">Inactive</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -79,8 +79,8 @@
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<div class="rounded-md bg-gray-50 p-4">
|
||||
<p class="text-sm text-gray-500">This group is not assigned to any applications.</p>
|
||||
<div class="rounded-md bg-gray-50 dark:bg-gray-700 p-4">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">This group is not assigned to any applications.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user