nitro-web 0.0.86 → 0.0.87
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/client/globals.ts +10 -6
- package/package.json +14 -6
- package/types/{required-globals.d.ts → globals.d.ts} +3 -1
- package/.editorconfig +0 -9
- package/components/auth/auth.api.js +0 -411
- package/components/auth/reset.tsx +0 -86
- package/components/auth/signin.tsx +0 -76
- package/components/auth/signup.tsx +0 -62
- package/components/billing/stripe.api.js +0 -268
- package/components/dashboard/dashboard.tsx +0 -32
- package/components/partials/element/accordion.tsx +0 -102
- package/components/partials/element/avatar.tsx +0 -40
- package/components/partials/element/button.tsx +0 -98
- package/components/partials/element/calendar.tsx +0 -125
- package/components/partials/element/dropdown.tsx +0 -248
- package/components/partials/element/filters.tsx +0 -194
- package/components/partials/element/github-link.tsx +0 -16
- package/components/partials/element/initials.tsx +0 -66
- package/components/partials/element/message.tsx +0 -141
- package/components/partials/element/modal.tsx +0 -90
- package/components/partials/element/sidebar.tsx +0 -195
- package/components/partials/element/tooltip.tsx +0 -154
- package/components/partials/element/topbar.tsx +0 -15
- package/components/partials/form/checkbox.tsx +0 -150
- package/components/partials/form/drop-handler.tsx +0 -68
- package/components/partials/form/drop.tsx +0 -141
- package/components/partials/form/field-color.tsx +0 -86
- package/components/partials/form/field-currency.tsx +0 -158
- package/components/partials/form/field-date.tsx +0 -252
- package/components/partials/form/field.tsx +0 -231
- package/components/partials/form/form-error.tsx +0 -27
- package/components/partials/form/location.tsx +0 -225
- package/components/partials/form/select.tsx +0 -360
- package/components/partials/is-first-render.ts +0 -14
- package/components/partials/not-found.tsx +0 -7
- package/components/partials/styleguide.tsx +0 -407
- package/semver-updater.cjs +0 -13
- package/tsconfig.json +0 -38
- package/tsconfig.types.json +0 -15
- package/types/core-only-globals.d.ts +0 -9
- package/types.ts +0 -60
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { twMerge, deepFind, getErrorFromState } from 'nitro-web/util'
|
|
3
|
-
import { Errors, type Error } from 'nitro-web/types'
|
|
4
|
-
|
|
5
|
-
type CheckboxProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
6
|
-
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' */
|
|
7
|
-
name: string
|
|
8
|
-
/** name is applied if id is not provided. Used for radios */
|
|
9
|
-
id?: string
|
|
10
|
-
/** state object to get the value, and check errors against */
|
|
11
|
-
state?: { errors?: Errors, [key: string]: any }
|
|
12
|
-
size?: number
|
|
13
|
-
subtext?: string|React.ReactNode
|
|
14
|
-
text?: string|React.ReactNode
|
|
15
|
-
type?: 'checkbox' | 'radio' | 'toggle'
|
|
16
|
-
checkboxClassName?: string
|
|
17
|
-
svgClassName?: string
|
|
18
|
-
labelClassName?: string
|
|
19
|
-
/** title used to find related error messages */
|
|
20
|
-
errorTitle?: string|RegExp
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function Checkbox({
|
|
24
|
-
state, size, subtext, text, type='checkbox', className, checkboxClassName, svgClassName, labelClassName, errorTitle, ...props
|
|
25
|
-
}: CheckboxProps) {
|
|
26
|
-
// Checkbox/radio/toggle component
|
|
27
|
-
let value!: boolean
|
|
28
|
-
const error = getErrorFromState(state, errorTitle || props.name)
|
|
29
|
-
const id = props.id || props.name
|
|
30
|
-
|
|
31
|
-
if (!props.name) throw new Error('Checkbox requires a `name` prop')
|
|
32
|
-
|
|
33
|
-
// Value: Input is always controlled if state is passed in
|
|
34
|
-
if (typeof props.checked !== 'undefined') value = props.checked
|
|
35
|
-
else if (typeof state == 'object') {
|
|
36
|
-
const v = deepFind(state, props.name) as boolean | undefined
|
|
37
|
-
value = v ?? false
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const BORDER = 2
|
|
41
|
-
const checkboxSize = size ?? 14
|
|
42
|
-
const toggleHeight = size ?? 18
|
|
43
|
-
const toggleWidth = toggleHeight * 2 - BORDER * 2
|
|
44
|
-
const toggleAfterSize = toggleHeight - BORDER * 2
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<div
|
|
48
|
-
className={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after text-sm nitro-checkbox ${className}`)}
|
|
49
|
-
>
|
|
50
|
-
<div className="flex gap-3 items-baseline">
|
|
51
|
-
<div className="shrink-0 flex items-center">
|
|
52
|
-
<div className="w-0"> </div>
|
|
53
|
-
<div className="group relative">
|
|
54
|
-
{
|
|
55
|
-
type !== 'toggle'
|
|
56
|
-
? <>
|
|
57
|
-
<input
|
|
58
|
-
{...props}
|
|
59
|
-
id={id}
|
|
60
|
-
type={type}
|
|
61
|
-
style={{ width: checkboxSize, height: checkboxSize }}
|
|
62
|
-
checked={value}
|
|
63
|
-
className={
|
|
64
|
-
twMerge(
|
|
65
|
-
`${type === 'radio' ? 'rounded-full' : 'rounded'} appearance-none border border-gray-300 bg-white forced-colors:appearance-auto disabled:border-gray-300 disabled:bg-gray-100 disabled:checked:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 ` +
|
|
66
|
-
// Variable-selected theme colors (was .*-blue-600)
|
|
67
|
-
'checked:border-variable-selected checked:bg-variable-selected indeterminate:border-variable-selected indeterminate:bg-variable-selected focus-visible:outline-variable-selected ' +
|
|
68
|
-
// Dark mode not used yet... dark:focus-visible:outline-blue-800
|
|
69
|
-
checkboxClassName
|
|
70
|
-
)
|
|
71
|
-
}
|
|
72
|
-
/>
|
|
73
|
-
<svg
|
|
74
|
-
fill="none"
|
|
75
|
-
viewBox="0 0 14 14"
|
|
76
|
-
style={{ width: checkboxSize, height: checkboxSize }}
|
|
77
|
-
className={twMerge('absolute top-0 left-0 pointer-events-none justify-self-center stroke-white group-has-[:disabled]:stroke-gray-950/25', svgClassName)}
|
|
78
|
-
>
|
|
79
|
-
{
|
|
80
|
-
type === 'radio'
|
|
81
|
-
? <circle
|
|
82
|
-
// cx={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 2}
|
|
83
|
-
// cy={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 2}
|
|
84
|
-
// r={(_size.checkbox.match(/\d+/)?.[0] as unknown as number) / 6}
|
|
85
|
-
cx={7}
|
|
86
|
-
cy={7}
|
|
87
|
-
r={2.5}
|
|
88
|
-
className="fill-white opacity-0 group-has-[:checked]:opacity-100"
|
|
89
|
-
/>
|
|
90
|
-
: <>
|
|
91
|
-
<path
|
|
92
|
-
d="M4 8L6 10L10 4.5"
|
|
93
|
-
strokeWidth={2}
|
|
94
|
-
strokeLinecap="round"
|
|
95
|
-
strokeLinejoin="round"
|
|
96
|
-
className="opacity-0 group-has-[:checked]:opacity-100"
|
|
97
|
-
/>
|
|
98
|
-
<path
|
|
99
|
-
d="M4 7H10"
|
|
100
|
-
strokeWidth={2}
|
|
101
|
-
strokeLinecap="round"
|
|
102
|
-
strokeLinejoin="round"
|
|
103
|
-
className="opacity-0 group-has-[:indeterminate]:opacity-100"
|
|
104
|
-
/>
|
|
105
|
-
</>
|
|
106
|
-
}
|
|
107
|
-
</svg>
|
|
108
|
-
</>
|
|
109
|
-
: <>
|
|
110
|
-
<input
|
|
111
|
-
{...props}
|
|
112
|
-
id={id}
|
|
113
|
-
type="checkbox"
|
|
114
|
-
className="sr-only peer"
|
|
115
|
-
checked={value}
|
|
116
|
-
/>
|
|
117
|
-
<label
|
|
118
|
-
for={id}
|
|
119
|
-
style={{ width: toggleWidth, height: toggleHeight }}
|
|
120
|
-
className={
|
|
121
|
-
twMerge(
|
|
122
|
-
'block bg-gray-200 rounded-full transition-colors peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 ' +
|
|
123
|
-
// Variable-selected theme colors (was .*-blue-600)
|
|
124
|
-
'peer-checked:bg-variable-selected peer-focus-visible:outline-variable-selected ' +
|
|
125
|
-
labelClassName
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
>
|
|
129
|
-
<span
|
|
130
|
-
style={{ width: toggleAfterSize, height: toggleAfterSize }}
|
|
131
|
-
className={
|
|
132
|
-
'absolute top-[2px] start-[2px] bg-white border-gray-300 border rounded-full transition-all group-has-[:checked]:border-white group-has-[:checked]:translate-x-full '
|
|
133
|
-
}
|
|
134
|
-
/>
|
|
135
|
-
</label>
|
|
136
|
-
</>
|
|
137
|
-
}
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
{text &&
|
|
141
|
-
<label for={id} className="text-[length:inherit] leading-[inherit] select-none">
|
|
142
|
-
<span className="text-gray-900">{text}</span>
|
|
143
|
-
<span className="ml-2 text-gray-500">{subtext}</span>
|
|
144
|
-
</label>
|
|
145
|
-
}
|
|
146
|
-
</div>
|
|
147
|
-
{error && <div class="mt-1.5 text-xs text-danger-foreground nitro-error">{error.detail}</div>}
|
|
148
|
-
</div>
|
|
149
|
-
)
|
|
150
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
type DropHandlerProps = {
|
|
2
|
-
onDrop: (files: FileList) => void
|
|
3
|
-
children: React.ReactNode
|
|
4
|
-
className?: string
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export const DropHandler = ({ onDrop, children, className }: DropHandlerProps) => {
|
|
8
|
-
const dropRef = useRef<HTMLDivElement>(null)
|
|
9
|
-
let dragCounter = useRef(0).current
|
|
10
|
-
const [dragging, setDragging] = useState(false)
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
const div = dropRef.current
|
|
14
|
-
div?.addEventListener('dragenter', handleDragIn)
|
|
15
|
-
div?.addEventListener('dragleave', handleDragOut)
|
|
16
|
-
div?.addEventListener('dragover', handleDragOver)
|
|
17
|
-
div?.addEventListener('drop', handleDrop)
|
|
18
|
-
return () => {
|
|
19
|
-
div?.removeEventListener('dragenter', handleDragIn)
|
|
20
|
-
div?.removeEventListener('dragleave', handleDragOut)
|
|
21
|
-
div?.removeEventListener('dragover', handleDragOver)
|
|
22
|
-
div?.removeEventListener('drop', handleDrop)
|
|
23
|
-
}
|
|
24
|
-
}, [])
|
|
25
|
-
|
|
26
|
-
const handleDragIn = (e: DragEvent) => {
|
|
27
|
-
e.preventDefault()
|
|
28
|
-
e.stopPropagation()
|
|
29
|
-
dragCounter++
|
|
30
|
-
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
|
|
31
|
-
setDragging(true)
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const handleDragOut = (e: DragEvent) => {
|
|
36
|
-
e.preventDefault()
|
|
37
|
-
e.stopPropagation()
|
|
38
|
-
dragCounter--
|
|
39
|
-
if (dragCounter === 0) {
|
|
40
|
-
setDragging(false)
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const handleDragOver = (e: DragEvent) => {
|
|
45
|
-
e.preventDefault()
|
|
46
|
-
e.stopPropagation()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const handleDrop = (e: DragEvent) => {
|
|
50
|
-
e.preventDefault()
|
|
51
|
-
e.stopPropagation()
|
|
52
|
-
setDragging(false)
|
|
53
|
-
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
|
54
|
-
onDrop(e.dataTransfer.files)
|
|
55
|
-
// e.dataTransfer.clearData() // causes an error in firefox
|
|
56
|
-
dragCounter = 0
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div
|
|
62
|
-
ref={dropRef}
|
|
63
|
-
class={`${className} relative w-full p-[20px] border-2 border-dashed border-input-border rounded-md ${dragging ? 'border-primary before:content-[""] before:absolute before:inset-0 before:bg-primary before:opacity-5' : ''}`}
|
|
64
|
-
>
|
|
65
|
-
{children}
|
|
66
|
-
</div>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { deepFind, s3Image, getErrorFromState } from 'nitro-web/util'
|
|
3
|
-
import { DropHandler } from 'nitro-web'
|
|
4
|
-
import noImage from 'nitro-web/client/imgs/no-image.svg'
|
|
5
|
-
import { Errors, MonasteryImage } from 'nitro-web/types'
|
|
6
|
-
import { twMerge } from 'nitro-web/util'
|
|
7
|
-
|
|
8
|
-
type DropProps = {
|
|
9
|
-
awsUrl?: string
|
|
10
|
-
className?: string
|
|
11
|
-
/** Field name or path on state (used to match errors), e.g. 'avatar', 'company.avatar' */
|
|
12
|
-
name: string
|
|
13
|
-
/** Optional ID for the input element. Defaults to name if not provided */
|
|
14
|
-
id?: string
|
|
15
|
-
/** Called when file is selected or dropped */
|
|
16
|
-
onChange?: (event: { target: { name: string, value: File|FileList } }) => void
|
|
17
|
-
/** Whether to allow multiple file selection */
|
|
18
|
-
multiple?: boolean
|
|
19
|
-
/** State object to get the value and check errors against */
|
|
20
|
-
state?: {
|
|
21
|
-
errors?: Errors
|
|
22
|
-
[key: string]: unknown
|
|
23
|
-
}
|
|
24
|
-
/** title used to find related error messages */
|
|
25
|
-
errorTitle?: string|RegExp
|
|
26
|
-
/** Props to pass to the input element */
|
|
27
|
-
[key: string]: unknown
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type Image = File | FileList | MonasteryImage | null
|
|
31
|
-
|
|
32
|
-
export function Drop({ awsUrl, className, id, name, onChange, multiple, state, errorTitle, ...props }: DropProps) {
|
|
33
|
-
if (!name) throw new Error('Drop component requires a `name` prop')
|
|
34
|
-
let value: Image = null
|
|
35
|
-
const error = getErrorFromState(state, errorTitle || name)
|
|
36
|
-
const inputId = id || name
|
|
37
|
-
const [urls, setUrls] = useState([])
|
|
38
|
-
const stateRef = useRef(state)
|
|
39
|
-
stateRef.current = state
|
|
40
|
-
|
|
41
|
-
// Input is always controlled if state is passed in
|
|
42
|
-
if (typeof props.value !== 'undefined') value = props.value as Image
|
|
43
|
-
else if (typeof state == 'object') value = deepFind(state, name) as Image
|
|
44
|
-
if (typeof value == 'undefined') value = null
|
|
45
|
-
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
(async () => setUrls(await getUrls(value as File | FileList | MonasteryImage | null)))()
|
|
48
|
-
}, [value])
|
|
49
|
-
|
|
50
|
-
function tryAgain (e: { preventDefault: Function }) {
|
|
51
|
-
e.preventDefault()
|
|
52
|
-
// clear file input to allow reupload
|
|
53
|
-
const input = document.getElementById(name) as HTMLInputElement
|
|
54
|
-
if (input) input.value = ''
|
|
55
|
-
if (onChange) {
|
|
56
|
-
const errors = (stateRef?.current?.errors || []).filter((e: Errors[]) => e?.title != name)
|
|
57
|
-
onChange({
|
|
58
|
-
// remove file from state
|
|
59
|
-
target: { name: name, value: null },
|
|
60
|
-
// reset (server) errors
|
|
61
|
-
errors: errors.length ? errors : undefined,
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function onFileAttach (files: FileList) {
|
|
67
|
-
// files is a FileList object
|
|
68
|
-
if (onChange) onChange({ target: { name: name, value: multiple ? files : files[0] } })
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
async function getUrls(objectOrFileListItem: File | FileList | MonasteryImage | null) {
|
|
72
|
-
/**
|
|
73
|
-
* @param {object|FileList} objectOrFileListItem - FileList object or monastery image object
|
|
74
|
-
* @returns {Promise} - Resolves to an array of image URLs
|
|
75
|
-
*/
|
|
76
|
-
// Make sure FileLists are converted to a real array
|
|
77
|
-
if (!objectOrFileListItem) return []
|
|
78
|
-
const array = 'length' in objectOrFileListItem ? Array.from(objectOrFileListItem) : [objectOrFileListItem]
|
|
79
|
-
return Promise.all(array.map((item) => {
|
|
80
|
-
return new Promise((resolve, reject) => {
|
|
81
|
-
if ('lastModified' in item) {
|
|
82
|
-
const reader = new FileReader()
|
|
83
|
-
reader.onload = () => resolve(reader.result)
|
|
84
|
-
reader.onerror = reject
|
|
85
|
-
reader.readAsDataURL(item)
|
|
86
|
-
} else {
|
|
87
|
-
resolve(s3Image(awsUrl, item))
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
}))
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// function getFilename (objectOrFile) {
|
|
94
|
-
// if (objectOrFile.lastModified) return objectOrFile.name
|
|
95
|
-
// else return 'avatar.jpg'
|
|
96
|
-
// }
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<div class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-field nitro-drop ${className || ''}`)}>
|
|
100
|
-
<input
|
|
101
|
-
{...props}
|
|
102
|
-
id={inputId}
|
|
103
|
-
type="file"
|
|
104
|
-
onChange={(e) => onFileAttach(e.target.files as FileList)}
|
|
105
|
-
hidden
|
|
106
|
-
/>
|
|
107
|
-
<DropHandler
|
|
108
|
-
onDrop={onFileAttach}
|
|
109
|
-
className="flex flex-column justify-center items-center text-center gap-2 text-grey-300 text-sm px-8 min-h-[300px]"
|
|
110
|
-
>
|
|
111
|
-
{
|
|
112
|
-
!value &&
|
|
113
|
-
<>
|
|
114
|
-
{/* {todo upload svg here} */}
|
|
115
|
-
<div>
|
|
116
|
-
Drag and drop your file here
|
|
117
|
-
<label class="weight-500 inline-block text-sm text-primary" for={inputId}>or select a file</label>
|
|
118
|
-
</div>
|
|
119
|
-
</>
|
|
120
|
-
}
|
|
121
|
-
{
|
|
122
|
-
!!value &&
|
|
123
|
-
<>
|
|
124
|
-
{
|
|
125
|
-
urls.map((url, i) => (
|
|
126
|
-
<div key={i} class="flex align-items-center gap-1">
|
|
127
|
-
<img src={url || noImage} width="100%" />
|
|
128
|
-
</div>
|
|
129
|
-
))
|
|
130
|
-
}
|
|
131
|
-
<div>
|
|
132
|
-
Your file has been added successfully.
|
|
133
|
-
<Link to="#" class="text-primary" onClick={tryAgain}>Use another file?</Link>
|
|
134
|
-
</div>
|
|
135
|
-
</>
|
|
136
|
-
}
|
|
137
|
-
</DropHandler>
|
|
138
|
-
{error && <div class="form-error mt-0-5">{error.detail}</div>}
|
|
139
|
-
</div>
|
|
140
|
-
)
|
|
141
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { hsvaToHex, hexToHsva, validHex, HsvaColor } from '@uiw/color-convert'
|
|
2
|
-
import Saturation from '@uiw/react-color-saturation'
|
|
3
|
-
import Hue from '@uiw/react-color-hue'
|
|
4
|
-
import { Dropdown, util } from 'nitro-web'
|
|
5
|
-
|
|
6
|
-
export type FieldColorProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange'|'value'> & {
|
|
7
|
-
name: string
|
|
8
|
-
/** name is applied if id is not provided */
|
|
9
|
-
id?: string
|
|
10
|
-
defaultColor?: string
|
|
11
|
-
Icon?: React.ReactNode
|
|
12
|
-
onChange?: (event: { target: { name: string, value: string } }) => void
|
|
13
|
-
value?: string
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function FieldColor({ defaultColor='#333', Icon, onChange: onChangeProp, value: valueProp, ...props }: FieldColorProps) {
|
|
17
|
-
const [lastChanged, setLastChanged] = useState(() => `ic-${Date.now()}`)
|
|
18
|
-
const isInvalid = props.className?.includes('is-invalid') ? 'is-invalid' : ''
|
|
19
|
-
const id = props.id || props.name
|
|
20
|
-
|
|
21
|
-
// Since value and onChange are optional, we need to hold the value in state if not provided
|
|
22
|
-
const [internalValue, setInternalValue] = useState(valueProp ?? defaultColor)
|
|
23
|
-
const value = valueProp ?? internalValue
|
|
24
|
-
const onChange = onChangeProp ?? ((e: { target: { name: string, value: string } }) => setInternalValue(e.target.value))
|
|
25
|
-
|
|
26
|
-
function onInputChange(e: { target: { name: string, value: string } }) {
|
|
27
|
-
setLastChanged(`ic-${Date.now()}`)
|
|
28
|
-
onChange(e)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
<Dropdown
|
|
33
|
-
dir="bottom-left"
|
|
34
|
-
menuToggles={false}
|
|
35
|
-
menuContent={
|
|
36
|
-
<ColorPicker key={lastChanged} defaultColor={defaultColor} name={props.name} value={value} onChange={onChange} />
|
|
37
|
-
}
|
|
38
|
-
>
|
|
39
|
-
<div className="grid grid-cols-1">
|
|
40
|
-
{Icon}
|
|
41
|
-
<input
|
|
42
|
-
{...props}
|
|
43
|
-
className={(props.className || '') + ' ' + isInvalid}
|
|
44
|
-
id={id}
|
|
45
|
-
value={value}
|
|
46
|
-
onChange={onInputChange}
|
|
47
|
-
onBlur={() => !validHex(value||'') && onInputChange({ target: { name: props.name, value: '' }})}
|
|
48
|
-
autoComplete="off"
|
|
49
|
-
type="text"
|
|
50
|
-
/>
|
|
51
|
-
</div>
|
|
52
|
-
</Dropdown>
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function ColorPicker({ name='', onChange, value='', defaultColor='' }: FieldColorProps) {
|
|
57
|
-
const [hsva, setHsva] = useState(() => hexToHsva(validHex(value) ? value : defaultColor))
|
|
58
|
-
const [debounce] = useState(() => util.throttle(callOnChange, 50))
|
|
59
|
-
|
|
60
|
-
function callOnChange(newHsva: HsvaColor) {
|
|
61
|
-
if (onChange) onChange({ target: { name: name, value: hsvaToHex(newHsva) }})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<>
|
|
66
|
-
<Saturation
|
|
67
|
-
className="!w-[100%] !h-[150px]"
|
|
68
|
-
hsva={hsva}
|
|
69
|
-
onChange={(newHsva) => {
|
|
70
|
-
setHsva(newHsva)
|
|
71
|
-
if (onChange) debounce(newHsva)
|
|
72
|
-
}}
|
|
73
|
-
/>
|
|
74
|
-
<Hue
|
|
75
|
-
hue={hsva.h}
|
|
76
|
-
onChange={(newHue) => {
|
|
77
|
-
setHsva({ ...hsva, ...newHue })
|
|
78
|
-
if (onChange) debounce({ ...hsva, ...newHue })
|
|
79
|
-
}}
|
|
80
|
-
/>
|
|
81
|
-
</>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import { NumericFormat } from 'react-number-format'
|
|
2
|
-
import { getPrefixWidth } from 'nitro-web/util'
|
|
3
|
-
|
|
4
|
-
// Declaring the type here because typescript fails to infer type when referencing NumericFormatProps from react-number-format
|
|
5
|
-
type NumericFormatProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
6
|
-
thousandSeparator?: boolean | string;
|
|
7
|
-
decimalSeparator?: string;
|
|
8
|
-
allowedDecimalSeparators?: Array<string>;
|
|
9
|
-
thousandsGroupStyle?: 'thousand' | 'lakh' | 'wan' | 'none';
|
|
10
|
-
decimalScale?: number;
|
|
11
|
-
fixedDecimalScale?: boolean;
|
|
12
|
-
allowNegative?: boolean;
|
|
13
|
-
allowLeadingZeros?: boolean;
|
|
14
|
-
suffix?: string;
|
|
15
|
-
prefix?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type FieldCurrencyProps = NumericFormatProps & {
|
|
19
|
-
name: string
|
|
20
|
-
/** name is applied if id is not provided */
|
|
21
|
-
id?: string
|
|
22
|
-
/** e.g. { currencies: { nzd: { symbol: '$', digits: 2 } } } (check out the nitro example for more info) */
|
|
23
|
-
config: {
|
|
24
|
-
currencies: { [key: string]: { symbol: string, digits: number } },
|
|
25
|
-
countries: { [key: string]: { numberFormats: { currency: string } } }
|
|
26
|
-
}
|
|
27
|
-
/** currency iso, e.g. 'nzd' */
|
|
28
|
-
currency: string
|
|
29
|
-
onChange?: (event: { target: { name: string, value: string|number|null } }) => void
|
|
30
|
-
/** value should be in cents */
|
|
31
|
-
value?: string|number|null
|
|
32
|
-
defaultValue?: number | string | null
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function FieldCurrency({ config, currency='nzd', onChange, value, defaultValue, ...props }: FieldCurrencyProps) {
|
|
36
|
-
const [dontFix, setDontFix] = useState(false)
|
|
37
|
-
const [settings, setSettings] = useState(() => getCurrencySettings(currency))
|
|
38
|
-
const [dollars, setDollars] = useState(() => toDollars(value, true, settings))
|
|
39
|
-
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
40
|
-
const ref = useRef({ settings, dontFix }) // was null
|
|
41
|
-
const id = props.id || props.name
|
|
42
|
-
ref.current = { settings, dontFix }
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (settings.currency !== currency) {
|
|
46
|
-
const settings = getCurrencySettings(currency)
|
|
47
|
-
setSettings(settings)
|
|
48
|
-
setDollars(toDollars(value, true, settings)) // required latest _settings
|
|
49
|
-
}
|
|
50
|
-
}, [currency])
|
|
51
|
-
|
|
52
|
-
useEffect(() => {
|
|
53
|
-
if (ref.current.dontFix) {
|
|
54
|
-
setDollars(toDollars(value))
|
|
55
|
-
setDontFix(false)
|
|
56
|
-
} else {
|
|
57
|
-
setDollars(toDollars(value, true))
|
|
58
|
-
}
|
|
59
|
-
}, [value])
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
// Get the prefix content width
|
|
64
|
-
setPrefixWidth(settings.prefix == '$' ? getPrefixWidth(settings.prefix, 1) : 0)
|
|
65
|
-
}, [settings.prefix])
|
|
66
|
-
|
|
67
|
-
function toCents(value?: string|number|null) {
|
|
68
|
-
const maxDecimals = ref.current.settings.maxDecimals
|
|
69
|
-
const parsed = parseFloat(value + '')
|
|
70
|
-
if (!parsed && parsed !== 0) return null
|
|
71
|
-
if (!maxDecimals) return parsed
|
|
72
|
-
const value2 = Math.round(parsed * Math.pow(10, maxDecimals)) // e.g. 1.23 => 123
|
|
73
|
-
// console.log('toCents', parsed, value2)
|
|
74
|
-
return value2
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function toDollars(value?: string|number|null, toFixed?: boolean, settings?: { maxDecimals?: number }) {
|
|
78
|
-
const maxDecimals = (settings || ref.current.settings).maxDecimals
|
|
79
|
-
const parsed = parseFloat(value + '')
|
|
80
|
-
if (!parsed && parsed !== 0) return null
|
|
81
|
-
if (!maxDecimals) return parsed
|
|
82
|
-
const value2 = parsed / Math.pow(10, maxDecimals) // e.g. 1.23 => 123
|
|
83
|
-
// console.log('toDollars', value, value2)
|
|
84
|
-
return toFixed ? value2.toFixed(maxDecimals) : value2
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function getCurrencySettings(currency: string) {
|
|
88
|
-
// parse CLDR currency string format, e.g. '¤#,##0.00'
|
|
89
|
-
const output: {
|
|
90
|
-
currency: string, // e.g. 'nzd'
|
|
91
|
-
decimalSeparator?: string, // e.g. '.'
|
|
92
|
-
thousandSeparator?: string, // e.g. ','
|
|
93
|
-
minDecimals?: number, // e.g. 2
|
|
94
|
-
maxDecimals?: number, // e.g. 2
|
|
95
|
-
prefix?: string, // e.g. '$'
|
|
96
|
-
suffix?: string // e.g. ''
|
|
97
|
-
} = { currency }
|
|
98
|
-
const { symbol, digits } = config.currencies[currency]
|
|
99
|
-
let format = config.countries['nz'].numberFormats.currency
|
|
100
|
-
|
|
101
|
-
// Check for currency symbol (¤) and determine its position
|
|
102
|
-
if (format.indexOf('¤') !== -1) {
|
|
103
|
-
const position = format.indexOf('¤') === 0 ? 'prefix' : 'suffix'
|
|
104
|
-
output[position] = symbol
|
|
105
|
-
format = format.replace('¤', '')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Find and set the thousands separator
|
|
109
|
-
const thousandMatch = format.match(/[^0-9#]/)
|
|
110
|
-
if (thousandMatch) output.thousandSeparator = thousandMatch[0]
|
|
111
|
-
|
|
112
|
-
// Find and set the decimal separator and fraction digits
|
|
113
|
-
const decimalMatch = format.match(/0[^0-9]/)
|
|
114
|
-
if (decimalMatch) {
|
|
115
|
-
output.decimalSeparator = decimalMatch[0].slice(1)
|
|
116
|
-
if (typeof digits !== 'undefined') {
|
|
117
|
-
output.minDecimals = digits
|
|
118
|
-
output.maxDecimals = digits
|
|
119
|
-
} else {
|
|
120
|
-
const fractionDigits = format.split(output.decimalSeparator)[1]
|
|
121
|
-
if (fractionDigits) {
|
|
122
|
-
output.minDecimals = fractionDigits.length
|
|
123
|
-
output.maxDecimals = fractionDigits.length
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return output
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<div className="relative">
|
|
132
|
-
<NumericFormat
|
|
133
|
-
{...props}
|
|
134
|
-
id={id}
|
|
135
|
-
name={props.name}
|
|
136
|
-
decimalSeparator={settings.decimalSeparator}
|
|
137
|
-
thousandSeparator={settings.thousandSeparator}
|
|
138
|
-
decimalScale={settings.maxDecimals}
|
|
139
|
-
onValueChange={!onChange ? undefined : ({ floatValue }, e) => {
|
|
140
|
-
// console.log('onValueChange', floatValue, e)
|
|
141
|
-
if (e.source === 'event') setDontFix(true)
|
|
142
|
-
onChange({ target: { name: props.name, value: toCents(floatValue) }})
|
|
143
|
-
}}
|
|
144
|
-
onBlur={() => { setDollars(toDollars(value, true))}}
|
|
145
|
-
placeholder={props.placeholder || '0.00'}
|
|
146
|
-
value={dollars}
|
|
147
|
-
style={{ textIndent: `${prefixWidth}px` }}
|
|
148
|
-
type="text"
|
|
149
|
-
defaultValue={defaultValue}
|
|
150
|
-
/>
|
|
151
|
-
<span
|
|
152
|
-
class={`absolute top-0 bottom-0 left-[12px] left-input-x inline-flex items-center select-none text-gray-500 text-input-base ${dollars !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
|
|
153
|
-
>
|
|
154
|
-
{settings.prefix || settings.suffix}
|
|
155
|
-
</span>
|
|
156
|
-
</div>
|
|
157
|
-
)
|
|
158
|
-
}
|