nitro-web 0.2.12 → 0.2.13

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.
@@ -22,8 +22,14 @@ export type FieldCurrencyProps = Omit<NumericFormatProps, 'onChange' | 'value' |
22
22
  name: string
23
23
  /** name is applied if id is not provided */
24
24
  id?: string
25
- /** currency iso, e.g. 'nzd' */
26
- currency: string
25
+ /** render as a percent field: no currency symbol, '%' suffix */
26
+ percent?: boolean
27
+ /** minimum decimal places to show (percent only; defaults to 0) */
28
+ minDecimals?: number
29
+ /** maximum decimal places to show (percent only; defaults to 2) */
30
+ maxDecimals?: number
31
+ /** currency iso, e.g. 'nzd' (defaults to 'nzd'; ignored when percent) */
32
+ currency?: string
27
33
  /** override the default currencies array used to lookup currency symbol and digits, e.g. {nzd: { symbol: '$', digits: 2 }} */
28
34
  currencies?: Currencies,
29
35
  /** override the default CLDR country currency format, e.g. '¤#,##0.00' */
@@ -34,10 +40,14 @@ export type FieldCurrencyProps = Omit<NumericFormatProps, 'onChange' | 'value' |
34
40
  defaultValue?: Cents // defined to just fix typescript error
35
41
  }
36
42
 
37
- export function FieldCurrency({ currency='nzd', currencies, format, onChange: onChangeProp, ...props }: FieldCurrencyProps) {
43
+ export function FieldCurrency({
44
+ currency='nzd', currencies, format, percent, minDecimals, maxDecimals, onChange: onChangeProp, ...props
45
+ }: FieldCurrencyProps) {
38
46
  const [dontFix, setDontFix] = useState(false)
39
47
  const [lastBlurred, setLastBlurred] = useState(0)
40
- const [settings, setSettings] = useState(() => getCurrencySettings(currency, currencies, format, props.name))
48
+ const [settings, setSettings] = useState(() => (
49
+ getCurrencySettings(currency, currencies, format, props.name, percent, minDecimals, maxDecimals)
50
+ ))
41
51
  const [prefixWidth, setPrefixWidth] = useState(0)
42
52
  const [inputPaddingLeft, setInputPaddingLeft] = useState(12)
43
53
  const ref = useRef({ dontFix }) // was null
@@ -64,30 +74,36 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange: on
64
74
 
65
75
  // Update the settings if the setting parameters change
66
76
  useEffect(() => {
67
- const _settings = getCurrencySettings(currency, currencies, format, props.name)
77
+ const _settings = getCurrencySettings(currency, currencies, format, props.name, percent, minDecimals, maxDecimals)
68
78
  if (settings.key !== _settings.key) setSettings(_settings)
69
- }, [currency, currencies, format])
79
+ }, [currency, currencies, format, percent, minDecimals, maxDecimals])
70
80
 
71
81
  // Get the prefix content width
72
82
  useEffect(() => {
73
83
  setPrefixWidth(settings.prefix ? getPrefixWidth(settings.prefix, 1) : 0)
74
84
  }, [settings.prefix])
75
85
 
