nitro-web 0.2.11 → 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.
@@ -739,8 +739,9 @@ export function resolveBaseUrl(reqUrl, cfgUrl) {
739
739
  try {
740
740
  const reqHost = new URL(reqUrl).hostname
741
741
  const cfgHost = new URL(cfgUrl).hostname
742
- const reqApex = getDomain(reqHost)
743
- const cfgApex = getDomain(cfgHost)
742
+ // tldts treats "localhost" as an unknown suffix, so we add an override
743
+ const reqApex = /(^|\.)localhost$/.test(reqHost) ? 'localhost' : getDomain(reqHost)
744
+ const cfgApex = /(^|\.)localhost$/.test(cfgHost) ? 'localhost' : getDomain(cfgHost)
744
745
  const errorMessage = 'Nitro warning: auth.resolveBaseUrl: The request origin and the config baseUrl ' +
745
746
  'are not the same apex domain. Defaulting to the Cfg baseUrl. Req:'
746
747
 
@@ -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.11",
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 🚀",
@@ -31,7 +31,7 @@
31
31
  "minor": "npm run types && standard-version -a --release-as minor && npm publish && cd ../webpack && npm publish",
32
32
  "patch": "npm run types && standard-version -a --release-as patch && npm publish && cd ../webpack && npm publish",
33
33
  "republish": "npm publish && cd ../webpack && npm publish",
34
- "test": "node --test components/**/*.test.js",
34
+ "test": "node --test tests/",
35
35
  "types": "tsc util.js ./server/index.js --declaration --declarationMap --allowJs --emitDeclarationOnly --jsx react-jsx --esModuleInterop --skipLibCheck --outDir types && cd ../webpack && npm run types -w . && cd ../core"
36
36
  },
37
37
  "dependencies": {
@@ -26,8 +26,8 @@ export const optionalEmailConfigKeys = ['emailReplyTo', 'emailTestMode', 'mailgu
26
26
  * @param {string} opts.to - Recipient(s), e.g. "Bruce<bruce@wayneenterprises.com>,..."
27
27
  * @param {Config} opts.config - Config object
28
28
  * @param {string} [opts.bcc] - BCC, e.g. "Bruce<bruce@wayneenterprises.com>" (not sent in development)
29
- * @param {object} [opts.data] - template data shared across recipients
30
- * @param {object} [opts.swigData] - vars for Nunjucks/Swig render only (not sent to Mailgun)
29
+ * @param {object} [opts.data] - common template data shared across recipients
30
+ * @param {object} [opts.swigData] - vars for Nunjucks/Swig render only (not sent to Mailgun). Inherits data.
31
31
  * @param {string} [opts.from] - sender address, e.g. "Bruce<bruce@wayneenterprises.com>"
32
32
  * @param {string} [opts.replyTo] - reply-to address, e.g. "Bruce<bruce@wayneenterprises.com>"
33
33
  * @param {object} [opts.recipientVariables] - Mailgun recipient-variables for batch sending
@@ -1,6 +1,6 @@
1
1
  import { test } from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
- import { resolveBaseUrl } from './auth.api.js'
3
+ import { resolveBaseUrl } from '../components/auth/auth.api.js'
4
4
 
5
5
  const cases = [
6
6
  // [cfgUrl, reqUrl, expected, description]
@@ -16,6 +16,7 @@ const cases = [
16
16
 
17
17
  ['http://localhost:3000', 'http://localhost:3001', 'http://localhost:3001', 'match: localhost: port (ignored)'],
18
18
  ['http://localhost:3000', 'http://app.localhost:3001', 'http://app.localhost:3001', 'match: localhost: nested'],
19
+ ['http://app.localhost:3000', 'http://app2.localhost:3001', 'http://app2.localhost:3001', 'match: localhost: sibling'],
19
20
  ['http://127.0.0.1:3000', 'http://127.0.0.1:3000', 'http://127.0.0.1:3000', 'match: IP exact'],
20
21
 
21
22
  // cfg: mismatch
@@ -1 +1 @@
1
- {"version":3,"file":"auth.api.d.ts","sourceRoot":"","sources":["../../../components/auth/auth.api.js"],"names":[],"mappings":"AAqLA,qEAUC;AAED,qEAwEC;AAED,qEAGC;AAED,gEAMC;AAED,iEAgBC;AAED,oEAGC;AAED,oEA8BC;AAED,gEAgCC;AAID,qGAkDC;AAED;;;GAOC;AAED;;GAKC;AAID;;;;;;;;;GASG;AACH,8EAPG;IAA0B,QAAQ,GAA1B,MAAM;IACY,SAAS,GAA3B,MAAM;IACY,OAAO,GAAzB,MAAM;CACd,YAAQ,MAAM,+BACN,OAAO,GACL,OAAO,CAAC,MAAM,CAAC,CAqE3B;AAED,kFAiBC;AAED,mEAOC;AAED,+EAWC;AAED,sHAiBC;AAED;;;GAEC;AAED;;;GA0BC;AAED;;;;;;;;;GAmCC;AAED;;;;;;;;;;;;;;GAcG;AACH,mGAZG;IAAsD,IAAI,EAAlD,OAAO,GAAG,QAAQ,GAAG,eAAe;IACpB,EAAE,EAAlB,MAAM;IAKH,OAAO,EAJV;QACN,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QACtB,CAAK,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB;IAC0B,YAAY;IACZ,eAAe;IACjB,OAAO,GAAxB,MAAM;CACd,GAAU,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAC,CAAC,CAmEtE;AAED,0CAEC;AAED,8DA6BC;AAED,gFAKC;;;;;;;;;;;;;;;;;;;;AAzuBD;;;;;;;;;;;;;;;EAiBC;AA0ED,0DAEC;AAgCD,mDAEC;AAtBD,iDAkBC;AA5BD,2DAQC;AA0BD,2DAwBC;AAtID,0EAsEC"}
1
+ {"version":3,"file":"auth.api.d.ts","sourceRoot":"","sources":["../../../components/auth/auth.api.js"],"names":[],"mappings":"AAqLA,qEAUC;AAED,qEAwEC;AAED,qEAGC;AAED,gEAMC;AAED,iEAgBC;AAED,oEAGC;AAED,oEA8BC;AAED,gEAgCC;AAID,qGAkDC;AAED;;;GAOC;AAED;;GAKC;AAID;;;;;;;;;GASG;AACH,8EAPG;IAA0B,QAAQ,GAA1B,MAAM;IACY,SAAS,GAA3B,MAAM;IACY,OAAO,GAAzB,MAAM;CACd,YAAQ,MAAM,+BACN,OAAO,GACL,OAAO,CAAC,MAAM,CAAC,CAqE3B;AAED,kFAiBC;AAED,mEAOC;AAED,+EAWC;AAED,sHAiBC;AAED;;;GAEC;AAED;;;GA0BC;AAED;;;;;;;;;GAmCC;AAED;;;;;;;;;;;;;;GAcG;AACH,mGAZG;IAAsD,IAAI,EAAlD,OAAO,GAAG,QAAQ,GAAG,eAAe;IACpB,EAAE,EAAlB,MAAM;IAKH,OAAO,EAJV;QACN,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QACtB,CAAK,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;KACpB;IAC0B,YAAY;IACZ,eAAe;IACjB,OAAO,GAAxB,MAAM;CACd,GAAU,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;CAAC,CAAC,CAmEtE;AAED,0CAEC;AAED,8DA8BC;AAED,gFAKC;;;;;;;;;;;;;;;;;;;;AA1uBD;;;;;;;;;;;;;;;EAiBC;AA0ED,0DAEC;AAgCD,mDAEC;AAtBD,iDAkBC;AA5BD,2DAQC;AA0BD,2DAwBC;AAtID,0EAsEC"}
@@ -7,8 +7,8 @@
7
7
  * @param {string} opts.to - Recipient(s), e.g. "Bruce<bruce@wayneenterprises.com>,..."
8
8
  * @param {Config} opts.config - Config object
9
9
  * @param {string} [opts.bcc] - BCC, e.g. "Bruce<bruce@wayneenterprises.com>" (not sent in development)
10
- * @param {object} [opts.data] - template data shared across recipients
11
- * @param {object} [opts.swigData] - vars for Nunjucks/Swig render only (not sent to Mailgun)
10
+ * @param {object} [opts.data] - common template data shared across recipients
11
+ * @param {object} [opts.swigData] - vars for Nunjucks/Swig render only (not sent to Mailgun). Inherits data.
12
12
  * @param {string} [opts.from] - sender address, e.g. "Bruce<bruce@wayneenterprises.com>"
13
13
  * @param {string} [opts.replyTo] - reply-to address, e.g. "Bruce<bruce@wayneenterprises.com>"
14
14
  * @param {object} [opts.recipientVariables] - Mailgun recipient-variables for batch sending