nitro-web 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +9 -0
- package/.eslintrc.json +86 -0
- package/_example/.env-example +16 -0
- package/_example/client/config.ts +5 -0
- package/_example/client/css/index.css +35 -0
- package/_example/client/fonts/Roboto-Bold.ttf +0 -0
- package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Italic.ttf +0 -0
- package/_example/client/fonts/Roboto-Medium.ttf +0 -0
- package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
- package/_example/client/fonts/Roboto-Regular.ttf +0 -0
- package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
- package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
- package/_example/client/imgs/android-chrome-512x512.png +0 -0
- package/_example/client/imgs/favicon.png +0 -0
- package/_example/client/imgs/icons/calendar.svg +3 -0
- package/_example/client/imgs/icons/email.svg +6 -0
- package/_example/client/imgs/icons/eye-open.svg +4 -0
- package/_example/client/imgs/icons/eye.svg +5 -0
- package/_example/client/imgs/icons/filter.svg +7 -0
- package/_example/client/imgs/icons/left-circle.svg +3 -0
- package/_example/client/imgs/icons/left.svg +3 -0
- package/_example/client/imgs/icons/line-options.svg +5 -0
- package/_example/client/imgs/icons/line.svg +3 -0
- package/_example/client/imgs/icons/person.svg +7 -0
- package/_example/client/imgs/icons/plus-circle.svg +5 -0
- package/_example/client/imgs/icons/plus.svg +5 -0
- package/_example/client/imgs/icons/right-circle.svg +3 -0
- package/_example/client/imgs/icons/right.svg +3 -0
- package/_example/client/imgs/icons/search.svg +3 -0
- package/_example/client/imgs/icons/shield.svg +6 -0
- package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/_example/client/imgs/icons/tick-circle.svg +6 -0
- package/_example/client/imgs/icons/tick.svg +5 -0
- package/_example/client/imgs/icons/up2-small.svg +4 -0
- package/_example/client/imgs/icons/up2.svg +4 -0
- package/_example/client/imgs/icons/updown.svg +6 -0
- package/_example/client/imgs/icons/v-big-dark.svg +3 -0
- package/_example/client/imgs/icons/v-dark.svg +3 -0
- package/_example/client/imgs/icons/v.svg +3 -0
- package/_example/client/imgs/icons/v2-active.svg +6 -0
- package/_example/client/imgs/icons/x1.svg +4 -0
- package/_example/client/imgs/logo/logo-white.svg +20 -0
- package/_example/client/imgs/logo/logo.svg +20 -0
- package/_example/client/imgs/no-image.jpg +0 -0
- package/_example/client/imgs/user.jpg +0 -0
- package/_example/client/index.html +12 -0
- package/_example/client/index.ts +47 -0
- package/_example/components/auth.api.js +1 -0
- package/_example/components/index.tsx +225 -0
- package/_example/components/partials/layouts.tsx +5 -0
- package/_example/components/settings.api.js +1 -0
- package/_example/server/config.js +120 -0
- package/_example/server/email/welcome.html +27 -0
- package/_example/server/index.js +32 -0
- package/_example/tailwind.config.js +84 -0
- package/_example/tsconfig.json +32 -0
- package/_example/types.d.ts +7 -0
- package/_example/webpack.config.js +4 -0
- package/client/app.js +300 -0
- package/client/css/components.css +84 -0
- package/client/css/fonts.css +67 -0
- package/client/imgs/icons/calendar.svg +3 -0
- package/client/imgs/icons/email.svg +6 -0
- package/client/imgs/icons/eye-open.svg +4 -0
- package/client/imgs/icons/eye.svg +5 -0
- package/client/imgs/icons/filter.svg +7 -0
- package/client/imgs/icons/left-circle.svg +3 -0
- package/client/imgs/icons/left.svg +3 -0
- package/client/imgs/icons/line-options.svg +5 -0
- package/client/imgs/icons/line.svg +3 -0
- package/client/imgs/icons/person.svg +7 -0
- package/client/imgs/icons/plus-circle.svg +5 -0
- package/client/imgs/icons/plus.svg +5 -0
- package/client/imgs/icons/right-circle.svg +3 -0
- package/client/imgs/icons/right.svg +3 -0
- package/client/imgs/icons/search.svg +3 -0
- package/client/imgs/icons/shield.svg +6 -0
- package/client/imgs/icons/tick-circle-solid.svg +8 -0
- package/client/imgs/icons/tick-circle.svg +6 -0
- package/client/imgs/icons/tick.svg +5 -0
- package/client/imgs/icons/up2-small.svg +4 -0
- package/client/imgs/icons/up2.svg +4 -0
- package/client/imgs/icons/updown.svg +6 -0
- package/client/imgs/icons/v-big-dark.svg +3 -0
- package/client/imgs/icons/v-dark.svg +3 -0
- package/client/imgs/icons/v.svg +3 -0
- package/client/imgs/icons/v2-active.svg +6 -0
- package/client/imgs/icons/x1.svg +4 -0
- package/client.js +42 -0
- package/components/auth/auth.api.js +419 -0
- package/components/auth/reset.jsx +88 -0
- package/components/auth/signin.jsx +74 -0
- package/components/auth/signup.jsx +62 -0
- package/components/billing/stripe.api.js +267 -0
- package/components/partials/element/accordion.jsx +82 -0
- package/components/partials/element/avatar.jsx +28 -0
- package/components/partials/element/button.jsx +66 -0
- package/components/partials/element/dropdown.jsx +185 -0
- package/components/partials/element/initials.jsx +56 -0
- package/components/partials/element/message.jsx +124 -0
- package/components/partials/element/modal.jsx +229 -0
- package/components/partials/element/sidebar.jsx +166 -0
- package/components/partials/element/tooltip.jsx +146 -0
- package/components/partials/element/topbar.jsx +25 -0
- package/components/partials/form/checkbox.jsx +74 -0
- package/components/partials/form/drop-handler.jsx +62 -0
- package/components/partials/form/drop.jsx +125 -0
- package/components/partials/form/form-error.jsx +21 -0
- package/components/partials/form/input-color.jsx +77 -0
- package/components/partials/form/input-currency.jsx +133 -0
- package/components/partials/form/input-date.jsx +223 -0
- package/components/partials/form/input.jsx +131 -0
- package/components/partials/form/location.jsx +212 -0
- package/components/partials/form/select.jsx +369 -0
- package/components/partials/form/toggle.jsx +46 -0
- package/components/partials/is-first-render.js +15 -0
- package/components/partials/layout/layout1.jsx +32 -0
- package/components/partials/layout/layout2.jsx +47 -0
- package/components/partials/not-found.jsx +7 -0
- package/components/partials/styleguide.jsx +252 -0
- package/components/settings/settings-account.jsx +143 -0
- package/components/settings/settings-business.jsx +121 -0
- package/components/settings/settings-team--member.jsx +108 -0
- package/components/settings/settings-team.jsx +76 -0
- package/components/settings/settings.api.js +54 -0
- package/package.json +175 -0
- package/readme.md +43 -0
- package/server/email/index.js +192 -0
- package/server/email/partials/email.css +153 -0
- package/server/email/partials/layout1.swig +92 -0
- package/server/email/partials/line.swig +8 -0
- package/server/email/partials/vert-10.swig +8 -0
- package/server/email/partials/vert-15.swig +8 -0
- package/server/email/partials/vert-20.swig +8 -0
- package/server/email/partials/vert-25.swig +8 -0
- package/server/email/partials/vert-30.swig +8 -0
- package/server/email/partials/vert-35.swig +8 -0
- package/server/email/partials/vert-50.swig +8 -0
- package/server/email/reset-password.html +21 -0
- package/server/email/welcome.html +21 -0
- package/server/models/company.js +76 -0
- package/server/models/user.js +45 -0
- package/server/router.js +355 -0
- package/server.js +20 -0
- package/util.js +1145 -0
- package/webpack.config.js +302 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Todo: show correct message type, e.g. error, warning, info, success `${store.message.type || 'success'}`
|
|
2
|
+
import { isObject, isString, queryObject } from '../../../util.js'
|
|
3
|
+
import { Transition } from '@headlessui/react'
|
|
4
|
+
import { CheckCircleIcon } from '@heroicons/react/24/outline'
|
|
5
|
+
import { XMarkIcon } from '@heroicons/react/20/solid'
|
|
6
|
+
|
|
7
|
+
export function Message() {
|
|
8
|
+
/**
|
|
9
|
+
* Shows a message
|
|
10
|
+
* Triggered by navigating to a link with a valid query string, or
|
|
11
|
+
* by setting store.message to a string or more explicitly, to an object:
|
|
12
|
+
* {
|
|
13
|
+
* text: {string|JSX} - Text to be shown
|
|
14
|
+
* type: <string> - 'warning', 'error', 'info', 'success' (default)
|
|
15
|
+
* timeout: <integer> - Seconds to automatically close the message, 0 = never, (default 9s)
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
const devDontHide = false
|
|
19
|
+
const [store, setStore] = sharedStore.useTracked()
|
|
20
|
+
const [visible, setVisible] = useState(false)
|
|
21
|
+
const location = useLocation()
|
|
22
|
+
const messageQueryMap = {
|
|
23
|
+
'added': { type: 'success', text: 'Added successfully 👍️' },
|
|
24
|
+
'created': { type: 'success', text: 'Created successfully 👍️' },
|
|
25
|
+
'error': { type: 'error', text: 'Sorry, there was an error' },
|
|
26
|
+
'oauth-error': { type: 'error', text: 'There was an error trying to signin, please try again' },
|
|
27
|
+
'removed': { type: 'success', text: 'Removed' },
|
|
28
|
+
'signin': { type: 'error', text: 'Please sign in to access this page' },
|
|
29
|
+
'updated': { type: 'success', text: 'Updated successfully' },
|
|
30
|
+
'unauth': { type: 'error', text: 'You are unauthorised' },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
return () => {
|
|
35
|
+
setStore(s => ({ ...s, message: '' }))
|
|
36
|
+
}
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
// Finds a message in a query string and show it
|
|
41
|
+
let message
|
|
42
|
+
let query = queryObject(location.search, true)
|
|
43
|
+
for (let key in query) {
|
|
44
|
+
if (!query.hasOwnProperty(key)) continue
|
|
45
|
+
for (let key2 in messageQueryMap) {
|
|
46
|
+
if (key != key2) continue
|
|
47
|
+
message = { ...messageQueryMap[key] }
|
|
48
|
+
if (query[key] !== true) message.text = decodeURIComponent(query[key])
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (message) setStore(s => ({ ...s, message: message }))
|
|
52
|
+
}, [location.search])
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
// Message detection and autohiding
|
|
56
|
+
let now = new Date().getTime()
|
|
57
|
+
|
|
58
|
+
if (!store.message) {
|
|
59
|
+
return
|
|
60
|
+
// Convert a string into a message object
|
|
61
|
+
} else if (isString(store.message)) {
|
|
62
|
+
setStore(s => ({ ...s, message: { type: 'success', text: store.message, date: now }}))
|
|
63
|
+
// Add a date to the message
|
|
64
|
+
} else if (!store.message.date) {
|
|
65
|
+
setStore(s => ({ ...s, message: { ...store.message, date: now }}))
|
|
66
|
+
// Show message and hide it again after some time. Send back cleanup if store.message changes
|
|
67
|
+
} else if (store.message && now - 500 < store.message.date) {
|
|
68
|
+
let timeout1 = setTimeout(() => setVisible(true), 50)
|
|
69
|
+
if (store.message.timeout !== 0 && !devDontHide) var timeout2 = setTimeout(hide, store.message.timeout || 5000)
|
|
70
|
+
return () => {
|
|
71
|
+
clearTimeout(timeout1)
|
|
72
|
+
clearTimeout(timeout2)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}, [JSON.stringify(store.message)])
|
|
76
|
+
|
|
77
|
+
function hide() {
|
|
78
|
+
setVisible(false)
|
|
79
|
+
setTimeout(() => setStore(s => ({ ...s, message: null })), 250)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
{/* Global notification live region, render this permanently at the end of the document */}
|
|
85
|
+
<div
|
|
86
|
+
aria-live="assertive"
|
|
87
|
+
className="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6 z-20"
|
|
88
|
+
>
|
|
89
|
+
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
|
|
90
|
+
{/* Notification panel, dynamically insert this into the live region when it needs to be displayed */}
|
|
91
|
+
<Transition show={isObject(store.message) && visible}>
|
|
92
|
+
<div className="pointer-events-auto max-w-[350px] overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black/5
|
|
93
|
+
transition data-[closed]:data-[enter]:translate-y-2 data-[enter]:transform data-[closed]:opacity-0 data-[enter]:duration-300
|
|
94
|
+
data-[leave]:duration-100 data-[enter]:ease-out data-[leave]:ease-in data-[closed]:data-[enter]:sm:translate-x-2
|
|
95
|
+
data-[closed]:data-[enter]:sm:translate-y-0">
|
|
96
|
+
<div className="p-4">
|
|
97
|
+
<div className="flex items-start">
|
|
98
|
+
<div className="shrink-0">
|
|
99
|
+
<CheckCircleIcon aria-hidden="true" className="size-6 text-green-400" />
|
|
100
|
+
</div>
|
|
101
|
+
<div className="ml-3 flex-1 pt-0.5">
|
|
102
|
+
<p className="text-sm font-medium text-gray-900">{store.message?.text}</p>
|
|
103
|
+
{/* <p className="mt-1 text-sm text-gray-500">{store.message.text}</p> */}
|
|
104
|
+
</div>
|
|
105
|
+
<div className="ml-4 flex shrink-0">
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
onClick={hide}
|
|
109
|
+
className="inline-flex rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2
|
|
110
|
+
focus:ring-indigo-500 focus:ring-offset-2"
|
|
111
|
+
>
|
|
112
|
+
<span className="sr-only">Close</span>
|
|
113
|
+
<XMarkIcon aria-hidden="true" className="size-5" />
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</Transition>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// todo: finish tailwind conversion
|
|
2
|
+
import { css } from 'twin.macro'
|
|
3
|
+
import { IsFirstRender } from '../is-first-render.js'
|
|
4
|
+
import SvgX1 from '../../../client/imgs/icons/x1.svg'
|
|
5
|
+
|
|
6
|
+
export function Modal({ show, setShow, children, className, maxWidth, minHeight, dismissable = true }) {
|
|
7
|
+
const [state, setState] = useState()
|
|
8
|
+
const containerEl = useRef()
|
|
9
|
+
const isFirst = IsFirstRender()
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
createScrollbarClasses()
|
|
13
|
+
return () => {
|
|
14
|
+
elementWithScrollbar().classList.remove('scrollbarPadding')
|
|
15
|
+
} // cleanup
|
|
16
|
+
}, [])
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (show) {
|
|
20
|
+
elementWithScrollbar().classList.add('scrollbarPadding')
|
|
21
|
+
setState('modal-open')
|
|
22
|
+
} else if (!isFirst) {
|
|
23
|
+
// Dont close if first render (forgot what use case this was needed for)
|
|
24
|
+
setTimeout(() => {
|
|
25
|
+
// If another modal is being opened, force close the container for a smoother transition
|
|
26
|
+
if (document.getElementsByClassName('modal-open').length > 1) {
|
|
27
|
+
setState('modal-close-immediately')
|
|
28
|
+
} else {
|
|
29
|
+
setState('')
|
|
30
|
+
elementWithScrollbar().classList.remove('scrollbarPadding')
|
|
31
|
+
}
|
|
32
|
+
}, 10)
|
|
33
|
+
}
|
|
34
|
+
// There is a bug during hot-reloading where the modal does't open if we don't ensure
|
|
35
|
+
// the same truthy/falsey type is used.
|
|
36
|
+
}, [!!show])
|
|
37
|
+
|
|
38
|
+
function elementWithScrollbar() {
|
|
39
|
+
// this needs to be non-body element otherwise the Modal.jsx doesn't open/close smoothly
|
|
40
|
+
//document.getElementsByTagName('body')[0] // document.getElementsByClassName('page')[0]
|
|
41
|
+
return document.getElementById('app')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function onClick(e) {
|
|
45
|
+
let clickedOnContainer = containerEl.current && containerEl.current.contains(e.target)
|
|
46
|
+
if (!clickedOnContainer && dismissable) {
|
|
47
|
+
setShow(false)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createScrollbarClasses() {
|
|
52
|
+
/**
|
|
53
|
+
* Creates reusable margin and padding classes containing the scrollbar width and
|
|
54
|
+
* sets window.scrollbarWidth
|
|
55
|
+
* @return width
|
|
56
|
+
*/
|
|
57
|
+
if (typeof window.scrollbarWidth !== 'undefined') return
|
|
58
|
+
|
|
59
|
+
var outer = document.createElement('div')
|
|
60
|
+
outer.style.visibility = 'hidden'
|
|
61
|
+
outer.style.width = '100px'
|
|
62
|
+
outer.style.margin = '0px'
|
|
63
|
+
outer.style.padding = '0px'
|
|
64
|
+
outer.style.border = '0'
|
|
65
|
+
document.body.appendChild(outer)
|
|
66
|
+
|
|
67
|
+
var widthNoScroll = outer.offsetWidth
|
|
68
|
+
// force scrollbars
|
|
69
|
+
outer.style.overflow = 'scroll'
|
|
70
|
+
|
|
71
|
+
// add innerdiv
|
|
72
|
+
var inner = document.createElement('div')
|
|
73
|
+
inner.style.width = '100%'
|
|
74
|
+
outer.appendChild(inner)
|
|
75
|
+
|
|
76
|
+
var widthWithScroll = inner.offsetWidth
|
|
77
|
+
|
|
78
|
+
// Remove divs
|
|
79
|
+
outer.parentNode.removeChild(outer)
|
|
80
|
+
let width = (window.scrollbarWidth = widthNoScroll - widthWithScroll)
|
|
81
|
+
|
|
82
|
+
// Create new inline stylesheet and append to the head
|
|
83
|
+
let style = document.createElement('style')
|
|
84
|
+
let css = (
|
|
85
|
+
'.scrollbarPadding {padding-right:' + width + 'px !important; overflow:hidden !important;}' +
|
|
86
|
+
'.scrollbarMargin {margin-right:' + width + 'px !important; overflow:hidden !important;}'
|
|
87
|
+
)
|
|
88
|
+
style.type = 'text/css'
|
|
89
|
+
if (style.styleSheet) style.styleSheet.cssText = css //<=IE8
|
|
90
|
+
else style.appendChild(document.createTextNode(css))
|
|
91
|
+
document.getElementsByTagName('head')[0].appendChild(style)
|
|
92
|
+
|
|
93
|
+
return width
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
<div css={style} class={`${state}`} onClick={(e) => e.stopPropagation()}>
|
|
98
|
+
<div class="modal-bg wrapper scrollbarPadding"></div>
|
|
99
|
+
<div class="modal-container">
|
|
100
|
+
{/* we also need to be able to scroll without closing */}
|
|
101
|
+
<div onMouseDown={onClick}>
|
|
102
|
+
<div
|
|
103
|
+
ref={containerEl}
|
|
104
|
+
style={{ maxWidth: maxWidth || '740px', minHeight: typeof minHeight == 'undefined' ? '487px' : minHeight }}
|
|
105
|
+
class={`modal1 ${className}`}
|
|
106
|
+
>
|
|
107
|
+
<div class="modal-close" onClick={() => { if (dismissable) { setShow(false) }}}><SvgX1 /></div>
|
|
108
|
+
{children}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const style = () => css`
|
|
117
|
+
/* Modal structure */
|
|
118
|
+
& {
|
|
119
|
+
position: fixed;
|
|
120
|
+
top: 0;
|
|
121
|
+
width: 100%;
|
|
122
|
+
height: calc(100vh);
|
|
123
|
+
z-index: 699;
|
|
124
|
+
.modal-bg {
|
|
125
|
+
position: absolute !important;
|
|
126
|
+
display: flex;
|
|
127
|
+
top: 0;
|
|
128
|
+
left: 0;
|
|
129
|
+
right: 0;
|
|
130
|
+
bottom: 0;
|
|
131
|
+
box-sizing: content-box;
|
|
132
|
+
&:before {
|
|
133
|
+
content: '';
|
|
134
|
+
display: block;
|
|
135
|
+
flex: 1;
|
|
136
|
+
background: rgba(255, 255, 255, 0.82);
|
|
137
|
+
/* backdrop-filter: blur(1px);
|
|
138
|
+
-webkit-backdrop-filter: blur(1px); */
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
.modal-container {
|
|
142
|
+
position: relative;
|
|
143
|
+
height: calc(100vh);
|
|
144
|
+
// horisontal centering
|
|
145
|
+
> div {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
justify-content: center;
|
|
149
|
+
min-height: 100%;
|
|
150
|
+
// vertical centering
|
|
151
|
+
> div {
|
|
152
|
+
margin: 30px 20px 90px;
|
|
153
|
+
width: 100%;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
&.modal-close-immediately {
|
|
158
|
+
.modal-container > div > div {
|
|
159
|
+
transition: none !important;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Animation */
|
|
165
|
+
|
|
166
|
+
& {
|
|
167
|
+
left: -100%;
|
|
168
|
+
transition: left 0s 0.2s;
|
|
169
|
+
}
|
|
170
|
+
.modal-bg {
|
|
171
|
+
opacity: 0;
|
|
172
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
173
|
+
}
|
|
174
|
+
.modal-container {
|
|
175
|
+
/*overflow: hidden;*/
|
|
176
|
+
overflow-y: scroll;
|
|
177
|
+
overflow-x: auto;
|
|
178
|
+
}
|
|
179
|
+
.modal-container > div > div {
|
|
180
|
+
opacity: 0;
|
|
181
|
+
transform: scale(0.97);
|
|
182
|
+
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
183
|
+
}
|
|
184
|
+
&.modal-open {
|
|
185
|
+
left: 0;
|
|
186
|
+
transition: none;
|
|
187
|
+
.modal-bg {
|
|
188
|
+
opacity: 1;
|
|
189
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
190
|
+
}
|
|
191
|
+
.modal-container {
|
|
192
|
+
overflow-y: scroll;
|
|
193
|
+
overflow-x: auto;
|
|
194
|
+
}
|
|
195
|
+
.modal-container > div > div {
|
|
196
|
+
opacity: 1;
|
|
197
|
+
transform: scale(1);
|
|
198
|
+
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Modal customisations */
|
|
203
|
+
|
|
204
|
+
.modal1 {
|
|
205
|
+
background: white;
|
|
206
|
+
border: 2px solid #27242C;
|
|
207
|
+
box-shadow: 0px 1px 29px rgba(31, 29, 36, 0.07);
|
|
208
|
+
border-radius: 8px;
|
|
209
|
+
.subtitle {
|
|
210
|
+
margin-bottom: 34px; // same as form pages
|
|
211
|
+
}
|
|
212
|
+
.modal-close {
|
|
213
|
+
position: absolute;
|
|
214
|
+
margin: 10px;
|
|
215
|
+
padding: 15px 20px;
|
|
216
|
+
top: 0;
|
|
217
|
+
right: 0;
|
|
218
|
+
cursor: pointer;
|
|
219
|
+
line {
|
|
220
|
+
transition: all 0.1s;
|
|
221
|
+
}
|
|
222
|
+
&:hover {
|
|
223
|
+
line {
|
|
224
|
+
stroke: ${theme('colors.primary-dark')};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
`
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// Component: https://tailwindui.com/components/application-ui/application-shells/sidebar#component-a69d85b6237ea2ad506c00ef1cd39a38
|
|
2
|
+
import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessui/react'
|
|
3
|
+
import {
|
|
4
|
+
Bars3Icon,
|
|
5
|
+
HomeIcon,
|
|
6
|
+
UsersIcon,
|
|
7
|
+
XMarkIcon,
|
|
8
|
+
ArrowLeftCircleIcon,
|
|
9
|
+
PaintBrushIcon,
|
|
10
|
+
} from '@heroicons/react/24/outline'
|
|
11
|
+
|
|
12
|
+
const sidebarWidth = 'lg:w-80'
|
|
13
|
+
|
|
14
|
+
function classNames(...classes) {
|
|
15
|
+
return classes.filter(Boolean).join(' ')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Sidebar({ Logo }) {
|
|
19
|
+
const [sidebarOpen, setSidebarOpen] = useState(false)
|
|
20
|
+
return (
|
|
21
|
+
<>
|
|
22
|
+
{/* mobile sidebar opened */}
|
|
23
|
+
<Dialog open={sidebarOpen} onClose={setSidebarOpen} className="relative z-50 lg:hidden">
|
|
24
|
+
<DialogBackdrop
|
|
25
|
+
transition
|
|
26
|
+
className="fixed inset-0 bg-gray-900/80 transition-opacity duration-300 ease-linear data-[closed]:opacity-0"
|
|
27
|
+
/>
|
|
28
|
+
<div className="fixed inset-0 flex">
|
|
29
|
+
<DialogPanel
|
|
30
|
+
transition
|
|
31
|
+
className="relative mr-16 flex w-full max-w-xs flex-1 transform transition duration-300 ease-in-out
|
|
32
|
+
data-[closed]:-translate-x-full"
|
|
33
|
+
>
|
|
34
|
+
<TransitionChild>
|
|
35
|
+
<div className="absolute left-full top-0 flex w-16 justify-center pt-5 duration-300 ease-in-out data-[closed]:opacity-0">
|
|
36
|
+
<button type="button" onClick={() => setSidebarOpen(false)} className="-m-2.5 p-2.5">
|
|
37
|
+
<XMarkIcon aria-hidden="true" className="size-6 text-white" />
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
</TransitionChild>
|
|
41
|
+
<SidebarContents Logo={Logo} />
|
|
42
|
+
</DialogPanel>
|
|
43
|
+
</div>
|
|
44
|
+
</Dialog>
|
|
45
|
+
|
|
46
|
+
{/* Static sidebar for desktop */}
|
|
47
|
+
<div className={`hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:flex-col ${sidebarWidth}`}>
|
|
48
|
+
<SidebarContents Logo={Logo} />
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* mobile sidebar closed */}
|
|
52
|
+
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden">
|
|
53
|
+
<button type="button" onClick={() => setSidebarOpen(true)} className="-m-2.5 p-2.5 text-gray-700 lg:hidden">
|
|
54
|
+
<Bars3Icon aria-hidden="true" className="size-6" />
|
|
55
|
+
</button>
|
|
56
|
+
<div className="flex-1 text-sm/6 font-semibold text-gray-900">Dashboard</div>
|
|
57
|
+
<Link to="#">
|
|
58
|
+
<img alt="" src="/assets/imgs/user.jpg" className="size-8 rounded-full bg-gray-50" />
|
|
59
|
+
</Link>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class={`${sidebarWidth}`} />
|
|
63
|
+
</>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function SidebarContents ({ Logo }) {
|
|
68
|
+
const location = useLocation()
|
|
69
|
+
const [{ user }] = sharedStore.useTracked()
|
|
70
|
+
|
|
71
|
+
function isActive(path) {
|
|
72
|
+
if (path == '/' && location.pathname == path) return 'is-active'
|
|
73
|
+
else if (path != '/' && location.pathname.match(`^${path}`)) return 'is-active'
|
|
74
|
+
else return ''
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const navigation = [
|
|
78
|
+
{ name: 'Dashboard', href: '/', icon: HomeIcon },
|
|
79
|
+
{ name: 'Pricing (example)', href: '/pricing', icon: UsersIcon },
|
|
80
|
+
{ name: 'Styleguide', href: '/styleguide', icon: PaintBrushIcon },
|
|
81
|
+
{ name: 'Signout', href: '/signout', icon: ArrowLeftCircleIcon },
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
const teams = [
|
|
85
|
+
{ id: 1, name: 'Team 1', href: '#', initial: 'T' },
|
|
86
|
+
{ id: 2, name: 'Team 2', href: '#', initial: 'H' },
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
// Sidebar component, swap this element with another sidebar if you like
|
|
90
|
+
return (
|
|
91
|
+
<div className="flex grow flex-col gap-y-8 overflow-y-auto bg-white py-5 px-10 lg:border-r lg:border-gray-200">
|
|
92
|
+
<div className="flex h-16 shrink-0 items-center">
|
|
93
|
+
<Logo alt="Nitro" width="70" />
|
|
94
|
+
</div>
|
|
95
|
+
<nav className="flex flex-1 flex-col">
|
|
96
|
+
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
|
97
|
+
<li>
|
|
98
|
+
<ul role="list" className="-mx-2 space-y-1">
|
|
99
|
+
{navigation.map((item) => (
|
|
100
|
+
<li key={item.name}>
|
|
101
|
+
<Link
|
|
102
|
+
to={item.href}
|
|
103
|
+
className={classNames(
|
|
104
|
+
isActive(item.href)
|
|
105
|
+
? 'bg-gray-50 text-indigo-600'
|
|
106
|
+
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
|
|
107
|
+
'group flex gap-x-3 items-center rounded-md p-2 text-sm/6 font-semibold'
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
<item.icon
|
|
111
|
+
className={classNames(
|
|
112
|
+
isActive(item.href) ? 'text-indigo-600' : 'text-gray-400 group-hover:text-indigo-600',
|
|
113
|
+
'size-5 shrink-0'
|
|
114
|
+
)}
|
|
115
|
+
/>
|
|
116
|
+
{item.name}
|
|
117
|
+
</Link>
|
|
118
|
+
</li>
|
|
119
|
+
))}
|
|
120
|
+
</ul>
|
|
121
|
+
</li>
|
|
122
|
+
<li>
|
|
123
|
+
<div className="text-xs/6 font-semibold text-gray-400">Your teams</div>
|
|
124
|
+
<ul role="list" className="-mx-2 mt-2 space-y-1">
|
|
125
|
+
{teams.map((team) => (
|
|
126
|
+
<li key={team.name}>
|
|
127
|
+
<Link
|
|
128
|
+
to={team.href}
|
|
129
|
+
className={classNames(
|
|
130
|
+
isActive(team.href)
|
|
131
|
+
? 'bg-gray-50 text-indigo-600'
|
|
132
|
+
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
|
|
133
|
+
'group flex gap-x-3 rounded-md p-2 text-sm/6 font-semibold'
|
|
134
|
+
)}
|
|
135
|
+
>
|
|
136
|
+
<span
|
|
137
|
+
className={classNames(
|
|
138
|
+
isActive(team.href)
|
|
139
|
+
? 'border-indigo-600 text-indigo-600'
|
|
140
|
+
: 'border-gray-200 text-gray-400 group-hover:border-indigo-600 group-hover:text-indigo-600',
|
|
141
|
+
'flex size-6 shrink-0 items-center justify-center rounded-lg border bg-white text-[0.625rem] font-medium'
|
|
142
|
+
)}
|
|
143
|
+
>
|
|
144
|
+
{team.initial}
|
|
145
|
+
</span>
|
|
146
|
+
<span className="truncate">{team.name}</span>
|
|
147
|
+
</Link>
|
|
148
|
+
</li>
|
|
149
|
+
))}
|
|
150
|
+
</ul>
|
|
151
|
+
</li>
|
|
152
|
+
|
|
153
|
+
<li className="-mx-6 mt-auto hidden lg:block">
|
|
154
|
+
<Link
|
|
155
|
+
to="#"
|
|
156
|
+
className="flex items-center gap-x-4 px-6 py-3 text-sm/6 font-semibold text-gray-900 hover:bg-gray-50"
|
|
157
|
+
>
|
|
158
|
+
<img alt="" src="/assets/imgs/user.jpg" className="size-8 rounded-full bg-gray-50" />
|
|
159
|
+
<span aria-hidden="true" class="truncate1">{user?.name || 'Guest'}</span>
|
|
160
|
+
</Link>
|
|
161
|
+
</li>
|
|
162
|
+
</ul>
|
|
163
|
+
</nav>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// todo: finish tailwind conversion
|
|
2
|
+
import { css } from 'twin.macro'
|
|
3
|
+
|
|
4
|
+
export function Tooltip({ text, children, className, classNamePopup, isSmall }) {
|
|
5
|
+
return (
|
|
6
|
+
<div class={`${className} relative inline-block align-middle`} css={style}>
|
|
7
|
+
{
|
|
8
|
+
text?.length || text?.props
|
|
9
|
+
? <>
|
|
10
|
+
<div class="tooltip-trigger ">{children}</div>
|
|
11
|
+
<div class={`tooltip-popup ${classNamePopup||''} ${isSmall ? 'is-small' : ''}`}>{text}</div>
|
|
12
|
+
</>
|
|
13
|
+
: children
|
|
14
|
+
}
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const style = () => css`
|
|
20
|
+
.tooltip-popup {
|
|
21
|
+
position: absolute;
|
|
22
|
+
display: block;
|
|
23
|
+
margin-top: -10000px;
|
|
24
|
+
width: 200px;
|
|
25
|
+
padding: 14px;
|
|
26
|
+
font-weight: 400;
|
|
27
|
+
font-size: 11.5px;
|
|
28
|
+
line-height: 1.3;
|
|
29
|
+
letter-spacing: 0.5px;
|
|
30
|
+
text-align: center;
|
|
31
|
+
border-radius: 6px;
|
|
32
|
+
background: black;
|
|
33
|
+
color: white;
|
|
34
|
+
opacity: 0;
|
|
35
|
+
transition: opacity 0.15s ease, transform 0.15s ease, margin-top 0s 0.15s;
|
|
36
|
+
white-space: break-spaces;
|
|
37
|
+
overflow-wrap: break-word;
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
z-index: 9999;
|
|
40
|
+
&:after {
|
|
41
|
+
content: '';
|
|
42
|
+
position: absolute;
|
|
43
|
+
border-width: 6px;
|
|
44
|
+
border-style: solid;
|
|
45
|
+
}
|
|
46
|
+
// Variation
|
|
47
|
+
&.is-small {
|
|
48
|
+
width: 160px;
|
|
49
|
+
padding: 10px;
|
|
50
|
+
font-size: 11px;
|
|
51
|
+
}
|
|
52
|
+
// Positions
|
|
53
|
+
&.is-top,
|
|
54
|
+
&.is-top-left,
|
|
55
|
+
&:not(.is-top-left):not(.is-left):not(.is-right):not(.is-bottom):not(.is-bottom-left) {
|
|
56
|
+
bottom: 100%;
|
|
57
|
+
left: 50%;
|
|
58
|
+
transform: translateX(-50%) translateY(-15px);
|
|
59
|
+
&:after {
|
|
60
|
+
top: 100%;
|
|
61
|
+
left: 50%;
|
|
62
|
+
margin-left: -6px;
|
|
63
|
+
border-color: black transparent transparent transparent;
|
|
64
|
+
}
|
|
65
|
+
&.is-top-left {
|
|
66
|
+
left: 0px;
|
|
67
|
+
transform: translateX(0%) translateY(-15px);
|
|
68
|
+
&:after {
|
|
69
|
+
left: 28px;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
&.is-bottom,
|
|
74
|
+
&.is-bottom-left {
|
|
75
|
+
top: 100%;
|
|
76
|
+
left: 50%;
|
|
77
|
+
transform: translateX(-50%) translateY(15px) ;
|
|
78
|
+
&:after {
|
|
79
|
+
bottom: 100%;
|
|
80
|
+
left: 50%;
|
|
81
|
+
margin-left: -6px;
|
|
82
|
+
border-color: transparent transparent black transparent;
|
|
83
|
+
}
|
|
84
|
+
&.is-bottom-left {
|
|
85
|
+
left: 0px;
|
|
86
|
+
transform: translateX(0%) translateY(15px);
|
|
87
|
+
&:after {
|
|
88
|
+
left: 28px;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
&.is-left {
|
|
93
|
+
top: 50%;
|
|
94
|
+
right: 100%;
|
|
95
|
+
transform: translateX(-15px) translateY(-50%);
|
|
96
|
+
&:after {
|
|
97
|
+
top: 50%;
|
|
98
|
+
right: -12px;
|
|
99
|
+
margin-top: -6px;
|
|
100
|
+
border-color: transparent transparent transparent black;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
&.is-right {
|
|
104
|
+
top: 50%;
|
|
105
|
+
left: 100%;
|
|
106
|
+
transform: translateX(15px) translateY(-50%);
|
|
107
|
+
&:after {
|
|
108
|
+
top: 50%;
|
|
109
|
+
left: -12px;
|
|
110
|
+
margin-top: -6px;
|
|
111
|
+
border-color: transparent black transparent transparent;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
.tooltip-trigger {
|
|
116
|
+
/* trigger can come before tooltip-popup or wrap it */
|
|
117
|
+
position: relative !important;
|
|
118
|
+
&:hover .tooltip-popup,
|
|
119
|
+
&:hover + .tooltip-popup,
|
|
120
|
+
.tooltip-popup.is-active,
|
|
121
|
+
& + .tooltip-popup.is-active {
|
|
122
|
+
opacity: 1;
|
|
123
|
+
margin-top: 0;
|
|
124
|
+
transition: opacity 0.15s ease, transform 0.15s ease, margin-top 0s 0s;
|
|
125
|
+
&.is-top,
|
|
126
|
+
&:not(.is-top-left):not(.is-left):not(.is-right):not(.is-bottom):not(.is-bottom-left) {
|
|
127
|
+
transform: translateX(-50%) translateY(-10px);
|
|
128
|
+
}
|
|
129
|
+
&.is-top-left {
|
|
130
|
+
transform: translateX(0%) translateY(-10px);
|
|
131
|
+
}
|
|
132
|
+
&.is-bottom {
|
|
133
|
+
transform: translateX(-50%) translateY(10px);
|
|
134
|
+
}
|
|
135
|
+
&.is-bottom-left {
|
|
136
|
+
transform: translateX(0%) translateY(10px);
|
|
137
|
+
}
|
|
138
|
+
&.is-left {
|
|
139
|
+
transform: translateX(-10px) translateY(-50%);
|
|
140
|
+
}
|
|
141
|
+
&.is-right {
|
|
142
|
+
transform: translateX(10px) translateY(-50%);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
`
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function Topbar({ title, subtitle, submenu, btns, className }) {
|
|
2
|
+
/**
|
|
3
|
+
* @param {string|JSX} title
|
|
4
|
+
* @param {string|JSX} <subtitle>
|
|
5
|
+
* @param {string|JSX} <submenu>
|
|
6
|
+
* @param {url|function} <plusIconAction>
|
|
7
|
+
*/
|
|
8
|
+
return (
|
|
9
|
+
<div class={`flex justify-between items-end mb-6 ${className||''}`}>
|
|
10
|
+
<div class="flex flex-col min-h-12">
|
|
11
|
+
{ subtitle && <div class="py-2 text-sm">{subtitle}</div>}
|
|
12
|
+
<div class="flex items-center py-2">
|
|
13
|
+
<h1 class="h1 mb-0">{title}</h1>
|
|
14
|
+
</div>
|
|
15
|
+
{
|
|
16
|
+
submenu &&
|
|
17
|
+
<div class="pt-2 text-large weight-500">{submenu}</div>
|
|
18
|
+
}
|
|
19
|
+
</div>
|
|
20
|
+
<div class="">
|
|
21
|
+
{btns}
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
)
|
|
25
|
+
}
|