@neko-os/ui 0.0.13 → 0.1.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/abstractions/KeyboardAvoidingView.js +1 -0
- package/dist/abstractions/KeyboardAvoidingView.native.js +1 -0
- package/dist/components/actions/ActionsDrawer.js +1 -0
- package/dist/components/actions/Button.js +1 -1
- package/dist/components/actions/FloatingMenu.js +1 -0
- package/dist/components/actions/index.js +1 -1
- package/dist/components/animations/AnimatedTopBar.js +1 -0
- package/dist/components/animations/AnimatedTopBar.native.js +1 -0
- package/dist/components/animations/AnimatedTopBar.web.js +1 -0
- package/dist/components/animations/ParallaxHeader.js +1 -0
- package/dist/components/animations/ParallaxHeader.native.js +1 -0
- package/dist/components/animations/ParallaxHeader.web.js +1 -0
- package/dist/components/animations/ReanimatedScrollHandler.js +1 -0
- package/dist/components/animations/ReanimatedScrollHandler.native.js +1 -0
- package/dist/components/animations/ReanimatedScrollHandler.web.js +1 -0
- package/dist/components/animations/index.js +1 -1
- package/dist/components/form/FormItem.js +1 -1
- package/dist/components/form/FormList.js +1 -1
- package/dist/components/form/SubmitButton.js +1 -1
- package/dist/components/form/index.js +1 -1
- package/dist/components/form/useNewForm.js +1 -1
- package/dist/components/form/validation/defaultMessages.js +1 -0
- package/dist/components/form/validation/index.js +1 -0
- package/dist/components/form/validation/normalizeRules.js +1 -0
- package/dist/components/form/validation/shouldValidateOn.js +1 -0
- package/dist/components/form/validation/validateRules.js +1 -0
- package/dist/components/form/validation/validators.js +1 -0
- package/dist/components/index.js +1 -1
- package/dist/components/inputs/InputWrapper.js +1 -1
- package/dist/components/inputs/NumberInput.js +1 -1
- package/dist/components/inputs/Picker.js +1 -1
- package/dist/components/inputs/Select.js +1 -1
- package/dist/components/presentation/Avatar.js +1 -1
- package/dist/components/presentation/AvatarLabel.js +1 -1
- package/dist/components/presentation/LabelValue.js +1 -1
- package/dist/components/presentation/Result.js +1 -1
- package/dist/components/presentation/Tooltip.js +1 -1
- package/dist/components/sections/Section.js +1 -0
- package/dist/components/sections/SectionItem.js +1 -0
- package/dist/components/sections/SectionItemLink.js +1 -0
- package/dist/components/sections/index.js +1 -0
- package/dist/components/state/StatePresenter.js +1 -0
- package/dist/components/state/index.js +1 -1
- package/dist/components/structure/BlurView.js +1 -1
- package/dist/components/structure/KeyboardAvoidingView.js +1 -0
- package/dist/components/structure/TopBar.js +1 -0
- package/dist/components/structure/bottomDrawer/index.js +1 -1
- package/dist/components/structure/bottomDrawer/index.native.js +1 -1
- package/dist/components/structure/bottomDrawer/index.web.js +1 -1
- package/dist/components/structure/bottomDrawer/native/BottomDrawer.js +1 -1
- package/dist/components/structure/bottomDrawer/native/DrawerScrollView.js +1 -1
- package/dist/components/structure/bottomDrawer/native/createDrawerScrollComponent.js +1 -0
- package/dist/components/structure/index.js +1 -1
- package/dist/components/text/DateText.js +1 -0
- package/dist/components/text/index.js +1 -1
- package/dist/components/theme/ThemePicker.js +1 -1
- package/dist/helpers/index.js +1 -1
- package/dist/helpers/storage.js +1 -1
- package/dist/responsive/responsiveHooks.js +1 -1
- package/dist/theme/ThemeHandler.js +1 -1
- package/dist/theme/default/base.js +1 -1
- package/dist/theme/default/blackTheme.js +1 -1
- package/dist/theme/default/cyberpunkTheme.js +1 -1
- package/dist/theme/default/darkTheme.js +1 -1
- package/dist/theme/default/hackerTheme.js +1 -1
- package/dist/theme/default/lightTheme.js +1 -1
- package/dist/theme/default/msdosTheme.js +1 -1
- package/dist/theme/default/paperTheme.js +1 -1
- package/package.json +1 -1
- package/src/abstractions/KeyboardAvoidingView.js +3 -0
- package/src/abstractions/KeyboardAvoidingView.native.js +3 -0
- package/src/components/actions/ActionsDrawer.js +68 -0
- package/src/components/actions/Button.js +2 -1
- package/src/components/actions/FloatingMenu.js +39 -0
- package/src/components/actions/index.js +2 -0
- package/src/components/animations/AnimatedTopBar.js +10 -0
- package/src/components/animations/AnimatedTopBar.native.js +34 -0
- package/src/components/animations/AnimatedTopBar.web.js +1 -0
- package/src/components/animations/ParallaxHeader.js +9 -0
- package/src/components/animations/ParallaxHeader.native.js +32 -0
- package/src/components/animations/ParallaxHeader.web.js +32 -0
- package/src/components/animations/ReanimatedScrollHandler.js +8 -0
- package/src/components/animations/ReanimatedScrollHandler.native.js +24 -0
- package/src/components/animations/ReanimatedScrollHandler.web.js +1 -0
- package/src/components/animations/index.js +3 -0
- package/src/components/form/FormItem.js +42 -5
- package/src/components/form/FormList.js +23 -4
- package/src/components/form/SubmitButton.js +4 -2
- package/src/components/form/index.js +1 -0
- package/src/components/form/useNewForm.js +108 -15
- package/src/components/form/validation/defaultMessages.js +20 -0
- package/src/components/form/validation/index.js +5 -0
- package/src/components/form/validation/normalizeRules.js +22 -0
- package/src/components/form/validation/shouldValidateOn.js +21 -0
- package/src/components/form/validation/validateRules.js +83 -0
- package/src/components/form/validation/validators.js +82 -0
- package/src/components/index.js +1 -0
- package/src/components/inputs/InputWrapper.js +1 -1
- package/src/components/inputs/NumberInput.js +6 -5
- package/src/components/inputs/Picker.js +3 -2
- package/src/components/inputs/Select.js +31 -15
- package/src/components/presentation/Avatar.js +2 -2
- package/src/components/presentation/AvatarLabel.js +2 -0
- package/src/components/presentation/LabelValue.js +7 -5
- package/src/components/presentation/Result.js +2 -2
- package/src/components/presentation/Tooltip.js +1 -1
- package/src/components/sections/Section.js +50 -0
- package/src/components/sections/SectionItem.js +24 -0
- package/src/components/sections/SectionItemLink.js +33 -0
- package/src/components/sections/index.js +3 -0
- package/src/components/state/StatePresenter.js +41 -0
- package/src/components/state/index.js +1 -0
- package/src/components/structure/BlurView.js +1 -0
- package/src/components/structure/KeyboardAvoidingView.js +52 -0
- package/src/components/structure/TopBar.js +45 -0
- package/src/components/structure/bottomDrawer/index.js +2 -0
- package/src/components/structure/bottomDrawer/index.native.js +2 -1
- package/src/components/structure/bottomDrawer/index.web.js +2 -1
- package/src/components/structure/bottomDrawer/native/BottomDrawer.js +14 -20
- package/src/components/structure/bottomDrawer/native/DrawerScrollView.js +4 -82
- package/src/components/structure/bottomDrawer/native/createDrawerScrollComponent.js +131 -0
- package/src/components/structure/index.js +2 -0
- package/src/components/text/DateText.js +11 -0
- package/src/components/text/index.js +1 -0
- package/src/components/theme/ThemePicker.js +1 -2
- package/src/helpers/index.js +1 -0
- package/src/helpers/storage.js +32 -9
- package/src/responsive/responsiveHooks.js +6 -0
- package/src/theme/ThemeHandler.js +6 -3
- package/src/theme/default/base.js +16 -4
- package/src/theme/default/blackTheme.js +1 -0
- package/src/theme/default/cyberpunkTheme.js +10 -0
- package/src/theme/default/darkTheme.js +1 -0
- package/src/theme/default/hackerTheme.js +17 -3
- package/src/theme/default/lightTheme.js +1 -0
- package/src/theme/default/msdosTheme.js +9 -10
- package/src/theme/default/paperTheme.js +10 -0
|
@@ -1,34 +1,61 @@
|
|
|
1
1
|
import { assocPath, path } from 'ramda'
|
|
2
2
|
import React from 'react'
|
|
3
|
+
import { validateRules, validateAllFields, normalizeRules } from './validation'
|
|
3
4
|
|
|
4
5
|
export function useNewForm({ initialValues = {}, validate, onSubmit } = {}) {
|
|
5
6
|
const valuesRef = React.useRef({ ...initialValues })
|
|
6
|
-
const errorsRef = React.useRef({})
|
|
7
|
+
const errorsRef = React.useRef({}) // Flat structure: { 'users': 'error', 'users.0.name': 'error' }
|
|
7
8
|
const listenersRef = React.useRef({})
|
|
9
|
+
const errorListenersRef = React.useRef({})
|
|
10
|
+
const rulesRegistryRef = React.useRef(new Map())
|
|
8
11
|
|
|
9
12
|
const formApi = React.useMemo(() => {
|
|
13
|
+
const toKey = (name) => (Array.isArray(name) ? name.join('.') : name)
|
|
14
|
+
const toPath = (name) => (Array.isArray(name) ? name : [name])
|
|
15
|
+
|
|
10
16
|
const notify = (name) => {
|
|
11
|
-
const key =
|
|
17
|
+
const key = toKey(name)
|
|
12
18
|
if (listenersRef.current[key]) {
|
|
13
|
-
listenersRef.current[key].forEach((cb) => cb(path(name, valuesRef.current)))
|
|
19
|
+
listenersRef.current[key].forEach((cb) => cb(path(toPath(name), valuesRef.current)))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const notifyError = (name) => {
|
|
24
|
+
const key = toKey(name)
|
|
25
|
+
if (errorListenersRef.current[key]) {
|
|
26
|
+
errorListenersRef.current[key].forEach((cb) => cb(errorsRef.current[key]))
|
|
14
27
|
}
|
|
15
28
|
}
|
|
16
29
|
|
|
17
30
|
const setFieldValue = (name, value) => {
|
|
18
|
-
valuesRef.current = assocPath(name, value, valuesRef.current)
|
|
31
|
+
valuesRef.current = assocPath(toPath(name), value, valuesRef.current)
|
|
19
32
|
notify(name)
|
|
20
33
|
}
|
|
21
34
|
|
|
22
|
-
const getFieldValue = (name) => path(name, valuesRef.current)
|
|
35
|
+
const getFieldValue = (name) => path(toPath(name), valuesRef.current)
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
// Flat error lookup by key
|
|
38
|
+
const getError = (name) => {
|
|
39
|
+
const key = toKey(name)
|
|
40
|
+
return errorsRef.current[key]
|
|
41
|
+
}
|
|
25
42
|
|
|
26
43
|
const setError = (name, error) => {
|
|
27
|
-
|
|
44
|
+
const key = toKey(name)
|
|
45
|
+
if (error) {
|
|
46
|
+
errorsRef.current[key] = error
|
|
47
|
+
} else {
|
|
48
|
+
delete errorsRef.current[key]
|
|
49
|
+
}
|
|
50
|
+
notifyError(name)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const clearErrors = () => {
|
|
54
|
+
errorsRef.current = {}
|
|
28
55
|
}
|
|
29
56
|
|
|
30
57
|
const registerListener = (name, cb) => {
|
|
31
|
-
const key =
|
|
58
|
+
const key = toKey(name)
|
|
32
59
|
if (!listenersRef.current[key]) {
|
|
33
60
|
listenersRef.current[key] = []
|
|
34
61
|
}
|
|
@@ -38,15 +65,77 @@ export function useNewForm({ initialValues = {}, validate, onSubmit } = {}) {
|
|
|
38
65
|
}
|
|
39
66
|
}
|
|
40
67
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
const registerErrorListener = (name, cb) => {
|
|
69
|
+
const key = toKey(name)
|
|
70
|
+
if (!errorListenersRef.current[key]) {
|
|
71
|
+
errorListenersRef.current[key] = []
|
|
72
|
+
}
|
|
73
|
+
errorListenersRef.current[key].push(cb)
|
|
74
|
+
return () => {
|
|
75
|
+
errorListenersRef.current[key] = errorListenersRef.current[key].filter((fn) => fn !== cb)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const registerRules = (name, rules, defaultTrigger = 'onSubmit') => {
|
|
80
|
+
if (!rules) return
|
|
81
|
+
const key = toKey(name)
|
|
82
|
+
const rulesArray = normalizeRules(rules).map((rule) => ({
|
|
83
|
+
...rule,
|
|
84
|
+
trigger: rule.trigger || defaultTrigger,
|
|
85
|
+
}))
|
|
86
|
+
rulesRegistryRef.current.set(key, { path: name, rules: rulesArray })
|
|
87
|
+
return () => rulesRegistryRef.current.delete(key)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const validateField = async (name, trigger = 'onSubmit') => {
|
|
91
|
+
const key = toKey(name)
|
|
92
|
+
const entry = rulesRegistryRef.current.get(key)
|
|
93
|
+
if (!entry) return null
|
|
94
|
+
|
|
95
|
+
const value = path(name, valuesRef.current)
|
|
96
|
+
const error = await validateRules(value, entry.rules, trigger)
|
|
97
|
+
|
|
98
|
+
if (error) {
|
|
99
|
+
errorsRef.current[key] = error
|
|
100
|
+
} else {
|
|
101
|
+
delete errorsRef.current[key]
|
|
102
|
+
}
|
|
103
|
+
notifyError(name)
|
|
104
|
+
return error
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const validateForm = async () => {
|
|
108
|
+
// Clear previous errors
|
|
109
|
+
errorsRef.current = {}
|
|
110
|
+
|
|
111
|
+
// Run rules-based validation
|
|
112
|
+
const rulesErrors = await validateAllFields(valuesRef.current, rulesRegistryRef.current)
|
|
113
|
+
|
|
114
|
+
// Run legacy validate function if provided
|
|
115
|
+
const legacyErrors = validate ? validate(valuesRef.current) || {} : {}
|
|
116
|
+
|
|
117
|
+
// Store errors in flat structure
|
|
118
|
+
Object.entries(rulesErrors).forEach(([key, error]) => {
|
|
119
|
+
errorsRef.current[key] = error
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Legacy errors are already flat (or should be converted)
|
|
123
|
+
Object.entries(legacyErrors).forEach(([key, error]) => {
|
|
124
|
+
if (!errorsRef.current[key]) {
|
|
125
|
+
errorsRef.current[key] = error
|
|
126
|
+
}
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
// Notify all error listeners
|
|
130
|
+
rulesRegistryRef.current.forEach((_, key) => {
|
|
131
|
+
notifyError(key)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return Object.keys(errorsRef.current).length === 0
|
|
46
135
|
}
|
|
47
136
|
|
|
48
|
-
const handleSubmit = () => {
|
|
49
|
-
const isValid = validateForm()
|
|
137
|
+
const handleSubmit = async () => {
|
|
138
|
+
const isValid = await validateForm()
|
|
50
139
|
if (!isValid) return
|
|
51
140
|
console.log('SUBMIT')
|
|
52
141
|
onSubmit(valuesRef.current)
|
|
@@ -57,7 +146,11 @@ export function useNewForm({ initialValues = {}, validate, onSubmit } = {}) {
|
|
|
57
146
|
getFieldValue,
|
|
58
147
|
getError,
|
|
59
148
|
setError,
|
|
149
|
+
clearErrors,
|
|
60
150
|
registerListener,
|
|
151
|
+
registerErrorListener,
|
|
152
|
+
registerRules,
|
|
153
|
+
validateField,
|
|
61
154
|
handleSubmit,
|
|
62
155
|
valuesRef,
|
|
63
156
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const defaultMessages = {
|
|
2
|
+
required: 'This field is required',
|
|
3
|
+
type: {
|
|
4
|
+
email: 'Please enter a valid email address',
|
|
5
|
+
url: 'Please enter a valid URL',
|
|
6
|
+
number: 'Please enter a valid number',
|
|
7
|
+
integer: 'Please enter a valid integer',
|
|
8
|
+
},
|
|
9
|
+
min: {
|
|
10
|
+
string: (min) => `Must be at least ${min} characters`,
|
|
11
|
+
number: (min) => `Must be at least ${min}`,
|
|
12
|
+
array: (min) => `Must have at least ${min} items`,
|
|
13
|
+
},
|
|
14
|
+
max: {
|
|
15
|
+
string: (max) => `Must be at most ${max} characters`,
|
|
16
|
+
number: (max) => `Must be at most ${max}`,
|
|
17
|
+
array: (max) => `Must have at most ${max} items`,
|
|
18
|
+
},
|
|
19
|
+
pattern: 'Invalid format',
|
|
20
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { defaultMessages } from './defaultMessages'
|
|
2
|
+
export { validators } from './validators'
|
|
3
|
+
export { validateRules, validateAllFields } from './validateRules'
|
|
4
|
+
export { normalizeRules } from './normalizeRules'
|
|
5
|
+
export { shouldValidateOn } from './shouldValidateOn'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes rules to array format.
|
|
3
|
+
* Accepts either array format or object shorthand.
|
|
4
|
+
*
|
|
5
|
+
* Array format (full control):
|
|
6
|
+
* [{ required: true, message: 'Required' }, { min: 2 }]
|
|
7
|
+
*
|
|
8
|
+
* Object shorthand (simple cases):
|
|
9
|
+
* { required: true, min: 2, max: 7, type: 'email' }
|
|
10
|
+
* -> converts to: [{ required: true }, { min: 2 }, { max: 7 }, { type: 'email' }]
|
|
11
|
+
*
|
|
12
|
+
* @param {Array|Object} rules
|
|
13
|
+
* @returns {Array}
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeRules(rules) {
|
|
16
|
+
if (!rules) return []
|
|
17
|
+
if (Array.isArray(rules)) return rules
|
|
18
|
+
if (typeof rules === 'object') {
|
|
19
|
+
return Object.entries(rules).map(([key, value]) => ({ [key]: value }))
|
|
20
|
+
}
|
|
21
|
+
return []
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeRules } from './normalizeRules'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if validation should run for a given trigger.
|
|
5
|
+
* @param {string} trigger - The trigger to check ('onChange', 'onBlur', 'onSubmit')
|
|
6
|
+
* @param {Array|Object} rules - The rules (array or object format)
|
|
7
|
+
* @param {string|string[]} validateTrigger - The default trigger(s) for the field
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function shouldValidateOn(trigger, rules, validateTrigger = 'onSubmit') {
|
|
11
|
+
if (!rules) return false
|
|
12
|
+
|
|
13
|
+
const triggers = Array.isArray(validateTrigger) ? validateTrigger : [validateTrigger]
|
|
14
|
+
if (triggers.includes(trigger)) return true
|
|
15
|
+
|
|
16
|
+
// Check per-rule triggers
|
|
17
|
+
const rulesArray = normalizeRules(rules)
|
|
18
|
+
return rulesArray.some(
|
|
19
|
+
(rule) => rule.trigger === trigger || (Array.isArray(rule.trigger) && rule.trigger.includes(trigger))
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { path as getPath } from 'ramda'
|
|
2
|
+
import { validators } from './validators'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validates a value against an array of rules
|
|
6
|
+
* @param {any} value - The value to validate
|
|
7
|
+
* @param {Array} rules - Array of rule objects
|
|
8
|
+
* @param {string} trigger - Current validation trigger ('onSubmit', 'onBlur', 'onChange')
|
|
9
|
+
* @returns {Promise<string|null>} - First error message or null if valid
|
|
10
|
+
*/
|
|
11
|
+
export async function validateRules(value, rules, trigger = 'onSubmit') {
|
|
12
|
+
if (!rules || rules.length === 0) return null
|
|
13
|
+
|
|
14
|
+
for (const rule of rules) {
|
|
15
|
+
const ruleTrigger = rule.trigger || 'onSubmit'
|
|
16
|
+
const triggers = Array.isArray(ruleTrigger) ? ruleTrigger : [ruleTrigger]
|
|
17
|
+
|
|
18
|
+
// Always run on submit, otherwise check trigger match
|
|
19
|
+
if (trigger !== 'onSubmit' && !triggers.includes(trigger)) {
|
|
20
|
+
continue
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let error = null
|
|
24
|
+
|
|
25
|
+
// Custom validator takes precedence
|
|
26
|
+
if (rule.validator) {
|
|
27
|
+
try {
|
|
28
|
+
await rule.validator(rule, value)
|
|
29
|
+
} catch (e) {
|
|
30
|
+
error = e.message || rule.message || 'Validation failed'
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
// Run built-in validators
|
|
34
|
+
if (rule.required) {
|
|
35
|
+
error = validators.required(value, rule)
|
|
36
|
+
}
|
|
37
|
+
if (!error && rule.type) {
|
|
38
|
+
error = validators.type(value, rule)
|
|
39
|
+
}
|
|
40
|
+
if (!error && rule.min !== undefined) {
|
|
41
|
+
error = validators.min(value, rule)
|
|
42
|
+
}
|
|
43
|
+
if (!error && rule.max !== undefined) {
|
|
44
|
+
error = validators.max(value, rule)
|
|
45
|
+
}
|
|
46
|
+
if (!error && rule.pattern) {
|
|
47
|
+
error = validators.pattern(value, rule)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (error) {
|
|
52
|
+
return error
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Validates multiple fields at once (for form-level validation)
|
|
61
|
+
* @param {Object} values - All form values
|
|
62
|
+
* @param {Map} rulesRegistry - Map of field path keys to { path: array, rules: array }
|
|
63
|
+
* @returns {Promise<Object>} - Object of path key -> error message
|
|
64
|
+
*/
|
|
65
|
+
export async function validateAllFields(values, rulesRegistry) {
|
|
66
|
+
const errors = {}
|
|
67
|
+
const validationPromises = []
|
|
68
|
+
|
|
69
|
+
rulesRegistry.forEach(({ path, rules }, pathKey) => {
|
|
70
|
+
const value = getPath(path, values)
|
|
71
|
+
|
|
72
|
+
validationPromises.push(
|
|
73
|
+
validateRules(value, rules, 'onSubmit').then((error) => {
|
|
74
|
+
if (error) {
|
|
75
|
+
errors[pathKey] = error
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
await Promise.all(validationPromises)
|
|
82
|
+
return errors
|
|
83
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { defaultMessages } from './defaultMessages'
|
|
2
|
+
|
|
3
|
+
const isEmpty = (value) => {
|
|
4
|
+
if (value === undefined || value === null) return true
|
|
5
|
+
if (typeof value === 'string' && value.trim() === '') return true
|
|
6
|
+
if (Array.isArray(value) && value.length === 0) return true
|
|
7
|
+
return false
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const validators = {
|
|
11
|
+
required: (value, rule) => {
|
|
12
|
+
if (isEmpty(value)) {
|
|
13
|
+
return rule.message || defaultMessages.required
|
|
14
|
+
}
|
|
15
|
+
return null
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
type: (value, rule) => {
|
|
19
|
+
if (isEmpty(value)) return null
|
|
20
|
+
|
|
21
|
+
const typeValidators = {
|
|
22
|
+
email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
|
|
23
|
+
url: (v) => {
|
|
24
|
+
try {
|
|
25
|
+
new URL(v)
|
|
26
|
+
return true
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
number: (v) => !isNaN(Number(v)),
|
|
32
|
+
integer: (v) => Number.isInteger(Number(v)) && !isNaN(Number(v)),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const validator = typeValidators[rule.type]
|
|
36
|
+
if (validator && !validator(value)) {
|
|
37
|
+
return rule.message || defaultMessages.type[rule.type] || `Invalid ${rule.type}`
|
|
38
|
+
}
|
|
39
|
+
return null
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
min: (value, rule) => {
|
|
43
|
+
if (isEmpty(value)) return null
|
|
44
|
+
|
|
45
|
+
if (typeof value === 'string' && value.length < rule.min) {
|
|
46
|
+
return rule.message || defaultMessages.min.string(rule.min)
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'number' && value < rule.min) {
|
|
49
|
+
return rule.message || defaultMessages.min.number(rule.min)
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(value) && value.length < rule.min) {
|
|
52
|
+
return rule.message || defaultMessages.min.array(rule.min)
|
|
53
|
+
}
|
|
54
|
+
return null
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
max: (value, rule) => {
|
|
58
|
+
if (isEmpty(value)) return null
|
|
59
|
+
|
|
60
|
+
if (typeof value === 'string' && value.length > rule.max) {
|
|
61
|
+
return rule.message || defaultMessages.max.string(rule.max)
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === 'number' && value > rule.max) {
|
|
64
|
+
return rule.message || defaultMessages.max.number(rule.max)
|
|
65
|
+
}
|
|
66
|
+
if (Array.isArray(value) && value.length > rule.max) {
|
|
67
|
+
return rule.message || defaultMessages.max.array(rule.max)
|
|
68
|
+
}
|
|
69
|
+
return null
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
pattern: (value, rule) => {
|
|
73
|
+
if (isEmpty(value)) return null
|
|
74
|
+
|
|
75
|
+
const regex = rule.pattern instanceof RegExp ? rule.pattern : new RegExp(rule.pattern)
|
|
76
|
+
|
|
77
|
+
if (!regex.test(String(value))) {
|
|
78
|
+
return rule.message || defaultMessages.pattern
|
|
79
|
+
}
|
|
80
|
+
return null
|
|
81
|
+
},
|
|
82
|
+
}
|
package/src/components/index.js
CHANGED
|
@@ -48,7 +48,7 @@ export function InputWrapper({
|
|
|
48
48
|
if (!!suffix && is(String, suffix)) suffix = <Text>{suffix}</Text>
|
|
49
49
|
if (!prefix && !!prefixIcon) prefix = <Icon name={prefixIcon} size={sizeCode} color={prefixIconColor} />
|
|
50
50
|
if (!suffix && !!suffixIcon) suffix = <Icon name={suffixIcon} size={sizeCode} color={suffixIconColor} />
|
|
51
|
-
if (!prefix && !!error) suffix = <Icon name="
|
|
51
|
+
if (!prefix && !!error) suffix = <Icon name="alert-fill" size={sizeCode} red />
|
|
52
52
|
if (!!loading) suffix = <Loading size={sizeCode} />
|
|
53
53
|
|
|
54
54
|
let borderColor = !!hover ? 'primary_op40' : 'divider'
|
|
@@ -73,11 +73,11 @@ export function formatNumericValue(newValue, prevValue, options = {}) {
|
|
|
73
73
|
return numericValue
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export function NumberInput({ onChange, value, useInt, precision, min, max, error, ...props }) {
|
|
76
|
+
export function NumberInput({ onChange, onBlur, value, useInt, precision, min, max, error, ...props }) {
|
|
77
77
|
const [hasError, setHasError] = React.useState(false)
|
|
78
78
|
const [inputValue, setInputValue] = React.useState(value)
|
|
79
79
|
const [localValue, setLocalValue] = React.useState(value)
|
|
80
|
-
React.useEffect(() => setInputValue(value), [value])
|
|
80
|
+
React.useEffect(() => setInputValue(value?.toString() || ''), [value])
|
|
81
81
|
|
|
82
82
|
if (useInt) precision = 0
|
|
83
83
|
if (!useInt && precision === 0) useInt = true
|
|
@@ -85,6 +85,7 @@ export function NumberInput({ onChange, value, useInt, precision, min, max, erro
|
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
87
|
<TextInput
|
|
88
|
+
{...props}
|
|
88
89
|
onChange={(newValue) => {
|
|
89
90
|
const numericValue = formatNumericValue(newValue, localValue, opts)
|
|
90
91
|
setInputValue(newValue?.toString() || '')
|
|
@@ -92,14 +93,14 @@ export function NumberInput({ onChange, value, useInt, precision, min, max, erro
|
|
|
92
93
|
onChange?.(numericValue)
|
|
93
94
|
setHasError(!isValidNumber(newValue, opts))
|
|
94
95
|
}}
|
|
95
|
-
onBlur={() => {
|
|
96
|
-
setInputValue(localValue)
|
|
96
|
+
onBlur={(e) => {
|
|
97
|
+
setInputValue(localValue?.toString() || '')
|
|
97
98
|
setHasError(!isValidNumber(localValue, opts))
|
|
99
|
+
onBlur?.(e)
|
|
98
100
|
}}
|
|
99
101
|
value={inputValue}
|
|
100
102
|
keyboardType={useInt ? 'number-pad' : 'decimal-pad'}
|
|
101
103
|
error={error || hasError}
|
|
102
|
-
{...props}
|
|
103
104
|
/>
|
|
104
105
|
)
|
|
105
106
|
}
|
|
@@ -2,9 +2,10 @@ import { is } from 'ramda'
|
|
|
2
2
|
import React from 'react'
|
|
3
3
|
|
|
4
4
|
import { Col } from '../structure/Col'
|
|
5
|
-
import {
|
|
5
|
+
import { DrawerFlatList } from '../structure/bottomDrawer'
|
|
6
6
|
import { LoadingView } from '../state/LoadingView'
|
|
7
7
|
import { Row } from '../structure/Row'
|
|
8
|
+
import { View } from '../structure'
|
|
8
9
|
import { normalizeString } from '../../helpers/string'
|
|
9
10
|
import { useOptions } from '../../helpers/options'
|
|
10
11
|
|
|
@@ -94,7 +95,7 @@ function DefaultPickerWrapper({ renderItem, options, ...props }) {
|
|
|
94
95
|
|
|
95
96
|
function FlatListPickerWrapper({ renderItem, options, valueKey, ...props }) {
|
|
96
97
|
return (
|
|
97
|
-
<
|
|
98
|
+
<DrawerFlatList
|
|
98
99
|
keyExtractor={(i) => i[valueKey]}
|
|
99
100
|
data={options}
|
|
100
101
|
divider
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dissoc } from 'ramda'
|
|
1
2
|
import React from 'react'
|
|
2
3
|
|
|
3
4
|
import { Icon, IconLabel } from '../presentation'
|
|
@@ -38,6 +39,7 @@ export function Select({
|
|
|
38
39
|
pickerProps,
|
|
39
40
|
popoverProps,
|
|
40
41
|
popoverMaxHeight,
|
|
42
|
+
snapPoints,
|
|
41
43
|
...props
|
|
42
44
|
}) {
|
|
43
45
|
const [focus, setFocus] = React.useState(false)
|
|
@@ -52,7 +54,17 @@ export function Select({
|
|
|
52
54
|
onEndReached = onEndReached || pickerProps?.onEndReached
|
|
53
55
|
renderFooter = renderFooter || pickerProps?.renderFooter
|
|
54
56
|
renderHeader = renderHeader || pickerProps?.renderHeader
|
|
55
|
-
pickerProps = {
|
|
57
|
+
pickerProps = {
|
|
58
|
+
...pickerProps,
|
|
59
|
+
|
|
60
|
+
labelKey,
|
|
61
|
+
valueKey,
|
|
62
|
+
useRawOption,
|
|
63
|
+
multiple,
|
|
64
|
+
onEndReached,
|
|
65
|
+
renderFooter,
|
|
66
|
+
renderHeader,
|
|
67
|
+
}
|
|
56
68
|
|
|
57
69
|
popoverMaxHeight = popoverMaxHeight || 300
|
|
58
70
|
|
|
@@ -93,7 +105,10 @@ export function Select({
|
|
|
93
105
|
const finalRenderOption = React.useCallback(
|
|
94
106
|
(params) => {
|
|
95
107
|
if (!!renderOption) return renderOption(params)
|
|
96
|
-
|
|
108
|
+
let { option, labelKey, selected } = params
|
|
109
|
+
if (option?.id) option = dissoc('id', option)
|
|
110
|
+
if (option?.color) option = { ...option, color: undefined, iconColor: option.color }
|
|
111
|
+
|
|
97
112
|
return <IconLabel {...option} label={option?.[labelKey]} flex strong={selected} />
|
|
98
113
|
},
|
|
99
114
|
[renderOption]
|
|
@@ -103,7 +118,7 @@ export function Select({
|
|
|
103
118
|
<Popover
|
|
104
119
|
trigger="click"
|
|
105
120
|
placement={placement || 'bottomLeft'}
|
|
106
|
-
snapPoints={[450]}
|
|
121
|
+
snapPoints={snapPoints || [450]}
|
|
107
122
|
useBottomDrawer={useBottomDrawer}
|
|
108
123
|
parentWidth
|
|
109
124
|
padding={0}
|
|
@@ -112,17 +127,6 @@ export function Select({
|
|
|
112
127
|
maxHeight={popoverMaxHeight}
|
|
113
128
|
{...popoverProps}
|
|
114
129
|
renderContent={({ onClose }) => (
|
|
115
|
-
<>
|
|
116
|
-
{useBottomDrawer && useSearch && (
|
|
117
|
-
<View padding="md" paddingB="xs">
|
|
118
|
-
<TextInput
|
|
119
|
-
prefixIcon="search-line"
|
|
120
|
-
prefixIconColor="text4"
|
|
121
|
-
value={search}
|
|
122
|
-
onChange={handleChangeSearch}
|
|
123
|
-
/>
|
|
124
|
-
</View>
|
|
125
|
-
)}
|
|
126
130
|
<Picker
|
|
127
131
|
row={false}
|
|
128
132
|
options={searchOptions(options, search, { labelKey })}
|
|
@@ -137,6 +141,19 @@ export function Select({
|
|
|
137
141
|
if (!multiple) onClose()
|
|
138
142
|
}}
|
|
139
143
|
{...pickerProps}
|
|
144
|
+
renderHeader={useBottomDrawer && useSearch ? () => (
|
|
145
|
+
<>
|
|
146
|
+
<View padding="md" paddingB="xs">
|
|
147
|
+
<TextInput
|
|
148
|
+
prefixIcon="search-line"
|
|
149
|
+
prefixIconColor="text4"
|
|
150
|
+
value={search}
|
|
151
|
+
onChange={handleChangeSearch}
|
|
152
|
+
/>
|
|
153
|
+
</View>
|
|
154
|
+
{renderHeader?.()}
|
|
155
|
+
</>
|
|
156
|
+
) : renderHeader}
|
|
140
157
|
renderOption={({ option, selected, onChange }) => (
|
|
141
158
|
<Link
|
|
142
159
|
row
|
|
@@ -156,7 +173,6 @@ export function Select({
|
|
|
156
173
|
</Link>
|
|
157
174
|
)}
|
|
158
175
|
/>
|
|
159
|
-
</>
|
|
160
176
|
)}
|
|
161
177
|
>
|
|
162
178
|
<Input
|
|
@@ -54,7 +54,7 @@ export function Avatar(rootProps) {
|
|
|
54
54
|
useOverflowModifier
|
|
55
55
|
)([{}, rootProps])
|
|
56
56
|
|
|
57
|
-
let { initials, name, icon, src, invert, textProps, iconProps, ...props } = formattedProps
|
|
57
|
+
let { initials, name, icon, src, invert, textProps, iconProps, iconSize, ...props } = formattedProps
|
|
58
58
|
initials = initials || getInitials(name)
|
|
59
59
|
|
|
60
60
|
let content = (
|
|
@@ -66,7 +66,7 @@ export function Avatar(rootProps) {
|
|
|
66
66
|
icon={icon}
|
|
67
67
|
invert={invert}
|
|
68
68
|
textProps={{ strong: true, ...textProps }}
|
|
69
|
-
iconProps={iconProps}
|
|
69
|
+
iconProps={{ size: iconSize, ...iconProps }}
|
|
70
70
|
/>
|
|
71
71
|
)
|
|
72
72
|
if (!!src) content = <Image br={0} src={src} width={sizeCode} height={sizeCode} />
|
|
@@ -26,6 +26,7 @@ export function AvatarLabel(rootProps) {
|
|
|
26
26
|
avatarProps,
|
|
27
27
|
moveAvatarSizeScale,
|
|
28
28
|
avatarSize,
|
|
29
|
+
iconSize,
|
|
29
30
|
...props
|
|
30
31
|
} = formattedProps
|
|
31
32
|
const hasAvatar = !!name || !!initials || !!src
|
|
@@ -47,6 +48,7 @@ export function AvatarLabel(rootProps) {
|
|
|
47
48
|
dynamicColor={dynamicColor}
|
|
48
49
|
square={square}
|
|
49
50
|
marginH={2}
|
|
51
|
+
iconSize={iconSize}
|
|
50
52
|
{...avatarProps}
|
|
51
53
|
/>
|
|
52
54
|
}
|
|
@@ -4,11 +4,12 @@ import { IconLabel } from './IconLabel'
|
|
|
4
4
|
import { Text } from '../text/Text'
|
|
5
5
|
import { View } from '../structure/View'
|
|
6
6
|
import { moveScale } from '../../theme/helpers/sizeScale'
|
|
7
|
+
import { useColorConverter } from '../../modifiers/colorConverter'
|
|
7
8
|
import { useDefaultModifier } from '../../modifiers/default'
|
|
8
9
|
import { useSizeConverter } from '../../modifiers/sizeConverter'
|
|
9
10
|
import { useThemeComponentModifier } from '../../modifiers/themeComponent'
|
|
10
11
|
|
|
11
|
-
const DEFAULT_PROPS = ([{ sizeCode }, { vertical, spread }]) => {
|
|
12
|
+
const DEFAULT_PROPS = ([{ sizeCode, color }, { vertical, spread }]) => {
|
|
12
13
|
return {
|
|
13
14
|
row: !vertical,
|
|
14
15
|
centerV: !vertical,
|
|
@@ -17,7 +18,7 @@ const DEFAULT_PROPS = ([{ sizeCode }, { vertical, spread }]) => {
|
|
|
17
18
|
labelProps: {
|
|
18
19
|
size: moveScale(sizeCode, !vertical ? 0 : -2),
|
|
19
20
|
moveIconSizeScale: !vertical ? -1 : -2,
|
|
20
|
-
color: 'text3',
|
|
21
|
+
color: color || 'text3',
|
|
21
22
|
},
|
|
22
23
|
valueProps: {
|
|
23
24
|
size: sizeCode,
|
|
@@ -28,17 +29,18 @@ const DEFAULT_PROPS = ([{ sizeCode }, { vertical, spread }]) => {
|
|
|
28
29
|
|
|
29
30
|
export function LabelValue({ children, ...rootProps }) {
|
|
30
31
|
const [{ sizeCode, color }, formattedProps] = pipe(
|
|
31
|
-
|
|
32
|
+
useColorConverter(),
|
|
32
33
|
useSizeConverter('elementHeights', 'md'),
|
|
33
34
|
useThemeComponentModifier('Labelvalue'), //
|
|
34
35
|
useDefaultModifier(DEFAULT_PROPS)
|
|
35
36
|
)([{}, rootProps])
|
|
36
37
|
|
|
37
|
-
const { icon, label, iconColor, labelProps, value, valueProps, vertical, spread, ...props } =
|
|
38
|
+
const { icon, label, iconColor, labelProps, value, valueColor, valueProps, vertical, spread, ...props } =
|
|
39
|
+
formattedProps
|
|
38
40
|
let separator = !vertical && !spread ? ':' : ''
|
|
39
41
|
|
|
40
42
|
let content = children || value
|
|
41
|
-
if (is(String, value)) content = <Text label={value} {...valueProps} />
|
|
43
|
+
if (is(String, value)) content = <Text label={value} color={valueColor || color} {...valueProps} />
|
|
42
44
|
|
|
43
45
|
return (
|
|
44
46
|
<View className="neko-label-value" {...props}>
|
|
@@ -46,11 +46,11 @@ export function Result({
|
|
|
46
46
|
<View className="neko-result" center padding="lg" {...props}>
|
|
47
47
|
{!!icon && <Icon name={icon} color={iconColor} size={42} primary {...iconProps} />}
|
|
48
48
|
{!!icon && <Divider height={10} />}
|
|
49
|
-
<Text h4 {...textProps} {...titleProps}>
|
|
49
|
+
<Text h4 center {...textProps} {...titleProps}>
|
|
50
50
|
{title}
|
|
51
51
|
</Text>
|
|
52
52
|
{!!description && (
|
|
53
|
-
<Text text3 sm marginT="sm" {...textProps} {...descriptionProps}>
|
|
53
|
+
<Text text3 sm marginT="sm" center {...textProps} {...descriptionProps}>
|
|
54
54
|
{description}
|
|
55
55
|
</Text>
|
|
56
56
|
)}
|