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
19 changes: 13 additions & 6 deletions frontend/src/app/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ export default function SettingsPage() {
});

return (
<main className="page-surface pb-20 pt-8 sm:pt-10 lg:pb-24 lg:pt-12">
<main className="page-surface pb-16 pt-8 sm:pb-20 sm:pt-10 lg:pb-24 lg:pt-12">
<div className="mx-auto w-full max-w-6xl">
<header className="mb-7 border-b border-[color:var(--frost)] pb-5 sm:mb-8">
<header className="mb-6 border-b border-[color:var(--frost)] pb-6 sm:mb-8">
<div className="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<div className="flex flex-wrap items-baseline gap-2">
Expand All @@ -63,7 +63,7 @@ export default function SettingsPage() {
Settings
</h1>
</div>
<p className="mt-2 text-sm text-[color:var(--silver)]">
<p className="mt-2 max-w-2xl text-sm leading-6 text-[color:var(--silver)]">
Appearance, local AI, privacy, and retention.
</p>
</div>
Expand All @@ -88,7 +88,12 @@ export default function SettingsPage() {
</div>
</header>

<div className="grid gap-8 lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-10">
{/* The single-column track must be minmax(0,1fr), not the implicit
`auto`. An auto track sizes to max-content, so one non-wrapping
value in the About card stretched the column to ~650px inside a
358px container and pushed the whole page into horizontal scroll
on phones. */}
<div className="grid grid-cols-[minmax(0,1fr)] gap-6 sm:gap-8 lg:grid-cols-[13rem_minmax(0,1fr)] lg:gap-10">
<aside className="lg:sticky lg:top-[calc(var(--nav-height)+2rem)] lg:self-start">
<nav
aria-label="Settings sections"
Expand All @@ -99,7 +104,7 @@ export default function SettingsPage() {
<a
key={href}
href={href}
className="flex min-w-max items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-medium text-[color:var(--silver)] outline-none transition hover:bg-[color:var(--surface-hover)] hover:text-[color:var(--near-white)] focus-visible:ring-2 focus-visible:ring-[color:var(--blue)] lg:min-w-0"
className="flex min-h-11 min-w-max items-center gap-2 rounded-xl px-3 py-2.5 text-sm font-medium text-[color:var(--silver)] outline-none transition hover:bg-[color:var(--surface-hover)] hover:text-[color:var(--near-white)] active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-[color:var(--blue)] lg:min-w-0"
>
<Icon aria-hidden="true" size={16} />
{label}
Expand Down Expand Up @@ -150,7 +155,9 @@ export default function SettingsPage() {
<button
type="button"
onClick={() => settingsQuery.refetch()}
className="mt-5 inline-flex h-10 items-center gap-2 rounded-xl bg-[color:var(--near-white)] px-4 text-sm font-semibold text-[color:var(--void)] outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--blue)]"
disabled={settingsQuery.isFetching}
aria-busy={settingsQuery.isFetching}
className="mt-5 inline-flex h-11 items-center gap-2 rounded-xl bg-[color:var(--near-white)] px-4 text-sm font-semibold text-[color:var(--void)] outline-none transition hover:opacity-90 active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-[color:var(--blue)] disabled:cursor-wait disabled:opacity-60"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
>
<RefreshCw aria-hidden="true" size={15} />
Retry
Expand Down
36 changes: 28 additions & 8 deletions frontend/src/components/about-settings.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"use client";

import { useQuery } from "@tanstack/react-query";
import { BadgeInfo, Check, ClipboardCopy } from "lucide-react";
import { BadgeInfo, Check, ClipboardCopy, LoaderCircle } from "lucide-react";
import { useState } from "react";
import { getAppConfig, getRuntimeConfig } from "@/lib/api";
import { buildSupportSummary, formatWorkerAge } from "@/lib/support-summary";

type CopyState = "idle" | "copied" | "error";
type CopyState = "idle" | "copying" | "copied" | "error";

const WORKER_LABEL: Record<string, string> = {
healthy: "Healthy",
Expand All @@ -17,9 +17,13 @@ const WORKER_LABEL: Record<string, string> = {

function Row({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-baseline justify-between gap-4 py-1.5">
<div className="flex flex-col gap-0.5 py-2 sm:flex-row sm:items-baseline sm:justify-between sm:gap-4">
<dt className="text-sm text-[color:var(--silver)]">{label}</dt>
<dd className="truncate text-sm font-medium text-[color:var(--near-white)]">
{/* Wrap on phones, where the row is stacked and the value has a whole
line to itself; only truncate from sm: up, where label and value
share one line. `truncate` implies white-space: nowrap, so applying
it unconditionally also gave this cell a max-content minimum. */}
<dd className="break-words text-sm font-medium text-[color:var(--near-white)] sm:max-w-[60%] sm:truncate sm:text-right">
{value}
</dd>
</div>
Expand Down Expand Up @@ -56,6 +60,7 @@ export function AboutSettings() {
};

const onCopy = async () => {
setCopyState("copying");
try {
await navigator.clipboard.writeText(buildSupportSummary(summaryInput));
setCopyState("copied");
Expand All @@ -72,9 +77,15 @@ export function AboutSettings() {
aria-labelledby="about-heading"
>
<div className="flex gap-3">
<BadgeInfo className="mt-0.5 h-5 w-5 text-[color:var(--silver)]" />
<BadgeInfo
aria-hidden="true"
className="mt-0.5 h-5 w-5 shrink-0 text-[color:var(--silver)]"
/>
<div className="min-w-0 flex-1">
<h2 id="about-heading" className="font-semibold">
<h2
id="about-heading"
className="text-base font-semibold tracking-tight"
>
About this instance
</h2>
<p className="mt-1 text-sm text-[color:var(--silver)]">
Expand Down Expand Up @@ -133,9 +144,17 @@ export function AboutSettings() {
data-testid="copy-support-summary"
onClick={onCopy}
aria-label="Copy support summary to the clipboard"
className="inline-flex h-10 items-center gap-2 rounded-xl border border-[color:var(--frost)] px-4 text-sm font-medium text-[color:var(--silver)] outline-none transition hover:bg-[color:var(--surface-hover)] hover:text-[color:var(--near-white)] focus-visible:ring-2 focus-visible:ring-[color:var(--blue)]"
aria-busy={copyState === "copying"}
disabled={copyState === "copying"}
className="inline-flex h-11 items-center gap-2 rounded-xl border border-[color:var(--frost)] px-4 text-sm font-medium text-[color:var(--silver)] outline-none transition hover:bg-[color:var(--surface-hover)] hover:text-[color:var(--near-white)] active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-[color:var(--blue)] disabled:cursor-wait disabled:opacity-60"
>
{copyState === "copied" ? (
{copyState === "copying" ? (
<LoaderCircle
aria-hidden="true"
className="animate-spin"
size={15}
/>
) : copyState === "copied" ? (
<Check aria-hidden="true" size={15} />
) : (
<ClipboardCopy aria-hidden="true" size={15} />
Expand All @@ -153,6 +172,7 @@ export function AboutSettings() {
: "text-[color:var(--silver)]"
}`}
>
{copyState === "copying" && "Copying support summary…"}
{copyState === "copied" && "Support summary copied."}
{copyState === "error" &&
"Couldn't copy. Select the details above and copy manually."}
Expand Down
15 changes: 10 additions & 5 deletions frontend/src/components/ai-runtime-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,17 @@ export function AiRuntimeSettings({
aria-checked={checked}
disabled={enabled === undefined || pending}
onClick={() => onChange(!checked)}
className="relative h-7 w-12 shrink-0 rounded-full border border-[color:var(--frost-strong)] bg-[color:var(--surface-hover)] transition disabled:cursor-wait disabled:opacity-50 aria-checked:border-[color:var(--green)] aria-checked:bg-[color:var(--green)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--blue)]"
aria-busy={pending}
className="relative h-7 w-12 shrink-0 rounded-full border border-[color:var(--frost-strong)] bg-[color:var(--surface-hover)] transition before:absolute before:-inset-2 before:content-[''] hover:border-[color:var(--frost-strong)] active:scale-95 disabled:cursor-wait disabled:opacity-50 aria-checked:border-[color:var(--green)] aria-checked:bg-[color:var(--green)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--blue)]"
>
<span
aria-hidden="true"
className="absolute left-1 top-1 size-[1.125rem] rounded-full bg-white shadow transition-transform"
style={{ transform: checked ? "translateX(1.25rem)" : undefined }}
className="absolute left-1 top-1/2 size-5 rounded-full bg-white shadow transition-transform"
style={{
transform: checked
? "translate(1.25rem, -50%)"
: "translateY(-50%)",
}}
/>
</button>
</div>
Expand Down Expand Up @@ -122,7 +127,7 @@ export function AiRuntimeSettings({
onChange={(event) =>
onModeChange(event.target.value as "disabled" | "full" | "mock")
}
className="mt-3 h-11 w-full rounded-xl border border-[color:var(--frost)] bg-[color:var(--void)] px-3 text-sm text-[color:var(--near-white)] outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--blue)] sm:max-w-xs"
className="mt-3 h-11 w-full rounded-xl border border-[color:var(--frost)] bg-[color:var(--void)] px-3 text-sm text-[color:var(--near-white)] outline-none transition hover:border-[color:var(--frost-strong)] focus-visible:ring-2 focus-visible:ring-[color:var(--blue)] disabled:cursor-wait disabled:opacity-60 sm:max-w-xs"
>
{mode && !supportedModes.includes(mode) && (
<option value={mode} disabled>
Expand All @@ -142,7 +147,7 @@ export function AiRuntimeSettings({
</div>

<details className="border-t border-[color:var(--frost)] px-5 py-4">
<summary className="cursor-pointer text-sm font-semibold">
<summary className="cursor-pointer rounded-lg text-sm font-semibold outline-none transition hover:text-[color:var(--near-white)] focus-visible:ring-2 focus-visible:ring-[color:var(--blue)]">
Change the installed build
</summary>
<p className="mt-2 text-xs leading-5 text-[color:var(--silver)]">
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/components/appearance-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ export function AppearanceSettings() {
<section
id="appearance"
aria-labelledby="appearance-heading"
className="rounded-2xl border border-[color:var(--frost)] bg-[color:var(--surface-soft)] p-5 sm:p-6"
className="rounded-2xl border border-[color:var(--frost)] bg-[color:var(--surface-soft)] p-5 shadow-[0_18px_60px_rgba(0,0,0,0.08)] sm:p-6"
>
<h2 id="appearance-heading" className="text-base font-semibold">
<h2
id="appearance-heading"
className="text-base font-semibold tracking-tight"
>
Appearance
</h2>
<p className="mt-1 text-sm text-[color:var(--silver)]">
Expand All @@ -39,7 +42,7 @@ export function AppearanceSettings() {
{OPTIONS.map(({ value: option, label, icon: Icon }) => (
<label
key={option}
className={`flex cursor-pointer items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium transition ${value === option ? "bg-[color:var(--near-white)] text-[color:var(--void)] shadow-sm" : "text-[color:var(--silver)] hover:bg-[color:var(--surface-hover)]"}`}
className={`flex min-h-11 cursor-pointer items-center justify-center gap-2 rounded-lg px-3 py-2.5 text-sm font-medium outline-none transition active:scale-[0.98] has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-[color:var(--blue)] ${value === option ? "bg-[color:var(--near-white)] text-[color:var(--void)] shadow-sm" : "text-[color:var(--silver)] hover:bg-[color:var(--surface-hover)] hover:text-[color:var(--near-white)]"}`}
>
<input
className="sr-only"
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/hardware-accel-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export function HardwareAccelSettings({
<label
key={mode.value}
data-testid={`accel-option-${mode.value}`}
className="group relative flex min-h-28 cursor-pointer flex-col gap-2 bg-[color:var(--void)]/90 p-4 transition hover:bg-[color:var(--surface-hover)] has-[:checked]:bg-[color:var(--blue-soft)] has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50"
className="group relative flex min-h-28 cursor-pointer flex-col gap-2 bg-[color:var(--void)]/90 p-4 outline-none transition hover:bg-[color:var(--surface-hover)] active:bg-[color:var(--blue-soft)] has-[:checked]:bg-[color:var(--blue-soft)] has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 has-[:focus-visible]:z-10 has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-[color:var(--blue)]"
title={
gpuNotInstalled
? "GPU runtime is not included in this installed build."
Expand Down Expand Up @@ -118,6 +118,8 @@ export function HardwareAccelSettings({
<p
className="px-5 py-4 text-sm text-[color:var(--silver)]"
data-testid="accel-loading"
role="status"
aria-live="polite"
>
Detecting hardware…
</p>
Expand Down
13 changes: 9 additions & 4 deletions frontend/src/components/map-privacy-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,17 @@ export function MapPrivacySettings({
aria-checked={checked}
disabled={enabled === undefined || pending}
onClick={() => onChange(!checked)}
className="relative h-7 w-12 shrink-0 rounded-full border border-[color:var(--frost-strong)] bg-[color:var(--surface-hover)] transition disabled:cursor-wait disabled:opacity-50 aria-checked:border-[color:var(--near-white)] aria-checked:bg-[color:var(--near-white)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--blue)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--void)]"
aria-busy={pending}
className="relative h-7 w-12 shrink-0 rounded-full border border-[color:var(--frost-strong)] bg-[color:var(--surface-hover)] transition before:absolute before:-inset-2 before:content-[''] hover:border-[color:var(--frost-strong)] active:scale-95 disabled:cursor-wait disabled:opacity-50 aria-checked:border-[color:var(--near-white)] aria-checked:bg-[color:var(--near-white)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--blue)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--void)]"
>
<span
aria-hidden="true"
className="absolute left-1 top-1 size-[1.125rem] rounded-full bg-white shadow transition-transform"
style={{ transform: checked ? "translateX(1.25rem)" : undefined }}
className="absolute left-1 top-1/2 size-5 rounded-full bg-white shadow transition-transform"
style={{
transform: checked
? "translate(1.25rem, -50%)"
: "translateY(-50%)",
}}
/>
</button>
</div>
Expand Down Expand Up @@ -86,7 +91,7 @@ export function MapPrivacySettings({
{checked && (
<Link
href="/map"
className="shrink-0 rounded-lg px-2 py-1 text-xs font-semibold text-[color:var(--near-white)] hover:bg-[color:var(--surface-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--blue)]"
className="inline-flex min-h-11 shrink-0 items-center rounded-lg px-3 text-xs font-semibold text-[color:var(--near-white)] outline-none transition hover:bg-[color:var(--surface-hover)] active:scale-[0.98] focus-visible:ring-2 focus-visible:ring-[color:var(--blue)]"
>
Open map
</Link>
Expand Down
Loading
Loading