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.
@@ -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
- export function Modal({ show, setShow, children, className, maxWidth, minHeight, dismissable = true }) {
8
- const [state, setState] = useState()
9
- const containerEl = useRef()
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
- useEffect(() => {
13
- createScrollbarClasses()
14
- return () => {
15
- elementWithScrollbar().classList.remove('scrollbarPadding')
16
- } // cleanup
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
- elementWithScrollbar().classList.add('scrollbarPadding')
22
- setState('modal-open')
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('modal-close-immediately')
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
- let clickedOnContainer = containerEl.current && containerEl.current.contains(e.target)
47
- if (!clickedOnContainer && dismissable) {
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 css={style} class={`${state}`} onClick={(e) => e.stopPropagation()}>
99
- <div class="modal-bg wrapper scrollbarPadding"></div>
100
- <div class="modal-container">
101
- {/* we also need to be able to scroll without closing */}
102
- <div onMouseDown={onClick}>
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 || '740px', minHeight: typeof minHeight == 'undefined' ? '487px' : minHeight }}
106
- class={`modal1 ${className}`}
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 class="modal-close" onClick={() => { if (dismissable) { setShow(false) }}}><SvgX1 /></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: 'Styleguide', to: '/styleguide', Icon: PaintBrushIcon },
88
- { name: 'Pricing (example)', to: '/pricing', Icon: UsersIcon },
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-sm/6 font-semibold'
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-sm/6 font-semibold'
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-label select-none">
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 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' : ''}`}