diff --git a/app/components/DocumentCard.tsx b/app/components/DocumentCard.tsx index 7fce7b7bb..db51a0857 100644 --- a/app/components/DocumentCard.tsx +++ b/app/components/DocumentCard.tsx @@ -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 ( - {collection?.icon && - collection?.icon !== "collection" && - !pin?.collectionId ? ( - + + + + + {document.emoji ? ( + + + ) : ( - + + {collection?.icon && + collection?.icon !== "collection" && + !pin?.collectionId ? ( + + ) : ( + + )} + )}
- {document.titleWithDefault} + + {document.emoji + ? document.titleWithDefault.replace(document.emoji, "") + : document.titleWithDefault} + {" "} -
+ {canUpdatePin && ( + + {!isDragging && pin && ( + + + + + + )} + + )}
- {canUpdatePin && ( - - {!isDragging && pin && ( - - - - - - )} - {isDraggable && ( - - ::: - - )} - - )}
); } +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; `; diff --git a/app/components/PinnedDocuments.tsx b/app/components/PinnedDocuments.tsx index ce466afa7..9fbeba93b 100644 --- a/app/components/PinnedDocuments.tsx +++ b/app/components/PinnedDocuments.tsx @@ -42,7 +42,12 @@ function PinnedDocuments({ limit, pins, canUpdate, ...rest }: Props) { }, [pins]); const sensors = useSensors( - useSensor(PointerSensor), + useSensor(PointerSensor, { + activationConstraint: { + delay: 150, + tolerance: 5, + }, + }), useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates, }) @@ -122,7 +127,7 @@ const List = styled.div` display: grid; column-gap: 8px; row-gap: 8px; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); padding: 0; list-style: none; margin: 16px 0 32px; @@ -132,10 +137,6 @@ const List = styled.div` } ${breakpoint("tablet")` - grid-template-columns: repeat(3, minmax(0, 1fr)); - `}; - - ${breakpoint("desktop")` grid-template-columns: repeat(4, minmax(0, 1fr)); `}; `; diff --git a/app/components/Squircle.tsx b/app/components/Squircle.tsx new file mode 100644 index 000000000..880d36809 --- /dev/null +++ b/app/components/Squircle.tsx @@ -0,0 +1,40 @@ +import * as React from "react"; +import styled from "styled-components"; +import Flex from "./Flex"; + +type Props = { + size?: number; + color?: string; +}; + +const Squircle: React.FC = ({ color, size = 28, children }) => { + return ( + + + + + {children} + + ); +}; + +const Wrapper = styled(Flex)` + position: relative; +`; + +const Content = styled.div` + display: flex; + transform: translate(-50%, -50%); + position: absolute; + top: 50%; + left: 50%; +`; + +export default Squircle;