diff --git a/extensions/flags/less/forum.less b/extensions/flags/less/forum.less index 9b89587c0c..caf53d2eb6 100644 --- a/extensions/flags/less/forum.less +++ b/extensions/flags/less/forum.less @@ -1,3 +1,11 @@ +[data-theme^=light] { + .light-contents-vars(@color: @body-bg-light; @control-color: @body-bg-light; @name: 'flagged-post'); +} + +[data-theme^=dark] { + .light-contents-vars(@color: @body-bg-dark; @control-color: @body-bg-dark; @name: 'flagged-post'); +} + .Post--flagged { --border-width: 2px; padding-top: 0 !important; @@ -16,7 +24,7 @@ padding: 10px; border-radius: var(--border-radius) var(--border-radius) 0 0; overflow: hidden; - .light-contents(@color: @body-bg; @control-color: @body-bg); + .light-contents(@name: 'flagged-post'); display: flex; align-items: center; diff --git a/extensions/package-manager/less/admin.less b/extensions/package-manager/less/admin.less index 7c290b2ff2..c58a01d7f1 100755 --- a/extensions/package-manager/less/admin.less +++ b/extensions/package-manager/less/admin.less @@ -22,12 +22,6 @@ background-color: transparent; } -// @TODO add to core -.Checkbox--switch.disabled { - opacity: 0.6; - cursor: not-allowed; -} - .ButtonGroup--full { width: 100%; display: flex; diff --git a/extensions/subscriptions/less/forum.less b/extensions/subscriptions/less/forum.less index c35f76e0a8..de55a0ead2 100644 --- a/extensions/subscriptions/less/forum.less +++ b/extensions/subscriptions/less/forum.less @@ -1,9 +1,26 @@ +@following-bg: #ffea7b; +@following-color: #de8e00; + :root { - --following-bg: #ffea7b; - --following-color: #de8e00; + --following-bg: @following-bg; + --following-color: @following-color; --ignoring-bg: #aaa; } +[data-theme^=light] { + .Button--color-vars(@following-color, #fff2ae, 'button--follow'); +} + +[data-theme=light-hc], [data-theme=dark-hc] { + @following-color-hc: darken(@following-color, 23%); + --following-color: @following-color-hc; + .Button--color-vars(@following-color-hc, #fff2ae, 'button--follow'); +} + +[data-theme^=dark] { + .Button--color-vars(#784d00, #fbb94c, 'button--follow'); +} + .Badge--following { --badge-bg: var(--following-bg); --badge-color: var(--following-color); @@ -12,12 +29,7 @@ --badge-bg: var(--ignoring-bg); } .SubscriptionMenu-button--follow { - & when (@config-dark-mode = false) { - .Button--color(#de8e00, #fff2ae); - } - & when (@config-dark-mode = true) { - .Button--color(#784d00, #fbb94c); - } + .Button--color-auto('button--follow'); } .SubscriptionMenu .Dropdown-menu { min-width: 260px; diff --git a/extensions/tags/less/forum/ToggleButton.less b/extensions/tags/less/forum/ToggleButton.less index 35d43a2228..74db40c135 100644 --- a/extensions/tags/less/forum/ToggleButton.less +++ b/extensions/tags/less/forum/ToggleButton.less @@ -1,5 +1,9 @@ -:root { - .Button--color-vars(@control-bg, @control-color, 'button-toggled'); +[data-theme^=light] { + .Button--color-vars(@control-bg-light, @control-color-light, 'button-toggled'); +} + +[data-theme^=dark] { + .Button--color-vars(@control-bg-dark, @control-color-dark, 'button-toggled'); } .Button--toggled { diff --git a/framework/core/js/src/admin/components/AppearancePage.tsx b/framework/core/js/src/admin/components/AppearancePage.tsx index 9dc3389dd4..91681c745b 100644 --- a/framework/core/js/src/admin/components/AppearancePage.tsx +++ b/framework/core/js/src/admin/components/AppearancePage.tsx @@ -9,6 +9,7 @@ import ItemList from '../../common/utils/ItemList'; import type Mithril from 'mithril'; import Form from '../../common/components/Form'; import FieldSet from '../../common/components/FieldSet'; +import ThemeMode from '../../common/components/ThemeMode'; export default class AppearancePage extends AdminPage { headerInfo() { @@ -97,11 +98,29 @@ export default class AppearancePage extends AdminPage { ); items.add( - 'dark-mode', - this.buildSettingComponent({ - type: 'switch', - setting: 'theme_dark_mode', - label: app.translator.trans('core.admin.appearance.dark_mode_label'), + 'theme-modes', + this.buildSettingComponent(function () { + return ( +
+ +
+ {ThemeMode.colorSchemes.map((mode) => ( + { + this.setting('color_scheme')(mode.id); + + this.setting('allow_user_color_scheme')(mode.id === 'auto' ? '1' : '0'); + + app.setColorScheme(mode.id); + }} + selected={this.setting('color_scheme')() === mode.id} + /> + ))} +
+
+ ); }), 60 ); @@ -112,6 +131,10 @@ export default class AppearancePage extends AdminPage { type: 'switch', setting: 'theme_colored_header', label: app.translator.trans('core.admin.appearance.colored_header_label'), + onchange: (value: boolean) => { + this.setting('theme_colored_header')(value ? '1' : '0'); + app.setColoredHeader(value); + }, }), 50 ); diff --git a/framework/core/js/src/common/Application.tsx b/framework/core/js/src/common/Application.tsx index 2e362e9699..7d0a13af38 100644 --- a/framework/core/js/src/common/Application.tsx +++ b/framework/core/js/src/common/Application.tsx @@ -38,6 +38,7 @@ import IHistory from './IHistory'; import IExtender from './extenders/IExtender'; import AccessToken from './models/AccessToken'; import SearchManager from './SearchManager'; +import { ColorScheme } from './components/ThemeMode'; export type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd'; @@ -245,6 +246,8 @@ export default class Application { data!: ApplicationData; + allowUserColorScheme!: boolean; + private _title: string = ''; private _titleCount: number = 0; @@ -356,9 +359,61 @@ export default class Application { document.body.classList.add('ontouchstart' in window ? 'touch' : 'no-touch'); + this.initColorScheme(); + liveHumanTimes(); } + private initColorScheme(forumDefault: string | null = null): void { + forumDefault ??= document.documentElement.getAttribute('data-theme') ?? 'auto'; + this.allowUserColorScheme = forumDefault === 'auto'; + const userConfiguredPreference = this.session.user?.preferences()?.colorScheme; + + let scheme; + + if (this.allowUserColorScheme) { + scheme = userConfiguredPreference; + } + + scheme ||= forumDefault; + + this.setColorScheme(scheme); + + // Listen for browser color scheme changes and update the theme accordingly + if (this.allowUserColorScheme) { + this.watchSystemColorSchemePreference(() => { + this.initColorScheme(forumDefault); + }); + } + } + + getSystemColorSchemePreference(): ColorScheme | string { + let colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + + if (window.matchMedia('(prefers-contrast: more)').matches) { + colorScheme += '-hc'; + } + + return colorScheme; + } + + watchSystemColorSchemePreference(callback: () => void): void { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', callback); + window.matchMedia('(prefers-contrast: more)').addEventListener('change', callback); + } + + setColorScheme(scheme: ColorScheme | string): void { + if (scheme === ColorScheme.Auto) { + scheme = this.getSystemColorSchemePreference(); + } + + document.documentElement.setAttribute('data-theme', scheme); + } + + setColoredHeader(value: boolean): void { + document.documentElement.setAttribute('data-colored-header', value ? 'true' : 'false'); + } + /** * Get the API response document that has been preloaded into the application. */ diff --git a/framework/core/js/src/common/components/ThemeMode.tsx b/framework/core/js/src/common/components/ThemeMode.tsx new file mode 100644 index 0000000000..b226cad0f8 --- /dev/null +++ b/framework/core/js/src/common/components/ThemeMode.tsx @@ -0,0 +1,96 @@ +import Component, { type ComponentAttrs } from '../../common/Component'; +import type Mithril from 'mithril'; +import classList from '../../common/utils/classList'; + +export interface IThemeModeAttrs extends ComponentAttrs { + label: string; + mode: string; + selected?: boolean; + alternate?: boolean; +} + +export enum ColorScheme { + Auto = 'auto', + Light = 'light', + Dark = 'dark', + LightHighContrast = 'light-hc', + DarkHighContrast = 'dark-hc', +} + +export type ColorSchemeData = { + id: ColorScheme | string; + label?: string | null; +}; + +export default class ThemeMode extends Component { + static colorSchemes: ColorSchemeData[] = [ + { id: ColorScheme.Auto }, + { id: ColorScheme.Light }, + { id: ColorScheme.Dark }, + { id: ColorScheme.LightHighContrast }, + { id: ColorScheme.DarkHighContrast }, + ]; + + view(vnode: Mithril.Vnode): Mithril.Children { + const { mode, selected, className, alternate, label, ...attrs } = vnode.attrs; + + return ( + + ); + } +} diff --git a/framework/core/js/src/common/extenders/ThemeMode.ts b/framework/core/js/src/common/extenders/ThemeMode.ts new file mode 100644 index 0000000000..64d760b7f0 --- /dev/null +++ b/framework/core/js/src/common/extenders/ThemeMode.ts @@ -0,0 +1,20 @@ +import Application from '../Application'; +import IExtender, { IExtensionModule } from './IExtender'; +import ThemeModeComponent, { type ColorSchemeData } from '../components/ThemeMode'; + +export default class ThemeMode implements IExtender { + private readonly colorSchemes: ColorSchemeData[] = []; + + public add(mode: string, label: string): this { + this.colorSchemes.push({ + id: mode, + label, + }); + + return this; + } + + extend(app: Application, extension: IExtensionModule): void { + ThemeModeComponent.colorSchemes = Array.from(new Set([...ThemeModeComponent.colorSchemes, ...this.colorSchemes])); + } +} diff --git a/framework/core/js/src/common/extenders/index.ts b/framework/core/js/src/common/extenders/index.ts index 8702e00d35..f776f6f711 100644 --- a/framework/core/js/src/common/extenders/index.ts +++ b/framework/core/js/src/common/extenders/index.ts @@ -4,6 +4,7 @@ import Routes from './Routes'; import Store from './Store'; import Search from './Search'; import Notification from './Notification'; +import ThemeMode from './ThemeMode'; const extenders = { Model, @@ -12,6 +13,7 @@ const extenders = { Store, Search, Notification, + ThemeMode, }; export default extenders; diff --git a/framework/core/js/src/forum/components/SettingsPage.tsx b/framework/core/js/src/forum/components/SettingsPage.tsx index 15e18b95a8..bdf1069610 100644 --- a/framework/core/js/src/forum/components/SettingsPage.tsx +++ b/framework/core/js/src/forum/components/SettingsPage.tsx @@ -11,6 +11,9 @@ import listItems from '../../common/helpers/listItems'; import extractText from '../../common/utils/extractText'; import type Mithril from 'mithril'; import classList from '../../common/utils/classList'; +import ThemeMode from '../../common/components/ThemeMode'; +import { camelCaseToSnakeCase } from '../../common/utils/string'; +import { ComponentAttrs } from '../../common/Component'; /** * The `SettingsPage` component displays the user's settings control panel, in @@ -18,6 +21,7 @@ import classList from '../../common/utils/classList'; */ export default class SettingsPage extends UserPage { discloseOnlineLoading?: boolean; + colorSchemeLoading?: boolean; oninit(vnode: Mithril.Vnode) { super.oninit(vnode); @@ -35,20 +39,35 @@ export default class SettingsPage { + return { + account: { className: 'FieldSet--col' }, + colorScheme: { + className: 'FieldSet--col', + visible: () => app.allowUserColorScheme, + }, + }; + } + /** * Build an item list for the user's settings controls. */ settingsItems() { const items = new ItemList(); - ['account', 'notifications', 'privacy'].forEach((section, index) => { + ['account', 'notifications', 'privacy', 'colorScheme'].forEach((section, index) => { const sectionItems = `${section}Items` as 'accountItems' | 'notificationsItems' | 'privacyItems'; + const { className, visible, ...props } = this.sectionProps()[section] || {}; + + if (visible && visible() === false) return; + items.add( section,
{this[sectionItems]().toArray()}
, @@ -122,4 +141,35 @@ export default class SettingsPage(); + + ThemeMode.colorSchemes.forEach((mode) => { + items.add( + mode.id, + { + this.colorSchemeLoading = true; + + this.user!.savePreferences({ colorScheme: mode.id }).then(() => { + this.colorSchemeLoading = false; + app.setColorScheme(mode.id); + m.redraw(); + }); + }} + />, + 100 + ); + }); + + return items; + } } diff --git a/framework/core/less/admin/UsersListPage.less b/framework/core/less/admin/UsersListPage.less index 54ee110926..3f92b72ab5 100644 --- a/framework/core/less/admin/UsersListPage.less +++ b/framework/core/less/admin/UsersListPage.less @@ -82,7 +82,7 @@ &--shaded { background: var(--body-bg-shaded); - & when (@config-dark-mode = true) { + [data-theme^=dark] & { background: var(--body-bg-light); } } diff --git a/framework/core/less/common/App.less b/framework/core/less/common/App.less index 0162ef2ed7..c053c5c23c 100644 --- a/framework/core/less/common/App.less +++ b/framework/core/less/common/App.less @@ -227,10 +227,8 @@ // the page toolbar that we styled earlier. We lay its contents out // horizontally. @media @phone { - .App-drawer { - & when (@config-colored-header = true) { - .light-contents(@name: 'header-colored'); - } + [data-colored-header=true] .App-drawer { + .light-contents(@name: 'header-colored'); } } @@ -258,7 +256,7 @@ box-shadow: 0 2px 6px var(--shadow-color); } - & when (@config-colored-header = true) { + [data-colored-header=true] & { .light-contents(@name: 'header-colored'); } } diff --git a/framework/core/less/common/Checkbox.less b/framework/core/less/common/Checkbox.less index 79949e7fb3..7ebbfd44fd 100644 --- a/framework/core/less/common/Checkbox.less +++ b/framework/core/less/common/Checkbox.less @@ -42,6 +42,11 @@ } } +.Checkbox--switch.disabled { + opacity: 0.6; + cursor: not-allowed; +} + .Checkbox--switch .Checkbox-display { width: 50px; height: 28px; diff --git a/framework/core/less/common/Form.less b/framework/core/less/common/Form.less index 1278afc8e1..06802be484 100644 --- a/framework/core/less/common/Form.less +++ b/framework/core/less/common/Form.less @@ -66,6 +66,10 @@ } } +.FieldSet--min .FieldSet-items > * { + width: auto; +} + .FieldSet--col .FieldSet-items, .Form-controls { flex-wrap: wrap; flex-direction: row; diff --git a/framework/core/less/common/ThemeMode.less b/framework/core/less/common/ThemeMode.less new file mode 100644 index 0000000000..4fccc5f2a6 --- /dev/null +++ b/framework/core/less/common/ThemeMode.less @@ -0,0 +1,211 @@ +.ThemeMode { + display: block; + cursor: pointer; + max-width: 150px; + + &-container { + border-radius: var(--border-radius); + border: 2px solid var(--control-bg); + overflow: hidden; + position: relative; + } + + &:hover &-container { + border-color: var(--control-color); + } + + &--active &-container, &--active:hover &-container { + border-color: var(--primary-color); + } + + &--switch { + position: absolute; + top: 0; + left: 0; + clip-path: polygon(0% 0%, 100% 0%, 0% 100%); + } + + &--switch &-container { + border-radius: 0; + border: none; + } +} + +.ThemeMode-preview { + height: 150px; + width: 150px; + display: flex; + flex-direction: column; + background-color: var(--body-bg); +} + +.ThemeMode-list { + display: flex; + gap: 4px; + flex-wrap: wrap; +} + +.ThemeMode-header { + height: 25px; + flex-shrink: 0; + background-color: var(--header-bg); + display: flex; + align-items: center; + justify-content: flex-end; + padding-inline-end: 15px; + gap: 8px; + + &-text { + height: 2px; + width: 15px; + background: var(--header-colored-color); + } + + &-icon { + height: 5px; + width: 5px; + background: var(--header-colored-color); + border-radius: 100%; + } +} + +.ThemeMode-hero { + background-color: var(--control-bg); + height: 40px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + gap: 6px; +} + +.ThemeMode-hero-title { + width: 25%; + height: 2px; + background-color: var(--control-color); + border-radius: var(--border-radius); +} + +.ThemeMode-hero-desc { + width: 50%; + height: 1px; + background-color: var(--control-color); +} + +.ThemeMode-main { + padding: 14px 0; + width: 75%; + flex-grow: 1; + margin: 0 auto; + display: grid; + grid-template-columns: 30px 1fr; +} + +.ThemeMode-startDiscussion { + border-radius: calc(var(--border-radius) - 2px); + width: 100%; + height: 10px; + background-color: var(--primary-color); + margin-bottom: 8px; + display: flex; + align-items: center; + justify-content: center; + + &-text { + height: 2px; + width: 40%; + background-color: var(--button-primary-color); + } +} + +.ThemeMode-items { + display: flex; + flex-direction: column; + gap: 3px; +} + +.ThemeMode-sidebar-line { + display: flex; + gap: 4px; + width: 100%; + align-items: center; +} + +.ThemeMode-sidebar-text { + height: 1px; + background-color: var(--control-color); + flex-grow: 1; +} + +.ThemeMode-sidebar-icon { + content: ""; + display: block; + border-radius: 100%; + height: 3px; + width: 3px; + background-color: var(--control-color); +} + +.ThemeMode-button { + height: 10px; + width: 20px; + background-color: var(--control-bg); +} + +.ThemeMode-toolbar { + display: flex; + justify-content: space-between; + margin-bottom: 10px; +} + +.ThemeMode-content { + padding: 0 8px; +} + +.ThemeMode-content-item { + height: 10px; + width: 100%; + display: flex; + align-items: center; +} + +.ThemeMode-toolbar :last-child { + width: 10px; +} + +.ThemeMode-content-item-author { + background-color: var(--control-bg); + height: 9px; + width: 9px; + border-radius: 100%; + margin-inline-end: 4px; +} + +.ThemeMode-content-item-title { + width: 25px; + height: 1px; + background-color: var(--discussion-title-color); + margin-bottom: 3px; +} + +.ThemeMode-content-item-excerpt { + height: 1px; + width: 45px; + background-color: var(--muted-more-color); +} + +.ThemeMode-container[data-theme=light], .ThemeMode-container[data-theme=dark] { + .ThemeMode-hero-title, + .ThemeMode-hero-desc, + .ThemeMode-sidebar-line, + .ThemeMode-content-item-title, + .ThemeMode-content-item-excerpt { + opacity: 0.4; + } +} + +.ThemeMode-legend { + padding: 8px 0; + color: var(--control-color); +} diff --git a/framework/core/less/common/common.less b/framework/core/less/common/common.less index e2eb37bb83..78cd44e932 100644 --- a/framework/core/less/common/common.less +++ b/framework/core/less/common/common.less @@ -30,5 +30,6 @@ @import "Select"; @import "Table"; @import "TextEditor"; +@import "ThemeMode"; @import "Tooltip"; @import "ValidationError"; diff --git a/framework/core/less/common/mixins/light-contents.less b/framework/core/less/common/mixins/light-contents.less index 8a726b812a..0eb3efa3c7 100644 --- a/framework/core/less/common/mixins/light-contents.less +++ b/framework/core/less/common/mixins/light-contents.less @@ -33,6 +33,10 @@ background: var(~"--@{name}-control-bg-fadedin", fadein(@control-bg, 5%)); color: var(~"--@{name}-control-color", @control-color); } + + .Dropdown-menu>li>a, .Dropdown-menu>li>button, .Dropdown-menu>li>span { + color: var(--text-color); + } } .light-contents-vars(@color: #fff; @control-bg: fade(#000, 10%); @control-color: #fff; @name: 'light-content') { diff --git a/framework/core/less/common/root.less b/framework/core/less/common/root.less index 3ddf7d58e1..03b6ba5a8a 100644 --- a/framework/core/less/common/root.less +++ b/framework/core/less/common/root.less @@ -1,65 +1,182 @@ -:root { +.scheme(@mode) { + @body-bg: ~"body-bg-@{mode}"; + @text-color: ~"text-color-@{mode}"; + @heading-color: ~"heading-color-@{mode}"; + @muted-color: ~"muted-color-@{mode}"; + @muted-more-color: ~"muted-more-color-@{mode}"; + @shadow-color: ~"shadow-color-@{mode}"; + @control-bg: ~"control-bg-@{mode}"; + @control-color: ~"control-color-@{mode}"; + @control-danger-bg: ~"control-danger-bg-@{mode}"; + @control-danger-color: ~"control-danger-color-@{mode}"; + @header-bg: ~"header-bg-@{mode}"; + @header-color: ~"header-color-@{mode}"; + @header-control-bg: ~"header-control-bg-@{mode}"; + @header-control-color: ~"header-control-color-@{mode}"; + @header-bg-colored: ~"header-bg-colored-@{mode}"; + @header-color-colored: ~"header-color-colored-@{mode}"; + @header-control-bg-colored: ~"header-control-bg-colored-@{mode}"; + @header-control-color-colored: ~"header-control-color-colored-@{mode}"; + @overlay-bg: ~"overlay-bg-@{mode}"; + @code-bg: ~"code-bg-@{mode}"; + @code-color: ~"code-color-@{mode}"; // --------------------------------- // COLORS - --primary-color: @primary-color; - --secondary-color: @secondary-color; + --body-bg: @@body-bg; + --body-bg-shaded: darken(@@body-bg, 3%); + --body-bg-light: lighten(@@body-bg, 5%); + --body-bg-faded: fade(@@body-bg, 93%); + --text-color: @@text-color; + --heading-color: @@heading-color; + + --muted-color: @@muted-color; + --muted-color-light: lighten(@@muted-color, 10%); + --muted-color-dark: darken(@@muted-color, 50%); + --muted-more-color: @@muted-more-color; + + --shadow-color: @@shadow-color; + + --control-bg: @@control-bg; + --control-bg-light: lighten(@@control-bg, 3%); + --control-bg-shaded: darken(@@control-bg, 4%); + --control-color: @@control-color; + --control-danger-bg: @@control-danger-bg; + --control-danger-color: @@control-danger-color; + --control-body-bg-mix: mix(@@control-bg, @@body-bg, 50%); + --control-muted-color: lighten(@@control-color, 40%); - --body-bg: @body-bg; - --body-bg-shaded: darken(@body-bg, 3%); - --body-bg-light: lighten(@body-bg, 5%); - --body-bg-faded: fade(@body-bg, 93%); - --text-color: @text-color; - --link-color: @link-color; - --heading-color: @heading-color; + // --------------------------------- + // COMPONENT COLORS + + --header-bg: @@header-bg; + --header-color: @@header-color; + --header-control-bg: @@header-control-bg; + --header-control-color: @@header-control-color; + + [data-colored-header=true]& { + --header-bg: @@header-bg-colored; + --header-color: @@header-color-colored; + --header-control-bg: @@header-control-bg-colored; + --header-control-color: @@header-control-color-colored; + + .light-contents-vars(@@header-color-colored, @@header-control-bg-colored, @@header-control-color-colored, 'header-colored'); + } + + --overlay-bg: @@overlay-bg; + --code-bg: @@code-bg; + --code-color: @@code-color; + + --discussion-title-color: mix(@@heading-color, @@body-bg, 55%); + + .Button--color-vars(@@control-color, @@control-bg, 'button'); + .Button--color-vars(@@body-bg, @primary-color, 'button-primary'); + .Button--color-vars(@@control-danger-color, @@control-danger-bg, 'control-danger'); + .Button--color-vars(@@muted-more-color, fade(@@muted-more-color, 30%), 'muted-more'); + .Button--color-vars(@@control-color, @@body-bg, 'button-inverted'); +} + +// --------------------------------- +// Light colors + +[data-theme^=light] { + .scheme(light); + + // --------------------------------- + // UTILITIES + + --yiq-threshold: 150; +} + +// High contrast + +[data-theme=light-hc] { + @control-color: darken(@control-color-light, 20%); + @muted-color: darken(@muted-color-light, 20%); + + --control-color: @control-color; + --control-muted-color: lighten(@control-color, 20%); --muted-color: @muted-color; --muted-color-light: lighten(@muted-color, 10%); --muted-color-dark: darken(@muted-color, 50%); - --muted-more-color: @muted-more-color; + --muted-more-color: darken(@muted-more-color-light, 20%); - --shadow-color: @shadow-color; + --discussion-title-color: darken(@discussion-title-color-light, 20%); - --control-bg: @control-bg; - --control-bg-light: lighten(@control-bg, 3%); - --control-bg-shaded: darken(@control-bg, 4%); - --control-color: @control-color; - --control-danger-bg: @control-danger-bg; - --control-danger-color: @control-danger-color; - --control-success-bg: @control-success-bg; - --control-success-color: @control-success-color; - --control-warning-bg: @control-warning-bg; - --control-warning-color: @control-warning-color; - --control-body-bg-mix: mix(@control-bg, @body-bg, 50%); - --control-muted-color: lighten(@control-color, 40%); + .Button--color-vars(@control-color, @control-bg-light, 'button'); + .Button--color-vars(@control-color, @body-bg-light, 'button-inverted'); +} - --error-color: @error-color; +// --------------------------------- +// Dark colors + +[data-theme^=dark] { + .scheme(dark); // --------------------------------- // UTILITIES - --text-on-dark: @text-on-dark; - --text-on-light: @text-on-light; + --yiq-threshold: 108; +} - & when (@config-dark-mode = true) { - --yiq-threshold: 108; - } - & when (@config-dark-mode = false) { - --yiq-threshold: 150; +// High contrast + +[data-theme=dark-hc] { + @control-color: lighten(@control-color-dark, 20%); + @muted-color: lighten(@muted-color-dark, 20%); + + --control-color: @control-color; + --control-muted-color: darken(@control-color, 20%); + + --muted-color: @muted-color; + --muted-color-light: darken(@muted-color, 10%); + --muted-color-dark: lighten(@muted-color, 50%); + --muted-more-color: lighten(@muted-more-color-dark, 20%); + + --discussion-title-color: lighten(@discussion-title-color-dark, 20%); + + .Button--color-vars(@control-color, @control-bg-dark, 'button'); + .Button--color-vars(@control-color, @body-bg-dark, 'button-inverted'); +} + +// COMMON LIGHT/DARK HIGH CONTRAST COLORS + +[data-theme=dark-hc], [data-theme=light-hc] { + .Button--color-vars(darken(@body-bg-dark, 10), @primary-color, 'button-primary'); + + [data-colored-header=true]& { + --header-bg: @header-bg-colored-dark; + --header-color: @header-color-colored-dark; + --header-control-bg: @header-control-bg-colored-dark; + --header-control-color: darken(@header-control-color-colored-dark, 15); + + .light-contents-vars(@header-color-colored-dark, @header-control-bg-colored-dark, darken(@header-control-color-colored-dark, 15), 'header-colored'); } - // --------------------------------- - // COMPONENT COLORS + --yiq-threshold: 80; +} + +// --------------------------------- +// Common light/dark colors + +:root { - --header-bg: @header-bg; - --header-color: @header-color; - --header-control-bg: @header-control-bg; - --header-control-color: @header-control-color; + --primary-color: @primary-color; + --secondary-color: @secondary-color; - --overlay-bg: @overlay-bg; - --code-bg: @code-bg; - --code-color: @code-color; + --link-color: @link-color; + + --control-success-bg: @control-success-bg; + --control-success-color: @control-success-color; + --control-warning-bg: @control-warning-bg; + --control-warning-color: @control-warning-color; + + --error-color: @error-color; + + --text-on-dark: @text-on-dark; + --text-on-light: @text-on-light; --alert-bg: @alert-bg; --alert-color: @alert-color; @@ -76,34 +193,30 @@ --validation-error-color: @validation-error-color; - --avatar-bg: var(--control-bg); - --badge-bg: var(--muted-color); - --badge-color: #fff; - --badge-hidden-bg: #888; - --usercard-bg: var(--control-bg); - --hero-bg: @hero-bg; - --hero-color: @hero-color; - --tooltip-bg: @tooltip-bg; --tooltip-color: @tooltip-color; --loading-indicator-color: var(--muted-color); --online-user-circle-color: @online-user-circle-color; - - --discussion-title-color: mix(@heading-color, @body-bg, 55%); --discussion-list-item-bg-hover: var(--control-body-bg-mix); - .Button--color-vars(@control-color, @control-bg, 'button'); - .Button--color-vars(@body-bg, @primary-color, 'button-primary'); - .Button--color-vars(@control-danger-color, @control-danger-bg, 'control-danger'); + --avatar-bg: var(--control-bg); + --badge-bg: var(--muted-color); + --badge-color: #fff; + --badge-hidden-bg: #888; + --usercard-bg: var(--control-bg); + --hero-bg: var(--control-bg); + --hero-color: var(--control-color); + + .light-contents-vars(); + .Button--color-vars(@control-success-color, @control-success-bg, 'control-success'); .Button--color-vars(@control-warning-color, @control-warning-bg, 'control-warning'); - .Button--color-vars(@muted-more-color, fade(@muted-more-color, 30%), 'muted-more'); - .Button--color-vars(@control-color, @body-bg, 'button-inverted'); - .light-contents-vars(); - .light-contents-vars(@header-color, @header-control-bg, @header-control-color, 'header-colored'); +} + +:root { // --------------------------------- // LAYOUT diff --git a/framework/core/less/common/variables.less b/framework/core/less/common/variables.less index 4995e850a4..04d1d45983 100644 --- a/framework/core/less/common/variables.less +++ b/framework/core/less/common/variables.less @@ -3,88 +3,95 @@ @config-primary-color: #536F90; @config-secondary-color: #536F90; -@config-dark-mode: false; -@config-colored-header: false; // --------------------------------- // COLORS +@primary-color: @config-primary-color; +@secondary-color: @config-secondary-color; + @primary-hue: hue(@primary-color); @primary-sat: saturation(@primary-color); @secondary-hue: hue(@secondary-color); @secondary-sat: saturation(@secondary-color); +// SCHEMES + @body-bg-light: #fff; @body-bg-dark: hsl(@secondary-hue, min(20%, @secondary-sat), 10%); -// Derive the primary/secondary colors from the config variables. In dark mode, -// we make the user-set colors a bit darker, otherwise they will stand out too -// much. -.define-colors(@config-dark-mode); -.define-colors(false) { - @primary-color: @config-primary-color; - @secondary-color: @config-secondary-color; - - @body-bg: @body-bg-light; - @text-color: #111; - @link-color: saturate(@primary-color, 10%); - @heading-color: @text-color; - @muted-color: hsl(@secondary-hue, min(20%, @secondary-sat), 50%); - @muted-more-color: #aaa; - @shadow-color: rgba(0, 0, 0, 0.35); - - @control-bg: hsl(@secondary-hue, min(50%, @secondary-sat), 93%); - @control-color: @muted-color; - @control-danger-bg: #fdd; - @control-danger-color: #d66; - @control-success-bg: #B4F1AF; - @control-success-color: #33722D; - @control-warning-bg: #fff2ae; - @control-warning-color: #ad6c00; - - @overlay-bg: fade(@secondary-color, 90%); - - @code-bg: darken(@body-bg, 3%); - @code-color: lighten(@text-color, 30%); -} -.define-colors(true) { - @primary-color: @config-primary-color; - @secondary-color: @config-secondary-color; - - @body-bg: @body-bg-dark; - @text-color: #ddd; - @link-color: saturate(@primary-color, 10%); - @heading-color: @text-color; - @muted-color: hsl(@secondary-hue, min(15%, @secondary-sat), 50%); - @muted-more-color: hsl(@secondary-hue, min(10%, @secondary-sat), 40%); - @shadow-color: rgba(0, 0, 0, 0.5); - - @control-bg: hsl(@secondary-hue, min(20%, @secondary-sat), 13%); - @control-color: @muted-color; - @control-danger-bg: #411; - @control-danger-color: #a88; - @control-success-bg: #B4F1AF; - @control-success-color: #33722D; - @control-warning-bg: #fff2ae; - @control-warning-color: #ad6c00; - - @overlay-bg: fade(darken(@body-bg, 5%), 90%); - - @code-bg: darken(@body-bg, 3%); - @code-color: #fff; -} - -// Beyond dark or light mode, we need stable colors depending on the luminosity -// of the parents element's background. This allow not to change the color -// variable depending on the dark/light mode to get the same final color, and -// thus to simplify the logic. +@muted-color-light: hsl(@secondary-hue, min(20%, @secondary-sat), 50%); +@muted-color-dark: hsl(@secondary-hue, min(15%, @secondary-sat), 50%); +@muted-more-color-light: #aaa; +@muted-more-color-dark: hsl(@secondary-hue, min(10%, @secondary-sat), 40%); + +@link-color: saturate(@primary-color, 10%); + +@control-bg-light: hsl(@secondary-hue, min(50%, @secondary-sat), 93%); +@control-bg-dark: hsl(@secondary-hue, min(20%, @secondary-sat), 13%); + +@control-color-light: @muted-color-light; +@control-color-dark: @muted-color-dark; + +@control-danger-bg-light: #fdd; +@control-danger-bg-dark: #411; +@control-danger-color-light: #d66; +@control-danger-color-dark: #a88; + +@text-color-light: #111; +@text-color-dark: #ddd; + +@heading-color-light: @text-color-light; +@heading-color-dark: @text-color-dark; + +@shadow-color-light: rgba(0, 0, 0, 0.35); +@shadow-color-dark: rgba(0, 0, 0, 0.5); + +@overlay-bg-light: fade(@secondary-color, 90%); +@overlay-bg-dark: fade(darken(@body-bg-dark, 5%), 90%); + +@code-bg-light: darken(@body-bg-light, 3%); +@code-bg-dark: darken(@body-bg-dark, 3%); + +@code-color-light: lighten(@text-color-light, 30%); +@code-color-dark: #fff; + @text-on-dark: @body-bg-light; @text-on-light: @body-bg-dark; -@hero-bg: @control-bg; -@hero-color: @control-color; -@hero-muted-color: @control-color; +@discussion-title-color-light: mix(@heading-color-light, @body-bg-light, 55%); +@discussion-title-color-dark: mix(@heading-color-dark, @body-bg-dark, 55%); + +// Header colors depend on whether the header is colored, +// and whether we are on light or dark modes. + +@header-bg-light: @body-bg-light; +@header-color-light: @primary-color; +@header-control-bg-light: @control-bg-light; +@header-control-color-light: @control-color-light; + +@header-bg-dark: @body-bg-dark; +@header-color-dark: @primary-color; +@header-control-bg-dark: @control-bg-dark; +@header-control-color-dark: @control-color-dark; + +@header-bg-colored-light: @primary-color; +@header-color-colored-light: @body-bg-light; +@header-control-bg-colored-light: mix(#000, @header-bg-colored-light, 10%); +@header-control-color-colored-light: mix(@body-bg-light, @header-bg-colored-light, 60%); + +@header-bg-colored-dark: @primary-color; +@header-color-colored-dark: @body-bg-dark; +@header-control-bg-colored-dark: mix(#000, @header-bg-colored-dark, 10%); +@header-control-color-colored-dark: mix(@body-bg-dark, @header-bg-colored-dark, 60%); + +// COMMON COLORS + +@control-success-bg: #B4F1AF; +@control-success-color: #33722D; +@control-warning-bg: #fff2ae; +@control-warning-color: #ad6c00; @error-color: #d83e3e; @@ -99,20 +106,6 @@ @validation-error-color: @error-color; -.define-header(@config-colored-header); -.define-header(false) { - @header-bg: @body-bg; - @header-color: @primary-color; - @header-control-bg: @control-bg; - @header-control-color: @control-color; -} -.define-header(true) { - @header-bg: @primary-color; - @header-color: @body-bg; - @header-control-bg: mix(#000, @header-bg, 10%); - @header-control-color: mix(@body-bg, @header-bg, 60%); -} - // --------------------------------- // LAYOUT diff --git a/framework/core/less/forum/HeaderList.less b/framework/core/less/forum/HeaderList.less index 125135fa5b..54dc58b190 100644 --- a/framework/core/less/forum/HeaderList.less +++ b/framework/core/less/forum/HeaderList.less @@ -32,7 +32,7 @@ // dropdown menu – but the drawer may have .light-contents() applied to // it. In this case we will need to reset the button's styles back to // normal. - & when (@config-colored-header = true) { + [data-colored-header=true] & { color: var(--control-color); &:hover, @@ -196,7 +196,7 @@ .add-keyboard-focus-ring-offset(4px); // Needs more specificity to fix hover/focus styles not applying in dropdown - .HeaderListItem & when (@config-colored-header = true) { + [data-colored-header=true] .HeaderListItem & { color: var(--control-color); &:hover, diff --git a/framework/core/less/forum/PageStructure.less b/framework/core/less/forum/PageStructure.less index 6b22dfe9fe..ec32af4a97 100644 --- a/framework/core/less/forum/PageStructure.less +++ b/framework/core/less/forum/PageStructure.less @@ -1,3 +1,22 @@ +.Page--cols { + &-container { + display: flex; + } + + &-content { + width: var(--content-width); + } + + &-sidebar { + width: var(--sidebar-width); + flex-shrink: 0; + } + + &-content, &-sidebar { + margin-top: 30px; + } +} + .Page { --content-width: 100%; --sidebar-width: 190px; @@ -17,22 +36,3 @@ } } } - -.Page--cols { - &-container { - display: flex; - } - - &-content { - width: var(--content-width); - } - - &-sidebar { - width: var(--sidebar-width); - flex-shrink: 0; - } - - &-content, &-sidebar { - margin-top: 30px; - } -} diff --git a/framework/core/locale/core.yml b/framework/core/locale/core.yml index 7f540a16f0..9367cacd1c 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -56,6 +56,7 @@ core: # These translations are used in the Appearance page. appearance: + allow_user_color_scheme_label: Allow users to choose their own color scheme colored_header_label: Colored Header colors_heading: Colors colors_primary_label: Primary Color @@ -68,7 +69,6 @@ core: custom_styles_cannot_use_less_features: "The @import and data-uri features are not allowed in custom LESS." custom_styles_heading: Custom Styles custom_styles_text: Customize your forum's appearance by adding your own Less/CSS code to be applied on top of Flarum's default styles. - dark_mode_label: Dark Mode description: "Customize your forum's colors, logos, and other variables." edit_css_button: Edit Custom CSS edit_footer_button: => core.ref.custom_footer_title @@ -79,6 +79,13 @@ core: logo_heading: Logo logo_text: Upload an image to be displayed in place of the forum title. title: Appearance + color_scheme_label: Color Scheme (default) + color_schemes: + auto_mode_label: User system or configured preference + dark_hc_mode_label: => core.ref.dark_hc_mode_label + dark_mode_label: => core.ref.dark_mode_label + light_hc_mode_label: => core.ref.light_hc_mode_label + light_mode_label: => core.ref.light_mode_label # These translations are used in the Basics page. basics: @@ -606,6 +613,13 @@ core: account_heading: Account change_email_button: => core.ref.change_email change_password_button: => core.ref.change_password + color_scheme_heading: Color Scheme + color_schemes: + auto_mode_label: System preference + dark_hc_mode_label: => core.ref.dark_hc_mode_label + dark_mode_label: => core.ref.dark_mode_label + light_hc_mode_label: => core.ref.light_hc_mode_label + light_mode_label: => core.ref.light_mode_label notification_checkbox_a11y_label_template: 'Receive "{description}" notifications via {method}' notifications_heading: => core.ref.notifications notify_by_email_heading: => core.ref.email @@ -961,6 +975,8 @@ core: custom_footer_title: Edit Custom Footer custom_header_text: "Add HTML to be displayed at the very top of the page, above Flarum's own header." custom_header_title: Edit Custom Header + dark_hc_mode_label: Dark High Contrast Mode + dark_mode_label: Dark Mode delete: Delete delete_forever: Delete Forever discussions: Discussions # Referenced by flarum-statistics.yml @@ -972,6 +988,8 @@ core: icon: Icon icon_text: "Enter the name of any FontAwesome icon class, including the fas fa- prefix." invalid_login_message: Your login details were incorrect. + light_hc_mode_label: Light High Contrast Mode + light_mode_label: Light Mode load_more: Load More loading: Loading... log_in: Log In diff --git a/framework/core/migrations/2024_06_07_000000_change_to_theme_mode_setting_and_preference.php b/framework/core/migrations/2024_06_07_000000_change_to_theme_mode_setting_and_preference.php new file mode 100644 index 0000000000..85b0721d45 --- /dev/null +++ b/framework/core/migrations/2024_06_07_000000_change_to_theme_mode_setting_and_preference.php @@ -0,0 +1,56 @@ + function (Builder $schema) { + $darkMode = $schema->getConnection() + ->table('settings') + ->where('key', 'theme_dark_mode') + ->first(); + + $schema->getConnection() + ->table('settings') + ->insert([ + [ + 'key' => 'color_scheme', + 'value' => $darkMode === '1' ? 'dark' : 'auto', + ], + [ + 'key' => 'allow_user_color_scheme', + 'value' => '1', + ] + ]); + + $schema->getConnection() + ->table('settings') + ->where('key', 'theme_dark_mode') + ->delete(); + }, + + 'down' => function (Builder $schema) { + $themeMode = $schema->getConnection() + ->table('settings') + ->where('key', 'color_scheme') + ->first(); + + $schema->getConnection() + ->table('settings') + ->insert([ + 'key' => 'theme_dark_mode', + 'value' => $themeMode === 'dark' ? '1' : '0', + ]); + + $schema->getConnection() + ->table('settings') + ->whereIn('key', ['color_scheme', 'allow_user_color_scheme']) + ->delete(); + } +]; diff --git a/framework/core/src/Extend/Frontend.php b/framework/core/src/Extend/Frontend.php index 331f63a024..ef05abe898 100644 --- a/framework/core/src/Extend/Frontend.php +++ b/framework/core/src/Extend/Frontend.php @@ -36,6 +36,7 @@ class Frontend implements ExtenderInterface private array $preloadArrs = []; private ?string $titleDriver = null; private array $jsDirectory = []; + private array $extraDocumentAttributes = []; /** * @param string $frontend: The name of the frontend. @@ -187,6 +188,35 @@ public function title(string $driverClass): self return $this; } + /** + * Adds document root attributes. + * + * @example ['data-test' => 'value'] + * @example ['data-test' => function (ServerRequestInterface $request) { return 'value'; }] + * + * @param array $attributes + */ + public function extraDocumentAttributes(array $attributes): self + { + $this->extraDocumentAttributes[] = $attributes; + + return $this; + } + + /** + * Adds document root classes. + * + * Can either be a string or an array of strings. + * + * An array can be of a format acceptable by the @class blade directive. + * + * @example ['class1', 'class2' => true, 'class3' => false] + */ + public function extraDocumentClasses(string|array|callable $classes): self + { + return $this->extraDocumentAttributes(['class' => $classes]); + } + public function extend(Container $container, Extension $extension = null): void { $this->registerAssets($container, $this->getModuleName($extension)); @@ -194,6 +224,7 @@ public function extend(Container $container, Extension $extension = null): void $this->registerContent($container); $this->registerPreloads($container); $this->registerTitleDriver($container); + $this->registerDocumentAttributes($container); } private function registerAssets(Container $container, string $moduleName): void @@ -330,4 +361,23 @@ private function registerTitleDriver(Container $container): void }); } } + + private function registerDocumentAttributes(Container $container): void + { + if (empty($this->extraDocumentAttributes)) { + return; + } + + $container->resolving( + "flarum.frontend.$this->frontend", + function (ActualFrontend $frontend, Container $container) { + $frontend->content(function (Document $document) use ($container) { + foreach ($this->extraDocumentAttributes as $classes) { + $classes = is_callable($classes) ? ContainerUtil::wrapCallback($classes, $container) : $classes; + $document->extraAttributes[] = $classes; + } + }, 111); + } + ); + } } diff --git a/framework/core/src/Frontend/Document.php b/framework/core/src/Frontend/Document.php index 3b03602cc1..ed242495af 100644 --- a/framework/core/src/Frontend/Document.php +++ b/framework/core/src/Frontend/Document.php @@ -135,6 +135,15 @@ class Document implements Renderable */ public array $preloads = []; + /** + * Document extra attributes. + * + * @var array + */ + public array $extraAttributes = [ + 'class' => [], + ]; + /** * We need the versioner to get the revisions of split chunks. */ @@ -171,6 +180,8 @@ protected function makeView(): View 'js' => $this->makeJs(), 'head' => $this->makeHead(), 'foot' => $this->makeFoot(), + 'extraAttributes' => $this->makeExtraAttributes(), + 'extraClasses' => $this->makeExtraClasses(), 'revisions' => $this->versioner->allRevisions(), 'debug' => $this->config->inDebugMode(), ]); @@ -208,6 +219,50 @@ protected function makePreloads(): array }, $this->preloads); } + protected function makeExtraClasses(): array + { + $classes = []; + + $extraClasses = $this->extraAttributes['class'] ?? []; + + foreach ($extraClasses as $class) { + if (is_callable($class)) { + $class = $class($this->request); + } + + $classes = array_merge($classes, (array) $class); + } + + return $classes; + } + + protected function makeExtraAttributes(): string + { + $attributes = []; + + foreach ($this->extraAttributes as $key => $value) { + if ($key === 'class') { + continue; + } + + if (is_callable($value)) { + $value = $value($this->request); + } + + $attributes[$key] = $value; + } + + return array_reduce(array_keys($attributes), function (string $carry, string $key) use ($attributes): string { + $value = $attributes[$key]; + + if (is_array($value)) { + $value = implode(' ', $value); + } + + return $carry.' '.$key.'="'.e($value).'"'; + }, ''); + } + protected function makeHead(): string { $head = array_map(function ($url) { diff --git a/framework/core/src/Frontend/FrontendServiceProvider.php b/framework/core/src/Frontend/FrontendServiceProvider.php index 6df1d685b1..bc859d89e9 100644 --- a/framework/core/src/Frontend/FrontendServiceProvider.php +++ b/framework/core/src/Frontend/FrontendServiceProvider.php @@ -17,6 +17,7 @@ use Flarum\Frontend\Compiler\Source\SourceCollector; use Flarum\Frontend\Driver\BasicTitleDriver; use Flarum\Frontend\Driver\TitleDriverInterface; +use Flarum\Http\RequestUtil; use Flarum\Http\SlugManager; use Flarum\Http\UrlGenerator; use Flarum\Locale\LocaleManager; @@ -24,6 +25,7 @@ use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\View\Factory as ViewFactory; +use Psr\Http\Message\ServerRequestInterface; class FrontendServiceProvider extends AbstractServiceProvider { @@ -95,6 +97,16 @@ public function register(): void $default_preloads, $document->preloads, ); + + /** @var SettingsRepositoryInterface $settings */ + $settings = $container->make(SettingsRepositoryInterface::class); + + // Add document classes/attributes for design use cases. + $document->extraAttributes['data-theme'] = $settings->get('color_scheme'); + $document->extraAttributes['data-colored-header'] = $settings->get('theme_colored_header') ? 'true' : 'false'; + $document->extraAttributes['class'][] = function (ServerRequestInterface $request) { + return RequestUtil::getActor($request)->isGuest() ? 'guest-user' : 'logged-in'; + }; }, 160); return $frontend; @@ -152,18 +164,6 @@ function (Container $container) { 'config-secondary-color' => [ 'key' => 'theme_secondary_color', ], - 'config-dark-mode' => [ - 'key' => 'theme_dark_mode', - 'callback' => function ($value) { - return $value ? 'true' : 'false'; - }, - ], - 'config-colored-header' => [ - 'key' => 'theme_colored_header', - 'callback' => function ($value) { - return $value ? 'true' : 'false'; - }, - ], ]; }); diff --git a/framework/core/src/Install/Steps/WriteSettings.php b/framework/core/src/Install/Steps/WriteSettings.php index b62c535157..e4bb582e0a 100644 --- a/framework/core/src/Install/Steps/WriteSettings.php +++ b/framework/core/src/Install/Steps/WriteSettings.php @@ -62,7 +62,7 @@ private function getDefaults(): array 'mail_from' => 'noreply@localhost', 'slug_driver_Flarum\User\User' => 'default', 'theme_colored_header' => '0', - 'theme_dark_mode' => '0', + 'color_scheme' => 'auto', 'theme_primary_color' => '#4D698E', 'theme_secondary_color' => '#4D698E', 'welcome_message' => 'Enjoy your new forum! Hop over to discuss.flarum.org if you have any questions, or to join our community!', diff --git a/framework/core/src/User/UserServiceProvider.php b/framework/core/src/User/UserServiceProvider.php index 7334cd5c51..524871feaa 100644 --- a/framework/core/src/User/UserServiceProvider.php +++ b/framework/core/src/User/UserServiceProvider.php @@ -124,6 +124,7 @@ public function boot(Container $container, Dispatcher $events): void User::registerPreference('discloseOnline', 'boolval', true); User::registerPreference('indexProfile', 'boolval', true); User::registerPreference('locale'); + User::registerPreference('colorScheme', 'strval', 'auto'); User::registerVisibilityScoper(new ScopeUserVisibility(), 'view'); } diff --git a/framework/core/views/frontend/app.blade.php b/framework/core/views/frontend/app.blade.php index ff025f38d2..764d53c6c1 100644 --- a/framework/core/views/frontend/app.blade.php +++ b/framework/core/views/frontend/app.blade.php @@ -1,6 +1,5 @@ - + {{ $title }}