nitro-web 0.0.15 → 0.0.17
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/_example/client/css/index.css +1 -1
- package/_example/client/index.ts +1 -3
- package/_example/components/index.tsx +10 -4
- package/_example/tailwind.config.js +17 -17
- package/client/index.ts +9 -5
- package/components/auth/reset.tsx +4 -4
- package/components/auth/signin.tsx +3 -3
- package/components/auth/signup.tsx +5 -5
- package/components/dashboard/dashboard.tsx +3 -3
- package/components/partials/element/button.tsx +4 -3
- package/components/partials/element/calendar.tsx +108 -0
- package/components/partials/element/modal.tsx +54 -196
- package/components/partials/element/sidebar.tsx +5 -4
- package/components/partials/form/checkbox.tsx +1 -1
- package/components/partials/form/input-color.tsx +21 -20
- package/components/partials/form/input-currency.tsx +51 -35
- package/components/partials/form/input-date.tsx +55 -167
- package/components/partials/form/input.tsx +123 -92
- package/components/partials/form/select.tsx +4 -4
- package/components/partials/styleguide.tsx +85 -43
- package/components/settings/settings-account.tsx +6 -6
- package/components/settings/settings-business.tsx +12 -12
- package/components/settings/settings-team--member.tsx +8 -8
- package/package.json +10 -6
- package/readme.md +11 -7
- package/types/required-globals.d.ts +1 -0
- package/types/util.d.ts +19 -10
- package/types/util.d.ts.map +1 -1
- package/types.ts +5 -3
- package/util.js +22 -14
- package/webpack.config.js +1 -5
|
@@ -1,111 +1,84 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// todo: finish tailwind conversion
|
|
3
|
-
import { css } from 'twin.macro'
|
|
4
1
|
import { IsFirstRender } from 'nitro-web'
|
|
5
2
|
import SvgX1 from 'nitro-web/client/imgs/icons/x1.svg'
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
type ModalProps = {
|
|
5
|
+
show: boolean
|
|
6
|
+
setShow: (show: boolean) => void
|
|
7
|
+
children: React.ReactNode
|
|
8
|
+
maxWidth?: string
|
|
9
|
+
minHeight?: string
|
|
10
|
+
dismissable?: boolean
|
|
11
|
+
[key: string]: unknown
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Modal({ show, setShow, children, maxWidth, minHeight, dismissable = true, ...props }: ModalProps) {
|
|
15
|
+
const [state, setState] = useState(show ? 'open' : 'close')
|
|
16
|
+
const containerEl = useRef<HTMLDivElement>(null)
|
|
10
17
|
const isFirst = IsFirstRender()
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
const states = {
|
|
20
|
+
'close': {
|
|
21
|
+
root: 'left-[-100vw] transition-[left] duration-0 delay-200',
|
|
22
|
+
bg: 'opacity-0',
|
|
23
|
+
container: 'opacity-0 scale-[0.97]',
|
|
24
|
+
},
|
|
25
|
+
'close-now': {
|
|
26
|
+
root: '',
|
|
27
|
+
bg: '',
|
|
28
|
+
container: 'opacity-0 !transition-none',
|
|
29
|
+
},
|
|
30
|
+
'open': {
|
|
31
|
+
root: 'left-0 transition-none model-open',
|
|
32
|
+
bg: 'opacity-100 duration-200',
|
|
33
|
+
container: 'opacity-100 scale-[1] duration-200',
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
const _state = states[state as keyof typeof states]
|
|
37
|
+
|
|
18
38
|
|
|
19
39
|
useEffect(() => {
|
|
40
|
+
if (isFirst) return
|
|
20
41
|
if (show) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} else if (!isFirst) {
|
|
24
|
-
// Dont close if first render (forgot what use case this was needed for)
|
|
42
|
+
setState('open')
|
|
43
|
+
} else {
|
|
25
44
|
setTimeout(() => {
|
|
26
45
|
// If another modal is being opened, force close the container for a smoother transition
|
|
27
46
|
if (document.getElementsByClassName('modal-open').length > 1) {
|
|
28
|
-
setState('
|
|
47
|
+
setState('close-now')
|
|
29
48
|
} else {
|
|
30
|
-
setState('')
|
|
31
|
-
elementWithScrollbar().classList.remove('scrollbarPadding')
|
|
49
|
+
setState('close')
|
|
32
50
|
}
|
|
33
51
|
}, 10)
|
|
34
52
|
}
|
|
35
53
|
// There is a bug during hot-reloading where the modal does't open if we don't ensure
|
|
36
54
|
// the same truthy/falsey type is used.
|
|
37
55
|
}, [!!show])
|
|
38
|
-
|
|
39
|
-
function elementWithScrollbar() {
|
|
40
|
-
// this needs to be non-body element otherwise the Modal.jsx doesn't open/close smoothly
|
|
41
|
-
//document.getElementsByTagName('body')[0] // document.getElementsByClassName('page')[0]
|
|
42
|
-
return document.getElementById('app')
|
|
43
|
-
}
|
|
44
56
|
|
|
45
|
-
function onClick(e) {
|
|
46
|
-
|
|
47
|
-
if (!
|
|
57
|
+
function onClick(e: React.MouseEvent) {
|
|
58
|
+
const clickedOnModal = containerEl.current && containerEl.current.contains(e.target as Node)
|
|
59
|
+
if (!clickedOnModal && dismissable) {
|
|
48
60
|
setShow(false)
|
|
49
61
|
}
|
|
50
62
|
}
|
|
51
63
|
|
|
52
|
-
function createScrollbarClasses() {
|
|
53
|
-
/**
|
|
54
|
-
* Creates reusable margin and padding classes containing the scrollbar width and
|
|
55
|
-
* sets window.scrollbarWidth
|
|
56
|
-
* @return width
|
|
57
|
-
*/
|
|
58
|
-
if (typeof window.scrollbarWidth !== 'undefined') return
|
|
59
|
-
|
|
60
|
-
var outer = document.createElement('div')
|
|
61
|
-
outer.style.visibility = 'hidden'
|
|
62
|
-
outer.style.width = '100px'
|
|
63
|
-
outer.style.margin = '0px'
|
|
64
|
-
outer.style.padding = '0px'
|
|
65
|
-
outer.style.border = '0'
|
|
66
|
-
document.body.appendChild(outer)
|
|
67
|
-
|
|
68
|
-
var widthNoScroll = outer.offsetWidth
|
|
69
|
-
// force scrollbars
|
|
70
|
-
outer.style.overflow = 'scroll'
|
|
71
|
-
|
|
72
|
-
// add innerdiv
|
|
73
|
-
var inner = document.createElement('div')
|
|
74
|
-
inner.style.width = '100%'
|
|
75
|
-
outer.appendChild(inner)
|
|
76
|
-
|
|
77
|
-
var widthWithScroll = inner.offsetWidth
|
|
78
|
-
|
|
79
|
-
// Remove divs
|
|
80
|
-
outer.parentNode.removeChild(outer)
|
|
81
|
-
let width = (window.scrollbarWidth = widthNoScroll - widthWithScroll)
|
|
82
|
-
|
|
83
|
-
// Create new inline stylesheet and append to the head
|
|
84
|
-
let style = document.createElement('style')
|
|
85
|
-
let css = (
|
|
86
|
-
'.scrollbarPadding {padding-right:' + width + 'px !important; overflow:hidden !important;}' +
|
|
87
|
-
'.scrollbarMargin {margin-right:' + width + 'px !important; overflow:hidden !important;}'
|
|
88
|
-
)
|
|
89
|
-
style.type = 'text/css'
|
|
90
|
-
if (style.styleSheet) style.styleSheet.cssText = css //<=IE8
|
|
91
|
-
else style.appendChild(document.createTextNode(css))
|
|
92
|
-
document.getElementsByTagName('head')[0].appendChild(style)
|
|
93
|
-
|
|
94
|
-
return width
|
|
95
|
-
}
|
|
96
|
-
|
|
97
64
|
return (
|
|
98
|
-
<div
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
65
|
+
<div
|
|
66
|
+
onClick={(e) => e.stopPropagation()}
|
|
67
|
+
class={`fixed top-0 w-[100vw] h-[100vh] z-[700] ${_state.root}`}
|
|
68
|
+
>
|
|
69
|
+
<div class={`!absolute inset-0 box-content bg-gray-500/70 transition-opacity ${_state.bg}`}></div>
|
|
70
|
+
<div class={`relative h-[100vh] overflow-y-auto transition-[opacity,transform] ${_state.container}`}>
|
|
71
|
+
<div class="flex items-center justify-center min-h-full" onMouseDown={onClick}>
|
|
103
72
|
<div
|
|
104
73
|
ref={containerEl}
|
|
105
|
-
style={{ maxWidth: maxWidth || '
|
|
106
|
-
class={`
|
|
74
|
+
style={{ maxWidth: maxWidth || '550px', minHeight: minHeight }}
|
|
75
|
+
class={`relative w-full mx-6 mt-4 mb-8 bg-white rounded-lg shadow-lg ${props.className}`}
|
|
107
76
|
>
|
|
108
|
-
<div
|
|
77
|
+
<div
|
|
78
|
+
class="absolute top-0 right-0 p-3 m-1 cursor-pointer"
|
|
79
|
+
onClick={() => { if (dismissable) { setShow(false) }}}>
|
|
80
|
+
<SvgX1 />
|
|
81
|
+
</div>
|
|
109
82
|
{children}
|
|
110
83
|
</div>
|
|
111
84
|
</div>
|
|
@@ -113,118 +86,3 @@ export function Modal({ show, setShow, children, className, maxWidth, minHeight,
|
|
|
113
86
|
</div>
|
|
114
87
|
)
|
|
115
88
|
}
|
|
116
|
-
|
|
117
|
-
const style = css`
|
|
118
|
-
/* Modal structure */
|
|
119
|
-
& {
|
|
120
|
-
position: fixed;
|
|
121
|
-
top: 0;
|
|
122
|
-
width: 100%;
|
|
123
|
-
height: calc(100vh);
|
|
124
|
-
z-index: 699;
|
|
125
|
-
.modal-bg {
|
|
126
|
-
position: absolute !important;
|
|
127
|
-
display: flex;
|
|
128
|
-
top: 0;
|
|
129
|
-
left: 0;
|
|
130
|
-
right: 0;
|
|
131
|
-
bottom: 0;
|
|
132
|
-
box-sizing: content-box;
|
|
133
|
-
&:before {
|
|
134
|
-
content: '';
|
|
135
|
-
display: block;
|
|
136
|
-
flex: 1;
|
|
137
|
-
background: rgba(255, 255, 255, 0.82);
|
|
138
|
-
/* backdrop-filter: blur(1px);
|
|
139
|
-
-webkit-backdrop-filter: blur(1px); */
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
.modal-container {
|
|
143
|
-
position: relative;
|
|
144
|
-
height: calc(100vh);
|
|
145
|
-
// horisontal centering
|
|
146
|
-
> div {
|
|
147
|
-
display: flex;
|
|
148
|
-
align-items: center;
|
|
149
|
-
justify-content: center;
|
|
150
|
-
min-height: 100%;
|
|
151
|
-
// vertical centering
|
|
152
|
-
> div {
|
|
153
|
-
margin: 30px 20px 90px;
|
|
154
|
-
width: 100%;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
&.modal-close-immediately {
|
|
159
|
-
.modal-container > div > div {
|
|
160
|
-
transition: none !important;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/* Animation */
|
|
166
|
-
|
|
167
|
-
& {
|
|
168
|
-
left: -100%;
|
|
169
|
-
transition: left 0s 0.2s;
|
|
170
|
-
}
|
|
171
|
-
.modal-bg {
|
|
172
|
-
opacity: 0;
|
|
173
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
174
|
-
}
|
|
175
|
-
.modal-container {
|
|
176
|
-
/*overflow: hidden;*/
|
|
177
|
-
overflow-y: scroll;
|
|
178
|
-
overflow-x: auto;
|
|
179
|
-
}
|
|
180
|
-
.modal-container > div > div {
|
|
181
|
-
opacity: 0;
|
|
182
|
-
transform: scale(0.97);
|
|
183
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
184
|
-
}
|
|
185
|
-
&.modal-open {
|
|
186
|
-
left: 0;
|
|
187
|
-
transition: none;
|
|
188
|
-
.modal-bg {
|
|
189
|
-
opacity: 1;
|
|
190
|
-
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
191
|
-
}
|
|
192
|
-
.modal-container {
|
|
193
|
-
overflow-y: scroll;
|
|
194
|
-
overflow-x: auto;
|
|
195
|
-
}
|
|
196
|
-
.modal-container > div > div {
|
|
197
|
-
opacity: 1;
|
|
198
|
-
transform: scale(1);
|
|
199
|
-
transition: opacity 0.2s ease, transform 0.2s ease;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/* Modal customisations */
|
|
204
|
-
|
|
205
|
-
.modal1 {
|
|
206
|
-
background: white;
|
|
207
|
-
border: 2px solid #27242C;
|
|
208
|
-
box-shadow: 0px 1px 29px rgba(31, 29, 36, 0.07);
|
|
209
|
-
border-radius: 8px;
|
|
210
|
-
.subtitle {
|
|
211
|
-
margin-bottom: 34px; // same as form pages
|
|
212
|
-
}
|
|
213
|
-
.modal-close {
|
|
214
|
-
position: absolute;
|
|
215
|
-
margin: 10px;
|
|
216
|
-
padding: 15px 20px;
|
|
217
|
-
top: 0;
|
|
218
|
-
right: 0;
|
|
219
|
-
cursor: pointer;
|
|
220
|
-
line {
|
|
221
|
-
transition: all 0.1s;
|
|
222
|
-
}
|
|
223
|
-
&:hover {
|
|
224
|
-
line {
|
|
225
|
-
/* stroke: theme'colors.primary-dark'; */
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Component: https://tailwindui.com/components/application-ui/application-shells/sidebar#component-a69d85b6237ea2ad506c00ef1cd39a38
|
|
2
2
|
import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessui/react'
|
|
3
3
|
import avatarImg from 'nitro-web/client/imgs/avatar.jpg'
|
|
4
|
+
import { isDemo } from 'nitro-web'
|
|
4
5
|
import {
|
|
5
6
|
Bars3Icon,
|
|
6
7
|
HomeIcon,
|
|
@@ -84,8 +85,8 @@ function SidebarContents ({ Logo, menu, links }: SidebarProps) {
|
|
|
84
85
|
|
|
85
86
|
const _menu = menu || [
|
|
86
87
|
{ name: 'Dashboard', to: '/', Icon: HomeIcon },
|
|
87
|
-
{ name: '
|
|
88
|
-
{ name: 'Pricing
|
|
88
|
+
{ name: isDemo ? 'Design System' : 'Style Guide', to: '/styleguide', Icon: PaintBrushIcon },
|
|
89
|
+
{ name: 'Pricing', to: '/pricing', Icon: UsersIcon },
|
|
89
90
|
{ name: 'Signout', to: '/signout', Icon: ArrowLeftCircleIcon },
|
|
90
91
|
]
|
|
91
92
|
|
|
@@ -114,7 +115,7 @@ function SidebarContents ({ Logo, menu, links }: SidebarProps) {
|
|
|
114
115
|
isActive(item.to)
|
|
115
116
|
? 'bg-gray-50 text-indigo-600'
|
|
116
117
|
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
|
|
117
|
-
'group flex gap-x-3 items-center rounded-md p-2 text-
|
|
118
|
+
'group flex gap-x-3 items-center rounded-md p-2 text-md/6 font-semibold'
|
|
118
119
|
)}
|
|
119
120
|
>
|
|
120
121
|
{ item.Icon &&
|
|
@@ -142,7 +143,7 @@ function SidebarContents ({ Logo, menu, links }: SidebarProps) {
|
|
|
142
143
|
isActive(team.to)
|
|
143
144
|
? 'bg-gray-50 text-indigo-600'
|
|
144
145
|
: 'text-gray-700 hover:bg-gray-50 hover:text-indigo-600',
|
|
145
|
-
'group flex gap-x-3 rounded-md p-2 text-
|
|
146
|
+
'group flex gap-x-3 rounded-md p-2 text-md/6 font-semibold'
|
|
146
147
|
)}
|
|
147
148
|
>
|
|
148
149
|
<span
|
|
@@ -95,7 +95,7 @@ export function Checkbox({ name, id, size='sm', subtext, text, type='checkbox',
|
|
|
95
95
|
}
|
|
96
96
|
</div>
|
|
97
97
|
{text &&
|
|
98
|
-
<label for={id} className="self-center text-sm
|
|
98
|
+
<label for={id} className="self-center text-sm select-none">
|
|
99
99
|
<span className="text-gray-900">{text}</span>
|
|
100
100
|
<span className="ml-2 text-gray-500">{subtext}</span>
|
|
101
101
|
</label>
|
|
@@ -3,21 +3,21 @@ import Saturation from '@uiw/react-color-saturation'
|
|
|
3
3
|
import Hue from '@uiw/react-color-hue'
|
|
4
4
|
import { Dropdown, util } from 'nitro-web'
|
|
5
5
|
|
|
6
|
-
type
|
|
7
|
-
|
|
6
|
+
export type FieldColorProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
|
7
|
+
name: string
|
|
8
|
+
id?: string
|
|
8
9
|
defaultColor?: string
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
value?: string
|
|
13
|
-
[key: string]: unknown
|
|
10
|
+
Icon?: React.ReactNode
|
|
11
|
+
onChange?: (event: { target: { id: string, value: string|null } }) => void
|
|
12
|
+
value?: string|null
|
|
14
13
|
}
|
|
15
14
|
|
|
16
|
-
export function
|
|
15
|
+
export function FieldColor({ defaultColor='#333', Icon, onChange, value, ...props }: FieldColorProps) {
|
|
17
16
|
const [lastChanged, setLastChanged] = useState(() => `ic-${Date.now()}`)
|
|
18
|
-
const isInvalid = className?.includes('is-invalid') ? 'is-invalid' : ''
|
|
17
|
+
const isInvalid = props.className?.includes('is-invalid') ? 'is-invalid' : ''
|
|
18
|
+
const id = props.id || props.name
|
|
19
19
|
|
|
20
|
-
function onInputChange(e: { target: { id: string, value: string } }) {
|
|
20
|
+
function onInputChange(e: { target: { id: string, value: string|null } }) {
|
|
21
21
|
setLastChanged(`ic-${Date.now()}`)
|
|
22
22
|
if (onChange) onChange(e)
|
|
23
23
|
}
|
|
@@ -27,26 +27,27 @@ export function InputColor({ className, defaultColor='#333', iconEl, id, onChang
|
|
|
27
27
|
dir="bottom-left"
|
|
28
28
|
menuToggles={false}
|
|
29
29
|
menuChildren={
|
|
30
|
-
<ColorPicker key={lastChanged} defaultColor={defaultColor} id={id} value={value} onChange={onChange} />
|
|
30
|
+
<ColorPicker key={lastChanged} defaultColor={defaultColor} id={id} name={props.name} value={value} onChange={onChange} />
|
|
31
31
|
}
|
|
32
32
|
>
|
|
33
33
|
<div className="grid grid-cols-1">
|
|
34
|
-
{
|
|
35
|
-
<input
|
|
36
|
-
{...props}
|
|
37
|
-
className={className + ' ' + isInvalid}
|
|
38
|
-
id={id}
|
|
39
|
-
value={value}
|
|
34
|
+
{Icon}
|
|
35
|
+
<input
|
|
36
|
+
{...props}
|
|
37
|
+
className={(props.className || '') + ' ' + isInvalid}
|
|
38
|
+
id={id}
|
|
39
|
+
value={value}
|
|
40
40
|
onChange={onInputChange}
|
|
41
|
-
onBlur={() => !validHex(value||'') && onInputChange({ target: { id: id
|
|
42
|
-
autoComplete="off"
|
|
41
|
+
onBlur={() => !validHex(value||'') && onInputChange({ target: { id: id, value: '' }})}
|
|
42
|
+
autoComplete="off"
|
|
43
|
+
type="text"
|
|
43
44
|
/>
|
|
44
45
|
</div>
|
|
45
46
|
</Dropdown>
|
|
46
47
|
)
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function ColorPicker({ id='', onChange, value='', defaultColor='' }:
|
|
50
|
+
function ColorPicker({ id='', onChange, value='', defaultColor='' }: FieldColorProps) {
|
|
50
51
|
const [hsva, setHsva] = useState(() => hexToHsva(validHex(value) ? value : defaultColor))
|
|
51
52
|
const [debounce] = useState(() => util.throttle(callOnChange, 50))
|
|
52
53
|
|
|
@@ -1,36 +1,43 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
1
|
import { NumericFormat } from 'react-number-format'
|
|
3
|
-
import {
|
|
2
|
+
import { getPrefixWidth } from 'nitro-web/util'
|
|
4
3
|
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
+
id?: string
|
|
21
|
+
/** e.g. { currencies: { nzd: { symbol: '$', digits: 2 } } } (check out the nitro example for more info) */
|
|
9
22
|
config: {
|
|
10
23
|
currencies: { [key: string]: { symbol: string, digits: number } },
|
|
11
24
|
countries: { [key: string]: { numberFormats: { currency: string } } }
|
|
12
25
|
}
|
|
13
|
-
|
|
14
|
-
/** currency iso */
|
|
26
|
+
/** currency iso, e.g. 'nzd' */
|
|
15
27
|
currency: string
|
|
16
|
-
onChange
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
value: number
|
|
28
|
+
onChange?: (event: { target: { id: string, value: string|number|null } }) => void
|
|
29
|
+
/** value should be in cents */
|
|
30
|
+
value?: string|number|null
|
|
31
|
+
defaultValue?: number | string | null
|
|
21
32
|
}
|
|
22
33
|
|
|
23
|
-
export function
|
|
24
|
-
|
|
25
|
-
throw new Error(
|
|
26
|
-
'InputCurrency: `config.currencies` and `config.countries` is required, check out the nitro example for more info.'
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
const [dontFix, setDontFix] = useState()
|
|
34
|
+
export function FieldCurrency({ config, currency='nzd', onChange, value, defaultValue, ...props }: FieldCurrencyProps) {
|
|
35
|
+
const [dontFix, setDontFix] = useState(false)
|
|
30
36
|
const [settings, setSettings] = useState(() => getCurrencySettings(currency))
|
|
31
37
|
const [dollars, setDollars] = useState(() => toDollars(value, true, settings))
|
|
32
|
-
const [prefixWidth, setPrefixWidth] = useState()
|
|
38
|
+
const [prefixWidth, setPrefixWidth] = useState(0)
|
|
33
39
|
const ref = useRef({ settings, dontFix }) // was null
|
|
40
|
+
const id = props.id || props.name
|
|
34
41
|
ref.current = { settings, dontFix }
|
|
35
42
|
|
|
36
43
|
useEffect(() => {
|
|
@@ -53,21 +60,27 @@ export function InputCurrency({ id, config, className, currency='nzd', onChange,
|
|
|
53
60
|
|
|
54
61
|
useEffect(() => {
|
|
55
62
|
// Get the prefix content width
|
|
56
|
-
setPrefixWidth(settings.prefix == '$' ?
|
|
63
|
+
setPrefixWidth(settings.prefix == '$' ? getPrefixWidth(settings.prefix, 1) : 0)
|
|
57
64
|
}, [settings.prefix])
|
|
58
65
|
|
|
59
|
-
function toCents(
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
return
|
|
66
|
+
function toCents(value?: string|number|null) {
|
|
67
|
+
const maxDecimals = ref.current.settings.maxDecimals
|
|
68
|
+
const parsed = parseFloat(value + '')
|
|
69
|
+
if (!parsed && parsed !== 0) return null
|
|
70
|
+
if (!maxDecimals) return parsed
|
|
71
|
+
const value2 = Math.round(parsed * Math.pow(10, maxDecimals)) // e.g. 1.23 => 123
|
|
72
|
+
// console.log('toCents', parsed, value2)
|
|
73
|
+
return value2
|
|
64
74
|
}
|
|
65
75
|
|
|
66
|
-
function toDollars(
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
function toDollars(value?: string|number|null, toFixed?: boolean, settings?: { maxDecimals?: number }) {
|
|
77
|
+
const maxDecimals = (settings || ref.current.settings).maxDecimals
|
|
78
|
+
const parsed = parseFloat(value + '')
|
|
79
|
+
if (!parsed && parsed !== 0) return null
|
|
80
|
+
if (!maxDecimals) return parsed
|
|
81
|
+
const value2 = parsed / Math.pow(10, maxDecimals) // e.g. 1.23 => 123
|
|
82
|
+
// console.log('toDollars', value, value2)
|
|
83
|
+
return toFixed ? value2.toFixed(maxDecimals) : value2
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
function getCurrencySettings(currency: string) {
|
|
@@ -116,8 +129,9 @@ export function InputCurrency({ id, config, className, currency='nzd', onChange,
|
|
|
116
129
|
return (
|
|
117
130
|
<div className="relative">
|
|
118
131
|
<NumericFormat
|
|
119
|
-
|
|
120
|
-
|
|
132
|
+
{...props}
|
|
133
|
+
id={id}
|
|
134
|
+
name={props.name}
|
|
121
135
|
decimalSeparator={settings.decimalSeparator}
|
|
122
136
|
thousandSeparator={settings.thousandSeparator}
|
|
123
137
|
decimalScale={settings.maxDecimals}
|
|
@@ -127,9 +141,11 @@ export function InputCurrency({ id, config, className, currency='nzd', onChange,
|
|
|
127
141
|
onChange({ target: { id: id, value: toCents(floatValue) }})
|
|
128
142
|
}}
|
|
129
143
|
onBlur={() => { setDollars(toDollars(value, true))}}
|
|
130
|
-
placeholder={placeholder || '0.00'}
|
|
144
|
+
placeholder={props.placeholder || '0.00'}
|
|
131
145
|
value={dollars}
|
|
132
146
|
style={{ textIndent: `${prefixWidth}px` }}
|
|
147
|
+
type="text"
|
|
148
|
+
defaultValue={defaultValue}
|
|
133
149
|
/>
|
|
134
150
|
<span
|
|
135
151
|
class={`absolute top-[1px] bottom-0 left-3 inline-flex items-center select-none text-gray-500 text-sm sm:text-sm/6 ${dollars !== null && settings.prefix == '$' ? 'text-dark' : ''}`}
|