1
- import type { Interpolation } from "@emotion/react" ;
2
- import CircularProgress from "@mui/material/CircularProgress" ;
3
- import FormControl from "@mui/material/FormControl" ;
4
- import FormControlLabel from "@mui/material/FormControlLabel" ;
5
- import Radio from "@mui/material/Radio" ;
6
- import RadioGroup from "@mui/material/RadioGroup" ;
7
- import { visuallyHidden } from "@mui/utils" ;
8
1
import {
9
2
type TerminalFontName ,
10
3
TerminalFontNames ,
11
4
type UpdateUserAppearanceSettingsRequest ,
12
5
} from "api/typesGenerated" ;
13
6
import { ErrorAlert } from "components/Alert/ErrorAlert" ;
14
7
import { PreviewBadge } from "components/Badges/Badges" ;
15
- import { Stack } from "components/Stack/Stack" ;
16
- import { ThemeOverride } from "contexts/ThemeProvider" ;
8
+ import { Label } from "components/Label/Label" ;
9
+ import { RadioGroup , RadioGroupItem } from "components/RadioGroup/RadioGroup" ;
10
+ import { Spinner } from "components/Spinner/Spinner" ;
17
11
import type { FC } from "react" ;
18
- import themes , { DEFAULT_THEME , type Theme } from "theme" ;
12
+ import { DEFAULT_THEME } from "theme" ;
19
13
import {
20
14
DEFAULT_TERMINAL_FONT ,
21
15
terminalFontLabels ,
@@ -67,67 +61,65 @@ export const AppearanceForm: FC<AppearanceFormProps> = ({
67
61
68
62
< Section
69
63
title = {
70
- < Stack direction = " row" alignItems = " center">
64
+ < div className = "flex flex- row items- center gap-2 ">
71
65
< span > Theme</ span >
72
- { isUpdating && < CircularProgress size = { 16 } /> }
73
- </ Stack >
66
+ < Spinner loading = { isUpdating } size = "sm" />
67
+ </ div >
74
68
}
75
69
layout = "fluid"
76
70
>
77
- < Stack direction = " row" wrap = "wrap ">
71
+ < div className = "flex flex- row flex- wrap gap-4 ">
78
72
< AutoThemePreviewButton
79
73
displayName = "Auto"
80
74
active = { currentTheme === "auto" }
81
- themes = { [ themes . dark , themes . light ] }
75
+ themes = { [ " dark" , " light" ] }
82
76
onSelect = { ( ) => onChangeTheme ( "auto" ) }
83
77
/>
84
78
< ThemePreviewButton
85
79
displayName = "Dark"
86
80
active = { currentTheme === "dark" }
87
- theme = { themes . dark }
81
+ theme = " dark"
88
82
onSelect = { ( ) => onChangeTheme ( "dark" ) }
89
83
/>
90
84
< ThemePreviewButton
91
85
displayName = "Light"
92
86
active = { currentTheme === "light" }
93
- theme = { themes . light }
87
+ theme = " light"
94
88
onSelect = { ( ) => onChangeTheme ( "light" ) }
95
89
/>
96
- </ Stack >
90
+ </ div >
97
91
</ Section >
98
- < div css = { { marginBottom : 48 } } > </ div >
92
+ < div className = "mb-12" / >
99
93
< Section
100
94
title = {
101
- < Stack direction = " row" alignItems = " center">
95
+ < div className = "flex flex- row items- center gap-2 ">
102
96
< span > Terminal Font</ span >
103
- { isUpdating && < CircularProgress size = { 16 } /> }
104
- </ Stack >
97
+ < Spinner loading = { isUpdating } size = "sm" />
98
+ </ div >
105
99
}
106
100
layout = "fluid"
107
101
>
108
- < FormControl >
109
- < RadioGroup
110
- aria-labelledby = "fonts-radio-buttons-group-label"
111
- defaultValue = { currentTerminalFont }
112
- name = "fonts-radio-buttons-group"
113
- onChange = { ( _ , value ) =>
114
- onChangeTerminalFont ( toTerminalFontName ( value ) )
115
- }
116
- >
117
- { TerminalFontNames . filter ( ( name ) => name !== "" ) . map ( ( name ) => (
118
- < FormControlLabel
119
- key = { name }
120
- value = { name }
121
- control = { < Radio /> }
122
- label = {
123
- < div css = { { fontFamily :terminalFonts [ name ] } } >
124
- { terminalFontLabels [ name ] }
125
- </ div >
126
- }
127
- />
128
- ) ) }
129
- </ RadioGroup >
130
- </ FormControl >
102
+ < RadioGroup
103
+ aria-labelledby = "fonts-radio-buttons-group-label"
104
+ defaultValue = { currentTerminalFont }
105
+ name = "fonts-radio-buttons-group"
106
+ onValueChange = { ( value ) =>
107
+ onChangeTerminalFont ( toTerminalFontName ( value ) )
108
+ }
109
+ >
110
+ { TerminalFontNames . filter ( ( name ) => name !== "" ) . map ( ( name ) => (
111
+ < div key = { name } className = "flex items-center space-x-2" >
112
+ < RadioGroupItem value = { name } id = { name } />
113
+ < Label
114
+ htmlFor = { name }
115
+ className = "cursor-pointer font-normal"
116
+ style = { { fontFamily :terminalFonts [ name ] } }
117
+ >
118
+ { terminalFontLabels [ name ] }
119
+ </ Label >
120
+ </ div >
121
+ ) ) }
122
+ </ RadioGroup >
131
123
</ Section >
132
124
</ form >
133
125
) ;
@@ -139,8 +131,10 @@ function toTerminalFontName(value: string): TerminalFontName {
139
131
:"" ;
140
132
}
141
133
134
+ type ThemeMode = "dark" | "light" ;
135
+
142
136
interface AutoThemePreviewButtonProps extends Omit < ThemePreviewProps , "theme" > {
143
- themes :[ Theme , Theme ] ;
137
+ themes :[ ThemeMode , ThemeMode ] ;
144
138
onSelect ?:( ) => void ;
145
139
}
146
140
@@ -163,13 +157,15 @@ const AutoThemePreviewButton: FC<AutoThemePreviewButtonProps> = ({
163
157
value = { displayName }
164
158
checked = { active }
165
159
onChange = { onSelect }
166
- css = { { ... visuallyHidden } }
160
+ className = "sr-only"
167
161
/>
168
- < label htmlFor = { displayName } className = { cn ( "relative" , className ) } >
162
+ < label
163
+ htmlFor = { displayName }
164
+ className = { cn ( "relative cursor-pointer" , className ) }
165
+ >
169
166
< ThemePreview
170
- css = { {
171
- // This half is absolute to not advance the layout (which would offset the second half)
172
- position :"absolute" ,
167
+ className = "absolute"
168
+ style = { {
173
169
// Slightly past the bounding box to avoid cutting off the outline
174
170
clipPath :"polygon(-5% -5%, 50% -5%, 50% 105%, -5% 105%)" ,
175
171
} }
@@ -210,9 +206,9 @@ const ThemePreviewButton: FC<ThemePreviewButtonProps> = ({
210
206
value = { displayName }
211
207
checked = { active }
212
208
onChange = { onSelect }
213
- css = { { ... visuallyHidden } }
209
+ className = "sr-only"
214
210
/>
215
- < label htmlFor = { displayName } className = { className } >
211
+ < label htmlFor = { displayName } className = { cn ( "cursor-pointer" , className ) } >
216
212
< ThemePreview
217
213
active = { active }
218
214
preview = { preview }
@@ -228,152 +224,65 @@ interface ThemePreviewProps {
228
224
active ?:boolean ;
229
225
preview ?:boolean ;
230
226
className ?:string ;
227
+ style ?:React . CSSProperties ;
231
228
displayName :string ;
232
- theme :Theme ;
229
+ theme :ThemeMode ;
233
230
}
234
231
235
232
const ThemePreview :FC < ThemePreviewProps > = ( {
236
233
active,
237
234
preview,
238
235
className,
236
+ style,
239
237
displayName,
240
238
theme,
241
239
} ) => {
242
240
return (
243
- < ThemeOverride theme = { theme } >
241
+ < div className = { theme } >
244
242
< div
245
- css = { [ styles . container , active && styles . containerActive ] }
246
- className = { className }
243
+ className = { cn (
244
+ "w-56 overflow-clip rounded-md border border-border border-solid bg-surface-primary text-content-primary select-none" ,
245
+ active && "outline outline-2 outline-content-link" ,
246
+ className ,
247
+ ) }
248
+ style = { style }
247
249
>
248
- < div css = { styles . page } >
249
- < div css = { styles . header } >
250
- < div css = { styles . headerLinks } >
251
- < div css = { [ styles . headerLink , styles . activeHeaderLink ] } />
252
- < div css = { styles . headerLink } />
253
- < div css = { styles . headerLink } />
250
+ < div className = "bg-surface-primary text-content-primary" >
251
+ < div className = "bg-surface-secondary flex items-center justify-between px-2.5 py-1.5 mb-2 border-0 border-b border-border border-solid" >
252
+ < div className = "flex items-center gap-1.5" >
253
+ < div className = "bg-content-primary h-1.5 w-5 rounded" />
254
+ < div className = "bg-content-secondary h-1.5 w-5 rounded" />
255
+ < div className = "bg-content-secondary h-1.5 w-5 rounded" />
254
256
</ div >
255
- < div css = { styles . headerLinks } >
256
- < div css = { styles . proxy } />
257
- < div css = { styles . user } />
257
+ < div className = "flex items-center gap-1.5" >
258
+ < div className = "bg-green-400 h-1.5 w-3 rounded" />
259
+ < div className = "bg-content-primary h-2 w-2 rounded-full" />
258
260
</ div >
259
261
</ div >
260
- < div css = { styles . body } >
261
- < div css = { styles . title } />
262
- < div css = { styles . table } >
263
- < div css = { styles . tableHeader } />
264
- < div css = { styles . workspace } />
265
- < div css = { styles . workspace } />
266
- < div css = { styles . workspace } />
267
- < div css = { styles . workspace } />
262
+ < div className = "w-32 mx-auto" >
263
+ < div className = "bg-content-primary h-2 w-11 rounded mb-1.5" />
264
+ < div className = "border border-solid rounded-t overflow-clip" >
265
+ < div className = "bg-surface-secondary h-2.5 -m-px" />
266
+ < div className = "h-4 border-0 border-t border-border border-solid" >
267
+ < div className = "bg-content-disabled h-1.5 w-8 rounded mt-1 ml-1" />
268
+ </ div >
269
+ < div className = "h-4 border-0 border-t border-border border-solid" >
270
+ < div className = "bg-content-disabled h-1.5 w-8 rounded mt-1 ml-1" />
271
+ </ div >
272
+ < div className = "h-4 border-0 border-t border-border border-solid" >
273
+ < div className = "bg-content-disabled h-1.5 w-8 rounded mt-1 ml-1" />
274
+ </ div >
275
+ < div className = "h-4 border-0 border-t border-border border-solid" >
276
+ < div className = "bg-content-disabled h-1.5 w-8 rounded mt-1 ml-1" />
277
+ </ div >
268
278
</ div >
269
279
</ div >
270
280
</ div >
271
- < div css = { styles . label } >
281
+ < div className = "flex items-center justify-between border-0 border-t border-border border-solid px-3 py-1 text-sm" >
272
282
< span > { displayName } </ span >
273
283
{ preview && < PreviewBadge /> }
274
284
</ div >
275
285
</ div >
276
- </ ThemeOverride >
286
+ </ div >
277
287
) ;
278
288
} ;
279
-
280
- const styles = {
281
- container :( theme ) => ( {
282
- backgroundColor :theme . palette . background . default ,
283
- border :`1px solid${ theme . palette . divider } ` ,
284
- width :220 ,
285
- color :theme . palette . text . primary ,
286
- borderRadius :6 ,
287
- overflow :"clip" ,
288
- userSelect :"none" ,
289
- } ) ,
290
- containerActive :( theme ) => ( {
291
- outline :`2px solid${ theme . roles . active . outline } ` ,
292
- } ) ,
293
- page :( theme ) => ( {
294
- backgroundColor :theme . palette . background . default ,
295
- color :theme . palette . text . primary ,
296
- } ) ,
297
- header :( theme ) => ( {
298
- backgroundColor :theme . palette . background . paper ,
299
- display :"flex" ,
300
- alignItems :"center" ,
301
- justifyContent :"space-between" ,
302
- padding :"6px 10px" ,
303
- marginBottom :8 ,
304
- borderBottom :`1px solid${ theme . palette . divider } ` ,
305
- } ) ,
306
- headerLinks :{
307
- display :"flex" ,
308
- alignItems :"center" ,
309
- gap :6 ,
310
- } ,
311
- headerLink :( theme ) => ( {
312
- backgroundColor :theme . palette . text . secondary ,
313
- height :6 ,
314
- width :20 ,
315
- borderRadius :3 ,
316
- } ) ,
317
- activeHeaderLink :( theme ) => ( {
318
- backgroundColor :theme . palette . text . primary ,
319
- } ) ,
320
- proxy :( theme ) => ( {
321
- backgroundColor :theme . palette . success . light ,
322
- height :6 ,
323
- width :12 ,
324
- borderRadius :3 ,
325
- } ) ,
326
- user :( theme ) => ( {
327
- backgroundColor :theme . palette . text . primary ,
328
- height :8 ,
329
- width :8 ,
330
- borderRadius :4 ,
331
- float :"right" ,
332
- } ) ,
333
- body :{
334
- width :120 ,
335
- margin :"auto" ,
336
- } ,
337
- title :( theme ) => ( {
338
- backgroundColor :theme . palette . text . primary ,
339
- height :8 ,
340
- width :45 ,
341
- borderRadius :4 ,
342
- marginBottom :6 ,
343
- } ) ,
344
- table :( theme ) => ( {
345
- border :`1px solid${ theme . palette . divider } ` ,
346
- borderBottom :"none" ,
347
- borderTopLeftRadius :3 ,
348
- borderTopRightRadius :3 ,
349
- overflow :"clip" ,
350
- } ) ,
351
- tableHeader :( theme ) => ( {
352
- backgroundColor :theme . palette . background . paper ,
353
- height :10 ,
354
- margin :- 1 ,
355
- } ) ,
356
- label :( theme ) => ( {
357
- display :"flex" ,
358
- alignItems :"center" ,
359
- justifyContent :"space-between" ,
360
- borderTop :`1px solid${ theme . palette . divider } ` ,
361
- padding :"4px 12px" ,
362
- fontSize :14 ,
363
- } ) ,
364
- workspace :( theme ) => ( {
365
- borderTop :`1px solid${ theme . palette . divider } ` ,
366
- height :15 ,
367
-
368
- "&::after" :{
369
- content :'""' ,
370
- display :"block" ,
371
- marginTop :4 ,
372
- marginLeft :4 ,
373
- backgroundColor :theme . palette . text . disabled ,
374
- height :6 ,
375
- width :30 ,
376
- borderRadius :3 ,
377
- } ,
378
- } ) ,
379
- } satisfies Record < string , Interpolation < Theme > > ;