Skip to content

Commit 7a29e77

Browse files
authored
Merge pull request #7 from ryanlanciaux/addSidebar
Add sidebar
2 parents 340767e + 3417219 commit 7a29e77

File tree

12 files changed

+405
-1
lines changed

12 files changed

+405
-1
lines changed

src/Sidebar/FileDark.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
3+
const FileLight = () => (
4+
<svg
5+
width="16"
6+
height="16"
7+
viewBox="0 0 16 16"
8+
fill="none"
9+
xmlns="http://www.w3.org/2000/svg"
10+
>
11+
<mask id="path-2-inside-1" fill="white">
12+
<path
13+
fill-rule="evenodd"
14+
clip-rule="evenodd"
15+
d="M2.5 2C2.22386 2 2 2.22386 2 2.5V13.5C2 13.7761 2.22386 14 2.5 14H13.5C13.7761 14 14 13.7761 14 13.5V2.5C14 2.22386 13.7761 2 13.5 2H2.5ZM9 10H5V11H9V10ZM5 8H11V9H5V8ZM9 6H5V7H9V6Z"
16+
/>
17+
</mask>
18+
<path
19+
fill-rule="evenodd"
20+
clip-rule="evenodd"
21+
d="M2.5 2C2.22386 2 2 2.22386 2 2.5V13.5C2 13.7761 2.22386 14 2.5 14H13.5C13.7761 14 14 13.7761 14 13.5V2.5C14 2.22386 13.7761 2 13.5 2H2.5ZM9 10H5V11H9V10ZM5 8H11V9H5V8ZM9 6H5V7H9V6Z"
22+
fill="white"
23+
/>
24+
<path
25+
d="M5 10V9H4V10H5ZM9 10H10V9H9V10ZM5 11H4V12H5V11ZM9 11V12H10V11H9ZM11 8H12V7H11V8ZM5 8V7H4V8H5ZM11 9V10H12V9H11ZM5 9H4V10H5V9ZM5 6V5H4V6H5ZM9 6H10V5H9V6ZM5 7H4V8H5V7ZM9 7V8H10V7H9ZM3 2.5C3 2.77614 2.77614 3 2.5 3V1C1.67157 1 1 1.67157 1 2.5H3ZM3 13.5V2.5H1V13.5H3ZM2.5 13C2.77614 13 3 13.2239 3 13.5H1C1 14.3284 1.67157 15 2.5 15V13ZM13.5 13H2.5V15H13.5V13ZM13 13.5C13 13.2239 13.2239 13 13.5 13V15C14.3284 15 15 14.3284 15 13.5H13ZM13 2.5V13.5H15V2.5H13ZM13.5 3C13.2239 3 13 2.77614 13 2.5H15C15 1.67157 14.3284 1 13.5 1V3ZM2.5 3H13.5V1H2.5V3ZM5 11H9V9H5V11ZM6 11V10H4V11H6ZM9 10H5V12H9V10ZM8 10V11H10V10H8ZM11 7H5V9H11V7ZM12 9V8H10V9H12ZM5 10H11V8H5V10ZM4 8V9H6V8H4ZM5 7H9V5H5V7ZM6 7V6H4V7H6ZM9 6H5V8H9V6ZM8 6V7H10V6H8Z"
26+
fill="white"
27+
mask="url(#path-2-inside-1)"
28+
/>
29+
</svg>
30+
);

