Settings Navigation Repositioned

This commit is contained in:
2026-03-06 22:02:23 -04:00
parent af75ea54a6
commit 8ce1c36bb9
10 changed files with 321 additions and 195 deletions

View File

@@ -153,90 +153,7 @@
} }
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
// 6. SMOOTH HORIZONTAL DRAG — trending discussions // 6. APP BADGE DETECTION
// Prevents link clicks during drag, smooth momentum scrolling
// ═══════════════════════════════════════════════════════════════════
$$(".cl-topics__scroll").forEach(function (scrollEl) {
var isDown = false;
var startX = 0;
var scrollStart = 0;
var moved = false;
var velX = 0;
var lastX = 0;
var lastTime = 0;
var momentumId = null;
function stopMomentum() {
if (momentumId) {
cancelAnimationFrame(momentumId);
momentumId = null;
}
}
function doMomentum() {
if (Math.abs(velX) < 0.5) return;
scrollEl.scrollLeft -= velX;
velX *= 0.92;
momentumId = requestAnimationFrame(doMomentum);
}
scrollEl.addEventListener("mousedown", function (ev) {
stopMomentum();
isDown = true;
moved = false;
startX = ev.pageX;
scrollStart = scrollEl.scrollLeft;
lastX = ev.pageX;
lastTime = Date.now();
velX = 0;
scrollEl.style.scrollSnapType = "none";
scrollEl.style.userSelect = "none";
});
window.addEventListener("mousemove", function (ev) {
if (!isDown) return;
var dx = ev.pageX - startX;
if (Math.abs(dx) > 3) moved = true;
var now = Date.now();
var dt = now - lastTime;
if (dt > 0) {
velX = (ev.pageX - lastX) / dt * 16;
}
lastX = ev.pageX;
lastTime = now;
scrollEl.scrollLeft = scrollStart - dx;
});
function endDrag() {
if (!isDown) return;
isDown = false;
scrollEl.style.userSelect = "";
if (Math.abs(velX) > 1) {
doMomentum();
}
// Re-enable snap after a tick
setTimeout(function () {
scrollEl.style.scrollSnapType = "";
}, 100);
}
window.addEventListener("mouseup", endDrag);
scrollEl.addEventListener("mouseleave", endDrag);
// Prevent link navigation if we dragged
scrollEl.addEventListener("click", function (ev) {
if (moved) {
ev.preventDefault();
ev.stopPropagation();
moved = false;
}
}, true);
// Touch support — native scrolling works, no extra handling needed
});
// ═══════════════════════════════════════════════════════════════════
// 7. APP BADGE DETECTION
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
var ua = navigator.userAgent || ""; var ua = navigator.userAgent || "";
if (/iPhone|iPad|iPod/.test(ua)) $$(".cl-app-badge--ios").forEach(function (e) { e.classList.add("highlighted"); }); if (/iPhone|iPad|iPod/.test(ua)) $$(".cl-app-badge--ios").forEach(function (e) { e.classList.add("highlighted"); });

View File

