Skip to content

Commit 80a3fe6

Browse files
refactor(settings): align style according new rebranding (#1675)
* refactor(settings): extract shared UI components for settings tabs Add SettingsSectionTitle, SettingsRow, StatusBadge, and ExternalCliAlert as reusable components. Update WrapperField line-heights to match Figma design specs (leading-5.5 = 22px). Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(settings): redesign general tab to match Figma spacing Replace heading with SettingsSectionTitle, add Separators between fields, and align spacing values (gap-3, pt-1, pb-5) to the Figma design specs. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(settings): redesign version tab to match Figma spacing Replace VersionInfoWrapper with SettingsSectionTitle and SettingsRow, add Separators, and use WrapperField for the Downloads toggle to match the Figma design structure. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(settings): redesign logs tab to match Figma spacing Replace heading with SettingsSectionTitle, add description text and Separator, and use default button variant to match Figma design. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(settings): redesign registry tab to match Figma spacing Replace heading with SettingsSectionTitle, add description text and Separator in the form, and align spacing to the consistent gap-3 pattern. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(settings): redesign CLI tab with consistent spacing and extracted components Use SettingsSectionTitle, SettingsRow, StatusBadge, and ExternalCliAlert from shared components. Add Separators between rows and align spacing to the consistent gap-3 pattern. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(settings): update layout with full-bleed sidebar and Figma-aligned navigation Use negative margins to extend sidebar border through Main's padding, replace viewport-based ScrollArea height with h-full, add tab icons, and remove CLI tab from valid tabs. Update tests for new headings. Co-authored-by: Cursor <cursoragent@cursor.com> * refactor(settings): update layout with full-bleed sidebar and Figma-aligned navigation Use negative margins to extend sidebar border through Main's padding, replace viewport-based ScrollArea height with h-full, add tab icons, and remove CLI tab from valid tabs. Update tests for new headings. Co-authored-by: Cursor <cursoragent@cursor.com> * tab cli leftover check * test: remove unneeded check * fix(test): split flaky secrets test and silence Radix act() warnings Split the heavy "handles secrets correctly" test into two focused tests (inline secret / store secret) to avoid CI timeout. Silence Radix UI act() warnings in vitest setup since they originate from library internals and are not actionable. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(test): silence Radix UI act() warnings in vitest setup These console.error warnings originate from Radix UI internals (react-select, react-presence, react-focus-scope) and cause false positive failures via vitest-fail-on-console in CI. Co-authored-by: Cursor <cursoragent@cursor.com> * fix: add sr-only label for update icon and missing CLI tab test assertion Add screen-reader text to the ArrowUpCircle update indicator in the settings sidebar. Add the missing CLI tab assertion in the "renders all tab triggers" test. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 39318c0 commit 80a3fe6

23 files changed

Lines changed: 514 additions & 432 deletions

renderer/src/common/components/settings/registry/registry-form.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Form } from '../../ui/form'
33
import { RegistrySourceField } from './registry-source-field'
44
import { Button } from '../../ui/button'
55
import { RegistryTypeField } from './registry-type-field'
6+
import { Separator } from '../../ui/separator'
67
import type { RegistryFormData } from './schema'
78

