@meonode/ui
Version:
A structured approach to component composition with built-in theming, prop separation, and dynamic children handling.
703 lines (619 loc) • 22.9 kB
Markdown
# @meonode/ui
[](https://www.npmjs.com/package/@meonode/ui)
[](https://opensource.org/licenses/MIT)
[](https://bundlephobia.com/package/@meonode/ui)
**Build React UIs with Type-Safe Fluency**
A structured approach to component composition with built-in theming, prop separation, and dynamic children handling.
```tsx
// Quick Start Example
import { Component, Div, H1, Button } from '@meonode/ui';
// Create a reusable styled component
const BlueButton = Component((props) =>
Button('Blue', {
padding: '12px 24px',
borderRadius: '8px',
backgroundColor: 'dodgerblue',
color: 'white',
...props // Merge with incoming props
})
);
// Compose your app
const App = Component(() =>
Div({
padding: '40px',
children: [
H1('Welcome to Meonode', { fontSize: '2rem' }),
BlueButton({
onClick: () => alert('Hello World!'),
children: 'Get Started'
})
]
})
);
```
## Why @meonode/ui?
- 🎯 **Type-Safe Design** - Full TypeScript support with autocomplete for styles, props, and theme paths
- 🎨 **Theme-Aware Styles** - Write styles directly in props with dynamic theme resolution
- 🧩 **Component-First Architecture** - Compose UIs from structured nodes instead of JSX
- 🌐 **Contextual Theming** - Theme values propagate automatically through nested components
- ⚡ **Optimized Bundle** - Efficient core with tree-shaking support
- 🔄 **Seamless React Integration** - Works with hooks, HOCs, and React 18+ features
## Installation
```bash
# Using npm
npm install @meonode/ui react
# Using yarn
yarn add @meonode/ui react
# Using pnpm
pnpm add @meonode/ui react
```
---
## Core Concepts
### 1. Component Creation
Create elements using either the `Node` factory or pre-built components:
```tsx
import { Node, Div, H1 } from '@meonode/ui';
// Method 1: Node factory for custom elements
const Card = Node('div', {
padding: '20px',
borderRadius: '8px',
boxShadow: '0 2px 12px rgba(0,0,0,0.1)'
});
// Method 2: Pre-built semantic components
const Header = () =>
Div({
padding: '20px',
backgroundColor: 'navy',
children: H1('App Header', { color: 'white' })
});
```
### 2. Theming System
Define themes and access values using dot-notation:
```tsx
// theme.ts
export const theme = {
colors: {
primary: '#2196F3',
text: {
primary: '#1A237E',
secondary: '#455A64'
}
},
spacing: {
md: '16px',
lg: '24px'
}
};
// ThemedComponent.tsx
import { Component, Div, H1, P } from '@meonode/ui';
import { theme } from './theme';
const ThemedCard = Component(() =>
Div({
theme, // Provide theme context
padding: 'theme.spacing.lg',
backgroundColor: 'theme.colors.primary',
children: [
H1('Themed Title', { color: 'theme.colors.text.primary' }),
P('Content...', { color: 'theme.colors.text.secondary' })
]
})
);
```
### 3. Prop Handling
Automatic separation of CSS props from DOM attributes:
```tsx
const ProfileCard = Component(({ user }) =>
Div({
// CSS Props
padding: '20px',
borderRadius: '8px',
// DOM Props
'aria-role': 'article',
tabIndex: 0,
// Children
children: `Welcome ${user.name}!`
})
);
```
---
## Key Features
| Feature | Description |
|----------------------|-----------------------------------------------------------------------------|
| **Smart Prop Merge** | CSS props merge with style objects; DOM props pass to underlying elements |
| **Theme Resolution** | `theme.` references resolve through component hierarchy |
| **Type Guards** | Autocomplete for CSS properties and theme paths with strict type checks |
| **HOC Support** | Wrap existing components with `Component()` for theme integration |
| **Dynamic Children** | Supports function-as-child pattern with automatic theme propagation |
---
## Advanced Patterns
### Component Composition
```tsx
const Dashboard = Component(() =>
Div({
display: 'grid',
gridTemplateColumns: '1fr 3fr',
gap: '20px',
children: [
Sidebar({ width: '240px' }),
MainContent({
padding: '40px',
children: AnalyticsChart({ dataset })
})
]
})
);
```
### Material UI Integration
```bash
yarn add @meonode/mui @mui/material
```
```tsx
import { Button, TextField } from '@meonode/mui';
const LoginForm = Component(() =>
Div({
maxWidth: '400px',
margin: '0 auto',
children: [
TextField({ label: 'Email', fullWidth: true }),
TextField({ label: 'Password', type: 'password' }),
Button({
variant: 'contained',
children: 'Sign In'
})
]
})
);
```
## Comprehensive Example: Theme-Switching & Conditional Components
```tsx
'use client'
/**
* This file showcases the integration of React hooks with `@meonode/ui` components
* for building declarative user interfaces. It demonstrates different rendering
* approaches, the use of Higher-Order Components (HOCs), and how theme context
* is managed and propagated within the @meonode/ui component tree.
*/
import {
Button,
Center,
Column,
Component,
Fixed,
Node,
type NodeInstance,
P,
Portal,
Row,
type Theme
} from '@meonode/ui'
import { useState, useEffect, ReactElement, ReactNode } from 'react'
import { CssBaseline, FormControlLabel, TextField } from '@meonode/mui'
import { Switch as MUISwitch } from '@mui/material'
import { styled } from '@mui/material'
// Styled Material UI Switch component for theme toggling
const MaterialUISwitch = styled(MUISwitch)(({ theme }) => ({
width: 62,
height: 34,
padding: 7,
'& .MuiSwitch-switchBase': {
margin: 1,
padding: 0,
transform: 'translateX(6px)',
'&.Mui-checked': {
color: '#fff',
transform: 'translateX(22px)',
'& .MuiSwitch-thumb:before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff',
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`,
},
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: '#aab4be',
...theme.applyStyles('dark', {
backgroundColor: '#8796A5',
}),
},
},
},
'& .MuiSwitch-thumb': {
backgroundColor: '#001e3c',
width: 32,
height: 32,
'&::before': {
content: "''",
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff',
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`,
},
...theme.applyStyles('dark', {
backgroundColor: '#003892',
}),
},
'& .MuiSwitch-track': {
opacity: 1,
backgroundColor: '#aab4be',
borderRadius: 20 / 2,
...theme.applyStyles('dark', {
backgroundColor: '#8796A5',
}),
},
}))
/**
* Light theme configuration containing color palette values.
* Used by `@meonode/ui` components when resolving theme references in light mode.
*/
const lightTheme: Theme = {
mode: 'light',
colors: {
primary: '#2563eb',
secondary: '#64748b',
accent: '#10b981',
background: '#ffffff',
foreground: '#0f172a',
border: '#e2e8f0',
muted: '#f8fafc',
success: '#16a34a',
warning: '#eab308',
danger: '#dc2626',
},
}
/**
* Dark theme configuration containing color palette values.
* Used by `@meonode/ui` components when resolving theme references in dark mode.
*/
const darkTheme: Theme = {
mode: 'dark',
colors: {
primary: '#3b82f6',
secondary: '#94a3b8',
accent: '#34d399',
background: '#0f172a',
foreground: '#f8fafc',
border: '#334155',
muted: '#1e293b',
success: '#22c55e',
warning: '#facc15',
danger: '#ef4444',
},
}
/**
* Main page component using React hooks for state management.
* Manages theme mode and visibility of additional content sections.
* Wrapped by `Component` HOC to ensure React compatibility and SSR/CSR support.
*/
export default Component(() => {
const [showMore, setShowDetails] = useState(false)
const [mode, setMode] = useState<'dark' | 'light'>('light')
const theme = mode === 'dark' ? darkTheme : lightTheme
return Column({
theme: theme.colors,
padding: 20,
gap: 15,
minHeight: '100vh',
backgroundColor: 'theme.background',
color: 'theme.foreground',
children: [
CssBaseline,
Center({
children: FormControlLabel({
control: Node(MaterialUISwitch).render() as ReactElement,
alignItems: 'center',
label: mode === 'dark' ? 'Dark Mode' : 'Light Mode',
labelPlacement: 'start',
checked: mode === 'dark',
onChange: () => setMode(prev => (prev === 'dark' ? 'light' : 'dark')),
}),
}),
Button('Show Modal', {
onClick: () => Modal({ theme: theme.colors }),
cursor: 'pointer',
userSelect: 'none',
padding: '10px 20px',
backgroundColor: 'theme.primary',
borderRadius: 5,
fontWeight: 'bold',
color: 'white',
}),
Button(showMore ? 'Hide Details' : 'Show More Details', {
onClick: () => setShowDetails(prev => !prev),
cursor: 'pointer',
userSelect: 'none',
padding: '10px 20px',
backgroundColor: 'theme.accent',
borderRadius: 5,
fontWeight: 'bold',
color: 'white',
}),
/**
* Component rendering examples demonstrating theme context propagation:
* - Direct Node instance rendering
* - Rendered Node instances
* - ReactNode components
* - HOC usage patterns
*/
DetailComponent({
info: 'Detail 1: Rendering a component that returns a @meonode/ui Node instance directly. The internal Row component correctly receives theme context from the parent Column.',
}),
DetailComponent({
info: 'Detail 2: Rendering a component that returns a @meonode/ui Node instance, then calling .render(). The internal Row component also correctly receives theme context.',
}).render(),
// ❌ Fails: The Node HOC expects the wrapped function to return a ReactNode, not a @meonode/ui Node instance.
// Node(DetailComponent, { info: 'Detail 3: Attempting to wrap a component returning a Node instance with Node HOC.' }),
ReturnRenderedDetailComponent({
info: 'Detail 4: Rendering a component that explicitly returns a ReactNode (.render() is called internally). The internal Row component correctly receives theme context from the parent Column.',
}),
Node(ReturnRenderedDetailComponent, {
info: "Detail 5: Wrapping a component returning ReactNode with Node HOC (without .render()). Renders successfully. However, the Node HOC does NOT propagate theme context to the wrapped component's children.",
}),
Node(ReturnRenderedDetailComponent, {
info: 'Detail 6: Wrapping a component returning ReactNode with Node HOC, then calling .render(). Renders successfully. Theme context is NOT propagated by the Node HOC.',
}).render(),
WrappedDetailComponent({
info: 'Detail 7: Using Component HOC with Node instance returns. Theme context is correctly propagated.',
}),
/**
* Conditional rendering examples (visible when showMore is true)
* Demonstrates:
* - Inline function wrappers
* - Component HOC usage
* - Theme context propagation patterns
*/
showMore &&
(() =>
DetailComponent({
info: 'Detail 8: Conditional rendering of a Node instance component using inline function wrapper. Theme context is correctly received.',
})),
showMore &&
(() =>
DetailComponent({
info: 'Detail 9: Conditional rendering of a Node instance component with .render() call. Theme context is correctly propagated.',
}).render()),
showMore &&
WrappedDetailComponent({
info: 'Detail 10: Conditional rendering using Component HOC with Node instance. Theme context is correctly propagated.',
}),
showMore &&
(() =>
ReturnRenderedDetailComponent({
info: 'Detail 11: Conditional rendering of ReactNode component using inline function. Theme context is properly propagated.',
})),
showMore &&
Component(() =>
ReturnRenderedDetailComponent({
info: 'Detail 12: Conditional rendering with Component HOC wrapping ReactNode component. Note that theme context is not propagated in this case.',
}),
),
// showMore && ReturnRenderedDetailComponent({
// info: 'Detail 15: Direct component call violates Rules of Hooks!'
// }), // ❌ Fails: Direct call to a component function using hooks inside render logic without a React-aware wrapper.
],
})
})
/**
* Styled detail section component returning a Node instance.
* Uses useEffect for lifecycle logging.
* Theme context is received from parent @meonode/ui components.
*/
const DetailComponent = ({ info }: { info: string }): NodeInstance => {
useEffect(() => {
console.log('DetailComponent mounted:', info)
return () => {
console.log('DetailComponent unmounted:', info)
}
}, [info])
return Row({
alignItems: 'center',
gap: 10,
padding: 4,
border: '2px solid theme.accent',
borderRadius: 6,
backgroundColor: 'theme.warning',
color: 'theme.danger',
children: [P(info, { flex: 1, padding: '0 20px' }), TextField({ flex: 1, sx: { background: 'theme.primary' } })],
})
}
/**
* Styled detail section component wrapped with Component HOC.
* Similar to DetailComponent but demonstrates HOC pattern.
* Theme context is correctly propagated through the HOC.
*/
const WrappedDetailComponent = Component(({ info }): NodeInstance => {
useEffect(() => {
console.log('DetailComponent mounted')
return () => {
console.log('DetailComponent unmounted')
}
}, [info])
return Row({
alignItems: 'center',
gap: 10,
padding: 4,
border: '2px solid theme.accent',
borderRadius: 6,
backgroundColor: 'theme.warning',
color: 'theme.danger',
children: [P(info, { flex: 1, padding: '0 20px' }), TextField({ flex: 1, sx: { background: 'theme.primary' } })],
})
})
/**
* Alternative detail component implementation returning ReactNode.
* Explicitly calls .render() for React compatibility.
* Demonstrates theme context usage with standard React patterns.
*/
const ReturnRenderedDetailComponent = ({ info }: { info: string }): ReactNode => {
useEffect(() => {
console.log('ReturnRenderedDetailComponent mounted')
return () => {
console.log('ReturnRenderedDetailComponent unmounted')
}
}, [info])
return Row({
alignItems: 'center',
gap: 10,
padding: 4,
border: '2px solid theme.accent',
borderRadius: 6,
backgroundColor: 'theme.warning',
color: 'theme.danger',
children: [P(info, { flex: 1, padding: '0 20px' }), TextField({ flex: 1, sx: { background: 'theme.primary' } })],
}).render()
}
/**
* Modal component using Portal HOC for DOM placement.
* Demonstrates theme-aware content rendering outside main hierarchy.
* Includes nested modal support and Material UI integration.
*/
const Modal = Portal(({ theme, portal }) => {
useEffect(() => {
console.log('Modal mounted')
return () => {
console.log('Modal unmounted')
}
}, [])
return Fixed({
theme,
top: 0,
left: 0,
right: 0,
bottom: 0,
display: 'flex',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
justifyContent: 'center',
alignItems: 'center',
onClick: e => {
if (e.target === e.currentTarget) {
portal.unmount()
}
},
children: [
Column({
width: '50%',
height: '80%',
backgroundColor: 'theme.background',
borderRadius: '8px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
padding: 10,
gap: 10,
color: 'theme.foreground',
children: [
Button('More Modal', {
onClick: () => Modal({ theme }),
cursor: 'pointer',
userSelect: 'none',
padding: '10px 20px',
backgroundColor: 'theme.primary',
borderRadius: 5,
fontWeight: 'bold',
color: 'white',
}),
Center({ fontWeight: 'bold', children: 'Modal' }),
Center({ children: Math.random() * 1000 }),
TextField({
sx: {
'& .MuiFormLabel-root': {
color: 'theme.foreground',
'&.Mui-focused': {
color: 'theme.foreground',
},
},
'& .MuiOutlinedInput-root': {
color: 'theme.foreground',
'& fieldset': {
borderColor: 'theme.foreground',
},
'&:hover fieldset': {
borderColor: 'theme.foreground',
},
'&.Mui-focused fieldset': {
borderColor: 'theme.foreground',
},
borderRadius: 2,
},
},
label: 'Hello',
fullWidth: true,
}),
],
}),
],
})
})
```
## Passing Context Wrapper To Portal
```ts
import { Provider, useSelector } from 'react-redux'
import store from '/path/to/redux/store'
/**
* ReduxProvider
*
* A wrapper component that integrates the Redux store with a React application.
* It utilizes the `Provider` component from `react-redux` to make the Redux store
* available to the entire component tree.
*
* @constant
* @type {NodeElement}
* @param {Object} store - The Redux store instance to be passed down to the application.
*/
const ReduxProvider = Node(Provider, { store })
/**
* Represents a modal component that is implemented using a portal
* and wrapped with a Redux provider. This component retrieves state
* from the Redux store and responds to state changes.
*
* The `Modal` leverages the Redux `useSelector` hook to access specific
* Redux state values and ensure dynamic behavior within the modal based
* on the application's state.
*
* Dependencies:
* - ReduxProvider: Ensures the modal has access to the Redux store.
* - Portal: Renders the modal outside of its parent hierarchy and provides
* control methods such as `unmount` for cleaning up the modal.
* - useSelector: Accesses specific data from the Redux state.
*
* Side Effects:
* - The component logs the specific Redux state changes to the console
* when the state is updated.
* - The modal listens for specific user interactions (e.g., clicking outside
* the modal area) and programmatically unmounts itself using `portal.unmount()`.
*
* Props:
* - portal: Includes a method `unmount` to remove the modal from the DOM.
*/
const Modal = Portal(ReduxProvider, ({ portal }) => {
const someReduxState = useSelector(state => state.someReduxState)
useEffect(() => {
console.log('Redux State value: ', someReduxState)
}, [])
// ...
})
```
---
## API Reference
### Core Functions
| Function | Parameters | Description |
|-------------|---------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `Node` | `element: NodeElement \| React.ComponentType`, `baseProps: object` | Constructs a configurable UI node that supports flexible properties and dynamic styling. |
| `Component` | `(props: P) => ComponentNode` | Transforms node trees into reusable React components with built-in type safety and seamless integration. |
| `Portal` | • `(component: (props: P) => ComponentNode)` or <br/> • `(providers: NodeElement \| NodeElement[], component: (props: P) => ComponentNode)` | Creates a React Portal component. Accepts either a component function directly, or a tuple of providers (e.g. Redux Provider) and the component. The component receives portal controls for mounting/unmounting. |
---
## Contributing
We welcome contributions! Please follow these steps:
1. **Fork** the repository
2. **Clone** your fork: `git clone https://github.com/your-username/meonode-ui.git`
3. **Install dependencies**: `yarn install` (or npm/pnpm)
4. **Create a feature branch**: `git checkout -b feature/amazing-feature`
5. **Commit changes** with descriptive messages
6. **Push** to your branch: `git push origin feature/amazing-feature`
7. **Open a Pull Request**
For major changes, please open an issue first to discuss your proposal.
---
**MIT Licensed** | Copyright © 2024 Ukasyah Rahmatullah Zada
*Empowering developers to build better UIs*