mirror of
https://github.com/flarum/framework.git
synced 2025-02-21 04:31:57 +08:00
feat: vanilla CSS color scheme changes (#3996)
* feat: vanilla CSS color scheme changes * chore: scheme mixin * chore: remove darkmode & colored header less variables * feat: high contrast schemes
This commit is contained in:
parent
379298acb0
commit
b91caec30b
@ -1,3 +1,11 @@
|
||||
[data-theme^=light] {
|
||||
.light-contents-vars(@color: @body-bg-light; @control-color: @body-bg-light; @name: 'flagged-post');
|
||||
}
|
||||
|
||||
[data-theme^=dark] {
|
||||
.light-contents-vars(@color: @body-bg-dark; @control-color: @body-bg-dark; @name: 'flagged-post');
|
||||
}
|
||||
|
||||
.Post--flagged {
|
||||
--border-width: 2px;
|
||||
padding-top: 0 !important;
|
||||
@ -16,7 +24,7 @@
|
||||
padding: 10px;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
overflow: hidden;
|
||||
.light-contents(@color: @body-bg; @control-color: @body-bg);
|
||||
.light-contents(@name: 'flagged-post');
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -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;
|
||||
|
@ -1,9 +1,26 @@
|
||||
@following-bg: #ffea7b;
|
||||
@following-color: #de8e00;
|
||||
|
||||
:root {
|
||||
--following-bg: #ffea7b;
|
||||
--following-color: #de8e00;
|
||||
--following-bg: @following-bg;
|
||||
--following-color: @following-color;
|
||||
--ignoring-bg: #aaa;
|
||||
}
|
||||
|
||||
[data-theme^=light] {
|
||||
.Button--color-vars(@following-color, #fff2ae, 'button--follow');
|
||||
}
|
||||
|
||||
[data-theme=light-hc], [data-theme=dark-hc] {
|
||||
@following-color-hc: darken(@following-color, 23%);
|
||||
--following-color: @following-color-hc;
|
||||
.Button--color-vars(@following-color-hc, #fff2ae, 'button--follow');
|
||||
}
|
||||
|
||||
[data-theme^=dark] {
|
||||
.Button--color-vars(#784d00, #fbb94c, 'button--follow');
|
||||
}
|
||||
|
||||
.Badge--following {
|
||||
--badge-bg: var(--following-bg);
|
||||
--badge-color: var(--following-color);
|
||||
@ -12,12 +29,7 @@
|
||||
--badge-bg: var(--ignoring-bg);
|
||||
}
|
||||
.SubscriptionMenu-button--follow {
|
||||
& when (@config-dark-mode = false) {
|
||||
.Button--color(#de8e00, #fff2ae);
|
||||
}
|
||||
& when (@config-dark-mode = true) {
|
||||
.Button--color(#784d00, #fbb94c);
|
||||
}
|
||||
.Button--color-auto('button--follow');
|
||||
}
|
||||
.SubscriptionMenu .Dropdown-menu {
|
||||
min-width: 260px;
|
||||
|
@ -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 {
|
||||
|
@ -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 (
|
||||
<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
|
||||
);
|
||||
@ -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
|
||||
);
|
||||
|
@ -38,6 +38,7 @@ import IHistory from './IHistory';
|
||||
import IExtender from './extenders/IExtender';
|
||||
import AccessToken from './models/AccessToken';
|
||||
import SearchManager from './SearchManager';
|
||||
import { ColorScheme } from './components/ThemeMode';
|
||||
|
||||
export type FlarumScreens = 'phone' | 'tablet' | 'desktop' | 'desktop-hd';
|
||||
|
||||
@ -245,6 +246,8 @@ export default class Application {
|
||||
|
||||
data!: ApplicationData;
|
||||
|
||||
allowUserColorScheme!: boolean;
|
||||
|
||||
private _title: string = '';
|
||||
private _titleCount: number = 0;
|
||||
|
||||
@ -356,9 +359,61 @@ export default class Application {
|
||||
|
||||
document.body.classList.add('ontouchstart' in window ? 'touch' : 'no-touch');
|
||||
|
||||
this.initColorScheme();
|
||||
|
||||
liveHumanTimes();
|
||||
}
|
||||
|
||||
private initColorScheme(forumDefault: string | null = null): void {
|
||||
forumDefault ??= document.documentElement.getAttribute('data-theme') ?? 'auto';
|
||||
this.allowUserColorScheme = forumDefault === 'auto';
|
||||
const userConfiguredPreference = this.session.user?.preferences()?.colorScheme;
|
||||
|
||||
let scheme;
|
||||
|
||||
if (this.allowUserColorScheme) {
|
||||
scheme = userConfiguredPreference;
|
||||
}
|
||||
|
||||
scheme ||= forumDefault;
|
||||
|
||||
this.setColorScheme(scheme);
|
||||
|
||||
// Listen for browser color scheme changes and update the theme accordingly
|
||||
if (this.allowUserColorScheme) {
|
||||
this.watchSystemColorSchemePreference(() => {
|
||||
this.initColorScheme(forumDefault);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getSystemColorSchemePreference(): ColorScheme | string {
|
||||
let colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
|
||||
if (window.matchMedia('(prefers-contrast: more)').matches) {
|
||||
colorScheme += '-hc';
|
||||
}
|
||||
|
||||
return colorScheme;
|
||||
}
|
||||
|
||||
watchSystemColorSchemePreference(callback: () => void): void {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', callback);
|
||||
window.matchMedia('(prefers-contrast: more)').addEventListener('change', callback);
|
||||
}
|
||||
|
||||
setColorScheme(scheme: ColorScheme | string): void {
|
||||
if (scheme === ColorScheme.Auto) {
|
||||
scheme = this.getSystemColorSchemePreference();
|
||||
}
|
||||
|
||||
document.documentElement.setAttribute('data-theme', scheme);
|
||||
}
|
||||
|
||||
setColoredHeader(value: boolean): void {
|
||||
document.documentElement.setAttribute('data-colored-header', value ? 'true' : 'false');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API response document that has been preloaded into the application.
|
||||
*/
|
||||
|
96
framework/core/js/src/common/components/ThemeMode.tsx
Normal file
96
framework/core/js/src/common/components/ThemeMode.tsx
Normal file
@ -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
framework/core/js/src/common/extenders/ThemeMode.ts
Normal file
20
framework/core/js/src/common/extenders/ThemeMode.ts
Normal file
@ -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]));
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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<CustomAttrs extends IUserPageAttrs = IUserPageAttrs> extends UserPage<CustomAttrs> {
|
||||
discloseOnlineLoading?: boolean;
|
||||
colorSchemeLoading?: boolean;
|
||||
|
||||
oninit(vnode: Mithril.Vnode<CustomAttrs, this>) {
|
||||
super.oninit(vnode);
|
||||
@ -35,20 +39,35 @@ export default class SettingsPage<CustomAttrs extends IUserPageAttrs = IUserPage
|
||||
);
|
||||
}
|
||||
|
||||
sectionProps(): Record<string, ComponentAttrs> {
|
||||
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<Mithril.Children>();
|
||||
|
||||
['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,
|
||||
<FieldSet
|
||||
className={classList(`Settings-${section}`, { 'FieldSet--col': section === 'account' })}
|
||||
label={app.translator.trans(`core.forum.settings.${section}_heading`)}
|
||||
className={classList(`Settings-${section} FieldSet--min`, className || '')}
|
||||
label={app.translator.trans(`core.forum.settings.${camelCaseToSnakeCase(section)}_heading`)}
|
||||
{...props}
|
||||
>
|
||||
{this[sectionItems]().toArray()}
|
||||
</FieldSet>,
|
||||
@ -122,4 +141,35 @@ export default class SettingsPage<CustomAttrs extends IUserPageAttrs = IUserPage
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Color schemes.
|
||||
*/
|
||||
colorSchemeItems() {
|
||||
const items = new ItemList<Mithril.Children>();
|
||||
|
||||
ThemeMode.colorSchemes.forEach((mode) => {
|
||||
items.add(
|
||||
mode.id,
|
||||
<ThemeMode
|
||||
mode={mode.id}
|
||||
label={mode.label || app.translator.trans('core.forum.settings.color_schemes.' + mode.id.replace('-', '_') + '_mode_label')}
|
||||
selected={this.user!.preferences()?.colorScheme === mode.id}
|
||||
loading={this.colorSchemeLoading}
|
||||
onclick={() => {
|
||||
this.colorSchemeLoading = true;
|
||||
|
||||
this.user!.savePreferences({ colorScheme: mode.id }).then(() => {
|
||||
this.colorSchemeLoading = false;
|
||||
app.setColorScheme(mode.id);
|
||||
m.redraw();
|
||||
});
|
||||
}}
|
||||
/>,
|
||||
100
|
||||
);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
@ -82,7 +82,7 @@
|
||||
&--shaded {
|
||||
background: var(--body-bg-shaded);
|
||||
|
||||
& when (@config-dark-mode = true) {
|
||||
[data-theme^=dark] & {
|
||||
background: var(--body-bg-light);
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +42,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.Checkbox--switch.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.Checkbox--switch .Checkbox-display {
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
|
@ -66,6 +66,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.FieldSet--min .FieldSet-items > * {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.FieldSet--col .FieldSet-items, .Form-controls {
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
|
211
framework/core/less/common/ThemeMode.less
Normal file
211
framework/core/less/common/ThemeMode.less
Normal file
@ -0,0 +1,211 @@
|
||||
.ThemeMode {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
max-width: 150px;
|
||||
|
||||
&-container {
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px solid var(--control-bg);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&:hover &-container {
|
||||
border-color: var(--control-color);
|
||||
}
|
||||
|
||||
&--active &-container, &--active:hover &-container {
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
&--switch {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
clip-path: polygon(0% 0%, 100% 0%, 0% 100%);
|
||||
}
|
||||
|
||||
&--switch &-container {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ThemeMode-preview {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--body-bg);
|
||||
}
|
||||
|
||||
.ThemeMode-list {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ThemeMode-header {
|
||||
height: 25px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--header-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-inline-end: 15px;
|
||||
gap: 8px;
|
||||
|
||||
&-text {
|
||||
height: 2px;
|
||||
width: 15px;
|
||||
background: var(--header-colored-color);
|
||||
}
|
||||
|
||||
&-icon {
|
||||
height: 5px;
|
||||
width: 5px;
|
||||
background: var(--header-colored-color);
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ThemeMode-hero {
|
||||
background-color: var(--control-bg);
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.ThemeMode-hero-title {
|
||||
width: 25%;
|
||||
height: 2px;
|
||||
background-color: var(--control-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.ThemeMode-hero-desc {
|
||||
width: 50%;
|
||||
height: 1px;
|
||||
background-color: var(--control-color);
|
||||
}
|
||||
|
||||
.ThemeMode-main {
|
||||
padding: 14px 0;
|
||||
width: 75%;
|
||||
flex-grow: 1;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
grid-template-columns: 30px 1fr;
|
||||
}
|
||||
|
||||
.ThemeMode-startDiscussion {
|
||||
border-radius: calc(var(--border-radius) - 2px);
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background-color: var(--primary-color);
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&-text {
|
||||
height: 2px;
|
||||
width: 40%;
|
||||
background-color: var(--button-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ThemeMode-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.ThemeMode-sidebar-line {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ThemeMode-sidebar-text {
|
||||
height: 1px;
|
||||
background-color: var(--control-color);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.ThemeMode-sidebar-icon {
|
||||
content: "";
|
||||
display: block;
|
||||
border-radius: 100%;
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
background-color: var(--control-color);
|
||||
}
|
||||
|
||||
.ThemeMode-button {
|
||||
height: 10px;
|
||||
width: 20px;
|
||||
background-color: var(--control-bg);
|
||||
}
|
||||
|
||||
.ThemeMode-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.ThemeMode-content {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.ThemeMode-content-item {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ThemeMode-toolbar :last-child {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.ThemeMode-content-item-author {
|
||||
background-color: var(--control-bg);
|
||||
height: 9px;
|
||||
width: 9px;
|
||||
border-radius: 100%;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
|
||||
.ThemeMode-content-item-title {
|
||||
width: 25px;
|
||||
height: 1px;
|
||||
background-color: var(--discussion-title-color);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.ThemeMode-content-item-excerpt {
|
||||
height: 1px;
|
||||
width: 45px;
|
||||
background-color: var(--muted-more-color);
|
||||
}
|
||||
|
||||
.ThemeMode-container[data-theme=light], .ThemeMode-container[data-theme=dark] {
|
||||
.ThemeMode-hero-title,
|
||||
.ThemeMode-hero-desc,
|
||||
.ThemeMode-sidebar-line,
|
||||
.ThemeMode-content-item-title,
|
||||
.ThemeMode-content-item-excerpt {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.ThemeMode-legend {
|
||||
padding: 8px 0;
|
||||
color: var(--control-color);
|
||||
}
|
@ -30,5 +30,6 @@
|
||||
@import "Select";
|
||||
@import "Table";
|
||||
@import "TextEditor";
|
||||
@import "ThemeMode";
|
||||
@import "Tooltip";
|
||||
@import "ValidationError";
|
||||
|
@ -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') {
|
||||
|
@ -1,65 +1,182 @@
|
||||
:root {
|
||||
.scheme(@mode) {
|
||||
@body-bg: ~"body-bg-@{mode}";
|
||||
@text-color: ~"text-color-@{mode}";
|
||||
@heading-color: ~"heading-color-@{mode}";
|
||||
@muted-color: ~"muted-color-@{mode}";
|
||||
@muted-more-color: ~"muted-more-color-@{mode}";
|
||||
@shadow-color: ~"shadow-color-@{mode}";
|
||||
@control-bg: ~"control-bg-@{mode}";
|
||||
@control-color: ~"control-color-@{mode}";
|
||||
@control-danger-bg: ~"control-danger-bg-@{mode}";
|
||||
@control-danger-color: ~"control-danger-color-@{mode}";
|
||||
@header-bg: ~"header-bg-@{mode}";
|
||||
@header-color: ~"header-color-@{mode}";
|
||||
@header-control-bg: ~"header-control-bg-@{mode}";
|
||||
@header-control-color: ~"header-control-color-@{mode}";
|
||||
@header-bg-colored: ~"header-bg-colored-@{mode}";
|
||||
@header-color-colored: ~"header-color-colored-@{mode}";
|
||||
@header-control-bg-colored: ~"header-control-bg-colored-@{mode}";
|
||||
@header-control-color-colored: ~"header-control-color-colored-@{mode}";
|
||||
@overlay-bg: ~"overlay-bg-@{mode}";
|
||||
@code-bg: ~"code-bg-@{mode}";
|
||||
@code-color: ~"code-color-@{mode}";
|
||||
|
||||
// ---------------------------------
|
||||
// COLORS
|
||||
|
||||
--primary-color: @primary-color;
|
||||
--secondary-color: @secondary-color;
|
||||
--body-bg: @@body-bg;
|
||||
--body-bg-shaded: darken(@@body-bg, 3%);
|
||||
--body-bg-light: lighten(@@body-bg, 5%);
|
||||
--body-bg-faded: fade(@@body-bg, 93%);
|
||||
--text-color: @@text-color;
|
||||
--heading-color: @@heading-color;
|
||||
|
||||
--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;
|
||||
--muted-color: @@muted-color;
|
||||
--muted-color-light: lighten(@@muted-color, 10%);
|
||||
--muted-color-dark: darken(@@muted-color, 50%);
|
||||
--muted-more-color: @@muted-more-color;
|
||||
|
||||
--muted-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;
|
||||
|
||||
--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-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%);
|
||||
|
||||
--error-color: @error-color;
|
||||
|
||||
// ---------------------------------
|
||||
// UTILITIES
|
||||
|
||||
--text-on-dark: @text-on-dark;
|
||||
--text-on-light: @text-on-light;
|
||||
|
||||
& when (@config-dark-mode = true) {
|
||||
--yiq-threshold: 108;
|
||||
}
|
||||
& when (@config-dark-mode = false) {
|
||||
--yiq-threshold: 150;
|
||||
}
|
||||
--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;
|
||||
--header-color: @header-color;
|
||||
--header-control-bg: @header-control-bg;
|
||||
--header-control-color: @header-control-color;
|
||||
--header-bg: @@header-bg;
|
||||
--header-color: @@header-color;
|
||||
--header-control-bg: @@header-control-bg;
|
||||
--header-control-color: @@header-control-color;
|
||||
|
||||
--overlay-bg: @overlay-bg;
|
||||
--code-bg: @code-bg;
|
||||
--code-color: @code-color;
|
||||
[data-colored-header=true]& {
|
||||
--header-bg: @@header-bg-colored;
|
||||
--header-color: @@header-color-colored;
|
||||
--header-control-bg: @@header-control-bg-colored;
|
||||
--header-control-color: @@header-control-color-colored;
|
||||
|
||||
.light-contents-vars(@@header-color-colored, @@header-control-bg-colored, @@header-control-color-colored, 'header-colored');
|
||||
}
|
||||
|
||||
--overlay-bg: @@overlay-bg;
|
||||
--code-bg: @@code-bg;
|
||||
--code-color: @@code-color;
|
||||
|
||||
--discussion-title-color: mix(@@heading-color, @@body-bg, 55%);
|
||||
|
||||
.Button--color-vars(@@control-color, @@control-bg, 'button');
|
||||
.Button--color-vars(@@body-bg, @primary-color, 'button-primary');
|
||||
.Button--color-vars(@@control-danger-color, @@control-danger-bg, 'control-danger');
|
||||
.Button--color-vars(@@muted-more-color, fade(@@muted-more-color, 30%), 'muted-more');
|
||||
.Button--color-vars(@@control-color, @@body-bg, 'button-inverted');
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// Light colors
|
||||
|
||||
[data-theme^=light] {
|
||||
.scheme(light);
|
||||
|
||||
// ---------------------------------
|
||||
// UTILITIES
|
||||
|
||||
--yiq-threshold: 150;
|
||||
}
|
||||
|
||||
// High contrast
|
||||
|
||||
[data-theme=light-hc] {
|
||||
@control-color: darken(@control-color-light, 20%);
|
||||
@muted-color: darken(@muted-color-light, 20%);
|
||||
|
||||
--control-color: @control-color;
|
||||
--control-muted-color: lighten(@control-color, 20%);
|
||||
|
||||
--muted-color: @muted-color;
|
||||
--muted-color-light: lighten(@muted-color, 10%);
|
||||
--muted-color-dark: darken(@muted-color, 50%);
|
||||
--muted-more-color: 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] {
|
||||
.scheme(dark);
|
||||
|
||||
// ---------------------------------
|
||||
// UTILITIES
|
||||
|
||||
--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');
|
||||
}
|
||||
|
||||
--yiq-threshold: 80;
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// 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;
|
||||
|
||||
--text-on-dark: @text-on-dark;
|
||||
--text-on-light: @text-on-light;
|
||||
|
||||
--alert-bg: @alert-bg;
|
||||
--alert-color: @alert-color;
|
||||
@ -76,34 +193,30 @@
|
||||
|
||||
--validation-error-color: @validation-error-color;
|
||||
|
||||
--avatar-bg: var(--control-bg);
|
||||
--badge-bg: var(--muted-color);
|
||||
--badge-color: #fff;
|
||||
--badge-hidden-bg: #888;
|
||||
--usercard-bg: var(--control-bg);
|
||||
--hero-bg: @hero-bg;
|
||||
--hero-color: @hero-color;
|
||||
|
||||
--tooltip-bg: @tooltip-bg;
|
||||
--tooltip-color: @tooltip-color;
|
||||
|
||||
--loading-indicator-color: var(--muted-color);
|
||||
|
||||
--online-user-circle-color: @online-user-circle-color;
|
||||
|
||||
--discussion-title-color: mix(@heading-color, @body-bg, 55%);
|
||||
--discussion-list-item-bg-hover: var(--control-body-bg-mix);
|
||||
|
||||
.Button--color-vars(@control-color, @control-bg, 'button');
|
||||
.Button--color-vars(@body-bg, @primary-color, 'button-primary');
|
||||
.Button--color-vars(@control-danger-color, @control-danger-bg, 'control-danger');
|
||||
.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');
|
||||
--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();
|
||||
.light-contents-vars(@header-color, @header-control-bg, @header-control-color, 'header-colored');
|
||||
|
||||
.Button--color-vars(@control-success-color, @control-success-bg, 'control-success');
|
||||
.Button--color-vars(@control-warning-color, @control-warning-bg, 'control-warning');
|
||||
|
||||
}
|
||||
|
||||
:root {
|
||||
|
||||
// ---------------------------------
|
||||
// LAYOUT
|
||||
|
@ -3,88 +3,95 @@
|
||||
|
||||
@config-primary-color: #536F90;
|
||||
@config-secondary-color: #536F90;
|
||||
@config-dark-mode: false;
|
||||
@config-colored-header: false;
|
||||
|
||||
// ---------------------------------
|
||||
// COLORS
|
||||
|
||||
@primary-color: @config-primary-color;
|
||||
@secondary-color: @config-secondary-color;
|
||||
|
||||
@primary-hue: hue(@primary-color);
|
||||
@primary-sat: saturation(@primary-color);
|
||||
|
||||
@secondary-hue: hue(@secondary-color);
|
||||
@secondary-sat: saturation(@secondary-color);
|
||||
|
||||
// SCHEMES
|
||||
|
||||
@body-bg-light: #fff;
|
||||
@body-bg-dark: hsl(@secondary-hue, min(20%, @secondary-sat), 10%);
|
||||
|
||||
// Derive the primary/secondary colors from the config variables. In dark mode,
|
||||
// we make the user-set colors a bit darker, otherwise they will stand out too
|
||||
// much.
|
||||
.define-colors(@config-dark-mode);
|
||||
.define-colors(false) {
|
||||
@primary-color: @config-primary-color;
|
||||
@secondary-color: @config-secondary-color;
|
||||
@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%);
|
||||
|
||||
@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);
|
||||
@link-color: saturate(@primary-color, 10%);
|
||||
|
||||
@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;
|
||||
@control-bg-light: hsl(@secondary-hue, min(50%, @secondary-sat), 93%);
|
||||
@control-bg-dark: hsl(@secondary-hue, min(20%, @secondary-sat), 13%);
|
||||
|
||||
@overlay-bg: fade(@secondary-color, 90%);
|
||||
@control-color-light: @muted-color-light;
|
||||
@control-color-dark: @muted-color-dark;
|
||||
|
||||
@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;
|
||||
@control-danger-bg-light: #fdd;
|
||||
@control-danger-bg-dark: #411;
|
||||
@control-danger-color-light: #d66;
|
||||
@control-danger-color-dark: #a88;
|
||||
|
||||
@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);
|
||||
@text-color-light: #111;
|
||||
@text-color-dark: #ddd;
|
||||
|
||||
@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;
|
||||
@heading-color-light: @text-color-light;
|
||||
@heading-color-dark: @text-color-dark;
|
||||
|
||||
@overlay-bg: fade(darken(@body-bg, 5%), 90%);
|
||||
@shadow-color-light: rgba(0, 0, 0, 0.35);
|
||||
@shadow-color-dark: rgba(0, 0, 0, 0.5);
|
||||
|
||||
@code-bg: darken(@body-bg, 3%);
|
||||
@code-color: #fff;
|
||||
}
|
||||
@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;
|
||||
|
||||
// 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.
|
||||
@text-on-dark: @body-bg-light;
|
||||
@text-on-light: @body-bg-dark;
|
||||
|
||||
@hero-bg: @control-bg;
|
||||
@hero-color: @control-color;
|
||||
@hero-muted-color: @control-color;
|
||||
@discussion-title-color-light: mix(@heading-color-light, @body-bg-light, 55%);
|
||||
@discussion-title-color-dark: mix(@heading-color-dark, @body-bg-dark, 55%);
|
||||
|
||||
// Header colors depend on whether the header is colored,
|
||||
// and whether we are on light or dark modes.
|
||||
|
||||
@header-bg-light: @body-bg-light;
|
||||
@header-color-light: @primary-color;
|
||||
@header-control-bg-light: @control-bg-light;
|
||||
@header-control-color-light: @control-color-light;
|
||||
|
||||
@header-bg-dark: @body-bg-dark;
|
||||
@header-color-dark: @primary-color;
|
||||
@header-control-bg-dark: @control-bg-dark;
|
||||
@header-control-color-dark: @control-color-dark;
|
||||
|
||||
@header-bg-colored-light: @primary-color;
|
||||
@header-color-colored-light: @body-bg-light;
|
||||
@header-control-bg-colored-light: mix(#000, @header-bg-colored-light, 10%);
|
||||
@header-control-color-colored-light: mix(@body-bg-light, @header-bg-colored-light, 60%);
|
||||
|
||||
@header-bg-colored-dark: @primary-color;
|
||||
@header-color-colored-dark: @body-bg-dark;
|
||||
@header-control-bg-colored-dark: mix(#000, @header-bg-colored-dark, 10%);
|
||||
@header-control-color-colored-dark: mix(@body-bg-dark, @header-bg-colored-dark, 60%);
|
||||
|
||||
// COMMON COLORS
|
||||
|
||||
@control-success-bg: #B4F1AF;
|
||||
@control-success-color: #33722D;
|
||||
@control-warning-bg: #fff2ae;
|
||||
@control-warning-color: #ad6c00;
|
||||
|
||||
@error-color: #d83e3e;
|
||||
|
||||
@ -99,20 +106,6 @@
|
||||
|
||||
@validation-error-color: @error-color;
|
||||
|
||||
.define-header(@config-colored-header);
|
||||
.define-header(false) {
|
||||
@header-bg: @body-bg;
|
||||
@header-color: @primary-color;
|
||||
@header-control-bg: @control-bg;
|
||||
@header-control-color: @control-color;
|
||||
}
|
||||
.define-header(true) {
|
||||
@header-bg: @primary-color;
|
||||
@header-color: @body-bg;
|
||||
@header-control-bg: mix(#000, @header-bg, 10%);
|
||||
@header-control-color: mix(@body-bg, @header-bg, 60%);
|
||||
}
|
||||
|
||||
// ---------------------------------
|
||||
// LAYOUT
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -58,6 +58,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
|
||||
@ -70,7 +71,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
|
||||
@ -81,6 +81,13 @@ core:
|
||||
logo_heading: Logo
|
||||
logo_text: Upload an image to be displayed in place of the forum title.
|
||||
title: Appearance
|
||||
color_scheme_label: Color Scheme (default)
|
||||
color_schemes:
|
||||
auto_mode_label: User system or configured preference
|
||||
dark_hc_mode_label: => core.ref.dark_hc_mode_label
|
||||
dark_mode_label: => core.ref.dark_mode_label
|
||||
light_hc_mode_label: => core.ref.light_hc_mode_label
|
||||
light_mode_label: => core.ref.light_mode_label
|
||||
|
||||
# These translations are used in the Basics page.
|
||||
basics:
|
||||
@ -609,6 +616,13 @@ core:
|
||||
account_heading: Account
|
||||
change_email_button: => core.ref.change_email
|
||||
change_password_button: => core.ref.change_password
|
||||
color_scheme_heading: Color Scheme
|
||||
color_schemes:
|
||||
auto_mode_label: System preference
|
||||
dark_hc_mode_label: => core.ref.dark_hc_mode_label
|
||||
dark_mode_label: => core.ref.dark_mode_label
|
||||
light_hc_mode_label: => core.ref.light_hc_mode_label
|
||||
light_mode_label: => core.ref.light_mode_label
|
||||
notification_checkbox_a11y_label_template: 'Receive "{description}" notifications via {method}'
|
||||
notifications_heading: => core.ref.notifications
|
||||
notify_by_email_heading: => core.ref.email
|
||||
@ -965,6 +979,8 @@ core:
|
||||
custom_footer_title: Edit Custom Footer
|
||||
custom_header_text: "Add HTML to be displayed at the very top of the page, above Flarum's own header."
|
||||
custom_header_title: Edit Custom Header
|
||||
dark_hc_mode_label: Dark High Contrast Mode
|
||||
dark_mode_label: Dark Mode
|
||||
delete: Delete
|
||||
delete_forever: Delete Forever
|
||||
discussions: Discussions # Referenced by flarum-statistics.yml
|
||||
@ -976,6 +992,8 @@ core:
|
||||
icon: Icon
|
||||
icon_text: "Enter the name of any <a>FontAwesome</a> icon class, <em>including</em> the <code>fas fa-</code> prefix."
|
||||
invalid_login_message: Your login details were incorrect.
|
||||
light_hc_mode_label: Light High Contrast Mode
|
||||
light_mode_label: Light Mode
|
||||
load_more: Load More
|
||||
loading: Loading...
|
||||
log_in: Log In
|
||||
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => 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();
|
||||
}
|
||||
];
|
@ -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 @@ class Frontend implements ExtenderInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds document root attributes.
|
||||
*
|
||||
* @example ['data-test' => 'value']
|
||||
* @example ['data-test' => function (ServerRequestInterface $request) { return 'value'; }]
|
||||
*
|
||||
* @param array<string, string|callable> $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 @@ class Frontend implements ExtenderInterface
|
||||
$this->registerContent($container);
|
||||
$this->registerPreloads($container);
|
||||
$this->registerTitleDriver($container);
|
||||
$this->registerDocumentAttributes($container);
|
||||
}
|
||||
|
||||
private function registerAssets(Container $container, string $moduleName): void
|
||||
@ -330,4 +361,23 @@ class Frontend implements ExtenderInterface
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +135,15 @@ class Document implements Renderable
|
||||
*/
|
||||
public array $preloads = [];
|
||||
|
||||
/**
|
||||
* Document extra attributes.
|
||||
*
|
||||
* @var array<string, string|callable|array>
|
||||
*/
|
||||
public array $extraAttributes = [
|
||||
'class' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* We need the versioner to get the revisions of split chunks.
|
||||
*/
|
||||
@ -171,6 +180,8 @@ class Document implements Renderable
|
||||
'js' => $this->makeJs(),
|
||||
'head' => $this->makeHead(),
|
||||
'foot' => $this->makeFoot(),
|
||||
'extraAttributes' => $this->makeExtraAttributes(),
|
||||
'extraClasses' => $this->makeExtraClasses(),
|
||||
'revisions' => $this->versioner->allRevisions(),
|
||||
'debug' => $this->config->inDebugMode(),
|
||||
]);
|
||||
@ -208,6 +219,50 @@ class Document implements Renderable
|
||||
}, $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) {
|
||||
|
@ -17,6 +17,7 @@ use Flarum\Foundation\Paths;
|
||||
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 Flarum\Settings\SettingsRepositoryInterface;
|
||||
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 @@ class FrontendServiceProvider extends AbstractServiceProvider
|
||||
$default_preloads,
|
||||
$document->preloads,
|
||||
);
|
||||
|
||||
/** @var SettingsRepositoryInterface $settings */
|
||||
$settings = $container->make(SettingsRepositoryInterface::class);
|
||||
|
||||
// Add document classes/attributes for design use cases.
|
||||
$document->extraAttributes['data-theme'] = $settings->get('color_scheme');
|
||||
$document->extraAttributes['data-colored-header'] = $settings->get('theme_colored_header') ? 'true' : 'false';
|
||||
$document->extraAttributes['class'][] = function (ServerRequestInterface $request) {
|
||||
return RequestUtil::getActor($request)->isGuest() ? 'guest-user' : 'logged-in';
|
||||
};
|
||||
}, 160);
|
||||
|
||||
return $frontend;
|
||||
@ -152,18 +164,6 @@ class FrontendServiceProvider extends AbstractServiceProvider
|
||||
'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';
|
||||
},
|
||||
],
|
||||
];
|
||||
});
|
||||
|
||||
|
@ -62,7 +62,7 @@ class WriteSettings implements Step
|
||||
'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!',
|
||||
|
@ -124,6 +124,7 @@ class UserServiceProvider extends AbstractServiceProvider
|
||||
User::registerPreference('discloseOnline', 'boolval', true);
|
||||
User::registerPreference('indexProfile', 'boolval', true);
|
||||
User::registerPreference('locale');
|
||||
User::registerPreference('colorScheme', 'strval', 'auto');
|
||||
|
||||
User::registerVisibilityScoper(new ScopeUserVisibility(), 'view');
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
<!doctype html>
|
||||
<html @if ($direction) dir="{{ $direction }}" @endif
|
||||
@if ($language) lang="{{ $language }}" @endif>
|
||||
<html @if ($direction) dir="{{ $direction }}" @endif @if ($language) lang="{{ $language }}" @endif @class($extraClasses) {!! $extraAttributes !!}>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{ $title }}</title>
|
||||
|
Loading…
x
Reference in New Issue
Block a user