@@ -33,7 +33,7 @@ const TABS = [
id: "stats", id: "stats",
label: "Stats", label: "Stats",
settings: new Set([ settings: new Set([
"stats_title", "stat_icon_color", "stats_title", "stat_icon_color", "stat_icon_bg_color", "stat_icon_shape", "stat_counter_color",
"stat_members_label", "stat_topics_label", "stat_posts_label", "stat_members_label", "stat_topics_label", "stat_posts_label",
"stat_likes_label", "stat_chats_label", "stat_likes_label", "stat_chats_label",
"stats_bg_dark", "stats_bg_light", "stats_min_height", "stats_border_style" "stats_bg_dark", "stats_bg_light", "stats_min_height", "stats_border_style"
@@ -70,7 +70,7 @@ const TABS = [
id: "groups", id: "groups",
label: "Spaces", label: "Spaces",
settings: new Set([ settings: new Set([
"groups_enabled", "groups_title", "groups_count", "groups_enabled", "groups_title", "groups_count", "groups_selected", "groups_card_bg_color",
"groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style" "groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style"
]) ])
}, },
@@ -94,46 +94,73 @@ const TABS = [
"footer_description", "footer_text", "footer_links", "footer_description", "footer_text", "footer_links",
"footer_bg_dark", "footer_bg_light", "footer_border_style" "footer_bg_dark", "footer_bg_light", "footer_border_style"
]) ])
},
{
id: "css",
label: "Custom CSS",
settings: new Set(["custom_css"])
} }
]; ];
let currentTab = "general"; let currentTab = "general";
let filterActive = false;
let isActive = false;
let recheckTimer = null;
function getContainer() {
return (
document.querySelector(".admin-plugin-config-area") ||
document.querySelector(".admin-detail")
);
}
function applyTabFilter() {
const container = getContainer();
if (!container) return;
function applyTabFilter(container) {
const tab = TABS.find((t) => t.id === currentTab); const tab = TABS.find((t) => t.id === currentTab);
if (!tab) return; if (!tab) return;
container.querySelectorAll(".row.setting[data-setting]").forEach((row) => { container.querySelectorAll(".row.setting[data-setting]").forEach((row) => {
const name = row.getAttribute("data-setting"); const name = row.getAttribute("data-setting");
row.style.display = tab.settings.has(name) ? "" : "none"; row.classList.toggle(
"cl-tab-hidden",
!filterActive && !tab.settings.has(name)
);
}); });
const tabBar = container.querySelector(".cl-admin-tabs");
if (tabBar) {
tabBar.classList.toggle("filter-active", filterActive);
}
}
function findFilterInput(container) {
for (const input of container.querySelectorAll("input")) {
if (input.closest(".row.setting") || input.closest(".cl-admin-tabs")) {
continue;
}
const t = (input.type || "text").toLowerCase();
if (t === "text" || t === "search") return input;
}
return null;
} }
function buildTabsUI() { function buildTabsUI() {
const container = const container = getContainer();
document.querySelector(".admin-plugin-config-area") ||
document.querySelector(".admin-detail");
if (!container) return false; if (!container) return false;
// Already injected? // Already injected — just re-apply filter
if (container.querySelector(".cl-admin-tabs")) return true; if (container.querySelector(".cl-admin-tabs")) {
applyTabFilter();
return true;
}
const allRows = container.querySelectorAll(".row.setting[data-setting]"); const allRows = container.querySelectorAll(".row.setting[data-setting]");
if (allRows.length < 5) return false; if (allRows.length < 5) return false;
// Verify our settings are present // Verify our plugin settings are present
const firstTab = TABS[0];
const hasOurs = Array.from(allRows).some((row) => const hasOurs = Array.from(allRows).some((row) =>
firstTab.settings.has(row.getAttribute("data-setting")) TABS[0].settings.has(row.getAttribute("data-setting"))
); );
if (!hasOurs) return false; if (!hasOurs) return false;
// Create tab bar // Build tab bar
const tabBar = document.createElement("div"); const tabBar = document.createElement("div");
tabBar.className = "cl-admin-tabs"; tabBar.className = "cl-admin-tabs";
@@ -141,30 +168,85 @@ function buildTabsUI() {
const btn = document.createElement("button"); const btn = document.createElement("button");
btn.className = "cl-admin-tab" + (tab.id === currentTab ? " active" : ""); btn.className = "cl-admin-tab" + (tab.id === currentTab ? " active" : "");
btn.textContent = tab.label; btn.textContent = tab.label;
btn.setAttribute("data-tab", tab.id); btn.dataset.tab = tab.id;
btn.addEventListener("click", () => { btn.addEventListener("click", () => {
currentTab = tab.id; 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 tabBar
.querySelectorAll(".cl-admin-tab") .querySelectorAll(".cl-admin-tab")
.forEach((b) => b.classList.remove("active")); .forEach((b) => b.classList.remove("active"));
btn.classList.add("active"); btn.classList.add("active");
applyTabFilter(container); applyTabFilter();
}); });
tabBar.appendChild(btn); tabBar.appendChild(btn);
}); });
// Insert tab bar before the first setting row // ── Insertion strategy: place tabs as high as possible ──
const settingsParent = allRows[0].parentNode;
settingsParent.insertBefore(tabBar, allRows[0]); let inserted = false;
// Strategy 1: Top of .admin-plugin-config-area__content (above filter bar)
const contentArea = container.querySelector(
".admin-plugin-config-area__content"
);
if (contentArea) {
const form = contentArea.querySelector("form");
const target = form || contentArea;
target.insertBefore(tabBar, target.firstChild);
inserted = true;
}
// Strategy 2: Before the filter controls
if (!inserted) {
const filterArea = container.querySelector(
".admin-site-settings-filter-controls, .setting-filter"
);
if (filterArea) {
filterArea.parentNode.insertBefore(tabBar, filterArea);
inserted = true;
}
}
// Strategy 3: Before the first setting row (fallback)
if (!inserted) {
allRows[0].parentNode.insertBefore(tabBar, allRows[0]);
}
// Add class to disable separator borders
container.classList.add("cl-tabs-active"); container.classList.add("cl-tabs-active");
applyTabFilter();
// Apply initial filter
applyTabFilter(container);
return true; return true;
} }
// ── Global filter detection via event delegation ──
// This survives DOM re-renders because it's on document, not on a specific input
document.addEventListener(
"input",
(e) => {
if (!isActive) return;
const t = e.target;
if (!t || !t.closest) return;
if (t.closest(".row.setting") || t.closest(".cl-admin-tabs")) return;
const container = getContainer();
if (!container || !container.contains(t)) return;
const hasText = t.value.trim().length > 0;
if (hasText !== filterActive) {
filterActive = hasText;
applyTabFilter();
}
},
true
);
export default { export default {
name: "community-landing-admin-tabs", name: "community-landing-admin-tabs",
@@ -175,6 +257,10 @@ export default {
url.includes("community-landing") || url.includes("community-landing") ||
url.includes("community_landing") url.includes("community_landing")
) { ) {
isActive = true;
filterActive = false;
// Initial injection with retries
let attempts = 0; let attempts = 0;
const tryInject = () => { const tryInject = () => {
if (buildTabsUI() || attempts > 15) return; if (buildTabsUI() || attempts > 15) return;
@@ -182,6 +268,28 @@ export default {
setTimeout(tryInject, 200); setTimeout(tryInject, 200);
}; };
tryInject(); tryInject();
// Periodic re-check: re-injects tab bar if Discourse re-renders the DOM
if (!recheckTimer) {
recheckTimer = setInterval(() => {
if (!isActive) {
clearInterval(recheckTimer);
recheckTimer = null;
return;
}
const c = getContainer();
if (c && !c.querySelector(".cl-admin-tabs")) {
buildTabsUI();
}
}, 500);
}
} else {
// Left plugin settings page — clean up
isActive = false;
if (recheckTimer) {
clearInterval(recheckTimer);
recheckTimer = null;
}
} }
}); });
}); });

