Barazo default frontend barazo.forum
2
fork

Configure Feed

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

style(ui): sentence case headings, settings page restructure (#117)

* refactor(settings): move reports into community section, add sidebar nav

Reports are scoped to the current AppView, not global across forums.
Moved the reports link into the community-scoped section where it belongs.
Added a sticky sidebar with anchor links between community and global
settings sections, visible on desktop only (hidden below 768px).

* style(ui): apply sentence case to all headings, buttons, and labels

Codified as writing principle #12 in brand voice guide: capitalize only
the first word and proper nouns. Applied across 25 files including page
headings, admin panel, legal pages, legends, breadcrumbs, and buttons.

authored by

Guido X Jansen and committed by
GitHub
7e4771c7 11a38e38

+347 -265
+7 -7
src/app/accessibility/page.tsx
··· 33 33 <div className="mx-auto max-w-2xl space-y-8"> 34 34 <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Accessibility' }]} /> 35 35 36 - <h1 className="text-2xl font-bold text-foreground">Accessibility Statement</h1> 36 + <h1 className="text-2xl font-bold text-foreground">Accessibility statement</h1> 37 37 38 38 <section className="space-y-3"> 39 - <h2 className="text-lg font-semibold text-foreground">Our Commitment</h2> 39 + <h2 className="text-lg font-semibold text-foreground">Our commitment</h2> 40 40 <p className="text-sm leading-relaxed text-muted-foreground"> 41 41 Barazo is committed to ensuring digital accessibility for people with disabilities. We 42 42 continually improve the user experience for everyone and apply the relevant ··· 45 45 </section> 46 46 47 47 <section className="space-y-3"> 48 - <h2 className="text-lg font-semibold text-foreground">Conformance Status</h2> 48 + <h2 className="text-lg font-semibold text-foreground">Conformance status</h2> 49 49 <p className="text-sm leading-relaxed text-muted-foreground"> 50 50 We aim to conform to the{' '} 51 51 <strong>Web Content Accessibility Guidelines (WCAG) 2.2 Level AA</strong>. These ··· 55 55 </section> 56 56 57 57 <section className="space-y-3"> 58 - <h2 className="text-lg font-semibold text-foreground">Testing Methods</h2> 58 + <h2 className="text-lg font-semibold text-foreground">Testing methods</h2> 59 59 <p className="text-sm leading-relaxed text-muted-foreground"> 60 60 We test accessibility through a combination of methods: 61 61 </p> ··· 80 80 </section> 81 81 82 82 <section className="space-y-3"> 83 - <h2 className="text-lg font-semibold text-foreground">Accessibility Features</h2> 83 + <h2 className="text-lg font-semibold text-foreground">Accessibility features</h2> 84 84 <ul className="list-inside list-disc space-y-2 text-sm text-muted-foreground"> 85 85 <li>Semantic HTML with proper heading hierarchy and landmark regions.</li> 86 86 <li>Skip links for jumping to main content and the reply editor.</li> ··· 93 93 </section> 94 94 95 95 <section className="space-y-3"> 96 - <h2 className="text-lg font-semibold text-foreground">Known Limitations</h2> 96 + <h2 className="text-lg font-semibold text-foreground">Known limitations</h2> 97 97 <p className="text-sm leading-relaxed text-muted-foreground"> 98 98 While we strive for full accessibility, some areas may have limitations: 99 99 </p> ··· 107 107 </section> 108 108 109 109 <section className="space-y-3"> 110 - <h2 className="text-lg font-semibold text-foreground">Contact Us</h2> 110 + <h2 className="text-lg font-semibold text-foreground">Contact us</h2> 111 111 <p className="text-sm leading-relaxed text-muted-foreground"> 112 112 If you encounter accessibility barriers on Barazo, please contact us. We take 113 113 accessibility feedback seriously and will work to address issues promptly.
+4 -4
src/app/admin/content-ratings/page.tsx
··· 77 77 return ( 78 78 <AdminLayout> 79 79 <div className="space-y-6"> 80 - <h1 className="text-2xl font-bold text-foreground">Content Ratings</h1> 80 + <h1 className="text-2xl font-bold text-foreground">Content ratings</h1> 81 81 82 82 {/* Rating level explanation */} 83 83 <div className="rounded-lg border border-border bg-card p-4"> 84 - <h2 className="mb-3 text-lg font-semibold text-foreground">Maturity Levels</h2> 84 + <h2 className="mb-3 text-lg font-semibold text-foreground">Maturity levels</h2> 85 85 <dl className="space-y-2"> 86 86 {(Object.entries(MATURITY_DESCRIPTIONS) as [MaturityRating, string][]).map( 87 87 ([rating, description]) => ( ··· 110 110 {/* Community rating */} 111 111 {communitySettings && ( 112 112 <div className="rounded-lg border border-border bg-card p-4"> 113 - <h2 className="mb-2 text-lg font-semibold text-foreground">Community Rating</h2> 113 + <h2 className="mb-2 text-lg font-semibold text-foreground">Community rating</h2> 114 114 <p className="text-sm text-muted-foreground"> 115 115 Current community maturity rating:{' '} 116 116 <span ··· 128 128 {/* Category ratings table */} 129 129 {!loading && categories.length > 0 && ( 130 130 <div className="rounded-lg border border-border bg-card p-4"> 131 - <h2 className="mb-3 text-lg font-semibold text-foreground">Category Ratings</h2> 131 + <h2 className="mb-3 text-lg font-semibold text-foreground">Category ratings</h2> 132 132 <table className="w-full"> 133 133 <thead> 134 134 <tr className="border-b border-border text-left">
+2 -2
src/app/admin/onboarding/page.tsx
··· 38 38 <div className="space-y-6"> 39 39 <div className="flex items-center justify-between"> 40 40 <div> 41 - <h1 className="text-2xl font-bold text-foreground">Onboarding Fields</h1> 41 + <h1 className="text-2xl font-bold text-foreground">Onboarding fields</h1> 42 42 <p className="mt-1 text-sm text-muted-foreground"> 43 43 Configure fields that users must complete before they can post in this community. 44 44 </p> ··· 49 49 className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90" 50 50 > 51 51 <Plus size={16} aria-hidden="true" /> 52 - Add Field 52 + Add field 53 53 </button> 54 54 </div> 55 55
+2 -2
src/app/admin/plugins/page.test.tsx
··· 97 97 98 98 // Should show dependency warning dialog 99 99 await waitFor(() => { 100 - expect(screen.getByText('Dependency Warning')).toBeInTheDocument() 100 + expect(screen.getByText('Dependency warning')).toBeInTheDocument() 101 101 }) 102 - expect(screen.getByText('Disable Anyway')).toBeInTheDocument() 102 + expect(screen.getByText('Disable anyway')).toBeInTheDocument() 103 103 }) 104 104 105 105 it('opens settings modal when settings button is clicked', async () => {
+1 -1
src/app/admin/settings/page.tsx
··· 99 99 return ( 100 100 <AdminLayout> 101 101 <div className="space-y-6"> 102 - <h1 className="text-2xl font-bold text-foreground">Community Settings</h1> 102 + <h1 className="text-2xl font-bold text-foreground">Community settings</h1> 103 103 104 104 {loading && <p className="text-sm text-muted-foreground">Loading settings...</p>} 105 105
+1 -1
src/app/admin/sybil-detection/page.tsx
··· 43 43 <AdminLayout> 44 44 <div className="space-y-6"> 45 45 <div> 46 - <h1 className="text-2xl font-bold text-foreground">Sybil Detection</h1> 46 + <h1 className="text-2xl font-bold text-foreground">Sybil detection</h1> 47 47 <p className="mt-1 text-sm text-muted-foreground"> 48 48 Monitor the trust graph for suspicious account clusters. The system uses EigenTrust 49 49 scores and behavioral heuristics to detect coordinated inauthentic activity.
+2 -2
src/app/admin/trust-seeds/page.tsx
··· 80 80 <AdminLayout> 81 81 <div className="space-y-6"> 82 82 <div> 83 - <h1 className="text-2xl font-bold text-foreground">Trust Seeds</h1> 83 + <h1 className="text-2xl font-bold text-foreground">Trust seeds</h1> 84 84 <p className="mt-1 text-sm text-muted-foreground"> 85 85 Trusted accounts that anchor the EigenTrust computation. Manual seeds are explicitly 86 86 added by admins. Automatic seeds are derived from moderators and admins. ··· 108 108 aria-label="Add trust seed" 109 109 className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90" 110 110 > 111 - Add Trust Seed 111 + Add trust seed 112 112 </button> 113 113 </div> 114 114
+1 -1
src/app/admin/users/page.tsx
··· 76 76 return ( 77 77 <AdminLayout> 78 78 <div className="space-y-6"> 79 - <h1 className="text-2xl font-bold text-foreground">User Management</h1> 79 + <h1 className="text-2xl font-bold text-foreground">User management</h1> 80 80 81 81 {loadError && ( 82 82 <ErrorAlert message={loadError} variant="page" onRetry={() => void fetchUsers()} />
+7 -7
src/app/legal/cookies/page.tsx
··· 31 31 return ( 32 32 <ForumLayout communityName={communityName}> 33 33 <div className="mx-auto max-w-2xl space-y-8"> 34 - <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Cookie Policy' }]} /> 34 + <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Cookie policy' }]} /> 35 35 36 - <h1 className="text-2xl font-bold text-foreground">Cookie Policy</h1> 36 + <h1 className="text-2xl font-bold text-foreground">Cookie policy</h1> 37 37 38 38 <section className="space-y-3"> 39 39 <h2 className="text-lg font-semibold text-foreground">Overview</h2> ··· 45 45 </section> 46 46 47 47 <section className="space-y-3"> 48 - <h2 className="text-lg font-semibold text-foreground">Cookies We Use</h2> 48 + <h2 className="text-lg font-semibold text-foreground">Cookies we use</h2> 49 49 <p className="text-sm leading-relaxed text-muted-foreground"> 50 50 Barazo uses a single essential cookie: 51 51 </p> ··· 74 74 </section> 75 75 76 76 <section className="space-y-3"> 77 - <h2 className="text-lg font-semibold text-foreground">Technical Details</h2> 77 + <h2 className="text-lg font-semibold text-foreground">Technical details</h2> 78 78 <p className="text-sm leading-relaxed text-muted-foreground"> 79 79 The refresh token cookie has the following security properties: 80 80 </p> ··· 98 98 </section> 99 99 100 100 <section className="space-y-3"> 101 - <h2 className="text-lg font-semibold text-foreground">What We Do Not Use</h2> 101 + <h2 className="text-lg font-semibold text-foreground">What we do not use</h2> 102 102 <ul className="list-inside list-disc space-y-2 text-sm text-muted-foreground"> 103 103 <li>No tracking or advertising cookies.</li> 104 104 <li>No third-party analytics (Google Analytics, etc.).</li> ··· 108 108 </section> 109 109 110 110 <section className="space-y-3"> 111 - <h2 className="text-lg font-semibold text-foreground">Cookie Consent</h2> 111 + <h2 className="text-lg font-semibold text-foreground">Cookie consent</h2> 112 112 <p className="text-sm leading-relaxed text-muted-foreground"> 113 113 Because we only use a single essential cookie required for the service to function, a 114 114 cookie consent banner is not required under the ePrivacy Directive (EU Directive ··· 118 118 </section> 119 119 120 120 <section className="space-y-3"> 121 - <h2 className="text-lg font-semibold text-foreground">Theme Preference</h2> 121 + <h2 className="text-lg font-semibold text-foreground">Theme preference</h2> 122 122 <p className="text-sm leading-relaxed text-muted-foreground"> 123 123 Your light/dark mode preference is stored in localStorage (not a cookie). This is a 124 124 client-side preference that is never sent to our servers.
+11 -11
src/app/legal/privacy/page.tsx
··· 31 31 return ( 32 32 <ForumLayout communityName={communityName}> 33 33 <div className="mx-auto max-w-2xl space-y-8"> 34 - <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Privacy Policy' }]} /> 34 + <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Privacy policy' }]} /> 35 35 36 - <h1 className="text-2xl font-bold text-foreground">Privacy Policy</h1> 36 + <h1 className="text-2xl font-bold text-foreground">Privacy policy</h1> 37 37 38 38 <section className="space-y-3"> 39 39 <h2 className="text-lg font-semibold text-foreground">Overview</h2> ··· 46 46 </section> 47 47 48 48 <section className="space-y-3"> 49 - <h2 className="text-lg font-semibold text-foreground">What We Collect</h2> 49 + <h2 className="text-lg font-semibold text-foreground">What we collect</h2> 50 50 <p className="text-sm leading-relaxed text-muted-foreground"> 51 51 When you use Barazo, we process the following data: 52 52 </p> ··· 89 89 </section> 90 90 91 91 <section className="space-y-3"> 92 - <h2 className="text-lg font-semibold text-foreground">What We Do Not Collect</h2> 92 + <h2 className="text-lg font-semibold text-foreground">What we do not collect</h2> 93 93 <ul className="list-inside list-disc space-y-2 text-sm text-muted-foreground"> 94 94 <li> 95 95 We do not collect or store your password (authentication is handled via AT Protocol ··· 106 106 </section> 107 107 108 108 <section className="space-y-3"> 109 - <h2 className="text-lg font-semibold text-foreground">Legal Basis</h2> 109 + <h2 className="text-lg font-semibold text-foreground">Legal basis</h2> 110 110 <p className="text-sm leading-relaxed text-muted-foreground"> 111 111 We process your data under the following legal bases (GDPR Art. 6): 112 112 </p> ··· 124 124 </section> 125 125 126 126 <section className="space-y-3"> 127 - <h2 className="text-lg font-semibold text-foreground">Data Storage and Transfers</h2> 127 + <h2 className="text-lg font-semibold text-foreground">Data storage and transfers</h2> 128 128 <p className="text-sm leading-relaxed text-muted-foreground"> 129 129 Our servers are hosted in the European Union (Hetzner, Germany). We use the following 130 130 sub-processors: ··· 141 141 </section> 142 142 143 143 <section className="space-y-3"> 144 - <h2 className="text-lg font-semibold text-foreground">Data Retention and Deletion</h2> 144 + <h2 className="text-lg font-semibold text-foreground">Data retention and deletion</h2> 145 145 <p className="text-sm leading-relaxed text-muted-foreground"> 146 146 Your indexed data is retained while the source exists on your AT Protocol PDS. When you 147 147 delete content or your account via the AT Protocol, we process the deletion event ··· 176 176 </section> 177 177 178 178 <section className="space-y-3"> 179 - <h2 className="text-lg font-semibold text-foreground">AI Features</h2> 179 + <h2 className="text-lg font-semibold text-foreground">AI features</h2> 180 180 <p className="text-sm leading-relaxed text-muted-foreground"> 181 181 Barazo offers optional AI features including thread summaries, semantic search, and 182 182 content moderation assistance. Here is how they work: ··· 202 202 </section> 203 203 204 204 <section className="space-y-3"> 205 - <h2 className="text-lg font-semibold text-foreground">Content Labels</h2> 205 + <h2 className="text-lg font-semibold text-foreground">Content labels</h2> 206 206 <p className="text-sm leading-relaxed text-muted-foreground"> 207 207 We subscribe to content labeling services (such as Bluesky&apos;s Ozone) for spam 208 208 detection and content moderation. Labels applied to your account may affect posting ··· 212 212 </section> 213 213 214 214 <section className="space-y-3"> 215 - <h2 className="text-lg font-semibold text-foreground">Your Rights</h2> 215 + <h2 className="text-lg font-semibold text-foreground">Your rights</h2> 216 216 <p className="text-sm leading-relaxed text-muted-foreground"> 217 217 Under the GDPR, you have the right to: 218 218 </p> ··· 242 242 </section> 243 243 244 244 <section className="space-y-3"> 245 - <h2 className="text-lg font-semibold text-foreground">Data Breach Notification</h2> 245 + <h2 className="text-lg font-semibold text-foreground">Data breach notification</h2> 246 246 <p className="text-sm leading-relaxed text-muted-foreground"> 247 247 In the event of a data breach, we will notify the Dutch Data Protection Authority within 248 248 72 hours (GDPR Art. 33). For high-risk breaches, we will notify affected users without
+12 -12
src/app/legal/terms/page.tsx
··· 31 31 return ( 32 32 <ForumLayout communityName={communityName}> 33 33 <div className="mx-auto max-w-2xl space-y-8"> 34 - <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Terms of Service' }]} /> 34 + <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Terms of service' }]} /> 35 35 36 - <h1 className="text-2xl font-bold text-foreground">Terms of Service</h1> 36 + <h1 className="text-2xl font-bold text-foreground">Terms of service</h1> 37 37 38 38 <section className="space-y-3"> 39 - <h2 className="text-lg font-semibold text-foreground">Acceptance of Terms</h2> 39 + <h2 className="text-lg font-semibold text-foreground">Acceptance of terms</h2> 40 40 <p className="text-sm leading-relaxed text-muted-foreground"> 41 41 By accessing or using Barazo, you agree to be bound by these Terms of Service. If you do 42 42 not agree to these terms, you may not use the service. Barazo reserves the right to ··· 55 55 </section> 56 56 57 57 <section className="space-y-3"> 58 - <h2 className="text-lg font-semibold text-foreground">Account and Authentication</h2> 58 + <h2 className="text-lg font-semibold text-foreground">Account and authentication</h2> 59 59 <p className="text-sm leading-relaxed text-muted-foreground"> 60 60 Barazo uses the AT Protocol for authentication. You log in using your existing AT 61 61 Protocol identity (e.g., a Bluesky account). You are responsible for maintaining the ··· 64 64 </section> 65 65 66 66 <section className="space-y-3"> 67 - <h2 className="text-lg font-semibold text-foreground">Content and Conduct</h2> 67 + <h2 className="text-lg font-semibold text-foreground">Content and conduct</h2> 68 68 <p className="text-sm leading-relaxed text-muted-foreground"> 69 69 You retain ownership of content you post on Barazo. By posting, you grant Barazo a 70 70 license to display, index, and distribute your content as part of the forum service and ··· 88 88 </section> 89 89 90 90 <section className="space-y-3"> 91 - <h2 className="text-lg font-semibold text-foreground">Content Maturity Ratings</h2> 91 + <h2 className="text-lg font-semibold text-foreground">Content maturity ratings</h2> 92 92 <p className="text-sm leading-relaxed text-muted-foreground"> 93 93 Communities and categories may be rated for content maturity (Safe for Work, Mature, or 94 94 Adult). You are responsible for accurately labeling your content. Communities may ··· 98 98 </section> 99 99 100 100 <section className="space-y-3"> 101 - <h2 className="text-lg font-semibold text-foreground">Cross-Posting</h2> 101 + <h2 className="text-lg font-semibold text-foreground">Cross-posting</h2> 102 102 <p className="text-sm leading-relaxed text-muted-foreground"> 103 103 Barazo may cross-post your content to connected platforms (such as Bluesky or Frontpage) 104 104 when you enable this feature. Cross-posting is optional and can be controlled in your ··· 107 107 </section> 108 108 109 109 <section className="space-y-3"> 110 - <h2 className="text-lg font-semibold text-foreground">Moderation and Labels</h2> 110 + <h2 className="text-lg font-semibold text-foreground">Moderation and labels</h2> 111 111 <p className="text-sm leading-relaxed text-muted-foreground"> 112 112 Your account may be labeled by independent moderation services (such as Bluesky&apos;s 113 113 Ozone). Labels affect posting limits and content visibility. You cannot delete labels ··· 117 117 </section> 118 118 119 119 <section className="space-y-3"> 120 - <h2 className="text-lg font-semibold text-foreground">AI-Generated Summaries</h2> 120 + <h2 className="text-lg font-semibold text-foreground">AI-generated summaries</h2> 121 121 <p className="text-sm leading-relaxed text-muted-foreground"> 122 122 Barazo may generate AI-powered summaries of discussion threads. These summaries are 123 123 anonymized derivative works that do not contain personal data (no usernames or verbatim ··· 128 128 </section> 129 129 130 130 <section className="space-y-3"> 131 - <h2 className="text-lg font-semibold text-foreground">AT Protocol and Federation</h2> 131 + <h2 className="text-lg font-semibold text-foreground">AT Protocol and federation</h2> 132 132 <p className="text-sm leading-relaxed text-muted-foreground"> 133 133 Barazo is built on the AT Protocol, which is a federated, open network. Content you post 134 134 may be indexed by other services on the AT Protocol network. Barazo cannot control how ··· 146 146 </section> 147 147 148 148 <section className="space-y-3"> 149 - <h2 className="text-lg font-semibold text-foreground">Limitation of Liability</h2> 149 + <h2 className="text-lg font-semibold text-foreground">Limitation of liability</h2> 150 150 <p className="text-sm leading-relaxed text-muted-foreground"> 151 151 Barazo is provided &quot;as is&quot; without warranties of any kind. We are not liable 152 152 for any damages arising from your use of the service, including but not limited to loss ··· 156 156 </section> 157 157 158 158 <section className="space-y-3"> 159 - <h2 className="text-lg font-semibold text-foreground">Governing Law</h2> 159 + <h2 className="text-lg font-semibold text-foreground">Governing law</h2> 160 160 <p className="text-sm leading-relaxed text-muted-foreground"> 161 161 These terms are governed by the laws of the Netherlands. Any disputes arising from these 162 162 terms will be subject to the exclusive jurisdiction of the courts of the Netherlands.
+2 -2
src/app/new/page.test.tsx
··· 65 65 describe('NewTopicPage', () => { 66 66 it('renders create topic heading', () => { 67 67 render(<NewTopicPage />) 68 - expect(screen.getByRole('heading', { name: 'Create New Topic' })).toBeInTheDocument() 68 + expect(screen.getByRole('heading', { name: 'Create new topic' })).toBeInTheDocument() 69 69 }) 70 70 71 71 it('renders topic form', () => { ··· 78 78 it('renders breadcrumbs', () => { 79 79 render(<NewTopicPage />) 80 80 expect(screen.getByText('Home')).toBeInTheDocument() 81 - expect(screen.getByText('New Topic')).toBeInTheDocument() 81 + expect(screen.getByText('New topic')).toBeInTheDocument() 82 82 }) 83 83 })
+2 -2
src/app/new/page.tsx
··· 71 71 return ( 72 72 <ForumLayout communityName={communityName}> 73 73 <div className="space-y-6"> 74 - <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'New Topic' }]} /> 74 + <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'New topic' }]} /> 75 75 76 - <h1 className="text-2xl font-bold text-foreground">Create New Topic</h1> 76 + <h1 className="text-2xl font-bold text-foreground">Create new topic</h1> 77 77 78 78 {error && ( 79 79 <div
+7 -9
src/app/settings/page.test.tsx
··· 106 106 await waitFor(() => { 107 107 expect(screen.getByText('Blocked users')).toBeInTheDocument() 108 108 }) 109 - const safetyFieldset = screen.getByText('Content Safety').closest('fieldset')! 109 + const safetyFieldset = screen.getByText('Content safety').closest('fieldset')! 110 110 expect(within(safetyFieldset).getByLabelText('Handle to block')).toBeInTheDocument() 111 111 expect(within(safetyFieldset).getByText('No users blocked.')).toBeInTheDocument() 112 112 }) ··· 114 114 it('renders cross-posting section', async () => { 115 115 render(<SettingsPage />) 116 116 await waitFor(() => { 117 - expect(screen.getByText('Cross-Posting')).toBeInTheDocument() 117 + expect(screen.getByText('Cross-posting')).toBeInTheDocument() 118 118 }) 119 119 expect(screen.getByLabelText(/bluesky/i)).toBeInTheDocument() 120 120 expect(screen.getByLabelText(/frontpage/i)).toBeInTheDocument() ··· 212 212 ).toBeTruthy() 213 213 }) 214 214 215 - it('renders reports link below both sections', async () => { 215 + it('renders reports link inside the community section', async () => { 216 216 render(<SettingsPage />) 217 217 await waitFor(() => { 218 218 expect(screen.getByRole('link', { name: /view my reports/i })).toBeInTheDocument() 219 219 }) 220 220 221 221 const reportsLink = screen.getByRole('link', { name: /view my reports/i }) 222 - const globalSection = screen 223 - .getByRole('heading', { name: /your settings across all barazo forums/i, level: 2 }) 222 + const communitySection = screen 223 + .getByRole('heading', { name: /your .+ settings/i, level: 2 }) 224 224 .closest('section')! 225 225 226 - // Reports link comes after the global section 227 - expect( 228 - globalSection.compareDocumentPosition(reportsLink) & Node.DOCUMENT_POSITION_FOLLOWING 229 - ).toBeTruthy() 226 + // Reports link is inside the community section (scoped to this AppView) 227 + expect(communitySection.contains(reportsLink)).toBe(true) 230 228 }) 231 229 232 230 it('does not render community overrides content', async () => {
+186 -175
src/app/settings/page.tsx
··· 2 2 * User settings page. 3 3 * URL: /settings 4 4 * Two scope-grouped sections: community-specific and global. 5 + * Desktop sidebar with sticky anchor-link navigation. 5 6 * Client component (form state). 6 7 * @see specs/prd-web.md Section M8 (Settings page) 7 8 */ ··· 19 20 import { ContentSafetySection } from '@/components/settings/content-safety-section' 20 21 import { CrossPostingSection } from '@/components/settings/cross-posting-section' 21 22 import { NotificationsSection } from '@/components/settings/notifications-section' 23 + import { SettingsSidebar } from '@/components/settings/settings-sidebar' 22 24 import { AGE_OPTIONS } from '@/lib/constants' 23 25 import { cn } from '@/lib/utils' 24 26 import { useSettingsForm } from '@/hooks/use-settings-form' ··· 62 64 return ( 63 65 <ForumLayout communityName={communityName}> 64 66 <div className="space-y-6"> 65 - <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Account Settings' }]} /> 67 + <Breadcrumbs items={[{ label: 'Home', href: '/' }, { label: 'Account settings' }]} /> 66 68 67 - <h1 className="text-2xl font-bold text-foreground">Account Settings</h1> 69 + <h1 className="text-2xl font-bold text-foreground">Account settings</h1> 68 70 69 71 {loading ? ( 70 72 <div className="max-w-2xl animate-pulse space-y-4"> ··· 73 75 <div className="h-24 rounded-lg bg-muted" /> 74 76 </div> 75 77 ) : ( 76 - <div className="max-w-2xl space-y-12"> 77 - {/* Section 1: Community-scoped settings */} 78 - <section aria-labelledby="community-settings-heading"> 79 - <h2 id="community-settings-heading" className="text-lg font-semibold text-foreground"> 80 - Your {displayName} Settings 81 - </h2> 82 - <p className="mt-1 text-sm text-muted-foreground"> 83 - These settings only affect this community. 84 - </p> 78 + <div className="flex gap-8"> 79 + {/* Sticky sidebar navigation (desktop only) */} 80 + <SettingsSidebar communityName={displayName} /> 85 81 86 - {communityError && ( 87 - <p 88 - className="mt-4 rounded-md bg-destructive/10 px-4 py-2 text-sm text-destructive" 89 - role="alert" 82 + <div className="max-w-2xl flex-1 space-y-12"> 83 + {/* Section 1: Community-scoped settings */} 84 + <section id="community-settings" aria-labelledby="community-settings-heading"> 85 + <h2 86 + id="community-settings-heading" 87 + className="text-lg font-semibold text-foreground" 90 88 > 91 - {communityError} 89 + Your {displayName} settings 90 + </h2> 91 + <p className="mt-1 text-sm text-muted-foreground"> 92 + These settings only affect this community. 92 93 </p> 93 - )} 94 + 95 + {communityError && ( 96 + <p 97 + className="mt-4 rounded-md bg-destructive/10 px-4 py-2 text-sm text-destructive" 98 + role="alert" 99 + > 100 + {communityError} 101 + </p> 102 + )} 103 + 104 + {communitySuccess && ( 105 + <p 106 + className="mt-4 rounded-md bg-green-500/10 px-4 py-2 text-sm text-green-700 dark:text-green-400" 107 + role="status" 108 + > 109 + Community settings saved successfully. 110 + </p> 111 + )} 112 + 113 + <div className="mt-6"> 114 + <CommunityProfileSettings /> 115 + </div> 116 + 117 + <form onSubmit={handleSaveCommunitySettings} className="mt-8 space-y-8" noValidate> 118 + <fieldset className="space-y-4 rounded-lg border border-border p-4"> 119 + <legend className="px-2 text-sm font-semibold text-foreground"> 120 + Age declaration 121 + </legend> 94 122 95 - {communitySuccess && ( 96 - <p 97 - className="mt-4 rounded-md bg-green-500/10 px-4 py-2 text-sm text-green-700 dark:text-green-400" 98 - role="status" 99 - > 100 - Community settings saved successfully. 101 - </p> 102 - )} 123 + <div className="space-y-1"> 124 + <label 125 + htmlFor="age-bracket" 126 + className="block text-sm font-medium text-foreground" 127 + > 128 + Age bracket 129 + </label> 130 + <select 131 + id="age-bracket" 132 + value={declaredAge ?? ''} 133 + onChange={(e) => handleAgeChange(Number(e.target.value))} 134 + className={cn( 135 + 'block w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground', 136 + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring' 137 + )} 138 + > 139 + <option value="" disabled> 140 + Select your age bracket 141 + </option> 142 + {AGE_OPTIONS.map((opt) => ( 143 + <option key={opt.value} value={opt.value}> 144 + {opt.label} 145 + </option> 146 + ))} 147 + </select> 148 + <p className="text-xs text-muted-foreground"> 149 + Your age bracket determines which content is available. Selecting 150 + &quot;Rather not say&quot; restricts you to safe content only. 151 + </p> 152 + </div> 153 + </fieldset> 103 154 104 - <div className="mt-6"> 105 - <CommunityProfileSettings /> 106 - </div> 155 + <CrossPostingSection 156 + authorized={crossPostScopesGranted} 157 + crossPostBluesky={values.crossPostBluesky} 158 + crossPostFrontpage={values.crossPostFrontpage} 159 + onBlueskyChange={(v) => setValues({ ...values, crossPostBluesky: v })} 160 + onFrontpageChange={(v) => setValues({ ...values, crossPostFrontpage: v })} 161 + onAuthorize={() => setShowCrossPostAuthDialog(true)} 162 + /> 107 163 108 - <form onSubmit={handleSaveCommunitySettings} className="mt-8 space-y-8" noValidate> 109 - <fieldset className="space-y-4 rounded-lg border border-border p-4"> 110 - <legend className="px-2 text-sm font-semibold text-foreground"> 111 - Age Declaration 112 - </legend> 164 + <NotificationsSection 165 + notifyReplies={values.notifyReplies} 166 + notifyMentions={values.notifyMentions} 167 + notifyReactions={values.notifyReactions} 168 + onRepliesChange={(v) => setValues({ ...values, notifyReplies: v })} 169 + onMentionsChange={(v) => setValues({ ...values, notifyMentions: v })} 170 + onReactionsChange={(v) => setValues({ ...values, notifyReactions: v })} 171 + /> 113 172 114 - <div className="space-y-1"> 115 - <label 116 - htmlFor="age-bracket" 117 - className="block text-sm font-medium text-foreground" 118 - > 119 - Age bracket 120 - </label> 121 - <select 122 - id="age-bracket" 123 - value={declaredAge ?? ''} 124 - onChange={(e) => handleAgeChange(Number(e.target.value))} 173 + {/* Reports link -- scoped to this community's AppView */} 174 + <div className="rounded-lg border border-border p-4"> 175 + <Link 176 + href="/settings/reports" 125 177 className={cn( 126 - 'block w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground', 178 + 'text-sm font-medium text-primary transition-colors', 179 + 'hover:text-primary-hover underline underline-offset-4', 127 180 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring' 128 181 )} 129 182 > 130 - <option value="" disabled> 131 - Select your age bracket 132 - </option> 133 - {AGE_OPTIONS.map((opt) => ( 134 - <option key={opt.value} value={opt.value}> 135 - {opt.label} 136 - </option> 137 - ))} 138 - </select> 139 - <p className="text-xs text-muted-foreground"> 140 - Your age bracket determines which content is available. Selecting &quot;Rather 141 - not say&quot; restricts you to safe content only. 183 + View my reports and appeals 184 + </Link> 185 + <p className="mt-1 text-xs text-muted-foreground"> 186 + Track the status of reports you have submitted to this community and appeal 187 + dismissed reports. 142 188 </p> 143 189 </div> 144 - </fieldset> 145 190 146 - <CrossPostingSection 147 - authorized={crossPostScopesGranted} 148 - crossPostBluesky={values.crossPostBluesky} 149 - crossPostFrontpage={values.crossPostFrontpage} 150 - onBlueskyChange={(v) => setValues({ ...values, crossPostBluesky: v })} 151 - onFrontpageChange={(v) => setValues({ ...values, crossPostFrontpage: v })} 152 - onAuthorize={() => setShowCrossPostAuthDialog(true)} 153 - /> 191 + <div className="flex justify-end"> 192 + <button 193 + type="submit" 194 + disabled={savingCommunity} 195 + className={cn( 196 + 'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors', 197 + 'hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', 198 + 'disabled:cursor-not-allowed disabled:opacity-50' 199 + )} 200 + > 201 + {savingCommunity ? 'Saving...' : 'Save community settings'} 202 + </button> 203 + </div> 204 + </form> 205 + </section> 154 206 155 - <NotificationsSection 156 - notifyReplies={values.notifyReplies} 157 - notifyMentions={values.notifyMentions} 158 - notifyReactions={values.notifyReactions} 159 - onRepliesChange={(v) => setValues({ ...values, notifyReplies: v })} 160 - onMentionsChange={(v) => setValues({ ...values, notifyMentions: v })} 161 - onReactionsChange={(v) => setValues({ ...values, notifyReactions: v })} 162 - /> 207 + {/* Visual separator */} 208 + <div className="border-t border-border" /> 163 209 164 - <div className="flex justify-end"> 165 - <button 166 - type="submit" 167 - disabled={savingCommunity} 168 - className={cn( 169 - 'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors', 170 - 'hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', 171 - 'disabled:cursor-not-allowed disabled:opacity-50' 172 - )} 210 + {/* Section 2: Global settings */} 211 + <section id="global-settings" aria-labelledby="global-settings-heading"> 212 + <h2 id="global-settings-heading" className="text-lg font-semibold text-foreground"> 213 + Your settings across all Barazo forums 214 + </h2> 215 + <p className="mt-1 text-sm text-muted-foreground"> 216 + {displayName} is built with{' '} 217 + <a 218 + href="https://barazo.forum" 219 + target="_blank" 220 + rel="noopener noreferrer" 221 + className="text-primary underline underline-offset-2 hover:text-primary-hover" 173 222 > 174 - {savingCommunity ? 'Saving...' : 'Save Community Settings'} 175 - </button> 176 - </div> 177 - </form> 178 - </section> 179 - 180 - {/* Visual separator */} 181 - <div className="border-t border-border" /> 182 - 183 - {/* Section 2: Global settings */} 184 - <section aria-labelledby="global-settings-heading"> 185 - <h2 id="global-settings-heading" className="text-lg font-semibold text-foreground"> 186 - Your Settings Across All Barazo Forums 187 - </h2> 188 - <p className="mt-1 text-sm text-muted-foreground"> 189 - {displayName} is built with{' '} 190 - <a 191 - href="https://barazo.forum" 192 - target="_blank" 193 - rel="noopener noreferrer" 194 - className="text-primary underline underline-offset-2 hover:text-primary-hover" 195 - > 196 - Barazo 197 - </a> 198 - , forum software built on the{' '} 199 - <a 200 - href="https://atproto.com/" 201 - target="_blank" 202 - rel="noopener noreferrer" 203 - className="text-primary underline underline-offset-2 hover:text-primary-hover" 204 - > 205 - AT Protocol 206 - </a> 207 - , allowing you to use the same identity and login across all Barazo forums. The 208 - settings below apply everywhere. 209 - </p> 210 - 211 - {globalError && ( 212 - <p 213 - className="mt-4 rounded-md bg-destructive/10 px-4 py-2 text-sm text-destructive" 214 - role="alert" 215 - > 216 - {globalError} 217 - </p> 218 - )} 219 - 220 - {globalSuccess && ( 221 - <p 222 - className="mt-4 rounded-md bg-green-500/10 px-4 py-2 text-sm text-green-700 dark:text-green-400" 223 - role="status" 224 - > 225 - Global settings saved successfully. 223 + Barazo 224 + </a> 225 + , forum software built on the{' '} 226 + <a 227 + href="https://atproto.com/" 228 + target="_blank" 229 + rel="noopener noreferrer" 230 + className="text-primary underline underline-offset-2 hover:text-primary-hover" 231 + > 232 + AT Protocol 233 + </a> 234 + , allowing you to use the same identity and login across all Barazo forums. The 235 + settings below apply everywhere. 226 236 </p> 227 - )} 228 237 229 - <form onSubmit={handleSaveGlobalSettings} className="mt-6 space-y-8" noValidate> 230 - <ContentSafetySection 231 - maturityLevel={values.maturityLevel} 232 - mutedWords={values.mutedWords} 233 - blockedUsers={values.blockedUsers} 234 - onMaturityChange={(level) => setValues({ ...values, maturityLevel: level })} 235 - onMutedWordsChange={(words) => setValues({ ...values, mutedWords: words })} 236 - onBlockUser={handleBlockUser} 237 - onUnblockUser={handleUnblockUser} 238 - /> 239 - 240 - <div className="flex justify-end"> 241 - <button 242 - type="submit" 243 - disabled={savingGlobal} 244 - className={cn( 245 - 'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors', 246 - 'hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', 247 - 'disabled:cursor-not-allowed disabled:opacity-50' 248 - )} 238 + {globalError && ( 239 + <p 240 + className="mt-4 rounded-md bg-destructive/10 px-4 py-2 text-sm text-destructive" 241 + role="alert" 249 242 > 250 - {savingGlobal ? 'Saving...' : 'Save Global Settings'} 251 - </button> 252 - </div> 253 - </form> 254 - </section> 243 + {globalError} 244 + </p> 245 + )} 255 246 256 - {/* Reports link below both sections */} 257 - <div className="rounded-lg border border-border p-4"> 258 - <Link 259 - href="/settings/reports" 260 - className={cn( 261 - 'text-sm font-medium text-primary transition-colors', 262 - 'hover:text-primary-hover underline underline-offset-4', 263 - 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring' 247 + {globalSuccess && ( 248 + <p 249 + className="mt-4 rounded-md bg-green-500/10 px-4 py-2 text-sm text-green-700 dark:text-green-400" 250 + role="status" 251 + > 252 + Global settings saved successfully. 253 + </p> 264 254 )} 265 - > 266 - View my reports and appeals 267 - </Link> 268 - <p className="mt-1 text-xs text-muted-foreground"> 269 - Track the status of reports you have submitted and appeal dismissed reports. 270 - </p> 255 + 256 + <form onSubmit={handleSaveGlobalSettings} className="mt-6 space-y-8" noValidate> 257 + <ContentSafetySection 258 + maturityLevel={values.maturityLevel} 259 + mutedWords={values.mutedWords} 260 + blockedUsers={values.blockedUsers} 261 + onMaturityChange={(level) => setValues({ ...values, maturityLevel: level })} 262 + onMutedWordsChange={(words) => setValues({ ...values, mutedWords: words })} 263 + onBlockUser={handleBlockUser} 264 + onUnblockUser={handleUnblockUser} 265 + /> 266 + 267 + <div className="flex justify-end"> 268 + <button 269 + type="submit" 270 + disabled={savingGlobal} 271 + className={cn( 272 + 'rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors', 273 + 'hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', 274 + 'disabled:cursor-not-allowed disabled:opacity-50' 275 + )} 276 + > 277 + {savingGlobal ? 'Saving...' : 'Save global settings'} 278 + </button> 279 + </div> 280 + </form> 281 + </section> 271 282 </div> 272 283 </div> 273 284 )}
+1 -1
src/app/settings/reports/page.test.tsx
··· 54 54 55 55 it('renders breadcrumbs with link to settings', async () => { 56 56 render(<MyReportsPage />) 57 - expect(screen.getByText('Account Settings')).toBeInTheDocument() 57 + expect(screen.getByText('Account settings')).toBeInTheDocument() 58 58 }) 59 59 60 60 it('loads and displays user reports from API', async () => {
+3 -3
src/app/settings/reports/page.tsx
··· 108 108 <Breadcrumbs 109 109 items={[ 110 110 { label: 'Home', href: '/' }, 111 - { label: 'Account Settings', href: '/settings' }, 112 - { label: 'My Reports' }, 111 + { label: 'Account settings', href: '/settings' }, 112 + { label: 'My reports' }, 113 113 ]} 114 114 /> 115 115 116 - <h1 className="text-2xl font-bold text-foreground">My Reports</h1> 116 + <h1 className="text-2xl font-bold text-foreground">My reports</h1> 117 117 118 118 {loading ? ( 119 119 <div className="max-w-2xl animate-pulse space-y-4">
+4 -4
src/app/setup/page.tsx
··· 67 67 return ( 68 68 <div className="flex min-h-screen items-center justify-center p-6"> 69 69 <div className="w-full max-w-md space-y-4 text-center"> 70 - <h1 className="text-2xl font-bold">Community Setup</h1> 70 + <h1 className="text-2xl font-bold">Community setup</h1> 71 71 <p className="text-muted-foreground"> 72 72 This community hasn&apos;t been set up yet. Log in to initialize it. 73 73 </p> ··· 86 86 <div className="flex min-h-screen items-center justify-center p-6"> 87 87 <form onSubmit={handleSubmit} className="w-full max-w-md space-y-6"> 88 88 <div className="space-y-2 text-center"> 89 - <h1 className="text-2xl font-bold">Set Up Your Community</h1> 89 + <h1 className="text-2xl font-bold">Set up your community</h1> 90 90 <p className="text-muted-foreground">You&apos;ll become the admin of this community.</p> 91 91 </div> 92 92 93 93 <div className="space-y-2"> 94 94 <label htmlFor="community-name" className="text-sm font-medium"> 95 - Community Name 95 + Community name 96 96 </label> 97 97 <input 98 98 id="community-name" ··· 119 119 disabled={submitting} 120 120 className="w-full rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50" 121 121 > 122 - {submitting ? 'Initializing...' : 'Initialize Community'} 122 + {submitting ? 'Initializing...' : 'Initialize community'} 123 123 </button> 124 124 </form> 125 125 </div>
+1 -1
src/app/t/[slug]/[rkey]/edit/page.test.tsx
··· 42 42 describe('EditTopicPage', () => { 43 43 it('renders edit topic heading', async () => { 44 44 render(<EditTopicPage params={{ slug: 'welcome-to-barazo-forums', rkey: '3kf1abc' }} />) 45 - expect(await screen.findByRole('heading', { name: 'Edit Topic' })).toBeInTheDocument() 45 + expect(await screen.findByRole('heading', { name: 'Edit topic' })).toBeInTheDocument() 46 46 }) 47 47 48 48 it('pre-populates form with topic data', async () => {
+1 -1
src/app/t/[slug]/[rkey]/edit/page.tsx
··· 132 132 ]} 133 133 /> 134 134 135 - <h1 className="text-2xl font-bold text-foreground">Edit Topic</h1> 135 + <h1 className="text-2xl font-bold text-foreground">Edit topic</h1> 136 136 137 137 {error && ( 138 138 <div
+1 -1
src/app/u/[handle]/page.tsx
··· 146 146 147 147 {/* Recent activity */} 148 148 <section> 149 - <h2 className="text-lg font-semibold text-foreground">Recent Activity</h2> 149 + <h2 className="text-lg font-semibold text-foreground">Recent activity</h2> 150 150 <p className="mt-2 text-sm text-muted-foreground"> 151 151 Recent posts and replies will appear here once the activity feed is implemented. 152 152 </p>
+2 -2
src/components/admin/plugins/dependency-warning-dialog.tsx
··· 29 29 <div className="w-full max-w-sm rounded-lg border border-border bg-card p-6 shadow-lg"> 30 30 <div className="mb-3 flex items-center gap-2 text-destructive"> 31 31 <WarningCircle size={20} aria-hidden="true" /> 32 - <h2 className="font-semibold">Dependency Warning</h2> 32 + <h2 className="font-semibold">Dependency warning</h2> 33 33 </div> 34 34 <p className="text-sm text-muted-foreground"> 35 35 Disabling <strong>{pluginName}</strong> will affect the following plugins that depend on ··· 48 48 onClick={onConfirm} 49 49 className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive/90" 50 50 > 51 - Disable Anyway 51 + Disable anyway 52 52 </button> 53 53 </div> 54 54 </div>
+2 -2
src/components/admin/settings/pds-trust-section.tsx
··· 49 49 <div className="space-y-4"> 50 50 <div className="flex items-center justify-between"> 51 51 <div> 52 - <h2 className="text-lg font-semibold text-foreground">PDS Provider Trust</h2> 52 + <h2 className="text-lg font-semibold text-foreground">PDS provider trust</h2> 53 53 <p className="mt-0.5 text-sm text-muted-foreground"> 54 54 Accounts from providers with higher trust factors earn reputation faster. Override the 55 55 default if you trust a specific self-hosted PDS provider. ··· 61 61 aria-label="Add override" 62 62 className="shrink-0 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90" 63 63 > 64 - Add Override 64 + Add override 65 65 </button> 66 66 </div> 67 67
+1 -1
src/components/admin/sybil/behavioral-flags-section.tsx
··· 15 15 export function BehavioralFlagsSection({ flags, onDismiss }: BehavioralFlagsSectionProps) { 16 16 return ( 17 17 <div className="space-y-3"> 18 - <h2 className="text-lg font-semibold text-foreground">Behavioral Flags</h2> 18 + <h2 className="text-lg font-semibold text-foreground">Behavioral flags</h2> 19 19 {flags.length === 0 && ( 20 20 <p className="py-4 text-center text-muted-foreground">No behavioral flags.</p> 21 21 )}
+1 -1
src/components/admin/sybil/cluster-detail-view.tsx
··· 81 81 <th className="pb-2 pr-4 font-medium text-muted-foreground">Role</th> 82 82 <th className="pb-2 pr-4 font-medium text-muted-foreground">Trust</th> 83 83 <th className="pb-2 pr-4 font-medium text-muted-foreground">Reputation</th> 84 - <th className="pb-2 pr-4 font-medium text-muted-foreground">Account Age</th> 84 + <th className="pb-2 pr-4 font-medium text-muted-foreground">Account age</th> 85 85 <th className="pb-2 font-medium text-muted-foreground">Communities</th> 86 86 </tr> 87 87 </thead>
+1 -1
src/components/admin/trust-seeds/add-seed-dialog.tsx
··· 52 52 aria-label="Add trust seed" 53 53 > 54 54 <div className="mx-4 w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg"> 55 - <h3 className="text-lg font-semibold text-foreground">Add Trust Seed</h3> 55 + <h3 className="text-lg font-semibold text-foreground">Add trust seed</h3> 56 56 <form onSubmit={handleSubmit} className="mt-4 space-y-4"> 57 57 <div> 58 58 <label htmlFor="seed-handle" className="block text-sm font-medium text-foreground">
+7 -7
src/components/community-profile-settings.tsx
··· 42 42 if (loading || !communityDid) { 43 43 return ( 44 44 <fieldset className="space-y-4 rounded-lg border border-border p-4"> 45 - <legend className="px-2 text-sm font-semibold text-foreground">Community Profile</legend> 45 + <legend className="px-2 text-sm font-semibold text-foreground">Community profile</legend> 46 46 {loading ? ( 47 47 <div className="animate-pulse space-y-3"> 48 48 <div className="h-24 w-24 rounded-full bg-muted" /> ··· 62 62 return ( 63 63 <> 64 64 <fieldset className="space-y-6 rounded-lg border border-border p-4"> 65 - <legend className="px-2 text-sm font-semibold text-foreground">Community Profile</legend> 65 + <legend className="px-2 text-sm font-semibold text-foreground">Community profile</legend> 66 66 67 67 <p className="text-sm text-muted-foreground"> 68 68 Customize how you appear in this community. Leave fields empty to use your AT Protocol ··· 132 132 )} 133 133 > 134 134 <ArrowCounterClockwise size={16} weight="bold" aria-hidden="true" /> 135 - Reset to AT Protocol Profile 135 + Reset to AT Protocol profile 136 136 </button> 137 137 138 138 <button ··· 145 145 'disabled:cursor-not-allowed disabled:opacity-50' 146 146 )} 147 147 > 148 - {saving ? 'Saving...' : 'Save Profile'} 148 + {saving ? 'Saving...' : 'Save profile'} 149 149 </button> 150 150 </div> 151 151 </fieldset> 152 152 153 153 <ConfirmDialog 154 154 open={showResetConfirm} 155 - title="Reset Community Profile" 155 + title="Reset community profile" 156 156 description="This will remove all community-specific overrides (display name, bio, avatar, banner) and revert to your AT Protocol profile. This action cannot be undone." 157 - confirmLabel="Reset Profile" 158 - cancelLabel="Keep Overrides" 157 + confirmLabel="Reset profile" 158 + cancelLabel="Keep overrides" 159 159 variant="destructive" 160 160 onConfirm={handleReset} 161 161 onCancel={() => setShowResetConfirm(false)}
+1 -1
src/components/settings/content-safety-section.tsx
··· 33 33 }: ContentSafetySectionProps) { 34 34 return ( 35 35 <fieldset className="space-y-4 rounded-lg border border-border p-4"> 36 - <legend className="px-2 text-sm font-semibold text-foreground">Content Safety</legend> 36 + <legend className="px-2 text-sm font-semibold text-foreground">Content safety</legend> 37 37 38 38 <div className="space-y-1"> 39 39 <label htmlFor="maturity-level" className="block text-sm font-medium text-foreground">
+1 -1
src/components/settings/cross-posting-section.tsx
··· 26 26 }: CrossPostingSectionProps) { 27 27 return ( 28 28 <fieldset className="space-y-4 rounded-lg border border-border p-4"> 29 - <legend className="px-2 text-sm font-semibold text-foreground">Cross-Posting</legend> 29 + <legend className="px-2 text-sm font-semibold text-foreground">Cross-posting</legend> 30 30 {authorized ? ( 31 31 <div className="space-y-3"> 32 32 <p className="text-xs text-muted-foreground">
+73
src/components/settings/settings-sidebar.tsx
··· 1 + /** 2 + * Sticky sidebar navigation for the settings page. 3 + * Anchor links to community-scoped and global settings sections. 4 + * Hidden below max-w-2xl breakpoint (768px). 5 + */ 6 + 7 + 'use client' 8 + 9 + import { useState, useEffect } from 'react' 10 + import { cn } from '@/lib/utils' 11 + 12 + interface SettingsSidebarProps { 13 + communityName: string 14 + } 15 + 16 + export function SettingsSidebar({ communityName }: SettingsSidebarProps) { 17 + const [activeSection, setActiveSection] = useState<string>('community-settings') 18 + 19 + useEffect(() => { 20 + if (typeof IntersectionObserver === 'undefined') return 21 + 22 + const sections = ['community-settings', 'global-settings'] 23 + const observer = new IntersectionObserver( 24 + (entries) => { 25 + for (const entry of entries) { 26 + if (entry.isIntersecting) { 27 + setActiveSection(entry.target.id) 28 + } 29 + } 30 + }, 31 + { rootMargin: '-80px 0px -60% 0px', threshold: 0 } 32 + ) 33 + 34 + for (const id of sections) { 35 + const el = document.getElementById(id) 36 + if (el) observer.observe(el) 37 + } 38 + 39 + return () => observer.disconnect() 40 + }, []) 41 + 42 + const links = [ 43 + { id: 'community-settings', label: `${communityName} settings` }, 44 + { id: 'global-settings', label: 'Global settings' }, 45 + ] 46 + 47 + return ( 48 + <nav 49 + aria-label="Settings sections" 50 + className="sticky top-20 hidden h-fit w-48 shrink-0 self-start min-[768px]:block" 51 + > 52 + <ul className="space-y-1"> 53 + {links.map((link) => ( 54 + <li key={link.id}> 55 + <a 56 + href={`#${link.id}`} 57 + className={cn( 58 + 'block rounded-md px-3 py-2 text-sm transition-colors', 59 + 'hover:bg-card-hover hover:text-foreground', 60 + 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', 61 + activeSection === link.id 62 + ? 'bg-card-hover font-medium text-foreground' 63 + : 'text-muted-foreground' 64 + )} 65 + > 66 + {link.label} 67 + </a> 68 + </li> 69 + ))} 70 + </ul> 71 + </nav> 72 + ) 73 + }