Skip to content

chore: remove some usage of useClassName #19346

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 our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 14, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions site/src/@types/storybook.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "api/typesGenerated";
import type { Permissions } from "modules/permissions";
import type { QueryKey } from "react-query";
import type { ReactRouterAddonStoryParameters } from "storybook-addon-remix-react-router";

declare module "@storybook/react-vite" {
type WebSocketEvent =
Expand All @@ -24,5 +25,6 @@ declare module "@storybook/react-vite" {
permissions?: Partial<Permissions>;
deploymentValues?: DeploymentValues;
deploymentOptions?: SerpentOption[];
reactRouter?: ReactRouterAddonStoryParameters;
}
}
52 changes: 13 additions & 39 deletions site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { Interpolation, Theme } from "@emotion/react";
import IconButton from "@mui/material/IconButton";
import Snackbar, {
type SnackbarProps as MuiSnackbarProps,
} from "@mui/material/Snackbar";
import { type ClassName, useClassName } from "hooks/useClassName";
import { X as XIcon } from "lucide-react";
import type { FC } from "react";
import { cn } from "utils/cn";

type EnterpriseSnackbarVariant = "error" | "info" | "success";

Expand Down Expand Up @@ -35,29 +34,30 @@ export const EnterpriseSnackbar: FC<EnterpriseSnackbarProps> = ({
action,
...snackbarProps
}) => {
const content = useClassName(classNames.content(variant), [variant]);

return (
<Snackbar
anchorOrigin={{
vertical: "bottom",
horizontal: "right",
}}
action={
<div css={styles.actionWrapper}>
<div className="flex items-center">
{action}
<IconButton onClick={onClose} css={{ padding: 0 }}>
<IconButton onClick={onClose} className="p-0">
<XIcon
css={styles.closeIcon}
aria-label="close"
className="size-icon-sm"
className="size-icon-sm text-content-primary"
/>
</IconButton>
</div>
}
ContentProps={{
...ContentProps,
className: content,
className: cn(
"rounded-lg bg-surface-secondary text-content-primary shadow",
"py-2 pl-6 pr-4 items-[inherit] border-0 border-l-[4px]",
variantColor(variant),
),
}}
onClose={onClose}
{...snackbarProps}
Expand All @@ -67,39 +67,13 @@ export const EnterpriseSnackbar: FC<EnterpriseSnackbarProps> = ({
);
};

const variantColor = (variant: EnterpriseSnackbarVariant, theme: Theme) => {
const variantColor = (variant: EnterpriseSnackbarVariant) => {
switch (variant) {
case "error":
return theme.palette.error.main;
return "border-border-destructive";
case "info":
return theme.palette.info.main;
return "border-highlight-sky";
case "success":
return theme.palette.success.main;
return "border-border-success";
}
};

const classNames = {
content:
(variant: EnterpriseSnackbarVariant): ClassName =>
(css, theme) =>
css`
border: 1px solid ${theme.palette.divider};
border-left: 4px solid ${variantColor(variant, theme)};
border-radius: 8px;
padding: 8px 24px 8px 16px;
box-shadow: ${theme.shadows[6]};
align-items: inherit;
background-color: ${theme.palette.background.paper};
color: ${theme.palette.text.secondary};
`,
};

const styles = {
actionWrapper: {
display: "flex",
alignItems: "center",
},
closeIcon: (theme) => ({
color: theme.palette.primary.contrastText,
}),
} satisfies Record<string, Interpolation<Theme>>;
94 changes: 69 additions & 25 deletions site/src/components/Sidebar/Sidebar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
LockIcon,
UserIcon,
} from "lucide-react";
import { Outlet } from "react-router";
import { Sidebar, SidebarHeader, SidebarNavItem } from "./Sidebar";

const meta: Meta<typeof Sidebar> = {
Expand All @@ -18,30 +19,73 @@ export default meta;
type Story = StoryObj<typeof Sidebar>;

export const Default: Story = {
args: {
children: (
<Sidebar>
<SidebarHeader
avatar={<Avatar fallback="Jon" />}
title="Jon"
subtitle="jon@coder.com"
/>
<SidebarNavItem href="account" icon={UserIcon}>
Account
</SidebarNavItem>
<SidebarNavItem href="schedule" icon={CalendarCogIcon}>
Schedule
</SidebarNavItem>
<SidebarNavItem href="security" icon={LockIcon}>
Security
</SidebarNavItem>
<SidebarNavItem href="ssh-keys" icon={FingerprintIcon}>
SSH Keys
</SidebarNavItem>
<SidebarNavItem href="tokens" icon={KeyIcon}>
Tokens
</SidebarNavItem>
</Sidebar>
),
decorators: [
(Story) => {
return (
<div className="flex gap-2">
<Story />
<Outlet />
</div>
);
},
],
render: () => (
<Sidebar>
<SidebarHeader
avatar={<Avatar fallback="Jon" />}
title="Jon"
subtitle="jon@coder.com"
/>
<SidebarNavItem href="account" icon={UserIcon}>
Account
</SidebarNavItem>
<SidebarNavItem href="schedule" icon={CalendarCogIcon}>
Schedule
</SidebarNavItem>
<SidebarNavItem href="security" icon={LockIcon}>
Security
</SidebarNavItem>
<SidebarNavItem href="ssh-keys" icon={FingerprintIcon}>
SSH Keys
</SidebarNavItem>
<SidebarNavItem href="tokens" icon={KeyIcon}>
Tokens
</SidebarNavItem>
</Sidebar>
),
parameters: {
reactRouter: {
location: {
path: "/account",
},
routing: [
{
path: "/",
useStoryElement: true,
children: [
{
path: "account",
element: <>Account page</>,
},
{
path: "schedule",
element: <>Schedule page</>,
},
{
path: "security",
element: <>Security page</>,
},
{
path: "ssh-keys",
element: <>SSH Keys</>,
},
{
path: "tokens",
element: <>Tokens page</>,
},
],
},
],
},
},
};
87 changes: 19 additions & 68 deletions site/src/components/Sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
import { cx } from "@emotion/css";
import type { CSSObject, Interpolation, Theme } from "@emotion/react";
import { Stack } from "components/Stack/Stack";
import { type ClassName, useClassName } from "hooks/useClassName";
import type { ElementType, FC, ReactNode } from "react";
import { Link, NavLink } from "react-router";
import { cn } from "utils/cn";
Expand All @@ -21,14 +18,19 @@ interface SidebarHeaderProps {
linkTo?: string;
}

const titleStyles = {
normal:
"text-semibold overflow-hidden whitespace-nowrap text-content-primary",
};

export const SidebarHeader: FC<SidebarHeaderProps> = ({
avatar,
title,
subtitle,
linkTo,
}) => {
return (
<Stack direction="row" spacing={1} css={styles.info}>
<Stack direction="row" spacing={1} className="mb-4">
{avatar}
<div
css={{
Expand All @@ -38,13 +40,15 @@ export const SidebarHeader: FC<SidebarHeaderProps> = ({
}}
>
{linkTo ? (
<Link css={styles.title} to={linkTo}>
<Link className={cn(titleStyles.normal, "no-underline")} to={linkTo}>
{title}
</Link>
) : (
<span css={styles.title}>{title}</span>
<span className={titleStyles.normal}>{title}</span>
)}
<span css={styles.subtitle}>{subtitle}</span>
<span className="text-content-secondary text-sm overflow-hidden overflow-ellipsis">
{subtitle}
</span>
</div>
</Stack>
);
Expand Down Expand Up @@ -88,14 +92,18 @@ export const SidebarNavItem: FC<SidebarNavItemProps> = ({
href,
icon: Icon,
}) => {
const link = useClassName(classNames.link, []);
const activeLink = useClassName(classNames.activeLink, []);

return (
<NavLink
end
to={href}
className={({ isActive }) => cx([link, isActive && activeLink])}
className={({ isActive }) =>
cn(
"block relative text-sm text-inherit mb-px p-3 pl-4 rounded-sm",
"transition-colors no-underline hover:bg-surface-secondary",
isActive &&
"bg-surface-secondary border-0 border-solid border-l-[3px] border-highlight-sky",
)
}
>
<Stack alignItems="center" spacing={1.5} direction="row">
<Icon css={{ width: 16, height: 16 }} />
Expand All @@ -104,60 +112,3 @@ export const SidebarNavItem: FC<SidebarNavItemProps> = ({
</NavLink>
);
};

const styles = {
info: (theme) => ({
...(theme.typography.body2 as CSSObject),
marginBottom: 16,
}),

title: (theme) => ({
fontWeight: 600,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
color: theme.palette.text.primary,
textDecoration: "none",
}),
subtitle: (theme) => ({
color: theme.palette.text.secondary,
fontSize: 12,
overflow: "hidden",
textOverflow: "ellipsis",
}),
} satisfies Record<string, Interpolation<Theme>>;

const classNames = {
link: (css, theme) => css`
color: inherit;
display: block;
font-size: 14px;
text-decoration: none;
padding: 12px 12px 12px 16px;
border-radius: 4px;
transition: background-color 0.15s ease-in-out;
margin-bottom: 1px;
position: relative;

&:hover {
background-color: ${theme.palette.action.hover};
}
`,

activeLink: (css, theme) => css`
background-color: ${theme.palette.action.hover};

&:before {
content: "";
display: block;
width: 3px;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: ${theme.palette.primary.main};
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
}
`,
} satisfies Record<string, ClassName>;
6 changes: 3 additions & 3 deletions site/src/hooks/useClassName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { type DependencyList, useMemo } from "react";
export type ClassName = (cssFn: typeof css, theme: Theme) => string;

/**
* An escape hatch for when you really need to manually pass around a
* `className`. Prefer using the `css` prop whenever possible. If you
* can't use that, then this might be helpful for you.
* @deprecated This hook was used as an escape hatch to generate class names
* using emotion when no other styling method would work. There is no valid new
* usage of this hook. Use Tailwind classes instead.
*/
export function useClassName(styles: ClassName, deps: DependencyList): string {
const theme = useTheme();
Expand Down
Loading
Loading