From 0d9da40102325ac38c4324a5bcd4a30adb38a193 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Wed, 11 Mar 2026 10:10:58 +0000 Subject: [PATCH] feat(ui): add unified settings screen --- packages/ui/src/App.tsx | 19 +- .../src/components/folder-selection-view.tsx | 58 +- packages/ui/src/components/instance-tabs.tsx | 56 +- .../ui/src/components/settings-screen.tsx | 109 ++++ .../settings/appearance-settings-section.tsx | 59 ++ .../notifications-settings-section.tsx | 227 ++++++++ .../settings/opencode-settings-section.tsx | 52 ++ .../remote-access-settings-section.tsx | 401 ++++++++++++++ packages/ui/src/lib/i18n/messages/en/app.ts | 4 +- .../lib/i18n/messages/en/folderSelection.ts | 1 + .../ui/src/lib/i18n/messages/en/settings.ts | 57 ++ packages/ui/src/lib/i18n/messages/es/app.ts | 4 +- .../lib/i18n/messages/es/folderSelection.ts | 1 + .../ui/src/lib/i18n/messages/es/settings.ts | 57 ++ packages/ui/src/lib/i18n/messages/fr/app.ts | 4 +- .../lib/i18n/messages/fr/folderSelection.ts | 1 + .../ui/src/lib/i18n/messages/fr/settings.ts | 57 ++ packages/ui/src/lib/i18n/messages/ja/app.ts | 4 +- .../lib/i18n/messages/ja/folderSelection.ts | 1 + .../ui/src/lib/i18n/messages/ja/settings.ts | 57 ++ packages/ui/src/lib/i18n/messages/ru/app.ts | 4 +- .../lib/i18n/messages/ru/folderSelection.ts | 1 + .../ui/src/lib/i18n/messages/ru/settings.ts | 57 ++ .../ui/src/lib/i18n/messages/zh-Hans/app.ts | 4 +- .../i18n/messages/zh-Hans/folderSelection.ts | 1 + .../src/lib/i18n/messages/zh-Hans/settings.ts | 57 ++ packages/ui/src/stores/settings-screen.ts | 17 + .../src/styles/components/settings-screen.css | 516 ++++++++++++++++++ packages/ui/src/styles/controls.css | 1 + packages/ui/src/styles/tokens.css | 3 + 30 files changed, 1802 insertions(+), 88 deletions(-) create mode 100644 packages/ui/src/components/settings-screen.tsx create mode 100644 packages/ui/src/components/settings/appearance-settings-section.tsx create mode 100644 packages/ui/src/components/settings/notifications-settings-section.tsx create mode 100644 packages/ui/src/components/settings/opencode-settings-section.tsx create mode 100644 packages/ui/src/components/settings/remote-access-settings-section.tsx create mode 100644 packages/ui/src/stores/settings-screen.ts create mode 100644 packages/ui/src/styles/components/settings-screen.css diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index cee2ab06..4420af92 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -9,7 +9,7 @@ import { showConfirmDialog } from "./stores/alerts" import InstanceTabs from "./components/instance-tabs" import InstanceDisconnectedModal from "./components/instance-disconnected-modal" import InstanceShell from "./components/instance/instance-shell2" -import { RemoteAccessOverlay } from "./components/remote-access-overlay" +import { SettingsScreen } from "./components/settings-screen" import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context" import { initMarkdown } from "./lib/markdown" import { initGithubStars } from "./stores/github-stars" @@ -54,6 +54,7 @@ import { } from "./stores/sessions" import { getInstanceSessionIndicatorStatus } from "./stores/session-status" +import { openSettings } from "./stores/settings-screen" const log = getLogger("actions") @@ -77,8 +78,6 @@ const App: Component = () => { setToolInputsVisibility, } = useConfig() const [escapeInDebounce, setEscapeInDebounce] = createSignal(false) - const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = createSignal(false) - const [remoteAccessOpen, setRemoteAccessOpen] = createSignal(false) const [instanceTabBarHeight, setInstanceTabBarHeight] = createSignal(0) const phoneQuery = useMediaQuery("(max-width: 767px)") @@ -252,7 +251,6 @@ const App: Component = () => { clearLaunchError() const instanceId = await createInstance(folderPath, selectedBinary) setShowFolderSelection(false) - setIsAdvancedSettingsOpen(false) log.info("Created instance", { instanceId, @@ -274,7 +272,7 @@ const App: Component = () => { function handleLaunchErrorAdvanced() { clearLaunchError() - setIsAdvancedSettingsOpen(true) + openSettings("opencode") } function handleNewInstanceRequest() { @@ -487,7 +485,6 @@ const App: Component = () => { onSelect={setActiveInstanceId} onClose={handleCloseInstance} onNew={handleNewInstanceRequest} - onOpenRemoteAccess={() => setRemoteAccessOpen(true)} /> @@ -533,10 +530,6 @@ const App: Component = () => { setIsAdvancedSettingsOpen(true)} - onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} - onOpenRemoteAccess={() => setRemoteAccessOpen(true)} /> @@ -546,12 +539,8 @@ const App: Component = () => { setIsAdvancedSettingsOpen(true)} - onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)} onClose={() => { setShowFolderSelection(false) - setIsAdvancedSettingsOpen(false) clearLaunchError() }} /> @@ -559,7 +548,7 @@ const App: Component = () => { - setRemoteAccessOpen(false)} /> + diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index 66362a8b..ad833fed 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -2,10 +2,8 @@ import { Select } from "@kobalte/core/select" import { Component, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js" import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star, Languages, ChevronDown, X } from "lucide-solid" import { useConfig } from "../stores/preferences" -import AdvancedSettingsModal from "./advanced-settings-modal" import DirectoryBrowserDialog from "./directory-browser-dialog" import Kbd from "./kbd" -import { ThemeModeToggle } from "./theme-mode-toggle" import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions" import { useFolderDrop } from "../lib/hooks/use-folder-drop" import VersionPill from "./version-pill" @@ -14,6 +12,7 @@ import { githubStars } from "../stores/github-stars" import { formatCompactCount } from "../lib/formatters" import { useI18n, type Locale } from "../lib/i18n" import { showAlertDialog } from "../stores/alerts" +import { openSettings, settingsOpen } from "../stores/settings-screen" const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href @@ -21,15 +20,11 @@ const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).h interface FolderSelectionViewProps { onSelectFolder: (folder: string, binaryPath?: string) => void isLoading?: boolean - advancedSettingsOpen?: boolean - onAdvancedSettingsOpen?: () => void - onAdvancedSettingsClose?: () => void - onOpenRemoteAccess?: () => void onClose?: () => void } const FolderSelectionView: Component = (props) => { - const { recentFolders, removeRecentFolder, preferences, updatePreferences, serverSettings, updateLastUsedBinary } = useConfig() + const { recentFolders, removeRecentFolder, preferences, updatePreferences, serverSettings } = useConfig() const { t, locale } = useI18n() const [selectedIndex, setSelectedIndex] = createSignal(0) const [focusMode, setFocusMode] = createSignal<"recent" | "new" | null>("recent") @@ -196,7 +191,7 @@ const FolderSelectionView: Component = (props) => { }) function dropTargetBlocked() { - return isLoading() || isFolderBrowserOpen() || Boolean(props.advancedSettingsOpen) + return isLoading() || isFolderBrowserOpen() || settingsOpen() } function showInvalidFolderDropAlert() { @@ -264,11 +259,6 @@ const FolderSelectionView: Component = (props) => { handleFolderSelect(path) } - function handleBinaryChange(binary: string) { - - setSelectedBinary(binary) - } - function handleRemove(path: string, e?: Event) { if (isLoading()) return e?.stopPropagation() @@ -398,16 +388,24 @@ const FolderSelectionView: Component = (props) => {
- - - - + +
- {/* Advanced settings section */} + {/* OpenCode settings section */}
- @@ -661,14 +659,6 @@ const FolderSelectionView: Component = (props) => {
- props.onAdvancedSettingsClose?.()} - selectedBinary={selectedBinary()} - onBinaryChange={handleBinaryChange} - isLoading={props.isLoading} - /> - @@ -17,13 +16,11 @@ interface InstanceTabsProps { onSelect: (instanceId: string) => void onClose: (instanceId: string) => void onNew: () => void - onOpenRemoteAccess?: () => void } const InstanceTabs: Component = (props) => { const { t } = useI18n() const { preferences } = useConfig() - const [notificationsOpen, setNotificationsOpen] = createSignal(false) const notificationsSupported = createMemo(() => isOsNotificationSupportedSync()) const notificationsEnabled = createMemo(() => Boolean(preferences().osNotificationsEnabled)) @@ -33,8 +30,10 @@ const InstanceTabs: Component = (props) => { }) const notificationTitle = createMemo(() => { - if (!notificationsSupported()) return "Notifications unsupported" - return notificationsEnabled() ? "Notifications enabled" : "Notifications disabled" + if (!notificationsSupported()) return t("settings.notifications.status.unsupported") + return notificationsEnabled() + ? t("settings.notifications.status.enabled") + : t("settings.notifications.status.disabled") }) return ( @@ -72,32 +71,35 @@ const InstanceTabs: Component = (props) => { /> - + - - - - + - - setNotificationsOpen(false)} /> ) diff --git a/packages/ui/src/components/settings-screen.tsx b/packages/ui/src/components/settings-screen.tsx new file mode 100644 index 00000000..48fe45d1 --- /dev/null +++ b/packages/ui/src/components/settings-screen.tsx @@ -0,0 +1,109 @@ +import { Dialog } from "@kobalte/core/dialog" +import { Settings, Bell, MonitorUp, Paintbrush, Terminal, X } from "lucide-solid" +import { createMemo, For, type Component } from "solid-js" +import { useI18n } from "../lib/i18n" +import { + activeSettingsSection, + closeSettings, + settingsOpen, + setActiveSettingsSection, + type SettingsSectionId, +} from "../stores/settings-screen" +import { AppearanceSettingsSection } from "./settings/appearance-settings-section" +import { NotificationsSettingsSection } from "./settings/notifications-settings-section" +import { OpenCodeSettingsSection } from "./settings/opencode-settings-section" +import { RemoteAccessSettingsSection } from "./settings/remote-access-settings-section" + +export const SettingsScreen: Component = () => { + const { t } = useI18n() + + const sections = createMemo(() => [ + { id: "appearance" as SettingsSectionId, icon: Paintbrush, label: t("settings.nav.appearance") }, + { id: "notifications" as SettingsSectionId, icon: Bell, label: t("settings.nav.notifications") }, + { id: "remote" as SettingsSectionId, icon: MonitorUp, label: t("settings.nav.remote") }, + { id: "opencode" as SettingsSectionId, icon: Terminal, label: t("settings.nav.opencode") }, + ]) + + const renderSection = () => { + switch (activeSettingsSection()) { + case "notifications": + return + case "remote": + return + case "opencode": + return + case "appearance": + default: + return + } + } + + return ( + !open && closeSettings()}> + + +
+ + {t("settings.title")} + {t("settings.description")} + + + +
+
+
+

{t("settings.content.eyebrow")}

+

+ {sections().find((section) => section.id === activeSettingsSection())?.label} +

+
+ +
+ +
{renderSection()}
+
+
+
+
+
+ ) +} diff --git a/packages/ui/src/components/settings/appearance-settings-section.tsx b/packages/ui/src/components/settings/appearance-settings-section.tsx new file mode 100644 index 00000000..e8e5df25 --- /dev/null +++ b/packages/ui/src/components/settings/appearance-settings-section.tsx @@ -0,0 +1,59 @@ +import type { Component } from "solid-js" +import { Check, Laptop, Moon, Sun } from "lucide-solid" +import { useI18n } from "../../lib/i18n" +import { useTheme, type ThemeMode } from "../../lib/theme" + +const themeModeOptions: Array<{ value: ThemeMode; icon: typeof Laptop }> = [ + { value: "system", icon: Laptop }, + { value: "light", icon: Sun }, + { value: "dark", icon: Moon }, +] + +export const AppearanceSettingsSection: Component = () => { + const { t } = useI18n() + const { themeMode, setThemeMode } = useTheme() + + const modeLabel = (mode: ThemeMode) => { + if (mode === "system") return t("theme.mode.system") + if (mode === "light") return t("theme.mode.light") + return t("theme.mode.dark") + } + + return ( +
+
+
+
+

{t("settings.appearance.theme.title")}

+

{t("settings.appearance.theme.subtitle")}

+
+ {t("settings.scope.device")} +
+
+ {themeModeOptions.map((option) => { + const Icon = option.icon + return ( + + ) + })} +
+
+
+ ) +} diff --git a/packages/ui/src/components/settings/notifications-settings-section.tsx b/packages/ui/src/components/settings/notifications-settings-section.tsx new file mode 100644 index 00000000..3dc01eb4 --- /dev/null +++ b/packages/ui/src/components/settings/notifications-settings-section.tsx @@ -0,0 +1,227 @@ +import { Show, createEffect, createResource, type Component } from "solid-js" +import { Bell } from "lucide-solid" +import { showToastNotification } from "../../lib/notifications" +import { + getOsNotificationCapability, + requestOsNotificationPermission, + type OsNotificationPermission, +} from "../../lib/os-notifications" +import { useConfig } from "../../stores/preferences" +import { useI18n } from "../../lib/i18n" + +function formatPermissionLabel(permission: OsNotificationPermission, t: ReturnType["t"]): string { + switch (permission) { + case "granted": + return t("settings.notifications.permission.granted") + case "denied": + return t("settings.notifications.permission.denied") + case "default": + return t("settings.notifications.permission.default") + case "unsupported": + return t("settings.notifications.permission.unsupported") + default: + return String(permission) + } +} + +export const NotificationsSettingsSection: Component = () => { + const { t } = useI18n() + const { preferences, updatePreferences } = useConfig() + const [capability, { refetch }] = createResource(() => getOsNotificationCapability()) + + createEffect(() => { + void refetch() + }) + + const handleEnableToggle = async (enabled: boolean) => { + if (!enabled) { + updatePreferences({ osNotificationsEnabled: false }) + return + } + + const cap = capability() + if (cap && !cap.supported) { + showToastNotification({ + title: t("settings.section.notifications.title"), + message: cap.info ?? t("settings.notifications.messages.unsupportedEnvironment"), + variant: "warning", + }) + updatePreferences({ osNotificationsEnabled: false }) + return + } + + const permission = await requestOsNotificationPermission() + if (permission !== "granted") { + showToastNotification({ + title: t("settings.section.notifications.title"), + message: + permission === "denied" + ? t("settings.notifications.messages.permissionDenied") + : t("settings.notifications.messages.permissionNotGranted"), + variant: "warning", + }) + updatePreferences({ osNotificationsEnabled: false }) + return + } + + updatePreferences({ osNotificationsEnabled: true }) + void refetch() + } + + const handleRequestPermission = async () => { + const cap = capability() + if (cap && !cap.supported) { + showToastNotification({ + title: t("settings.section.notifications.title"), + message: cap.info ?? t("settings.notifications.messages.unsupportedGeneral"), + variant: "warning", + }) + return + } + + const permission = await requestOsNotificationPermission() + if (permission === "granted") { + showToastNotification({ + title: t("settings.section.notifications.title"), + message: t("settings.notifications.messages.permissionGranted"), + variant: "success", + duration: 6000, + }) + void refetch() + return + } + + showToastNotification({ + title: t("settings.section.notifications.title"), + message: + permission === "denied" + ? t("settings.notifications.messages.permissionRequestDenied") + : t("settings.notifications.messages.permissionNotGranted"), + variant: "warning", + }) + void refetch() + } + + const supported = () => capability()?.supported ?? false + const permissionLabel = () => formatPermissionLabel(capability()?.permission ?? "unsupported", t) + const infoMessage = () => capability()?.info + + return ( +
+
+
+
+ +
+

{t("settings.notifications.sessionStatus.title")}

+

{t("settings.notifications.sessionStatus.subtitle")}

+
+
+ {t("settings.scope.device")} +
+ +
+
+
+
{t("settings.notifications.enable.title")}
+
+ {t("settings.notifications.enable.permission", { permission: permissionLabel() })} +
+
+ +
+ + +
+
+
{t("settings.notifications.requestPermission.title")}
+
{t("settings.notifications.requestPermission.subtitle")}
+
+ +
+
+ +
+
+
{t("settings.notifications.allowVisible.title")}
+
{t("settings.notifications.allowVisible.subtitle")}
+
+ +
+ + +
{infoMessage()}
+
+ + +
{t("settings.notifications.unsupportedNote")}
+
+
+
+ +
+
+
+

{t("settings.notifications.events.title")}

+

{t("settings.notifications.events.subtitle")}

+
+ {t("settings.scope.device")} +
+ +
+
+
+
{t("settings.notifications.events.needsInput")}
+
+ +
+ +
+
+
{t("settings.notifications.events.idle")}
+
+ +
+
+
+
+ ) +} diff --git a/packages/ui/src/components/settings/opencode-settings-section.tsx b/packages/ui/src/components/settings/opencode-settings-section.tsx new file mode 100644 index 00000000..8af940eb --- /dev/null +++ b/packages/ui/src/components/settings/opencode-settings-section.tsx @@ -0,0 +1,52 @@ +import { createEffect, createSignal, type Component } from "solid-js" +import { Terminal } from "lucide-solid" +import OpenCodeBinarySelector from "../opencode-binary-selector" +import EnvironmentVariablesEditor from "../environment-variables-editor" +import { useConfig } from "../../stores/preferences" +import { useI18n } from "../../lib/i18n" + +export const OpenCodeSettingsSection: Component = () => { + const { t } = useI18n() + const { serverSettings, updateLastUsedBinary } = useConfig() + const [selectedBinary, setSelectedBinary] = createSignal(serverSettings().opencodeBinary || "opencode") + + createEffect(() => { + const binary = serverSettings().opencodeBinary || "opencode" + setSelectedBinary((current) => (current === binary ? current : binary)) + }) + + const handleBinaryChange = (binary: string) => { + setSelectedBinary(binary) + updateLastUsedBinary(binary) + } + + return ( +
+
+
+
+ +
+

{t("settings.opencode.runtime.title")}

+

{t("settings.opencode.runtime.subtitle")}

+
+
+ {t("settings.scope.server")} +
+ + +
+ +
+
+
+

{t("advancedSettings.environmentVariables.title")}

+

{t("advancedSettings.environmentVariables.subtitle")}

+
+ {t("settings.scope.server")} +
+ +
+
+ ) +} diff --git a/packages/ui/src/components/settings/remote-access-settings-section.tsx b/packages/ui/src/components/settings/remote-access-settings-section.tsx new file mode 100644 index 00000000..049036dd --- /dev/null +++ b/packages/ui/src/components/settings/remote-access-settings-section.tsx @@ -0,0 +1,401 @@ +import { Switch } from "@kobalte/core/switch" +import { For, Show, createMemo, createSignal, type Component, onMount } from "solid-js" +import { toDataURL } from "qrcode" +import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid" +import type { NetworkAddress, ServerMeta } from "../../../../server/src/api-types" +import { serverApi } from "../../lib/api-client" +import { restartCli } from "../../lib/native/cli" +import { serverSettings, setListeningMode } from "../../stores/preferences" +import { showConfirmDialog } from "../../stores/alerts" +import { getLogger } from "../../lib/logger" +import { useI18n } from "../../lib/i18n" + +const log = getLogger("actions") + +export const RemoteAccessSettingsSection: Component = () => { + const { t } = useI18n() + const [meta, setMeta] = createSignal(null) + const [authStatus, setAuthStatus] = createSignal<{ + authenticated: boolean + username?: string + passwordUserProvided?: boolean + } | null>(null) + const [loading, setLoading] = createSignal(false) + const [applyingListeningMode, setApplyingListeningMode] = createSignal(false) + const [qrCodes, setQrCodes] = createSignal>({}) + const [expandedUrl, setExpandedUrl] = createSignal(null) + const [error, setError] = createSignal(null) + const [passwordFormOpen, setPasswordFormOpen] = createSignal(false) + const [passwordValue, setPasswordValue] = createSignal("") + const [passwordConfirm, setPasswordConfirm] = createSignal("") + const [passwordError, setPasswordError] = createSignal(null) + const [savingPassword, setSavingPassword] = createSignal(false) + + const addresses = createMemo(() => meta()?.addresses ?? []) + const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode) + const allowExternalConnections = createMemo(() => currentMode() === "all") + const displayAddresses = createMemo(() => { + const list = addresses() + if (!allowExternalConnections()) return [] + return list.filter((address) => address.scope !== "loopback") + }) + + const refreshMeta = async () => { + setLoading(true) + setError(null) + setPasswordError(null) + try { + const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()]) + setMeta(metaResult) + setAuthStatus(authResult) + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setLoading(false) + } + } + + onMount(() => { + void refreshMeta() + }) + + const toggleExpanded = async (url: string) => { + if (expandedUrl() === url) { + setExpandedUrl(null) + return + } + setExpandedUrl(url) + if (!qrCodes()[url]) { + try { + const dataUrl = await toDataURL(url, { margin: 1, scale: 4 }) + setQrCodes((prev) => ({ ...prev, [url]: dataUrl })) + } catch (err) { + log.error("Failed to generate QR code", err) + } + } + } + + const handleAllowConnectionsChange = async (checked: boolean) => { + const targetMode: "local" | "all" = checked ? "all" : "local" + if (targetMode === currentMode() || applyingListeningMode()) return + + const confirmed = await showConfirmDialog(t("remoteAccess.listeningMode.restartConfirm.message"), { + title: checked + ? t("remoteAccess.listeningMode.restartConfirm.title.all") + : t("remoteAccess.listeningMode.restartConfirm.title.local"), + variant: "warning", + confirmLabel: t("remoteAccess.listeningMode.restartConfirm.confirmLabel"), + cancelLabel: t("remoteAccess.listeningMode.restartConfirm.cancelLabel"), + }) + + if (!confirmed) return + + setApplyingListeningMode(true) + setError(null) + try { + await setListeningMode(targetMode) + const restarted = await restartCli() + if (!restarted) { + setError(t("remoteAccess.restart.errorManual")) + } else { + setMeta((prev) => (prev ? { ...prev, listeningMode: targetMode } : prev)) + } + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setApplyingListeningMode(false) + } + + void refreshMeta() + } + + const handleOpenUrl = (url: string) => { + try { + window.open(url, "_blank", "noopener,noreferrer") + } catch (err) { + log.error("Failed to open URL", err) + } + } + + const handleSubmitPassword = async () => { + setPasswordError(null) + + const next = passwordValue() + const confirm = passwordConfirm() + if (next.trim().length < 8) { + setPasswordError(t("remoteAccess.password.error.tooShort")) + return + } + if (next !== confirm) { + setPasswordError(t("remoteAccess.password.error.mismatch")) + return + } + + setSavingPassword(true) + try { + const result = await serverApi.setServerPassword(next) + setAuthStatus({ + authenticated: true, + username: result.username, + passwordUserProvided: result.passwordUserProvided, + }) + setPasswordValue("") + setPasswordConfirm("") + setPasswordFormOpen(false) + } catch (err) { + setPasswordError(err instanceof Error ? err.message : String(err)) + } finally { + setSavingPassword(false) + } + } + + return ( +
+
+
+
+ +
+

