@neko-os/ui 0.5.3 → 0.6.0
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/dist/NekoUI.js +12 -9
- package/dist/abstractions/Image.native.js +1 -1
- package/dist/abstractions/Image.web.js +1 -1
- package/dist/abstractions/WindowOverlay.js +3 -0
- package/dist/abstractions/WindowOverlay.native.js +21 -0
- package/dist/abstractions/helpers/storage.js +14 -4
- package/dist/abstractions/helpers/storage.native.js +9 -1
- package/dist/components/feedback/notifications/NotificationsHandler.js +10 -6
- package/dist/components/form/useNewForm.js +2 -0
- package/dist/components/index.js +3 -1
- package/dist/components/inputs/DateInput.js +10 -6
- package/dist/components/inputs/InputWrapper.js +11 -11
- package/dist/components/inputs/NumberWheelInput.js +50 -0
- package/dist/components/inputs/NumberWheelPicker.js +43 -0
- package/dist/components/inputs/SegmentedPicker.js +3 -2
- package/dist/components/inputs/UploadInput.js +4 -4
- package/dist/components/inputs/WheelPicker.js +49 -0
- package/dist/components/inputs/WheelPicker.native.js +88 -0
- package/dist/components/inputs/WheelPicker.web.js +1 -0
- package/dist/components/inputs/dateWheelPicker/DateWheelPicker.js +24 -0
- package/dist/components/inputs/dateWheelPicker/DayWheelPicker.js +48 -0
- package/dist/components/inputs/dateWheelPicker/MonthWheelPicker.js +19 -0
- package/dist/components/inputs/dateWheelPicker/QuarterWheelPicker.js +61 -0
- package/dist/components/inputs/dateWheelPicker/WeekWheelPicker.js +66 -0
- package/dist/components/inputs/dateWheelPicker/YearWheelPicker.js +35 -0
- package/dist/components/inputs/index.js +5 -1
- package/dist/components/inputs/upload/Upload.native.js +60 -52
- package/dist/components/inputs/upload/useUploadState.js +11 -3
- package/dist/components/measurements/FeetInchesInput.js +91 -0
- package/dist/components/measurements/LengthInput.js +32 -0
- package/dist/components/measurements/LengthText.js +10 -0
- package/dist/components/measurements/MeasurementHandler.js +26 -0
- package/dist/components/measurements/WeightInput.js +25 -0
- package/dist/components/measurements/WeightText.js +10 -0
- package/dist/components/measurements/helpers/detectMeasurementSystem.js +15 -0
- package/dist/components/measurements/helpers/detectMeasurementSystem.native.js +9 -0
- package/dist/components/measurements/helpers/index.js +2 -0
- package/dist/components/measurements/helpers/length.js +112 -0
- package/dist/components/measurements/helpers/weight.js +56 -0
- package/dist/components/measurements/index.js +9 -0
- package/dist/components/measurements/useLengthFormatter.js +35 -0
- package/dist/components/measurements/useLocalInputValue.js +32 -0
- package/dist/components/measurements/useWeightFormatter.js +29 -0
- package/dist/components/presentation/Avatar.js +3 -3
- package/dist/components/routing/ReturnButton.js +20 -0
- package/dist/components/routing/ReturnButton.native.js +20 -0
- package/dist/components/routing/ReturnButton.web.js +2 -0
- package/dist/components/routing/ReturnLink.js +25 -0
- package/dist/components/routing/ReturnLink.native.js +25 -0
- package/dist/components/routing/ReturnLink.web.js +2 -0
- package/dist/components/routing/RoutedStepsContent.js +21 -0
- package/dist/components/routing/RoutedStepsContent.native.js +94 -0
- package/dist/components/routing/RoutedStepsContent.web.js +3 -0
- package/dist/components/routing/index.js +3 -0
- package/dist/components/state/StatePresenter.js +1 -1
- package/dist/components/steps/StepsHandler.js +2 -0
- package/dist/components/structure/TopBar.js +18 -16
- package/dist/components/theme/ThemePickerDrawer.js +1 -1
- package/dist/helpers/compress.js +61 -0
- package/dist/helpers/compress.native.js +49 -0
- package/dist/helpers/files.js +7 -0
- package/dist/helpers/files.native.js +55 -0
- package/dist/helpers/index.js +6 -1
- package/dist/helpers/media.js +4 -0
- package/dist/helpers/media.native.js +41 -0
- package/dist/helpers/numbers.js +13 -0
- package/dist/helpers/pickAssets.js +7 -0
- package/dist/helpers/pickAssets.native.js +66 -0
- package/dist/helpers/storage.js +17 -0
- package/dist/i18n/I18n.js +4 -4
- package/dist/index.js +1 -1
- package/dist/modifiers/flex.js +8 -3
- package/dist/responsive/responsiveHooks.js +14 -0
- package/dist/theme/default/blackTheme.js +3 -1
- package/dist/theme/default/cyberpunkTheme.js +3 -1
- package/dist/theme/default/darkTheme.js +3 -1
- package/dist/theme/default/hackerTheme.js +3 -1
- package/dist/theme/default/lightTheme.js +3 -1
- package/dist/theme/default/paperTheme.js +3 -1
- package/package.json +2 -14
- package/src/NekoUI.js +16 -13
- package/src/abstractions/Image.native.js +1 -1
- package/src/abstractions/Image.web.js +1 -1
- package/src/abstractions/WindowOverlay.js +3 -0
- package/src/abstractions/WindowOverlay.native.js +21 -0
- package/src/abstractions/helpers/storage.js +13 -3
- package/src/abstractions/helpers/storage.native.js +8 -0
- package/src/components/feedback/notifications/NotificationsHandler.js +12 -8
- package/src/components/form/useNewForm.js +2 -0
- package/src/components/index.js +2 -0
- package/src/components/inputs/DateInput.js +8 -4
- package/src/components/inputs/InputWrapper.js +3 -3
- package/src/components/inputs/NumberWheelInput.js +50 -0
- package/src/components/inputs/NumberWheelPicker.js +43 -0
- package/src/components/inputs/SegmentedPicker.js +2 -1
- package/src/components/inputs/UploadInput.js +2 -2
- package/src/components/inputs/WheelPicker.js +49 -0
- package/src/components/inputs/WheelPicker.native.js +88 -0
- package/src/components/inputs/WheelPicker.web.js +1 -0
- package/src/components/inputs/dateWheelPicker/DateWheelPicker.js +24 -0
- package/src/components/inputs/dateWheelPicker/DayWheelPicker.js +48 -0
- package/src/components/inputs/dateWheelPicker/MonthWheelPicker.js +19 -0
- package/src/components/inputs/dateWheelPicker/QuarterWheelPicker.js +61 -0
- package/src/components/inputs/dateWheelPicker/WeekWheelPicker.js +66 -0
- package/src/components/inputs/dateWheelPicker/YearWheelPicker.js +35 -0
- package/src/components/inputs/index.js +4 -0
- package/src/components/inputs/upload/Upload.native.js +58 -50
- package/src/components/inputs/upload/useUploadState.js +11 -3
- package/src/components/measurements/FeetInchesInput.js +91 -0
- package/src/components/measurements/LengthInput.js +32 -0
- package/src/components/measurements/LengthText.js +10 -0
- package/src/components/measurements/MeasurementHandler.js +26 -0
- package/src/components/measurements/WeightInput.js +25 -0
- package/src/components/measurements/WeightText.js +10 -0
- package/src/components/measurements/helpers/detectMeasurementSystem.js +15 -0
- package/src/components/measurements/helpers/detectMeasurementSystem.native.js +9 -0
- package/src/components/measurements/helpers/index.js +2 -0
- package/src/components/measurements/helpers/length.js +112 -0
- package/src/components/measurements/helpers/weight.js +56 -0
- package/src/components/measurements/index.js +9 -0
- package/src/components/measurements/useLengthFormatter.js +35 -0
- package/src/components/measurements/useLocalInputValue.js +32 -0
- package/src/components/measurements/useWeightFormatter.js +29 -0
- package/src/components/presentation/Avatar.js +2 -2
- package/src/components/routing/ReturnButton.js +20 -0
- package/src/components/routing/ReturnButton.native.js +20 -0
- package/src/components/routing/ReturnButton.web.js +2 -0
- package/src/components/routing/ReturnLink.js +25 -0
- package/src/components/routing/ReturnLink.native.js +25 -0
- package/src/components/routing/ReturnLink.web.js +2 -0
- package/src/components/routing/RoutedStepsContent.js +21 -0
- package/src/components/routing/RoutedStepsContent.native.js +94 -0
- package/src/components/routing/RoutedStepsContent.web.js +3 -0
- package/src/components/routing/index.js +3 -0
- package/src/components/state/StatePresenter.js +1 -1
- package/src/components/steps/StepsHandler.js +2 -0
- package/src/components/structure/TopBar.js +16 -14
- package/src/components/theme/ThemePickerDrawer.js +1 -1
- package/src/helpers/compress.js +61 -0
- package/src/helpers/compress.native.js +49 -0
- package/src/helpers/files.js +7 -0
- package/src/helpers/files.native.js +55 -0
- package/src/helpers/index.js +6 -1
- package/src/helpers/media.js +4 -0
- package/src/helpers/media.native.js +41 -0
- package/src/helpers/numbers.js +13 -0
- package/src/helpers/pickAssets.js +7 -0
- package/src/helpers/pickAssets.native.js +66 -0
- package/src/helpers/storage.js +17 -0
- package/src/i18n/I18n.js +2 -2
- package/src/index.js +1 -1
- package/src/modifiers/flex.js +7 -2
- package/src/responsive/responsiveHooks.js +14 -0
- package/src/theme/default/blackTheme.js +2 -0
- package/src/theme/default/cyberpunkTheme.js +2 -0
- package/src/theme/default/darkTheme.js +2 -0
- package/src/theme/default/hackerTheme.js +2 -0
- package/src/theme/default/lightTheme.js +2 -0
- package/src/theme/default/paperTheme.js +2 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import dayjs from 'dayjs'
|
|
3
|
+
|
|
4
|
+
import { ClearLink } from '../../actions/ClearLink'
|
|
5
|
+
import { View } from '../../structure'
|
|
6
|
+
import { WheelPicker } from '../WheelPicker'
|
|
7
|
+
|
|
8
|
+
const QUARTERS = [
|
|
9
|
+
{ value: 1, label: 'Q1' },
|
|
10
|
+
{ value: 2, label: 'Q2' },
|
|
11
|
+
{ value: 3, label: 'Q3' },
|
|
12
|
+
{ value: 4, label: 'Q4' },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
function getYearOptions(min, max) {
|
|
16
|
+
const minYear = min ? dayjs(min).year() : dayjs().year() - 100
|
|
17
|
+
const maxYear = max ? dayjs(max).year() : dayjs().year() + 100
|
|
18
|
+
const options = []
|
|
19
|
+
for (let y = minYear; y <= maxYear; y++) options.push({ value: y, label: String(y) })
|
|
20
|
+
return options
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function QuarterWheelPicker({ value, onChange, min, max, allowClear }) {
|
|
24
|
+
const current = value ? dayjs(value) : dayjs()
|
|
25
|
+
const quarter = Math.floor(current.month() / 3) + 1
|
|
26
|
+
const year = current.year()
|
|
27
|
+
|
|
28
|
+
const yearOptions = useMemo(() => getYearOptions(min, max), [min, max])
|
|
29
|
+
|
|
30
|
+
const handleQuarterChange = (q) => {
|
|
31
|
+
onChange?.(
|
|
32
|
+
dayjs()
|
|
33
|
+
.year(year)
|
|
34
|
+
.month((q - 1) * 3)
|
|
35
|
+
.startOf('month')
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const handleYearChange = (y) => {
|
|
40
|
+
onChange?.(
|
|
41
|
+
dayjs()
|
|
42
|
+
.year(y)
|
|
43
|
+
.month((quarter - 1) * 3)
|
|
44
|
+
.startOf('month')
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View>
|
|
50
|
+
<View row>
|
|
51
|
+
<View flex>
|
|
52
|
+
<WheelPicker options={QUARTERS} value={quarter} onChange={handleQuarterChange} />
|
|
53
|
+
</View>
|
|
54
|
+
<View flex>
|
|
55
|
+
<WheelPicker options={yearOptions} value={year} onChange={handleYearChange} />
|
|
56
|
+
</View>
|
|
57
|
+
</View>
|
|
58
|
+
<ClearLink hide={!allowClear} value={value} onChange={onChange} />
|
|
59
|
+
</View>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import dayjs from 'dayjs'
|
|
3
|
+
import weekOfYear from 'dayjs/esm/plugin/weekOfYear'
|
|
4
|
+
|
|
5
|
+
import { ClearLink } from '../../actions/ClearLink'
|
|
6
|
+
import { View } from '../../structure'
|
|
7
|
+
import { WheelPicker } from '../WheelPicker'
|
|
8
|
+
|
|
9
|
+
dayjs.extend(weekOfYear)
|
|
10
|
+
|
|
11
|
+
function getWeekOptions(year) {
|
|
12
|
+
const options = []
|
|
13
|
+
let d = dayjs().year(year).startOf('year').startOf('week')
|
|
14
|
+
|
|
15
|
+
while (d.year() <= year) {
|
|
16
|
+
const weekStart = d
|
|
17
|
+
const weekEnd = d.endOf('week')
|
|
18
|
+
const label = `W${d.week()} ${weekStart.format('MMM D')} - ${weekEnd.format('MMM D')}`
|
|
19
|
+
options.push({ value: d.week(), label, date: weekStart })
|
|
20
|
+
d = d.add(1, 'week')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return options
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getYearOptions(min, max) {
|
|
27
|
+
const minYear = min ? dayjs(min).year() : dayjs().year() - 100
|
|
28
|
+
const maxYear = max ? dayjs(max).year() : dayjs().year() + 100
|
|
29
|
+
const options = []
|
|
30
|
+
for (let y = minYear; y <= maxYear; y++) options.push({ value: y, label: String(y) })
|
|
31
|
+
return options
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function WeekWheelPicker({ value, onChange, min, max, allowClear }) {
|
|
35
|
+
const current = value ? dayjs(value) : dayjs()
|
|
36
|
+
const week = current.week()
|
|
37
|
+
const year = current.year()
|
|
38
|
+
|
|
39
|
+
const weekOptions = useMemo(() => getWeekOptions(year), [year])
|
|
40
|
+
const yearOptions = useMemo(() => getYearOptions(min, max), [min, max])
|
|
41
|
+
|
|
42
|
+
const handleWeekChange = (w) => {
|
|
43
|
+
const opt = weekOptions.find((o) => o.value === w)
|
|
44
|
+
if (opt) onChange?.(opt.date.startOf('week'))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const handleYearChange = (y) => {
|
|
48
|
+
const newWeeks = getWeekOptions(y)
|
|
49
|
+
const closest = newWeeks.find((o) => o.value === week) || newWeeks[0]
|
|
50
|
+
onChange?.(closest.date.startOf('week'))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View>
|
|
55
|
+
<View row>
|
|
56
|
+
<View flex>
|
|
57
|
+
<WheelPicker options={weekOptions} value={week} onChange={handleWeekChange} />
|
|
58
|
+
</View>
|
|
59
|
+
<View width={120}>
|
|
60
|
+
<WheelPicker options={yearOptions} value={year} onChange={handleYearChange} />
|
|
61
|
+
</View>
|
|
62
|
+
</View>
|
|
63
|
+
<ClearLink hide={!allowClear} value={value} onChange={onChange} />
|
|
64
|
+
</View>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { View } from '../../structure'
|
|
2
|
+
import { DayWheelPicker } from './DayWheelPicker'
|
|
3
|
+
|
|
4
|
+
let DatePickerDate, DatePickerMonth
|
|
5
|
+
try {
|
|
6
|
+
const dp = require('@quidone/react-native-wheel-picker').DatePicker
|
|
7
|
+
DatePickerDate = dp.Date
|
|
8
|
+
DatePickerMonth = dp.Month
|
|
9
|
+
} catch {
|
|
10
|
+
DatePickerDate = null
|
|
11
|
+
DatePickerMonth = null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const renderHiddenDate = () => (
|
|
15
|
+
<View width={0} hiddenOverflow pointerEvents="none">
|
|
16
|
+
{DatePickerDate ? <DatePickerDate /> : null}
|
|
17
|
+
</View>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const renderHiddenMonth = () => (
|
|
21
|
+
<View width={0} hiddenOverflow pointerEvents="none">
|
|
22
|
+
{DatePickerMonth ? <DatePickerMonth /> : null}
|
|
23
|
+
</View>
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export function YearWheelPicker({ value, ...props }) {
|
|
27
|
+
return (
|
|
28
|
+
<DayWheelPicker
|
|
29
|
+
{...props}
|
|
30
|
+
value={value?.startOf?.('year') ?? value}
|
|
31
|
+
renderDate={renderHiddenDate}
|
|
32
|
+
renderMonth={renderHiddenMonth}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -21,3 +21,7 @@ export * from './SegmentedPicker'
|
|
|
21
21
|
export * from './Editable'
|
|
22
22
|
export * from './upload/Upload'
|
|
23
23
|
export * from './UploadInput'
|
|
24
|
+
export * from './WheelPicker'
|
|
25
|
+
export * from './dateWheelPicker/DateWheelPicker'
|
|
26
|
+
export * from './NumberWheelInput'
|
|
27
|
+
export * from './NumberWheelPicker'
|
|
@@ -5,13 +5,11 @@ import { Icon } from '../../presentation/Icon'
|
|
|
5
5
|
import { Link } from '../../actions/Link'
|
|
6
6
|
import { Text } from '../../text/Text'
|
|
7
7
|
import { View } from '../../structure/View'
|
|
8
|
+
import { compressAssets } from '../../../helpers/compress'
|
|
9
|
+
import { persistFile } from '../../../helpers/files'
|
|
10
|
+
import { pickFromCamera, pickFromLibrary } from '../../../helpers/pickAssets'
|
|
8
11
|
import { useUploadState } from './useUploadState'
|
|
9
12
|
|
|
10
|
-
let ImagePicker
|
|
11
|
-
try {
|
|
12
|
-
ImagePicker = require('expo-image-picker')
|
|
13
|
-
} catch {}
|
|
14
|
-
|
|
15
13
|
let DocumentPicker
|
|
16
14
|
try {
|
|
17
15
|
DocumentPicker = require('expo-document-picker')
|
|
@@ -28,17 +26,6 @@ function needsFilePicker(accept) {
|
|
|
28
26
|
return parts.some((p) => !p.startsWith('image/'))
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
function normalizeImageResult(asset) {
|
|
32
|
-
return {
|
|
33
|
-
uri: asset.uri,
|
|
34
|
-
name: asset.fileName || asset.uri.split('/').pop(),
|
|
35
|
-
type: asset.mimeType || asset.type || 'image/jpeg',
|
|
36
|
-
size: asset.fileSize || asset.filesize,
|
|
37
|
-
width: asset.width,
|
|
38
|
-
height: asset.height,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
29
|
function normalizeDocumentResult(doc) {
|
|
43
30
|
return {
|
|
44
31
|
uri: doc.uri,
|
|
@@ -56,53 +43,74 @@ function getMediaTypes(accept) {
|
|
|
56
43
|
return undefined
|
|
57
44
|
}
|
|
58
45
|
|
|
59
|
-
export function Upload({ children, onChange, onUpload, value: valueProp, accept, multiple = false, maxCount, disabled = false, ...props }) {
|
|
46
|
+
export function Upload({ children, onChange, onUpload, value: valueProp, accept, multiple = false, maxCount, disabled = false, persistTo, saveToLibrary, compress = true, ...props }) {
|
|
60
47
|
const { value, addFiles, remove } = useUploadState({ onUpload, onChange, multiple, maxCount, value: valueProp })
|
|
61
48
|
const [open, setOpen] = useState(false)
|
|
62
49
|
|
|
50
|
+
const commitFiles = useCallback(
|
|
51
|
+
async (assets) => {
|
|
52
|
+
const compressOpts = typeof compress === 'object' ? compress : {}
|
|
53
|
+
const processed = compress !== false ? await compressAssets(assets, compressOpts) : assets
|
|
54
|
+
if (persistTo && !onUpload) {
|
|
55
|
+
addFiles(processed.map((a) => ({ ...a, uri: persistFile(a.uri, persistTo, { name: a.name }) })))
|
|
56
|
+
} else {
|
|
57
|
+
addFiles(processed)
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
[compress, persistTo, onUpload, addFiles]
|
|
61
|
+
)
|
|
62
|
+
|
|
63
63
|
const handlePress = useCallback(() => {
|
|
64
64
|
if (disabled) return
|
|
65
65
|
setOpen(true)
|
|
66
66
|
}, [disabled])
|
|
67
67
|
|
|
68
|
+
// NOTE: keep the drawer OPEN while presenting the native picker, and close it
|
|
69
|
+
// only after the picker returns. The drawer is a RN <Modal>; closing it first
|
|
70
|
+
// makes the picker present from a modal that's mid-dismiss (and then unmounts
|
|
71
|
+
// under the picker) -> iOS present/dismiss collision that freezes the screen on
|
|
72
|
+
// the second open. Closing in `finally` avoids any concurrent modal transition.
|
|
68
73
|
const handleCamera = useCallback(async () => {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}, [multiple, maxCount, accept, addFiles])
|
|
74
|
+
try {
|
|
75
|
+
const result = await pickFromCamera({
|
|
76
|
+
multiple, maxCount, compress, saveToLibrary,
|
|
77
|
+
mediaTypes: getMediaTypes(accept),
|
|
78
|
+
persistTo: !onUpload ? persistTo : undefined,
|
|
79
|
+
})
|
|
80
|
+
if (multiple ? !result.length : !result) return
|
|
81
|
+
addFiles(multiple ? result : [result])
|
|
82
|
+
} finally {
|
|
83
|
+
setOpen(false)
|
|
84
|
+
}
|
|
85
|
+
}, [multiple, maxCount, accept, compress, saveToLibrary, onUpload, persistTo, addFiles])
|
|
81
86
|
|
|
82
87
|
const handleLibrary = useCallback(async () => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}, [multiple, maxCount, accept, addFiles])
|
|
88
|
+
try {
|
|
89
|
+
const result = await pickFromLibrary({
|
|
90
|
+
multiple, maxCount, compress,
|
|
91
|
+
mediaTypes: getMediaTypes(accept),
|
|
92
|
+
persistTo: !onUpload ? persistTo : undefined,
|
|
93
|
+
})
|
|
94
|
+
if (multiple ? !result.length : !result) return
|
|
95
|
+
addFiles(multiple ? result : [result])
|
|
96
|
+
} finally {
|
|
97
|
+
setOpen(false)
|
|
98
|
+
}
|
|
99
|
+
}, [multiple, maxCount, accept, compress, onUpload, persistTo, addFiles])
|
|
95
100
|
|
|
96
101
|
const handleFiles = useCallback(async () => {
|
|
97
|
-
setOpen(false)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
if (!DocumentPicker) return setOpen(false)
|
|
103
|
+
try {
|
|
104
|
+
const result = await DocumentPicker.getDocumentAsync({
|
|
105
|
+
multiple,
|
|
106
|
+
type: accept || '*/*',
|
|
107
|
+
})
|
|
108
|
+
if (result.canceled) return
|
|
109
|
+
commitFiles(result.assets.map(normalizeDocumentResult))
|
|
110
|
+
} finally {
|
|
111
|
+
setOpen(false)
|
|
112
|
+
}
|
|
113
|
+
}, [multiple, accept, commitFiles])
|
|
106
114
|
|
|
107
115
|
const showCamera = acceptsImages(accept)
|
|
108
116
|
const showLibrary = acceptsImages(accept)
|
|
@@ -5,6 +5,13 @@ function toArray(val) {
|
|
|
5
5
|
return Array.isArray(val) ? val : [val]
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// Stable identity for a file item. Newly picked files get an internal `_id`;
|
|
9
|
+
// externally-provided value items (e.g. persisted records) are keyed by their
|
|
10
|
+
// own `id`, so consumers never need to stamp `_id` themselves.
|
|
11
|
+
function keyOf(file) {
|
|
12
|
+
return file?._id ?? file?.id
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export function useUploadState({ onUpload, onChange, multiple, maxCount, value: valueProp }) {
|
|
9
16
|
const [inFlight, setInFlight] = useState([])
|
|
10
17
|
const [committed, setCommitted] = useState(null)
|
|
@@ -125,12 +132,13 @@ export function useUploadState({ onUpload, onChange, multiple, maxCount, value:
|
|
|
125
132
|
const remove = useCallback(
|
|
126
133
|
(file) => {
|
|
127
134
|
if (!file) return
|
|
135
|
+
const key = keyOf(file)
|
|
128
136
|
|
|
129
|
-
setInFlight((prev) => prev.filter((f) => f
|
|
137
|
+
setInFlight((prev) => prev.filter((f) => keyOf(f) !== key))
|
|
130
138
|
|
|
131
139
|
const ext = externalArrayRef.current
|
|
132
|
-
if (ext.some((f) => f
|
|
133
|
-
const next = ext.filter((f) => f
|
|
140
|
+
if (ext.some((f) => keyOf(f) === key)) {
|
|
141
|
+
const next = ext.filter((f) => keyOf(f) !== key)
|
|
134
142
|
const val = multiple ? next : next[0] || null
|
|
135
143
|
if (!isControlled) setCommitted(val)
|
|
136
144
|
onChangeRef.current?.(val)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { BottomDrawer } from '../modals/bottomDrawer'
|
|
4
|
+
import { LinkInput } from '../inputs/LinkInput'
|
|
5
|
+
import { NumberInput } from '../inputs'
|
|
6
|
+
import { View } from '../structure'
|
|
7
|
+
import { WheelPicker } from '../inputs/WheelPicker'
|
|
8
|
+
import { useResponsiveValue } from '../../responsive'
|
|
9
|
+
|
|
10
|
+
const USE_BOTTOM_DRAWER = { native: true, sm: true, md: true }
|
|
11
|
+
|
|
12
|
+
function formatFeetInches(value) {
|
|
13
|
+
const { feet = 0, inches = 0 } = value || {}
|
|
14
|
+
if (!value || (!feet && !inches)) return ''
|
|
15
|
+
return `${feet}'${inches}"`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function FeetInchesInline({ value, onChange, label, ...props }) {
|
|
19
|
+
const { feet, inches } = value || {}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<View row gap="sm" toBottom>
|
|
23
|
+
<NumberInput
|
|
24
|
+
value={feet}
|
|
25
|
+
onChange={(newFeet) => onChange({ feet: newFeet, inches })}
|
|
26
|
+
min={0}
|
|
27
|
+
precision={0}
|
|
28
|
+
flex
|
|
29
|
+
label={label}
|
|
30
|
+
{...props}
|
|
31
|
+
suffix="ft"
|
|
32
|
+
/>
|
|
33
|
+
<NumberInput
|
|
34
|
+
value={inches}
|
|
35
|
+
onChange={(newInches) => onChange({ feet, inches: newInches })}
|
|
36
|
+
min={0}
|
|
37
|
+
max={11}
|
|
38
|
+
precision={0}
|
|
39
|
+
flex
|
|
40
|
+
{...props}
|
|
41
|
+
suffix="in"
|
|
42
|
+
/>
|
|
43
|
+
</View>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const FEET_OPTIONS = Array.from({ length: 9 }, (_, i) => ({ label: i, value: i }))
|
|
48
|
+
const INCH_OPTIONS = Array.from({ length: 12 }, (_, i) => ({ label: i, value: i }))
|
|
49
|
+
|
|
50
|
+
function FeetInchesDrawer({ value, onChange, label, ...props }) {
|
|
51
|
+
const [open, setOpen] = React.useState(false)
|
|
52
|
+
const { feet = 0, inches = 0 } = value || {}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<LinkInput
|
|
57
|
+
value={formatFeetInches(value)}
|
|
58
|
+
placeholder={label || props.placeholder}
|
|
59
|
+
onPress={() => setOpen(true)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
<BottomDrawer open={open} onClose={() => setOpen(false)} snapPoints={[350]}>
|
|
63
|
+
<View row gap="sm" center flex padding="md">
|
|
64
|
+
<View flex>
|
|
65
|
+
<WheelPicker
|
|
66
|
+
options={FEET_OPTIONS}
|
|
67
|
+
value={feet}
|
|
68
|
+
suffix="ft"
|
|
69
|
+
onChange={(f) => onChange({ feet: f, inches })}
|
|
70
|
+
/>
|
|
71
|
+
</View>
|
|
72
|
+
<View flex>
|
|
73
|
+
<WheelPicker
|
|
74
|
+
options={INCH_OPTIONS}
|
|
75
|
+
value={inches}
|
|
76
|
+
suffix="in"
|
|
77
|
+
onChange={(i) => onChange({ feet, inches: i })}
|
|
78
|
+
/>
|
|
79
|
+
</View>
|
|
80
|
+
</View>
|
|
81
|
+
</BottomDrawer>
|
|
82
|
+
</>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function FeetInchesInput(props) {
|
|
87
|
+
const shouldUseDrawer = useResponsiveValue(USE_BOTTOM_DRAWER)
|
|
88
|
+
|
|
89
|
+
if (shouldUseDrawer) return <FeetInchesDrawer {...props} />
|
|
90
|
+
return <FeetInchesInline {...props} />
|
|
91
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NumberInput } from '../inputs'
|
|
2
|
+
import { fixedDecimals } from '../../helpers/numbers'
|
|
3
|
+
import { LENGTH_CONVERTERS, LENGTH_IMPERIAL_DEFAULTS } from './helpers/length'
|
|
4
|
+
import { useIsImperial } from './MeasurementHandler'
|
|
5
|
+
import { useLocalInputValue } from './useLocalInputValue'
|
|
6
|
+
import { FeetInchesInput } from './FeetInchesInput'
|
|
7
|
+
|
|
8
|
+
export function LengthInput({
|
|
9
|
+
value,
|
|
10
|
+
onChange,
|
|
11
|
+
measurementSystem,
|
|
12
|
+
metricPrecision = 'cm',
|
|
13
|
+
imperialPrecision,
|
|
14
|
+
...props
|
|
15
|
+
}) {
|
|
16
|
+
const isImperial = useIsImperial(measurementSystem)
|
|
17
|
+
const impPrec = imperialPrecision || LENGTH_IMPERIAL_DEFAULTS[metricPrecision] || 'ft+in'
|
|
18
|
+
const converter = isImperial ? LENGTH_CONVERTERS[metricPrecision]?.[impPrec] : null
|
|
19
|
+
const isFtIn = isImperial && impPrec === 'ft+in' && !!converter
|
|
20
|
+
|
|
21
|
+
const Input = isFtIn ? FeetInchesInput : NumberInput
|
|
22
|
+
const suffix = isFtIn ? false : converter ? impPrec : metricPrecision
|
|
23
|
+
|
|
24
|
+
let formattedValue
|
|
25
|
+
if (isFtIn) formattedValue = converter.to(value)
|
|
26
|
+
else if (converter) formattedValue = fixedDecimals(converter.to(value))
|
|
27
|
+
else formattedValue = fixedDecimals(value)
|
|
28
|
+
|
|
29
|
+
const [localValue, handleChange] = useLocalInputValue({ value, formattedValue, suffix, onChange, converter })
|
|
30
|
+
|
|
31
|
+
return <Input {...props} value={localValue} onChange={handleChange} suffix={suffix} />
|
|
32
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Text } from '../text'
|
|
2
|
+
import { useLengthFormatter } from './useLengthFormatter'
|
|
3
|
+
|
|
4
|
+
export function LengthText({ value, measurementSystem, metricPrecision, imperialPrecision, withoutSuffix, ...props }) {
|
|
5
|
+
const format = useLengthFormatter({ measurementSystem, metricPrecision, imperialPrecision, withoutSuffix })
|
|
6
|
+
|
|
7
|
+
if (!value && value !== 0) return false
|
|
8
|
+
|
|
9
|
+
return <Text {...props}>{format(value)}</Text>
|
|
10
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { detectMeasurementSystem } from './helpers/detectMeasurementSystem'
|
|
4
|
+
|
|
5
|
+
const MeasurementContext = React.createContext(null)
|
|
6
|
+
|
|
7
|
+
export function MeasurementHandler({ children, measurementSystem }) {
|
|
8
|
+
const value = React.useMemo(() => ({ measurementSystem }), [measurementSystem])
|
|
9
|
+
|
|
10
|
+
return <MeasurementContext.Provider value={value}>{children}</MeasurementContext.Provider>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useMeasurementSystem(override) {
|
|
14
|
+
const ctx = React.useContext(MeasurementContext)
|
|
15
|
+
|
|
16
|
+
return React.useMemo(() => {
|
|
17
|
+
if (override) return override
|
|
18
|
+
if (ctx?.measurementSystem) return ctx.measurementSystem
|
|
19
|
+
return detectMeasurementSystem()
|
|
20
|
+
}, [override, ctx?.measurementSystem])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useIsImperial(override) {
|
|
24
|
+
const system = useMeasurementSystem(override)
|
|
25
|
+
return system === 'imperial'
|
|
26
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NumberInput } from '../inputs'
|
|
2
|
+
import { fixedDecimals } from '../../helpers/numbers'
|
|
3
|
+
import { WEIGHT_CONVERTERS, WEIGHT_IMPERIAL_DEFAULTS } from './helpers/weight'
|
|
4
|
+
import { useIsImperial } from './MeasurementHandler'
|
|
5
|
+
import { useLocalInputValue } from './useLocalInputValue'
|
|
6
|
+
|
|
7
|
+
export function WeightInput({
|
|
8
|
+
value,
|
|
9
|
+
onChange,
|
|
10
|
+
measurementSystem,
|
|
11
|
+
metricPrecision = 'kg',
|
|
12
|
+
imperialPrecision,
|
|
13
|
+
...props
|
|
14
|
+
}) {
|
|
15
|
+
const isImperial = useIsImperial(measurementSystem)
|
|
16
|
+
const impPrec = imperialPrecision || WEIGHT_IMPERIAL_DEFAULTS[metricPrecision] || 'lbs'
|
|
17
|
+
const converter = isImperial ? WEIGHT_CONVERTERS[metricPrecision]?.[impPrec] : null
|
|
18
|
+
|
|
19
|
+
const suffix = converter ? impPrec : metricPrecision
|
|
20
|
+
const formattedValue = converter ? fixedDecimals(converter.to(value)) : fixedDecimals(value)
|
|
21
|
+
|
|
22
|
+
const [localValue, handleChange] = useLocalInputValue({ value, formattedValue, suffix, onChange, converter })
|
|
23
|
+
|
|
24
|
+
return <NumberInput {...props} value={localValue} onChange={handleChange} suffix={suffix} />
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Text } from '../text'
|
|
2
|
+
import { useWeightFormatter } from './useWeightFormatter'
|
|
3
|
+
|
|
4
|
+
export function WeightText({ value, measurementSystem, metricPrecision, imperialPrecision, withoutSuffix, ...props }) {
|
|
5
|
+
const format = useWeightFormatter({ measurementSystem, metricPrecision, imperialPrecision, withoutSuffix })
|
|
6
|
+
|
|
7
|
+
if (!value && value !== 0) return false
|
|
8
|
+
|
|
9
|
+
return <Text {...props}>{format(value)}</Text>
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function detectMeasurementSystem() {
|
|
2
|
+
try {
|
|
3
|
+
const locale = new Intl.Locale(navigator.language)
|
|
4
|
+
const info = locale.textInfo ?? locale.getTextInfo?.()
|
|
5
|
+
const ms = locale.measurementSystem ?? info?.measurementSystem
|
|
6
|
+
if (ms) return ['us', 'imperial'].includes(ms) ? 'imperial' : 'metric'
|
|
7
|
+
|
|
8
|
+
// Fallback: only US, Myanmar, Liberia use imperial
|
|
9
|
+
const lang = navigator.language || ''
|
|
10
|
+
if (lang.startsWith('en-US') || lang.startsWith('my-MM') || lang.startsWith('en-LR')) {
|
|
11
|
+
return 'imperial'
|
|
12
|
+
}
|
|
13
|
+
} catch {}
|
|
14
|
+
return 'metric'
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function detectMeasurementSystem() {
|
|
2
|
+
try {
|
|
3
|
+
const Localization = require('expo-localization')
|
|
4
|
+
const locales = Localization.getLocales()
|
|
5
|
+
const ms = locales?.[0]?.measurementSystem
|
|
6
|
+
if (ms) return ['us', 'imperial'].includes(ms) ? 'imperial' : 'metric'
|
|
7
|
+
} catch {}
|
|
8
|
+
return 'metric'
|
|
9
|
+
}
|