nitro-web 0.0.87 → 0.0.88

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 (33) hide show
  1. package/components/auth/auth.api.js +411 -0
  2. package/components/auth/reset.tsx +86 -0
  3. package/components/auth/signin.tsx +76 -0
  4. package/components/auth/signup.tsx +62 -0
  5. package/components/billing/stripe.api.js +268 -0
  6. package/components/dashboard/dashboard.tsx +32 -0
  7. package/components/partials/element/accordion.tsx +102 -0
  8. package/components/partials/element/avatar.tsx +40 -0
  9. package/components/partials/element/button.tsx +98 -0
  10. package/components/partials/element/calendar.tsx +125 -0
  11. package/components/partials/element/dropdown.tsx +248 -0
  12. package/components/partials/element/filters.tsx +194 -0
  13. package/components/partials/element/github-link.tsx +16 -0
  14. package/components/partials/element/initials.tsx +66 -0
  15. package/components/partials/element/message.tsx +141 -0
  16. package/components/partials/element/modal.tsx +90 -0
  17. package/components/partials/element/sidebar.tsx +195 -0
  18. package/components/partials/element/tooltip.tsx +154 -0
  19. package/components/partials/element/topbar.tsx +15 -0
  20. package/components/partials/form/checkbox.tsx +150 -0
  21. package/components/partials/form/drop-handler.tsx +68 -0
  22. package/components/partials/form/drop.tsx +141 -0
  23. package/components/partials/form/field-color.tsx +86 -0
  24. package/components/partials/form/field-currency.tsx +158 -0
  25. package/components/partials/form/field-date.tsx +252 -0
  26. package/components/partials/form/field.tsx +231 -0
  27. package/components/partials/form/form-error.tsx +27 -0
  28. package/components/partials/form/location.tsx +225 -0
  29. package/components/partials/form/select.tsx +360 -0
  30. package/components/partials/is-first-render.ts +14 -0
  31. package/components/partials/not-found.tsx +7 -0
  32. package/components/partials/styleguide.tsx +407 -0
  33. package/package.json +2 -1
