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/react": "^1.0.4",
|
||||
"@chakra-ui/theme-tools": "^1.0.2",
|
||||
"@emotion/css": "^11.1.3",
|
||||
"@emotion/react": "^11.1.4",
|
||||
"@emotion/styled": "^11.0.0",
|
||||
"@loadable/component": "^5.12.0",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import gql from "graphql-tag";
|
||||
import {
|
||||
Box,
|
||||
|
@ -213,37 +213,41 @@ function SubmitPetForm() {
|
|||
const buttonBgColorHover = useColorModeValue("green.700", "green.200");
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Flex>
|
||||
<Input
|
||||
value={petName}
|
||||
onChange={(e) => setPetName(e.target.value)}
|
||||
isDisabled={loading}
|
||||
placeholder="Enter a pet's name"
|
||||
aria-label="Enter a pet's name"
|
||||
borderColor={inputBorderColor}
|
||||
_hover={{ borderColor: inputBorderColorHover }}
|
||||
boxShadow="md"
|
||||
width="14em"
|
||||
className={css`
|
||||
&::placeholder {
|
||||
color: ${theme.colors.gray["500"]};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<Box width="4" />
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="green"
|
||||
isDisabled={!petName}
|
||||
isLoading={loading}
|
||||
backgroundColor={buttonBgColor} // for AA contrast
|
||||
_hover={{ backgroundColor: buttonBgColorHover }}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Flex>
|
||||
<Input
|
||||
value={petName}
|
||||
onChange={(e) => setPetName(e.target.value)}
|
||||
isDisabled={loading}
|
||||
placeholder="Enter a pet's name"
|
||||
aria-label="Enter a pet's name"
|
||||
borderColor={inputBorderColor}
|
||||
_hover={{ borderColor: inputBorderColorHover }}
|
||||
boxShadow="md"
|
||||
width="14em"
|
||||
className={css`
|
||||
&::placeholder {
|
||||
color: ${theme.colors.gray["500"]};
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<Box width="4" />
|
||||
<Button
|
||||
type="submit"
|
||||
colorScheme="green"
|
||||
isDisabled={!petName}
|
||||
isLoading={loading}
|
||||
backgroundColor={buttonBgColor} // for AA contrast
|
||||
_hover={{ backgroundColor: buttonBgColorHover }}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import {
|
||||
AspectRatio,
|
||||
Button,
|
||||
|
@ -205,55 +205,62 @@ function ItemPageOwnButton({ itemId, isChecked }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Box as="label">
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
sendAddMutation().catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: "We had trouble adding this to the items you own.",
|
||||
description: "Check your internet connection, and try again.",
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendRemoveMutation().catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: "We had trouble removing this from the items you own.",
|
||||
description: "Check your internet connection, and try again.",
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
as="div"
|
||||
colorScheme={isChecked ? "green" : "gray"}
|
||||
size="lg"
|
||||
cursor="pointer"
|
||||
transitionDuration="0.4s"
|
||||
className={css`
|
||||
input:focus + & {
|
||||
box-shadow: ${theme.shadows.outline};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<IconCheckbox
|
||||
icon={<CheckIcon />}
|
||||
isChecked={isChecked}
|
||||
marginRight="0.5em"
|
||||
/>
|
||||
I own this
|
||||
</Button>
|
||||
</Box>
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box as="label">
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
sendAddMutation().catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: "We had trouble adding this to the items you own.",
|
||||
description:
|
||||
"Check your internet connection, and try again.",
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendRemoveMutation().catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
title:
|
||||
"We had trouble removing this from the items you own.",
|
||||
description:
|
||||
"Check your internet connection, and try again.",
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
as="div"
|
||||
colorScheme={isChecked ? "green" : "gray"}
|
||||
size="lg"
|
||||
cursor="pointer"
|
||||
transitionDuration="0.4s"
|
||||
className={css`
|
||||
input:focus + & {
|
||||
box-shadow: ${theme.shadows.outline};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<IconCheckbox
|
||||
icon={<CheckIcon />}
|
||||
isChecked={isChecked}
|
||||
marginRight="0.5em"
|
||||
/>
|
||||
I own this
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -306,55 +313,62 @@ function ItemPageWantButton({ itemId, isChecked }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Box as="label">
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="checkbox"
|
||||
isChecked={isChecked}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
sendAddMutation().catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: "We had trouble adding this to the items you want.",
|
||||
description: "Check your internet connection, and try again.",
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendRemoveMutation().catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: "We had trouble removing this from the items you want.",
|
||||
description: "Check your internet connection, and try again.",
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
as="div"
|
||||
colorScheme={isChecked ? "blue" : "gray"}
|
||||
size="lg"
|
||||
cursor="pointer"
|
||||
transitionDuration="0.4s"
|
||||
className={css`
|
||||
input:focus + & {
|
||||
box-shadow: ${theme.shadows.outline};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<IconCheckbox
|
||||
icon={<StarIcon />}
|
||||
isChecked={isChecked}
|
||||
marginRight="0.5em"
|
||||
/>
|
||||
I want this
|
||||
</Button>
|
||||
</Box>
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box as="label">
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="checkbox"
|
||||
isChecked={isChecked}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
sendAddMutation().catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
title: "We had trouble adding this to the items you want.",
|
||||
description:
|
||||
"Check your internet connection, and try again.",
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendRemoveMutation().catch((e) => {
|
||||
console.error(e);
|
||||
toast({
|
||||
title:
|
||||
"We had trouble removing this from the items you want.",
|
||||
description:
|
||||
"Check your internet connection, and try again.",
|
||||
status: "error",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
as="div"
|
||||
colorScheme={isChecked ? "blue" : "gray"}
|
||||
size="lg"
|
||||
cursor="pointer"
|
||||
transitionDuration="0.4s"
|
||||
className={css`
|
||||
input:focus + & {
|
||||
box-shadow: ${theme.shadows.outline};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<IconCheckbox
|
||||
icon={<StarIcon />}
|
||||
isChecked={isChecked}
|
||||
marginRight="0.5em"
|
||||
/>
|
||||
I want this
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import { Box, Skeleton, useColorModeValue, useToken } from "@chakra-ui/react";
|
||||
import gql from "graphql-tag";
|
||||
import { useQuery } from "@apollo/client";
|
||||
|
@ -165,84 +165,88 @@ function ItemTradesTable({
|
|||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="table"
|
||||
width="100%"
|
||||
boxShadow="md"
|
||||
className={css`
|
||||
/* Chakra doesn't have props for these! */
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
`}
|
||||
>
|
||||
<Box as="thead" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Box as="tr">
|
||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
||||
{/* A small wording tweak to fit better on the xsmall screens! */}
|
||||
<Box display={{ base: "none", sm: "block" }}>Last active</Box>
|
||||
<Box display={{ base: "block", sm: "none" }}>Last edit</Box>
|
||||
</ItemTradesTableCell>
|
||||
{shouldShowCompareColumn && (
|
||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
||||
<Box display={{ base: "none", sm: "block" }}>
|
||||
{compareColumnLabel}
|
||||
</Box>
|
||||
<Box display={{ base: "block", sm: "none" }}>Matches</Box>
|
||||
</ItemTradesTableCell>
|
||||
)}
|
||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
||||
{userHeading}
|
||||
</ItemTradesTableCell>
|
||||
<ItemTradesTableCell as="th">List</ItemTradesTableCell>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as="tbody">
|
||||
{loading && (
|
||||
<>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!loading &&
|
||||
trades.length > 0 &&
|
||||
trades.map((trade) => (
|
||||
<ItemTradesTableRow
|
||||
key={trade.id}
|
||||
href={`/user/${trade.user.id}/items#list-${trade.closetList.id}`}
|
||||
username={trade.user.username}
|
||||
listName={trade.closetList.name}
|
||||
lastTradeActivity={trade.user.lastTradeActivity}
|
||||
matchingItems={trade.user.matchingItems}
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
))}
|
||||
{!loading && trades.length === 0 && (
|
||||
<Box as="tr">
|
||||
<ItemTradesTableCell
|
||||
colSpan={shouldShowCompareColumn ? 4 : 3}
|
||||
textAlign="center"
|
||||
fontStyle="italic"
|
||||
>
|
||||
No trades yet!
|
||||
</ItemTradesTableCell>
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
as="table"
|
||||
width="100%"
|
||||
boxShadow="md"
|
||||
className={css`
|
||||
/* Chakra doesn't have props for these! */
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
table-layout: fixed;
|
||||
`}
|
||||
>
|
||||
<Box as="thead" fontSize={{ base: "xs", sm: "sm" }}>
|
||||
<Box as="tr">
|
||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
||||
{/* A small wording tweak to fit better on the xsmall screens! */}
|
||||
<Box display={{ base: "none", sm: "block" }}>Last active</Box>
|
||||
<Box display={{ base: "block", sm: "none" }}>Last edit</Box>
|
||||
</ItemTradesTableCell>
|
||||
{shouldShowCompareColumn && (
|
||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
||||
<Box display={{ base: "none", sm: "block" }}>
|
||||
{compareColumnLabel}
|
||||
</Box>
|
||||
<Box display={{ base: "block", sm: "none" }}>Matches</Box>
|
||||
</ItemTradesTableCell>
|
||||
)}
|
||||
<ItemTradesTableCell as="th" width={minorColumnWidth}>
|
||||
{userHeading}
|
||||
</ItemTradesTableCell>
|
||||
<ItemTradesTableCell as="th">List</ItemTradesTableCell>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box as="tbody">
|
||||
{loading && (
|
||||
<>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
<ItemTradesTableRowSkeleton
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!loading &&
|
||||
trades.length > 0 &&
|
||||
trades.map((trade) => (
|
||||
<ItemTradesTableRow
|
||||
key={trade.id}
|
||||
href={`/user/${trade.user.id}/items#list-${trade.closetList.id}`}
|
||||
username={trade.user.username}
|
||||
listName={trade.closetList.name}
|
||||
lastTradeActivity={trade.user.lastTradeActivity}
|
||||
matchingItems={trade.user.matchingItems}
|
||||
shouldShowCompareColumn={shouldShowCompareColumn}
|
||||
/>
|
||||
))}
|
||||
{!loading && trades.length === 0 && (
|
||||
<Box as="tr">
|
||||
<ItemTradesTableCell
|
||||
colSpan={shouldShowCompareColumn ? 4 : 3}
|
||||
textAlign="center"
|
||||
fontStyle="italic"
|
||||
>
|
||||
No trades yet!
|
||||
</ItemTradesTableCell>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -263,63 +267,67 @@ function ItemTradesTableRow({
|
|||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="tr"
|
||||
cursor="pointer"
|
||||
_hover={{ background: focusBackground }}
|
||||
_focusWithin={{ background: focusBackground }}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ItemTradesTableCell fontSize="xs">
|
||||
{formatVagueDate(lastTradeActivity)}
|
||||
</ItemTradesTableCell>
|
||||
{shouldShowCompareColumn && (
|
||||
<ItemTradesTableCell fontSize="xs">
|
||||
{matchingItems.length > 0 ? (
|
||||
<Box as="ul">
|
||||
{sortedMatchingItems.slice(0, 4).map((item) => (
|
||||
<Box key={item.id} as="li">
|
||||
<Box
|
||||
lineHeight="1.5"
|
||||
maxHeight="1.5em"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
{matchingItems.length > 4 && (
|
||||
<Box as="li">+ {matchingItems.length - 4} more</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box display={{ base: "none", sm: "block" }}>No matches</Box>
|
||||
<Box display={{ base: "block", sm: "none" }}>None</Box>
|
||||
</>
|
||||
)}
|
||||
</ItemTradesTableCell>
|
||||
)}
|
||||
<ItemTradesTableCell fontSize="xs">{username}</ItemTradesTableCell>
|
||||
<ItemTradesTableCell fontSize="sm">
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
as={Link}
|
||||
to={href}
|
||||
className={css`
|
||||
&:hover,
|
||||
&:focus,
|
||||
tr:hover &,
|
||||
tr:focus-within & {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}
|
||||
as="tr"
|
||||
cursor="pointer"
|
||||
_hover={{ background: focusBackground }}
|
||||
_focusWithin={{ background: focusBackground }}
|
||||
onClick={onClick}
|
||||
>
|
||||
{listName}
|
||||
<ItemTradesTableCell fontSize="xs">
|
||||
{formatVagueDate(lastTradeActivity)}
|
||||
</ItemTradesTableCell>
|
||||
{shouldShowCompareColumn && (
|
||||
<ItemTradesTableCell fontSize="xs">
|
||||
{matchingItems.length > 0 ? (
|
||||
<Box as="ul">
|
||||
{sortedMatchingItems.slice(0, 4).map((item) => (
|
||||
<Box key={item.id} as="li">
|
||||
<Box
|
||||
lineHeight="1.5"
|
||||
maxHeight="1.5em"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
{matchingItems.length > 4 && (
|
||||
<Box as="li">+ {matchingItems.length - 4} more</Box>
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box display={{ base: "none", sm: "block" }}>No matches</Box>
|
||||
<Box display={{ base: "block", sm: "none" }}>None</Box>
|
||||
</>
|
||||
)}
|
||||
</ItemTradesTableCell>
|
||||
)}
|
||||
<ItemTradesTableCell fontSize="xs">{username}</ItemTradesTableCell>
|
||||
<ItemTradesTableCell fontSize="sm">
|
||||
<Box
|
||||
as={Link}
|
||||
to={href}
|
||||
className={css`
|
||||
&:hover,
|
||||
&:focus,
|
||||
tr:hover &,
|
||||
tr:focus-within & {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{listName}
|
||||
</Box>
|
||||
</ItemTradesTableCell>
|
||||
</Box>
|
||||
</ItemTradesTableCell>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -350,47 +358,51 @@ function ItemTradesTableCell({ children, as = "td", ...props }) {
|
|||
const borderRadiusCss = useToken("radii", "md");
|
||||
|
||||
return (
|
||||
<Box
|
||||
as={as}
|
||||
paddingX="4"
|
||||
paddingY="2"
|
||||
textAlign="left"
|
||||
className={css`
|
||||
/* Lol sigh, getting this right is way more involved than I wish it
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
as={as}
|
||||
paddingX="4"
|
||||
paddingY="2"
|
||||
textAlign="left"
|
||||
className={css`
|
||||
/* Lol sigh, getting this right is way more involved than I wish it
|
||||
* were. What I really want is border-collapse and a simple 1px border,
|
||||
* but that disables border-radius. So, we homebrew it by giving all
|
||||
* cells bottom and right borders, but only the cells on the edges a
|
||||
* top or left border; and then target the exact 4 corner cells to
|
||||
* round them. Pretty old-school tbh 🙃 */
|
||||
|
||||
border-bottom: 1px solid ${borderColorCss};
|
||||
border-right: 1px solid ${borderColorCss};
|
||||
border-bottom: 1px solid ${borderColorCss};
|
||||
border-right: 1px solid ${borderColorCss};
|
||||
|
||||
thead tr:first-of-type & {
|
||||
border-top: 1px solid ${borderColorCss};
|
||||
}
|
||||
thead tr:first-of-type & {
|
||||
border-top: 1px solid ${borderColorCss};
|
||||
}
|
||||
|
||||
&:first-of-type {
|
||||
border-left: 1px solid ${borderColorCss};
|
||||
}
|
||||
&:first-of-type {
|
||||
border-left: 1px solid ${borderColorCss};
|
||||
}
|
||||
|
||||
thead tr:first-of-type &:first-of-type {
|
||||
border-top-left-radius: ${borderRadiusCss};
|
||||
}
|
||||
thead tr:first-of-type &:last-of-type {
|
||||
border-top-right-radius: ${borderRadiusCss};
|
||||
}
|
||||
tbody tr:last-of-type &:first-of-type {
|
||||
border-bottom-left-radius: ${borderRadiusCss};
|
||||
}
|
||||
tbody tr:last-of-type &:last-of-type {
|
||||
border-bottom-right-radius: ${borderRadiusCss};
|
||||
}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
thead tr:first-of-type &:first-of-type {
|
||||
border-top-left-radius: ${borderRadiusCss};
|
||||
}
|
||||
thead tr:first-of-type &:last-of-type {
|
||||
border-top-right-radius: ${borderRadiusCss};
|
||||
}
|
||||
tbody tr:last-of-type &:first-of-type {
|
||||
border-bottom-left-radius: ${borderRadiusCss};
|
||||
}
|
||||
tbody tr:last-of-type &:last-of-type {
|
||||
border-bottom-right-radius: ${borderRadiusCss};
|
||||
}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { css } from "@emotion/react";
|
||||
import { VStack } from "@chakra-ui/react";
|
||||
|
||||
import { Heading1, Heading2, Heading3 } from "./util";
|
||||
|
@ -11,7 +11,7 @@ function PrivacyPolicyPage() {
|
|||
<VStack
|
||||
spacing="4"
|
||||
alignItems="flex-start"
|
||||
className={css`
|
||||
css={css`
|
||||
max-width: 800px;
|
||||
|
||||
p {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
|
@ -149,139 +149,147 @@ function UserItemsPage() {
|
|||
).size;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Flex align="center" wrap="wrap-reverse">
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box>
|
||||
<Heading1>
|
||||
{isCurrentUser ? "Your items" : `${data.user.username}'s items`}
|
||||
</Heading1>
|
||||
<Wrap spacing="2" opacity="0.7">
|
||||
{data.user.contactNeopetsUsername && (
|
||||
<WrapItem>
|
||||
<Badge
|
||||
as="a"
|
||||
href={`http://www.neopets.com/userlookup.phtml?user=${data.user.contactNeopetsUsername}`}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<NeopetsStarIcon marginRight="1" />
|
||||
{data.user.contactNeopetsUsername}
|
||||
</Badge>
|
||||
</WrapItem>
|
||||
)}
|
||||
{data.user.contactNeopetsUsername && (
|
||||
<WrapItem>
|
||||
<Badge
|
||||
as="a"
|
||||
href={`http://www.neopets.com/neomessages.phtml?type=send&recipient=${data.user.contactNeopetsUsername}`}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<EmailIcon marginRight="1" />
|
||||
Neomail
|
||||
</Badge>
|
||||
</WrapItem>
|
||||
)}
|
||||
<SupportOnly>
|
||||
<WrapItem>
|
||||
<UserSupportMenu user={data.user}>
|
||||
<MenuButton
|
||||
as={BadgeButton}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<EditIcon marginRight="1" />
|
||||
Support
|
||||
</MenuButton>
|
||||
</UserSupportMenu>
|
||||
</WrapItem>
|
||||
</SupportOnly>
|
||||
{/* Usually I put "Own" before "Want", but this matches the natural
|
||||
* order on the page: the _matches_ for things you want are things
|
||||
* _this user_ owns, so they come first. I think it's also probably a
|
||||
* more natural train of thought: you come to someone's list _wanting_
|
||||
* something, and _then_ thinking about what you can offer. */}
|
||||
{!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && (
|
||||
<WrapItem>
|
||||
<Badge
|
||||
as="a"
|
||||
href="#owned-items"
|
||||
colorScheme="blue"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<StarIcon marginRight="1" />
|
||||
{numItemsTheyOwnThatYouWant > 1
|
||||
? `${numItemsTheyOwnThatYouWant} items you want`
|
||||
: "1 item you want"}
|
||||
</Badge>
|
||||
</WrapItem>
|
||||
)}
|
||||
{!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && (
|
||||
<WrapItem>
|
||||
<Badge
|
||||
as="a"
|
||||
href="#wanted-items"
|
||||
colorScheme="green"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<CheckIcon marginRight="1" />
|
||||
{numItemsTheyWantThatYouOwn > 1
|
||||
? `${numItemsTheyWantThatYouOwn} items you own`
|
||||
: "1 item you own"}
|
||||
</Badge>
|
||||
</WrapItem>
|
||||
)}
|
||||
</Wrap>
|
||||
</Box>
|
||||
<Box flex="1 0 auto" width="2" />
|
||||
<Box marginBottom="1">
|
||||
<UserSearchForm />
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex align="center" wrap="wrap-reverse">
|
||||
<Box>
|
||||
<Heading1>
|
||||
{isCurrentUser ? "Your items" : `${data.user.username}'s items`}
|
||||
</Heading1>
|
||||
<Wrap spacing="2" opacity="0.7">
|
||||
{data.user.contactNeopetsUsername && (
|
||||
<WrapItem>
|
||||
<Badge
|
||||
as="a"
|
||||
href={`http://www.neopets.com/userlookup.phtml?user=${data.user.contactNeopetsUsername}`}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<NeopetsStarIcon marginRight="1" />
|
||||
{data.user.contactNeopetsUsername}
|
||||
</Badge>
|
||||
</WrapItem>
|
||||
)}
|
||||
{data.user.contactNeopetsUsername && (
|
||||
<WrapItem>
|
||||
<Badge
|
||||
as="a"
|
||||
href={`http://www.neopets.com/neomessages.phtml?type=send&recipient=${data.user.contactNeopetsUsername}`}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<EmailIcon marginRight="1" />
|
||||
Neomail
|
||||
</Badge>
|
||||
</WrapItem>
|
||||
)}
|
||||
<SupportOnly>
|
||||
<WrapItem>
|
||||
<UserSupportMenu user={data.user}>
|
||||
<MenuButton
|
||||
as={BadgeButton}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<EditIcon marginRight="1" />
|
||||
Support
|
||||
</MenuButton>
|
||||
</UserSupportMenu>
|
||||
</WrapItem>
|
||||
</SupportOnly>
|
||||
{/* Usually I put "Own" before "Want", but this matches the natural
|
||||
* order on the page: the _matches_ for things you want are things
|
||||
* _this user_ owns, so they come first. I think it's also probably a
|
||||
* more natural train of thought: you come to someone's list _wanting_
|
||||
* something, and _then_ thinking about what you can offer. */}
|
||||
{!isCurrentUser && numItemsTheyOwnThatYouWant > 0 && (
|
||||
<WrapItem>
|
||||
<Badge
|
||||
as="a"
|
||||
href="#owned-items"
|
||||
colorScheme="blue"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<StarIcon marginRight="1" />
|
||||
{numItemsTheyOwnThatYouWant > 1
|
||||
? `${numItemsTheyOwnThatYouWant} items you want`
|
||||
: "1 item you want"}
|
||||
</Badge>
|
||||
</WrapItem>
|
||||
)}
|
||||
{!isCurrentUser && numItemsTheyWantThatYouOwn > 0 && (
|
||||
<WrapItem>
|
||||
<Badge
|
||||
as="a"
|
||||
href="#wanted-items"
|
||||
colorScheme="green"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<CheckIcon marginRight="1" />
|
||||
{numItemsTheyWantThatYouOwn > 1
|
||||
? `${numItemsTheyWantThatYouOwn} items you own`
|
||||
: "1 item you own"}
|
||||
</Badge>
|
||||
</WrapItem>
|
||||
)}
|
||||
</Wrap>
|
||||
</Box>
|
||||
<Box flex="1 0 auto" width="2" />
|
||||
<Box marginBottom="1">
|
||||
<UserSearchForm />
|
||||
</Box>
|
||||
</Flex>
|
||||
|
||||
<Box marginTop="4">
|
||||
{isCurrentUser && (
|
||||
<Box float="right">
|
||||
<WIPCallout details="These lists are read-only for now. To edit, head back to Classic DTI!" />
|
||||
<Box marginTop="4">
|
||||
{isCurrentUser && (
|
||||
<Box float="right">
|
||||
<WIPCallout details="These lists are read-only for now. To edit, head back to Classic DTI!" />
|
||||
</Box>
|
||||
)}
|
||||
<Heading2 id="owned-items" marginBottom="2">
|
||||
{isCurrentUser
|
||||
? "Items you own"
|
||||
: `Items ${data.user.username} owns`}
|
||||
</Heading2>
|
||||
<VStack
|
||||
spacing="8"
|
||||
alignItems="stretch"
|
||||
className={css`
|
||||
clear: both;
|
||||
`}
|
||||
>
|
||||
{listsOfOwnedItems.map((closetList) => (
|
||||
<ClosetList
|
||||
key={closetList.id}
|
||||
closetList={closetList}
|
||||
isCurrentUser={isCurrentUser}
|
||||
showHeading={listsOfOwnedItems.length > 1}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
<Heading2 id="owned-items" marginBottom="2">
|
||||
{isCurrentUser ? "Items you own" : `Items ${data.user.username} owns`}
|
||||
</Heading2>
|
||||
<VStack
|
||||
spacing="8"
|
||||
alignItems="stretch"
|
||||
className={css`
|
||||
clear: both;
|
||||
`}
|
||||
>
|
||||
{listsOfOwnedItems.map((closetList) => (
|
||||
<ClosetList
|
||||
key={closetList.id}
|
||||
closetList={closetList}
|
||||
isCurrentUser={isCurrentUser}
|
||||
showHeading={listsOfOwnedItems.length > 1}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
<Heading2 id="wanted-items" marginTop="10" marginBottom="2">
|
||||
{isCurrentUser ? "Items you want" : `Items ${data.user.username} wants`}
|
||||
</Heading2>
|
||||
<VStack spacing="4" alignItems="stretch">
|
||||
{listsOfWantedItems.map((closetList) => (
|
||||
<ClosetList
|
||||
key={closetList.id}
|
||||
closetList={closetList}
|
||||
isCurrentUser={isCurrentUser}
|
||||
showHeading={listsOfWantedItems.length > 1}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
<Heading2 id="wanted-items" marginTop="10" marginBottom="2">
|
||||
{isCurrentUser
|
||||
? "Items you want"
|
||||
: `Items ${data.user.username} wants`}
|
||||
</Heading2>
|
||||
<VStack spacing="4" alignItems="stretch">
|
||||
{listsOfWantedItems.map((closetList) => (
|
||||
<ClosetList
|
||||
key={closetList.id}
|
||||
closetList={closetList}
|
||||
isCurrentUser={isCurrentUser}
|
||||
showHeading={listsOfWantedItems.length > 1}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -588,21 +596,25 @@ function MarkdownAndSafeHTML({ children }) {
|
|||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
className={css`
|
||||
.paragraph,
|
||||
ol,
|
||||
ul {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
className={css`
|
||||
.paragraph,
|
||||
ol,
|
||||
ul {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
margin-left: 2em;
|
||||
}
|
||||
`}
|
||||
></Box>
|
||||
ol,
|
||||
ul {
|
||||
margin-left: 2em;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css, cx } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
|
@ -166,35 +166,39 @@ function ItemContainer({ children, isDisabled = false }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
p="1"
|
||||
my="1"
|
||||
borderRadius="lg"
|
||||
d="flex"
|
||||
cursor={isDisabled ? undefined : "pointer"}
|
||||
border="1px"
|
||||
borderColor="transparent"
|
||||
className={cx([
|
||||
"item-container",
|
||||
!isDisabled &&
|
||||
css`
|
||||
&:hover,
|
||||
input:focus + & {
|
||||
background-color: ${focusBackgroundColor};
|
||||
}
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<Box
|
||||
p="1"
|
||||
my="1"
|
||||
borderRadius="lg"
|
||||
d="flex"
|
||||
cursor={isDisabled ? undefined : "pointer"}
|
||||
border="1px"
|
||||
borderColor="transparent"
|
||||
className={cx([
|
||||
"item-container",
|
||||
!isDisabled &&
|
||||
css`
|
||||
&:hover,
|
||||
input:focus + & {
|
||||
background-color: ${focusBackgroundColor};
|
||||
}
|
||||
|
||||
input:active + & {
|
||||
border-color: ${activeBorderColor};
|
||||
}
|
||||
input:active + & {
|
||||
border-color: ${activeBorderColor};
|
||||
}
|
||||
|
||||
input:checked:focus + & {
|
||||
border-color: ${focusCheckedBorderColor};
|
||||
}
|
||||
`,
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
input:checked:focus + & {
|
||||
border-color: ${focusCheckedBorderColor};
|
||||
}
|
||||
`,
|
||||
])}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -242,39 +246,43 @@ function ItemActionButton({ icon, label, to, onClick }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<Tooltip label={label} placement="top">
|
||||
<IconButton
|
||||
as={to ? Link : "button"}
|
||||
icon={icon}
|
||||
aria-label={label}
|
||||
variant="ghost"
|
||||
color="gray.400"
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
className={css`
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Tooltip label={label} placement="top">
|
||||
<IconButton
|
||||
as={to ? Link : "button"}
|
||||
icon={icon}
|
||||
aria-label={label}
|
||||
variant="ghost"
|
||||
color="gray.400"
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
className={css`
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
|
||||
${containerHasFocus} {
|
||||
opacity: 1;
|
||||
}
|
||||
${containerHasFocus} {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: ${focusBackgroundColor};
|
||||
color: ${focusColor};
|
||||
}
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: ${focusBackgroundColor};
|
||||
color: ${focusColor};
|
||||
}
|
||||
|
||||
/* On touch devices, always show the buttons! This avoids having to
|
||||
/* On touch devices, always show the buttons! This avoids having to
|
||||
* tap to reveal them (which toggles the item), or worse,
|
||||
* accidentally tapping a hidden button without realizing! */
|
||||
@media (hover: none) {
|
||||
opacity: 1;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</Tooltip>
|
||||
@media (hover: none) {
|
||||
opacity: 1;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import {
|
||||
Box,
|
||||
Editable,
|
||||
|
@ -34,50 +34,59 @@ function ItemsPanel({ outfitState, loading, dispatchToOutfit }) {
|
|||
const { zonesAndItems, incompatibleItems } = outfitState;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box px="1">
|
||||
<OutfitHeading
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
<Flex direction="column">
|
||||
{loading ? (
|
||||
<ItemZoneGroupsSkeleton itemCount={outfitState.allItemIds.length} />
|
||||
) : (
|
||||
<TransitionGroup component={null}>
|
||||
{zonesAndItems.map(({ zoneLabel, items }) => (
|
||||
<CSSTransition key={zoneLabel} {...fadeOutAndRollUpTransition}>
|
||||
<ItemZoneGroup
|
||||
zoneLabel={zoneLabel}
|
||||
items={items}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</CSSTransition>
|
||||
))}
|
||||
{incompatibleItems.length > 0 && (
|
||||
<ItemZoneGroup
|
||||
zoneLabel="Incompatible"
|
||||
afterHeader={
|
||||
<Tooltip
|
||||
label="These items don't fit this pet"
|
||||
placement="top"
|
||||
openDelay={100}
|
||||
>
|
||||
<QuestionIcon fontSize="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
items={incompatibleItems}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
isDisabled
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box>
|
||||
<Box px="1">
|
||||
<OutfitHeading
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</Box>
|
||||
<Flex direction="column">
|
||||
{loading ? (
|
||||
<ItemZoneGroupsSkeleton
|
||||
itemCount={outfitState.allItemIds.length}
|
||||
/>
|
||||
) : (
|
||||
<TransitionGroup component={null}>
|
||||
{zonesAndItems.map(({ zoneLabel, items }) => (
|
||||
<CSSTransition
|
||||
key={zoneLabel}
|
||||
{...fadeOutAndRollUpTransition(css)}
|
||||
>
|
||||
<ItemZoneGroup
|
||||
zoneLabel={zoneLabel}
|
||||
items={items}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
</CSSTransition>
|
||||
))}
|
||||
{incompatibleItems.length > 0 && (
|
||||
<ItemZoneGroup
|
||||
zoneLabel="Incompatible"
|
||||
afterHeader={
|
||||
<Tooltip
|
||||
label="These items don't fit this pet"
|
||||
placement="top"
|
||||
openDelay={100}
|
||||
>
|
||||
<QuestionIcon fontSize="sm" />
|
||||
</Tooltip>
|
||||
}
|
||||
items={incompatibleItems}
|
||||
outfitState={outfitState}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
isDisabled
|
||||
/>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
)}
|
||||
</TransitionGroup>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -125,59 +134,66 @@ function ItemZoneGroup({
|
|||
);
|
||||
|
||||
return (
|
||||
<Box mb="10">
|
||||
<Heading2 display="flex" alignItems="center" mx="1">
|
||||
{zoneLabel}
|
||||
{afterHeader && <Box marginLeft="2">{afterHeader}</Box>}
|
||||
</Heading2>
|
||||
<ItemListContainer>
|
||||
<TransitionGroup component={null}>
|
||||
{items.map((item) => {
|
||||
const itemNameId =
|
||||
zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`;
|
||||
const itemNode = (
|
||||
<Item
|
||||
item={item}
|
||||
itemNameId={itemNameId}
|
||||
isWorn={
|
||||
!isDisabled && outfitState.wornItemIds.includes(item.id)
|
||||
}
|
||||
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
||||
onRemove={onRemove}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box mb="10">
|
||||
<Heading2 display="flex" alignItems="center" mx="1">
|
||||
{zoneLabel}
|
||||
{afterHeader && <Box marginLeft="2">{afterHeader}</Box>}
|
||||
</Heading2>
|
||||
<ItemListContainer>
|
||||
<TransitionGroup component={null}>
|
||||
{items.map((item) => {
|
||||
const itemNameId =
|
||||
zoneLabel.replace(/ /g, "-") + `-item-${item.id}-name`;
|
||||
const itemNode = (
|
||||
<Item
|
||||
item={item}
|
||||
itemNameId={itemNameId}
|
||||
isWorn={
|
||||
!isDisabled && outfitState.wornItemIds.includes(item.id)
|
||||
}
|
||||
isInOutfit={outfitState.allItemIds.includes(item.id)}
|
||||
onRemove={onRemove}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<CSSTransition key={item.id} {...fadeOutAndRollUpTransition}>
|
||||
{isDisabled ? (
|
||||
itemNode
|
||||
) : (
|
||||
<label>
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="radio"
|
||||
aria-labelledby={itemNameId}
|
||||
name={zoneLabel}
|
||||
value={item.id}
|
||||
checked={outfitState.wornItemIds.includes(item.id)}
|
||||
onChange={onChange}
|
||||
onClick={onClick}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === " ") {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{itemNode}
|
||||
</label>
|
||||
)}
|
||||
</CSSTransition>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
</ItemListContainer>
|
||||
</Box>
|
||||
return (
|
||||
<CSSTransition
|
||||
key={item.id}
|
||||
{...fadeOutAndRollUpTransition(css)}
|
||||
>
|
||||
{isDisabled ? (
|
||||
itemNode
|
||||
) : (
|
||||
<label>
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="radio"
|
||||
aria-labelledby={itemNameId}
|
||||
name={zoneLabel}
|
||||
value={item.id}
|
||||
checked={outfitState.wornItemIds.includes(item.id)}
|
||||
onChange={onChange}
|
||||
onClick={onClick}
|
||||
onKeyUp={(e) => {
|
||||
if (e.key === " ") {
|
||||
onClick(e);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{itemNode}
|
||||
</label>
|
||||
)}
|
||||
</CSSTransition>
|
||||
);
|
||||
})}
|
||||
</TransitionGroup>
|
||||
</ItemListContainer>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -273,7 +289,7 @@ function OutfitHeading({ outfitState, dispatchToOutfit }) {
|
|||
*
|
||||
* See react-transition-group docs for more info!
|
||||
*/
|
||||
const fadeOutAndRollUpTransition = {
|
||||
const fadeOutAndRollUpTransition = (css) => ({
|
||||
classNames: css`
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
|
@ -292,6 +308,6 @@ const fadeOutAndRollUpTransition = {
|
|||
onExit: (e) => {
|
||||
e.style.height = e.offsetHeight + "px";
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export default ItemsPanel;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css, cx } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
@ -84,120 +84,126 @@ function OutfitControls({
|
|||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
role="group"
|
||||
pos="absolute"
|
||||
left="0"
|
||||
right="0"
|
||||
top="0"
|
||||
bottom="0"
|
||||
height="100%" // Required for Safari to size the grid correctly
|
||||
padding={{ base: 2, lg: 6 }}
|
||||
display="grid"
|
||||
overflow="auto"
|
||||
gridTemplateAreas={`"back play-pause sharing"
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<Box
|
||||
role="group"
|
||||
pos="absolute"
|
||||
left="0"
|
||||
right="0"
|
||||
top="0"
|
||||
bottom="0"
|
||||
height="100%" // Required for Safari to size the grid correctly
|
||||
padding={{ base: 2, lg: 6 }}
|
||||
display="grid"
|
||||
overflow="auto"
|
||||
gridTemplateAreas={`"back play-pause sharing"
|
||||
"space space space"
|
||||
"picker picker picker"`}
|
||||
gridTemplateRows="auto minmax(1rem, 1fr) auto"
|
||||
className={cx(
|
||||
css`
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
gridTemplateRows="auto minmax(1rem, 1fr) auto"
|
||||
className={cx(
|
||||
css`
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:focus-within,
|
||||
&.focus-is-locked {
|
||||
opacity: 1;
|
||||
}
|
||||
&:focus-within,
|
||||
&.focus-is-locked {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Ignore simulated hovers, only reveal for _real_ hovers. This helps
|
||||
/* Ignore simulated hovers, only reveal for _real_ hovers. This helps
|
||||
* us avoid state conflicts with the focus-lock from clicks. */
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
focusIsLocked && "focus-is-locked"
|
||||
)}
|
||||
onClickCapture={(e) => {
|
||||
const opacity = parseFloat(getComputedStyle(e.currentTarget).opacity);
|
||||
if (opacity < 0.5) {
|
||||
// If the controls aren't visible right now, then clicks on them are
|
||||
// probably accidental. Ignore them! (We prevent default to block
|
||||
// built-in behaviors like link nav, and we stop propagation to block
|
||||
// our own custom click handlers. I don't know if I can prevent the
|
||||
// select clicks though?)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
focusIsLocked && "focus-is-locked"
|
||||
)}
|
||||
onClickCapture={(e) => {
|
||||
const opacity = parseFloat(
|
||||
getComputedStyle(e.currentTarget).opacity
|
||||
);
|
||||
if (opacity < 0.5) {
|
||||
// If the controls aren't visible right now, then clicks on them are
|
||||
// probably accidental. Ignore them! (We prevent default to block
|
||||
// built-in behaviors like link nav, and we stop propagation to block
|
||||
// our own custom click handlers. I don't know if I can prevent the
|
||||
// select clicks though?)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// We also show the controls, by locking focus. We'll undo this when
|
||||
// the user taps elsewhere (because it will trigger a blur event from
|
||||
// our child components), in `maybeUnlockFocus`.
|
||||
setFocusIsLocked(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box gridArea="back" onClick={maybeUnlockFocus}>
|
||||
<ControlButton
|
||||
as={Link}
|
||||
to="/"
|
||||
icon={<ArrowBackIcon />}
|
||||
aria-label="Leave this outfit"
|
||||
d="inline-flex" // Not sure why <a> requires this to style right! ^^`
|
||||
/>
|
||||
</Box>
|
||||
{showAnimationControls && (
|
||||
<Box gridArea="play-pause" display="flex" justifyContent="center">
|
||||
<DarkMode>
|
||||
<PlayPauseButton />
|
||||
</DarkMode>
|
||||
// We also show the controls, by locking focus. We'll undo this when
|
||||
// the user taps elsewhere (because it will trigger a blur event from
|
||||
// our child components), in `maybeUnlockFocus`.
|
||||
setFocusIsLocked(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box gridArea="back" onClick={maybeUnlockFocus}>
|
||||
<ControlButton
|
||||
as={Link}
|
||||
to="/"
|
||||
icon={<ArrowBackIcon />}
|
||||
aria-label="Leave this outfit"
|
||||
d="inline-flex" // Not sure why <a> requires this to style right! ^^`
|
||||
/>
|
||||
</Box>
|
||||
{showAnimationControls && (
|
||||
<Box gridArea="play-pause" display="flex" justifyContent="center">
|
||||
<DarkMode>
|
||||
<PlayPauseButton />
|
||||
</DarkMode>
|
||||
</Box>
|
||||
)}
|
||||
<Stack
|
||||
gridArea="sharing"
|
||||
alignSelf="flex-end"
|
||||
spacing={{ base: "2", lg: "4" }}
|
||||
align="flex-end"
|
||||
onClick={maybeUnlockFocus}
|
||||
>
|
||||
<Box>
|
||||
<DownloadButton outfitState={outfitState} />
|
||||
</Box>
|
||||
<Box>
|
||||
<CopyLinkButton outfitState={outfitState} />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box gridArea="space" onClick={maybeUnlockFocus} />
|
||||
<Flex gridArea="picker" justify="center" onClick={maybeUnlockFocus}>
|
||||
{/**
|
||||
* We try to center the species/color picker, but the left spacer will
|
||||
* shrink more than the pose picker container if we run out of space!
|
||||
*/}
|
||||
<Box flex="1 1 0" />
|
||||
<Box flex="0 0 auto">
|
||||
<DarkMode>
|
||||
<SpeciesColorPicker
|
||||
speciesId={outfitState.speciesId}
|
||||
colorId={outfitState.colorId}
|
||||
idealPose={outfitState.pose}
|
||||
onChange={onSpeciesColorChange}
|
||||
stateMustAlwaysBeValid
|
||||
/>
|
||||
</DarkMode>
|
||||
</Box>
|
||||
<Flex flex="1 1 0" align="center" pl="4">
|
||||
<PosePicker
|
||||
speciesId={outfitState.speciesId}
|
||||
colorId={outfitState.colorId}
|
||||
pose={outfitState.pose}
|
||||
appearanceId={outfitState.appearanceId}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
onLockFocus={onLockFocus}
|
||||
onUnlockFocus={onUnlockFocus}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
<Stack
|
||||
gridArea="sharing"
|
||||
alignSelf="flex-end"
|
||||
spacing={{ base: "2", lg: "4" }}
|
||||
align="flex-end"
|
||||
onClick={maybeUnlockFocus}
|
||||
>
|
||||
<Box>
|
||||
<DownloadButton outfitState={outfitState} />
|
||||
</Box>
|
||||
<Box>
|
||||
<CopyLinkButton outfitState={outfitState} />
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box gridArea="space" onClick={maybeUnlockFocus} />
|
||||
<Flex gridArea="picker" justify="center" onClick={maybeUnlockFocus}>
|
||||
{/**
|
||||
* We try to center the species/color picker, but the left spacer will
|
||||
* shrink more than the pose picker container if we run out of space!
|
||||
*/}
|
||||
<Box flex="1 1 0" />
|
||||
<Box flex="0 0 auto">
|
||||
<DarkMode>
|
||||
<SpeciesColorPicker
|
||||
speciesId={outfitState.speciesId}
|
||||
colorId={outfitState.colorId}
|
||||
idealPose={outfitState.pose}
|
||||
onChange={onSpeciesColorChange}
|
||||
stateMustAlwaysBeValid
|
||||
/>
|
||||
</DarkMode>
|
||||
</Box>
|
||||
<Flex flex="1 1 0" align="center" pl="4">
|
||||
<PosePicker
|
||||
speciesId={outfitState.speciesId}
|
||||
colorId={outfitState.colorId}
|
||||
pose={outfitState.pose}
|
||||
appearanceId={outfitState.appearanceId}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
onLockFocus={onLockFocus}
|
||||
onUnlockFocus={onUnlockFocus}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -277,55 +283,59 @@ function PlayPauseButton() {
|
|||
}, [blinkInState, setBlinkInState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PlayPauseButtonContent
|
||||
isPaused={isPaused}
|
||||
setIsPaused={setIsPaused}
|
||||
marginTop="0.3rem" // to center-align with buttons (not sure on amt?)
|
||||
ref={buttonRef}
|
||||
/>
|
||||
{blinkInState.type === "started" && (
|
||||
<Portal>
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<>
|
||||
<PlayPauseButtonContent
|
||||
isPaused={isPaused}
|
||||
setIsPaused={setIsPaused}
|
||||
position="absolute"
|
||||
left={blinkInState.position.left}
|
||||
top={blinkInState.position.top}
|
||||
backgroundColor="gray.600"
|
||||
borderColor="gray.50"
|
||||
color="gray.50"
|
||||
onAnimationEnd={() => setBlinkInState({ type: "done" })}
|
||||
// Don't disrupt the hover state of the controls! (And the button
|
||||
// doesn't seem to click correctly, not sure why, but instead of
|
||||
// debugging I'm adding this :p)
|
||||
pointerEvents="none"
|
||||
className={css`
|
||||
@keyframes fade-in-out {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 0;
|
||||
animation: fade-in-out 2s;
|
||||
`}
|
||||
marginTop="0.3rem" // to center-align with buttons (not sure on amt?)
|
||||
ref={buttonRef}
|
||||
/>
|
||||
</Portal>
|
||||
{blinkInState.type === "started" && (
|
||||
<Portal>
|
||||
<PlayPauseButtonContent
|
||||
isPaused={isPaused}
|
||||
setIsPaused={setIsPaused}
|
||||
position="absolute"
|
||||
left={blinkInState.position.left}
|
||||
top={blinkInState.position.top}
|
||||
backgroundColor="gray.600"
|
||||
borderColor="gray.50"
|
||||
color="gray.50"
|
||||
onAnimationEnd={() => setBlinkInState({ type: "done" })}
|
||||
// Don't disrupt the hover state of the controls! (And the button
|
||||
// doesn't seem to click correctly, not sure why, but instead of
|
||||
// debugging I'm adding this :p)
|
||||
pointerEvents="none"
|
||||
className={css`
|
||||
@keyframes fade-in-out {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
10% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
opacity: 0;
|
||||
animation: fade-in-out 2s;
|
||||
`}
|
||||
/>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import gql from "graphql-tag";
|
||||
import { useQuery } from "@apollo/client";
|
||||
import { css, cx } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
@ -107,97 +107,101 @@ function PosePicker({
|
|||
};
|
||||
|
||||
return (
|
||||
<Popover
|
||||
placement="bottom-end"
|
||||
returnFocusOnClose
|
||||
onOpen={onLockFocus}
|
||||
onClose={onUnlockFocus}
|
||||
initialFocusRef={initialFocusRef}
|
||||
>
|
||||
{({ isOpen }) => (
|
||||
<>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
boxShadow="md"
|
||||
d="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
_focus={{ borderColor: "gray.50" }}
|
||||
_hover={{ borderColor: "gray.50" }}
|
||||
outline="initial"
|
||||
className={cx(
|
||||
css`
|
||||
border: 1px solid transparent !important;
|
||||
transition: border-color 0.2s !important;
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<Popover
|
||||
placement="bottom-end"
|
||||
returnFocusOnClose
|
||||
onOpen={onLockFocus}
|
||||
onClose={onUnlockFocus}
|
||||
initialFocusRef={initialFocusRef}
|
||||
>
|
||||
{({ isOpen }) => (
|
||||
<>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
variant="unstyled"
|
||||
boxShadow="md"
|
||||
d="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
_focus={{ borderColor: "gray.50" }}
|
||||
_hover={{ borderColor: "gray.50" }}
|
||||
outline="initial"
|
||||
className={cx(
|
||||
css`
|
||||
border: 1px solid transparent !important;
|
||||
transition: border-color 0.2s !important;
|
||||
|
||||
&:focus,
|
||||
&:hover,
|
||||
&.is-open {
|
||||
border-color: ${theme.colors.gray["50"]} !important;
|
||||
}
|
||||
&:focus,
|
||||
&:hover,
|
||||
&.is-open {
|
||||
border-color: ${theme.colors.gray["50"]} !important;
|
||||
}
|
||||
|
||||
&.is-open {
|
||||
border-width: 2px !important;
|
||||
}
|
||||
`,
|
||||
isOpen && "is-open"
|
||||
)}
|
||||
>
|
||||
<EmojiImage src={getIcon(pose)} alt="Choose a pose" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent>
|
||||
<Box p="4" position="relative">
|
||||
{isInSupportMode ? (
|
||||
<PosePickerSupport
|
||||
speciesId={speciesId}
|
||||
colorId={colorId}
|
||||
pose={pose}
|
||||
appearanceId={appearanceId}
|
||||
initialFocusRef={initialFocusRef}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<PosePickerTable
|
||||
poseInfos={poseInfos}
|
||||
onChange={onChange}
|
||||
initialFocusRef={initialFocusRef}
|
||||
/>
|
||||
{numAvailablePoses <= 1 && (
|
||||
<SupportOnly>
|
||||
<Box
|
||||
fontSize="xs"
|
||||
fontStyle="italic"
|
||||
textAlign="center"
|
||||
opacity="0.7"
|
||||
marginTop="2"
|
||||
>
|
||||
The empty picker is hidden for most users!
|
||||
<br />
|
||||
You can see it because you're a Support user.
|
||||
</Box>
|
||||
</SupportOnly>
|
||||
&.is-open {
|
||||
border-width: 2px !important;
|
||||
}
|
||||
`,
|
||||
isOpen && "is-open"
|
||||
)}
|
||||
>
|
||||
<EmojiImage src={getIcon(pose)} alt="Choose a pose" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<Portal>
|
||||
<PopoverContent>
|
||||
<Box p="4" position="relative">
|
||||
{isInSupportMode ? (
|
||||
<PosePickerSupport
|
||||
speciesId={speciesId}
|
||||
colorId={colorId}
|
||||
pose={pose}
|
||||
appearanceId={appearanceId}
|
||||
initialFocusRef={initialFocusRef}
|
||||
dispatchToOutfit={dispatchToOutfit}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<PosePickerTable
|
||||
poseInfos={poseInfos}
|
||||
onChange={onChange}
|
||||
initialFocusRef={initialFocusRef}
|
||||
/>
|
||||
{numAvailablePoses <= 1 && (
|
||||
<SupportOnly>
|
||||
<Box
|
||||
fontSize="xs"
|
||||
fontStyle="italic"
|
||||
textAlign="center"
|
||||
opacity="0.7"
|
||||
marginTop="2"
|
||||
>
|
||||
The empty picker is hidden for most users!
|
||||
<br />
|
||||
You can see it because you're a Support user.
|
||||
</Box>
|
||||
</SupportOnly>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<SupportOnly>
|
||||
<Box position="absolute" top="5" left="3">
|
||||
<PosePickerSupportSwitch
|
||||
isChecked={isInSupportMode}
|
||||
onChange={(e) => setIsInSupportMode(e.target.checked)}
|
||||
/>
|
||||
<SupportOnly>
|
||||
<Box position="absolute" top="5" left="3">
|
||||
<PosePickerSupportSwitch
|
||||
isChecked={isInSupportMode}
|
||||
onChange={(e) => setIsInSupportMode(e.target.checked)}
|
||||
/>
|
||||
</Box>
|
||||
</SupportOnly>
|
||||
</Box>
|
||||
</SupportOnly>
|
||||
</Box>
|
||||
<PopoverArrow />
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</>
|
||||
<PopoverArrow />
|
||||
</PopoverContent>
|
||||
</Portal>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
)}
|
||||
</Popover>
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -343,110 +347,118 @@ function PoseOption({
|
|||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="label"
|
||||
cursor="pointer"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
borderColor={poseInfo.isSelected ? borderColor : "gray.400"}
|
||||
boxShadow={label ? "md" : "none"}
|
||||
borderWidth={label ? "1px" : "0"}
|
||||
borderRadius={label ? "full" : "0"}
|
||||
paddingRight={label ? "3" : "0"}
|
||||
onClick={(e) => {
|
||||
// HACK: We need the timeout to beat the popover's focus stealing!
|
||||
const input = e.currentTarget.querySelector("input");
|
||||
setTimeout(() => input.focus(), 0);
|
||||
}}
|
||||
{...otherProps}
|
||||
>
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="radio"
|
||||
aria-label={poseName}
|
||||
name="pose"
|
||||
value={poseInfo.pose}
|
||||
checked={poseInfo.isSelected}
|
||||
disabled={!poseInfo.isAvailable}
|
||||
onChange={onChange}
|
||||
ref={inputRef || null}
|
||||
/>
|
||||
<Box
|
||||
aria-hidden
|
||||
borderRadius="full"
|
||||
boxShadow="md"
|
||||
overflow="hidden"
|
||||
width={size === "sm" ? "30px" : "50px"}
|
||||
height={size === "sm" ? "30px" : "50px"}
|
||||
title={
|
||||
poseInfo.isAvailable
|
||||
? // A lil debug output, so that we can quickly identify glitched
|
||||
// PetStates and manually mark them as glitched!
|
||||
window.location.hostname.includes("localhost") &&
|
||||
`#${poseInfo.id}`
|
||||
: "Not modeled yet"
|
||||
}
|
||||
position="relative"
|
||||
className={css`
|
||||
transform: scale(0.8);
|
||||
opacity: 0.8;
|
||||
transition: all 0.2s;
|
||||
|
||||
input:checked + & {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<Box
|
||||
borderRadius="full"
|
||||
position="absolute"
|
||||
top="0"
|
||||
bottom="0"
|
||||
left="0"
|
||||
right="0"
|
||||
zIndex="2"
|
||||
className={cx(
|
||||
css`
|
||||
border: 0px solid ${borderColor};
|
||||
transition: border-width 0.2s;
|
||||
|
||||
&.not-available {
|
||||
border-color: ${theme.colors.gray["500"]};
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
input:checked + * & {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
input:focus + * & {
|
||||
border-width: 3px;
|
||||
}
|
||||
`,
|
||||
!poseInfo.isAvailable && "not-available"
|
||||
)}
|
||||
/>
|
||||
{poseInfo.isAvailable ? (
|
||||
<Box width="100%" height="100%" transform={getTransform(poseInfo)}>
|
||||
<OutfitLayers visibleLayers={getVisibleLayers(poseInfo, [])} />
|
||||
</Box>
|
||||
) : (
|
||||
<Flex align="center" justify="center" width="100%" height="100%">
|
||||
<EmojiImage src={twemojiQuestion} boxSize="24px" />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
{label && (
|
||||
<Box
|
||||
marginLeft="2"
|
||||
fontSize="xs"
|
||||
fontWeight={poseInfo.isSelected ? "bold" : "normal"}
|
||||
as="label"
|
||||
cursor="pointer"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
borderColor={poseInfo.isSelected ? borderColor : "gray.400"}
|
||||
boxShadow={label ? "md" : "none"}
|
||||
borderWidth={label ? "1px" : "0"}
|
||||
borderRadius={label ? "full" : "0"}
|
||||
paddingRight={label ? "3" : "0"}
|
||||
onClick={(e) => {
|
||||
// HACK: We need the timeout to beat the popover's focus stealing!
|
||||
const input = e.currentTarget.querySelector("input");
|
||||
setTimeout(() => input.focus(), 0);
|
||||
}}
|
||||
{...otherProps}
|
||||
>
|
||||
{label}
|
||||
<VisuallyHidden
|
||||
as="input"
|
||||
type="radio"
|
||||
aria-label={poseName}
|
||||
name="pose"
|
||||
value={poseInfo.pose}
|
||||
checked={poseInfo.isSelected}
|
||||
disabled={!poseInfo.isAvailable}
|
||||
onChange={onChange}
|
||||
ref={inputRef || null}
|
||||
/>
|
||||
<Box
|
||||
aria-hidden
|
||||
borderRadius="full"
|
||||
boxShadow="md"
|
||||
overflow="hidden"
|
||||
width={size === "sm" ? "30px" : "50px"}
|
||||
height={size === "sm" ? "30px" : "50px"}
|
||||
title={
|
||||
poseInfo.isAvailable
|
||||
? // A lil debug output, so that we can quickly identify glitched
|
||||
// PetStates and manually mark them as glitched!
|
||||
window.location.hostname.includes("localhost") &&
|
||||
`#${poseInfo.id}`
|
||||
: "Not modeled yet"
|
||||
}
|
||||
position="relative"
|
||||
className={css`
|
||||
transform: scale(0.8);
|
||||
opacity: 0.8;
|
||||
transition: all 0.2s;
|
||||
|
||||
input:checked + & {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
borderRadius="full"
|
||||
position="absolute"
|
||||
top="0"
|
||||
bottom="0"
|
||||
left="0"
|
||||
right="0"
|
||||
zIndex="2"
|
||||
className={cx(
|
||||
css`
|
||||
border: 0px solid ${borderColor};
|
||||
transition: border-width 0.2s;
|
||||
|
||||
&.not-available {
|
||||
border-color: ${theme.colors.gray["500"]};
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
input:checked + * & {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
input:focus + * & {
|
||||
border-width: 3px;
|
||||
}
|
||||
`,
|
||||
!poseInfo.isAvailable && "not-available"
|
||||
)}
|
||||
/>
|
||||
{poseInfo.isAvailable ? (
|
||||
<Box
|
||||
width="100%"
|
||||
height="100%"
|
||||
transform={getTransform(poseInfo)}
|
||||
>
|
||||
<OutfitLayers visibleLayers={getVisibleLayers(poseInfo, [])} />
|
||||
</Box>
|
||||
) : (
|
||||
<Flex align="center" justify="center" width="100%" height="100%">
|
||||
<EmojiImage src={twemojiQuestion} boxSize="24px" />
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
{label && (
|
||||
<Box
|
||||
marginLeft="2"
|
||||
fontSize="xs"
|
||||
fontWeight={poseInfo.isSelected ? "bold" : "normal"}
|
||||
>
|
||||
{label}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
useColorModeValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { CloseIcon, SearchIcon } from "@chakra-ui/icons";
|
||||
import { css, cx } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import Autosuggest from "react-autosuggest";
|
||||
|
||||
/**
|
||||
|
@ -79,23 +79,27 @@ function SearchToolbar({
|
|||
({ containerProps, children }) => {
|
||||
const { className, ...otherContainerProps } = containerProps;
|
||||
return (
|
||||
<Box
|
||||
{...otherContainerProps}
|
||||
borderBottomRadius="md"
|
||||
boxShadow="md"
|
||||
overflow="hidden"
|
||||
transition="all 0.4s"
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
`
|
||||
<ClassNames>
|
||||
{({ css, cx }) => (
|
||||
<Box
|
||||
{...otherContainerProps}
|
||||
borderBottomRadius="md"
|
||||
boxShadow="md"
|
||||
overflow="hidden"
|
||||
transition="all 0.4s"
|
||||
className={cx(
|
||||
className,
|
||||
css`
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
`
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
</ClassNames>
|
||||
);
|
||||
},
|
||||
[]
|
||||
|
@ -111,108 +115,116 @@ function SearchToolbar({
|
|||
const focusBorderColor = useColorModeValue("green.600", "green.400");
|
||||
|
||||
return (
|
||||
<Autosuggest
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={({ value }) =>
|
||||
setSuggestions(getSuggestions(value, query, zoneLabels))
|
||||
}
|
||||
onSuggestionsClearRequested={() => setSuggestions([])}
|
||||
onSuggestionSelected={(e, { suggestion }) => {
|
||||
const valueWithoutLastWord = query.value.match(/^(.*?)\s*\S+$/)[1];
|
||||
onChange({
|
||||
...query,
|
||||
value: valueWithoutLastWord,
|
||||
filterToZoneLabel: suggestion.zoneLabel || query.filterToZoneLabel,
|
||||
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
|
||||
});
|
||||
}}
|
||||
getSuggestionValue={(zl) => zl}
|
||||
highlightFirstSuggestion={true}
|
||||
renderSuggestion={renderSuggestion}
|
||||
renderSuggestionsContainer={renderSuggestionsContainer}
|
||||
renderInputComponent={(props) => (
|
||||
<InputGroup>
|
||||
{queryFilterText ? (
|
||||
<InputLeftAddon>
|
||||
<SearchIcon color="gray.400" marginRight="3" />
|
||||
<Box fontSize="sm">{queryFilterText}</Box>
|
||||
</InputLeftAddon>
|
||||
) : (
|
||||
<InputLeftElement>
|
||||
<SearchIcon color="gray.400" />
|
||||
</InputLeftElement>
|
||||
)}
|
||||
<Input {...props} />
|
||||
{(query.value || queryFilterText) && (
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
color="gray.400"
|
||||
variant="ghost"
|
||||
colorScheme="green"
|
||||
aria-label="Clear search"
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
}}
|
||||
// Big style hacks here!
|
||||
height="calc(100% - 2px)"
|
||||
marginRight="2px"
|
||||
/>
|
||||
</InputRightElement>
|
||||
)}
|
||||
</InputGroup>
|
||||
)}
|
||||
inputProps={{
|
||||
// placeholder: "Search for items to add…",
|
||||
"aria-label": "Search for items to add…",
|
||||
focusBorderColor: focusBorderColor,
|
||||
value: query.value || "",
|
||||
ref: searchQueryRef,
|
||||
minWidth: 0,
|
||||
borderBottomRadius: suggestions.length > 0 ? "0" : "md",
|
||||
// HACK: Chakra isn't noticing the InputLeftElement swapping out
|
||||
// for the InputLeftAddon, so the styles aren't updating...
|
||||
// Hard override!
|
||||
className: css`
|
||||
padding-left: ${queryFilterText ? "1rem" : "2.5rem"} !important;
|
||||
border-bottom-left-radius: ${queryFilterText
|
||||
? "0"
|
||||
: "0.25rem"} !important;
|
||||
border-top-left-radius: ${queryFilterText
|
||||
? "0"
|
||||
: "0.25rem"} !important;
|
||||
`,
|
||||
onChange: (e, { newValue, method }) => {
|
||||
// The Autosuggest tries to change the _entire_ value of the element
|
||||
// when navigating suggestions, which isn't actually what we want.
|
||||
// Only accept value changes that are typed by the user!
|
||||
if (method === "type") {
|
||||
onChange({ ...query, value: newValue });
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Autosuggest
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={({ value }) =>
|
||||
setSuggestions(getSuggestions(value, query, zoneLabels))
|
||||
}
|
||||
},
|
||||
onKeyDown: (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (suggestions.length > 0) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
onChange(null);
|
||||
e.target.blur();
|
||||
} else if (e.key === "ArrowDown") {
|
||||
if (suggestions.length > 0) {
|
||||
return;
|
||||
}
|
||||
onMoveFocusDownToResults(e);
|
||||
} else if (e.key === "Backspace" && e.target.selectionStart === 0) {
|
||||
onSuggestionsClearRequested={() => setSuggestions([])}
|
||||
onSuggestionSelected={(e, { suggestion }) => {
|
||||
const valueWithoutLastWord = query.value.match(/^(.*?)\s*\S+$/)[1];
|
||||
onChange({
|
||||
...query,
|
||||
filterToItemKind: null,
|
||||
filterToZoneLabel: null,
|
||||
value: valueWithoutLastWord,
|
||||
filterToZoneLabel:
|
||||
suggestion.zoneLabel || query.filterToZoneLabel,
|
||||
filterToItemKind: suggestion.itemKind || query.filterToItemKind,
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
getSuggestionValue={(zl) => zl}
|
||||
highlightFirstSuggestion={true}
|
||||
renderSuggestion={renderSuggestion}
|
||||
renderSuggestionsContainer={renderSuggestionsContainer}
|
||||
renderInputComponent={(props) => (
|
||||
<InputGroup>
|
||||
{queryFilterText ? (
|
||||
<InputLeftAddon>
|
||||
<SearchIcon color="gray.400" marginRight="3" />
|
||||
<Box fontSize="sm">{queryFilterText}</Box>
|
||||
</InputLeftAddon>
|
||||
) : (
|
||||
<InputLeftElement>
|
||||
<SearchIcon color="gray.400" />
|
||||
</InputLeftElement>
|
||||
)}
|
||||
<Input {...props} />
|
||||
{(query.value || queryFilterText) && (
|
||||
<InputRightElement>
|
||||
<IconButton
|
||||
icon={<CloseIcon />}
|
||||
color="gray.400"
|
||||
variant="ghost"
|
||||
colorScheme="green"
|
||||
aria-label="Clear search"
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
}}
|
||||
// Big style hacks here!
|
||||
height="calc(100% - 2px)"
|
||||
marginRight="2px"
|
||||
/>
|
||||
</InputRightElement>
|
||||
)}
|
||||
</InputGroup>
|
||||
)}
|
||||
inputProps={{
|
||||
// placeholder: "Search for items to add…",
|
||||
"aria-label": "Search for items to add…",
|
||||
focusBorderColor: focusBorderColor,
|
||||
value: query.value || "",
|
||||
ref: searchQueryRef,
|
||||
minWidth: 0,
|
||||
borderBottomRadius: suggestions.length > 0 ? "0" : "md",
|
||||
// HACK: Chakra isn't noticing the InputLeftElement swapping out
|
||||
// for the InputLeftAddon, so the styles aren't updating...
|
||||
// Hard override!
|
||||
className: css`
|
||||
padding-left: ${queryFilterText ? "1rem" : "2.5rem"} !important;
|
||||
border-bottom-left-radius: ${queryFilterText
|
||||
? "0"
|
||||
: "0.25rem"} !important;
|
||||
border-top-left-radius: ${queryFilterText
|
||||
? "0"
|
||||
: "0.25rem"} !important;
|
||||
`,
|
||||
onChange: (e, { newValue, method }) => {
|
||||
// The Autosuggest tries to change the _entire_ value of the element
|
||||
// when navigating suggestions, which isn't actually what we want.
|
||||
// Only accept value changes that are typed by the user!
|
||||
if (method === "type") {
|
||||
onChange({ ...query, value: newValue });
|
||||
}
|
||||
},
|
||||
onKeyDown: (e) => {
|
||||
if (e.key === "Escape") {
|
||||
if (suggestions.length > 0) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
onChange(null);
|
||||
e.target.blur();
|
||||
} else if (e.key === "ArrowDown") {
|
||||
if (suggestions.length > 0) {
|
||||
return;
|
||||
}
|
||||
onMoveFocusDownToResults(e);
|
||||
} else if (
|
||||
e.key === "Backspace" &&
|
||||
e.target.selectionStart === 0
|
||||
) {
|
||||
onChange({
|
||||
...query,
|
||||
filterToItemKind: null,
|
||||
filterToZoneLabel: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from "react";
|
||||
import gql from "graphql-tag";
|
||||
import { useQuery, useMutation } from "@apollo/client";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
|
@ -454,71 +454,75 @@ function ItemSupportAppearanceLayer({
|
|||
const iconButtonColor = useColorModeValue("green.800", "gray.900");
|
||||
|
||||
return (
|
||||
<Box
|
||||
as="button"
|
||||
width="150px"
|
||||
textAlign="center"
|
||||
fontSize="xs"
|
||||
onClick={onOpen}
|
||||
>
|
||||
<Box
|
||||
width="150px"
|
||||
height="150px"
|
||||
marginBottom="1"
|
||||
boxShadow="md"
|
||||
borderRadius="md"
|
||||
position="relative"
|
||||
>
|
||||
<OutfitLayers visibleLayers={[...biologyLayers, itemLayer]} />
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
className={css`
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
as="button"
|
||||
width="150px"
|
||||
textAlign="center"
|
||||
fontSize="xs"
|
||||
onClick={onOpen}
|
||||
>
|
||||
<Box
|
||||
width="150px"
|
||||
height="150px"
|
||||
marginBottom="1"
|
||||
boxShadow="md"
|
||||
borderRadius="md"
|
||||
position="relative"
|
||||
>
|
||||
<OutfitLayers visibleLayers={[...biologyLayers, itemLayer]} />
|
||||
<Box
|
||||
className={css`
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
button:hover &,
|
||||
button:focus & {
|
||||
opacity: 1;
|
||||
}
|
||||
button:hover &,
|
||||
button:focus & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* On touch devices, always show the icon, to clarify that this is
|
||||
/* On touch devices, always show the icon, to clarify that this is
|
||||
* an interactable object! (Whereas I expect other devices to
|
||||
* discover things by exploratory hover or focus!) */
|
||||
@media (hover: none) {
|
||||
opacity: 1;
|
||||
}
|
||||
`}
|
||||
background={iconButtonBgColor}
|
||||
color={iconButtonColor}
|
||||
borderRadius="full"
|
||||
boxShadow="sm"
|
||||
position="absolute"
|
||||
bottom="2"
|
||||
right="2"
|
||||
padding="2"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="32px"
|
||||
height="32px"
|
||||
>
|
||||
<EditIcon
|
||||
boxSize="16px"
|
||||
position="relative"
|
||||
top="-2px"
|
||||
right="-1px"
|
||||
@media (hover: none) {
|
||||
opacity: 1;
|
||||
}
|
||||
`}
|
||||
background={iconButtonBgColor}
|
||||
color={iconButtonColor}
|
||||
borderRadius="full"
|
||||
boxShadow="sm"
|
||||
position="absolute"
|
||||
bottom="2"
|
||||
right="2"
|
||||
padding="2"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
width="32px"
|
||||
height="32px"
|
||||
>
|
||||
<EditIcon
|
||||
boxSize="16px"
|
||||
position="relative"
|
||||
top="-2px"
|
||||
right="-1px"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box fontWeight="bold">{itemLayer.zone.label}</Box>
|
||||
<Box>Zone ID: {itemLayer.zone.id}</Box>
|
||||
<Box>DTI ID: {itemLayer.id}</Box>
|
||||
<ItemLayerSupportModal
|
||||
item={item}
|
||||
itemLayer={itemLayer}
|
||||
outfitState={outfitState}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box fontWeight="bold">{itemLayer.zone.label}</Box>
|
||||
<Box>Zone ID: {itemLayer.zone.id}</Box>
|
||||
<Box>DTI ID: {itemLayer.id}</Box>
|
||||
<ItemLayerSupportModal
|
||||
item={item}
|
||||
itemLayer={itemLayer}
|
||||
outfitState={outfitState}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import { Box, useColorModeValue } from "@chakra-ui/react";
|
||||
import { createIcon } from "@chakra-ui/icons";
|
||||
|
||||
|
@ -21,74 +21,76 @@ function HangerSpinner({ size = "md", ...props }) {
|
|||
const color = useColorModeValue("green.500", "green.300");
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
className={css`
|
||||
/*
|
||||
Adapted from animate.css "swing". We spend 75% of the time swinging,
|
||||
then 25% of the time pausing before the next loop.
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
className={css`
|
||||
/*
|
||||
Adapted from animate.css "swing". We spend 75% of the time swinging,
|
||||
then 25% of the time pausing before the next loop.
|
||||
|
||||
We use this animation for folks who are okay with dizzy-ish motion.
|
||||
For reduced motion, we use a pulse-fade instead.
|
||||
*/
|
||||
@keyframes swing {
|
||||
15% {
|
||||
transform: rotate3d(0, 0, 1, 15deg);
|
||||
We use this animation for folks who are okay with dizzy-ish motion.
|
||||
For reduced motion, we use a pulse-fade instead.
|
||||
*/
|
||||
@keyframes swing {
|
||||
15% {
|
||||
transform: rotate3d(0, 0, 1, 15deg);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotate3d(0, 0, 1, -10deg);
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: rotate3d(0, 0, 1, 5deg);
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate3d(0, 0, 1, -5deg);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: rotate3d(0, 0, 1, -10deg);
|
||||
/*
|
||||
A homebrew fade-pulse animation. We use this for folks who don't
|
||||
like motion. It's an important accessibility thing!
|
||||
*/
|
||||
@keyframes fade-pulse {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
45% {
|
||||
transform: rotate3d(0, 0, 1, 5deg);
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: 1.2s infinite swing;
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
60% {
|
||||
transform: rotate3d(0, 0, 1, -5deg);
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: 1.6s infinite fade-pulse;
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate3d(0, 0, 1, 0deg);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
A homebrew fade-pulse animation. We use this for folks who don't
|
||||
like motion. It's an important accessibility thing!
|
||||
*/
|
||||
@keyframes fade-pulse {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
animation: 1.2s infinite swing;
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
animation: 1.6s infinite fade-pulse;
|
||||
}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<HangerIcon boxSize={boxSize} color={color} transition="color 0.2s" />
|
||||
</Box>
|
||||
</>
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<HangerIcon boxSize={boxSize} color={color} transition="color 0.2s" />
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import {
|
||||
Badge,
|
||||
Box,
|
||||
|
@ -102,59 +102,63 @@ export function ItemThumbnail({
|
|||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
width={size === "lg" ? "80px" : "50px"}
|
||||
height={size === "lg" ? "80px" : "50px"}
|
||||
transition="all 0.15s"
|
||||
transformOrigin="center"
|
||||
position="relative"
|
||||
className={css([
|
||||
{
|
||||
transform: "scale(0.8)",
|
||||
},
|
||||
!isDisabled &&
|
||||
!isActive && {
|
||||
[focusSelector]: {
|
||||
opacity: "0.9",
|
||||
transform: "scale(0.9)",
|
||||
},
|
||||
},
|
||||
!isDisabled &&
|
||||
isActive && {
|
||||
opacity: 1,
|
||||
transform: "none",
|
||||
},
|
||||
])}
|
||||
{...props}
|
||||
>
|
||||
<Box
|
||||
borderRadius="lg"
|
||||
boxShadow="md"
|
||||
border="1px"
|
||||
overflow="hidden"
|
||||
width="100%"
|
||||
height="100%"
|
||||
className={css([
|
||||
{
|
||||
borderColor: `${borderColor} !important`,
|
||||
},
|
||||
!isDisabled &&
|
||||
!isActive && {
|
||||
[focusSelector]: {
|
||||
borderColor: `${focusBorderColor} !important`,
|
||||
},
|
||||
},
|
||||
])}
|
||||
>
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
as="img"
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={safeImageUrl(item.thumbnailUrl)}
|
||||
alt={`Thumbnail art for ${item.name}`}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
width={size === "lg" ? "80px" : "50px"}
|
||||
height={size === "lg" ? "80px" : "50px"}
|
||||
transition="all 0.15s"
|
||||
transformOrigin="center"
|
||||
position="relative"
|
||||
className={css([
|
||||
{
|
||||
transform: "scale(0.8)",
|
||||
},
|
||||
!isDisabled &&
|
||||
!isActive && {
|
||||
[focusSelector]: {
|
||||
opacity: "0.9",
|
||||
transform: "scale(0.9)",
|
||||
},
|
||||
},
|
||||
!isDisabled &&
|
||||
isActive && {
|
||||
opacity: 1,
|
||||
transform: "none",
|
||||
},
|
||||
])}
|
||||
{...props}
|
||||
>
|
||||
<Box
|
||||
borderRadius="lg"
|
||||
boxShadow="md"
|
||||
border="1px"
|
||||
overflow="hidden"
|
||||
width="100%"
|
||||
height="100%"
|
||||
className={css([
|
||||
{
|
||||
borderColor: `${borderColor} !important`,
|
||||
},
|
||||
!isDisabled &&
|
||||
!isActive && {
|
||||
[focusSelector]: {
|
||||
borderColor: `${focusBorderColor} !important`,
|
||||
},
|
||||
},
|
||||
])}
|
||||
>
|
||||
<Box
|
||||
as="img"
|
||||
width="100%"
|
||||
height="100%"
|
||||
src={safeImageUrl(item.thumbnailUrl)}
|
||||
alt={`Thumbnail art for ${item.name}`}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -166,30 +170,34 @@ function ItemName({ children, isDisabled, focusSelector, ...props }) {
|
|||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
fontSize="md"
|
||||
transition="all 0.15s"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
className={
|
||||
!isDisabled &&
|
||||
css`
|
||||
${focusSelector} {
|
||||
opacity: 0.9;
|
||||
font-weight: ${theme.fontWeights.medium};
|
||||
}
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
fontSize="md"
|
||||
transition="all 0.15s"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
className={
|
||||
!isDisabled &&
|
||||
css`
|
||||
${focusSelector} {
|
||||
opacity: 0.9;
|
||||
font-weight: ${theme.fontWeights.medium};
|
||||
}
|
||||
|
||||
input:checked + .item-container & {
|
||||
opacity: 1;
|
||||
font-weight: ${theme.fontWeights.bold};
|
||||
input:checked + .item-container & {
|
||||
opacity: 1;
|
||||
font-weight: ${theme.fontWeights.bold};
|
||||
}
|
||||
`
|
||||
}
|
||||
`
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { Box, DarkMode, Flex, Text } from "@chakra-ui/react";
|
||||
import { WarningIcon } from "@chakra-ui/icons";
|
||||
import { css } from "@emotion/css";
|
||||
import { ClassNames } from "@emotion/react";
|
||||
import { CSSTransition, TransitionGroup } from "react-transition-group";
|
||||
|
||||
import OutfitMovieLayer, {
|
||||
|
@ -149,104 +149,113 @@ export function OutfitLayers({
|
|||
}, [setCanvasSize]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
pos="relative"
|
||||
height="100%"
|
||||
width="100%"
|
||||
// Create a stacking context, so the z-indexed layers don't escape!
|
||||
zIndex="0"
|
||||
ref={containerRef}
|
||||
>
|
||||
{placeholder && (
|
||||
<FullScreenCenter>
|
||||
<Box
|
||||
// We show the placeholder until there are visible layers, at which
|
||||
// point we fade it out.
|
||||
opacity={visibleLayers.length === 0 ? 1 : 0}
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Box
|
||||
pos="relative"
|
||||
height="100%"
|
||||
width="100%"
|
||||
// Create a stacking context, so the z-indexed layers don't escape!
|
||||
zIndex="0"
|
||||
ref={containerRef}
|
||||
>
|
||||
{placeholder && (
|
||||
<FullScreenCenter>
|
||||
<Box
|
||||
// We show the placeholder until there are visible layers, at which
|
||||
// point we fade it out.
|
||||
opacity={visibleLayers.length === 0 ? 1 : 0}
|
||||
transition="opacity 0.2s"
|
||||
>
|
||||
{placeholder}
|
||||
</Box>
|
||||
</FullScreenCenter>
|
||||
)}
|
||||
<TransitionGroup enter={false} exit={doTransitions}>
|
||||
{visibleLayers.map((layer) => (
|
||||
<CSSTransition
|
||||
// We manage the fade-in and fade-out separately! The fade-out
|
||||
// happens here, when the layer exits the DOM.
|
||||
key={layer.id}
|
||||
classNames={css`
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
`}
|
||||
timeout={200}
|
||||
>
|
||||
<FadeInOnLoad as={FullScreenCenter} zIndex={layer.zone.depth}>
|
||||
{layer.canvasMovieLibraryUrl ? (
|
||||
<OutfitMovieLayer
|
||||
libraryUrl={layer.canvasMovieLibraryUrl}
|
||||
width={canvasSize}
|
||||
height={canvasSize}
|
||||
isPaused={isPaused}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
as="img"
|
||||
src={getBestImageUrlForLayer(layer).src}
|
||||
// The crossOrigin prop isn't strictly necessary for loading
|
||||
// here (<img> tags are always allowed through CORS), but
|
||||
// this means we make the same request that the Download
|
||||
// button makes, so it can use the cached version of this
|
||||
// image instead of requesting it again with crossOrigin!
|
||||
crossOrigin={getBestImageUrlForLayer(layer).crossOrigin}
|
||||
alt=""
|
||||
objectFit="contain"
|
||||
maxWidth="100%"
|
||||
maxHeight="100%"
|
||||
/>
|
||||
)}
|
||||
</FadeInOnLoad>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
<FullScreenCenter
|
||||
zIndex="9000"
|
||||
// This is similar to our Delay util component, but Delay disappears
|
||||
// immediately on load, whereas we want this to fade out smoothly. We
|
||||
// also use a timeout to delay the fade-in by 0.5s, but don't delay the
|
||||
// fade-out at all. (The timeout was an awkward choice, it was hard to
|
||||
// find a good CSS way to specify this delay well!)
|
||||
opacity={loadingAnything && loadingDelayHasPassed ? 1 : 0}
|
||||
transition="opacity 0.2s"
|
||||
>
|
||||
{placeholder}
|
||||
</Box>
|
||||
</FullScreenCenter>
|
||||
)}
|
||||
<TransitionGroup enter={false} exit={doTransitions}>
|
||||
{visibleLayers.map((layer) => (
|
||||
<CSSTransition
|
||||
// We manage the fade-in and fade-out separately! The fade-out
|
||||
// happens here, when the layer exits the DOM.
|
||||
key={layer.id}
|
||||
classNames={css`
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
`}
|
||||
timeout={200}
|
||||
>
|
||||
<FadeInOnLoad as={FullScreenCenter} zIndex={layer.zone.depth}>
|
||||
{layer.canvasMovieLibraryUrl ? (
|
||||
<OutfitMovieLayer
|
||||
libraryUrl={layer.canvasMovieLibraryUrl}
|
||||
width={canvasSize}
|
||||
height={canvasSize}
|
||||
isPaused={isPaused}
|
||||
/>
|
||||
) : (
|
||||
{spinnerVariant === "overlay" && (
|
||||
<>
|
||||
<Box
|
||||
as="img"
|
||||
src={getBestImageUrlForLayer(layer).src}
|
||||
// The crossOrigin prop isn't strictly necessary for loading
|
||||
// here (<img> tags are always allowed through CORS), but
|
||||
// this means we make the same request that the Download
|
||||
// button makes, so it can use the cached version of this
|
||||
// image instead of requesting it again with crossOrigin!
|
||||
crossOrigin={getBestImageUrlForLayer(layer).crossOrigin}
|
||||
alt=""
|
||||
objectFit="contain"
|
||||
maxWidth="100%"
|
||||
maxHeight="100%"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
bottom="0"
|
||||
backgroundColor="gray.900"
|
||||
opacity="0.7"
|
||||
/>
|
||||
)}
|
||||
</FadeInOnLoad>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
<FullScreenCenter
|
||||
zIndex="9000"
|
||||
// This is similar to our Delay util component, but Delay disappears
|
||||
// immediately on load, whereas we want this to fade out smoothly. We
|
||||
// also use a timeout to delay the fade-in by 0.5s, but don't delay the
|
||||
// fade-out at all. (The timeout was an awkward choice, it was hard to
|
||||
// find a good CSS way to specify this delay well!)
|
||||
opacity={loadingAnything && loadingDelayHasPassed ? 1 : 0}
|
||||
transition="opacity 0.2s"
|
||||
>
|
||||
{spinnerVariant === "overlay" && (
|
||||
<>
|
||||
<Box
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
bottom="0"
|
||||
backgroundColor="gray.900"
|
||||
opacity="0.7"
|
||||
/>
|
||||
{/* Against the dark overlay, use the Dark Mode spinner. */}
|
||||
<DarkMode>
|
||||
<HangerSpinner />
|
||||
</DarkMode>
|
||||
</>
|
||||
)}
|
||||
{spinnerVariant === "corner" && (
|
||||
<HangerSpinner size="sm" position="absolute" bottom="2" right="2" />
|
||||
)}
|
||||
</FullScreenCenter>
|
||||
</Box>
|
||||
{/* Against the dark overlay, use the Dark Mode spinner. */}
|
||||
<DarkMode>
|
||||
<HangerSpinner />
|
||||
</DarkMode>
|
||||
</>
|
||||
)}
|
||||
{spinnerVariant === "corner" && (
|
||||
<HangerSpinner
|
||||
size="sm"
|
||||
position="absolute"
|
||||
bottom="2"
|
||||
right="2"
|
||||
/>
|
||||
)}
|
||||
</FullScreenCenter>
|
||||
</Box>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
11
yarn.lock
11
yarn.lock
|
@ -2750,17 +2750,6 @@
|
|||
"@emotion/weak-memoize" "^0.2.5"
|
||||
stylis "^4.0.3"
|
||||
|
||||
"@emotion/css@^11.1.3":
|
||||
version "11.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.1.3.tgz#9ed44478b19e5d281ccbbd46d74d123d59be793f"
|
||||
integrity sha512-RSQP59qtCNTf5NWD6xM08xsQdCZmVYnX/panPYvB6LQAPKQB6GL49Njf0EMbS3CyDtrlWsBcmqBtysFvfWT3rA==
|
||||
dependencies:
|
||||
"@emotion/babel-plugin" "^11.0.0"
|
||||
"@emotion/cache" "^11.1.3"
|
||||
"@emotion/serialize" "^1.0.0"
|
||||
"@emotion/sheet" "^1.0.0"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
|
||||
"@emotion/hash@^0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413"
|
||||
|
|
Loading…
Reference in a new issue