Skip to content

Commit bccf71e

Browse files
Apply PR #40845: feat(app): redesign non-modal settings
2 parents 70d7564 + 2f73f42 commit bccf71e

17 files changed

Lines changed: 794 additions & 341 deletions

packages/app/src/components/dialog-connect-provider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ function ProviderConnection(props: {
389389
const settings = useSettings()
390390
const newLayout = settings.general.newLayoutDesigns
391391
const providers = useProviders(() => props.directory?.())
392-
const directory = () => props.directory?.() ?? decode64(params.dir)
392+
const directory = () => (props.directory ? props.directory() : decode64(params.dir))
393393
const location = () => {
394394
const value = directory()
395395
return value ? { directory: value } : undefined

packages/app/src/components/settings-server-picker.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ export function SettingsServerScope(props: ParentProps) {
1717

1818
return (
1919
<Show when={settings.general.newLayoutDesigns()} fallback={props.children}>
20-
<Show when={global.settings.server.selected()}>
21-
{(server) => <SettingsServerDataProviders server={server()}>{props.children}</SettingsServerDataProviders>}
20+
<Show when={global.settings.server.selected()} keyed>
21+
{(server) => <SettingsServerDataProviders server={server}>{props.children}</SettingsServerDataProviders>}
2222
</Show>
2323
</Show>
2424
)
@@ -31,7 +31,7 @@ function SettingsServerDataProviders(props: ParentProps<{ server: ServerConnecti
3131
return (
3232
<QueryClientProvider client={serverCtx().queryClient}>
3333
<ServerSDKProvider server={() => props.server}>
34-
<ServerSyncProvider>
34+
<ServerSyncProvider server={() => props.server}>
3535
<ModelsProvider>{props.children}</ModelsProvider>
3636
</ServerSyncProvider>
3737
</ServerSDKProvider>
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { Component, Show, createMemo, createResource } from "solid-js"
2+
import { SelectV2 } from "@opencode-ai/ui/v2/select-v2"
3+
import { Switch } from "@opencode-ai/ui/v2/switch-v2"
4+
import { TextInputV2 } from "@opencode-ai/ui/v2/text-input-v2"
5+
import { useLanguage } from "@/context/language"
6+
import { usePlatform } from "@/context/platform"
7+
import { ExternalLink } from "../external-link"
8+
import { SettingsListV2 } from "./parts/list"
9+
import { SettingsRowV2 } from "./parts/row"
10+
import { createAppearanceSettingsController, type AppearanceSettingsController } from "./general-controllers"
11+
import "./settings-v2.css"
12+
13+
const schemeOptions: ("system" | "light" | "dark")[] = ["system", "light", "dark"]
14+
const fontSettings = {
15+
ui: {
16+
action: "settings-ui-font",
17+
title: "settings.general.row.uiFont.title",
18+
description: "settings.general.row.uiFont.description",
19+
font: "ui",
20+
input: "setUI",
21+
},
22+
code: {
23+
action: "settings-code-font",
24+
title: "settings.general.row.font.title",
25+
description: "settings.general.row.font.description",
26+
font: "code",
27+
input: "setCode",
28+
},
29+
terminal: {
30+
action: "settings-terminal-font",
31+
title: "settings.general.row.terminalFont.title",
32+
description: "settings.general.row.terminalFont.description",
33+
font: "terminal",
34+
input: "setTerminal",
35+
},
36+
} as const
37+
38+
const FontSetting: Component<{
39+
kind: "ui" | "code" | "terminal"
40+
fonts: AppearanceSettingsController["fonts"]
41+
}> = (props) => {
42+
const language = useLanguage()
43+
const config = () => fontSettings[props.kind]
44+
return (
45+
<SettingsRowV2 title={language.t(config().title)} description={language.t(config().description)}>
46+
<div class="w-full sm:w-[220px]">
47+
<TextInputV2
48+
data-action={config().action}
49+
type="text"
50+
appearance="base"
51+
value={props.fonts[config().font]().value}
52+
onInput={(event) => props.fonts[config().input](event.currentTarget.value)}
53+
placeholder={props.fonts[config().font]().placeholder}
54+
spellcheck={false}
55+
autocorrect="off"
56+
autocomplete="off"
57+
autocapitalize="off"
58+
aria-label={language.t(config().title)}
59+
style={{ "font-family": props.fonts[config().font]().family }}
60+
/>
61+
</div>
62+
</SettingsRowV2>
63+
)
64+
}
65+
66+
export const SettingsAppearanceV2: Component = () => {
67+
const language = useLanguage()
68+
const platform = usePlatform()
69+
const appearance = createAppearanceSettingsController()
70+
const desktop = createMemo(() => platform.platform === "desktop")
71+
72+
const [pinchZoom, { mutate: setPinchZoom }] = createResource(
73+
() => desktop() && "getPinchZoomEnabled" in platform,
74+
() => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false),
75+
{ initialValue: false },
76+
)
77+
78+
const onPinchZoomChange = (checked: boolean) => {
79+
setPinchZoom(checked)
80+
const update = platform.setPinchZoomEnabled?.(checked)
81+
if (!update) return
82+
void update.catch(() => setPinchZoom(!checked))
83+
}
84+
85+
return (
86+
<>
87+
<div class="settings-v2-tab-header">
88+
<div class="settings-v2-tab-header-row">
89+
<h2 class="settings-v2-tab-title">{language.t("settings.general.section.appearance")}</h2>
90+
</div>
91+
</div>
92+
93+
<div class="settings-v2-tab-body">
94+
<div class="settings-v2-section">
95+
<SettingsListV2>
96+
<SettingsRowV2
97+
title={language.t("settings.general.row.colorScheme.title")}
98+
description={language.t("settings.general.row.colorScheme.description")}
99+
>
100+
<SelectV2
101+
appearance="inline"
102+
data-action="settings-color-scheme"
103+
options={schemeOptions}
104+
current={schemeOptions.find((option) => option === appearance.scheme.current())}
105+
placement="bottom-end"
106+
gutter={6}
107+
label={(option) => {
108+
if (option === "system") return language.t("theme.scheme.system")
109+
if (option === "light") return language.t("theme.scheme.light")
110+
return language.t("theme.scheme.dark")
111+
}}
112+
onSelect={(option) => option && appearance.scheme.select(option)}
113+
/>
114+
</SettingsRowV2>
115+
116+
<SettingsRowV2
117+
title={language.t("settings.general.row.theme.title")}
118+
description={
119+
<>
120+
{language.t("settings.general.row.theme.description")}{" "}
121+
<ExternalLink class="settings-v2-link" href="https://opencode.ai/docs/themes/">
122+
{language.t("common.learnMore")}
123+
</ExternalLink>
124+
</>
125+
}
126+
>
127+
<SelectV2
128+
appearance="inline"
129+
data-action="settings-theme"
130+
options={appearance.theme.options()}
131+
current={appearance.theme.current()}
132+
placement="bottom-end"
133+
gutter={6}
134+
value={(option) => option.id}
135+
label={(option) => option.name}
136+
onSelect={appearance.theme.select}
137+
/>
138+
</SettingsRowV2>
139+
140+
<FontSetting kind="ui" fonts={appearance.fonts} />
141+
<FontSetting kind="code" fonts={appearance.fonts} />
142+
<FontSetting kind="terminal" fonts={appearance.fonts} />
143+
</SettingsListV2>
144+
</div>
145+
146+
<Show when={desktop()}>
147+
<div class="settings-v2-section">
148+
<h3 class="settings-v2-section-title">{language.t("settings.general.section.display")}</h3>
149+
<SettingsListV2>
150+
<SettingsRowV2
151+
title={language.t("settings.general.row.pinchZoom.title")}
152+
description={language.t("settings.general.row.pinchZoom.description")}
153+
>
154+
<div data-action="settings-pinch-zoom">
155+
<Switch checked={pinchZoom.latest} onChange={onPinchZoomChange} />
156+
</div>
157+
</SettingsRowV2>
158+
</SettingsListV2>
159+
</div>
160+
</Show>
161+
</div>
162+
</>
163+
)
164+
}

packages/app/src/components/settings-v2/dialog-settings-v2.tsx

Lines changed: 86 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ import { Icon } from "@opencode-ai/ui/icon"
55
import { useLanguage } from "@/context/language"
66
import { usePlatform } from "@/context/platform"
77
import { SettingsGeneralV2 } from "./general"
8+
import { SettingsAppearanceV2 } from "./appearance"
89
import { SettingsKeybinds } from "../settings-keybinds"
10+
import { SettingsNotificationsV2 } from "./notifications"
911
import { SettingsProvidersV2 } from "./providers"
1012
import { SettingsModelsV2 } from "./models"
11-
import "./settings-v2.css"
1213
import { SettingsServersV2 } from "./servers"
1314
import { SettingsWorkspacesV2 } from "./workspaces"
15+
import { SettingsProjectsV2 } from "./projects"
16+
import { SettingsExtensionsV2 } from "./extensions"
17+
import { SettingsServerScope } from "../settings-server-picker"
1418
import { useDialog } from "@opencode-ai/ui/context/dialog"
19+
import { useLayout } from "@/context/layout"
1520
import { useTabs } from "@/context/tabs"
1621
import { useServerSync } from "@/context/server-sync"
22+
import { useGlobal } from "@/context/global"
23+
import "./settings-v2.css"
1724

1825
export const DialogSettings: Component<{
1926
sessionID?: string
@@ -22,16 +29,21 @@ export const DialogSettings: Component<{
2229
const language = useLanguage()
2330
const platform = usePlatform()
2431
const dialog = useDialog()
32+
const layout = useLayout()
2533
const tabs = useTabs()
2634
const serverSync = useServerSync()
35+
const global = useGlobal()
2736
const [tab, setTab] = createSignal(props.defaultValue ?? "general")
2837
const directory = createMemo(() => {
29-
const active = tabs.active()
30-
if (active?.type === "draft") return active.directory
31-
if (active?.type === "session") {
32-
const session = serverSync().session.get(active.sessionId)
33-
if (session) return session.directory
38+
const server = global.settings.server.selected()
39+
if (!server || serverSync() !== global.ensureServerCtx(server).sync) return
40+
const route = layout.route()
41+
if (route.type === "dir-new-sesssion") return route.dir
42+
if (route.type === "draft") {
43+
const draft = tabs.store.find((item) => item.type === "draft" && item.draftID === route.draftID)
44+
return draft?.type === "draft" ? draft.directory : undefined
3445
}
46+
if (route.type === "session") return serverSync().session.get(route.sessionId)?.directory
3547
return undefined
3648
})
3749

@@ -50,54 +62,73 @@ export const DialogSettings: Component<{
5062
>
5163
<TabsV2.List>
5264
<div class="flex flex-col justify-between h-full w-full">
53-
<div class="flex flex-col gap-3 w-full">
54-
<div class="flex flex-col gap-3">
55-
<div class="flex flex-col gap-1.5">
56-
<TabsV2.SectionTitle>{language.t("settings.section.desktop")}</TabsV2.SectionTitle>
57-
<div class="flex flex-col gap-1.5 w-full">
58-
<TabsV2.Trigger value="general">
59-
<Icon name="sliders" size="small" />
60-
{language.t("settings.tab.general")}
61-
</TabsV2.Trigger>
62-
<TabsV2.Trigger value="shortcuts">
63-
<Icon name="keyboard" size="small" />
64-
{language.t("settings.tab.shortcuts")}
65-
</TabsV2.Trigger>
66-
</div>
67-
</div>
65+
<div class="flex flex-col gap-4 w-full">
66+
<div class="flex flex-col gap-1 w-full">
67+
<TabsV2.Trigger value="general">
68+
<Icon name="sliders" />
69+
{language.t("settings.tab.preferences")}
70+
</TabsV2.Trigger>
71+
<TabsV2.Trigger value="appearance">
72+
<Icon name="appearance" />
73+
{language.t("settings.general.section.appearance")}
74+
</TabsV2.Trigger>
75+
<TabsV2.Trigger value="notifications">
76+
<Icon name="notifications" />
77+
{language.t("settings.tab.notifications")}
78+
</TabsV2.Trigger>
79+
<TabsV2.Trigger value="shortcuts">
80+
<Icon name="keyboard" />
81+
{language.t("settings.tab.shortcuts")}
82+
</TabsV2.Trigger>
83+
</div>
84+
85+
<div class="flex flex-col gap-1 w-full">
86+
<TabsV2.Trigger value="workspaces">
87+
<Icon name="workspace-isolated" />
88+
{language.t("settings.tab.workspaces")}
89+
</TabsV2.Trigger>
90+
<TabsV2.Trigger value="servers">
91+
<Icon name="server" />
92+
{language.t("status.popover.tab.servers")}
93+
</TabsV2.Trigger>
94+
<TabsV2.Trigger value="projects">
95+
<Icon name="folder" />
96+
{language.t("settings.tab.projects")}
97+
</TabsV2.Trigger>
98+
</div>
6899

69-
<div class="flex flex-col gap-1.5">
70-
<TabsV2.SectionTitle>{language.t("settings.section.server")}</TabsV2.SectionTitle>
71-
<div class="flex flex-col gap-1.5 w-full">
72-
<TabsV2.Trigger value="workspaces">
73-
<Icon name="workspace-isolated" size="small" />
74-
{language.t("settings.tab.workspaces")}
75-
</TabsV2.Trigger>
76-
<TabsV2.Trigger value="servers">
77-
<Icon name="server" size="small" />
78-
{language.t("status.popover.tab.servers")}
79-
</TabsV2.Trigger>
80-
<TabsV2.Trigger value="providers">
81-
<Icon name="providers" size="small" />
82-
{language.t("settings.providers.title")}
83-
</TabsV2.Trigger>
84-
<TabsV2.Trigger value="models">
85-
<Icon name="models" size="small" />
86-
{language.t("settings.models.title")}
87-
</TabsV2.Trigger>
88-
</div>
89-
</div>
100+
<div class="flex flex-col gap-1 w-full">
101+
<TabsV2.Trigger value="providers">
102+
<Icon name="providers" />
103+
{language.t("settings.providers.title")}
104+
</TabsV2.Trigger>
105+
<TabsV2.Trigger value="models">
106+
<Icon name="models" />
107+
{language.t("settings.models.title")}
108+
</TabsV2.Trigger>
109+
<TabsV2.Trigger value="extensions">
110+
<Icon name="extensions" />
111+
{language.t("settings.tab.extensions")}
112+
</TabsV2.Trigger>
90113
</div>
91114
</div>
115+
92116
<div class="settings-v2-nav-footer">
93117
<span>{language.t("app.name.desktop")}</span>
94118
<span>v{platform.version}</span>
95119
</div>
96120
</div>
97121
</TabsV2.List>
122+
98123
<TabsV2.Content value="general" class="settings-v2-panel">
99124
<SettingsGeneralV2 sessionID={props.sessionID} />
100125
</TabsV2.Content>
126+
<TabsV2.Content value="appearance" class="settings-v2-panel">
127+
<SettingsAppearanceV2 />
128+
</TabsV2.Content>
129+
<TabsV2.Content value="notifications" class="settings-v2-panel">
130+
<SettingsNotificationsV2 />
131+
</TabsV2.Content>
101132
<TabsV2.Content value="shortcuts" class="settings-v2-panel">
102133
<SettingsKeybinds v2 />
103134
</TabsV2.Content>
@@ -107,12 +138,20 @@ export const DialogSettings: Component<{
107138
<TabsV2.Content value="servers" class="settings-v2-panel">
108139
<SettingsServersV2 />
109140
</TabsV2.Content>
110-
<TabsV2.Content value="providers" class="settings-v2-panel">
111-
<SettingsProvidersV2 directory={directory} onBack={showProviders} />
112-
</TabsV2.Content>
113-
<TabsV2.Content value="models" class="settings-v2-panel">
114-
<SettingsModelsV2 />
141+
<TabsV2.Content value="projects" class="settings-v2-panel">
142+
<SettingsProjectsV2 />
115143
</TabsV2.Content>
144+
<SettingsServerScope>
145+
<TabsV2.Content value="providers" class="settings-v2-panel">
146+
<SettingsProvidersV2 directory={directory} onBack={showProviders} />
147+
</TabsV2.Content>
148+
<TabsV2.Content value="models" class="settings-v2-panel">
149+
<SettingsModelsV2 />
150+
</TabsV2.Content>
151+
<TabsV2.Content value="extensions" class="settings-v2-panel">
152+
<SettingsExtensionsV2 />
153+
</TabsV2.Content>
154+
</SettingsServerScope>
116155
</TabsV2>
117156
</Dialog>
118157
)

0 commit comments

Comments
 (0)