Group settings into categories

This commit is contained in:
2026-07-18 19:40:47 -04:00
parent 5cb37ff80d
commit 1f9d08920c
3 changed files with 229 additions and 234 deletions
-107
View File
@@ -1,107 +0,0 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import { openUrl } from "@tauri-apps/plugin-opener";
import Modal from "./Modal.svelte";
import * as api from "$lib/ts/api";
import type { AppInfo } from "$lib/ts/api";
interface Props {
onclose: () => void;
}
let { onclose }: Props = $props();
let info = $state<AppInfo | null>(null);
$effect(() => {
api
.appInfo()
.then((result) => (info = result))
.catch(() => (info = null));
});
const links = [
{
label: "Typst Documentation",
url: "https://typst.app/docs/",
icon: "ph:book-open",
},
{
label: "Typst Universe",
url: "https://typst.app/universe/",
icon: "ph:planet",
},
];
</script>
<Modal title="About Typst Desktop" icon="ph:info" {onclose}>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<Icon icon="ph:file-code" class="text-3xl text-[var(--color-accent)]" />
<div>
<p class="text-sm font-semibold">Typst Desktop</p>
<p class="text-xs text-[var(--color-ink-muted)]">
A local editor for Typst documents
</p>
</div>
</div>
{#if info}
<dl class="flex flex-col gap-1 text-xs">
<div class="flex justify-between border-b border-[var(--color-line)] py-1.5">
<dt class="text-[var(--color-ink-muted)]">Version</dt>
<dd class="font-medium">{info.version}</dd>
</div>
<div class="flex justify-between border-b border-[var(--color-line)] py-1.5">
<dt class="text-[var(--color-ink-muted)]">Typst</dt>
<dd class="font-medium">{info.typst_version}</dd>
</div>
<div class="flex justify-between border-b border-[var(--color-line)] py-1.5">
<dt class="text-[var(--color-ink-muted)]">Tauri</dt>
<dd class="font-medium">{info.tauri_version}</dd>
</div>
<div class="flex justify-between border-b border-[var(--color-line)] py-1.5">
<dt class="text-[var(--color-ink-muted)]">Author</dt>
<dd class="font-medium">{info.authors}</dd>
</div>
<div class="flex justify-between py-1.5">
<dt class="text-[var(--color-ink-muted)]">License</dt>
<dd class="font-medium">{info.license}</dd>
</div>
</dl>
{/if}
<div class="flex flex-col gap-1.5">
{#each links as link}
<button
class="flex items-center gap-2 rounded-md border border-[var(--color-line)] px-3 py-2 text-xs transition hover:border-[var(--color-accent)] hover:bg-[var(--color-surface-muted)]"
onclick={() => openUrl(link.url)}
>
<Icon icon={link.icon} class="text-base text-[var(--color-accent)]" />
<span class="flex-1 text-left">{link.label}</span>
<Icon
icon="ph:arrow-square-out"
class="text-[var(--color-ink-muted)]"
/>
</button>
{/each}
</div>
<p
class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface-muted)] p-3 text-[11px] leading-relaxed text-[var(--color-ink-muted)]"
>
Typst Desktop is a community application and is not affiliated with,
endorsed by, or supported by the official Typst project. The links above
open the official Typst website in your browser.
</p>
</div>
{#snippet footer()}
<button
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90"
onclick={onclose}
>
Close
</button>
{/snippet}
</Modal>
+229 -115
View File
@@ -1,9 +1,11 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import { untrack } from "svelte";
import { open } from "@tauri-apps/plugin-dialog";
import { openUrl } from "@tauri-apps/plugin-opener";
import Modal from "./Modal.svelte";
import * as api from "$lib/ts/api";
import { untrack } from "svelte";
import type { AppInfo } from "$lib/ts/api";
import {
app,
applyTheme,
@@ -19,11 +21,32 @@
let { onclose, onsignin }: Props = $props();
type Section = "files" | "appearance" | "account" | "about";
const sections: { id: Section; label: string; icon: string }[] = [
{ id: "files", label: "Files", icon: "ph:folder" },
{ id: "appearance", label: "Appearance", icon: "ph:palette" },
{ id: "account", label: "Account", icon: "ph:user-circle" },
{ id: "about", label: "About", icon: "ph:info" },
];
let section = $state<Section>("files");
let workspaceRoot = $state(untrack(() => app.settings?.workspace_root ?? ""));
let serverUrl = $state(untrack(() => app.settings?.server_url ?? ""));
let autosaveSeconds = $state(untrack(() => app.settings?.autosave_seconds ?? 0));
let syncMinutes = $state(untrack(() => app.settings?.sync_minutes ?? 0));
let saving = $state(false);
let info = $state<AppInfo | null>(null);
$effect(() => {
if (section === "about" && !info) {
api
.appInfo()
.then((result) => (info = result))
.catch(() => (info = null));
}
});
const autosaveOptions = [
{ value: 0, label: "Off" },
@@ -39,6 +62,19 @@
{ value: 5, label: "5 minutes" },
];
const links = [
{
label: "Typst Documentation",
url: "https://typst.app/docs/",
icon: "ph:book-open",
},
{
label: "Typst Universe",
url: "https://typst.app/universe/",
icon: "ph:planet",
},
];
async function browse() {
const selected = await open({ directory: true, multiple: false });
if (typeof selected === "string") {
@@ -75,120 +111,196 @@
setError(error);
}
}
const fieldClass =
"rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none";
</script>
<Modal title="Settings" icon="ph:gear-six" {onclose}>
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">Workspace folder</span>
<div class="flex gap-2">
<input
class="flex-1 rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none"
bind:value={workspaceRoot}
/>
<Modal title="Settings" icon="ph:gear-six" width="max-w-3xl" {onclose}>
<div class="flex min-h-[360px] gap-5">
<nav class="flex w-36 shrink-0 flex-col gap-0.5">
{#each sections as item}
<button
class="flex items-center gap-1 rounded-md border border-[var(--color-line)] px-3 text-xs hover:bg-[var(--color-surface-muted)]"
onclick={browse}
class="flex items-center gap-2 rounded-md px-2.5 py-2 text-xs transition
{section === item.id
? 'bg-[var(--color-accent-soft)] font-medium text-[var(--color-accent)]'
: 'text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]'}"
onclick={() => (section = item.id)}
>
<Icon icon="ph:folder-open" />
Browse
<Icon icon={item.icon} class="text-base" />
{item.label}
</button>
</div>
<span class="text-[var(--color-ink-muted)]">
Projects are stored as plain folders here.
</span>
</div>
{/each}
</nav>
<div class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">TypstDrive server</span>
<input
class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none"
bind:value={serverUrl}
/>
</div>
<div class="min-w-0 flex-1">
{#if section === "files"}
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">
Workspace folder
</span>
<div class="flex gap-2">
<input class="flex-1 {fieldClass}" bind:value={workspaceRoot} />
<button
class="flex items-center gap-1 rounded-md border border-[var(--color-line)] px-3 text-xs hover:bg-[var(--color-surface-muted)]"
onclick={browse}
>
<Icon icon="ph:folder-open" />
Browse
</button>
</div>
<span class="text-[var(--color-ink-muted)]">
Projects and documents are stored as plain files here.
</span>
</div>
<div class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">Autosave</span>
<select
class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none"
bind:value={autosaveSeconds}
>
{#each autosaveOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<span class="text-[var(--color-ink-muted)]">
Saves the file being edited after you stop typing.
</span>
</div>
<div class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">Automatic sync</span>
<select
class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface)] px-3 py-2 text-sm focus:border-[var(--color-accent)] focus:outline-none"
bind:value={syncMinutes}
>
{#each syncOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<span class="text-[var(--color-ink-muted)]">
Pulls and pushes cloud-linked projects on a timer. Conflicts pause
syncing until they are resolved.
</span>
</div>
<div
class="flex items-center gap-3 rounded-md border border-[var(--color-line)] bg-[var(--color-surface-muted)] p-3"
>
<Icon
icon={app.account ? "ph:user-circle-check" : "ph:user-circle"}
class="text-2xl {app.account ? 'text-[var(--color-success)]' : 'text-[var(--color-ink-muted)]'}"
/>
<div class="flex-1 text-xs">
{#if app.account}
<p class="font-medium">{app.account.username}</p>
<p class="text-[var(--color-ink-muted)]">{app.account.email}</p>
{:else}
<p class="font-medium">Not connected</p>
<p class="text-[var(--color-ink-muted)]">
Sign in to sync projects to the cloud.
</p>
{/if}
</div>
{#if app.account}
<button
class="rounded-md border border-[var(--color-line)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)]"
onclick={signOut}
>
Sign out
</button>
{:else}
<button
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white hover:opacity-90"
onclick={onsignin}
>
Sign in
</button>
{/if}
</div>
<div class="flex items-center justify-between text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">Appearance</span>
<div class="flex gap-1">
{#each [["light", "ph:sun"], ["dark", "ph:moon"]] as [value, icon]}
<button
class="flex items-center gap-1.5 rounded-md border px-3 py-1.5 transition
{app.theme === value
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
onclick={() => applyTheme(value as "light" | "dark")}
<div class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">Autosave</span>
<select class={fieldClass} bind:value={autosaveSeconds}>
{#each autosaveOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<span class="text-[var(--color-ink-muted)]">
Saves the file being edited after you stop typing.
</span>
</div>
</div>
{:else if section === "appearance"}
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-2 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">Theme</span>
<div class="flex gap-2">
{#each [["light", "Light", "ph:sun"], ["dark", "Dark", "ph:moon"]] as [value, label, icon]}
<button
class="flex flex-1 items-center justify-center gap-1.5 rounded-md border px-3 py-2.5 transition
{app.theme === value
? 'border-[var(--color-accent)] bg-[var(--color-accent-soft)] text-[var(--color-accent)]'
: 'border-[var(--color-line)] text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-muted)]'}"
onclick={() => applyTheme(value as "light" | "dark")}
>
<Icon {icon} class="text-base" />
{label}
</button>
{/each}
</div>
<span class="text-[var(--color-ink-muted)]">
Applies immediately to the editor and preview.
</span>
</div>
</div>
{:else if section === "account"}
<div class="flex flex-col gap-5">
<div
class="flex items-center gap-3 rounded-md border border-[var(--color-line)] bg-[var(--color-surface-muted)] p-3"
>
<Icon {icon} />
{value === "light" ? "Light" : "Dark"}
</button>
{/each}
</div>
<Icon
icon={app.account ? "ph:user-circle-check" : "ph:user-circle"}
class="text-2xl {app.account
? 'text-[var(--color-success)]'
: 'text-[var(--color-ink-muted)]'}"
/>
<div class="flex-1 text-xs">
{#if app.account}
<p class="font-medium">{app.account.username}</p>
<p class="text-[var(--color-ink-muted)]">{app.account.email}</p>
{:else}
<p class="font-medium">Not connected</p>
<p class="text-[var(--color-ink-muted)]">
Sign in to sync projects to the cloud.
</p>
{/if}
</div>
{#if app.account}
<button
class="rounded-md border border-[var(--color-line)] px-3 py-1.5 text-xs hover:bg-[var(--color-surface)]"
onclick={signOut}
>
Sign out
</button>
{:else}
<button
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white hover:opacity-90"
onclick={onsignin}
>
Sign in
</button>
{/if}
</div>
<div class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">
TypstDrive server
</span>
<input class={fieldClass} bind:value={serverUrl} />
</div>
<div class="flex flex-col gap-1 text-xs">
<span class="font-medium text-[var(--color-ink-muted)]">
Automatic sync
</span>
<select class={fieldClass} bind:value={syncMinutes}>
{#each syncOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
<span class="text-[var(--color-ink-muted)]">
Pulls and pushes cloud-linked projects on a timer. Conflicts pause
syncing until they are resolved.
</span>
</div>
</div>
{:else}
<div class="flex flex-col gap-4">
<div class="flex items-center gap-3">
<Icon icon="ph:file-code" class="text-3xl text-[var(--color-accent)]" />
<div>
<p class="text-sm font-semibold">Typst Desktop</p>
<p class="text-xs text-[var(--color-ink-muted)]">
A local editor for Typst documents
</p>
</div>
</div>
{#if info}
<dl class="flex flex-col text-xs">
{#each [["Version", info.version], ["Typst", info.typst_version], ["Tauri", info.tauri_version], ["Author", info.authors], ["License", info.license]] as [label, value]}
<div
class="flex justify-between border-b border-[var(--color-line)] py-1.5 last:border-0"
>
<dt class="text-[var(--color-ink-muted)]">{label}</dt>
<dd class="font-medium">{value}</dd>
</div>
{/each}
</dl>
{/if}
<div class="flex flex-col gap-1.5">
{#each links as link}
<button
class="flex items-center gap-2 rounded-md border border-[var(--color-line)] px-3 py-2 text-xs transition hover:border-[var(--color-accent)] hover:bg-[var(--color-surface-muted)]"
onclick={() => openUrl(link.url)}
>
<Icon icon={link.icon} class="text-base text-[var(--color-accent)]" />
<span class="flex-1 text-left">{link.label}</span>
<Icon
icon="ph:arrow-square-out"
class="text-[var(--color-ink-muted)]"
/>
</button>
{/each}
</div>
<p
class="rounded-md border border-[var(--color-line)] bg-[var(--color-surface-muted)] p-3 text-[11px] leading-relaxed text-[var(--color-ink-muted)]"
>
Typst Desktop is a community application and is not affiliated with,
endorsed by, or supported by the official Typst project. The links
above open the official Typst website in your browser.
</p>
</div>
{/if}
</div>
</div>
@@ -197,14 +309,16 @@
class="rounded-md px-3 py-1.5 text-xs text-[var(--color-ink-muted)] hover:bg-[var(--color-surface-sunken)]"
onclick={onclose}
>
Cancel
</button>
<button
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
disabled={saving}
onclick={save}
>
Save
{section === "about" || section === "appearance" ? "Close" : "Cancel"}
</button>
{#if section !== "about" && section !== "appearance"}
<button
class="rounded-md bg-[var(--color-accent)] px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:opacity-50"
disabled={saving}
onclick={save}
>
{saving ? "Saving..." : "Save"}
</button>
{/if}
{/snippet}
</Modal>
-12
View File
@@ -17,7 +17,6 @@
import AssetsModal from "$lib/components/AssetsModal.svelte";
import WindowControls from "$lib/components/WindowControls.svelte";
import ImageViewer from "$lib/components/ImageViewer.svelte";
import InfoModal from "$lib/components/InfoModal.svelte";
import EditorToolbar from "$lib/components/EditorToolbar.svelte";
import PageSettingsModal from "$lib/components/PageSettingsModal.svelte";
@@ -67,7 +66,6 @@
| { kind: "delete-file"; path: string }
| { kind: "login" }
| { kind: "settings" }
| { kind: "info" }
| { kind: "assets" }
| { kind: "page-settings" }
| { kind: "conflicts" };
@@ -514,14 +512,6 @@
<Icon icon="ph:gear-six" />
</button>
<button
class="rounded-md p-1.5 text-[var(--color-ink-muted)] transition hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-ink)]"
onclick={() => (dialog = { kind: "info" })}
aria-label="About"
>
<Icon icon="ph:info" />
</button>
<div class="ml-1 h-5 w-px bg-[var(--color-line)]"></div>
<WindowControls />
@@ -838,8 +828,6 @@
/>
{:else if dialog.kind === "settings"}
<SettingsModal onclose={close} onsignin={() => (dialog = { kind: "login" })} />
{:else if dialog.kind === "info"}
<InfoModal onclose={close} />
{:else if dialog.kind === "assets"}
<AssetsModal
oninsert={app.view === "editor" && editorView