diff --git a/backend/open_webui/static/custom.css b/backend/open_webui/static/custom.css index e69de29bb2d..b7e121f7819 100644 --- a/backend/open_webui/static/custom.css +++ b/backend/open_webui/static/custom.css @@ -0,0 +1,91 @@ +.language-top-bar { + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 1.5rem; + box-sizing: border-box; + background: rgba(15, 23, 42, 0.05); + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + backdrop-filter: blur(12px); + z-index: 30; +} + +.language-top-bar__header { + margin-top: 0 !important; +} + +.language-top-bar__switcher { + display: flex !important; + align-items: center; + justify-content: flex-end; + gap: 0.35rem !important; + flex-wrap: wrap; + margin: 0 !important; + padding: 0 !important; +} + +.language-top-bar__switcher > * { + flex: 0 0 auto; + display: flex !important; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 9999px; + padding: 0 !important; + margin: 0 !important; +} + +.language-top-bar__switcher img, +.language-top-bar__switcher svg { + width: 1.35rem; + height: 1.35rem; + object-fit: contain; +} + +.language-top-bar__switcher button, +.language-top-bar__switcher a { + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.language-top-bar__switcher button:hover, +.language-top-bar__switcher a:hover { + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(15, 23, 42, 0.12); +} + +@media (max-width: 1024px) { + .language-top-bar { + padding-inline: 1rem; + gap: 0.4rem; + } +} + +@media (max-width: 640px) { + .language-top-bar { + justify-content: center; + padding: 0.35rem 0.75rem; + gap: 0.3rem; + } + + .language-top-bar__switcher { + justify-content: center; + gap: 0.25rem !important; + } + + .language-top-bar__switcher > * { + width: 1.75rem; + height: 1.75rem; + } + + .language-top-bar__switcher img, + .language-top-bar__switcher svg { + width: 1.1rem; + height: 1.1rem; + } +} diff --git a/backend/open_webui/static/loader.js b/backend/open_webui/static/loader.js index e69de29bb2d..0aec33bde46 100644 --- a/backend/open_webui/static/loader.js +++ b/backend/open_webui/static/loader.js @@ -0,0 +1,122 @@ +(() => { + const headerSelectors = ['header', '.site-header', '[data-role="header"]']; + const switcherSelectors = [ + '[data-language-switcher]', + '[data-component="language-switcher"]', + '.language-switcher', + '.locale-switcher', + '[class*="language"]', + '[class*="Locale"]', + '[id*="language"]' + ]; + + const findHeader = () => { + for (const selector of headerSelectors) { + const node = document.querySelector(selector); + if (node) { + return node; + } + } + return null; + }; + + const findSwitcher = (header) => { + for (const selector of switcherSelectors) { + const candidate = header.querySelector(selector); + if (candidate) { + const container = candidate.closest('.language-switcher, .locale-switcher'); + return container ?? candidate; + } + } + + const candidates = Array.from(header.querySelectorAll('*')).filter((node) => { + if (!(node instanceof HTMLElement)) { + return false; + } + if (node.dataset.languageTopBar === 'moved') { + return false; + } + const className = node.className ?? ''; + const id = node.id ?? ''; + if (typeof className === 'string' && /lang|locale/i.test(className)) { + return true; + } + if (typeof id === 'string' && /lang|locale/i.test(id)) { + return true; + } + return false; + }); + + return candidates[0] ?? null; + }; + + const applyInlineTweaks = (element) => { + element.style.display = 'flex'; + element.style.flexWrap = 'wrap'; + element.style.gap = '0.35rem'; + element.style.margin = '0'; + element.style.padding = '0'; + }; + + const moveSwitcher = () => { + if (document.querySelector('.language-top-bar [data-language-top-bar="true"]')) { + return true; + } + + const header = findHeader(); + if (!header) { + return false; + } + + let switcher = findSwitcher(header); + if (!switcher) { + return false; + } + + if (switcher.dataset.languageTopBar === 'true') { + return true; + } + + const existingBar = document.querySelector('.language-top-bar'); + const parent = header.parentElement ?? document.body; + const bar = existingBar ?? document.createElement('div'); + + if (!existingBar) { + bar.className = 'language-top-bar'; + parent.insertBefore(bar, header); + } + + applyInlineTweaks(switcher); + + switcher.dataset.languageTopBar = 'true'; + switcher.classList.add('language-top-bar__switcher'); + bar.appendChild(switcher); + + header.classList.add('language-top-bar__header'); + + return true; + }; + + const init = () => { + if (moveSwitcher()) { + return; + } + + const observer = new MutationObserver(() => { + if (moveSwitcher()) { + observer.disconnect(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); + } else { + init(); + } +})(); diff --git a/static/static/custom.css b/static/static/custom.css index e69de29bb2d..b7e121f7819 100644 --- a/static/static/custom.css +++ b/static/static/custom.css @@ -0,0 +1,91 @@ +.language-top-bar { + width: 100%; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 1.5rem; + box-sizing: border-box; + background: rgba(15, 23, 42, 0.05); + border-bottom: 1px solid rgba(15, 23, 42, 0.08); + backdrop-filter: blur(12px); + z-index: 30; +} + +.language-top-bar__header { + margin-top: 0 !important; +} + +.language-top-bar__switcher { + display: flex !important; + align-items: center; + justify-content: flex-end; + gap: 0.35rem !important; + flex-wrap: wrap; + margin: 0 !important; + padding: 0 !important; +} + +.language-top-bar__switcher > * { + flex: 0 0 auto; + display: flex !important; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 9999px; + padding: 0 !important; + margin: 0 !important; +} + +.language-top-bar__switcher img, +.language-top-bar__switcher svg { + width: 1.35rem; + height: 1.35rem; + object-fit: contain; +} + +.language-top-bar__switcher button, +.language-top-bar__switcher a { + border: none; + background: transparent; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.language-top-bar__switcher button:hover, +.language-top-bar__switcher a:hover { + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(15, 23, 42, 0.12); +} + +@media (max-width: 1024px) { + .language-top-bar { + padding-inline: 1rem; + gap: 0.4rem; + } +} + +@media (max-width: 640px) { + .language-top-bar { + justify-content: center; + padding: 0.35rem 0.75rem; + gap: 0.3rem; + } + + .language-top-bar__switcher { + justify-content: center; + gap: 0.25rem !important; + } + + .language-top-bar__switcher > * { + width: 1.75rem; + height: 1.75rem; + } + + .language-top-bar__switcher img, + .language-top-bar__switcher svg { + width: 1.1rem; + height: 1.1rem; + } +} diff --git a/static/static/loader.js b/static/static/loader.js index e69de29bb2d..0aec33bde46 100644 --- a/static/static/loader.js +++ b/static/static/loader.js @@ -0,0 +1,122 @@ +(() => { + const headerSelectors = ['header', '.site-header', '[data-role="header"]']; + const switcherSelectors = [ + '[data-language-switcher]', + '[data-component="language-switcher"]', + '.language-switcher', + '.locale-switcher', + '[class*="language"]', + '[class*="Locale"]', + '[id*="language"]' + ]; + + const findHeader = () => { + for (const selector of headerSelectors) { + const node = document.querySelector(selector); + if (node) { + return node; + } + } + return null; + }; + + const findSwitcher = (header) => { + for (const selector of switcherSelectors) { + const candidate = header.querySelector(selector); + if (candidate) { + const container = candidate.closest('.language-switcher, .locale-switcher'); + return container ?? candidate; + } + } + + const candidates = Array.from(header.querySelectorAll('*')).filter((node) => { + if (!(node instanceof HTMLElement)) { + return false; + } + if (node.dataset.languageTopBar === 'moved') { + return false; + } + const className = node.className ?? ''; + const id = node.id ?? ''; + if (typeof className === 'string' && /lang|locale/i.test(className)) { + return true; + } + if (typeof id === 'string' && /lang|locale/i.test(id)) { + return true; + } + return false; + }); + + return candidates[0] ?? null; + }; + + const applyInlineTweaks = (element) => { + element.style.display = 'flex'; + element.style.flexWrap = 'wrap'; + element.style.gap = '0.35rem'; + element.style.margin = '0'; + element.style.padding = '0'; + }; + + const moveSwitcher = () => { + if (document.querySelector('.language-top-bar [data-language-top-bar="true"]')) { + return true; + } + + const header = findHeader(); + if (!header) { + return false; + } + + let switcher = findSwitcher(header); + if (!switcher) { + return false; + } + + if (switcher.dataset.languageTopBar === 'true') { + return true; + } + + const existingBar = document.querySelector('.language-top-bar'); + const parent = header.parentElement ?? document.body; + const bar = existingBar ?? document.createElement('div'); + + if (!existingBar) { + bar.className = 'language-top-bar'; + parent.insertBefore(bar, header); + } + + applyInlineTweaks(switcher); + + switcher.dataset.languageTopBar = 'true'; + switcher.classList.add('language-top-bar__switcher'); + bar.appendChild(switcher); + + header.classList.add('language-top-bar__header'); + + return true; + }; + + const init = () => { + if (moveSwitcher()) { + return; + } + + const observer = new MutationObserver(() => { + if (moveSwitcher()) { + observer.disconnect(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + }; + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); + } else { + init(); + } +})();