Admin Tab Reordering

This commit is contained in:
2026-03-07 19:36:32 -04:00
parent c1d295c232
commit 2eb0d79394
3 changed files with 145 additions and 42 deletions

View File

@@ -2,13 +2,14 @@ import { withPluginApi } from "discourse/lib/plugin-api";
const TABS = [
{
id: "general",
label: "General",
id: "settings",
label: "Settings",
settings: new Set([
"community_landing_enabled",
"logo_dark_url", "logo_light_url", "logo_height", "footer_logo_url",
"accent_color", "accent_hover_color", "dark_bg_color", "light_bg_color",
"scroll_animation"
"scroll_animation", "staggered_reveal_enabled", "dynamic_background_enabled",
"mouse_parallax_enabled", "scroll_progress_enabled"
])
},
{
@@ -26,6 +27,7 @@ const TABS = [
"hero_image_urls", "hero_image_max_height",
"hero_primary_button_label", "hero_primary_button_url",
"hero_secondary_button_label", "hero_secondary_button_url",
"hero_video_url",
"hero_bg_dark", "hero_bg_light", "hero_min_height", "hero_border_style"
])
},
@@ -54,7 +56,7 @@ const TABS = [
id: "topics",
label: "Trending",
settings: new Set([
"topics_enabled", "topics_title", "topics_count",
"topics_enabled", "topics_title", "topics_count", "topics_card_bg_color",
"topics_bg_dark", "topics_bg_light", "topics_min_height", "topics_border_style"
])
},
@@ -97,7 +99,7 @@ const TABS = [
}
];
let currentTab = "general";
let currentTab = "settings";
let filterActive = false;
let isActive = false;
let recheckTimer = null;
@@ -124,9 +126,30 @@ function applyTabFilter() {
);
});
const tabBar = container.querySelector(".cl-admin-tabs");
if (tabBar) {
tabBar.classList.toggle("filter-active", filterActive);
// Update filter-active dimming on whichever tab container exists
const nativeTabs = container.querySelector(".admin-plugin-config-area__tabs");
if (nativeTabs) {
nativeTabs.classList.toggle("cl-filter-active", filterActive);
}
const standaloneTabs = container.querySelector(".cl-admin-tabs");
if (standaloneTabs) {
standaloneTabs.classList.toggle("filter-active", filterActive);
}
}
function updateActiveStates(activeId) {
const container = getContainer();
if (!container) return;
// Update all our injected tabs
container.querySelectorAll(".cl-admin-tab").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.tab === activeId);
});
// Update native Settings link if present
const nativeLink = container.querySelector(".cl-native-settings-link");
if (nativeLink) {
nativeLink.classList.toggle("active", activeId === "settings");
}
}
@@ -141,12 +164,27 @@ function findFilterInput(container) {
return null;
}
function handleTabClick(container, tabId) {
currentTab = tabId;
filterActive = false;
// Clear Discourse filter input so it doesn't conflict
const fi = findFilterInput(container);
if (fi && fi.value) {
fi.value = "";
fi.dispatchEvent(new Event("input", { bubbles: true }));
}
updateActiveStates(tabId);
applyTabFilter();
}
function buildTabsUI() {
const container = getContainer();
if (!container) return false;
// Already injected — just re-apply filter
if (container.querySelector(".cl-admin-tabs")) {
if (container.querySelector(".cl-admin-tab")) {
applyTabFilter();
return true;
}
@@ -160,7 +198,38 @@ function buildTabsUI() {
);
if (!hasOurs) return false;
// Build tab bar
// ── Strategy 1: Inject into native Discourse tab bar ──
const nativeTabsEl = container.querySelector(".admin-plugin-config-area__tabs");
if (nativeTabsEl) {
// Find the native "Settings" link and hook into it
const nativeLink = nativeTabsEl.querySelector("a");
if (nativeLink) {
nativeLink.classList.add("cl-native-settings-link", "active");
nativeLink.addEventListener("click", (e) => {
e.preventDefault();
handleTabClick(container, "settings");
});
}
// Inject our section tabs into the native bar (skip "settings" — native link handles that)
TABS.forEach((tab) => {
if (tab.id === "settings") return;
const btn = document.createElement("button");
btn.className = "cl-admin-tab";
btn.textContent = tab.label;
btn.dataset.tab = tab.id;
btn.addEventListener("click", () => handleTabClick(container, tab.id));
nativeTabsEl.appendChild(btn);
});
nativeTabsEl.classList.add("cl-tabs-injected");
container.classList.add("cl-tabs-active");
applyTabFilter();
return true;
}
// ── Strategy 2 (fallback): Standalone tab bar for older Discourse ──
const tabBar = document.createElement("div");
tabBar.className = "cl-admin-tabs";
@@ -169,31 +238,12 @@ function buildTabsUI() {
btn.className = "cl-admin-tab" + (tab.id === currentTab ? " active" : "");
btn.textContent = tab.label;
btn.dataset.tab = tab.id;
btn.addEventListener("click", () => {
currentTab = tab.id;
filterActive = false;
// Clear Discourse filter input so it doesn't conflict
const fi = findFilterInput(container);
if (fi && fi.value) {
fi.value = "";
fi.dispatchEvent(new Event("input", { bubbles: true }));
}
tabBar
.querySelectorAll(".cl-admin-tab")
.forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
applyTabFilter();
});
btn.addEventListener("click", () => handleTabClick(container, tab.id));
tabBar.appendChild(btn);
});
// ── Insertion strategy: place tabs as high as possible ──
let inserted = false;
// Strategy 1: Top of .admin-plugin-config-area__content (above filter bar)
const contentArea = container.querySelector(
".admin-plugin-config-area__content"
);
@@ -204,7 +254,6 @@ function buildTabsUI() {
inserted = true;
}
// Strategy 2: Before the filter controls
if (!inserted) {
const filterArea = container.querySelector(
".admin-site-settings-filter-controls, .setting-filter"
@@ -215,7 +264,6 @@ function buildTabsUI() {
}
}
// Strategy 3: Before the first setting row (fallback)
if (!inserted) {
allRows[0].parentNode.insertBefore(tabBar, allRows[0]);
}
@@ -269,7 +317,7 @@ export default {
};
tryInject();
// Periodic re-check: re-injects tab bar if Discourse re-renders the DOM
// Periodic re-check: re-injects tabs if Discourse re-renders the DOM
if (!recheckTimer) {
recheckTimer = setInterval(() => {
if (!isActive) {
@@ -278,7 +326,7 @@ export default {
return;
}
const c = getContainer();
if (c && !c.querySelector(".cl-admin-tabs")) {
if (c && !c.querySelector(".cl-admin-tab")) {
buildTabsUI();
}
}, 500);

View File

@@ -9,7 +9,64 @@
display: none !important;
}
/* ── Tab Navigation ── */
/* ── Injected tabs inside native Discourse tab bar ── */
.admin-plugin-config-area__tabs.cl-tabs-injected {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0;
}
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-admin-tab {
padding: 10px 14px;
border: none;
background: none;
color: var(--primary-medium, #888);
font-size: 0.875em;
font-weight: 600;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-admin-tab:hover {
color: var(--primary, #333);
}
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-admin-tab.active {
color: var(--tertiary, #0088cc);
border-bottom-color: var(--tertiary, #0088cc);
}
/* Native "Settings" link — match our tab styling when active/inactive */
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-native-settings-link {
transition: color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
.admin-plugin-config-area__tabs.cl-tabs-injected .cl-native-settings-link:not(.active) {
color: var(--primary-medium, #888);
border-bottom-color: transparent;
}
/* Dimmed state when Discourse filter/search is active */
.admin-plugin-config-area__tabs.cl-filter-active .cl-admin-tab,
.admin-plugin-config-area__tabs.cl-filter-active .cl-native-settings-link {
opacity: 0.4;
}
.admin-plugin-config-area__tabs.cl-filter-active .cl-admin-tab.active,
.admin-plugin-config-area__tabs.cl-filter-active .cl-native-settings-link.active {
border-bottom-color: transparent;
}
/* Dark mode */
html.dark-scheme .admin-plugin-config-area__tabs.cl-tabs-injected .cl-admin-tab:hover {
color: var(--primary, #ddd);
}
/* ── Standalone tab bar (fallback for older Discourse without native tabs) ── */
.cl-admin-tabs {
display: flex;
@@ -20,7 +77,7 @@
border-bottom: 1px solid var(--primary-low, rgba(0, 0, 0, 0.1));
}
.cl-admin-tab {
.cl-admin-tabs .cl-admin-tab {
padding: 10px 14px;
border: none;
background: none;
@@ -33,16 +90,15 @@
transition: color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
}
.cl-admin-tab:hover {
.cl-admin-tabs .cl-admin-tab:hover {
color: var(--primary, #333);
}
.cl-admin-tab.active {
.cl-admin-tabs .cl-admin-tab.active {
color: var(--tertiary, #0088cc);
border-bottom-color: var(--tertiary, #0088cc);
}
/* Dimmed state when Discourse filter/search is active */
.cl-admin-tabs.filter-active .cl-admin-tab {
opacity: 0.4;
}
@@ -51,12 +107,11 @@
border-bottom-color: transparent;
}
/* Dark mode tabs */
html.dark-scheme .cl-admin-tabs {
border-bottom-color: rgba(255, 255, 255, 0.1);
}
html.dark-scheme .cl-admin-tab:hover {
html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover {
color: var(--primary, #ddd);
}

View File

@@ -1,7 +1,7 @@
en:
site_settings:
# ── Master Switch ──
community_landing_enabled: "Enable the branded community landing page. When on, logged-out visitors see a custom welcome page instead of the default Discourse homepage. All sections below are configurable."
community_landing_enabled: "Enable the community landing page."
# ── Branding: Logo ──
logo_dark_url: "━━ BRANDING ━━ — Logo image URL for dark mode. Displayed in the navbar and footer. Leave blank to show the site name as text."