Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

chore: migrate appearanceform to Tailwind and shadcn#20204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
jaaydenh merged 3 commits intomainfromjaaydenh/migrate-appearance-form
Oct 8, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletionsite/src/index.css
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,7 +8,8 @@
@tailwind utilities;

@layer base {
:root {
:root,
.light {
--content-primary: 240 10% 4%;
--content-secondary: 240 5% 34%;
--content-link: 221 83% 53%;
Expand Down
265 changes: 87 additions & 178 deletionssite/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,15 @@
import type { Interpolation } from "@emotion/react";
import CircularProgress from "@mui/material/CircularProgress";
import FormControl from "@mui/material/FormControl";
import FormControlLabel from "@mui/material/FormControlLabel";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import { visuallyHidden } from "@mui/utils";
Comment on lines -1 to -7
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

that's a satisfying chunk of imports to watch vanish

jaaydenh reacted with hooray emoji
import {
type TerminalFontName,
TerminalFontNames,
type UpdateUserAppearanceSettingsRequest,
} from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { PreviewBadge } from "components/Badges/Badges";
import { Stack } from "components/Stack/Stack";
import { ThemeOverride } from "contexts/ThemeProvider";
import { Label } from "components/Label/Label";
import { RadioGroup, RadioGroupItem } from "components/RadioGroup/RadioGroup";
import { Spinner } from "components/Spinner/Spinner";
import type { FC } from "react";
importthemes,{ DEFAULT_THEME, type Theme } from "theme";
import { DEFAULT_THEME } from "theme";
import {
DEFAULT_TERMINAL_FONT,
terminalFontLabels,
Expand DownExpand Up@@ -67,67 +61,65 @@ export const AppearanceForm: FC<AppearanceFormProps> = ({

<Section
title={
<Stack direction="row" alignItems="center">
<div className="flex flex-row items-center gap-2">
<span>Theme</span>
{isUpdating && <CircularProgress size={16} />}
</Stack>
<Spinner loading={isUpdating} size="sm" />
</div>
}
layout="fluid"
className="mb-12"
>
<Stack direction="row"wrap="wrap">
<div className="flex flex-row flex-wrap gap-4">
<AutoThemePreviewButton
displayName="Auto"
active={currentTheme === "auto"}
themes={[themes.dark, themes.light]}
themes={["dark", "light"]}
onSelect={() => onChangeTheme("auto")}
/>
<ThemePreviewButton
displayName="Dark"
active={currentTheme === "dark"}
theme={themes.dark}
theme="dark"
onSelect={() => onChangeTheme("dark")}
/>
<ThemePreviewButton
displayName="Light"
active={currentTheme === "light"}
theme={themes.light}
theme="light"
onSelect={() => onChangeTheme("light")}
/>
</Stack>
</div>
</Section>
<div css={{ marginBottom: 48 }}></div>
<Section
title={
<Stack direction="row" alignItems="center">
<span>Terminal Font</span>
{isUpdating && <CircularProgress size={16} />}
</Stack>
<div className="flex flex-row items-center gap-2">
<span id="fonts-radio-buttons-group-label">Terminal Font</span>
<Spinner loading={isUpdating} size="sm" />
</div>
}
layout="fluid"
>
<FormControl>
<RadioGroup
aria-labelledby="fonts-radio-buttons-group-label"
defaultValue={currentTerminalFont}
name="fonts-radio-buttons-group"
onChange={(_, value) =>
onChangeTerminalFont(toTerminalFontName(value))
}
>
{TerminalFontNames.filter((name) => name !== "").map((name) => (
<FormControlLabel
key={name}
value={name}
control={<Radio />}
label={
<div css={{ fontFamily: terminalFonts[name] }}>
{terminalFontLabels[name]}
</div>
}
/>
))}
</RadioGroup>
</FormControl>
<RadioGroup
aria-labelledby="fonts-radio-buttons-group-label"
Copy link
Contributor

@buenos-nachosbuenos-nachosOct 8, 2025
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This looks like invalid HTML to me. If we havearia-labelledby, we need another element in the HTML that has (1) anid attribute with a matching value and (2) some kind of labeling text

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

added the id to the section title "Terminal Font" span

defaultValue={currentTerminalFont}
name="fonts-radio-buttons-group"
onValueChange={(value) =>
onChangeTerminalFont(toTerminalFontName(value))
}
>
{TerminalFontNames.filter((name) => name !== "").map((name) => (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Do we need thisfilter call, or can we trust the backend-generated types to not include a zero value?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

It looks like TerminalFontName in typesGenerated comes from codersdk/users.go with an empty string option. So it seems like we need to filter out the empty string if we don't want to display that.

<div key={name} className="flex items-center space-x-2">
<RadioGroupItem value={name} id={name} />
<Label
htmlFor={name}
className="cursor-pointer font-normal"
style={{ fontFamily: terminalFonts[name] }}
>
{terminalFontLabels[name]}
</Label>
</div>
))}
</RadioGroup>
</Section>
</form>
);
Expand All@@ -139,8 +131,10 @@ function toTerminalFontName(value: string): TerminalFontName {
: "";
}

type ThemeMode = "dark" | "light";

interface AutoThemePreviewButtonProps extends Omit<ThemePreviewProps, "theme"> {
themes: [Theme, Theme];
themes: [ThemeMode, ThemeMode];
onSelect?: () => void;
}

Expand All@@ -163,13 +157,15 @@ const AutoThemePreviewButton: FC<AutoThemePreviewButtonProps> = ({
value={displayName}
checked={active}
onChange={onSelect}
css={{ ...visuallyHidden }}
className="sr-only"
/>
<label htmlFor={displayName} className={cn("relative", className)}>
<label
htmlFor={displayName}
className={cn("relative cursor-pointer", className)}
>
<ThemePreview
css={{
// This half is absolute to not advance the layout (which would offset the second half)
position: "absolute",
className="absolute"
style={{
// Slightly past the bounding box to avoid cutting off the outline
clipPath: "polygon(-5% -5%, 50% -5%, 50% 105%, -5% 105%)",
}}
Expand DownExpand Up@@ -210,9 +206,9 @@ const ThemePreviewButton: FC<ThemePreviewButtonProps> = ({
value={displayName}
checked={active}
onChange={onSelect}
css={{ ...visuallyHidden }}
className="sr-only"
/>
<label htmlFor={displayName} className={className}>
<label htmlFor={displayName} className={cn("cursor-pointer",className)}>
<ThemePreview
active={active}
preview={preview}
Expand All@@ -228,152 +224,65 @@ interface ThemePreviewProps {
active?: boolean;
preview?: boolean;
className?: string;
style?: React.CSSProperties;
displayName: string;
theme:Theme;
theme:ThemeMode;
}

const ThemePreview: FC<ThemePreviewProps> = ({
active,
preview,
className,
style,
displayName,
theme,
}) => {
return (
<ThemeOverride theme={theme}>
<div className={theme}>
<div
css={[styles.container, active && styles.containerActive]}
className={className}
className={cn(
"w-56 overflow-clip rounded-md border border-border border-solid bg-surface-primary text-content-primary select-none",
active && "outline outline-2 outline-content-link",
className,
)}
style={style}
>
<divcss={styles.page}>
<divcss={styles.header}>
<divcss={styles.headerLinks}>
<divcss={[styles.headerLink, styles.activeHeaderLink]} />
<divcss={styles.headerLink} />
<divcss={styles.headerLink} />
<divclassName="bg-surface-primary text-content-primary">
<divclassName="bg-surface-secondary flex items-center justify-between px-2.5 py-1.5 mb-2 border-0 border-b border-border border-solid">
<divclassName="flex items-center gap-1.5">
<divclassName="bg-content-primary h-1.5 w-5 rounded" />
<divclassName="bg-content-secondary h-1.5 w-5 rounded" />
<divclassName="bg-content-secondary h-1.5 w-5 rounded" />
</div>
<divcss={styles.headerLinks}>
<divcss={styles.proxy} />
<divcss={styles.user} />
<divclassName="flex items-center gap-1.5">
<divclassName="bg-green-400 h-1.5 w-3 rounded" />
<divclassName="bg-content-primary h-2 w-2 rounded-full" />
</div>
</div>
<div css={styles.body}>
<div css={styles.title} />
<div css={styles.table}>
<div css={styles.tableHeader} />
<div css={styles.workspace} />
<div css={styles.workspace} />
<div css={styles.workspace} />
<div css={styles.workspace} />
<div className="w-32 mx-auto">
<div className="bg-content-primary h-2 w-11 rounded mb-1.5" />
<div className="border border-solid rounded-t overflow-clip">
<div className="bg-surface-secondary h-2.5 -m-px" />
<div className="h-4 border-0 border-t border-border border-solid">
<div className="bg-content-disabled h-1.5 w-8 rounded mt-1 ml-1" />
</div>
<div className="h-4 border-0 border-t border-border border-solid">
<div className="bg-content-disabled h-1.5 w-8 rounded mt-1 ml-1" />
</div>
<div className="h-4 border-0 border-t border-border border-solid">
<div className="bg-content-disabled h-1.5 w-8 rounded mt-1 ml-1" />
</div>
<div className="h-4 border-0 border-t border-border border-solid">
<div className="bg-content-disabled h-1.5 w-8 rounded mt-1 ml-1" />
</div>
</div>
</div>
</div>
<divcss={styles.label}>
<divclassName="flex items-center justify-between border-0 border-t border-border border-solid px-3 py-1 text-sm">
<span>{displayName}</span>
{preview && <PreviewBadge />}
</div>
</div>
</ThemeOverride>
</div>
);
};

const styles = {
container: (theme) => ({
backgroundColor: theme.palette.background.default,
border: `1px solid ${theme.palette.divider}`,
width: 220,
color: theme.palette.text.primary,
borderRadius: 6,
overflow: "clip",
userSelect: "none",
}),
containerActive: (theme) => ({
outline: `2px solid ${theme.roles.active.outline}`,
}),
page: (theme) => ({
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
}),
header: (theme) => ({
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "6px 10px",
marginBottom: 8,
borderBottom: `1px solid ${theme.palette.divider}`,
}),
headerLinks: {
display: "flex",
alignItems: "center",
gap: 6,
},
headerLink: (theme) => ({
backgroundColor: theme.palette.text.secondary,
height: 6,
width: 20,
borderRadius: 3,
}),
activeHeaderLink: (theme) => ({
backgroundColor: theme.palette.text.primary,
}),
proxy: (theme) => ({
backgroundColor: theme.palette.success.light,
height: 6,
width: 12,
borderRadius: 3,
}),
user: (theme) => ({
backgroundColor: theme.palette.text.primary,
height: 8,
width: 8,
borderRadius: 4,
float: "right",
}),
body: {
width: 120,
margin: "auto",
},
title: (theme) => ({
backgroundColor: theme.palette.text.primary,
height: 8,
width: 45,
borderRadius: 4,
marginBottom: 6,
}),
table: (theme) => ({
border: `1px solid ${theme.palette.divider}`,
borderBottom: "none",
borderTopLeftRadius: 3,
borderTopRightRadius: 3,
overflow: "clip",
}),
tableHeader: (theme) => ({
backgroundColor: theme.palette.background.paper,
height: 10,
margin: -1,
}),
label: (theme) => ({
display: "flex",
alignItems: "center",
justifyContent: "space-between",
borderTop: `1px solid ${theme.palette.divider}`,
padding: "4px 12px",
fontSize: 14,
}),
workspace: (theme) => ({
borderTop: `1px solid ${theme.palette.divider}`,
height: 15,

"&::after": {
content: '""',
display: "block",
marginTop: 4,
marginLeft: 4,
backgroundColor: theme.palette.text.disabled,
height: 6,
width: 30,
borderRadius: 3,
},
}),
} satisfies Record<string, Interpolation<Theme>>;
Loading

[8]ページ先頭

©2009-2025 Movatter.jp