src/Sidebar/Icons.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, { HTMLAttributes } from 'react';
2+
3+
export const File = (props: HTMLAttributes<SVGElement>) => (
4+
<svg
5+
width="16"
6+
height="16"
7+
viewBox="0 0 16 16"
8+
fill="none"
9+
xmlns="http://www.w3.org/2000/svg"
10+
{...props}
11+
>
12+
<mask id="path-2-inside-1" fill="white">
13+
<path
14+
fill-rule="evenodd"
15+
clip-rule="evenodd"
16+
d="M2.5 2C2.22386 2 2 2.22386 2 2.5V13.5C2 13.7761 2.22386 14 2.5 14H13.5C13.7761 14 14 13.7761 14 13.5V2.5C14 2.22386 13.7761 2 13.5 2H2.5ZM9 10H5V11H9V10ZM5 8H11V9H5V8ZM9 6H5V7H9V6Z"
17+
/>
18+
</mask>
19+
<path
20+
fill-rule="evenodd"
21+
clip-rule="evenodd"
22+
d="M2.5 2C2.22386 2 2 2.22386 2 2.5V13.5C2 13.7761 2.22386 14 2.5 14H13.5C13.7761 14 14 13.7761 14 13.5V2.5C14 2.22386 13.7761 2 13.5 2H2.5ZM9 10H5V11H9V10ZM5 8H11V9H5V8ZM9 6H5V7H9V6Z"
23+
fill="#757F88"
24+
/>
25+
<path
26+
d="M5 10V9H4V10H5ZM9 10H10V9H9V10ZM5 11H4V12H5V11ZM9 11V12H10V11H9ZM11 8H12V7H11V8ZM5 8V7H4V8H5ZM11 9V10H12V9H11ZM5 9H4V10H5V9ZM5 6V5H4V6H5ZM9 6H10V5H9V6ZM5 7H4V8H5V7ZM9 7V8H10V7H9ZM3 2.5C3 2.77614 2.77614 3 2.5 3V1C1.67157 1 1 1.67157 1 2.5H3ZM3 13.5V2.5H1V13.5H3ZM2.5 13C2.77614 13 3 13.2239 3 13.5H1C1 14.3284 1.67157 15 2.5 15V13ZM13.5 13H2.5V15H13.5V13ZM13 13.5C13 13.2239 13.2239 13 13.5 13V15C14.3284 15 15 14.3284 15 13.5H13ZM13 2.5V13.5H15V2.5H13ZM13.5 3C13.2239 3 13 2.77614 13 2.5H15C15 1.67157 14.3284 1 13.5 1V3ZM2.5 3H13.5V1H2.5V3ZM5 11H9V9H5V11ZM6 11V10H4V11H6ZM9 10H5V12H9V10ZM8 10V11H10V10H8ZM11 7H5V9H11V7ZM12 9V8H10V9H12ZM5 10H11V8H5V10ZM4 8V9H6V8H4ZM5 7H9V5H5V7ZM6 7V6H4V7H6ZM9 6H5V8H9V6ZM8 6V7H10V6H8Z"
27+
fill="#757F88"
28+
mask="url(#path-2-inside-1)"
29+
/>
30+
</svg>
31+
);
32+
33+
export const Folder = (props: HTMLAttributes<SVGElement>) => (
34+
<svg
35+
width="16"
36+
height="16"
37+
viewBox="0 0 16 16"
38+
fill="none"
39+
xmlns="http://www.w3.org/2000/svg"
40+
{...props}
41+
>
42+
<path
43+
d="M7.98881 5.0109L7.98395 5.01562H8H13.5C13.7679 5.01562 13.9844 5.23207 13.9844 5.5V12.5C13.9844 12.7679 13.7679 12.9844 13.5 12.9844H2.5C2.23207 12.9844 2.01562 12.7679 2.01562 12.5V3.5C2.01562 3.23207 2.23207 3.01562 2.5 3.01562H5.99993C6.02781 3.01588 6.05459 3.02655 6.075 3.04554L7.98881 5.0109Z"
44+
fill="#757F88"
45+
stroke="#757F88"
46+
stroke-width="0.03125"
47+
/>
48+
</svg>
49+
);
50+
51+
export const Chevron = (props: HTMLAttributes<SVGElement>) => (
52+
<svg
53+
width="7"
54+
height="4"
55+
viewBox="0 0 7 4"
56+
fill="none"
57+
xmlns="http://www.w3.org/2000/svg"
58+
{...props}
59+
>
60+
<path
61+
d="M6 1L3.5 3.5L1 1"
62+
stroke="#757F88"
63+
stroke-linecap="round"
64+
stroke-linejoin="round"
65+
/>
66+
</svg>
67+
);

