Add letter icon option for collections

This commit is contained in:
Tom Moor
2023-09-15 08:54:22 -04:00
parent b79f86d347
commit 6b4feb51e0
9 changed files with 107 additions and 25 deletions

View File

@@ -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) => {
<Position {...menuProps}>
<Background
dir="auto"
maxWidth={props.maxWidth}
topAnchor={topAnchor}
rightAnchor={rightAnchor}
ref={backgroundRef}
@@ -226,6 +230,7 @@ export const Position = styled.div`
type BackgroundProps = {
topAnchor?: boolean;
rightAnchor?: boolean;
maxWidth?: number;
theme: DefaultTheme;
};
@@ -251,7 +256,7 @@ export const Background = styled(Scrollable)<BackgroundProps>`
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};
`};

View File

@@ -116,6 +116,7 @@ function DocumentCard(props: Props) {
) : (
<Squircle color={collection?.color}>
{collection?.icon &&
collection?.icon !== "letter" &&
collection?.icon !== "collection" &&
!pin?.collectionId ? (
<CollectionIcon collection={collection} color="white" />

View File

@@ -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}
</Icon>
</Button>
)}
</MenuButton>
@@ -238,6 +248,7 @@ function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) {
{...menu}
onOpen={onOpen}
onClose={onClose}
maxWidth={308}
aria-label={t("Choose icon")}
>
<Icons>
@@ -251,13 +262,14 @@ function IconPicker({ onOpen, onClose, icon, color, onChange }: Props) {
<IconButton
style={
{
...style,
"--delay": `${index * 8}ms`,
} as React.CSSProperties
}
{...props}
>
<Icon as={icons[name].component} color={color} size={30} />
<Icon as={icons[name].component} color={color} size={30}>
{initial}
</Icon>
</IconButton>
)}
</MenuItem>
@@ -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;

View File

@@ -39,7 +39,11 @@ function ResolvedCollectionIcon({
if (collection.icon && collection.icon !== "collection") {
try {
const Component = icons[collection.icon].component;
return <Component color={color} size={size} />;
return (
<Component color={color} size={size}>
{collection.initial}
</Component>
);
} catch (error) {
Logger.warn("Failed to render custom icon", {
icon: collection.icon,

View File

@@ -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) => (
<LetterIconWrapper $size={size}>
<Squircle size={size - 10} {...rest}>
{children}
</Squircle>
</LetterIconWrapper>
);
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;

View File

@@ -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<Props> = ({ color, size = 28, children }: Props) => (
<Wrapper
style={{ width: size, height: size }}
align="center"
justify="center"
>
<svg width={size} height={size} viewBox="0 0 28 28">
<path
fill={color}
d="M0 11.1776C0 1.97285 1.97285 0 11.1776 0H16.8224C26.0272 0 28 1.97285 28 11.1776V16.8224C28 26.0272 26.0272 28 16.8224 28H11.1776C1.97285 28 0 26.0272 0 16.8224V11.1776Z"
/>
const Squircle: React.FC<Props> = ({
color,
size = 28,
children,
className,
}: Props) => (
<Wrapper size={size} align="center" justify="center" className={className}>
<svg width={size} height={size} fill={color} viewBox="0 0 28 28">
<path d="M0 11.1776C0 1.97285 1.97285 0 11.1776 0H16.8224C26.0272 0 28 1.97285 28 11.1776V16.8224C28 26.0272 26.0272 28 16.8224 28H11.1776C1.97285 28 0 26.0272 0 16.8224V11.1776Z" />
</svg>
<Content>{children}</Content>
</Wrapper>
);
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`

View File

@@ -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;

View File

@@ -100,7 +100,12 @@ const CollectionEdit = ({ collectionId, onSubmit }: Props) => {
autoFocus
flex
/>
<IconPicker onChange={handleChange} color={color} icon={icon} />
<IconPicker
onChange={handleChange}
color={color}
initial={name[0]}
icon={icon}
/>
</Flex>
<InputSelect
label={t("Sort in sidebar")}

View File

@@ -141,6 +141,7 @@ class CollectionNew extends React.Component<Props> {
<IconPicker
onOpen={this.handleIconPickerOpen}
onChange={this.handleChange}
initial={this.name[0]}
color={this.color}
icon={this.icon}
/>