Move components to ui
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
const App = ({ children }) => (
|
||||
<div className="flex h-screen flex-col">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default App;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
const EdgeDrag = () => null;
|
||||
|
||||
export default EdgeDrag;
|
||||
@@ -0,0 +1,3 @@
|
||||
const List = ({ children, ...props }) => <div {...props}>{children}</div>;
|
||||
|
||||
export default List;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user