diff --git a/site/package.json b/site/package.json index 1951d32ad82c2..e4d65d55f7c5f 100644 --- a/site/package.json +++ b/site/package.json @@ -50,6 +50,7 @@ "@mui/system": "5.16.7", "@mui/utils": "5.16.6", "@mui/x-tree-view": "7.18.0", + "@radix-ui/react-avatar": "1.1.2", "@radix-ui/react-dialog": "1.1.2", "@radix-ui/react-label": "2.1.0", "@radix-ui/react-slider": "1.2.1", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index a9218c49473e1..e0d1cfab8b2f3 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: '@mui/x-tree-view': specifier: 7.18.0 version: 7.18.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@mui/material@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@5.16.7(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@emotion/styled@11.13.0(@emotion/react@11.13.3(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react@18.3.1))(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-avatar': + specifier: 1.1.2 + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: 1.1.2 version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -1550,6 +1553,19 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/react-avatar@1.1.2': + resolution: {integrity: sha512-GaC7bXQZ5VgZvVvsJ5mu/AEbjYLnhhkoidOboC50Z6FFlLA03wG2ianUoH+zgDQ31/9gCF59bE4+2bBgTyMiig==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1581,6 +1597,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.0.1': resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: @@ -1822,6 +1847,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slider@1.2.1': resolution: {integrity: sha512-bEzQoDW0XP+h/oGbutF5VMWJPAl/UU8IJjr7h02SOHDIIIxq+cep8nItVNoBV+OMmahCdqdF38FTpmXoqQUGvw==} peerDependencies: @@ -1853,6 +1891,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-switch@1.1.1': resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==} peerDependencies: @@ -3513,7 +3560,6 @@ packages: eslint@8.52.0: resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -7213,6 +7259,18 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/react-avatar@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1) @@ -7238,6 +7296,12 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + '@radix-ui/react-context@1.0.1(@types/react@18.3.12)(react@18.3.1)': dependencies: '@babel/runtime': 7.25.6 @@ -7455,6 +7519,15 @@ snapshots: '@types/react': 18.3.12 '@types/react-dom': 18.3.1 + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + '@radix-ui/react-slider@1.2.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/number': 1.1.0 @@ -7489,6 +7562,13 @@ snapshots: optionalDependencies: '@types/react': 18.3.12 + '@radix-ui/react-slot@1.1.1(@types/react@18.3.12)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.12 + '@radix-ui/react-switch@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 diff --git a/site/src/components/Avatar/Avatar.stories.tsx b/site/src/components/Avatar/Avatar.stories.tsx index 944185b8e2350..c1e2f590a91d3 100644 --- a/site/src/components/Avatar/Avatar.stories.tsx +++ b/site/src/components/Avatar/Avatar.stories.tsx @@ -1,59 +1,73 @@ -import PauseIcon from "@mui/icons-material/PauseOutlined"; import type { Meta, StoryObj } from "@storybook/react"; -import { Avatar, AvatarIcon } from "./Avatar"; +import { Avatar, AvatarFallback, AvatarImage } from "./Avatar"; const meta: Meta = { title: "components/Avatar", component: Avatar, + args: { + children: , + }, }; export default meta; type Story = StoryObj; -export const WithLetter: Story = { - args: { - children: "Coder", - }, +export const ImageLgSize: Story = { + args: { size: "lg" }, }; -export const WithLetterXL = { +export const ImageDefaultSize: Story = {}; + +export const ImageSmSize: Story = { + args: { size: "sm" }, +}; + +export const IconLgSize: Story = { args: { - children: "Coder", - size: "xl", + size: "lg", + variant: "icon", + children: ( + + ), }, }; -export const WithImage = { +export const IconDefaultSize: Story = { args: { - src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", + variant: "icon", + children: ( + + ), }, }; -export const WithImageXL = { +export const IconSmSize: Story = { args: { - src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", - size: "xl", + variant: "icon", + size: "sm", + children: ( + + ), }, }; -export const WithMuiIcon = { +export const FallbackLgSize: Story = { args: { - background: true, - children: , + size: "lg", + + children: AR, }, }; -export const WithMuiIconXL = { +export const FallbackDefaultSize: Story = { args: { - background: true, - children: , - size: "xl", + children: AR, }, }; -export const WithAvatarIcon = { +export const FallbackSmSize: Story = { args: { - background: true, - children: , + size: "sm", + children: AR, }, }; diff --git a/site/src/components/Avatar/Avatar.tsx b/site/src/components/Avatar/Avatar.tsx index 6e0e4c650573d..54807455dd030 100644 --- a/site/src/components/Avatar/Avatar.tsx +++ b/site/src/components/Avatar/Avatar.tsx @@ -1,116 +1,94 @@ -import { type Interpolation, type Theme, css, useTheme } from "@emotion/react"; -import MuiAvatar, { - type AvatarProps as MuiAvatarProps, - // biome-ignore lint/nursery/noRestrictedImports: Used as base component -} from "@mui/material/Avatar"; -import { visuallyHidden } from "@mui/utils"; -import { type FC, useId } from "react"; -import { getExternalImageStylesFromUrl } from "theme/externalImages"; - -export type AvatarProps = MuiAvatarProps & { - size?: "xs" | "sm" | "md" | "xl"; - background?: boolean; - fitImage?: boolean; -}; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; +import { type VariantProps, cva } from "class-variance-authority"; +/** + * Copied from shadc/ui on 12/16/2024 + * @see {@link https://ui.shadcn.com/docs/components/avatar} + * + * This component was updated to support the variants and match the styles from + * the Figma design: + * @see {@link https://www.figma.com/design/WfqIgsTFXN2BscBSSyXWF8/Coder-kit?node-id=711-383&t=xqxOSUk48GvDsjGK-0} + */ +import * as React from "react"; +import { cn } from "utils/cn"; -const sizeStyles = { - xs: { - width: 16, - height: 16, - fontSize: 8, - fontWeight: 700, +const avatarVariants = cva( + "relative flex shrink-0 overflow-hidden rounded border border-solid bg-surface-secondary text-content-secondary", + { + variants: { + size: { + lg: "h-10 w-10 rounded-[6px] text-sm font-medium", + default: "h-6 w-6 text-2xs", + sm: "h-[18px] w-[18px] text-[8px]", + }, + variant: { + default: "", + icon: "", + }, + }, + defaultVariants: { + size: "default", + }, + compoundVariants: [ + { + size: "lg", + variant: "icon", + className: "p-[9px]", + }, + { + size: "default", + variant: "icon", + className: "p-[3px]", + }, + { + size: "sm", + variant: "icon", + className: "p-[2px]", + }, + ], }, - sm: { - width: 24, - height: 24, - fontSize: 12, - fontWeight: 600, - }, - md: {}, - xl: { - width: 48, - height: 48, - fontSize: 24, - }, -} satisfies Record>; - -const fitImageStyles = css` - & .MuiAvatar-img { - object-fit: contain; - } -`; +); -export const Avatar: FC = ({ - size = "md", - fitImage, - children, - background, - ...muiProps -}) => { - const fromName = !muiProps.src && typeof children === "string"; +export interface AvatarProps + extends React.ComponentPropsWithoutRef, + VariantProps {} - return ( - ({ - background: - background || fromName ? theme.palette.divider : undefined, - color: theme.palette.text.primary, - }), - ]} - > - {typeof children === "string" ? firstLetter(children) : children} - - ); -}; - -export const ExternalAvatar: FC = (props) => { - const theme = useTheme(); - - return ( - - ); -}; - -type AvatarIconProps = { - src: string; - alt: string; -}; - -/** - * Use it to make an img element behaves like a MaterialUI Icon component - */ -export const AvatarIcon: FC = ({ src, alt }) => { - const hookId = useId(); - const avatarId = `${hookId}-avatar`; +const Avatar = React.forwardRef< + React.ElementRef, + AvatarProps +>(({ className, size, variant, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; - // We use a `visuallyHidden` element instead of setting `alt` to avoid - // splatting the text out on the screen if the image fails to load. - return ( - <> - -
- {alt} -
- - ); -}; +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; -const firstLetter = (str: string): string => { - if (str.length > 0) { - return str[0].toLocaleUpperCase(); - } +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; - return ""; -}; +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/site/src/components/AvatarCard/AvatarCard.tsx b/site/src/components/AvatarCard/AvatarCard.tsx index 5605b4c239732..a98dfc54b519d 100644 --- a/site/src/components/AvatarCard/AvatarCard.tsx +++ b/site/src/components/AvatarCard/AvatarCard.tsx @@ -1,5 +1,5 @@ import { type CSSObject, useTheme } from "@emotion/react"; -import { Avatar } from "components/Avatar/Avatar"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import type { FC, ReactNode } from "react"; type AvatarCardProps = { diff --git a/site/src/components/AvatarData/AvatarData.tsx b/site/src/components/AvatarData/AvatarData.tsx index eb9fa81d4981d..93952bb84d2fc 100644 --- a/site/src/components/AvatarData/AvatarData.tsx +++ b/site/src/components/AvatarData/AvatarData.tsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; -import { Avatar } from "components/Avatar/Avatar"; import { Stack } from "components/Stack/Stack"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import type { FC, ReactNode } from "react"; export interface AvatarDataProps { diff --git a/site/src/components/BuildAvatar/BuildAvatar.tsx b/site/src/components/BuildAvatar/BuildAvatar.tsx index e9870b4e746be..ae9e9e9af4904 100644 --- a/site/src/components/BuildAvatar/BuildAvatar.tsx +++ b/site/src/components/BuildAvatar/BuildAvatar.tsx @@ -2,8 +2,8 @@ import { css, cx } from "@emotion/css"; import { useTheme } from "@emotion/react"; import Badge from "@mui/material/Badge"; import type { WorkspaceBuild } from "api/typesGenerated"; -import { Avatar, type AvatarProps } from "components/Avatar/Avatar"; import { BuildIcon } from "components/BuildIcon/BuildIcon"; +import { Avatar, type AvatarProps } from "components/deprecated/Avatar/Avatar"; import { useClassName } from "hooks/useClassName"; import type { FC } from "react"; import { getDisplayWorkspaceBuildStatus } from "utils/workspace"; diff --git a/site/src/components/FullPageLayout/Topbar.tsx b/site/src/components/FullPageLayout/Topbar.tsx index 7787b0fe50295..ab51e93b395df 100644 --- a/site/src/components/FullPageLayout/Topbar.tsx +++ b/site/src/components/FullPageLayout/Topbar.tsx @@ -2,7 +2,10 @@ import { css } from "@emotion/css"; import { useTheme } from "@emotion/react"; import Button, { type ButtonProps } from "@mui/material/Button"; import IconButton, { type IconButtonProps } from "@mui/material/IconButton"; -import { type AvatarProps, ExternalAvatar } from "components/Avatar/Avatar"; +import { + type AvatarProps, + ExternalAvatar, +} from "components/deprecated/Avatar/Avatar"; import { type FC, type ForwardedRef, diff --git a/site/src/components/GroupAvatar/GroupAvatar.tsx b/site/src/components/GroupAvatar/GroupAvatar.tsx index c65e68f4f0dc7..fb65c448aef9e 100644 --- a/site/src/components/GroupAvatar/GroupAvatar.tsx +++ b/site/src/components/GroupAvatar/GroupAvatar.tsx @@ -1,6 +1,6 @@ import Group from "@mui/icons-material/Group"; import Badge from "@mui/material/Badge"; -import { Avatar } from "components/Avatar/Avatar"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { type ClassName, useClassName } from "hooks/useClassName"; import type { FC } from "react"; diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx index 2c4fc89f6ea21..6adf28f852f04 100644 --- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx +++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx @@ -5,8 +5,8 @@ import TextField from "@mui/material/TextField"; import { checkAuthorization } from "api/queries/authCheck"; import { organizations } from "api/queries/organizations"; import type { AuthorizationCheck, Organization } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { useDebouncedFunction } from "hooks/debounce"; import { type ChangeEvent, diff --git a/site/src/components/TemplateAvatar/TemplateAvatar.tsx b/site/src/components/TemplateAvatar/TemplateAvatar.tsx index 1882ae5cbd758..dc0017f459b19 100644 --- a/site/src/components/TemplateAvatar/TemplateAvatar.tsx +++ b/site/src/components/TemplateAvatar/TemplateAvatar.tsx @@ -1,5 +1,5 @@ import type { Template } from "api/typesGenerated"; -import { Avatar, type AvatarProps } from "components/Avatar/Avatar"; +import { Avatar, type AvatarProps } from "components/deprecated/Avatar/Avatar"; import type { FC } from "react"; interface TemplateAvatarProps extends AvatarProps { diff --git a/site/src/components/UserAutocomplete/UserAutocomplete.tsx b/site/src/components/UserAutocomplete/UserAutocomplete.tsx index d80773cb4de16..cf9c9ea6ecc0b 100644 --- a/site/src/components/UserAutocomplete/UserAutocomplete.tsx +++ b/site/src/components/UserAutocomplete/UserAutocomplete.tsx @@ -6,8 +6,8 @@ import { getErrorMessage } from "api/errors"; import { organizationMembers } from "api/queries/organizations"; import { users } from "api/queries/users"; import type { OrganizationMemberWithUserData, User } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { useDebouncedFunction } from "hooks/debounce"; import { type ChangeEvent, diff --git a/site/src/components/UserAvatar/UserAvatar.tsx b/site/src/components/UserAvatar/UserAvatar.tsx index c0815651ea348..5244dc6de20e0 100644 --- a/site/src/components/UserAvatar/UserAvatar.tsx +++ b/site/src/components/UserAvatar/UserAvatar.tsx @@ -1,4 +1,4 @@ -import { Avatar, type AvatarProps } from "components/Avatar/Avatar"; +import { Avatar, type AvatarProps } from "components/deprecated/Avatar/Avatar"; import type { FC } from "react"; export type UserAvatarProps = { diff --git a/site/src/components/deprecated/Avatar/Avatar.stories.tsx b/site/src/components/deprecated/Avatar/Avatar.stories.tsx new file mode 100644 index 0000000000000..c06390c450d89 --- /dev/null +++ b/site/src/components/deprecated/Avatar/Avatar.stories.tsx @@ -0,0 +1,59 @@ +import PauseIcon from "@mui/icons-material/PauseOutlined"; +import type { Meta, StoryObj } from "@storybook/react"; +import { Avatar, AvatarIcon } from "./Avatar"; + +const meta: Meta = { + title: "components/DeprecatedAvatar", + component: Avatar, +}; + +export default meta; +type Story = StoryObj; + +export const WithLetter: Story = { + args: { + children: "Coder", + }, +}; + +export const WithLetterXL = { + args: { + children: "Coder", + size: "xl", + }, +}; + +export const WithImage = { + args: { + src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", + }, +}; + +export const WithImageXL = { + args: { + src: "https://avatars.githubusercontent.com/u/95932066?s=200&v=4", + size: "xl", + }, +}; + +export const WithMuiIcon = { + args: { + background: true, + children: , + }, +}; + +export const WithMuiIconXL = { + args: { + background: true, + children: , + size: "xl", + }, +}; + +export const WithAvatarIcon = { + args: { + background: true, + children: , + }, +}; diff --git a/site/src/components/deprecated/Avatar/Avatar.tsx b/site/src/components/deprecated/Avatar/Avatar.tsx new file mode 100644 index 0000000000000..1fcd6208655d2 --- /dev/null +++ b/site/src/components/deprecated/Avatar/Avatar.tsx @@ -0,0 +1,124 @@ +import { type Interpolation, type Theme, css, useTheme } from "@emotion/react"; +import MuiAvatar, { + type AvatarProps as MuiAvatarProps, + // biome-ignore lint/nursery/noRestrictedImports: Used as base component +} from "@mui/material/Avatar"; +import { visuallyHidden } from "@mui/utils"; +import { type FC, useId } from "react"; +import { getExternalImageStylesFromUrl } from "theme/externalImages"; + +export type AvatarProps = MuiAvatarProps & { + size?: "xs" | "sm" | "md" | "xl"; + background?: boolean; + fitImage?: boolean; +}; + +const sizeStyles = { + xs: { + width: 16, + height: 16, + fontSize: 8, + fontWeight: 700, + }, + sm: { + width: 24, + height: 24, + fontSize: 12, + fontWeight: 600, + }, + md: {}, + xl: { + width: 48, + height: 48, + fontSize: 24, + }, +} satisfies Record>; + +const fitImageStyles = css` + & .MuiAvatar-img { + object-fit: contain; + } +`; + +/** + * @deprecated Use `Avatar` from `@components/Avatar` instead. + */ +export const Avatar: FC = ({ + size = "md", + fitImage, + children, + background, + ...muiProps +}) => { + const fromName = !muiProps.src && typeof children === "string"; + + return ( + ({ + background: + background || fromName ? theme.palette.divider : undefined, + color: theme.palette.text.primary, + }), + ]} + > + {typeof children === "string" ? firstLetter(children) : children} + + ); +}; + +/** + * @deprecated Use `Avatar` from `@components/Avatar` instead. + */ +export const ExternalAvatar: FC = (props) => { + const theme = useTheme(); + + return ( + + ); +}; + +type AvatarIconProps = { + src: string; + alt: string; +}; + +/** + * Use it to make an img element behaves like a MaterialUI Icon component + * + * @deprecated Use `AvatarIcon` from `@components/Avatar` instead. + */ +export const AvatarIcon: FC = ({ src, alt }) => { + const hookId = useId(); + const avatarId = `${hookId}-avatar`; + + // We use a `visuallyHidden` element instead of setting `alt` to avoid + // splatting the text out on the screen if the image fails to load. + return ( + <> + +
+ {alt} +
+ + ); +}; + +const firstLetter = (str: string): string => { + if (str.length > 0) { + return str[0].toLocaleUpperCase(); + } + + return ""; +}; diff --git a/site/src/modules/resources/ResourceAvatar.tsx b/site/src/modules/resources/ResourceAvatar.tsx index d910bbe0c6678..14d3b3248a882 100644 --- a/site/src/modules/resources/ResourceAvatar.tsx +++ b/site/src/modules/resources/ResourceAvatar.tsx @@ -1,7 +1,7 @@ import { visuallyHidden } from "@mui/utils"; import type { WorkspaceResource } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { ExternalImage } from "components/ExternalImage/ExternalImage"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { type FC, useId } from "react"; import { getResourceIconPath } from "utils/workspace"; diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index 1c158b8225e2f..5cda28cb495b5 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -5,7 +5,6 @@ import TextField from "@mui/material/TextField"; import type * as TypesGen from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Avatar } from "components/Avatar/Avatar"; import { FormFields, FormFooter, @@ -22,6 +21,7 @@ import { Pill } from "components/Pill/Pill"; import { RichParameterInput } from "components/RichParameterInput/RichParameterInput"; import { Stack } from "components/Stack/Stack"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { type FormikContextType, useFormik } from "formik"; import { generateWorkspaceName } from "modules/workspaces/generateWorkspaceName"; import { type FC, useCallback, useEffect, useMemo, useState } from "react"; diff --git a/site/src/pages/CreateWorkspacePage/SelectedTemplate.tsx b/site/src/pages/CreateWorkspacePage/SelectedTemplate.tsx index 7d949c3a3ed40..921dd9d173557 100644 --- a/site/src/pages/CreateWorkspacePage/SelectedTemplate.tsx +++ b/site/src/pages/CreateWorkspacePage/SelectedTemplate.tsx @@ -1,7 +1,7 @@ import type { Interpolation, Theme } from "@emotion/react"; import type { Template, TemplateExample } from "api/typesGenerated"; -import { ExternalAvatar } from "components/Avatar/Avatar"; import { Stack } from "components/Stack/Stack"; +import { ExternalAvatar } from "components/deprecated/Avatar/Avatar"; import type { FC } from "react"; export interface SelectedTemplateProps { diff --git a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx index 7481d8d5c1684..47d19f48d68eb 100644 --- a/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/OAuth2AppsSettingsPage/OAuth2AppsSettingsPageView.tsx @@ -10,11 +10,11 @@ import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import type * as TypesGen from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; import { TableLoader } from "components/TableLoader/TableLoader"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { useClickableTableRow } from "hooks/useClickableTableRow"; import type { FC } from "react"; import { Link, useNavigate } from "react-router-dom"; diff --git a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx index 73248c66fd21d..d6e19665cdf92 100644 --- a/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx +++ b/site/src/pages/ExternalAuthPage/ExternalAuthPageView.tsx @@ -8,10 +8,10 @@ import Tooltip from "@mui/material/Tooltip"; import type { ApiErrorResponse } from "api/errors"; import type { ExternalAuth, ExternalAuthDevice } from "api/typesGenerated"; import { Alert, AlertDetail } from "components/Alert/Alert"; -import { Avatar } from "components/Avatar/Avatar"; import { CopyButton } from "components/CopyButton/CopyButton"; import { SignInLayout } from "components/SignInLayout/SignInLayout"; import { Welcome } from "components/Welcome/Welcome"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import type { FC, ReactNode } from "react"; export interface ExternalAuthPageViewProps { diff --git a/site/src/pages/TemplatePage/TemplatePageHeader.tsx b/site/src/pages/TemplatePage/TemplatePageHeader.tsx index 4ed1103194225..4f6d345b0c4fb 100644 --- a/site/src/pages/TemplatePage/TemplatePageHeader.tsx +++ b/site/src/pages/TemplatePage/TemplatePageHeader.tsx @@ -11,7 +11,6 @@ import type { Template, TemplateVersion, } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"; import { Margins } from "components/Margins/Margins"; @@ -30,6 +29,7 @@ import { } from "components/PageHeader/PageHeader"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; import { useQuery } from "react-query"; diff --git a/site/src/pages/TemplateSettingsPage/Sidebar.tsx b/site/src/pages/TemplateSettingsPage/Sidebar.tsx index 67294d248e373..b28c27cda005c 100644 --- a/site/src/pages/TemplateSettingsPage/Sidebar.tsx +++ b/site/src/pages/TemplateSettingsPage/Sidebar.tsx @@ -3,12 +3,12 @@ import SecurityIcon from "@mui/icons-material/LockOutlined"; import GeneralIcon from "@mui/icons-material/SettingsOutlined"; import ScheduleIcon from "@mui/icons-material/TimerOutlined"; import type { Template } from "api/typesGenerated"; -import { ExternalAvatar } from "components/Avatar/Avatar"; import { Sidebar as BaseSidebar, SidebarHeader, SidebarNavItem, } from "components/Sidebar/Sidebar"; +import { ExternalAvatar } from "components/deprecated/Avatar/Avatar"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index da6d4e113229b..34667c437ceee 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -12,7 +12,6 @@ import TableRow from "@mui/material/TableRow"; import { hasError, isApiValidationError } from "api/errors"; import type { Template, TemplateExample } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { ExternalAvatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; import { DeprecatedBadge } from "components/Badges/Badges"; @@ -37,6 +36,7 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { ExternalAvatar } from "components/deprecated/Avatar/Avatar"; import { useClickableTableRow } from "hooks/useClickableTableRow"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; diff --git a/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx b/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx index cb3334a9a8bf0..09ec684bf56c6 100644 --- a/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx +++ b/site/src/pages/UserSettingsPage/ExternalAuthPage/ExternalAuthPageView.tsx @@ -20,7 +20,6 @@ import type { ListUserExternalAuthResponse, } from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Avatar, ExternalAvatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { Loader } from "components/Loader/Loader"; import { @@ -31,6 +30,7 @@ import { ThreeDotsButton, } from "components/MoreMenu/MoreMenu"; import { TableEmpty } from "components/TableEmpty/TableEmpty"; +import { Avatar, ExternalAvatar } from "components/deprecated/Avatar/Avatar"; import type { ExternalAuthPollingState } from "pages/CreateWorkspacePage/CreateWorkspacePage"; import { type FC, useCallback, useEffect, useState } from "react"; import { useQuery } from "react-query"; diff --git a/site/src/pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPageView.tsx b/site/src/pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPageView.tsx index 446f62ee5e922..17e953222c07e 100644 --- a/site/src/pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPageView.tsx +++ b/site/src/pages/UserSettingsPage/OAuth2ProviderPage/OAuth2ProviderPageView.tsx @@ -7,9 +7,9 @@ import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import type * as TypesGen from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; -import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { TableLoader } from "components/TableLoader/TableLoader"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import type { FC } from "react"; export type OAuth2ProviderPageViewProps = { diff --git a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx index 39f3d4e8ae60a..0005e1fe28a68 100644 --- a/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx +++ b/site/src/pages/UserSettingsPage/WorkspaceProxyPage/WorkspaceProxyRow.tsx @@ -2,7 +2,6 @@ import { useTheme } from "@emotion/react"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; import type { Region, WorkspaceProxy } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { HealthyBadge, @@ -10,6 +9,7 @@ import { NotReachableBadge, NotRegisteredBadge, } from "components/Badges/Badges"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import type { ProxyLatencyReport } from "contexts/useProxyLatency"; import type { FC, ReactNode } from "react"; import { getLatencyColor } from "utils/latency"; diff --git a/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx b/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx index 0400c5ac05bab..f26bcb36c8b13 100644 --- a/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx +++ b/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx @@ -4,7 +4,6 @@ import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; import TableCell from "@mui/material/TableCell"; import type { Group } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { OverflowY } from "components/OverflowY/OverflowY"; import { Popover, @@ -12,6 +11,7 @@ import { PopoverTrigger, } from "components/Popover/Popover"; import { Stack } from "components/Stack/Stack"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import type { FC } from "react"; type GroupsCellProps = { diff --git a/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx b/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx index 206f7b743f28f..cd12862097f45 100644 --- a/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx +++ b/site/src/pages/WorkspacePage/ChangeVersionDialog.tsx @@ -6,7 +6,6 @@ import CircularProgress from "@mui/material/CircularProgress"; import TextField from "@mui/material/TextField"; import type { Template, TemplateVersion } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; -import { Avatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; import type { DialogProps } from "components/Dialogs/Dialog"; @@ -14,6 +13,7 @@ import { FormFields } from "components/Form/Form"; import { Loader } from "components/Loader/Loader"; import { Pill } from "components/Pill/Pill"; import { Stack } from "components/Stack/Stack"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { TemplateUpdateMessage } from "modules/templates/TemplateUpdateMessage"; import { type FC, useRef, useState } from "react"; import { createDayString } from "utils/createDayString"; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 7ca112befb4e5..25d1b0e7a9fab 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -6,7 +6,6 @@ import Link from "@mui/material/Link"; import Tooltip from "@mui/material/Tooltip"; import { workspaceQuota } from "api/queries/workspaceQuota"; import type * as TypesGen from "api/typesGenerated"; -import { ExternalAvatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { Topbar, @@ -19,6 +18,7 @@ import { import { HelpTooltipContent } from "components/HelpTooltip/HelpTooltip"; import { Popover, PopoverTrigger } from "components/Popover/Popover"; import { UserAvatar } from "components/UserAvatar/UserAvatar"; +import { ExternalAvatar } from "components/deprecated/Avatar/Avatar"; import { useDashboard } from "modules/dashboard/useDashboard"; import { linkToTemplate, useLinks } from "modules/navigation"; import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/WorkspaceStatusBadge"; diff --git a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx index f10729d2c5c35..b89e6f5b7d887 100644 --- a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx +++ b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx @@ -2,12 +2,12 @@ import ParameterIcon from "@mui/icons-material/CodeOutlined"; import GeneralIcon from "@mui/icons-material/SettingsOutlined"; import ScheduleIcon from "@mui/icons-material/TimerOutlined"; import type { Workspace } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { Sidebar as BaseSidebar, SidebarHeader, SidebarNavItem, } from "components/Sidebar/Sidebar"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import type { FC } from "react"; interface SidebarProps { diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index 8723469ad4a7b..2cbca11fcef60 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -3,7 +3,6 @@ import OpenIcon from "@mui/icons-material/OpenInNewOutlined"; import Button from "@mui/material/Button"; import Link from "@mui/material/Link"; import type { Template } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { Loader } from "components/Loader/Loader"; import { MenuSearch } from "components/Menu/MenuSearch"; import { OverflowY } from "components/OverflowY/OverflowY"; @@ -13,6 +12,7 @@ import { PopoverTrigger, } from "components/Popover/Popover"; import { SearchEmpty, searchStyles } from "components/Search/Search"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { linkToTemplate, useLinks } from "modules/navigation"; import { type FC, type ReactNode, useState } from "react"; import type { UseQueryResult } from "react-query"; diff --git a/site/src/pages/WorkspacesPage/WorkspacesEmpty.tsx b/site/src/pages/WorkspacesPage/WorkspacesEmpty.tsx index c2a1ced06800d..dcf96ec5c0d8b 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesEmpty.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesEmpty.tsx @@ -1,8 +1,8 @@ import ArrowForwardOutlined from "@mui/icons-material/ArrowForwardOutlined"; import Button from "@mui/material/Button"; import type { Template } from "api/typesGenerated"; -import { Avatar } from "components/Avatar/Avatar"; import { TableEmpty } from "components/TableEmpty/TableEmpty"; +import { Avatar } from "components/deprecated/Avatar/Avatar"; import { linkToTemplate, useLinks } from "modules/navigation"; import type { FC } from "react"; import { Link } from "react-router-dom"; diff --git a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx index cff9873274a55..2ca1b28ca1632 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesTable.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesTable.tsx @@ -11,7 +11,6 @@ import TableHead from "@mui/material/TableHead"; import TableRow from "@mui/material/TableRow"; import { visuallyHidden } from "@mui/utils"; import type { Template, Workspace } from "api/typesGenerated"; -import { ExternalAvatar } from "components/Avatar/Avatar"; import { AvatarData } from "components/AvatarData/AvatarData"; import { AvatarDataSkeleton } from "components/AvatarData/AvatarDataSkeleton"; import { InfoTooltip } from "components/InfoTooltip/InfoTooltip"; @@ -20,6 +19,7 @@ import { TableLoaderSkeleton, TableRowSkeleton, } from "components/TableLoader/TableLoader"; +import { ExternalAvatar } from "components/deprecated/Avatar/Avatar"; import { useClickableTableRow } from "hooks/useClickableTableRow"; import { useDashboard } from "modules/dashboard/useDashboard"; import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge";