From d3306788592c0b3b1649417062b1fafd464c8dbc Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Sun, 31 May 2026 23:47:28 +0300 Subject: [PATCH] move settings to the settings page --- AGENTS.md | 12 +++ src/layout.tsx | 132 +----------------------------- src/pages/settings.tsx | 181 ++++++++++++++++++++++++++++++++++++++++- src/routes.tsx | 4 +- 4 files changed, 198 insertions(+), 131 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d6e564c..3fb67f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -124,6 +124,10 @@ upstream class literally, compare nearby local pages and shared components. - Favor a quiet, dense tool UI: compact spacing, small radii, restrained borders, and flat page sections. Avoid marketing-style cards, oversized type, decorative gradients, and unnecessary nested panels. +- When extending an existing multi-section page, copy its spacing rhythm before + inventing a new one. Match the surrounding stack gaps, heading margins, + description spacing, and form row density so sibling sections feel authored + together rather than assembled from separate patterns. - Top-level route titles are usually plain lowercase labels. Use uppercase tracking only for compact metadata labels, table/list section labels, or places where the local surrounding UI already uses that treatment. @@ -140,6 +144,14 @@ upstream class literally, compare nearby local pages and shared components. - Badges shown next to controls should match the controls' visual density and height. Avoid badge styles that look like separate one-off pills unless a shared badge component already does that. +- For repeated control rows such as checkboxes, toggles, or small preferences, + reuse the same DOM structure and utility classes as nearby controls instead + of building a visually similar variant. Small markup differences often create + visible spacing drift even when the text and colors match. +- Prefer lightweight interaction patterns for local preferences: save + immediately when the change is unambiguous, keep temporary drafts only where + validation requires it, and avoid extra cards or explicit save actions unless + the setting genuinely needs batching or confirmation. - Dialogs and destructive/creation popovers should feel like app chrome, not a page section: center modal dialogs in the viewport, use a real backdrop when blocking interaction, and prefer spacing over extra separator lines when the diff --git a/src/layout.tsx b/src/layout.tsx index a39dbe1..e676261 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -1,15 +1,14 @@ import clsx from 'clsx'; -import { Bell, BookMarked, LogOut, RotateCcw, Save, Search, Settings, UserRound, Plus, LineSquiggle, BookPlus } from 'lucide-solid'; +import { Bell, BookMarked, LogOut, Search, Settings, UserRound, Plus, LineSquiggle, BookPlus } from 'lucide-solid'; import { A, useLocation, useNavigate } from '@solidjs/router'; -import { createQuery, useQueryClient } from '@tanstack/solid-query'; +import { createQuery } from '@tanstack/solid-query'; import { For, Show, createEffect, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js'; import { Avatar, buttonStyles, inputStyles } from './components/common'; -import { clearApiCaches, resolveActor } from './lib/api/identity'; +import { resolveActor } from './lib/api/identity'; import { useAuth } from './lib/auth'; import { useLiveEvents, type LiveEvent } from './lib/live-events'; -import { formatRelativeTime, getErrorMessage } from './lib/repo-utils'; -import { useAppSettings } from './lib/settings'; +import { formatRelativeTime } from './lib/repo-utils'; const TOPBAR_MENU_SELECTOR = 'details[data-topbar-menu]'; @@ -33,128 +32,6 @@ const eventLabel = (event: LiveEvent) => { return `${event.source} -> ${event.subject}`; }; -const AppSettingsMenu: Component = () => { - const settings = useAppSettings(); - const queryClient = useQueryClient(); - const [draftAppviewUrl, setDraftAppviewUrl] = createSignal(settings.appviewUrl()); - const [draftRouteKnotRequestsThroughAppview, setDraftRouteKnotRequestsThroughAppview] = createSignal( - settings.routeKnotRequestsThroughAppview(), - ); - const [draftForcePds, setDraftForcePds] = createSignal(settings.forcePds()); - const [error, setError] = createSignal(null); - const [saved, setSaved] = createSignal(false); - - createEffect(() => { - setDraftAppviewUrl(settings.appviewUrl()); - setDraftRouteKnotRequestsThroughAppview(settings.routeKnotRequestsThroughAppview()); - setDraftForcePds(settings.forcePds()); - }); - - const reloadAppviewBackedQueries = () => { - clearApiCaches(); - void queryClient.invalidateQueries(); - }; - - const onSave = (event: SubmitEvent) => { - event.preventDefault(); - try { - settings.setAppviewUrl(draftAppviewUrl()); - settings.setRouteKnotRequestsThroughAppview(draftRouteKnotRequestsThroughAppview()); - settings.setForcePds(draftForcePds()); - setError(null); - setSaved(true); - reloadAppviewBackedQueries(); - } catch (cause) { - setSaved(false); - setError(getErrorMessage(cause)); - } - }; - - const onReset = () => { - settings.resetAppviewSettings(); - setDraftAppviewUrl(settings.defaultAppviewUrl); - setDraftRouteKnotRequestsThroughAppview(false); - setDraftForcePds(false); - setError(null); - setSaved(true); - reloadAppviewBackedQueries(); - }; - - return ( -
- - - -
-
-
- - { - setDraftAppviewUrl(event.currentTarget.value); - setSaved(false); - setError(null); - }} - placeholder={settings.defaultAppviewUrl} - class={inputStyles()} - /> -
- - - - - -
- - -
- - -

{error()}

-
- -

saved

-
-
-
-
- ); -}; - const LoginMenu: Component = () => { const auth = useAuth(); const [identifier, setIdentifier] = createSignal(''); @@ -429,7 +306,6 @@ const Topbar: Component = () => { - } diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 9c4c384..c132c51 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -12,6 +12,7 @@ import { Portal } from 'solid-js/web'; import { A, useNavigate, useParams, useLocation, type RouteSectionProps } from '@solidjs/router'; import { createQuery, useQueryClient } from '@tanstack/solid-query'; import { + Settings, User, Key, Mail, @@ -22,6 +23,7 @@ import { Plus, Trash2, RotateCw, + RotateCcw, X, Book, ShieldCheck, @@ -31,9 +33,10 @@ import { } from 'lucide-solid'; import { useAuth } from '../lib/auth'; import { formatRelativeTime, getErrorMessage } from '../lib/repo-utils'; -import { Avatar, inputStyles, textareaStyles } from '../components/common'; -import { resolveActor } from '../lib/api/identity'; +import { Avatar, buttonStyles, inputStyles, textareaStyles } from '../components/common'; +import { clearApiCaches, resolveActor } from '../lib/api/identity'; import { listRepoRecords } from '../lib/api/repos'; +import { useAppSettings } from '../lib/settings'; import { listPublicKeys, addPublicKey, @@ -103,6 +106,7 @@ const Modal: Component<{ // Sidebar active/inactive styling matching upstream template const UserSettingsTabs = [ + { name: 'app', label: 'app', icon: Settings, path: '/settings/app' }, { name: 'profile', label: 'profile', icon: User, path: '/settings/profile' }, { name: 'keys', label: 'keys', icon: Key, path: '/settings/keys' }, { name: 'emails', label: 'emails', icon: Mail, path: '/settings/emails' }, @@ -190,6 +194,179 @@ export const SettingsLayout: Component = (props) => { ); }; +export const AppSettingsTab: Component = () => { + const settings = useAppSettings(); + const queryClient = useQueryClient(); + const [draftAppviewUrl, setDraftAppviewUrl] = createSignal(settings.appviewUrl()); + const [draftRouteKnotRequestsThroughAppview, setDraftRouteKnotRequestsThroughAppview] = createSignal( + settings.routeKnotRequestsThroughAppview(), + ); + const [draftForcePds, setDraftForcePds] = createSignal(settings.forcePds()); + const [error, setError] = createSignal(null); + const [saved, setSaved] = createSignal(false); + + createEffect(() => { + setDraftAppviewUrl(settings.appviewUrl()); + setDraftRouteKnotRequestsThroughAppview(settings.routeKnotRequestsThroughAppview()); + setDraftForcePds(settings.forcePds()); + }); + + const reloadAppviewBackedQueries = () => { + clearApiCaches(); + void queryClient.invalidateQueries(); + }; + + const applyAppviewUrl = () => { + try { + if (draftAppviewUrl().trim() === settings.appviewUrl()) { + return; + } + settings.setAppviewUrl(draftAppviewUrl()); + setError(null); + setSaved(true); + reloadAppviewBackedQueries(); + } catch (cause) { + setSaved(false); + setError(getErrorMessage(cause)); + } + }; + + const updateRouteKnotRequestsThroughAppview = (enabled: boolean) => { + setDraftRouteKnotRequestsThroughAppview(enabled); + try { + settings.setRouteKnotRequestsThroughAppview(enabled); + setSaved(true); + setError(null); + reloadAppviewBackedQueries(); + } catch (cause) { + setError(getErrorMessage(cause)); + } + }; + + const updateForcePds = (enabled: boolean) => { + setDraftForcePds(enabled); + try { + settings.setForcePds(enabled); + setSaved(true); + setError(null); + reloadAppviewBackedQueries(); + } catch (cause) { + setError(getErrorMessage(cause)); + } + }; + + const onReset = () => { + settings.resetAppviewSettings(); + setDraftAppviewUrl(settings.defaultAppviewUrl); + setDraftRouteKnotRequestsThroughAppview(false); + setDraftForcePds(false); + setError(null); + setSaved(true); + reloadAppviewBackedQueries(); + }; + + return ( +
+
+

APP

+

+ Configure the local app settings. Changes save automatically. +

+
+ +
+

APPVIEW URL

+

+ Set the appview used by the local client. +

+
+ { + setDraftAppviewUrl(event.currentTarget.value); + setSaved(false); + setError(null); + }} + onBlur={applyAppviewUrl} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault(); + applyAppviewUrl(); + event.currentTarget.blur(); + } + }} + placeholder={settings.defaultAppviewUrl} + class={inputStyles()} + /> +
+
+ +
+

REQUEST ROUTING

+

+ Control how this client routes repository requests. +

+
+
+ { + setSaved(false); + setError(null); + updateRouteKnotRequestsThroughAppview(event.currentTarget.checked); + }} + class="my-0 py-0 rounded border-gray-300 dark:border-gray-600 text-blue-600 size-4 p-0 m-0 untangled-checkbox" + /> + +
+ +
+ { + setSaved(false); + setError(null); + updateForcePds(event.currentTarget.checked); + }} + class="my-0 py-0 rounded border-gray-300 dark:border-gray-600 text-blue-600 size-4 p-0 m-0 untangled-checkbox" + /> + +
+
+
+ +
+ + +

saved

+
+
+ + +

{error()}

+
+
+ ); +}; + // Profile settings tab implementation export const ProfileSettingsTab: Component = () => { const auth = useAuth(); diff --git a/src/routes.tsx b/src/routes.tsx index d993de2..ed847a9 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -25,6 +25,7 @@ import { ProfilePage } from './pages/profile'; import { StringPage, RawStringPage, NewStringPage, EditStringPage } from './pages/string'; import { SettingsLayout, + AppSettingsTab, ProfileSettingsTab, KeysSettingsTab, EmailsSettingsTab, @@ -46,7 +47,8 @@ export const AppRoutes: Component = () => ( - + + -- 2.51.2