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.
Files changed (152) hide show
  1. package/.editorconfig +9 -0
  2. package/.eslintrc.json +86 -0
  3. package/_example/.env-example +16 -0
  4. package/_example/client/config.ts +5 -0
  5. package/_example/client/css/index.css +35 -0
  6. package/_example/client/fonts/Roboto-Bold.ttf +0 -0
  7. package/_example/client/fonts/Roboto-BoldItalic.ttf +0 -0
  8. package/_example/client/fonts/Roboto-Italic.ttf +0 -0
  9. package/_example/client/fonts/Roboto-Medium.ttf +0 -0
  10. package/_example/client/fonts/Roboto-MediumItalic.ttf +0 -0
  11. package/_example/client/fonts/Roboto-Regular.ttf +0 -0
  12. package/_example/client/fonts/inter-v13-latin-300.woff2 +0 -0
  13. package/_example/client/fonts/inter-v13-latin-500.woff2 +0 -0
  14. package/_example/client/fonts/inter-v13-latin-600.woff2 +0 -0
  15. package/_example/client/fonts/inter-v13-latin-700.woff2 +0 -0
  16. package/_example/client/fonts/inter-v13-latin-800.woff2 +0 -0
  17. package/_example/client/fonts/inter-v13-latin-900.woff2 +0 -0
  18. package/_example/client/fonts/inter-v13-latin-regular.woff2 +0 -0
  19. package/_example/client/imgs/android-chrome-512x512.png +0 -0
  20. package/_example/client/imgs/favicon.png +0 -0
  21. package/_example/client/imgs/icons/calendar.svg +3 -0
  22. package/_example/client/imgs/icons/email.svg +6 -0
  23. package/_example/client/imgs/icons/eye-open.svg +4 -0
  24. package/_example/client/imgs/icons/eye.svg +5 -0
  25. package/_example/client/imgs/icons/filter.svg +7 -0
  26. package/_example/client/imgs/icons/left-circle.svg +3 -0
  27. package/_example/client/imgs/icons/left.svg +3 -0
  28. package/_example/client/imgs/icons/line-options.svg +5 -0
  29. package/_example/client/imgs/icons/line.svg +3 -0
  30. package/_example/client/imgs/icons/person.svg +7 -0
  31. package/_example/client/imgs/icons/plus-circle.svg +5 -0
  32. package/_example/client/imgs/icons/plus.svg +5 -0
  33. package/_example/client/imgs/icons/right-circle.svg +3 -0
  34. package/_example/client/imgs/icons/right.svg +3 -0
  35. package/_example/client/imgs/icons/search.svg +3 -0
  36. package/_example/client/imgs/icons/shield.svg +6 -0
  37. package/_example/client/imgs/icons/tick-circle-solid.svg +8 -0
  38. package/_example/client/imgs/icons/tick-circle.svg +6 -0
  39. package/_example/client/imgs/icons/tick.svg +5 -0
  40. package/_example/client/imgs/icons/up2-small.svg +4 -0
  41. package/_example/client/imgs/icons/up2.svg +4 -0
  42. package/_example/client/imgs/icons/updown.svg +6 -0
  43. package/_example/client/imgs/icons/v-big-dark.svg +3 -0
  44. package/_example/client/imgs/icons/v-dark.svg +3 -0
  45. package/_example/client/imgs/icons/v.svg +3 -0
  46. package/_example/client/imgs/icons/v2-active.svg +6 -0
  47. package/_example/client/imgs/icons/x1.svg +4 -0
  48. package/_example/client/imgs/logo/logo-white.svg +20 -0
  49. package/_example/client/imgs/logo/logo.svg +20 -0
  50. package/_example/client/imgs/no-image.jpg +0 -0
  51. package/_example/client/imgs/user.jpg +0 -0
  52. package/_example/client/index.html +12 -0
  53. package/_example/client/index.ts +47 -0
  54. package/_example/components/auth.api.js +1 -0
  55. package/_example/components/index.tsx +225 -0
  56. package/_example/components/partials/layouts.tsx +5 -0
  57. package/_example/components/settings.api.js +1 -0
  58. package/_example/server/config.js +120 -0
  59. package/_example/server/email/welcome.html +27 -0
  60. package/_example/server/index.js +32 -0
  61. package/_example/tailwind.config.js +84 -0
  62. package/_example/tsconfig.json +32 -0
  63. package/_example/types.d.ts +7 -0
  64. package/_example/webpack.config.js +4 -0
  65. package/client/app.js +300 -0
  66. package/client/css/components.css +84 -0
  67. package/client/css/fonts.css +67 -0
  68. package/client/imgs/icons/calendar.svg +3 -0
  69. package/client/imgs/icons/email.svg +6 -0
  70. package/client/imgs/icons/eye-open.svg +4 -0
  71. package/client/imgs/icons/eye.svg +5 -0
  72. package/client/imgs/icons/filter.svg +7 -0
  73. package/client/imgs/icons/left-circle.svg +3 -0
  74. package/client/imgs/icons/left.svg +3 -0
  75. package/client/imgs/icons/line-options.svg +5 -0
  76. package/client/imgs/icons/line.svg +3 -0
  77. package/client/imgs/icons/person.svg +7 -0
  78. package/client/imgs/icons/plus-circle.svg +5 -0
  79. package/client/imgs/icons/plus.svg +5 -0
  80. package/client/imgs/icons/right-circle.svg +3 -0
  81. package/client/imgs/icons/right.svg +3 -0
  82. package/client/imgs/icons/search.svg +3 -0
  83. package/client/imgs/icons/shield.svg +6 -0
  84. package/client/imgs/icons/tick-circle-solid.svg +8 -0
  85. package/client/imgs/icons/tick-circle.svg +6 -0
  86. package/client/imgs/icons/tick.svg +5 -0
  87. package/client/imgs/icons/up2-small.svg +4 -0
  88. package/client/imgs/icons/up2.svg +4 -0
  89. package/client/imgs/icons/updown.svg +6 -0
  90. package/client/imgs/icons/v-big-dark.svg +3 -0
  91. package/client/imgs/icons/v-dark.svg +3 -0
  92. package/client/imgs/icons/v.svg +3 -0
  93. package/client/imgs/icons/v2-active.svg +6 -0
  94. package/client/imgs/icons/x1.svg +4 -0
  95. package/client.js +42 -0
  96. package/components/auth/auth.api.js +419 -0
  97. package/components/auth/reset.jsx +88 -0
  98. package/components/auth/signin.jsx +74 -0
  99. package/components/auth/signup.jsx +62 -0
  100. package/components/billing/stripe.api.js +267 -0
  101. package/components/partials/element/accordion.jsx +82 -0
  102. package/components/partials/element/avatar.jsx +28 -0
  103. package/components/partials/element/button.jsx +66 -0
  104. package/components/partials/element/dropdown.jsx +185 -0
  105. package/components/partials/element/initials.jsx +56 -0
  106. package/components/partials/element/message.jsx +124 -0
  107. package/components/partials/element/modal.jsx +229 -0
  108. package/components/partials/element/sidebar.jsx +166 -0
  109. package/components/partials/element/tooltip.jsx +146 -0
  110. package/components/partials/element/topbar.jsx +25 -0
  111. package/components/partials/form/checkbox.jsx +74 -0
  112. package/components/partials/form/drop-handler.jsx +62 -0
  113. package/components/partials/form/drop.jsx +125 -0
  114. package/components/partials/form/form-error.jsx +21 -0
  115. package/components/partials/form/input-color.jsx +77 -0
  116. package/components/partials/form/input-currency.jsx +133 -0
  117. package/components/partials/form/input-date.jsx +223 -0
  118. package/components/partials/form/input.jsx +131 -0
  119. package/components/partials/form/location.jsx +212 -0
  120. package/components/partials/form/select.jsx +369 -0
  121. package/components/partials/form/toggle.jsx +46 -0
  122. package/components/partials/is-first-render.js +15 -0
  123. package/components/partials/layout/layout1.jsx +32 -0
  124. package/components/partials/layout/layout2.jsx +47 -0
  125. package/components/partials/not-found.jsx +7 -0
  126. package/components/partials/styleguide.jsx +252 -0
  127. package/components/settings/settings-account.jsx +143 -0
  128. package/components/settings/settings-business.jsx +121 -0
  129. package/components/settings/settings-team--member.jsx +108 -0
  130. package/components/settings/settings-team.jsx +76 -0
  131. package/components/settings/settings.api.js +54 -0
  132. package/package.json +175 -0
  133. package/readme.md +43 -0
  134. package/server/email/index.js +192 -0
  135. package/server/email/partials/email.css +153 -0
  136. package/server/email/partials/layout1.swig +92 -0
  137. package/server/email/partials/line.swig +8 -0
  138. package/server/email/partials/vert-10.swig +8 -0
  139. package/server/email/partials/vert-15.swig +8 -0
  140. package/server/email/partials/vert-20.swig +8 -0
  141. package/server/email/partials/vert-25.swig +8 -0
  142. package/server/email/partials/vert-30.swig +8 -0
  143. package/server/email/partials/vert-35.swig +8 -0
  144. package/server/email/partials/vert-50.swig +8 -0
  145. package/server/email/reset-password.html +21 -0
  146. package/server/email/welcome.html +21 -0
  147. package/server/models/company.js +76 -0
  148. package/server/models/user.js +45 -0
  149. package/server/router.js +355 -0
  150. package/server.js +20 -0
  151. package/util.js +1145 -0
  152. 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
+ }