Skip to content

Commit

Permalink
clean-up sidebar / more Tags usage
Browse files Browse the repository at this point in the history
- clean-up sidebar styling (consistent headings, real tags, remove evergreen components)
- more abstraction around Tag component (variants, etc)
- abstract Collapse
- replace Badge with Tag in documents index view
  • Loading branch information
cloverich committed Sep 13, 2024
1 parent d86d1d0 commit 73d4306
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 93 deletions.
32 changes: 32 additions & 0 deletions src/components/Collapse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react";

import { Icons } from "./icons";

type CollapseProps = React.PropsWithChildren<{
defaultOpen?: boolean;
heading: string;
}>;

/**
* Collapse component that can be toggled open and closed.
*/
export function Collapse({ heading, defaultOpen, children }: CollapseProps) {
const [isOpen, setIsOpen] = React.useState(
defaultOpen == null ? false : defaultOpen,
);

const Icon = isOpen ? Icons.chevronDown : Icons.chevronRight;

return (
<div>
<div
className="text-md mb-2 flex cursor-pointer items-center font-medium tracking-tight"
onClick={() => setIsOpen(!isOpen)}
>
{heading}
<Icon size={18} />
</div>
{isOpen && children}
</div>
);
}
2 changes: 1 addition & 1 deletion src/components/Sidesheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const SheetOverlay = React.forwardRef<
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;

