fix large-icon visual bug

Looks like there was some kind of runtime conflict when running @emotion/css and @emotion/react at the same time in this app? Some styles would just get clobbered, making things look all weird.

Here, I've removed our @emotion/css dependency, and use the `<ClassNames>` utility element from `@emotion/react` instead. I'm not thrilled about the solution, but it seems okay for now...

...one other thing I tried was passing a `css` prop to Chakra elements, which seemed to work, but to clobber the element's own Emotion-based styles. I assumed that the Babel macro wouldn't help us, and wouldn't convert css props to className props for non-HTML elements... but I suppose I'm not sure!

Anyway, I don't love this syntax... but I'm happy for the site to be working again. I wonder if we can find something better.
This commit is contained in:
Emi Matchu 2021-01-04 03:11:46 +00:00
parent 40728daa99
commit 4120c7aa88
16 changed files with 1460 additions and 1349 deletions

View file

@ -9,7 +9,6 @@
"@chakra-ui/icons": "^1.0.2", "@chakra-ui/icons": "^1.0.2",
"@chakra-ui/react": "^1.0.4", "@chakra-ui/react": "^1.0.4",
"@chakra-ui/theme-tools": "^1.0.2", "@chakra-ui/theme-tools": "^1.0.2",
"@emotion/css": "^11.1.3",
"@emotion/react": "^11.1.4", "@emotion/react": "^11.1.4",
"@emotion/styled": "^11.0.0", "@emotion/styled": "^11.0.0",
"@loadable/component": "^5.12.0", "@loadable/component": "^5.12.0",

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { import {
Box, Box,
@ -213,37 +213,41 @@ function SubmitPetForm() {
const buttonBgColorHover = useColorModeValue("green.700", "green.200"); const buttonBgColorHover = useColorModeValue("green.700", "green.200");
return ( return (
<form onSubmit={onSubmit}> <ClassNames>
<Flex> {({ css }) => (
<Input <form onSubmit={onSubmit}>
value={petName} <Flex>
onChange={(e) => setPetName(e.target.value)} <Input
isDisabled={loading} value={petName}
placeholder="Enter a pet's name" onChange={(e) => setPetName(e.target.value)}
aria-label="Enter a pet's name" isDisabled={loading}
borderColor={inputBorderColor} placeholder="Enter a pet's name"
_hover={{ borderColor: inputBorderColorHover }} aria-label="Enter a pet's name"
boxShadow="md" borderColor={inputBorderColor}
width="14em" _hover={{ borderColor: inputBorderColorHover }}
className={css` boxShadow="md"
&::placeholder { width="14em"
color: ${theme.colors.gray["500"]}; className={css`
} &::placeholder {
`} color: ${theme.colors.gray["500"]};
/> }
<Box width="4" /> `}
<Button />
type="submit" <Box width="4" />
colorScheme="green" <Button
isDisabled={!petName} type="submit"
isLoading={loading} colorScheme="green"
backgroundColor={buttonBgColor} // for AA contrast isDisabled={!petName}
_hover={{ backgroundColor: buttonBgColorHover }} isLoading={loading}
> backgroundColor={buttonBgColor} // for AA contrast
Start _hover={{ backgroundColor: buttonBgColorHover }}
</Button> >
</Flex> Start
</form> </Button>
</Flex>
</form>
)}
</ClassNames>
); );
} }

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { import {
AspectRatio, AspectRatio,
Button, Button,
@ -205,55 +205,62 @@ function ItemPageOwnButton({ itemId, isChecked }) {
); );
return ( return (
<Box as="label"> <ClassNames>
<VisuallyHidden {({ css }) => (
as="input" <Box as="label">
type="checkbox" <VisuallyHidden
checked={isChecked} as="input"
onChange={(e) => { type="checkbox"
if (e.target.checked) { checked={isChecked}
sendAddMutation().catch((e) => { onChange={(e) => {
console.error(e); if (e.target.checked) {
toast({ sendAddMutation().catch((e) => {
title: "We had trouble adding this to the items you own.", console.error(e);
description: "Check your internet connection, and try again.", toast({
status: "error", title: "We had trouble adding this to the items you own.",
duration: 5000, description:
}); "Check your internet connection, and try again.",
}); status: "error",
} else { duration: 5000,
sendRemoveMutation().catch((e) => { });
console.error(e); });
toast({ } else {
title: "We had trouble removing this from the items you own.", sendRemoveMutation().catch((e) => {
description: "Check your internet connection, and try again.", console.error(e);
status: "error", toast({
duration: 5000, title:
}); "We had trouble removing this from the items you own.",
}); description:
} "Check your internet connection, and try again.",
}} status: "error",
/> duration: 5000,
<Button });
as="div" });
colorScheme={isChecked ? "green" : "gray"} }
size="lg" }}
cursor="pointer" />
transitionDuration="0.4s" <Button
className={css` as="div"
input:focus + & { colorScheme={isChecked ? "green" : "gray"}
box-shadow: ${theme.shadows.outline}; size="lg"
} cursor="pointer"
`} transitionDuration="0.4s"
> className={css`
<IconCheckbox input:focus + & {
icon={<CheckIcon />} box-shadow: ${theme.shadows.outline};
isChecked={isChecked} }
marginRight="0.5em" `}
/> >
I own this <IconCheckbox
</Button> icon={<CheckIcon />}
</Box> isChecked={isChecked}
marginRight="0.5em"
/>
I own this
</Button>
</Box>
)}
</ClassNames>
); );
} }
@ -306,55 +313,62 @@ function ItemPageWantButton({ itemId, isChecked }) {
); );
return ( return (
<Box as="label"> <ClassNames>
<VisuallyHidden {({ css }) => (
as="input" <Box as="label">
type="checkbox" <VisuallyHidden
isChecked={isChecked} as="input"
onChange={(e) => { type="checkbox"
if (e.target.checked) { isChecked={isChecked}
sendAddMutation().catch((e) => { onChange={(e) => {
console.error(e); if (e.target.checked) {
toast({ sendAddMutation().catch((e) => {
title: "We had trouble adding this to the items you want.", console.error(e);
description: "Check your internet connection, and try again.", toast({
status: "error", title: "We had trouble adding this to the items you want.",
duration: 5000, description:
}); "Check your internet connection, and try again.",
}); status: "error",
} else { duration: 5000,
sendRemoveMutation().catch((e) => { });
console.error(e); });
toast({ } else {
title: "We had trouble removing this from the items you want.", sendRemoveMutation().catch((e) => {
description: "Check your internet connection, and try again.", console.error(e);
status: "error", toast({
duration: 5000, title:
}); "We had trouble removing this from the items you want.",
}); description:
} "Check your internet connection, and try again.",
}} status: "error",
/> duration: 5000,
<Button });
as="div" });
colorScheme={isChecked ? "blue" : "gray"} }
size="lg" }}
cursor="pointer" />
transitionDuration="0.4s" <Button
className={css` as="div"
input:focus + & { colorScheme={isChecked ? "blue" : "gray"}
box-shadow: ${theme.shadows.outline}; size="lg"
} cursor="pointer"
`} transitionDuration="0.4s"
> className={css`
<IconCheckbox input:focus + & {
icon={<StarIcon />} box-shadow: ${theme.shadows.outline};
isChecked={isChecked} }
marginRight="0.5em" `}
/> >
I want this <IconCheckbox
</Button> icon={<StarIcon />}
</Box> isChecked={isChecked}
marginRight="0.5em"
/>
I want this
</Button>
</Box>
)}
</ClassNames>
); );
} }

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { Box, Skeleton, useColorModeValue, useToken } from "@chakra-ui/react"; import { Box, Skeleton, useColorModeValue, useToken } from "@chakra-ui/react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
@ -165,84 +165,88 @@ function ItemTradesTable({
}; };
return ( return (
<Box <ClassNames>
as="table" {({ css }) => (
width="100%" <Box
boxShadow="md" as="table"
className={css` width="100%"
/* Chakra doesn't have props for these! */ boxShadow="md"
border-collapse: separate; className={css`
border-spacing: 0; /* Chakra doesn't have props for these! */
table-layout: fixed; border-collapse: separate;
`} border-spacing: 0;
> table-layout: fixed;
<Box as="thead" fontSize={{ base: "xs", sm: "sm" }}> `}
<Box as="tr"> >
<ItemTradesTableCell as="th" width={minorColumnWidth}> <Box as="thead" fontSize={{ base: "xs", sm: "sm" }}>
{/* A small wording tweak to fit better on the xsmall screens! */} <Box as="tr">
<Box display={{ base: "none", sm: "block" }}>Last active</Box> <ItemTradesTableCell as="th" width={minorColumnWidth}>
<Box display={{ base: "block", sm: "none" }}>Last edit</Box> {/* A small wording tweak to fit better on the xsmall screens! */}
</ItemTradesTableCell> <Box display={{ base: "none", sm: "block" }}>Last active</Box>
{shouldShowCompareColumn && ( <Box display={{ base: "block", sm: "none" }}>Last edit</Box>
<ItemTradesTableCell as="th" width={minorColumnWidth}> </ItemTradesTableCell>
<Box display={{ base: "none", sm: "block" }}> {shouldShowCompareColumn && (
{compareColumnLabel} <ItemTradesTableCell as="th" width={minorColumnWidth}>
</Box> <Box display={{ base: "none", sm: "block" }}>
<Box display={{ base: "block", sm: "none" }}>Matches</Box> {compareColumnLabel}
</ItemTradesTableCell> </Box>
)} <Box display={{ base: "block", sm: "none" }}>Matches</Box>
<ItemTradesTableCell as="th" width={minorColumnWidth}> </ItemTradesTableCell>
{userHeading} )}
</ItemTradesTableCell> <ItemTradesTableCell as="th" width={minorColumnWidth}>
<ItemTradesTableCell as="th">List</ItemTradesTableCell> {userHeading}
</Box> </ItemTradesTableCell>
</Box> <ItemTradesTableCell as="th">List</ItemTradesTableCell>
<Box as="tbody"> </Box>
{loading && (
<>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
</>
)}
{!loading &&
trades.length > 0 &&
trades.map((trade) => (
<ItemTradesTableRow
key={trade.id}
href={`/user/${trade.user.id}/items#list-${trade.closetList.id}`}
username={trade.user.username}
listName={trade.closetList.name}
lastTradeActivity={trade.user.lastTradeActivity}
matchingItems={trade.user.matchingItems}
shouldShowCompareColumn={shouldShowCompareColumn}
/>
))}
{!loading && trades.length === 0 && (
<Box as="tr">
<ItemTradesTableCell
colSpan={shouldShowCompareColumn ? 4 : 3}
textAlign="center"
fontStyle="italic"
>
No trades yet!
</ItemTradesTableCell>
</Box> </Box>
)} <Box as="tbody">
</Box> {loading && (
</Box> <>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
<ItemTradesTableRowSkeleton
shouldShowCompareColumn={shouldShowCompareColumn}
/>
</>
)}
{!loading &&
trades.length > 0 &&
trades.map((trade) => (
<ItemTradesTableRow
key={trade.id}
href={`/user/${trade.user.id}/items#list-${trade.closetList.id}`}
username={trade.user.username}
listName={trade.closetList.name}
lastTradeActivity={trade.user.lastTradeActivity}
matchingItems={trade.user.matchingItems}
shouldShowCompareColumn={shouldShowCompareColumn}
/>
))}
{!loading && trades.length === 0 && (
<Box as="tr">
<ItemTradesTableCell
colSpan={shouldShowCompareColumn ? 4 : 3}
textAlign="center"
fontStyle="italic"
>
No trades yet!
</ItemTradesTableCell>
</Box>
)}
</Box>
</Box>
)}
</ClassNames>
); );
} }
@ -263,63 +267,67 @@ function ItemTradesTableRow({
); );
return ( return (
<Box <ClassNames>
as="tr" {({ css }) => (
cursor="pointer"
_hover={{ background: focusBackground }}
_focusWithin={{ background: focusBackground }}
onClick={onClick}
>
<ItemTradesTableCell fontSize="xs">
{formatVagueDate(lastTradeActivity)}
</ItemTradesTableCell>
{shouldShowCompareColumn && (
<ItemTradesTableCell fontSize="xs">
{matchingItems.length > 0 ? (
<Box as="ul">
{sortedMatchingItems.slice(0, 4).map((item) => (
<Box key={item.id} as="li">
<Box
lineHeight="1.5"
maxHeight="1.5em"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
{item.name}
</Box>
</Box>
))}
{matchingItems.length > 4 && (
<Box as="li">+ {matchingItems.length - 4} more</Box>
)}
</Box>
) : (
<>
<Box display={{ base: "none", sm: "block" }}>No matches</Box>
<Box display={{ base: "block", sm: "none" }}>None</Box>
</>
)}
</ItemTradesTableCell>
)}
<ItemTradesTableCell fontSize="xs">{username}</ItemTradesTableCell>
<ItemTradesTableCell fontSize="sm">
<Box <Box
as={Link} as="tr"
to={href} cursor="pointer"
className={css` _hover={{ background: focusBackground }}
&:hover, _focusWithin={{ background: focusBackground }}
&:focus, onClick={onClick}
tr:hover &,
tr:focus-within & {
text-decoration: underline;
}
`}
> >
{listName} <ItemTradesTableCell fontSize="xs">
{formatVagueDate(lastTradeActivity)}
</ItemTradesTableCell>
{shouldShowCompareColumn && (
<ItemTradesTableCell fontSize="xs">
{matchingItems.length > 0 ? (
<Box as="ul">
{sortedMatchingItems.slice(0, 4).map((item) => (
<Box key={item.id} as="li">
<Box
lineHeight="1.5"
maxHeight="1.5em"
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
{item.name}
</Box>
</Box>
))}
{matchingItems.length > 4 && (
<Box as="li">+ {matchingItems.length - 4} more</Box>
)}
</Box>
) : (
<>
<Box display={{ base: "none", sm: "block" }}>No matches</Box>
<Box display={{ base: "block", sm: "none" }}>None</Box>
</>
)}
</ItemTradesTableCell>
)}
<ItemTradesTableCell fontSize="xs">{username}</ItemTradesTableCell>
<ItemTradesTableCell fontSize="sm">
<Box
as={Link}
to={href}
className={css`
&:hover,
&:focus,
tr:hover &,
tr:focus-within & {
text-decoration: underline;
}
`}
>
{listName}
</Box>
</ItemTradesTableCell>
</Box> </Box>
</ItemTradesTableCell> )}
</Box> </ClassNames>
); );
} }
@ -350,47 +358,51 @@ function ItemTradesTableCell({ children, as = "td", ...props }) {
const borderRadiusCss = useToken("radii", "md"); const borderRadiusCss = useToken("radii", "md");
return ( return (
<Box <ClassNames>
as={as} {({ css }) => (
paddingX="4" <Box
paddingY="2" as={as}
textAlign="left" paddingX="4"
className={css` paddingY="2"
/* Lol sigh, getting this right is way more involved than I wish it textAlign="left"
className={css`
/* Lol sigh, getting this right is way more involved than I wish it
* were. What I really want is border-collapse and a simple 1px border, * were. What I really want is border-collapse and a simple 1px border,
* but that disables border-radius. So, we homebrew it by giving all * but that disables border-radius. So, we homebrew it by giving all
* cells bottom and right borders, but only the cells on the edges a * cells bottom and right borders, but only the cells on the edges a
* top or left border; and then target the exact 4 corner cells to * top or left border; and then target the exact 4 corner cells to
* round them. Pretty old-school tbh 🙃 */ * round them. Pretty old-school tbh 🙃 */
border-bottom: 1px solid ${borderColorCss}; border-bottom: 1px solid ${borderColorCss};
border-right: 1px solid ${borderColorCss}; border-right: 1px solid ${borderColorCss};
thead tr:first-of-type & { thead tr:first-of-type & {
border-top: 1px solid ${borderColorCss}; border-top: 1px solid ${borderColorCss};
} }
&:first-of-type { &:first-of-type {
border-left: 1px solid ${borderColorCss}; border-left: 1px solid ${borderColorCss};
} }
thead tr:first-of-type &:first-of-type { thead tr:first-of-type &:first-of-type {
border-top-left-radius: ${borderRadiusCss}; border-top-left-radius: ${borderRadiusCss};
} }
thead tr:first-of-type &:last-of-type { thead tr:first-of-type &:last-of-type {
border-top-right-radius: ${borderRadiusCss}; border-top-right-radius: ${borderRadiusCss};
} }
tbody tr:last-of-type &:first-of-type { tbody tr:last-of-type &:first-of-type {
border-bottom-left-radius: ${borderRadiusCss}; border-bottom-left-radius: ${borderRadiusCss};
} }
tbody tr:last-of-type &:last-of-type { tbody tr:last-of-type &:last-of-type {
border-bottom-right-radius: ${borderRadiusCss}; border-bottom-right-radius: ${borderRadiusCss};
} }
`} `}
{...props} {...props}
> >
{children} {children}
</Box> </Box>
)}
</ClassNames>
); );
} }

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css } from "@emotion/css"; import { css } from "@emotion/react";
import { VStack } from "@chakra-ui/react"; import { VStack } from "@chakra-ui/react";
import { Heading1, Heading2, Heading3 } from "./util"; import { Heading1, Heading2, Heading3 } from "./util";
@ -11,7 +11,7 @@ function PrivacyPolicyPage() {
<VStack <VStack
spacing="4" spacing="4"
alignItems="flex-start" alignItems="flex-start"
className={css` css={css`
max-width: 800px; max-width: 800px;
p { p {

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { import {
Badge, Badge,
Box, Box,
@ -149,139 +149,147 @@ function UserItemsPage() {
).size; ).size;
return ( return (
<Box> <ClassNames>
<Flex align="center" wrap="wrap-reverse"> {({ css }) => (
<Box> <Box>
<Heading1> <Flex align="center" wrap="wrap-reverse">
{isCurrentUser ? "Your items" : `${data.user.username}'s items`} <Box>
</Heading1> <Heading1>
<Wrap spacing="2" opacity="0.7"> {isCurrentUser ? "Your items" : `${data.user.username}'s items`}
{data.user.contactNeopetsUsername && ( </Heading1>
<WrapItem> <Wrap spacing="2" opacity="0.7">
<Badge {data.user.contactNeopetsUsername && (
as="a" <WrapItem>
href={`http://www.neopets.com/userlookup.phtml?user=${data.user.contactNeopetsUsername}`} <Badge
display="flex" as="a"
alignItems="center" href={`http://www.neopets.com/userlookup.phtml?user=${data.user.contactNeopetsUsername}`}
> display="flex"
<NeopetsStarIcon marginRight="1" /> alignItems="center"
{data.user.contactNeopetsUsername} >
</Badge> <NeopetsStarIcon marginRight="1" />
</WrapItem> {data.user.contactNeopetsUsername}
)} </Badge>
{data.user.contactNeopetsUsername && ( </WrapItem>
<WrapItem> )}
<Badge {data.user.contactNeopetsUsername && (
as="a" <WrapItem>
href={`http://www.neopets.com/neomessages.phtml?type=send&recipient=${data.user.contactNeopetsUsername}`} <Badge
display="flex" as="a"
alignItems="center" href={`http://www.neopets.com/neomessages.phtml?type=send&recipient=${data.user.contactNeopetsUsername}`}
> display="flex"
<EmailIcon marginRight="1" /> alignItems="center"
Neomail >
</Badge> <EmailIcon marginRight="1" />
</WrapItem> Neomail
)} </Badge>
<SupportOnly> </WrapItem>
<WrapItem> )}
<UserSupportMenu user={data.user}> <SupportOnly>
<MenuButton <WrapItem>
as={BadgeButton} <UserSupportMenu user={data.user}>
display="flex" <MenuButton
alignItems="center" as={BadgeButton}
> display="flex"
<EditIcon marginRight="1" /> alignItems="center"
Support >
</MenuButton> <EditIcon marginRight="1" />
</UserSupportMenu> Support
</WrapItem> </MenuButton>
</SupportOnly> </UserSupportMenu>
{/* Usually I put "Own" before "Want", but this matches the natural </WrapItem>
* order on the page: the _matches_ for things you want are things </SupportOnly>
* _this user_ owns, so they come first. I think it's also probably a {/* Usually I put "Own" before "Want", but this matches the natural
* more natural train of thought: you come to someone's list _wanting_ * order on the page: the _matches_ for things you want are things
* something, and _then_ thinking about what you can offer. */} * _this user_ owns, so they come first. I think it's also probably a
{!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && ( * more natural train of thought: you come to someone's list _wanting_
<WrapItem> * something, and _then_ thinking about what you can offer. */}
<Badge {!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && (
as="a" <WrapItem>
href="#owned-items" <Badge
colorScheme="blue" as="a"
display="flex" href="#owned-items"
alignItems="center" colorScheme="blue"
> display="flex"
<StarIcon marginRight="1" /> alignItems="center"
{numItemsTheyOwnThatYouWant > 1 >
? `${numItemsTheyOwnThatYouWant} items you want` <StarIcon marginRight="1" />
: "1 item you want"} {numItemsTheyOwnThatYouWant > 1
</Badge> ? `${numItemsTheyOwnThatYouWant} items you want`
</WrapItem> : "1 item you want"}
)} </Badge>
{!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && ( </WrapItem>
<WrapItem> )}
<Badge {!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && (
as="a" <WrapItem>
href="#wanted-items" <Badge
colorScheme="green" as="a"
display="flex" href="#wanted-items"
alignItems="center" colorScheme="green"
> display="flex"
<CheckIcon marginRight="1" /> alignItems="center"
{numItemsTheyWantThatYouOwn > 1 >
? `${numItemsTheyWantThatYouOwn} items you own` <CheckIcon marginRight="1" />
: "1 item you own"} {numItemsTheyWantThatYouOwn > 1
</Badge> ? `${numItemsTheyWantThatYouOwn} items you own`
</WrapItem> : "1 item you own"}
)} </Badge>
</Wrap> </WrapItem>
</Box> )}
<Box flex="1 0 auto" width="2" /> </Wrap>
<Box marginBottom="1"> </Box>
<UserSearchForm /> <Box flex="1 0 auto" width="2" />
</Box> <Box marginBottom="1">
</Flex> <UserSearchForm />
</Box>
</Flex>
<Box marginTop="4"> <Box marginTop="4">
{isCurrentUser && ( {isCurrentUser && (
<Box float="right"> <Box float="right">
<WIPCallout details="These lists are read-only for now. To edit, head back to Classic DTI!" /> <WIPCallout details="These lists are read-only for now. To edit, head back to Classic DTI!" />
</Box>
)}
<Heading2 id="owned-items" marginBottom="2">
{isCurrentUser
? "Items you own"
: `Items ${data.user.username} owns`}
</Heading2>
<VStack
spacing="8"
alignItems="stretch"
className={css`
clear: both;
`}
>
{listsOfOwnedItems.map((closetList) => (
<ClosetList
key={closetList.id}
closetList={closetList}
isCurrentUser={isCurrentUser}
showHeading={listsOfOwnedItems.length > 1}
/>
))}
</VStack>
</Box> </Box>
)}
<Heading2 id="owned-items" marginBottom="2">
{isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`}
</Heading2>
<VStack
spacing="8"
alignItems="stretch"
className={css`
clear: both;
`}
>
{listsOfOwnedItems.map((closetList) => (
<ClosetList
key={closetList.id}
closetList={closetList}
isCurrentUser={isCurrentUser}
showHeading={listsOfOwnedItems.length > 1}
/>
))}
</VStack>
</Box>
<Heading2 id="wanted-items" marginTop="10" marginBottom="2"> <Heading2 id="wanted-items" marginTop="10" marginBottom="2">
{isCurrentUser ? "Items you want" : `Items ${data.user.username} wants`} {isCurrentUser
</Heading2> ? "Items you want"
<VStack spacing="4" alignItems="stretch"> : `Items ${data.user.username} wants`}
{listsOfWantedItems.map((closetList) => ( </Heading2>
<ClosetList <VStack spacing="4" alignItems="stretch">
key={closetList.id} {listsOfWantedItems.map((closetList) => (
closetList={closetList} <ClosetList
isCurrentUser={isCurrentUser} key={closetList.id}
showHeading={listsOfWantedItems.length > 1} closetList={closetList}
/> isCurrentUser={isCurrentUser}
))} showHeading={listsOfWantedItems.length > 1}
</VStack> />
</Box> ))}
</VStack>
</Box>
)}
</ClassNames>
); );
} }
@ -588,21 +596,25 @@ function MarkdownAndSafeHTML({ children }) {
}); });
return ( return (
<Box <ClassNames>
dangerouslySetInnerHTML={{ __html: sanitizedHtml }} {({ css }) => (
className={css` <Box
.paragraph, dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
ol, className={css`
ul { .paragraph,
margin-bottom: 1em; ol,
} ul {
margin-bottom: 1em;
}
ol, ol,
ul { ul {
margin-left: 2em; margin-left: 2em;
} }
`} `}
></Box> />
)}
</ClassNames>
); );
} }

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css, cx } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { import {
Box, Box,
Flex, Flex,
@ -166,35 +166,39 @@ function ItemContainer({ children, isDisabled = false }) {
); );
return ( return (
<Box <ClassNames>
p="1" {({ css, cx }) => (
my="1" <Box
borderRadius="lg" p="1"
d="flex" my="1"
cursor={isDisabled ? undefined : "pointer"} borderRadius="lg"
border="1px" d="flex"
borderColor="transparent" cursor={isDisabled ? undefined : "pointer"}
className={cx([ border="1px"
"item-container", borderColor="transparent"
!isDisabled && className={cx([
css` "item-container",
&:hover, !isDisabled &&
input:focus + & { css`
background-color: ${focusBackgroundColor}; &:hover,
} input:focus + & {
background-color: ${focusBackgroundColor};
}
input:active + & { input:active + & {
border-color: ${activeBorderColor}; border-color: ${activeBorderColor};
} }
input:checked:focus + & { input:checked:focus + & {
border-color: ${focusCheckedBorderColor}; border-color: ${focusCheckedBorderColor};
} }
`, `,
])} ])}
> >
{children} {children}
</Box> </Box>
)}
</ClassNames>
); );
} }
@ -242,39 +246,43 @@ function ItemActionButton({ icon, label, to, onClick }) {
); );
return ( return (
<Tooltip label={label} placement="top"> <ClassNames>
<IconButton {({ css }) => (
as={to ? Link : "button"} <Tooltip label={label} placement="top">
icon={icon} <IconButton
aria-label={label} as={to ? Link : "button"}
variant="ghost" icon={icon}
color="gray.400" aria-label={label}
to={to} variant="ghost"
onClick={onClick} color="gray.400"
className={css` to={to}
opacity: 0; onClick={onClick}
transition: all 0.2s; className={css`
opacity: 0;
transition: all 0.2s;
${containerHasFocus} { ${containerHasFocus} {
opacity: 1; opacity: 1;
} }
&:focus, &:focus,
&:hover { &:hover {
opacity: 1; opacity: 1;
background-color: ${focusBackgroundColor}; background-color: ${focusBackgroundColor};
color: ${focusColor}; color: ${focusColor};
} }
/* On touch devices, always show the buttons! This avoids having to /* On touch devices, always show the buttons! This avoids having to
* tap to reveal them (which toggles the item), or worse, * tap to reveal them (which toggles the item), or worse,
* accidentally tapping a hidden button without realizing! */ * accidentally tapping a hidden button without realizing! */
@media (hover: none) { @media (hover: none) {
opacity: 1; opacity: 1;
} }
`} `}
/> />
</Tooltip> </Tooltip>
)}
</ClassNames>
); );
} }

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { import {
Box, Box,
Editable, Editable,
@ -34,50 +34,59 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
const { zonesAndItems, incompatibleItems } = outfitState; const { zonesAndItems, incompatibleItems } = outfitState;
return ( return (
<Box> <ClassNames>
<Box px="1"> {({ css }) => (
<OutfitHeading <Box>
outfitState={outfitState} <Box px="1">
dispatchToOutfit={dispatchToOutfit} <OutfitHeading
/> outfitState={outfitState}
</Box> dispatchToOutfit={dispatchToOutfit}
<Flex direction="column"> />
{loading ? ( </Box>
<ItemZoneGroupsSkeleton itemCount={outfitState.allItemIds.length} /> <Flex direction="column">
) : ( {loading ? (
<TransitionGroup component={null}> <ItemZoneGroupsSkeleton
{zonesAndItems.map(({ zoneLabel, items }) => ( itemCount={outfitState.allItemIds.length}
<CSSTransition key={zoneLabel} {...fadeOutAndRollUpTransition}>
<ItemZoneGroup
zoneLabel={zoneLabel}
items={items}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
/>
</CSSTransition>
))}
{incompatibleItems.length > 0 && (
<ItemZoneGroup
zoneLabel="Incompatible"
afterHeader={
<Tooltip
label="These items don't fit this pet"
placement="top"
openDelay={100}
>
<QuestionIcon fontSize="sm" />
</Tooltip>
}
items={incompatibleItems}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
isDisabled
/> />
) : (
<TransitionGroup component={null}>
{zonesAndItems.map(({ zoneLabel, items }) => (
<CSSTransition
key={zoneLabel}
{...fadeOutAndRollUpTransition(css)}
>
<ItemZoneGroup
zoneLabel={zoneLabel}
items={items}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
/>
</CSSTransition>
))}
{incompatibleItems.length > 0 && (
<ItemZoneGroup
zoneLabel="Incompatible"
afterHeader={
<Tooltip
label="These items don't fit this pet"
placement="top"
openDelay={100}
>
<QuestionIcon fontSize="sm" />
</Tooltip>
}
items={incompatibleItems}
outfitState={outfitState}
dispatchToOutfit={dispatchToOutfit}
isDisabled
/>
)}
</TransitionGroup>
)} )}
</TransitionGroup> </Flex>
)} </Box>
</Flex> )}
</Box> </ClassNames>
); );
} }
@ -125,59 +134,66 @@ function ItemZoneGroup({
); );
return ( return (
<Box mb="10"> <ClassNames>
<Heading2 display="flex" alignItems="center" mx="1"> {({ css }) => (
{zoneLabel} <Box mb="10">
{afterHeader && <Box marginLeft="2">{afterHeader}</Box>} <Heading2 display="flex" alignItems="center" mx="1">
</Heading2> {zoneLabel}
<ItemListContainer> {afterHeader && <Box marginLeft="2">{afterHeader}</Box>}
<TransitionGroup component={null}> </Heading2>
{items.map((item) => { <ItemListContainer>
const itemNameId = <TransitionGroup component={null}>
zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`; {items.map((item) => {
const itemNode = ( const itemNameId =
<Item zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`;
item={item} const itemNode = (
itemNameId={itemNameId} <Item
isWorn={ item={item}
!isDisabled && outfitState.wornItemIds.includes(item.id) itemNameId={itemNameId}
} isWorn={
isInOutfit={outfitState.allItemIds.includes(item.id)} !isDisabled && outfitState.wornItemIds.includes(item.id)
onRemove={onRemove} }
isDisabled={isDisabled} isInOutfit={outfitState.allItemIds.includes(item.id)}
/> onRemove={onRemove}
); isDisabled={isDisabled}
/>
);
return ( return (
<CSSTransition key={item.id} {...fadeOutAndRollUpTransition}> <CSSTransition
{isDisabled ? ( key={item.id}
itemNode {...fadeOutAndRollUpTransition(css)}
) : ( >
<label> {isDisabled ? (
<VisuallyHidden itemNode
as="input" ) : (
type="radio" <label>
aria-labelledby={itemNameId} <VisuallyHidden
name={zoneLabel} as="input"
value={item.id} type="radio"
checked={outfitState.wornItemIds.includes(item.id)} aria-labelledby={itemNameId}
onChange={onChange} name={zoneLabel}
onClick={onClick} value={item.id}
onKeyUp={(e) => { checked={outfitState.wornItemIds.includes(item.id)}
if (e.key === " ") { onChange={onChange}
onClick(e); onClick={onClick}
} onKeyUp={(e) => {
}} if (e.key === " ") {
/> onClick(e);
{itemNode} }
</label> }}
)} />
</CSSTransition> {itemNode}
); </label>
})} )}
</TransitionGroup> </CSSTransition>
</ItemListContainer> );
</Box> })}
</TransitionGroup>
</ItemListContainer>
</Box>
)}
</ClassNames>
); );
} }
@ -273,7 +289,7 @@ function OutfitHeading({ outfitState, dispatchToOutfit }) {
* *
* See react-transition-group docs for more info! * See react-transition-group docs for more info!
*/ */
const fadeOutAndRollUpTransition = { const fadeOutAndRollUpTransition = (css) => ({
classNames: css` classNames: css`
&-exit { &-exit {
opacity: 1; opacity: 1;
@ -292,6 +308,6 @@ const fadeOutAndRollUpTransition = {
onExit: (e) => { onExit: (e) => {
e.style.height = e.offsetHeight + "px"; e.style.height = e.offsetHeight + "px";
}, },
}; });
export default ItemsPanel; export default ItemsPanel;

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css, cx } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { import {
Box, Box,
Button, Button,
@ -84,120 +84,126 @@ function OutfitControls({
}; };
return ( return (
<Box <ClassNames>
role="group" {({ css, cx }) => (
pos="absolute" <Box
left="0" role="group"
right="0" pos="absolute"
top="0" left="0"
bottom="0" right="0"
height="100%" // Required for Safari to size the grid correctly top="0"
padding={{ base: 2, lg: 6 }} bottom="0"
display="grid" height="100%" // Required for Safari to size the grid correctly
overflow="auto" padding={{ base: 2, lg: 6 }}
gridTemplateAreas={`"back play-pause sharing" display="grid"
overflow="auto"
gridTemplateAreas={`"back play-pause sharing"
"space space space" "space space space"
"picker picker picker"`} "picker picker picker"`}
gridTemplateRows="auto minmax(1rem, 1fr) auto" gridTemplateRows="auto minmax(1rem, 1fr) auto"
className={cx( className={cx(
css` css`
opacity: 0; opacity: 0;
transition: opacity 0.2s; transition: opacity 0.2s;
&:focus-within, &:focus-within,
&.focus-is-locked { &.focus-is-locked {
opacity: 1; opacity: 1;
} }
/* Ignore simulated hovers, only reveal for _real_ hovers. This helps /* Ignore simulated hovers, only reveal for _real_ hovers. This helps
* us avoid state conflicts with the focus-lock from clicks. */ * us avoid state conflicts with the focus-lock from clicks. */
@media (hover: hover) { @media (hover: hover) {
&:hover { &:hover {
opacity: 1; opacity: 1;
} }
} }
`, `,
focusIsLocked && "focus-is-locked" focusIsLocked && "focus-is-locked"
)} )}
onClickCapture={(e) => { onClickCapture={(e) => {
const opacity = parseFloat(getComputedStyle(e.currentTarget).opacity); const opacity = parseFloat(
if (opacity < 0.5) { getComputedStyle(e.currentTarget).opacity
// If the controls aren't visible right now, then clicks on them are );
// probably accidental. Ignore them! (We prevent default to block if (opacity < 0.5) {
// built-in behaviors like link nav, and we stop propagation to block // If the controls aren't visible right now, then clicks on them are
// our own custom click handlers. I don't know if I can prevent the // probably accidental. Ignore them! (We prevent default to block
// select clicks though?) // built-in behaviors like link nav, and we stop propagation to block
e.preventDefault(); // our own custom click handlers. I don't know if I can prevent the
e.stopPropagation(); // select clicks though?)
e.preventDefault();
e.stopPropagation();
// We also show the controls, by locking focus. We'll undo this when // We also show the controls, by locking focus. We'll undo this when
// the user taps elsewhere (because it will trigger a blur event from // the user taps elsewhere (because it will trigger a blur event from
// our child components), in `maybeUnlockFocus`. // our child components), in `maybeUnlockFocus`.
setFocusIsLocked(true); setFocusIsLocked(true);
} }
}} }}
> >
<Box gridArea="back" onClick={maybeUnlockFocus}> <Box gridArea="back" onClick={maybeUnlockFocus}>
<ControlButton <ControlButton
as={Link} as={Link}
to="/" to="/"
icon={<ArrowBackIcon />} icon={<ArrowBackIcon />}
aria-label="Leave this outfit" aria-label="Leave this outfit"
d="inline-flex" // Not sure why <a> requires this to style right! ^^` d="inline-flex" // Not sure why <a> requires this to style right! ^^`
/> />
</Box> </Box>
{showAnimationControls && ( {showAnimationControls && (
<Box gridArea="play-pause" display="flex" justifyContent="center"> <Box gridArea="play-pause" display="flex" justifyContent="center">
<DarkMode> <DarkMode>
<PlayPauseButton /> <PlayPauseButton />
</DarkMode> </DarkMode>
</Box>
)}
<Stack
gridArea="sharing"
alignSelf="flex-end"
spacing={{ base: "2", lg: "4" }}
align="flex-end"
onClick={maybeUnlockFocus}
>
<Box>
<DownloadButton outfitState={outfitState} />
</Box>
<Box>
<CopyLinkButton outfitState={outfitState} />
</Box>
</Stack>
<Box gridArea="space" onClick={maybeUnlockFocus} />
<Flex gridArea="picker" justify="center" onClick={maybeUnlockFocus}>
{/**
* We try to center the species/color picker, but the left spacer will
* shrink more than the pose picker container if we run out of space!
*/}
<Box flex="1 1 0" />
<Box flex="0 0 auto">
<DarkMode>
<SpeciesColorPicker
speciesId={outfitState.speciesId}
colorId={outfitState.colorId}
idealPose={outfitState.pose}
onChange={onSpeciesColorChange}
stateMustAlwaysBeValid
/>
</DarkMode>
</Box>
<Flex flex="1 1 0" align="center" pl="4">
<PosePicker
speciesId={outfitState.speciesId}
colorId={outfitState.colorId}
pose={outfitState.pose}
appearanceId={outfitState.appearanceId}
dispatchToOutfit={dispatchToOutfit}
onLockFocus={onLockFocus}
onUnlockFocus={onUnlockFocus}
/>
</Flex>
</Flex>
</Box> </Box>
)} )}
<Stack </ClassNames>
gridArea="sharing"
alignSelf="flex-end"
spacing={{ base: "2", lg: "4" }}
align="flex-end"
onClick={maybeUnlockFocus}
>
<Box>
<DownloadButton outfitState={outfitState} />
</Box>
<Box>
<CopyLinkButton outfitState={outfitState} />
</Box>
</Stack>
<Box gridArea="space" onClick={maybeUnlockFocus} />
<Flex gridArea="picker" justify="center" onClick={maybeUnlockFocus}>
{/**
* We try to center the species/color picker, but the left spacer will
* shrink more than the pose picker container if we run out of space!
*/}
<Box flex="1 1 0" />
<Box flex="0 0 auto">
<DarkMode>
<SpeciesColorPicker
speciesId={outfitState.speciesId}
colorId={outfitState.colorId}
idealPose={outfitState.pose}
onChange={onSpeciesColorChange}
stateMustAlwaysBeValid
/>
</DarkMode>
</Box>
<Flex flex="1 1 0" align="center" pl="4">
<PosePicker
speciesId={outfitState.speciesId}
colorId={outfitState.colorId}
pose={outfitState.pose}
appearanceId={outfitState.appearanceId}
dispatchToOutfit={dispatchToOutfit}
onLockFocus={onLockFocus}
onUnlockFocus={onUnlockFocus}
/>
</Flex>
</Flex>
</Box>
); );
} }
@ -277,55 +283,59 @@ function PlayPauseButton() {
}, [blinkInState, setBlinkInState]); }, [blinkInState, setBlinkInState]);
return ( return (
<> <ClassNames>
<PlayPauseButtonContent {({ css }) => (
isPaused={isPaused} <>
setIsPaused={setIsPaused}
marginTop="0.3rem" // to center-align with buttons (not sure on amt?)
ref={buttonRef}
/>
{blinkInState.type === "started" && (
<Portal>
<PlayPauseButtonContent <PlayPauseButtonContent
isPaused={isPaused} isPaused={isPaused}
setIsPaused={setIsPaused} setIsPaused={setIsPaused}
position="absolute" marginTop="0.3rem" // to center-align with buttons (not sure on amt?)
left={blinkInState.position.left} ref={buttonRef}
top={blinkInState.position.top}
backgroundColor="gray.600"
borderColor="gray.50"
color="gray.50"
onAnimationEnd={() => setBlinkInState({ type: "done" })}
// Don't disrupt the hover state of the controls! (And the button
// doesn't seem to click correctly, not sure why, but instead of
// debugging I'm adding this :p)
pointerEvents="none"
className={css`
@keyframes fade-in-out {
0% {
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
opacity: 0;
animation: fade-in-out 2s;
`}
/> />
</Portal> {blinkInState.type === "started" && (
<Portal>
<PlayPauseButtonContent
isPaused={isPaused}
setIsPaused={setIsPaused}
position="absolute"
left={blinkInState.position.left}
top={blinkInState.position.top}
backgroundColor="gray.600"
borderColor="gray.50"
color="gray.50"
onAnimationEnd={() => setBlinkInState({ type: "done" })}
// Don't disrupt the hover state of the controls! (And the button
// doesn't seem to click correctly, not sure why, but instead of
// debugging I'm adding this :p)
pointerEvents="none"
className={css`
@keyframes fade-in-out {
0% {
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
opacity: 0;
}
}
opacity: 0;
animation: fade-in-out 2s;
`}
/>
</Portal>
)}
</>
)} )}
</> </ClassNames>
); );
} }

View file

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useQuery } from "@apollo/client"; import { useQuery } from "@apollo/client";
import { css, cx } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { import {
Box, Box,
Button, Button,
@ -107,97 +107,101 @@ function PosePicker({
}; };
return ( return (
<Popover <ClassNames>
placement="bottom-end" {({ css, cx }) => (
returnFocusOnClose <Popover
onOpen={onLockFocus} placement="bottom-end"
onClose={onUnlockFocus} returnFocusOnClose
initialFocusRef={initialFocusRef} onOpen={onLockFocus}
> onClose={onUnlockFocus}
{({ isOpen }) => ( initialFocusRef={initialFocusRef}
<> >
<PopoverTrigger> {({ isOpen }) => (
<Button <>
variant="unstyled" <PopoverTrigger>
boxShadow="md" <Button
d="flex" variant="unstyled"
alignItems="center" boxShadow="md"
justifyContent="center" d="flex"
_focus={{ borderColor: "gray.50" }} alignItems="center"
_hover={{ borderColor: "gray.50" }} justifyContent="center"
outline="initial" _focus={{ borderColor: "gray.50" }}
className={cx( _hover={{ borderColor: "gray.50" }}
css` outline="initial"
border: 1px solid transparent !important; className={cx(
transition: border-color 0.2s !important; css`
border: 1px solid transparent !important;
transition: border-color 0.2s !important;
&:focus, &:focus,
&:hover, &:hover,
&.is-open { &.is-open {
border-color: ${theme.colors.gray["50"]} !important; border-color: ${theme.colors.gray["50"]} !important;
} }
&.is-open { &.is-open {
border-width: 2px !important; border-width: 2px !important;
} }
`, `,
isOpen && "is-open" isOpen && "is-open"
)} )}
> >
<EmojiImage src={getIcon(pose)} alt="Choose a pose" /> <EmojiImage src={getIcon(pose)} alt="Choose a pose" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<Portal> <Portal>
<PopoverContent> <PopoverContent>
<Box p="4" position="relative"> <Box p="4" position="relative">
{isInSupportMode ? ( {isInSupportMode ? (
<PosePickerSupport <PosePickerSupport
speciesId={speciesId} speciesId={speciesId}
colorId={colorId} colorId={colorId}
pose={pose} pose={pose}
appearanceId={appearanceId} appearanceId={appearanceId}
initialFocusRef={initialFocusRef} initialFocusRef={initialFocusRef}
dispatchToOutfit={dispatchToOutfit} dispatchToOutfit={dispatchToOutfit}
/> />
) : ( ) : (
<> <>
<PosePickerTable <PosePickerTable
poseInfos={poseInfos} poseInfos={poseInfos}
onChange={onChange} onChange={onChange}
initialFocusRef={initialFocusRef} initialFocusRef={initialFocusRef}
/> />
{numAvailablePoses <= 1 && ( {numAvailablePoses <= 1 && (
<SupportOnly> <SupportOnly>
<Box <Box
fontSize="xs" fontSize="xs"
fontStyle="italic" fontStyle="italic"
textAlign="center" textAlign="center"
opacity="0.7" opacity="0.7"
marginTop="2" marginTop="2"
> >
The empty picker is hidden for most users! The empty picker is hidden for most users!
<br /> <br />
You can see it because you're a Support user. You can see it because you're a Support user.
</Box> </Box>
</SupportOnly> </SupportOnly>
)}
</>
)} )}
</> <SupportOnly>
)} <Box position="absolute" top="5" left="3">
<SupportOnly> <PosePickerSupportSwitch
<Box position="absolute" top="5" left="3"> isChecked={isInSupportMode}
<PosePickerSupportSwitch onChange={(e) => setIsInSupportMode(e.target.checked)}
isChecked={isInSupportMode} />
onChange={(e) => setIsInSupportMode(e.target.checked)} </Box>
/> </SupportOnly>
</Box> </Box>
</SupportOnly> <PopoverArrow />
</Box> </PopoverContent>
<PopoverArrow /> </Portal>
</PopoverContent> </>
</Portal> )}
</> </Popover>
)} )}
</Popover> </ClassNames>
); );
} }
@ -343,110 +347,118 @@ function PoseOption({
); );
return ( return (
<Box <ClassNames>
as="label" {({ css, cx }) => (
cursor="pointer"
display="flex"
alignItems="center"
borderColor={poseInfo.isSelected ? borderColor : "gray.400"}
boxShadow={label ? "md" : "none"}
borderWidth={label ? "1px" : "0"}
borderRadius={label ? "full" : "0"}
paddingRight={label ? "3" : "0"}
onClick={(e) => {
// HACK: We need the timeout to beat the popover's focus stealing!
const input = e.currentTarget.querySelector("input");
setTimeout(() => input.focus(), 0);
}}
{...otherProps}
>
<VisuallyHidden
as="input"
type="radio"
aria-label={poseName}
name="pose"
value={poseInfo.pose}
checked={poseInfo.isSelected}
disabled={!poseInfo.isAvailable}
onChange={onChange}
ref={inputRef || null}
/>
<Box
aria-hidden
borderRadius="full"
boxShadow="md"
overflow="hidden"
width={size === "sm" ? "30px" : "50px"}
height={size === "sm" ? "30px" : "50px"}
title={
poseInfo.isAvailable
? // A lil debug output, so that we can quickly identify glitched
// PetStates and manually mark them as glitched!
window.location.hostname.includes("localhost") &&
`#${poseInfo.id}`
: "Not modeled yet"
}
position="relative"
className={css`
transform: scale(0.8);
opacity: 0.8;
transition: all 0.2s;
input:checked + & {
transform: scale(1);
opacity: 1;
}
`}
>
<Box <Box
borderRadius="full" as="label"
position="absolute" cursor="pointer"
top="0" display="flex"
bottom="0" alignItems="center"
left="0" borderColor={poseInfo.isSelected ? borderColor : "gray.400"}
right="0" boxShadow={label ? "md" : "none"}
zIndex="2" borderWidth={label ? "1px" : "0"}
className={cx( borderRadius={label ? "full" : "0"}
css` paddingRight={label ? "3" : "0"}
border: 0px solid ${borderColor}; onClick={(e) => {
transition: border-width 0.2s; // HACK: We need the timeout to beat the popover's focus stealing!
const input = e.currentTarget.querySelector("input");
&.not-available { setTimeout(() => input.focus(), 0);
border-color: ${theme.colors.gray["500"]}; }}
border-width: 1px; {...otherProps}
}
input:checked + * & {
border-width: 1px;
}
input:focus + * & {
border-width: 3px;
}
`,
!poseInfo.isAvailable && "not-available"
)}
/>
{poseInfo.isAvailable ? (
<Box width="100%" height="100%" transform={getTransform(poseInfo)}>
<OutfitLayers visibleLayers={getVisibleLayers(poseInfo, [])} />
</Box>
) : (
<Flex align="center" justify="center" width="100%" height="100%">
<EmojiImage src={twemojiQuestion} boxSize="24px" />
</Flex>
)}
</Box>
{label && (
<Box
marginLeft="2"
fontSize="xs"
fontWeight={poseInfo.isSelected ? "bold" : "normal"}
> >
{label} <VisuallyHidden
as="input"
type="radio"
aria-label={poseName}
name="pose"
value={poseInfo.pose}
checked={poseInfo.isSelected}
disabled={!poseInfo.isAvailable}
onChange={onChange}
ref={inputRef || null}
/>
<Box
aria-hidden
borderRadius="full"
boxShadow="md"
overflow="hidden"
width={size === "sm" ? "30px" : "50px"}
height={size === "sm" ? "30px" : "50px"}
title={
poseInfo.isAvailable
? // A lil debug output, so that we can quickly identify glitched
// PetStates and manually mark them as glitched!
window.location.hostname.includes("localhost") &&
`#${poseInfo.id}`
: "Not modeled yet"
}
position="relative"
className={css`
transform: scale(0.8);
opacity: 0.8;
transition: all 0.2s;
input:checked + & {
transform: scale(1);
opacity: 1;
}
`}
>
<Box
borderRadius="full"
position="absolute"
top="0"
bottom="0"
left="0"
right="0"
zIndex="2"
className={cx(
css`
border: 0px solid ${borderColor};
transition: border-width 0.2s;
&.not-available {
border-color: ${theme.colors.gray["500"]};
border-width: 1px;
}
input:checked + * & {
border-width: 1px;
}
input:focus + * & {
border-width: 3px;
}
`,
!poseInfo.isAvailable && "not-available"
)}
/>
{poseInfo.isAvailable ? (
<Box
width="100%"
height="100%"
transform={getTransform(poseInfo)}
>
<OutfitLayers visibleLayers={getVisibleLayers(poseInfo, [])} />
</Box>
) : (
<Flex align="center" justify="center" width="100%" height="100%">
<EmojiImage src={twemojiQuestion} boxSize="24px" />
</Flex>
)}
</Box>
{label && (
<Box
marginLeft="2"
fontSize="xs"
fontWeight={poseInfo.isSelected ? "bold" : "normal"}
>
{label}
</Box>
)}
</Box> </Box>
)} )}
</Box> </ClassNames>
); );
} }

