csr tangled client in solid-js
15

Configure Feed

Select the types of activity you want to include in your feed.

move settings to the settings page

dawn (May 31, 2026, 11:47 PM +0300) d3306788 25710187

+198 -131
+12
AGENTS.md
··· 124 124 - Favor a quiet, dense tool UI: compact spacing, small radii, restrained 125 125 borders, and flat page sections. Avoid marketing-style cards, oversized 126 126 type, decorative gradients, and unnecessary nested panels. 127 + - When extending an existing multi-section page, copy its spacing rhythm before 128 + inventing a new one. Match the surrounding stack gaps, heading margins, 129 + description spacing, and form row density so sibling sections feel authored 130 + together rather than assembled from separate patterns. 127 131 - Top-level route titles are usually plain lowercase labels. Use uppercase 128 132 tracking only for compact metadata labels, table/list section labels, or 129 133 places where the local surrounding UI already uses that treatment. ··· 140 144 - Badges shown next to controls should match the controls' visual density and 141 145 height. Avoid badge styles that look like separate one-off pills unless a 142 146 shared badge component already does that. 147 + - For repeated control rows such as checkboxes, toggles, or small preferences, 148 + reuse the same DOM structure and utility classes as nearby controls instead 149 + of building a visually similar variant. Small markup differences often create 150 + visible spacing drift even when the text and colors match. 151 + - Prefer lightweight interaction patterns for local preferences: save 152 + immediately when the change is unambiguous, keep temporary drafts only where 153 + validation requires it, and avoid extra cards or explicit save actions unless 154 + the setting genuinely needs batching or confirmation. 143 155 - Dialogs and destructive/creation popovers should feel like app chrome, not a 144 156 page section: center modal dialogs in the viewport, use a real backdrop when 145 157 blocking interaction, and prefer spacing over extra separator lines when the
+4 -128
src/layout.tsx
··· 1 1 import clsx from 'clsx'; 2 - import { Bell, BookMarked, LogOut, RotateCcw, Save, Search, Settings, UserRound, Plus, LineSquiggle, BookPlus } from 'lucide-solid'; 2 + import { Bell, BookMarked, LogOut, Search, Settings, UserRound, Plus, LineSquiggle, BookPlus } from 'lucide-solid'; 3 3 import { A, useLocation, useNavigate } from '@solidjs/router'; 4 - import { createQuery, useQueryClient } from '@tanstack/solid-query'; 4 + import { createQuery } from '@tanstack/solid-query'; 5 5 import { For, Show, createEffect, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js'; 6 6 7 7 import { Avatar, buttonStyles, inputStyles } from './components/common'; 8 - import { clearApiCaches, resolveActor } from './lib/api/identity'; 8 + import { resolveActor } from './lib/api/identity'; 9 9 import { useAuth } from './lib/auth'; 10 10 import { useLiveEvents, type LiveEvent } from './lib/live-events'; 11 - import { formatRelativeTime, getErrorMessage } from './lib/repo-utils'; 12 - import { useAppSettings } from './lib/settings'; 11 + import { formatRelativeTime } from './lib/repo-utils'; 13 12 14 13 const TOPBAR_MENU_SELECTOR = 'details[data-topbar-menu]'; 15 14 ··· 31 30 } 32 31 33 32 return `${event.source} -> ${event.subject}`; 34 - }; 35 - 36 - const AppSettingsMenu: Component = () => { 37 - const settings = useAppSettings(); 38 - const queryClient = useQueryClient(); 39 - const [draftAppviewUrl, setDraftAppviewUrl] = createSignal(settings.appviewUrl()); 40 - const [draftRouteKnotRequestsThroughAppview, setDraftRouteKnotRequestsThroughAppview] = createSignal( 41 - settings.routeKnotRequestsThroughAppview(), 42 - ); 43 - const [draftForcePds, setDraftForcePds] = createSignal(settings.forcePds()); 44 - const [error, setError] = createSignal<string | null>(null); 45 - const [saved, setSaved] = createSignal(false); 46 - 47 - createEffect(() => { 48 - setDraftAppviewUrl(settings.appviewUrl()); 49 - setDraftRouteKnotRequestsThroughAppview(settings.routeKnotRequestsThroughAppview()); 50 - setDraftForcePds(settings.forcePds()); 51 - }); 52 - 53 - const reloadAppviewBackedQueries = () => { 54 - clearApiCaches(); 55 - void queryClient.invalidateQueries(); 56 - }; 57 - 58 - const onSave = (event: SubmitEvent) => { 59 - event.preventDefault(); 60 - try { 61 - settings.setAppviewUrl(draftAppviewUrl()); 62 - settings.setRouteKnotRequestsThroughAppview(draftRouteKnotRequestsThroughAppview()); 63 - settings.setForcePds(draftForcePds()); 64 - setError(null); 65 - setSaved(true); 66 - reloadAppviewBackedQueries(); 67 - } catch (cause) { 68 - setSaved(false); 69 - setError(getErrorMessage(cause)); 70 - } 71 - }; 72 - 73 - const onReset = () => { 74 - settings.resetAppviewSettings(); 75 - setDraftAppviewUrl(settings.defaultAppviewUrl); 76 - setDraftRouteKnotRequestsThroughAppview(false); 77 - setDraftForcePds(false); 78 - setError(null); 79 - setSaved(true); 80 - reloadAppviewBackedQueries(); 81 - }; 82 - 83 - return ( 84 - <details data-topbar-menu class="relative"> 85 - <summary class={clsx('cursor-pointer list-none')} title="App settings"> 86 - <Settings class="size-5 text-gray-500 dark:text-gray-400" /> 87 - </summary> 88 - <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"> 89 - <form onSubmit={onSave} class="flex flex-col gap-4"> 90 - <div class="space-y-1"> 91 - <label for="appview-url" class="block text-sm font-medium text-gray-700 dark:text-gray-200"> 92 - appview URL 93 - </label> 94 - <input 95 - id="appview-url" 96 - value={draftAppviewUrl()} 97 - onInput={(event) => { 98 - setDraftAppviewUrl(event.currentTarget.value); 99 - setSaved(false); 100 - setError(null); 101 - }} 102 - placeholder={settings.defaultAppviewUrl} 103 - class={inputStyles()} 104 - /> 105 - </div> 106 - 107 - <label class="flex items-center gap-3 text-sm text-gray-700 dark:text-gray-200"> 108 - <input 109 - type="checkbox" 110 - checked={draftRouteKnotRequestsThroughAppview()} 111 - onChange={(event) => { 112 - setDraftRouteKnotRequestsThroughAppview(event.currentTarget.checked); 113 - setSaved(false); 114 - setError(null); 115 - }} 116 - class="size-4 rounded border-gray-300 dark:border-gray-600" 117 - /> 118 - <span>route knot requests through appview</span> 119 - </label> 120 - 121 - <label class="flex items-center gap-3 text-sm text-gray-700 dark:text-gray-200"> 122 - <input 123 - type="checkbox" 124 - checked={draftForcePds()} 125 - onChange={(event) => { 126 - setDraftForcePds(event.currentTarget.checked); 127 - setSaved(false); 128 - setError(null); 129 - }} 130 - class="size-4 rounded border-gray-300 dark:border-gray-600" 131 - /> 132 - <span>force requests through PDS (bypass appview)</span> 133 - </label> 134 - 135 - <div class="flex items-center justify-between gap-3"> 136 - <button type="button" class={buttonStyles()} onClick={onReset}> 137 - <RotateCcw class="size-4" /> 138 - reset 139 - </button> 140 - <button type="submit" class={buttonStyles('primary')}> 141 - <Save class="size-4" /> 142 - save 143 - </button> 144 - </div> 145 - 146 - <Show when={error()}> 147 - <p class="text-sm text-red-500">{error()}</p> 148 - </Show> 149 - <Show when={saved()}> 150 - <p class="text-sm text-gray-500 dark:text-gray-400">saved</p> 151 - </Show> 152 - </form> 153 - </div> 154 - </details> 155 - ); 156 33 }; 157 34 158 35 const LoginMenu: Component = () => { ··· 429 306 <A href="/search" class="md:hidden text-gray-500 dark:text-gray-400" title="Search"> 430 307 <Search class="size-5" /> 431 308 </A> 432 - <AppSettingsMenu /> 433 309 <Show 434 310 when={auth.currentDid()} 435 311 fallback={<LoginMenu />}
+179 -2
src/pages/settings.tsx
··· 12 12 import { A, useNavigate, useParams, useLocation, type RouteSectionProps } from '@solidjs/router'; 13 13 import { createQuery, useQueryClient } from '@tanstack/solid-query'; 14 14 import { 15 + Settings, 15 16 User, 16 17 Key, 17 18 Mail, ··· 22 23 Plus, 23 24 Trash2, 24 25 RotateCw, 26 + RotateCcw, 25 27 X, 26 28 Book, 27 29 ShieldCheck, ··· 31 33 } from 'lucide-solid'; 32 34 import { useAuth } from '../lib/auth'; 33 35 import { formatRelativeTime, getErrorMessage } from '../lib/repo-utils'; 34 - import { Avatar, inputStyles, textareaStyles } from '../components/common'; 35 - import { resolveActor } from '../lib/api/identity'; 36 + import { Avatar, buttonStyles, inputStyles, textareaStyles } from '../components/common'; 37 + import { clearApiCaches, resolveActor } from '../lib/api/identity'; 36 38 import { listRepoRecords } from '../lib/api/repos'; 39 + import { useAppSettings } from '../lib/settings'; 37 40 import { 38 41 listPublicKeys, 39 42 addPublicKey, ··· 103 106 104 107 // Sidebar active/inactive styling matching upstream template 105 108 const UserSettingsTabs = [ 109 + { name: 'app', label: 'app', icon: Settings, path: '/settings/app' }, 106 110 { name: 'profile', label: 'profile', icon: User, path: '/settings/profile' }, 107 111 { name: 'keys', label: 'keys', icon: Key, path: '/settings/keys' }, 108 112 { name: 'emails', label: 'emails', icon: Mail, path: '/settings/emails' }, ··· 187 191 </section> 188 192 </div> 189 193 </Show> 194 + ); 195 + }; 196 + 197 + export const AppSettingsTab: Component = () => { 198 + const settings = useAppSettings(); 199 + const queryClient = useQueryClient(); 200 + const [draftAppviewUrl, setDraftAppviewUrl] = createSignal(settings.appviewUrl()); 201 + const [draftRouteKnotRequestsThroughAppview, setDraftRouteKnotRequestsThroughAppview] = createSignal( 202 + settings.routeKnotRequestsThroughAppview(), 203 + ); 204 + const [draftForcePds, setDraftForcePds] = createSignal(settings.forcePds()); 205 + const [error, setError] = createSignal<string | null>(null); 206 + const [saved, setSaved] = createSignal(false); 207 + 208 + createEffect(() => { 209 + setDraftAppviewUrl(settings.appviewUrl()); 210 + setDraftRouteKnotRequestsThroughAppview(settings.routeKnotRequestsThroughAppview()); 211 + setDraftForcePds(settings.forcePds()); 212 + }); 213 + 214 + const reloadAppviewBackedQueries = () => { 215 + clearApiCaches(); 216 + void queryClient.invalidateQueries(); 217 + }; 218 + 219 + const applyAppviewUrl = () => { 220 + try { 221 + if (draftAppviewUrl().trim() === settings.appviewUrl()) { 222 + return; 223 + } 224 + settings.setAppviewUrl(draftAppviewUrl()); 225 + setError(null); 226 + setSaved(true); 227 + reloadAppviewBackedQueries(); 228 + } catch (cause) { 229 + setSaved(false); 230 + setError(getErrorMessage(cause)); 231 + } 232 + }; 233 + 234 + const updateRouteKnotRequestsThroughAppview = (enabled: boolean) => { 235 + setDraftRouteKnotRequestsThroughAppview(enabled); 236 + try { 237 + settings.setRouteKnotRequestsThroughAppview(enabled); 238 + setSaved(true); 239 + setError(null); 240 + reloadAppviewBackedQueries(); 241 + } catch (cause) { 242 + setError(getErrorMessage(cause)); 243 + } 244 + }; 245 + 246 + const updateForcePds = (enabled: boolean) => { 247 + setDraftForcePds(enabled); 248 + try { 249 + settings.setForcePds(enabled); 250 + setSaved(true); 251 + setError(null); 252 + reloadAppviewBackedQueries(); 253 + } catch (cause) { 254 + setError(getErrorMessage(cause)); 255 + } 256 + }; 257 + 258 + const onReset = () => { 259 + settings.resetAppviewSettings(); 260 + setDraftAppviewUrl(settings.defaultAppviewUrl); 261 + setDraftRouteKnotRequestsThroughAppview(false); 262 + setDraftForcePds(false); 263 + setError(null); 264 + setSaved(true); 265 + reloadAppviewBackedQueries(); 266 + }; 267 + 268 + return ( 269 + <div class="flex flex-col gap-6"> 270 + <div> 271 + <h2 class="text-sm font-bold uppercase tracking-wider text-gray-900 dark:text-gray-100 mb-2">APP</h2> 272 + <p class="text-gray-500 dark:text-gray-400 pb-2"> 273 + Configure the local app settings. Changes save automatically. 274 + </p> 275 + </div> 276 + 277 + <div> 278 + <h2 class="text-sm font-bold uppercase tracking-wider text-gray-900 dark:text-gray-100 mb-2">APPVIEW URL</h2> 279 + <p class="text-gray-500 dark:text-gray-400 pb-2"> 280 + Set the appview used by the local client. 281 + </p> 282 + <div class="max-w-xl"> 283 + <input 284 + id="appview-url" 285 + value={draftAppviewUrl()} 286 + onInput={(event) => { 287 + setDraftAppviewUrl(event.currentTarget.value); 288 + setSaved(false); 289 + setError(null); 290 + }} 291 + onBlur={applyAppviewUrl} 292 + onKeyDown={(event) => { 293 + if (event.key === 'Enter') { 294 + event.preventDefault(); 295 + applyAppviewUrl(); 296 + event.currentTarget.blur(); 297 + } 298 + }} 299 + placeholder={settings.defaultAppviewUrl} 300 + class={inputStyles()} 301 + /> 302 + </div> 303 + </div> 304 + 305 + <div> 306 + <h2 class="text-sm font-bold uppercase tracking-wider text-gray-900 dark:text-gray-100 mb-2">REQUEST ROUTING</h2> 307 + <p class="text-gray-500 dark:text-gray-400 pb-2"> 308 + Control how this client routes repository requests. 309 + </p> 310 + <div class="flex flex-col gap-1.5"> 311 + <div class="flex items-center gap-2"> 312 + <input 313 + type="checkbox" 314 + id="routeKnotRequestsThroughAppview" 315 + checked={draftRouteKnotRequestsThroughAppview()} 316 + onChange={(event) => { 317 + setSaved(false); 318 + setError(null); 319 + updateRouteKnotRequestsThroughAppview(event.currentTarget.checked); 320 + }} 321 + class="my-0 py-0 rounded border-gray-300 dark:border-gray-600 text-blue-600 size-4 p-0 m-0 untangled-checkbox" 322 + /> 323 + <label 324 + for="routeKnotRequestsThroughAppview" 325 + class="my-0 py-0 normal-case font-normal text-sm text-gray-700 dark:text-gray-300 cursor-pointer" 326 + > 327 + route knot requests through appview 328 + </label> 329 + </div> 330 + 331 + <div class="flex items-center gap-2"> 332 + <input 333 + type="checkbox" 334 + id="forcePds" 335 + checked={draftForcePds()} 336 + onChange={(event) => { 337 + setSaved(false); 338 + setError(null); 339 + updateForcePds(event.currentTarget.checked); 340 + }} 341 + class="my-0 py-0 rounded border-gray-300 dark:border-gray-600 text-blue-600 size-4 p-0 m-0 untangled-checkbox" 342 + /> 343 + <label 344 + for="forcePds" 345 + class="my-0 py-0 normal-case font-normal text-sm text-gray-700 dark:text-gray-300 cursor-pointer" 346 + > 347 + force requests through PDS (bypass appview) 348 + </label> 349 + </div> 350 + </div> 351 + </div> 352 + 353 + <div class="flex items-center justify-between gap-3 pt-1 max-w-xl"> 354 + <button type="button" class={buttonStyles()} onClick={onReset}> 355 + <RotateCcw class="size-4" /> 356 + reset 357 + </button> 358 + <Show when={saved() && !error()}> 359 + <p class="text-sm text-gray-500 dark:text-gray-400">saved</p> 360 + </Show> 361 + </div> 362 + 363 + <Show when={error()}> 364 + <p class="text-sm text-red-500">{error()}</p> 365 + </Show> 366 + </div> 190 367 ); 191 368 }; 192 369
+3 -1
src/routes.tsx
··· 25 25 import { StringPage, RawStringPage, NewStringPage, EditStringPage } from './pages/string'; 26 26 import { 27 27 SettingsLayout, 28 + AppSettingsTab, 28 29 ProfileSettingsTab, 29 30 KeysSettingsTab, 30 31 EmailsSettingsTab, ··· 46 47 <Route path="/strings/:actor/:rkey/raw" component={RawStringPage} /> 47 48 <Route path="/repo/new" component={NewRepoPage} /> 48 49 <Route path="/settings" component={SettingsLayout}> 49 - <Route path="/" component={ProfileSettingsTab} /> 50 + <Route path="/" component={AppSettingsTab} /> 51 + <Route path="/app" component={AppSettingsTab} /> 50 52 <Route path="/profile" component={ProfileSettingsTab} /> 51 53 <Route path="/keys" component={KeysSettingsTab} /> 52 54 <Route path="/emails" component={EmailsSettingsTab} />