@@ -3,11 +3,10 @@ import { CSS } from "@dnd-kit/utilities";
|
||||
import { m } from "framer-motion";
|
||||
import { observer } from "mobx-react";
|
||||
import { CloseIcon, DocumentIcon, ClockIcon } from "outline-icons";
|
||||
import { getLuminance, transparentize } from "polished";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import styled, { css } from "styled-components";
|
||||
import styled, { useTheme } from "styled-components";
|
||||
import Document from "~/models/Document";
|
||||
import Pin from "~/models/Pin";
|
||||
import Flex from "~/components/Flex";
|
||||
@@ -15,6 +14,8 @@ import NudeButton from "~/components/NudeButton";
|
||||
import Time from "~/components/Time";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import CollectionIcon from "./CollectionIcon";
|
||||
import EmojiIcon from "./EmojiIcon";
|
||||
import Squircle from "./Squircle";
|
||||
import Text from "./Text";
|
||||
import Tooltip from "./Tooltip";
|
||||
|
||||
@@ -32,6 +33,7 @@ type Props = {
|
||||
function DocumentCard(props: Props) {
|
||||
const { t } = useTranslation();
|
||||
const { collections } = useStores();
|
||||
const theme = useTheme();
|
||||
const { document, pin, canUpdatePin, isDraggable } = props;
|
||||
const collection = collections.get(document.collectionId);
|
||||
const {
|
||||
@@ -41,16 +43,24 @@ function DocumentCard(props: Props) {
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: props.document.id });
|
||||
} = useSortable({
|
||||
id: props.document.id,
|
||||
disabled: !isDraggable || !canUpdatePin,
|
||||
});
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
};
|
||||
|
||||
const handleUnpin = React.useCallback(() => {
|
||||
pin?.delete();
|
||||
}, [pin]);
|
||||
const handleUnpin = React.useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
pin?.delete();
|
||||
},
|
||||
[pin]
|
||||
);
|
||||
|
||||
return (
|
||||
<Reorderable
|
||||
@@ -58,6 +68,7 @@ function DocumentCard(props: Props) {
|
||||
style={style}
|
||||
$isDragging={isDragging}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
>
|
||||
<AnimatePresence
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
@@ -73,12 +84,6 @@ function DocumentCard(props: Props) {
|
||||
>
|
||||
<DocumentLink
|
||||
dir={document.dir}
|
||||
style={{
|
||||
background:
|
||||
collection?.color && getLuminance(collection.color) < 0.6
|
||||
? collection.color
|
||||
: undefined,
|
||||
}}
|
||||
$isDragging={isDragging}
|
||||
to={{
|
||||
pathname: document.url,
|
||||
@@ -88,89 +93,111 @@ function DocumentCard(props: Props) {
|
||||
}}
|
||||
>
|
||||
<Content justify="space-between" column>
|
||||
{collection?.icon &&
|
||||
collection?.icon !== "collection" &&
|
||||
!pin?.collectionId ? (
|
||||
<CollectionIcon collection={collection} color="white" />
|
||||
<Fold
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M19.5 19.5H6C2.96243 19.5 0.5 17.0376 0.5 14V0.5H0.792893L19.5 19.2071V19.5Z" />
|
||||
</Fold>
|
||||
|
||||
{document.emoji ? (
|
||||
<Squircle color={theme.slateLight}>
|
||||
<EmojiIcon emoji={document.emoji} size={26} />
|
||||
</Squircle>
|
||||
) : (
|
||||
<DocumentIcon color="white" />
|
||||
<Squircle color={collection?.color}>
|
||||
{collection?.icon &&
|
||||
collection?.icon !== "collection" &&
|
||||
!pin?.collectionId ? (
|
||||
<CollectionIcon collection={collection} color="white" />
|
||||
) : (
|
||||
<DocumentIcon color="white" />
|
||||
)}
|
||||
</Squircle>
|
||||
)}
|
||||
<div>
|
||||
<Heading dir={document.dir}>{document.titleWithDefault}</Heading>
|
||||
<Heading dir={document.dir}>
|
||||
{document.emoji
|
||||
? document.titleWithDefault.replace(document.emoji, "")
|
||||
: document.titleWithDefault}
|
||||
</Heading>
|
||||
<DocumentMeta size="xsmall">
|
||||
<ClockIcon color="currentColor" size={18} />{" "}
|
||||
<Time dateTime={document.updatedAt} addSuffix shorten />
|
||||
<Time
|
||||
dateTime={document.updatedAt}
|
||||
tooltipDelay={500}
|
||||
addSuffix
|
||||
shorten
|
||||
/>
|
||||
</DocumentMeta>
|
||||
</div>
|
||||
</Content>
|
||||
{canUpdatePin && (
|
||||
<Actions dir={document.dir} gap={4}>
|
||||
{!isDragging && pin && (
|
||||
<Tooltip tooltip={t("Unpin")}>
|
||||
<PinButton onClick={handleUnpin} aria-label={t("Unpin")}>
|
||||
<CloseIcon color="currentColor" />
|
||||
</PinButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Actions>
|
||||
)}
|
||||
</DocumentLink>
|
||||
{canUpdatePin && (
|
||||
<Actions dir={document.dir} gap={4}>
|
||||
{!isDragging && pin && (
|
||||
<Tooltip tooltip={t("Unpin")}>
|
||||
<PinButton onClick={handleUnpin} aria-label={t("Unpin")}>
|
||||
<CloseIcon color="currentColor" />
|
||||
</PinButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isDraggable && (
|
||||
<DragHandle $isDragging={isDragging} {...listeners}>
|
||||
:::
|
||||
</DragHandle>
|
||||
)}
|
||||
</Actions>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Reorderable>
|
||||
);
|
||||
}
|
||||
|
||||
const AnimatePresence = styled(m.div)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
const Fold = styled.svg`
|
||||
fill: ${(props) => props.theme.background};
|
||||
stroke: ${(props) => props.theme.inputBorder};
|
||||
background: ${(props) => props.theme.background};
|
||||
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -2px;
|
||||
`;
|
||||
|
||||
const PinButton = styled(NudeButton)`
|
||||
color: ${(props) => props.theme.white75};
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: ${(props) => props.theme.white};
|
||||
color: ${(props) => props.theme.text};
|
||||
}
|
||||
`;
|
||||
|
||||
const Actions = styled(Flex)`
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: ${(props) => (props.dir === "rtl" ? "auto" : "12px")};
|
||||
left: ${(props) => (props.dir === "rtl" ? "12px" : "auto")};
|
||||
top: 4px;
|
||||
right: ${(props) => (props.dir === "rtl" ? "auto" : "4px")};
|
||||
left: ${(props) => (props.dir === "rtl" ? "4px" : "auto")};
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-in-out;
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
|
||||
// move actions above content
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const DragHandle = styled.div<{ $isDragging: boolean }>`
|
||||
cursor: ${(props) => (props.$isDragging ? "grabbing" : "grab")};
|
||||
padding: 0 4px;
|
||||
font-weight: bold;
|
||||
color: ${(props) => props.theme.white75};
|
||||
line-height: 1.35;
|
||||
|
||||
&:hover,
|
||||
&:active {
|
||||
color: ${(props) => props.theme.white};
|
||||
}
|
||||
`;
|
||||
|
||||
const AnimatePresence = m.div;
|
||||
|
||||
const Reorderable = styled.div<{ $isDragging: boolean }>`
|
||||
position: relative;
|
||||
user-select: none;
|
||||
border-radius: 8px;
|
||||
touch-action: none;
|
||||
width: 170px;
|
||||
height: 180px;
|
||||
transition: box-shadow 200ms ease;
|
||||
|
||||
// move above other cards when dragging
|
||||
z-index: ${(props) => (props.$isDragging ? 1 : "inherit")};
|
||||
transform: scale(${(props) => (props.$isDragging ? "1.025" : "1")});
|
||||
box-shadow: ${(props) =>
|
||||
props.$isDragging ? "0 0 20px rgba(0,0,0,0.3);" : "0 0 0 rgba(0,0,0,0)"};
|
||||
|
||||
&:hover ${Actions} {
|
||||
opacity: 1;
|
||||
@@ -180,45 +207,30 @@ const Reorderable = styled.div<{ $isDragging: boolean }>`
|
||||
const Content = styled(Flex)`
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
|
||||
// move content above ::after
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const DocumentMeta = styled(Text)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
color: ${(props) => transparentize(0.25, props.theme.white)};
|
||||
margin: 0;
|
||||
color: ${(props) => props.theme.textTertiary};
|
||||
margin: 0 0 0 -2px;
|
||||
`;
|
||||
|
||||
const DocumentLink = styled(Link)<{
|
||||
$menuOpen?: boolean;
|
||||
$isDragging?: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 12px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
height: 160px;
|
||||
background: ${(props) => props.theme.slate};
|
||||
color: ${(props) => props.theme.white};
|
||||
background: ${(props) => props.theme.background};
|
||||
transition: transform 50ms ease-in-out;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.1));
|
||||
border-radius: 8px;
|
||||
pointer-events: none;
|
||||
}
|
||||
border: 1px solid ${(props) => props.theme.inputBorder};
|
||||
border-bottom-width: 2px;
|
||||
border-right-width: 2px;
|
||||
|
||||
${Actions} {
|
||||
opacity: 0;
|
||||
@@ -228,28 +240,22 @@ const DocumentLink = styled(Link)<{
|
||||
&:active,
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
transform: ${(props) => (props.$isDragging ? "scale(1.1)" : "scale(1.08)")}
|
||||
rotate(-2deg);
|
||||
box-shadow: ${(props) =>
|
||||
props.$isDragging
|
||||
? "0 0 20px rgba(0,0,0,0.2);"
|
||||
: "0 0 10px rgba(0,0,0,0.1)"};
|
||||
z-index: 1;
|
||||
|
||||
${Fold} {
|
||||
display: none;
|
||||
}
|
||||
|
||||
${Actions} {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
!props.$isDragging &&
|
||||
css`
|
||||
&:after {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.$menuOpen &&
|
||||
css`
|
||||
background: ${(props) => props.theme.listItemHoverBackground};
|
||||
|
||||
${Actions} {
|
||||
opacity: 1;
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const Heading = styled.h3`
|
||||
@@ -259,7 +265,7 @@ const Heading = styled.h3`
|
||||
max-height: 66px; // 3*line-height
|
||||
overflow: hidden;
|
||||
|
||||
color: ${(props) => props.theme.white};
|
||||
color: ${(props) => props.theme.text};
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user