fix: Broken header in public Help Center portal (#11704)
Fixes https://linear.app/chatwoot/issue/CW-4473/broken-header-in-help-center-portal
This commit is contained in:
@@ -59,17 +59,13 @@ export const openExternalLinksInNewTab = () => {
|
|||||||
|
|
||||||
export const InitializationHelpers = {
|
export const InitializationHelpers = {
|
||||||
navigateToLocalePage: () => {
|
navigateToLocalePage: () => {
|
||||||
const allLocaleSwitcher = document.querySelector('.locale-switcher');
|
document.addEventListener('change', e => {
|
||||||
|
const localeSwitcher = e.target.closest('.locale-switcher');
|
||||||
|
if (!localeSwitcher) return;
|
||||||
|
|
||||||
if (!allLocaleSwitcher) {
|
const { portalSlug } = localeSwitcher.dataset;
|
||||||
return false;
|
window.location.href = `/hc/${encodeURIComponent(portalSlug)}/${encodeURIComponent(localeSwitcher.value)}/`;
|
||||||
}
|
|
||||||
|
|
||||||
const { portalSlug } = allLocaleSwitcher.dataset;
|
|
||||||
allLocaleSwitcher.addEventListener('change', event => {
|
|
||||||
window.location = `/hc/${portalSlug}/${event.target.value}/`;
|
|
||||||
});
|
});
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
initializeSearch: () => {
|
initializeSearch: () => {
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { adjustColorForContrast } from '../shared/helpers/colorHelper.js';
|
import { adjustColorForContrast } from '../shared/helpers/colorHelper.js';
|
||||||
|
|
||||||
|
const getResolvedTheme = theme => {
|
||||||
|
// Helper to get resolved theme (handles 'system' -> 'dark'/'light')
|
||||||
|
if (theme === 'system') {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
? 'dark'
|
||||||
|
: 'light';
|
||||||
|
}
|
||||||
|
return theme;
|
||||||
|
};
|
||||||
|
|
||||||
export const setPortalHoverColor = theme => {
|
export const setPortalHoverColor = theme => {
|
||||||
// This function is to set the hover color for the portal
|
// This function is to set the hover color for the portal
|
||||||
if (theme === 'system') {
|
const resolvedTheme = getResolvedTheme(theme);
|
||||||
const prefersDarkMode = window.matchMedia(
|
|
||||||
'(prefers-color-scheme: dark)'
|
|
||||||
).matches;
|
|
||||||
theme = prefersDarkMode ? 'dark' : 'light';
|
|
||||||
}
|
|
||||||
|
|
||||||
const portalColor = window.portalConfig.portalColor;
|
const portalColor = window.portalConfig.portalColor;
|
||||||
const bgColor = theme === 'dark' ? '#151718' : 'white';
|
const bgColor = resolvedTheme === 'dark' ? '#151718' : 'white';
|
||||||
const hoverColor = adjustColorForContrast(portalColor, bgColor);
|
const hoverColor = adjustColorForContrast(portalColor, bgColor);
|
||||||
|
|
||||||
// Set hover color for border and text dynamically
|
// Set hover color for border and text dynamically
|
||||||
@@ -36,67 +40,80 @@ export const removeQueryParamsFromUrl = (queryParam = 'theme') => {
|
|||||||
export const updateThemeInHeader = theme => {
|
export const updateThemeInHeader = theme => {
|
||||||
// This function is to update the theme selection in the header in real time
|
// This function is to update the theme selection in the header in real time
|
||||||
const themeToggleButton = document.getElementById('toggle-appearance');
|
const themeToggleButton = document.getElementById('toggle-appearance');
|
||||||
|
|
||||||
if (!themeToggleButton) return;
|
if (!themeToggleButton) return;
|
||||||
const allElementInButton =
|
|
||||||
themeToggleButton.querySelectorAll('.theme-button');
|
|
||||||
|
|
||||||
if (!allElementInButton) return;
|
const allThemeButtons = themeToggleButton.querySelectorAll('.theme-button');
|
||||||
allElementInButton.forEach(button => {
|
if (!allThemeButtons.length) return;
|
||||||
button.classList.toggle('hidden', button.dataset.theme !== theme);
|
|
||||||
button.classList.toggle('flex', button.dataset.theme === theme);
|
allThemeButtons.forEach(button => {
|
||||||
|
const isActive = button.dataset.theme === theme;
|
||||||
|
button.classList.toggle('hidden', !isActive);
|
||||||
|
button.classList.toggle('flex', isActive);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const switchTheme = theme => {
|
export const switchTheme = theme => {
|
||||||
|
// Update localStorage
|
||||||
if (theme === 'system') {
|
if (theme === 'system') {
|
||||||
localStorage.removeItem('theme');
|
localStorage.removeItem('theme');
|
||||||
const prefersDarkMode = window.matchMedia(
|
|
||||||
'(prefers-color-scheme: dark)'
|
|
||||||
).matches;
|
|
||||||
// remove this so that the system theme is used
|
|
||||||
|
|
||||||
document.documentElement.classList.remove('dark', 'light');
|
|
||||||
document.documentElement.classList.add(prefersDarkMode ? 'dark' : 'light');
|
|
||||||
} else {
|
} else {
|
||||||
localStorage.theme = theme;
|
localStorage.theme = theme;
|
||||||
|
|
||||||
document.documentElement.classList.remove('dark', 'light');
|
|
||||||
document.documentElement.classList.add(theme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resolvedTheme = getResolvedTheme(theme);
|
||||||
|
document.documentElement.classList.remove('dark', 'light');
|
||||||
|
document.documentElement.classList.add(resolvedTheme);
|
||||||
|
|
||||||
setPortalHoverColor(theme);
|
setPortalHoverColor(theme);
|
||||||
updateThemeInHeader(theme);
|
updateThemeInHeader(theme);
|
||||||
removeQueryParamsFromUrl();
|
removeQueryParamsFromUrl();
|
||||||
};
|
// Update both dropdown data attributes
|
||||||
|
document.querySelectorAll('.appearance-menu').forEach(menu => {
|
||||||
export const initializeThemeSwitchButtons = () => {
|
menu.dataset.currentTheme = theme;
|
||||||
const appearanceDropdown = document.getElementById('appearance-dropdown');
|
|
||||||
appearanceDropdown.dataset.currentTheme = localStorage.theme || 'system';
|
|
||||||
|
|
||||||
appearanceDropdown.addEventListener('click', event => {
|
|
||||||
const target = event.target.closest('button[data-theme]');
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
const { theme } = target.dataset;
|
|
||||||
// setting this data property will automatically toggle the checkmark using CSS
|
|
||||||
appearanceDropdown.dataset.currentTheme = theme;
|
|
||||||
switchTheme(theme);
|
|
||||||
// wait for a bit before hiding the dropdown
|
|
||||||
appearanceDropdown.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const initializeToggleButton = () => {
|
export const initializeThemeHandlers = () => {
|
||||||
const themeToggleButton = document.getElementById('toggle-appearance');
|
const toggle = document.getElementById('toggle-appearance');
|
||||||
|
const dropdown = document.getElementById('appearance-dropdown');
|
||||||
|
if (!toggle || !dropdown) return;
|
||||||
|
|
||||||
themeToggleButton?.addEventListener('click', () => {
|
// Toggle appearance dropdown
|
||||||
const appearanceDropdown = document.getElementById('appearance-dropdown');
|
toggle.addEventListener('click', e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dropdown.dataset.dropdownOpen = String(
|
||||||
|
dropdown.dataset.dropdownOpen !== 'true'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const isCurrentlyHidden = appearanceDropdown.style.display === 'none';
|
document.addEventListener('click', ({ target }) => {
|
||||||
// Toggle the appearanceDropdown
|
if (toggle.contains(target)) return;
|
||||||
appearanceDropdown.style.display = isCurrentlyHidden ? 'flex' : 'none';
|
|
||||||
|
const themeBtn = target.closest('.appearance-menu button[data-theme]');
|
||||||
|
const menu = themeBtn?.closest('.appearance-menu');
|
||||||
|
|
||||||
|
if (themeBtn && menu) {
|
||||||
|
switchTheme(themeBtn.dataset.theme);
|
||||||
|
menu.dataset.dropdownOpen = 'false';
|
||||||
|
|
||||||
|
if (menu.id === 'mobile-appearance-dropdown') {
|
||||||
|
// Set the mobile menu toggle to false after a delay to ensure the transition is completed
|
||||||
|
setTimeout(() => {
|
||||||
|
const mobileToggle = document.getElementById('mobile-menu-toggle');
|
||||||
|
if (mobileToggle) mobileToggle.checked = false;
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the desktop appearance dropdown if clicked outside
|
||||||
|
if (
|
||||||
|
dropdown.dataset.dropdownOpen === 'true' &&
|
||||||
|
!dropdown.contains(target)
|
||||||
|
) {
|
||||||
|
dropdown.dataset.dropdownOpen = 'false';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -114,13 +131,12 @@ export const initializeTheme = () => {
|
|||||||
if (window.portalConfig.isPlainLayoutEnabled === 'true') return;
|
if (window.portalConfig.isPlainLayoutEnabled === 'true') return;
|
||||||
// start with updating the theme in the header, this will set the current theme on the button
|
// start with updating the theme in the header, this will set the current theme on the button
|
||||||
// and set the hover color at the start of init, this is set again when the theme is switched
|
// and set the hover color at the start of init, this is set again when the theme is switched
|
||||||
setPortalHoverColor(localStorage.theme || 'system');
|
switchTheme(localStorage.theme || 'system');
|
||||||
|
|
||||||
window.updateThemeInHeader = updateThemeInHeader;
|
window.updateThemeInHeader = updateThemeInHeader;
|
||||||
updateThemeInHeader(localStorage.theme || 'system');
|
|
||||||
|
|
||||||
// add the event listeners for the dropdown toggle and theme buttons
|
// add the event listeners for the dropdown toggle and theme buttons
|
||||||
initializeToggleButton();
|
initializeThemeHandlers();
|
||||||
initializeThemeSwitchButtons();
|
|
||||||
|
|
||||||
// add the media query listener to update the theme when the system theme changes
|
// add the media query listener to update the theme when the system theme changes
|
||||||
initializeMediaQueryListener();
|
initializeMediaQueryListener();
|
||||||
|
|||||||
@@ -29,22 +29,21 @@ describe('InitializationHelpers.navigateToLocalePage', () => {
|
|||||||
delete global.window;
|
delete global.window;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false if .locale-switcher is not found', () => {
|
it('sets up document event listener regardless of locale-switcher existence', () => {
|
||||||
document.querySelector('.locale-switcher').remove();
|
document.querySelector('.locale-switcher').remove();
|
||||||
const result = InitializationHelpers.navigateToLocalePage();
|
const documentSpy = vi.spyOn(document, 'addEventListener');
|
||||||
expect(result).toBe(false);
|
InitializationHelpers.navigateToLocalePage();
|
||||||
|
expect(documentSpy).toHaveBeenCalledWith('change', expect.any(Function));
|
||||||
|
documentSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add change event listener to .locale-switcher', () => {
|
it('adds document-level event listener to handle locale switching', () => {
|
||||||
const localeSwitcher = document.querySelector('.locale-switcher');
|
const documentSpy = vi.spyOn(document, 'addEventListener');
|
||||||
const addEventListenerSpy = vi.spyOn(localeSwitcher, 'addEventListener');
|
|
||||||
|
|
||||||
InitializationHelpers.navigateToLocalePage();
|
InitializationHelpers.navigateToLocalePage();
|
||||||
|
|
||||||
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
expect(documentSpy).toHaveBeenCalledWith('change', expect.any(Function));
|
||||||
'change',
|
documentSpy.mockRestore();
|
||||||
expect.any(Function)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import {
|
|||||||
removeQueryParamsFromUrl,
|
removeQueryParamsFromUrl,
|
||||||
updateThemeInHeader,
|
updateThemeInHeader,
|
||||||
switchTheme,
|
switchTheme,
|
||||||
initializeThemeSwitchButtons,
|
initializeThemeHandlers,
|
||||||
initializeToggleButton,
|
|
||||||
initializeMediaQueryListener,
|
initializeMediaQueryListener,
|
||||||
initializeTheme,
|
initializeTheme,
|
||||||
} from '../portalThemeHelper.js';
|
} from '../portalThemeHelper.js';
|
||||||
@@ -21,6 +20,7 @@ describe('portalThemeHelper', () => {
|
|||||||
|
|
||||||
appearanceDropdown = document.createElement('div');
|
appearanceDropdown = document.createElement('div');
|
||||||
appearanceDropdown.id = 'appearance-dropdown';
|
appearanceDropdown.id = 'appearance-dropdown';
|
||||||
|
appearanceDropdown.classList.add('appearance-menu');
|
||||||
document.body.appendChild(appearanceDropdown);
|
document.body.appendChild(appearanceDropdown);
|
||||||
|
|
||||||
window.matchMedia = vi.fn().mockImplementation(query => ({
|
window.matchMedia = vi.fn().mockImplementation(query => ({
|
||||||
@@ -142,7 +142,7 @@ describe('portalThemeHelper', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#initializeThemeSwitchButtons', () => {
|
describe('#initializeThemeHandlers', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
appearanceDropdown.innerHTML = `
|
appearanceDropdown.innerHTML = `
|
||||||
<button data-theme="light"><span class="check-mark-icon light-theme"></span></button>
|
<button data-theme="light"><span class="check-mark-icon light-theme"></span></button>
|
||||||
@@ -153,43 +153,58 @@ describe('portalThemeHelper', () => {
|
|||||||
|
|
||||||
it('does nothing if the appearance dropdown is not found', () => {
|
it('does nothing if the appearance dropdown is not found', () => {
|
||||||
appearanceDropdown.remove();
|
appearanceDropdown.remove();
|
||||||
expect(appearanceDropdown.dataset.currentTheme).toBeUndefined();
|
expect(() => initializeThemeHandlers()).not.toThrow();
|
||||||
});
|
|
||||||
it('should set current theme to system if no theme in localStorage', () => {
|
|
||||||
localStorage.removeItem('theme');
|
|
||||||
initializeThemeSwitchButtons();
|
|
||||||
expect(appearanceDropdown.dataset.currentTheme).toBe('system');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the current theme to the light theme', () => {
|
it('should handle theme button clicks', () => {
|
||||||
localStorage.theme = 'light';
|
initializeThemeHandlers();
|
||||||
appearanceDropdown.dataset.currentTheme = 'light';
|
|
||||||
initializeThemeSwitchButtons();
|
// Simulate clicking a theme button
|
||||||
|
const lightButton = appearanceDropdown.querySelector(
|
||||||
|
'button[data-theme="light"]'
|
||||||
|
);
|
||||||
|
const clickEvent = new Event('click', { bubbles: true });
|
||||||
|
Object.defineProperty(clickEvent, 'target', {
|
||||||
|
value: lightButton,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.dispatchEvent(clickEvent);
|
||||||
|
|
||||||
|
expect(localStorage.theme).toBe('light');
|
||||||
expect(appearanceDropdown.dataset.currentTheme).toBe('light');
|
expect(appearanceDropdown.dataset.currentTheme).toBe('light');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the current theme to the dark theme', () => {
|
it('should toggle dropdown visibility on toggle button click', () => {
|
||||||
localStorage.theme = 'dark';
|
initializeThemeHandlers();
|
||||||
appearanceDropdown.dataset.currentTheme = 'dark';
|
|
||||||
initializeThemeSwitchButtons();
|
|
||||||
expect(appearanceDropdown.dataset.currentTheme).toBe('dark');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('#initializeToggleButton', () => {
|
// Initially closed
|
||||||
it('does nothing if the theme toggle button is not found', () => {
|
expect(appearanceDropdown.dataset.dropdownOpen).toBeUndefined();
|
||||||
themeToggleButton.remove();
|
|
||||||
initializeToggleButton();
|
// Click to open
|
||||||
expect(appearanceDropdown.style.display).toBe('');
|
themeToggleButton.click();
|
||||||
|
expect(appearanceDropdown.dataset.dropdownOpen).toBe('true');
|
||||||
|
|
||||||
|
// Click to close
|
||||||
|
themeToggleButton.click();
|
||||||
|
expect(appearanceDropdown.dataset.dropdownOpen).toBe('false');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('toggles the appearance dropdown show/hide', () => {
|
it('should close dropdown when clicking outside', () => {
|
||||||
themeToggleButton.click();
|
initializeThemeHandlers();
|
||||||
appearanceDropdown.style.display = 'flex';
|
|
||||||
expect(appearanceDropdown.style.display).toBe('flex');
|
// Open dropdown
|
||||||
themeToggleButton.click();
|
appearanceDropdown.dataset.dropdownOpen = 'true';
|
||||||
appearanceDropdown.style.display = 'none';
|
|
||||||
expect(appearanceDropdown.style.display).toBe('none');
|
// Click outside
|
||||||
|
const outsideClick = new Event('click', { bubbles: true });
|
||||||
|
Object.defineProperty(outsideClick, 'target', {
|
||||||
|
value: document.body,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
document.dispatchEvent(outsideClick);
|
||||||
|
|
||||||
|
expect(appearanceDropdown.dataset.dropdownOpen).toBe('false');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
3
app/views/icons/_close.html.erb
Normal file
3
app/views/icons/_close.html.erb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 6L6 18M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 226 B |
3
app/views/icons/_hamburger.html.erb
Normal file
3
app/views/icons/_hamburger.html.erb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 12h16M4 6h16M4 18h16"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 229 B |
@@ -24,12 +24,9 @@ By default, it renders:
|
|||||||
<%= vite_client_tag %>
|
<%= vite_client_tag %>
|
||||||
<%= vite_javascript_tag 'portal' %>
|
<%= vite_javascript_tag 'portal' %>
|
||||||
<style>
|
<style>
|
||||||
#appearance-dropdown[data-current-theme="system"] .check-mark-icon.light-theme,
|
.appearance-menu[data-current-theme="system"] .check-mark-icon:is(.light-theme, .dark-theme),
|
||||||
#appearance-dropdown[data-current-theme="system"] .check-mark-icon.dark-theme,
|
.appearance-menu[data-current-theme="dark"] .check-mark-icon:is(.light-theme, .system-theme),
|
||||||
#appearance-dropdown[data-current-theme="dark"] .check-mark-icon.light-theme,
|
.appearance-menu[data-current-theme="light"] .check-mark-icon:is(.dark-theme, .system-theme) {
|
||||||
#appearance-dropdown[data-current-theme="dark"] .check-mark-icon.system-theme,
|
|
||||||
#appearance-dropdown[data-current-theme="light"] .check-mark-icon.dark-theme,
|
|
||||||
#appearance-dropdown[data-current-theme="light"] .check-mark-icon.system-theme {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<header class="sticky top-0 z-50 w-full bg-white shadow-sm dark:bg-slate-900">
|
<header class="sticky top-0 z-50 w-full bg-white shadow-sm dark:bg-slate-900">
|
||||||
<nav class="flex max-w-5xl px-4 mx-auto md:px-8" aria-label="Top">
|
<nav class="hidden sm:flex max-w-5xl px-4 mx-auto md:px-8" aria-label="Top">
|
||||||
<div class="flex items-center w-full py-5 overflow-hidden">
|
<div class="flex items-center w-full py-5 overflow-hidden">
|
||||||
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center h-10 text-lg font-semibold text-slate-900 dark:text-white">
|
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center h-10">
|
||||||
<% if @portal.logo.present? %>
|
<% if @portal.logo.present? %>
|
||||||
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" />
|
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" />
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= @portal.name %>
|
<span class="text-lg font-semibold text-slate-900 dark:text-white <%= @portal.logo.present? ? 'hidden' : 'hidden sm:block' %>"><%= @portal.name %></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -42,8 +42,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<%# Appearance dropdown section %>
|
<%# Appearance dropdown section %>
|
||||||
<div id="appearance-dropdown" data-current-theme="<%= @theme_from_params %>" class="absolute flex-col w-32 h-auto bg-white border border-solid rounded dark:bg-slate-900 top-9 ltr:right-1 rtl:left-1 border-slate-100 dark:border-slate-800" aria-hidden="true" style="display: none;" data-dropdown="appearance-dropdown">
|
<div id="appearance-dropdown"
|
||||||
<button id="toggle-theme-button" data-theme="system" class="flex flex-row items-center justify-between gap-1 px-2 py-2 border-b border-solid border-slate-100 dark:border-slate-800 stroke-slate-700 dark:stroke-slate-200 text-slate-800 dark:text-slate-100">
|
data-current-theme="<%= @theme_from_params %>"
|
||||||
|
class="appearance-menu absolute hidden flex-col w-32 h-auto bg-white border border-solid rounded dark:bg-slate-900 top-9 ltr:right-1 rtl:left-1 border-slate-100 dark:border-slate-800 shadow-lg transition-all duration-200 ease-out opacity-0 scale-95 data-[dropdown-open=true]:opacity-100 data-[dropdown-open=true]:scale-100 data-[dropdown-open=true]:flex"
|
||||||
|
aria-hidden="true"
|
||||||
|
data-dropdown="appearance-dropdown">
|
||||||
|
<button class="desktop-theme-button flex flex-row items-center justify-between gap-1 px-2 py-2 border-b border-solid border-slate-100 dark:border-slate-800 stroke-slate-700 dark:stroke-slate-200 text-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors" data-theme="system">
|
||||||
<div class="flex flex-row items-center gap-1">
|
<div class="flex flex-row items-center gap-1">
|
||||||
<%= render partial: 'icons/monitor' %>
|
<%= render partial: 'icons/monitor' %>
|
||||||
<span class="text-xs font-medium"><%= I18n.t('public_portal.header.appearance.system') %></span>
|
<span class="text-xs font-medium"><%= I18n.t('public_portal.header.appearance.system') %></span>
|
||||||
@@ -52,7 +56,7 @@
|
|||||||
<%= render partial: 'icons/check-mark' %>
|
<%= render partial: 'icons/check-mark' %>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="toggle-theme-button" data-theme="light" class="flex flex-row items-center justify-between gap-1 px-2 py-2 border-b border-solid border-slate-100 dark:border-slate-800 stroke-slate-700 dark:stroke-slate-200 text-slate-800 dark:text-slate-100">
|
<button class="desktop-theme-button flex flex-row items-center justify-between gap-1 px-2 py-2 border-b border-solid border-slate-100 dark:border-slate-800 stroke-slate-700 dark:stroke-slate-200 text-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors" data-theme="light">
|
||||||
<div class="flex flex-row items-center gap-1">
|
<div class="flex flex-row items-center gap-1">
|
||||||
<%= render partial: 'icons/sun' %>
|
<%= render partial: 'icons/sun' %>
|
||||||
<span class="text-xs font-medium"><%= I18n.t('public_portal.header.appearance.light') %></span>
|
<span class="text-xs font-medium"><%= I18n.t('public_portal.header.appearance.light') %></span>
|
||||||
@@ -61,7 +65,7 @@
|
|||||||
<%= render partial: 'icons/check-mark' %>
|
<%= render partial: 'icons/check-mark' %>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="toggle-theme-button" data-theme="dark" class="flex flex-row items-center justify-between gap-1 px-2 py-2 stroke-slate-700 dark:stroke-slate-200 text-slate-800 dark:text-slate-100">
|
<button class="desktop-theme-button flex flex-row items-center justify-between gap-1 px-2 py-2 stroke-slate-700 dark:stroke-slate-200 text-slate-800 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors rounded-b" data-theme="dark">
|
||||||
<div class="flex flex-row items-center gap-1">
|
<div class="flex flex-row items-center gap-1">
|
||||||
<%= render partial: 'icons/moon' %>
|
<%= render partial: 'icons/moon' %>
|
||||||
<span class="text-xs font-medium"><%= I18n.t('public_portal.header.appearance.dark') %></span>
|
<span class="text-xs font-medium"><%= I18n.t('public_portal.header.appearance.dark') %></span>
|
||||||
@@ -92,4 +96,22 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<nav class="flex sm:hidden max-w-5xl px-4 mx-auto" aria-label="Mobile Top">
|
||||||
|
<div class="flex items-center justify-between w-full py-5">
|
||||||
|
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center h-10 text-lg font-semibold text-slate-900 dark:text-white">
|
||||||
|
<% if @portal.logo.present? %>
|
||||||
|
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" />
|
||||||
|
<% end %>
|
||||||
|
<span class="text-lg font-semibold text-slate-900 dark:text-white <%= @portal.logo.present? ? 'hidden' : 'sm:hidden block' %>"><%= @portal.name %></span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Component -->
|
||||||
|
<%= render partial: 'public/api/v1/portals/mobile_menu', locals: {
|
||||||
|
portal: @portal,
|
||||||
|
locale: @locale,
|
||||||
|
theme_from_params: @theme_from_params
|
||||||
|
} %>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
<% if !@is_plain_layout_enabled %>
|
<% if !@is_plain_layout_enabled %>
|
||||||
<section id="portal-bg" class="w-full bg-white dark:bg-slate-900 shadow-inner">
|
<section id="portal-bg" class="w-full bg-white dark:bg-slate-900 shadow-inner">
|
||||||
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6 min-h-[240px] md:min-h-[260px]">
|
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6 min-h-[240px] md:min-h-[260px]">
|
||||||
<div class="mx-auto max-w-5xl px-4 md:px-8 flex flex-col items-center sm:items-start">
|
<div class="mx-auto max-w-5xl px-4 md:px-8 flex flex-col items-start">
|
||||||
|
<span class="text-sm leading-[24px] font-semibold text-slate-600 dark:text-slate-300 mb-1 <%= @portal.logo.present? ? 'block' : 'hidden' %>"><%= @portal.name %></span>
|
||||||
<h1 class="text-2xl md:text-4xl text-slate-900 dark:text-white font-semibold leading-normal">
|
<h1 class="text-2xl md:text-4xl text-slate-900 dark:text-white font-semibold leading-normal">
|
||||||
<%= portal.header_text %>
|
<%= portal.header_text %>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-slate-600 dark:text-slate-200 text-center text-lg leading-normal pt-4 pb-4"><%= I18n.t('public_portal.hero.sub_title') %></p>
|
<p class="text-slate-600 dark:text-slate-200 text-start text-lg leading-normal pt-2 pb-4"><%= I18n.t('public_portal.hero.sub_title') %></p>
|
||||||
<div id="search-wrap" class="w-full"></div>
|
<div id="search-wrap" class="w-full"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
83
app/views/public/api/v1/portals/_mobile_menu.html.erb
Normal file
83
app/views/public/api/v1/portals/_mobile_menu.html.erb
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
|
||||||
|
<input type="checkbox" id="mobile-menu-toggle" class="peer/menu hidden" />
|
||||||
|
|
||||||
|
<label for="mobile-menu-toggle" class="relative p-2 text-slate-700 dark:text-slate-200 cursor-pointer z-[60] hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors" aria-label="Toggle menu">
|
||||||
|
<%= render partial: 'icons/hamburger' %>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="fixed inset-0 z-[1000] invisible select-none opacity-0 peer-checked/menu:visible peer-checked/menu:opacity-100 transition-all duration-300 sm:hidden">
|
||||||
|
<div class="w-full h-full bg-white dark:bg-slate-900 shadow-xl transition-transform duration-300 ease-out">
|
||||||
|
<div class="flex flex-col h-full">
|
||||||
|
<div class="flex items-center justify-end py-5 px-4 border-b border-slate-100 dark:border-slate-800">
|
||||||
|
<label for="mobile-menu-toggle" class="p-2 text-slate-700 dark:text-slate-200 cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors" aria-label="Close menu">
|
||||||
|
<%= render partial: 'icons/close' %>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto px-5 pb-5 pt-2 flex flex-col gap-4">
|
||||||
|
|
||||||
|
<!-- Theme Switcher Section -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h3 class="text-base font-medium text-slate-700 dark:text-slate-300 my-2">
|
||||||
|
<%= I18n.t('public_portal.header.appearance.title', default: 'Appearance') %>
|
||||||
|
</h3>
|
||||||
|
<div id="mobile-appearance-dropdown" data-current-theme="<%= @theme_from_params %>" class="appearance-menu space-y-1">
|
||||||
|
<button class="mobile-theme-button group flex items-center gap-3 justify-start w-full py-2 hover:text-slate-700 dark:hover:text-slate-200 text-slate-800 dark:text-slate-100 transition-colors stroke-slate-800 dark:stroke-slate-100" data-theme="system">
|
||||||
|
<%= render partial: 'icons/monitor' %>
|
||||||
|
<span class="text-lg font-medium"><%= I18n.t('public_portal.header.appearance.system') %></span>
|
||||||
|
<span class="check-mark-icon system-theme">
|
||||||
|
<%= render partial: 'icons/check-mark' %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="mobile-theme-button group flex items-center gap-3 justify-start w-full py-2 hover:text-slate-700 dark:hover:text-slate-200 text-slate-800 dark:text-slate-100 transition-colors stroke-slate-800 dark:stroke-slate-100" data-theme="light">
|
||||||
|
<%= render partial: 'icons/sun' %>
|
||||||
|
<span class="text-lg font-medium"><%= I18n.t('public_portal.header.appearance.light') %></span>
|
||||||
|
<span class="check-mark-icon light-theme">
|
||||||
|
<%= render partial: 'icons/check-mark' %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button class="mobile-theme-button group flex items-center gap-3 justify-start py-2 hover:text-slate-700 dark:hover:text-slate-200 text-slate-800 dark:text-slate-100 transition-colors stroke-slate-800 dark:stroke-slate-100" data-theme="dark">
|
||||||
|
<%= render partial: 'icons/moon' %>
|
||||||
|
<span class="text-lg font-medium"><%= I18n.t('public_portal.header.appearance.dark') %></span>
|
||||||
|
<span class="check-mark-icon dark-theme">
|
||||||
|
<%= render partial: 'icons/check-mark' %>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span class="h-px bg-slate-100/70 dark:bg-slate-800/70 w-full"></span>
|
||||||
|
|
||||||
|
<!-- Locale Switcher -->
|
||||||
|
<% if @portal.config["allowed_locales"].length > 1 %>
|
||||||
|
<div id="header-action-button" class="flex flex-col gap-2">
|
||||||
|
<h3 class="text-base font-medium text-slate-700 dark:text-slate-300 my-2">
|
||||||
|
<%= I18n.t('public_portal.header.language', default: 'Language') %>
|
||||||
|
</h3>
|
||||||
|
<div class="flex items-center gap-3 py-2 cursor-pointer stroke-slate-800 dark:stroke-slate-100">
|
||||||
|
<%= render partial: 'icons/globe' %>
|
||||||
|
<select
|
||||||
|
data-portal-slug="<%= @portal.slug %>"
|
||||||
|
class="flex-1 bg-transparent text-lg font-medium cursor-pointer focus:outline-none locale-switcher text-slate-800 dark:text-slate-100 hover:text-slate-700 dark:hover:text-slate-200"
|
||||||
|
>
|
||||||
|
<% @portal.config["allowed_locales"].each do |locale| %>
|
||||||
|
<option <%= locale == @locale ? 'selected': '' %> value="<%= locale %>"><%= "#{language_name(locale)} (#{locale})" %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<span class="h-px bg-slate-100/70 dark:bg-slate-800/70 w-full"></span>
|
||||||
|
|
||||||
|
<!-- Homepage Link -->
|
||||||
|
<% if @portal.homepage_link %>
|
||||||
|
<a href="<%= @portal.homepage_link %>" target="_blank" rel="noopener noreferrer nofollow" class="flex items-center gap-3 py-2 cursor-pointer stroke-slate-800 dark:stroke-slate-100 text-slate-800 dark:text-slate-100 hover:text-slate-700 dark:hover:text-slate-200 transition-colors">
|
||||||
|
<%= render partial: 'icons/redirect' %>
|
||||||
|
<span class="text-lg font-medium"><%= I18n.t('public_portal.header.visit_website') %></span>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -288,6 +288,7 @@ en:
|
|||||||
made_with: Made with
|
made_with: Made with
|
||||||
header:
|
header:
|
||||||
go_to_homepage: Website
|
go_to_homepage: Website
|
||||||
|
visit_website: Visit website
|
||||||
appearance:
|
appearance:
|
||||||
system: System
|
system: System
|
||||||
light: Light
|
light: Light
|
||||||
|
|||||||
Reference in New Issue
Block a user