System Reworked v4

This commit is contained in:
2026-03-08 14:05:27 -04:00
parent 3442e615b2
commit adf3183cb8
8 changed files with 161 additions and 152 deletions

View File

@@ -46,20 +46,16 @@ const DESCRIPTIONS = {
fontawesome_enabled: "Load FontAwesome 6 Free icons from CDN for use on buttons.", fontawesome_enabled: "Load FontAwesome 6 Free icons from CDN for use on buttons.",
// ── Navbar ── // ── Navbar ──
navbar_signin_label: "Text for the sign-in link in the navbar.", navbar_signin_label: "Sign-in link text. Use 'icon | Label' for FA icon before or 'Label | icon' for after (e.g. 'right-to-bracket | Sign In').",
navbar_signin_enabled: "Show 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_dark: "Sign-in link color for dark mode. Leave blank for default.",
navbar_signin_color_light: "Sign-in link color for light mode.", 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_label: "Join button text. Use 'icon | Label' for FA icon before or 'Label | icon' for after (e.g. 'user-plus | Get Started').",
navbar_join_enabled: "Show the join/register 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_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_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_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_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_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_facebook_url: "Facebook page or profile URL. Leave blank to hide.",
social_instagram_url: "Instagram profile URL. Leave blank to hide.", social_instagram_url: "Instagram profile URL. Leave blank to hide.",
@@ -75,18 +71,14 @@ const DESCRIPTIONS = {
hero_card_enabled: "Display hero content inside a rounded card with border and shadow.", 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_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_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_urls: "Images for the right side of the hero. One URL per line, up to 5. One is shown randomly per page load. Use the Add Image button or paste URLs.",
hero_image_max_height: "Maximum height for the hero image in pixels (1001200).", 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_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_label: "Primary button text. Use 'icon | Label' for FA icon before or 'Label | icon' for after (e.g. 'rocket | Get Started').",
hero_primary_button_url: "URL the primary button links to. Relative path or absolute URL.", 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_enabled: "Show the secondary CTA button in the hero.",
hero_secondary_button_label: "Text on the secondary (outlined) CTA button.", hero_secondary_button_label: "Secondary button text. Use 'icon | Label' for FA icon before or 'Label | icon' for after (e.g. 'arrow-right | Learn More').",
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 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_dark: "Primary button background for dark mode. Leave blank for accent color.",
hero_primary_btn_color_light: "Primary button background for light mode.", 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_dark: "Secondary button background for dark mode. Leave blank for glass style.",
@@ -118,7 +110,7 @@ const DESCRIPTIONS = {
// ── Participation ── // ── Participation ──
participation_enabled: "Show Participation section: testimonial cards with leaderboard bios (positions 410).", participation_enabled: "Show Participation section: testimonial cards with leaderboard bios (positions 410).",
participation_title_enabled: "Show heading above participation cards.", participation_title_enabled: "Show heading above participation cards.",
participation_title: "Heading text above participation cards.", participation_title: "Heading text above participation cards. Use 'icon | Title' for FA icon (e.g. 'users | Participation').",
participation_bio_max_length: "Max characters from each user's bio (50500). Longer bios are truncated.", 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 and avatar border. Leave blank for accent color.", participation_icon_color: "Color for the decorative quote icon and avatar border. Leave blank for accent color.",
participation_card_bg_dark: "Participation card background for dark mode.", participation_card_bg_dark: "Participation card background for dark mode.",
@@ -128,6 +120,9 @@ const DESCRIPTIONS = {
participation_min_height: "Minimum section height in pixels. 0 = auto.", participation_min_height: "Minimum section height in pixels. 0 = auto.",
participation_border_style: "Border style at the bottom of the section.", participation_border_style: "Border style at the bottom of the section.",
participation_title_size: "Section title font size in pixels. 0 = use default.", participation_title_size: "Section title font size in pixels. 0 = use default.",
participation_topics_label: "Label for the Topics stat. Use 'icon | Label' for FA icon (e.g. 'file-lines | Topics'). Default SVG used when no icon specified.",
participation_posts_label: "Label for the Posts stat. Use 'icon | Label' for FA icon (e.g. 'comment | Posts'). Default SVG used when no icon specified.",
participation_likes_label: "Label for the Likes stat. Use 'icon | Label' for FA icon (e.g. 'heart | Likes'). Default SVG used when no icon specified.",
participation_stat_color: "Color for stat numbers (Topics, Posts, Likes). Leave blank for default text color.", participation_stat_color: "Color for stat numbers (Topics, Posts, Likes). Leave blank for default text color.",
participation_stat_label_color: "Color for stat labels below the numbers. Leave blank for muted text.", participation_stat_label_color: "Color for stat labels below the numbers. Leave blank for muted text.",
participation_bio_color: "Color for the bio excerpt text. Leave blank for default text color.", participation_bio_color: "Color for the bio excerpt text. Leave blank for default text color.",
@@ -138,7 +133,7 @@ const DESCRIPTIONS = {
stats_enabled: "Show the stats section with live community counters.", 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.", 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_enabled: "Show section heading above the stats row.",
stats_title: "Section heading text above the stats.", stats_title: "Section heading text above the stats. Use 'icon | Title' for FA icon (e.g. 'chart-bar | Premium Stats').",
stats_title_size: "Stats title font size in pixels. 0 = use default.", 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_card_style: "Stat card style: rectangle, rounded, pill, or minimal (no background).",
stat_icon_color: "Color for stat counter icons.", stat_icon_color: "Color for stat counter icons.",
@@ -161,7 +156,7 @@ const DESCRIPTIONS = {
// ── About ── // ── About ──
about_enabled: "Show the About section: card with heading, quote icon, description, and author attribution.", 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_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_heading: "Heading text at the top of the About card. Use 'icon | Title' for FA icon (e.g. 'info-circle | About Community').",
about_title: "Author or community name in the card's bottom attribution.", 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_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_role: "Subtitle below author name (e.g. 'Community Manager'). Blank = site name.",
@@ -178,7 +173,7 @@ const DESCRIPTIONS = {
// ── Trending ── // ── Trending ──
topics_enabled: "Show Trending Discussions: scrollable row of active topic cards with live data.", 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_enabled: "Show heading above the topic cards.",
topics_title: "Heading text above the topic cards.", topics_title: "Heading text above the topic cards. Use 'icon | Title' for FA icon (e.g. 'fire | Trending Discussions').",
topics_title_size: "Trending title font size in pixels. 0 = use default.", topics_title_size: "Trending title font size in pixels. 0 = use default.",
topics_count: "Number of trending topic cards to display.", topics_count: "Number of trending topic cards to display.",
topics_card_bg_dark: "Topic card background for dark mode.", topics_card_bg_dark: "Topic card background for dark mode.",
@@ -191,7 +186,7 @@ const DESCRIPTIONS = {
// ── Spaces ── // ── Spaces ──
groups_enabled: "Show Community Spaces: grid of group cards with icon, name, and member count.", 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_enabled: "Show heading above group cards.",
groups_title: "Heading text above group cards.", groups_title: "Heading text above group cards. Use 'icon | Title' for FA icon (e.g. 'layer-group | Community Spaces').",
groups_title_size: "Spaces title font size in pixels. 0 = use default.", groups_title_size: "Spaces title font size in pixels. 0 = use default.",
groups_count: "Number of group cards to display.", 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_selected: "Show only specific groups. Enter names separated by pipes (e.g. designers|developers). Blank = auto-select.",
@@ -199,15 +194,12 @@ const DESCRIPTIONS = {
groups_description_max_length: "Max characters for group descriptions (30500). Longer text is truncated.", 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_dark: "Space card background for dark mode.",
groups_card_bg_light: "Space card background for light mode.", groups_card_bg_light: "Space card background for light mode.",
groups_bg_dark: "Section background for dark mode. Leave blank for default.", splits_background_image_url: "Background image for the splits section (Groups + FAQ row).",
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 ──
faq_enabled: "Show FAQ accordion alongside the Spaces section. One item opens at a time.", 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_enabled: "Show heading above the FAQ accordion.",
faq_title: "Heading text above the FAQ.", faq_title: "Heading text above the FAQ. Use 'icon | Title' for FA icon (e.g. 'circle-question | FAQ').",
faq_title_size: "FAQ title font size in pixels. 0 = use default.", 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_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_dark: "FAQ card background for dark mode.",
@@ -221,7 +213,7 @@ const DESCRIPTIONS = {
android_app_badge_image_url: "Custom Android 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_height: "Badge height in pixels (3080).",
app_badge_style: "Badge border-radius: rounded, pill, or square.", app_badge_style: "Badge border-radius: rounded, pill, or square.",
app_cta_headline: "Bold headline in the app download banner.", app_cta_headline: "Bold headline in the app download banner. Use 'icon | Title' for FA icon (e.g. 'mobile-screen | Get the App').",
app_cta_title_size: "App CTA headline font size in pixels. 0 = use default.", app_cta_title_size: "App CTA headline font size in pixels. 0 = use default.",
app_cta_subtext: "Supporting text below the headline.", 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_dark: "Gradient start color for dark mode. Leave blank for accent.",
@@ -231,6 +223,10 @@ const DESCRIPTIONS = {
app_cta_gradient_end_dark: "Gradient end color for dark 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_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_image_url: "Promo image on the right (e.g. phone mockup). PNG for transparent bg.",
app_cta_headline_color_dark: "Headline text color for dark mode. Default: white.",
app_cta_headline_color_light: "Headline text color for light mode. Default: dark navy.",
app_cta_subtext_color_dark: "Subtext color for dark mode. Default: white at 75%.",
app_cta_subtext_color_light: "Subtext color for light mode. Default: dark navy at 70%.",
app_cta_bg_dark: "Section background for dark mode. Leave blank for default.", app_cta_bg_dark: "Section background for dark mode. Leave blank for default.",
app_cta_bg_light: "Section background for light mode.", app_cta_bg_light: "Section background for light mode.",
app_cta_min_height: "Minimum section height in pixels. 0 = auto.", app_cta_min_height: "Minimum section height in pixels. 0 = auto.",
@@ -269,8 +265,6 @@ 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"
@@ -284,9 +278,7 @@ const TABS = [
"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",
@@ -306,6 +298,7 @@ const TABS = [
"participation_enabled", "participation_title_enabled", "participation_enabled", "participation_title_enabled",
"participation_title", "participation_title_size", "participation_title", "participation_title_size",
"participation_bio_max_length", "participation_bio_max_length",
"participation_topics_label", "participation_posts_label", "participation_likes_label",
"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",
@@ -357,7 +350,7 @@ const TABS = [
"groups_count", "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" "splits_background_image_url"
]) ])
}, },
{ {
@@ -381,6 +374,8 @@ const TABS = [
"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",
"app_cta_image_url", "app_cta_image_url",
"app_cta_headline_color_dark", "app_cta_headline_color_light",
"app_cta_subtext_color_dark", "app_cta_subtext_color_light",
"app_cta_bg_dark", "app_cta_bg_light", "app_cta_min_height", "app_cta_border_style" "app_cta_bg_dark", "app_cta_bg_light", "app_cta_min_height", "app_cta_border_style"
]) ])
}, },
@@ -428,6 +423,7 @@ const IMAGE_UPLOAD_SETTINGS = {
ios_app_badge_image_url: { label: "Upload Badge", multi: false }, ios_app_badge_image_url: { label: "Upload Badge", multi: false },
android_app_badge_image_url: { label: "Upload Badge", multi: false }, android_app_badge_image_url: { label: "Upload Badge", multi: false },
app_cta_image_url: { label: "Upload Image", multi: false }, app_cta_image_url: { label: "Upload Image", multi: false },
splits_background_image_url: { label: "Upload Image", multi: false },
}; };
let currentTab = "settings"; let currentTab = "settings";
@@ -848,7 +844,8 @@ function getCsrfToken() {
async function uploadFile(file) { async function uploadFile(file) {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("type", "composer"); formData.append("type", "site_setting");
formData.append("for_site_setting", "true");
formData.append("synchronous_uploads", "true"); formData.append("synchronous_uploads", "true");
const response = await fetch("/uploads.json", { const response = await fetch("/uploads.json", {
@@ -880,28 +877,21 @@ async function pinUpload(uploadId, settingName) {
} }
function setSettingValue(row, newValue, multi) { function setSettingValue(row, newValue, multi) {
// For list-type settings (hero_image_urls), Discourse renders a .values .value-list
// or a plain text input. Try the text input first.
const input = const input =
row.querySelector('.setting-value input[type="text"]') || row.querySelector(".setting-value textarea") ||
row.querySelector(".setting-value textarea"); row.querySelector('.setting-value input[type="text"]');
if (!input) return; if (!input) return;
if (multi) { if (multi) {
// hero_image_urls is a textarea — append URL on a new line
const current = input.value.trim(); const current = input.value.trim();
input.value = current ? current + "|" + newValue : newValue; input.value = current ? current + "\n" + newValue : newValue;
} else { } else {
input.value = newValue; input.value = newValue;
} }
// Dispatch events so Discourse's admin UI picks up the change
input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true }));
// Some Discourse admin UIs require a keydown Enter to register
input.dispatchEvent(
new KeyboardEvent("keydown", { key: "Enter", keyCode: 13, bubbles: true })
);
} }
function updatePreviewThumbnail(wrapper, settingName) { function updatePreviewThumbnail(wrapper, settingName) {

View File

@@ -1345,12 +1345,23 @@ html {
} }
.cl-participation-stat__value { .cl-participation-stat__value {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 700; font-weight: 700;
color: var(--cl-participation-stat-color, var(--cl-text-strong)); color: var(--cl-participation-stat-color, var(--cl-text-strong));
line-height: 1.2; line-height: 1.2;
} }
.cl-participation-stat__icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--cl-participation-icon-color, var(--cl-accent));
}
.cl-participation-stat__label { .cl-participation-stat__label {
font-size: 0.7rem; font-size: 0.7rem;
text-transform: uppercase; text-transform: uppercase;
@@ -1594,7 +1605,6 @@ html {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
gap: 2rem; gap: 2rem;
align-items: start; align-items: start;
min-height: 400px;
} }
.cl-spaces__col { .cl-spaces__col {
@@ -1807,7 +1817,6 @@ html {
@media (max-width: 767px) { @media (max-width: 767px) {
.cl-spaces__split { .cl-spaces__split {
grid-template-columns: 1fr; grid-template-columns: 1fr;
min-height: auto;
} }
} }
@@ -1855,13 +1864,13 @@ html {
.cl-app-cta__headline { .cl-app-cta__headline {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 700; font-weight: 700;
color: #fff; color: var(--cl-cta-headline-color, #fff);
margin: 0 0 0.5rem; margin: 0 0 0.5rem;
line-height: 1.3; line-height: 1.3;
} }
.cl-app-cta__subtext { .cl-app-cta__subtext {
color: rgba(255, 255, 255, 0.75); color: var(--cl-cta-subtext-color, rgba(255, 255, 255, 0.75));
font-size: 0.9rem; font-size: 0.9rem;
margin: 0 0 1.25rem; margin: 0 0 1.25rem;
} }
@@ -1934,16 +1943,10 @@ html {
display: block; display: block;
} }
.cl-app-badge-img.cl-app-badge--rounded { .cl-app-badge-img.cl-app-badge--rounded,
border-radius: var(--cl-radius-sm); .cl-app-badge-img.cl-app-badge--pill,
}
.cl-app-badge-img.cl-app-badge--pill {
border-radius: 50px;
}
.cl-app-badge-img.cl-app-badge--square { .cl-app-badge-img.cl-app-badge--square {
border-radius: 4px; border-radius: 0;
} }
/* CTA image (right side) */ /* CTA image (right side) */

View File

@@ -56,10 +56,6 @@ 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."
@@ -77,10 +73,6 @@ 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."
@@ -185,10 +177,7 @@ en:
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_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_dark: "Space card background color. Dark (left) and light (right) pickers. Leave blank for default card styling." groups_card_bg_dark: "Space card background color. Dark (left) and light (right) pickers. Leave blank for default card styling."
groups_card_bg_light: "Light mode background for space cards." groups_card_bg_light: "Light mode background for space cards."
groups_bg_dark: "Section background color override. Dark (left) and light (right) color pickers. Leave blank for default." splits_background_image_url: "Background image for the splits section (Groups + FAQ row)."
groups_bg_light: "Light mode background for the spaces section."
groups_min_height: "Minimum height for the spaces section in pixels. Set to 0 for auto height."
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." groups_title_size: "Spaces section title font size in pixels. 0 = use default."

View File

@@ -182,24 +182,6 @@ 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
@@ -231,7 +213,7 @@ plugins:
type: string type: string
hero_image_urls: hero_image_urls:
default: "" default: ""
type: list type: text_area
hero_image_max_height: hero_image_max_height:
default: 500 default: 500
type: integer type: integer
@@ -255,24 +237,6 @@ 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
@@ -606,6 +570,15 @@ plugins:
type: integer type: integer
min: 0 min: 0
max: 80 max: 80
participation_topics_label:
default: "Topics"
type: string
participation_posts_label:
default: "Posts"
type: string
participation_likes_label:
default: "Likes"
type: string
participation_stat_color: participation_stat_color:
default: "" default: ""
type: color type: color
@@ -646,25 +619,9 @@ plugins:
groups_card_bg_light: groups_card_bg_light:
default: "" default: ""
type: color type: color
groups_bg_dark: splits_background_image_url:
default: "" default: ""
type: color type: string
groups_bg_light:
default: ""
type: color
groups_min_height:
default: 0
type: integer
min: 0
max: 2000
groups_border_style:
default: "none"
type: enum
choices:
- none
- solid
- dashed
- dotted
groups_show_description: groups_show_description:
default: true default: true
type: bool type: bool
@@ -787,6 +744,18 @@ plugins:
type: integer type: integer
min: 0 min: 0
max: 80 max: 80
app_cta_headline_color_dark:
default: ""
type: color
app_cta_headline_color_light:
default: ""
type: color
app_cta_subtext_color_dark:
default: ""
type: color
app_cta_subtext_color_light:
default: ""
type: color
# ══════════════════════════════════════════ # ══════════════════════════════════════════
# 9. Footer # 9. Footer

View File

@@ -12,6 +12,11 @@ module CommunityLanding
STAT_LIKES_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>' STAT_LIKES_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>'
STAT_CHATS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>' STAT_CHATS_SVG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>'
# Participation stat icons (16×16, inline with stat values)
PART_TOPICS_SVG = '<svg class="cl-participation-stat__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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>'
PART_POSTS_SVG = '<svg class="cl-participation-stat__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>'
PART_LIKES_SVG = '<svg class="cl-participation-stat__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>'
PLAY_SVG = '<svg class="cl-icon-play" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>' PLAY_SVG = '<svg class="cl-icon-play" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>'
COMMENT_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>' COMMENT_SVG = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>'

View File

@@ -179,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\">#{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--ghost\">#{button_with_icon(signin_label)}</a>\n"
end end
if join_enabled if join_enabled
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 << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--primary\">#{button_with_icon(join_label)}</a>\n"
end end
html << "</div>" html << "</div>"
@@ -190,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\">#{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--ghost\">#{button_with_icon(signin_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 << "<a href=\"#{login_url}\" class=\"cl-navbar__link cl-btn--primary\">#{button_with_icon(join_label)}</a>\n"
html << "</div>" html << "</div>"
html << "</div></nav>\n" html << "</div></nav>\n"
html html
@@ -250,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\">#{button_with_icon(primary_label, :hero_primary_button_icon, :hero_primary_button_icon_position)}</a>\n" if primary_on html << "<a href=\"#{primary_url}\" class=\"cl-btn cl-btn--primary cl-btn--lg\">#{button_with_icon(primary_label)}</a>\n" if primary_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 << "<a href=\"#{secondary_url}\" class=\"cl-btn cl-btn--ghost cl-btn--lg\">#{button_with_icon(secondary_label)}</a>\n" if secondary_on
html << "</div>\n" html << "</div>\n"
end end
@@ -297,7 +297,7 @@ module CommunityLanding
has_images = false has_images = false
if hero_image_urls_raw if hero_image_urls_raw
urls = hero_image_urls_raw.split("|").map(&:strip).reject(&:empty?).first(5) urls = hero_image_urls_raw.split(/[|\n\r]+/).map(&:strip).reject(&:empty?).first(5)
if urls.any? if urls.any?
has_images = true has_images = true
img_max_h = @s.hero_image_max_height rescue 500 img_max_h = @s.hero_image_max_height rescue 500
@@ -341,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\"#{title_style(:stats_title_size)}>#{e(stats_title)}</h2>\n" if show_title html << "<h2 class=\"cl-section-title\"#{title_style(:stats_title_size)}>#{button_with_icon(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)
@@ -379,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\"#{title_style(:about_title_size)}>#{e(about_heading)}</h2>\n" if about_heading_on html << "<h2 class=\"cl-about__heading\"#{title_style(:about_title_size)}>#{button_with_icon(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"
@@ -419,9 +419,14 @@ module CommunityLanding
title_text = @s.participation_title.presence || "Participation" title_text = @s.participation_title.presence || "Participation"
border = @s.participation_border_style rescue "none" border = @s.participation_border_style rescue "none"
min_h = @s.participation_min_height rescue 0 min_h = @s.participation_min_height rescue 0
topics_label = @s.participation_topics_label.presence || "Topics"
posts_label = @s.participation_posts_label.presence || "Posts"
likes_label = @s.participation_likes_label.presence || "Likes"
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\"#{title_style(:participation_title_size)}>#{e(title_text)}</h2>\n" if show_title html << "<h2 class=\"cl-section-title\"#{title_style(:participation_title_size)}>#{button_with_icon(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"
@@ -442,9 +447,9 @@ module CommunityLanding
html << "<p class=\"cl-participation-card__bio\">#{e(bio_text)}</p>\n" html << "<p class=\"cl-participation-card__bio\">#{e(bio_text)}</p>\n"
html << "</div>\n" html << "</div>\n"
html << "<div class=\"cl-participation-card__stats\">\n" html << "<div class=\"cl-participation-card__stats\">\n"
html << "<div class=\"cl-participation-stat\"><span class=\"cl-participation-stat__value\">#{topic_count}</span><span class=\"cl-participation-stat__label\">Topics</span></div>\n" html << participation_stat(topic_count, topics_label, Icons::PART_TOPICS_SVG)
html << "<div class=\"cl-participation-stat\"><span class=\"cl-participation-stat__value\">#{post_count}</span><span class=\"cl-participation-stat__label\">Posts</span></div>\n" html << participation_stat(post_count, posts_label, Icons::PART_POSTS_SVG)
html << "<div class=\"cl-participation-stat\"><span class=\"cl-participation-stat__value\">#{likes_received}</span><span class=\"cl-participation-stat__label\">Likes</span></div>\n" html << participation_stat(likes_received, likes_label, Icons::PART_LIKES_SVG)
html << "</div>\n" html << "</div>\n"
html << "<div class=\"cl-participation-card__footer\">\n" html << "<div class=\"cl-participation-card__footer\">\n"
html << "<img src=\"#{avatar_url}\" alt=\"#{e(user.username)}\" class=\"cl-participation-card__avatar\" loading=\"lazy\">\n" html << "<img src=\"#{avatar_url}\" alt=\"#{e(user.username)}\" class=\"cl-participation-card__avatar\" loading=\"lazy\">\n"
@@ -473,7 +478,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\"#{title_style(:topics_title_size)}>#{e(@s.topics_title)}</h2>\n" if show_title html << "<h2 class=\"cl-section-title\"#{title_style(:topics_title_size)}>#{button_with_icon(@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"
@@ -505,14 +510,16 @@ module CommunityLanding
return "" unless has_groups || faq_on return "" unless has_groups || faq_on
border = @s.groups_border_style rescue "none"
min_h = @s.groups_min_height rescue 0
show_title = @s.groups_title_enabled rescue true show_title = @s.groups_title_enabled rescue true
show_desc = @s.groups_show_description rescue true show_desc = @s.groups_show_description rescue true
desc_max = (@s.groups_description_max_length rescue 100).to_i desc_max = (@s.groups_description_max_length rescue 100).to_i
html = +"" html = +""
html << "<section class=\"cl-spaces cl-anim\" id=\"cl-groups\"#{section_style(border, min_h)}><div class=\"cl-container\">\n" groups_bg_img = (@s.splits_background_image_url.presence rescue nil)
section_style_parts = []
section_style_parts << "background: url('#{groups_bg_img}') center/cover no-repeat;" if groups_bg_img
section_attr = section_style_parts.any? ? " style=\"#{section_style_parts.join(' ')}\"" : ""
html << "<section class=\"cl-spaces cl-anim\" id=\"cl-splits\"#{section_attr}><div class=\"cl-container\">\n"
if has_groups && faq_on if has_groups && faq_on
# ── Split layout: both titles at same level ── # ── Split layout: both titles at same level ──
@@ -520,7 +527,7 @@ module CommunityLanding
# Left column: Groups # Left column: Groups
html << "<div class=\"cl-spaces__col\">\n" 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 << "<h2 class=\"cl-section-title\"#{title_style(:groups_title_size)}>#{button_with_icon(@s.groups_title)}</h2>\n" if show_title
html << render_groups_grid(groups, show_desc, desc_max) html << render_groups_grid(groups, show_desc, desc_max)
html << "</div>\n" html << "</div>\n"
@@ -532,7 +539,7 @@ module CommunityLanding
html << "</div>\n" html << "</div>\n"
elsif has_groups elsif has_groups
# ── Groups only (full width) ── # ── 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 << "<h2 class=\"cl-section-title\"#{title_style(:groups_title_size)}>#{button_with_icon(@s.groups_title)}</h2>\n" if show_title
html << "<div class=\"cl-spaces__full\">\n" html << "<div class=\"cl-spaces__full\">\n"
html << render_groups_grid(groups, show_desc, desc_max) html << render_groups_grid(groups, show_desc, desc_max)
html << "</div>\n" html << "</div>\n"
@@ -592,7 +599,7 @@ 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 << "<h2 class=\"cl-section-title\"#{title_style(:faq_title_size)}>#{button_with_icon(faq_title)}</h2>\n" if faq_title_on
html << "<div class=\"cl-faq\">\n" html << "<div class=\"cl-faq\">\n"
if faq_raw if faq_raw
@@ -632,7 +639,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\"#{title_style(:app_cta_title_size)}>#{e(@s.app_cta_headline)}</h2>\n" html << "<h2 class=\"cl-app-cta__headline\"#{title_style(:app_cta_title_size)}>#{button_with_icon(@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"
@@ -818,14 +825,50 @@ module CommunityLanding
size > 0 ? " style=\"font-size: #{size}px\"" : "" size > 0 ? " style=\"font-size: #{size}px\"" : ""
end end
def button_with_icon(label, icon_setting, position_setting) def participation_stat(count, raw_label, default_svg)
icon_name = (@s.public_send(icon_setting).presence rescue nil)
fa_enabled = (@s.fontawesome_enabled rescue false) fa_enabled = (@s.fontawesome_enabled rescue false)
return e(label) unless icon_name && fa_enabled # Parse "icon | Label" format — if present, use FA icon instead of default SVG
if fa_enabled && raw_label.include?("|")
parts = raw_label.split("|", 2).map(&:strip)
left, right = parts
if left.match?(/\A[\w-]+\z/) && left.length < 30
icon_html = "<i class=\"fa-solid fa-#{e(left)} cl-participation-stat__icon\"></i>"
label = right
elsif right.match?(/\A[\w-]+\z/) && right.length < 30
icon_html = "<i class=\"fa-solid fa-#{e(right)} cl-participation-stat__icon\"></i>"
label = left
else
icon_html = default_svg
label = raw_label
end
else
icon_html = default_svg
label = raw_label
end
"<div class=\"cl-participation-stat\">" \
"<span class=\"cl-participation-stat__value\">#{icon_html}#{count}</span>" \
"<span class=\"cl-participation-stat__label\">#{e(label)}</span>" \
"</div>\n"
end
position = (@s.public_send(position_setting) rescue "before") def button_with_icon(raw_label)
icon_html = "<i class=\"fa-solid fa-#{e(icon_name)}\"></i>" fa_enabled = (@s.fontawesome_enabled rescue false)
position == "after" ? "#{e(label)} #{icon_html}" : "#{icon_html} #{e(label)}" return e(raw_label) unless fa_enabled && raw_label.include?("|")
parts = raw_label.split("|", 2).map(&:strip)
# Try to detect which side is the icon name (no spaces, short) vs label text
left, right = parts
if left.match?(/\A[\w-]+\z/) && left.length < 30
# "iconname | Label" — icon before
icon_html = "<i class=\"fa-solid fa-#{e(left)}\"></i>"
"#{icon_html} #{e(right)}"
elsif right.match?(/\A[\w-]+\z/) && right.length < 30
# "Label | iconname" — icon after
icon_html = "<i class=\"fa-solid fa-#{e(right)}\"></i>"
"#{e(left)} #{icon_html}"
else
e(raw_label)
end
end end
end end
end end

View File

@@ -61,6 +61,10 @@ module CommunityLanding
part_bio_color = safe_hex(:participation_bio_color) part_bio_color = safe_hex(:participation_bio_color)
part_name_color = safe_hex(:participation_name_color) part_name_color = safe_hex(:participation_name_color)
part_meta_color = safe_hex(:participation_meta_color) part_meta_color = safe_hex(:participation_meta_color)
cta_headline_dark = safe_hex(:app_cta_headline_color_dark)
cta_headline_light = safe_hex(:app_cta_headline_color_light)
cta_subtext_dark = safe_hex(:app_cta_subtext_color_dark)
cta_subtext_light = safe_hex(:app_cta_subtext_color_light)
orb_color = safe_hex(:orb_color) orb_color = safe_hex(:orb_color)
orb_opacity = [[@s.orb_opacity.to_i, 0].max, 100].min rescue 50 orb_opacity = [[@s.orb_opacity.to_i, 0].max, 100].min rescue 50
@@ -139,7 +143,9 @@ module CommunityLanding
--cl-participation-name-color: #{part_name_color || 'var(--cl-text-strong)'}; --cl-participation-name-color: #{part_name_color || 'var(--cl-text-strong)'};
--cl-participation-meta-color: #{part_meta_color || 'var(--cl-participation-icon-color)'}; --cl-participation-meta-color: #{part_meta_color || 'var(--cl-participation-icon-color)'};
--cl-faq-card-bg: #{faq_card_dark || 'var(--cl-card)'}; --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});
--cl-cta-headline-color: #{cta_headline_dark || '#ffffff'};
--cl-cta-subtext-color: #{cta_subtext_dark || 'rgba(255, 255, 255, 0.75)'};#{dark_extras}
} }
[data-theme=\"light\"] { [data-theme=\"light\"] {
--cl-accent: #{accent}; --cl-accent: #{accent};
@@ -169,7 +175,9 @@ module CommunityLanding
--cl-participation-name-color: #{part_name_color || 'var(--cl-text-strong)'}; --cl-participation-name-color: #{part_name_color || 'var(--cl-text-strong)'};
--cl-participation-meta-color: #{part_meta_color || 'var(--cl-participation-icon-color)'}; --cl-participation-meta-color: #{part_meta_color || 'var(--cl-participation-icon-color)'};
--cl-faq-card-bg: #{faq_card_light || faq_card_dark || 'var(--cl-card)'}; --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});
--cl-cta-headline-color: #{cta_headline_light || '#1a1a2e'};
--cl-cta-subtext-color: #{cta_subtext_light || 'rgba(26, 26, 46, 0.7)'};#{light_extras}
} }
@media (prefers-color-scheme: light) { @media (prefers-color-scheme: light) {
:root:not([data-theme=\"dark\"]) { :root:not([data-theme=\"dark\"]) {
@@ -197,7 +205,9 @@ module CommunityLanding
--cl-participation-name-color: #{part_name_color || 'var(--cl-text-strong)'}; --cl-participation-name-color: #{part_name_color || 'var(--cl-text-strong)'};
--cl-participation-meta-color: #{part_meta_color || 'var(--cl-participation-icon-color)'}; --cl-participation-meta-color: #{part_meta_color || 'var(--cl-participation-icon-color)'};
--cl-faq-card-bg: #{faq_card_light || faq_card_dark || 'var(--cl-card)'}; --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});
--cl-cta-headline-color: #{cta_headline_light || '#1a1a2e'};
--cl-cta-subtext-color: #{cta_subtext_light || 'rgba(26, 26, 46, 0.7)'};#{light_extras}
} }
} }
</style>\n" </style>\n"
@@ -212,7 +222,6 @@ module CommunityLanding
["#cl-about", safe_hex(:about_bg_dark), safe_hex(:about_bg_light)], ["#cl-about", safe_hex(:about_bg_dark), safe_hex(:about_bg_light)],
["#cl-participation", safe_hex(:participation_bg_dark), safe_hex(:participation_bg_light)], ["#cl-participation", safe_hex(:participation_bg_dark), safe_hex(:participation_bg_light)],
["#cl-topics", safe_hex(:topics_bg_dark), safe_hex(:topics_bg_light)], ["#cl-topics", safe_hex(:topics_bg_dark), safe_hex(:topics_bg_light)],
["#cl-groups", safe_hex(:groups_bg_dark), safe_hex(:groups_bg_light)],
["#cl-app-cta", safe_hex(:app_cta_bg_dark), safe_hex(:app_cta_bg_light)], ["#cl-app-cta", safe_hex(:app_cta_bg_dark), safe_hex(:app_cta_bg_light)],
["#cl-footer", safe_hex(:footer_bg_dark), safe_hex(:footer_bg_light)], ["#cl-footer", safe_hex(:footer_bg_dark), safe_hex(:footer_bg_light)],
] ]

View File

@@ -87,6 +87,7 @@ after_initialize do
hero_background_image_url hero_image_urls about_image_url hero_background_image_url hero_image_urls about_image_url
about_background_image_url ios_app_badge_image_url about_background_image_url ios_app_badge_image_url
android_app_badge_image_url app_cta_image_url android_app_badge_image_url app_cta_image_url
splits_background_image_url
].freeze ].freeze
# POST /community-landing/admin/pin-upload # POST /community-landing/admin/pin-upload