nitro-web 0.0.86 → 0.0.87
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/client/globals.ts +10 -6
- package/package.json +14 -6
- package/types/{required-globals.d.ts → globals.d.ts} +3 -1
- package/.editorconfig +0 -9
- package/components/auth/auth.api.js +0 -411
- package/components/auth/reset.tsx +0 -86
- package/components/auth/signin.tsx +0 -76
- package/components/auth/signup.tsx +0 -62
- package/components/billing/stripe.api.js +0 -268
- package/components/dashboard/dashboard.tsx +0 -32
- package/components/partials/element/accordion.tsx +0 -102
- package/components/partials/element/avatar.tsx +0 -40
- package/components/partials/element/button.tsx +0 -98
- package/components/partials/element/calendar.tsx +0 -125
- package/components/partials/element/dropdown.tsx +0 -248
- package/components/partials/element/filters.tsx +0 -194
- package/components/partials/element/github-link.tsx +0 -16
- package/components/partials/element/initials.tsx +0 -66
- package/components/partials/element/message.tsx +0 -141
- package/components/partials/element/modal.tsx +0 -90
- package/components/partials/element/sidebar.tsx +0 -195
- package/components/partials/element/tooltip.tsx +0 -154
- package/components/partials/element/topbar.tsx +0 -15
- package/components/partials/form/checkbox.tsx +0 -150
- package/components/partials/form/drop-handler.tsx +0 -68
- package/components/partials/form/drop.tsx +0 -141
- package/components/partials/form/field-color.tsx +0 -86
- package/components/partials/form/field-currency.tsx +0 -158
- package/components/partials/form/field-date.tsx +0 -252
- package/components/partials/form/field.tsx +0 -231
- package/components/partials/form/form-error.tsx +0 -27
- package/components/partials/form/location.tsx +0 -225
- package/components/partials/form/select.tsx +0 -360
- package/components/partials/is-first-render.ts +0 -14
- package/components/partials/not-found.tsx +0 -7
- package/components/partials/styleguide.tsx +0 -407
- package/semver-updater.cjs +0 -13
- package/tsconfig.json +0 -38
- package/tsconfig.types.json +0 -15
- package/types/core-only-globals.d.ts +0 -9
- package/types.ts +0 -60
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
// todo: finish tailwind conversion
|
|
3
|
-
import * as util from 'nitro-web/util'
|
|
4
|
-
|
|
5
|
-
type LocationProps = {
|
|
6
|
-
clear: boolean
|
|
7
|
-
id?: string
|
|
8
|
-
name: string
|
|
9
|
-
onInput?: (place: Place) => void
|
|
10
|
-
onSelect?: (place: Place) => void
|
|
11
|
-
placeholder?: string
|
|
12
|
-
placeTypes?: string[]
|
|
13
|
-
value?: Place
|
|
14
|
-
googleMapsApiKey: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function Location({ clear, id, name, onInput, onSelect, placeholder, placeTypes, value, googleMapsApiKey }: LocationProps) {
|
|
18
|
-
/**
|
|
19
|
-
* Get location or area of place (requires both 'maps javascript' and 'places' APIs)
|
|
20
|
-
*
|
|
21
|
-
* @param {boolean} clear - clear input after select
|
|
22
|
-
* @param {function(place)} onInput - called when the input value changes, with an
|
|
23
|
-
* empty place, e.g. {full: '...', fullModified: true}
|
|
24
|
-
* @param {function(place)} onSelect - called when a place is selected
|
|
25
|
-
* @param {object} value - {full, line1, ..etc}
|
|
26
|
-
*
|
|
27
|
-
* Handy box tester (see also util.mongoAddKmsToBox())
|
|
28
|
-
* https://www.keene.edu/campus/maps/tool/
|
|
29
|
-
*
|
|
30
|
-
* Returned Google places viewport (area), i.e. `place.geometry.viewport`
|
|
31
|
-
* {
|
|
32
|
-
* Qa: {g: 174.4438160493033, h: 174.9684260722261} == [btmLng, topLng]
|
|
33
|
-
* zb: {g: -37.05901990116617, h: -36.66060184426172} == [btmLat, topLat]
|
|
34
|
-
* }
|
|
35
|
-
*/
|
|
36
|
-
const inputRef = useRef(null)
|
|
37
|
-
const full = (value || {}).full || ''
|
|
38
|
-
const [inputValue, setInputValue] = useState(full)
|
|
39
|
-
|
|
40
|
-
useEffect(() => {
|
|
41
|
-
if (!onSelect) console.error('Please pass `onSelect` to location.jsx')
|
|
42
|
-
let autoComplete
|
|
43
|
-
loadGoogleMaps(googleMapsApiKey).then(() => {
|
|
44
|
-
if (inputRef.current) {
|
|
45
|
-
autoComplete = new window.google.maps.places.Autocomplete(inputRef.current, {
|
|
46
|
-
types: placeTypes ? placeTypes : ['address'],
|
|
47
|
-
componentRestrictions: { country: ['nz'] },
|
|
48
|
-
})
|
|
49
|
-
autoComplete.setFields(['address_components', 'formatted_address', 'geometry'])
|
|
50
|
-
autoComplete.addListener('place_changed', onPlaceSelect)
|
|
51
|
-
inputRef.current.addEventListener('keydown', onKeyDown)
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
return () => {
|
|
55
|
-
// It seems like autoComplete cleans up both listeners, handy links if needing to remove sooner..
|
|
56
|
-
// Cleanup listners: https://stackoverflow.com/a/22862011/1900648
|
|
57
|
-
// Cleanup .pac-container: https://stackoverflow.com/a/21419890/1900648
|
|
58
|
-
for (const elem of document.getElementsByClassName('pac-container')) elem.remove()
|
|
59
|
-
}
|
|
60
|
-
}, [])
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (full !== inputValue) setInputValue(full)
|
|
64
|
-
}, [full])
|
|
65
|
-
|
|
66
|
-
function formatAddressObject(place) {
|
|
67
|
-
console.log(place)
|
|
68
|
-
var addressMap = {
|
|
69
|
-
city: ['locality'],
|
|
70
|
-
country: ['country'],
|
|
71
|
-
number: ['street_number'],
|
|
72
|
-
postcode: ['postal_code'],
|
|
73
|
-
region: [
|
|
74
|
-
'administrative_area_level_1',
|
|
75
|
-
'administrative_area_level_2',
|
|
76
|
-
'administrative_area_level_3',
|
|
77
|
-
'administrative_area_level_4',
|
|
78
|
-
'administrative_area_level_5',
|
|
79
|
-
],
|
|
80
|
-
street: ['street_address', 'route'],
|
|
81
|
-
suburb: [
|
|
82
|
-
'sublocality',
|
|
83
|
-
'sublocality_level_1',
|
|
84
|
-
'sublocality_level_2',
|
|
85
|
-
'sublocality_level_3',
|
|
86
|
-
'sublocality_level_4',
|
|
87
|
-
],
|
|
88
|
-
unit: ['subpremise'],
|
|
89
|
-
}
|
|
90
|
-
var address = {
|
|
91
|
-
city: '',
|
|
92
|
-
country: '',
|
|
93
|
-
number: '',
|
|
94
|
-
postcode: '',
|
|
95
|
-
region: '',
|
|
96
|
-
street: '',
|
|
97
|
-
suburb: '',
|
|
98
|
-
unit: '',
|
|
99
|
-
}
|
|
100
|
-
place.address_components.forEach((component) => {
|
|
101
|
-
for (var key in addressMap) {
|
|
102
|
-
if (addressMap[key].indexOf(component.types[0]) !== -1) {
|
|
103
|
-
address[key] = component.long_name
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
})
|
|
107
|
-
if (!address.city) {
|
|
108
|
-
address.city = address.suburb
|
|
109
|
-
address.suburb = ''
|
|
110
|
-
}
|
|
111
|
-
return address
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function onPlaceSelect() {
|
|
115
|
-
const place = this.getPlace()
|
|
116
|
-
if (!place.geometry) return
|
|
117
|
-
if (clear) setInputValue('')
|
|
118
|
-
else setInputValue(place.formatted_address)
|
|
119
|
-
const addressObject = formatAddressObject(place)
|
|
120
|
-
onSelect({
|
|
121
|
-
city: addressObject.city,
|
|
122
|
-
country: addressObject.country,
|
|
123
|
-
line1: [[addressObject.unit, addressObject.number].filter(o=>o).join('/'), addressObject.street].join(' '),
|
|
124
|
-
line2: [addressObject.suburb, addressObject.postcode].filter(o=>o).join(', '),
|
|
125
|
-
full: place.formatted_address,
|
|
126
|
-
number: addressObject.number,
|
|
127
|
-
postcode: addressObject.postcode,
|
|
128
|
-
suburb: addressObject.suburb,
|
|
129
|
-
location: {
|
|
130
|
-
coordinates: [place.geometry.location.lng(), place.geometry.location.lat()],
|
|
131
|
-
type: 'Point',
|
|
132
|
-
},
|
|
133
|
-
unit: addressObject.unit,
|
|
134
|
-
area: !util.deepFind(place, 'geometry.viewport') ? undefined : {
|
|
135
|
-
bottomLeft: [
|
|
136
|
-
place.geometry.viewport.getSouthWest().lng(),
|
|
137
|
-
place.geometry.viewport.getSouthWest().lat(),
|
|
138
|
-
],
|
|
139
|
-
topRight: [
|
|
140
|
-
place.geometry.viewport.getNorthEast().lng(),
|
|
141
|
-
place.geometry.viewport.getNorthEast().lat(),
|
|
142
|
-
],
|
|
143
|
-
},
|
|
144
|
-
})
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function onChange(event) {
|
|
148
|
-
// On input change
|
|
149
|
-
setInputValue(event.target.value)
|
|
150
|
-
if (onInput) onInput({
|
|
151
|
-
full: event.target.value,
|
|
152
|
-
fullModified: true,
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function onFocus(event) {
|
|
157
|
-
// Required to disable the chrome autocomplete, https://stackoverflow.com/a/57131179/4553162
|
|
158
|
-
if (event.target.autocomplete) {
|
|
159
|
-
event.target.autocomplete = 'off'
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function onKeyDown(event) {
|
|
164
|
-
// Stop form submission if there is a google autocomplete dropdown opened
|
|
165
|
-
let prevented
|
|
166
|
-
if (event.key === 'Enter') {
|
|
167
|
-
for (const el of document.getElementsByClassName('pac-container')) {
|
|
168
|
-
if (el.offsetParent !== null && !prevented) { // google autocomplete opened somewhere
|
|
169
|
-
event.preventDefault()
|
|
170
|
-
prevented = true
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return (
|
|
177
|
-
<input
|
|
178
|
-
id={id||name}
|
|
179
|
-
name={name||id}
|
|
180
|
-
onChange={onChange}
|
|
181
|
-
onFocus={onFocus}
|
|
182
|
-
placeholder={placeholder}
|
|
183
|
-
ref={inputRef}
|
|
184
|
-
type="text"
|
|
185
|
-
value={inputValue}
|
|
186
|
-
/>
|
|
187
|
-
)
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function loadGoogleMaps(googleMapsApiKey) {
|
|
191
|
-
// Requires both 'maps javascript api' and 'places api' within Goolge Cloud Platform
|
|
192
|
-
if (!window.initMap) {
|
|
193
|
-
window.initMap = () => {/*noop to prevent warning*/}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return new Promise((res) => {
|
|
197
|
-
const scriptId = 'googleMapsUrl'
|
|
198
|
-
let script = document.getElementById(scriptId)
|
|
199
|
-
// script not yet inserted
|
|
200
|
-
if (script === null) {
|
|
201
|
-
script = document.createElement('script')
|
|
202
|
-
script.type = 'text/javascript'
|
|
203
|
-
script.id = scriptId
|
|
204
|
-
script.src = `https://maps.googleapis.com/maps/api/js?key=${googleMapsApiKey}`+
|
|
205
|
-
'&libraries=places&callback=initMap'
|
|
206
|
-
script.onload = () => res()
|
|
207
|
-
document.getElementsByTagName('head')[0].appendChild(script)
|
|
208
|
-
// script has already been inserted
|
|
209
|
-
} else {
|
|
210
|
-
// script has already loaded
|
|
211
|
-
if (window.google) {
|
|
212
|
-
res()
|
|
213
|
-
// script hasn't been loaded yet
|
|
214
|
-
} else {
|
|
215
|
-
const cachedCallback = script.onload
|
|
216
|
-
script.onload = () => {
|
|
217
|
-
cachedCallback()
|
|
218
|
-
res()
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
})
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Styles are in custom.css
|
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { css } from 'twin.macro'
|
|
3
|
-
import { memo } from 'react'
|
|
4
|
-
import ReactSelect, {
|
|
5
|
-
components, ControlProps, createFilter, OptionProps, SingleValueProps, ClearIndicatorProps,
|
|
6
|
-
DropdownIndicatorProps, MultiValueRemoveProps,
|
|
7
|
-
} from 'react-select'
|
|
8
|
-
import { CheckCircleIcon } from '@heroicons/react/20/solid'
|
|
9
|
-
import { ChevronsUpDownIcon, XIcon } from 'lucide-react'
|
|
10
|
-
import { isFieldCached } from 'nitro-web'
|
|
11
|
-
import { getErrorFromState, deepFind, twMerge } from 'nitro-web/util'
|
|
12
|
-
import { Errors } from 'nitro-web/types'
|
|
13
|
-
|
|
14
|
-
const filterFn = createFilter()
|
|
15
|
-
|
|
16
|
-
type GetSelectStyle = {
|
|
17
|
-
name: string
|
|
18
|
-
isFocused?: boolean
|
|
19
|
-
isSelected?: boolean
|
|
20
|
-
hasError?: boolean
|
|
21
|
-
usePrefixes?: boolean
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** Select (all other props are passed to react-select) **/
|
|
25
|
-
export type SelectProps = {
|
|
26
|
-
/** field name or path on state (used to match errors), e.g. 'date', 'company.email' **/
|
|
27
|
-
name: string
|
|
28
|
-
/** inputId, the name is used if not provided **/
|
|
29
|
-
id?: string
|
|
30
|
-
/** 'container' id to pass to react-select **/
|
|
31
|
-
containerId?: string
|
|
32
|
-
/** The minimum width of the dropdown menu **/
|
|
33
|
-
minMenuWidth?: number
|
|
34
|
-
/** The prefix to add to the input **/
|
|
35
|
-
prefix?: string
|
|
36
|
-
/** The onChange handler **/
|
|
37
|
-
onChange?: (event: { target: { name: string, value: unknown } }) => void
|
|
38
|
-
/** The options to display in the dropdown **/
|
|
39
|
-
options: { value: unknown, label: string | React.ReactNode, fixed?: boolean, [key: string]: unknown }[]
|
|
40
|
-
/** The state object to get the value and check errors from **/
|
|
41
|
-
state?: { errors?: Errors, [key: string]: any } // was unknown|unknown[]
|
|
42
|
-
/** Select variations **/
|
|
43
|
-
mode?: 'country'|'customer'|''
|
|
44
|
-
/** Pass dependencies to break memoization, handy for onChange/onInputChange **/
|
|
45
|
-
deps?: unknown[]
|
|
46
|
-
/** title used to find related error messages */
|
|
47
|
-
errorTitle?: string|RegExp
|
|
48
|
-
/** All other props are passed to react-select **/
|
|
49
|
-
[key: string]: unknown
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const Select = memo(SelectBase, (prev, next) => {
|
|
53
|
-
return isFieldCached(prev, next)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
function SelectBase({
|
|
57
|
-
id, containerId, minMenuWidth, name, prefix='', onChange, options, state, mode='', errorTitle, ...props
|
|
58
|
-
}: SelectProps) {
|
|
59
|
-
let value: unknown|unknown[]
|
|
60
|
-
const error = getErrorFromState(state, errorTitle || name)
|
|
61
|
-
if (!name) throw new Error('Select component requires a `name` and `options` prop')
|
|
62
|
-
|
|
63
|
-
// Get value from value or state
|
|
64
|
-
if (typeof props.value !== 'undefined') value = props.value
|
|
65
|
-
else if (typeof state == 'object') value = deepFind(state, name)
|
|
66
|
-
|
|
67
|
-
// If multi-select, filter options by value
|
|
68
|
-
if (Array.isArray(value)) value = options.filter(o => (value as unknown[]).includes(o.value))
|
|
69
|
-
else value = options.find(o => value === o.value)
|
|
70
|
-
|
|
71
|
-
// Input is always controlled if state is passed in
|
|
72
|
-
if (typeof state == 'object' && typeof value == 'undefined') value = ''
|
|
73
|
-
|
|
74
|
-
return (
|
|
75
|
-
<div css={style} class={'mt-2.5 mb-6 ' + twMerge(`mt-input-before mb-input-after nitro-select ${props.className||''}`)}>
|
|
76
|
-
<ReactSelect
|
|
77
|
-
/**
|
|
78
|
-
* react-select prop quick reference (https://react-select.com/props#api):
|
|
79
|
-
* isDisabled={false}
|
|
80
|
-
* isMulti={false}
|
|
81
|
-
* isSearchable={true}
|
|
82
|
-
* options={[{ value: 'chocolate', label: 'Chocolate' }]}
|
|
83
|
-
* placeholder="Select a color"
|
|
84
|
-
* value={options.find(o => o.code == state.color)} // to clear you need to set to null, not undefined
|
|
85
|
-
* isClearable={false}
|
|
86
|
-
* menuIsOpen={false}
|
|
87
|
-
*/
|
|
88
|
-
{...props}
|
|
89
|
-
// @ts-expect-error
|
|
90
|
-
_nitro={{ prefix, mode }}
|
|
91
|
-
key={value as string}
|
|
92
|
-
unstyled={true}
|
|
93
|
-
inputId={id || name}
|
|
94
|
-
id={containerId}
|
|
95
|
-
filterOption={(option, searchText) => {
|
|
96
|
-
if ((option.data as {fixed?: boolean}).fixed) return true
|
|
97
|
-
return filterFn(option, searchText)
|
|
98
|
-
}}
|
|
99
|
-
menuPlacement="auto"
|
|
100
|
-
minMenuHeight={250}
|
|
101
|
-
onChange={!onChange ? undefined : (o) => {
|
|
102
|
-
// Array returned for multi-select
|
|
103
|
-
const value = Array.isArray(o)
|
|
104
|
-
? o.map(v => typeof v == 'object' && v !== null && 'value' in v ? v.value : v)
|
|
105
|
-
: (typeof o == 'object' && o !== null && 'value' in o ? o.value : o)
|
|
106
|
-
return onChange({ target: { name: name, value: value }})
|
|
107
|
-
}}
|
|
108
|
-
options={options}
|
|
109
|
-
value={value}
|
|
110
|
-
classNames={{
|
|
111
|
-
// Input container
|
|
112
|
-
control: (p) => getSelectStyle({ name: 'control', hasError: !!error, ...p }),
|
|
113
|
-
valueContainer: () => getSelectStyle({ name: 'valueContainer' }),
|
|
114
|
-
// Input container objects
|
|
115
|
-
input: () => getSelectStyle({ name: 'input', hasError: !!error }),
|
|
116
|
-
multiValue: () => getSelectStyle({ name: 'multiValue' }),
|
|
117
|
-
multiValueLabel: () => '',
|
|
118
|
-
multiValueRemove: () => getSelectStyle({ name: 'multiValueRemove' }),
|
|
119
|
-
placeholder: () => getSelectStyle({ name: 'placeholder' }),
|
|
120
|
-
singleValue: () => getSelectStyle({ name: 'singleValue', hasError: !!error }),
|
|
121
|
-
// Indicators
|
|
122
|
-
clearIndicator: () => getSelectStyle({ name: 'clearIndicator' }),
|
|
123
|
-
dropdownIndicator: () => getSelectStyle({ name: 'dropdownIndicator' }),
|
|
124
|
-
indicatorsContainer: () => getSelectStyle({ name: 'indicatorsContainer' }),
|
|
125
|
-
indicatorSeparator: () => getSelectStyle({ name: 'indicatorSeparator' }),
|
|
126
|
-
// Dropmenu
|
|
127
|
-
menu: () => getSelectStyle({ name: 'menu' }),
|
|
128
|
-
groupHeading: () => getSelectStyle({ name: 'groupHeading' }),
|
|
129
|
-
noOptionsMessage: () => getSelectStyle({ name: 'noOptionsMessage' }),
|
|
130
|
-
option: (p) => getSelectStyle({ name: 'option', ...p }),
|
|
131
|
-
}}
|
|
132
|
-
components={{
|
|
133
|
-
Control,
|
|
134
|
-
SingleValue,
|
|
135
|
-
Option,
|
|
136
|
-
DropdownIndicator,
|
|
137
|
-
ClearIndicator,
|
|
138
|
-
MultiValueRemove,
|
|
139
|
-
}}
|
|
140
|
-
styles={{
|
|
141
|
-
menu: (base) => ({
|
|
142
|
-
...base, minWidth: minMenuWidth,
|
|
143
|
-
}),
|
|
144
|
-
// On mobile, the label will truncate automatically, so we want to
|
|
145
|
-
// override that behaviour.
|
|
146
|
-
multiValueLabel: (base) => ({
|
|
147
|
-
...base,
|
|
148
|
-
whiteSpace: 'normal',
|
|
149
|
-
overflow: 'visible',
|
|
150
|
-
}),
|
|
151
|
-
control: (base) => ({
|
|
152
|
-
...base,
|
|
153
|
-
outline: undefined,
|
|
154
|
-
transition: 'none',
|
|
155
|
-
}),
|
|
156
|
-
}}
|
|
157
|
-
// menuIsOpen={true}
|
|
158
|
-
// isSearchable={false}
|
|
159
|
-
// isClearable={true}
|
|
160
|
-
// isMulti={true}
|
|
161
|
-
// isDisabled={true}
|
|
162
|
-
// maxMenuHeight={200}
|
|
163
|
-
/>
|
|
164
|
-
{error && <div class="mt-1.5 text-xs text-danger-foreground">{error.detail}</div>}
|
|
165
|
-
</div>
|
|
166
|
-
)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function Control({ children, ...props }: ControlProps) {
|
|
170
|
-
// Add flag and prefix to the input (control)
|
|
171
|
-
// todo: check that the flag/prefix looks okay
|
|
172
|
-
const selectedOption = props.getValue()[0]
|
|
173
|
-
const optionFlag = (selectedOption as { flag?: string })?.flag
|
|
174
|
-
const _nitro = (props.selectProps as { _nitro?: { prefix?: string, mode?: string } })?._nitro
|
|
175
|
-
return (
|
|
176
|
-
<components.Control {...props}>
|
|
177
|
-
{
|
|
178
|
-
(() => {
|
|
179
|
-
if (_nitro?.prefix) {
|
|
180
|
-
return (
|
|
181
|
-
<>
|
|
182
|
-
<span class="relative right-[2px]">{_nitro?.prefix}</span>
|
|
183
|
-
{children}
|
|
184
|
-
</>
|
|
185
|
-
)
|
|
186
|
-
} else if (_nitro?.mode == 'country') {
|
|
187
|
-
return (
|
|
188
|
-
<>
|
|
189
|
-
{ optionFlag && <Flag flag={optionFlag} /> }
|
|
190
|
-
{children}
|
|
191
|
-
</>
|
|
192
|
-
)
|
|
193
|
-
} else {
|
|
194
|
-
return children
|
|
195
|
-
}
|
|
196
|
-
})()
|
|
197
|
-
}
|
|
198
|
-
</components.Control>
|
|
199
|
-
)
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function SingleValue(props: SingleValueProps) {
|
|
203
|
-
const selectedOption = props.getValue()[0] as { labelControl?: string }
|
|
204
|
-
return (
|
|
205
|
-
<components.SingleValue {...props}>
|
|
206
|
-
<>{selectedOption?.labelControl || props.children}</>
|
|
207
|
-
</components.SingleValue>
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function Option(props: OptionProps) {
|
|
212
|
-
// todo: check that the flag looks okay
|
|
213
|
-
const data = props.data as { className?: string, flag?: string }
|
|
214
|
-
const _nitro = (props.selectProps as { _nitro?: { mode?: string } })?._nitro
|
|
215
|
-
return (
|
|
216
|
-
<components.Option className={data.className} {...props}>
|
|
217
|
-
{ _nitro?.mode == 'country' && <Flag flag={data.flag} /> }
|
|
218
|
-
<span class="flex-auto">{props.label}</span>
|
|
219
|
-
{props.isSelected && <CheckCircleIcon className="size-[22px] text-primary -my-1 -mx-0.5" />}
|
|
220
|
-
</components.Option>
|
|
221
|
-
)
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const DropdownIndicator = (props: DropdownIndicatorProps) => {
|
|
225
|
-
return (
|
|
226
|
-
<components.DropdownIndicator {...props}>
|
|
227
|
-
<ChevronsUpDownIcon size={15} className="text-gray-400 -my-0.5 -mx-[1px]" />
|
|
228
|
-
</components.DropdownIndicator>
|
|
229
|
-
)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const ClearIndicator = (props: ClearIndicatorProps) => {
|
|
233
|
-
return (
|
|
234
|
-
<components.ClearIndicator {...props}>
|
|
235
|
-
<XIcon size={14} />
|
|
236
|
-
</components.ClearIndicator>
|
|
237
|
-
)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
const MultiValueRemove = (props: MultiValueRemoveProps) => {
|
|
241
|
-
return (
|
|
242
|
-
<components.MultiValueRemove {...props}>
|
|
243
|
-
<XIcon className="size-[1em] p-[1px]" />
|
|
244
|
-
</components.MultiValueRemove>
|
|
245
|
-
)
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function Flag({ flag }: { flag?: string }) {
|
|
249
|
-
if (!flag) return null
|
|
250
|
-
// todo: public needs to come from webpack
|
|
251
|
-
const publicPath = '/'
|
|
252
|
-
return (
|
|
253
|
-
<span class="flag" style={{ backgroundImage: `url(${publicPath}assets/imgs/flags/${flag}.svg)` }} />
|
|
254
|
-
)
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const selectStyles = {
|
|
258
|
-
// Based off https://www.jussivirtanen.fi/writing/styling-react-select-with-tailwind
|
|
259
|
-
// Input container
|
|
260
|
-
control: {
|
|
261
|
-
base: 'rounded-md bg-white hover:cursor-pointer text-input-base outline outline-1 -outline-offset-1 '
|
|
262
|
-
+ '!min-h-0 outline-input-border',
|
|
263
|
-
focus: 'outline-2 -outline-offset-2 outline-input-border-focus',
|
|
264
|
-
error: 'outline-danger',
|
|
265
|
-
},
|
|
266
|
-
valueContainer: 'py-[9px] px-[12px] py-input-y px-input-x gap-1', // dont twMerge (input-x is optional)
|
|
267
|
-
// Input container objects
|
|
268
|
-
input: {
|
|
269
|
-
base: 'text-input',
|
|
270
|
-
error: 'text-danger-foreground',
|
|
271
|
-
},
|
|
272
|
-
multiValue: 'bg-primary text-white rounded items-center pl-2 pr-1.5 gap-1.5',
|
|
273
|
-
multiValueLabel: 'text-xs',
|
|
274
|
-
multiValueRemove: 'border border-black/10 bg-clip-content bg-white rounded-md text-foreground hover:bg-red-50',
|
|
275
|
-
placeholder: 'text-input-placeholder',
|
|
276
|
-
singleValue: {
|
|
277
|
-
base: 'text-input',
|
|
278
|
-
error: 'text-danger-foreground',
|
|
279
|
-
},
|
|
280
|
-
// Icon indicators
|
|
281
|
-
clearIndicator: 'text-gray-500 p-1 rounded-md hover:bg-red-50 hover:text-danger-foreground',
|
|
282
|
-
dropdownIndicator: 'p-1 hover:bg-gray-100 text-gray-500 rounded-md hover:text-black',
|
|
283
|
-
indicatorsContainer: 'p-1 px-2 gap-1',
|
|
284
|
-
indicatorSeparator: 'py-0.5 before:content-[""] before:block before:bg-gray-100 before:w-px before:h-full',
|
|
285
|
-
// Dropdown menu
|
|
286
|
-
menu: 'mt-1.5 border border-dropdown-ul-border bg-white rounded-md text-input-base overflow-hidden shadow-dropdown-ul',
|
|
287
|
-
groupHeading: 'ml-3 mt-2 mb-1 text-gray-500 text-input-base',
|
|
288
|
-
noOptionsMessage: 'm-1 text-gray-500 p-2 bg-gray-50 border border-dashed border-gray-200 rounded-sm',
|
|
289
|
-
option: {
|
|
290
|
-
base: 'relative px-3 py-2 !flex items-center gap-2 cursor-default',
|
|
291
|
-
hover: 'bg-gray-50',
|
|
292
|
-
selected: '!bg-gray-100 text-dropdown-selected-foreground',
|
|
293
|
-
},
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function getSelectStyle({ name, isFocused, isSelected, hasError, usePrefixes }: GetSelectStyle) {
|
|
297
|
-
// Returns a class list that conditionally includes hover/focus modifier classes, or uses CSS modifiers, e.g. hover:, focus:
|
|
298
|
-
// @ts-expect-error
|
|
299
|
-
const obj = selectStyles[name]
|
|
300
|
-
let output = obj?.base
|
|
301
|
-
if (typeof obj == 'string') return obj // no modifiers
|
|
302
|
-
|
|
303
|
-
if (usePrefixes) {
|
|
304
|
-
if (obj.focus) output += ' ' + obj.focus.split(' ').map((part: string) => `focus:${part}`).join(' ')
|
|
305
|
-
if (obj.hover) output += ' ' + obj.hover.split(' ').map((part: string) => `hover:${part}`).join(' ')
|
|
306
|
-
} else {
|
|
307
|
-
if (obj.focus && isFocused) output += ` ${obj.focus}`
|
|
308
|
-
if (obj.hover && isFocused) output += ` ${obj.hover}`
|
|
309
|
-
}
|
|
310
|
-
if (obj.error && hasError) output += ` ${obj.error}`
|
|
311
|
-
if (obj.selected && isSelected) output += ` ${obj.selected}`
|
|
312
|
-
|
|
313
|
-
return twMerge(output)
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
const style = css`
|
|
317
|
-
/*
|
|
318
|
-
todo: add these as tailwind classes
|
|
319
|
-
|
|
320
|
-
&.rs-medium {
|
|
321
|
-
.rs__control {
|
|
322
|
-
padding: 9px 13px;
|
|
323
|
-
font-size: 13px;
|
|
324
|
-
font-weight: 400;
|
|
325
|
-
min-height: 0;
|
|
326
|
-
}
|
|
327
|
-
.rs__menu {
|
|
328
|
-
.rs__option {
|
|
329
|
-
font-size: 0.85rem;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
&.rs-small {
|
|
334
|
-
.rs__control {
|
|
335
|
-
padding: 5px 13px;
|
|
336
|
-
font-size: 12.5px;
|
|
337
|
-
font-weight: 400;
|
|
338
|
-
min-height: 0;
|
|
339
|
-
}
|
|
340
|
-
.rs__menu {
|
|
341
|
-
.rs__option {
|
|
342
|
-
font-size: 0.8rem;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
} */
|
|
346
|
-
|
|
347
|
-
/*
|
|
348
|
-
.flag {
|
|
349
|
-
// https://github.com/lipis/flag-icons
|
|
350
|
-
flex-shrink: 0;
|
|
351
|
-
margin-right: 10px;
|
|
352
|
-
width: 21px;
|
|
353
|
-
height: 14px;
|
|
354
|
-
background-size: cover;
|
|
355
|
-
background-repeat: no-repeat;
|
|
356
|
-
background-position: center;
|
|
357
|
-
border-radius: 3px;
|
|
358
|
-
overflow: hidden;
|
|
359
|
-
}*/
|
|
360
|
-
`
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export function IsFirstRender(delay?: number) {
|
|
2
|
-
/*
|
|
3
|
-
* Checks if the current render of a react component is the first
|
|
4
|
-
* E.g. const isFirst = isFirstRender()
|
|
5
|
-
* @link https://stackoverflow.com/a/56267719/1900648
|
|
6
|
-
* @return boolean
|
|
7
|
-
*/
|
|
8
|
-
const isMountRef = useRef(true)
|
|
9
|
-
useEffect(() => {
|
|
10
|
-
if (delay) setTimeout(() => isMountRef.current = false, delay)
|
|
11
|
-
else isMountRef.current = false
|
|
12
|
-
}, [])
|
|
13
|
-
return isMountRef.current
|
|
14
|
-
}
|