diff --git a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js index cb53f91..3eaf17c 100644 --- a/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js +++ b/assets/javascripts/discourse/initializers/community-landing-admin-tabs.js @@ -21,6 +21,7 @@ const DESCRIPTIONS = { 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.", + logo_use_accent_color: "Tint the logo to match the accent color. Works best with monochrome SVG or PNG logos.", footer_logo_url: "Override logo for the footer only. If not set, the navbar logo is reused.", // ── Colors ── @@ -71,7 +72,9 @@ const DESCRIPTIONS = { 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. One URL per line, up to 5. One is shown randomly per page load. Use the Add Image button or paste URLs.", + hero_image_url: "Single hero image displayed on the right side of the hero. Use the upload button or paste a URL.", + hero_multiple_images_enabled: "Enable multiple hero images (up to 5) that rotate randomly on each page load. Disables the single image upload.", + 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.", 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: "Primary button text. Use 'icon | Label' for FA icon before or 'Label | icon' for after (e.g. 'rocket | Get Started').", @@ -253,7 +256,7 @@ const TABS = [ "community_landing_enabled", "section_order", "custom_css", "meta_description", "og_image_url", "favicon_url", "json_ld_enabled", - "logo_dark_url", "logo_light_url", "logo_height", "footer_logo_url", + "logo_dark_url", "logo_light_url", "logo_height", "logo_use_accent_color", "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", @@ -280,7 +283,7 @@ const TABS = [ settings: new Set([ "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_background_image_url", "hero_image_url", "hero_multiple_images_enabled", "hero_image_urls", "hero_image_max_height", "hero_primary_button_enabled", "hero_primary_button_label", "hero_primary_button_url", "hero_secondary_button_enabled", "hero_secondary_button_label", "hero_secondary_button_url", "hero_primary_btn_color_dark", "hero_primary_btn_color_light", @@ -415,7 +418,7 @@ const IMAGE_UPLOAD_SETTINGS = { logo_light_url: { label: "Upload Logo", multi: false }, footer_logo_url: { label: "Upload Logo", multi: false }, hero_background_image_url: { label: "Upload Image", multi: false }, - hero_image_urls: { label: "Add Image", multi: true }, + hero_image_url: { label: "Upload Image", multi: false }, about_image_url: { label: "Upload Image", multi: false }, about_background_image_url: { label: "Upload Image", multi: false }, ios_app_badge_image_url: { label: "Upload Badge", multi: false }, @@ -510,8 +513,10 @@ function handleTabClick(container, tabId) { clearDisabledNotice(container); updateActiveStates(tabId); applyTabFilter(); + applyHeroImageVisibility(container); injectUploadButtons(); updateDisabledNotice(container); + listenForHeroImageToggle(container); } /** @@ -760,8 +765,10 @@ function buildTabsUI() { cleanBooleanLabels(); injectUploadButtons(); applyTabFilter(); + applyHeroImageVisibility(container); updateDisabledNotice(container); listenForEnableToggles(container); + listenForHeroImageToggle(container); return true; } @@ -809,8 +816,10 @@ function buildTabsUI() { cleanBooleanLabels(); injectUploadButtons(); applyTabFilter(); + applyHeroImageVisibility(container); updateDisabledNotice(container); listenForEnableToggles(container); + listenForHeroImageToggle(container); return true; } @@ -832,6 +841,48 @@ function listenForEnableToggles(container) { }); } +// ── Conditional Visibility: Hero Single vs Multi Image ── + +const HERO_IMAGE_CONDITIONAL = { + toggle: "hero_multiple_images_enabled", + whenOff: ["hero_image_url"], // single image with upload button + whenOn: ["hero_image_urls"], // multi-image textarea (paste URLs) +}; + +function applyHeroImageVisibility(container) { + const toggleRow = container.querySelector( + `.row.setting[data-setting="${HERO_IMAGE_CONDITIONAL.toggle}"]` + ); + if (!toggleRow) return; + + const cb = toggleRow.querySelector('input[type="checkbox"]'); + const multiEnabled = cb ? cb.checked : false; + + HERO_IMAGE_CONDITIONAL.whenOff.forEach((name) => { + const row = container.querySelector(`.row.setting[data-setting="${name}"]`); + if (row) row.classList.toggle("cl-tab-hidden", multiEnabled); + }); + + HERO_IMAGE_CONDITIONAL.whenOn.forEach((name) => { + const row = container.querySelector(`.row.setting[data-setting="${name}"]`); + if (row) row.classList.toggle("cl-tab-hidden", !multiEnabled); + }); +} + +function listenForHeroImageToggle(container) { + const toggleRow = container.querySelector( + `.row.setting[data-setting="${HERO_IMAGE_CONDITIONAL.toggle}"]` + ); + if (!toggleRow) return; + + const cb = toggleRow.querySelector('input[type="checkbox"]'); + if (!cb || cb.dataset.clHeroToggleListening) return; + cb.dataset.clHeroToggleListening = "1"; + cb.addEventListener("change", () => { + applyHeroImageVisibility(container); + }); +} + // ── Image Upload Helpers ── function getCsrfToken() { diff --git a/assets/stylesheets/community_landing/landing.css b/assets/stylesheets/community_landing/landing.css index a282eae..157a532 100644 --- a/assets/stylesheets/community_landing/landing.css +++ b/assets/stylesheets/community_landing/landing.css @@ -128,6 +128,7 @@ /* ── Smooth Scroll ── */ html { scroll-behavior: smooth; + overflow-x: hidden; } /* ── Focus-visible Accessibility ── */ @@ -393,10 +394,12 @@ html { position: fixed; top: 0; left: 0; - right: 0; + width: 100%; + max-width: 100vw; z-index: 1000; padding: 0.85rem 0; transition: all 0.3s ease; + overflow: hidden; } .cl-navbar.scrolled { @@ -422,6 +425,7 @@ html { .cl-navbar__inner { max-width: 1200px; + width: 100%; margin: 0 auto; padding: 0 1.25rem; display: flex; @@ -456,6 +460,24 @@ html { object-fit: contain; } +/* Accent-colored logo via CSS mask — hidden inside provides natural dimensions */ +.cl-logo--accent { + display: inline-block; + background-color: var(--cl-accent); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: left center; + mask-position: left center; +} + +.cl-logo--accent img { + display: block; + width: auto; + visibility: hidden; +} + .cl-navbar__site-name { font-size: 1.05rem; font-weight: 700; @@ -778,6 +800,7 @@ html { /* ── Hero Image ── */ .cl-hero__image { flex: 1; + min-width: 0; display: flex; align-items: center; justify-content: center; @@ -1010,20 +1033,40 @@ html { } .cl-stats__grid { - display: grid; - grid-template-columns: repeat(2, 1fr); + display: flex; gap: 0.6rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + scroll-snap-type: x mandatory; + scroll-padding: 0 1rem; + padding-bottom: 4px; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.cl-stats__grid::-webkit-scrollbar { + display: none; +} + +.cl-stats__grid > * { + flex: 0 0 calc(70% - 0.3rem); + scroll-snap-align: start; } @media (min-width: 480px) { - .cl-stats__grid { - grid-template-columns: repeat(3, 1fr); + .cl-stats__grid > * { + flex: 0 0 calc(45% - 0.4rem); } } @media (min-width: 768px) { .cl-stats__grid { - grid-template-columns: repeat(5, 1fr); + overflow-x: visible; + scroll-snap-type: none; + } + + .cl-stats__grid > * { + flex: 1 1 0; } } diff --git a/config/locales/en.yml b/config/locales/en.yml index 1362526..4842f26 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -19,6 +19,7 @@ en: logo_dark_url: "━━ BRANDING ━━ — Logo image URL for dark mode. Displayed in the navbar and footer. Leave blank to show the site name as text." 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 the navbar and footer logos." + logo_use_accent_color: "Tint the logo to match the accent color. Works best with monochrome SVG or PNG logos." footer_logo_url: "Override logo specifically for the footer. If not set, the navbar logo is reused." # ── Appearance: Color Scheme ── @@ -65,7 +66,9 @@ en: hero_card_enabled: "Display the hero content inside a rounded card container with border and shadow. When off, the hero uses a flat full-width layout." hero_image_first: "Show the hero image above the text on mobile and to the left on desktop. When off, text appears first (default)." hero_background_image_url: "Full-bleed background image behind the hero section. In card mode, fills the card with a dark overlay. In flat mode, covers the entire section." - hero_image_urls: "Images displayed on the right side of the hero. Add up to 5 URLs — a random one is shown on each page load." + hero_image_url: "Single hero image displayed on the right side of the hero. Use the upload button or paste a URL." + hero_multiple_images_enabled: "Enable multiple hero images (up to 5) that rotate randomly on each page load. Disables the single image upload." + hero_image_urls: "Images displayed on the right side of the hero. One URL per line, up to 5. A random one is shown on each page load." hero_image_max_height: "Maximum height in pixels for the hero image (100–1200)." hero_primary_button_enabled: "Show the primary CTA button in the hero section." hero_primary_button_label: "Text on the primary (filled, accent-colored) CTA button." diff --git a/config/settings.yml b/config/settings.yml index 8fecd76..7832c82 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -50,6 +50,9 @@ plugins: type: integer min: 16 max: 80 + logo_use_accent_color: + default: false + type: bool footer_logo_url: default: "" type: string @@ -211,6 +214,12 @@ plugins: hero_background_image_url: default: "" type: string + hero_image_url: + default: "" + type: string + hero_multiple_images_enabled: + default: false + type: bool hero_image_urls: default: "" type: text_area diff --git a/lib/community_landing/helpers.rb b/lib/community_landing/helpers.rb index 297eb2e..dbf325f 100644 --- a/lib/community_landing/helpers.rb +++ b/lib/community_landing/helpers.rb @@ -27,16 +27,20 @@ module CommunityLanding parts.any? ? " style=\"#{parts.join(' ')}\"" : "" end - def logo_img(url, alt, css_class, height) - "\"#{e(alt)}\"" + def logo_img(url, alt, css_class, height, accent: false) + if accent + "\"#{e(alt)}\"" + else + "\"#{e(alt)}\"" + end end - def render_logo(dark_url, light_url, site_name, base_class, height) + def render_logo(dark_url, light_url, site_name, base_class, height, accent: false) if dark_url && light_url - logo_img(dark_url, site_name, "#{base_class} cl-logo--dark", height) + - logo_img(light_url, site_name, "#{base_class} cl-logo--light", height) + logo_img(dark_url, site_name, "#{base_class} cl-logo--dark", height, accent: accent) + + logo_img(light_url, site_name, "#{base_class} cl-logo--light", height, accent: accent) else - logo_img(dark_url || light_url, site_name, base_class, height) + logo_img(dark_url || light_url, site_name, base_class, height, accent: accent) end end end diff --git a/lib/community_landing/page_builder.rb b/lib/community_landing/page_builder.rb index b7182c8..2b7c419 100644 --- a/lib/community_landing/page_builder.rb +++ b/lib/community_landing/page_builder.rb @@ -165,8 +165,9 @@ module CommunityLanding html << "
\n" html << "
" html << "" + logo_accent = (@s.logo_use_accent_color rescue false) if has_logo? - html << render_logo(logo_dark_url, logo_light_url, site_name, "cl-navbar__logo", logo_height) + html << render_logo(logo_dark_url, logo_light_url, site_name, "cl-navbar__logo", logo_height, accent: logo_accent) else html << "#{e(site_name)}" end @@ -291,13 +292,22 @@ module CommunityLanding html << "
\n" - hero_image_urls_raw = (@s.hero_image_urls.presence rescue nil) + hero_multi = (@s.hero_multiple_images_enabled rescue false) + if hero_multi + hero_image_urls_raw = (@s.hero_image_urls.presence rescue nil) + else + hero_image_urls_raw = (@s.hero_image_url.presence rescue nil) + end hero_video = (@s.hero_video_url.presence rescue nil) blur_attr = (@s.hero_video_blur_on_hover rescue true) ? " data-blur-hover=\"true\"" : "" has_images = false if hero_image_urls_raw - urls = hero_image_urls_raw.split(/[|\n\r]+/).map(&:strip).reject(&:empty?).first(5) + if hero_multi + urls = hero_image_urls_raw.split(/[|\n\r]+/).map(&:strip).reject(&:empty?).first(5) + else + urls = [hero_image_urls_raw.strip] + end if urls.any? has_images = true img_max_h = @s.hero_image_max_height rescue 500 @@ -685,10 +695,11 @@ module CommunityLanding html << "