Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified com.woltlab.wcf/defaultStyle.tar
Binary file not shown.
4 changes: 2 additions & 2 deletions com.woltlab.wcf/templates/__devtoolsLanguageChooser.tpl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{* The `system_pageHeader` renders this chooser on the server. *}
{if ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS && !$__wcf->user->isGuest() && $__wcf->getStyleHandler()->getStyle()->usesClassicPageHeader()}
{* The `system_pageHeader` renders this chooser on the server, the new page footers host it instead. *}
{if ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS && !$__wcf->user->isGuest() && $__wcf->getStyleHandler()->getStyle()->usesClassicPageHeader() && $__wcf->getStyleHandler()->getStyle()->usesClassicPageFooter()}
<script data-relocate="true">
require(['WoltLabSuite/Core/Api/Devtools/SetLanguage', 'WoltLabSuite/Core/Language/Chooser'], function(SetLanguage, LanguageChooser) {
var item = elCreate('li');
Expand Down
11 changes: 10 additions & 1 deletion com.woltlab.wcf/templates/headIncludeJavaScript.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,16 @@

{if $__wcf->getStyleHandler()->getColorScheme() === 'system'}
{
const colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
let colorScheme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
{if $__wcf->user->isGuest()}
{* Guests pick the color scheme in the page footer, see `WoltLabSuite/Core/Controller/Style/ColorScheme`. *}
try {
const value = localStorage.getItem("wsc_colorScheme");
if (value === "dark" || value === "light") {
colorScheme = value;
}
} catch {}
{/if}
document.documentElement.dataset.colorScheme = colorScheme;
}
{/if}
Expand Down
3 changes: 2 additions & 1 deletion com.woltlab.wcf/templates/pageHeaderUser.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@
</li>
{/if}
{else}
{if $__wcf->getLanguage()->getLanguages()|count > 1}
{* The new page footers host the language switch instead. *}
{if $__wcf->getLanguage()->getLanguages()|count > 1 && $__wcf->getStyleHandler()->getStyle()->usesClassicPageFooter()}
<li id="pageLanguageContainer">
<script data-relocate="true">
require(['WoltLabSuite/Core/Language/Chooser'], function(LanguageChooser) {
Expand Down
154 changes: 154 additions & 0 deletions com.woltlab.wcf/templates/system_pageFooter.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
{if !$errorField|empty || !$errorType|empty}
<!--
DEBUG: FORM_VALIDATION_FAILED

errorField: {if $errorField|empty}(empty){else}{$errorField|print_r:true}{/if}

errorType: {if $errorType|empty}(empty){else}{$errorType|print_r:true}{/if}

-->
{if ENABLE_DEBUG_MODE}
<script>
console.debug('FORM_VALIDATION_FAILED');
console.debug('errorField:', '{if $errorField|empty}(empty){else}{$errorField|json}{/if}');
console.debug('errorType:', '{if $errorType|empty}(empty){else}{$errorType|json}{/if}');
</script>
{/if}
{/if}

{capture assign='__pageFooterBoxes'}
{if !$boxesFooter|empty}
{unsafe:$boxesFooter}
{/if}

{foreach from=$__wcf->getBoxHandler()->getBoxes('footer') item=box}
{unsafe:$box->render()}
{/foreach}
{/capture}
{assign var='__pageFooterBoxes' value=$__pageFooterBoxes|trim}

{capture assign='__pageFooterCopyright'}
{event name='footerContents'}

{if ENABLE_BENCHMARK}
{include file='shared_benchmark'}
{/if}

{include file='pageFooterCopyright'}
{/capture}
{assign var='__pageFooterCopyright' value=$__pageFooterCopyright|trim}

{capture assign='__pageFooterPreferences'}
{if $__wcf->getStyleHandler()->showColorSchemeSelector()}
{* The browser keeps the choice of a guest, `ColorSchemeSwitch` checks it. *}
{if $__wcf->user->isGuest()}
{assign var='__pageFooterColorScheme' value=''}
{else}
{assign var='__pageFooterColorScheme' value=$__wcf->getStyleHandler()->getColorScheme()}
{/if}
<fieldset class="pageFooterScheme jsOnly">
<legend class="visuallyHidden">{lang}wcf.style.setColorScheme{/lang}</legend>
<label class="pageFooterScheme__option" title="{lang}wcf.style.setColorScheme.system{/lang}">
<input type="radio" class="pageFooterScheme__input" name="pageFooterColorScheme" value="system"{if $__pageFooterColorScheme === 'system'} checked{/if}>
{icon name='circle-half-stroke'}
<span class="visuallyHidden">{lang}wcf.style.setColorScheme.system{/lang}</span>
</label>
<label class="pageFooterScheme__option" title="{lang}wcf.style.setColorScheme.light{/lang}">
<input type="radio" class="pageFooterScheme__input" name="pageFooterColorScheme" value="light"{if $__pageFooterColorScheme === 'light'} checked{/if}>
{icon name='sun'}
<span class="visuallyHidden">{lang}wcf.style.setColorScheme.light{/lang}</span>
</label>
<label class="pageFooterScheme__option" title="{lang}wcf.style.setColorScheme.dark{/lang}">
<input type="radio" class="pageFooterScheme__input" name="pageFooterColorScheme" value="dark"{if $__pageFooterColorScheme === 'dark'} checked{/if}>
{icon name='moon'}
<span class="visuallyHidden">{lang}wcf.style.setColorScheme.dark{/lang}</span>
</label>
</fieldset>
{/if}

{if $__wcf->getStyleHandler()->showStyleChanger()}
<button type="button" class="pageFooterPreferences__button jsButtonStyleChanger jsOnly">
{icon name='palette'}
<span>{lang}wcf.style.changeStyle{/lang}</span>
</button>
{/if}

{* Members only switch the language through the developer tools. *}
{if $__wcf->getLanguage()->getLanguages()|count > 1 && ($__wcf->user->isGuest() || (ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS))}
<div class="pageFooterLanguage dropdown jsOnly">
<button type="button" class="pageFooterPreferences__button dropdownToggle">
<img src="{$__wcf->getLanguage()->getIconPath()}" alt="" class="iconFlag">
<span><span class="visuallyHidden">{lang}wcf.user.language{/lang} </span>{$__wcf->getLanguage()}</span>
{icon name='chevron-up'}
</button>
<div class="dropdownMenu pageFooterLanguageMenu" data-dropdown-alignment-vertical="top" data-dropdown-alignment-horizontal="right">
<ul class="scrollableDropdownMenu">
{foreach from=$__wcf->getLanguage()->getLanguages() item=_language}
<li{if $_language->languageID === $__wcf->getLanguage()->languageID} class="active"{/if}>
<button
type="button"
data-switch-language="{$_language->languageID}"
data-language-code="{$_language->languageCode}"
lang="{$_language->languageCode}"
{if $_language->languageID === $__wcf->getLanguage()->languageID} aria-current="true"{/if}
>
<img src="{$_language->getIconPath()}" alt="" class="iconFlag">
<span>{$_language}</span>
</button>
</li>
{/foreach}
</ul>
</div>
</div>
{/if}
{/capture}
{assign var='__pageFooterPreferences' value=$__pageFooterPreferences|trim}

{capture assign='__pageFooterAd'}
{if MODULE_WCF_AD && $__disableAds|empty}
{unsafe:$__wcf->getAdHandler()->getAds('com.woltlab.wcf.footer.bottom')}
{/if}
{/capture}
{assign var='__pageFooterAd' value=$__pageFooterAd|trim}

{if $__pageFooterBoxes || $__pageFooterCopyright || $__pageFooterPreferences || $__pageFooterAd}
<footer id="pageFooter" class="pageFooter pageFooter--compact">
{if $__pageFooterBoxes}
<div class="pageFooter__part pageFooter__part--main">
<div class="layoutBoundary">
<div class="boxContainer">
{unsafe:$__pageFooterBoxes}
</div>
</div>
</div>
{/if}

{if $__pageFooterCopyright || $__pageFooterPreferences || $__pageFooterAd}
<div class="pageFooter__part pageFooter__part--bottom{if !$__pageFooterCopyright && !$__pageFooterAd} jsOnly{/if}">
<div class="layoutBoundary">
{if $__pageFooterCopyright || $__pageFooterPreferences}
<div class="pageFooter__bottom">
{if $__pageFooterCopyright}
<div id="pageFooterCopyright" class="pageFooter__copyright">
{unsafe:$__pageFooterCopyright}
</div>
{/if}

{if $__pageFooterPreferences}
<div class="pageFooterPreferences jsOnly">
{unsafe:$__pageFooterPreferences}
</div>
{/if}
</div>
{/if}

{if $__pageFooterAd}
<div class="pageFooter__ad">
{unsafe:$__pageFooterAd}
</div>
{/if}
</div>
</div>
{/if}
</footer>
{/if}
4 changes: 2 additions & 2 deletions com.woltlab.wcf/templates/system_pageHeaderUser.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
>{icon size=32 name='magnifying-glass'} <span>{lang}wcf.global.search{/lang}</span></a>
</li>

{* Members only switch the language through the developer tools. *}
{if $__wcf->getLanguage()->getLanguages()|count > 1 && ($__wcf->user->isGuest() || (ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS))}
{* Members only switch the language through the developer tools. The new page footers host the switch instead. *}
{if $__wcf->getLanguage()->getLanguages()|count > 1 && ($__wcf->user->isGuest() || (ENABLE_DEBUG_MODE && ENABLE_DEVELOPER_TOOLS)) && $__wcf->getStyleHandler()->getStyle()->usesClassicPageFooter()}
<li id="pageLanguageContainer" class="dropdown">
<button
type="button"
Expand Down
1 change: 1 addition & 0 deletions defaultStyle/variables.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<variables xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/6.0/styleVariables.xsd">
<variable name="pageFooterLayout"><![CDATA[compact]]></variable>
<variable name="pageHeaderLayout"><![CDATA[logoTop]]></variable>
</variables>
10 changes: 5 additions & 5 deletions ts/WoltLabSuite/Core/Acp/Ui/Style/Editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ function handleProtection(styleId: number): void {
}

/**
* Mirrors the selected page header layout in the preview.
* Mirrors the selected style options in the preview, e.g. `data-page-header-layout`.
*/
function handlePageHeaderLayout(): void {
document.querySelectorAll<HTMLInputElement>('input[name="pageHeaderLayout"]').forEach((input) => {
function handleStyleOptions(): void {
document.querySelectorAll<HTMLInputElement>('.styleOptionList input[type="radio"]').forEach((input) => {
input.addEventListener("change", () => {
_stylePreviewWindow.dataset.pageHeaderLayout = input.value;
_stylePreviewWindow.dataset[input.name] = input.value;
});
});
}
Expand Down Expand Up @@ -286,7 +286,7 @@ export function setup(options: StyleEditorOptions): void {
}

initVisualEditor();
handlePageHeaderLayout();
handleStyleOptions();

UiScreen.on("screen-sm-down", {
match() {
Expand Down
21 changes: 21 additions & 0 deletions ts/WoltLabSuite/Core/Api/Users/SetColorScheme.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Sets the preferred color scheme of the active user.
*
* @author Alexander Ebert
* @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.3
*/

import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { fromInfallibleApiRequest } from "../Result";
import type { ColorSchemePreference } from "../../Controller/Style/ColorScheme";

export function setColorScheme(colorScheme: ColorSchemePreference): Promise<[]> {
return fromInfallibleApiRequest(() => {
return prepareRequest(`${window.WSC_RPC_API_URL}core/users/color-scheme`)
.post({ colorScheme })
.disableLoadingIndicator()
.fetchAsJson();
});
}
15 changes: 13 additions & 2 deletions ts/WoltLabSuite/Core/BootstrapFrontend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { setup as setupLanguageSwitcher } from "./Component/LanguageSwitcher";
import { setup as setupUserMenuDrawer } from "./Component/UserMenuDrawer";
import { setup as setupPageHeaderSearch } from "./Component/PageHeaderSearch";
import { setup as setupPageHeaderShadow } from "./Component/PageHeaderShadow";
import { setup as setupColorSchemeSwitch } from "./Component/PageFooter/ColorSchemeSwitch";
import { getStoredColorScheme } from "./Controller/Style/ColorScheme";

interface BootstrapOptions {
backgroundQueue: {
Expand Down Expand Up @@ -89,7 +91,8 @@ export function setup(options: BootstrapOptions): void {
const hasSystemPageHeader = options.templateVariants.includes("pageHeader");

Bootstrap.setup({
dynamicColorScheme: options.dynamicColorScheme,
// A color scheme picked by a guest is applied by the inline script in the page head already.
dynamicColorScheme: options.dynamicColorScheme && (User.userId !== 0 || getStoredColorScheme() === undefined),
enableMobileMenu: !hasSystemPageHeader,
enableSearch: !hasSystemPageHeader,
pageMenuMainProvider: new UiPageMenuMainFrontend(),
Expand All @@ -111,14 +114,22 @@ export function setup(options: BootstrapOptions): void {
if (hasSystemPageHeader) {
setupDrawers();
setupMainMenu();
setupLanguageSwitcher();
setupUserMenuDrawer();
setupPageHeaderSearch();
setupPageHeaderShadow();
} else {
UiPageHeaderMenu.init();
}

const hasSystemPageFooter = options.templateVariants.includes("pageFooter");
if (hasSystemPageFooter) {
setupColorSchemeSwitch();
}

if (hasSystemPageHeader || hasSystemPageFooter) {
setupLanguageSwitcher();
}

if (options.styleChanger) {
void import("./Controller/Style/Changer").then((ControllerStyleChanger) => {
ControllerStyleChanger.setup();
Expand Down
65 changes: 65 additions & 0 deletions ts/WoltLabSuite/Core/Component/PageFooter/ColorSchemeSwitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Switches the color scheme from the page footer. Members store the choice in
* their account, guests in the browser.
*
* @author Alexander Ebert
* @copyright 2001-2026 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.3
*/

import User from "../../User";
import {
ColorSchemePreference,
getStoredColorScheme,
setColorScheme,
storeColorScheme,
} from "../../Controller/Style/ColorScheme";
import { setColorScheme as saveColorScheme } from "../../Api/Users/SetColorScheme";

let isSaving = false;
let pendingColorScheme: ColorSchemePreference | undefined;

// Saves one request at a time and skips intermediate values, so a late response
// cannot overwrite a newer choice, e.g. while stepping through the radios.
async function save(colorScheme: ColorSchemePreference): Promise<void> {
pendingColorScheme = colorScheme;
if (isSaving) {
return;
}

isSaving = true;
try {
while (pendingColorScheme !== undefined) {
const next = pendingColorScheme;
pendingColorScheme = undefined;
await saveColorScheme(next);
}
} finally {
isSaving = false;
}
}

export function setup(): void {
const fieldset = document.querySelector<HTMLFieldSetElement>(".pageFooterScheme");
if (fieldset === null) {
return;
}

// The server does not know the choice of a guest.
if (User.userId === 0) {
const colorScheme = getStoredColorScheme() ?? "system";
fieldset.querySelector<HTMLInputElement>(`input[value="${colorScheme}"]`)!.checked = true;
}

fieldset.addEventListener("change", (event) => {
const colorScheme = (event.target as HTMLInputElement).value as ColorSchemePreference;
setColorScheme(colorScheme);

if (User.userId === 0) {
storeColorScheme(colorScheme);
} else {
void save(colorScheme);
}
});
}
Loading
Loading