76
- function to(type: 'dollars' | 'cents', value?: Cents, settings?: { maxDecimals?: number }) {
86
+ function to(type: 'dollars' | 'cents', value?: Cents, settings?: { maxDecimals?: number, minDecimals?: number }) {
77
87
  const maxDecimals = settings?.maxDecimals
88
+ const minDecimals = Math.min(settings?.minDecimals ?? 0, maxDecimals ?? 0)
78
89
  const parsed = parseFloat(value + '')
79
90
  if (!parsed && parsed !== 0) return null
80
91
  if (!maxDecimals) return parsed
81
92
 
82
- const value2 = type === 'dollars'
83
- ? parsed / Math.pow(10, maxDecimals)
93
+ const value2 = type === 'dollars'
94
+ ? parsed / Math.pow(10, maxDecimals)
84
95
  : parsed * Math.pow(10, maxDecimals) // e.g. 1.23 => 123
85
96
 
86
97
  // dont fix when the user is typing.
87
98
  if (type === 'dollars' && ref.current.dontFix) { setDontFix(false) }
88
99
 
89
- // console.log('to', type, value, value2)
90
- return type === 'cents' || ref.current.dontFix ? value2 : value2.toFixed(maxDecimals)
100
+ if (type === 'cents' || ref.current.dontFix || !minDecimals) return value2
101
+
102
+ // pad to maxDecimals then trim trailing zeros back down to (at least) minDecimals
103
+ const [whole, decimals=''] = value2.toFixed(maxDecimals).split('.')
104
+ let trimmed = decimals
105
+ while (trimmed.length > minDecimals && trimmed.endsWith('0')) trimmed = trimmed.slice(0, -1)
106
+ return trimmed ? `${whole}.${trimmed}` : whole
91
107
  }
92
108
 
93
109
  function onChange(source: 'event' | 'prop', floatValue?: number) {
@@ -96,6 +112,10 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange: on
96
112
  else setInternalValue(to('cents', floatValue, settings))
97
113
  }
98
114
 
115
+ useEffect(() => {
116
+ console.log('settings', settings)
117
+ }, [settings])
118
+
99
119
  return (
100
120
  <div className="relative">
101
121
  <NumericFormat
@@ -110,6 +130,7 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange: on
110
130
  onBlur={() => { setLastBlurred(Date.now())}}
111
131
  placeholder={props.placeholder || '0.00'}
112
132
  value={inputValue}
133
+ suffix={settings.suffix}
113
134
  style={{ textIndent: `${prefixWidth}px` }}
114
135
  type="text"
115
136
  />
@@ -117,15 +138,28 @@ export function FieldCurrency({ currency='nzd', currencies, format, onChange: on
117
138
  class={`absolute top-0 bottom-0 inline-flex items-center select-none text-gray-500 text-input-base ${inputValue !== null && settings.prefix == '$' ? 'text-foreground' : ''}`}
118
139
  style={{ left: `${inputPaddingLeft}px` }}
119
140
  >
120
- {settings.prefix || settings.suffix}
141
+ {settings.prefix}
121
142
  </span>
122
143
  </div>
123
144
  )
124
145
  }
125
146
 
126
- function getCurrencySettings(currency: string, currencies?: Currencies, format?: string, name?: string) {
147
+ function getCurrencySettings(
148
+ currency: string, currencies?: Currencies, format?: string, name?: string, percent?: boolean,
149
+ minDecimals?: number, maxDecimals?: number
150
+ ) {
151
+ // percent reuses the currency machinery but with a '%' suffix and no currency lookup
152
+ if (percent) {
153
+ const _minDecimals = minDecimals ?? 0
154
+ const _maxDecimals = maxDecimals ?? 2
155
+ return {
156
+ key: `percent:${_minDecimals}:${_maxDecimals}`, currency: currency, decimalSeparator: '.', thousandSeparator: ',',
157
+ minDecimals: _minDecimals, maxDecimals: _maxDecimals, prefix: undefined, suffix: '%',
158
+ }
159
+ }
160
+
127
161
  // parse CLDR currency string format, e.g. '¤#,##0.00'
128
- const output: {
162
+ const output: {
129
163
  key?: string
130
164
  currency: string, // e.g. 'nzd'
131
165
  decimalSeparator?: string, // e.g. '.'
@@ -9,7 +9,7 @@ import { Errors, type Error } from 'nitro-web/types'
9
9
  import { MailIcon, CalendarIcon, FunnelIcon, SearchIcon, EyeIcon, EyeOffIcon, ClockIcon, PaperclipIcon } from 'lucide-react'
10
10
  import { memo, useState } from 'react'
11
11
 
12
- type FieldType = 'text' | 'number' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'date' | 'color' | 'file'
12
+ type FieldType = 'text' | 'number' | 'password' | 'email' | 'filter' | 'search' | 'textarea' | 'currency' | 'percent' | 'date' | 'color' | 'file'
13
13
  type InputProps = React.InputHTMLAttributes<HTMLInputElement>
14
14
  type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
15
15
  type FieldExtraProps = {
@@ -42,6 +42,7 @@ export type FieldProps = (
42
42
  | ({ type?: 'text' | 'number' | 'password' | 'email' | 'filter' | 'search' } & InputProps & FieldExtraProps)
43
43
  | ({ type: 'textarea' } & TextareaProps & FieldExtraProps)
44
44
  | ({ type: 'currency' } & FieldCurrencyProps & FieldExtraProps)
45
+ | ({ type: 'percent' } & Omit<FieldCurrencyProps, 'currency'> & FieldExtraProps)
45
46
  | ({ type: 'color' } & FieldColorProps & FieldExtraProps)
46
47
  | ({ type: 'date' } & FieldDateProps & FieldExtraProps)
47
48
  | ({ type: 'file' } & InputProps & FieldExtraProps)
@@ -140,7 +141,13 @@ function FieldBase({ state, icon, iconPos: ip, errorTitle, inputClassName, ...pr
140
141
  {Icon}<FieldCurrency {...props} {...commonProps} />
141
142
  </FieldContainer>
142
143
  )
143
- } else if (type == 'date') {
144
+ } else if (type == 'percent') {
145
+ return (
146
+ <FieldContainer error={error} className={props.className}>
147
+ {Icon}<FieldCurrency {...props} {...commonProps} percent />
148
+ </FieldContainer>
149
+ )
150
+ } else if (type == 'date') {
144
151
  return (
145
152
  <FieldContainer error={error} className={props.className}>
146
153
  <FieldDate {...props} {...commonProps} Icon={Icon} />
@@ -69,6 +69,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
69
69
  colorsMulti: ['blue', 'green'],
70
70
  country: 'cd',
71
71
  currency: 'nzd',
72
+ percent: 1250,
72
73
  customer: '1',
73
74
  date: Date.now(),
74
75
  dateRange: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 33],
@@ -89,7 +90,8 @@ export function Styleguide({ className, elements, children, currencies, groups }
89
90
  brandColor: '#8656ED',
90
91
  colorsMulti: ['blue'],
91
92
  country: 'au',
92
- currency: 'btc',
93
+ currency: 'btc',
94
+ percent: 875,
93
95
  date: Date.now() + 1000 * 60 * 60 * 24 * 1.2,
94
96
  dateRange: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 5.2],
95
97
  dateMultiple: [Date.now(), Date.now() + 1000 * 60 * 60 * 24 * 3.2],
@@ -223,7 +225,7 @@ export function Styleguide({ className, elements, children, currencies, groups }
223
225
  options={[
224
226
  { label: 'Set Status', onClick: () => { console.log('set', row._id) } },
225
227
  { label: 'Delete', onClick: () => { console.log('remove', row._id) } },
226
- ]}
228
+ ]}
227
229
  dir={rows.slice(0, perPage).length - 3 < i ? 'top-right' : 'bottom-right'}
228
230
  minWidth={100}
229
231
  >
@@ -583,13 +585,20 @@ export function Styleguide({ className, elements, children, currencies, groups }
583
585
  <div>
584
586
  <label for="amount">Amount ({state.amount})</label>
585
587
  <Field
586
- name="amount" type="currency" state={state} currency={state.currency || 'nzd'}
588
+ name="amount"
589
+ type="currency"
590
+ state={state}
591
+ currency={state.currency || 'nzd'}
587
592
  // Example of using a custom format and currencies, e.g.
588
593
  format={'¤#,##0.00'}
589
594
  currencies={currencies}
590
595
  onChange={(e) => onChange(e, setState)}
591
596
  />
592
597
  </div>
598
+ <div>
599
+ <label for="percent">Percent ({state.percent})</label>
600
+ <Field name="percent" type="percent" state={state} onChange={(e) => onChange(e, setState)} minDecimals={1} maxDecimals={4} />
601
+ </div>
593
602
  <div>
594
603
  <label for="firstName">First Name (disabled)</label>
595
604
  <Field name="firstName" state={state} onChange={(e) => onChange(e, setState)} disabled />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nitro-web",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "repository": "github:boycce/nitro-web",
5
5
  "homepage": "https://boycce.github.io/nitro-web/",
6
6
  "description": "Nitro is a battle-tested, modular base project to turbocharge your projects, styled using Tailwind 🚀",