View file

@ -12,7 +12,7 @@ import {
useColorModeValue, useColorModeValue,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { CloseIcon, SearchIcon } from "@chakra-ui/icons"; import { CloseIcon, SearchIcon } from "@chakra-ui/icons";
import { css, cx } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import Autosuggest from "react-autosuggest"; import Autosuggest from "react-autosuggest";
/** /**
@ -79,23 +79,27 @@ function SearchToolbar({
({ containerProps, children }) => { ({ containerProps, children }) => {
const { className, ...otherContainerProps } = containerProps; const { className, ...otherContainerProps } = containerProps;
return ( return (
<Box <ClassNames>
{...otherContainerProps} {({ css, cx }) => (
borderBottomRadius="md" <Box
boxShadow="md" {...otherContainerProps}
overflow="hidden" borderBottomRadius="md"
transition="all 0.4s" boxShadow="md"
className={cx( overflow="hidden"
className, transition="all 0.4s"
css` className={cx(
li { className,
list-style: none; css`
} li {
` list-style: none;
}
`
)}
>
{children}
</Box>
)} )}
> </ClassNames>
{children}
</Box>
); );
}, },
[] []
@ -111,108 +115,116 @@ function SearchToolbar({
const focusBorderColor = useColorModeValue("green.600", "green.400"); const focusBorderColor = useColorModeValue("green.600", "green.400");
return ( return (
<Autosuggest <ClassNames>
suggestions={suggestions} {({ css }) => (
onSuggestionsFetchRequested={({ value }) => <Autosuggest
setSuggestions(getSuggestions(value, query, zoneLabels)) suggestions={suggestions}
} onSuggestionsFetchRequested={({ value }) =>
onSuggestionsClearRequested={() => setSuggestions([])} setSuggestions(getSuggestions(value, query, zoneLabels))
onSuggestionSelected={(e, { suggestion }) => {
const valueWithoutLastWord = query.value.match(/^(.*?)\s*\S+$/)[1];
onChange({
...query,
value: valueWithoutLastWord,
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
});
}}
getSuggestionValue={(zl) => zl}
highlightFirstSuggestion={true}
renderSuggestion={renderSuggestion}
renderSuggestionsContainer={renderSuggestionsContainer}
renderInputComponent={(props) => (
<InputGroup>
{queryFilterText ? (
<InputLeftAddon>
<SearchIcon color="gray.400" marginRight="3" />
<Box fontSize="sm">{queryFilterText}</Box>
</InputLeftAddon>
) : (
<InputLeftElement>
<SearchIcon color="gray.400" />
</InputLeftElement>
)}
<Input {...props} />
{(query.value || queryFilterText) && (
<InputRightElement>
<IconButton
icon={<CloseIcon />}
color="gray.400"
variant="ghost"
colorScheme="green"
aria-label="Clear search"
onClick={() => {
onChange(null);
}}
// Big style hacks here!
height="calc(100% - 2px)"
marginRight="2px"
/>
</InputRightElement>
)}
</InputGroup>
)}
inputProps={{
// placeholder: "Search for items to add…",
"aria-label": "Search for items to add…",
focusBorderColor: focusBorderColor,
value: query.value || "",
ref: searchQueryRef,
minWidth: 0,
borderBottomRadius: suggestions.length > 0 ? "0" : "md",
// HACK: Chakra isn't noticing the InputLeftElement swapping out
// for the InputLeftAddon, so the styles aren't updating...
// Hard override!
className: css`
padding-left: ${queryFilterText ? "1rem" : "2.5rem"} !important;
border-bottom-left-radius: ${queryFilterText
? "0"
: "0.25rem"} !important;
border-top-left-radius: ${queryFilterText
? "0"
: "0.25rem"} !important;
`,
onChange: (e, { newValue, method }) => {
// The Autosuggest tries to change the _entire_ value of the element
// when navigating suggestions, which isn't actually what we want.
// Only accept value changes that are typed by the user!
if (method === "type") {
onChange({ ...query, value: newValue });
} }
}, onSuggestionsClearRequested={() => setSuggestions([])}
onKeyDown: (e) => { onSuggestionSelected={(e, { suggestion }) => {
if (e.key === "Escape") { const valueWithoutLastWord = query.value.match(/^(.*?)\s*\S+$/)[1];
if (suggestions.length > 0) {
setSuggestions([]);
return;
}
onChange(null);
e.target.blur();
} else if (e.key === "ArrowDown") {
if (suggestions.length > 0) {
return;
}
onMoveFocusDownToResults(e);
} else if (e.key === "Backspace" && e.target.selectionStart === 0) {
onChange({ onChange({
...query, ...query,
filterToItemKind: null, value: valueWithoutLastWord,
filterToZoneLabel: null, filterToZoneLabel:
suggestion.zoneLabel || query.filterToZoneLabel,
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
}); });
} }}
}, getSuggestionValue={(zl) => zl}
}} highlightFirstSuggestion={true}
/> renderSuggestion={renderSuggestion}
renderSuggestionsContainer={renderSuggestionsContainer}
renderInputComponent={(props) => (
<InputGroup>
{queryFilterText ? (
<InputLeftAddon>
<SearchIcon color="gray.400" marginRight="3" />
<Box fontSize="sm">{queryFilterText}</Box>
</InputLeftAddon>
) : (
<InputLeftElement>
<SearchIcon color="gray.400" />
</InputLeftElement>
)}
<Input {...props} />
{(query.value || queryFilterText) && (
<InputRightElement>
<IconButton
icon={<CloseIcon />}
color="gray.400"
variant="ghost"
colorScheme="green"
aria-label="Clear search"
onClick={() => {
onChange(null);
}}
// Big style hacks here!
height="calc(100% - 2px)"
marginRight="2px"
/>
</InputRightElement>
)}
</InputGroup>
)}
inputProps={{
// placeholder: "Search for items to add…",
"aria-label": "Search for items to add…",
focusBorderColor: focusBorderColor,
value: query.value || "",
ref: searchQueryRef,
minWidth: 0,
borderBottomRadius: suggestions.length > 0 ? "0" : "md",
// HACK: Chakra isn't noticing the InputLeftElement swapping out
// for the InputLeftAddon, so the styles aren't updating...
// Hard override!
className: css`
padding-left: ${queryFilterText ? "1rem" : "2.5rem"} !important;
border-bottom-left-radius: ${queryFilterText
? "0"
: "0.25rem"} !important;
border-top-left-radius: ${queryFilterText
? "0"
: "0.25rem"} !important;
`,
onChange: (e, { newValue, method }) => {
// The Autosuggest tries to change the _entire_ value of the element
// when navigating suggestions, which isn't actually what we want.
// Only accept value changes that are typed by the user!
if (method === "type") {
onChange({ ...query, value: newValue });
}
},
onKeyDown: (e) => {
if (e.key === "Escape") {
if (suggestions.length > 0) {
setSuggestions([]);
return;
}
onChange(null);
e.target.blur();
} else if (e.key === "ArrowDown") {
if (suggestions.length > 0) {
return;
}
onMoveFocusDownToResults(e);
} else if (
e.key === "Backspace" &&
e.target.selectionStart === 0
) {
onChange({
...query,
filterToItemKind: null,
filterToZoneLabel: null,
});
}
},
}}
/>
)}
</ClassNames>
); );
} }

View file

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import gql from "graphql-tag"; import gql from "graphql-tag";
import { useQuery, useMutation } from "@apollo/client"; import { useQuery, useMutation } from "@apollo/client";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { import {
Badge, Badge,
Box, Box,
@ -454,71 +454,75 @@ function ItemSupportAppearanceLayer({
const iconButtonColor = useColorModeValue("green.800", "gray.900"); const iconButtonColor = useColorModeValue("green.800", "gray.900");
return ( return (
<Box <ClassNames>
as="button" {({ css }) => (
width="150px"
textAlign="center"
fontSize="xs"
onClick={onOpen}
>
<Box
width="150px"
height="150px"
marginBottom="1"
boxShadow="md"
borderRadius="md"
position="relative"
>
<OutfitLayers visibleLayers={[...biologyLayers, itemLayer]} />
<Box <Box
className={css` as="button"
opacity: 0; width="150px"
transition: opacity 0.2s; textAlign="center"
fontSize="xs"
onClick={onOpen}
>
<Box
width="150px"
height="150px"
marginBottom="1"
boxShadow="md"
borderRadius="md"
position="relative"
>
<OutfitLayers visibleLayers={[...biologyLayers, itemLayer]} />
<Box
className={css`
opacity: 0;
transition: opacity 0.2s;
button:hover &, button:hover &,
button:focus & { button:focus & {
opacity: 1; opacity: 1;
} }
/* On touch devices, always show the icon, to clarify that this is /* On touch devices, always show the icon, to clarify that this is
* an interactable object! (Whereas I expect other devices to * an interactable object! (Whereas I expect other devices to
* discover things by exploratory hover or focus!) */ * discover things by exploratory hover or focus!) */
@media (hover: none) { @media (hover: none) {
opacity: 1; opacity: 1;
} }
`} `}
background={iconButtonBgColor} background={iconButtonBgColor}
color={iconButtonColor} color={iconButtonColor}
borderRadius="full" borderRadius="full"
boxShadow="sm" boxShadow="sm"
position="absolute" position="absolute"
bottom="2" bottom="2"
right="2" right="2"
padding="2" padding="2"
alignItems="center" alignItems="center"
justifyContent="center" justifyContent="center"
width="32px" width="32px"
height="32px" height="32px"
> >
<EditIcon <EditIcon
boxSize="16px" boxSize="16px"
position="relative" position="relative"
top="-2px" top="-2px"
right="-1px" right="-1px"
/>
</Box>
</Box>
<Box fontWeight="bold">{itemLayer.zone.label}</Box>
<Box>Zone ID: {itemLayer.zone.id}</Box>
<Box>DTI ID: {itemLayer.id}</Box>
<ItemLayerSupportModal
item={item}
itemLayer={itemLayer}
outfitState={outfitState}
isOpen={isOpen}
onClose={onClose}
/> />
</Box> </Box>
</Box> )}
<Box fontWeight="bold">{itemLayer.zone.label}</Box> </ClassNames>
<Box>Zone ID: {itemLayer.zone.id}</Box>
<Box>DTI ID: {itemLayer.id}</Box>
<ItemLayerSupportModal
item={item}
itemLayer={itemLayer}
outfitState={outfitState}
isOpen={isOpen}
onClose={onClose}
/>
</Box>
); );
} }

