Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
9.8 kB · 318 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319import { useState, type ReactNode } from 'react';import { LOGIN_NAME, useHabitatAuth } from '../auth/habitatAuth';import { WORKSPACE_DESCRIPTORS, type WorkspaceKind,} from '../workspaces/workspace';import { Modal } from './Modal';import { SettingsPaneLayout, SettingsPaneSection } from './SettingsPane';import { HotkeysPane } from './HotkeysPane';
type SettingsModalProps = { open: boolean; onClose: () => void; theme: 'light' | 'dark'; onToggleTheme: () => void; workspaceKind: WorkspaceKind; onWorkspaceKindChange: (kind: WorkspaceKind) => void;};
type SettingsCategory = 'atmosphere' | 'appearance' | 'hotkeys';
function AtmosphereAccountPane() { const { authInfo, login, logout } = useHabitatAuth(); const [accountInput, setAccountInput] = useState(''); const [error, setError] = useState<string | null>(null); const [isSubmitting, setIsSubmitting] = useState(false);
const onLogin = async () => { try { setIsSubmitting(true); setError(null); await login(accountInput); setAccountInput(''); } catch (loginError) { setError( loginError instanceof Error ? loginError.message : `Unable to complete ${LOGIN_NAME} Account sign-in.`, ); } finally { setIsSubmitting(false); } };
if (authInfo) { return ( <div className="space-y-4"> <div className="border-border bg-background/70 space-y-2 rounded-md border p-3 text-sm"> <div> <span className="text-foreground/60">{`${LOGIN_NAME} Handle`}</span> <span className="text-foreground"> {authInfo.handle}</span> </div> <div> <span className="text-foreground/60">DID:</span>{' '} <span className="text-foreground break-all">{authInfo.did}</span> </div> </div> <button type="button" onClick={logout} className="border-border bg-background text-foreground hover:bg-border/30 rounded border px-3 py-1.5 text-xs" > Log out </button> </div> ); }
return ( <div className="space-y-4"> {/* TODO: Add account creation / sign-up flow when product requirements are ready. */} <label htmlFor={`${LOGIN_NAME.toLowerCase()}-account-input`} className="block space-y-1" > <span className="text-foreground/70 text-sm">{`${LOGIN_NAME} Account`}</span> <div className="flex min-w-0 items-center gap-2"> <input id={`${LOGIN_NAME.toLowerCase()}-account-input`} type="text" value={accountInput} onChange={(event) => setAccountInput(event.target.value)} placeholder="alice.bsky.social" className="border-border bg-background text-foreground min-w-0 flex-1 rounded-md border px-3 py-2 text-sm outline-none" /> <button type="button" onClick={onLogin} disabled={isSubmitting} className="bg-primary text-primary-foreground shrink-0 rounded px-3 py-2 text-xs hover:opacity-90 disabled:opacity-50" > {isSubmitting ? 'Signing in…' : 'Sign in'} </button> </div> </label> {error ? <p className="text-destructive text-sm">{error}</p> : null} </div> );}
function AccountIcon() { return ( <svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden > <circle cx="12" cy="8" r="4" /> <path d="M4 20c1.6-3.4 4.5-5 8-5s6.4 1.6 8 5" /> </svg> );}
function AppearanceIcon() { return ( <svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden > <path d="M12 3a9 9 0 1 0 9 9c0-4.97-4.03-9-9-9Z" /> <path d="M12 3v18" /> </svg> );}
function SunIcon() { return ( <svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden > <circle cx="12" cy="12" r="4" /> <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" /> </svg> );}
function MoonIcon() { return ( <svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden > <path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z" /> </svg> );}
function KeyboardIcon() { return ( <svg className="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden > <rect x="2" y="4" width="20" height="16" rx="2" /> <path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M8 12h.01M12 12h.01M16 12h.01M7 16h10" /> </svg> );}
export function SettingsModal({ open, onClose, theme, onToggleTheme, workspaceKind, onWorkspaceKindChange,}: SettingsModalProps) { const [selectedCategory, setSelectedCategory] = useState<SettingsCategory>('atmosphere');
if (!open) return null;
const categories: { id: SettingsCategory; label: string; icon: ReactNode }[] = [ { id: 'atmosphere', label: `${LOGIN_NAME} Account`, icon: <AccountIcon />, }, { id: 'appearance', label: 'Appearance', icon: <AppearanceIcon />, }, { id: 'hotkeys', label: 'Hotkeys', icon: <KeyboardIcon />, }, ];
return ( <Modal open={open} onClose={onClose} ariaLabel="Settings" title="Settings" panelClassName="h-[460px] w-[720px]" > <SettingsPaneLayout sidebar={ <div className="space-y-1"> {categories.map((category) => ( <button key={category.id} type="button" onClick={() => setSelectedCategory(category.id)} className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm ${ selectedCategory === category.id ? 'bg-border/50 text-foreground' : 'text-foreground/90 hover:bg-border/40' }`} > {category.icon} <span>{category.label}</span> </button> ))} </div> } > {selectedCategory === 'atmosphere' ? ( <AtmosphereAccountPane /> ) : selectedCategory === 'hotkeys' ? ( <HotkeysPane /> ) : ( <div className="space-y-4"> <SettingsPaneSection title="Theme"> <button type="button" onClick={onToggleTheme} role="switch" aria-checked={theme === 'dark'} aria-label="Toggle dark mode" className="border-border bg-background hover:bg-border/20 inline-flex h-8 w-16 items-center rounded-full border px-1 transition-colors" > <span className={`bg-foreground text-background inline-flex size-6 items-center justify-center rounded-full transition-transform duration-500 ease-in-out ${ theme === 'dark' ? 'translate-x-8' : 'translate-x-0' }`} > {theme === 'dark' ? <MoonIcon /> : <SunIcon />} </span> </button> </SettingsPaneSection> <div className="border-border border-t pt-3"> <p className="text-foreground/70 text-sm">Workspace</p> <p className="text-foreground/60 mt-1 text-xs"> Choose how the main editing surface is laid out. Switching resets the open documents in the workspace. </p> <div role="radiogroup" aria-label="Workspace layout" className="mt-3 space-y-1" > {WORKSPACE_DESCRIPTORS.map((descriptor) => { const selected = descriptor.kind === workspaceKind; return ( <button key={descriptor.kind} type="button" role="radio" aria-checked={selected} onClick={() => onWorkspaceKindChange(descriptor.kind)} className={`focus-visible:ring-accent/50 flex w-full flex-col items-start gap-0.5 rounded-md border px-3 py-2 text-left outline-none focus-visible:ring-2 ${ selected ? 'border-border bg-border/40 text-foreground' : 'border-border/60 text-foreground/90 hover:bg-border/25' }`} > <span className="text-sm font-medium"> {descriptor.label} </span> <span className="text-foreground/60 text-xs leading-snug"> {descriptor.description} </span> </button> ); })} </div> </div> {/* TODO: Add configurable theme transition controls with richer theme presets. */} </div> )} </SettingsPaneLayout> </Modal> );}