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 e7c9fbcce3863..04b214995c814 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,8 +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 +67,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-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>; 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..a09d9bfbaa517 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; 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(); diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx index e1bdec0973b83..86e79aa9ec69e 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,17 +8,26 @@ 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"; +const linkStyles = { + normal: ` + 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 + `, + active: "bg-surface-secondary text-content-primary", +}; + export const HealthLayout: FC = () => { const theme = useTheme(); const queryClient = useQueryClient(); @@ -44,9 +52,6 @@ export const HealthLayout: FC = () => { } as const; const visibleSections = filterVisibleSections(sections); - const link = useClassName(classNames.link, []); - const activeLink = useClassName(classNames.activeLink, []); - if (isLoading) { return (
@@ -70,38 +75,11 @@ export const HealthLayout: FC = () => { -
-
-
+
+
+
-
+
@@ -116,20 +94,15 @@ export const HealthLayout: FC = () => { {isRefreshing ? ( ) : ( - + )}
-
+
{healthStatus.healthy ? "Healthy" : "Unhealthy"}
-
+
{healthStatus.healthy ? Object.keys(visibleSections).some((key) => { const section = @@ -142,34 +115,28 @@ export const HealthLayout: FC = () => {
-
- Last check +
+ Last check {createDayString(healthStatus.time)}
-
- Version +
+ Version {healthStatus.coder_version}
-
-
+
}> @@ -229,35 +196,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;