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,369 @@
1
+ /*eslint-disable*/
2
+ import { css } from 'twin.macro'
3
+ import { twMerge } from 'tailwind-merge'
4
+ import ReactSelect, { components, createFilter } from 'react-select'
5
+ import { ChevronDownIcon, CheckCircleIcon, XMarkIcon } from '@heroicons/react/20/solid'
6
+ import * as util from '../../../util.js'
7
+ const filterFn = createFilter()
8
+
9
+ const selectStyles = {
10
+ // Based off https://www.jussivirtanen.fi/writing/styling-react-select-with-tailwind
11
+ // Input container
12
+ control: {
13
+ base: `rounded-md bg-white hover:cursor-pointer text-sm sm:text-sm/6 outline outline-1 -outline-offset-1 outline-input-border`,
14
+ focus: `outline-2 -outline-offset-2 outline-primary`,
15
+ error: `outline-danger`,
16
+ },
17
+ valueContainer: 'py-2 px-3 gap-1',
18
+ // Input container objects
19
+ input: {
20
+ base: 'text-input',
21
+ error: 'text-red-900',
22
+ },
23
+ multiValue: 'bg-primary text-white rounded items-center pl-2 pr-1.5 gap-1.5',
24
+ multiValueLabel: '',
25
+ multiValueRemove: `border border-primary-dark bg-white rounded-md text-dark hover:bg-red-50`,
26
+ placeholder: 'text-input-placeholder',
27
+ singleValue: {
28
+ base: 'text-input',
29
+ error: 'text-red-900',
30
+ },
31
+ // Icon indicators
32
+ clearIndicator: 'text-gray-500 p-1 rounded-md hover:bg-red-50 hover:text-red-800',
33
+ dropdownIndicator: 'p-1 hover:bg-gray-100 text-gray-500 rounded-md hover:text-black',
34
+ indicatorsContainer: 'p-1 px-2 gap-1',
35
+ indicatorSeparator: 'py-0.5 before:content-[""] before:block before:bg-gray-100 before:w-px before:h-full',
36
+ // Dropdown menu
37
+ menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-sm overflow-hidden shadow-dropdown-ul',
38
+ groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-sm',
39
+ noOptionsMessage: 'm-1 text-gray-500 p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm',
40
+ option: {
41
+ base: 'relative px-3 py-2 !flex items-center gap-2 cursor-default',
42
+ hover: 'bg-gray-50',
43
+ selected: '!bg-gray-100 text-primary-dark',
44
+ }
45
+ }
46
+
47
+ export function getSelectStyle({ name, isFocused, isSelected, hasError, usePrefixes }) {
48
+ // Returns a class list that conditionally includes hover/focus modifier classes, or uses CSS modifiers, e.g. hover:, focus:
49
+ const obj = selectStyles[name]
50
+ let output = obj?.base
51
+ if (typeof obj == 'string') return obj // no modifiers
52
+
53
+ if (usePrefixes) {
54
+ if (obj.focus) output += ' ' + obj.focus.split(' ').map(part => `focus:${part}`).join(' ')
55
+ if (obj.hover) output += ' ' + obj.hover.split(' ').map(part => `hover:${part}`).join(' ')
56
+ } else {
57
+ if (obj.focus && isFocused) output += ` ${obj.focus}`
58
+ if (obj.hover && isFocused) output += ` ${obj.hover}`
59
+ }
60
+ if (obj.error && hasError) output += ` ${obj.error}`
61
+ if (obj.selected && isSelected) output += ` ${obj.selected}`
62
+
63
+ return twMerge(output)
64
+ }
65
+
66
+ /**
67
+ * @param {string} name - field name or path on state (used to match errors), e.g. 'date', 'company.email'
68
+ * @param {string} [minMenuWidth] - width of the dropdown menu
69
+ * @param {string} [inputId] - name used if not provided
70
+ * @param {function} [onChange] - e.g. (event) => onInputChange(event)
71
+ * @param {object} [state] - object to get value from, and check errors against
72
+ * @param {string} [type] - speical types: 'country', 'customer', 'customer-big'
73
+ *
74
+ * react-select prop quick reference (https://react-select.com/props#api):
75
+ * isDisabled={false}
76
+ * isMulti={false}
77
+ * isSearchable={true}
78
+ * options={[{ value: 'chocolate', label: 'Chocolate' }]}
79
+ * placeholder="Select a color"
80
+ * value={options.find(o => o.code == state.color)} // to clear you need to set to null, not undefined
81
+ * isClearable={false}
82
+ * menuIsOpen={false}
83
+ */
84
+ export function Select({ inputId, minMenuWidth, name, prefix='', onChange, options, state, type, ...props }) {
85
+ if (!name) throw new Error('Select component requires a `name` and `options` prop')
86
+
87
+ // Input is always controlled if state is passed in
88
+ if (props.value) {
89
+ var value = props.value
90
+ } else if (typeof state == 'object') {
91
+ value = options.find(o => o.value == util.deepFind(state, name))
92
+ if (typeof value == 'undefined') value = ''
93
+ }
94
+
95
+ // An error matches this input path
96
+ for (let item of (state?.errors || [])) {
97
+ if (util.isRegex(name) && (item.title||'').match(name)) var hasError = item
98
+ else if (item.title == name) hasError = item
99
+ }
100
+
101
+
102
+ // classNames={{
103
+ // // *Same classes as input.jsx*
104
+ // // Based off https://www.jussivirtanen.fi/writing/styling-react-select-with-tailwind
105
+ // //
106
+ // // Input container
107
+ // control: ({ isFocused }) => `rounded-md bg-white hover:cursor-pointer text-sm sm:text-sm/6
108
+ // ${isFocused
109
+ // ? `outline outline-2 -outline-offset-2 ${error ? 'outline-danger' : 'outline-primary'}`
110
+ // : `outline outline-1 -outline-offset-1 ${error ? 'outline-danger' : 'outline-input-border'}`}`,
111
+ // valueContainer: () => 'py-2 px-3 gap-1',
112
+ // // Input container objects
113
+ // input: () => `${error ? 'text-red-900' : 'text-input'}`,
114
+ // multiValue: () => 'bg-primary text-white rounded items-center pl-2 pr-1.5 gap-1.5',
115
+ // multiValueLabel: () => '',
116
+ // multiValueRemove: () => `border border-primary-dark bg-white rounded-md text-dark hover:bg-red-50`,
117
+ // placeholder: () => 'text-input-placeholder',
118
+ // singleValue: () => `${error ? 'text-red-900' : 'text-input'}`,
119
+ // // Indicators
120
+ // clearIndicator: () =>'text-gray-500 p-1 rounded-md hover:bg-red-50 hover:text-red-800',
121
+ // dropdownIndicator: () => 'p-1 hover:bg-gray-100 text-gray-500 rounded-md hover:text-black',
122
+ // indicatorsContainer: () => 'p-1 px-2 gap-1',
123
+ // indicatorSeparator: () => 'py-0.5 before:content-[""] before:block before:bg-gray-100 before:w-px before:h-full',
124
+ // // Dropmenu
125
+ // menu: () => 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-sm overflow-hidden shadow-dropdown-ul',
126
+ // groupHeading: () => 'ml-3 mt-2 mb-1 text-gray-500 text-sm',
127
+ // noOptionsMessage: () => 'm-1 text-gray-500 p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm',
128
+ // option: ({ isFocused, isSelected }) => `hover:cursor-pointer px-3 py-2 !flex items-center gap-2
129
+ // ${isFocused ? 'bg-gray-50 active:bg-gray-200' : ''}
130
+ // ${isSelected ? 'bg-gray-100 text-primary-dark' : ''}`,
131
+ // }}
132
+
133
+
134
+ return (
135
+ <div css={style} class="mt-input-before mb-input-after">
136
+ <ReactSelect
137
+ {...props}
138
+ _prefix={prefix}
139
+ _type={type||''}
140
+ _error={hasError}
141
+ key={value}
142
+ unstyled={true}
143
+ inputId={inputId || name}
144
+ filterOption={(option, searchText) => {
145
+ if (option.data?.fixed) return true
146
+ return filterFn(option, searchText)
147
+ }}
148
+ menuPlacement="auto"
149
+ minMenuHeight={250}
150
+ onChange={!onChange ? undefined : (o) => onChange({ target: { id: inputId || name, value: o?.value || o }})}
151
+ options={options}
152
+ value={value}
153
+ classNames={{
154
+ // Input container
155
+ control: (p) => getSelectStyle({ name: 'control', hasError, ...p }),
156
+ valueContainer: () => getSelectStyle({ name: 'valueContainer' }),
157
+ // Input container objects
158
+ input: () => getSelectStyle({ name: 'input', hasError }),
159
+ multiValue: () => getSelectStyle({ name: 'multiValue' }),
160
+ multiValueLabel: () => '',
161
+ multiValueRemove: () => getSelectStyle({ name: 'multiValueRemove' }),
162
+ placeholder: () => getSelectStyle({ name: 'placeholder' }),
163
+ singleValue: () => getSelectStyle({ name: 'singleValue', hasError }),
164
+ // Indicators
165
+ clearIndicator: () => getSelectStyle({ name: 'clearIndicator' }),
166
+ dropdownIndicator: () => getSelectStyle({ name: 'dropdownIndicator' }),
167
+ indicatorsContainer: () => getSelectStyle({ name: 'indicatorsContainer' }),
168
+ indicatorSeparator: () => getSelectStyle({ name: 'indicatorSeparator' }),
169
+ // Dropmenu
170
+ menu: () => getSelectStyle({ name: 'menu' }),
171
+ groupHeading: () => getSelectStyle({ name: 'groupHeading' }),
172
+ noOptionsMessage: () => getSelectStyle({ name: 'noOptionsMessage' }),
173
+ option: (p) => getSelectStyle({ name: 'option', ...p }),
174
+ }}
175
+ components={{
176
+ Control,
177
+ SingleValue,
178
+ Option,
179
+ DropdownIndicator, ClearIndicator, MultiValueRemove,
180
+ }}
181
+ styles={{
182
+ menu: (base) => ({
183
+ ...base, minWidth: minMenuWidth
184
+ }),
185
+ // On mobile, the label will truncate automatically, so we want to
186
+ // override that behaviour.
187
+ multiValueLabel: (base) => ({
188
+ ...base,
189
+ whiteSpace: "normal",
190
+ overflow: "visible",
191
+ }),
192
+ control: (base) => ({
193
+ ...base,
194
+ outline: undefined,
195
+ transition: "none",
196
+ }),
197
+ }}
198
+ // menuIsOpen={true}
199
+ // isSearchable={false}
200
+ // isClearable={true}
201
+ // isMulti={true}
202
+ // isDisabled={true}
203
+ // maxMenuHeight={200}
204
+ />
205
+ {hasError && <div class="mt-1.5 text-xs text-danger">{hasError.detail}</div>}
206
+ </div>
207
+ )
208
+ }
209
+
210
+ function Control({ children, ...props }) {
211
+ // Add flag and prefix to the input (control)
212
+ // todo: check that the flag/prefix looks okay
213
+ const selectedOption = props.getValue()[0] || {}
214
+ return (
215
+ <components.Control {...props}>
216
+ {
217
+ (() => {
218
+ if (props.selectProps._prefix) {
219
+ return (
220
+ <>
221
+ <span class="relative right-[2px]">{props.selectProps._prefix}</span>
222
+ {children}
223
+ </>
224
+ )
225
+ } else if (props.selectProps._type == 'country') {
226
+ return (
227
+ <>
228
+ <Flag flag={selectedOption.flag} />
229
+ {children}
230
+ </>
231
+ )
232
+ } else {
233
+ return children
234
+ }
235
+ })()
236
+ }
237
+ </components.Control>
238
+ )
239
+ }
240
+
241
+ function SingleValue(props) {
242
+ const selectedOption = props.getValue()[0] || {}
243
+ return (
244
+ <components.SingleValue {...props}>
245
+ <>{selectedOption.labelControl || props.children}</>
246
+ </components.SingleValue>
247
+ )
248
+ }
249
+
250
+ function Option(props) {
251
+ // todo: check that the flag looks okay
252
+ return (
253
+ <components.Option className={props.data.className} {...props}>
254
+ { props.selectProps._type == 'country' && <Flag flag={props.data.flag} /> }
255
+ <span class="flex-auto">{props.label}</span>
256
+ {props.isSelected && <CheckCircleIcon className="size-[22px] text-primary -my-1 -mx-1" />}
257
+ </components.Option>
258
+ )
259
+ }
260
+
261
+ const DropdownIndicator = (props) => {
262
+ return (
263
+ <components.DropdownIndicator {...props}>
264
+ {/* <img src="/assets/imgs/icons/v.svg" /> */}
265
+ <ChevronDownIcon className="size-6 -my-0.5 -mx-1" />
266
+ {/* <ChevronDownIcon /> */}
267
+ </components.DropdownIndicator>
268
+ )
269
+ }
270
+
271
+ const ClearIndicator = (props) => {
272
+ return (
273
+ <components.ClearIndicator {...props}>
274
+ <XMarkIcon className="size-4 my-0.5" />
275
+ </components.ClearIndicator>
276
+ )
277
+ }
278
+
279
+ const MultiValueRemove = (props) => {
280
+ return (
281
+ <components.MultiValueRemove {...props}>
282
+ <XMarkIcon className="size-4 p-[1px]" />
283
+ </components.MultiValueRemove>
284
+ )
285
+ }
286
+
287
+
288
+ function Flag({ flag }) {
289
+ if (!flag) return null
290
+ return (
291
+ <span class="flag" style={{ backgroundImage: `url(/assets/imgs/flags/${flag}.svg)` }} />
292
+ )
293
+ }
294
+
295
+ const style = () => css`
296
+ /*
297
+ todo: add these as tailwind classes
298
+
299
+ &.rs-medium {
300
+ .rs__control {
301
+ padding: 9px 13px;
302
+ font-size: 13px;
303
+ font-weight: 400;
304
+ min-height: 0;
305
+ }
306
+ .rs__menu {
307
+ .rs__option {
308
+ font-size: 0.85rem;
309
+ }
310
+ }
311
+ }
312
+ &.rs-small {
313
+ .rs__control {
314
+ padding: 5px 13px;
315
+ font-size: 12.5px;
316
+ font-weight: 400;
317
+ min-height: 0;
318
+ }
319
+ .rs__menu {
320
+ .rs__option {
321
+ font-size: 0.8rem;
322
+ }
323
+ }
324
+ } */
325
+
326
+ /*
327
+ todo: not yet accounted for (maybe remove)
328
+
329
+ .bb:not(:last-child) {
330
+ // option border
331
+ position: relative;
332
+ margin-bottom: 6px;
333
+ &:after {
334
+ content: "";
335
+ position: absolute;
336
+ bottom: -4px;
337
+ left: 0;
338
+ height: 2px;
339
+ width: 100%;
340
+ background-color: $border-color;
341
+ }
342
+ }
343
+ .bt:not(:first-child) {
344
+ // option border
345
+ position: relative;
346
+ margin-top: 6px;
347
+ &:after {
348
+ content: "";
349
+ position: absolute;
350
+ top: -4px;
351
+ left: 0;
352
+ height: 2px;
353
+ width: 100%;
354
+ background-color: $border-color;
355
+ }
356
+ }
357
+ .flag {
358
+ // https://github.com/lipis/flag-icons
359
+ flex-shrink: 0;
360
+ margin-right: 10px;
361
+ width: 21px;
362
+ height: 14px;
363
+ background-size: cover;
364
+ background-repeat: no-repeat;
365
+ background-position: center;
366
+ border-radius: 3px;
367
+ overflow: hidden;
368
+ }*/
369
+ `
@@ -0,0 +1,46 @@
1
+ export function Toggle({ name, id, subtext, text, type = 'checkbox', ...props }) {
2
+ if (!name) throw new Error('Toggle requires a `name` prop')
3
+ id = id || name
4
+ // https://tailwindui.com/components/application-ui/forms/checkboxes#component-744ed4fa65ba36b925701eb4da5c6e31
5
+ return (
6
+ <div className={`mt-input-before mb-input-after flex gap-3 ${props.className || ''}`}>
7
+ <div className="flex h-6 shrink-0 items-center">
8
+ <div className="group grid size-4 grid-cols-1">
9
+ <input
10
+ {...props}
11
+ id={id}
12
+ name={name}
13
+ type={type}
14
+ className="col-start-1 row-start-1 appearance-none rounded border border-gray-300 bg-white checked:border-indigo-600 checked:bg-indigo-600 indeterminate:border-indigo-600 indeterminate:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 forced-colors:appearance-auto"
15
+ />
16
+ <svg
17
+ fill="none"
18
+ viewBox="0 0 14 14"
19
+ className="pointer-events-none col-start-1 row-start-1 size-3.5 self-center justify-self-center stroke-white group-has-[:disabled]:stroke-gray-950/25"
20
+ >
21
+ <path
22
+ d="M3 8L6 11L11 3.5"
23
+ strokeWidth={2}
24
+ strokeLinecap="round"
25
+ strokeLinejoin="round"
26
+ className="opacity-0 group-has-[:checked]:opacity-100"
27
+ />
28
+ <path
29
+ d="M3 7H11"
30
+ strokeWidth={2}
31
+ strokeLinecap="round"
32
+ strokeLinejoin="round"
33
+ className="opacity-0 group-has-[:indeterminate]:opacity-100"
34
+ />
35
+ </svg>
36
+ </div>
37
+ </div>
38
+ {text && <div className="text-sm/6">
39
+ <label for={id}>
40
+ <span className="font-medium text-gray-900">{text}</span>
41
+ <span className="ml-2 text-gray-500">{subtext}</span>
42
+ </label>
43
+ </div>}
44
+ </div>
45
+ )
46
+ }
@@ -0,0 +1,15 @@
1
+ export function IsFirstRender(delay) {
2
+ /*
3
+ * Checks if the current render of a react component is the first
4
+ * E.g. const isFirst = isFirstRender()
5
+ * @param {boolean} delay
6
+ * @link https://stackoverflow.com/a/56267719/1900648
7
+ * @return boolean
8
+ */
9
+ const isMountRef = useRef(true)
10
+ useEffect(() => {
11
+ if (delay) setTimeout(() => isMountRef.current = false, delay)
12
+ else isMountRef.current = false
13
+ }, [])
14
+ return isMountRef.current
15
+ }
@@ -0,0 +1,32 @@
1
+ import { css } from 'twin.macro'
2
+ import { Outlet } from 'react-router-dom'
3
+ import { Message } from '../element/message.jsx'
4
+ import { Sidebar } from '../element/sidebar.jsx'
5
+
6
+ // Dashboard, app screens (only the <Outlet/> receives `params` and `location`)
7
+ export function Layout1({ Logo }) {
8
+ const [store] = sharedStore.useTracked()
9
+
10
+ return (
11
+ <div css={style} class="bg-[#F3F3F3]">
12
+ <Message />
13
+ <div class="flex-1">
14
+ <div class="wrapper lg:flex min-h-[100%] w-[100%] bg-[#FDFDFD] shadow-[0_0_40px_0_rgb(237_237_237)]">
15
+ <Sidebar Logo={Logo} />
16
+ <div class="py-10 px-14 flex-1" key={store?.company?._id}>
17
+ <Outlet />
18
+ </div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ )
23
+ }
24
+
25
+ const style = () => css`
26
+ .wrapper {
27
+ position: relative;
28
+ max-width: 1800px;
29
+ margin: 0 auto;
30
+ }
31
+ `
32
+
@@ -0,0 +1,47 @@
1
+ import { css } from 'twin.macro'
2
+ import { Outlet } from 'react-router-dom'
3
+ import { Message } from '../element/message.jsx'
4
+
5
+ // Signin, reset password, etc
6
+ export function Layout2({ Logo }) {
7
+ return (
8
+ <div css={style} class="bg-[#F3F3F3]">
9
+ <Message />
10
+ <div class="wrapper bg-[#FDFDFD] shadow-[0_0_40px_0_rgb(237_237_237)] flex flex-col min-h-full w-full">
11
+ <div class="flex-1 w-full wrapper-2 px-5 py-10">
12
+ <div class="border-b mb-6">
13
+ <Link to="/signin" class="logo relative block -ml-1 -mt-1 p-1">
14
+ <Logo alt="Nitro" width="60" />
15
+ </Link>
16
+ </div>
17
+ <Outlet />
18
+ </div>
19
+
20
+ <div class="wrapper-2 w-full px-5 pb-4 flex items-center w-full text-sm text-[#1F1F1F]">
21
+ <ul class="flex-1 flex gap-4 list-style-none">
22
+ <li><Link class="underline1" to="/">Home</Link></li>
23
+ <li><Link class="underline1" to="/signin">About</Link></li>
24
+ <li><Link class="underline1" to="/signin">Support</Link></li>
25
+ </ul>
26
+ <div>
27
+ 2025 © Nitro
28
+ </div>
29
+ </div>
30
+
31
+ </div>
32
+ </div>
33
+ )
34
+ }
35
+
36
+ const style = () => css`
37
+ .wrapper {
38
+ position: relative;
39
+ max-width: 1800px;
40
+ margin: 0 auto;
41
+ }
42
+ .wrapper-2 {
43
+ max-width: 700px;
44
+ margin: 0 auto;
45
+ box-sizing: border-box;
46
+ }
47
+ `
@@ -0,0 +1,7 @@
1
+ export function NotFound() {
2
+ return (
3
+ <div class="pt-14 text-center" style={{'minHeight': '300px'}}>
4
+ Sorry, nothing found.
5
+ </div>
6
+ )
7
+ }