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:
parent
40728daa99
commit
4120c7aa88
16 changed files with 1460 additions and 1349 deletions
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue