From 84b411647838861fa19df957022464f3b662f0bd Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sat, 8 Jun 2024 09:19:37 +0100 Subject: [PATCH 1/9] feat: vanilla CSS color scheme changes --- extensions/flags/less/forum.less | 10 +- extensions/package-manager/less/admin.less | 6 - extensions/tags/less/forum/ToggleButton.less | 6 +- .../src/admin/components/AppearancePage.tsx | 33 ++- framework/core/js/src/common/Application.tsx | 47 ++++ .../js/src/common/components/ThemeMode.tsx | 62 +++++ .../js/src/forum/components/SettingsPage.tsx | 56 ++++- framework/core/less/common/App.less | 8 +- framework/core/less/common/Checkbox.less | 5 + framework/core/less/common/Form.less | 4 + framework/core/less/common/ThemeMode.less | 156 +++++++++++++ framework/core/less/common/common.less | 1 + framework/core/less/common/root.less | 211 +++++++++++++----- framework/core/less/common/variables.less | 148 ++++++------ framework/core/less/forum/HeaderList.less | 4 +- framework/core/locale/core.yml | 14 +- ...e_to_theme_mode_setting_and_preference.php | 57 +++++ .../src/Api/Serializer/ForumSerializer.php | 2 +- framework/core/src/Extend/Frontend.php | 50 +++++ framework/core/src/Frontend/Document.php | 56 +++++ .../src/Frontend/FrontendServiceProvider.php | 12 + .../core/src/Install/Steps/WriteSettings.php | 2 +- .../core/src/User/UserServiceProvider.php | 1 + framework/core/views/frontend/app.blade.php | 3 +- 24 files changed, 788 insertions(+), 166 deletions(-) create mode 100644 framework/core/js/src/common/components/ThemeMode.tsx create mode 100644 framework/core/less/common/ThemeMode.less create mode 100644 framework/core/migrations/2024_06_07_000000_change_to_theme_mode_setting_and_preference.php diff --git a/extensions/flags/less/forum.less b/extensions/flags/less/forum.less index 9b89587c0c..4ba42eacfe 100644 --- a/extensions/flags/less/forum.less +++ b/extensions/flags/less/forum.less @@ -1,3 +1,11 @@ +:root { + .light-contents-vars(@color: @body-bg-light; @control-color: @body-bg-light; @name: 'flagged-post'); +} + +[data-theme=dark]:root { + .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/tags/less/forum/ToggleButton.less b/extensions/tags/less/forum/ToggleButton.less index 35d43a2228..d0198a12fd 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'); + .Button--color-vars(@control-bg-light, @control-color-light, 'button-toggled'); +} + +[data-theme=dark]:root { + .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..fa9c09ab7d 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 ( +
+ +
+ {['auto', 'light', 'dark'].map((mode: string) => ( + { + this.setting('color_scheme')(mode); + + this.setting('allow_user_color_scheme')(mode === 'auto' ? '1' : '0'); + + app.setColorScheme(mode); + }} + selected={this.setting('color_scheme')() === mode} + /> + ))} +
+
+ ); }), 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..97d0c9ecec 100644 --- a/framework/core/js/src/common/Application.tsx +++ b/framework/core/js/src/common/Application.tsx @@ -245,6 +245,8 @@ export default class Application { data!: ApplicationData; + allowUserColorScheme!: boolean; + private _title: string = ''; private _titleCount: number = 0; @@ -356,9 +358,54 @@ 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(): string { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + } + + watchSystemColorSchemePreference(callback: () => void): void { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', callback); + } + + setColorScheme(scheme: string): void { + if (scheme === '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..ba2188f874 --- /dev/null +++ b/framework/core/js/src/common/components/ThemeMode.tsx @@ -0,0 +1,62 @@ +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 default class ThemeMode extends Component { + view(vnode: Mithril.Vnode): Mithril.Children { + const { mode, selected, className, alternate, label, ...attrs } = vnode.attrs; + + return ( + + ); + } +} diff --git a/framework/core/js/src/forum/components/SettingsPage.tsx b/framework/core/js/src/forum/components/SettingsPage.tsx index 15e18b95a8..c10508f11c 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(); + + ['auto', 'light', 'dark'].forEach((mode) => { + items.add( + mode, + { + this.colorSchemeLoading = true; + + this.user!.savePreferences({ colorScheme: mode }).then(() => { + this.colorSchemeLoading = false; + app.setColorScheme(mode); + m.redraw(); + }); + }} + />, + 100 + ); + }); + + return items; + } } 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..cdecf14f2d --- /dev/null +++ b/framework/core/less/common/ThemeMode.less @@ -0,0 +1,156 @@ +.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); +} + +.ThemeMode-hero { + background-color: var(--control-bg); + height: 40px; + flex-shrink: 0; +} + +.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; +} + +.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-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/root.less b/framework/core/less/common/root.less index 3ddf7d58e1..afe83a8a0a 100644 --- a/framework/core/less/common/root.less +++ b/framework/core/less/common/root.less @@ -1,65 +1,168 @@ -:root { +// --------------------------------- +// Light colors +[data-theme=light] { // --------------------------------- // COLORS - --primary-color: @primary-color; - --secondary-color: @secondary-color; + --body-bg: @body-bg-light; + --body-bg-shaded: darken(@body-bg-light, 3%); + --body-bg-light: lighten(@body-bg-light, 5%); + --body-bg-faded: fade(@body-bg-light, 93%); + --text-color: @text-color-light; + --heading-color: @heading-color-light; + + --muted-color: @muted-color-light; + --muted-color-light: lighten(@muted-color-light, 10%); + --muted-color-dark: darken(@muted-color-light, 50%); + --muted-more-color: @muted-more-color-light; + + --shadow-color: @shadow-color-light; + + --control-bg: @control-bg-light; + --control-bg-light: lighten(@control-bg-light, 3%); + --control-bg-shaded: darken(@control-bg-light, 4%); + --control-color: @control-color-light; + --control-danger-bg: @control-danger-bg-light; + --control-danger-color: @control-danger-color-light; + --control-body-bg-mix: mix(@control-bg-light, @body-bg-light, 50%); + --control-muted-color: lighten(@control-color-light, 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 - --muted-color: @muted-color; - --muted-color-light: lighten(@muted-color, 10%); - --muted-color-dark: darken(@muted-color, 50%); - --muted-more-color: @muted-more-color; + --header-bg: @header-bg-light; + --header-color: @header-color-light; + --header-control-bg: @header-control-bg-light; + --header-control-color: @header-control-color-light; - --shadow-color: @shadow-color; + [data-colored-header=true]& { + --header-bg: @header-bg-colored-light; + --header-color: @header-color-colored-light; + --header-control-bg: @header-control-bg-colored-light; + --header-control-color: @header-control-color-colored-light; - --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%); + .light-contents-vars(@header-color-colored-light, @header-control-bg-colored-light, @header-control-color-colored-light, 'header-colored'); + } - --error-color: @error-color; + --overlay-bg: @overlay-bg-light; + --code-bg: @code-bg-light; + --code-color: @code-color-light; + + --avatar-bg: var(--control-bg); + --badge-bg: var(--muted-color); + --badge-color: #fff; + --badge-hidden-bg: #888; + --usercard-bg: var(--control-bg); + --hero-bg: @control-bg-light; + --hero-color: @control-color-light; + + --discussion-title-color: mix(@heading-color-light, @body-bg-light, 55%); + + .Button--color-vars(@control-color-light, @control-bg-light, 'button'); + .Button--color-vars(@body-bg-light, @primary-color, 'button-primary'); + .Button--color-vars(@control-danger-color-light, @control-danger-bg-light, 'control-danger'); + .Button--color-vars(@muted-more-color-light, fade(@muted-more-color-light, 30%), 'muted-more'); + .Button--color-vars(@control-color-light, @body-bg-light, 'button-inverted'); // --------------------------------- // UTILITIES - --text-on-dark: @text-on-dark; - --text-on-light: @text-on-light; + --yiq-threshold: 150; +} - & when (@config-dark-mode = true) { - --yiq-threshold: 108; - } - & when (@config-dark-mode = false) { - --yiq-threshold: 150; - } +// --------------------------------- +// Dark colors + +[data-theme=dark] { + // --------------------------------- + // COLORS + + --body-bg: @body-bg-dark; + --body-bg-shaded: darken(@body-bg-dark, 3%); + --body-bg-light: lighten(@body-bg-dark, 5%); + --body-bg-faded: fade(@body-bg-dark, 93%); + --text-color: @text-color-dark; + --heading-color: @heading-color-dark; + + --muted-color: @muted-color-dark; + --muted-color-light: lighten(@muted-color-dark, 10%); + --muted-color-dark: darken(@muted-color-dark, 50%); + --muted-more-color: @muted-more-color-dark; + + --shadow-color: @shadow-color-dark; + + --control-bg: @control-bg-dark; + --control-bg-light: lighten(@control-bg-dark, 3%); + --control-bg-shaded: darken(@control-bg-dark, 4%); + --control-color: @control-color-dark; + --control-danger-bg: @control-danger-bg-dark; + --control-danger-color: @control-danger-color-dark; + --control-body-bg-mix: mix(@control-bg-dark, @body-bg-dark, 50%); + --control-muted-color: lighten(@control-color-dark, 40%); // --------------------------------- // COMPONENT COLORS - --header-bg: @header-bg; - --header-color: @header-color; - --header-control-bg: @header-control-bg; - --header-control-color: @header-control-color; + --header-bg: @header-bg-dark; + --header-color: @header-color-dark; + --header-control-bg: @header-control-bg-dark; + --header-control-color: @header-control-color-dark; + + [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: @header-control-color-colored-dark; + + .light-contents-vars(@header-color-colored-dark, @header-control-bg-colored-dark, @header-control-color-colored-dark, 'header-colored'); + } + + --overlay-bg: @overlay-bg-dark; + --code-bg: @code-bg-dark; + --code-color: @code-color-dark; + + --avatar-bg: var(--control-bg); + --badge-bg: var(--muted-color); + --badge-color: #fff; + --badge-hidden-bg: #888; + --usercard-bg: var(--control-bg); + --hero-bg: @control-bg-dark; + --hero-color: @control-color-dark; + + --discussion-title-color: mix(@heading-color-dark, @body-bg-dark, 55%); + + .Button--color-vars(@control-color-dark, @control-bg-dark, 'button'); + .Button--color-vars(@body-bg-dark, @primary-color, 'button-primary'); + .Button--color-vars(@control-danger-color-dark, @control-danger-bg-dark, 'control-danger'); + .Button--color-vars(@muted-more-color-dark, fade(@muted-more-color-dark, 30%), 'muted-more'); + .Button--color-vars(@control-color-dark, @body-bg-dark, 'button-inverted'); + + // --------------------------------- + // UTILITIES + + --yiq-threshold: 108; +} + +// --------------------------------- +// Common light/dark colors + +:root { + + --primary-color: @primary-color; + --secondary-color: @secondary-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; - --overlay-bg: @overlay-bg; - --code-bg: @code-bg; - --code-color: @code-color; + --text-on-dark: @text-on-dark; + --text-on-light: @text-on-light; --alert-bg: @alert-bg; --alert-color: @alert-color; @@ -76,34 +179,22 @@ --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'); + .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..ee14496a8f 100644 --- a/framework/core/less/common/variables.less +++ b/framework/core/less/common/variables.less @@ -9,83 +9,66 @@ // --------------------------------- // 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); -@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. + +@body-bg-light: #fff; +@body-bg-dark: hsl(@secondary-hue, min(20%, @secondary-sat), 10%); + +@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-success-bg: #B4F1AF; +@control-success-color: #33722D; +@control-warning-bg: #fff2ae; +@control-warning-color: #ad6c00; + +@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; - @error-color: #d83e3e; @alert-bg: #fff2ae; @@ -99,19 +82,28 @@ @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%); -} +// 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%); // --------------------------------- // 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/locale/core.yml b/framework/core/locale/core.yml index 7f540a16f0..fd2aab5a60 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,11 @@ 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_mode_label: => core.ref.dark_mode_label + light_mode_label: => core.ref.light_mode_label # These translations are used in the Basics page. basics: @@ -606,6 +611,11 @@ 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_mode_label: => core.ref.dark_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 +971,7 @@ 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_mode_label: Dark Mode delete: Delete delete_forever: Delete Forever discussions: Discussions # Referenced by flarum-statistics.yml @@ -972,6 +983,7 @@ 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_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..2b853c027a --- /dev/null +++ b/framework/core/migrations/2024_06_07_000000_change_to_theme_mode_setting_and_preference.php @@ -0,0 +1,57 @@ + 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/Api/Serializer/ForumSerializer.php b/framework/core/src/Api/Serializer/ForumSerializer.php index 29f467e3a3..911811b714 100644 --- a/framework/core/src/Api/Serializer/ForumSerializer.php +++ b/framework/core/src/Api/Serializer/ForumSerializer.php @@ -91,7 +91,7 @@ protected function getDefaultAttributes(object|array $model): array 'canModerateAccessTokens' => $this->actor->can('moderateAccessTokens'), 'canEditUserCredentials' => $this->actor->hasPermission('user.editCredentials'), 'assetsBaseUrl' => rtrim($this->assetsFilesystem->url(''), '/'), - 'jsChunksBaseUrl' => $this->assetsFilesystem->url('js'), + 'jsChunksBaseUrl' => $this->assetsFilesystem->url('js') ]; if ($this->actor->can('administrate')) { 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..b1a1bc606f 100644 --- a/framework/core/src/Frontend/Document.php +++ b/framework/core/src/Frontend/Document.php @@ -13,6 +13,7 @@ use Flarum\Frontend\Compiler\FileVersioner; use Flarum\Frontend\Compiler\VersionerInterface; use Flarum\Frontend\Driver\TitleDriverInterface; +use Flarum\Http\RequestUtil; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\View\Factory; @@ -135,6 +136,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 +181,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 +220,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..995514b7af 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; 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 }} From 8c5c1ffab2140d1753c413ddaf0510e92e5741c9 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sat, 8 Jun 2024 10:26:42 +0100 Subject: [PATCH 2/9] chore: scheme mixin --- framework/core/less/common/root.less | 179 +++++++++++---------------- 1 file changed, 72 insertions(+), 107 deletions(-) diff --git a/framework/core/less/common/root.less b/framework/core/less/common/root.less index afe83a8a0a..7ada6c605a 100644 --- a/framework/core/less/common/root.less +++ b/framework/core/less/common/root.less @@ -1,69 +1,95 @@ -// --------------------------------- -// Light colors +.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}"; -[data-theme=light] { // --------------------------------- // COLORS - --body-bg: @body-bg-light; - --body-bg-shaded: darken(@body-bg-light, 3%); - --body-bg-light: lighten(@body-bg-light, 5%); - --body-bg-faded: fade(@body-bg-light, 93%); - --text-color: @text-color-light; - --heading-color: @heading-color-light; - - --muted-color: @muted-color-light; - --muted-color-light: lighten(@muted-color-light, 10%); - --muted-color-dark: darken(@muted-color-light, 50%); - --muted-more-color: @muted-more-color-light; - - --shadow-color: @shadow-color-light; - - --control-bg: @control-bg-light; - --control-bg-light: lighten(@control-bg-light, 3%); - --control-bg-shaded: darken(@control-bg-light, 4%); - --control-color: @control-color-light; - --control-danger-bg: @control-danger-bg-light; - --control-danger-color: @control-danger-color-light; - --control-body-bg-mix: mix(@control-bg-light, @body-bg-light, 50%); - --control-muted-color: lighten(@control-color-light, 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; + --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%); // --------------------------------- // COMPONENT COLORS - --header-bg: @header-bg-light; - --header-color: @header-color-light; - --header-control-bg: @header-control-bg-light; - --header-control-color: @header-control-color-light; + --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-light; - --header-color: @header-color-colored-light; - --header-control-bg: @header-control-bg-colored-light; - --header-control-color: @header-control-color-colored-light; + --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-light, @header-control-bg-colored-light, @header-control-color-colored-light, 'header-colored'); + .light-contents-vars(@@header-color-colored, @@header-control-bg-colored, @@header-control-color-colored, 'header-colored'); } - --overlay-bg: @overlay-bg-light; - --code-bg: @code-bg-light; - --code-color: @code-color-light; + --overlay-bg: @@overlay-bg; + --code-bg: @@code-bg; + --code-color: @@code-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: @control-bg-light; - --hero-color: @control-color-light; + --hero-bg: @@control-bg; + --hero-color: @@control-color; + + --discussion-title-color: mix(@@heading-color, @@body-bg, 55%); - --discussion-title-color: mix(@heading-color-light, @body-bg-light, 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'); +} - .Button--color-vars(@control-color-light, @control-bg-light, 'button'); - .Button--color-vars(@body-bg-light, @primary-color, 'button-primary'); - .Button--color-vars(@control-danger-color-light, @control-danger-bg-light, 'control-danger'); - .Button--color-vars(@muted-more-color-light, fade(@muted-more-color-light, 30%), 'muted-more'); - .Button--color-vars(@control-color-light, @body-bg-light, 'button-inverted'); +// --------------------------------- +// Light colors + +[data-theme=light] { + .scheme(light); // --------------------------------- // UTILITIES @@ -75,68 +101,7 @@ // Dark colors [data-theme=dark] { - // --------------------------------- - // COLORS - - --body-bg: @body-bg-dark; - --body-bg-shaded: darken(@body-bg-dark, 3%); - --body-bg-light: lighten(@body-bg-dark, 5%); - --body-bg-faded: fade(@body-bg-dark, 93%); - --text-color: @text-color-dark; - --heading-color: @heading-color-dark; - - --muted-color: @muted-color-dark; - --muted-color-light: lighten(@muted-color-dark, 10%); - --muted-color-dark: darken(@muted-color-dark, 50%); - --muted-more-color: @muted-more-color-dark; - - --shadow-color: @shadow-color-dark; - - --control-bg: @control-bg-dark; - --control-bg-light: lighten(@control-bg-dark, 3%); - --control-bg-shaded: darken(@control-bg-dark, 4%); - --control-color: @control-color-dark; - --control-danger-bg: @control-danger-bg-dark; - --control-danger-color: @control-danger-color-dark; - --control-body-bg-mix: mix(@control-bg-dark, @body-bg-dark, 50%); - --control-muted-color: lighten(@control-color-dark, 40%); - - // --------------------------------- - // COMPONENT COLORS - - --header-bg: @header-bg-dark; - --header-color: @header-color-dark; - --header-control-bg: @header-control-bg-dark; - --header-control-color: @header-control-color-dark; - - [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: @header-control-color-colored-dark; - - .light-contents-vars(@header-color-colored-dark, @header-control-bg-colored-dark, @header-control-color-colored-dark, 'header-colored'); - } - - --overlay-bg: @overlay-bg-dark; - --code-bg: @code-bg-dark; - --code-color: @code-color-dark; - - --avatar-bg: var(--control-bg); - --badge-bg: var(--muted-color); - --badge-color: #fff; - --badge-hidden-bg: #888; - --usercard-bg: var(--control-bg); - --hero-bg: @control-bg-dark; - --hero-color: @control-color-dark; - - --discussion-title-color: mix(@heading-color-dark, @body-bg-dark, 55%); - - .Button--color-vars(@control-color-dark, @control-bg-dark, 'button'); - .Button--color-vars(@body-bg-dark, @primary-color, 'button-primary'); - .Button--color-vars(@control-danger-color-dark, @control-danger-bg-dark, 'control-danger'); - .Button--color-vars(@muted-more-color-dark, fade(@muted-more-color-dark, 30%), 'muted-more'); - .Button--color-vars(@control-color-dark, @body-bg-dark, 'button-inverted'); + .scheme(dark); // --------------------------------- // UTILITIES From 477f1f40ba1cfe0cc17845603f1883fef4b23c02 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sat, 8 Jun 2024 09:27:11 +0000 Subject: [PATCH 3/9] Apply fixes from StyleCI --- ..._06_07_000000_change_to_theme_mode_setting_and_preference.php | 1 - framework/core/src/Frontend/Document.php | 1 - 2 files changed, 2 deletions(-) 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 index 2b853c027a..85b0721d45 100644 --- 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 @@ -7,7 +7,6 @@ * LICENSE file that was distributed with this source code. */ -use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Schema\Builder; return [ diff --git a/framework/core/src/Frontend/Document.php b/framework/core/src/Frontend/Document.php index b1a1bc606f..ed242495af 100644 --- a/framework/core/src/Frontend/Document.php +++ b/framework/core/src/Frontend/Document.php @@ -13,7 +13,6 @@ use Flarum\Frontend\Compiler\FileVersioner; use Flarum\Frontend\Compiler\VersionerInterface; use Flarum\Frontend\Driver\TitleDriverInterface; -use Flarum\Http\RequestUtil; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Illuminate\Contracts\Support\Renderable; use Illuminate\Contracts\View\Factory; From 648342199607897eb2de1b5d1025fe071808dda1 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sat, 8 Jun 2024 10:38:24 +0100 Subject: [PATCH 4/9] chore: remove darkmode & colored header less variables --- extensions/subscriptions/less/forum.less | 22 ++++++++++++------- framework/core/less/admin/UsersListPage.less | 2 +- framework/core/less/common/variables.less | 2 -- .../src/Api/Serializer/ForumSerializer.php | 2 +- .../src/Frontend/FrontendServiceProvider.php | 12 ---------- 5 files changed, 16 insertions(+), 24 deletions(-) diff --git a/extensions/subscriptions/less/forum.less b/extensions/subscriptions/less/forum.less index c35f76e0a8..5f624abb26 100644 --- a/extensions/subscriptions/less/forum.less +++ b/extensions/subscriptions/less/forum.less @@ -1,9 +1,20 @@ +@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=dark] { + .Button--color-vars(#784d00, #fbb94c, 'button--follow'); +} + .Badge--following { --badge-bg: var(--following-bg); --badge-color: var(--following-color); @@ -12,12 +23,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/framework/core/less/admin/UsersListPage.less b/framework/core/less/admin/UsersListPage.less index 54ee110926..eb8fc81071 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/variables.less b/framework/core/less/common/variables.less index ee14496a8f..850a4d48ea 100644 --- a/framework/core/less/common/variables.less +++ b/framework/core/less/common/variables.less @@ -3,8 +3,6 @@ @config-primary-color: #536F90; @config-secondary-color: #536F90; -@config-dark-mode: false; -@config-colored-header: false; // --------------------------------- // COLORS diff --git a/framework/core/src/Api/Serializer/ForumSerializer.php b/framework/core/src/Api/Serializer/ForumSerializer.php index 911811b714..29f467e3a3 100644 --- a/framework/core/src/Api/Serializer/ForumSerializer.php +++ b/framework/core/src/Api/Serializer/ForumSerializer.php @@ -91,7 +91,7 @@ protected function getDefaultAttributes(object|array $model): array 'canModerateAccessTokens' => $this->actor->can('moderateAccessTokens'), 'canEditUserCredentials' => $this->actor->hasPermission('user.editCredentials'), 'assetsBaseUrl' => rtrim($this->assetsFilesystem->url(''), '/'), - 'jsChunksBaseUrl' => $this->assetsFilesystem->url('js') + 'jsChunksBaseUrl' => $this->assetsFilesystem->url('js'), ]; if ($this->actor->can('administrate')) { diff --git a/framework/core/src/Frontend/FrontendServiceProvider.php b/framework/core/src/Frontend/FrontendServiceProvider.php index 995514b7af..bc859d89e9 100644 --- a/framework/core/src/Frontend/FrontendServiceProvider.php +++ b/framework/core/src/Frontend/FrontendServiceProvider.php @@ -164,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'; - }, - ], ]; }); From aaaaec1a65b669725c09c7ee0c9da5c17fc114e9 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sat, 8 Jun 2024 10:42:06 +0100 Subject: [PATCH 5/9] chore --- extensions/flags/less/forum.less | 4 ++-- extensions/tags/less/forum/ToggleButton.less | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/extensions/flags/less/forum.less b/extensions/flags/less/forum.less index 4ba42eacfe..13eaa52353 100644 --- a/extensions/flags/less/forum.less +++ b/extensions/flags/less/forum.less @@ -1,8 +1,8 @@ -:root { +[data-theme=light] { .light-contents-vars(@color: @body-bg-light; @control-color: @body-bg-light; @name: 'flagged-post'); } -[data-theme=dark]:root { +[data-theme=dark] { .light-contents-vars(@color: @body-bg-dark; @control-color: @body-bg-dark; @name: 'flagged-post'); } diff --git a/extensions/tags/less/forum/ToggleButton.less b/extensions/tags/less/forum/ToggleButton.less index d0198a12fd..5d48b14bcf 100644 --- a/extensions/tags/less/forum/ToggleButton.less +++ b/extensions/tags/less/forum/ToggleButton.less @@ -1,8 +1,8 @@ -:root { +[data-theme=light] { .Button--color-vars(@control-bg-light, @control-color-light, 'button-toggled'); } -[data-theme=dark]:root { +[data-theme=dark] { .Button--color-vars(@control-bg-dark, @control-color-dark, 'button-toggled'); } From 26bc2ad9f717b237ea99aec3eb87e19d96bb0464 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sat, 8 Jun 2024 13:18:02 +0100 Subject: [PATCH 6/9] chore --- framework/core/less/common/root.less | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/framework/core/less/common/root.less b/framework/core/less/common/root.less index 7ada6c605a..338ebb12a5 100644 --- a/framework/core/less/common/root.less +++ b/framework/core/less/common/root.less @@ -68,14 +68,6 @@ --code-bg: @@code-bg; --code-color: @@code-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: @@control-bg; - --hero-color: @@control-color; - --discussion-title-color: mix(@@heading-color, @@body-bg, 55%); .Button--color-vars(@@control-color, @@control-bg, 'button'); @@ -152,6 +144,14 @@ --online-user-circle-color: @online-user-circle-color; --discussion-list-item-bg-hover: var(--control-body-bg-mix); + --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'); From 57d04ab7950d1de761cadc90504cd466f522aa93 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 14 Jun 2024 09:45:52 +0100 Subject: [PATCH 7/9] feat: high contrast schemes --- extensions/flags/less/forum.less | 4 +- extensions/subscriptions/less/forum.less | 10 +++- extensions/tags/less/forum/ToggleButton.less | 4 +- .../src/admin/components/AppearancePage.tsx | 4 +- framework/core/js/src/common/Application.tsx | 9 ++- .../js/src/common/components/ThemeMode.tsx | 41 +++++++++++-- .../core/js/src/common/extenders/ThemeMode.ts | 17 ++++++ .../core/js/src/common/extenders/index.ts | 2 + .../js/src/forum/components/SettingsPage.tsx | 4 +- framework/core/less/admin/UsersListPage.less | 2 +- framework/core/less/common/ThemeMode.less | 55 +++++++++++++++++ .../less/common/mixins/light-contents.less | 4 ++ framework/core/less/common/root.less | 59 ++++++++++++++++++- framework/core/less/common/variables.less | 43 +++++++------- framework/core/less/forum/PageStructure.less | 38 ++++++------ framework/core/locale/core.yml | 6 ++ 16 files changed, 243 insertions(+), 59 deletions(-) create mode 100644 framework/core/js/src/common/extenders/ThemeMode.ts diff --git a/extensions/flags/less/forum.less b/extensions/flags/less/forum.less index 13eaa52353..caf53d2eb6 100644 --- a/extensions/flags/less/forum.less +++ b/extensions/flags/less/forum.less @@ -1,8 +1,8 @@ -[data-theme=light] { +[data-theme^=light] { .light-contents-vars(@color: @body-bg-light; @control-color: @body-bg-light; @name: 'flagged-post'); } -[data-theme=dark] { +[data-theme^=dark] { .light-contents-vars(@color: @body-bg-dark; @control-color: @body-bg-dark; @name: 'flagged-post'); } diff --git a/extensions/subscriptions/less/forum.less b/extensions/subscriptions/less/forum.less index 5f624abb26..de55a0ead2 100644 --- a/extensions/subscriptions/less/forum.less +++ b/extensions/subscriptions/less/forum.less @@ -7,11 +7,17 @@ --ignoring-bg: #aaa; } -[data-theme=light] { +[data-theme^=light] { .Button--color-vars(@following-color, #fff2ae, 'button--follow'); } -[data-theme=dark] { +[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'); } diff --git a/extensions/tags/less/forum/ToggleButton.less b/extensions/tags/less/forum/ToggleButton.less index 5d48b14bcf..74db40c135 100644 --- a/extensions/tags/less/forum/ToggleButton.less +++ b/extensions/tags/less/forum/ToggleButton.less @@ -1,8 +1,8 @@ -[data-theme=light] { +[data-theme^=light] { .Button--color-vars(@control-bg-light, @control-color-light, 'button-toggled'); } -[data-theme=dark] { +[data-theme^=dark] { .Button--color-vars(@control-bg-dark, @control-color-dark, 'button-toggled'); } diff --git a/framework/core/js/src/admin/components/AppearancePage.tsx b/framework/core/js/src/admin/components/AppearancePage.tsx index fa9c09ab7d..d89d52ab20 100644 --- a/framework/core/js/src/admin/components/AppearancePage.tsx +++ b/framework/core/js/src/admin/components/AppearancePage.tsx @@ -104,10 +104,10 @@ export default class AppearancePage extends AdminPage {
- {['auto', 'light', 'dark'].map((mode: string) => ( + {ThemeMode.colorSchemes.map((mode) => ( { this.setting('color_scheme')(mode); diff --git a/framework/core/js/src/common/Application.tsx b/framework/core/js/src/common/Application.tsx index 97d0c9ecec..03058588d7 100644 --- a/framework/core/js/src/common/Application.tsx +++ b/framework/core/js/src/common/Application.tsx @@ -387,11 +387,18 @@ export default class Application { } getSystemColorSchemePreference(): string { - return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; + 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: string): void { diff --git a/framework/core/js/src/common/components/ThemeMode.tsx b/framework/core/js/src/common/components/ThemeMode.tsx index ba2188f874..d67942e8a1 100644 --- a/framework/core/js/src/common/components/ThemeMode.tsx +++ b/framework/core/js/src/common/components/ThemeMode.tsx @@ -9,7 +9,23 @@ export interface IThemeModeAttrs extends ComponentAttrs { alternate?: boolean; } +export enum ColorScheme { + Auto = 'auto', + Light = 'light', + Dark = 'dark', + LightHighContrast = 'light-hc', + DarkHighContrast = 'dark-hc', +} + export default class ThemeMode extends Component { + static colorSchemes: string[] = [ + ColorScheme.Auto, + ColorScheme.Light, + ColorScheme.Dark, + ColorScheme.LightHighContrast, + ColorScheme.DarkHighContrast, + ]; + view(vnode: Mithril.Vnode): Mithril.Children { const { mode, selected, className, alternate, label, ...attrs } = vnode.attrs; @@ -18,16 +34,29 @@ export default class ThemeMode -
+
-
-
+
+
+
+
+
+
+
+
+
-
+
+
+
{Array.from({ length: 3 }).map((_, i) => ( -
+
@@ -41,7 +70,7 @@ export default class ThemeMode
{Array.from({ length: 3 }).map((_, i) => ( -
+
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..e7863368b9 --- /dev/null +++ b/framework/core/js/src/common/extenders/ThemeMode.ts @@ -0,0 +1,17 @@ +import Application from '../Application'; +import IExtender, { IExtensionModule } from './IExtender'; +import ThemeModeComponent from '../components/ThemeMode'; + +export default class ThemeMode implements IExtender { + private readonly colorSchemes: string[] = []; + + public add(mode: string): this { + this.colorSchemes.push(mode); + + 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 c10508f11c..8f98ad3551 100644 --- a/framework/core/js/src/forum/components/SettingsPage.tsx +++ b/framework/core/js/src/forum/components/SettingsPage.tsx @@ -148,12 +148,12 @@ export default class SettingsPage(); - ['auto', 'light', 'dark'].forEach((mode) => { + ThemeMode.colorSchemes.forEach((mode) => { items.add( mode, { diff --git a/framework/core/less/admin/UsersListPage.less b/framework/core/less/admin/UsersListPage.less index eb8fc81071..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); - [data-theme=dark] & { + [data-theme^=dark] & { background: var(--body-bg-light); } } diff --git a/framework/core/less/common/ThemeMode.less b/framework/core/less/common/ThemeMode.less index cdecf14f2d..4fccc5f2a6 100644 --- a/framework/core/less/common/ThemeMode.less +++ b/framework/core/less/common/ThemeMode.less @@ -49,12 +49,48 @@ 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 { @@ -72,6 +108,15 @@ 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 { @@ -150,6 +195,16 @@ 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/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 338ebb12a5..2f90e2609f 100644 --- a/framework/core/less/common/root.less +++ b/framework/core/less/common/root.less @@ -80,7 +80,7 @@ // --------------------------------- // Light colors -[data-theme=light] { +[data-theme^=light] { .scheme(light); // --------------------------------- @@ -89,10 +89,30 @@ --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: darken(@muted-more-color-light, 20%); + + --discussion-title-color: darken(@discussion-title-color-light, 20%); + + .Button--color-vars(@control-color, @control-bg-light, 'button'); + .Button--color-vars(@control-color, @body-bg-light, 'button-inverted'); +} + // --------------------------------- // Dark colors -[data-theme=dark] { +[data-theme^=dark] { .scheme(dark); // --------------------------------- @@ -101,6 +121,41 @@ --yiq-threshold: 108; } +// 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'); + } +} + // --------------------------------- // Common light/dark colors diff --git a/framework/core/less/common/variables.less b/framework/core/less/common/variables.less index 850a4d48ea..04d1d45983 100644 --- a/framework/core/less/common/variables.less +++ b/framework/core/less/common/variables.less @@ -16,9 +16,7 @@ @secondary-hue: hue(@secondary-color); @secondary-sat: saturation(@secondary-color); -// 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. +// SCHEMES @body-bg-light: #fff; @body-bg-dark: hsl(@secondary-hue, min(20%, @secondary-sat), 10%); @@ -36,11 +34,6 @@ @control-color-light: @muted-color-light; @control-color-dark: @muted-color-dark; -@control-success-bg: #B4F1AF; -@control-success-color: #33722D; -@control-warning-bg: #fff2ae; -@control-warning-color: #ad6c00; - @control-danger-bg-light: #fdd; @control-danger-bg-dark: #411; @control-danger-color-light: #d66; @@ -67,18 +60,8 @@ @text-on-dark: @body-bg-light; @text-on-light: @body-bg-dark; -@error-color: #d83e3e; - -@alert-bg: #fff2ae; -@alert-color: #ad6c00; - -@alert-error-bg: @error-color; -@alert-error-color: #fff; - -@alert-success-bg: #B4F1AF; -@alert-success-color: #33722D; - -@validation-error-color: @error-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. @@ -103,6 +86,26 @@ @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; + +@alert-bg: #fff2ae; +@alert-color: #ad6c00; + +@alert-error-bg: @error-color; +@alert-error-color: #fff; + +@alert-success-bg: #B4F1AF; +@alert-success-color: #33722D; + +@validation-error-color: @error-color; + // --------------------------------- // LAYOUT 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 fd2aab5a60..9367cacd1c 100644 --- a/framework/core/locale/core.yml +++ b/framework/core/locale/core.yml @@ -82,7 +82,9 @@ core: 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. @@ -614,7 +616,9 @@ core: 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 @@ -971,6 +975,7 @@ 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 @@ -983,6 +988,7 @@ 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... From 5cf21d9bc662e2e8aa244ebe526b466afc367f06 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 14 Jun 2024 10:01:35 +0100 Subject: [PATCH 8/9] fixes --- .../js/src/admin/components/AppearancePage.tsx | 12 ++++++------ .../core/js/src/common/components/ThemeMode.tsx | 17 +++++++++++------ .../core/js/src/common/extenders/ThemeMode.ts | 11 +++++++---- .../js/src/forum/components/SettingsPage.tsx | 12 ++++++------ framework/core/less/common/root.less | 2 ++ 5 files changed, 32 insertions(+), 22 deletions(-) diff --git a/framework/core/js/src/admin/components/AppearancePage.tsx b/framework/core/js/src/admin/components/AppearancePage.tsx index d89d52ab20..91681c745b 100644 --- a/framework/core/js/src/admin/components/AppearancePage.tsx +++ b/framework/core/js/src/admin/components/AppearancePage.tsx @@ -106,16 +106,16 @@ export default class AppearancePage extends AdminPage {
{ThemeMode.colorSchemes.map((mode) => ( { - this.setting('color_scheme')(mode); + this.setting('color_scheme')(mode.id); - this.setting('allow_user_color_scheme')(mode === 'auto' ? '1' : '0'); + this.setting('allow_user_color_scheme')(mode.id === 'auto' ? '1' : '0'); - app.setColorScheme(mode); + app.setColorScheme(mode.id); }} - selected={this.setting('color_scheme')() === mode} + selected={this.setting('color_scheme')() === mode.id} /> ))}
diff --git a/framework/core/js/src/common/components/ThemeMode.tsx b/framework/core/js/src/common/components/ThemeMode.tsx index d67942e8a1..b226cad0f8 100644 --- a/framework/core/js/src/common/components/ThemeMode.tsx +++ b/framework/core/js/src/common/components/ThemeMode.tsx @@ -17,13 +17,18 @@ export enum ColorScheme { DarkHighContrast = 'dark-hc', } +export type ColorSchemeData = { + id: ColorScheme | string; + label?: string | null; +}; + export default class ThemeMode extends Component { - static colorSchemes: string[] = [ - ColorScheme.Auto, - ColorScheme.Light, - ColorScheme.Dark, - ColorScheme.LightHighContrast, - ColorScheme.DarkHighContrast, + static colorSchemes: ColorSchemeData[] = [ + { id: ColorScheme.Auto }, + { id: ColorScheme.Light }, + { id: ColorScheme.Dark }, + { id: ColorScheme.LightHighContrast }, + { id: ColorScheme.DarkHighContrast }, ]; view(vnode: Mithril.Vnode): Mithril.Children { diff --git a/framework/core/js/src/common/extenders/ThemeMode.ts b/framework/core/js/src/common/extenders/ThemeMode.ts index e7863368b9..64d760b7f0 100644 --- a/framework/core/js/src/common/extenders/ThemeMode.ts +++ b/framework/core/js/src/common/extenders/ThemeMode.ts @@ -1,12 +1,15 @@ import Application from '../Application'; import IExtender, { IExtensionModule } from './IExtender'; -import ThemeModeComponent from '../components/ThemeMode'; +import ThemeModeComponent, { type ColorSchemeData } from '../components/ThemeMode'; export default class ThemeMode implements IExtender { - private readonly colorSchemes: string[] = []; + private readonly colorSchemes: ColorSchemeData[] = []; - public add(mode: string): this { - this.colorSchemes.push(mode); + public add(mode: string, label: string): this { + this.colorSchemes.push({ + id: mode, + label, + }); return this; } diff --git a/framework/core/js/src/forum/components/SettingsPage.tsx b/framework/core/js/src/forum/components/SettingsPage.tsx index 8f98ad3551..bdf1069610 100644 --- a/framework/core/js/src/forum/components/SettingsPage.tsx +++ b/framework/core/js/src/forum/components/SettingsPage.tsx @@ -150,18 +150,18 @@ export default class SettingsPage { items.add( - mode, + mode.id, { this.colorSchemeLoading = true; - this.user!.savePreferences({ colorScheme: mode }).then(() => { + this.user!.savePreferences({ colorScheme: mode.id }).then(() => { this.colorSchemeLoading = false; - app.setColorScheme(mode); + app.setColorScheme(mode.id); m.redraw(); }); }} diff --git a/framework/core/less/common/root.less b/framework/core/less/common/root.less index 2f90e2609f..03b6ba5a8a 100644 --- a/framework/core/less/common/root.less +++ b/framework/core/less/common/root.less @@ -154,6 +154,8 @@ .light-contents-vars(@header-color-colored-dark, @header-control-bg-colored-dark, darken(@header-control-color-colored-dark, 15), 'header-colored'); } + + --yiq-threshold: 80; } // --------------------------------- From 70d5aa6f8d6c23ea26ab4e5d84513c9a63a3fbdc Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Fri, 21 Jun 2024 12:22:44 +0100 Subject: [PATCH 9/9] chore --- framework/core/js/src/common/Application.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/framework/core/js/src/common/Application.tsx b/framework/core/js/src/common/Application.tsx index 03058588d7..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'; @@ -386,7 +387,7 @@ export default class Application { } } - getSystemColorSchemePreference(): string { + getSystemColorSchemePreference(): ColorScheme | string { let colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; if (window.matchMedia('(prefers-contrast: more)').matches) { @@ -401,8 +402,8 @@ export default class Application { window.matchMedia('(prefers-contrast: more)').addEventListener('change', callback); } - setColorScheme(scheme: string): void { - if (scheme === 'auto') { + setColorScheme(scheme: ColorScheme | string): void { + if (scheme === ColorScheme.Auto) { scheme = this.getSystemColorSchemePreference(); }