diff --git a/packages/app/src/components/Settings/index.jsx b/packages/app/src/components/Settings/index.jsx
deleted file mode 100755
index ab87bd33..00000000
--- a/packages/app/src/components/Settings/index.jsx
+++ /dev/null
@@ -1,492 +0,0 @@
-import React from "react"
-import * as antd from "antd"
-import { SliderPicker } from "react-color"
-import { Translation } from "react-i18next"
-import classnames from "classnames"
-
-import config from "config"
-import { Icons, createIconRender } from "components/Icons"
-
-import SettingsList from "schemas/settings"
-import groupsDecorator from "schemas/settingsGroupsDecorator.json"
-
-import "./index.less"
-
-const ItemTypes = {
- Button: antd.Button,
- Switch: antd.Switch,
- Slider: antd.Slider,
- Checkbox: antd.Checkbox,
- Input: antd.Input,
- TextArea: antd.Input.TextArea,
- InputNumber: antd.InputNumber,
- Select: antd.Select,
- SliderColorPicker: SliderPicker,
-}
-
-const SettingsFooter = (props) => {
- const isDevMode = window.__evite?.env?.NODE_ENV !== "production"
-
- return
-
-
{config.app?.siteName}
-
-
- v{window.app.version}
-
-
-
-
- {isDevMode ? : }
- {isDevMode ? "development" : "stable"}
-
-
-
-
-
-
- {t => t("about")}
-
-
-
-
-}
-
-const SettingItem = (props) => {
- let { item } = props
-
- const [loading, setLoading] = React.useState(true)
- const [value, setValue] = React.useState(null)
- const [delayedValue, setDelayedValue] = React.useState(null)
- const [disabled, setDisabled] = React.useState(false)
-
- let SettingComponent = item.component
-
- if (!SettingComponent) {
- console.error(`Item [${item.id}] has no an component!`)
- return null
- }
-
- if (typeof item.props === "undefined") {
- item.props = {}
- }
-
- const dispatchUpdate = async (updateValue) => {
- if (typeof item.onUpdate === "function") {
- const result = await item.onUpdate(updateValue).catch((error) => {
- console.error(error)
- antd.message.error(error.message)
- return false
- })
-
- if (!result) {
- return false
- }
- updateValue = result
- } else {
- const storagedValue = await window.app.settings.get(item.id)
-
- if (typeof updateValue === "undefined") {
- updateValue = !storagedValue
- }
- }
-
- if (item.storaged) {
- await window.app.settings.set(item.id, updateValue)
- }
-
- if (typeof item.emitEvent !== "undefined") {
- let emissionPayload = updateValue
-
- if (typeof item.emissionValueUpdate === "function") {
- emissionPayload = item.emissionValueUpdate(emissionPayload)
- }
-
- if (Array.isArray(item.emitEvent)) {
- window.app.eventBus.emit(...item.emitEvent, emissionPayload)
- } else if (typeof item.emitEvent === "string") {
- window.app.eventBus.emit(item.emitEvent, emissionPayload)
- }
- }
-
- if (item.noUpdate) {
- return false
- }
-
- if (item.debounced) {
- setDelayedValue(null)
- }
-
- setValue(updateValue)
- }
-
- const onUpdateItem = async (updateValue) => {
- setValue(updateValue)
-
- if (!item.debounced) {
- await dispatchUpdate(updateValue)
- } else {
- setDelayedValue(updateValue)
- }
- }
-
- const checkDependsValidation = () => {
- return !Boolean(Object.keys(item.dependsOn).every((key) => {
- const storagedValue = window.app.settings.get(key)
-
- console.debug(`Checking validation for [${key}] with now value [${storagedValue}]`)
-
- if (typeof item.dependsOn[key] === "function") {
- return item.dependsOn[key](storagedValue)
- }
-
- return storagedValue === item.dependsOn[key]
- }))
- }
-
- const settingInitialization = async () => {
- if (item.storaged) {
- const storagedValue = window.app.settings.get(item.id)
- setValue(storagedValue)
- }
-
- if (typeof item.defaultValue === "function") {
- setLoading(true)
-
- setValue(await item.defaultValue(props.ctx))
-
- setLoading(false)
- }
-
- if (item.disabled === true) {
- setDisabled(true)
- }
-
- if (typeof item.dependsOn === "object") {
- // create a event handler to watch changes
- Object.keys(item.dependsOn).forEach((key) => {
- window.app.eventBus.on(`setting.update.${key}`, () => {
- setDisabled(checkDependsValidation())
- })
- })
-
- // by default check depends validation
- setDisabled(checkDependsValidation())
- }
-
- if (typeof item.listenUpdateValue === "string") {
- window.app.eventBus.on(`setting.update.${item.listenUpdateValue}`, (value) => setValue(value))
- }
-
- if (item.reloadValueOnUpdateEvent) {
- window.app.eventBus.on(item.reloadValueOnUpdateEvent, () => {
- console.log(`Reloading value for item [${item.id}]`)
- settingInitialization()
- })
- }
-
- setLoading(false)
- }
-
- React.useEffect(() => {
- settingInitialization()
-
- return () => {
- if (typeof item.dependsOn === "object") {
- for (let key in item.dependsOn) {
- window.app.eventBus.off(`setting.update.${key}`, onUpdateItem)
- }
- }
- }
- }, [])
-
- if (typeof SettingComponent === "string") {
- if (typeof ItemTypes[SettingComponent] === "undefined") {
- console.error(`Item [${item.id}] has an invalid component: ${item.component}`)
- return null
- }
-
- switch (SettingComponent.toLowerCase()) {
- case "slidercolorpicker": {
- item.props.onChange = (color) => {
- item.props.color = color.hex
- }
- item.props.onChangeComplete = (color) => {
- onUpdateItem(color.hex)
- }
-
- item.props.color = value
-
- break
- }
- case "textarea": {
- item.props.defaultValue = value
- item.props.onPressEnter = (event) => dispatchUpdate(event.target.value)
- item.props.onChange = (event) => onUpdateItem(event.target.value)
- break
- }
- case "input": {
- item.props.defaultValue = value
- item.props.onPressEnter = (event) => dispatchUpdate(event.target.value)
- item.props.onChange = (event) => onUpdateItem(event.target.value)
- break
- }
- case "switch": {
- item.props.checked = value
- item.props.onClick = (event) => onUpdateItem(event)
- break
- }
- case "select": {
- item.props.onChange = (value) => onUpdateItem(value)
- item.props.defaultValue = value
- break
- }
- case "slider": {
- item.props.defaultValue = value
- item.props.onAfterChange = (value) => onUpdateItem(value)
- break
- }
- default: {
- if (!item.props.children) {
- item.props.children = item.title ?? item.id
- }
-
- item.props.value = item.defaultValue
- item.props.onClick = (event) => onUpdateItem(event)
-
- break
- }
- }
-
- // override with default item component
- SettingComponent = ItemTypes[SettingComponent]
- }
-
- item.props["disabled"] = disabled
-
- return
-
-
-
-
- {Icons[item.icon] ? React.createElement(Icons[item.icon]) : null}
- {
- t => t(item.title ?? item.id)
- }
-
-
{
- t => t(item.description)
- }
-
-
- {item.experimental &&
Experimental }
-
-
- {item.extraActions &&
-
- {item.extraActions.map((action, index) => {
- return
- })}
-
- }
-
-
-
- {
- loading
- ?
Loading...
- : React.createElement(SettingComponent, {
- ...item.props,
- ctx: {
- currentValue: value,
- dispatchUpdate,
- onUpdateItem,
- ...props.ctx,
- }
- })}
-
-
- {
- delayedValue &&
-
}
- onClick={async () => await dispatchUpdate(value)}
- >
- Save
-
-
- }
-
-
-}
-
-const SettingGroup = React.memo((props) => {
- const {
- ctx,
- groupKey,
- settings,
- loading,
- } = props
-
- const fromDecoratorIcon = groupsDecorator[groupKey]?.icon
- const fromDecoratorTitle = groupsDecorator[groupKey]?.title
-
- if (loading) {
- return
- }
-
- return
-
- {
- fromDecoratorIcon ? React.createElement(Icons[fromDecoratorIcon]) : null
- }
-
- {
- t => t(fromDecoratorTitle ?? groupKey)
- }
-
-
-
- {
- settings.map((item) => )
- }
-
-
-})
-
-const SettingTab = React.memo((props) => {
- const { tab } = props
-
- const [loading, setLoading] = React.useState(true)
- const [ctxData, setCtxData] = React.useState(null)
-
- let groupsSettings = {}
-
- if (!Array.isArray(tab.settings)) {
- console.error("Cannot generate settings from non-array")
- return groupsSettings
- }
-
- tab.settings.forEach((item) => {
- if (!groupsSettings[item.group]) {
- groupsSettings[item.group] = []
- }
-
- groupsSettings[item.group].push(item)
- })
-
- const processCtx = async () => {
- setLoading(true)
-
- if (typeof tab.ctxData === "function") {
- const resultCtx = await tab.ctxData()
-
- setCtxData(resultCtx)
- }
-
- setLoading(false)
- }
-
- React.useEffect(() => {
- processCtx()
- }, [])
-
- return Object.keys(groupsSettings).map((groupKey) => {
- return
- })
-})
-
-const SettingsTabs = Object.keys(SettingsList).map((settingsKey) => {
- const tab = SettingsList[settingsKey]
-
- return {
- key: settingsKey,
- label: <>
- {createIconRender(tab.icon ?? "Settings")}
- {tab.label}
- >,
- children:
- }
-})
-
-export default class SettingsMenu extends React.PureComponent {
- state = {
- activeKey: "app"
- }
-
- componentDidMount = async () => {
- if (typeof this.props.close === "function") {
- // register escape key to close settings menu
- window.addEventListener("keydown", this.handleKeyDown)
- }
- }
-
- componentWillUnmount() {
- if (typeof this.props.close === "function") {
- window.removeEventListener("keydown", this.handleKeyDown)
- }
- }
-
- handleKeyDown = (event) => {
- if (event.key === "Escape") {
- this.props.close()
- }
- }
-
- onClickAppAbout = () => {
- window.app.setLocation("/about")
-
- if (typeof this.props.close === "function") {
- this.props.close()
- }
- }
-
- changeTab = (activeKey) => {
- this.setState({ activeKey })
- }
-
- render() {
- return
- }
-}
\ No newline at end of file
diff --git a/packages/app/src/components/Settings/index.less b/packages/app/src/components/Settings/index.less
deleted file mode 100755
index f5aa84ce..00000000
--- a/packages/app/src/components/Settings/index.less
+++ /dev/null
@@ -1,163 +0,0 @@
-.settings_wrapper {
- .settings {
- display: flex;
- flex-direction: column;
-
- >div {
- margin-bottom: 25px;
- }
-
- &.mobile {
- .ant-tabs-nav-list {
- width: 100%;
- justify-content: space-evenly;
- }
- }
-
- .ant-tabs-nav {
- height: fit-content;
- position: sticky;
-
- top: 0;
- left: 0;
-
- .ant-tabs-nav-wrap {
- height: fit-content;
-
- .ant-tabs-nav-list {
- height: fit-content;
-
- .ant-tabs-tab {
- padding: 5px 0 !important;
- margin-right: 10px !important;
- }
- }
- }
- }
-
- .group {
- display: flex;
- flex-direction: column;
- color: var(--background-color-contrast);
-
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- color: var(--background-color-contrast);
- }
-
- .content {
- >div {
- margin-bottom: 25px;
- }
- }
- }
-
- .settingItem {
- padding: 0 20px;
-
- >div {
- margin-bottom: 10px;
- }
-
- .header {
- display: inline-flex;
- flex-direction: row;
- align-items: center;
- justify-content: space-between;
- width: 100%;
-
- .title {
- display: flex;
- align-items: center;
- color: var(--background-color-contrast);
-
- h1,
- h2,
- h3,
- h4,
- h5,
- h6 {
- margin: 0;
- color: var(--background-color-contrast);
- }
-
- p {
- font-size: 11px;
- color: var(--background-color-contrast);
- margin: 0;
- }
-
- >div {
- margin-right: 10px;
- }
- }
-
- .extraActions {
- display: inline-flex;
- align-items: center;
-
- >div {
- margin-right: 10px;
- }
- }
- }
-
- .component {
- display: flex;
- flex-direction: column;
-
- --ignore-dragger: true;
- padding: 0 20px;
-
- span {
- color: var(--background-color-contrast);
- }
-
- >div {
- margin-bottom: 10px;
- }
- }
- }
-
- .footer {
- position: relative;
- width: 100%;
-
- padding-top: 20px;
- padding-bottom: 20px;
-
- display: flex;
- flex-direction: column;
-
- justify-content: center;
- align-items: center;
-
- >div {
- margin-bottom: 10px;
-
- font-family: "DM Mono", monospace;
- font-size: 10px;
-
- display: flex;
- flex-direction: row;
-
- align-items: center;
- justify-content: center;
-
- .ant-tag {
- height: 18px;
- line-height: 18px;
- font-size: 10px;
- }
-
- >div {
- padding: 0 7px;
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/packages/app/src/pages/settings/index.jsx b/packages/app/src/pages/settings/index.jsx
new file mode 100644
index 00000000..48543f89
--- /dev/null
+++ b/packages/app/src/pages/settings/index.jsx
@@ -0,0 +1,506 @@
+import React from "react"
+import * as antd from "antd"
+import { SliderPicker } from "react-color"
+import { Translation } from "react-i18next"
+import classnames from "classnames"
+
+import { Icons, createIconRender } from "components/Icons"
+
+import SettingsList from "schemas/settings"
+import menuGroupsDecorators from "schemas/settingsMenuGroupsDecorators"
+import groupsDecorators from "schemas/settingsGroupsDecorators"
+
+import "./index.less"
+
+const ItemTypes = {
+ Button: antd.Button,
+ Switch: antd.Switch,
+ Slider: antd.Slider,
+ Checkbox: antd.Checkbox,
+ Input: antd.Input,
+ TextArea: antd.Input.TextArea,
+ InputNumber: antd.InputNumber,
+ Select: antd.Select,
+ SliderColorPicker: SliderPicker,
+}
+
+const SettingItem = (props) => {
+ let { item } = props
+
+ const [loading, setLoading] = React.useState(true)
+ const [value, setValue] = React.useState(null)
+ const [delayedValue, setDelayedValue] = React.useState(null)
+ const [disabled, setDisabled] = React.useState(false)
+
+ let SettingComponent = item.component
+
+ if (!SettingComponent) {
+ console.error(`Item [${item.id}] has no an component!`)
+ return null
+ }
+
+ if (typeof item.props === "undefined") {
+ item.props = {}
+ }
+
+ const dispatchUpdate = async (updateValue) => {
+ if (typeof item.onUpdate === "function") {
+ try {
+ const result = await item.onUpdate(updateValue)
+
+ if (result) {
+ updateValue = result
+ }
+ } catch (error) {
+ console.error(error)
+
+ if (error.response.data.error) {
+ app.message.error(error.response.data.error)
+ } else {
+ app.message.error(error.message)
+ }
+
+ return false
+ }
+ } else {
+ const storagedValue = await window.app.settings.get(item.id)
+
+ if (typeof updateValue === "undefined") {
+ updateValue = !storagedValue
+ }
+ }
+
+ if (item.storaged) {
+ await window.app.settings.set(item.id, updateValue)
+ }
+
+ if (typeof item.emitEvent !== "undefined") {
+ let emissionPayload = updateValue
+
+ if (typeof item.emissionValueUpdate === "function") {
+ emissionPayload = item.emissionValueUpdate(emissionPayload)
+ }
+
+ if (Array.isArray(item.emitEvent)) {
+ window.app.eventBus.emit(...item.emitEvent, emissionPayload)
+ } else if (typeof item.emitEvent === "string") {
+ window.app.eventBus.emit(item.emitEvent, emissionPayload)
+ }
+ }
+
+ if (item.noUpdate) {
+ return false
+ }
+
+ if (item.debounced) {
+ setDelayedValue(null)
+ }
+
+ setValue(updateValue)
+ }
+
+ const onUpdateItem = async (updateValue) => {
+ setValue(updateValue)
+
+ if (!item.debounced) {
+ await dispatchUpdate(updateValue)
+ } else {
+ setDelayedValue(updateValue)
+ }
+ }
+
+ const checkDependsValidation = () => {
+ return !Boolean(Object.keys(item.dependsOn).every((key) => {
+ const storagedValue = window.app.settings.get(key)
+
+ console.debug(`Checking validation for [${key}] with now value [${storagedValue}]`)
+
+ if (typeof item.dependsOn[key] === "function") {
+ return item.dependsOn[key](storagedValue)
+ }
+
+ return storagedValue === item.dependsOn[key]
+ }))
+ }
+
+ const settingInitialization = async () => {
+ if (item.storaged) {
+ const storagedValue = window.app.settings.get(item.id)
+ setValue(storagedValue)
+ }
+
+ if (typeof item.defaultValue === "function") {
+ setLoading(true)
+
+ setValue(await item.defaultValue(props.ctx))
+
+ setLoading(false)
+ }
+
+ if (item.disabled === true) {
+ setDisabled(true)
+ }
+
+ if (typeof item.dependsOn === "object") {
+ // create a event handler to watch changes
+ Object.keys(item.dependsOn).forEach((key) => {
+ window.app.eventBus.on(`setting.update.${key}`, () => {
+ setDisabled(checkDependsValidation())
+ })
+ })
+
+ // by default check depends validation
+ setDisabled(checkDependsValidation())
+ }
+
+ if (typeof item.listenUpdateValue === "string") {
+ window.app.eventBus.on(`setting.update.${item.listenUpdateValue}`, (value) => setValue(value))
+ }
+
+ if (item.reloadValueOnUpdateEvent) {
+ window.app.eventBus.on(item.reloadValueOnUpdateEvent, () => {
+ console.log(`Reloading value for item [${item.id}]`)
+ settingInitialization()
+ })
+ }
+
+ setLoading(false)
+ }
+
+ React.useEffect(() => {
+ settingInitialization()
+
+ return () => {
+ if (typeof item.dependsOn === "object") {
+ for (let key in item.dependsOn) {
+ window.app.eventBus.off(`setting.update.${key}`, onUpdateItem)
+ }
+ }
+ }
+ }, [])
+
+ if (typeof SettingComponent === "string") {
+ if (typeof ItemTypes[SettingComponent] === "undefined") {
+ console.error(`Item [${item.id}] has an invalid component: ${item.component}`)
+ return null
+ }
+
+ switch (SettingComponent.toLowerCase()) {
+ case "slidercolorpicker": {
+ item.props.onChange = (color) => {
+ item.props.color = color.hex
+ }
+ item.props.onChangeComplete = (color) => {
+ onUpdateItem(color.hex)
+ }
+
+ item.props.color = value
+
+ break
+ }
+ case "textarea": {
+ item.props.defaultValue = value
+ item.props.onPressEnter = (event) => dispatchUpdate(event.target.value)
+ item.props.onChange = (event) => onUpdateItem(event.target.value)
+ break
+ }
+ case "input": {
+ item.props.defaultValue = value
+ item.props.onPressEnter = (event) => dispatchUpdate(event.target.value)
+ item.props.onChange = (event) => onUpdateItem(event.target.value)
+ break
+ }
+ case "switch": {
+ item.props.checked = value
+ item.props.onClick = (event) => onUpdateItem(event)
+ break
+ }
+ case "select": {
+ item.props.onChange = (value) => onUpdateItem(value)
+ item.props.defaultValue = value
+ break
+ }
+ case "slider": {
+ item.props.defaultValue = value
+ item.props.onAfterChange = (value) => onUpdateItem(value)
+ break
+ }
+ default: {
+ if (!item.props.children) {
+ item.props.children = item.title ?? item.id
+ }
+
+ item.props.value = item.defaultValue
+ item.props.onClick = (event) => onUpdateItem(event)
+
+ break
+ }
+ }
+
+ // override with default item component
+ SettingComponent = ItemTypes[SettingComponent]
+ }
+
+ item.props["disabled"] = disabled
+
+ return
+
+
+
+
+ {Icons[item.icon] ? React.createElement(Icons[item.icon]) : null}
+ {
+ t => t(item.title ?? item.id)
+ }
+
+
{
+ t => t(item.description)
+ }
+
+
+ {item.experimental &&
Experimental }
+
+
+ {item.extraActions &&
+
+ {item.extraActions.map((action, index) => {
+ return
+ })}
+
+ }
+
+
+
+ {
+ loading
+ ?
Loading...
+ : React.createElement(SettingComponent, {
+ ...item.props,
+ ctx: {
+ currentValue: value,
+ dispatchUpdate,
+ onUpdateItem,
+ ...props.ctx,
+ }
+ })}
+
+
+ {
+ delayedValue &&
+
}
+ onClick={async () => await dispatchUpdate(value)}
+ >
+ Save
+
+
+ }
+
+
+}
+
+const SettingGroup = React.memo((props) => {
+ const {
+ ctx,
+ groupKey,
+ settings,
+ loading,
+ } = props
+
+ const fromDecoratorIcon = groupsDecorators[groupKey]?.icon
+ const fromDecoratorTitle = groupsDecorators[groupKey]?.title
+
+ if (loading) {
+ return
+ }
+
+ return
+
+ {
+ fromDecoratorIcon ? React.createElement(Icons[fromDecoratorIcon]) : null
+ }
+
+ {
+ t => t(fromDecoratorTitle ?? groupKey)
+ }
+
+
+
+ {
+ settings.map((item) => )
+ }
+
+
+})
+
+const SettingTab = (props) => {
+ const [groups, setGroups] = React.useState({})
+ const [loading, setLoading] = React.useState(true)
+ const [ctxData, setCtxData] = React.useState({})
+
+ const processCtx = async () => {
+ setLoading(true)
+
+ if (typeof props.tab.ctxData === "function") {
+ const resultCtx = await props.tab.ctxData()
+
+ setCtxData(resultCtx)
+ }
+
+ setLoading(false)
+ }
+
+ React.useEffect(() => {
+ if (!Array.isArray(props.tab.settings)) {
+ console.error("Cannot generate settings from non-array")
+
+ return []
+ }
+
+ let groupsSettings = {}
+
+ props.tab.settings.forEach((item) => {
+ if (!groupsSettings[item.group]) {
+ groupsSettings[item.group] = []
+ }
+
+ groupsSettings[item.group].push(item)
+ })
+
+ setGroups(groupsSettings)
+
+ processCtx()
+ }, [props.tab])
+
+ if (loading) {
+ return
+ }
+
+ return Object.keys(groups).map((groupKey) => {
+ return
+ })
+}
+
+const generateMenuItems = () => {
+ const groups = {}
+
+ Object.keys(SettingsList).forEach((tabKey) => {
+ const tab = SettingsList[tabKey]
+
+ if (!tab.group) {
+ tab.group = "Others"
+ }
+
+ groups[tab.group] = groups[tab.group] ?? []
+
+ groups[tab.group].push(tab)
+ })
+
+ let groupsKeys = Object.keys(groups)
+
+ // make "bottom" group last
+ groupsKeys = groupsKeys.sort((a, b) => {
+ if (a === "bottom") {
+ return 1
+ }
+
+ if (b === "bottom") {
+ return -1
+ }
+
+ return 0
+ })
+
+ return groupsKeys.map((groupKey, index) => {
+ const children = groups[groupKey].map((item) => {
+ return {
+ key: item.id,
+ label: <>
+ {createIconRender(item.icon ?? "Settings")}
+ {item.label}
+ >,
+ }
+ })
+
+ if (index !== groupsKeys.length - 1) {
+ children.push({
+ type: "divider",
+ })
+ }
+
+ return {
+ key: groupKey,
+ label: groupKey === "bottom" ? null : <>
+ {
+ menuGroupsDecorators[groupKey]?.icon && createIconRender(menuGroupsDecorators[groupKey]?.icon ?? "Settings")
+ }
+
+ {
+ t => t(menuGroupsDecorators[groupKey]?.label ?? groupKey)
+ }
+
+ >,
+ type: "group",
+ children: children
+ }
+ })
+}
+
+export default React.memo(() => {
+ const [activeKey, setActiveKey] = React.useState("general")
+ const [menuItems, setMenuItems] = React.useState([])
+
+ const onChangeTab = (event) => {
+ setActiveKey(event.key)
+ }
+
+ React.useEffect(() => {
+ setMenuItems(generateMenuItems())
+ }, [])
+
+ return
+
+
+
+ {
+ SettingsList[activeKey] &&
+ React.createElement(SettingsList[activeKey].render ?? SettingTab, {
+ tab: SettingsList[activeKey],
+ })
+ }
+
+
+})
\ No newline at end of file
diff --git a/packages/app/src/pages/settings/index.less b/packages/app/src/pages/settings/index.less
new file mode 100644
index 00000000..9ea1b615
--- /dev/null
+++ b/packages/app/src/pages/settings/index.less
@@ -0,0 +1,152 @@
+.settings_wrapper {
+ display: flex;
+ flex-direction: row;
+
+ .settings_menu {
+ position: sticky;
+
+ top: 0;
+ left: 0;
+
+ height: 100%;
+
+ display: flex;
+ flex-direction: column;
+
+ align-items: center;
+
+ width: 30%;
+ }
+
+ .settings_content {
+ display: flex;
+ flex-direction: column;
+
+ width: 70%;
+
+ .group {
+ display: flex;
+ flex-direction: column;
+ color: var(--background-color-contrast);
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ color: var(--background-color-contrast);
+ }
+
+ .content {
+ >div {
+ margin-bottom: 25px;
+ }
+ }
+ }
+
+ .settingItem {
+ padding: 0 20px;
+
+ >div {
+ margin-bottom: 10px;
+ }
+
+ .header {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+
+ .title {
+ display: flex;
+ align-items: center;
+ color: var(--background-color-contrast);
+
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ h6 {
+ margin: 0;
+ color: var(--background-color-contrast);
+ }
+
+ p {
+ font-size: 11px;
+ color: var(--background-color-contrast);
+ margin: 0;
+ }
+
+ >div {
+ margin-right: 10px;
+ }
+ }
+
+ .extraActions {
+ display: inline-flex;
+ align-items: center;
+
+ >div {
+ margin-right: 10px;
+ }
+ }
+ }
+
+ .component {
+ display: flex;
+ flex-direction: column;
+
+ --ignore-dragger: true;
+ padding: 0 20px;
+
+ span {
+ color: var(--background-color-contrast);
+ }
+
+ >div {
+ margin-bottom: 10px;
+ }
+ }
+ }
+
+ .footer {
+ position: relative;
+ width: 100%;
+
+ padding-top: 20px;
+ padding-bottom: 20px;
+
+ display: flex;
+ flex-direction: column;
+
+ justify-content: center;
+ align-items: center;
+
+ >div {
+ margin-bottom: 10px;
+
+ font-family: "DM Mono", monospace;
+ font-size: 10px;
+
+ display: flex;
+ flex-direction: row;
+
+ align-items: center;
+ justify-content: center;
+
+ .ant-tag {
+ height: 18px;
+ line-height: 18px;
+ font-size: 10px;
+ }
+
+ >div {
+ padding: 0 7px;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file