View File

@@ -3,13 +3,19 @@
Tab navigation + fallback separators Tab navigation + fallback separators
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
/* ── Tab-hidden class (used instead of inline display:none) ── */
.cl-tab-hidden {
display: none !important;
}
/* ── Tab Navigation ── */ /* ── Tab Navigation ── */
.cl-admin-tabs { .cl-admin-tabs {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0; gap: 0;
margin: 0 0 24px 0; margin: 0 0 20px 0;
padding: 0; padding: 0;
border-bottom: 1px solid var(--primary-low, rgba(0, 0, 0, 0.1)); border-bottom: 1px solid var(--primary-low, rgba(0, 0, 0, 0.1));
} }
@@ -24,7 +30,7 @@
cursor: pointer; cursor: pointer;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
margin-bottom: -1px; margin-bottom: -1px;
transition: color 0.15s ease, border-color 0.15s ease; transition: color 0.15s ease, border-color 0.15s ease, opacity 0.15s ease;
} }
.cl-admin-tab:hover { .cl-admin-tab:hover {
@@ -36,6 +42,15 @@
border-bottom-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;
}
.cl-admin-tabs.filter-active .cl-admin-tab.active {
border-bottom-color: transparent;
}
/* Dark mode tabs */ /* Dark mode tabs */
html.dark-scheme .cl-admin-tabs { html.dark-scheme .cl-admin-tabs {
border-bottom-color: rgba(255, 255, 255, 0.1); border-bottom-color: rgba(255, 255, 255, 0.1);
@@ -75,8 +90,7 @@ html.dark-scheme .cl-admin-tab:hover {
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="ios_"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="ios_"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="android_"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="android_"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="app_"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="app_"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="footer_"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="footer_"] {
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="custom_css"] {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -92,8 +106,7 @@ html.dark-scheme .cl-admin-tab:hover {
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="contributors_enabled"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="contributors_enabled"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="groups_enabled"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="groups_enabled"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="show_app_ctas"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="show_app_ctas"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="footer_description"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="footer_description"] {
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="custom_css"] {
border-top: 2px solid rgba(0, 0, 0, 0.12); border-top: 2px solid rgba(0, 0, 0, 0.12);
margin-top: 28px; margin-top: 28px;
padding-top: 24px; padding-top: 24px;

View File

@@ -329,7 +329,7 @@
.cl-hero__actions { display: flex; flex-wrap: wrap; gap: 0.6rem; } .cl-hero__actions { display: flex; flex-wrap: wrap; gap: 0.6rem; }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
3. PREMIUM STATS — icon + label on one line, counter below 3. PREMIUM STATS — icon with bg, label, animated counter
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
.cl-stats { padding: 2.5rem 0 2rem; } .cl-stats { padding: 2.5rem 0 2rem; }
@@ -340,7 +340,7 @@
@media (min-width: 1024px) { .cl-stats__grid { grid-template-columns: repeat(5, 1fr); } } @media (min-width: 1024px) { .cl-stats__grid { grid-template-columns: repeat(5, 1fr); } }
.cl-stat-card { .cl-stat-card {
display: flex; flex-direction: column; align-items: center; gap: 0.5rem; display: flex; flex-direction: column; align-items: center; gap: 0.6rem;
padding: 1.5rem 1rem; padding: 1.5rem 1rem;
background: var(--cl-card); border: 1px solid var(--cl-border); background: var(--cl-card); border: 1px solid var(--cl-border);
border-radius: var(--cl-radius); text-align: center; border-radius: var(--cl-radius); text-align: center;
@@ -348,42 +348,74 @@
} }
.cl-stat-card:hover { border-color: var(--cl-border-hover); transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); } .cl-stat-card:hover { border-color: var(--cl-border-hover); transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
/* Icon + Label on same line */ /* Icon with background */
.cl-stat-card__top { .cl-stat-card__icon-wrap {
display: flex; align-items: center; gap: 0.4rem; display: flex; align-items: center; justify-content: center;
width: 48px; height: 48px;
background: var(--cl-stat-icon-bg);
color: var(--cl-stat-icon-color);
transition: transform 0.2s ease;
} }
.cl-stat-card__icon { color: var(--cl-stat-icon-color); display: flex; align-items: center; } .cl-stat-card:hover .cl-stat-card__icon-wrap { transform: scale(1.08); }
.cl-stat-card__icon svg { width: 22px; height: 22px; } .cl-stat-card__icon-wrap svg { width: 24px; height: 24px; }
.cl-stat-icon--circle { border-radius: 50%; }
.cl-stat-icon--rounded { border-radius: 12px; }
.cl-stat-card__label { .cl-stat-card__label {
font-size: 0.78rem; color: var(--cl-muted); font-size: 0.78rem; color: var(--cl-muted);
font-weight: 600; font-weight: 600;
} }
/* Counter below */ /* Counter */
.cl-stat-card__value { .cl-stat-card__value {
font-size: 1.75rem; font-weight: 800; font-size: 1.75rem; font-weight: 800;
color: var(--cl-text-strong); letter-spacing: -0.02em; color: var(--cl-stat-counter-color); letter-spacing: -0.02em;
line-height: 1; line-height: 1;
} }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
4. ABOUT COMMUNITY — full-width gradient card 4. ABOUT — split layout: image left on gradient, text right
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
.cl-about { padding: 1rem 0 2rem; } .cl-about { padding: 1.5rem 0 2rem; }
.cl-about__card { .cl-about__card {
background: var(--cl-about-gradient); background: var(--cl-about-gradient);
border: 1px solid var(--cl-border); border: 1px solid var(--cl-border);
border-radius: 20px; border-radius: 20px;
padding: 2rem 2.5rem; overflow: hidden;
position: relative;
box-shadow: 0 2px 16px rgba(0,0,0,0.04); box-shadow: 0 2px 16px rgba(0,0,0,0.04);
background-size: cover; background-position: center; background-size: cover; background-position: center;
display: flex; flex-direction: column;
}
@media (min-width: 768px) {
.cl-about__card { flex-direction: row; min-height: 340px; }
}
/* Left side — image on gradient */
.cl-about__left {
display: flex; align-items: center; justify-content: center;
padding: 2rem;
position: relative;
}
@media (min-width: 768px) {
.cl-about__left { flex: 0 0 40%; max-width: 40%; }
}
.cl-about__image {
width: 100%; max-width: 280px; height: auto;
border-radius: 16px; object-fit: cover;
box-shadow: 0 8px 32px rgba(0,0,0,0.15);
position: relative; z-index: 1;
}
/* Right side — text content */
.cl-about__right {
flex: 1; padding: 2rem 2.5rem;
display: flex; flex-direction: column; justify-content: center;
} }
.cl-about__heading { .cl-about__heading {
font-size: 1.35rem; font-weight: 800; color: var(--cl-text-strong); font-size: 1.35rem; font-weight: 800; color: var(--cl-text-strong);
margin: 0 0 1rem; margin: 0 0 0.75rem;
} }
.cl-about__quote-mark { .cl-about__quote-mark {
@@ -393,43 +425,32 @@
.cl-about__body { .cl-about__body {
color: var(--cl-text); font-size: 0.95rem; line-height: 1.75; color: var(--cl-text); font-size: 0.95rem; line-height: 1.75;
margin-bottom: 1rem; max-width: 800px; margin-bottom: 1rem;
} }
.cl-about__body p { margin: 0 0 0.75rem; } .cl-about__body p { margin: 0 0 0.75rem; }
.cl-about__body a { color: var(--cl-accent); } .cl-about__body a { color: var(--cl-accent); }
.cl-about__meta { .cl-about__meta {
display: flex; align-items: center; gap: 0.75rem; display: flex; align-items: center; gap: 0.75rem;
padding-top: 1rem; border-top: 1px solid var(--cl-border); padding-top: 1rem; border-top: 1px solid rgba(0,0,0,0.08);
}
.cl-about__avatar {
width: 44px; height: 44px; border-radius: 50%; object-fit: cover;
border: 2px solid var(--cl-border);
} }
.cl-about__meta-text { display: flex; flex-direction: column; } .cl-about__meta-text { display: flex; flex-direction: column; }
.cl-about__author { font-size: 0.88rem; font-weight: 600; color: var(--cl-text-strong); } .cl-about__author { font-size: 0.88rem; font-weight: 600; color: var(--cl-text-strong); }
.cl-about__role { font-size: 0.78rem; color: var(--cl-muted); } .cl-about__role { font-size: 0.78rem; color: var(--cl-muted); }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
5. TRENDING DISCUSSIONS — horizontal scrollable cards 5. TRENDING DISCUSSIONS — 4-per-row grid
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
.cl-topics { padding: 1.5rem 0 2rem; } .cl-topics { padding: 1.5rem 0 2rem; }
.cl-topics__scroll { .cl-topics__grid {
display: flex; gap: 0.75rem; display: grid; grid-template-columns: repeat(1, 1fr); gap: 0.75rem;
overflow-x: auto; overflow-y: hidden;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
padding-bottom: 0.75rem;
scrollbar-width: none;
cursor: grab;
} }
.cl-topics__scroll::-webkit-scrollbar { display: none; } @media (min-width: 480px) { .cl-topics__grid { grid-template-columns: repeat(2, 1fr); } }
.cl-topics__scroll:active { cursor: grabbing; } @media (min-width: 768px) { .cl-topics__grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1024px) { .cl-topics__grid { grid-template-columns: repeat(4, 1fr); } }
.cl-topic-card { .cl-topic-card {
flex: 0 0 230px;
scroll-snap-align: start;
display: flex; flex-direction: column; display: flex; flex-direction: column;
padding: 1.15rem 1.25rem; padding: 1.15rem 1.25rem;
background: var(--cl-card); background: var(--cl-card);
@@ -439,7 +460,6 @@
transition: all 0.2s ease; transition: all 0.2s ease;
min-height: 145px; min-height: 145px;
} }
@media (min-width: 640px) { .cl-topic-card { flex: 0 0 250px; } }
.cl-topic-card:hover { border-color: var(--cl-border-hover); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.08); } .cl-topic-card:hover { border-color: var(--cl-border-hover); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.08); }
.cl-topic-card__cat { .cl-topic-card__cat {
@@ -500,7 +520,7 @@
} }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
7. COMMUNITY SPACES — large colored icon cards 7. COMMUNITY SPACES — colored header cards
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
.cl-spaces { padding: 1.5rem 0 2rem; } .cl-spaces { padding: 1.5rem 0 2rem; }
@@ -512,31 +532,50 @@
@media (min-width: 1024px) { .cl-spaces__grid { grid-template-columns: repeat(5, 1fr); } } @media (min-width: 1024px) { .cl-spaces__grid { grid-template-columns: repeat(5, 1fr); } }
.cl-space-card { .cl-space-card {
display: flex; flex-direction: column; align-items: center; gap: 0.6rem; display: flex; flex-direction: column;
padding: 1.5rem 1rem 1.25rem; background: var(--cl-space-card-bg); border: 1px solid var(--cl-border);
background: var(--cl-card); border: 1px solid var(--cl-border);
border-radius: var(--cl-radius); border-radius: var(--cl-radius);
text-decoration: none; text-align: center; text-decoration: none; overflow: hidden;
transition: all 0.25s ease; transition: all 0.25s ease;
} }
.cl-space-card:hover { border-color: var(--cl-border-hover); transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); } .cl-space-card:hover { border-color: var(--cl-border-hover); transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
/* Colored header band */
.cl-space-card__header {
background: var(--space-color);
padding: 1.25rem 1rem;
display: flex; align-items: center; justify-content: center;
position: relative;
}
.cl-space-card__header::after {
content: ""; position: absolute; inset: 0;
background: linear-gradient(180deg, rgba(255,255,255,0.1), rgba(0,0,0,0.05));
pointer-events: none;
}
.cl-space-card__icon { .cl-space-card__icon {
width: 64px; height: 64px; border-radius: 16px; width: 52px; height: 52px; border-radius: 14px;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.15); background: rgba(255,255,255,0.2);
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
position: relative; z-index: 1;
} }
.cl-space-card__icon img { width: 100%; height: 100%; object-fit: cover; } .cl-space-card__icon img { width: 100%; height: 100%; object-fit: cover; }
.cl-space-card__letter { .cl-space-card__letter {
font-size: 1.6rem; font-weight: 800; color: #fff; font-size: 1.4rem; font-weight: 800; color: #fff;
line-height: 1; text-shadow: 0 1px 2px rgba(0,0,0,0.2); line-height: 1; text-shadow: 0 1px 2px rgba(0,0,0,0.2);
} }
/* Card body */
.cl-space-card__body {
padding: 0.85rem 1rem;
display: flex; flex-direction: column; gap: 0.15rem;
}
.cl-space-card__name { .cl-space-card__name {
font-size: 0.85rem; font-weight: 700; color: var(--cl-text-strong); font-size: 0.82rem; font-weight: 700; color: var(--cl-text-strong);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
} }
.cl-space-card__sub { font-size: 0.72rem; color: var(--cl-muted); font-weight: 500; } .cl-space-card__sub { font-size: 0.72rem; color: var(--cl-muted); font-weight: 500; }

View File

@@ -43,6 +43,9 @@ en:
# ── 3. Stats Section ── # ── 3. Stats Section ──
stats_title: "━━ ROW 3: PREMIUM STATS ━━ — Full-width row of live community statistics (members, topics, posts, likes, chats) with animated counters and icons. Each card shows icon + label on one line with the counter below. This is the section heading." stats_title: "━━ ROW 3: PREMIUM STATS ━━ — Full-width row of live community statistics (members, topics, posts, likes, chats) with animated counters and icons. Each card shows icon + label on one line with the counter below. This is the section heading."
stat_icon_color: "Color for all stat counter icons. Hex value (e.g. #d4a24e)." stat_icon_color: "Color for all stat counter icons. Hex value (e.g. #d4a24e)."
stat_icon_bg_color: "Background color behind each stat icon. Leave blank for a subtle accent tint."
stat_icon_shape: "Shape of the icon background: circle or rounded square."
stat_counter_color: "Color for the stat counter numbers. Leave blank for default text color."
stat_members_label: "Custom label for the Members stat card." stat_members_label: "Custom label for the Members stat card."
stat_topics_label: "Custom label for the Topics stat card." stat_topics_label: "Custom label for the Topics stat card."
stat_posts_label: "Custom label for the Posts stat card." stat_posts_label: "Custom label for the Posts stat card."
@@ -93,6 +96,8 @@ en:
groups_enabled: "━━ ROW 7: SPACES ━━ — Show the Community Spaces section: a grid of colorful cards representing your public groups. Each card shows a colored icon (with group's first letter or flair), group name, and member count. Only public, non-automatic groups are shown." groups_enabled: "━━ ROW 7: SPACES ━━ — Show the Community Spaces section: a grid of colorful cards representing your public groups. Each card shows a colored icon (with group's first letter or flair), group name, and member count. Only public, non-automatic groups are shown."
groups_title: "Heading text above the group cards." groups_title: "Heading text above the group cards."
groups_count: "Number of group cards to display." groups_count: "Number of group cards to display."
groups_selected: "Show only specific groups. Enter group names separated by pipes (e.g. designers|developers|artists). Leave blank to auto-select public groups."
groups_card_bg_color: "Background color for each space card. Leave blank for default card styling."
groups_bg_dark: "Background color for the spaces section in dark mode. Leave blank for default." groups_bg_dark: "Background color for the spaces section in dark mode. Leave blank for default."
groups_bg_light: "Background color for the spaces section in light mode. Leave blank for default." groups_bg_light: "Background color for the spaces section in light mode. Leave blank for default."
groups_min_height: "Minimum height for the spaces section in pixels. Set to 0 for auto height." groups_min_height: "Minimum height for the spaces section in pixels. Set to 0 for auto height."
@@ -124,6 +129,3 @@ en:
footer_bg_dark: "Background color for the footer bar in dark mode. Leave blank for default." footer_bg_dark: "Background color for the footer bar in dark mode. Leave blank for default."
footer_bg_light: "Background color for the footer bar in light mode. Leave blank for default." footer_bg_light: "Background color for the footer bar in light mode. Leave blank for default."
footer_border_style: "Border style at the top of the footer bar." footer_border_style: "Border style at the top of the footer bar."
# ── Custom CSS ──
custom_css: "━━ CUSTOM CSS ━━ — Paste your own CSS rules directly into the landing page. This CSS loads after all other styles, giving it the highest priority. No style tags needed."

View File

@@ -140,6 +140,18 @@ plugins:
stat_icon_color: stat_icon_color:
default: "d4a24e" default: "d4a24e"
type: color type: color
stat_icon_bg_color:
default: ""
type: string
stat_icon_shape:
default: "circle"
type: enum
choices:
- circle
- rounded
stat_counter_color:
default: ""
type: string
stat_members_label: stat_members_label:
default: "Members" default: "Members"
type: string type: string
@@ -310,6 +322,12 @@ plugins:
groups_count: groups_count:
default: 5 default: 5
type: integer type: integer
groups_selected:
default: ""
type: list
groups_card_bg_color:
default: ""
type: string
groups_bg_dark: groups_bg_dark:
default: "" default: ""
type: string type: string
@@ -424,10 +442,3 @@ plugins:
- solid - solid
- dashed - dashed
- dotted - dotted
# ══════════════════════════════════════════
# Custom CSS (last)
# ══════════════════════════════════════════
custom_css:
default: ""
type: text

View File

@@ -19,12 +19,19 @@ module CommunityLanding
.select("users.*, COUNT(posts.id) AS post_count") .select("users.*, COUNT(posts.id) AS post_count")
end end
# Public groups # Public groups — optionally filtered by selected names
data[:groups] = if s.groups_enabled data[:groups] = if s.groups_enabled
Group selected = s.groups_selected.presence
scope = Group
.where(visibility_level: Group.visibility_levels[:public]) .where(visibility_level: Group.visibility_levels[:public])
.where(automatic: false) .where(automatic: false)
.limit(s.groups_count)
if selected
names = selected.split("|").map(&:strip).reject(&:empty?)
scope = scope.where(name: names) if names.any?
end
scope.limit(s.groups_count)
end end
# Trending topics # Trending topics

View File

@@ -4,7 +4,7 @@ module CommunityLanding
module Icons module Icons
SUN_SVG = '<svg class="cl-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>' SUN_SVG = '<svg class="cl-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>'
MOON_SVG = '<svg class="cl-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>' MOON_SVG = '<svg class="cl-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
QUOTE_SVG = '<svg class="cl-about__quote-mark" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M6 7h3l2 4v6H5v-6h3zm8 0h3l2 4v6h-6v-6h3z"/></svg>' QUOTE_SVG = '<svg class="cl-about__quote-mark" viewBox="0 0 24 24" fill="currentColor" width="32" height="32"><path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/></svg>'
STAT_MEMBERS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>' STAT_MEMBERS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>'
STAT_TOPICS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>' STAT_TOPICS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>'

View File

@@ -58,9 +58,6 @@ module CommunityLanding
html << @styles.color_overrides html << @styles.color_overrides
html << @styles.section_backgrounds html << @styles.section_backgrounds
custom_css = @s.custom_css.presence rescue nil
html << "<style>\n/* Custom CSS */\n#{custom_css}\n</style>\n" if custom_css
html << "</head>\n" html << "</head>\n"
html html
end end
@@ -169,21 +166,22 @@ module CommunityLanding
stats_title = @s.stats_title.presence || "Premium Stats" stats_title = @s.stats_title.presence || "Premium Stats"
border = @s.stats_border_style rescue "none" border = @s.stats_border_style rescue "none"
min_h = @s.stats_min_height rescue 0 min_h = @s.stats_min_height rescue 0
icon_shape = @s.stat_icon_shape rescue "circle"
html = +"" html = +""
html << "<section class=\"cl-stats cl-anim\" id=\"cl-stats-row\"#{section_style(border, min_h)}><div class=\"cl-container\">\n" html << "<section class=\"cl-stats cl-anim\" id=\"cl-stats-row\"#{section_style(border, min_h)}><div class=\"cl-container\">\n"
html << "<h2 class=\"cl-section-title\">#{e(stats_title)}</h2>\n" html << "<h2 class=\"cl-section-title\">#{e(stats_title)}</h2>\n"
html << "<div class=\"cl-stats__grid\">\n" html << "<div class=\"cl-stats__grid\">\n"
html << stat_card(Icons::STAT_MEMBERS_SVG, stats[:members], @s.stat_members_label) html << stat_card(Icons::STAT_MEMBERS_SVG, stats[:members], @s.stat_members_label, icon_shape)
html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label) html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label, icon_shape)
html << stat_card(Icons::STAT_POSTS_SVG, stats[:posts], @s.stat_posts_label) html << stat_card(Icons::STAT_POSTS_SVG, stats[:posts], @s.stat_posts_label, icon_shape)
html << stat_card(Icons::STAT_LIKES_SVG, stats[:likes], @s.stat_likes_label) html << stat_card(Icons::STAT_LIKES_SVG, stats[:likes], @s.stat_likes_label, icon_shape)
html << stat_card(Icons::STAT_CHATS_SVG, stats[:chats], @s.stat_chats_label) html << stat_card(Icons::STAT_CHATS_SVG, stats[:chats], @s.stat_chats_label, icon_shape)
html << "</div>\n</div></section>\n" html << "</div>\n</div></section>\n"
html html
end end
# ── 4. ABOUT ── # ── 4. ABOUT — split layout: image left on gradient, text right ──
def render_about def render_about
return "" unless @s.about_enabled return "" unless @s.about_enabled
@@ -193,21 +191,34 @@ module CommunityLanding
about_role = @s.about_role.presence || @s.title about_role = @s.about_role.presence || @s.title
about_heading_on = @s.about_heading_enabled rescue true about_heading_on = @s.about_heading_enabled rescue true
about_heading = @s.about_heading.presence || "About Community" about_heading = @s.about_heading.presence || "About Community"
about_bg_img = @s.about_background_image_url.presence
border = @s.about_border_style rescue "none" border = @s.about_border_style rescue "none"
min_h = @s.about_min_height rescue 0 min_h = @s.about_min_height rescue 0
html = +"" html = +""
html << "<section class=\"cl-about cl-anim\" id=\"cl-about\"#{section_style(border, min_h)}><div class=\"cl-container\">\n" html << "<section class=\"cl-about cl-anim\" id=\"cl-about\"#{section_style(border, min_h)}><div class=\"cl-container\">\n"
html << "<div class=\"cl-about__card\">\n" html << "<div class=\"cl-about__card\">\n"
# Left side — image on gradient background
html << "<div class=\"cl-about__left\">\n"
if about_image
html << "<img src=\"#{about_image}\" alt=\"#{e(@s.about_title)}\" class=\"cl-about__image\">\n"
end
html << "</div>\n"
# Right side — text content
html << "<div class=\"cl-about__right\">\n"
html << "<h2 class=\"cl-about__heading\">#{e(about_heading)}</h2>\n" if about_heading_on html << "<h2 class=\"cl-about__heading\">#{e(about_heading)}</h2>\n" if about_heading_on
html << Icons::QUOTE_SVG html << Icons::QUOTE_SVG
html << "<div class=\"cl-about__body\">#{about_body}</div>\n" if about_body.present? html << "<div class=\"cl-about__body\">#{about_body}</div>\n" if about_body.present?
html << "<div class=\"cl-about__meta\">\n" html << "<div class=\"cl-about__meta\">\n"
html << "<img src=\"#{about_image}\" alt=\"\" class=\"cl-about__avatar\">\n" if about_image
html << "<div class=\"cl-about__meta-text\">\n" html << "<div class=\"cl-about__meta-text\">\n"
html << "<span class=\"cl-about__author\">#{e(@s.about_title)}</span>\n" html << "<span class=\"cl-about__author\">#{e(@s.about_title)}</span>\n"
html << "<span class=\"cl-about__role\">#{e(about_role)}</span>\n" html << "<span class=\"cl-about__role\">#{e(about_role)}</span>\n"
html << "</div></div>\n</div>\n</div></section>\n" html << "</div></div>\n"
html << "</div>\n"
html << "</div>\n</div></section>\n"
html html
end end
@@ -223,7 +234,7 @@ module CommunityLanding
html = +"" html = +""
html << "<section class=\"cl-topics cl-anim\" id=\"cl-topics\"#{section_style(border, min_h)}><div class=\"cl-container\">\n" html << "<section class=\"cl-topics cl-anim\" id=\"cl-topics\"#{section_style(border, min_h)}><div class=\"cl-container\">\n"
html << "<h2 class=\"cl-section-title\">#{e(@s.topics_title)}</h2>\n" html << "<h2 class=\"cl-section-title\">#{e(@s.topics_title)}</h2>\n"
html << "<div class=\"cl-topics__scroll\">\n" html << "<div class=\"cl-topics__grid\">\n"
topics.each do |topic| topics.each do |topic|
topic_likes = topic.like_count rescue 0 topic_likes = topic.like_count rescue 0
@@ -288,21 +299,26 @@ module CommunityLanding
html << "<div class=\"cl-spaces__grid\">\n" html << "<div class=\"cl-spaces__grid\">\n"
groups.each do |group| groups.each do |group|
display_name = group.name.tr("_-", " ").gsub(/\b\w/, &:upcase) display_name = group.full_name.presence || group.name.tr("_-", " ").gsub(/\b\w/, &:upcase)
hue = group.name.bytes.sum % 360 hue = group.name.bytes.sum % 360
sat = 50 + (group.name.bytes.first.to_i % 20) sat = 55 + (group.name.bytes.first.to_i % 15)
light = 40 + (group.name.bytes.last.to_i % 15) light = 45 + (group.name.bytes.last.to_i % 12)
icon_color = "hsl(#{hue}, #{sat}%, #{light}%)"
html << "<a href=\"#{login_url}\" class=\"cl-space-card\">\n" html << "<a href=\"#{login_url}\" class=\"cl-space-card\">\n"
html << "<div class=\"cl-space-card__icon\" style=\"background: hsl(#{hue}, #{sat}%, #{light}%)\">" html << "<div class=\"cl-space-card__header\" style=\"--space-color: #{icon_color}\">\n"
html << "<div class=\"cl-space-card__icon\">"
if group.flair_url.present? if group.flair_url.present?
html << "<img src=\"#{group.flair_url}\" alt=\"\">" html << "<img src=\"#{group.flair_url}\" alt=\"\">"
else else
html << "<span class=\"cl-space-card__letter\">#{group.name[0].upcase}</span>" html << "<span class=\"cl-space-card__letter\">#{group.name[0].upcase}</span>"
end end
html << "</div>\n" html << "</div>\n"
html << "</div>\n"
html << "<div class=\"cl-space-card__body\">\n"
html << "<span class=\"cl-space-card__name\">#{e(display_name)}</span>\n" html << "<span class=\"cl-space-card__name\">#{e(display_name)}</span>\n"
html << "<span class=\"cl-space-card__sub\">#{group.user_count} members</span>\n" html << "<span class=\"cl-space-card__sub\">#{group.user_count} members</span>\n"
html << "</div>\n"
html << "</a>\n" html << "</a>\n"
end end
@@ -397,12 +413,11 @@ module CommunityLanding
# ── Shared helpers ── # ── Shared helpers ──
def stat_card(icon_svg, count, label) def stat_card(icon_svg, count, label, icon_shape = "circle")
shape_class = icon_shape == "rounded" ? "cl-stat-icon--rounded" : "cl-stat-icon--circle"
"<div class=\"cl-stat-card\">\n" \ "<div class=\"cl-stat-card\">\n" \
"<div class=\"cl-stat-card__top\">\n" \ "<div class=\"cl-stat-card__icon-wrap #{shape_class}\">#{icon_svg}</div>\n" \
"<span class=\"cl-stat-card__icon\">#{icon_svg}</span>\n" \
"<span class=\"cl-stat-card__label\">#{e(label)}</span>\n" \ "<span class=\"cl-stat-card__label\">#{e(label)}</span>\n" \
"</div>\n" \
"<span class=\"cl-stat-card__value\" data-count=\"#{count}\">0</span>\n" \ "<span class=\"cl-stat-card__value\" data-count=\"#{count}\">0</span>\n" \
"</div>\n" "</div>\n"
end end

View File

@@ -22,7 +22,15 @@ module CommunityLanding
app_g1 = hex(@s.app_cta_gradient_start) || accent app_g1 = hex(@s.app_cta_gradient_start) || accent
app_g2 = hex(@s.app_cta_gradient_mid) || accent_hover app_g2 = hex(@s.app_cta_gradient_mid) || accent_hover
app_g3 = hex(@s.app_cta_gradient_end) || accent_hover app_g3 = hex(@s.app_cta_gradient_end) || accent_hover
stat_icon_bg = hex(@s.stat_icon_bg_color.presence) rescue nil
stat_counter = hex(@s.stat_counter_color.presence) rescue nil
space_card_bg = hex(@s.groups_card_bg_color.presence) rescue nil
accent_rgb = hex_to_rgb(accent) accent_rgb = hex_to_rgb(accent)
stat_icon_rgb = hex_to_rgb(stat_icon)
stat_icon_bg_val = stat_icon_bg || "rgba(#{stat_icon_rgb}, 0.1)"
stat_counter_val = stat_counter || "var(--cl-text-strong)"
space_card_bg_val = space_card_bg || "var(--cl-card)"
about_bg_extra = about_bg_img ? ", url('#{about_bg_img}') center/cover no-repeat" : "" about_bg_extra = about_bg_img ? ", url('#{about_bg_img}') center/cover no-repeat" : ""
@@ -38,6 +46,9 @@ module CommunityLanding
--cl-border-hover: rgba(#{accent_rgb}, 0.25); --cl-border-hover: rgba(#{accent_rgb}, 0.25);
--cl-orb-1: rgba(#{accent_rgb}, 0.12); --cl-orb-1: rgba(#{accent_rgb}, 0.12);
--cl-stat-icon-color: #{stat_icon}; --cl-stat-icon-color: #{stat_icon};
--cl-stat-icon-bg: #{stat_icon_bg_val};
--cl-stat-counter-color: #{stat_counter_val};
--cl-space-card-bg: #{space_card_bg_val};
--cl-about-gradient: linear-gradient(135deg, #{about_g1}, #{about_g2}, #{about_g3})#{about_bg_extra}; --cl-about-gradient: linear-gradient(135deg, #{about_g1}, #{about_g2}, #{about_g3})#{about_bg_extra};
--cl-app-gradient: linear-gradient(135deg, #{app_g1}, #{app_g2}, #{app_g3}); --cl-app-gradient: linear-gradient(135deg, #{app_g1}, #{app_g2}, #{app_g3});
} }
@@ -52,6 +63,9 @@ module CommunityLanding
--cl-border-hover: rgba(#{accent_rgb}, 0.3); --cl-border-hover: rgba(#{accent_rgb}, 0.3);
--cl-orb-1: rgba(#{accent_rgb}, 0.08); --cl-orb-1: rgba(#{accent_rgb}, 0.08);
--cl-stat-icon-color: #{stat_icon}; --cl-stat-icon-color: #{stat_icon};
--cl-stat-icon-bg: #{stat_icon_bg_val};
--cl-stat-counter-color: #{stat_counter_val};
--cl-space-card-bg: #{space_card_bg_val};
--cl-about-gradient: linear-gradient(135deg, #{about_g1}, #{about_g2}, #{about_g3})#{about_bg_extra}; --cl-about-gradient: linear-gradient(135deg, #{about_g1}, #{about_g2}, #{about_g3})#{about_bg_extra};
--cl-app-gradient: linear-gradient(135deg, #{app_g1}, #{app_g2}, #{app_g3}); --cl-app-gradient: linear-gradient(135deg, #{app_g1}, #{app_g2}, #{app_g3});
} }