@neko-os/ui 0.0.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.
Files changed (44) hide show
  1. package/README.md +79 -0
  2. package/dist/form/Form.js +1 -0
  3. package/dist/form/FormGroup.js +1 -0
  4. package/dist/form/FormItem.js +1 -0
  5. package/dist/form/FormList.js +1 -0
  6. package/dist/form/FormWrapperComponent.js +1 -0
  7. package/dist/form/FormWrapperComponent.native.js +1 -0
  8. package/dist/form/index.js +1 -0
  9. package/dist/form/useForm.js +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/modifiers/_helpers.js +1 -0
  12. package/dist/modifiers/flex.js +1 -0
  13. package/dist/modifiers/flexWrapper.js +1 -0
  14. package/dist/modifiers/margin.js +1 -0
  15. package/dist/modifiers/padding.js +1 -0
  16. package/dist/modifiers/position.js +1 -0
  17. package/dist/modifiers/size.js +1 -0
  18. package/dist/modifiers/text.js +1 -0
  19. package/dist/structure/View.js +1 -0
  20. package/dist/structure/index.js +1 -0
  21. package/dist/theme/ThemeHandler.js +1 -0
  22. package/dist/theme/index.js +1 -0
  23. package/package.json +37 -0
  24. package/src/form/Form.js +14 -0
  25. package/src/form/FormGroup.js +21 -0
  26. package/src/form/FormItem.js +34 -0
  27. package/src/form/FormList.js +88 -0
  28. package/src/form/FormWrapperComponent.js +8 -0
  29. package/src/form/FormWrapperComponent.native.js +5 -0
  30. package/src/form/index.js +6 -0
  31. package/src/form/useForm.js +65 -0
  32. package/src/index.js +4 -0
  33. package/src/modifiers/_helpers.js +3 -0
  34. package/src/modifiers/flex.js +17 -0
  35. package/src/modifiers/flexWrapper.js +39 -0
  36. package/src/modifiers/margin.js +20 -0
  37. package/src/modifiers/padding.js +20 -0
  38. package/src/modifiers/position.js +18 -0
  39. package/src/modifiers/size.js +14 -0
  40. package/src/modifiers/text.js +36 -0
  41. package/src/structure/View.js +23 -0
  42. package/src/structure/index.js +1 -0
  43. package/src/theme/ThemeHandler.js +10 -0
  44. package/src/theme/index.js +1 -0
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # Neko UI
2
+
3
+ Universal React Native UI library (works on react-native, react-native-web and react).
4
+
5
+ ## Install
6
+
7
+ ```
8
+ yarn add @neko-os/ui
9
+ ```
10
+
11
+ ### Web or native-web
12
+
13
+ ```
14
+ yarn add react-native-web
15
+ ```
16
+
17
+ ### Vite config
18
+
19
+ vite.config.js:
20
+
21
+ ```js
22
+ import { defineConfig } from 'vite'
23
+ import react from '@vitejs/plugin-react'
24
+ import path from 'path'
25
+
26
+ export default defineConfig({
27
+ plugins: [react()],
28
+ resolve: {
29
+ alias: {
30
+ 'react-native': 'react-native-web',
31
+ 'react-native-svg': 'react-native-svg-web',
32
+ },
33
+ extensions: ['.web.js', '.js', '.jsx', '.json'],
34
+ },
35
+ })
36
+ ```
37
+
38
+ ## Usage (TODO)
39
+
40
+ ```js
41
+ import { SimpleLinesChart } from '@neko-os/ui
42
+
43
+ const DATA = [
44
+ {
45
+ serie: 'A',
46
+ data: [
47
+ { x: 'Jan', y: 50 },
48
+ { x: 'Feb', y: 80 },
49
+ { x: 'Mar', y: 40 },
50
+ { x: 'Apr', y: 120 },
51
+ { x: 'May', y: 70 },
52
+ ],
53
+ },
54
+ {
55
+ serie: 'B',
56
+ data: [
57
+ { x: 'Jan', y: 30 },
58
+ { x: 'Feb', y: 60 },
59
+ { x: 'Mar', y: 90 },
60
+ { x: 'Apr', y: 75 },
61
+ { x: 'May', y: 100 },
62
+ ],
63
+ },
64
+
65
+ {
66
+ serie: 'C',
67
+ data: [
68
+ { x: 'Jan', y: 3 },
69
+ { x: 'Feb', y: 6 },
70
+ { x: 'Mar', y: 9 },
71
+ { x: 'Apr', y: 7 },
72
+ { x: 'May', y: 10 },
73
+ ],
74
+ },
75
+ ]
76
+
77
+
78
+ <SimpleLinesChart data={DATA} />
79
+ ```
@@ -0,0 +1 @@
1
+ var _jsxFileName="/Users/christianstorch/Apps/nekoapps/libs/neko-ui/src/form/Form.js";import React from'react';import{FormWrapperComponent}from"./FormWrapperComponent";import{jsx as _jsx}from"react/jsx-runtime";var FormContext=React.createContext(null);export var useFormInstance=function useFormInstance(){return React.useContext(FormContext);};export function Form(_ref){var form=_ref.form,children=_ref.children;return _jsx(FormContext.Provider,{value:form,children:_jsx(FormWrapperComponent,{form:form,children:children})});}
@@ -0,0 +1 @@
1
+ import _toConsumableArray from"@babel/runtime/helpers/toConsumableArray";var _jsxFileName="/Users/christianstorch/Apps/nekoapps/libs/neko-ui/src/form/FormGroup.js";import React from'react';import{jsx as _jsx}from"react/jsx-runtime";var FormGroupContext=React.createContext(null);var useGroupPath=function useGroupPath(){var _React$useContext;return((_React$useContext=React.useContext(FormGroupContext))==null?void 0:_React$useContext.path)||[];};export function useRelativePath(name,opts){var relative=opts.relative;var listPath=!!name?Array.isArray(name)?name:[name]:[];var parentPath=useGroupPath();if(!relative)return listPath;return[].concat(_toConsumableArray(parentPath),_toConsumableArray(listPath));}export function FormGroup(_ref){var name=_ref.name;var path=useRelativePath(name,{relative:true});var value={path:path};return _jsx(FormGroupContext.Provider,{value:value,children:children});}
@@ -0,0 +1 @@
1
+ import _slicedToArray from"@babel/runtime/helpers/slicedToArray";var _jsxFileName="/Users/christianstorch/Apps/nekoapps/libs/neko-ui/src/form/FormItem.js";import{Text}from"react-native-web";import React from'react';import{FormGroup,useRelativePath}from"./FormGroup";import{useFormInstance}from"./Form";import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";export function FormItem(_ref){var name=_ref.name,relative=_ref.relative,children=_ref.children;var form=useFormInstance();var listPath=useRelativePath(name,{relative:relative});var _React$useState=React.useState(form.getFieldValue(listPath)),_React$useState2=_slicedToArray(_React$useState,2),value=_React$useState2[0],setValue=_React$useState2[1];var error=form.getError(listPath);React.useEffect(function(){return form.registerListener(listPath,function(val){return setValue(val);});},[listPath.join('$NEKOJOIN$')]);var handleChange=function handleChange(e){var _e$target$value,_e$target;var val=(_e$target$value=e==null?void 0:(_e$target=e.target)==null?void 0:_e$target.value)!=null?_e$target$value:e;form.setFieldValue(listPath,val);};var child=React.Children.only(children);var childWithProps=React.cloneElement(child,{value:value,onChange:handleChange});return _jsxs(FormGroup,{name:listPath,children:[childWithProps,error&&_jsx(Text,{style:{color:'red'},children:error})]});}
@@ -0,0 +1 @@
1
+ import _toConsumableArray from"@babel/runtime/helpers/toConsumableArray";import _slicedToArray from"@babel/runtime/helpers/slicedToArray";var _jsxFileName="/Users/christianstorch/Apps/nekoapps/libs/neko-ui/src/form/FormList.js";import{Text}from"react-native-web";import React from'react';import{FormGroup,useRelativePath}from"./FormGroup";import{useFormInstance}from"./Form";import{jsx as _jsx,jsxs as _jsxs}from"react/jsx-runtime";var FormListContext=React.createContext(null);var useFormList=function useFormList(){return React.useContext(FormListContext);};export function FormList(_ref){var name=_ref.name,relative=_ref.relative,children=_ref.children;var form=useFormInstance();var listPath=useRelativePath(name,{relative:relative});var listPathStr=listPath.join('$NEKOJOIN$');var error=form.getError(listPath);var _React$useState=React.useState(function(){var initial=form.getFieldValue(listPath)||[];return initial.map(function(_,index){return{key:index,name:index};});}),_React$useState2=_slicedToArray(_React$useState,2),fields=_React$useState2[0],setFields=_React$useState2[1];React.useEffect(function(){return form.registerListener(listPath,function(val){if(Array.isArray(val)){setFields(val.map(function(_,index){return{key:index,name:index};}));}});},[listPathStr]);var add=function add(){var defaultValue=arguments.length>0&&arguments[0]!==undefined?arguments[0]:{};var current=form.getFieldValue(listPath)||[];form.setFieldValue(listPath,[].concat(_toConsumableArray(current),[defaultValue]));};var addOn=function addOn(index){var defaultValue=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};var current=form.getFieldValue(listPath)||[];form.setFieldValue(listPath,[].concat(_toConsumableArray(current.slice(0,index)),[defaultValue],_toConsumableArray(current.slice(index))));};var replace=function replace(index,value){var current=form.getFieldValue(listPath)||[];form.setFieldValue(listPath,current.map(function(item,i){return i===index?value:item;}));};var move=function move(fromIndex,toIndex){var current=form.getFieldValue(listPath)||[];if(fromIndex<0||fromIndex>=current.length)return;if(toIndex<0||toIndex>=current.length)return;var item=current[fromIndex];var updated=_toConsumableArray(current);updated.splice(fromIndex,1);updated.splice(toIndex,0,item);form.setFieldValue(listPath,updated);};var remove=function remove(index){var current=form.getFieldValue(listPath)||[];form.setFieldValue(listPath,current.filter(function(_,i){return i!==index;}));};var actions=React.useMemo(function(){return{add:add,addOn:addOn,replace:replace,remove:remove,move:move};},[listPathStr]);return _jsx(FormGroup,{name:listPath,children:_jsxs(FormListContext.Provider,{value:actions,children:[typeof children==='function'?children(fields,actions):React.cloneElement(React.Children.only(children),Object.assign({fields:fields},actions)),error&&_jsx(Text,{style:{color:'red'},children:error})]})});}
@@ -0,0 +1 @@
1
+ import _objectWithoutProperties from"@babel/runtime/helpers/objectWithoutProperties";var _jsxFileName="/Users/christianstorch/Apps/nekoapps/libs/neko-ui/src/form/FormWrapperComponent.js";var _excluded=["children","form"];import{jsx as _jsx}from"react/jsx-runtime";export function FormWrapperComponent(_ref){var children=_ref.children,form=_ref.form,props=_objectWithoutProperties(_ref,_excluded);var handleSubmit=function handleSubmit(e){e.preventDefault();form.handleSubmit();};return _jsx("form",{onSubmit:handleSubmit,children:children});}
@@ -0,0 +1 @@
1
+ import _objectWithoutProperties from"@babel/runtime/helpers/objectWithoutProperties";var _excluded=["children"];import React from'react';export function FormWrapperComponent(_ref){var children=_ref.children,props=_objectWithoutProperties(_ref,_excluded);return children;}
@@ -0,0 +1 @@
1
+ export*from"./Form";export*from"./FormItem";export*from"./FormList";export*from"./FormWrapperComponent";export*from"./FormGroup";export*from"./useForm";
@@ -0,0 +1 @@
1
+ import{assocPath,path}from'ramda';import React from'react';export function useForm(){var _ref=arguments.length>0&&arguments[0]!==undefined?arguments[0]:{},_ref$initialValues=_ref.initialValues,initialValues=_ref$initialValues===void 0?{}:_ref$initialValues,validate=_ref.validate;var valuesRef=React.useRef(Object.assign({},initialValues));var errorsRef=React.useRef({});var listenersRef=React.useRef({});var notify=function notify(name){var key=Array.isArray(name)?name.join('.'):name;if(listenersRef.current[key]){listenersRef.current[key].forEach(function(cb){return cb(path(name,valuesRef.current));});}};var setFieldValue=function setFieldValue(name,value){valuesRef.current=assocPath(name,value,valuesRef.current);notify(name);};var getFieldValue=function getFieldValue(name){return path(name,valuesRef.current);};var getError=function getError(name){return path(name,errorsRef.current);};var setError=function setError(name,error){errorsRef.current=assocPath(name,error,errorsRef.current);};var registerListener=function registerListener(name,cb){var key=Array.isArray(name)?name.join('.'):name;if(!listenersRef.current[key]){listenersRef.current[key]=[];}listenersRef.current[key].push(cb);return function(){listenersRef.current[key]=listenersRef.current[key].filter(function(fn){return fn!==cb;});};};var validateForm=function validateForm(){if(!validate)return true;var newErrors=validate(valuesRef.current)||{};errorsRef.current=newErrors;return Object.keys(newErrors).length===0;};var handleSubmit=function handleSubmit(onValid){return function(){var isValid=validateForm();if(isValid){onValid(valuesRef.current);}};};return{setFieldValue:setFieldValue,getFieldValue:getFieldValue,getError:getError,setError:setError,registerListener:registerListener,handleSubmit:handleSubmit,valuesRef:valuesRef};}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export*from"./structure";export var version=2;
@@ -0,0 +1 @@
1
+ import{pickBy}from'ramda';export var clearProps=pickBy(function(item){return item!==undefined;});
@@ -0,0 +1 @@
1
+ import{clearProps}from"./_helpers";export function useFlexModifier(props){var flex=props.flex;if(flex===true)flex=1;var style=clearProps({flex:flex});return Object.assign({},props,{style:Object.assign({},props.style,style)});}
@@ -0,0 +1 @@
1
+ import{clearProps}from"./_helpers";export function useFlexWrapperModifier(props){var justify=props.justify,align=props.align,center=props.center,centerV=props.centerV,centerH=props.centerH,toRight=props.toRight,toBottom=props.toBottom,direction=props.direction,row=props.row,wrap=props.wrap,gap=props.gap;var justifyContent=justify;var alignItems=align;var flexDirection=direction;var flexWrap;if(center){justifyContent='center';alignItems='center';}else{if(centerV)justifyContent='center';if(toBottom)justifyContent='flex-end';if(centerH)alignItems='center';if(toRight)alignItems='flex-end';}if(row)flexDirection='row';if(wrap)flexWrap='wrap';var style=clearProps({justifyContent:justifyContent,alignItems:alignItems,flexDirection:flexDirection,flexWrap:flexWrap,gap:gap});return Object.assign({},props,{style:Object.assign({},props.style,style)});}
@@ -0,0 +1 @@
1
+ import{clearProps}from"./_helpers";export function useMarginModifier(props){var _ref,_ref2,_ref3,_ref4;var marginT=props.marginT,marginB=props.marginB,marginL=props.marginL,marginR=props.marginR,marginV=props.marginV,marginH=props.marginH,margin=props.margin;var marginTop=(_ref=marginT!=null?marginT:marginV)!=null?_ref:margin;var marginBottom=(_ref2=marginB!=null?marginB:marginV)!=null?_ref2:margin;var marginRight=(_ref3=marginR!=null?marginR:marginH)!=null?_ref3:margin;var marginLeft=(_ref4=marginL!=null?marginL:marginH)!=null?_ref4:margin;var style=clearProps({marginTop:marginTop,marginBottom:marginBottom,marginRight:marginRight,marginLeft:marginLeft});return Object.assign({},props,{style:Object.assign({},props.style,style)});}
@@ -0,0 +1 @@
1
+ import{clearProps}from"./_helpers";export function usePaddingModifier(props){var _ref,_ref2,_ref3,_ref4;var paddingT=props.paddingT,paddingB=props.paddingB,paddingL=props.paddingL,paddingR=props.paddingR,paddingV=props.paddingV,paddingH=props.paddingH,padding=props.padding;var paddingTop=(_ref=paddingT!=null?paddingT:paddingV)!=null?_ref:padding;var paddingBottom=(_ref2=paddingB!=null?paddingB:paddingV)!=null?_ref2:padding;var paddingRight=(_ref3=paddingR!=null?paddingR:paddingH)!=null?_ref3:padding;var paddingLeft=(_ref4=paddingL!=null?paddingL:paddingH)!=null?_ref4:padding;var style=clearProps({paddingTop:paddingTop,paddingBottom:paddingBottom,paddingRight:paddingRight,paddingLeft:paddingLeft});return Object.assign({},props,{style:Object.assign({},props.style,style)});}
@@ -0,0 +1 @@
1
+ import{clearProps}from"./_helpers";export function usePositionModifier(props){var position=props.position,absolute=props.absolute,relative=props.relative,top=props.top,bottom=props.bottom,left=props.left,right=props.right;if(absolute)position='absolute';if(relative)position='relative';var style=clearProps({position:position,top:top,bottom:bottom,left:left,right:right});return Object.assign({},props,{style:Object.assign({},props.style,style)});}
@@ -0,0 +1 @@
1
+ import{clearProps}from"./_helpers";export function useSizeModifier(props){var _ref=props||{},width=_ref.width,height=_ref.height,fullWidth=_ref.fullWidth,fullHeight=_ref.fullHeight;if(fullWidth)width='100%';if(fullHeight)height='100%';var style=clearProps({height:height,width:width});return Object.assign({},props,{style:Object.assign({},props.style,style)});}
@@ -0,0 +1 @@
1
+ import{clearProps}from"./_helpers";export function useTextModifier(props){var opacity=props.opacity,bold=props.bold,strong=props.strong,fontWeight=props.fontWeight,italic=props.italic,underline=props.underline,lineHeight=props.lineHeight,align=props.align,center=props.center,toRight=props.toRight;var fontStyle;if(italic)fontStyle='italic';var textDecorationLine;if(underline)textDecorationLine='underline';var textAlign=align;if(center)textAlign='center';if(toRight)textAlign='right';if(bold||strong)fontWeight=600;var style=clearProps({fontWeight:fontWeight,fontStyle:fontStyle,textDecorationLine:textDecorationLine,lineHeight:lineHeight,textAlign:textAlign,opacity:opacity});return Object.assign({},props,{style:Object.assign({},props.style,style)});}
@@ -0,0 +1 @@
1
+ var _jsxFileName="/Users/christianstorch/Apps/nekoapps/libs/neko-ui/src/structure/View.js";import React from'react';import{View as NativeView}from"react-native-web";import{pipe}from'ramda';import{useFlexModifier}from"../modifiers/flex";import{useFlexWrapperModifier}from"../modifiers/flexWrapper";import{useMarginModifier}from"../modifiers/margin";import{usePaddingModifier}from"../modifiers/padding";import{usePositionModifier}from"../modifiers/position";import{useSizeModifier}from"../modifiers/size";import{jsx as _jsx}from"react/jsx-runtime";export function View(props){props=pipe(useSizeModifier,usePositionModifier,usePaddingModifier,useMarginModifier,useFlexWrapperModifier,useFlexModifier);return _jsx(NativeView,Object.assign({},props));}
@@ -0,0 +1 @@
1
+ export*from"./View";
@@ -0,0 +1 @@
1
+ var _jsxFileName="/Users/christianstorch/Apps/nekoapps/libs/neko-ui/src/theme/ThemeHandler.js";import React from'react';import{jsx as _jsx}from"react/jsx-runtime";var ThemeContext=React.createContext(null);export var useThemeHandler=function useThemeHandler(){return React.useContext(ThemeContext)||{};};export function ThemeHandler(_ref){var themes=_ref.themes,children=_ref.children;var value={};return _jsx(ThemeContext.Provider,{value:value,children:children});}
@@ -0,0 +1 @@
1
+ export*from"./ThemeHandler";
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@neko-os/ui",
3
+ "version": "0.0.1",
4
+ "author": "Christian Storch <ccstorch@gmail.com>",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "react-native": "src/index.js",
8
+ "files": ["dist", "src"],
9
+ "scripts": {
10
+ "build": "rm -rf dist && babel src --out-dir dist --extensions \".js,.jsx\" --copy-files",
11
+ "watch": "babel src --out-dir dist --extensions \".js,.jsx\" --copy-files --watch",
12
+ "dev": "yarn build && yarn watch",
13
+ "prepublishOnly": "yarn build",
14
+ "publish": "npm publish --access public"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "peerDependencies": {
20
+ "dayjs": "^1.11.13",
21
+ "ramda": "^0.31.3",
22
+ "react": "*",
23
+ "react-native": "*"
24
+ },
25
+ "dependencies": {
26
+ "dayjs": "^1.11.13",
27
+ "prop-types": "^15.8.1",
28
+ "ramda": "^0.31.3",
29
+ "react-native-web": "^0.21.0"
30
+ },
31
+ "devDependencies": {
32
+ "@babel/cli": "^7",
33
+ "@babel/core": "^7",
34
+ "babel-plugin-module-resolver": "^5.0.2",
35
+ "metro-react-native-babel-preset": "^0.77.0"
36
+ }
37
+ }
@@ -0,0 +1,14 @@
1
+ import React from 'react'
2
+
3
+ import { FormWrapperComponent } from './FormWrapperComponent'
4
+
5
+ const FormContext = React.createContext(null)
6
+ export const useFormInstance = () => React.useContext(FormContext)
7
+
8
+ export function Form({ form, children }) {
9
+ return (
10
+ <FormContext.Provider value={form}>
11
+ <FormWrapperComponent form={form}>{children}</FormWrapperComponent>
12
+ </FormContext.Provider>
13
+ )
14
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react'
2
+
3
+ const FormGroupContext = React.createContext(null)
4
+ const useGroupPath = () => React.useContext(FormGroupContext)?.path || []
5
+
6
+ export function useRelativePath(name, opts) {
7
+ const { relative } = opts
8
+ const listPath = !!name ? (Array.isArray(name) ? name : [name]) : []
9
+ const parentPath = useGroupPath()
10
+
11
+ if (!relative) return listPath
12
+
13
+ return [...parentPath, ...listPath]
14
+ }
15
+
16
+ export function FormGroup({ name }) {
17
+ const path = useRelativePath(name, { relative: true })
18
+ const value = { path }
19
+
20
+ return <FormGroupContext.Provider value={value}>{children}</FormGroupContext.Provider>
21
+ }
@@ -0,0 +1,34 @@
1
+ import { Text } from 'react-native'
2
+ import React from 'react'
3
+
4
+ import { FormGroup, useRelativePath } from './FormGroup'
5
+ import { useFormInstance } from './Form'
6
+
7
+ export function FormItem({ name, relative, children }) {
8
+ const form = useFormInstance()
9
+ const listPath = useRelativePath(name, { relative })
10
+ const [value, setValue] = React.useState(form.getFieldValue(listPath))
11
+ const error = form.getError(listPath)
12
+
13
+ React.useEffect(() => {
14
+ return form.registerListener(listPath, (val) => setValue(val))
15
+ }, [listPath.join('$NEKOJOIN$')])
16
+
17
+ const handleChange = (e) => {
18
+ const val = e?.target?.value ?? e
19
+ form.setFieldValue(listPath, val)
20
+ }
21
+
22
+ const child = React.Children.only(children)
23
+ const childWithProps = React.cloneElement(child, {
24
+ value,
25
+ onChange: handleChange,
26
+ })
27
+
28
+ return (
29
+ <FormGroup name={listPath}>
30
+ {childWithProps}
31
+ {error && <Text style={{ color: 'red' }}>{error}</Text>}
32
+ </FormGroup>
33
+ )
34
+ }
@@ -0,0 +1,88 @@
1
+ import { Text } from 'react-native'
2
+ import React from 'react'
3
+
4
+ import { FormGroup, useRelativePath } from './FormGroup'
5
+ import { useFormInstance } from './Form'
6
+
7
+ const FormListContext = React.createContext(null)
8
+ const useFormList = () => React.useContext(FormListContext)
9
+
10
+ export function FormList({ name, relative, children }) {
11
+ const form = useFormInstance()
12
+ const listPath = useRelativePath(name, { relative })
13
+ // To avoid watch being recalled
14
+ const listPathStr = listPath.join('$NEKOJOIN$')
15
+ const error = form.getError(listPath)
16
+
17
+ const [fields, setFields] = React.useState(() => {
18
+ const initial = form.getFieldValue(listPath) || []
19
+ return initial.map((_, index) => ({ key: index, name: index }))
20
+ })
21
+
22
+ React.useEffect(() => {
23
+ return form.registerListener(listPath, (val) => {
24
+ if (Array.isArray(val)) {
25
+ setFields(val.map((_, index) => ({ key: index, name: index })))
26
+ }
27
+ })
28
+ }, [listPathStr])
29
+
30
+ const add = (defaultValue = {}) => {
31
+ const current = form.getFieldValue(listPath) || []
32
+ form.setFieldValue(listPath, [...current, defaultValue])
33
+ }
34
+
35
+ const addOn = (index, defaultValue = {}) => {
36
+ const current = form.getFieldValue(listPath) || []
37
+ form.setFieldValue(listPath, [...current.slice(0, index), defaultValue, ...current.slice(index)])
38
+ }
39
+
40
+ const replace = (index, value) => {
41
+ const current = form.getFieldValue(listPath) || []
42
+ form.setFieldValue(
43
+ listPath,
44
+ current.map((item, i) => (i === index ? value : item))
45
+ )
46
+ }
47
+
48
+ const move = (fromIndex, toIndex) => {
49
+ const current = form.getFieldValue(listPath) || []
50
+ if (fromIndex < 0 || fromIndex >= current.length) return
51
+ if (toIndex < 0 || toIndex >= current.length) return
52
+ const item = current[fromIndex]
53
+ const updated = [...current]
54
+ updated.splice(fromIndex, 1)
55
+ updated.splice(toIndex, 0, item)
56
+ form.setFieldValue(listPath, updated)
57
+ }
58
+
59
+ const remove = (index) => {
60
+ const current = form.getFieldValue(listPath) || []
61
+ form.setFieldValue(
62
+ listPath,
63
+ current.filter((_, i) => i !== index)
64
+ )
65
+ }
66
+
67
+ const actions = React.useMemo(
68
+ () => ({
69
+ add,
70
+ addOn,
71
+ replace,
72
+ remove,
73
+ move,
74
+ }),
75
+ [listPathStr]
76
+ )
77
+
78
+ return (
79
+ <FormGroup name={listPath}>
80
+ <FormListContext.Provider value={actions}>
81
+ {typeof children === 'function'
82
+ ? children(fields, actions)
83
+ : React.cloneElement(React.Children.only(children), { fields, ...actions })}
84
+ {error && <Text style={{ color: 'red' }}>{error}</Text>}
85
+ </FormListContext.Provider>
86
+ </FormGroup>
87
+ )
88
+ }
@@ -0,0 +1,8 @@
1
+ export function FormWrapperComponent({ children, form, ...props }) {
2
+ const handleSubmit = (e) => {
3
+ e.preventDefault()
4
+ form.handleSubmit()
5
+ }
6
+
7
+ return <form onSubmit={handleSubmit}>{children}</form>
8
+ }
@@ -0,0 +1,5 @@
1
+ import React from 'react'
2
+
3
+ export function FormWrapperComponent({ children, ...props }) {
4
+ return children
5
+ }
@@ -0,0 +1,6 @@
1
+ export * from './Form'
2
+ export * from './FormItem'
3
+ export * from './FormList'
4
+ export * from './FormWrapperComponent'
5
+ export * from './FormGroup'
6
+ export * from './useForm'
@@ -0,0 +1,65 @@
1
+ import { assocPath, path } from 'ramda'
2
+ import React from 'react'
3
+
4
+ export function useForm({ initialValues = {}, validate } = {}) {
5
+ const valuesRef = React.useRef({ ...initialValues })
6
+ const errorsRef = React.useRef({})
7
+ const listenersRef = React.useRef({})
8
+
9
+ const notify = (name) => {
10
+ const key = Array.isArray(name) ? name.join('.') : name
11
+ if (listenersRef.current[key]) {
12
+ listenersRef.current[key].forEach((cb) => cb(path(name, valuesRef.current)))
13
+ }
14
+ }
15
+
16
+ const setFieldValue = (name, value) => {
17
+ valuesRef.current = assocPath(name, value, valuesRef.current)
18
+ notify(name)
19
+ }
20
+
21
+ const getFieldValue = (name) => path(name, valuesRef.current)
22
+
23
+ const getError = (name) => path(name, errorsRef.current)
24
+
25
+ const setError = (name, error) => {
26
+ errorsRef.current = assocPath(name, error, errorsRef.current)
27
+ }
28
+
29
+ const registerListener = (name, cb) => {
30
+ const key = Array.isArray(name) ? name.join('.') : name
31
+ if (!listenersRef.current[key]) {
32
+ listenersRef.current[key] = []
33
+ }
34
+ listenersRef.current[key].push(cb)
35
+ return () => {
36
+ listenersRef.current[key] = listenersRef.current[key].filter((fn) => fn !== cb)
37
+ }
38
+ }
39
+
40
+ const validateForm = () => {
41
+ if (!validate) return true
42
+ const newErrors = validate(valuesRef.current) || {}
43
+ errorsRef.current = newErrors
44
+ return Object.keys(newErrors).length === 0
45
+ }
46
+
47
+ const handleSubmit = (onValid) => {
48
+ return () => {
49
+ const isValid = validateForm()
50
+ if (isValid) {
51
+ onValid(valuesRef.current)
52
+ }
53
+ }
54
+ }
55
+
56
+ return {
57
+ setFieldValue,
58
+ getFieldValue,
59
+ getError,
60
+ setError,
61
+ registerListener,
62
+ handleSubmit,
63
+ valuesRef,
64
+ }
65
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // export * from './form'
2
+ export * from './structure'
3
+
4
+ export const version = 2
@@ -0,0 +1,3 @@
1
+ import { pickBy } from 'ramda'
2
+
3
+ export const clearProps = pickBy((item) => item !== undefined)
@@ -0,0 +1,17 @@
1
+ import { clearProps } from './_helpers'
2
+
3
+ export function useFlexModifier(props) {
4
+ let { flex } = props
5
+
6
+ if (flex === true) flex = 1
7
+
8
+ const style = clearProps({ flex })
9
+
10
+ return {
11
+ ...props,
12
+ style: {
13
+ ...props.style,
14
+ ...style,
15
+ },
16
+ }
17
+ }
@@ -0,0 +1,39 @@
1
+ import { clearProps } from './_helpers'
2
+
3
+ export function useFlexWrapperModifier(props) {
4
+ const { justify, align, center, centerV, centerH, toRight, toBottom, direction, row, wrap, gap } = props
5
+
6
+ let justifyContent = justify
7
+ let alignItems = align
8
+ let flexDirection = direction
9
+ let flexWrap
10
+
11
+ if (center) {
12
+ justifyContent = 'center'
13
+ alignItems = 'center'
14
+ } else {
15
+ if (centerV) justifyContent = 'center'
16
+ if (toBottom) justifyContent = 'flex-end'
17
+ if (centerH) alignItems = 'center'
18
+ if (toRight) alignItems = 'flex-end'
19
+ }
20
+
21
+ if (row) flexDirection = 'row'
22
+ if (wrap) flexWrap = 'wrap'
23
+
24
+ const style = clearProps({
25
+ justifyContent,
26
+ alignItems,
27
+ flexDirection,
28
+ flexWrap,
29
+ gap,
30
+ })
31
+
32
+ return {
33
+ ...props,
34
+ style: {
35
+ ...props.style,
36
+ ...style,
37
+ },
38
+ }
39
+ }
@@ -0,0 +1,20 @@
1
+ import { clearProps } from './_helpers'
2
+
3
+ export function useMarginModifier(props) {
4
+ const { marginT, marginB, marginL, marginR, marginV, marginH, margin } = props
5
+
6
+ const marginTop = marginT ?? marginV ?? margin
7
+ const marginBottom = marginB ?? marginV ?? margin
8
+ const marginRight = marginR ?? marginH ?? margin
9
+ const marginLeft = marginL ?? marginH ?? margin
10
+
11
+ const style = clearProps({ marginTop, marginBottom, marginRight, marginLeft })
12
+
13
+ return {
14
+ ...props,
15
+ style: {
16
+ ...props.style,
17
+ ...style,
18
+ },
19
+ }
20
+ }
@@ -0,0 +1,20 @@
1
+ import { clearProps } from './_helpers'
2
+
3
+ export function usePaddingModifier(props) {
4
+ const { paddingT, paddingB, paddingL, paddingR, paddingV, paddingH, padding } = props
5
+
6
+ const paddingTop = paddingT ?? paddingV ?? padding
7
+ const paddingBottom = paddingB ?? paddingV ?? padding
8
+ const paddingRight = paddingR ?? paddingH ?? padding
9
+ const paddingLeft = paddingL ?? paddingH ?? padding
10
+
11
+ const style = clearProps({ paddingTop, paddingBottom, paddingRight, paddingLeft })
12
+
13
+ return {
14
+ ...props,
15
+ style: {
16
+ ...props.style,
17
+ ...style,
18
+ },
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ import { clearProps } from './_helpers'
2
+
3
+ export function usePositionModifier(props) {
4
+ let { position, absolute, relative, top, bottom, left, right } = props
5
+
6
+ if (absolute) position = 'absolute'
7
+ if (relative) position = 'relative'
8
+
9
+ const style = clearProps({ position, top, bottom, left, right })
10
+
11
+ return {
12
+ ...props,
13
+ style: {
14
+ ...props.style,
15
+ ...style,
16
+ },
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ import { clearProps } from './_helpers'
2
+
3
+ export function useSizeModifier(props) {
4
+ let { width, height, fullWidth, fullHeight } = props || {}
5
+ if (fullWidth) width = '100%'
6
+ if (fullHeight) height = '100%'
7
+
8
+ const style = clearProps({ height, width })
9
+
10
+ return {
11
+ ...props,
12
+ style: { ...props.style, ...style },
13
+ }
14
+ }
@@ -0,0 +1,36 @@
1
+ import { clearProps } from './_helpers'
2
+
3
+ export function useTextModifier(props) {
4
+ let { opacity, bold, strong, fontWeight, italic, underline, lineHeight, align, center, toRight } = props
5
+
6
+ let fontStyle
7
+ if (italic) fontStyle = 'italic'
8
+
9
+ let textDecorationLine
10
+ if (underline) textDecorationLine = 'underline'
11
+
12
+ let textAlign = align
13
+ if (center) textAlign = 'center'
14
+ if (toRight) textAlign = 'right'
15
+
16
+ if (bold || strong) fontWeight = 600
17
+
18
+ const style = clearProps({
19
+ fontWeight,
20
+ fontStyle,
21
+ textDecorationLine,
22
+ lineHeight,
23
+ textAlign,
24
+ opacity,
25
+ })
26
+
27
+ // TODO: Handle font sizes based on theme
28
+
29
+ return {
30
+ ...props,
31
+ style: {
32
+ ...props.style,
33
+ ...style,
34
+ },
35
+ }
36
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react'
2
+ import { View as NativeView } from 'react-native'
3
+ import { pipe } from 'ramda'
4
+
5
+ import { useFlexModifier } from '../modifiers/flex'
6
+ import { useFlexWrapperModifier } from '../modifiers/flexWrapper'
7
+ import { useMarginModifier } from '../modifiers/margin'
8
+ import { usePaddingModifier } from '../modifiers/padding'
9
+ import { usePositionModifier } from '../modifiers/position'
10
+ import { useSizeModifier } from '../modifiers/size'
11
+
12
+ export function View(props) {
13
+ props = pipe(
14
+ useSizeModifier, //
15
+ usePositionModifier,
16
+ usePaddingModifier,
17
+ useMarginModifier,
18
+ useFlexWrapperModifier,
19
+ useFlexModifier
20
+ )
21
+
22
+ return <NativeView {...props} />
23
+ }
@@ -0,0 +1 @@
1
+ export * from './View'
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+
3
+ const ThemeContext = React.createContext(null)
4
+ export const useThemeHandler = () => React.useContext(ThemeContext) || {}
5
+
6
+ export function ThemeHandler({ themes, children }) {
7
+ const value = {}
8
+
9
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
10
+ }
@@ -0,0 +1 @@
1
+ export * from './ThemeHandler'