You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@answer.apache.org by sh...@apache.org on 2024/01/16 02:38:47 UTC
(incubator-answer) 01/01: feat: add dark mode
This is an automated email from the ASF dual-hosted git repository.
shuai pushed a commit to branch feat/1.2.5/ui
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git
commit 8e1e55e089522f2e1e5bc811601b8e92c88711a7
Author: shuai <li...@sifou.com>
AuthorDate: Tue Jan 16 10:38:20 2024 +0800
feat: add dark mode
---
i18n/en_US.yaml | 11 +++--
ui/public/index.html | 2 +-
ui/src/common/color.scss | 52 ++++++++++++++++++++++++
ui/src/common/constants.ts | 1 +
ui/src/common/interface.ts | 1 +
ui/src/components/Editor/index.scss | 10 ++---
ui/src/components/SideNav/index.scss | 5 +--
ui/src/index.scss | 17 ++++----
ui/src/pages/Admin/Themes/index.tsx | 19 ++++++++-
ui/src/pages/Layout/index.tsx | 20 ++++++++++
ui/src/pages/Questions/EditAnswer/index.scss | 4 ++
ui/src/pages/Questions/EditAnswer/index.tsx | 2 +-
ui/src/pages/Users/Settings/Interface/index.tsx | 53 +++++++++++++++++++++----
ui/src/services/client/settings.ts | 10 +++--
ui/src/stores/loggedUserInfo.ts | 1 +
ui/src/stores/themeSetting.ts | 2 +
ui/src/utils/common.ts | 16 ++++++++
ui/src/utils/guard.ts | 3 +-
ui/src/utils/localize.ts | 26 +++++++++++-
19 files changed, 220 insertions(+), 35 deletions(-)
diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index cd1e5b20..c96ed6ec 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -1159,6 +1159,10 @@ ui:
close: Close
reopen: Reopen
ok: OK
+ light: Light
+ dark: Dark
+ system_setting: System setting
+ default: Default
search:
title: Search Results
keywords: Keywords
@@ -1687,11 +1691,12 @@ ui:
themes:
label: Themes
text: Select an existing theme.
+ color_scheme:
+ label: Color scheme
navbar_style:
- label: Navbar Style
- text: Select an existing theme.
+ label: Navbar style
primary_color:
- label: Primary Color
+ label: Primary color
text: Modify the colors used by your themes
css_and_html:
page_title: CSS and HTML
diff --git a/ui/public/index.html b/ui/public/index.html
index eeb5fb11..f5a9c141 100644
--- a/ui/public/index.html
+++ b/ui/public/index.html
@@ -19,7 +19,7 @@
-->
<!DOCTYPE html>
-<html>
+<html data-bs-theme="">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
diff --git a/ui/src/common/color.scss b/ui/src/common/color.scss
new file mode 100644
index 00000000..01d2d08f
--- /dev/null
+++ b/ui/src/common/color.scss
@@ -0,0 +1,52 @@
+
+
+:root {
+ --an-side-nav-link: rgba(0, 0, 0, 0.65);
+ --an-toolbar-divider: rgba(0, 0, 0, 0.1);
+ --an-ced4da: #ced4da;
+ --an-e9ecef: #e9ecef;
+ --an-pre: #161b22;
+ --an-6c757d: #6c757d;
+ --an-212529: #212529;
+ --an-gray-300: var(--bs-gray-300);
+ --an-white: #fff;
+ --an-inbox-warning:#fff3cd80;
+ --an-f5: #f5f5f5;
+}
+
+
+[data-bs-theme="dark"] {
+ --an-side-nav-link: rgba(255, 255, 255, 0.65);
+ --an-toolbar-divider: rgba(255, 255, 255, 0.3);
+ --an-ced4da: var(--bs-border-color);
+ --an-e9ecef: #161b22;
+ --an-pre: #161b22;
+ --an-6c757d: var(--bs-body-color);
+ --an-212529: var(--bs-body-color);
+ --an-gray-300: #161b22;
+ --an-white: #000;
+ --an-inbox-warning:#38363180;
+ --an-f5: var(--bs-body-bg);
+ }
+
+[data-bs-theme="dark"] {
+ .link-dark {
+ color: rgba(var(--bs-emphasis-color-rgb),0.8)!important;
+ }
+ /** editor **/
+
+ .CodeMirror {
+ background: var(--bs-body-bg);
+ color: var(--bs-body-color);
+ }
+ .CodeMirror-cursor {
+ border-left: 1px solid var(--bs-body-color);
+ }
+ .cm-header, .cm-link, .cm-url {
+ color: var(--bs-body-color);
+ }
+
+ .bg-light {
+ background-color: rgba(0, 0, 0, 0.5) !important;
+ }
+}
diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts
index 87b2377b..275fe27d 100644
--- a/ui/src/common/constants.ts
+++ b/ui/src/common/constants.ts
@@ -28,6 +28,7 @@ export const DRAFT_QUESTION_STORAGE_KEY = '_a_dq_';
export const DRAFT_ANSWER_STORAGE_KEY = '_a_da_';
export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|';
export const QUESTIONS_ORDER_STORAGE_KEY = '_a_qok_';
+export const DEFAULT_THEME = 'system';
export const USER_AGENT_NAMES = {
SegmentFault: 'SegmentFault',
diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts
index 68f3be95..56bbc69f 100644
--- a/ui/src/common/interface.ts
+++ b/ui/src/common/interface.ts
@@ -440,6 +440,7 @@ export type themeConfig = {
};
export interface AdminSettingsTheme {
theme: string;
+ color_scheme: string;
theme_options?: { label: string; value: string }[];
theme_config: Record<string, themeConfig>;
}
diff --git a/ui/src/components/Editor/index.scss b/ui/src/components/Editor/index.scss
index e71d27fb..5b940e19 100644
--- a/ui/src/components/Editor/index.scss
+++ b/ui/src/components/Editor/index.scss
@@ -20,16 +20,16 @@
.md-editor-wrap {
display: flex;
flex-direction: column;
- background-color: #fff;
- border: 1px solid #ced4da;
+ background-color: var(-bs-body-bg);
+ border: 1px solid var(--an-ced4da);
overflow: hidden;
.toolbar-wrap {
- border-bottom: 1px solid #ced4da;
+ border-bottom: 1px solid var(--an-ced4da);
.toolbar-divider {
float: left;
width: 1px;
height: 15px;
- background-color: rgba(0, 0, 0, 0.1);
+ background-color: var(--an-toolbar-divider);
margin: 10px 8px;
}
.toolbar-item-wrap {
@@ -57,7 +57,7 @@
box-sizing: border-box;
outline: none;
cursor: pointer;
- background-color: #fff;
+ background-color: var(--bs-body-bg);
height: 100%;
width: 100%;
border-radius: 0;
diff --git a/ui/src/components/SideNav/index.scss b/ui/src/components/SideNav/index.scss
index 802dbcd1..c987591d 100644
--- a/ui/src/components/SideNav/index.scss
+++ b/ui/src/components/SideNav/index.scss
@@ -17,7 +17,6 @@
* under the License.
*/
-
#sideNav {
.nav-wrap {
position: sticky;
@@ -32,10 +31,10 @@
max-width: calc(100% - 24px);
}
.nav-link {
- color: rgba(0, 0, 0, 0.65);
+ color: var(--an-side-nav-link);
}
.nav-link:hover {
- color: rgba(0, 0, 0);
+ color: black;
background-color: var(--bs-gray-100);
}
.nav-link.active {
diff --git a/ui/src/index.scss b/ui/src/index.scss
index e9535748..0c618e80 100644
--- a/ui/src/index.scss
+++ b/ui/src/index.scss
@@ -20,9 +20,10 @@
@import 'common/variable';
@import '~bootstrap/scss/bootstrap';
@import '~bootstrap-icons';
+@import 'common/color';
.bg-gray-300 {
- background-color: $gray-300;
+ background-color: var(--an-gray-300);
}
.focus {
@@ -132,7 +133,7 @@ img[src=""] {
}
.bg-f5 {
- background-color: #f5f5f5;
+ background-color: var(--an-f5);
}
.btn-no-border,
@@ -198,7 +199,7 @@ img[src=""] {
}
.warning {
- background-color: #fff3cd80;
+ background-color: var(--an-inbox-warning);
}
.fit-content {
@@ -246,14 +247,14 @@ img[src=""] {
}
p {
> code {
- background-color: #e9ecef;
- color: #212529;
+ background-color: var(--an-e9ecef);
+ color: var(--an-212529);
padding: 2px 4px;
border-radius: 0.25rem;
}
}
pre {
- background-color: #e9ecef;
+ background-color: var(--an-e9ecef);
border-radius: 0.25rem;
padding: 1rem;
}
@@ -261,9 +262,9 @@ img[src=""] {
border-left: 0.25rem solid #ced4da;
padding: 1rem;
color: #6c757d;
- background-color: #e9ecef;
+ background-color: var(--an-e9ecef);
p {
- color: $body-color;
+ color: var(--bs-body-color);
}
> p:last-child {
margin-bottom: 0;
diff --git a/ui/src/pages/Admin/Themes/index.tsx b/ui/src/pages/Admin/Themes/index.tsx
index 34cb42d9..3f817712 100644
--- a/ui/src/pages/Admin/Themes/index.tsx
+++ b/ui/src/pages/Admin/Themes/index.tsx
@@ -26,6 +26,7 @@ import { SchemaForm, JSONSchema, initFormData, UISchema } from '@/components';
import { useToast } from '@/hooks';
import { handleFormError } from '@/utils';
import { themeSettingStore } from '@/stores';
+import { setupAppTheme } from '@/utils/localize';
const Index: FC = () => {
const { t } = useTranslation('translation', {
@@ -44,10 +45,20 @@ const Index: FC = () => {
enumNames: themeSetting?.theme_options?.map((_) => _.label),
default: themeSetting?.theme_options?.[0]?.value,
},
+ color_scheme: {
+ type: 'string',
+ title: t('color_scheme.label'),
+ enum: ['setting', 'light', 'dark'],
+ enumNames: [
+ t('system_setting', { keyPrefix: 'btns' }),
+ t('light', { keyPrefix: 'btns' }),
+ t('dark', { keyPrefix: 'btns' }),
+ ],
+ default: themeSetting?.color_scheme,
+ },
navbar_style: {
type: 'string',
title: t('navbar_style.label'),
- description: t('navbar_style.text'),
enum: ['colored', 'light'],
enumNames: ['Colored', 'Light'],
default: 'colored',
@@ -64,6 +75,9 @@ const Index: FC = () => {
themes: {
'ui:widget': 'select',
},
+ color_scheme: {
+ 'ui:widget': 'select',
+ },
navbar_style: {
'ui:widget': 'select',
},
@@ -81,6 +95,7 @@ const Index: FC = () => {
const themeName = formData.themes.value;
const reqParams: Type.AdminSettingsTheme = {
theme: themeName,
+ color_scheme: formData.color_scheme.value,
theme_config: {
[themeName]: {
navbar_style: formData.navbar_style.value,
@@ -96,6 +111,7 @@ const Index: FC = () => {
variant: 'success',
});
updateThemeSetting(reqParams);
+ setupAppTheme();
})
.catch((err) => {
if (err.isError) {
@@ -115,6 +131,7 @@ const Index: FC = () => {
formMeta.themes.value = themeName;
formMeta.navbar_style.value = themeConfig?.navbar_style;
formMeta.primary_color.value = themeConfig?.primary_color;
+ formData.color_scheme.value = setting?.color_scheme || 'system';
setFormData({ ...formMeta });
}
});
diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx
index 1be44d9d..eb44e3e3 100644
--- a/ui/src/pages/Layout/index.tsx
+++ b/ui/src/pages/Layout/index.tsx
@@ -34,6 +34,7 @@ import {
HttpErrorContent,
} from '@/components';
import { LoginToContinueModal } from '@/components/Modal';
+import { changeTheme } from '@/utils';
const Layout: FC = () => {
const location = useLocation();
@@ -47,6 +48,25 @@ const Layout: FC = () => {
useEffect(() => {
httpStatusReset();
}, [location]);
+
+ useEffect(() => {
+ const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ function handleSystemThemeChange(event) {
+ if (event.matches) {
+ console.log('system theme is dark');
+ changeTheme('dark');
+ } else {
+ console.log('system theme is light');
+ changeTheme('light');
+ }
+ }
+
+ systemThemeQuery.addListener(handleSystemThemeChange);
+
+ return () => {
+ systemThemeQuery.removeListener(handleSystemThemeChange);
+ };
+ }, []);
return (
<HelmetProvider>
<PageTags />
diff --git a/ui/src/pages/Questions/EditAnswer/index.scss b/ui/src/pages/Questions/EditAnswer/index.scss
index 9bae8ebe..68dfb023 100644
--- a/ui/src/pages/Questions/EditAnswer/index.scss
+++ b/ui/src/pages/Questions/EditAnswer/index.scss
@@ -27,6 +27,7 @@
height: calc(100% - 12px);
overflow-y: scroll;
z-index: 1;
+ background: var(--an-white);
}
.scroll-bar {
height: 12px;
@@ -46,6 +47,9 @@
transform: scale(110, 1);
height: 100%;
}
+ .resize-bottom::-webkit-resizer {
+ border: none;
+ }
.resize-bottom + .line {
left: 0;
width: 100%;
diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx
index d693794d..9f4d9d65 100644
--- a/ui/src/pages/Questions/EditAnswer/index.tsx
+++ b/ui/src/pages/Questions/EditAnswer/index.tsx
@@ -227,7 +227,7 @@ const Index = () => {
<div className="question-content-wrap">
<div
ref={questionContentRef}
- className="content position-absolute top-0 w-100 bg-white"
+ className="content position-absolute top-0 w-100"
dangerouslySetInnerHTML={{ __html: data?.question.html }}
/>
<div
diff --git a/ui/src/pages/Users/Settings/Interface/index.tsx b/ui/src/pages/Users/Settings/Interface/index.tsx
index 4164394f..db91acf1 100644
--- a/ui/src/pages/Users/Settings/Interface/index.tsx
+++ b/ui/src/pages/Users/Settings/Interface/index.tsx
@@ -25,7 +25,7 @@ import { useToast } from '@/hooks';
import { updateUserInterface } from '@/services';
import { localize } from '@/utils';
import { loggedUserInfoStore } from '@/stores';
-import { SchemaForm, JSONSchema, UISchema, initFormData } from '@/components';
+import { SchemaForm, JSONSchema, UISchema } from '@/components';
const Index = () => {
const { t } = useTranslation('translation', {
@@ -34,10 +34,22 @@ const Index = () => {
const loggedUserInfo = loggedUserInfoStore.getState().user;
const toast = useToast();
const [langs, setLangs] = useState<LangsType[]>();
+ const [formData, setFormData] = useState<FormDataType>({
+ language: {
+ value: loggedUserInfo.language,
+ isInvalid: false,
+ errorMsg: '',
+ },
+ color_scheme: {
+ value: loggedUserInfo.color_scheme || 'default',
+ isInvalid: false,
+ errorMsg: '',
+ },
+ });
const schema: JSONSchema = {
title: t('heading'),
properties: {
- lang: {
+ language: {
type: 'string',
title: t('lang.label'),
description: t('lang.text'),
@@ -45,32 +57,59 @@ const Index = () => {
enumNames: langs?.map((_) => _.label),
default: loggedUserInfo.language,
},
+ color_scheme: {
+ type: 'string',
+ title: t('color_scheme.label', { keyPrefix: 'admin.themes' }),
+ enum: ['default', 'system', 'light', 'dark'],
+ enumNames: [
+ t('default', { keyPrefix: 'btns' }),
+ t('system_setting', { keyPrefix: 'btns' }),
+ t('light', { keyPrefix: 'btns' }),
+ t('dark', { keyPrefix: 'btns' }),
+ ],
+ default: loggedUserInfo.color_scheme,
+ },
},
};
const uiSchema: UISchema = {
- lang: {
+ language: {
+ 'ui:widget': 'select',
+ },
+ color_scheme: {
'ui:widget': 'select',
},
};
- const [formData, setFormData] = useState<FormDataType>(initFormData(schema));
const getLangs = async () => {
const res: LangsType[] = await localize.loadLanguageOptions();
+ setFormData({
+ ...formData,
+ language: {
+ ...formData.language,
+ value: res[0].value,
+ },
+ });
setLangs(res);
};
+ console.log(formData.color_scheme, '3333');
+
const handleOnChange = (d) => {
setFormData(d);
};
const handleSubmit = (event: FormEvent) => {
event.preventDefault();
- const lang = formData.lang.value;
- updateUserInterface(lang).then(() => {
+ const params = {
+ language: formData.language.value,
+ color_scheme: formData.color_scheme.value,
+ };
+ updateUserInterface(params).then(() => {
loggedUserInfoStore.getState().update({
...loggedUserInfo,
- language: lang,
+ ...params,
});
localize.setupAppLanguage();
+ localize.setupAppTheme();
toast.onShow({
msg: t('update', { keyPrefix: 'toast' }),
variant: 'success',
diff --git a/ui/src/services/client/settings.ts b/ui/src/services/client/settings.ts
index 51b3a6a1..05cea96c 100644
--- a/ui/src/services/client/settings.ts
+++ b/ui/src/services/client/settings.ts
@@ -30,10 +30,12 @@ export const getLanguageOptions = () => {
return request.get<Type.LangsType[]>('/answer/api/v1/language/options');
};
-export const updateUserInterface = (lang: string) => {
- return request.put('/answer/api/v1/user/interface', {
- language: lang,
- });
+interface userSettingInterface {
+ language: '';
+ color_scheme: '';
+}
+export const updateUserInterface = (data: userSettingInterface) => {
+ return request.put('/answer/api/v1/user/interface', data);
};
export const useGetNotificationConfig = () => {
diff --git a/ui/src/stores/loggedUserInfo.ts b/ui/src/stores/loggedUserInfo.ts
index e1d5e4af..1157e631 100644
--- a/ui/src/stores/loggedUserInfo.ts
+++ b/ui/src/stores/loggedUserInfo.ts
@@ -42,6 +42,7 @@ const initUser: UserInfoRes = {
status: 'normal',
mail_status: 1,
language: 'Default',
+ color_scheme: 'default',
is_admin: false,
have_password: true,
role_id: 1,
diff --git a/ui/src/stores/themeSetting.ts b/ui/src/stores/themeSetting.ts
index 1100a027..d0788cc8 100644
--- a/ui/src/stores/themeSetting.ts
+++ b/ui/src/stores/themeSetting.ts
@@ -25,11 +25,13 @@ interface IType {
theme: AdminSettingsTheme['theme'];
theme_config: AdminSettingsTheme['theme_config'];
theme_options: AdminSettingsTheme['theme_options'];
+ color_scheme: AdminSettingsTheme['color_scheme'];
update: (params: AdminSettingsTheme) => void;
}
const store = create<IType>((set) => ({
theme: 'default',
+ color_scheme: 'system',
theme_options: [{ label: 'Default', value: 'default' }],
theme_config: {
default: {
diff --git a/ui/src/utils/common.ts b/ui/src/utils/common.ts
index b6382a4e..22452019 100644
--- a/ui/src/utils/common.ts
+++ b/ui/src/utils/common.ts
@@ -246,6 +246,21 @@ function getUaType() {
return null;
}
+function changeTheme(mode: '' | 'light' | 'dark' | 'system') {
+ const htmlTag = document.querySelector('html') as HTMLHtmlElement;
+ if (mode === 'system') {
+ const systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
+
+ if (systemThemeQuery.matches) {
+ htmlTag.setAttribute('data-bs-theme', 'dark');
+ } else {
+ htmlTag.setAttribute('data-bs-theme', 'light');
+ }
+ } else {
+ htmlTag.setAttribute('data-bs-theme', mode);
+ }
+}
+
export {
thousandthDivision,
formatCount,
@@ -260,4 +275,5 @@ export {
diffText,
base64ToSvg,
getUaType,
+ changeTheme,
};
diff --git a/ui/src/utils/guard.ts b/ui/src/utils/guard.ts
index b424b87f..3abf1054 100644
--- a/ui/src/utils/guard.ts
+++ b/ui/src/utils/guard.ts
@@ -38,7 +38,7 @@ import {
} from '@/common/constants';
import Storage from '@/utils/storage';
-import { setupAppLanguage, setupAppTimeZone } from './localize';
+import { setupAppLanguage, setupAppTimeZone, setupAppTheme } from './localize';
import { floppyNavigation, NavigateConfig } from './floppyNavigation';
import { pullUcAgent, getSignUpUrl } from './userCenter';
@@ -453,6 +453,7 @@ export const setupApp = async () => {
await Promise.allSettled([pullUcAgent()]);
setupAppLanguage();
setupAppTimeZone();
+ setupAppTheme();
/**
* WARN:
* Initialization must be completed after all initialization actions,
diff --git a/ui/src/utils/localize.ts b/ui/src/utils/localize.ts
index 760b7967..850a1a2c 100644
--- a/ui/src/utils/localize.ts
+++ b/ui/src/utils/localize.ts
@@ -22,17 +22,23 @@ import i18next from 'i18next';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
-import { interfaceStore, loggedUserInfoStore } from '@/stores';
+import {
+ interfaceStore,
+ loggedUserInfoStore,
+ themeSettingStore,
+} from '@/stores';
import {
CURRENT_LANG_STORAGE_KEY,
DEFAULT_LANG,
LANG_RESOURCE_STORAGE_KEY,
+ DEFAULT_THEME,
} from '@/common/constants';
import {
getAdminLanguageOptions,
getLanguageConfig,
getLanguageOptions,
} from '@/services';
+import { changeTheme } from '@/utils/common';
import Storage from './storage';
@@ -103,6 +109,18 @@ export const getCurrentLang = () => {
return currentLang;
};
+export const getCurrentTheme = () => {
+ const loggedUser = loggedUserInfoStore.getState().user;
+ const adminTheme = themeSettingStore.getState().color_scheme;
+ const fallbackTheme = DEFAULT_THEME;
+ let currentTheme = loggedUser.color_scheme;
+ if (/default/i.test(currentTheme)) {
+ currentTheme = adminTheme;
+ }
+ currentTheme ||= fallbackTheme;
+ return currentTheme;
+};
+
/**
* localize for Day.js
*/
@@ -128,3 +146,9 @@ export const setupAppTimeZone = () => {
dayjs.tz.setDefault(adminInterface.time_zone);
}
};
+
+export const setupAppTheme = () => {
+ const theme = getCurrentTheme();
+ console.log(333, theme);
+ changeTheme(theme);
+};