Skip to content

Commit dfd2a6d

Browse files
authored
feat: references panel styles (#421)
* feat: update styles for references panel * fix: update tests * fix: make the whole search bar clickable
1 parent cd219c7 commit dfd2a6d

8 files changed

Lines changed: 177 additions & 225 deletions

File tree

src/components/PanelWrapper.tsx

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,49 @@
11
import React from 'react';
22

3-
export function PanelWrapper({
4-
title,
5-
children,
6-
}: {
3+
import { cx } from '../lib/cx';
4+
5+
interface Action {
6+
key: string;
7+
disabled?: boolean;
78
title: string;
8-
closable?: boolean;
9-
onCloseClick?: () => void;
9+
Icon: React.ReactElement;
10+
onClick: () => void;
11+
}
12+
13+
interface PanelWrapperProps {
14+
title: string;
15+
actions?: Action[];
1016
children: React.ReactNode;
11-
}) {
17+
}
18+
export function PanelWrapper({ title, children, actions }: PanelWrapperProps) {
1219
return (
1320
<div className="flex h-full flex-col items-start self-stretch bg-side-bar-bg-primary text-side-bar-txt">
14-
<div className="g-2 flex items-start self-stretch p-4">
15-
<h1 className="self-stretch">{title}</h1>
21+
<div className="flex items-start gap-2 self-stretch px-4 py-3">
22+
<h1 className="flex-1 self-stretch">{title}</h1>
23+
{actions && actions.length > 0 && (
24+
<div className="flex items-center gap-1">
25+
{actions.map((action) => (
26+
<button
27+
aria-disabled={action.disabled}
28+
className={cx(
29+
'flex h-8 w-8 flex-col items-center justify-center rounded-default',
30+
'bg-btn-bg-side-bar-icon-default',
31+
'text-btn-ico-side-bar-tool-default',
32+
{
33+
'hover:bg-btn-bg-side-bar-icon-hover hover:text-btn-ico-side-bar-tool-hover': !action.disabled,
34+
'bg-btn-bg-side-bar-tool-disabled text-btn-ico-tool-disabled': action.disabled,
35+
},
36+
)}
37+
disabled={action.disabled}
38+
key={action.key}
39+
title={action.title}
40+
onClick={action.onClick}
41+
>
42+
{action.Icon}
43+
</button>
44+
))}
45+
</div>
46+
)}
1647
</div>
1748
{children}
1849
</div>

src/components/SearchBar.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useRef, useState } from 'react';
2+
3+
import { useDebouncedCallback } from '../hooks/useDebouncedCallback';
4+
import { cx } from '../lib/cx';
5+
import { SearchIcon } from './icons';
6+
7+
interface SearchBarProps {
8+
placeholder: string;
9+
onChange: (value: string) => void;
10+
}
11+
export function SearchBar({ placeholder, onChange }: SearchBarProps) {
12+
const [value, setValue] = useState('');
13+
const debouncedOnChange = useDebouncedCallback(onChange, 200);
14+
const inputRef = useRef<HTMLInputElement | null>(null);
15+
16+
return (
17+
<div
18+
className={cx(
19+
'flex items-start gap-2 self-stretch rounded-default',
20+
'cursor-text border border-solid border-input-border p-3',
21+
)}
22+
onClick={() => {
23+
inputRef.current?.focus();
24+
}}
25+
>
26+
<div className="text-input-ico-placeholder">
27+
<SearchIcon />
28+
</div>
29+
<input
30+
className="text-input-txt-primary outline-none placeholder:text-input-txt-placeholder"
31+
placeholder={placeholder}
32+
ref={inputRef}
33+
type="text"
34+
value={value}
35+
onChange={(evt) => {
36+
setValue(evt.target.value);
37+
debouncedOnChange(evt.target.value);
38+
}}
39+
/>
40+
</div>
41+
);
42+
}

src/components/icons.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,14 @@ export const CircleIcon = () => (
2727
<div className="h-2 w-2 shrink-0 rounded-2xl bg-current" />
2828
</div>
2929
);
30+
31+
export const SearchIcon = () => (
32+
<div className="flex h-6 w-6 items-center justify-center">
33+
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
34+
<path
35+
d="M19.6 21L13.3 14.7C12.8 15.1 12.225 15.4167 11.575 15.65C10.925 15.8833 10.2333 16 9.5 16C7.68333 16 6.146 15.3707 4.888 14.112C3.63 12.8533 3.00067 11.316 3 9.5C3 7.68333 3.62933 6.146 4.888 4.888C6.14667 3.63 7.684 3.00067 9.5 3C11.3167 3 12.854 3.62933 14.112 4.888C15.37 6.14667 15.9993 7.684 16 9.5C16 10.2333 15.8833 10.925 15.65 11.575C15.4167 12.225 15.1 12.8 14.7 13.3L21 19.6L19.6 21ZM9.5 14C10.75 14 11.8127 13.5623 12.688 12.687C13.5633 11.8117 14.0007 10.7493 14 9.5C14 8.25 13.5623 7.18733 12.687 6.312C11.8117 5.43667 10.7493 4.99933 9.5 5C8.25 5 7.18733 5.43767 6.312 6.313C5.43667 7.18833 4.99933 8.25067 5 9.5C5 10.75 5.43767 11.8127 6.313 12.688C7.18833 13.5633 8.25067 14.0007 9.5 14Z"
36+
fill="currentcolor"
37+
/>
38+
</svg>
39+
</div>
40+
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export const TableIcon = () => (
2+
<div className="flex h-6 w-6 shrink-0 items-center justify-center">
3+
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg">
4+
<path d="M0 1C0 0.447715 0.447715 0 1 0H11C11.5523 0 12 0.447715 12 1V5.5H0V1Z" fill="currentcolor" />
5+
<path d="M0 6.5H5.5V12H1C0.447715 12 0 11.5523 0 11V6.5Z" fill="currentcolor" />
6+
<path d="M6.5 6.5H12V11C12 11.5523 11.5523 12 11 12H6.5V6.5Z" fill="currentcolor" />
7+
</svg>
8+
</div>
9+
);
10+
11+
export const ExportIcon = () => (
12+
<div className="flex h-6 w-6 shrink-0 items-center justify-center">
13+
<svg height="12" viewBox="0 0 20 12" width="20" xmlns="http://www.w3.org/2000/svg">
14+
<path
15+
d="M19.5 6L15.5 2V5H6.5V7H15.5V10M0.5 10V2C0.5 1.46957 0.710714 0.960859 1.08579 0.585786C1.46086 0.210714 1.96957 0 2.5 0H11.5C12.0304 0 12.5391 0.210714 12.9142 0.585786C13.2893 0.960859 13.5 1.46957 13.5 2V3H11.5V2H2.5V10H11.5V9H13.5V10C13.5 10.5304 13.2893 11.0391 12.9142 11.4142C12.5391 11.7893 12.0304 12 11.5 12H2.5C1.96957 12 1.46086 11.7893 1.08579 11.4142C0.710714 11.0391 0.5 10.5304 0.5 10Z"
16+
fill="currentcolor"
17+
/>
18+
</svg>
19+
</div>
20+
);
Lines changed: 22 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,14 @@
1-
import { VscFile, VscFilePdf, VscTrash } from 'react-icons/vsc';
2-
31
import { cx } from '../../../lib/cx';
42
import { Author, ReferenceItem } from '../../../types/ReferenceItem';
5-
import { ReferencesItemStatusLabel } from '../components/ReferencesItemStatusLabel';
63

74
export function ReferencesList({
85
references,
96
onRefClicked,
107
onAuthorClicked,
11-
onRemoveClicked,
128
}: {
139
references: ReferenceItem[];
1410
onRefClicked: (item: ReferenceItem, openPdf?: boolean) => void;
1511
onAuthorClicked: (author: Author, item: ReferenceItem) => void;
16-
onRemoveClicked: (item: ReferenceItem) => void;
1712
}) {
1813
const handleClickFor: (ref: ReferenceItem, openPdf: boolean) => React.MouseEventHandler = (ref, openPdf) => (e) => {
1914
e.preventDefault();
@@ -22,18 +17,23 @@ export function ReferencesList({
2217
};
2318

2419
return (
25-
<div className="" data-testid={ReferencesList.name}>
26-
<ul className="space-y-4" role="list">
27-
{references.map((reference) => (
28-
<li
29-
className="group/ref-item relative cursor-pointer bg-white p-1 px-4 even:bg-slate-50 hover:bg-slate-200"
30-
key={reference.id}
31-
role="listitem"
32-
title={reference.title}
33-
onClick={handleClickFor(reference, false)}
34-
>
35-
<div className="truncate whitespace-nowrap">{reference.title}</div>
36-
<div className="whitespace truncate text-xs">
20+
<ul className="flex flex-col items-start gap-2 self-stretch" data-testid={ReferencesList.name} role="list">
21+
{references.map((reference) => (
22+
<li
23+
className={cx(
24+
'cursor-pointer',
25+
'flex gap-1 self-stretch px-2.5 py-1.5',
26+
'text-btn-txt-side-bar-item-primary',
27+
'rounded-default bg-btn-bg-side-bar-item-default hover:bg-btn-bg-side-bar-item-hover',
28+
)}
29+
key={reference.id}
30+
role="listitem"
31+
title={reference.title}
32+
onClick={handleClickFor(reference, false)}
33+
>
34+
<div className="flex flex-1 flex-col items-start justify-center">
35+
<h2 className="line-clamp-1 overflow-ellipsis">{reference.title}</h2>
36+
<div className="line-clamp-1 overflow-ellipsis">
3737
{reference.authors.map((author, index) => (
3838
<span key={author.fullName}>
3939
{index > 0 && ', '}
@@ -50,34 +50,10 @@ export function ReferencesList({
5050
</span>
5151
))}
5252
</div>
53-
<div className="mt-2 flex justify-between text-xs">
54-
<div className="font-mono">[{reference.citationKey}]</div>
55-
<ReferencesItemStatusLabel status={reference.status} />
56-
</div>
57-
{/* ACTIONS */}
58-
<div
59-
className={cx(
60-
'absolute right-4 top-1 rounded-md', // position: ;
61-
'bg-white px-1 py-1 ', // style
62-
'hidden gap-2 group-hover/ref-item:flex', // hidden / show on hover
63-
)}
64-
>
65-
<VscFile size={20} title="Open Reference" onClick={handleClickFor(reference, false)} />
66-
<VscFilePdf size={20} title="Open PDF" onClick={handleClickFor(reference, true)} />
67-
<VscTrash
68-
className="text-slate-600"
69-
size={20}
70-
title="Remove Reference"
71-
onClick={(e) => {
72-
e.stopPropagation();
73-
e.preventDefault();
74-
onRemoveClicked(reference);
75-
}}
76-
/>
77-
</div>
78-
</li>
79-
))}
80-
</ul>
81-
</div>
53+
<div className="line-clamp-1 overflow-ellipsis">[{reference.citationKey}]</div>
54+
</div>
55+
</li>
56+
))}
57+
</ul>
8258
);
8359
}
Lines changed: 28 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { useAtomValue, useSetAtom } from 'jotai';
22
import { useEffect, useState } from 'react';
3-
import { VscDesktopDownload, VscNewFile, VscOpenPreview } from 'react-icons/vsc';
43

54
import { openReferenceAtom, openReferencePdfAtom, openReferencesAtom } from '../../../atoms/editorActions';
65
import { getReferencesAtom } from '../../../atoms/referencesState';
7-
import { PanelSection } from '../../../components/PanelSection';
86
import { PanelWrapper } from '../../../components/PanelWrapper';
7+
import { SearchBar } from '../../../components/SearchBar';
98
import { emitEvent } from '../../../events';
10-
import { useDebouncedCallback } from '../../../hooks/useDebouncedCallback';
119
import { Author, ReferenceItem } from '../../../types/ReferenceItem';
12-
import { UploadTipInstructions } from '../components/UploadTipInstructions';
10+
import { ExportIcon, TableIcon } from '../components/icons';
1311
import { ReferencesList } from './ReferencesList';
1412

1513
export function ReferencesPanel() {
@@ -23,10 +21,6 @@ export function ReferencesPanel() {
2321
setVisibleReferences(references);
2422
}, [references]);
2523

26-
const handleAddReferences = () => {
27-
emitEvent('refstudio://menu/references/upload');
28-
};
29-
3024
const handleOpenReferences = () => {
3125
emitEvent('refstudio://menu/references/open');
3226
};
@@ -45,10 +39,6 @@ export function ReferencesPanel() {
4539

4640
const handleAuthorClicked = (author: Author) => openReferences(author.lastName);
4741

48-
const handleRemoveClicked = (reference: ReferenceItem) => {
49-
emitEvent('refstudio://references/remove', { referenceIds: [reference.id] });
50-
};
51-
5242
const handleFilterChanged = (filter: string) => {
5343
if (filter.trim() === '') {
5444
return setVisibleReferences(references);
@@ -65,58 +55,32 @@ export function ReferencesPanel() {
6555
};
6656

6757
return (
68-
<PanelWrapper title="References">
69-
<PanelSection
70-
grow
71-
rightIcons={[
72-
{ key: 'new', Icon: VscNewFile, title: 'Add References', onClick: handleAddReferences },
73-
{ key: 'open', Icon: VscOpenPreview, title: 'Open References', onClick: handleOpenReferences },
74-
{
75-
key: 'export',
76-
disabled: references.length === 0,
77-
Icon: VscDesktopDownload,
78-
title: 'Export References',
79-
onClick: () => handleExportReferences(),
80-
},
81-
]}
82-
title="Library"
83-
>
84-
<div className="min-h-[200px] ">
85-
<FilterInput placeholder="Filter (e.g. title, author)" onChange={handleFilterChanged} />
86-
<ReferencesList
87-
references={visibleReferences}
88-
onAuthorClicked={handleAuthorClicked}
89-
onRefClicked={handleRefClicked}
90-
onRemoveClicked={handleRemoveClicked}
91-
/>
92-
93-
{references.length === 0 && (
94-
<div className="p-2">Welcome to your RefStudio references library. Start by uploading some PDFs.</div>
95-
)}
96-
97-
<UploadTipInstructions />
98-
</div>
99-
</PanelSection>
58+
<PanelWrapper
59+
actions={[
60+
{
61+
key: 'new',
62+
title: 'Open References',
63+
Icon: <TableIcon />,
64+
onClick: handleOpenReferences,
65+
},
66+
{
67+
key: 'export',
68+
disabled: references.length === 0,
69+
title: 'Export References',
70+
Icon: <ExportIcon />,
71+
onClick: handleExportReferences,
72+
},
73+
]}
74+
title="References"
75+
>
76+
<div className="flex flex-1 flex-col items-start gap-4 self-stretch p-4 pt-2">
77+
<SearchBar placeholder="Filter author/title..." onChange={handleFilterChanged} />
78+
<ReferencesList
79+
references={visibleReferences}
80+
onAuthorClicked={handleAuthorClicked}
81+
onRefClicked={handleRefClicked}
82+
/>
83+
</div>
10084
</PanelWrapper>
10185
);
10286
}
103-
104-
function FilterInput({ placeholder, onChange }: { placeholder: string; onChange: (filter: string) => void }) {
105-
const [value, setValue] = useState('');
106-
const debouncedOnChange = useDebouncedCallback(onChange, 200);
107-
108-
return (
109-
<div className="relative mx-1 flex">
110-
<input
111-
className="grow border border-slate-400 bg-slate-100 px-4 py-2 outline-none"
112-
placeholder={placeholder}
113-
type="text"
114-
value={value}
115-
onChange={(evt) => {
116-
setValue(evt.target.value);
117-
debouncedOnChange(evt.target.value);
118-
}}
119-
/>
120-
</div>
121-
);
122-
}

0 commit comments

Comments
 (0)