@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.
Files changed (137) hide show
  1. package/dist/abstractions/KeyboardAvoidingView.js +1 -0
  2. package/dist/abstractions/KeyboardAvoidingView.native.js +1 -0
  3. package/dist/components/actions/ActionsDrawer.js +1 -0
  4. package/dist/components/actions/Button.js +1 -1
  5. package/dist/components/actions/FloatingMenu.js +1 -0
  6. package/dist/components/actions/index.js +1 -1
  7. package/dist/components/animations/AnimatedTopBar.js +1 -0
  8. package/dist/components/animations/AnimatedTopBar.native.js +1 -0
  9. package/dist/components/animations/AnimatedTopBar.web.js +1 -0
  10. package/dist/components/animations/ParallaxHeader.js +1 -0
  11. package/dist/components/animations/ParallaxHeader.native.js +1 -0
  12. package/dist/components/animations/ParallaxHeader.web.js +1 -0
  13. package/dist/components/animations/ReanimatedScrollHandler.js +1 -0
  14. package/dist/components/animations/ReanimatedScrollHandler.native.js +1 -0
  15. package/dist/components/animations/ReanimatedScrollHandler.web.js +1 -0
  16. package/dist/components/animations/index.js +1 -1
  17. package/dist/components/form/FormItem.js +1 -1
  18. package/dist/components/form/FormList.js +1 -1
  19. package/dist/components/form/SubmitButton.js +1 -1
  20. package/dist/components/form/index.js +1 -1
  21. package/dist/components/form/useNewForm.js +1 -1
  22. package/dist/components/form/validation/defaultMessages.js +1 -0
  23. package/dist/components/form/validation/index.js +1 -0
  24. package/dist/components/form/validation/normalizeRules.js +1 -0
  25. package/dist/components/form/validation/shouldValidateOn.js +1 -0
  26. package/dist/components/form/validation/validateRules.js +1 -0
  27. package/dist/components/form/validation/validators.js +1 -0
  28. package/dist/components/index.js +1 -1
  29. package/dist/components/inputs/InputWrapper.js +1 -1
  30. package/dist/components/inputs/NumberInput.js +1 -1
  31. package/dist/components/inputs/Picker.js +1 -1
  32. package/dist/components/inputs/Select.js +1 -1
  33. package/dist/components/presentation/Avatar.js +1 -1
  34. package/dist/components/presentation/AvatarLabel.js +1 -1
  35. package/dist/components/presentation/LabelValue.js +1 -1
  36. package/dist/components/presentation/Result.js +1 -1
  37. package/dist/components/presentation/Tooltip.js +1 -1
  38. package/dist/components/sections/Section.js +1 -0
  39. package/dist/components/sections/SectionItem.js +1 -0
  40. package/dist/components/sections/SectionItemLink.js +1 -0
  41. package/dist/components/sections/index.js +1 -0
  42. package/dist/components/state/StatePresenter.js +1 -0
  43. package/dist/components/state/index.js +1 -1
  44. package/dist/components/structure/BlurView.js +1 -1
  45. package/dist/components/structure/KeyboardAvoidingView.js +1 -0
  46. package/dist/components/structure/TopBar.js +1 -0
  47. package/dist/components/structure/bottomDrawer/index.js +1 -1
  48. package/dist/components/structure/bottomDrawer/index.native.js +1 -1
  49. package/dist/components/structure/bottomDrawer/index.web.js +1 -1
  50. package/dist/components/structure/bottomDrawer/native/BottomDrawer.js +1 -1
  51. package/dist/components/structure/bottomDrawer/native/DrawerScrollView.js +1 -1
  52. package/dist/components/structure/bottomDrawer/native/createDrawerScrollComponent.js +1 -0
  53. package/dist/components/structure/index.js +1 -1
  54. package/dist/components/text/DateText.js +1 -0
  55. package/dist/components/text/index.js +1 -1
  56. package/dist/components/theme/ThemePicker.js +1 -1
  57. package/dist/helpers/index.js +1 -1
  58. package/dist/helpers/storage.js +1 -1
  59. package/dist/responsive/responsiveHooks.js +1 -1
  60. package/dist/theme/ThemeHandler.js +1 -1
  61. package/dist/theme/default/base.js +1 -1
  62. package/dist/theme/default/blackTheme.js +1 -1
  63. package/dist/theme/default/cyberpunkTheme.js +1 -1
  64. package/dist/theme/default/darkTheme.js +1 -1
  65. package/dist/theme/default/hackerTheme.js +1 -1
  66. package/dist/theme/default/lightTheme.js +1 -1
  67. package/dist/theme/default/msdosTheme.js +1 -1
  68. package/dist/theme/default/paperTheme.js +1 -1
  69. package/package.json +1 -1
  70. package/src/abstractions/KeyboardAvoidingView.js +3 -0
  71. package/src/abstractions/KeyboardAvoidingView.native.js +3 -0
  72. package/src/components/actions/ActionsDrawer.js +68 -0
  73. package/src/components/actions/Button.js +2 -1
  74. package/src/components/actions/FloatingMenu.js +39 -0
  75. package/src/components/actions/index.js +2 -0
  76. package/src/components/animations/AnimatedTopBar.js +10 -0
  77. package/src/components/animations/AnimatedTopBar.native.js +34 -0
  78. package/src/components/animations/AnimatedTopBar.web.js +1 -0
  79. package/src/components/animations/ParallaxHeader.js +9 -0
  80. package/src/components/animations/ParallaxHeader.native.js +32 -0
  81. package/src/components/animations/ParallaxHeader.web.js +32 -0
  82. package/src/components/animations/ReanimatedScrollHandler.js +8 -0
  83. package/src/components/animations/ReanimatedScrollHandler.native.js +24 -0
  84. package/src/components/animations/ReanimatedScrollHandler.web.js +1 -0
  85. package/src/components/animations/index.js +3 -0
  86. package/src/components/form/FormItem.js +42 -5
  87. package/src/components/form/FormList.js +23 -4
  88. package/src/components/form/SubmitButton.js +4 -2
  89. package/src/components/form/index.js +1 -0
  90. package/src/components/form/useNewForm.js +108 -15
  91. package/src/components/form/validation/defaultMessages.js +20 -0
  92. package/src/components/form/validation/index.js +5 -0
  93. package/src/components/form/validation/normalizeRules.js +22 -0
  94. package/src/components/form/validation/shouldValidateOn.js +21 -0
  95. package/src/components/form/validation/validateRules.js +83 -0
  96. package/src/components/form/validation/validators.js +82 -0
  97. package/src/components/index.js +1 -0
  98. package/src/components/inputs/InputWrapper.js +1 -1
  99. package/src/components/inputs/NumberInput.js +6 -5
  100. package/src/components/inputs/Picker.js +3 -2
  101. package/src/components/inputs/Select.js +31 -15
  102. package/src/components/presentation/Avatar.js +2 -2
  103. package/src/components/presentation/AvatarLabel.js +2 -0
  104. package/src/components/presentation/LabelValue.js +7 -5
  105. package/src/components/presentation/Result.js +2 -2
  106. package/src/components/presentation/Tooltip.js +1 -1
  107. package/src/components/sections/Section.js +50 -0
  108. package/src/components/sections/SectionItem.js +24 -0
  109. package/src/components/sections/SectionItemLink.js +33 -0
  110. package/src/components/sections/index.js +3 -0
  111. package/src/components/state/StatePresenter.js +41 -0
  112. package/src/components/state/index.js +1 -0
  113. package/src/components/structure/BlurView.js +1 -0
  114. package/src/components/structure/KeyboardAvoidingView.js +52 -0
  115. package/src/components/structure/TopBar.js +45 -0
  116. package/src/components/structure/bottomDrawer/index.js +2 -0
  117. package/src/components/structure/bottomDrawer/index.native.js +2 -1
  118. package/src/components/structure/bottomDrawer/index.web.js +2 -1
  119. package/src/components/structure/bottomDrawer/native/BottomDrawer.js +14 -20
  120. package/src/components/structure/bottomDrawer/native/DrawerScrollView.js +4 -82
  121. package/src/components/structure/bottomDrawer/native/createDrawerScrollComponent.js +131 -0
  122. package/src/components/structure/index.js +2 -0
  123. package/src/components/text/DateText.js +11 -0
  124. package/src/components/text/index.js +1 -0
  125. package/src/components/theme/ThemePicker.js +1 -2
  126. package/src/helpers/index.js +1 -0
  127. package/src/helpers/storage.js +32 -9
  128. package/src/responsive/responsiveHooks.js +6 -0
  129. package/src/theme/ThemeHandler.js +6 -3
  130. package/src/theme/default/base.js +16 -4
  131. package/src/theme/default/blackTheme.js +1 -0
  132. package/src/theme/default/cyberpunkTheme.js +10 -0
  133. package/src/theme/default/darkTheme.js +1 -0
  134. package/src/theme/default/hackerTheme.js +17 -3
  135. package/src/theme/default/lightTheme.js +1 -0
  136. package/src/theme/default/msdosTheme.js +9 -10
  137. package/src/theme/default/paperTheme.js +10 -0
@@ -6,3 +6,4 @@ export * from './FormGroup'
6
6
  export * from './useNewForm'
7
7
  export * from './useWatch'
8
8
  export * from './SubmitButton'
9
+ export * from './validation'
@@ -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 = Array.isArray(name) ? name.join('.') : name
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
- const getError = (name) => path(name, errorsRef.current)
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
- errorsRef.current = assocPath(name, error, errorsRef.current)
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 = Array.isArray(name) ? name.join('.') : name
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 validateForm = () => {
42
- if (!validate) return true
43
- const newErrors = validate(valuesRef.current) || {}
44
- errorsRef.current = newErrors
45
- return Object.keys(newErrors).length === 0
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
+ }
@@ -14,3 +14,4 @@ export * from './calendar'
14
14
  export * from './list'
15
15
  export * from './tabs'
16
16
  export * from './theme'
17
+ export * from './sections'
@@ -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="close-circle-fill" size={sizeCode} red />
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 { FlatList } from '../list/FlatList'
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
- <FlatList
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 = { ...pickerProps, labelKey, valueKey, useRawOption, multiple, onEndReached, renderFooter, renderHeader }
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
- const { option, labelKey, selected } = params
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
- // useColorConverter(),
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 } = formattedProps
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
  )}