feat(ui): add unified settings screen
This commit is contained in:
@@ -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)}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@@ -533,10 +530,6 @@ const App: Component = () => {
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
isLoading={isSelectingFolder()}
|
||||
advancedSettingsOpen={isAdvancedSettingsOpen()}
|
||||
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
|
||||
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
|
||||
onOpenRemoteAccess={() => setRemoteAccessOpen(true)}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@@ -546,12 +539,8 @@ const App: Component = () => {
|
||||
<FolderSelectionView
|
||||
onSelectFolder={handleSelectFolder}
|
||||
isLoading={isSelectingFolder()}
|
||||
advancedSettingsOpen={isAdvancedSettingsOpen()}
|
||||
onAdvancedSettingsOpen={() => setIsAdvancedSettingsOpen(true)}
|
||||
onAdvancedSettingsClose={() => setIsAdvancedSettingsOpen(false)}
|
||||
onClose={() => {
|
||||
setShowFolderSelection(false)
|
||||
setIsAdvancedSettingsOpen(false)
|
||||
clearLaunchError()
|
||||
}}
|
||||
/>
|
||||
@@ -559,7 +548,7 @@ const App: Component = () => {
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<RemoteAccessOverlay open={remoteAccessOpen()} onClose={() => setRemoteAccessOpen(false)} />
|
||||
<SettingsScreen />
|
||||
|
||||
<AlertDialog />
|
||||
|
||||
|
||||
@@ -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<FolderSelectionViewProps> = (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<FolderSelectionViewProps> = (props) => {
|
||||
})
|
||||
|
||||
function dropTargetBlocked() {
|
||||
return isLoading() || isFolderBrowserOpen() || Boolean(props.advancedSettingsOpen)
|
||||
return isLoading() || isFolderBrowserOpen() || settingsOpen()
|
||||
}
|
||||
|
||||
function showInvalidFolderDropAlert() {
|
||||
@@ -264,11 +259,6 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (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<FolderSelectionViewProps> = (props) => {
|
||||
</Select>
|
||||
</div>
|
||||
<div class="absolute top-4 right-6 flex items-center gap-2">
|
||||
<ThemeModeToggle class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center" />
|
||||
<Show when={props.onOpenRemoteAccess}>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
||||
onClick={() => props.onOpenRemoteAccess?.()}
|
||||
>
|
||||
<MonitorUp class="w-4 h-4" />
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
||||
onClick={() => openSettings("appearance")}
|
||||
aria-label={t("settings.open.title")}
|
||||
title={t("settings.open.title")}
|
||||
>
|
||||
<Settings class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary w-auto p-2 inline-flex items-center justify-center"
|
||||
onClick={() => openSettings("remote")}
|
||||
aria-label={t("instanceTabs.remote.ariaLabel")}
|
||||
title={t("instanceTabs.remote.title")}
|
||||
>
|
||||
<MonitorUp class="w-4 h-4" />
|
||||
</button>
|
||||
<Show when={props.onClose}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -595,12 +593,12 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Advanced settings section */}
|
||||
{/* OpenCode settings section */}
|
||||
<div class="panel-section w-full">
|
||||
<button onClick={() => props.onAdvancedSettingsOpen?.()} class="panel-section-header w-full justify-between">
|
||||
<button onClick={() => openSettings("opencode")} class="panel-section-header w-full justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<Settings class="w-4 h-4 icon-muted" />
|
||||
<span class="text-sm font-medium text-secondary">{t("folderSelection.advancedSettings")}</span>
|
||||
<span class="text-sm font-medium text-secondary">{t("folderSelection.opencode")}</span>
|
||||
</div>
|
||||
<ChevronRight class="w-4 h-4 icon-muted" />
|
||||
</button>
|
||||
@@ -661,14 +659,6 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<AdvancedSettingsModal
|
||||
open={Boolean(props.advancedSettingsOpen)}
|
||||
onClose={() => props.onAdvancedSettingsClose?.()}
|
||||
selectedBinary={selectedBinary()}
|
||||
onBinaryChange={handleBinaryChange}
|
||||
isLoading={props.isLoading}
|
||||
/>
|
||||
|
||||
<DirectoryBrowserDialog
|
||||
open={isFolderBrowserOpen()}
|
||||
title={t("folderSelection.dialog.title")}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { Component, For, Show, createMemo, createSignal } from "solid-js"
|
||||
import { Component, For, Show, createMemo } from "solid-js"
|
||||
import { Dynamic } from "solid-js/web"
|
||||
import type { Instance } from "../types/instance"
|
||||
import InstanceTab from "./instance-tab"
|
||||
import KeyboardHint from "./keyboard-hint"
|
||||
import { Plus, MonitorUp, Bell, BellOff } from "lucide-solid"
|
||||
import { Plus, MonitorUp, Bell, BellOff, Settings } from "lucide-solid"
|
||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
import { ThemeModeToggle } from "./theme-mode-toggle"
|
||||
import NotificationsSettingsModal from "./notifications-settings-modal"
|
||||
import { isOsNotificationSupportedSync } from "../lib/os-notifications"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
import { openSettings } from "../stores/settings-screen"
|
||||
|
||||
interface InstanceTabsProps {
|
||||
instances: Map<string, Instance>
|
||||
@@ -17,13 +16,11 @@ interface InstanceTabsProps {
|
||||
onSelect: (instanceId: string) => void
|
||||
onClose: (instanceId: string) => void
|
||||
onNew: () => void
|
||||
onOpenRemoteAccess?: () => void
|
||||
}
|
||||
|
||||
const InstanceTabs: Component<InstanceTabsProps> = (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<InstanceTabsProps> = (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<InstanceTabsProps> = (props) => {
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<ThemeModeToggle class="new-tab-button" />
|
||||
<button
|
||||
class="new-tab-button"
|
||||
onClick={() => openSettings("appearance")}
|
||||
title={t("settings.open.title")}
|
||||
aria-label={t("settings.open.ariaLabel")}
|
||||
>
|
||||
<Settings class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
class={`new-tab-button ${!notificationsSupported() ? "opacity-50" : ""}`}
|
||||
onClick={() => setNotificationsOpen(true)}
|
||||
title={notificationTitle()}
|
||||
aria-label={notificationTitle()}
|
||||
>
|
||||
<button
|
||||
class={`new-tab-button ${!notificationsSupported() ? "opacity-50" : ""}`}
|
||||
onClick={() => openSettings("notifications")}
|
||||
title={notificationTitle()}
|
||||
aria-label={notificationTitle()}
|
||||
>
|
||||
<Dynamic component={notificationIcon()} class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<Show when={Boolean(props.onOpenRemoteAccess)}>
|
||||
<button
|
||||
class="new-tab-button tab-remote-button"
|
||||
onClick={() => props.onOpenRemoteAccess?.()}
|
||||
title={t("instanceTabs.remote.title")}
|
||||
aria-label={t("instanceTabs.remote.ariaLabel")}
|
||||
>
|
||||
<MonitorUp class="w-4 h-4" />
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
class="new-tab-button tab-remote-button"
|
||||
onClick={() => openSettings("remote")}
|
||||
title={t("instanceTabs.remote.title")}
|
||||
aria-label={t("instanceTabs.remote.ariaLabel")}
|
||||
>
|
||||
<MonitorUp class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NotificationsSettingsModal open={notificationsOpen()} onClose={() => setNotificationsOpen(false)} />
|
||||
</div>
|
||||
|
||||
)
|
||||
|
||||
109
packages/ui/src/components/settings-screen.tsx
Normal file
109
packages/ui/src/components/settings-screen.tsx
Normal file
@@ -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 <NotificationsSettingsSection />
|
||||
case "remote":
|
||||
return <RemoteAccessSettingsSection />
|
||||
case "opencode":
|
||||
return <OpenCodeSettingsSection />
|
||||
case "appearance":
|
||||
default:
|
||||
return <AppearanceSettingsSection />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={settingsOpen()} onOpenChange={(open) => !open && closeSettings()}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="modal-overlay" />
|
||||
<div class="settings-screen-frame">
|
||||
<Dialog.Content class="modal-surface settings-screen-shell">
|
||||
<Dialog.Title class="sr-only">{t("settings.title")}</Dialog.Title>
|
||||
<Dialog.Description class="sr-only">{t("settings.description")}</Dialog.Description>
|
||||
|
||||
<aside class="settings-screen-nav">
|
||||
<div class="settings-screen-nav-header">
|
||||
<div class="settings-screen-nav-title-row">
|
||||
<span class="settings-screen-nav-icon-wrap">
|
||||
<Settings class="settings-screen-nav-icon" />
|
||||
</span>
|
||||
<div>
|
||||
<h2 class="settings-screen-title">{t("settings.title")}</h2>
|
||||
<p class="settings-screen-subtitle">{t("settings.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="settings-screen-nav-list" aria-label={t("settings.navigationAriaLabel")}>
|
||||
<For each={sections()}>
|
||||
{(section) => {
|
||||
const Icon = section.icon
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="settings-nav-button"
|
||||
data-selected={activeSettingsSection() === section.id ? "true" : "false"}
|
||||
onClick={() => setActiveSettingsSection(section.id)}
|
||||
>
|
||||
<Icon class="settings-nav-button-icon" />
|
||||
<span>{section.label}</span>
|
||||
</button>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<div class="settings-screen-content">
|
||||
<header class="settings-screen-content-header">
|
||||
<div class="settings-screen-content-header-title-group">
|
||||
<p class="settings-screen-content-eyebrow">{t("settings.content.eyebrow")}</p>
|
||||
<h1 class="settings-screen-content-title">
|
||||
{sections().find((section) => section.id === activeSettingsSection())?.label}
|
||||
</h1>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary settings-screen-close"
|
||||
onClick={closeSettings}
|
||||
aria-label={t("settings.close")}
|
||||
title={t("settings.close")}
|
||||
>
|
||||
<X class="w-4 h-4" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<div class="settings-screen-scroll">{renderSection()}</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div class="settings-section-stack">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("settings.appearance.theme.title")}</h3>
|
||||
<p class="settings-card-subtitle">{t("settings.appearance.theme.subtitle")}</p>
|
||||
</div>
|
||||
<span class="settings-scope-badge">{t("settings.scope.device")}</span>
|
||||
</div>
|
||||
<div class="settings-choice-grid">
|
||||
{themeModeOptions.map((option) => {
|
||||
const Icon = option.icon
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
class="settings-choice"
|
||||
data-selected={themeMode() === option.value ? "true" : "false"}
|
||||
onClick={() => setThemeMode(option.value)}
|
||||
>
|
||||
<span class="settings-choice-icon-wrap">
|
||||
<Icon class="settings-choice-icon" />
|
||||
</span>
|
||||
<span class="settings-choice-copy">
|
||||
<span class="settings-choice-label">{modeLabel(option.value)}</span>
|
||||
<span class="settings-choice-description">{t(`settings.appearance.theme.option.${option.value}`)}</span>
|
||||
</span>
|
||||
<span class="settings-choice-check" aria-hidden="true">
|
||||
<Check class="w-4 h-4" />
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof useI18n>["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 (
|
||||
<div class="settings-section-stack">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-heading-with-icon">
|
||||
<Bell class="settings-card-heading-icon" />
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("settings.notifications.sessionStatus.title")}</h3>
|
||||
<p class="settings-card-subtitle">{t("settings.notifications.sessionStatus.subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="settings-scope-badge">{t("settings.scope.device")}</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-stack">
|
||||
<div class="settings-toggle-row">
|
||||
<div>
|
||||
<div class="settings-toggle-title">{t("settings.notifications.enable.title")}</div>
|
||||
<div class="settings-toggle-caption">
|
||||
{t("settings.notifications.enable.permission", { permission: permissionLabel() })}
|
||||
</div>
|
||||
</div>
|
||||
<label class="settings-checkbox-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(preferences().osNotificationsEnabled)}
|
||||
disabled={!supported() && capability.state === "ready"}
|
||||
onChange={(event) => void handleEnableToggle(event.currentTarget.checked)}
|
||||
/>
|
||||
<span>{t("settings.common.enabled")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Show when={supported() && (capability()?.permission ?? "unsupported") !== "granted"}>
|
||||
<div class="settings-toggle-row settings-toggle-row-compact">
|
||||
<div>
|
||||
<div class="settings-toggle-title">{t("settings.notifications.requestPermission.title")}</div>
|
||||
<div class="settings-toggle-caption">{t("settings.notifications.requestPermission.subtitle")}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="selector-button selector-button-secondary w-auto whitespace-nowrap"
|
||||
onClick={() => void handleRequestPermission()}
|
||||
>
|
||||
{t("settings.notifications.requestPermission.action")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="settings-toggle-row">
|
||||
<div>
|
||||
<div class="settings-toggle-title">{t("settings.notifications.allowVisible.title")}</div>
|
||||
<div class="settings-toggle-caption">{t("settings.notifications.allowVisible.subtitle")}</div>
|
||||
</div>
|
||||
<label class="settings-checkbox-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(preferences().osNotificationsAllowWhenVisible)}
|
||||
disabled={!preferences().osNotificationsEnabled}
|
||||
onChange={(event) => updatePreferences({ osNotificationsAllowWhenVisible: event.currentTarget.checked })}
|
||||
/>
|
||||
<span>{t("settings.common.enabled")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Show when={Boolean(infoMessage())}>
|
||||
<div class="settings-inline-note">{infoMessage()}</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!supported() && capability.state === "ready"}>
|
||||
<div class="settings-inline-note">{t("settings.notifications.unsupportedNote")}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("settings.notifications.events.title")}</h3>
|
||||
<p class="settings-card-subtitle">{t("settings.notifications.events.subtitle")}</p>
|
||||
</div>
|
||||
<span class="settings-scope-badge">{t("settings.scope.device")}</span>
|
||||
</div>
|
||||
|
||||
<div class="settings-stack">
|
||||
<div class="settings-toggle-row">
|
||||
<div>
|
||||
<div class="settings-toggle-title">{t("settings.notifications.events.needsInput")}</div>
|
||||
</div>
|
||||
<label class="settings-checkbox-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(preferences().notifyOnNeedsInput)}
|
||||
disabled={!preferences().osNotificationsEnabled}
|
||||
onChange={(event) => updatePreferences({ notifyOnNeedsInput: event.currentTarget.checked })}
|
||||
/>
|
||||
<span>{t("settings.common.enabled")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="settings-toggle-row">
|
||||
<div>
|
||||
<div class="settings-toggle-title">{t("settings.notifications.events.idle")}</div>
|
||||
</div>
|
||||
<label class="settings-checkbox-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(preferences().notifyOnIdle)}
|
||||
disabled={!preferences().osNotificationsEnabled}
|
||||
onChange={(event) => updatePreferences({ notifyOnIdle: event.currentTarget.checked })}
|
||||
/>
|
||||
<span>{t("settings.common.enabled")}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div class="settings-section-stack">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-heading-with-icon">
|
||||
<Terminal class="settings-card-heading-icon" />
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("settings.opencode.runtime.title")}</h3>
|
||||
<p class="settings-card-subtitle">{t("settings.opencode.runtime.subtitle")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="settings-scope-badge settings-scope-badge-server">{t("settings.scope.server")}</span>
|
||||
</div>
|
||||
|
||||
<OpenCodeBinarySelector selectedBinary={selectedBinary()} onBinaryChange={handleBinaryChange} isVisible />
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("advancedSettings.environmentVariables.title")}</h3>
|
||||
<p class="settings-card-subtitle">{t("advancedSettings.environmentVariables.subtitle")}</p>
|
||||
</div>
|
||||
<span class="settings-scope-badge settings-scope-badge-server">{t("settings.scope.server")}</span>
|
||||
</div>
|
||||
<EnvironmentVariablesEditor />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<ServerMeta | null>(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<Record<string, string>>({})
|
||||
const [expandedUrl, setExpandedUrl] = createSignal<string | null>(null)
|
||||
const [error, setError] = createSignal<string | null>(null)
|
||||
const [passwordFormOpen, setPasswordFormOpen] = createSignal(false)
|
||||
const [passwordValue, setPasswordValue] = createSignal("")
|
||||
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
||||
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
||||
const [savingPassword, setSavingPassword] = createSignal(false)
|
||||
|
||||
const addresses = createMemo<NetworkAddress[]>(() => 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 (
|
||||
<div class="settings-section-stack">
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-heading-with-icon">
|
||||
<Shield class="settings-card-heading-icon" />
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("remoteAccess.sections.listeningMode.label")}</h3>
|
||||
<p class="settings-card-subtitle">{t("remoteAccess.sections.listeningMode.help")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-toolbar-inline">
|
||||
<span class="settings-scope-badge settings-scope-badge-server">{t("settings.scope.server")}</span>
|
||||
<button
|
||||
class="selector-button selector-button-secondary w-auto"
|
||||
type="button"
|
||||
onClick={() => void refreshMeta()}
|
||||
disabled={loading()}
|
||||
>
|
||||
<RefreshCw class={`w-4 h-4 ${loading() ? "remote-spin" : ""}`} />
|
||||
<span>{t("remoteAccess.refresh")}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Switch
|
||||
class="remote-toggle"
|
||||
checked={allowExternalConnections()}
|
||||
onChange={(nextChecked) => void handleAllowConnectionsChange(nextChecked)}
|
||||
disabled={loading() || applyingListeningMode()}
|
||||
>
|
||||
<Switch.Input />
|
||||
<Switch.Control class="remote-toggle-switch" data-checked={allowExternalConnections()}>
|
||||
<span class="remote-toggle-state">
|
||||
{allowExternalConnections() ? t("remoteAccess.toggle.on") : t("remoteAccess.toggle.off")}
|
||||
</span>
|
||||
<Switch.Thumb class="remote-toggle-thumb" />
|
||||
</Switch.Control>
|
||||
<div class="remote-toggle-copy">
|
||||
<span class="remote-toggle-title">{t("remoteAccess.toggle.title")}</span>
|
||||
<span class="remote-toggle-caption">
|
||||
{allowExternalConnections()
|
||||
? t("remoteAccess.toggle.caption.all")
|
||||
: t("remoteAccess.toggle.caption.local")}
|
||||
</span>
|
||||
</div>
|
||||
</Switch>
|
||||
|
||||
<p class="remote-toggle-note">{t("remoteAccess.toggle.note")}</p>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-heading-with-icon">
|
||||
<Shield class="settings-card-heading-icon" />
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("remoteAccess.sections.serverPassword.label")}</h3>
|
||||
<p class="settings-card-subtitle">{t("remoteAccess.sections.serverPassword.help")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="settings-scope-badge settings-scope-badge-server">{t("settings.scope.server")}</span>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={authStatus() && authStatus()!.authenticated}
|
||||
fallback={<div class="settings-card-message">{t("remoteAccess.authStatus.unavailable")}</div>}
|
||||
>
|
||||
<div class="settings-card-content">
|
||||
<p class="settings-help-text">{t("remoteAccess.username", { username: authStatus()!.username ?? "codenomad" })}</p>
|
||||
<p class="settings-help-text">
|
||||
{authStatus()!.passwordUserProvided
|
||||
? t("remoteAccess.password.status.set")
|
||||
: t("remoteAccess.password.status.unset")}
|
||||
</p>
|
||||
|
||||
<div class="settings-password-actions">
|
||||
<button
|
||||
class="settings-pill-button"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setPasswordFormOpen(!passwordFormOpen())
|
||||
setPasswordError(null)
|
||||
}}
|
||||
>
|
||||
{passwordFormOpen()
|
||||
? t("remoteAccess.password.actions.cancel")
|
||||
: authStatus()!.passwordUserProvided
|
||||
? t("remoteAccess.password.actions.change")
|
||||
: t("remoteAccess.password.actions.set")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={passwordFormOpen()}>
|
||||
<div class="settings-form-group">
|
||||
<label class="settings-form-label">{t("remoteAccess.password.form.newPassword")}</label>
|
||||
<input
|
||||
class="selector-input w-full"
|
||||
type="password"
|
||||
value={passwordValue()}
|
||||
onInput={(event) => setPasswordValue(event.currentTarget.value)}
|
||||
placeholder={t("remoteAccess.password.form.placeholder")}
|
||||
/>
|
||||
</div>
|
||||
<div class="settings-form-group">
|
||||
<label class="settings-form-label">{t("remoteAccess.password.form.confirmPassword")}</label>
|
||||
<input
|
||||
class="selector-input w-full"
|
||||
type="password"
|
||||
value={passwordConfirm()}
|
||||
onInput={(event) => setPasswordConfirm(event.currentTarget.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Show when={passwordError()}>
|
||||
{(message) => <div class="settings-error-message">{message()}</div>}
|
||||
</Show>
|
||||
|
||||
<div class="settings-password-actions">
|
||||
<button class="settings-pill-button" type="button" disabled={savingPassword()} onClick={() => void handleSubmitPassword()}>
|
||||
{savingPassword() ? t("remoteAccess.password.save.saving") : t("remoteAccess.password.save.label")}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-card-header">
|
||||
<div class="settings-card-heading-with-icon">
|
||||
<Wifi class="settings-card-heading-icon" />
|
||||
<div>
|
||||
<h3 class="settings-card-title">{t("remoteAccess.sections.addresses.label")}</h3>
|
||||
<p class="settings-card-subtitle">{t("remoteAccess.sections.addresses.help")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="settings-scope-badge settings-scope-badge-server">{t("settings.scope.server")}</span>
|
||||
</div>
|
||||
|
||||
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
||||
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
||||
<Show
|
||||
when={displayAddresses().length > 0 || meta()?.localUrl}
|
||||
fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}
|
||||
>
|
||||
<div class="remote-address-list">
|
||||
<Show when={meta()?.localUrl}>
|
||||
{(url) => {
|
||||
const value = () => url()
|
||||
const expandedState = () => expandedUrl() === value()
|
||||
const qr = () => qrCodes()[value()]
|
||||
return (
|
||||
<div class="remote-address">
|
||||
<div class="remote-address-main">
|
||||
<div>
|
||||
<p class="remote-address-url">{value()}</p>
|
||||
<p class="remote-address-meta">{t("remoteAccess.address.scope.loopback")}</p>
|
||||
</div>
|
||||
<div class="remote-actions">
|
||||
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(value())}>
|
||||
<ExternalLink class="remote-icon" />
|
||||
{t("remoteAccess.address.open")}
|
||||
</button>
|
||||
<button
|
||||
class="remote-pill"
|
||||
type="button"
|
||||
onClick={() => void toggleExpanded(value())}
|
||||
aria-expanded={expandedState()}
|
||||
>
|
||||
<Link2 class="remote-icon" />
|
||||
{expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={expandedState()}>
|
||||
<div class="remote-qr">
|
||||
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||
{(dataUrl) => (
|
||||
<img
|
||||
src={dataUrl()}
|
||||
alt={t("remoteAccess.address.qrAlt", { url: value() })}
|
||||
class="remote-qr-img"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Show>
|
||||
|
||||
<For each={displayAddresses()}>
|
||||
{(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 (
|
||||
<div class="remote-address">
|
||||
<div class="remote-address-main">
|
||||
<div>
|
||||
<p class="remote-address-url">{url}</p>
|
||||
<p class="remote-address-meta">
|
||||
{address.family.toUpperCase()} - {scopeLabel()} - {address.ip}
|
||||
</p>
|
||||
</div>
|
||||
<div class="remote-actions">
|
||||
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}>
|
||||
<ExternalLink class="remote-icon" />
|
||||
{t("remoteAccess.address.open")}
|
||||
</button>
|
||||
<button
|
||||
class="remote-pill"
|
||||
type="button"
|
||||
onClick={() => void toggleExpanded(url)}
|
||||
aria-expanded={expandedState()}
|
||||
>
|
||||
<Link2 class="remote-icon" />
|
||||
{expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={expandedState()}>
|
||||
<div class="remote-qr">
|
||||
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||
{(dataUrl) => (
|
||||
<img src={dataUrl()} alt={t("remoteAccess.address.qrAlt", { url })} class="remote-qr-img" />
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "ワークスペースの起動に失敗しました",
|
||||
|
||||
@@ -22,6 +22,7 @@ export const folderSelectionMessages = {
|
||||
"folderSelection.browse.buttonOpening": "開いています...",
|
||||
|
||||
"folderSelection.advancedSettings": "詳細設定",
|
||||
"folderSelection.opencode": "OpenCode",
|
||||
|
||||
"folderSelection.hints.navigate": "移動",
|
||||
"folderSelection.hints.select": "選択",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "Не удалось запустить рабочее пространство",
|
||||
|
||||
@@ -22,6 +22,7 @@ export const folderSelectionMessages = {
|
||||
"folderSelection.browse.buttonOpening": "Открытие…",
|
||||
|
||||
"folderSelection.advancedSettings": "Расширенные настройки",
|
||||
"folderSelection.opencode": "OpenCode",
|
||||
|
||||
"folderSelection.hints.navigate": "Навигация",
|
||||
"folderSelection.hints.select": "Выбрать",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": "启动工作区失败",
|
||||
|
||||
@@ -22,6 +22,7 @@ export const folderSelectionMessages = {
|
||||
"folderSelection.browse.buttonOpening": "正在打开...",
|
||||
|
||||
"folderSelection.advancedSettings": "高级设置",
|
||||
"folderSelection.opencode": "OpenCode",
|
||||
|
||||
"folderSelection.hints.navigate": "导航",
|
||||
"folderSelection.hints.select": "选择",
|
||||
|
||||
@@ -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
|
||||
|
||||
17
packages/ui/src/stores/settings-screen.ts
Normal file
17
packages/ui/src/stores/settings-screen.ts
Normal file
@@ -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<SettingsSectionId>("appearance")
|
||||
|
||||
export function openSettings(section: SettingsSectionId = "appearance") {
|
||||
setActiveSettingsSection(section)
|
||||
setSettingsOpen(true)
|
||||
}
|
||||
|
||||
export function closeSettings() {
|
||||
setSettingsOpen(false)
|
||||
}
|
||||
|
||||
export { settingsOpen, activeSettingsSection, setActiveSettingsSection }
|
||||
516
packages/ui/src/styles/components/settings-screen.css
Normal file
516
packages/ui/src/styles/components/settings-screen.css
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -8,3 +8,4 @@
|
||||
@import "./components/directory-browser.css";
|
||||
@import "./components/remote-access.css";
|
||||
@import "./components/permission-notification.css";
|
||||
@import "./components/settings-screen.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;
|
||||
|
||||
Reference in New Issue
Block a user