diff --git a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js index 5da0b4e..e875751 100644 --- a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js +++ b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js @@ -1,5 +1,244 @@ 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 (16–80). 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 (0–100). 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 (100–1200).", + 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 (0–1). 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 (200–600).", + 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, 4–10 in Participation).", + + // ── Participation ── + participation_enabled: "Show Participation section: testimonial cards with leaderboard bios (positions 4–10).", + 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 (50–500). 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 (30–500). 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 (30–80).", + 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 = [ { id: "settings", @@ -10,8 +249,10 @@ const TABS = [ "meta_description", "og_image_url", "favicon_url", "json_ld_enabled", "logo_dark_url", "logo_light_url", "logo_height", "footer_logo_url", "accent_color", "accent_hover_color", "dark_bg_color", "light_bg_color", + "orb_color", "orb_opacity", "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_join_label", "navbar_join_enabled", "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", "social_twitter_url", "social_facebook_url", "social_instagram_url", "social_youtube_url", "social_tiktok_url", "social_github_url" @@ -31,11 +274,13 @@ const TABS = [ id: "hero", label: "Hero", 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_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_icon", "hero_primary_button_icon_position", "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_secondary_btn_color_dark", "hero_secondary_btn_color_light", "hero_video_url", "hero_video_button_color", "hero_video_blur_on_hover", @@ -53,7 +298,8 @@ const TABS = [ label: "Participation", settings: new Set([ "participation_enabled", "participation_title_enabled", - "participation_title", "participation_bio_max_length", + "participation_title", "participation_title_size", + "participation_bio_max_length", "participation_icon_color", "participation_card_bg_dark", "participation_card_bg_light", "participation_bg_dark", "participation_bg_light", @@ -65,7 +311,7 @@ const TABS = [ label: "Stats", settings: new Set([ "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_members_label", "stat_topics_label", "stat_posts_label", "stat_likes_label", "stat_chats_label", "stat_round_numbers", @@ -78,7 +324,7 @@ const TABS = [ label: "About", settings: new Set([ "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_background_image_url", "about_bg_dark", "about_bg_light", "about_min_height", "about_border_style" @@ -88,21 +334,30 @@ const TABS = [ id: "topics", label: "Trending", 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_bg_dark", "topics_bg_light", "topics_min_height", "topics_border_style" ]) }, { id: "groups", - label: "Spaces & FAQ", + label: "Spaces", settings: new Set([ - "groups_enabled", "groups_title_enabled", "groups_title", "groups_count", - "groups_selected", + "groups_enabled", "groups_title_enabled", "groups_title", "groups_title_size", + "groups_count", "groups_selected", "groups_show_description", "groups_description_max_length", "groups_card_bg_dark", "groups_card_bg_light", - "groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style", - "faq_enabled", "faq_title_enabled", "faq_title", "faq_items" + "groups_bg_dark", "groups_bg_light", "groups_min_height", "groups_border_style" + ]) + }, + { + 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", "ios_app_badge_image_url", "android_app_badge_image_url", "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_mid_dark", "app_cta_gradient_mid_light", "app_cta_gradient_end_dark", "app_cta_gradient_end_light", @@ -156,6 +411,8 @@ const BG_PAIRS = [ // Spaces ["groups_card_bg_dark", "groups_card_bg_light"], ["groups_bg_dark", "groups_bg_light"], + // FAQ + ["faq_card_bg_dark", "faq_card_bg_light"], // App CTA ["app_cta_gradient_start_dark", "app_cta_gradient_start_light"], ["app_cta_gradient_mid_dark", "app_cta_gradient_mid_light"], @@ -185,8 +442,6 @@ function applyTabFilter() { if (!tab) return; 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"); row.classList.toggle( "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 const nativeNav = document.querySelector(".d-nav-submenu__tabs"); if (nativeNav) { @@ -294,15 +538,9 @@ function cleanupTabs() { el.classList.remove("cl-tab-hidden"); }); - // Unwrap merge wrappers — restore rows to their original position - container.querySelectorAll(".cl-merge-wrapper").forEach((wrapper) => { - const parent = wrapper.parentNode; - while (wrapper.firstChild) { - const child = wrapper.firstChild; - child.classList.remove("cl-merged-dark", "cl-merged-light"); - parent.insertBefore(child, wrapper); - } - wrapper.remove(); + // Remove merge classes + container.querySelectorAll(".cl-merged-dark, .cl-merged-light").forEach((el) => { + el.classList.remove("cl-merged-dark", "cl-merged-light"); }); } @@ -311,10 +549,37 @@ function cleanupTabs() { 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. - * Uses a CSS wrapper approach — both rows stay intact in the DOM - * (preserving Ember bindings and undo/reset buttons). + * CSS-only approach — elements stay in their original DOM positions + * (preserving Ember bindings, undo/reset buttons, and re-renders). */ function mergeBgPairs() { const container = getContainer(); @@ -349,17 +614,9 @@ function mergeBgPairs() { lightValue.insertBefore(lbl, lightValue.firstChild); } - // Wrap both rows in a flex container - 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 + // Just add classes — NO DOM moves, preserves all Ember bindings darkRow.classList.add("cl-merged-dark"); 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"); + injectDescriptions(); mergeBgPairs(); applyTabFilter(); return true; @@ -468,6 +726,7 @@ function buildTabsUI() { } container.classList.add("cl-tabs-active"); + injectDescriptions(); mergeBgPairs(); applyTabFilter(); return true; diff --git a/assets/stylesheets/community_landing/admin.css b/assets/stylesheets/community_landing/admin.css index fbf99fd..671c5af 100644 --- a/assets/stylesheets/community_landing/admin.css +++ b/assets/stylesheets/community_landing/admin.css @@ -82,37 +82,76 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover { color: var(--primary, #ddd); } -/* ── Merged dark/light color pairs (wrapper approach) ── */ +/* ── Merged dark/light color pairs (CSS-only, no DOM moves) ── */ -.cl-merge-wrapper { - display: flex; - gap: 16px; - width: 100%; - padding-bottom: 20px; -} - -.cl-merge-wrapper > .row.setting { - flex: 1; - min-width: 0; +.cl-tabs-active .row.setting.cl-merged-dark { + float: left; + width: calc(50% - 8px); + margin-right: 16px; padding-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 */ -.cl-merge-wrapper > .cl-merged-light > .setting-label { +.cl-tabs-active .row.setting.cl-merged-light > .setting-label { display: none; } -.cl-merge-wrapper > .cl-merged-light .desc { +.cl-tabs-active .row.setting.cl-merged-light .desc { display: none; } /* 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%; 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 { display: block; font-size: var(--font-down-1); @@ -124,12 +163,14 @@ html.dark-scheme .cl-admin-tabs .cl-admin-tab:hover { } @media (max-width: 767px) { - .cl-merge-wrapper { - flex-direction: column; - gap: 0; + .cl-tabs-active .row.setting.cl-merged-dark, + .cl-tabs-active .row.setting.cl-merged-light { + 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; } } @@ -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^="app_"], .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; } @@ -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="accent_color"], .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="navbar_signin_label"], .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="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="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="navbar_signin_label"], 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 .cl-desc, .cl-tabs-active .row.setting .validation-error { padding-top: 3px; font-size: var(--font-down-1); 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); } diff --git a/assets/stylesheets/community_landing/landing.css b/assets/stylesheets/community_landing/landing.css index d003718..e98c036 100644 --- a/assets/stylesheets/community_landing/landing.css +++ b/assets/stylesheets/community_landing/landing.css @@ -100,7 +100,7 @@ padding: 0; background: var(--cl-bg); 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; -webkit-font-smoothing: antialiased; overflow-x: hidden; @@ -177,7 +177,7 @@ html { position: absolute; border-radius: 50%; filter: blur(100px); - opacity: 0.5; + opacity: var(--cl-orb-opacity, 0.5); animation: cl-orb-float 20s infinite alternate ease-in-out; } @@ -223,6 +223,7 @@ html { .cl-section-title { font-size: 1.6rem; font-weight: 800; + font-family: var(--cl-font-title, var(--cl-font-body, 'Outfit')), sans-serif; color: var(--cl-text-strong); margin: 0 0 2.5rem; letter-spacing: -0.02em; @@ -380,6 +381,11 @@ html { 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 ═══════════════════════════════════════════════════════════════════ */ @@ -731,6 +737,7 @@ html { .cl-hero__title { font-size: clamp(2.5rem, 8vw, 4.5rem); font-weight: 900; + font-family: var(--cl-font-title, var(--cl-font-body, 'Outfit')), sans-serif; color: var(--cl-hero-text); margin: 0 0 1.5rem; line-height: 0.95; @@ -1552,6 +1559,15 @@ html { min-height: 400px; } +.cl-spaces__col { + display: flex; + flex-direction: column; +} + +.cl-spaces__col .cl-section-title { + font-size: 1.3rem; +} + .cl-spaces__full { display: block; } @@ -1663,26 +1679,28 @@ html { overflow: hidden; } -/* ── FAQ Accordion ── */ +/* ── FAQ Card Accordion ── */ .cl-faq { display: flex; flex-direction: column; - gap: 0; + gap: 0.6rem; } -.cl-faq__title { - font-size: 1.2rem; - font-weight: 800; - color: var(--cl-text-strong); - margin: 0 0 1rem; +.cl-faq__card { + background: var(--cl-faq-card-bg, var(--cl-card)); + border: 1px solid var(--cl-border); + border-radius: var(--cl-radius-sm); + padding: 0 1.2rem; + transition: border-color 0.3s, box-shadow 0.3s; } -.cl-faq__item { - border-bottom: 1px solid var(--cl-border); +.cl-faq__card:hover { + border-color: var(--cl-border-hover); } -.cl-faq__item:first-of-type { - border-top: 1px solid var(--cl-border); +.cl-faq__card[open] { + border-color: var(--cl-border-hover); + box-shadow: 0 4px 16px var(--cl-shadow); } .cl-faq__question { @@ -1719,7 +1737,7 @@ html { 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); border-color: var(--cl-accent); } @@ -1930,7 +1948,6 @@ html { color: var(--cl-muted); font-size: 0.88rem; line-height: 1.7; - max-width: 700px; } /* ═══════════════════════════════════════════════════════════════════ diff --git a/config/locales/en.yml b/config/locales/en.yml index bbb94e0..117b0a4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4,7 +4,7 @@ en: community_landing_enabled: "Enable the community landing page." # ── 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 ━━ — 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." dark_bg_color: "Overall page background color for dark 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 (0–100). Default: 50." # ── 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." + # ── 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 ── 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." @@ -47,9 +56,14 @@ en: 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." + 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 ── 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_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." @@ -63,6 +77,10 @@ en: 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_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_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." @@ -100,6 +118,7 @@ en: 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_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 ── 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_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_title_size: "About section heading font size in pixels. 0 = use default." # ── 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." @@ -128,6 +148,7 @@ en: 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_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 ── 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_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_title_size: "Participation section title font size in pixels. 0 = use default." # ── 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." @@ -169,12 +191,16 @@ en: 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_description_max_length: "Maximum characters for group description text (30–500). Longer descriptions are truncated." + groups_title_size: "Spaces section title font size in pixels. 0 = use default." # ── 7b. FAQ Accordion ── 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: "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_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 ── 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_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_title_size: "App CTA headline font size in pixels. 0 = use default." # ── 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." diff --git a/config/settings.yml b/config/settings.yml index 0ef0f91..3cb720d 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -11,7 +11,7 @@ plugins: # ══════════════════════════════════════════ section_order: default: "hero|stats|about|participation|topics|groups|app_cta" - type: string + type: list # ══════════════════════════════════════════ # Custom CSS @@ -67,8 +67,16 @@ plugins: default: "06060f" type: color light_bg_color: - default: "faf6f0" + default: "F2F4F7" type: color + orb_color: + default: "" + type: color + orb_opacity: + default: 50 + type: integer + min: 0 + max: 100 # ══════════════════════════════════════════ # Scroll Animations & Effects @@ -101,6 +109,23 @@ plugins: default: true 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 # ══════════════════════════════════════════ @@ -157,6 +182,24 @@ plugins: social_github_url: default: "" 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 @@ -169,6 +212,11 @@ plugins: type: integer min: 0 max: 50 + hero_title_size: + default: 0 + type: integer + min: 0 + max: 120 hero_subtitle: default: "Are you ready to start your creative journey?" type: string @@ -207,6 +255,24 @@ plugins: hero_secondary_button_url: default: "/login" 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: default: "" type: color @@ -338,6 +404,11 @@ plugins: - solid - dashed - dotted + stats_title_size: + default: 0 + type: integer + min: 0 + max: 80 # ══════════════════════════════════════════ # 4. About Community Section @@ -391,6 +462,11 @@ plugins: - solid - dashed - dotted + about_title_size: + default: 0 + type: integer + min: 0 + max: 80 # ══════════════════════════════════════════ # 5. Trending Discussions Section @@ -432,6 +508,11 @@ plugins: - solid - dashed - dotted + topics_title_size: + default: 0 + type: integer + min: 0 + max: 80 # ══════════════════════════════════════════ # 6. Hero Creators (Top 3 in Hero) @@ -469,7 +550,7 @@ plugins: default: "" type: color contributors_days: - default: 90 + default: 30 type: integer contributors_count: default: 10 @@ -520,6 +601,11 @@ plugins: - solid - dashed - dotted + participation_title_size: + default: 0 + type: integer + min: 0 + max: 80 # ══════════════════════════════════════════ # 7. Community Spaces Section @@ -572,6 +658,11 @@ plugins: type: integer min: 30 max: 500 + groups_title_size: + default: 0 + type: integer + min: 0 + max: 80 # ══════════════════════════════════════════ # 7b. FAQ Accordion @@ -588,6 +679,17 @@ plugins: 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."}]' 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 @@ -665,6 +767,11 @@ plugins: - solid - dashed - dotted + app_cta_title_size: + default: 0 + type: integer + min: 0 + max: 80 # ══════════════════════════════════════════ # 9. Footer diff --git a/lib/community_landing/data_fetcher.rb b/lib/community_landing/data_fetcher.rb index 8b6a44d..2069375 100644 --- a/lib/community_landing/data_fetcher.rb +++ b/lib/community_landing/data_fetcher.rb @@ -8,7 +8,7 @@ module CommunityLanding # Top contributors data[:contributors] = begin - if s.contributors_enabled + if s.contributors_enabled || (s.participation_enabled rescue true) User .joins(:posts) .includes(:user_profile) diff --git a/lib/community_landing/page_builder.rb b/lib/community_landing/page_builder.rb index fcfe1bf..cdd8c0f 100644 --- a/lib/community_landing/page_builder.rb +++ b/lib/community_landing/page_builder.rb @@ -68,9 +68,18 @@ module CommunityLanding html << " data-parallax=\"#{@s.mouse_parallax_enabled}\"" html << ">\n\n" html << "\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 << "\n" html << "\n" - html << "\n" + html << "\n" + if @s.fontawesome_enabled rescue false + html << "\n" + end html << "\n" html << "\n" @@ -113,6 +122,17 @@ module CommunityLanding html << @styles.color_overrides 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 << "\n" + # Custom CSS (injected last so it can override everything) custom_css = @s.custom_css.presence rescue nil if custom_css @@ -159,10 +179,10 @@ module CommunityLanding html << theme_toggle html << render_social_icons if signin_enabled - html << "#{e(signin_label)}\n" + html << "#{button_with_icon(signin_label, :navbar_signin_icon, :navbar_signin_icon_position)}\n" end if join_enabled - html << "#{e(join_label)}\n" + html << "#{button_with_icon(join_label, :navbar_join_icon, :navbar_join_icon_position)}\n" end html << "" @@ -170,8 +190,8 @@ module CommunityLanding html << "
\n" html << theme_toggle html << render_social_icons - html << "#{e(signin_label)}\n" - html << "#{e(join_label)}\n" + html << "#{button_with_icon(signin_label, :navbar_signin_icon, :navbar_signin_icon_position)}\n" + html << "#{button_with_icon(join_label, :navbar_join_icon, :navbar_join_icon_position)}\n" html << "
" html << "\n" html @@ -214,9 +234,9 @@ module CommunityLanding parts << "#{e(before.join(' '))} " if before.any? parts << "#{e(accent)}" parts << " #{e(after.join(' '))}" if after.any? - html << "

#{parts}

\n" + html << "

#{parts}

\n" else - html << "

#{e(@s.hero_title)}

\n" + html << "

#{e(@s.hero_title)}

\n" end html << "

#{e(@s.hero_subtitle)}

\n" @@ -230,8 +250,8 @@ module CommunityLanding if primary_on || secondary_on html << "
\n" - html << "#{e(primary_label)}\n" if primary_on - html << "#{e(secondary_label)}\n" if secondary_on + html << "#{button_with_icon(primary_label, :hero_primary_button_icon, :hero_primary_button_icon_position)}\n" if primary_on + html << "#{button_with_icon(secondary_label, :hero_secondary_button_icon, :hero_secondary_button_icon_position)}\n" if secondary_on html << "
\n" end @@ -321,7 +341,7 @@ module CommunityLanding html = +"" html << "
\n" - html << "

#{e(stats_title)}

\n" if show_title + html << "

#{e(stats_title)}

\n" if show_title html << "
\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_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 html << "
\n" - html << "

#{e(about_heading)}

\n" if about_heading_on + html << "

#{e(about_heading)}

\n" if about_heading_on html << Icons::QUOTE_SVG html << "
#{about_body}
\n" if about_body.present? html << "
\n" @@ -379,11 +399,19 @@ module CommunityLanding return "" unless (@s.participation_enabled rescue true) contributors = @data[:contributors] - return "" unless contributors&.length.to_i > 3 + hero_contributors_on = (@s.contributors_enabled rescue false) - # Positions 4–10: users with a public bio - bio_max = (@s.participation_bio_max_length rescue 150).to_i - candidates = contributors[3..9] || [] + if hero_contributors_on + # Hero shows top 3, participation shows 4–10 + return "" unless contributors&.length.to_i > 3 + candidates = contributors[3..9] || [] + else + # Hero contributors disabled, participation shows 1–10 + 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 } return "" if users_with_bio.empty? @@ -396,7 +424,7 @@ module CommunityLanding html = +"" html << "
\n" - html << "

#{e(title_text)}

\n" if show_title + html << "

#{e(title_text)}

\n" if show_title stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : "" html << "
\n" @@ -437,7 +465,7 @@ module CommunityLanding html = +"" html << "
\n" - html << "

#{e(@s.topics_title)}

\n" if show_title + html << "

#{e(@s.topics_title)}

\n" if show_title stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : "" html << "
\n" @@ -477,57 +505,76 @@ module CommunityLanding html = +"" html << "
\n" - html << "

#{e(@s.groups_title)}

\n" if show_title - layout_class = (has_groups && faq_on) ? "cl-spaces__split" : "cl-spaces__full" - html << "
\n" + if has_groups && faq_on + # ── Split layout: both titles at same level ── + html << "
\n" - # ── Left: Groups Grid ── - if has_groups - stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : "" - html << "
\n" - html << "
\n" + # Left column: Groups + html << "
\n" + html << "

#{e(@s.groups_title)}

\n" if show_title + html << render_groups_grid(groups, show_desc, desc_max) + html << "
\n" - groups.each do |group| - display_name = group.full_name.presence || group.name.tr("_-", " ").gsub(/\b\w/, &:upcase) - hue = group.name.bytes.sum % 360 - sat = 55 + (group.name.bytes.first.to_i % 15) - light = 45 + (group.name.bytes.last.to_i % 12) - icon_color = "hsl(#{hue}, #{sat}%, #{light}%)" + # Right column: FAQ + html << "
\n" + html << render_faq + html << "
\n" - # Group description from bio_raw - desc_text = nil - if show_desc - raw_bio = (group.bio_raw.to_s.strip rescue "") - if raw_bio.present? - plain = raw_bio.gsub(/<[^>]*>/, "").strip - desc_text = plain.length > desc_max ? "#{plain[0...desc_max]}..." : plain - end - end - - html << "\n" - html << "
" - if group.flair_url.present? - html << "\"\"" - else - html << "#{group.name[0].upcase}" - end - html << "
\n" - html << "
\n" - html << "#{e(display_name)}\n" - html << "#{group.user_count} members\n" - html << "

#{e(desc_text)}

\n" if desc_text - html << "
\n" - html << "
\n" - end - - html << "
\n
\n" + html << "
\n" + elsif has_groups + # ── Groups only (full width) ── + html << "

#{e(@s.groups_title)}

\n" if show_title + html << "
\n" + html << render_groups_grid(groups, show_desc, desc_max) + html << "
\n" + else + # ── FAQ only (full width) ── + html << render_faq end - # ── Right: FAQ Accordion ── - html << render_faq if faq_on + html << "
\n" + html + end - html << "
\n
\n" + def render_groups_grid(groups, show_desc, desc_max) + stagger_class = @s.staggered_reveal_enabled ? " cl-stagger" : "" + html = +"" + html << "
\n" + + groups.each do |group| + display_name = group.full_name.presence || group.name.tr("_-", " ").gsub(/\b\w/, &:upcase) + hue = group.name.bytes.sum % 360 + sat = 55 + (group.name.bytes.first.to_i % 15) + light = 45 + (group.name.bytes.last.to_i % 12) + icon_color = "hsl(#{hue}, #{sat}%, #{light}%)" + + desc_text = nil + if show_desc + raw_bio = (group.bio_raw.to_s.strip rescue "") + if raw_bio.present? + plain = raw_bio.gsub(/<[^>]*>/, "").strip + desc_text = plain.length > desc_max ? "#{plain[0...desc_max]}..." : plain + end + end + + html << "\n" + html << "
" + if group.flair_url.present? + html << "\"\"" + else + html << "#{group.name[0].upcase}" + end + html << "
\n" + html << "
\n" + html << "#{e(display_name)}\n" + html << "#{group.user_count} members\n" + html << "

#{e(desc_text)}

\n" if desc_text + html << "
\n" + html << "
\n" + end + + html << "
\n" html end @@ -537,8 +584,8 @@ module CommunityLanding faq_raw = @s.faq_items.presence rescue nil html = +"" + html << "

#{e(faq_title)}

\n" if faq_title_on html << "
\n" - html << "

#{e(faq_title)}

\n" if faq_title_on if faq_raw begin @@ -547,7 +594,7 @@ module CommunityLanding q = item["q"].to_s a = item["a"].to_s next if q.blank? - html << "
\n" + html << "
\n" html << "#{e(q)}\n" html << "
#{a}
\n" html << "
\n" @@ -577,7 +624,7 @@ module CommunityLanding html = +"" html << "
\n" html << "
\n
\n" - html << "

#{e(@s.app_cta_headline)}

\n" + html << "

#{e(@s.app_cta_headline)}

\n" html << "

#{e(@s.app_cta_subtext)}

\n" if @s.app_cta_subtext.present? html << "
\n" @@ -757,5 +804,20 @@ module CommunityLanding def logo_height @logo_height ||= (@s.logo_height rescue 30) 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 = "" + position == "after" ? "#{e(label)} #{icon_html}" : "#{icon_html} #{e(label)}" + end end end diff --git a/lib/community_landing/style_builder.rb b/lib/community_landing/style_builder.rb index 74131a0..9862975 100644 --- a/lib/community_landing/style_builder.rb +++ b/lib/community_landing/style_builder.rb @@ -51,11 +51,18 @@ module CommunityLanding topic_card_light = safe_hex(:topics_card_bg_light) space_card_dark = safe_hex(:groups_card_bg_dark) 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_light = safe_hex(:participation_card_bg_light) 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) + orb_rgb = orb_color ? hex_to_rgb(orb_color) : accent_rgb stat_icon_rgb = hex_to_rgb(stat_icon) 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-gradient-text: linear-gradient(135deg, #{accent_hover}, #{accent}, #{accent_hover}); --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-bg: #{stat_icon_bg_val}; --cl-stat-counter-color: #{stat_counter_val}; @@ -119,6 +128,7 @@ module CommunityLanding --cl-about-card-bg: #{about_dark_css}; --cl-participation-card-bg: #{part_card_dark || 'var(--cl-card)'}; --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} } [data-theme=\"light\"] { @@ -130,7 +140,9 @@ module CommunityLanding --cl-hero-bg: #{light_bg}; --cl-gradient-text: linear-gradient(135deg, #{accent}, #{accent_hover}, #{accent}); --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-bg: #{stat_icon_bg_val}; --cl-stat-counter-color: #{stat_counter_val}; @@ -141,6 +153,7 @@ module CommunityLanding --cl-about-card-bg: #{about_light_css}; --cl-participation-card-bg: #{part_card_light || part_card_dark || 'var(--cl-card)'}; --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} } @media (prefers-color-scheme: light) { @@ -153,7 +166,9 @@ module CommunityLanding --cl-hero-bg: #{light_bg}; --cl-gradient-text: linear-gradient(135deg, #{accent}, #{accent_hover}, #{accent}); --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-card-bg: #{stat_card_light || stat_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-participation-card-bg: #{part_card_light || part_card_dark || 'var(--cl-card)'}; --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} } } diff --git a/plugin.rb b/plugin.rb index f7a4a5b..c25c763 100644 --- a/plugin.rb +++ b/plugin.rb @@ -42,10 +42,10 @@ after_initialize do base_url = Discourse.base_url csp = "default-src 'self' #{base_url}; " \ "script-src 'self' 'unsafe-inline'; " \ - "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " \ - "style-src-elem '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 https://cdnjs.cloudflare.com; " \ "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:; " \ "connect-src 'self' #{base_url}; " \ "frame-src https://www.youtube.com https://www.youtube-nocookie.com; " \