View file

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { Box, useColorModeValue } from "@chakra-ui/react"; import { Box, useColorModeValue } from "@chakra-ui/react";
import { createIcon } from "@chakra-ui/icons"; import { createIcon } from "@chakra-ui/icons";
@ -21,74 +21,76 @@ function HangerSpinner({ size = "md", ...props }) {
const color = useColorModeValue("green.500", "green.300"); const color = useColorModeValue("green.500", "green.300");
return ( return (
<> <ClassNames>
<Box {({ css }) => (
className={css` <Box
/* className={css`
Adapted from animate.css "swing". We spend 75% of the time swinging, /*
then 25% of the time pausing before the next loop. Adapted from animate.css "swing". We spend 75% of the time swinging,
then 25% of the time pausing before the next loop.
We use this animation for folks who are okay with dizzy-ish motion. We use this animation for folks who are okay with dizzy-ish motion.
For reduced motion, we use a pulse-fade instead. For reduced motion, we use a pulse-fade instead.
*/ */
@keyframes swing { @keyframes swing {
15% { 15% {
transform: rotate3d(0, 0, 1, 15deg); transform: rotate3d(0, 0, 1, 15deg);
}
30% {
transform: rotate3d(0, 0, 1, -10deg);
}
45% {
transform: rotate3d(0, 0, 1, 5deg);
}
60% {
transform: rotate3d(0, 0, 1, -5deg);
}
75% {
transform: rotate3d(0, 0, 1, 0deg);
}
100% {
transform: rotate3d(0, 0, 1, 0deg);
}
} }
30% { /*
transform: rotate3d(0, 0, 1, -10deg); A homebrew fade-pulse animation. We use this for folks who don't
like motion. It's an important accessibility thing!
*/
@keyframes fade-pulse {
0% {
opacity: 0.2;
}
50% {
opacity: 1;
}
100% {
opacity: 0.2;
}
} }
45% { @media (prefers-reduced-motion: no-preference) {
transform: rotate3d(0, 0, 1, 5deg); animation: 1.2s infinite swing;
transform-origin: top center;
} }
60% { @media (prefers-reduced-motion: reduce) {
transform: rotate3d(0, 0, 1, -5deg); animation: 1.6s infinite fade-pulse;
} }
`}
75% { {...props}
transform: rotate3d(0, 0, 1, 0deg); >
} <HangerIcon boxSize={boxSize} color={color} transition="color 0.2s" />
</Box>
100% { )}
transform: rotate3d(0, 0, 1, 0deg); </ClassNames>
}
}
/*
A homebrew fade-pulse animation. We use this for folks who don't
like motion. It's an important accessibility thing!
*/
@keyframes fade-pulse {
0% {
opacity: 0.2;
}
50% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion: no-preference) {
animation: 1.2s infinite swing;
transform-origin: top center;
}
@media (prefers-reduced-motion: reduce) {
animation: 1.6s infinite fade-pulse;
}
`}
{...props}
>
<HangerIcon boxSize={boxSize} color={color} transition="color 0.2s" />
</Box>
</>
); );
} }

View file

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { import {
Badge, Badge,
Box, Box,
@ -102,59 +102,63 @@ export function ItemThumbnail({
); );
return ( return (
<Box <ClassNames>
width={size === "lg" ? "80px" : "50px"} {({ css }) => (
height={size === "lg" ? "80px" : "50px"}
transition="all 0.15s"
transformOrigin="center"
position="relative"
className={css([
{
transform: "scale(0.8)",
},
!isDisabled &&
!isActive && {
[focusSelector]: {
opacity: "0.9",
transform: "scale(0.9)",
},
},
!isDisabled &&
isActive && {
opacity: 1,
transform: "none",
},
])}
{...props}
>
<Box
borderRadius="lg"
boxShadow="md"
border="1px"
overflow="hidden"
width="100%"
height="100%"
className={css([
{
borderColor: `${borderColor} !important`,
},
!isDisabled &&
!isActive && {
[focusSelector]: {
borderColor: `${focusBorderColor} !important`,
},
},
])}
>
<Box <Box
as="img" width={size === "lg" ? "80px" : "50px"}
width="100%" height={size === "lg" ? "80px" : "50px"}
height="100%" transition="all 0.15s"
src={safeImageUrl(item.thumbnailUrl)} transformOrigin="center"
alt={`Thumbnail art for ${item.name}`} position="relative"
/> className={css([
</Box> {
</Box> transform: "scale(0.8)",
},
!isDisabled &&
!isActive && {
[focusSelector]: {
opacity: "0.9",
transform: "scale(0.9)",
},
},
!isDisabled &&
isActive && {
opacity: 1,
transform: "none",
},
])}
{...props}
>
<Box
borderRadius="lg"
boxShadow="md"
border="1px"
overflow="hidden"
width="100%"
height="100%"
className={css([
{
borderColor: `${borderColor} !important`,
},
!isDisabled &&
!isActive && {
[focusSelector]: {
borderColor: `${focusBorderColor} !important`,
},
},
])}
>
<Box
as="img"
width="100%"
height="100%"
src={safeImageUrl(item.thumbnailUrl)}
alt={`Thumbnail art for ${item.name}`}
/>
</Box>
</Box>
)}
</ClassNames>
); );
} }
@ -166,30 +170,34 @@ function ItemName({ children, isDisabled, focusSelector, ...props }) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Box <ClassNames>
fontSize="md" {({ css }) => (
transition="all 0.15s" <Box
overflow="hidden" fontSize="md"
whiteSpace="nowrap" transition="all 0.15s"
textOverflow="ellipsis" overflow="hidden"
className={ whiteSpace="nowrap"
!isDisabled && textOverflow="ellipsis"
css` className={
${focusSelector} { !isDisabled &&
opacity: 0.9; css`
font-weight: ${theme.fontWeights.medium}; ${focusSelector} {
} opacity: 0.9;
font-weight: ${theme.fontWeights.medium};
}
input:checked + .item-container & { input:checked + .item-container & {
opacity: 1; opacity: 1;
font-weight: ${theme.fontWeights.bold}; font-weight: ${theme.fontWeights.bold};
}
`
} }
` {...props}
} >
{...props} {children}
> </Box>
{children} )}
</Box> </ClassNames>
); );
} }

