Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: vanilla CSS color scheme changes #3996

Merged
merged 10 commits into from
Jun 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion extensions/flags/less/forum.less
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down
6 changes: 0 additions & 6 deletions extensions/package-manager/less/admin.less
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
28 changes: 20 additions & 8 deletions extensions/subscriptions/less/forum.less
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions extensions/tags/less/forum/ToggleButton.less
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
33 changes: 28 additions & 5 deletions framework/core/js/src/admin/components/AppearancePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 (
<div className="Form-group">
<label>{app.translator.trans('core.admin.appearance.color_scheme_label')}</label>
<div className="ThemeMode-list">
{ThemeMode.colorSchemes.map((mode) => (
<ThemeMode
mode={mode.id}
label={mode.label || app.translator.trans('core.admin.appearance.color_schemes.' + mode.id.replace('-', '_') + '_mode_label')}
onclick={() => {
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}
/>
))}
</div>
</div>
);
}),
60
);
Expand All @@ -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
);
Expand Down
55 changes: 55 additions & 0 deletions framework/core/js/src/common/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -245,6 +246,8 @@ export default class Application {

data!: ApplicationData;

allowUserColorScheme!: boolean;

private _title: string = '';
private _titleCount: number = 0;

Expand Down Expand Up @@ -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.
*/
Expand Down
96 changes: 96 additions & 0 deletions framework/core/js/src/common/components/ThemeMode.tsx
Original file line number Diff line number Diff line change
@@ -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<CustomAttrs extends IThemeModeAttrs = IThemeModeAttrs> extends Component<CustomAttrs> {
static colorSchemes: ColorSchemeData[] = [
{ id: ColorScheme.Auto },
{ id: ColorScheme.Light },
{ id: ColorScheme.Dark },
{ id: ColorScheme.LightHighContrast },
{ id: ColorScheme.DarkHighContrast },
];

view(vnode: Mithril.Vnode<CustomAttrs, this>): Mithril.Children {
const { mode, selected, className, alternate, label, ...attrs } = vnode.attrs;

return (
<label
className={classList('ThemeMode', className, `ThemeMode--${mode}`, { 'ThemeMode--active': selected, 'ThemeMode--switch': alternate })}
{...attrs}
>
<div
className="ThemeMode-container"
data-theme={mode === 'auto' ? 'light' : mode}
data-colored-header={document.documentElement.getAttribute('data-colored-header')}
>
<div className="ThemeMode-preview">
<div className="ThemeMode-header">
<div className="ThemeMode-header-text"></div>
<div className="ThemeMode-header-icon"></div>
<div className="ThemeMode-header-icon"></div>
</div>
<div className="ThemeMode-hero">
<div className="ThemeMode-hero-title"></div>
<div className="ThemeMode-hero-desc"></div>
</div>
<div className="ThemeMode-main">
<div className="ThemeMode-sidebar">
<div className="ThemeMode-startDiscussion">
<div className="ThemeMode-startDiscussion-text"></div>
</div>
<div className="ThemeMode-items">
{Array.from({ length: 3 }).map((_, i) => (
<div className="ThemeMode-sidebar-line" key={i}>
<div className="ThemeMode-sidebar-icon"></div>
<div className="ThemeMode-sidebar-text"></div>
</div>
))}
</div>
</div>
<div className="ThemeMode-content">
<div className="ThemeMode-toolbar">
<div className="ThemeMode-button"></div>
<div className="ThemeMode-button"></div>
</div>
<div className="ThemeMode-items">
{Array.from({ length: 3 }).map((_, i) => (
<div className="ThemeMode-content-item" key={i}>
<div className="ThemeMode-content-item-author"></div>
<div className="ThemeMode-content-item-meta">
<div className="ThemeMode-content-item-title"></div>
<div className="ThemeMode-content-item-excerpt"></div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
{mode === 'auto' ? <ThemeMode mode={mode === 'auto' ? 'dark' : null} alternate={true} selected={selected} {...attrs} /> : null}
</div>
{!alternate ? <div className="ThemeMode-legend">{label}</div> : null}
</label>
);
}
}
20 changes: 20 additions & 0 deletions framework/core/js/src/common/extenders/ThemeMode.ts
Original file line number Diff line number Diff line change
@@ -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]));
}
}
2 changes: 2 additions & 0 deletions framework/core/js/src/common/extenders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -12,6 +13,7 @@ const extenders = {
Store,
Search,
Notification,
ThemeMode,
};

export default extenders;
Loading
Loading