move settings to the settings page
122 unmodified linesExpand all1231241251261271281291301311321331348 unmodified lines143144145146147148149150151152153154155156157158104 unmodified linesExpand all122 unmodified linesExpand all
- 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.- Action labels, nav labels, badges, pagination, and small utility links are8 unmodified lines controls.- 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 header/body relationship is already clear.104 unmodified linesExpand all122344567889101112111213141515 unmodified lines313233343637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615735363738266 unmodified linesExpand all30530630730843230931031131228 unmodified linesimport 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]';
const closeOpenTopbarMenus = (root: HTMLElement, target: Node) => {15 unmodified lines
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<string | null>(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 ( <details data-topbar-menu class="relative"> <summary class={clsx('cursor-pointer list-none')} title="App settings"> <Settings class="size-5 text-gray-500 dark:text-gray-400" /> </summary> <div class="absolute right-0 mt-3 w-96 max-w-[calc(100vw-2rem)] rounded border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-4 shadow-lg z-50"> <form onSubmit={onSave} class="flex flex-col gap-4"> <div class="space-y-1"> <label for="appview-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200"> appview URL </label> <input id="appview-url" value={draftAppviewUrl()} onInput={(event) => { setDraftAppviewUrl(event.currentTarget.value); setSaved(false); setError(null); }} placeholder={settings.defaultAppviewUrl} class={inputStyles()} /> </div>
<label class="flex items-center gap-3 text-sm text-gray-700 dark:text-gray-200"> <input type="checkbox" checked={draftRouteKnotRequestsThroughAppview()} onChange={(event) => { setDraftRouteKnotRequestsThroughAppview(event.currentTarget.checked); setSaved(false); setError(null); }} class="size-4 rounded border-gray-300 dark:border-gray-600" /> <span>route knot requests through appview</span> </label>
<label class="flex items-center gap-3 text-sm text-gray-700 dark:text-gray-200"> <input type="checkbox" checked={draftForcePds()} onChange={(event) => { setDraftForcePds(event.currentTarget.checked); setSaved(false); setError(null); }} class="size-4 rounded border-gray-300 dark:border-gray-600" /> <span>force requests through PDS (bypass appview)</span> </label>
<div class="flex items-center justify-between gap-3"> <button type="button" class={buttonStyles()} onClick={onReset}> <RotateCcw class="size-4" /> reset </button> <button type="submit" class={buttonStyles('primary')}> <Save class="size-4" /> save </button> </div>
<Show when={error()}> <p class="text-sm text-red-500">{error()}</p> </Show> <Show when={saved()}> <p class="text-sm text-gray-500 dark:text-gray-400">saved</p> </Show> </form> </div> </details> );};
const LoginMenu: Component = () => { const auth = useAuth(); const [identifier, setIdentifier] = createSignal('');
266 unmodified linesExpand all <TopbarSearch /> <A href="/search" class="md:hidden text-gray-500 dark:text-gray-400" title="Search"> <Search class="size-5" /> </A> <AppSettingsMenu /> <Show when={auth.currentDid()} fallback={<LoginMenu />} >28 unmodified lines23 unmodified lines24252627282930313213 unmodified lines464748494950515253545524 unmodified lines23 unmodified linesimport { ProfilePage } from './pages/profile';import { StringPage, RawStringPage, NewStringPage, EditStringPage } from './pages/string';import { SettingsLayout, AppSettingsTab, ProfileSettingsTab, KeysSettingsTab, EmailsSettingsTab, NotificationsSettingsTab,13 unmodified lines <Route path="/strings/:actor/:rkey/edit" component={EditStringPage} /> <Route path="/strings/:actor/:rkey/raw" component={RawStringPage} /> <Route path="/repo/new" component={NewRepoPage} /> <Route path="/settings" component={SettingsLayout}> <Route path="/" component={ProfileSettingsTab} /> <Route path="/" component={AppSettingsTab} /> <Route path="/app" component={AppSettingsTab} /> <Route path="/profile" component={ProfileSettingsTab} /> <Route path="/keys" component={KeysSettingsTab} /> <Route path="/emails" component={EmailsSettingsTab} /> <Route path="/notifications" component={NotificationsSettingsTab} />24 unmodified lines10 unmodified lines1112131415161718192 unmodified lines2223242526272829301 unmodified line323334353435363738394041424361 unmodified lines10510610710810911011111211379 unmodified lines1931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723731144 unmodified linesExpand all10 unmodified linesimport { 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, Bell,2 unmodified lines Spool, Plus, Trash2, RotateCw, RotateCcw, X, Book, ShieldCheck, BookMarked,1 unmodified line Save,} 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, deletePublicKey,61 unmodified lines};
// Sidebar active/inactive styling matching upstream templateconst 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' }, { name: 'notifications', label: 'notifications', icon: Bell, path: '/settings/notifications' },79 unmodified lines </Show> );};
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<string | null>(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 ( <div class="flex flex-col gap-6"> <div> <h2 class="text-sm font-bold uppercase tracking-wider text-gray-900 dark:text-gray-100 mb-2">APP</h2> <p class="text-gray-500 dark:text-gray-400 pb-2"> Configure the local app settings. Changes save automatically. </p> </div>
<div> <h2 class="text-sm font-bold uppercase tracking-wider text-gray-900 dark:text-gray-100 mb-2">APPVIEW URL</h2> <p class="text-gray-500 dark:text-gray-400 pb-2"> Set the appview used by the local client. </p> <div class="max-w-xl"> <input id="appview-url" value={draftAppviewUrl()} onInput={(event) => { 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()} /> </div> </div>
<div> <h2 class="text-sm font-bold uppercase tracking-wider text-gray-900 dark:text-gray-100 mb-2">REQUEST ROUTING</h2> <p class="text-gray-500 dark:text-gray-400 pb-2"> Control how this client routes repository requests. </p> <div class="flex flex-col gap-1.5"> <div class="flex items-center gap-2"> <input type="checkbox" id="routeKnotRequestsThroughAppview" checked={draftRouteKnotRequestsThroughAppview()} onChange={(event) => { 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" /> <label for="routeKnotRequestsThroughAppview" class="my-0 py-0 normal-case font-normal text-sm text-gray-700 dark:text-gray-300 cursor-pointer" > route knot requests through appview </label> </div>
<div class="flex items-center gap-2"> <input type="checkbox" id="forcePds" checked={draftForcePds()} onChange={(event) => { 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" /> <label for="forcePds" class="my-0 py-0 normal-case font-normal text-sm text-gray-700 dark:text-gray-300 cursor-pointer" > force requests through PDS (bypass appview) </label> </div> </div> </div>
<div class="flex items-center justify-between gap-3 pt-1 max-w-xl"> <button type="button" class={buttonStyles()} onClick={onReset}> <RotateCcw class="size-4" /> reset </button> <Show when={saved() && !error()}> <p class="text-sm text-gray-500 dark:text-gray-400">saved</p> </Show> </div>
<Show when={error()}> <p class="text-sm text-red-500">{error()}</p> </Show> </div> );};
// Profile settings tab implementationexport const ProfileSettingsTab: Component = () => { const auth = useAuth(); const [punchcardHideMine, setPunchcardHideMine] = createSignal(false);1144 unmodified linesExpand all