diff --git a/client/ui/preference/Preference.tsx b/client/ui/preference/Preference.tsx
new file mode 100644
index 0000000..b3811d7
--- /dev/null
+++ b/client/ui/preference/Preference.tsx
@@ -0,0 +1,13 @@
+import { $ } from 'mdui/jq'
+import { Switch } from 'mdui'
+import React from 'react'
+import useEventListener from '../useEventListener.ts'
+
+export default function Preference({ title, icon, disabled, description, ...props } = {
+ disabled: false,
+}) {
+ return
+ {title}
+ {description && {description}}
+
+}
\ No newline at end of file
diff --git a/client/ui/preference/PreferenceHeader.tsx b/client/ui/preference/PreferenceHeader.tsx
new file mode 100644
index 0000000..c6ca694
--- /dev/null
+++ b/client/ui/preference/PreferenceHeader.tsx
@@ -0,0 +1,3 @@
+export default function PreferenceHeader({ title }) {
+ return {title}
+}
\ No newline at end of file
diff --git a/client/ui/preference/PreferenceLayout.tsx b/client/ui/preference/PreferenceLayout.tsx
new file mode 100644
index 0000000..a34a5ad
--- /dev/null
+++ b/client/ui/preference/PreferenceLayout.tsx
@@ -0,0 +1,8 @@
+export default function PreferenceLayout({ children, ...props }) {
+ return
+ {children}
+
+}
\ No newline at end of file
diff --git a/client/ui/preference/PreferenceStore.ts b/client/ui/preference/PreferenceStore.ts
new file mode 100644
index 0000000..5f2a8b5
--- /dev/null
+++ b/client/ui/preference/PreferenceStore.ts
@@ -0,0 +1,23 @@
+import React from 'react'
+
+export default class PreferenceStore {
+ constructor() {
+ const _ = React.useState<{ [key: string]: unknown }>({})
+ this.value = _[0]
+ this.setter = _[1]
+ }
+ // 创建一个用于子选项的更新函数
+ updater(key: string) {
+ return (value: unknown) => {
+ const newValue = JSON.parse(JSON.stringify({
+ ...this.value,
+ [key]: value,
+ }))
+ this.setter(newValue)
+ this.onUpdate?.(newValue)
+ }
+ }
+ setOnUpdate(onUpdate) {
+ this.onUpdate = onUpdate
+ }
+}
diff --git a/client/ui/preference/SelectPreference.tsx b/client/ui/preference/SelectPreference.tsx
new file mode 100644
index 0000000..f01a2be
--- /dev/null
+++ b/client/ui/preference/SelectPreference.tsx
@@ -0,0 +1,38 @@
+import { $ } from 'mdui/jq'
+import React from 'react'
+import { Dropdown } from 'mdui'
+import useEventListener from '../useEventListener.ts'
+
+// value as { [id: string]: string }
+export default function SelectPreference({ title, icon, updater, selections, defaultCheckedId, disabled } = {
+ disabled: false,
+}) {
+ const [ checkedId, setCheckedId ] = React.useState(defaultCheckedId)
+
+ const dropDownRef = React.useRef(null)
+ const [isDropDownOpen, setDropDownOpen] = React.useState(false)
+
+ useEventListener(dropDownRef, 'closed', (e) => {
+ setDropDownOpen(false)
+ })
+
+ return setDropDownOpen(!isDropDownOpen)}>
+
+ { title }
+ {
+ e.stopPropagation()
+ setDropDownOpen(false)
+ }}>
+ {
+ Object.keys(selections).map((id) =>
+ {
+ setCheckedId(id)
+ updater(id)
+ }}>{selections[id]}
+ )
+ }
+
+
+ { selections[checkedId] }
+
+}
\ No newline at end of file
diff --git a/client/ui/preference/SwitchPreference.tsx b/client/ui/preference/SwitchPreference.tsx
new file mode 100644
index 0000000..69bb271
--- /dev/null
+++ b/client/ui/preference/SwitchPreference.tsx
@@ -0,0 +1,19 @@
+import { $ } from 'mdui/jq'
+import { Switch } from 'mdui'
+import React from 'react'
+import useEventListener from '../useEventListener.ts'
+
+export default function SwitchPreference({ title, icon, updater, disabled, description } = {
+ disabled: false,
+}) {
+ const switchRef = React.useRef(null)
+
+ return {
+ switchRef.current!.checked = !switchRef.current!.checked
+ updater(switchRef.current!.checked)
+ }}>
+ {title}
+ {description && {description}}
+ e.preventDefault()}>
+
+}
\ No newline at end of file
diff --git a/client/ui/preference/TextFieldPreference.tsx b/client/ui/preference/TextFieldPreference.tsx
new file mode 100644
index 0000000..ac96e5d
--- /dev/null
+++ b/client/ui/preference/TextFieldPreference.tsx
@@ -0,0 +1,30 @@
+import { $ } from 'mdui/jq'
+import React from 'react'
+import { TextField, prompt } from 'mdui'
+import useEventListener from '../useEventListener.ts'
+
+export default function TextFieldPreference({ title, icon, description, updater, defaultValue, disabled } = {
+ disabled: false,
+}) {
+ const [ text, setText ] = React.useState(defaultValue)
+
+ return {
+ prompt({
+ headline: title,
+ confirmText: "确定",
+ cancelText: "取消",
+ onConfirm: (value) => {
+ setText(value)
+ updater(value)
+ },
+ onCancel: () => {},
+ textFieldOptions: {
+ label: description,
+ value: text,
+ },
+ })
+ }}>
+ {title}
+ {description && {description}}
+
+}
\ No newline at end of file