const sheetVariants = cva(
"drag-none fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300",
"drag-none fixed z-50 gap-4 bg-background p-4 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300",
{
variants: {
side: {
Expand Down
97 changes: 84 additions & 13 deletions src/components/TagInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cn } from "@udecode/cn";
import { cva } from "class-variance-authority";
import { observable } from "mobx";
import { observer } from "mobx-react-lite";
import * as React from "react";
Expand All @@ -16,12 +17,18 @@ interface TagInputProps {
placeholder?: string;
/** When true, hide the borders / disable padding */
ghost?: boolean;
/** A lazy hack to make the editors tags always start with a hash */
prefixHash?: boolean;
}

/**
* A multi-select input where values appear as tags
*/
const TagInput = observer((props: TagInputProps) => {
const inputRef = React.useRef<HTMLInputElement>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const [dropdown, _] = React.useState(observable({ open: false }));
const hash = props.prefixHash ? "#" : null;

// Close the typeahead menu when clicking outside of the dropdown
React.useEffect(() => {
Expand Down Expand Up @@ -53,7 +60,10 @@ const TagInput = observer((props: TagInputProps) => {
)}
>
{props.tokens.map((token, idx) => (
<Tag key={idx} token={token} remove={props.onRemove} />
<CloseableTag key={idx} remove={() => props.onRemove(token)}>
{hash}
{token}
</CloseableTag>
))}
<input
ref={inputRef}
Expand Down Expand Up @@ -122,21 +132,82 @@ const TagInput = observer((props: TagInputProps) => {

export default TagInput;

interface TagProps {
token: string;
remove: (token: string) => void;
}
const tagVariants = cva(
cn(
"mr-2 flex flex-shrink cursor-pointer items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-sm border border-slate-800 bg-violet-200 px-1 py-0.5 text-xs text-slate-600",
),
{
variants: {
variant: {
default: "",
// todo: bg-accent just happens to be muted rn; in the future
// likely need a bg-accent-muted or similar
muted: "border-default bg-accent",
},
size: {
default: "h-10 px-4 py-2",
xs: "py-0 px-0.5 text-xs",
sm: "h-7 px-2",
},
defaultVariants: {
variant: "default",
size: "default",
},
},
},
);

type PTag = React.PropsWithChildren<{
className?: string;
onClick?: () => void;
size?: "default" | "xs" | "sm";
variant?: "default" | "muted";
}>;

const Tag = ({ token, remove }: TagProps) => {
const Tag = ({ size, className, children, onClick, variant }: PTag) => {
return (
<span className="mr-2 flex flex-shrink items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-sm border border-slate-800 bg-violet-200 px-1 py-0.5 text-xs text-slate-600">
<span className="flex-shrink overflow-hidden text-ellipsis">{token}</span>
<button
className="text-grey-400 ml-1 flex-shrink-0"
onClick={() => remove(token)}
>
<span
className={cn(tagVariants({ size, className, variant }))}
onClick={onClick}
>
{children}
</span>
);
};

type ClickableTagProps = PTag & {
onClick: () => void;
};

/**
* Tag where user can click on any part of the tag to perform an action
*/
export const ClickableTag = ({ children, ...rest }: ClickableTagProps) => {
return (
<Tag {...rest}>
<span className="flex-shrink overflow-hidden text-ellipsis">
{children}
</span>
</Tag>
);
};

type PClosableTag = PTag & {
remove: () => void;
};

/**
* Tag where user can click on an 'x' to remove the tag
*/
const CloseableTag = ({ remove, children, ...rest }: PClosableTag) => {
return (
<Tag {...rest}>
<span className="flex-shrink overflow-hidden text-ellipsis">
{children}
</span>
<button className="text-grey-400 ml-1 flex-shrink-0" onClick={remove}>
×
</button>
</span>
</Tag>
);
};
33 changes: 15 additions & 18 deletions src/views/documents/DocumentItem.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,34 @@
import { Badge } from "evergreen-ui";
import React from "react";
import { SearchItem } from "./SearchStore";
import { ClickableTag } from "../../components/TagInput";
import { SearchItem, useSearchStore } from "./SearchStore";

export function DocumentItem(props: {
doc: SearchItem;
edit: (id: string) => any;
getName: (id: string) => string;
}) {
const { doc, edit, getName } = props;
const search = useSearchStore()!;

return (
<div
key={doc.id}
onClick={() => edit(doc.id)}
className="hover:underline-offset flex cursor-pointer hover:underline"
>
<div key={doc.id} className="flex items-center">
{/* Without mono font, dates won't be a uniform width */}
<div className="mr-6 shrink-0 font-mono text-sm tracking-tight">
{doc.createdAt.slice(0, 10)}
</div>
<div className="font-sans">
<div
className="hover:underline-offset mr-2 cursor-pointer font-sans hover:underline"
onClick={() => edit(doc.id)}
>
{doc.title}
<small>
<Badge
color="purple"
fontWeight={400}
textTransform="none"
marginLeft={8}
>
{getName(doc.journalId)}
</Badge>
</small>
</div>
<ClickableTag
size="xs"
variant="muted"
onClick={() => search.addToken(`in:${getName(doc.journalId)}`)}
>
in:{getName(doc.journalId)}
</ClickableTag>
</div>
);
}
23 changes: 1 addition & 22 deletions src/views/documents/sidebar/JournalItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as ContextMenu from "@radix-ui/react-context-menu";
import { cn } from "@udecode/cn";
import { Heading, Pane, toaster } from "evergreen-ui";
import { toaster } from "evergreen-ui";
import { noop } from "lodash";
import { observer } from "mobx-react-lite";
import React, { useContext } from "react";
Expand All @@ -11,27 +11,6 @@ import { useIsMounted } from "../../../hooks/useIsMounted";
import { JournalsStoreContext } from "../../../hooks/useJournalsLoader";
import { SidebarStore } from "./store";

/**
* Collapse component that can be toggled open and closed.
*/
export function Collapse(props: { defaultOpen?: boolean; children: any }) {
const [isOpen, setIsOpen] = React.useState(
props.defaultOpen == null ? false : props.defaultOpen,
);

const Icon = isOpen ? Icons.chevronDown : Icons.chevronRight;

return (
<Pane>
<Pane display="flex" onClick={() => setIsOpen(!isOpen)} cursor="pointer">
<Heading>Archived Journals</Heading>
<Icon size={18} />
</Pane>
{isOpen && props.children}
</Pane>
);
}

export function JournalCreateForm({ done }: { done: () => any }) {
const [journal, _] = React.useState<{ name: string }>({
name: "My new journal",
Expand Down
34 changes: 16 additions & 18 deletions src/views/documents/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Root as VisuallyHidden } from "@radix-ui/react-visually-hidden";
import React from "react";

import { Card, Heading, IconButton, Pane, PlusIcon } from "evergreen-ui";
import { IconButton, PlusIcon } from "evergreen-ui";

import { observer } from "mobx-react-lite";
import { Collapse } from "../../../components/Collapse";
import {
Sheet,
SheetContent,
Expand All @@ -12,7 +13,7 @@ import {
SheetTitle,
} from "../../../components/Sidesheet";
import { SearchStore } from "../SearchStore";
import { Collapse, JournalCreateForm, JournalItem } from "./JournalItem";
import { JournalCreateForm, JournalItem } from "./JournalItem";
import { TagsList } from "./TagsList";
import { SidebarStore, useSidebarStore } from "./store";

Expand Down Expand Up @@ -60,27 +61,22 @@ export default observer(function JournalSelectionSidebar(props: SidebarProps) {

const InnerContent = observer(({ store }: { store: SidebarStore }) => {
return (
<>
{" "}
<Card
backgroundColor="white"
elevation={0}
padding={16}
marginBottom={16}
>
<Pane>
<Heading>
Active Journals{" "}
<div className="mt-6">
<div className="mb-4 border p-4 shadow-md">
<div>
<div className="text-md mb-2 flex cursor-pointer items-center font-medium tracking-tight">
Active Journals
<IconButton
icon={PlusIcon}
size="small"
onClick={store.toggleAdding}
disabled={store.adding}
className="ml-1"
>
Add Journal
</IconButton>
</Heading>
</Pane>
</div>
</div>
<ul className="ml-0 text-sm">
{store.adding && (
<li>
Expand All @@ -102,8 +98,10 @@ const InnerContent = observer(({ store }: { store: SidebarStore }) => {
);
})}
</ul>
</div>

<Collapse>
<div className="mb-4 p-4 shadow-md">
<Collapse heading="Archived Journals">
<ul className="ml-0 text-sm">
{store.journalStore.archived.map((j) => {
return (
Expand All @@ -120,8 +118,8 @@ const InnerContent = observer(({ store }: { store: SidebarStore }) => {
})}
</ul>
</Collapse>
</Card>
</div>
<TagsList search={store.searchTag} />
</>
</div>
);
});
36 changes: 15 additions & 21 deletions src/views/documents/sidebar/TagsList.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import {
Card,
FolderCloseIcon,
Heading,
ListItem,
UnorderedList,
} from "evergreen-ui";
import React from "react";

import { ClickableTag as Tag } from "../../../components/TagInput";
import { useTags } from "../../../hooks/useTags";

/**
Expand All @@ -24,20 +18,20 @@ export function TagsList(props: { search: (tag: string) => boolean }) {
return "error loading tags";
}

const tagItems = tags.map((t) => {
return (
<ListItem key={t} icon={FolderCloseIcon}>
<a href="" onClick={() => props.search(t)}>
{t}
</a>
</ListItem>
);
});

return (
<Card backgroundColor="white" elevation={0} padding={16} marginBottom={16}>
<Heading>Tags</Heading>
<UnorderedList>{tagItems}</UnorderedList>
</Card>
<div className="mb-4 p-4 shadow-md">
<div className="text-md mb-2 flex cursor-pointer items-center font-medium tracking-tight">
Tags
</div>
<div className="flex flex-wrap">
{tags.map((t) => {
return (
<Tag className="mb-1 mr-1" key={t} onClick={() => props.search(t)}>
#{t}
</Tag>
);
})}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions src/views/edit/FrontMatter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ const FrontMatter = observer(
onRemove={onRemoveTag}
placeholder="Add tags"
ghost={true}
prefixHash={true}
/>
</div>
</>
Expand Down

0 comments on commit 73d4306

Please sign in to comment.