Move components to ui

This commit is contained in:
Max Lynch
2020-12-22 16:55:29 -06:00
parent 3cc3daefda
commit 5e2bd30d72
21 changed files with 59 additions and 47 deletions
+7
View File
@@ -0,0 +1,7 @@
const App = ({ children }) => (
<div className="flex h-screen flex-col">
{children}
</div>
);
export default App;
+17
View File
@@ -0,0 +1,17 @@
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
const Backdrop = ({ open, onClose }) => {
return (
<div
onClick={onClose}
className={classNames('fixed z-10 inset-0 bg-black transition-opacity w-full h-full', {
'pointer-events-none': !open,
'opacity-10': open,
'opacity-0': !open,
})}
></div>
);
};
export default Backdrop;
+15
View File
@@ -0,0 +1,15 @@
import classNames from 'classnames';
const Button = ({ children, className, ...props }) => (
<button
{...props}
class={classNames(
'inline-block text-xs font-medium leading-6 text-center uppercase transition rounded-lg ripple focus:outline-none',
className
)}
>
{children}
</button>
);
export default Button;
+9
View File
@@ -0,0 +1,9 @@
import classNames from 'classnames';
const Card = ({ children, className, ...props }) => (
<div {...props} className={classNames('m-auto px-4 py-4 max-w-xl', className)}>
<div className="bg-white shadow-md rounded-b-xl">{children}</div>
</div>
);
export default Card;
+15
View File
@@ -0,0 +1,15 @@
import classNames from 'classnames';
const Content = ({ className, visible, children, ...props }) => (
<div
{...props}
className={classNames(`h-full w-full overflow-auto py-2 absolute top-0`, className, {
visible,
invisible: !visible,
})}
>
{children}
</div>
);
export default Content;
+9
View File
@@ -0,0 +1,9 @@
const Dialog = () => (
<div className="fixed inset-0 w-full h-full flex align-center justify-center">
<div className="w-200 bg-white rounded-xl">
<div className="flex-1">{children}</div>
</div>
</div>
);
export default Dialog;
+3
View File
@@ -0,0 +1,3 @@
const EdgeDrag = () => null;
export default EdgeDrag;
+3
View File
@@ -0,0 +1,3 @@
const List = ({ children, ...props }) => <div {...props}>{children}</div>;
export default List;
+9
View File
@@ -0,0 +1,9 @@
import classNames from 'classnames';
const ListItem = ({ children, className, ...props }) => (
<div className={classNames('p-4', className)} {...props}>
{children}
</div>
);
export default ListItem;
+93
View File
@@ -0,0 +1,93 @@
import { Plugins } from '@capacitor/core';
import classNames from 'classnames';
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useDrag } from 'react-use-gesture';
const Menu = ({ open, onClose, children, className, ...props }) => {
const ref = useRef();
const [x, setX] = useState(-100000);
const [rect, setRect] = useState(null);
const [dragging, setDragging] = useState(false);
useEffect(() => {
try {
if (open) {
Plugins.StatusBar.setStyle({
style: 'LIGHT',
});
} else {
Plugins.StatusBar.setStyle({
style: 'DARK',
});
}
} catch (e) {}
}, [open]);
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect();
setRect(rect);
setX(-rect.width);
}, []);
useLayoutEffect(() => {
if (open) {
setX(0);
} else if (rect) {
setX(-rect.width);
}
}, [rect, open]);
const bind = useDrag(
({ down, movement: [mx] }) => {
setX(mx > 0 ? 0 : mx);
if (down) {
setDragging(true);
} else {
setDragging(false);
}
// If the drag ended, snap the menu back
if (!down) {
const mid = -rect.width;
if (x < mid / 2) {
// Close
setX(-rect.width);
onClose();
} else {
// Re-open
setX(0);
}
}
},
{
axis: 'x',
}
);
return (
<div
{...props}
{...bind()}
ref={ref}
style={{
paddingTop: `calc(env(safe-area-inset-top, 0px) + 8px)`,
paddingBottom: `calc(env(safe-area-inset-bottom, 0px) + 8px)`,
touchAction: 'pan-x',
transform: `translateX(${x}px)`,
}}
className={classNames(
'fixed z-40 transform transform-gpu translate w-48 h-full bg-gray-100',
className,
{
'transition-transform': !dragging,
}
)}
>
{children}
</div>
);
};
export default Menu;
+95
View File
@@ -0,0 +1,95 @@
import classNames from 'classnames';
import { useCallback, useContext, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useDrag } from 'react-use-gesture';
import Store from '../../store';
import { SafeAreaContext } from './SafeArea';
const Modal = ({ open, onClose, children }) => {
const ref = useRef();
const [dragging, setDragging] = useState(false);
const [rect, setRect] = useState(null);
const [y, setY] = useState(100000);
const { top } = useContext(SafeAreaContext);
const safeAreaTop = top;
const _open = useCallback(() => {
console.log('Opening modal!', safeAreaTop);
setY(safeAreaTop);
}, [safeAreaTop]);
const _close = useCallback(() => {
if (!rect) {
return;
}
setY(rect.height + safeAreaTop);
}, [safeAreaTop, rect]);
// Get the layout rectangle for the modal
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect();
setRect(rect);
_close();
}, [safeAreaTop]);
// If open changes, open/close the modal
useLayoutEffect(() => {
if (open) {
_open();
} else {
_close();
}
}, [rect, open, _open, _close]);
const bind = useDrag(
({ down, movement: [mx, my] }) => {
setY(my < 0 ? safeAreaTop : my + safeAreaTop);
if (down) {
setDragging(true);
} else {
setDragging(false);
}
// If the drag ended, snap the menu back open or close it
if (!down) {
const mid = rect.height;
if (y > mid / 2) {
// Close
_close();
onClose();
} else {
// Re-open
_open();
}
}
},
{
axis: 'y',
}
);
return (
<div
ref={ref}
{...bind()}
className={classNames(
'fixed z-40 top-5 transform transform-gpu ranslate w-full h-full bg-white rounded-t-xl',
{
'ease-in-out duration-300 transition-transformation': !dragging,
}
)}
style={{
'--safe-area-top': `env(safe-area-inset-top, 0px)`,
height: `calc(100% - env(safe-area-inset-top, 0px) - 1.25rem)`,
touchAction: 'pan-y',
transform: `translateY(${y}px)`,
}}
>
{children}
</div>
);
};
export default Modal;
+209
View File
@@ -0,0 +1,209 @@
import { useEffect, useState } from 'react';
import { Plugins } from '@capacitor/core';
import Store from '../../store';
const Nav = ({ page }) => {
const [showMenu, setShowMenu] = useState(false);
useEffect(() => {
Plugins.StatusBar.setStyle({
style: 'DARK',
});
}, []);
return (
<nav
className="bg-gray-800 w-full flex-0 flex items-end flex-row z-10"
style={{
height: `calc(env(safe-area-inset-bottom, 0px) + 64px)`,
}}
>
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 flex-1">
<div className="relative flex items-center justify-between h-16">
<div
className="absolute inset-y-0 left-0 flex items-center sm:hidden"
onClick={() =>
Store.update(s => {
s.showMenu = true;
})
}
>
{/* Mobile menu button*/}
<button
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
aria-expanded="false"
>
<span className="sr-only">Open main menu</span>
{/* Icon when menu is closed. */}
{/*
Heroicon name: menu
Menu open: "hidden", Menu closed: "block"
*/}
<svg
className="block h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
{/* Icon when menu is open. */}
{/*
Heroicon name: x
Menu open: "block", Menu closed: "hidden"
*/}
<svg
className="hidden h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
<div className="flex-shrink-0 flex items-center">
<h1 className="text-gray-50">{page.title}</h1>
</div>
<div className="hidden sm:block sm:ml-6">
<div className="flex space-x-4">
{/* Current: "bg-gray-900 text-white", Default: "text-gray-300 hover:bg-gray-700 hover:text-white" */}
<a
href="#"
className="bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium"
>
Dashboard
</a>
<a
href="#"
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
>
Team
</a>
<a
href="#"
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
>
Projects
</a>
<a
href="#"
className="text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium"
>
Calendar
</a>
</div>
</div>
</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
<button
className="bg-gray-800 p-1 rounded-full text-gray-400 hover:text-white focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
onClick={() =>
Store.update(s => {
s.showNotifications = true;
})
}
>
<span className="sr-only">View notifications</span>
{/* Heroicon name: bell */}
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
/>
</svg>
</button>
{/* Profile dropdown */}
<div className="ml-3 relative">
<div>
<button
className="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
id="user-menu"
aria-haspopup="true"
onClick={() => setShowMenu(!showMenu)}
>
<span className="sr-only">Open user menu</span>
<img
className="h-8 w-8 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</button>
</div>
{/*
Profile dropdown panel, show/hide based on dropdown state.
Entering: "transition ease-out duration-100"
From: "transform opacity-0 scale-95"
To: "transform opacity-100 scale-100"
Leaving: "transition ease-in duration-75"
From: "transform opacity-100 scale-100"
To: "transform opacity-0 scale-95"
*/}
<div
className={`${
showMenu ? '' : 'hidden'
} origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5`}
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu"
>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
role="menuitem"
>
Your Profile
</a>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
role="menuitem"
>
Settings
</a>
<a
href="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
role="menuitem"
>
Sign out
</a>
</div>
</div>
</div>
</div>
</div>
</nav>
);
};
export default Nav;
+5
View File
@@ -0,0 +1,5 @@
# UI Components
These components are a mini-tailwind UI framework for building native mobile apps with web technologies.
These components are meant to be modified and customized to fit your app, and provide many common mobile UI patterns.
+30
View File
@@ -0,0 +1,30 @@
import { createContext, useEffect, useState } from 'react';
export const SafeAreaContext = createContext({ top: 0, bottom: 0 });
export const SafeAreaProvider = ({ children }) => {
const [safeAreaTop, setSafeAreaTop] = useState(0);
const [safeAreaBottom, setSafeAreaBottom] = useState(0);
useEffect(() => {
// I don't know why, but we can't get the value of this CSS variable
// until a bit of a delay, maybe something with Next?
setTimeout(() => {
const safeAreaTop = parseInt(
window.getComputedStyle(document.documentElement).getPropertyValue('--safe-area-top')
);
const safeAreaBottom = window
.getComputedStyle(document.documentElement)
.getPropertyValue('--safe-area-bottom');
setSafeAreaTop(safeAreaTop);
setSafeAreaBottom(safeAreaBottom);
}, 500);
}, []);
return (
<SafeAreaContext.Provider value={{ top: safeAreaTop, bottom: safeAreaBottom }}>
{children}
</SafeAreaContext.Provider>
);
};
+13
View File
@@ -0,0 +1,13 @@
import classNames from 'classnames';
const Tab = ({ title, icon, selected, selectedIcon, onClick }) => (
<a onClick={onClick} href="#" className={classNames('px-6 rounded-md text-sm text-center font-medium cursor-pointer', {
'text-gray-500': !selected,
'text-gray-800': selected
})}>
{icon && <ion-icon name={selected ? selectedIcon : icon } className="cursor-pointer" style={{ fontSize: '18px' }}/>}
<label className="block cursor-pointer">{title}</label>
</a>
)
export default Tab;
+12
View File
@@ -0,0 +1,12 @@
const TabBar = ({ children }) => (
<nav
id="tab-bar"
className="py-2 h-16 bg-white w-full flex justify-center items-start bg-gray-50"
style={{
height: `calc(env(safe-area-inset-bottom, 0px) + 56px)`
}}>
{children}
</nav>
)
export default TabBar;