src/Sidebar/Sidebar.css

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
.sidebar {
2+
background-color: #E2E5E7;
3+
display: flex;
4+
flex-direction: column;
5+
font-size: 13px;
6+
}
7+
8+
.sidebar ul {
9+
--padding: 0px;
10+
list-style-type: none;
11+
padding: 0;
12+
}
13+
14+
.sidebar-item-group {
15+
visibility: hidden;
16+
height: 0;
17+
opacity: 0;
18+
}
19+
20+
.sidebar-item-group-expanded {
21+
visibility: visible;
22+
height: unset;
23+
opacity: 1;
24+
}
25+
26+
27+
.sidebar-item-group button {
28+
padding: 8px;
29+
padding-left: calc(16px + var(--padding));
30+
}
31+
32+
.sidebar-item-group button.active, .sidebar-item-group button:focus {
33+
background-color: #6100FF;
34+
color: #FFF;
35+
outline: 0;
36+
}
37+
38+
.sidebar-item-group button.active, .sidebar-item-group button:focus {
39+
background-color: #6100FF;
40+
color: #FFF;
41+
outline: 0;
42+
}
43+
.sidebar-item-group button.active svg *, .sidebar-item-group button:focus svg * {
44+
fill: #FFF;
45+
}
46+
47+
.sidebar-item-button {
48+
display: flex;
49+
align-items: center;
50+
width: 100%;
51+
padding: 0 16px;
52+
text-align: left;
53+
border: 0;
54+
background: 0;
55+
}
56+
57+
.sidebar-item-button .chevron.rotate {
58+
transform: rotate(-90deg);
59+
}
60+
61+
.sidebar-item:not(.sidebar-item-file) > button {
62+
margin-bottom: 0px;
63+
}
64+
65+
.sidebar-item svg{
66+
margin-right: 4px;
67+
fill: #FFF;
68+
}
69+
70+
.sidebar-item .chevron {
71+
margin-right: 2px;
72+
}
73+
74+
.sidebar-item .chevron.rotate {
75+
transform: rotate(-90deg);
76+
}

src/Sidebar/Sidebar.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import React, { FC, HTMLAttributes, ReactChild } from 'react';
2+
import classnames from 'classnames';
3+
import { DirectoryListItem, getSidebarItemsFromData } from './SidebarItem';
4+
import './Sidebar.css';
5+
6+
export interface Props extends HTMLAttributes<HTMLDivElement> {
7+
isVisible?: boolean;
8+
subItems?: DirectoryListItem[];
9+
children?: React.ReactNode;
10+
}
11+
12+
export const Sidebar: FC<Props> = ({ isVisible, subItems, children }) => (
13+
<nav className={classnames('sidebar', { 'sidebar-visible': isVisible })}>
14+
<ul className="sidebar-item-group sidebar-item-group-expanded">
15+
{subItems && getSidebarItemsFromData(subItems, true, 0)}
16+
{children}
17+
</ul>
18+
</nav>
19+
);

