nitro-web 0.0.16 → 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.
@@ -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 InputColorProps = {
7
- className?: string
6
+ export type FieldColorProps = React.InputHTMLAttributes<HTMLInputElement> & {
7
+ name: string
8
+ id?: string
8
9
  defaultColor?: string
9
- iconEl?: React.ReactNode
10
- id?: string
11
- onChange?: (e: { target: { id: string, value: string } }) => void
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 InputColor({ className, defaultColor='#333', iconEl, id, onChange, value, ...props }: InputColorProps) {
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
- {iconEl}
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 || '', value: '' }})}
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='' }: InputColorProps) {
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 { getCurrencyPrefixWidth } from 'nitro-web/util'
2
+ import { getPrefixWidth } from 'nitro-web/util'
4
3
 
5
- type InputCurrencyProps = {
6
- /** field name or path on state */
7
- id: string
8
- /** e.g. { currencies: { nzd: { symbol: '$', digits: 2 } } } */
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
- className: string
14
- /** currency iso */
26
+ /** currency iso, e.g. 'nzd' */
15
27
  currency: string
16
- onChange: (event: { target: { id: string, value: string } }) => void
17
- /** e.g. 'Amount' */
18
- placeholder: string
19
- /** e.g. 123 (input is always controlled if state is passed in) */
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 InputCurrency({ id, config, className, currency='nzd', onChange, placeholder, value }: InputCurrencyProps) {
24
- if (!config?.currencies || !config?.countries) {
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 == '$' ? getCurrencyPrefixWidth(settings.prefix, 1) : 0)
63
+ setPrefixWidth(settings.prefix == '$' ? getPrefixWidth(settings.prefix, 1) : 0)
57
64
  }, [settings.prefix])
58
65
 
59
- function toCents(num: number) {
60
- if (!num && num !== 0) return null
61
- const value = Math.round(num * Math.pow(10, ref.current.settings.maxDecimals)) // e.g. 1.23 => 123
62
- // console.log('toCents', num, value)
63
- return value
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(num: string|number, toFixed: boolean, settings: { maxDecimals: number }) {
67
- if (!num && num !== 0) return null
68
- const value = num / Math.pow(10, (settings || ref.current.settings).maxDecimals) // e.g. 1.23 => 123
69
- // console.log('toDollars', num, value)
70
- return toFixed ? value.toFixed((settings || ref.current.settings).maxDecimals) : value
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
- id={id}
120
- className={className}
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' : ''}`}
@@ -1,56 +1,56 @@
1
- // @ts-nocheck
2
- // todo: finish tailwind conversion
3
- import { css } from 'twin.macro'
4
- import { DayPicker } from 'react-day-picker'
5
1
  import { format, isValid, parse } from 'date-fns'
6
- import { getCurrencyPrefixWidth } from 'nitro-web/util'
7
- import { Dropdown } from 'nitro-web'
8
- import 'react-day-picker/dist/style.css'
2
+ import { getPrefixWidth } from 'nitro-web/util'
3
+ import { Calendar, Dropdown } from 'nitro-web'
9
4
 
10
- export function InputDate({ className, prefix, id, onChange, mode='single', value, ...props }) {
11
- /**
12
- * @param {string} mode - 'single'|'range'|'multiple' - an array is returned for non-single modes
13
- */
5
+ type Mode = 'single' | 'multiple' | 'range'
6
+ type DropdownRef = {
7
+ setIsActive: (value: boolean) => void
8
+ }
9
+ export type FieldDateProps = React.InputHTMLAttributes<HTMLInputElement> & {
10
+ name: string
11
+ id?: string
12
+ mode?: Mode
13
+ // an array is returned for non-single modes
14
+ onChange?: (e: { target: { id: string, value: null|number|(null|number)[] } }) => void
15
+ prefix?: string
16
+ value?: null|number|string|(null|number|string)[]
17
+ numberOfMonths?: number
18
+ Icon?: React.ReactNode
19
+ }
20
+
21
+ export function FieldDate({ mode='single', onChange, prefix='', value, numberOfMonths, Icon, ...props }: FieldDateProps) {
14
22
  const localePattern = 'd MMM yyyy'
15
- const isInvalid = className?.includes('is-invalid') ? 'is-invalid' : ''
16
- const [prefixWidth, setPrefixWidth] = useState()
17
- const ref = useRef(null)
23
+ const [prefixWidth, setPrefixWidth] = useState(0)
24
+ const dropdownRef = useRef<DropdownRef>(null)
25
+ const id = props.id || props.name
26
+ const [month, setMonth] = useState<number|undefined>()
18
27
 
28
+ // Convert the value to an array of valid* dates
19
29
  const dates = useMemo(() => {
20
- // Convert the value to an array of valid* dates
21
30
  const _dates = Array.isArray(value) ? value : [value]
22
- return _dates.map(date => isValid(date) ? new Date(date) : undefined)
31
+ return _dates.map(date => isValid(date) ? new Date(date as number) : null) /// change to null
23
32
  }, [value])
24
33
 
25
- // Hold the month in state to control the calendar when the input changes
26
- const [month, setMonth] = useState(dates[0])
27
-
28
34
  // Hold the input value in state
29
35
  const [inputValue, setInputValue] = useState(() => getInputValue(dates))
30
36
 
37
+ // Get the prefix content width
31
38
  useEffect(() => {
32
- // Get the prefix content width
33
- setPrefixWidth(getCurrencyPrefixWidth(prefix, 4))
39
+ setPrefixWidth(getPrefixWidth(prefix, 4))
34
40
  }, [prefix])
35
41
 
36
- function handleDayPickerSelect(newDate) {
37
- if (mode == 'single') {
38
- ref.current.setIsActive(false) // close the dropdown
39
- callOnChange(newDate?.getTime() || null)
40
- setInputValue(getInputValue([newDate]))
41
-
42
- } else if (mode == 'range') {
43
- const {from, to} = newDate || {} // may not exist
44
- callOnChange(from ? [from?.getTime() || null, to?.getTime() || null] : null)
45
- setInputValue(getInputValue(from ? [from, to] : []))
42
+ function onCalendarChange(mode: Mode, value: null|number|(null|number)[]) {
43
+ if (mode == 'single') dropdownRef.current?.setIsActive(false) // Close the dropdown
44
+ setInputValue(getInputValue(value))
45
+ if (onChange) onChange({ target: { id: id, value: value }})
46
+ }
46
47
 
47
- } else {
48
- callOnChange(newDate.filter(o => o).map(d => d.getTime()))
49
- setInputValue(getInputValue(newDate.filter(o => o)))
50
- }
48
+ function getInputValue(dates: Date|number|null|(Date|number|null)[]) {
49
+ const _dates = Array.isArray(dates) ? dates : [dates]
50
+ return _dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
51
51
  }
52
52
 
53
- function handleInputChange(e) {
53
+ function onInputChange(e: React.ChangeEvent<HTMLInputElement>) {
54
54
  setInputValue(e.target.value) // keep the input value in sync
55
55
 
56
56
  let split = e.target.value.split(/-|,/).map(o => {
@@ -63,162 +63,50 @@ export function InputDate({ className, prefix, id, onChange, mode='single', valu
63
63
  else if (mode == 'multiple') split = split.filter(o => o) // remove invalid dates
64
64
 
65
65
  // Swap dates if needed
66
- if (mode == 'range' && split[0] > split[1]) split = [split[0], split[0]]
66
+ if (mode == 'range' && (split[0] || 0) > (split[1] || 0)) split = [split[0], split[0]]
67
67
 
68
68
  // Set month
69
69
  for (let i=split.length; i--;) {
70
- if (split[i]) setMonth(split[i])
70
+ if (split[i]) setMonth((split[i] as Date).getTime())
71
71
  break
72
72
  }
73
73
 
74
- // Set dates
75
- callOnChange(mode == 'single' ? split[0] : split)
76
- }
77
-
78
- function getInputValue(dates) {
79
- return dates.map(o => o ? format(o, localePattern) : '').join(mode == 'range' ? ' - ' : ', ')
80
- }
81
-
82
- function callOnChange(value) {
83
- if (onChange) onChange({ target: { id: id, value: value }}) // timestamp|[timestamp]
74
+ // Update
75
+ const value = mode == 'single' ? split[0]?.getTime() ?? null : split.map(d => d?.getTime() ?? null)
76
+ if (onChange) onChange({ target: { id, value }})
84
77
  }
85
78
 
86
79
  return (
87
80
  <Dropdown
88
- ref={ref}
89
- css={style}
81
+ ref={dropdownRef}
90
82
  menuToggles={false}
91
83
  animate={false}
92
84
  // menuIsOpen={true}
85
+ minWidth={0}
93
86
  menuChildren={
94
- <DayPicker
95
- mode={mode}
96
- month={month}
97
- onMonthChange={setMonth}
98
- numberOfMonths={mode == 'range' ? 2 : 1}
99
- selected={mode === 'single' ? dates[0] : mode == 'range' ? { from: dates[0], to: dates[1] } : dates}
100
- onSelect={handleDayPickerSelect}
101
- />
87
+ <Calendar {...{ mode, value, numberOfMonths, month }} onChange={onCalendarChange} className="px-3 pt-1 pb-2" />
102
88
  }
103
89
  >
104
- <div>
105
- {prefix && <span class={`input-prefix ${inputValue ? 'has-value' : ''}`}>{prefix}</span>}
90
+ <div className="grid grid-cols-1">
91
+ {Icon}
92
+ {
93
+ prefix &&
94
+ // Similar classNames to the input.tsx:IconWrapper()
95
+ <span className="relative col-start-1 row-start-1 self-center select-none z-[1] justify-self-start text-sm ml-3">{prefix}</span>
96
+ }
106
97
  <input
107
98
  {...props}
108
- key={'k'+prefixWidth}
99
+ key={'k' + prefixWidth}
109
100
  id={id}
110
101
  autoComplete="off"
111
- className={
112
- className + ' ' + isInvalid
113
- }
102
+ className={(props.className||'')}// + props.className?.includes('is-invalid') ? ' is-invalid' : ''}
114
103
  value={inputValue}
115
- onChange={handleInputChange}
104
+ onChange={onInputChange}
116
105
  onBlur={() => setInputValue(getInputValue(dates))}
117
106
  style={{ textIndent: prefixWidth + 'px' }}
107
+ type="text"
118
108
  />
119
109
  </div>
120
110
  </Dropdown>
121
111
  )
122
112
  }
123
-
124
- const style = css`
125
- .rdp {
126
- --rdp-cell-size: 34px;
127
- --rdp-caption-font-size: 12px;
128
- --rdp-accent-color: blue; /* theme('colors.primary') */
129
- font-size: 13px;
130
- margin: 0 12px 11px;
131
- svg {
132
- width: 13px;
133
- height: 13px;
134
- }
135
- .rdp-caption_label {
136
- height: var(--rdp-cell-size);
137
- }
138
- .rdp-head_cell {
139
- text-align: center !important;
140
- }
141
- tr {
142
- display: flex;
143
- justify-content: space-around;
144
- align-items: center;
145
- th,
146
- td {
147
- display: flex;
148
- align-items: center;
149
- margin-left: -1px;
150
- margin-top: -1px;
151
- .rdp-day {
152
- border: 0 !important;
153
- position: relative;
154
- border-radius: 0 !important;
155
- color: inherit;
156
- background-color: transparent !important;
157
- &:before {
158
- content: '';
159
- position: absolute;
160
- display: block;
161
- left: 0px;
162
- top: 0px;
163
- bottom: 0px;
164
- right: 0px;
165
- z-index: -1;
166
- }
167
- }
168
- .rdp-day:focus,
169
- .rdp-day:hover,
170
- .rdp-day:active {
171
- &:not([disabled]):not(.rdp-day_selected) {
172
- &:before {
173
- left: 1px;
174
- top: 1px;
175
- bottom: 1px;
176
- right: 1px;
177
- border-radius: 50%;
178
- background-color: #e7edff;
179
- }
180
- &:active {
181
- color: white;
182
- &:before {
183
- background-color: blue; /* theme('colors.primary') */
184
- }
185
- }
186
- }
187
- }
188
- .rdp-day_selected {
189
- color: white;
190
- :before {
191
- border-radius: 50%;
192
- background-color: blue; /* theme('colors.primary') */
193
- }
194
- }
195
- .rdp-day_range_middle {
196
- color: black; /* theme('colors.dark') */
197
- :before {
198
- border-radius: 0;
199
- border: 1px solid rgb(151 133 185);
200
- background-color: blue; /* theme('colors.primary-light') */
201
- }
202
- }
203
- .rdp-day_range_start,
204
- .rdp-day_range_end {
205
- position: relative;
206
- z-index: 1;
207
- &.rdp-day_range_start:before {
208
- border-top-right-radius: 0px;
209
- border-bottom-right-radius: 0px;
210
- }
211
- &.rdp-day_range_end:before {
212
- border-top-left-radius: 0px;
213
- border-bottom-left-radius: 0px;
214
- }
215
- &.rdp-day_range_start.rdp-day_range_end:before {
216
- border-radius: 50%;
217
- }
218
- }
219
- }
220
- }
221
- }
222
- `
223
-
224
-