feat(ui): centralize interaction preferences
Expose interaction defaults in Settings and reuse the same registry for command palette actions.
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import type { Component } from "solid-js"
|
||||
import { Check, Laptop, Moon, Sun } from "lucide-solid"
|
||||
import { Select } from "@kobalte/core/select"
|
||||
import { createEffect, createMemo, createSignal, For, type Component } from "solid-js"
|
||||
import { Check, ChevronDown, Laptop, Moon, Sun } from "lucide-solid"
|
||||
import { useI18n } from "../../lib/i18n"
|
||||
import { useTheme, type ThemeMode } from "../../lib/theme"
|
||||
import { useConfig } from "../../stores/preferences"
|
||||
import { getBehaviorSettings, type BehaviorSetting } from "../../lib/settings/behavior-registry"
|
||||
|
||||
const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [
|
||||
{ value: "system", icon: Laptop },
|
||||
@@ -12,6 +15,200 @@ const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [
|
||||
export const AppearanceSettingsSection: Component = () => {
|
||||
const { t } = useI18n()
|
||||
const { themeMode, setThemeMode } = useTheme()
|
||||
const {
|
||||
preferences,
|
||||
updatePreferences,
|
||||
toggleShowThinkingBlocks,
|
||||
toggleKeyboardShortcutHints,
|
||||
toggleShowTimelineTools,
|
||||
toggleUsageMetrics,
|
||||
toggleAutoCleanupBlankSessions,
|
||||
togglePromptSubmitOnEnter,
|
||||
setDiffViewMode,
|
||||
setToolOutputExpansion,
|
||||
setDiagnosticsExpansion,
|
||||
setThinkingBlocksExpansion,
|
||||
setToolInputsVisibility,
|
||||
} = useConfig()
|
||||
|
||||
const behaviorSettings = createMemo(() =>
|
||||
getBehaviorSettings({
|
||||
preferences,
|
||||
updatePreferences,
|
||||
toggleShowThinkingBlocks,
|
||||
toggleKeyboardShortcutHints,
|
||||
toggleShowTimelineTools,
|
||||
toggleUsageMetrics,
|
||||
toggleAutoCleanupBlankSessions,
|
||||
togglePromptSubmitOnEnter,
|
||||
setDiffViewMode,
|
||||
setToolOutputExpansion,
|
||||
setDiagnosticsExpansion,
|
||||
setThinkingBlocksExpansion,
|
||||
setToolInputsVisibility,
|
||||
}),
|
||||
)
|
||||
|
||||
const [overrides, setOverrides] = createSignal<Map<string, unknown>>(new Map())
|
||||
|
||||
const setOverride = (id: string, value: unknown) => {
|
||||
setOverrides((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(id, value)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
const current = overrides()
|
||||
if (current.size === 0) return
|
||||
|
||||
const prefs = preferences()
|
||||
const settings = behaviorSettings()
|
||||
|
||||
let changed = false
|
||||
const next = new Map(current)
|
||||
for (const setting of settings) {
|
||||
if (!next.has(setting.id)) continue
|
||||
const overrideValue = next.get(setting.id)
|
||||
const actualValue = setting.get(prefs)
|
||||
if (Object.is(actualValue, overrideValue)) {
|
||||
next.delete(setting.id)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
setOverrides(next)
|
||||
}
|
||||
})
|
||||
|
||||
const readSettingValue = (setting: BehaviorSetting) => {
|
||||
const current = overrides()
|
||||
if (current.has(setting.id)) return current.get(setting.id)
|
||||
return setting.get(preferences())
|
||||
}
|
||||
|
||||
type SelectOption = { value: string; label: string }
|
||||
|
||||
const BehaviorRow: Component<{ setting: BehaviorSetting }> = (props) => {
|
||||
const setting = props.setting
|
||||
const disabled = createMemo(() => (setting.disabled ? Boolean(setting.disabled()) : false))
|
||||
|
||||
if (setting.kind === "toggle") {
|
||||
const options = createMemo<SelectOption[]>(() => [
|
||||
{ value: "true", label: t("settings.common.enabled") },
|
||||
{ value: "false", label: t("settings.common.disabled") },
|
||||
])
|
||||
const currentValue = createMemo(() => String(Boolean(readSettingValue(setting))))
|
||||
const selectedOption = createMemo(() => options().find((opt) => opt.value === currentValue()))
|
||||
|
||||
return (
|
||||
<div class={`settings-toggle-row ${disabled() ? "opacity-60" : ""}`}>
|
||||
<div>
|
||||
<div class="settings-toggle-title">{t(setting.titleKey)}</div>
|
||||
<div class="settings-toggle-caption">{t(setting.subtitleKey)}</div>
|
||||
</div>
|
||||
<Select<SelectOption>
|
||||
value={selectedOption()}
|
||||
onChange={(opt) => {
|
||||
if (!opt) return
|
||||
const next = opt.value === "true"
|
||||
setOverride(setting.id, next)
|
||||
setting.set(next)
|
||||
}}
|
||||
options={options()}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
disabled={disabled()}
|
||||
itemComponent={(itemProps) => (
|
||||
<Select.Item item={itemProps.item} class="selector-option">
|
||||
<Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.label}</Select.ItemLabel>
|
||||
</Select.Item>
|
||||
)}
|
||||
>
|
||||
<Select.Trigger class="selector-trigger" aria-label={t(setting.titleKey)}>
|
||||
<div class="flex-1 min-w-0">
|
||||
<Select.Value<SelectOption>>
|
||||
{(state) => (
|
||||
<span class="selector-trigger-primary selector-trigger-primary--align-left">
|
||||
{state.selectedOption()?.label}
|
||||
</span>
|
||||
)}
|
||||
</Select.Value>
|
||||
</div>
|
||||
<Select.Icon class="selector-trigger-icon">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Portal>
|
||||
<Select.Content class="selector-popover">
|
||||
<Select.Listbox class="selector-listbox" />
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const enumSetting = setting as Extract<BehaviorSetting, { kind: "enum" }>
|
||||
const options = createMemo<SelectOption[]>(() =>
|
||||
enumSetting.options.map((opt: { value: string; labelKey: string }) => ({
|
||||
value: String(opt.value),
|
||||
label: t(opt.labelKey),
|
||||
})),
|
||||
)
|
||||
const currentValue = createMemo(() => String(readSettingValue(setting) ?? ""))
|
||||
const selectedOption = createMemo(() => options().find((opt) => opt.value === currentValue()))
|
||||
|
||||
return (
|
||||
<div class={`settings-toggle-row ${disabled() ? "opacity-60" : ""}`}>
|
||||
<div>
|
||||
<div class="settings-toggle-title">{t(setting.titleKey)}</div>
|
||||
<div class="settings-toggle-caption">{t(setting.subtitleKey)}</div>
|
||||
</div>
|
||||
<Select<SelectOption>
|
||||
value={selectedOption()}
|
||||
onChange={(opt) => {
|
||||
if (!opt) return
|
||||
setOverride(setting.id, opt.value)
|
||||
enumSetting.set(opt.value as any)
|
||||
}}
|
||||
options={options()}
|
||||
optionValue="value"
|
||||
optionTextValue="label"
|
||||
disabled={disabled()}
|
||||
itemComponent={(itemProps) => (
|
||||
<Select.Item item={itemProps.item} class="selector-option">
|
||||
<Select.ItemLabel class="selector-option-label">{itemProps.item.rawValue.label}</Select.ItemLabel>
|
||||
</Select.Item>
|
||||
)}
|
||||
>
|
||||
<Select.Trigger class="selector-trigger" aria-label={t(setting.titleKey)}>
|
||||
<div class="flex-1 min-w-0">
|
||||
<Select.Value<SelectOption>>
|
||||
{(state) => (
|
||||
<span class="selector-trigger-primary selector-trigger-primary--align-left">
|
||||
{state.selectedOption()?.label}
|
||||
</span>
|
||||
)}
|
||||
</Select.Value>
|
||||
</div>
|
||||
<Select.Icon class="selector-trigger-icon">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Portal>
|
||||
<Select.Content class="selector-popover">
|
||||
<Select.Listbox class="selector-listbox" />
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const modeLabel = (mode: ThemeMode) => {
|
||||
if (mode === "system") return t("theme.mode.system")
|
||||
@@ -54,6 +251,20 @@ export const AppearanceSettingsSection: Component = () => {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("settings.appearance.behavior.title")}</h3>
|
||||
<p class="settings-card-subtitle">{t("settings.appearance.behavior.subtitle")}</p>
|
||||
</div>
|
||||
<span class="settings-scope-badge">{t("settings.scope.device")}</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-stack">
|
||||
<For each={behaviorSettings()}>{(setting) => <BehaviorRow setting={setting} />}</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user