src/Sidebar/SidebarItem.tsx

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import React, {
2+
FC,
3+
HTMLAttributes,
4+
ReactChild,
5+
useState,
6+
useRef,
7+
useEffect,
8+
} from 'react';
9+
import classnames from 'classnames';
10+
11+
import { File, Folder, Chevron } from './Icons';
12+
13+
export type DirectoryListItem = {
14+
name: string;
15+
subItems?: DirectoryListItem[];
16+
};
17+
18+
type Base = HTMLAttributes<HTMLLIElement> & DirectoryListItem;
19+
20+
export interface SidebarItemProps extends Base {
21+
onSelect?: () => void;
22+
defaultIsExpanded?: boolean;
23+
props?: HTMLAttributes<HTMLUListElement>;
24+
isOpen?: boolean;
25+
level: number;
26+
}
27+
28+
export function getSidebarItemsFromData(
29+
data: DirectoryListItem[],
30+
isOpen?: boolean,
31+
level: number = 0
32+
) {
33+
return data.map(({ name, subItems }) => (
34+
<SidebarItem
35+
level={level}
36+
defaultIsExpanded={isOpen}
37+
className={classnames('sidebar-item', {
38+
'sidebar-item-file': !subItems,
39+
})}
40+
name={name}
41+
subItems={subItems}
42+
/>
43+
));
44+
}
45+
46+
const openStates = {
47+
unset: undefined,
48+
open: true,
49+
closed: false,
50+
};
51+
52+
interface ListWrapperProps extends HTMLAttributes<HTMLUListElement> {
53+
level?: number;
54+
}
55+
const ListWrapper = ({ children, level = 0, ...props }: ListWrapperProps) => {
56+
const listEl: React.Ref<HTMLUListElement> | null = useRef(null);
57+
58+
useEffect(() => {
59+
listEl.current &&
60+
listEl.current.style.setProperty('--padding', `${(level + 1) * 16}px`);
61+
}, []);
62+
63+
return (
64+
<ul ref={listEl} {...props}>
65+
{children}
66+
</ul>
67+
);
68+
};
69+
70+
export const SidebarItem: FC<SidebarItemProps> = ({
71+
subItems,
72+
name,
73+
defaultIsExpanded = false,
74+
onSelect,
75+
level,
76+
...props
77+
}) => {
78+
const [isExpanded, setIsExpanded] = useState<undefined | boolean>(
79+
openStates.unset
80+
);
81+
82+
const isFolder = subItems !== undefined;
83+
const image = isFolder ? <Folder /> : <File />;
84+
const isOpen = isExpanded ?? defaultIsExpanded;
85+
86+
const folderImage = isFolder && (
87+
<Chevron className={classnames('chevron', { rotate: !isOpen })} />
88+
// <img src={chevron} className={classnames('chevron', { rotate: !isOpen })} />
89+
);
90+
91+
const onToggleExpanded = (e: React.MouseEvent) => {
92+
e.preventDefault();
93+
setIsExpanded(previous =>
94+
previous === openStates.open ||
95+
(defaultIsExpanded === true && isExpanded === openStates.unset)
96+
? openStates.closed
97+
: openStates.open
98+
);
99+
};
100+
101+
return (
102+
<li
103+
{...props}
104+
className={classnames('sidebar-item', {
105+
'sidebar-item-file': !subItems,
106+
})}
107+
>
108+
<button className="sidebar-item-button" onClick={onToggleExpanded}>
109+
{folderImage}
110+
{image} {name}
111+
</button>
112+
{subItems && subItems.length > 0 && (
113+
<ListWrapper
114+
level={level}
115+
className={classnames('sidebar-item-group', {
116+
'sidebar-item-group-expanded': isExpanded ?? defaultIsExpanded,
117+
})}
118+
>
119+
{getSidebarItemsFromData(subItems, false, level + 1)}
120+
</ListWrapper>
121+
)}
122+
</li>
123+
);
124+
};

src/Sidebar/chevron-down.svg

Lines changed: 9 additions & 0 deletions
Loading

src/Sidebar/file-light.svg

Lines changed: 19 additions & 0 deletions
Loading

src/Sidebar/file.svg

Lines changed: 19 additions & 0 deletions
Loading

src/Sidebar/folder.svg

Lines changed: 9 additions & 0 deletions
Loading

src/Sidebar/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './Sidebar';
2+
export * from './SidebarItem';

0 commit comments

Comments
 (0)