From 6b4feb51e0013532d4baf64379fa7d63f87a8701 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 15 Sep 2023 08:54:22 -0400 Subject: [PATCH] Add letter icon option for collections --- app/components/ContextMenu/index.tsx | 7 ++++- app/components/DocumentCard.tsx | 1 + app/components/IconPicker.tsx | 33 ++++++++++++++++------- app/components/Icons/CollectionIcon.tsx | 6 ++++- app/components/Icons/LetterIcon.tsx | 35 +++++++++++++++++++++++++ app/components/Squircle.tsx | 32 +++++++++++++--------- app/models/Collection.ts | 10 +++++++ app/scenes/CollectionEdit.tsx | 7 ++++- app/scenes/CollectionNew.tsx | 1 + 9 files changed, 107 insertions(+), 25 deletions(-) create mode 100644 app/components/Icons/LetterIcon.tsx diff --git a/app/components/ContextMenu/index.tsx b/app/components/ContextMenu/index.tsx index ac78a027d..a0ac33a07 100644 --- a/app/components/ContextMenu/index.tsx +++ b/app/components/ContextMenu/index.tsx @@ -46,6 +46,8 @@ type Props = MenuStateReturn & { onClose?: () => void; /** Called when the context menu is clicked. */ onClick?: (ev: React.MouseEvent) => void; + /** The maximum width of the context menu. */ + maxWidth?: number; children?: React.ReactNode; }; @@ -123,6 +125,7 @@ type InnerContextMenuProps = MenuStateReturn & { isSubMenu: boolean; menuProps: { style?: React.CSSProperties; placement: string }; children: React.ReactNode; + maxWidth?: number; }; /** @@ -173,6 +176,7 @@ const InnerContextMenu = (props: InnerContextMenuProps) => { ` props.topAnchor ? fadeAndSlideDown : fadeAndSlideUp} 200ms ease; transform-origin: ${(props: BackgroundProps) => props.rightAnchor ? "75%" : "25%"} 0; - max-width: 276px; + max-width: ${(props: BackgroundProps) => props.maxWidth ?? 276}px; background: ${(props: BackgroundProps) => props.theme.menuBackground}; box-shadow: ${(props: BackgroundProps) => props.theme.menuShadow}; `}; diff --git a/app/components/DocumentCard.tsx b/app/components/DocumentCard.tsx index 23345d10f..5da426d51 100644 --- a/app/components/DocumentCard.tsx +++ b/app/components/DocumentCard.tsx @@ -116,6 +116,7 @@ function DocumentCard(props: Props) { ) : ( {collection?.icon && + collection?.icon !== "letter" && collection?.icon !== "collection" && !pin?.collectionId ? ( diff --git a/app/components/IconPicker.tsx b/app/components/IconPicker.tsx index 84a861d9c..6fea6a2eb 100644 --- a/app/components/IconPicker.tsx +++ b/app/components/IconPicker.tsx @@ -49,11 +49,7 @@ import NudeButton from "~/components/NudeButton"; import Text from "~/components/Text"; import lazyWithRetry from "~/utils/lazyWithRetry"; import DelayedMount from "./DelayedMount"; - -const style = { - width: 30, - height: 30, -}; +import LetterIcon from "./Icons/LetterIcon"; const TwitterPicker = lazyWithRetry( () => import("react-color/lib/components/twitter/Twitter") @@ -136,6 +132,10 @@ export const icons = { component: LightningIcon, keywords: "lightning fast zap", }, + letter: { + component: LetterIcon, + keywords: "letter", + }, math: { component: MathIcon, keywords: "math formula", @@ -206,11 +206,19 @@ type Props = { onOpen?: () => void; onClose?: () => void; onChange: (color: string, icon: string) => void; + initial: string; icon: string; color: string; }; -function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) { +function IconPicker({ + onOpen, + onClose, + icon, + initial, + color, + onChange, +}: Props) { const { t } = useTranslation(); const theme = useTheme(); const menu = useMenuState({ @@ -230,7 +238,9 @@ function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) { as={icons[icon || "collection"].component} color={color} size={30} - /> + > + {initial} + )} @@ -238,6 +248,7 @@ function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) { {...menu} onOpen={onOpen} onClose={onClose} + maxWidth={308} aria-label={t("Choose icon")} > @@ -251,13 +262,14 @@ function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) { - + + {initial} + )} @@ -318,7 +330,7 @@ const Icons = styled.div` padding: 8px; ${breakpoint("tablet")` - width: 276px; + width: 304px; `}; `; @@ -329,6 +341,7 @@ const Button = styled(NudeButton)` `; const IconButton = styled(NudeButton)` + vertical-align: top; border-radius: 4px; margin: 0px 6px 6px 0px; width: 30px; diff --git a/app/components/Icons/CollectionIcon.tsx b/app/components/Icons/CollectionIcon.tsx index cef77a8f8..52cddac24 100644 --- a/app/components/Icons/CollectionIcon.tsx +++ b/app/components/Icons/CollectionIcon.tsx @@ -39,7 +39,11 @@ function ResolvedCollectionIcon({ if (collection.icon && collection.icon !== "collection") { try { const Component = icons[collection.icon].component; - return ; + return ( + + {collection.initial} + + ); } catch (error) { Logger.warn("Failed to render custom icon", { icon: collection.icon, diff --git a/app/components/Icons/LetterIcon.tsx b/app/components/Icons/LetterIcon.tsx new file mode 100644 index 000000000..3cd76dcae --- /dev/null +++ b/app/components/Icons/LetterIcon.tsx @@ -0,0 +1,35 @@ +import * as React from "react"; +import styled from "styled-components"; +import { s } from "@shared/styles"; +import Squircle from "../Squircle"; + +type Props = { + /** The width and height of the icon, including standard padding. */ + size?: number; + children: React.ReactNode; +}; + +/** + * A squircle shaped icon with a letter inside, used for collections. + */ +const LetterIcon = ({ children, size = 24, ...rest }: Props) => ( + + + {children} + + +); + +const LetterIconWrapper = styled.div<{ $size: number }>` + display: inline-flex; + align-items: center; + justify-content: center; + width: ${({ $size }) => $size}px; + height: ${({ $size }) => $size}px; + + font-weight: 500; + font-size: ${({ $size }) => $size / 2}px; + color: ${s("background")}; +`; + +export default LetterIcon; diff --git a/app/components/Squircle.tsx b/app/components/Squircle.tsx index 59cebd147..a0dbc5657 100644 --- a/app/components/Squircle.tsx +++ b/app/components/Squircle.tsx @@ -3,29 +3,37 @@ import styled from "styled-components"; import Flex from "./Flex"; type Props = { + /** The width and height of the squircle */ size?: number; + /** The color of the squircle */ color?: string; children?: React.ReactNode; + className?: string; }; -const Squircle: React.FC = ({ color, size = 28, children }: Props) => ( - - - +const Squircle: React.FC = ({ + color, + size = 28, + children, + className, +}: Props) => ( + + + {children} ); -const Wrapper = styled(Flex)` +const Wrapper = styled(Flex)<{ size: number }>` position: relative; + width: ${(props) => props.size}px; + height: ${(props) => props.size}px; + + svg { + transition: fill 150ms ease-in-out; + transition-delay: var(--delay); + } `; const Content = styled.div` diff --git a/app/models/Collection.ts b/app/models/Collection.ts index 7a81eed0e..38e738a48 100644 --- a/app/models/Collection.ts +++ b/app/models/Collection.ts @@ -126,6 +126,16 @@ export default class Collection extends ParanoidModel { return sortNavigationNodes(this.documents, this.sort); } + /** + * The initial letter of the collection name. + * + * @returns string + */ + @computed + get initial() { + return this.name ? this.name[0] : "?"; + } + fetchDocuments = async (options?: { force: boolean }) => { if (this.isFetching) { return; diff --git a/app/scenes/CollectionEdit.tsx b/app/scenes/CollectionEdit.tsx index 31a235ec7..607723da2 100644 --- a/app/scenes/CollectionEdit.tsx +++ b/app/scenes/CollectionEdit.tsx @@ -100,7 +100,12 @@ const CollectionEdit = ({ collectionId, onSubmit }: Props) => { autoFocus flex /> - + {