Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 13 additions & 45 deletions desktop/src/renderer/src/lib/components/update/UpdatesPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,10 @@ onMount(async () => {
})
</script>

<div class="space-y-8">
<!-- Status hero -->
<section class="space-y-3">
<div class="space-y-6">
<section aria-label="Update status">
<div class="rounded-lg border p-4">
<div class="flex items-start gap-3">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start">
<div class="mt-0.5 shrink-0">
{#if s.state === "checking"}
<Loader2 class="h-5 w-5 animate-spin text-muted-foreground" />
Expand Down Expand Up @@ -131,20 +130,9 @@ onMount(async () => {
</div>
{:else if s.state === "available"}
<p class="text-sm font-medium">Devsy {s.availableVersion} is available</p>
<div class="grid grid-cols-3 gap-2 rounded-md border bg-muted/40 p-2 text-xs">
<div>
<span class="text-muted-foreground">Installed:</span>
<span class="font-mono font-medium ml-1">v{installedVersion || "unknown"}</span>
</div>
<div>
<span class="text-muted-foreground">Available:</span>
<span class="font-mono font-medium text-primary ml-1">v{s.availableVersion}</span>
</div>
<div>
<span class="text-muted-foreground">Channel:</span>
<span class="font-medium ml-1">{channelLabel(releaseChannel)}</span>
</div>
</div>
<p class="text-xs text-muted-foreground">
Installed v{installedVersion || "unknown"} · {channelLabel(releaseChannel)} channel
</p>
{:else if s.state === "downloading"}
<p class="text-sm font-medium">Downloading Devsy {s.availableVersion}</p>
<Progress value={s.progress.percent} max={100} />
Expand Down Expand Up @@ -206,7 +194,7 @@ onMount(async () => {
{/if}
</div>

<div class="shrink-0">
<div class="shrink-0 self-start">
{#if s.state === "available"}
<Button size="sm" onclick={() => downloadUpdate()}>Download update</Button>
{:else if s.state === "downloaded"}
Expand Down Expand Up @@ -241,12 +229,8 @@ onMount(async () => {

<Separator />

<!-- Release channel -->
<section class="space-y-3">
<div>
<Label>Release Channel</Label>
<p class="text-xs text-muted-foreground">Choose how early you receive new versions</p>
</div>
<Label>Release channel</Label>
<div class="grid grid-cols-2 gap-3" role="radiogroup" aria-label="Release Channel">
{#each CHANNELS as c (c.value)}
<button
Expand All @@ -264,23 +248,17 @@ onMount(async () => {
<CheckCircle2 class="h-3.5 w-3.5 text-primary" />
{/if}
</div>
<p class="mt-1 text-xs text-muted-foreground">{c.cadence}</p>
{#if c.unstable}
<p class="mt-1 text-xs text-yellow-600 dark:text-yellow-400">{c.description}</p>
{:else}
<p class="mt-1 text-xs text-muted-foreground">{c.description}</p>
{/if}
<p class="mt-1 text-xs {c.unstable ? 'text-yellow-600 dark:text-yellow-400' : 'text-muted-foreground'}">{c.cadence}</p>
</button>
{/each}
</div>
</section>

<Separator />

<!-- Update behavior -->
<section class="space-y-3">
<Label>Update Behavior</Label>
<div class="flex items-center justify-between">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<p class="text-sm">Download updates automatically</p>
<p class="text-xs text-muted-foreground">
Expand All @@ -291,19 +269,9 @@ onMount(async () => {
</div>
</section>

<Separator />

<!-- Version footer -->
<section class="flex items-center justify-between rounded-lg border p-3">
<div>
<p class="text-sm font-medium">Devsy</p>
{#if appVersion}
<p class="font-mono text-xs text-muted-foreground">v{appVersion}</p>
{:else}
<p class="text-xs text-muted-foreground">Version unavailable</p>
{/if}
</div>
<span class="text-xs text-muted-foreground">{channelLabel(releaseChannel)} channel</span>
<section class="flex items-center justify-between text-xs text-muted-foreground">
<span>Devsy {appVersion ? `v${appVersion}` : "version unavailable"}</span>
<span>{channelLabel(releaseChannel)} channel</span>
</section>
</div>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,8 @@ describe("UpdatesPanel status display", () => {
await tick()

expect(document.body.textContent).toMatch(/devsy 1\.18\.0 is available/i)
expect(document.body.textContent).toMatch(/installed:\s*v1\.17\.0/i)
expect(document.body.textContent).toMatch(/available:\s*v1\.18\.0/i)
expect(document.body.textContent).toMatch(/installed v1\.17\.0/i)
expect(document.body.textContent).toMatch(/devsy 1\.18\.0 is available/i)
const btn = Array.from(document.querySelectorAll("button")).find((b) =>
/download update/i.test(b.textContent ?? ""),
)
Expand Down
109 changes: 37 additions & 72 deletions desktop/src/renderer/src/pages/SettingsPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,10 @@ import { Label } from "$lib/components/ui/label/index.js"
import { Separator } from "$lib/components/ui/separator/index.js"
import * as Command from "$lib/components/ui/command/index.js"
import * as Popover from "$lib/components/ui/popover/index.js"
import * as Tabs from "$lib/components/ui/tabs/index.js"
import { Switch } from "$lib/components/ui/switch/index.js"
import {
theme,
applyTheme,
colorScheme,
setColorScheme,
uiScale,
applyUIScale,
defaultIde,
Expand All @@ -25,16 +22,13 @@ import {
} from "$lib/stores/settings.js"
import type {
Theme,
ColorScheme,
UIScale,
LocalOptions,
OnBuildFailure,
} from "$lib/stores/settings.js"
import * as Select from "$lib/components/ui/select/index.js"
import UpdatesPanel from "$lib/components/update/UpdatesPanel.svelte"
import { Skeleton } from "$lib/components/ui/skeleton/index.js"
import { toasts } from "$lib/stores/toasts.js"
import { extractErrorMessage } from "$lib/utils/error.js"
import { trackEngagement } from "$lib/analytics.js"

const THEMES: { value: Theme; label: string }[] = [
Expand All @@ -48,12 +42,6 @@ function setTheme(value: Theme) {
applyTheme(value)
}

const COLOR_SCHEMES: { value: ColorScheme; label: string; swatch: string }[] = [
{ value: "default", label: "White", swatch: "bg-foreground" },
{ value: "emerald", label: "Emerald", swatch: "bg-emerald-600" },
{ value: "purple", label: "Purple", swatch: "bg-purple-600" },
]

const UI_SCALES: { value: UIScale; label: string }[] = [
{ value: "xs", label: "Extra Small" },
{ value: "sm", label: "Small" },
Expand Down Expand Up @@ -95,7 +83,6 @@ const IDE_OPTIONS = [
{ value: "rstudio", label: "RStudio Server" },
]

let activeTab = $state("general")
let loading = $state(true)
let saving = $state(false)
let ideComboOpen = $state(false)
Expand Down Expand Up @@ -152,42 +139,37 @@ function toggleLocal(key: keyof LocalOptions) {
}
</script>

<div class="mx-auto w-full max-w-2xl space-y-6">
<div class="space-y-6">
<h1 class="text-2xl font-bold">Settings</h1>

<Tabs.Root bind:value={activeTab} class="w-full">
<Tabs.List variant="line" class="w-full flex-wrap">
<Tabs.Trigger value="general">General</Tabs.Trigger>
<Tabs.Trigger value="appearance">Appearance</Tabs.Trigger>
<Tabs.Trigger value="updates">Updates</Tabs.Trigger>
<Tabs.Trigger value="experimental">Experimental</Tabs.Trigger>
</Tabs.List>

<Tabs.Content value="general" class="w-full">
<div class="grid items-start gap-6 lg:grid-cols-[10rem_minmax(0,1fr)]">
<nav aria-label="Settings sections" class="flex gap-1 overflow-x-auto lg:sticky lg:top-0 lg:flex-col">
<a class="rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-muted hover:text-foreground" href="#general">General</a>
<a class="rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-muted hover:text-foreground" href="#appearance">Appearance</a>
<a class="rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-muted hover:text-foreground" href="#updates">Updates</a>
<a class="rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-muted hover:text-foreground" href="#advanced">Advanced</a>
</nav>

<div class="min-w-0 max-w-3xl space-y-6">
<section id="general" aria-labelledby="general-heading" class="scroll-mt-6 rounded-lg border p-4 sm:p-6">
<h2 id="general-heading" class="text-lg font-semibold">General</h2>
{#if loading}
<div class="mt-4 space-y-6">
<!-- Toggle row skeleton (Debug Mode) -->
<div class="flex items-center justify-between">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="space-y-1.5">
<Skeleton class="h-4 w-32" />
<Skeleton class="h-3 w-48" />
</div>
<Skeleton class="h-5 w-10 rounded-full" />
</div>

<!-- Input row skeleton (SSH Key) -->
<div class="space-y-2">
<Skeleton class="h-4 w-40" />
<Skeleton class="h-3 w-56" />
<Skeleton class="h-9 w-full" />
</div>

<Separator />

<!-- Section header (Proxy Configuration) -->
<Skeleton class="h-5 w-40" />

<!-- Input row skeletons (HTTP Proxy, HTTPS Proxy) -->
<div class="space-y-2">
<Skeleton class="h-4 w-24" />
<Skeleton class="h-9 w-full" />
Expand All @@ -199,15 +181,15 @@ function toggleLocal(key: keyof LocalOptions) {
</div>
{:else}
<div class="mt-4 space-y-6">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<Label>Debug Mode</Label>
<p class="text-xs text-muted-foreground">Run all commands with --debug flag</p>
</div>
<Switch checked={local.debugFlag} onCheckedChange={() => toggleLocal("debugFlag")} disabled={loading || saving} />
</div>

<div class="flex items-center justify-between">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<Label>On Build Failure</Label>
<p class="text-xs text-muted-foreground">What to do when a dev container build fails</p>
Expand All @@ -222,7 +204,7 @@ function toggleLocal(key: keyof LocalOptions) {
}
}}
>
<Select.Trigger class="w-[280px] h-9">
<Select.Trigger class="h-9 w-full sm:w-[280px]">
<span>{ON_BUILD_FAILURE_OPTIONS.find((o) => o.value === local.onBuildFailure)?.label ?? "Prompt with recovery options"}</span>
</Select.Trigger>
<Select.Content>
Expand Down Expand Up @@ -297,13 +279,14 @@ function toggleLocal(key: keyof LocalOptions) {

</div>
{/if}
</Tabs.Content>
</section>

<Tabs.Content value="appearance" class="w-full">
<div class="mt-4 space-y-6">
<section id="appearance" aria-labelledby="appearance-heading" class="scroll-mt-6 rounded-lg border p-4 sm:p-6">
<h2 id="appearance-heading" class="text-lg font-semibold">Appearance</h2>
<div class="mt-4 space-y-6">
<div class="space-y-2">
<h2 class="text-lg font-semibold">Theme</h2>
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
{#each THEMES as t (t.value)}
<Button
variant={$theme === t.value ? "default" : "outline"}
Expand All @@ -317,31 +300,11 @@ function toggleLocal(key: keyof LocalOptions) {

<Separator />

<!-- Color Scheme picker kept but hidden — settled on purple. Re-enable by switching to {#if true}. -->
{#if false}
<div class="space-y-2">
<h2 class="text-lg font-semibold">Color Scheme</h2>
<div class="flex gap-2">
{#each COLOR_SCHEMES as c (c.value)}
<Button
variant={$colorScheme === c.value ? "default" : "outline"}
onclick={() => setColorScheme(c.value)}
class="gap-2"
>
<span class="h-3 w-3 rounded-full {c.swatch}"></span>
{c.label}
</Button>
{/each}
</div>
</div>

<Separator />
{/if}

<div class="space-y-2">
<h2 class="text-lg font-semibold">UI Scale</h2>
<p class="text-xs text-muted-foreground">Adjust the overall size of text and interface elements</p>
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
{#each UI_SCALES as s (s.value)}
<Button
variant={$uiScale === s.value ? "default" : "outline"}
Expand All @@ -354,24 +317,25 @@ function toggleLocal(key: keyof LocalOptions) {
</div>

</div>
</Tabs.Content>
</section>

<!-- Updates -->
<Tabs.Content value="updates" class="w-full">
<div class="mt-4">
<UpdatesPanel />
</div>
</Tabs.Content>
<section id="updates" aria-labelledby="updates-heading" class="scroll-mt-6 rounded-lg border p-4 sm:p-6">
<h2 id="updates-heading" class="text-lg font-semibold">Updates</h2>
<div class="mt-4">
<UpdatesPanel />
</div>
</section>

<Tabs.Content value="experimental" class="w-full">
<div class="mt-4 space-y-6">
<section id="advanced" aria-labelledby="advanced-heading" class="scroll-mt-6 rounded-lg border p-4 sm:p-6">
<h2 id="advanced-heading" class="text-lg font-semibold">Advanced</h2>
<div class="mt-4 space-y-6">
<div class="rounded-md border border-yellow-500/30 bg-yellow-500/5 p-3">
<p class="text-sm text-yellow-600 dark:text-yellow-400">
Experimental features may be unstable. Use at your own risk.
</p>
</div>

<div class="flex items-center justify-between">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<Label>Multiple Devcontainer Detection</Label>
<p class="text-xs text-muted-foreground">Check for multiple devcontainers when creating workspaces. May take longer for larger repos.</p>
Expand Down Expand Up @@ -404,7 +368,8 @@ function toggleLocal(key: keyof LocalOptions) {
disabled={loading || saving}
/>
</div>
</div>
</Tabs.Content>
</Tabs.Root>
</div>
</section>
</div>
</div>
</div>
Loading