@@ -0,0 +1,360 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { css } from 'twin.macro'
3
+ import { memo } from 'react'
4
+ import ReactSelect, {
5
+ components, ControlProps, createFilter, OptionProps, SingleValueProps, ClearIndicatorProps,
6
+ DropdownIndicatorProps, MultiValueRemoveProps,
7
+ } from 'react-select'
8
+ import { CheckCircleIcon } from '@heroicons/react/20/solid'
9
+ import { ChevronsUpDownIcon, XIcon } from 'lucide-react'
10
+ import { isFieldCached } from 'nitro-web'
11
+ import { getErrorFromState, deepFind, twMerge } from 'nitro-web/util'
12
+ import { Errors } from 'nitro-web/types'
13
+
14
+ const filterFn = createFilter()
15
+
16
+ type GetSelectStyle = {
17
+ name: string
18
+ isFocused?: boolean
19
+ isSelected?: boolean
20
+ hasError?: boolean
21
+ usePrefixes?: boolean
22
+ }
23
+
24
+ /** Select (all other props are passed to react-select) **/
25
+ export type SelectProps = {
26
+ /** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
27
+ name: string
28
+ /** inputId, the name is used if not provided **/
29
+ id?: string
30
+ /** 'container' id to pass to react-select **/
31
+ containerId?: string
32
+ /** The minimum width of the dropdown menu **/
33
+ minMenuWidth?: number
34
+ /** The prefix to add to the input **/
35
+ prefix?: string
36
+ /** The onChange handler **/
37
+ onChange?: (event: { target: { name: string, value: unknown } }) => void
38
+ /** The options to display in the dropdown **/
39
+ options: { value: unknown, label: string | React.ReactNode, fixed?: boolean, [key: string]: unknown }[]
40
+ /** The state object to get the value and check errors from **/
41
+ state?: { errors?: Errors, [key: string]: any } // was unknown|unknown[]
42
+ /** Select variations **/
43
+ mode?: 'country'|'customer'|''
44
+ /** Pass dependencies to break memoization, handy for onChange/onInputChange **/
45
+ deps?: unknown[]
46
+ /** title used to find related error messages */
47
+ errorTitle?: string|RegExp
48
+ /** All other props are passed to react-select **/
49
+ [key: string]: unknown
50
+ }
51
+
52
+ export const Select = memo(SelectBase, (prev, next) => {
53
+ return isFieldCached(prev, next)
54
+ })
55
+
56
+ function SelectBase({
57
+ id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode='', errorTitle, ...props
58
+ }: SelectProps) {
59
+ let value: unknown|unknown[]
60
+ const error = getErrorFromState(state, errorTitle || name)
61
+ if (!name) throw new Error('Select component requires a `name` and `options` prop')
62
+
63
+ // Get value from value or state
64
+ if (typeof props.value !== 'undefined') value = props.value
65
+ else if (typeof state == 'object') value = deepFind(state, name)
66
+
67
+ // If multi-select, filter options by value
68
+ if (Array.isArray(value)) value = options.filter(o => (value as unknown[]).includes(o.value))
69
+ else value = options.find(o => value === o.value)
70
+
71
+ // Input is always controlled if state is passed in
72
+ if (typeof state == 'object' && typeof value == 'undefined') value = ''
73
+
74
+ return (
75
+ <div css={style} class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-select ${props.className||''}`)}>
76
+ <ReactSelect
77
+ /**
78
+ * react-select prop quick reference (https://react-select.com/props#api):
79
+ * isDisabled={false}
80
+ * isMulti={false}
81
+ * isSearchable={true}
82
+ * options={[{ value: 'chocolate', label: 'Chocolate' }]}
83
+ * placeholder="Select a color"
84
+ * value={options.find(o => o.code == state.color)} // to clear you need to set to null, not undefined
85
+ * isClearable={false}
86
+ * menuIsOpen={false}
87
+ */
88
+ {...props}
89
+ // @ts-expect-error
90
+ _nitro={{ prefix, mode }}
91
+ key={value as string}
92
+ unstyled={true}
93
+ inputId={id || name}
94
+ id={containerId}
95
+ filterOption={(option, searchText) => {
96
+ if ((option.data as {fixed?: boolean}).fixed) return true
97
+ return filterFn(option, searchText)
98
+ }}
99
+ menuPlacement="auto"
100
+ minMenuHeight={250}
101
+ onChange={!onChange ? undefined : (o) => {
102
+ // Array returned for multi-select
103
+ const value = Array.isArray(o)
104
+ ? o.map(v => typeof v == 'object' && v !== null && 'value' in v ? v.value : v)
105
+ : (typeof o == 'object' && o !== null && 'value' in o ? o.value : o)
106
+ return onChange({ target: { name: name, value: value }})
107
+ }}
108
+ options={options}
109
+ value={value}
110
+ classNames={{
111
+ // Input container
112
+ control: (p) => getSelectStyle({ name: 'control', hasError: !!error, ...p }),
113
+ valueContainer: () => getSelectStyle({ name: 'valueContainer' }),
114
+ // Input container objects
115
+ input: () => getSelectStyle({ name: 'input', hasError: !!error }),
116
+ multiValue: () => getSelectStyle({ name: 'multiValue' }),
117
+ multiValueLabel: () => '',
118
+ multiValueRemove: () => getSelectStyle({ name: 'multiValueRemove' }),
119
+ placeholder: () => getSelectStyle({ name: 'placeholder' }),
120
+ singleValue: () => getSelectStyle({ name: 'singleValue', hasError: !!error }),
121
+ // Indicators
122
+ clearIndicator: () => getSelectStyle({ name: 'clearIndicator' }),
123
+ dropdownIndicator: () => getSelectStyle({ name: 'dropdownIndicator' }),
124
+ indicatorsContainer: () => getSelectStyle({ name: 'indicatorsContainer' }),
125
+ indicatorSeparator: () => getSelectStyle({ name: 'indicatorSeparator' }),
126
+ // Dropmenu
127
+ menu: () => getSelectStyle({ name: 'menu' }),
128
+ groupHeading: () => getSelectStyle({ name: 'groupHeading' }),
129
+ noOptionsMessage: () => getSelectStyle({ name: 'noOptionsMessage' }),
130
+ option: (p) => getSelectStyle({ name: 'option', ...p }),
131
+ }}
132
+ components={{
133
+ Control,
134
+ SingleValue,
135
+ Option,
136
+ DropdownIndicator,
137
+ ClearIndicator,
138
+ MultiValueRemove,
139
+ }}
140
+ styles={{
141
+ menu: (base) => ({
142
+ ...base, minWidth: minMenuWidth,
143
+ }),
144
+ // On mobile, the label will truncate automatically, so we want to
145
+ // override that behaviour.
146
+ multiValueLabel: (base) => ({
147
+ ...base,
148
+ whiteSpace: 'normal',
149
+ overflow: 'visible',
150
+ }),
151
+ control: (base) => ({
152
+ ...base,
153
+ outline: undefined,
154
+ transition: 'none',
155
+ }),
156
+ }}
157
+ // menuIsOpen={true}
158
+ // isSearchable={false}
159
+ // isClearable={true}
160
+ // isMulti={true}
161
+ // isDisabled={true}
162
+ // maxMenuHeight={200}
163
+ />
164
+ {error && <div class="mt-1.5 text-xs text-danger-foreground">{error.detail}</div>}
165
+ </div>
166
+ )
167
+ }
168
+
169
+ function Control({ children, ...props }: ControlProps) {
170
+ // Add flag and prefix to the input (control)
171
+ // todo: check that the flag/prefix looks okay
172
+ const selectedOption = props.getValue()[0]
173
+ const optionFlag = (selectedOption as { flag?: string })?.flag
174
+ const _nitro = (props.selectProps as { _nitro?: { prefix?: string, mode?: string } })?._nitro
175
+ return (
176
+ <components.Control {...props}>
177
+ {
178
+ (() => {
179
+ if (_nitro?.prefix) {
180
+ return (
181
+ <>
182
+ <span class="relative right-[2px]">{_nitro?.prefix}</span>
183
+ {children}
184
+ </>
185
+ )
186
+ } else if (_nitro?.mode == 'country') {
187
+ return (
188
+ <>
189
+ { optionFlag && <Flag flag={optionFlag} /> }
190
+ {children}
191
+ </>
192
+ )
193
+ } else {
194
+ return children
195
+ }
196
+ })()
197
+ }
198
+ </components.Control>
199
+ )
200
+ }
201
+
202
+ function SingleValue(props: SingleValueProps) {
203
+ const selectedOption = props.getValue()[0] as { labelControl?: string }
204
+ return (
205
+ <components.SingleValue {...props}>
206
+ <>{selectedOption?.labelControl || props.children}</>
207
+ </components.SingleValue>
208
+ )
209
+ }
210
+
211
+ function Option(props: OptionProps) {
212
+ // todo: check that the flag looks okay
213
+ const data = props.data as { className?: string, flag?: string }
214
+ const _nitro = (props.selectProps as { _nitro?: { mode?: string } })?._nitro
215
+ return (
216
+ <components.Option className={data.className} {...props}>
217
+ { _nitro?.mode == 'country' && <Flag flag={data.flag} /> }
218
+ <span class="flex-auto">{props.label}</span>
219
+ {props.isSelected && <CheckCircleIcon className="size-[22px] text-primary -my-1 -mx-0.5" />}
220
+ </components.Option>
221
+ )
222
+ }
223
+
224
+ const DropdownIndicator = (props: DropdownIndicatorProps) => {
225
+ return (
226
+ <components.DropdownIndicator {...props}>
227
+ <ChevronsUpDownIcon size={15} className="text-gray-400 -my-0.5 -mx-[1px]" />
228
+ </components.DropdownIndicator>
229
+ )
230
+ }
231
+
232
+ const ClearIndicator = (props: ClearIndicatorProps) => {
233
+ return (
234
+ <components.ClearIndicator {...props}>
235
+ <XIcon size={14} />
236
+ </components.ClearIndicator>
237
+ )
238
+ }
239
+
240
+ const MultiValueRemove = (props: MultiValueRemoveProps) => {
241
+ return (
242
+ <components.MultiValueRemove {...props}>
243
+ <XIcon className="size-[1em] p-[1px]" />
244
+ </components.MultiValueRemove>
245
+ )
246
+ }
247
+
248
+ function Flag({ flag }: { flag?: string }) {
249
+ if (!flag) return null
250
+ // todo: public needs to come from webpack
251
+ const publicPath = '/'
252
+ return (
253
+ <span class="flag" style={{ backgroundImage: `url(${publicPath}assets/imgs/flags/${flag}.svg)` }} />
254
+ )
255
+ }
256
+
257
+ const selectStyles = {
258
+ // Based off https://www.jussivirtanen.fi/writing/styling-react-select-with-tailwind
259
+ // Input container
260
+ control: {
261
+ base: 'rounded-md bg-white hover:cursor-pointer text-input-base outline outline-1 -outline-offset-1 '
262
+ + '!min-h-0 outline-input-border',
263
+ focus: 'outline-2 -outline-offset-2 outline-input-border-focus',
264
+ error: 'outline-danger',
265
+ },
266
+ valueContainer: 'py-[9px] px-[12px] py-input-y px-input-x gap-1', // dont twMerge (input-x is optional)
267
+ // Input container objects
268
+ input: {
269
+ base: 'text-input',
270
+ error: 'text-danger-foreground',
271
+ },
272
+ multiValue: 'bg-primary text-white rounded items-center pl-2 pr-1.5 gap-1.5',
273
+ multiValueLabel: 'text-xs',
274
+ multiValueRemove: 'border border-black/10 bg-clip-content bg-white rounded-md text-foreground hover:bg-red-50',
275
+ placeholder: 'text-input-placeholder',
276
+ singleValue: {
277
+ base: 'text-input',
278
+ error: 'text-danger-foreground',
279
+ },
280
+ // Icon indicators
281
+ clearIndicator: 'text-gray-500 p-1 rounded-md hover:bg-red-50 hover:text-danger-foreground',
282
+ dropdownIndicator: 'p-1 hover:bg-gray-100 text-gray-500 rounded-md hover:text-black',
283
+ indicatorsContainer: 'p-1 px-2 gap-1',
284
+ indicatorSeparator: 'py-0.5 before:content-[""] before:block before:bg-gray-100 before:w-px before:h-full',
285
+ // Dropdown menu
286
+ menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-input-base overflow-hidden shadow-dropdown-ul',
287
+ groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-input-base',
288
+ noOptionsMessage: 'm-1 text-gray-500 p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm',
289
+ option: {
290
+ base: 'relative px-3 py-2 !flex items-center gap-2 cursor-default',
291
+ hover: 'bg-gray-50',
292
+ selected: '!bg-gray-100 text-dropdown-selected-foreground',
293
+ },
294
+ }
295
+
296
+ export function getSelectStyle({ name, isFocused, isSelected, hasError, usePrefixes }: GetSelectStyle) {
297
+ // Returns a class list that conditionally includes hover/focus modifier classes, or uses CSS modifiers, e.g. hover:, focus:
298
+ // @ts-expect-error
299
+ const obj = selectStyles[name]
300
+ let output = obj?.base
301
+ if (typeof obj == 'string') return obj // no modifiers
302
+
303
+ if (usePrefixes) {
304
+ if (obj.focus) output += ' ' + obj.focus.split(' ').map((part: string) => `focus:${part}`).join(' ')
305
+ if (obj.hover) output += ' ' + obj.hover.split(' ').map((part: string) => `hover:${part}`).join(' ')
306
+ } else {
307
+ if (obj.focus && isFocused) output += ` ${obj.focus}`
308
+ if (obj.hover && isFocused) output += ` ${obj.hover}`
309
+ }
310
+ if (obj.error && hasError) output += ` ${obj.error}`
311
+ if (obj.selected && isSelected) output += ` ${obj.selected}`
312
+
313
+ return twMerge(output)
314
+ }
315
+
316
+ const style = css`
317
+ /*
318
+ todo: add these as tailwind classes
319
+
320
+ &.rs-medium {
321
+ .rs__control {
322
+ padding: 9px 13px;
323
+ font-size: 13px;
324
+ font-weight: 400;
325
+ min-height: 0;
326
+ }
327
+ .rs__menu {
328
+ .rs__option {
329
+ font-size: 0.85rem;
330
+ }
331
+ }
332
+ }
333
+ &.rs-small {
334
+ .rs__control {
335
+ padding: 5px 13px;
336
+ font-size: 12.5px;
337
+ font-weight: 400;
338
+ min-height: 0;
339
+ }
340
+ .rs__menu {
341
+ .rs__option {
342
+ font-size: 0.8rem;
343
+ }
344
+ }
345
+ } */
346
+
347
+ /*
348
+ .flag {
349
+ // https://github.com/lipis/flag-icons
350
+ flex-shrink: 0;
351
+ margin-right: 10px;
352
+ width: 21px;
353
+ height: 14px;
354
+ background-size: cover;
355
+ background-repeat: no-repeat;
356
+ background-position: center;
357
+ border-radius: 3px;
358
+ overflow: hidden;
359
+ }*/
360
+ `
@@ -0,0 +1,14 @@
1
+ export function IsFirstRender(delay?: number) {
2
+ /*
3
+ * Checks if the current render of a react component is the first
4
+ * E.g. const isFirst = isFirstRender()
5
+ * @link https://stackoverflow.com/a/56267719/1900648
6
+ * @return boolean
7
+ */
8
+ const isMountRef = useRef(true)
9
+ useEffect(() => {
10
+ if (delay) setTimeout(() => isMountRef.current = false, delay)
11
+ else isMountRef.current = false
12
+ }, [])
13
+ return isMountRef.current
14
+ }
@@ -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
+ }