View file

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Box, DarkMode, Flex, Text } from "@chakra-ui/react"; import { Box, DarkMode, Flex, Text } from "@chakra-ui/react";
import { WarningIcon } from "@chakra-ui/icons"; import { WarningIcon } from "@chakra-ui/icons";
import { css } from "@emotion/css"; import { ClassNames } from "@emotion/react";
import { CSSTransition, TransitionGroup } from "react-transition-group"; import { CSSTransition, TransitionGroup } from "react-transition-group";
import OutfitMovieLayer, { import OutfitMovieLayer, {
@ -149,104 +149,113 @@ export function OutfitLayers({
}, [setCanvasSize]); }, [setCanvasSize]);
return ( return (
<Box <ClassNames>
pos="relative" {({ css }) => (
height="100%" <Box
width="100%" pos="relative"
// Create a stacking context, so the z-indexed layers don't escape! height="100%"
zIndex="0" width="100%"
ref={containerRef} // Create a stacking context, so the z-indexed layers don't escape!
> zIndex="0"
{placeholder && ( ref={containerRef}
<FullScreenCenter> >
<Box {placeholder && (
// We show the placeholder until there are visible layers, at which <FullScreenCenter>
// point we fade it out. <Box
opacity={visibleLayers.length === 0 ? 1 : 0} // We show the placeholder until there are visible layers, at which
// point we fade it out.
opacity={visibleLayers.length === 0 ? 1 : 0}
transition="opacity 0.2s"
>
{placeholder}
</Box>
</FullScreenCenter>
)}
<TransitionGroup enter={false} exit={doTransitions}>
{visibleLayers.map((layer) => (
<CSSTransition
// We manage the fade-in and fade-out separately! The fade-out
// happens here, when the layer exits the DOM.
key={layer.id}
classNames={css`
&-exit {
opacity: 1;
}
&-exit-active {
opacity: 0;
transition: opacity 0.2s;
}
`}
timeout={200}
>
<FadeInOnLoad as={FullScreenCenter} zIndex={layer.zone.depth}>
{layer.canvasMovieLibraryUrl ? (
<OutfitMovieLayer
libraryUrl={layer.canvasMovieLibraryUrl}
width={canvasSize}
height={canvasSize}
isPaused={isPaused}
/>
) : (
<Box
as="img"
src={getBestImageUrlForLayer(layer).src}
// The crossOrigin prop isn't strictly necessary for loading
// here (<img> tags are always allowed through CORS), but
// this means we make the same request that the Download
// button makes, so it can use the cached version of this
// image instead of requesting it again with crossOrigin!
crossOrigin={getBestImageUrlForLayer(layer).crossOrigin}
alt=""
objectFit="contain"
maxWidth="100%"
maxHeight="100%"
/>
)}
</FadeInOnLoad>
</CSSTransition>
))}
</TransitionGroup>
<FullScreenCenter
zIndex="9000"
// This is similar to our Delay util component, but Delay disappears
// immediately on load, whereas we want this to fade out smoothly. We
// also use a timeout to delay the fade-in by 0.5s, but don't delay the
// fade-out at all. (The timeout was an awkward choice, it was hard to
// find a good CSS way to specify this delay well!)
opacity={loadingAnything && loadingDelayHasPassed ? 1 : 0}
transition="opacity 0.2s" transition="opacity 0.2s"
> >
{placeholder} {spinnerVariant === "overlay" && (
</Box> <>
</FullScreenCenter>
)}
<TransitionGroup enter={false} exit={doTransitions}>
{visibleLayers.map((layer) => (
<CSSTransition
// We manage the fade-in and fade-out separately! The fade-out
// happens here, when the layer exits the DOM.
key={layer.id}
classNames={css`
&-exit {
opacity: 1;
}
&-exit-active {
opacity: 0;
transition: opacity 0.2s;
}
`}
timeout={200}
>
<FadeInOnLoad as={FullScreenCenter} zIndex={layer.zone.depth}>
{layer.canvasMovieLibraryUrl ? (
<OutfitMovieLayer
libraryUrl={layer.canvasMovieLibraryUrl}
width={canvasSize}
height={canvasSize}
isPaused={isPaused}
/>
) : (
<Box <Box
as="img" position="absolute"
src={getBestImageUrlForLayer(layer).src} top="0"
// The crossOrigin prop isn't strictly necessary for loading left="0"
// here (<img> tags are always allowed through CORS), but right="0"
// this means we make the same request that the Download bottom="0"
// button makes, so it can use the cached version of this backgroundColor="gray.900"
// image instead of requesting it again with crossOrigin! opacity="0.7"
crossOrigin={getBestImageUrlForLayer(layer).crossOrigin}
alt=""
objectFit="contain"
maxWidth="100%"
maxHeight="100%"
/> />
)} {/* Against the dark overlay, use the Dark Mode spinner. */}
</FadeInOnLoad> <DarkMode>
</CSSTransition> <HangerSpinner />
))} </DarkMode>
</TransitionGroup> </>
<FullScreenCenter )}
zIndex="9000" {spinnerVariant === "corner" && (
// This is similar to our Delay util component, but Delay disappears <HangerSpinner
// immediately on load, whereas we want this to fade out smoothly. We size="sm"
// also use a timeout to delay the fade-in by 0.5s, but don't delay the position="absolute"
// fade-out at all. (The timeout was an awkward choice, it was hard to bottom="2"
// find a good CSS way to specify this delay well!) right="2"
opacity={loadingAnything && loadingDelayHasPassed ? 1 : 0} />
transition="opacity 0.2s" )}
> </FullScreenCenter>
{spinnerVariant === "overlay" && ( </Box>
<> )}
<Box </ClassNames>
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
backgroundColor="gray.900"
opacity="0.7"
/>
{/* Against the dark overlay, use the Dark Mode spinner. */}
<DarkMode>
<HangerSpinner />
</DarkMode>
</>
)}
{spinnerVariant === "corner" && (
<HangerSpinner size="sm" position="absolute" bottom="2" right="2" />
)}
</FullScreenCenter>
</Box>
); );
} }

View file

@ -2750,17 +2750,6 @@
"@emotion/weak-memoize" "^0.2.5" "@emotion/weak-memoize" "^0.2.5"
stylis "^4.0.3" stylis "^4.0.3"
"@emotion/css@^11.1.3":
version "11.1.3"
resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.1.3.tgz#9ed44478b19e5d281ccbbd46d74d123d59be793f"
integrity sha512-RSQP59qtCNTf5NWD6xM08xsQdCZmVYnX/panPYvB6LQAPKQB6GL49Njf0EMbS3CyDtrlWsBcmqBtysFvfWT3rA==
dependencies:
"@emotion/babel-plugin" "^11.0.0"
"@emotion/cache" "^11.1.3"
"@emotion/serialize" "^1.0.0"
"@emotion/sheet" "^1.0.0"
"@emotion/utils" "^1.0.0"
"@emotion/hash@^0.8.0": "@emotion/hash@^0.8.0":
version "0.8.0" version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"