{t("remoteAccess.sections.listeningMode.label")}

+

{t("remoteAccess.sections.listeningMode.help")}

+
+
+
+ {t("settings.scope.server")} + +
+
+ + void handleAllowConnectionsChange(nextChecked)} + disabled={loading() || applyingListeningMode()} + > + + + + {allowExternalConnections() ? t("remoteAccess.toggle.on") : t("remoteAccess.toggle.off")} + + + +
+ {t("remoteAccess.toggle.title")} + + {allowExternalConnections() + ? t("remoteAccess.toggle.caption.all") + : t("remoteAccess.toggle.caption.local")} + +
+
+ +

{t("remoteAccess.toggle.note")}

+
+ +
+
+
+ +
+

{t("remoteAccess.sections.serverPassword.label")}

+

{t("remoteAccess.sections.serverPassword.help")}

+
+
+ {t("settings.scope.server")} +
+ + {t("remoteAccess.authStatus.unavailable")}
} + > +
+

{t("remoteAccess.username", { username: authStatus()!.username ?? "codenomad" })}

+

+ {authStatus()!.passwordUserProvided + ? t("remoteAccess.password.status.set") + : t("remoteAccess.password.status.unset")} +

+ +
+ +
+ + +
+ + setPasswordValue(event.currentTarget.value)} + placeholder={t("remoteAccess.password.form.placeholder")} + /> +
+
+ + setPasswordConfirm(event.currentTarget.value)} + /> +
+ + + {(message) =>
{message()}
} +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+