89
interface RegistryFormProps {
@@ -21,15 +22,18 @@ export function RegistryForm({
2122
return (
2223
<Form {...form}>
2324
<form
24-
className="flex flex-col items-start gap-4"
25+
className="flex flex-col items-start gap-3"
2526
onSubmit={form.handleSubmit(onSubmit)}
2627
>
27-
<RegistryTypeField isPending={isLoading} form={form} />
28-
<RegistrySourceField
29-
isPending={isLoading}
30-
form={form}
31-
hasRegistryError={hasRegistryError}
32-
/>
28+
<div className="flex w-full flex-col gap-3 py-1">
29+
<RegistryTypeField isPending={isLoading} form={form} />
30+
<RegistrySourceField
31+
isPending={isLoading}
32+
form={form}
33+
hasRegistryError={hasRegistryError}
34+
/>
35+
<Separator className="my-1 w-full" />
36+
</div>
3337
<Button variant="action" type="submit" disabled={isLoading}>
3438
{isLoading ? 'Saving...' : 'Save'}
3539
</Button>

renderer/src/common/components/settings/registry/registry-tab.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { mapResponseTypeToFormType, mapFormTypeToResponseType } from './utils'
1313
import { RegistryForm } from './registry-form'
1414
import { delay } from '@utils/delay'
1515
import { trackEvent } from '@/common/lib/analytics'
16+
import { SettingsSectionTitle } from '../tabs/components/settings-section-title'
1617

1718
export function RegistryTab() {
1819
const queryClient = useQueryClient()
@@ -111,18 +112,21 @@ export function RegistryTab() {
111112
}
112113

113114
return (
114-
<div className="space-y-6">
115-
<div className="space-y-4">
116-
<h2 className="text-lg font-semibold">Registry</h2>
117-
</div>
118-
<div className="space-y-4">
119-
<RegistryForm
120-
form={form}
121-
onSubmit={onSubmit}
122-
isLoading={isLoading}
123-
hasRegistryError={!!registryError}
124-
/>
115+
<div className="space-y-3">
116+
<div className="flex flex-col gap-2">
117+
<SettingsSectionTitle>Registry</SettingsSectionTitle>
118+
<p className="text-muted-foreground text-sm leading-5.5">
119+
Choose between ToolHive default registry, a custom remote registry
120+
JSON URL, a custom local registry JSON file, or a custom registry
121+
server API URL.
122+
</p>
125123
</div>
124+
<RegistryForm
125+
form={form}
126+
onSubmit={onSubmit}
127+
isLoading={isLoading}
128+
hasRegistryError={!!registryError}
129+
/>
126130
</div>
127131
)
128132
}

renderer/src/common/components/settings/registry/registry-type-field.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,7 @@ import {
66
SelectItem,
77
} from '../../ui/select'
88
import type { UseFormReturn } from 'react-hook-form'
9-
import {
10-
FormField,
11-
FormItem,
12-
FormLabel,
13-
FormDescription,
14-
FormControl,
15-
FormMessage,
16-
} from '../../ui/form'
9+
import { FormField, FormItem, FormControl, FormMessage } from '../../ui/form'
1710
import type { RegistryFormData } from './schema'
1811
import { REGISTRY_TYPE_OPTIONS } from './utils'
1912

@@ -35,12 +28,6 @@ export function RegistryTypeField({
3528
name="type"
3629
render={({ field }) => (
3730
<FormItem className="w-full">
38-
<FormLabel>Registry Type</FormLabel>
39-
<FormDescription>
40-
Choose between ToolHive default registry, a custom remote registry
41-
JSON URL, a custom local registry JSON file, or a custom registry
42-
server API URL.
43-
</FormDescription>
4431
<Select
4532
onValueChange={(value) => {
4633
field.onChange(value)

renderer/src/common/components/settings/tabs/__tests__/general-tab.test.tsx

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,12 @@ describe('GeneralTab', () => {
103103
renderWithProviders(<GeneralTab />)
104104

105105
await waitFor(() => {
106-
expect(screen.getByText('General Settings')).toBeVisible()
106+
expect(screen.getByRole('heading', { name: 'General' })).toBeVisible()
107107
})
108108
expect(screen.getByText('Theme')).toBeVisible()
109109
expect(screen.getByText('Start on login')).toBeVisible()
110110
expect(screen.getByText('Error reporting')).toBeVisible()
111-
expect(screen.getByText('Skip quit confirmation')).toBeVisible()
111+
expect(screen.getByText('Quit confirmation')).toBeVisible()
112112
})
113113

114114
it('handles auto-launch toggle', async () => {
@@ -121,7 +121,7 @@ describe('GeneralTab', () => {
121121

122122
renderWithProviders(<GeneralTab />)
123123
await waitFor(() => {
124-
expect(screen.getByText('General Settings')).toBeVisible()
124+
expect(screen.getByRole('heading', { name: 'General' })).toBeVisible()
125125
})
126126

127127
const autoLaunchSwitch = screen.getByRole('switch', {
@@ -177,12 +177,12 @@ describe('GeneralTab', () => {
177177

178178
await waitFor(() => {
179179
expect(
180-
screen.getByRole('switch', { name: /skip quit confirmation/i })
180+
screen.getByRole('switch', { name: /quit confirmation/i })
181181
).not.toBeChecked()
182182
})
183183

184184
const quitConfirmationSwitch = screen.getByRole('switch', {
185-
name: /skip quit confirmation/i,
185+
name: /quit confirmation/i,
186186
})
187187

188188
await userEvent.click(quitConfirmationSwitch)
@@ -199,13 +199,13 @@ describe('GeneralTab', () => {
199199

200200
await waitFor(() => {
201201
const quitConfirmationSwitch = screen.getByRole('switch', {
202-
name: /skip quit confirmation/i,
202+
name: /quit confirmation/i,
203203
})
204204
expect(quitConfirmationSwitch).toBeChecked()
205205
})
206206

207207
const quitConfirmationSwitch = screen.getByRole('switch', {
208-
name: /skip quit confirmation/i,
208+
name: /quit confirmation/i,
209209
})
210210
await userEvent.click(quitConfirmationSwitch)
211211

@@ -222,7 +222,9 @@ describe('GeneralTab', () => {
222222
renderWithProviders(<GeneralTab />)
223223

224224
await waitFor(() => {
225-
expect(screen.getByText('Experimental Features')).toBeVisible()
225+
expect(
226+
screen.getByRole('heading', { name: 'Experimental' })
227+
).toBeVisible()
226228
expect(
227229
screen.getByText('No experimental features available')
228230
).toBeVisible()
@@ -242,7 +244,9 @@ describe('GeneralTab', () => {
242244
renderWithProviders(<GeneralTab />)
243245

244246
await waitFor(() => {
245-
expect(screen.getByText('Experimental Features')).toBeVisible()
247+
expect(
248+
screen.getByRole('heading', { name: 'Experimental' })
249+
).toBeVisible()
246250
expect(screen.getByText('Test Feature')).toBeVisible()
247251
})
248252

@@ -268,7 +272,9 @@ describe('GeneralTab', () => {
268272
renderWithProviders(<GeneralTab />)
269273

270274
await waitFor(() => {
271-
expect(screen.getByText('Experimental Features')).toBeVisible()
275+
expect(
276+
screen.getByRole('heading', { name: 'Experimental' })
277+
).toBeVisible()
272278
})
273279

274280
expect(screen.queryByText('Regular Feature')).not.toBeInTheDocument()

renderer/src/common/components/settings/tabs/__tests__/logs-tab.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ describe('LogsTab', () => {
3737
render(<LogsTab />)
3838

3939
await waitFor(() => {
40-
expect(screen.getByText('Application Logs')).toBeVisible()
40+
expect(screen.getByText('Logs')).toBeVisible()
4141
})
4242

4343
expect(

renderer/src/common/components/settings/tabs/__tests__/registry-tab.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ describe('RegistryTab', () => {
4343
await waitFor(() => {
4444
expect(screen.getByRole('button', { name: 'Save' })).toBeVisible()
4545
})
46-
expect(screen.getByText('Registry Type')).toBeVisible()
4746
})
4847

4948
it('handles remote registry configuration with valid URL', async () => {

renderer/src/common/components/settings/tabs/__tests__/settings-tabs.test.tsx

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -139,28 +139,22 @@ describe('SettingsTabs', () => {
139139
renderWithProviders(<SettingsTabs />)
140140

141141
await waitFor(() => {
142-
expect(
143-
screen.getByRole('heading', { name: 'General Settings' })
144-
).toBeVisible()
142+
expect(screen.getByRole('heading', { name: 'General' })).toBeVisible()
145143
})
146144
})
147145
it('shows Version tab when clicked', async () => {
148146
renderWithProviders(<SettingsTabs />)
149147
await userEvent.click(screen.getByRole('tab', { name: 'Version' }))
150148
await waitFor(() => {
151-
expect(
152-
screen.getByRole('heading', { name: 'Version Information' })
153-
).toBeVisible()
149+
expect(screen.getByRole('heading', { name: 'Version' })).toBeVisible()
154150
})
155151
})
156152
it('shows Logs tab when clicked', async () => {
157153
renderWithProviders(<SettingsTabs />)
158154

159155
await userEvent.click(screen.getByRole('tab', { name: 'Logs' }))
160156
await waitFor(() => {
161-
expect(
162-
screen.getByRole('heading', { name: 'Application Logs' })
163-
).toBeVisible()
157+
expect(screen.getByRole('heading', { name: 'Logs' })).toBeVisible()
164158
})
165159
})
166160

@@ -183,10 +177,8 @@ describe('SettingsTabs', () => {
183177
const versionTab = screen.getByRole('tab', { name: /Version/i })
184178
expect(versionTab).toBeVisible()
185179

186-
// Check that the ArrowUpCircle icon is present
187-
const icon = versionTab.querySelector('svg')
188-
expect(icon).toBeInTheDocument()
189-
expect(icon).toHaveClass('text-blue-500')
180+
const updateIcon = versionTab.querySelector('svg.text-blue-500')
181+
expect(updateIcon).toBeInTheDocument()
190182
})
191183

192184
it('does not show update icon on Version tab in development mode', async () => {
@@ -208,9 +200,8 @@ describe('SettingsTabs', () => {
208200
const versionTab = screen.getByRole('tab', { name: /Version/i })
209201
expect(versionTab).toBeVisible()
210202

211-
// Check that the icon is not present
212-
const icon = versionTab.querySelector('svg')
213-
expect(icon).not.toBeInTheDocument()
203+
const updateIcon = versionTab.querySelector('svg.text-blue-500')
204+
expect(updateIcon).not.toBeInTheDocument()
214205
})
215206

216207
it('does not show update icon on Version tab when no update is available', async () => {
@@ -232,9 +223,8 @@ describe('SettingsTabs', () => {
232223
const versionTab = screen.getByRole('tab', { name: /Version/i })
233224
expect(versionTab).toBeVisible()
234225

235-
// Check that the icon is not present
236-
const icon = versionTab.querySelector('svg')
237-
expect(icon).not.toBeInTheDocument()
226+
const updateIcon = versionTab.querySelector('svg.text-blue-500')
227+
expect(updateIcon).not.toBeInTheDocument()
238228
})
239229

240230
it('does not show update icon on other tabs', async () => {
@@ -256,8 +246,9 @@ describe('SettingsTabs', () => {
256246
const generalTab = screen.getByRole('tab', { name: 'General' })
257247
const logsTab = screen.getByRole('tab', { name: 'Logs' })
258248

259-
// Check that these tabs don't have the icon
260-
expect(generalTab.querySelector('svg')).not.toBeInTheDocument()
261-
expect(logsTab.querySelector('svg')).not.toBeInTheDocument()
249+
expect(
250+
generalTab.querySelector('svg.text-blue-500')
251+
).not.toBeInTheDocument()
252+
expect(logsTab.querySelector('svg.text-blue-500')).not.toBeInTheDocument()
262253
})
263254
})

renderer/src/common/components/settings/tabs/__tests__/version-tab.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ describe('VersionTab', () => {
6262
import.meta.env.MODE = originalEnv
6363
})
6464

65-
it('renders version information heading', () => {
65+
it('renders version heading', () => {
6666
renderWithProviders(
6767
<VersionTab appInfo={mockAppInfo} isLoading={false} error={null} />
6868
)
6969

70-
expect(screen.getByText('Version Information')).toBeVisible()
70+
expect(screen.getByText('Version')).toBeVisible()
7171
})
7272

7373
it('displays version information when loaded', () => {

0 commit comments

Comments
 (0)