@neko-os/ui 0.5.4 → 0.6.1
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/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/index.js +3 -1
- package/dist/components/inputs/DateInput.js +10 -6
- package/dist/components/inputs/InputWrapper.js +2 -3
- package/dist/components/inputs/NumberWheelInput.js +50 -0
- package/dist/components/inputs/NumberWheelPicker.js +43 -0
- 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/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/responsive/responsiveHooks.js +17 -0
- package/package.json +2 -14
- package/src/NekoUI.js +16 -13
- 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/index.js +2 -0
- package/src/components/inputs/DateInput.js +8 -4
- package/src/components/inputs/InputWrapper.js +1 -2
- package/src/components/inputs/NumberWheelInput.js +50 -0
- package/src/components/inputs/NumberWheelPicker.js +43 -0
- 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/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/responsive/responsiveHooks.js +17 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { WEIGHT_CONVERTERS, WEIGHT_IMPERIAL_DEFAULTS } from './helpers/weight'
|
|
2
|
+
import { fixedDecimals } from '../../helpers/numbers'
|
|
3
|
+
import { useIsImperial } from './MeasurementHandler'
|
|
4
|
+
|
|
5
|
+
export function useWeightFormatter({
|
|
6
|
+
measurementSystem,
|
|
7
|
+
metricPrecision = 'kg',
|
|
8
|
+
imperialPrecision,
|
|
9
|
+
withoutSuffix,
|
|
10
|
+
} = {}) {
|
|
11
|
+
const isImperial = useIsImperial(measurementSystem)
|
|
12
|
+
const impPrec = imperialPrecision || WEIGHT_IMPERIAL_DEFAULTS[metricPrecision] || 'lbs'
|
|
13
|
+
|
|
14
|
+
return (value) => {
|
|
15
|
+
if (!value && value !== 0) return null
|
|
16
|
+
|
|
17
|
+
if (isImperial) {
|
|
18
|
+
const converter = WEIGHT_CONVERTERS[metricPrecision]?.[impPrec]
|
|
19
|
+
if (!converter) return `${fixedDecimals(value)} ${metricPrecision}`
|
|
20
|
+
|
|
21
|
+
const converted = converter.to(value)
|
|
22
|
+
if (withoutSuffix) return fixedDecimals(converted)
|
|
23
|
+
return `${fixedDecimals(converted)} ${impPrec}`
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (withoutSuffix) return fixedDecimals(value)
|
|
27
|
+
return `${fixedDecimals(value)} ${metricPrecision}`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Button } from '../actions'
|
|
2
|
+
|
|
3
|
+
// Plain ReactJS (Vite/Next/CRA): react-router goBack via navigate(-1). require() in try/catch so a
|
|
4
|
+
// web app without react-router-dom degrades gracefully instead of failing the module load.
|
|
5
|
+
let useNavigate
|
|
6
|
+
try {
|
|
7
|
+
useNavigate = require('react-router-dom').useNavigate
|
|
8
|
+
} catch {
|
|
9
|
+
useNavigate = () => () => console.warn('ReturnButton: react-router-dom not installed.')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A back/close Button. Defaults to a left-arrow icon that calls navigate(-1). `close` swaps to a
|
|
13
|
+
// close icon; `icon` overrides the icon name; `onPress` overrides goBack. Extra props pass to the
|
|
14
|
+
// Button (label, outline, size, color, etc.).
|
|
15
|
+
export function ReturnButton({ icon, close, onPress, ...props }) {
|
|
16
|
+
const navigate = useNavigate()
|
|
17
|
+
const name = icon || (close ? 'close-line' : 'arrow-left-s-line')
|
|
18
|
+
|
|
19
|
+
return <Button icon={name} onPress={onPress || (() => navigate(-1))} {...props} />
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Button } from '../actions'
|
|
2
|
+
|
|
3
|
+
// react-navigation goBack. require() in try/catch so a native app without react-navigation degrades
|
|
4
|
+
// gracefully instead of failing the module load.
|
|
5
|
+
let useNavigation
|
|
6
|
+
try {
|
|
7
|
+
useNavigation = require('@react-navigation/native').useNavigation
|
|
8
|
+
} catch {
|
|
9
|
+
useNavigation = () => ({ goBack: () => console.warn('ReturnButton: @react-navigation/native not installed.') })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// A back/close Button. Defaults to a left-arrow icon that calls navigation.goBack(). `close` swaps to
|
|
13
|
+
// a close icon; `icon` overrides the icon name; `onPress` overrides goBack. Extra props pass to the
|
|
14
|
+
// Button (label, outline, size, color, etc.).
|
|
15
|
+
export function ReturnButton({ icon, close, onPress, ...props }) {
|
|
16
|
+
const navigation = useNavigation()
|
|
17
|
+
const name = icon || (close ? 'close-line' : 'arrow-left-s-line')
|
|
18
|
+
|
|
19
|
+
return <Button icon={name} onPress={onPress || (() => navigation.goBack())} {...props} />
|
|
20
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Icon } from '../presentation'
|
|
2
|
+
import { Link } from '../actions'
|
|
3
|
+
|
|
4
|
+
// Plain ReactJS (Vite/Next/CRA): react-router goBack via navigate(-1). require() in try/catch so a
|
|
5
|
+
// web app without react-router-dom degrades gracefully instead of failing the module load.
|
|
6
|
+
let useNavigate
|
|
7
|
+
try {
|
|
8
|
+
useNavigate = require('react-router-dom').useNavigate
|
|
9
|
+
} catch {
|
|
10
|
+
useNavigate = () => () => console.warn('ReturnLink: react-router-dom not installed.')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// A back/close Link: a Link wrapping an Icon. Defaults to a left-arrow that calls navigate(-1).
|
|
14
|
+
// `close` swaps to a close icon; `icon` overrides the icon name entirely; `onPress` overrides goBack.
|
|
15
|
+
// Extra props pass to the Icon (size, color, etc.).
|
|
16
|
+
export function ReturnLink({ icon, close, onPress, ...props }) {
|
|
17
|
+
const navigate = useNavigate()
|
|
18
|
+
const name = icon || (close ? 'close-line' : 'arrow-left-s-line')
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Link onPress={onPress || (() => navigate(-1))}>
|
|
22
|
+
<Icon name={name} {...props} />
|
|
23
|
+
</Link>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Icon } from '../presentation'
|
|
2
|
+
import { Link } from '../actions'
|
|
3
|
+
|
|
4
|
+
// react-navigation goBack. require() in try/catch so a native app without react-navigation degrades
|
|
5
|
+
// gracefully instead of failing the module load.
|
|
6
|
+
let useNavigation
|
|
7
|
+
try {
|
|
8
|
+
useNavigation = require('@react-navigation/native').useNavigation
|
|
9
|
+
} catch {
|
|
10
|
+
useNavigation = () => ({ goBack: () => console.warn('ReturnLink: @react-navigation/native not installed.') })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// A back/close Link: a Link wrapping an Icon. Defaults to a left-arrow that calls navigation.goBack().
|
|
14
|
+
// `close` swaps to a close icon; `icon` overrides the icon name entirely; `onPress` overrides goBack.
|
|
15
|
+
// Extra props pass to the Icon (size, color, etc.).
|
|
16
|
+
export function ReturnLink({ icon, close, onPress, ...props }) {
|
|
17
|
+
const navigation = useNavigation()
|
|
18
|
+
const name = icon || (close ? 'close-line' : 'arrow-left-s-line')
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Link onPress={onPress || (() => navigation.goBack())}>
|
|
22
|
+
<Icon name={name} {...props} />
|
|
23
|
+
</Link>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ActiveStepContent } from '../steps'
|
|
2
|
+
import { View } from '../structure'
|
|
3
|
+
|
|
4
|
+
// Plain ReactJS (Vite/Next/CRA) fallback — no react-navigation. Renders the active step in place,
|
|
5
|
+
// driven by StepsHandler's activeIndex, so a wizard built with these pieces still works on plain web
|
|
6
|
+
// (just without native stack transitions). React Native / RNW use the .native variant.
|
|
7
|
+
let warned = false
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line no-unused-vars -- absorb the native-only screenOptions so it doesn't leak onto View
|
|
10
|
+
export function RoutedStepsContent({ screenOptions, ...props }) {
|
|
11
|
+
if (!warned) {
|
|
12
|
+
warned = true
|
|
13
|
+
console.warn('RoutedStepsContent is native-only (react-navigation). Rendering steps in place on plain web.')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<View flex {...props}>
|
|
18
|
+
<ActiveStepContent />
|
|
19
|
+
</View>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
import { View } from '../structure'
|
|
4
|
+
import { useSteps } from '../steps'
|
|
5
|
+
|
|
6
|
+
// Native-only deps. require() inside try/catch keeps web bundlers from pulling react-navigation into
|
|
7
|
+
// the plain ReactJS build (which uses RoutedStepsContent.js instead).
|
|
8
|
+
let NavigationContainer
|
|
9
|
+
let NavigationIndependentTree
|
|
10
|
+
let StackActions
|
|
11
|
+
let createNativeStackNavigator
|
|
12
|
+
let available = true
|
|
13
|
+
try {
|
|
14
|
+
const nav = require('@react-navigation/native')
|
|
15
|
+
NavigationContainer = nav.NavigationContainer
|
|
16
|
+
NavigationIndependentTree = nav.NavigationIndependentTree
|
|
17
|
+
StackActions = nav.StackActions
|
|
18
|
+
createNativeStackNavigator = require('@react-navigation/native-stack').createNativeStackNavigator
|
|
19
|
+
} catch {
|
|
20
|
+
available = false
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let warned = false
|
|
24
|
+
|
|
25
|
+
// Renders a step's content exactly like ActiveStepContent — raw, no wrapper. Any scroll/padding is
|
|
26
|
+
// the consumer's call inside their own render/renderContent.
|
|
27
|
+
const stepContent = (item) => {
|
|
28
|
+
const Content = item.render || item.renderContent || item.Content
|
|
29
|
+
return Content ? <Content /> : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// The swapping content region — the routed equivalent of ActiveStepContent. Drop it inside any
|
|
33
|
+
// StepsHandler, alongside whatever fixed chrome you want. It adds NO state of its own: StepsHandler
|
|
34
|
+
// stays the single source of truth. This component just renders a self-contained native stack
|
|
35
|
+
// (independent from the host NavigationContainer) and keeps it in sync with activeIndex both ways.
|
|
36
|
+
export function RoutedStepsContent({ screenOptions, ...props }) {
|
|
37
|
+
const { items, activeIndex, moveToIndex } = useSteps()
|
|
38
|
+
const navRef = React.useRef(null)
|
|
39
|
+
const Stack = React.useRef(available ? createNativeStackNavigator() : null).current
|
|
40
|
+
|
|
41
|
+
// StepsHandler -> stack: when activeIndex changes (Next/Back buttons, StepsMenu, custom controls),
|
|
42
|
+
// drive the stack to the matching screen. Compare against the current TOP route (getCurrentRoute)
|
|
43
|
+
// and be explicit about direction: push forward, pop back. Using navigate() here is wrong — it can
|
|
44
|
+
// push a duplicate instead of popping. If the route already matches (e.g. a native gesture back
|
|
45
|
+
// already moved us), this is a no-op.
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
const ref = navRef.current
|
|
48
|
+
if (!ref?.isReady?.()) return
|
|
49
|
+
const current = Number(ref.getCurrentRoute?.()?.name) || 0
|
|
50
|
+
if (activeIndex === current) return
|
|
51
|
+
ref.dispatch(
|
|
52
|
+
activeIndex > current ? StackActions.push(String(activeIndex)) : StackActions.popTo(String(activeIndex))
|
|
53
|
+
)
|
|
54
|
+
}, [activeIndex])
|
|
55
|
+
|
|
56
|
+
if (!available) {
|
|
57
|
+
if (!warned) {
|
|
58
|
+
warned = true
|
|
59
|
+
console.warn(
|
|
60
|
+
'RoutedStepsContent requires @react-navigation/native and @react-navigation/native-stack; neither is installed.'
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// stack -> StepsHandler: native gesture / hardware back changes the route without going through
|
|
67
|
+
// moveToIndex. Push that index back into StepsHandler (backward never validates, matching Steps).
|
|
68
|
+
const handleStateChange = () => {
|
|
69
|
+
const index = Number(navRef.current?.getCurrentRoute?.()?.name) || 0
|
|
70
|
+
if (index !== activeIndex) moveToIndex(index)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const navigator = (
|
|
74
|
+
<Stack.Navigator screenOptions={{ headerShown: false, ...screenOptions }}>
|
|
75
|
+
{items.map((item, index) => (
|
|
76
|
+
<Stack.Screen key={index} name={String(index)} options={{ title: item.label }}>
|
|
77
|
+
{() => stepContent(item)}
|
|
78
|
+
</Stack.Screen>
|
|
79
|
+
))}
|
|
80
|
+
</Stack.Navigator>
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
// Self-contained: its own navigation world, independent of the host app's NavigationContainer, so
|
|
84
|
+
// it never touches the host back stack (react-navigation v7+).
|
|
85
|
+
return (
|
|
86
|
+
<View flex {...props}>
|
|
87
|
+
<NavigationIndependentTree>
|
|
88
|
+
<NavigationContainer ref={navRef} onStateChange={handleStateChange}>
|
|
89
|
+
{navigator}
|
|
90
|
+
</NavigationContainer>
|
|
91
|
+
</NavigationIndependentTree>
|
|
92
|
+
</View>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -24,7 +24,7 @@ export function StatePresenter({
|
|
|
24
24
|
if (error) {
|
|
25
25
|
return (
|
|
26
26
|
<View flex center {...props}>
|
|
27
|
-
<Result type="error" title={errorTitle} description={errorDescription || error.message} />
|
|
27
|
+
<Result type="error" title={errorTitle} description={errorDescription || (typeof error === 'string' ? error : error.message)} />
|
|
28
28
|
</View>
|
|
29
29
|
)
|
|
30
30
|
}
|
|
@@ -8,6 +8,7 @@ export function StepsHandler({ children, items, onSubmit, onValidateStep, onStep
|
|
|
8
8
|
const [maxIndexReleased, setMaxIndexReleased] = React.useState(0)
|
|
9
9
|
const [loading, setLoading] = React.useState(false)
|
|
10
10
|
const activeStep = items[activeIndex]
|
|
11
|
+
const isFirstStep = activeIndex === 0
|
|
11
12
|
const isLastStep = activeIndex === items.length - 1
|
|
12
13
|
|
|
13
14
|
const moveToIndex = async (index) => {
|
|
@@ -44,6 +45,7 @@ export function StepsHandler({ children, items, onSubmit, onValidateStep, onStep
|
|
|
44
45
|
activeStep,
|
|
45
46
|
maxIndexReleased,
|
|
46
47
|
loading,
|
|
48
|
+
isFirstStep,
|
|
47
49
|
isLastStep,
|
|
48
50
|
moveToNextStep,
|
|
49
51
|
moveToPrevStep,
|
|
@@ -4,14 +4,24 @@ import { Text } from '../text'
|
|
|
4
4
|
import { View } from './View'
|
|
5
5
|
import { useDefaultModifier } from '../../modifiers/default'
|
|
6
6
|
import { useResponsiveConverter } from '../../modifiers/responsiveConverter'
|
|
7
|
-
import { useThemeComponentModifier } from '../../modifiers/themeComponent'
|
|
8
7
|
import { useSafeAreaInsets } from '../../abstractions/helpers/useSafeAreaInsets'
|
|
8
|
+
import { useThemeComponentModifier } from '../../modifiers/themeComponent'
|
|
9
9
|
|
|
10
10
|
const DEFAULT_PROPS = {
|
|
11
11
|
borderB: 'overlayDivider',
|
|
12
|
+
titleProps: {
|
|
13
|
+
center: true,
|
|
14
|
+
size: 'h6',
|
|
15
|
+
numberOfLines: 1,
|
|
16
|
+
},
|
|
17
|
+
subtitleProps: {
|
|
18
|
+
center: true,
|
|
19
|
+
size: 'xs',
|
|
20
|
+
numberOfLines: 1,
|
|
21
|
+
},
|
|
12
22
|
}
|
|
13
23
|
|
|
14
|
-
export function TopBar({ right, left, WrapperView, children, ...rootProps }) {
|
|
24
|
+
export function TopBar({ title, subtitle, right, left, WrapperView, children, ...rootProps }) {
|
|
15
25
|
const { top: safeTop } = useSafeAreaInsets()
|
|
16
26
|
|
|
17
27
|
const [_, props] = pipe(
|
|
@@ -19,7 +29,7 @@ export function TopBar({ right, left, WrapperView, children, ...rootProps }) {
|
|
|
19
29
|
useDefaultModifier(DEFAULT_PROPS),
|
|
20
30
|
useResponsiveConverter([])
|
|
21
31
|
)([{}, rootProps])
|
|
22
|
-
let { useSafeArea = true,
|
|
32
|
+
let { useSafeArea = true, titleProps, subtitleProps } = props
|
|
23
33
|
|
|
24
34
|
const hasContent = !!title || !!subtitle || !!children || !!right || !!left
|
|
25
35
|
|
|
@@ -34,17 +44,9 @@ export function TopBar({ right, left, WrapperView, children, ...rootProps }) {
|
|
|
34
44
|
</View>
|
|
35
45
|
|
|
36
46
|
<View center flex={3}>
|
|
37
|
-
{children ||
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
</Text>
|
|
41
|
-
)}
|
|
42
|
-
|
|
43
|
-
{subtitle && (
|
|
44
|
-
<Text center xs numberOfLines={1}>
|
|
45
|
-
{subtitle}
|
|
46
|
-
</Text>
|
|
47
|
-
)}
|
|
47
|
+
{children || <Text {...titleProps}>{title}</Text>}
|
|
48
|
+
|
|
49
|
+
{subtitle && <Text {...subtitleProps}>{subtitle}</Text>}
|
|
48
50
|
</View>
|
|
49
51
|
|
|
50
52
|
<View flex={1} toRight>
|
|
@@ -3,7 +3,7 @@ import { ThemePicker } from './ThemePicker'
|
|
|
3
3
|
|
|
4
4
|
export function ThemePickerDrawer({ open, onClose, onChange }) {
|
|
5
5
|
return (
|
|
6
|
-
<BottomDrawer open={open} onClose={onClose} maxWidth={550} snapPoints={['
|
|
6
|
+
<BottomDrawer open={open} onClose={onClose} maxWidth={550} snapPoints={['60%', '85%']} useSafeArea={false}>
|
|
7
7
|
<DrawerScrollView padding="md">
|
|
8
8
|
<ThemePicker onChange={onChange} />
|
|
9
9
|
</DrawerScrollView>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const IMAGE_DEFAULTS = { maxWidth: 1920, maxHeight: 1920, quality: 0.8 }
|
|
2
|
+
|
|
3
|
+
function isImage(asset) {
|
|
4
|
+
return asset?.type?.startsWith('image/')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function loadImage(src) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const img = new Image()
|
|
10
|
+
img.onload = () => resolve(img)
|
|
11
|
+
img.onerror = reject
|
|
12
|
+
img.src = src
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const ALPHA_TYPES = new Set(['image/png', 'image/webp', 'image/gif'])
|
|
17
|
+
|
|
18
|
+
function compressWithCanvas(img, { maxWidth, maxHeight, quality, mimeType }) {
|
|
19
|
+
let { width, height } = img
|
|
20
|
+
if (width > maxWidth || height > maxHeight) {
|
|
21
|
+
const ratio = Math.min(maxWidth / width, maxHeight / height)
|
|
22
|
+
width = Math.round(width * ratio)
|
|
23
|
+
height = Math.round(height * ratio)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const canvas = document.createElement('canvas')
|
|
27
|
+
canvas.width = width
|
|
28
|
+
canvas.height = height
|
|
29
|
+
const ctx = canvas.getContext('2d')
|
|
30
|
+
ctx.drawImage(img, 0, 0, width, height)
|
|
31
|
+
|
|
32
|
+
const outputType = ALPHA_TYPES.has(mimeType) ? mimeType : 'image/jpeg'
|
|
33
|
+
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
canvas.toBlob(
|
|
36
|
+
(blob) => resolve(blob ? { uri: URL.createObjectURL(blob), width, height } : null),
|
|
37
|
+
outputType,
|
|
38
|
+
quality
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function compressAsset(asset, options = {}) {
|
|
44
|
+
if (!asset?.uri || !isImage(asset)) return asset
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const opts = { ...IMAGE_DEFAULTS, ...options.image }
|
|
48
|
+
const img = await loadImage(asset.uri)
|
|
49
|
+
const result = await compressWithCanvas(img, { ...opts, mimeType: asset.type })
|
|
50
|
+
if (!result) return asset
|
|
51
|
+
return { ...asset, uri: result.uri, width: result.width, height: result.height }
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.warn('[neko-ui compress] web image compression failed:', e?.message)
|
|
54
|
+
return asset
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function compressAssets(assets, options = {}) {
|
|
59
|
+
if (!assets?.length) return Promise.resolve(assets || [])
|
|
60
|
+
return Promise.all(assets.map((a) => compressAsset(a, options)))
|
|
61
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
let ImageCompressor
|
|
2
|
+
let VideoCompressor
|
|
3
|
+
try {
|
|
4
|
+
const RNC = require('react-native-compressor')
|
|
5
|
+
ImageCompressor = RNC.Image
|
|
6
|
+
VideoCompressor = RNC.Video
|
|
7
|
+
} catch {}
|
|
8
|
+
|
|
9
|
+
const IMAGE_DEFAULTS = { maxWidth: 1920, maxHeight: 1920, quality: 0.8 }
|
|
10
|
+
const VIDEO_DEFAULTS = { maxSize: 720 }
|
|
11
|
+
|
|
12
|
+
function isImage(asset) {
|
|
13
|
+
return asset?.type?.startsWith('image/')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isVideo(asset) {
|
|
17
|
+
return asset?.type?.startsWith('video/')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function compressAsset(asset, options = {}) {
|
|
21
|
+
if (!asset?.uri) return asset
|
|
22
|
+
|
|
23
|
+
if (isImage(asset) && ImageCompressor) {
|
|
24
|
+
try {
|
|
25
|
+
const uri = await ImageCompressor.compress(asset.uri, { ...IMAGE_DEFAULTS, ...options.image })
|
|
26
|
+
return { ...asset, uri }
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.warn('[neko-ui compress] image failed, keeping original:', e?.message)
|
|
29
|
+
return asset
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (isVideo(asset) && VideoCompressor) {
|
|
34
|
+
try {
|
|
35
|
+
const uri = await VideoCompressor.compress(asset.uri, { ...VIDEO_DEFAULTS, ...options.video })
|
|
36
|
+
return { ...asset, uri }
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.warn('[neko-ui compress] video failed, keeping original:', e?.message)
|
|
39
|
+
return asset
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return asset
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function compressAssets(assets, options = {}) {
|
|
47
|
+
if (!assets?.length) return Promise.resolve(assets || [])
|
|
48
|
+
return Promise.all(assets.map((a) => compressAsset(a, options)))
|
|
49
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
let FS
|
|
2
|
+
try {
|
|
3
|
+
FS = require('expo-file-system')
|
|
4
|
+
} catch {}
|
|
5
|
+
|
|
6
|
+
// `target="<base>/<subdir>"`, base is 'document' | 'cache'. Resolves to a
|
|
7
|
+
// permanent Directory (created if missing) via the modern sync expo-file-system API.
|
|
8
|
+
function resolveDir(target) {
|
|
9
|
+
const [base, ...rest] = (target || '').split('/').filter(Boolean)
|
|
10
|
+
if (base !== 'cache' && base !== 'document') {
|
|
11
|
+
console.warn(`[neko-ui files] target base "${base}" unknown — using document dir. Use "document" or "cache".`)
|
|
12
|
+
}
|
|
13
|
+
const root = base === 'cache' ? FS.Paths.cache : FS.Paths.document
|
|
14
|
+
const dir = new FS.Directory(root, ...rest)
|
|
15
|
+
try {
|
|
16
|
+
dir.create({ intermediates: true, idempotent: true })
|
|
17
|
+
} catch (e) {
|
|
18
|
+
console.warn('[neko-ui files] dir create failed:', e?.message)
|
|
19
|
+
}
|
|
20
|
+
return dir
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let seq = 0
|
|
24
|
+
|
|
25
|
+
// Copy a file at `uri` into a permanent app directory (`target`, default
|
|
26
|
+
// 'document/files'). Returns the durable uri string. Never throws — on failure
|
|
27
|
+
// or when expo-file-system is unavailable, returns the original uri.
|
|
28
|
+
// `opts.name` is used only to derive the file extension (falls back to the uri,
|
|
29
|
+
// then 'jpg'); the stored filename is always unique (timestamp + counter), so
|
|
30
|
+
// rapid multi-select can't collide. Expects a `file://` source (e.g.
|
|
31
|
+
// expo-image-picker output); `content://` sources can't be copied and fall back
|
|
32
|
+
// to the original uri.
|
|
33
|
+
export function persistFile(uri, target = 'document/files', { name } = {}) {
|
|
34
|
+
if (!FS || !uri) return uri
|
|
35
|
+
try {
|
|
36
|
+
const ext = name?.split('.').pop() || uri.split('?')[0].split('.').pop() || 'jpg'
|
|
37
|
+
const filename = `${Date.now()}_${seq++}_${Math.round(Math.random() * 1e6)}.${ext}`
|
|
38
|
+
const dest = new FS.File(resolveDir(target), filename)
|
|
39
|
+
new FS.File(uri).copy(dest)
|
|
40
|
+
return dest.uri
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.warn('[neko-ui files] persistFile failed, keeping uri:', e?.message)
|
|
43
|
+
return uri
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Delete a persisted file. No-op if missing / unavailable.
|
|
48
|
+
export function removeFile(uri) {
|
|
49
|
+
if (!FS || !uri) return
|
|
50
|
+
try {
|
|
51
|
+
new FS.File(uri).delete()
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.warn('[neko-ui files] removeFile failed:', e?.message)
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/helpers/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
export * from './compress'
|
|
1
2
|
export * from './debounce'
|
|
2
|
-
export * from './
|
|
3
|
+
export * from './files'
|
|
4
|
+
export * from './media'
|
|
5
|
+
export * from './numbers'
|
|
6
|
+
export * from './pickAssets'
|
|
3
7
|
export * from './random'
|
|
4
8
|
export * from './storage'
|
|
9
|
+
export * from './string'
|
|
5
10
|
export * from './weekStart'
|
|
6
11
|
export * from './weekStartSetup'
|
|
7
12
|
export * from './../abstractions/helpers/useSafeAreaInsets'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
let ImagePicker
|
|
2
|
+
try {
|
|
3
|
+
ImagePicker = require('expo-image-picker')
|
|
4
|
+
} catch {}
|
|
5
|
+
|
|
6
|
+
// Normalize an expo-image-picker asset into the shape neko-ui works with.
|
|
7
|
+
// Internal — callers consume the already-normalized assets from openCamera/openLibrary.
|
|
8
|
+
function normalizeImageResult(asset) {
|
|
9
|
+
return {
|
|
10
|
+
uri: asset.uri,
|
|
11
|
+
name: asset.fileName || asset.uri.split('/').pop(),
|
|
12
|
+
type: asset.mimeType || asset.type || 'image/jpeg',
|
|
13
|
+
size: asset.fileSize || asset.filesize,
|
|
14
|
+
width: asset.width,
|
|
15
|
+
height: asset.height,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Request camera permission, launch the camera, return normalized assets.
|
|
20
|
+
// Returns [] when expo-image-picker is missing, permission is denied, or the user
|
|
21
|
+
// cancels. The caller owns post-processing (persist, save-to-library, closing any
|
|
22
|
+
// drawer) — and any drawer should stay open until this resolves (iOS present/
|
|
23
|
+
// dismiss collision otherwise).
|
|
24
|
+
export async function openCamera(options = {}) {
|
|
25
|
+
if (!ImagePicker) return []
|
|
26
|
+
const permission = await ImagePicker.requestCameraPermissionsAsync()
|
|
27
|
+
if (!permission.granted) return []
|
|
28
|
+
const result = await ImagePicker.launchCameraAsync(options)
|
|
29
|
+
if (result.canceled) return []
|
|
30
|
+
return result.assets.map(normalizeImageResult)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Same as openCamera but for the photo library.
|
|
34
|
+
export async function openLibrary(options = {}) {
|
|
35
|
+
if (!ImagePicker) return []
|
|
36
|
+
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync()
|
|
37
|
+
if (!permission.granted) return []
|
|
38
|
+
const result = await ImagePicker.launchImageLibraryAsync(options)
|
|
39
|
+
if (result.canceled) return []
|
|
40
|
+
return result.assets.map(normalizeImageResult)
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { is } from 'ramda'
|
|
2
|
+
|
|
3
|
+
export function fixedDecimals(num, count = 2) {
|
|
4
|
+
if (!num) return num
|
|
5
|
+
if (Number.isInteger(num)) return num
|
|
6
|
+
if (is(String, num)) num = parseFloat(num)
|
|
7
|
+
|
|
8
|
+
const decimalPart = num.toString().split('.')[1]
|
|
9
|
+
if (decimalPart && decimalPart.length > count) {
|
|
10
|
+
return parseFloat(num.toFixed(count))
|
|
11
|
+
}
|
|
12
|
+
return num
|
|
13
|
+
}
|