{t("remoteAccess.sections.addresses.label")}

+

{t("remoteAccess.sections.addresses.help")}

+
+
+ {t("settings.scope.server")} +
+ + {t("remoteAccess.addresses.loading")}
}> + {error()}}> + 0 || meta()?.localUrl} + fallback={
{t("remoteAccess.addresses.none")}
} + > +
+ + {(url) => { + const value = () => url() + const expandedState = () => expandedUrl() === value() + const qr = () => qrCodes()[value()] + return ( +
+
+
+

{value()}

+

{t("remoteAccess.address.scope.loopback")}

+
+
+ + +
+
+ +
+ +
+
+
+ ) + }} +
+ + + {(address) => { + const url = address.remoteUrl + const expandedState = () => expandedUrl() === url + const qr = () => qrCodes()[url] + const scopeLabel = () => + address.scope === "external" + ? t("remoteAccess.address.scope.network") + : address.scope === "loopback" + ? t("remoteAccess.address.scope.loopback") + : t("remoteAccess.address.scope.internal") + + return ( +
+
+
+

{url}

+

+ {address.family.toUpperCase()} - {scopeLabel()} - {address.ip} +

+
+
+ + +
+
+ +
+ +
+
+
+ ) + }} +
+
+
+
+ + + + ) +} diff --git a/packages/ui/src/lib/i18n/messages/en/app.ts b/packages/ui/src/lib/i18n/messages/en/app.ts index c1d20689..510e5208 100644 --- a/packages/ui/src/lib/i18n/messages/en/app.ts +++ b/packages/ui/src/lib/i18n/messages/en/app.ts @@ -1,9 +1,9 @@ export const appMessages = { "app.launchError.title": "Unable to launch OpenCode", - "app.launchError.description": "We couldn't start the selected OpenCode binary. Review the error output below or choose a different binary from Advanced Settings.", + "app.launchError.description": "We couldn't start the selected OpenCode binary. Review the error output below or choose a different binary from OpenCode settings.", "app.launchError.binaryPathLabel": "Binary path", "app.launchError.errorOutputLabel": "Error output", - "app.launchError.openAdvancedSettings": "Open Advanced Settings", + "app.launchError.openAdvancedSettings": "Open OpenCode Settings", "app.launchError.close": "Close", "app.launchError.closeTitle": "Close (Esc)", "app.launchError.fallbackMessage": "Failed to launch workspace", diff --git a/packages/ui/src/lib/i18n/messages/en/folderSelection.ts b/packages/ui/src/lib/i18n/messages/en/folderSelection.ts index 8a86aad5..e548f92c 100644 --- a/packages/ui/src/lib/i18n/messages/en/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/en/folderSelection.ts @@ -22,6 +22,7 @@ export const folderSelectionMessages = { "folderSelection.browse.buttonOpening": "Opening...", "folderSelection.advancedSettings": "Advanced Settings", + "folderSelection.opencode": "OpenCode", "folderSelection.hints.navigate": "Navigate", "folderSelection.hints.select": "Select", diff --git a/packages/ui/src/lib/i18n/messages/en/settings.ts b/packages/ui/src/lib/i18n/messages/en/settings.ts index 6ba19291..47c7d990 100644 --- a/packages/ui/src/lib/i18n/messages/en/settings.ts +++ b/packages/ui/src/lib/i18n/messages/en/settings.ts @@ -55,4 +55,61 @@ export const settingsMessages = { "contextUsagePanel.labels.used": "Used", "contextUsagePanel.labels.available": "Avail", "contextUsagePanel.unavailable": "--", + + "settings.title": "Settings", + "settings.description": "Manage appearance, notifications, remote access, and OpenCode runtime options.", + "settings.navigationAriaLabel": "Settings sections", + "settings.close": "Close settings", + "settings.content.eyebrow": "Workspace preferences", + "settings.open.title": "Open settings", + "settings.open.ariaLabel": "Open settings", + "settings.nav.appearance": "Appearance", + "settings.nav.notifications": "Notifications", + "settings.nav.remote": "Remote Access", + "settings.nav.opencode": "OpenCode", + "settings.scope.device": "This device", + "settings.scope.server": "Server setting", + "settings.common.enabled": "Enabled", + "settings.section.appearance.title": "Appearance", + "settings.section.appearance.subtitle": "Adjust how the app looks on this device.", + "settings.appearance.theme.title": "Theme", + "settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.", + "settings.appearance.theme.option.system": "Match your operating system setting", + "settings.appearance.theme.option.light": "Use the light appearance", + "settings.appearance.theme.option.dark": "Use the dark appearance", + "settings.section.notifications.title": "Notifications", + "settings.section.notifications.subtitle": "Control OS-level notifications for session activity.", + "settings.notifications.permission.granted": "Granted", + "settings.notifications.permission.denied": "Denied", + "settings.notifications.permission.default": "Not granted", + "settings.notifications.permission.unsupported": "Unsupported", + "settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.", + "settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.", + "settings.notifications.messages.permissionNotGranted": "Notification permission not granted.", + "settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.", + "settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.", + "settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.", + "settings.notifications.sessionStatus.title": "Session status notifications", + "settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.", + "settings.notifications.enable.title": "Enable notifications", + "settings.notifications.enable.permission": "Permission: {permission}", + "settings.notifications.requestPermission.title": "Request permission", + "settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.", + "settings.notifications.requestPermission.action": "Request", + "settings.notifications.allowVisible.title": "Notify when the app is focused", + "settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.", + "settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.", + "settings.notifications.events.title": "Notify me when", + "settings.notifications.events.subtitle": "Choose which session events should send alerts.", + "settings.notifications.events.needsInput": "Session needs input", + "settings.notifications.events.idle": "Session becomes idle", + "settings.notifications.status.enabled": "Notifications enabled", + "settings.notifications.status.disabled": "Notifications disabled", + "settings.notifications.status.unsupported": "Notifications unsupported", + "settings.section.remote.title": "Remote Access", + "settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.", + "settings.section.opencode.title": "OpenCode", + "settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.", + "settings.opencode.runtime.title": "Runtime", + "settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.", } as const diff --git a/packages/ui/src/lib/i18n/messages/es/app.ts b/packages/ui/src/lib/i18n/messages/es/app.ts index 9fd24e92..554a5910 100644 --- a/packages/ui/src/lib/i18n/messages/es/app.ts +++ b/packages/ui/src/lib/i18n/messages/es/app.ts @@ -1,9 +1,9 @@ export const appMessages = { "app.launchError.title": "No se pudo iniciar OpenCode", - "app.launchError.description": "No pudimos iniciar el binario de OpenCode seleccionado. Revisa la salida de error abajo o elige un binario distinto en Configuración avanzada.", + "app.launchError.description": "No pudimos iniciar el binario de OpenCode seleccionado. Revisa la salida de error abajo o elige un binario distinto en la configuración de OpenCode.", "app.launchError.binaryPathLabel": "Ruta del binario", "app.launchError.errorOutputLabel": "Salida de error", - "app.launchError.openAdvancedSettings": "Abrir Configuración avanzada", + "app.launchError.openAdvancedSettings": "Abrir Configuración de OpenCode", "app.launchError.close": "Cerrar", "app.launchError.closeTitle": "Cerrar (Esc)", "app.launchError.fallbackMessage": "No se pudo iniciar el workspace", diff --git a/packages/ui/src/lib/i18n/messages/es/folderSelection.ts b/packages/ui/src/lib/i18n/messages/es/folderSelection.ts index 3879dcbf..56948be4 100644 --- a/packages/ui/src/lib/i18n/messages/es/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/es/folderSelection.ts @@ -22,6 +22,7 @@ export const folderSelectionMessages = { "folderSelection.browse.buttonOpening": "Abriendo...", "folderSelection.advancedSettings": "Configuración avanzada", + "folderSelection.opencode": "OpenCode", "folderSelection.hints.navigate": "Navegar", "folderSelection.hints.select": "Seleccionar", diff --git a/packages/ui/src/lib/i18n/messages/es/settings.ts b/packages/ui/src/lib/i18n/messages/es/settings.ts index a504c18a..750eff3e 100644 --- a/packages/ui/src/lib/i18n/messages/es/settings.ts +++ b/packages/ui/src/lib/i18n/messages/es/settings.ts @@ -55,4 +55,61 @@ export const settingsMessages = { "contextUsagePanel.labels.used": "Usado", "contextUsagePanel.labels.available": "Disp.", "contextUsagePanel.unavailable": "--", + + "settings.title": "Settings", + "settings.description": "Manage appearance, notifications, remote access, and OpenCode runtime options.", + "settings.navigationAriaLabel": "Settings sections", + "settings.close": "Close settings", + "settings.content.eyebrow": "Workspace preferences", + "settings.open.title": "Open settings", + "settings.open.ariaLabel": "Open settings", + "settings.nav.appearance": "Appearance", + "settings.nav.notifications": "Notifications", + "settings.nav.remote": "Remote Access", + "settings.nav.opencode": "OpenCode", + "settings.scope.device": "This device", + "settings.scope.server": "Server setting", + "settings.common.enabled": "Enabled", + "settings.section.appearance.title": "Appearance", + "settings.section.appearance.subtitle": "Adjust how the app looks on this device.", + "settings.appearance.theme.title": "Theme", + "settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.", + "settings.appearance.theme.option.system": "Match your operating system setting", + "settings.appearance.theme.option.light": "Use the light appearance", + "settings.appearance.theme.option.dark": "Use the dark appearance", + "settings.section.notifications.title": "Notifications", + "settings.section.notifications.subtitle": "Control OS-level notifications for session activity.", + "settings.notifications.permission.granted": "Granted", + "settings.notifications.permission.denied": "Denied", + "settings.notifications.permission.default": "Not granted", + "settings.notifications.permission.unsupported": "Unsupported", + "settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.", + "settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.", + "settings.notifications.messages.permissionNotGranted": "Notification permission not granted.", + "settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.", + "settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.", + "settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.", + "settings.notifications.sessionStatus.title": "Session status notifications", + "settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.", + "settings.notifications.enable.title": "Enable notifications", + "settings.notifications.enable.permission": "Permission: {permission}", + "settings.notifications.requestPermission.title": "Request permission", + "settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.", + "settings.notifications.requestPermission.action": "Request", + "settings.notifications.allowVisible.title": "Notify when the app is focused", + "settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.", + "settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.", + "settings.notifications.events.title": "Notify me when", + "settings.notifications.events.subtitle": "Choose which session events should send alerts.", + "settings.notifications.events.needsInput": "Session needs input", + "settings.notifications.events.idle": "Session becomes idle", + "settings.notifications.status.enabled": "Notifications enabled", + "settings.notifications.status.disabled": "Notifications disabled", + "settings.notifications.status.unsupported": "Notifications unsupported", + "settings.section.remote.title": "Remote Access", + "settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.", + "settings.section.opencode.title": "OpenCode", + "settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.", + "settings.opencode.runtime.title": "Runtime", + "settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.", } as const diff --git a/packages/ui/src/lib/i18n/messages/fr/app.ts b/packages/ui/src/lib/i18n/messages/fr/app.ts index 0dffcf65..46349919 100644 --- a/packages/ui/src/lib/i18n/messages/fr/app.ts +++ b/packages/ui/src/lib/i18n/messages/fr/app.ts @@ -1,9 +1,9 @@ export const appMessages = { "app.launchError.title": "Impossible de lancer OpenCode", - "app.launchError.description": "Nous n'avons pas pu démarrer le binaire OpenCode sélectionné. Consultez la sortie d'erreur ci-dessous ou choisissez un autre binaire dans les Paramètres avancés.", + "app.launchError.description": "Nous n'avons pas pu démarrer le binaire OpenCode sélectionné. Consultez la sortie d'erreur ci-dessous ou choisissez un autre binaire dans les paramètres OpenCode.", "app.launchError.binaryPathLabel": "Chemin du binaire", "app.launchError.errorOutputLabel": "Sortie d'erreur", - "app.launchError.openAdvancedSettings": "Ouvrir les paramètres avancés", + "app.launchError.openAdvancedSettings": "Ouvrir les paramètres OpenCode", "app.launchError.close": "Fermer", "app.launchError.closeTitle": "Fermer (Esc)", "app.launchError.fallbackMessage": "Échec du lancement de l'espace de travail", diff --git a/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts b/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts index 0fc4a7ba..cd1f2cdc 100644 --- a/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/fr/folderSelection.ts @@ -22,6 +22,7 @@ export const folderSelectionMessages = { "folderSelection.browse.buttonOpening": "Ouverture...", "folderSelection.advancedSettings": "Paramètres avancés", + "folderSelection.opencode": "OpenCode", "folderSelection.hints.navigate": "Naviguer", "folderSelection.hints.select": "Sélectionner", diff --git a/packages/ui/src/lib/i18n/messages/fr/settings.ts b/packages/ui/src/lib/i18n/messages/fr/settings.ts index facdaad2..7139acaa 100644 --- a/packages/ui/src/lib/i18n/messages/fr/settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr/settings.ts @@ -55,4 +55,61 @@ export const settingsMessages = { "contextUsagePanel.labels.used": "Utilisé", "contextUsagePanel.labels.available": "Dispo", "contextUsagePanel.unavailable": "--", + + "settings.title": "Settings", + "settings.description": "Manage appearance, notifications, remote access, and OpenCode runtime options.", + "settings.navigationAriaLabel": "Settings sections", + "settings.close": "Close settings", + "settings.content.eyebrow": "Workspace preferences", + "settings.open.title": "Open settings", + "settings.open.ariaLabel": "Open settings", + "settings.nav.appearance": "Appearance", + "settings.nav.notifications": "Notifications", + "settings.nav.remote": "Remote Access", + "settings.nav.opencode": "OpenCode", + "settings.scope.device": "This device", + "settings.scope.server": "Server setting", + "settings.common.enabled": "Enabled", + "settings.section.appearance.title": "Appearance", + "settings.section.appearance.subtitle": "Adjust how the app looks on this device.", + "settings.appearance.theme.title": "Theme", + "settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.", + "settings.appearance.theme.option.system": "Match your operating system setting", + "settings.appearance.theme.option.light": "Use the light appearance", + "settings.appearance.theme.option.dark": "Use the dark appearance", + "settings.section.notifications.title": "Notifications", + "settings.section.notifications.subtitle": "Control OS-level notifications for session activity.", + "settings.notifications.permission.granted": "Granted", + "settings.notifications.permission.denied": "Denied", + "settings.notifications.permission.default": "Not granted", + "settings.notifications.permission.unsupported": "Unsupported", + "settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.", + "settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.", + "settings.notifications.messages.permissionNotGranted": "Notification permission not granted.", + "settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.", + "settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.", + "settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.", + "settings.notifications.sessionStatus.title": "Session status notifications", + "settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.", + "settings.notifications.enable.title": "Enable notifications", + "settings.notifications.enable.permission": "Permission: {permission}", + "settings.notifications.requestPermission.title": "Request permission", + "settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.", + "settings.notifications.requestPermission.action": "Request", + "settings.notifications.allowVisible.title": "Notify when the app is focused", + "settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.", + "settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.", + "settings.notifications.events.title": "Notify me when", + "settings.notifications.events.subtitle": "Choose which session events should send alerts.", + "settings.notifications.events.needsInput": "Session needs input", + "settings.notifications.events.idle": "Session becomes idle", + "settings.notifications.status.enabled": "Notifications enabled", + "settings.notifications.status.disabled": "Notifications disabled", + "settings.notifications.status.unsupported": "Notifications unsupported", + "settings.section.remote.title": "Remote Access", + "settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.", + "settings.section.opencode.title": "OpenCode", + "settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.", + "settings.opencode.runtime.title": "Runtime", + "settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.", } as const diff --git a/packages/ui/src/lib/i18n/messages/ja/app.ts b/packages/ui/src/lib/i18n/messages/ja/app.ts index d1aea577..e96bf3ca 100644 --- a/packages/ui/src/lib/i18n/messages/ja/app.ts +++ b/packages/ui/src/lib/i18n/messages/ja/app.ts @@ -1,9 +1,9 @@ export const appMessages = { "app.launchError.title": "OpenCode を起動できません", - "app.launchError.description": "選択された OpenCode バイナリを起動できませんでした。下のエラー出力を確認するか、詳細設定から別のバイナリを選択してください。", + "app.launchError.description": "選択された OpenCode バイナリを起動できませんでした。下のエラー出力を確認するか、OpenCode 設定から別のバイナリを選択してください。", "app.launchError.binaryPathLabel": "バイナリのパス", "app.launchError.errorOutputLabel": "エラー出力", - "app.launchError.openAdvancedSettings": "詳細設定を開く", + "app.launchError.openAdvancedSettings": "OpenCode 設定を開く", "app.launchError.close": "閉じる", "app.launchError.closeTitle": "閉じる (Esc)", "app.launchError.fallbackMessage": "ワークスペースの起動に失敗しました", diff --git a/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts index 02291481..4c05e401 100644 --- a/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ja/folderSelection.ts @@ -22,6 +22,7 @@ export const folderSelectionMessages = { "folderSelection.browse.buttonOpening": "開いています...", "folderSelection.advancedSettings": "詳細設定", + "folderSelection.opencode": "OpenCode", "folderSelection.hints.navigate": "移動", "folderSelection.hints.select": "選択", diff --git a/packages/ui/src/lib/i18n/messages/ja/settings.ts b/packages/ui/src/lib/i18n/messages/ja/settings.ts index 3856fead..6cf894c0 100644 --- a/packages/ui/src/lib/i18n/messages/ja/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja/settings.ts @@ -55,4 +55,61 @@ export const settingsMessages = { "contextUsagePanel.labels.used": "使用", "contextUsagePanel.labels.available": "残り", "contextUsagePanel.unavailable": "--", + + "settings.title": "Settings", + "settings.description": "Manage appearance, notifications, remote access, and OpenCode runtime options.", + "settings.navigationAriaLabel": "Settings sections", + "settings.close": "Close settings", + "settings.content.eyebrow": "Workspace preferences", + "settings.open.title": "Open settings", + "settings.open.ariaLabel": "Open settings", + "settings.nav.appearance": "Appearance", + "settings.nav.notifications": "Notifications", + "settings.nav.remote": "Remote Access", + "settings.nav.opencode": "OpenCode", + "settings.scope.device": "This device", + "settings.scope.server": "Server setting", + "settings.common.enabled": "Enabled", + "settings.section.appearance.title": "Appearance", + "settings.section.appearance.subtitle": "Adjust how the app looks on this device.", + "settings.appearance.theme.title": "Theme", + "settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.", + "settings.appearance.theme.option.system": "Match your operating system setting", + "settings.appearance.theme.option.light": "Use the light appearance", + "settings.appearance.theme.option.dark": "Use the dark appearance", + "settings.section.notifications.title": "Notifications", + "settings.section.notifications.subtitle": "Control OS-level notifications for session activity.", + "settings.notifications.permission.granted": "Granted", + "settings.notifications.permission.denied": "Denied", + "settings.notifications.permission.default": "Not granted", + "settings.notifications.permission.unsupported": "Unsupported", + "settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.", + "settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.", + "settings.notifications.messages.permissionNotGranted": "Notification permission not granted.", + "settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.", + "settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.", + "settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.", + "settings.notifications.sessionStatus.title": "Session status notifications", + "settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.", + "settings.notifications.enable.title": "Enable notifications", + "settings.notifications.enable.permission": "Permission: {permission}", + "settings.notifications.requestPermission.title": "Request permission", + "settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.", + "settings.notifications.requestPermission.action": "Request", + "settings.notifications.allowVisible.title": "Notify when the app is focused", + "settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.", + "settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.", + "settings.notifications.events.title": "Notify me when", + "settings.notifications.events.subtitle": "Choose which session events should send alerts.", + "settings.notifications.events.needsInput": "Session needs input", + "settings.notifications.events.idle": "Session becomes idle", + "settings.notifications.status.enabled": "Notifications enabled", + "settings.notifications.status.disabled": "Notifications disabled", + "settings.notifications.status.unsupported": "Notifications unsupported", + "settings.section.remote.title": "Remote Access", + "settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.", + "settings.section.opencode.title": "OpenCode", + "settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.", + "settings.opencode.runtime.title": "Runtime", + "settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.", } as const diff --git a/packages/ui/src/lib/i18n/messages/ru/app.ts b/packages/ui/src/lib/i18n/messages/ru/app.ts index dd2c50fb..1f8c41cd 100644 --- a/packages/ui/src/lib/i18n/messages/ru/app.ts +++ b/packages/ui/src/lib/i18n/messages/ru/app.ts @@ -1,9 +1,9 @@ export const appMessages = { "app.launchError.title": "Не удалось запустить OpenCode", - "app.launchError.description": "Не удалось запустить выбранный бинарник OpenCode. Просмотрите вывод ошибки ниже или выберите другой бинарник в расширенных настройках.", + "app.launchError.description": "Не удалось запустить выбранный бинарник OpenCode. Просмотрите вывод ошибки ниже или выберите другой бинарник в настройках OpenCode.", "app.launchError.binaryPathLabel": "Путь к бинарнику", "app.launchError.errorOutputLabel": "Вывод ошибки", - "app.launchError.openAdvancedSettings": "Открыть расширенные настройки", + "app.launchError.openAdvancedSettings": "Открыть настройки OpenCode", "app.launchError.close": "Закрыть", "app.launchError.closeTitle": "Закрыть (Esc)", "app.launchError.fallbackMessage": "Не удалось запустить рабочее пространство", diff --git a/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts b/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts index 34d7d6c6..4a005938 100644 --- a/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/ru/folderSelection.ts @@ -22,6 +22,7 @@ export const folderSelectionMessages = { "folderSelection.browse.buttonOpening": "Открытие…", "folderSelection.advancedSettings": "Расширенные настройки", + "folderSelection.opencode": "OpenCode", "folderSelection.hints.navigate": "Навигация", "folderSelection.hints.select": "Выбрать", diff --git a/packages/ui/src/lib/i18n/messages/ru/settings.ts b/packages/ui/src/lib/i18n/messages/ru/settings.ts index 09cb228b..cdd028a0 100644 --- a/packages/ui/src/lib/i18n/messages/ru/settings.ts +++ b/packages/ui/src/lib/i18n/messages/ru/settings.ts @@ -55,4 +55,61 @@ export const settingsMessages = { "contextUsagePanel.labels.used": "Использовано", "contextUsagePanel.labels.available": "Доступно", "contextUsagePanel.unavailable": "--", + + "settings.title": "Settings", + "settings.description": "Manage appearance, notifications, remote access, and OpenCode runtime options.", + "settings.navigationAriaLabel": "Settings sections", + "settings.close": "Close settings", + "settings.content.eyebrow": "Workspace preferences", + "settings.open.title": "Open settings", + "settings.open.ariaLabel": "Open settings", + "settings.nav.appearance": "Appearance", + "settings.nav.notifications": "Notifications", + "settings.nav.remote": "Remote Access", + "settings.nav.opencode": "OpenCode", + "settings.scope.device": "This device", + "settings.scope.server": "Server setting", + "settings.common.enabled": "Enabled", + "settings.section.appearance.title": "Appearance", + "settings.section.appearance.subtitle": "Adjust how the app looks on this device.", + "settings.appearance.theme.title": "Theme", + "settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.", + "settings.appearance.theme.option.system": "Match your operating system setting", + "settings.appearance.theme.option.light": "Use the light appearance", + "settings.appearance.theme.option.dark": "Use the dark appearance", + "settings.section.notifications.title": "Notifications", + "settings.section.notifications.subtitle": "Control OS-level notifications for session activity.", + "settings.notifications.permission.granted": "Granted", + "settings.notifications.permission.denied": "Denied", + "settings.notifications.permission.default": "Not granted", + "settings.notifications.permission.unsupported": "Unsupported", + "settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.", + "settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.", + "settings.notifications.messages.permissionNotGranted": "Notification permission not granted.", + "settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.", + "settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.", + "settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.", + "settings.notifications.sessionStatus.title": "Session status notifications", + "settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.", + "settings.notifications.enable.title": "Enable notifications", + "settings.notifications.enable.permission": "Permission: {permission}", + "settings.notifications.requestPermission.title": "Request permission", + "settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.", + "settings.notifications.requestPermission.action": "Request", + "settings.notifications.allowVisible.title": "Notify when the app is focused", + "settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.", + "settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.", + "settings.notifications.events.title": "Notify me when", + "settings.notifications.events.subtitle": "Choose which session events should send alerts.", + "settings.notifications.events.needsInput": "Session needs input", + "settings.notifications.events.idle": "Session becomes idle", + "settings.notifications.status.enabled": "Notifications enabled", + "settings.notifications.status.disabled": "Notifications disabled", + "settings.notifications.status.unsupported": "Notifications unsupported", + "settings.section.remote.title": "Remote Access", + "settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.", + "settings.section.opencode.title": "OpenCode", + "settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.", + "settings.opencode.runtime.title": "Runtime", + "settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.", } as const diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/app.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/app.ts index 477a447b..cd2a82f8 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/app.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/app.ts @@ -1,9 +1,9 @@ export const appMessages = { "app.launchError.title": "无法启动 OpenCode", - "app.launchError.description": "我们无法启动所选的 OpenCode 可执行文件。请查看下面的错误输出,或在“高级设置”中选择其他可执行文件。", + "app.launchError.description": "我们无法启动所选的 OpenCode 可执行文件。请查看下面的错误输出,或在 OpenCode 设置中选择其他可执行文件。", "app.launchError.binaryPathLabel": "可执行文件路径", "app.launchError.errorOutputLabel": "错误输出", - "app.launchError.openAdvancedSettings": "打开高级设置", + "app.launchError.openAdvancedSettings": "打开 OpenCode 设置", "app.launchError.close": "关闭", "app.launchError.closeTitle": "关闭 (Esc)", "app.launchError.fallbackMessage": "启动工作区失败", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts index 4315823a..1c765fe9 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/folderSelection.ts @@ -22,6 +22,7 @@ export const folderSelectionMessages = { "folderSelection.browse.buttonOpening": "正在打开...", "folderSelection.advancedSettings": "高级设置", + "folderSelection.opencode": "OpenCode", "folderSelection.hints.navigate": "导航", "folderSelection.hints.select": "选择", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts index 77400102..fecd0a81 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/settings.ts @@ -55,4 +55,61 @@ export const settingsMessages = { "contextUsagePanel.labels.used": "已用", "contextUsagePanel.labels.available": "可用", "contextUsagePanel.unavailable": "--", + + "settings.title": "Settings", + "settings.description": "Manage appearance, notifications, remote access, and OpenCode runtime options.", + "settings.navigationAriaLabel": "Settings sections", + "settings.close": "Close settings", + "settings.content.eyebrow": "Workspace preferences", + "settings.open.title": "Open settings", + "settings.open.ariaLabel": "Open settings", + "settings.nav.appearance": "Appearance", + "settings.nav.notifications": "Notifications", + "settings.nav.remote": "Remote Access", + "settings.nav.opencode": "OpenCode", + "settings.scope.device": "This device", + "settings.scope.server": "Server setting", + "settings.common.enabled": "Enabled", + "settings.section.appearance.title": "Appearance", + "settings.section.appearance.subtitle": "Adjust how the app looks on this device.", + "settings.appearance.theme.title": "Theme", + "settings.appearance.theme.subtitle": "Choose the color mode used throughout the app.", + "settings.appearance.theme.option.system": "Match your operating system setting", + "settings.appearance.theme.option.light": "Use the light appearance", + "settings.appearance.theme.option.dark": "Use the dark appearance", + "settings.section.notifications.title": "Notifications", + "settings.section.notifications.subtitle": "Control OS-level notifications for session activity.", + "settings.notifications.permission.granted": "Granted", + "settings.notifications.permission.denied": "Denied", + "settings.notifications.permission.default": "Not granted", + "settings.notifications.permission.unsupported": "Unsupported", + "settings.notifications.messages.unsupportedEnvironment": "OS notifications are not supported in this environment.", + "settings.notifications.messages.permissionDenied": "Notification permission denied. Enable notifications in your system or browser settings.", + "settings.notifications.messages.permissionNotGranted": "Notification permission not granted.", + "settings.notifications.messages.unsupportedGeneral": "Notifications are not supported in this environment.", + "settings.notifications.messages.permissionGranted": "Permission granted. You can now enable notifications.", + "settings.notifications.messages.permissionRequestDenied": "Permission denied. You may need to enable notifications in your system or browser settings.", + "settings.notifications.sessionStatus.title": "Session status notifications", + "settings.notifications.sessionStatus.subtitle": "Receive alerts when sessions need your attention.", + "settings.notifications.enable.title": "Enable notifications", + "settings.notifications.enable.permission": "Permission: {permission}", + "settings.notifications.requestPermission.title": "Request permission", + "settings.notifications.requestPermission.subtitle": "Allow the app to send notifications on this device.", + "settings.notifications.requestPermission.action": "Request", + "settings.notifications.allowVisible.title": "Notify when the app is focused", + "settings.notifications.allowVisible.subtitle": "Keep alerts enabled even while this window is visible.", + "settings.notifications.unsupportedNote": "Notifications are not supported in this environment. The notifications control stays disabled.", + "settings.notifications.events.title": "Notify me when", + "settings.notifications.events.subtitle": "Choose which session events should send alerts.", + "settings.notifications.events.needsInput": "Session needs input", + "settings.notifications.events.idle": "Session becomes idle", + "settings.notifications.status.enabled": "Notifications enabled", + "settings.notifications.status.disabled": "Notifications disabled", + "settings.notifications.status.unsupported": "Notifications unsupported", + "settings.section.remote.title": "Remote Access", + "settings.section.remote.subtitle": "Review how this server is exposed on your network and secure access credentials.", + "settings.section.opencode.title": "OpenCode", + "settings.section.opencode.subtitle": "Choose the OpenCode binary and environment used for new instances.", + "settings.opencode.runtime.title": "Runtime", + "settings.opencode.runtime.subtitle": "Configure which OpenCode binary new instances launch with.", } as const diff --git a/packages/ui/src/stores/settings-screen.ts b/packages/ui/src/stores/settings-screen.ts new file mode 100644 index 00000000..3de9eb13 --- /dev/null +++ b/packages/ui/src/stores/settings-screen.ts @@ -0,0 +1,17 @@ +import { createSignal } from "solid-js" + +export type SettingsSectionId = "appearance" | "notifications" | "remote" | "opencode" + +const [settingsOpen, setSettingsOpen] = createSignal(false) +const [activeSettingsSection, setActiveSettingsSection] = createSignal("appearance") + +export function openSettings(section: SettingsSectionId = "appearance") { + setActiveSettingsSection(section) + setSettingsOpen(true) +} + +export function closeSettings() { + setSettingsOpen(false) +} + +export { settingsOpen, activeSettingsSection, setActiveSettingsSection } diff --git a/packages/ui/src/styles/components/settings-screen.css b/packages/ui/src/styles/components/settings-screen.css new file mode 100644 index 00000000..a7b87282 --- /dev/null +++ b/packages/ui/src/styles/components/settings-screen.css @@ -0,0 +1,516 @@ +.settings-screen-frame { + @apply fixed inset-0 z-50 flex items-center justify-center p-4; +} + +/* Override .modal-surface (defined later in panels.css). */ +.modal-surface.settings-screen-shell { + width: min(1120px, 100%); + height: min(88vh, 920px); + max-height: none; + display: grid; + grid-template-columns: minmax(220px, 260px) minmax(0, 1fr); + overflow: hidden; + border: 1px solid var(--border-base); + box-shadow: 0 32px 96px color-mix(in oklab, var(--overlay-scrim) 55%, transparent); +} + +.settings-screen-nav { + display: flex; + flex-direction: column; + gap: 1rem; + padding: 1.25rem; + background: + linear-gradient(180deg, color-mix(in oklab, var(--surface-secondary) 92%, var(--accent-primary) 8%), var(--surface-secondary)); + border-right: 1px solid var(--border-base); +} + +.settings-screen-nav-header { + padding-bottom: 0.75rem; + border-bottom: 1px solid color-mix(in oklab, var(--border-base) 82%, transparent); +} + +.settings-screen-nav-title-row { + display: flex; + align-items: flex-start; + gap: 0.875rem; +} + +.settings-screen-nav-icon-wrap { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 0.875rem; + background: color-mix(in oklab, var(--accent-primary) 16%, var(--surface-base)); + color: var(--accent-primary); +} + +.settings-screen-nav-icon { + width: 1.125rem; + height: 1.125rem; +} + +.settings-screen-title { + font-size: var(--font-size-xl); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); +} + +.settings-screen-subtitle { + margin-top: 0.25rem; + font-size: var(--font-size-sm); + color: var(--text-muted); +} + +.settings-screen-nav-list { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.settings-nav-button { + display: flex; + align-items: center; + gap: 0.75rem; + width: 100%; + padding: 0.75rem 0.875rem; + border-radius: 0.875rem; + border: 1px solid transparent; + background: transparent; + color: var(--text-secondary); + transition: background-color 140ms ease, color 140ms ease, border-color 140ms ease, transform 140ms ease; + outline: none; +} + +.settings-nav-button:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; +} + +.settings-nav-button:hover { + background: color-mix(in oklab, var(--surface-base) 70%, transparent); + color: var(--text-primary); +} + +.settings-nav-button[data-selected="true"] { + background: color-mix(in oklab, var(--accent-primary) 14%, var(--surface-base)); + border-color: color-mix(in oklab, var(--accent-primary) 26%, var(--border-base)); + color: var(--text-primary); + transform: translateX(2px); +} + +.settings-nav-button-icon { + width: 1rem; + height: 1rem; + flex-shrink: 0; +} + +.settings-screen-content { + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + overflow: hidden; + background: + radial-gradient(circle at top right, color-mix(in oklab, var(--accent-primary) 9%, transparent), transparent 28%), + var(--surface-base); +} + +.settings-screen-content-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.5rem; + padding: 1.25rem 1.5rem; + border-bottom: 1px solid var(--border-base); + background: color-mix(in oklab, var(--surface-base) 92%, var(--surface-secondary) 8%); + flex-shrink: 0; +} + +.settings-screen-content-header-title-group { + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 0; +} + +.settings-screen-content-eyebrow { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-semibold); + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-muted); +} + +.settings-screen-content-title { + font-size: clamp(1.35rem, 2vw, 1.85rem); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); + line-height: 1.2; +} + +.settings-screen-close { + flex-shrink: 0; + width: 2.25rem; + height: 2.25rem; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.settings-screen-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 1.5rem; +} + +.settings-section-stack, +.settings-panel-body, +.settings-stack { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.settings-card { + padding: 1.25rem; + border: 1px solid var(--border-base); + border-radius: 1rem; + background: color-mix(in oklab, var(--surface-base) 86%, var(--surface-secondary) 14%); +} + +.settings-card-padless { + padding: 0; + overflow: hidden; +} + +.settings-card-content, +.settings-card-header-padded { + padding: 1rem; +} + +.settings-card-content { + padding-top: 0; +} + +.settings-card-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.25rem; + padding-bottom: 1rem; + border-bottom: 1px solid color-mix(in oklab, var(--border-base) 65%, transparent); +} + +.settings-card-heading-with-icon { + display: flex; + align-items: flex-start; + gap: 0.75rem; +} + +.settings-card-heading-icon { + width: 1rem; + height: 1rem; + margin-top: 0.15rem; + color: var(--accent-primary); +} + +.settings-card-title { + font-size: var(--font-size-base); + font-weight: var(--font-weight-semibold); + color: var(--text-primary); +} + +.settings-card-subtitle { + margin-top: 0.2rem; + font-size: var(--font-size-sm); + color: var(--text-muted); +} + +.settings-card-message { + padding: 1rem; + border: 1px dashed var(--border-base); + border-radius: 0.625rem; + color: var(--text-muted); + font-size: var(--font-size-sm); +} + +.settings-card-content { + padding: 1rem; + border: 1px solid var(--border-base); + border-radius: 0.625rem; + background: var(--surface-base); +} + +.settings-help-text { + margin: 0; + color: var(--text-secondary); + font-size: var(--font-size-sm); +} + +.settings-password-actions { + display: flex; + justify-content: flex-start; + margin-top: 0.75rem; +} + +.settings-form-group { + margin-top: 0.75rem; +} + +.settings-form-label { + display: block; + margin-bottom: 0.375rem; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--text-secondary); +} + +.settings-pill-button { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.5rem 0.875rem; + border-radius: 999px; + border: 1px solid var(--border-base); + background: var(--surface-secondary); + color: var(--text-primary); + font-size: var(--font-size-sm); + cursor: pointer; + transition: background-color 140ms ease, border-color 140ms ease; +} + +.settings-pill-button:hover { + background: var(--surface-hover); + border-color: color-mix(in oklab, var(--accent-primary) 28%, var(--border-base)); +} + +.settings-pill-button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.settings-error-message { + margin-top: 0.625rem; + padding: 0.75rem; + border: 1px solid var(--border-critical, #e65c5c); + background: color-mix(in srgb, var(--border-critical, #e65c5c) 10%, transparent); + border-radius: 0.625rem; + color: var(--text-primary); + font-size: var(--font-size-sm); +} + +.settings-scope-badge { + display: inline-flex; + align-items: center; + padding: 0.375rem 0.6rem; + border-radius: 999px; + background: color-mix(in oklab, var(--surface-secondary) 75%, var(--surface-base)); + color: var(--text-secondary); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + white-space: nowrap; +} + +.settings-scope-badge-server { + background: color-mix(in oklab, var(--accent-primary) 12%, var(--surface-base)); + color: var(--accent-primary); +} + +.settings-choice-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.875rem; +} + +.settings-choice { + display: flex; + align-items: center; + gap: 0.875rem; + width: 100%; + padding: 0.95rem; + border-radius: 1rem; + border: 1px solid var(--border-base); + background: var(--surface-base); + color: var(--text-primary); + text-align: left; + transition: border-color 140ms ease, background-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; + outline: none; + cursor: pointer; +} + +.settings-choice:hover { + border-color: color-mix(in oklab, var(--accent-primary) 28%, var(--border-base)); + background: var(--surface-hover); +} + +.settings-choice:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 2px; +} + +.settings-choice[data-selected="true"] { + border-color: color-mix(in oklab, var(--accent-primary) 45%, var(--border-base)); + background: color-mix(in oklab, var(--accent-primary) 10%, var(--surface-base)); + box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--accent-primary) 20%, transparent); + transform: translateY(-1px); +} + +.settings-choice-icon-wrap { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + border-radius: 0.9rem; + background: color-mix(in oklab, var(--surface-secondary) 76%, var(--surface-base)); + color: var(--accent-primary); + flex-shrink: 0; +} + +.settings-choice-icon { + width: 1rem; + height: 1rem; +} + +.settings-choice-copy { + display: flex; + flex-direction: column; + min-width: 0; +} + +.settings-choice-label { + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); +} + +.settings-choice-description { + margin-top: 0.15rem; + font-size: var(--font-size-xs); + color: var(--text-muted); +} + +.settings-choice-check { + margin-left: auto; + color: var(--accent-primary); + opacity: 0; +} + +.settings-choice[data-selected="true"] .settings-choice-check { + opacity: 1; +} + +.settings-toggle-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.9rem 0; + border-top: 1px solid color-mix(in oklab, var(--border-base) 78%, transparent); +} + +.settings-toggle-row:first-child { + border-top: none; + padding-top: 0; +} + +.settings-toggle-row-compact { + align-items: flex-start; +} + +.settings-toggle-title { + font-size: var(--font-size-sm); + font-weight: var(--font-weight-medium); + color: var(--text-primary); +} + +.settings-toggle-caption, +.settings-inline-note { + margin-top: 0.2rem; + font-size: var(--font-size-xs); + color: var(--text-muted); +} + +.settings-checkbox-toggle { + display: inline-flex; + align-items: center; + gap: 0.5rem; + flex-shrink: 0; + color: var(--text-secondary); + font-size: var(--font-size-sm); +} + +.settings-checkbox-toggle input { + accent-color: var(--accent-primary); +} + +.settings-toolbar-inline { + display: inline-flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; + justify-content: flex-end; +} + +@media (max-width: 900px) { + .modal-surface.settings-screen-shell { + min-height: min(760px, calc(100vh - 1rem)); + grid-template-columns: 1fr; + } + + .settings-screen-nav { + gap: 0.75rem; + padding: 1rem; + border-right: none; + border-bottom: 1px solid var(--border-base); + } + + .settings-screen-nav-list { + flex-direction: row; + overflow-x: auto; + padding-bottom: 0.25rem; + } + + .settings-nav-button { + width: auto; + flex-shrink: 0; + } +} + +@media (max-width: 640px) { + .settings-screen-frame { + padding: 0; + } + + .modal-surface.settings-screen-shell { + width: 100%; + height: 100%; + max-height: none; + min-height: 100%; + border-radius: 0; + } + + .settings-screen-content-header, + .settings-screen-scroll { + padding: 1rem; + } + + .settings-card-header, + .settings-toggle-row { + flex-direction: column; + align-items: stretch; + } + + .settings-toolbar-inline { + justify-content: flex-start; + } + + .settings-choice-grid { + grid-template-columns: 1fr; + } +} diff --git a/packages/ui/src/styles/controls.css b/packages/ui/src/styles/controls.css index 253c82e3..e7862d0a 100644 --- a/packages/ui/src/styles/controls.css +++ b/packages/ui/src/styles/controls.css @@ -8,3 +8,4 @@ @import "./components/directory-browser.css"; @import "./components/remote-access.css"; @import "./components/permission-notification.css"; +@import "./components/settings-screen.css"; diff --git a/packages/ui/src/styles/tokens.css b/packages/ui/src/styles/tokens.css index c8a92642..0ec9d15a 100644 --- a/packages/ui/src/styles/tokens.css +++ b/packages/ui/src/styles/tokens.css @@ -2,6 +2,7 @@ color-scheme: light; /* Surface tokens */ --surface-base: #ffffff; + --surface-primary: var(--surface-base); --surface-secondary: #f5f5f5; --surface-muted: #f8fafc; --surface-code: #f1f5f9; @@ -178,6 +179,7 @@ color-scheme: dark; /* Surface tokens */ --surface-base: #1a1a1a; + --surface-primary: var(--surface-base); --surface-secondary: #2a2a2a; --surface-muted: #212529; --surface-code: #1a1a1a; @@ -347,6 +349,7 @@ color-scheme: dark; /* Surface tokens */ --surface-base: #1a1a1a; + --surface-primary: var(--surface-base); --surface-secondary: #2a2a2a; --surface-muted: #212529; --surface-code: #1a1a1a;