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/react": "^1.0.4",
"@chakra-ui/theme-tools": "^1.0.2",
"@emotion/css": "^11.1.3",
"@emotion/react": "^11.1.4",
"@emotion/styled": "^11.0.0",
"@loadable/component": "^5.12.0",

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import React from "react";
import { css } from "@emotion/css";
import { ClassNames } from "@emotion/react";
import { Box, Skeleton, useColorModeValue, useToken } from "@chakra-ui/react";
import gql from "graphql-tag";
import { useQuery } from "@apollo/client";
@ -165,84 +165,88 @@ function ItemTradesTable({
};
return (
<Box
as="table"
width="100%"
boxShadow="md"
className={css`
/* Chakra doesn't have props for these! */
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}>
{/* A small wording tweak to fit better on the xsmall screens! */}
<Box display={{ base: "none", sm: "block" }}>Last active</Box>
<Box display={{ base: "block", sm: "none" }}>Last edit</Box>
</ItemTradesTableCell>
{shouldShowCompareColumn && (
<ItemTradesTableCell as="th" width={minorColumnWidth}>
<Box display={{ base: "none", sm: "block" }}>
{compareColumnLabel}
</Box>
<Box display={{ base: "block", sm: "none" }}>Matches</Box>
</ItemTradesTableCell>
)}
<ItemTradesTableCell as="th" width={minorColumnWidth}>
{userHeading}
</ItemTradesTableCell>
<ItemTradesTableCell as="th">List</ItemTradesTableCell>
</Box>
</Box>
<Box as="tbody">
{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>
<ClassNames>
{({ css }) => (
<Box
as="table"
width="100%"
boxShadow="md"
className={css`
/* Chakra doesn't have props for these! */
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}>
{/* A small wording tweak to fit better on the xsmall screens! */}
<Box display={{ base: "none", sm: "block" }}>Last active</Box>
<Box display={{ base: "block", sm: "none" }}>Last edit</Box>
</ItemTradesTableCell>
{shouldShowCompareColumn && (
<ItemTradesTableCell as="th" width={minorColumnWidth}>
<Box display={{ base: "none", sm: "block" }}>
{compareColumnLabel}
</Box>
<Box display={{ base: "block", sm: "none" }}>Matches</Box>
</ItemTradesTableCell>
)}
<ItemTradesTableCell as="th" width={minorColumnWidth}>
{userHeading}
</ItemTradesTableCell>
<ItemTradesTableCell as="th">List</ItemTradesTableCell>
</Box>
</Box>
)}
</Box>
</Box>
<Box as="tbody">
{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>
)}
</ClassNames>
);
}
@ -263,63 +267,67 @@ function ItemTradesTableRow({
);
return (
<Box
as="tr"
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">
<ClassNames>
{({ css }) => (
<Box
as={Link}
to={href}
className={css`
&:hover,
&:focus,
tr:hover &,
tr:focus-within & {
text-decoration: underline;
}
`}
as="tr"
cursor="pointer"
_hover={{ background: focusBackground }}
_focusWithin={{ background: focusBackground }}
onClick={onClick}
>
{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>
</ItemTradesTableCell>
</Box>
)}
</ClassNames>
);
}
@ -350,47 +358,51 @@ function ItemTradesTableCell({ children, as = "td", ...props }) {
const borderRadiusCss = useToken("radii", "md");
return (
<Box
as={as}
paddingX="4"
paddingY="2"
textAlign="left"
className={css`
/* Lol sigh, getting this right is way more involved than I wish it
<ClassNames>
{({ css }) => (
<Box
as={as}
paddingX="4"
paddingY="2"
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,
* 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
* top or left border; and then target the exact 4 corner cells to
* round them. Pretty old-school tbh 🙃 */
border-bottom: 1px solid ${borderColorCss};
border-right: 1px solid ${borderColorCss};
border-bottom: 1px solid ${borderColorCss};
border-right: 1px solid ${borderColorCss};
thead tr:first-of-type & {
border-top: 1px solid ${borderColorCss};
}
thead tr:first-of-type & {
border-top: 1px solid ${borderColorCss};
}
&:first-of-type {
border-left: 1px solid ${borderColorCss};
}
&:first-of-type {
border-left: 1px solid ${borderColorCss};
}
thead tr:first-of-type &:first-of-type {
border-top-left-radius: ${borderRadiusCss};
}
thead tr:first-of-type &:last-of-type {
border-top-right-radius: ${borderRadiusCss};
}
tbody tr:last-of-type &:first-of-type {
border-bottom-left-radius: ${borderRadiusCss};
}
tbody tr:last-of-type &:last-of-type {
border-bottom-right-radius: ${borderRadiusCss};
}
`}
{...props}
>
{children}
</Box>
thead tr:first-of-type &:first-of-type {
border-top-left-radius: ${borderRadiusCss};
}
thead tr:first-of-type &:last-of-type {
border-top-right-radius: ${borderRadiusCss};
}
tbody tr:last-of-type &:first-of-type {
border-bottom-left-radius: ${borderRadiusCss};
}
tbody tr:last-of-type &:last-of-type {
border-bottom-right-radius: ${borderRadiusCss};
}
`}
{...props}
>
{children}
</Box>
)}
</ClassNames>
);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
import React from "react";
import { css, cx } from "@emotion/css";
import { ClassNames } from "@emotion/react";
import {
Box,
Button,
@ -84,120 +84,126 @@ function OutfitControls({
};
return (
<Box
role="group"
pos="absolute"
left="0"
right="0"
top="0"
bottom="0"
height="100%" // Required for Safari to size the grid correctly
padding={{ base: 2, lg: 6 }}
display="grid"
overflow="auto"
gridTemplateAreas={`"back play-pause sharing"
<ClassNames>
{({ css, cx }) => (
<Box
role="group"
pos="absolute"
left="0"
right="0"
top="0"
bottom="0"
height="100%" // Required for Safari to size the grid correctly
padding={{ base: 2, lg: 6 }}
display="grid"
overflow="auto"
gridTemplateAreas={`"back play-pause sharing"
"space space space"
"picker picker picker"`}
gridTemplateRows="auto minmax(1rem, 1fr) auto"
className={cx(
css`
opacity: 0;
transition: opacity 0.2s;
gridTemplateRows="auto minmax(1rem, 1fr) auto"
className={cx(
css`
opacity: 0;
transition: opacity 0.2s;
&:focus-within,
&.focus-is-locked {
opacity: 1;
}
&:focus-within,
&.focus-is-locked {
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. */
@media (hover: hover) {
&:hover {
opacity: 1;
}
}
`,
focusIsLocked && "focus-is-locked"
)}
onClickCapture={(e) => {
const opacity = parseFloat(getComputedStyle(e.currentTarget).opacity);
if (opacity < 0.5) {
// If the controls aren't visible right now, then clicks on them are
// probably accidental. Ignore them! (We prevent default to block
// built-in behaviors like link nav, and we stop propagation to block
// our own custom click handlers. I don't know if I can prevent the
// select clicks though?)
e.preventDefault();
e.stopPropagation();
@media (hover: hover) {
&:hover {
opacity: 1;
}
}
`,
focusIsLocked && "focus-is-locked"
)}
onClickCapture={(e) => {
const opacity = parseFloat(
getComputedStyle(e.currentTarget).opacity
);
if (opacity < 0.5) {
// If the controls aren't visible right now, then clicks on them are
// probably accidental. Ignore them! (We prevent default to block
// built-in behaviors like link nav, and we stop propagation to block
// our own custom click handlers. I don't know if I can prevent the
// select clicks though?)
e.preventDefault();
e.stopPropagation();
// 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
// our child components), in `maybeUnlockFocus`.
setFocusIsLocked(true);
}
}}
>
<Box gridArea="back" onClick={maybeUnlockFocus}>
<ControlButton
as={Link}
to="/"
icon={<ArrowBackIcon />}
aria-label="Leave this outfit"
d="inline-flex" // Not sure why <a> requires this to style right! ^^`
/>
</Box>
{showAnimationControls && (
<Box gridArea="play-pause" display="flex" justifyContent="center">
<DarkMode>
<PlayPauseButton />
</DarkMode>
// 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
// our child components), in `maybeUnlockFocus`.
setFocusIsLocked(true);
}
}}
>
<Box gridArea="back" onClick={maybeUnlockFocus}>
<ControlButton
as={Link}
to="/"
icon={<ArrowBackIcon />}
aria-label="Leave this outfit"
d="inline-flex" // Not sure why <a> requires this to style right! ^^`
/>
</Box>
{showAnimationControls && (
<Box gridArea="play-pause" display="flex" justifyContent="center">
<DarkMode>
<PlayPauseButton />
</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>
)}
<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>
</ClassNames>
);
}
@ -277,55 +283,59 @@ function PlayPauseButton() {
}, [blinkInState, setBlinkInState]);
return (
<>
<PlayPauseButtonContent
isPaused={isPaused}
setIsPaused={setIsPaused}
marginTop="0.3rem" // to center-align with buttons (not sure on amt?)
ref={buttonRef}
/>
{blinkInState.type === "started" && (
<Portal>
<ClassNames>
{({ css }) => (
<>
<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;
`}
marginTop="0.3rem" // to center-align with buttons (not sure on amt?)
ref={buttonRef}
/>
</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 gql from "graphql-tag";
import { useQuery } from "@apollo/client";
import { css, cx } from "@emotion/css";
import { ClassNames } from "@emotion/react";
import {
Box,
Button,
@ -107,97 +107,101 @@ function PosePicker({
};
return (
<Popover
placement="bottom-end"
returnFocusOnClose
onOpen={onLockFocus}
onClose={onUnlockFocus}
initialFocusRef={initialFocusRef}
>
{({ isOpen }) => (
<>
<PopoverTrigger>
<Button
variant="unstyled"
boxShadow="md"
d="flex"
alignItems="center"
justifyContent="center"
_focus={{ borderColor: "gray.50" }}
_hover={{ borderColor: "gray.50" }}
outline="initial"
className={cx(
css`
border: 1px solid transparent !important;
transition: border-color 0.2s !important;
<ClassNames>
{({ css, cx }) => (
<Popover
placement="bottom-end"
returnFocusOnClose
onOpen={onLockFocus}
onClose={onUnlockFocus}
initialFocusRef={initialFocusRef}
>
{({ isOpen }) => (
<>
<PopoverTrigger>
<Button
variant="unstyled"
boxShadow="md"
d="flex"
alignItems="center"
justifyContent="center"
_focus={{ borderColor: "gray.50" }}
_hover={{ borderColor: "gray.50" }}
outline="initial"
className={cx(
css`
border: 1px solid transparent !important;
transition: border-color 0.2s !important;
&:focus,
&:hover,
&.is-open {
border-color: ${theme.colors.gray["50"]} !important;
}
&:focus,
&:hover,
&.is-open {
border-color: ${theme.colors.gray["50"]} !important;
}
&.is-open {
border-width: 2px !important;
}
`,
isOpen && "is-open"
)}
>
<EmojiImage src={getIcon(pose)} alt="Choose a pose" />
</Button>
</PopoverTrigger>
<Portal>
<PopoverContent>
<Box p="4" position="relative">
{isInSupportMode ? (
<PosePickerSupport
speciesId={speciesId}
colorId={colorId}
pose={pose}
appearanceId={appearanceId}
initialFocusRef={initialFocusRef}
dispatchToOutfit={dispatchToOutfit}
/>
) : (
<>
<PosePickerTable
poseInfos={poseInfos}
onChange={onChange}
initialFocusRef={initialFocusRef}
/>
{numAvailablePoses <= 1 && (
<SupportOnly>
<Box
fontSize="xs"
fontStyle="italic"
textAlign="center"
opacity="0.7"
marginTop="2"
>
The empty picker is hidden for most users!
<br />
You can see it because you're a Support user.
</Box>
</SupportOnly>
&.is-open {
border-width: 2px !important;
}
`,
isOpen && "is-open"
)}
>
<EmojiImage src={getIcon(pose)} alt="Choose a pose" />
</Button>
</PopoverTrigger>
<Portal>
<PopoverContent>
<Box p="4" position="relative">
{isInSupportMode ? (
<PosePickerSupport
speciesId={speciesId}
colorId={colorId}
pose={pose}
appearanceId={appearanceId}
initialFocusRef={initialFocusRef}
dispatchToOutfit={dispatchToOutfit}
/>
) : (
<>
<PosePickerTable
poseInfos={poseInfos}
onChange={onChange}
initialFocusRef={initialFocusRef}
/>
{numAvailablePoses <= 1 && (
<SupportOnly>
<Box
fontSize="xs"
fontStyle="italic"
textAlign="center"
opacity="0.7"
marginTop="2"
>
The empty picker is hidden for most users!
<br />
You can see it because you're a Support user.
</Box>
</SupportOnly>
)}
</>
)}
</>
)}
<SupportOnly>
<Box position="absolute" top="5" left="3">
<PosePickerSupportSwitch
isChecked={isInSupportMode}
onChange={(e) => setIsInSupportMode(e.target.checked)}
/>
<SupportOnly>
<Box position="absolute" top="5" left="3">
<PosePickerSupportSwitch
isChecked={isInSupportMode}
onChange={(e) => setIsInSupportMode(e.target.checked)}
/>
</Box>
</SupportOnly>
</Box>
</SupportOnly>
</Box>
<PopoverArrow />
</PopoverContent>
</Portal>
</>
<PopoverArrow />
</PopoverContent>
</Portal>
</>
)}
</Popover>
)}
</Popover>
</ClassNames>
);
}
@ -343,110 +347,118 @@ function PoseOption({
);
return (
<Box
as="label"
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;
}
`}
>
<ClassNames>
{({ css, cx }) => (
<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"}
as="label"
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}
>
{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>
</ClassNames>
);
}

View file

@ -12,7 +12,7 @@ import {
useColorModeValue,
} from "@chakra-ui/react";
import { CloseIcon, SearchIcon } from "@chakra-ui/icons";
import { css, cx } from "@emotion/css";
import { ClassNames } from "@emotion/react";
import Autosuggest from "react-autosuggest";
/**
@ -79,23 +79,27 @@ function SearchToolbar({
({ containerProps, children }) => {
const { className, ...otherContainerProps } = containerProps;
return (
<Box
{...otherContainerProps}
borderBottomRadius="md"
boxShadow="md"
overflow="hidden"
transition="all 0.4s"
className={cx(
className,
css`
li {
list-style: none;
}
`
<ClassNames>
{({ css, cx }) => (
<Box
{...otherContainerProps}
borderBottomRadius="md"
boxShadow="md"
overflow="hidden"
transition="all 0.4s"
className={cx(
className,
css`
li {
list-style: none;
}
`
)}
>
{children}
</Box>
)}
>
{children}
</Box>
</ClassNames>
);
},
[]
@ -111,108 +115,116 @@ function SearchToolbar({
const focusBorderColor = useColorModeValue("green.600", "green.400");
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={({ value }) =>
setSuggestions(getSuggestions(value, query, zoneLabels))
}
onSuggestionsClearRequested={() => setSuggestions([])}
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 });
<ClassNames>
{({ css }) => (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={({ value }) =>
setSuggestions(getSuggestions(value, query, zoneLabels))
}
},
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) {
onSuggestionsClearRequested={() => setSuggestions([])}
onSuggestionSelected={(e, { suggestion }) => {
const valueWithoutLastWord = query.value.match(/^(.*?)\s*\S+$/)[1];
onChange({
...query,
filterToItemKind: null,
filterToZoneLabel: null,
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 });
}
},
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 gql from "graphql-tag";
import { useQuery, useMutation } from "@apollo/client";
import { css } from "@emotion/css";
import { ClassNames } from "@emotion/react";
import {
Badge,
Box,
@ -454,71 +454,75 @@ function ItemSupportAppearanceLayer({
const iconButtonColor = useColorModeValue("green.800", "gray.900");
return (
<Box
as="button"
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]} />
<ClassNames>
{({ css }) => (
<Box
className={css`
opacity: 0;
transition: opacity 0.2s;
as="button"
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
className={css`
opacity: 0;
transition: opacity 0.2s;
button:hover &,
button:focus & {
opacity: 1;
}
button:hover &,
button:focus & {
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
* discover things by exploratory hover or focus!) */
@media (hover: none) {
opacity: 1;
}
`}
background={iconButtonBgColor}
color={iconButtonColor}
borderRadius="full"
boxShadow="sm"
position="absolute"
bottom="2"
right="2"
padding="2"
alignItems="center"
justifyContent="center"
width="32px"
height="32px"
>
<EditIcon
boxSize="16px"
position="relative"
top="-2px"
right="-1px"
@media (hover: none) {
opacity: 1;
}
`}
background={iconButtonBgColor}
color={iconButtonColor}
borderRadius="full"
boxShadow="sm"
position="absolute"
bottom="2"
right="2"
padding="2"
alignItems="center"
justifyContent="center"
width="32px"
height="32px"
>
<EditIcon
boxSize="16px"
position="relative"
top="-2px"
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 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>
)}
</ClassNames>
);
}

View file

@ -1,5 +1,5 @@
import * as React from "react";
import { css } from "@emotion/css";
import { ClassNames } from "@emotion/react";
import { Box, useColorModeValue } from "@chakra-ui/react";
import { createIcon } from "@chakra-ui/icons";
@ -21,74 +21,76 @@ function HangerSpinner({ size = "md", ...props }) {
const color = useColorModeValue("green.500", "green.300");
return (
<>
<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.
<ClassNames>
{({ 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.
We use this animation for folks who are okay with dizzy-ish motion.
For reduced motion, we use a pulse-fade instead.
*/
@keyframes swing {
15% {
transform: rotate3d(0, 0, 1, 15deg);
We use this animation for folks who are okay with dizzy-ish motion.
For reduced motion, we use a pulse-fade instead.
*/
@keyframes swing {
15% {
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% {
transform: rotate3d(0, 0, 1, 5deg);
@media (prefers-reduced-motion: no-preference) {
animation: 1.2s infinite swing;
transform-origin: top center;
}
60% {
transform: rotate3d(0, 0, 1, -5deg);
@media (prefers-reduced-motion: reduce) {
animation: 1.6s infinite fade-pulse;
}
75% {
transform: rotate3d(0, 0, 1, 0deg);
}
100% {
transform: rotate3d(0, 0, 1, 0deg);
}
}
/*
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>
</>
`}
{...props}
>
<HangerIcon boxSize={boxSize} color={color} transition="color 0.2s" />
</Box>
)}
</ClassNames>
);
}

View file

@ -1,5 +1,5 @@
import React from "react";
import { css } from "@emotion/css";
import { ClassNames } from "@emotion/react";
import {
Badge,
Box,
@ -102,59 +102,63 @@ export function ItemThumbnail({
);
return (
<Box
width={size === "lg" ? "80px" : "50px"}
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`,
},
},
])}
>
<ClassNames>
{({ css }) => (
<Box
as="img"
width="100%"
height="100%"
src={safeImageUrl(item.thumbnailUrl)}
alt={`Thumbnail art for ${item.name}`}
/>
</Box>
</Box>
width={size === "lg" ? "80px" : "50px"}
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
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();
return (
<Box
fontSize="md"
transition="all 0.15s"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
className={
!isDisabled &&
css`
${focusSelector} {
opacity: 0.9;
font-weight: ${theme.fontWeights.medium};
}
<ClassNames>
{({ css }) => (
<Box
fontSize="md"
transition="all 0.15s"
overflow="hidden"
whiteSpace="nowrap"
textOverflow="ellipsis"
className={
!isDisabled &&
css`
${focusSelector} {
opacity: 0.9;
font-weight: ${theme.fontWeights.medium};
}
input:checked + .item-container & {
opacity: 1;
font-weight: ${theme.fontWeights.bold};
input:checked + .item-container & {
opacity: 1;
font-weight: ${theme.fontWeights.bold};
}
`
}
`
}
{...props}
>
{children}
</Box>
{...props}
>
{children}
</Box>
)}
</ClassNames>
);
}

View file

@ -1,7 +1,7 @@
import React from "react";
import { Box, DarkMode, Flex, Text } from "@chakra-ui/react";
import { WarningIcon } from "@chakra-ui/icons";
import { css } from "@emotion/css";
import { ClassNames } from "@emotion/react";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import OutfitMovieLayer, {
@ -149,104 +149,113 @@ export function OutfitLayers({
}, [setCanvasSize]);
return (
<Box
pos="relative"
height="100%"
width="100%"
// Create a stacking context, so the z-indexed layers don't escape!
zIndex="0"
ref={containerRef}
>
{placeholder && (
<FullScreenCenter>
<Box
// We show the placeholder until there are visible layers, at which
// point we fade it out.
opacity={visibleLayers.length === 0 ? 1 : 0}
<ClassNames>
{({ css }) => (
<Box
pos="relative"
height="100%"
width="100%"
// Create a stacking context, so the z-indexed layers don't escape!
zIndex="0"
ref={containerRef}
>
{placeholder && (
<FullScreenCenter>
<Box
// 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"
>
{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}
/>
) : (
{spinnerVariant === "overlay" && (
<>
<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%"
position="absolute"
top="0"
left="0"
right="0"
bottom="0"
backgroundColor="gray.900"
opacity="0.7"
/>
)}
</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"
>
{spinnerVariant === "overlay" && (
<>
<Box
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>
{/* 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>
)}
</ClassNames>
);
}

View file

@ -2750,17 +2750,6 @@
"@emotion/weak-memoize" "^0.2.5"
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":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"