UI Improvements v5

This commit is contained in:
2026-03-08 15:24:47 -04:00
parent b46c70a221
commit c35f312f8c
6 changed files with 143 additions and 22 deletions

View File

@@ -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_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_light_url: "Logo image URL for light mode. If not set, the dark logo is used for both themes.",
logo_height: "Logo height in pixels (1680). Applies to both navbar and footer logos.", logo_height: "Logo height in pixels (1680). 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.", footer_logo_url: "Override logo for the footer only. If not set, the navbar logo is reused.",
// ── Colors ── // ── Colors ──
@@ -71,7 +72,9 @@ 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. 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 (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: "Primary button text. Use 'icon | Label' for FA icon before or 'Label | icon' for after (e.g. 'rocket | Get Started').", 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", "community_landing_enabled",
"section_order", "custom_css", "section_order", "custom_css",
"meta_description", "og_image_url", "favicon_url", "json_ld_enabled", "meta_description", "og_image_url", "favicon_url", "json_ld_enabled",
"logo_dark_url", "logo_light_url", "logo_height", "footer_logo_url", "logo_dark_url", "logo_light_url", "logo_height", "logo_use_accent_color", "footer_logo_url",
"accent_color", "accent_hover_color", "dark_bg_color", "light_bg_color", "accent_color", "accent_hover_color", "dark_bg_color", "light_bg_color",
"orb_color", "orb_opacity", "orb_color", "orb_opacity",
"scroll_animation", "staggered_reveal_enabled", "dynamic_background_enabled", "scroll_animation", "staggered_reveal_enabled", "dynamic_background_enabled",
@@ -280,7 +283,7 @@ const TABS = [
settings: new Set([ settings: new Set([
"hero_title", "hero_accent_word", "hero_subtitle", "hero_title_size", "hero_title", "hero_accent_word", "hero_subtitle", "hero_title_size",
"hero_card_enabled", "hero_image_first", "hero_card_enabled", "hero_image_first",
"hero_background_image_url", "hero_image_urls", "hero_image_max_height", "hero_background_image_url", "hero_image_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_primary_button_enabled", "hero_primary_button_label", "hero_primary_button_url",
"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_primary_btn_color_dark", "hero_primary_btn_color_light", "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 }, logo_light_url: { label: "Upload Logo", multi: false },
footer_logo_url: { label: "Upload Logo", multi: false }, footer_logo_url: { label: "Upload Logo", multi: false },
hero_background_image_url: { label: "Upload Image", 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_image_url: { label: "Upload Image", multi: false },
about_background_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 }, ios_app_badge_image_url: { label: "Upload Badge", multi: false },
@@ -510,8 +513,10 @@ function handleTabClick(container, tabId) {
clearDisabledNotice(container); clearDisabledNotice(container);
updateActiveStates(tabId); updateActiveStates(tabId);
applyTabFilter(); applyTabFilter();
applyHeroImageVisibility(container);
injectUploadButtons(); injectUploadButtons();
updateDisabledNotice(container); updateDisabledNotice(container);
listenForHeroImageToggle(container);
} }
/** /**
@@ -760,8 +765,10 @@ function buildTabsUI() {
cleanBooleanLabels(); cleanBooleanLabels();
injectUploadButtons(); injectUploadButtons();
applyTabFilter(); applyTabFilter();
applyHeroImageVisibility(container);
updateDisabledNotice(container); updateDisabledNotice(container);
listenForEnableToggles(container); listenForEnableToggles(container);
listenForHeroImageToggle(container);
return true; return true;
} }
@@ -809,8 +816,10 @@ function buildTabsUI() {
cleanBooleanLabels(); cleanBooleanLabels();
injectUploadButtons(); injectUploadButtons();
applyTabFilter(); applyTabFilter();
applyHeroImageVisibility(container);
updateDisabledNotice(container); updateDisabledNotice(container);
listenForEnableToggles(container); listenForEnableToggles(container);
listenForHeroImageToggle(container);
return true; 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 ── // ── Image Upload Helpers ──
function getCsrfToken() { function getCsrfToken() {

View File

@@ -128,6 +128,7 @@
/* ── Smooth Scroll ── */ /* ── Smooth Scroll ── */
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
overflow-x: hidden;
} }
/* ── Focus-visible Accessibility ── */ /* ── Focus-visible Accessibility ── */
@@ -393,10 +394,12 @@ html {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; width: 100%;
max-width: 100vw;
z-index: 1000; z-index: 1000;
padding: 0.85rem 0; padding: 0.85rem 0;
transition: all 0.3s ease; transition: all 0.3s ease;
overflow: hidden;
} }
.cl-navbar.scrolled { .cl-navbar.scrolled {
@@ -422,6 +425,7 @@ html {
.cl-navbar__inner { .cl-navbar__inner {
max-width: 1200px; max-width: 1200px;
width: 100%;
margin: 0 auto; margin: 0 auto;
padding: 0 1.25rem; padding: 0 1.25rem;
display: flex; display: flex;
@@ -456,6 +460,24 @@ html {
object-fit: contain; object-fit: contain;
} }
/* Accent-colored logo via CSS mask — hidden <img> 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 { .cl-navbar__site-name {
font-size: 1.05rem; font-size: 1.05rem;
font-weight: 700; font-weight: 700;
@@ -778,6 +800,7 @@ html {
/* ── Hero Image ── */ /* ── Hero Image ── */
.cl-hero__image { .cl-hero__image {
flex: 1; flex: 1;
min-width: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -1010,20 +1033,40 @@ html {
} }
.cl-stats__grid { .cl-stats__grid {
display: grid; display: flex;
grid-template-columns: repeat(2, 1fr);
gap: 0.6rem; 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) { @media (min-width: 480px) {
.cl-stats__grid { .cl-stats__grid > * {
grid-template-columns: repeat(3, 1fr); flex: 0 0 calc(45% - 0.4rem);
} }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
.cl-stats__grid { .cl-stats__grid {
grid-template-columns: repeat(5, 1fr); overflow-x: visible;
scroll-snap-type: none;
}
.cl-stats__grid > * {
flex: 1 1 0;
} }
} }

View File

@@ -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_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_light_url: "Logo image URL for light mode. If not set, the dark logo is used for both themes."
logo_height: "Logo height in pixels (1680). Applies to both the navbar and footer logos." logo_height: "Logo height in pixels (1680). 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." footer_logo_url: "Override logo specifically for the footer. If not set, the navbar logo is reused."
# ── Appearance: Color Scheme ── # ── 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_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_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_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 (1001200)." hero_image_max_height: "Maximum height in pixels for the hero image (1001200)."
hero_primary_button_enabled: "Show the primary CTA button in the hero section." 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." hero_primary_button_label: "Text on the primary (filled, accent-colored) CTA button."

View File

@@ -50,6 +50,9 @@ plugins:
type: integer type: integer
min: 16 min: 16
max: 80 max: 80
logo_use_accent_color:
default: false
type: bool
footer_logo_url: footer_logo_url:
default: "" default: ""
type: string type: string
@@ -211,6 +214,12 @@ plugins:
hero_background_image_url: hero_background_image_url:
default: "" default: ""
type: string type: string
hero_image_url:
default: ""
type: string
hero_multiple_images_enabled:
default: false
type: bool
hero_image_urls: hero_image_urls:
default: "" default: ""
type: text_area type: text_area

View File

@@ -27,16 +27,20 @@ module CommunityLanding
parts.any? ? " style=\"#{parts.join(' ')}\"" : "" parts.any? ? " style=\"#{parts.join(' ')}\"" : ""
end end
def logo_img(url, alt, css_class, height) def logo_img(url, alt, css_class, height, accent: false)
if accent
"<span class=\"#{css_class} cl-logo--accent\" style=\"height: #{height}px; -webkit-mask-image: url('#{url}'); mask-image: url('#{url}');\"><img src=\"#{url}\" alt=\"#{e(alt)}\" style=\"height: #{height}px; visibility: hidden;\"></span>"
else
"<img src=\"#{url}\" alt=\"#{e(alt)}\" class=\"#{css_class}\" style=\"height: #{height}px;\">" "<img src=\"#{url}\" alt=\"#{e(alt)}\" class=\"#{css_class}\" style=\"height: #{height}px;\">"
end 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 if dark_url && light_url
logo_img(dark_url, site_name, "#{base_class} cl-logo--dark", 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) logo_img(light_url, site_name, "#{base_class} cl-logo--light", height, accent: accent)
else 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 end
end end

View File

@@ -165,8 +165,9 @@ module CommunityLanding
html << "<div class=\"cl-navbar__inner\">\n" html << "<div class=\"cl-navbar__inner\">\n"
html << "<div class=\"cl-navbar__left\">" html << "<div class=\"cl-navbar__left\">"
html << "<a href=\"/\" class=\"cl-navbar__brand\">" html << "<a href=\"/\" class=\"cl-navbar__brand\">"
logo_accent = (@s.logo_use_accent_color rescue false)
if has_logo? 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 else
html << "<span class=\"cl-navbar__site-name\">#{e(site_name)}</span>" html << "<span class=\"cl-navbar__site-name\">#{e(site_name)}</span>"
end end
@@ -291,13 +292,22 @@ module CommunityLanding
html << "</div>\n" html << "</div>\n"
hero_multi = (@s.hero_multiple_images_enabled rescue false)
if hero_multi
hero_image_urls_raw = (@s.hero_image_urls.presence rescue nil) 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) hero_video = (@s.hero_video_url.presence rescue nil)
blur_attr = (@s.hero_video_blur_on_hover rescue true) ? " data-blur-hover=\"true\"" : "" blur_attr = (@s.hero_video_blur_on_hover rescue true) ? " data-blur-hover=\"true\"" : ""
has_images = false has_images = false
if hero_image_urls_raw if hero_image_urls_raw
if hero_multi
urls = hero_image_urls_raw.split(/[|\n\r]+/).map(&:strip).reject(&:empty?).first(5) 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? 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
@@ -685,10 +695,11 @@ module CommunityLanding
html << "<div class=\"cl-footer__brand\">" html << "<div class=\"cl-footer__brand\">"
flogo = @s.footer_logo_url.presence flogo = @s.footer_logo_url.presence
footer_accent = (@s.logo_use_accent_color rescue false)
if flogo if flogo
html << "<img src=\"#{flogo}\" alt=\"#{e(site_name)}\" class=\"cl-footer__logo\" style=\"height: #{logo_height}px;\">" html << logo_img(flogo, site_name, "cl-footer__logo", logo_height, accent: footer_accent)
elsif has_logo? elsif has_logo?
html << render_logo(logo_dark_url, logo_light_url, site_name, "cl-footer__logo", logo_height) html << render_logo(logo_dark_url, logo_light_url, site_name, "cl-footer__logo", logo_height, accent: footer_accent)
else else
html << "<span class=\"cl-footer__site-name\">#{e(site_name)}</span>" html << "<span class=\"cl-footer__site-name\">#{e(site_name)}</span>"
end end