Major Improvements

Too numerous to mention
This commit is contained in:
2026-03-08 11:27:25 -04:00
parent c44db8cd3c
commit d74de05065
9 changed files with 693 additions and 155 deletions

View File

@@ -1,5 +1,244 @@
import { withPluginApi } from "discourse/lib/plugin-api"; import { withPluginApi } from "discourse/lib/plugin-api";
// Setting descriptions — injected into the admin DOM since the newer
// plugin settings page does not render .desc elements automatically.
const DESCRIPTIONS = {
// ── Master Switch ──
community_landing_enabled: "Enable the community landing page for logged-out visitors.",
// ── Layout ──
section_order: "Order of content sections. Drag to reorder. Available: hero, stats, about, participation, topics, groups, app_cta.",
custom_css: "Raw CSS injected after all plugin styles. Use for overrides and tweaks. No style tags needed.",
// ── SEO & Meta ──
meta_description: "Meta description for search engines and social sharing. If blank, the hero subtitle is used.",
og_image_url: "Open Graph image URL for social sharing (1200×630px recommended). If blank, the site logo is used.",
favicon_url: "Custom favicon URL (.ico, .png, .svg). If blank, the browser default is used.",
json_ld_enabled: "Add JSON-LD structured data (Organization + WebSite schema) for search engines.",
// ── Branding: Logo ──
logo_dark_url: "Logo image URL for dark mode. Shown in navbar and footer. Leave blank to show site name as text.",
logo_light_url: "Logo image URL for light mode. If not set, the dark logo is used for both themes.",
logo_height: "Logo height in pixels (1680). Applies to both navbar and footer logos.",
footer_logo_url: "Override logo for the footer only. If not set, the navbar logo is reused.",
// ── Colors ──
accent_color: "Primary accent color: buttons, links, highlights, gradients, stat icons. Hex value.",
accent_hover_color: "Accent color on hover. Should be slightly lighter or darker than the accent.",
dark_bg_color: "Page background color for dark mode.",
light_bg_color: "Page background color for light mode.",
orb_color: "Color of decorative background orbs. Leave blank to use the accent color.",
orb_opacity: "Opacity of the background orbs (0100). Default: 50.",
// ── Scroll Animations ──
scroll_animation: "How sections animate into view on scroll: fade_up, fade_in, slide_left, slide_right, zoom_in, flip_up, or none.",
staggered_reveal_enabled: "Animate child elements (cards, stats) with a staggered delay for a cascading reveal effect.",
dynamic_background_enabled: "Enable parallax background orbs that drift as the user scrolls.",
mouse_parallax_enabled: "Enable subtle parallax movement of background elements in response to mouse position.",
scroll_progress_enabled: "Show a thin progress bar at the top of the page indicating scroll position.",
// ── Fonts ──
google_font_name: "Google Font family for body text. Must match exact Google Fonts name (e.g. 'Inter', 'Poppins'). Default: Outfit.",
title_font_name: "Separate Google Font for titles and headings. Leave blank to use the body font.",
// ── Icons ──
fontawesome_enabled: "Load FontAwesome 6 Free icons from CDN for use on buttons.",
// ── Navbar ──
navbar_signin_label: "Text for the sign-in link in the navbar.",
navbar_signin_enabled: "Show the sign-in link in the navbar.",
navbar_signin_color_dark: "Sign-in link color for dark mode. Leave blank for default.",
navbar_signin_color_light: "Sign-in link color for light mode.",
navbar_join_label: "Text for the join/register CTA button in the navbar.",
navbar_join_enabled: "Show the join/register button in the navbar.",
navbar_join_color_dark: "Join button background color for dark mode. Leave blank for accent color.",
navbar_join_color_light: "Join button background color for light mode.",
navbar_bg_color: "Custom navbar background when scrolled. Leave blank for frosted glass effect.",
navbar_border_style: "Border style at the bottom of the navbar when scrolled.",
navbar_signin_icon: "FontAwesome icon name for sign-in (e.g. 'right-to-bracket'). Requires FontAwesome enabled.",
navbar_signin_icon_position: "Show the sign-in icon before or after the label.",
navbar_join_icon: "FontAwesome icon name for join button (e.g. 'user-plus'). Requires FontAwesome enabled.",
navbar_join_icon_position: "Show the join icon before or after the label.",
social_twitter_url: "Twitter / X profile URL. Leave blank to hide. Icons appear in navbar before auth buttons.",
social_facebook_url: "Facebook page or profile URL. Leave blank to hide.",
social_instagram_url: "Instagram profile URL. Leave blank to hide.",
social_youtube_url: "YouTube channel URL. Leave blank to hide.",
social_tiktok_url: "TikTok profile URL. Leave blank to hide.",
social_github_url: "GitHub organization or profile URL. Leave blank to hide.",
// ── Hero ──
hero_title: "Main headline text in the hero section.",
hero_title_size: "Hero title font size in pixels. 0 = use default responsive size.",
hero_accent_word: "Which word gets the accent shimmer. 0 = last word, 1 = first, 2 = second, etc.",
hero_subtitle: "Supporting text below the headline. Describe your community's purpose.",
hero_card_enabled: "Display hero content inside a rounded card with border and shadow.",
hero_image_first: "Show hero image above text on mobile / left on desktop. Off = text first.",
hero_background_image_url: "Full-bleed background image behind the hero. In card mode, fills the card with overlay.",
hero_image_urls: "Images for the right side of the hero. Up to 5 URLs — one shown randomly per page load.",
hero_image_max_height: "Maximum height for the hero image in pixels (1001200).",
hero_primary_button_enabled: "Show the primary CTA button in the hero.",
hero_primary_button_label: "Text on the primary (filled, accent-colored) CTA button.",
hero_primary_button_url: "URL the primary button links to. Relative path or absolute URL.",
hero_secondary_button_enabled: "Show the secondary CTA button in the hero.",
hero_secondary_button_label: "Text on the secondary (outlined) CTA button.",
hero_secondary_button_url: "URL the secondary button links to.",
hero_primary_button_icon: "FontAwesome icon for primary button (e.g. 'rocket'). Leave blank for no icon.",
hero_primary_button_icon_position: "Show the primary button icon before or after the label.",
hero_secondary_button_icon: "FontAwesome icon for secondary button. Leave blank for no icon.",
hero_secondary_button_icon_position: "Show the secondary button icon before or after the label.",
hero_primary_btn_color_dark: "Primary button background for dark mode. Leave blank for accent color.",
hero_primary_btn_color_light: "Primary button background for light mode.",
hero_secondary_btn_color_dark: "Secondary button background for dark mode. Leave blank for glass style.",
hero_secondary_btn_color_light: "Secondary button background for light mode.",
hero_video_url: "Hero video URL (MP4 or YouTube). Play button opens a lightbox modal.",
hero_video_button_color: "Custom color for the video play button. Leave blank for accent color.",
hero_video_blur_on_hover: "Blur the hero image when hovering the play button.",
hero_bg_dark: "Hero section background for dark mode. Leave blank for default.",
hero_bg_light: "Hero section background for light mode.",
hero_min_height: "Minimum hero section height in pixels. 0 = auto height.",
hero_border_style: "Border style at the bottom of the hero section.",
hero_card_bg_dark: "Hero card overlay background for dark mode. Only in card mode.",
hero_card_bg_light: "Hero card overlay background for light mode.",
hero_card_opacity: "Hero card background opacity (01). Lower = more transparent. Default: 0.85.",
// ── Contributors (Hero Creators) ──
contributors_enabled: "Show top 3 creators in the hero with gold, silver, bronze badges.",
contributors_title: "Heading above the creators list.",
contributors_title_enabled: "Show the heading above the creators list.",
contributors_count_label: "Label before each creator's count (e.g. 'Cheers'). Blank = no prefix.",
contributors_count_label_enabled: "Show the count label prefix before activity counts.",
contributors_alignment: "Horizontal alignment of the creators list: center or left.",
contributors_pill_max_width: "Max width per creator pill card in pixels (200600).",
contributors_pill_bg_dark: "Creator pill background for dark mode. Leave blank for glass styling.",
contributors_pill_bg_light: "Creator pill background for light mode.",
contributors_days: "Lookback period in days for calculating top contributors.",
contributors_count: "Number of top contributors to fetch (top 3 in hero, 410 in Participation).",
// ── Participation ──
participation_enabled: "Show Participation section: testimonial cards with leaderboard bios (positions 410).",
participation_title_enabled: "Show heading above participation cards.",
participation_title: "Heading text above participation cards.",
participation_bio_max_length: "Max characters from each user's bio (50500). Longer bios are truncated.",
participation_icon_color: "Color for the decorative quote icon on cards. Leave blank for accent color.",
participation_card_bg_dark: "Participation card background for dark mode.",
participation_card_bg_light: "Participation card background for light mode.",
participation_bg_dark: "Section background for dark mode. Leave blank for default.",
participation_bg_light: "Section background for light mode.",
participation_min_height: "Minimum section height in pixels. 0 = auto.",
participation_border_style: "Border style at the bottom of the section.",
participation_title_size: "Section title font size in pixels. 0 = use default.",
// ── Stats ──
stats_enabled: "Show the stats section with live community counters.",
stat_labels_enabled: "Show text labels below stat counters (e.g. 'Members'). Off = numbers and icons only.",
stats_title_enabled: "Show section heading above the stats row.",
stats_title: "Section heading text above the stats.",
stats_title_size: "Stats title font size in pixels. 0 = use default.",
stat_card_style: "Stat card style: rectangle, rounded, pill, or minimal (no background).",
stat_icon_color: "Color for stat counter icons.",
stat_icon_bg_color: "Background behind each stat icon. Leave blank for subtle accent tint.",
stat_icon_shape: "Icon background shape: circle or rounded square.",
stat_counter_color: "Color for stat counter numbers. Leave blank for default text color.",
stat_members_label: "Custom label for the Members stat.",
stat_topics_label: "Custom label for the Topics stat.",
stat_posts_label: "Custom label for the Posts stat.",
stat_likes_label: "Custom label for the Likes stat.",
stat_chats_label: "Custom label for the Chats stat. Shows chat messages if Chat plugin is active.",
stat_round_numbers: "Round large numbers: 1000 → 1K, 12345 → 12.3K, 1234567 → 1.2M.",
stat_card_bg_dark: "Stat card background for dark mode.",
stat_card_bg_light: "Stat card background for light mode.",
stats_bg_dark: "Section background for dark mode. Leave blank for default.",
stats_bg_light: "Section background for light mode.",
stats_min_height: "Minimum section height in pixels. 0 = auto.",
stats_border_style: "Border style at the bottom of the stats section.",
// ── About ──
about_enabled: "Show the About section: card with heading, quote icon, description, and author attribution.",
about_heading_enabled: "Show the bold heading at the top of the About card.",
about_heading: "Heading text at the top of the About card (e.g. 'About Community').",
about_title: "Author or community name in the card's bottom attribution.",
about_title_size: "About heading font size in pixels. 0 = use default.",
about_role: "Subtitle below author name (e.g. 'Community Manager'). Blank = site name.",
about_body: "Main body text. Supports HTML: p, a, strong, em, ul, li, br.",
about_image_url: "Avatar image next to author name. Square images work best.",
about_card_color_dark: "About card background for dark mode.",
about_card_color_light: "About card background for light mode.",
about_background_image_url: "Background image on the card. Use a subtle pattern or texture.",
about_bg_dark: "Section background for dark mode. Leave blank for default.",
about_bg_light: "Section background for light mode.",
about_min_height: "Minimum section height in pixels. 0 = auto.",
about_border_style: "Border style at the bottom of the about section.",
// ── Trending ──
topics_enabled: "Show Trending Discussions: scrollable row of active topic cards with live data.",
topics_title_enabled: "Show heading above the topic cards.",
topics_title: "Heading text above the topic cards.",
topics_title_size: "Trending title font size in pixels. 0 = use default.",
topics_count: "Number of trending topic cards to display.",
topics_card_bg_dark: "Topic card background for dark mode.",
topics_card_bg_light: "Topic card background for light mode.",
topics_bg_dark: "Section background for dark mode. Leave blank for default.",
topics_bg_light: "Section background for light mode.",
topics_min_height: "Minimum section height in pixels. 0 = auto.",
topics_border_style: "Border style at the bottom of the trending section.",
// ── Spaces ──
groups_enabled: "Show Community Spaces: grid of group cards with icon, name, and member count.",
groups_title_enabled: "Show heading above group cards.",
groups_title: "Heading text above group cards.",
groups_title_size: "Spaces title font size in pixels. 0 = use default.",
groups_count: "Number of group cards to display.",
groups_selected: "Show only specific groups. Enter names separated by pipes (e.g. designers|developers). Blank = auto-select.",
groups_show_description: "Show group description text below the group name on each card.",
groups_description_max_length: "Max characters for group descriptions (30500). Longer text is truncated.",
groups_card_bg_dark: "Space card background for dark mode.",
groups_card_bg_light: "Space card background for light mode.",
groups_bg_dark: "Section background for dark mode. Leave blank for default.",
groups_bg_light: "Section background for light mode.",
groups_min_height: "Minimum section height in pixels. 0 = auto.",
groups_border_style: "Border style at the bottom of the spaces section.",
// ── FAQ ──
faq_enabled: "Show FAQ accordion alongside the Spaces section. One item opens at a time.",
faq_title_enabled: "Show heading above the FAQ accordion.",
faq_title: "Heading text above the FAQ.",
faq_title_size: "FAQ title font size in pixels. 0 = use default.",
faq_items: 'FAQ items as JSON array: [{\"q\":\"Question\",\"a\":\"Answer\"}]. HTML supported in answers.',
faq_card_bg_dark: "FAQ card background for dark mode.",
faq_card_bg_light: "FAQ card background for light mode.",
// ── App CTA ──
show_app_ctas: "Show App Download CTA: gradient banner with headline, badges, and promo image.",
ios_app_url: "Apple App Store URL. Leave blank to hide iOS badge.",
android_app_url: "Google Play Store URL. Leave blank to hide Android badge.",
ios_app_badge_image_url: "Custom iOS badge image. Leave blank for default.",
android_app_badge_image_url: "Custom Android badge image. Leave blank for default.",
app_badge_height: "Badge height in pixels (3080).",
app_badge_style: "Badge border-radius: rounded, pill, or square.",
app_cta_headline: "Bold headline in the app download banner.",
app_cta_title_size: "App CTA headline font size in pixels. 0 = use default.",
app_cta_subtext: "Supporting text below the headline.",
app_cta_gradient_start_dark: "Gradient start color for dark mode. Leave blank for accent.",
app_cta_gradient_start_light: "Gradient start color for light mode.",
app_cta_gradient_mid_dark: "Gradient middle color for dark mode.",
app_cta_gradient_mid_light: "Gradient middle color for light mode.",
app_cta_gradient_end_dark: "Gradient end color for dark mode.",
app_cta_gradient_end_light: "Gradient end color for light mode.",
app_cta_image_url: "Promo image on the right (e.g. phone mockup). PNG for transparent bg.",
app_cta_bg_dark: "Section background for dark mode. Leave blank for default.",
app_cta_bg_light: "Section background for light mode.",
app_cta_min_height: "Minimum section height in pixels. 0 = auto.",
app_cta_border_style: "Border style at the bottom of the app CTA section.",
// ── Footer ──
footer_description: "Description paragraph above the footer bar.",
footer_text: "Optional HTML text inside the footer bar. Supports: p, a, strong, em, ul, li, br.",
footer_links: 'Footer links as JSON array: [{\"label\":\"Terms\",\"url\":\"/tos\"}].',
footer_bg_dark: "Footer background for dark mode. Leave blank for default.",
footer_bg_light: "Footer background for light mode.",
footer_border_style: "Border style at the top of the footer bar.",
};
const TABS = [ const TABS = [
{ {
id: "settings", id: "settings",
@@ -10,8 +249,10 @@ const TABS = [
"meta_description", "og_image_url", "favicon_url", "json_ld_enabled", "meta_description", "og_image_url", "favicon_url", "json_ld_enabled",
"logo_dark_url", "logo_light_url", "logo_height", "footer_logo_url", "logo_dark_url", "logo_light_url", "logo_height", "footer_logo_url",
"accent_color", "accent_hover_color", "dark_bg_color", "light_bg_color", "accent_color", "accent_hover_color", "dark_bg_color", "light_bg_color",
"orb_color", "orb_opacity",
"scroll_animation", "staggered_reveal_enabled", "dynamic_background_enabled", "scroll_animation", "staggered_reveal_enabled", "dynamic_background_enabled",
"mouse_parallax_enabled", "scroll_progress_enabled" "mouse_parallax_enabled", "scroll_progress_enabled",
"google_font_name", "title_font_name", "fontawesome_enabled"
]) ])
}, },
{ {
@@ -22,6 +263,8 @@ const TABS = [
"navbar_signin_color_dark", "navbar_signin_color_light", "navbar_signin_color_dark", "navbar_signin_color_light",
"navbar_join_label", "navbar_join_enabled", "navbar_join_label", "navbar_join_enabled",
"navbar_join_color_dark", "navbar_join_color_light", "navbar_join_color_dark", "navbar_join_color_light",
"navbar_signin_icon", "navbar_signin_icon_position",
"navbar_join_icon", "navbar_join_icon_position",
"navbar_bg_color", "navbar_border_style", "navbar_bg_color", "navbar_border_style",
"social_twitter_url", "social_facebook_url", "social_instagram_url", "social_twitter_url", "social_facebook_url", "social_instagram_url",
"social_youtube_url", "social_tiktok_url", "social_github_url" "social_youtube_url", "social_tiktok_url", "social_github_url"
@@ -31,11 +274,13 @@ const TABS = [
id: "hero", id: "hero",
label: "Hero", label: "Hero",
settings: new Set([ settings: new Set([
"hero_title", "hero_accent_word", "hero_subtitle", "hero_title", "hero_accent_word", "hero_subtitle", "hero_title_size",
"hero_card_enabled", "hero_image_first", "hero_card_enabled", "hero_image_first",
"hero_background_image_url", "hero_image_urls", "hero_image_max_height", "hero_background_image_url", "hero_image_urls", "hero_image_max_height",
"hero_primary_button_enabled", "hero_primary_button_label", "hero_primary_button_url", "hero_primary_button_enabled", "hero_primary_button_label", "hero_primary_button_url",
"hero_primary_button_icon", "hero_primary_button_icon_position",
"hero_secondary_button_enabled", "hero_secondary_button_label", "hero_secondary_button_url", "hero_secondary_button_enabled", "hero_secondary_button_label", "hero_secondary_button_url",
"hero_secondary_button_icon", "hero_secondary_button_icon_position",
"hero_primary_btn_color_dark", "hero_primary_btn_color_light", "hero_primary_btn_color_dark", "hero_primary_btn_color_light",
"hero_secondary_btn_color_dark", "hero_secondary_btn_color_light", "hero_secondary_btn_color_dark", "hero_secondary_btn_color_light",
"hero_video_url", "hero_video_button_color", "hero_video_blur_on_hover", "hero_video_url", "hero_video_button_color", "hero_video_blur_on_hover",
@@ -53,7 +298,8 @@ const TABS = [
label: "Participation", label: "Participation",
settings: new Set([ settings: new Set([
"participation_enabled", "participation_title_enabled", "participation_enabled", "participation_title_enabled",
"participation_title", "participation_bio_max_length", "participation_title", "participation_title_size",
"participation_bio_max_length",
"participation_icon_color", "participation_icon_color",
"participation_card_bg_dark", "participation_card_bg_light", "participation_card_bg_dark", "participation_card_bg_light",
"participation_bg_dark", "participation_bg_light", "participation_bg_dark", "participation_bg_light",
@@ -65,7 +311,7 @@ const TABS = [
label: "Stats", label: "Stats",
settings: new Set([ settings: new Set([
"stats_enabled", "stat_labels_enabled", "stats_title_enabled", "stats_enabled", "stat_labels_enabled", "stats_title_enabled",
"stats_title", "stat_card_style", "stats_title", "stats_title_size", "stat_card_style",
"stat_icon_color", "stat_icon_bg_color", "stat_icon_shape", "stat_counter_color", "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_round_numbers", "stat_likes_label", "stat_chats_label", "stat_round_numbers",
@@ -78,7 +324,7 @@ const TABS = [
label: "About", label: "About",
settings: new Set([ settings: new Set([
"about_enabled", "about_heading_enabled", "about_heading", "about_enabled", "about_heading_enabled", "about_heading",
"about_title", "about_role", "about_body", "about_image_url", "about_title", "about_title_size", "about_role", "about_body", "about_image_url",
"about_card_color_dark", "about_card_color_light", "about_card_color_dark", "about_card_color_light",
"about_background_image_url", "about_background_image_url",
"about_bg_dark", "about_bg_light", "about_min_height", "about_border_style" "about_bg_dark", "about_bg_light", "about_min_height", "about_border_style"
@@ -88,21 +334,30 @@ const TABS = [
id: "topics", id: "topics",
label: "Trending", label: "Trending",
settings: new Set([ settings: new Set([
"topics_enabled", "topics_title_enabled", "topics_title", "topics_count", "topics_enabled", "topics_title_enabled", "topics_title", "topics_title_size",
"topics_count",
"topics_card_bg_dark", "topics_card_bg_light", "topics_card_bg_dark", "topics_card_bg_light",
"topics_bg_dark", "topics_bg_light", "topics_min_height", "topics_border_style" "topics_bg_dark", "topics_bg_light", "topics_min_height", "topics_border_style"
]) ])
}, },
{ {
id: "groups", id: "groups",
label: "Spaces & FAQ", label: "Spaces",
settings: new Set([ settings: new Set([
"groups_enabled", "groups_title_enabled", "groups_title", "groups_count", "groups_enabled", "groups_title_enabled", "groups_title", "groups_title_size",
"groups_selected", "groups_count", "groups_selected",
"groups_show_description", "groups_description_max_length", "groups_show_description", "groups_description_max_length",
"groups_card_bg_dark", "groups_card_bg_light", "groups_card_bg_dark", "groups_card_bg_light",
"groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style", "groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style"
"faq_enabled", "faq_title_enabled", "faq_title", "faq_items" ])
},
{
id: "faq",
label: "FAQ",
settings: new Set([
"faq_enabled", "faq_title_enabled", "faq_title", "faq_title_size",
"faq_items",
"faq_card_bg_dark", "faq_card_bg_light"
]) ])
}, },
{ {
@@ -112,7 +367,7 @@ const TABS = [
"show_app_ctas", "ios_app_url", "android_app_url", "show_app_ctas", "ios_app_url", "android_app_url",
"ios_app_badge_image_url", "android_app_badge_image_url", "ios_app_badge_image_url", "android_app_badge_image_url",
"app_badge_height", "app_badge_style", "app_badge_height", "app_badge_style",
"app_cta_headline", "app_cta_subtext", "app_cta_headline", "app_cta_title_size", "app_cta_subtext",
"app_cta_gradient_start_dark", "app_cta_gradient_start_light", "app_cta_gradient_start_dark", "app_cta_gradient_start_light",
"app_cta_gradient_mid_dark", "app_cta_gradient_mid_light", "app_cta_gradient_mid_dark", "app_cta_gradient_mid_light",
"app_cta_gradient_end_dark", "app_cta_gradient_end_light", "app_cta_gradient_end_dark", "app_cta_gradient_end_light",
@@ -156,6 +411,8 @@ const BG_PAIRS = [
// Spaces // Spaces
["groups_card_bg_dark", "groups_card_bg_light"], ["groups_card_bg_dark", "groups_card_bg_light"],
["groups_bg_dark", "groups_bg_light"], ["groups_bg_dark", "groups_bg_light"],
// FAQ
["faq_card_bg_dark", "faq_card_bg_light"],
// App CTA // App CTA
["app_cta_gradient_start_dark", "app_cta_gradient_start_light"], ["app_cta_gradient_start_dark", "app_cta_gradient_start_light"],
["app_cta_gradient_mid_dark", "app_cta_gradient_mid_light"], ["app_cta_gradient_mid_dark", "app_cta_gradient_mid_light"],
@@ -185,8 +442,6 @@ function applyTabFilter() {
if (!tab) return; if (!tab) return;
container.querySelectorAll(".row.setting[data-setting]").forEach((row) => { container.querySelectorAll(".row.setting[data-setting]").forEach((row) => {
// Skip rows inside a merge wrapper — handled at wrapper level
if (row.closest(".cl-merge-wrapper")) return;
const name = row.getAttribute("data-setting"); const name = row.getAttribute("data-setting");
row.classList.toggle( row.classList.toggle(
"cl-tab-hidden", "cl-tab-hidden",
@@ -194,17 +449,6 @@ function applyTabFilter() {
); );
}); });
// Handle merge wrappers — show/hide based on dark row's setting
container.querySelectorAll(".cl-merge-wrapper").forEach((wrapper) => {
const darkRow = wrapper.querySelector(".cl-merged-dark");
if (!darkRow) return;
const name = darkRow.getAttribute("data-setting");
wrapper.classList.toggle(
"cl-tab-hidden",
!filterActive && !tab.settings.has(name)
);
});
// Update filter-active dimming on native nav or standalone tab bar // Update filter-active dimming on native nav or standalone tab bar
const nativeNav = document.querySelector(".d-nav-submenu__tabs"); const nativeNav = document.querySelector(".d-nav-submenu__tabs");
if (nativeNav) { if (nativeNav) {
@@ -294,15 +538,9 @@ function cleanupTabs() {
el.classList.remove("cl-tab-hidden"); el.classList.remove("cl-tab-hidden");
}); });
// Unwrap merge wrappers — restore rows to their original position // Remove merge classes
container.querySelectorAll(".cl-merge-wrapper").forEach((wrapper) => { container.querySelectorAll(".cl-merged-dark, .cl-merged-light").forEach((el) => {
const parent = wrapper.parentNode; el.classList.remove("cl-merged-dark", "cl-merged-light");
while (wrapper.firstChild) {
const child = wrapper.firstChild;
child.classList.remove("cl-merged-dark", "cl-merged-light");
parent.insertBefore(child, wrapper);
}
wrapper.remove();
}); });
} }
@@ -311,10 +549,37 @@ function cleanupTabs() {
filterActive = false; filterActive = false;
} }
/**
* Inject description text into each setting row.
* The newer Discourse plugin admin page doesn't render .desc elements,
* so we add them from the DESCRIPTIONS map.
*/
function injectDescriptions() {
const container = getContainer();
if (!container) return;
container.querySelectorAll(".row.setting[data-setting]").forEach((row) => {
const name = row.getAttribute("data-setting");
const text = DESCRIPTIONS[name];
if (!text) return;
const valueDiv = row.querySelector(".setting-value");
if (!valueDiv) return;
// Already injected
if (valueDiv.querySelector(".cl-desc")) return;
const desc = document.createElement("div");
desc.className = "cl-desc";
desc.textContent = text;
valueDiv.appendChild(desc);
});
}
/** /**
* Merge dark/light bg color pairs into a single visual row. * Merge dark/light bg color pairs into a single visual row.
* Uses a CSS wrapper approach — both rows stay intact in the DOM * CSS-only approach — elements stay in their original DOM positions
* (preserving Ember bindings and undo/reset buttons). * (preserving Ember bindings, undo/reset buttons, and re-renders).
*/ */
function mergeBgPairs() { function mergeBgPairs() {
const container = getContainer(); const container = getContainer();
@@ -349,17 +614,9 @@ function mergeBgPairs() {
lightValue.insertBefore(lbl, lightValue.firstChild); lightValue.insertBefore(lbl, lightValue.firstChild);
} }
// Wrap both rows in a flex container // Just add classes — NO DOM moves, preserves all Ember bindings
const wrapper = document.createElement("div");
wrapper.className = "cl-merge-wrapper";
darkRow.parentNode.insertBefore(wrapper, darkRow);
wrapper.appendChild(darkRow);
wrapper.appendChild(lightRow);
// Mark rows for CSS styling
darkRow.classList.add("cl-merged-dark"); darkRow.classList.add("cl-merged-dark");
lightRow.classList.add("cl-merged-light"); lightRow.classList.add("cl-merged-light");
// Light row is NOT hidden — it stays in the DOM with full Ember bindings
}); });
} }
@@ -423,6 +680,7 @@ function buildTabsUI() {
}); });
container.classList.add("cl-tabs-active"); container.classList.add("cl-tabs-active");
injectDescriptions();
mergeBgPairs(); mergeBgPairs();
applyTabFilter(); applyTabFilter();
return true; return true;
@@ -468,6 +726,7 @@ function buildTabsUI() {
} }
container.classList.add("cl-tabs-active"); container.classList.add("cl-tabs-active");
injectDescriptions();
mergeBgPairs(); mergeBgPairs();
applyTabFilter(); applyTabFilter();
return true; return true;

View File

@@ -82,37 +82,76 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover {
color: var(--primary, #ddd); color: var(--primary, #ddd);
} }
/* ── Merged dark/light color pairs (wrapper approach) ── */ /* ── Merged dark/light color pairs (CSS-only, no DOM moves) ── */
.cl-merge-wrapper { .cl-tabs-active .row.setting.cl-merged-dark {
display: flex; float: left;
gap: 16px; width: calc(50% - 8px);
width: 100%; margin-right: 16px;
padding-bottom: 20px;
}
.cl-merge-wrapper > .row.setting {
flex: 1;
min-width: 0;
padding-bottom: 0 !important; padding-bottom: 0 !important;
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.cl-tabs-active .row.setting.cl-merged-light {
float: left;
width: calc(50% - 8px);
padding-bottom: 0 !important;
margin-bottom: 20px !important;
}
/* Hide the light row's label + description — dark row's label covers both */ /* Hide the light row's label + description — dark row's label covers both */
.cl-merge-wrapper > .cl-merged-light > .setting-label { .cl-tabs-active .row.setting.cl-merged-light > .setting-label {
display: none; display: none;
} }
.cl-merge-wrapper > .cl-merged-light .desc { .cl-tabs-active .row.setting.cl-merged-light .desc {
display: none; display: none;
} }
/* Light row's value area fills the full width since label is hidden */ /* Light row's value area fills the full width since label is hidden */
.cl-tabs-active .cl-merge-wrapper > .cl-merged-light > .setting-value { .cl-tabs-active .row.setting.cl-merged-light > .setting-value {
width: 100%; width: 100%;
float: none; float: none;
} }
/* Dark row: label + value stack vertically inside the half-width */
.cl-tabs-active .row.setting.cl-merged-dark > .setting-label {
float: none;
width: 100%;
margin-bottom: 4px;
}
.cl-tabs-active .row.setting.cl-merged-dark > .setting-value {
float: none;
width: 100%;
padding-right: 0;
}
/* Controls (reset/undo) inside merged rows — inline after the color picker */
.cl-tabs-active .row.setting.cl-merged-dark > .setting-controls,
.cl-tabs-active .row.setting.cl-merged-light > .setting-controls {
float: none;
display: inline-block;
margin-top: 4px;
}
/* Clearfix after the light row to restore normal flow */
.cl-tabs-active .row.setting.cl-merged-light::after {
content: "";
display: block;
clear: both;
}
/* Insert a clear break after each pair to prevent stacking issues */
.cl-tabs-active .row.setting.cl-merged-light + .row.setting:not(.cl-merged-light) {
clear: both;
}
/* Also clear when a merged-light is followed by anything else */
.cl-tabs-active .cl-merged-light + *:not(.cl-merged-light):not(.cl-tab-hidden) {
clear: both;
}
.cl-color-col__label { .cl-color-col__label {
display: block; display: block;
font-size: var(--font-down-1); font-size: var(--font-down-1);
@@ -124,12 +163,14 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover {
} }
@media (max-width: 767px) { @media (max-width: 767px) {
.cl-merge-wrapper { .cl-tabs-active .row.setting.cl-merged-dark,
flex-direction: column; .cl-tabs-active .row.setting.cl-merged-light {
gap: 0; float: none;
width: 100%;
margin-right: 0;
} }
.cl-merge-wrapper > .cl-merged-light > .setting-label { .cl-tabs-active .row.setting.cl-merged-light > .setting-label {
display: none; display: none;
} }
} }
@@ -174,7 +215,10 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover {
.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^="social_"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="social_"],
.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^="google_"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting^="title_font"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="fontawesome_enabled"] {
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -185,6 +229,8 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover {
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="logo_dark_url"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="logo_dark_url"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="accent_color"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="accent_color"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="scroll_animation"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="scroll_animation"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="google_font_name"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="fontawesome_enabled"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="social_twitter_url"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="social_twitter_url"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="navbar_signin_label"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="navbar_signin_label"],
.admin-detail:not(.cl-tabs-active) .row.setting[data-setting="hero_title"], .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="hero_title"],
@@ -208,6 +254,8 @@ html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="m
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="logo_dark_url"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="logo_dark_url"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="accent_color"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="accent_color"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="scroll_animation"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="scroll_animation"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="google_font_name"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="fontawesome_enabled"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="social_twitter_url"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="social_twitter_url"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="navbar_signin_label"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="navbar_signin_label"],
html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="hero_title"], html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="hero_title"],
@@ -298,13 +346,15 @@ html.dark-scheme .admin-detail:not(.cl-tabs-active) .row.setting[data-setting="f
} }
.cl-tabs-active .row.setting .desc, .cl-tabs-active .row.setting .desc,
.cl-tabs-active .row.setting .cl-desc,
.cl-tabs-active .row.setting .validation-error { .cl-tabs-active .row.setting .validation-error {
padding-top: 3px; padding-top: 3px;
font-size: var(--font-down-1); font-size: var(--font-down-1);
line-height: var(--line-height-large); line-height: var(--line-height-large);
} }
.cl-tabs-active .row.setting .desc { .cl-tabs-active .row.setting .desc,
.cl-tabs-active .row.setting .cl-desc {
color: var(--primary-medium); color: var(--primary-medium);
} }

View File

@@ -100,7 +100,7 @@
padding: 0; padding: 0;
background: var(--cl-bg); background: var(--cl-bg);
color: var(--cl-text); color: var(--cl-text);
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-family: var(--cl-font-body, 'Outfit'), -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
line-height: 1.6; line-height: 1.6;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
overflow-x: hidden; overflow-x: hidden;
@@ -177,7 +177,7 @@ html {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
filter: blur(100px); filter: blur(100px);
opacity: 0.5; opacity: var(--cl-orb-opacity, 0.5);
animation: cl-orb-float 20s infinite alternate ease-in-out; animation: cl-orb-float 20s infinite alternate ease-in-out;
} }
@@ -223,6 +223,7 @@ html {
.cl-section-title { .cl-section-title {
font-size: 1.6rem; font-size: 1.6rem;
font-weight: 800; font-weight: 800;
font-family: var(--cl-font-title, var(--cl-font-body, 'Outfit')), sans-serif;
color: var(--cl-text-strong); color: var(--cl-text-strong);
margin: 0 0 2.5rem; margin: 0 0 2.5rem;
letter-spacing: -0.02em; letter-spacing: -0.02em;
@@ -380,6 +381,11 @@ html {
font-size: 1.05rem; font-size: 1.05rem;
} }
/* FontAwesome icon spacing inside buttons */
.cl-btn i.fa-solid {
margin: 0 0.35em;
}
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════
1. NAVBAR — logo left, theme toggle + auth right 1. NAVBAR — logo left, theme toggle + auth right
═══════════════════════════════════════════════════════════════════ */ ═══════════════════════════════════════════════════════════════════ */
@@ -731,6 +737,7 @@ html {
.cl-hero__title { .cl-hero__title {
font-size: clamp(2.5rem, 8vw, 4.5rem); font-size: clamp(2.5rem, 8vw, 4.5rem);
font-weight: 900; font-weight: 900;
font-family: var(--cl-font-title, var(--cl-font-body, 'Outfit')), sans-serif;
color: var(--cl-hero-text); color: var(--cl-hero-text);
margin: 0 0 1.5rem; margin: 0 0 1.5rem;
line-height: 0.95; line-height: 0.95;
@@ -1552,6 +1559,15 @@ html {
min-height: 400px; min-height: 400px;
} }
.cl-spaces__col {
display: flex;
flex-direction: column;
}
.cl-spaces__col .cl-section-title {
font-size: 1.3rem;
}
.cl-spaces__full { .cl-spaces__full {
display: block; display: block;
} }
@@ -1663,26 +1679,28 @@ html {
overflow: hidden; overflow: hidden;
} }
/* ── FAQ Accordion ── */ /* ── FAQ Card Accordion ── */
.cl-faq { .cl-faq {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0; gap: 0.6rem;
} }
.cl-faq__title { .cl-faq__card {
font-size: 1.2rem; background: var(--cl-faq-card-bg, var(--cl-card));
font-weight: 800; border: 1px solid var(--cl-border);
color: var(--cl-text-strong); border-radius: var(--cl-radius-sm);
margin: 0 0 1rem; padding: 0 1.2rem;
transition: border-color 0.3s, box-shadow 0.3s;
} }
.cl-faq__item { .cl-faq__card:hover {
border-bottom: 1px solid var(--cl-border); border-color: var(--cl-border-hover);
} }
.cl-faq__item:first-of-type { .cl-faq__card[open] {
border-top: 1px solid var(--cl-border); border-color: var(--cl-border-hover);
box-shadow: 0 4px 16px var(--cl-shadow);
} }
.cl-faq__question { .cl-faq__question {
@@ -1719,7 +1737,7 @@ html {
transition: transform 0.3s ease, border-color 0.2s; transition: transform 0.3s ease, border-color 0.2s;
} }
.cl-faq__item[open] > .cl-faq__question::after { .cl-faq__card[open] > .cl-faq__question::after {
transform: translateY(-30%) rotate(-135deg); transform: translateY(-30%) rotate(-135deg);
border-color: var(--cl-accent); border-color: var(--cl-accent);
} }
@@ -1930,7 +1948,6 @@ html {
color: var(--cl-muted); color: var(--cl-muted);
font-size: 0.88rem; font-size: 0.88rem;
line-height: 1.7; line-height: 1.7;
max-width: 700px;
} }
/* ═══════════════════════════════════════════════════════════════════ /* ═══════════════════════════════════════════════════════════════════

View File

@@ -4,7 +4,7 @@ en:
community_landing_enabled: "Enable the community landing page." community_landing_enabled: "Enable the community landing page."
# ── Layout ── # ── Layout ──
section_order: "━━ LAYOUT ━━ — Order of content sections, pipe-separated. IDs: hero, stats, about, participation, topics, groups, app_cta. Navbar and footer are always fixed." section_order: "━━ LAYOUT ━━ — Order of content sections. Use the arrows to reorder. Available sections: hero, stats, about, participation, topics, groups, app_cta. Navbar and footer are always fixed."
# ── Custom CSS ── # ── Custom CSS ──
custom_css: "━━ CUSTOM CSS ━━ — Raw CSS injected after all plugin styles. Use for overrides and tweaks. No style tags needed." custom_css: "━━ CUSTOM CSS ━━ — Raw CSS injected after all plugin styles. Use for overrides and tweaks. No style tags needed."
@@ -26,10 +26,19 @@ en:
accent_hover_color: "Accent color on hover states. Should be slightly lighter or darker than the primary accent. Hex value." accent_hover_color: "Accent color on hover states. Should be slightly lighter or darker than the primary accent. Hex value."
dark_bg_color: "Overall page background color for dark mode. Hex value." dark_bg_color: "Overall page background color for dark mode. Hex value."
light_bg_color: "Overall page background color for light mode. Hex value." light_bg_color: "Overall page background color for light mode. Hex value."
orb_color: "Color of the decorative background orbs. Leave blank to use the accent color."
orb_opacity: "Opacity of the background orbs (0100). Default: 50."
# ── Scroll Animations ── # ── Scroll Animations ──
scroll_animation: "━━ SCROLL ANIMATIONS ━━ — How sections animate into view on scroll. Options: fade_up, fade_in, slide_left, slide_right, zoom_in, flip_up, or none." scroll_animation: "━━ SCROLL ANIMATIONS ━━ — How sections animate into view on scroll. Options: fade_up, fade_in, slide_left, slide_right, zoom_in, flip_up, or none."
# ── Fonts ──
google_font_name: "━━ FONTS ━━ — Google Font family name for body text. Must match exact Google Fonts name (e.g. 'Inter', 'Poppins'). Default: Outfit."
title_font_name: "Separate Google Font for section titles and headings. Leave blank to use the body font. Must match exact Google Fonts name."
# ── Icons ──
fontawesome_enabled: "━━ ICONS ━━ — Enable FontAwesome 6 Free icons. Loads the icon library from CDN for use on buttons."
# ── 1. Navbar ── # ── 1. Navbar ──
navbar_signin_label: "━━ ROW 1: NAVBAR ━━ — Fixed navigation bar at the top with logo, theme toggle, sign-in link, and join button. This setting controls the sign-in link text." navbar_signin_label: "━━ ROW 1: NAVBAR ━━ — Fixed navigation bar at the top with logo, theme toggle, sign-in link, and join button. This setting controls the sign-in link text."
navbar_signin_enabled: "Show the sign-in link in the navbar." navbar_signin_enabled: "Show the sign-in link in the navbar."
@@ -47,9 +56,14 @@ en:
social_youtube_url: "YouTube channel URL. Leave blank to hide." social_youtube_url: "YouTube channel URL. Leave blank to hide."
social_tiktok_url: "TikTok profile URL. Leave blank to hide." social_tiktok_url: "TikTok profile URL. Leave blank to hide."
social_github_url: "GitHub organization or profile URL. Leave blank to hide." social_github_url: "GitHub organization or profile URL. Leave blank to hide."
navbar_signin_icon: "FontAwesome icon name for the sign-in button (e.g. 'right-to-bracket'). Leave blank for no icon. Requires FontAwesome enabled."
navbar_signin_icon_position: "Show the icon before or after the sign-in button label."
navbar_join_icon: "FontAwesome icon name for the join button (e.g. 'user-plus'). Leave blank for no icon. Requires FontAwesome enabled."
navbar_join_icon_position: "Show the icon before or after the join button label."
# ── 2. Hero Section ── # ── 2. Hero Section ──
hero_title: "━━ ROW 2: HERO ━━ — Large welcome area at the top with headline, subtitle, CTA buttons, and optional imagery. This is the main headline text." hero_title: "━━ ROW 2: HERO ━━ — Large welcome area at the top with headline, subtitle, CTA buttons, and optional imagery. This is the main headline text."
hero_title_size: "Hero title font size in pixels. 0 = use default responsive size."
hero_accent_word: "Which word in the title gets the accent shimmer animation. 0 = last word (default). 1 = first word, 2 = second word, etc." hero_accent_word: "Which word in the title gets the accent shimmer animation. 0 = last word (default). 1 = first word, 2 = second word, etc."
hero_subtitle: "Supporting text below the hero headline. Describe your community's purpose or value proposition." hero_subtitle: "Supporting text below the hero headline. Describe your community's purpose or value proposition."
hero_card_enabled: "Display the hero content inside a rounded card container with border and shadow. When off, the hero uses a flat full-width layout." hero_card_enabled: "Display the hero content inside a rounded card container with border and shadow. When off, the hero uses a flat full-width layout."
@@ -63,6 +77,10 @@ en:
hero_secondary_button_enabled: "Show the secondary CTA button in the hero section." hero_secondary_button_enabled: "Show the secondary CTA button in the hero section."
hero_secondary_button_label: "Text on the secondary (outlined) CTA button." hero_secondary_button_label: "Text on the secondary (outlined) CTA button."
hero_secondary_button_url: "URL the secondary button links to." hero_secondary_button_url: "URL the secondary button links to."
hero_primary_button_icon: "FontAwesome icon name for the primary hero button (e.g. 'rocket', 'arrow-right'). Leave blank for no icon."
hero_primary_button_icon_position: "Show the icon before or after the primary button label."
hero_secondary_button_icon: "FontAwesome icon name for the secondary hero button. Leave blank for no icon."
hero_secondary_button_icon_position: "Show the icon before or after the secondary button label."
hero_primary_btn_color_dark: "Primary button background color. Dark (left) and light (right) pickers. Leave blank for accent color." hero_primary_btn_color_dark: "Primary button background color. Dark (left) and light (right) pickers. Leave blank for accent color."
hero_primary_btn_color_light: "Light mode background for the primary button." hero_primary_btn_color_light: "Light mode background for the primary button."
hero_secondary_btn_color_dark: "Secondary button background color. Dark (left) and light (right) pickers. Leave blank for default glass style." hero_secondary_btn_color_dark: "Secondary button background color. Dark (left) and light (right) pickers. Leave blank for default glass style."
@@ -100,6 +118,7 @@ en:
stats_bg_light: "Light mode background for the stats section." stats_bg_light: "Light mode background for the stats section."
stats_min_height: "Minimum height for the stats section in pixels. Set to 0 for auto height." stats_min_height: "Minimum height for the stats section in pixels. Set to 0 for auto height."
stats_border_style: "Border style at the bottom of the stats section." stats_border_style: "Border style at the bottom of the stats section."
stats_title_size: "Stats section title font size in pixels. 0 = use default."
# ── 4. About Section ── # ── 4. About Section ──
about_enabled: "━━ ROW 4: ABOUT ━━ — Show the About section: a card with bold heading, decorative quote icon, community description, and author attribution (avatar, name, role)." about_enabled: "━━ ROW 4: ABOUT ━━ — Show the About section: a card with bold heading, decorative quote icon, community description, and author attribution (avatar, name, role)."
@@ -116,6 +135,7 @@ en:
about_bg_light: "Light mode background for the about section." about_bg_light: "Light mode background for the about section."
about_min_height: "Minimum height for the about section in pixels. Set to 0 for auto height." about_min_height: "Minimum height for the about section in pixels. Set to 0 for auto height."
about_border_style: "Border style at the bottom of the about section." about_border_style: "Border style at the bottom of the about section."
about_title_size: "About section heading font size in pixels. 0 = use default."
# ── 5. Trending Discussions ── # ── 5. Trending Discussions ──
topics_enabled: "━━ ROW 5: TRENDING ━━ — Show the Trending Discussions section: a horizontally scrollable row of topic cards showing the most active discussions. Each card displays category badge, title, reply count, and like count — all live data. Supports drag-to-scroll and native swipe." topics_enabled: "━━ ROW 5: TRENDING ━━ — Show the Trending Discussions section: a horizontally scrollable row of topic cards showing the most active discussions. Each card displays category badge, title, reply count, and like count — all live data. Supports drag-to-scroll and native swipe."
@@ -128,6 +148,7 @@ en:
topics_bg_light: "Light mode background for the trending section." topics_bg_light: "Light mode background for the trending section."
topics_min_height: "Minimum height for the trending section in pixels. Set to 0 for auto height." topics_min_height: "Minimum height for the trending section in pixels. Set to 0 for auto height."
topics_border_style: "Border style at the bottom of the trending section." topics_border_style: "Border style at the bottom of the trending section."
topics_title_size: "Trending section title font size in pixels. 0 = use default."
# ── 6. Hero Creators ── # ── 6. Hero Creators ──
contributors_enabled: "Show top 3 creators in the hero section with gold, silver, and bronze rank badges." contributors_enabled: "Show top 3 creators in the hero section with gold, silver, and bronze rank badges."
@@ -154,6 +175,7 @@ en:
participation_bg_light: "Light mode background for the participation section." participation_bg_light: "Light mode background for the participation section."
participation_min_height: "Minimum height for the participation section in pixels. Set to 0 for auto height." participation_min_height: "Minimum height for the participation section in pixels. Set to 0 for auto height."
participation_border_style: "Border style at the bottom of the participation section." participation_border_style: "Border style at the bottom of the participation section."
participation_title_size: "Participation section title font size in pixels. 0 = use default."
# ── 7. Community Spaces ── # ── 7. Community Spaces ──
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."
@@ -169,12 +191,16 @@ en:
groups_border_style: "Border style at the bottom of the spaces section." groups_border_style: "Border style at the bottom of the spaces section."
groups_show_description: "Show group description text (from the group's bio) below the group name on each card." groups_show_description: "Show group description text (from the group's bio) below the group name on each card."
groups_description_max_length: "Maximum characters for group description text (30500). Longer descriptions are truncated." groups_description_max_length: "Maximum characters for group description text (30500). Longer descriptions are truncated."
groups_title_size: "Spaces section title font size in pixels. 0 = use default."
# ── 7b. FAQ Accordion ── # ── 7b. FAQ Accordion ──
faq_enabled: "━━ FAQ ACCORDION ━━ — Show an FAQ accordion alongside the Spaces section. Only one item opens at a time." faq_enabled: "━━ FAQ ACCORDION ━━ — Show an FAQ accordion alongside the Spaces section. Only one item opens at a time."
faq_title_enabled: "Show a heading above the FAQ accordion." faq_title_enabled: "Show a heading above the FAQ accordion."
faq_title: "Heading text above the FAQ accordion." faq_title: "Heading text above the FAQ accordion."
faq_items: 'FAQ items as a JSON array. Format: [{"q":"Question","a":"Answer"}]. HTML is supported in answers.' faq_items: 'FAQ items as a JSON array. Format: [{"q":"Question","a":"Answer"}]. HTML is supported in answers.'
faq_title_size: "FAQ section title font size in pixels. 0 = use default."
faq_card_bg_dark: "FAQ card background color (dark mode). Leave blank for default card styling."
faq_card_bg_light: "FAQ card background color (light mode). Leave blank for default."
# ── 8. App Download CTA ── # ── 8. App Download CTA ──
show_app_ctas: "━━ ROW 8: APP CTA ━━ — Show the App Download CTA: a gradient banner promoting your mobile app with headline, subtitle, download badges (App Store / Google Play), and optional promotional image. Requires at least one app store URL." show_app_ctas: "━━ ROW 8: APP CTA ━━ — Show the App Download CTA: a gradient banner promoting your mobile app with headline, subtitle, download badges (App Store / Google Play), and optional promotional image. Requires at least one app store URL."
@@ -197,6 +223,7 @@ en:
app_cta_bg_light: "Light mode background for the app CTA section." app_cta_bg_light: "Light mode background for the app CTA section."
app_cta_min_height: "Minimum height for the app CTA section in pixels. Set to 0 for auto height." app_cta_min_height: "Minimum height for the app CTA section in pixels. Set to 0 for auto height."
app_cta_border_style: "Border style at the bottom of the app CTA section." app_cta_border_style: "Border style at the bottom of the app CTA section."
app_cta_title_size: "App CTA headline font size in pixels. 0 = use default."
# ── 9. Footer ── # ── 9. Footer ──
footer_description: "━━ ROW 9: FOOTER ━━ — Bottom of the page with logo, navigation links, copyright, and optional description. This adds a description paragraph above the footer bar." footer_description: "━━ ROW 9: FOOTER ━━ — Bottom of the page with logo, navigation links, copyright, and optional description. This adds a description paragraph above the footer bar."

View File

@@ -11,7 +11,7 @@ plugins:
# ══════════════════════════════════════════ # ══════════════════════════════════════════
section_order: section_order:
default: "hero|stats|about|participation|topics|groups|app_cta" default: "hero|stats|about|participation|topics|groups|app_cta"
type: string type: list
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# Custom CSS # Custom CSS
@@ -67,8 +67,16 @@ plugins:
default: "06060f" default: "06060f"
type: color type: color
light_bg_color: light_bg_color:
default: "faf6f0" default: "F2F4F7"
type: color type: color
orb_color:
default: ""
type: color
orb_opacity:
default: 50
type: integer
min: 0
max: 100
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# Scroll Animations & Effects # Scroll Animations & Effects
@@ -101,6 +109,23 @@ plugins:
default: true default: true
type: bool type: bool
# ══════════════════════════════════════════
# Fonts
# ══════════════════════════════════════════
google_font_name:
default: "Outfit"
type: string
title_font_name:
default: ""
type: string
# ══════════════════════════════════════════
# Icons (FontAwesome)
# ══════════════════════════════════════════
fontawesome_enabled:
default: false
type: bool
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 1. Navbar # 1. Navbar
# ══════════════════════════════════════════ # ══════════════════════════════════════════
@@ -157,6 +182,24 @@ plugins:
social_github_url: social_github_url:
default: "" default: ""
type: string type: string
navbar_signin_icon:
default: ""
type: string
navbar_signin_icon_position:
default: "before"
type: enum
choices:
- before
- after
navbar_join_icon:
default: ""
type: string
navbar_join_icon_position:
default: "before"
type: enum
choices:
- before
- after
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 2. Hero Section # 2. Hero Section
@@ -169,6 +212,11 @@ plugins:
type: integer type: integer
min: 0 min: 0
max: 50 max: 50
hero_title_size:
default: 0
type: integer
min: 0
max: 120
hero_subtitle: hero_subtitle:
default: "Are you ready to start your creative journey?" default: "Are you ready to start your creative journey?"
type: string type: string
@@ -207,6 +255,24 @@ plugins:
hero_secondary_button_url: hero_secondary_button_url:
default: "/login" default: "/login"
type: string type: string
hero_primary_button_icon:
default: ""
type: string
hero_primary_button_icon_position:
default: "before"
type: enum
choices:
- before
- after
hero_secondary_button_icon:
default: ""
type: string
hero_secondary_button_icon_position:
default: "before"
type: enum
choices:
- before
- after
hero_primary_btn_color_dark: hero_primary_btn_color_dark:
default: "" default: ""
type: color type: color
@@ -338,6 +404,11 @@ plugins:
- solid - solid
- dashed - dashed
- dotted - dotted
stats_title_size:
default: 0
type: integer
min: 0
max: 80
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 4. About Community Section # 4. About Community Section
@@ -391,6 +462,11 @@ plugins:
- solid - solid
- dashed - dashed
- dotted - dotted
about_title_size:
default: 0
type: integer
min: 0
max: 80
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 5. Trending Discussions Section # 5. Trending Discussions Section
@@ -432,6 +508,11 @@ plugins:
- solid - solid
- dashed - dashed
- dotted - dotted
topics_title_size:
default: 0
type: integer
min: 0
max: 80
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 6. Hero Creators (Top 3 in Hero) # 6. Hero Creators (Top 3 in Hero)
@@ -469,7 +550,7 @@ plugins:
default: "" default: ""
type: color type: color
contributors_days: contributors_days:
default: 90 default: 30
type: integer type: integer
contributors_count: contributors_count:
default: 10 default: 10
@@ -520,6 +601,11 @@ plugins:
- solid - solid
- dashed - dashed
- dotted - dotted
participation_title_size:
default: 0
type: integer
min: 0
max: 80
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 7. Community Spaces Section # 7. Community Spaces Section
@@ -572,6 +658,11 @@ plugins:
type: integer type: integer
min: 30 min: 30
max: 500 max: 500
groups_title_size:
default: 0
type: integer
min: 0
max: 80
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 7b. FAQ Accordion # 7b. FAQ Accordion
@@ -588,6 +679,17 @@ plugins:
faq_items: faq_items:
default: '[{"q":"What is this community about?","a":"A creative community focused on sharing knowledge and building together."},{"q":"How do I join?","a":"Click the Get Started button to create your free account."},{"q":"Is it free?","a":"Yes! Basic membership is completely free."}]' default: '[{"q":"What is this community about?","a":"A creative community focused on sharing knowledge and building together."},{"q":"How do I join?","a":"Click the Get Started button to create your free account."},{"q":"Is it free?","a":"Yes! Basic membership is completely free."}]'
type: text_area type: text_area
faq_title_size:
default: 0
type: integer
min: 0
max: 80
faq_card_bg_dark:
default: ""
type: color
faq_card_bg_light:
default: ""
type: color
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 8. App Download CTA Section # 8. App Download CTA Section
@@ -665,6 +767,11 @@ plugins:
- solid - solid
- dashed - dashed
- dotted - dotted
app_cta_title_size:
default: 0
type: integer
min: 0
max: 80
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 9. Footer # 9. Footer

View File

@@ -8,7 +8,7 @@ module CommunityLanding
# Top contributors # Top contributors
data[:contributors] = begin data[:contributors] = begin
if s.contributors_enabled if s.contributors_enabled || (s.participation_enabled rescue true)
User User
.joins(:posts) .joins(:posts)
.includes(:user_profile) .includes(:user_profile)

View File

@@ -68,9 +68,18 @@ module CommunityLanding
html << " data-parallax=\"#{@s.mouse_parallax_enabled}\"" html << " data-parallax=\"#{@s.mouse_parallax_enabled}\""
html << ">\n<head>\n" html << ">\n<head>\n"
html << "<meta charset=\"UTF-8\">\n" html << "<meta charset=\"UTF-8\">\n"
body_font = (@s.google_font_name.presence rescue nil) || "Outfit"
title_font = (@s.title_font_name.presence rescue nil)
font_families = [body_font]
font_families << title_font if title_font && title_font != body_font
font_params = font_families.map { |f| "family=#{f.gsub(' ', '+')}:wght@400;500;600;700;800;900" }.join("&")
html << "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n" html << "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n"
html << "<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n" html << "<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n"
html << "<link href=\"https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700;800;900&display=swap\" rel=\"stylesheet\">\n" html << "<link href=\"https://fonts.googleapis.com/css2?#{font_params}&display=swap\" rel=\"stylesheet\">\n"
if @s.fontawesome_enabled rescue false
html << "<link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css\" crossorigin=\"anonymous\">\n"
end
html << "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\">\n" html << "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\">\n"
html << "<meta name=\"color-scheme\" content=\"dark light\">\n" html << "<meta name=\"color-scheme\" content=\"dark light\">\n"
@@ -113,6 +122,17 @@ module CommunityLanding
html << @styles.color_overrides html << @styles.color_overrides
html << @styles.section_backgrounds html << @styles.section_backgrounds
# Font overrides
font_css = +""
font_css << ":root { --cl-font-body: \"#{body_font}\", sans-serif;"
if title_font
font_css << " --cl-font-title: \"#{title_font}\", serif;"
else
font_css << " --cl-font-title: var(--cl-font-body);"
end
font_css << " }\n"
html << "<style>#{font_css}</style>\n"
# Custom CSS (injected last so it can override everything) # Custom CSS (injected last so it can override everything)
custom_css = @s.custom_css.presence rescue nil custom_css = @s.custom_css.presence rescue nil
if custom_css if custom_css
@@ -159,10 +179,10 @@ module CommunityLanding
html << theme_toggle html << theme_toggle
html << render_social_icons html << render_social_icons
if signin_enabled if signin_enabled
html << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--ghost\">#{e(signin_label)}</a>\n" html << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--ghost\">#{button_with_icon(signin_label, :navbar_signin_icon, :navbar_signin_icon_position)}</a>\n"
end end
if join_enabled if join_enabled
html << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--primary\">#{e(join_label)}</a>\n" html << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--primary\">#{button_with_icon(join_label, :navbar_join_icon, :navbar_join_icon_position)}</a>\n"
end end
html << "</div>" html << "</div>"
@@ -170,8 +190,8 @@ module CommunityLanding
html << "<div class=\"cl-navbar__mobile-menu\" id=\"cl-nav-links\">\n" html << "<div class=\"cl-navbar__mobile-menu\" id=\"cl-nav-links\">\n"
html << theme_toggle html << theme_toggle
html << render_social_icons html << render_social_icons
html << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--ghost\">#{e(signin_label)}</a>\n" html << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--ghost\">#{button_with_icon(signin_label, :navbar_signin_icon, :navbar_signin_icon_position)}</a>\n"
html << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--primary\">#{e(join_label)}</a>\n" html << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--primary\">#{button_with_icon(join_label, :navbar_join_icon, :navbar_join_icon_position)}</a>\n"
html << "</div>" html << "</div>"
html << "</div></nav>\n" html << "</div></nav>\n"
html html
@@ -214,9 +234,9 @@ module CommunityLanding
parts << "#{e(before.join(' '))} " if before.any? parts << "#{e(before.join(' '))} " if before.any?
parts << "<span class=\"cl-hero__title-accent\">#{e(accent)}</span>" parts << "<span class=\"cl-hero__title-accent\">#{e(accent)}</span>"
parts << " #{e(after.join(' '))}" if after.any? parts << " #{e(after.join(' '))}" if after.any?
html << "<h1 class=\"cl-hero__title\">#{parts}</h1>\n" html << "<h1 class=\"cl-hero__title\"#{title_style(:hero_title_size)}>#{parts}</h1>\n"
else else
html << "<h1 class=\"cl-hero__title\"><span class=\"cl-hero__title-accent\">#{e(@s.hero_title)}</span></h1>\n" html << "<h1 class=\"cl-hero__title\"#{title_style(:hero_title_size)}><span class=\"cl-hero__title-accent\">#{e(@s.hero_title)}</span></h1>\n"
end end
html << "<p class=\"cl-hero__subtitle\">#{e(@s.hero_subtitle)}</p>\n" html << "<p class=\"cl-hero__subtitle\">#{e(@s.hero_subtitle)}</p>\n"
@@ -230,8 +250,8 @@ module CommunityLanding
if primary_on || secondary_on if primary_on || secondary_on
html << "<div class=\"cl-hero__actions\">\n" html << "<div class=\"cl-hero__actions\">\n"
html << "<a href=\"#{primary_url}\" class=\"cl-btn cl-btn--primary cl-btn--lg\">#{e(primary_label)}</a>\n" if primary_on html << "<a href=\"#{primary_url}\" class=\"cl-btn cl-btn--primary cl-btn--lg\">#{button_with_icon(primary_label, :hero_primary_button_icon, :hero_primary_button_icon_position)}</a>\n" if primary_on
html << "<a href=\"#{secondary_url}\" class=\"cl-btn cl-btn--ghost cl-btn--lg\">#{e(secondary_label)}</a>\n" if secondary_on html << "<a href=\"#{secondary_url}\" class=\"cl-btn cl-btn--ghost cl-btn--lg\">#{button_with_icon(secondary_label, :hero_secondary_button_icon, :hero_secondary_button_icon_position)}</a>\n" if secondary_on
html << "</div>\n" html << "</div>\n"
end end
@@ -321,7 +341,7 @@ module CommunityLanding
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" if show_title html << "<h2 class=\"cl-section-title\"#{title_style(:stats_title_size)}>#{e(stats_title)}</h2>\n" if show_title
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, icon_shape, card_style, round_nums, show_labels) html << stat_card(Icons::STAT_MEMBERS_SVG, stats[:members], @s.stat_members_label, icon_shape, card_style, round_nums, show_labels)
html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label, icon_shape, card_style, round_nums, show_labels) html << stat_card(Icons::STAT_TOPICS_SVG, stats[:topics], @s.stat_topics_label, icon_shape, card_style, round_nums, show_labels)
@@ -359,7 +379,7 @@ module CommunityLanding
# Right side — text content # Right side — text content
html << "<div class=\"cl-about__right\">\n" 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\"#{title_style(:about_title_size)}>#{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"
@@ -379,11 +399,19 @@ module CommunityLanding
return "" unless (@s.participation_enabled rescue true) return "" unless (@s.participation_enabled rescue true)
contributors = @data[:contributors] contributors = @data[:contributors]
return "" unless contributors&.length.to_i > 3 hero_contributors_on = (@s.contributors_enabled rescue false)
# Positions 410: users with a public bio if hero_contributors_on
bio_max = (@s.participation_bio_max_length rescue 150).to_i # Hero shows top 3, participation shows 410
return "" unless contributors&.length.to_i > 3
candidates = contributors[3..9] || [] candidates = contributors[3..9] || []
else
# Hero contributors disabled, participation shows 110
return "" unless contributors&.any?
candidates = contributors[0..9] || []
end
bio_max = (@s.participation_bio_max_length rescue 150).to_i
users_with_bio = candidates.select { |u| u.user_profile&.bio_excerpt.present? rescue false } users_with_bio = candidates.select { |u| u.user_profile&.bio_excerpt.present? rescue false }
return "" if users_with_bio.empty? return "" if users_with_bio.empty?
@@ -396,7 +424,7 @@ module CommunityLanding
html = +"" html = +""
html << "<section class=\"cl-participation cl-anim\" id=\"cl-participation\"#{section_style(border, min_h)}><div class=\"cl-container\">\n" html << "<section class=\"cl-participation cl-anim\" id=\"cl-participation\"#{section_style(border, min_h)}><div class=\"cl-container\">\n"
html << "<h2 class=\"cl-section-title\">#{e(title_text)}</h2>\n" if show_title html << "<h2 class=\"cl-section-title\"#{title_style(:participation_title_size)}>#{e(title_text)}</h2>\n" if show_title
stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : "" stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : ""
html << "<div class=\"cl-participation__grid#{stagger_class}\">\n" html << "<div class=\"cl-participation__grid#{stagger_class}\">\n"
@@ -437,7 +465,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" if show_title html << "<h2 class=\"cl-section-title\"#{title_style(:topics_title_size)}>#{e(@s.topics_title)}</h2>\n" if show_title
stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : "" stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : ""
html << "<div class=\"cl-topics__grid#{stagger_class}\">\n" html << "<div class=\"cl-topics__grid#{stagger_class}\">\n"
@@ -477,15 +505,41 @@ module CommunityLanding
html = +"" html = +""
html << "<section class=\"cl-spaces cl-anim\" id=\"cl-groups\"#{section_style(border, min_h)}><div class=\"cl-container\">\n" html << "<section class=\"cl-spaces cl-anim\" id=\"cl-groups\"#{section_style(border, min_h)}><div class=\"cl-container\">\n"
html << "<h2 class=\"cl-section-title\">#{e(@s.groups_title)}</h2>\n" if show_title
layout_class = (has_groups && faq_on) ? "cl-spaces__split" : "cl-spaces__full" if has_groups && faq_on
html << "<div class=\"#{layout_class}\">\n" # ── Split layout: both titles at same level ──
html << "<div class=\"cl-spaces__split\">\n"
# ── Left: Groups Grid ── # Left column: Groups
if has_groups html << "<div class=\"cl-spaces__col\">\n"
html << "<h2 class=\"cl-section-title\"#{title_style(:groups_title_size)}>#{e(@s.groups_title)}</h2>\n" if show_title
html << render_groups_grid(groups, show_desc, desc_max)
html << "</div>\n"
# Right column: FAQ
html << "<div class=\"cl-spaces__col\">\n"
html << render_faq
html << "</div>\n"
html << "</div>\n"
elsif has_groups
# ── Groups only (full width) ──
html << "<h2 class=\"cl-section-title\"#{title_style(:groups_title_size)}>#{e(@s.groups_title)}</h2>\n" if show_title
html << "<div class=\"cl-spaces__full\">\n"
html << render_groups_grid(groups, show_desc, desc_max)
html << "</div>\n"
else
# ── FAQ only (full width) ──
html << render_faq
end
html << "</div></section>\n"
html
end
def render_groups_grid(groups, show_desc, desc_max)
stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : "" stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : ""
html << "<div class=\"cl-spaces__left\">\n" html = +""
html << "<div class=\"cl-spaces__grid#{stagger_class}\">\n" html << "<div class=\"cl-spaces__grid#{stagger_class}\">\n"
groups.each do |group| groups.each do |group|
@@ -495,7 +549,6 @@ module CommunityLanding
light = 45 + (group.name.bytes.last.to_i % 12) light = 45 + (group.name.bytes.last.to_i % 12)
icon_color = "hsl(#{hue}, #{sat}%, #{light}%)" icon_color = "hsl(#{hue}, #{sat}%, #{light}%)"
# Group description from bio_raw
desc_text = nil desc_text = nil
if show_desc if show_desc
raw_bio = (group.bio_raw.to_s.strip rescue "") raw_bio = (group.bio_raw.to_s.strip rescue "")
@@ -521,13 +574,7 @@ module CommunityLanding
html << "</a>\n" html << "</a>\n"
end end
html << "</div>\n</div>\n" html << "</div>\n"
end
# ── Right: FAQ Accordion ──
html << render_faq if faq_on
html << "</div>\n</div></section>\n"
html html
end end
@@ -537,8 +584,8 @@ module CommunityLanding
faq_raw = @s.faq_items.presence rescue nil faq_raw = @s.faq_items.presence rescue nil
html = +"" html = +""
html << "<h2 class=\"cl-section-title\"#{title_style(:faq_title_size)}>#{e(faq_title)}</h2>\n" if faq_title_on
html << "<div class=\"cl-faq\">\n" html << "<div class=\"cl-faq\">\n"
html << "<h3 class=\"cl-faq__title\">#{e(faq_title)}</h3>\n" if faq_title_on
if faq_raw if faq_raw
begin begin
@@ -547,7 +594,7 @@ module CommunityLanding
q = item["q"].to_s q = item["q"].to_s
a = item["a"].to_s a = item["a"].to_s
next if q.blank? next if q.blank?
html << "<details class=\"cl-faq__item\" data-faq-exclusive>\n" html << "<details class=\"cl-faq__card\" data-faq-exclusive>\n"
html << "<summary class=\"cl-faq__question\">#{e(q)}</summary>\n" html << "<summary class=\"cl-faq__question\">#{e(q)}</summary>\n"
html << "<div class=\"cl-faq__answer\">#{a}</div>\n" html << "<div class=\"cl-faq__answer\">#{a}</div>\n"
html << "</details>\n" html << "</details>\n"
@@ -577,7 +624,7 @@ module CommunityLanding
html = +"" html = +""
html << "<section class=\"cl-app-cta cl-anim\" id=\"cl-app-cta\"#{section_style(border, min_h)}><div class=\"cl-container\">\n" html << "<section class=\"cl-app-cta cl-anim\" id=\"cl-app-cta\"#{section_style(border, min_h)}><div class=\"cl-container\">\n"
html << "<div class=\"cl-app-cta__inner\">\n<div class=\"cl-app-cta__content\">\n" html << "<div class=\"cl-app-cta__inner\">\n<div class=\"cl-app-cta__content\">\n"
html << "<h2 class=\"cl-app-cta__headline\">#{e(@s.app_cta_headline)}</h2>\n" html << "<h2 class=\"cl-app-cta__headline\"#{title_style(:app_cta_title_size)}>#{e(@s.app_cta_headline)}</h2>\n"
html << "<p class=\"cl-app-cta__subtext\">#{e(@s.app_cta_subtext)}</p>\n" if @s.app_cta_subtext.present? html << "<p class=\"cl-app-cta__subtext\">#{e(@s.app_cta_subtext)}</p>\n" if @s.app_cta_subtext.present?
html << "<div class=\"cl-app-cta__badges\">\n" html << "<div class=\"cl-app-cta__badges\">\n"
@@ -757,5 +804,20 @@ module CommunityLanding
def logo_height def logo_height
@logo_height ||= (@s.logo_height rescue 30) @logo_height ||= (@s.logo_height rescue 30)
end end
def title_style(setting_name)
size = (@s.public_send(setting_name) rescue 0).to_i
size > 0 ? " style=\"font-size: #{size}px\"" : ""
end
def button_with_icon(label, icon_setting, position_setting)
icon_name = (@s.public_send(icon_setting).presence rescue nil)
fa_enabled = (@s.fontawesome_enabled rescue false)
return e(label) unless icon_name && fa_enabled
position = (@s.public_send(position_setting) rescue "before")
icon_html = "<i class=\"fa-solid fa-#{e(icon_name)}\"></i>"
position == "after" ? "#{e(label)} #{icon_html}" : "#{icon_html} #{e(label)}"
end
end end
end end

View File

@@ -51,11 +51,18 @@ module CommunityLanding
topic_card_light = safe_hex(:topics_card_bg_light) topic_card_light = safe_hex(:topics_card_bg_light)
space_card_dark = safe_hex(:groups_card_bg_dark) space_card_dark = safe_hex(:groups_card_bg_dark)
space_card_light = safe_hex(:groups_card_bg_light) space_card_light = safe_hex(:groups_card_bg_light)
faq_card_dark = safe_hex(:faq_card_bg_dark)
faq_card_light = safe_hex(:faq_card_bg_light)
part_card_dark = safe_hex(:participation_card_bg_dark) part_card_dark = safe_hex(:participation_card_bg_dark)
part_card_light = safe_hex(:participation_card_bg_light) part_card_light = safe_hex(:participation_card_bg_light)
part_icon_color = safe_hex(:participation_icon_color) part_icon_color = safe_hex(:participation_icon_color)
orb_color = safe_hex(:orb_color)
orb_opacity = [[@s.orb_opacity.to_i, 0].max, 100].min rescue 50
orb_opacity = 50 if orb_opacity == 0 && (@s.orb_opacity.to_s.strip.empty? rescue true)
accent_rgb = hex_to_rgb(accent) accent_rgb = hex_to_rgb(accent)
orb_rgb = orb_color ? hex_to_rgb(orb_color) : accent_rgb
stat_icon_rgb = hex_to_rgb(stat_icon) stat_icon_rgb = hex_to_rgb(stat_icon)
stat_icon_bg_val = stat_icon_bg || "rgba(#{stat_icon_rgb}, 0.1)" stat_icon_bg_val = stat_icon_bg || "rgba(#{stat_icon_rgb}, 0.1)"
@@ -108,7 +115,9 @@ module CommunityLanding
--cl-hero-bg: #{dark_bg}; --cl-hero-bg: #{dark_bg};
--cl-gradient-text: linear-gradient(135deg, #{accent_hover}, #{accent}, #{accent_hover}); --cl-gradient-text: linear-gradient(135deg, #{accent_hover}, #{accent}, #{accent_hover});
--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(#{orb_rgb}, 0.12);
--cl-orb-2: rgba(#{orb_rgb}, 0.08);
--cl-orb-opacity: #{orb_opacity / 100.0};
--cl-stat-icon-color: #{stat_icon}; --cl-stat-icon-color: #{stat_icon};
--cl-stat-icon-bg: #{stat_icon_bg_val}; --cl-stat-icon-bg: #{stat_icon_bg_val};
--cl-stat-counter-color: #{stat_counter_val}; --cl-stat-counter-color: #{stat_counter_val};
@@ -119,6 +128,7 @@ module CommunityLanding
--cl-about-card-bg: #{about_dark_css}; --cl-about-card-bg: #{about_dark_css};
--cl-participation-card-bg: #{part_card_dark || 'var(--cl-card)'}; --cl-participation-card-bg: #{part_card_dark || 'var(--cl-card)'};
--cl-participation-icon-color: #{part_icon_color || 'var(--cl-accent)'}; --cl-participation-icon-color: #{part_icon_color || 'var(--cl-accent)'};
--cl-faq-card-bg: #{faq_card_dark || 'var(--cl-card)'};
--cl-app-gradient: linear-gradient(135deg, #{app_g1_dark}, #{app_g2_dark}, #{app_g3_dark});#{dark_extras} --cl-app-gradient: linear-gradient(135deg, #{app_g1_dark}, #{app_g2_dark}, #{app_g3_dark});#{dark_extras}
} }
[data-theme=\"light\"] { [data-theme=\"light\"] {
@@ -130,7 +140,9 @@ module CommunityLanding
--cl-hero-bg: #{light_bg}; --cl-hero-bg: #{light_bg};
--cl-gradient-text: linear-gradient(135deg, #{accent}, #{accent_hover}, #{accent}); --cl-gradient-text: linear-gradient(135deg, #{accent}, #{accent_hover}, #{accent});
--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(#{orb_rgb}, 0.08);
--cl-orb-2: rgba(#{orb_rgb}, 0.05);
--cl-orb-opacity: #{orb_opacity / 100.0};
--cl-stat-icon-color: #{stat_icon}; --cl-stat-icon-color: #{stat_icon};
--cl-stat-icon-bg: #{stat_icon_bg_val}; --cl-stat-icon-bg: #{stat_icon_bg_val};
--cl-stat-counter-color: #{stat_counter_val}; --cl-stat-counter-color: #{stat_counter_val};
@@ -141,6 +153,7 @@ module CommunityLanding
--cl-about-card-bg: #{about_light_css}; --cl-about-card-bg: #{about_light_css};
--cl-participation-card-bg: #{part_card_light || part_card_dark || 'var(--cl-card)'}; --cl-participation-card-bg: #{part_card_light || part_card_dark || 'var(--cl-card)'};
--cl-participation-icon-color: #{part_icon_color || 'var(--cl-accent)'}; --cl-participation-icon-color: #{part_icon_color || 'var(--cl-accent)'};
--cl-faq-card-bg: #{faq_card_light || faq_card_dark || 'var(--cl-card)'};
--cl-app-gradient: linear-gradient(135deg, #{app_g1_light || app_g1_dark}, #{app_g2_light || app_g2_dark}, #{app_g3_light || app_g3_dark});#{light_extras} --cl-app-gradient: linear-gradient(135deg, #{app_g1_light || app_g1_dark}, #{app_g2_light || app_g2_dark}, #{app_g3_light || app_g3_dark});#{light_extras}
} }
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
@@ -153,7 +166,9 @@ module CommunityLanding
--cl-hero-bg: #{light_bg}; --cl-hero-bg: #{light_bg};
--cl-gradient-text: linear-gradient(135deg, #{accent}, #{accent_hover}, #{accent}); --cl-gradient-text: linear-gradient(135deg, #{accent}, #{accent_hover}, #{accent});
--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(#{orb_rgb}, 0.08);
--cl-orb-2: rgba(#{orb_rgb}, 0.05);
--cl-orb-opacity: #{orb_opacity / 100.0};
--cl-stat-icon-color: #{stat_icon}; --cl-stat-icon-color: #{stat_icon};
--cl-stat-card-bg: #{stat_card_light || stat_card_dark || 'var(--cl-card)'}; --cl-stat-card-bg: #{stat_card_light || stat_card_dark || 'var(--cl-card)'};
--cl-space-card-bg: #{space_card_light || space_card_dark || 'var(--cl-card)'}; --cl-space-card-bg: #{space_card_light || space_card_dark || 'var(--cl-card)'};
@@ -161,6 +176,7 @@ module CommunityLanding
--cl-about-card-bg: #{about_light_css}; --cl-about-card-bg: #{about_light_css};
--cl-participation-card-bg: #{part_card_light || part_card_dark || 'var(--cl-card)'}; --cl-participation-card-bg: #{part_card_light || part_card_dark || 'var(--cl-card)'};
--cl-participation-icon-color: #{part_icon_color || 'var(--cl-accent)'}; --cl-participation-icon-color: #{part_icon_color || 'var(--cl-accent)'};
--cl-faq-card-bg: #{faq_card_light || faq_card_dark || 'var(--cl-card)'};
--cl-app-gradient: linear-gradient(135deg, #{app_g1_light || app_g1_dark}, #{app_g2_light || app_g2_dark}, #{app_g3_light || app_g3_dark});#{light_extras} --cl-app-gradient: linear-gradient(135deg, #{app_g1_light || app_g1_dark}, #{app_g2_light || app_g2_dark}, #{app_g3_light || app_g3_dark});#{light_extras}
} }
} }

View File

@@ -42,10 +42,10 @@ after_initialize do
base_url = Discourse.base_url base_url = Discourse.base_url
csp = "default-src 'self' #{base_url}; " \ csp = "default-src 'self' #{base_url}; " \
"script-src 'self' 'unsafe-inline'; " \ "script-src 'self' 'unsafe-inline'; " \
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " \ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; " \
"style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com; " \ "style-src-elem 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdnjs.cloudflare.com; " \
"img-src 'self' #{base_url} data: https:; " \ "img-src 'self' #{base_url} data: https:; " \
"font-src 'self' #{base_url} https://fonts.gstatic.com; " \ "font-src 'self' #{base_url} https://fonts.gstatic.com https://cdnjs.cloudflare.com; " \
"media-src 'self' https:; " \ "media-src 'self' https:; " \
"connect-src 'self' #{base_url}; " \ "connect-src 'self' #{base_url}; " \
"frame-src https://www.youtube.com https://www.youtube-nocookie.com; " \ "frame-src https://www.youtube.com https://www.youtube-nocookie.com; " \