From 8cbf9aab4278bccdc4490ee0e824a6dd2625e97e Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 13 Aug 2025 20:01:29 +0000 Subject: [PATCH 1/6] Update EnterpriseSnackbar.tsx --- .../GlobalSnackbar/EnterpriseSnackbar.tsx | 51 +++++-------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx b/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx index e7c9fbcce3863..725f6ef907c06 100644 --- a/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx +++ b/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx @@ -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"; @@ -35,7 +34,6 @@ export const EnterpriseSnackbar: FC = ({ action, ...snackbarProps }) => { - const content = useClassName(classNames.content(variant), [variant]); return ( = ({ horizontal: "right", }} action={ -
+
{action} - +
} 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} @@ -67,39 +68,13 @@ export const EnterpriseSnackbar: FC = ({ ); }; -const variantColor = (variant: EnterpriseSnackbarVariant, theme: Theme) => { +const variantColor = (variant: EnterpriseSnackbarVariant) => { switch (variant) { case "error": - return theme.palette.error.main; + return "border-highlight-red"; 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>; From f1ed76e7e8b697126be30117906056b5eae35f23 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 13 Aug 2025 21:48:30 +0000 Subject: [PATCH 2/6] Update Sidebar.tsx --- site/src/@types/storybook.d.ts | 2 + .../GlobalSnackbar/EnterpriseSnackbar.tsx | 3 +- .../components/Sidebar/Sidebar.stories.tsx | 94 ++++++++++++++----- site/src/components/Sidebar/Sidebar.tsx | 87 ++++------------- 4 files changed, 91 insertions(+), 95 deletions(-) diff --git a/site/src/@types/storybook.d.ts b/site/src/@types/storybook.d.ts index ccdecd690c9c8..599324a291ae4 100644 --- a/site/src/@types/storybook.d.ts +++ b/site/src/@types/storybook.d.ts @@ -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 = @@ -24,5 +25,6 @@ declare module "@storybook/react-vite" { permissions?: Partial; deploymentValues?: DeploymentValues; deploymentOptions?: SerpentOption[]; + reactRouter?: ReactRouterAddonStoryParameters; } } diff --git a/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx b/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx index 725f6ef907c06..17036fb2be8e4 100644 --- a/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx +++ b/site/src/components/GlobalSnackbar/EnterpriseSnackbar.tsx @@ -34,7 +34,6 @@ export const EnterpriseSnackbar: FC = ({ action, ...snackbarProps }) => { - return ( = ({ 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) + variantColor(variant), ), }} onClose={onClose} diff --git a/site/src/components/Sidebar/Sidebar.stories.tsx b/site/src/components/Sidebar/Sidebar.stories.tsx index 083bffa423fe4..f352118f5f69e 100644 --- a/site/src/components/Sidebar/Sidebar.stories.tsx +++ b/site/src/components/Sidebar/Sidebar.stories.tsx @@ -7,6 +7,7 @@ import { LockIcon, UserIcon, } from "lucide-react"; +import { Outlet } from "react-router"; import { Sidebar, SidebarHeader, SidebarNavItem } from "./Sidebar"; const meta: Meta = { @@ -18,30 +19,73 @@ export default meta; type Story = StoryObj; export const Default: Story = { - args: { - children: ( - - } - title="Jon" - subtitle="jon@coder.com" - /> - - Account - - - Schedule - - - Security - - - SSH Keys - - - Tokens - - - ), + decorators: [ + (Story) => { + return ( +
+ + +
+ ); + }, + ], + render: () => ( + + } + title="Jon" + subtitle="jon@coder.com" + /> + + Account + + + Schedule + + + Security + + + SSH Keys + + + Tokens + + + ), + 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, + }, + ], + }, + ], + }, }, }; diff --git a/site/src/components/Sidebar/Sidebar.tsx b/site/src/components/Sidebar/Sidebar.tsx index 813835baeb277..249362788d2c0 100644 --- a/site/src/components/Sidebar/Sidebar.tsx +++ b/site/src/components/Sidebar/Sidebar.tsx @@ -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"; @@ -21,6 +18,11 @@ interface SidebarHeaderProps { linkTo?: string; } +const titleStyles = { + normal: + "text-semibold overflow-hidden whitespace-nowrap text-content-primary", +}; + export const SidebarHeader: FC = ({ avatar, title, @@ -28,7 +30,7 @@ export const SidebarHeader: FC = ({ linkTo, }) => { return ( - + {avatar}
= ({ }} > {linkTo ? ( - + {title} ) : ( - {title} + {title} )} - {subtitle} + + {subtitle} +
); @@ -88,14 +92,18 @@ export const SidebarNavItem: FC = ({ href, icon: Icon, }) => { - const link = useClassName(classNames.link, []); - const activeLink = useClassName(classNames.activeLink, []); - return ( 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", + ) + } > @@ -104,60 +112,3 @@ export const SidebarNavItem: FC = ({ ); }; - -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>; - -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; From c412562d81d163b51fd5f6a2e8ceca4a995d2bb4 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 13 Aug 2025 22:27:28 +0000 Subject: [PATCH 3/6] Update HealthLayout.tsx --- site/src/pages/HealthPage/HealthLayout.tsx | 104 +++++---------------- 1 file changed, 25 insertions(+), 79 deletions(-) diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx index e1bdec0973b83..d7f5e2bfc9115 100644 --- a/site/src/pages/HealthPage/HealthLayout.tsx +++ b/site/src/pages/HealthPage/HealthLayout.tsx @@ -1,4 +1,3 @@ -import { cx } from "@emotion/css"; import { useTheme } from "@emotion/react"; import NotificationsOffOutlined from "@mui/icons-material/NotificationsOffOutlined"; import ReplayIcon from "@mui/icons-material/Replay"; @@ -9,13 +8,13 @@ import { health, refreshHealth } from "api/queries/debug"; import type { HealthSeverity } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; -import { type ClassName, useClassName } from "hooks/useClassName"; import kebabCase from "lodash/fp/kebabCase"; import { DashboardFullPage } from "modules/dashboard/DashboardLayout"; import { type FC, Suspense } from "react"; import { Helmet } from "react-helmet-async"; import { useMutation, useQuery, useQueryClient } from "react-query"; import { NavLink, Outlet } from "react-router"; +import { cn } from "utils/cn"; import { createDayString } from "utils/createDayString"; import { pageTitle } from "utils/page"; import { HealthIcon } from "./Content"; @@ -44,8 +43,12 @@ export const HealthLayout: FC = () => { } as const; const visibleSections = filterVisibleSections(sections); - const link = useClassName(classNames.link, []); - const activeLink = useClassName(classNames.activeLink, []); + const link = ` + text-content-secondary border-none text-sm w-full flex items-center gap-3 + text-left h-9 px-6 cursor-pointer no-underline transition-colors + hover:bg-surface-secondary hover:text-content-primary + `; + const activeLink = "bg-surface-secondary text-content-primary"; if (isLoading) { return ( @@ -71,36 +74,17 @@ export const HealthLayout: FC = () => {
@@ -116,12 +100,12 @@ export const HealthLayout: FC = () => { {isRefreshing ? ( ) : ( - + )}
-
+
{healthStatus.healthy ? "Healthy" : "Unhealthy"}
{ > {healthStatus.healthy ? Object.keys(visibleSections).some((key) => { - const section = - healthStatus[key as keyof typeof visibleSections]; - return section.warnings && section.warnings.length > 0; - }) + const section = + healthStatus[key as keyof typeof visibleSections]; + return section.warnings && section.warnings.length > 0; + }) ? "All systems operational, but performance might be degraded" : "All systems operational" : "Some issues have been detected"}
-
- Last check +
+ Last check {createDayString(healthStatus.time)}
- Version + Version {healthStatus.coder_version}
-
-
+
}> @@ -229,35 +207,3 @@ const filterVisibleSections = (sections: T) => { return visible; }; - -const classNames = { - link: (css, theme) => - css({ - background: "none", - pointerEvents: "auto", - color: theme.palette.text.secondary, - border: "none", - fontSize: 14, - width: "100%", - display: "flex", - alignItems: "center", - gap: 12, - textAlign: "left", - height: 36, - padding: "0 24px", - cursor: "pointer", - textDecoration: "none", - - "&:hover": { - background: theme.palette.action.hover, - color: theme.palette.text.primary, - }, - }), - - activeLink: (css, theme) => - css({ - background: theme.palette.action.hover, - pointerEvents: "none", - color: theme.palette.text.primary, - }), -} satisfies Record; From f92399587a712834add880cac8ee894af4ff96d6 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 13 Aug 2025 22:32:00 +0000 Subject: [PATCH 4/6] deprecate `useClassName` --- site/src/hooks/useClassName.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/hooks/useClassName.ts b/site/src/hooks/useClassName.ts index 472e8681a028e..5155d1795a4a5 100644 --- a/site/src/hooks/useClassName.ts +++ b/site/src/hooks/useClassName.ts @@ -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(); From fdf177edcd4c3de2535819f977f5b1752c9e8c54 Mon Sep 17 00:00:00 2001 From: McKayla Washburn Date: Wed, 13 Aug 2025 22:58:10 +0000 Subject: [PATCH 5/6] chore: convert emotion styles to tailwind --- site/src/components/EmptyState/EmptyState.tsx | 38 +++++----------- .../components/FullPageForm/FullPageForm.tsx | 2 +- .../src/components/FullPageLayout/Sidebar.tsx | 12 +---- site/src/components/LastSeen/LastSeen.tsx | 2 +- site/src/components/Latency/Latency.tsx | 16 ++----- .../PaginationWidget/PaginationWidgetBase.tsx | 11 +---- site/src/components/Paywall/Paywall.tsx | 44 +++---------------- .../SelectMenu/SelectMenu.stories.tsx | 8 ++-- site/src/components/SelectMenu/SelectMenu.tsx | 13 ++---- site/src/components/Sidebar/Sidebar.tsx | 2 +- .../SyntaxHighlighter/SyntaxHighlighter.tsx | 7 ++- .../TableEmpty/TableEmpty.stories.tsx | 8 +--- site/src/components/TableEmpty/TableEmpty.tsx | 2 +- .../components/TableToolbar/TableToolbar.tsx | 15 +------ site/src/pages/HealthPage/HealthLayout.tsx | 24 ++++------ 15 files changed, 48 insertions(+), 156 deletions(-) diff --git a/site/src/components/EmptyState/EmptyState.tsx b/site/src/components/EmptyState/EmptyState.tsx index 1371d7e9fa56e..7dfa2d4e08b34 100644 --- a/site/src/components/EmptyState/EmptyState.tsx +++ b/site/src/components/EmptyState/EmptyState.tsx @@ -1,4 +1,5 @@ import type { FC, HTMLAttributes, ReactNode } from "react"; +import { cn } from "utils/cn"; export interface EmptyStateProps extends HTMLAttributes { /** Text Message to display, placed inside Typography component */ @@ -21,44 +22,25 @@ export const EmptyState: FC = ({ cta, image, isCompact, + className, ...attrs }) => { return (
-
{message}
+
{message}
{description && ( -

({ - marginTop: 16, - fontSize: 16, - lineHeight: "140%", - maxWidth: 480, - color: theme.palette.text.secondary, - })} - > +

{description}

)} - {cta &&
{cta}
} + {cta &&
{cta}
} {image}
); diff --git a/site/src/components/FullPageForm/FullPageForm.tsx b/site/src/components/FullPageForm/FullPageForm.tsx index 571cc56ea9b0f..b4825731bcd43 100644 --- a/site/src/components/FullPageForm/FullPageForm.tsx +++ b/site/src/components/FullPageForm/FullPageForm.tsx @@ -19,7 +19,7 @@ export const FullPageForm: FC = ({ }) => { return ( - + {title} {detail && {detail}} diff --git a/site/src/components/FullPageLayout/Sidebar.tsx b/site/src/components/FullPageLayout/Sidebar.tsx index 8a5eaf9b4f6be..e35d3e78175a3 100644 --- a/site/src/components/FullPageLayout/Sidebar.tsx +++ b/site/src/components/FullPageLayout/Sidebar.tsx @@ -7,17 +7,7 @@ export const Sidebar: FC> = (props) => { const theme = useTheme(); return (
); diff --git a/site/src/components/LastSeen/LastSeen.tsx b/site/src/components/LastSeen/LastSeen.tsx index 23e9a2076984e..b5a375619975f 100644 --- a/site/src/components/LastSeen/LastSeen.tsx +++ b/site/src/components/LastSeen/LastSeen.tsx @@ -40,7 +40,7 @@ export const LastSeen: FC = ({ at, className, ...attrs }) => { return ( diff --git a/site/src/components/Latency/Latency.tsx b/site/src/components/Latency/Latency.tsx index b5509ba450847..88fd4571dc6d2 100644 --- a/site/src/components/Latency/Latency.tsx +++ b/site/src/components/Latency/Latency.tsx @@ -25,11 +25,7 @@ export const Latency: FC = ({ if (isLoading) { return ( - + ); } @@ -41,20 +37,14 @@ export const Latency: FC = ({ <> {notAvailableText} - + ); } return ( -

+

Latency: {latency.toFixed(0)} ms diff --git a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx index 2022461a401f6..02b5d39a6b445 100644 --- a/site/src/components/PaginationWidget/PaginationWidgetBase.tsx +++ b/site/src/components/PaginationWidget/PaginationWidgetBase.tsx @@ -39,16 +39,7 @@ export const PaginationWidgetBase: FC = ({ ); return ( -

+
= ({ return (
- -
{message}
+ +
{message}
- {description &&

{description}

} + {description &&

{description}

} Read the documentation
-
+
-