Add letter icon option for collections
This commit is contained in:
@@ -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};
|
||||
`};
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
35
app/components/Icons/LetterIcon.tsx
Normal file
35
app/components/Icons/LetterIcon.tsx
Normal 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;
|
||||
@@ -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`
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user