@meonode/ui 0.1.18 → 0.1.20
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/README.md +374 -149
- package/dist/core.node.d.ts +4 -4
- package/dist/core.node.d.ts.map +1 -1
- package/dist/core.node.js +8 -10
- package/dist/html.node.d.ts +135 -135
- package/dist/html.node.d.ts.map +1 -1
- package/dist/html.node.js +1 -1
- package/dist/node.type.d.ts +11 -8
- package/dist/node.type.d.ts.map +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,25 +1,30 @@
|
|
|
1
|
+
[file name]: README.md
|
|
2
|
+
[file content begin]
|
|
1
3
|
# @meonode/ui
|
|
2
4
|
|
|
3
5
|
[](https://www.npmjs.com/package/@meonode/ui)
|
|
4
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://bundlephobia.com/package/@meonode/ui)
|
|
5
8
|
|
|
6
9
|
**Build React UIs with Type-Safe Fluency**
|
|
7
10
|
A structured approach to component composition with built-in theming, prop separation, and dynamic children handling.
|
|
8
11
|
|
|
9
|
-
```
|
|
12
|
+
```tsx
|
|
10
13
|
// Quick Start Example
|
|
11
14
|
import { Component, Div, H1, Button } from '@meonode/ui';
|
|
12
15
|
|
|
16
|
+
// Create a reusable styled component
|
|
13
17
|
const BlueButton = Component((props) =>
|
|
14
18
|
Button('Blue', {
|
|
15
19
|
padding: '12px 24px',
|
|
16
20
|
borderRadius: '8px',
|
|
17
21
|
backgroundColor: 'dodgerblue',
|
|
18
22
|
color: 'white',
|
|
19
|
-
...props
|
|
23
|
+
...props // Merge with incoming props
|
|
20
24
|
})
|
|
21
25
|
);
|
|
22
26
|
|
|
27
|
+
// Compose your app
|
|
23
28
|
const App = Component(() =>
|
|
24
29
|
Div({
|
|
25
30
|
padding: '40px',
|
|
@@ -36,37 +41,45 @@ const App = Component(() =>
|
|
|
36
41
|
|
|
37
42
|
## Why @meonode/ui?
|
|
38
43
|
|
|
39
|
-
- 🎯 **Type-Safe Design** - Full TypeScript support with autocomplete for styles and
|
|
40
|
-
- 🎨 **
|
|
44
|
+
- 🎯 **Type-Safe Design** - Full TypeScript support with autocomplete for styles, props, and theme paths
|
|
45
|
+
- 🎨 **Theme-Aware Styles** - Write styles directly in props with dynamic theme resolution
|
|
41
46
|
- 🧩 **Component-First Architecture** - Compose UIs from structured nodes instead of JSX
|
|
42
|
-
- 🌐 **
|
|
43
|
-
- ⚡ **
|
|
47
|
+
- 🌐 **Contextual Theming** - Theme values propagate automatically through nested components
|
|
48
|
+
- ⚡ **Optimized Bundle** - Efficient core with tree-shaking support
|
|
49
|
+
- 🔄 **Seamless React Integration** - Works with hooks, HOCs, and React 18+ features
|
|
44
50
|
|
|
45
51
|
## Installation
|
|
46
52
|
|
|
47
|
-
```
|
|
53
|
+
```bash
|
|
54
|
+
# Using npm
|
|
48
55
|
npm install @meonode/ui react
|
|
49
|
-
|
|
56
|
+
|
|
57
|
+
# Using yarn
|
|
50
58
|
yarn add @meonode/ui react
|
|
59
|
+
|
|
60
|
+
# Using pnpm
|
|
61
|
+
pnpm add @meonode/ui react
|
|
51
62
|
```
|
|
52
63
|
|
|
64
|
+
---
|
|
65
|
+
|
|
53
66
|
## Core Concepts
|
|
54
67
|
|
|
55
68
|
### 1. Component Creation
|
|
56
69
|
|
|
57
|
-
Create elements using the `Node` factory or pre-built components:
|
|
70
|
+
Create elements using either the `Node` factory or pre-built components:
|
|
58
71
|
|
|
59
|
-
```
|
|
72
|
+
```tsx
|
|
60
73
|
import { Node, Div, H1 } from '@meonode/ui';
|
|
61
74
|
|
|
62
|
-
//
|
|
75
|
+
// Method 1: Node factory for custom elements
|
|
63
76
|
const Card = Node('div', {
|
|
64
77
|
padding: '20px',
|
|
65
78
|
borderRadius: '8px',
|
|
66
79
|
boxShadow: '0 2px 12px rgba(0,0,0,0.1)'
|
|
67
80
|
});
|
|
68
81
|
|
|
69
|
-
//
|
|
82
|
+
// Method 2: Pre-built semantic components
|
|
70
83
|
const Header = () =>
|
|
71
84
|
Div({
|
|
72
85
|
padding: '20px',
|
|
@@ -77,10 +90,11 @@ const Header = () =>
|
|
|
77
90
|
|
|
78
91
|
### 2. Theming System
|
|
79
92
|
|
|
80
|
-
Define and
|
|
93
|
+
Define themes and access values using dot-notation:
|
|
81
94
|
|
|
82
|
-
```
|
|
83
|
-
|
|
95
|
+
```tsx
|
|
96
|
+
// theme.ts
|
|
97
|
+
export const theme = {
|
|
84
98
|
colors: {
|
|
85
99
|
primary: '#2196F3',
|
|
86
100
|
text: {
|
|
@@ -94,9 +108,13 @@ const theme = {
|
|
|
94
108
|
}
|
|
95
109
|
};
|
|
96
110
|
|
|
111
|
+
// ThemedComponent.tsx
|
|
112
|
+
import { Component, Div, H1, P } from '@meonode/ui';
|
|
113
|
+
import { theme } from './theme';
|
|
114
|
+
|
|
97
115
|
const ThemedCard = Component(() =>
|
|
98
116
|
Div({
|
|
99
|
-
theme,
|
|
117
|
+
theme, // Provide theme context
|
|
100
118
|
padding: 'theme.spacing.lg',
|
|
101
119
|
backgroundColor: 'theme.colors.primary',
|
|
102
120
|
children: [
|
|
@@ -111,47 +129,49 @@ const ThemedCard = Component(() =>
|
|
|
111
129
|
|
|
112
130
|
Automatic separation of CSS props from DOM attributes:
|
|
113
131
|
|
|
114
|
-
```
|
|
132
|
+
```tsx
|
|
115
133
|
const ProfileCard = Component(({ user }) =>
|
|
116
134
|
Div({
|
|
117
135
|
// CSS Props
|
|
118
136
|
padding: '20px',
|
|
119
137
|
borderRadius: '8px',
|
|
138
|
+
|
|
120
139
|
// DOM Props
|
|
121
|
-
|
|
140
|
+
'aria-role': 'article',
|
|
122
141
|
tabIndex: 0,
|
|
142
|
+
|
|
123
143
|
// Children
|
|
124
144
|
children: `Welcome ${user.name}!`
|
|
125
145
|
})
|
|
126
146
|
);
|
|
127
147
|
```
|
|
128
148
|
|
|
149
|
+
---
|
|
129
150
|
|
|
130
151
|
## Key Features
|
|
131
152
|
|
|
132
|
-
| Feature | Description
|
|
133
|
-
|
|
134
|
-
| **Smart Prop Merge** | CSS
|
|
135
|
-
| **Theme Resolution** | `theme.` references resolve through component hierarchy
|
|
136
|
-
| **Type
|
|
137
|
-
| **HOC Support** | Wrap existing components with `Component()` for
|
|
138
|
-
| **Dynamic Children** |
|
|
153
|
+
| Feature | Description |
|
|
154
|
+
|----------------------|-----------------------------------------------------------------------------|
|
|
155
|
+
| **Smart Prop Merge** | CSS props merge with style objects; DOM props pass to underlying elements |
|
|
156
|
+
| **Theme Resolution** | `theme.` references resolve through component hierarchy |
|
|
157
|
+
| **Type Guards** | Autocomplete for CSS properties and theme paths with strict type checks |
|
|
158
|
+
| **HOC Support** | Wrap existing components with `Component()` for theme integration |
|
|
159
|
+
| **Dynamic Children** | Supports function-as-child pattern with automatic theme propagation |
|
|
160
|
+
|
|
161
|
+
---
|
|
139
162
|
|
|
140
|
-
## Advanced
|
|
163
|
+
## Advanced Patterns
|
|
141
164
|
|
|
142
165
|
### Component Composition
|
|
143
166
|
|
|
144
|
-
```
|
|
167
|
+
```tsx
|
|
145
168
|
const Dashboard = Component(() =>
|
|
146
169
|
Div({
|
|
147
170
|
display: 'grid',
|
|
148
171
|
gridTemplateColumns: '1fr 3fr',
|
|
149
172
|
gap: '20px',
|
|
150
173
|
children: [
|
|
151
|
-
Sidebar({
|
|
152
|
-
width: '240px',
|
|
153
|
-
items: navItems
|
|
154
|
-
}),
|
|
174
|
+
Sidebar({ width: '240px' }),
|
|
155
175
|
MainContent({
|
|
156
176
|
padding: '40px',
|
|
157
177
|
children: AnalyticsChart({ dataset })
|
|
@@ -161,98 +181,268 @@ const Dashboard = Component(() =>
|
|
|
161
181
|
);
|
|
162
182
|
```
|
|
163
183
|
|
|
164
|
-
###
|
|
184
|
+
### Material UI Integration
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
yarn add @meonode/mui @mui/material
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
import { Button, TextField } from '@meonode/mui';
|
|
192
|
+
|
|
193
|
+
const LoginForm = Component(() =>
|
|
194
|
+
Div({
|
|
195
|
+
maxWidth: '400px',
|
|
196
|
+
margin: '0 auto',
|
|
197
|
+
children: [
|
|
198
|
+
TextField({ label: 'Email', fullWidth: true }),
|
|
199
|
+
TextField({ label: 'Password', type: 'password' }),
|
|
200
|
+
Button({
|
|
201
|
+
variant: 'contained',
|
|
202
|
+
children: 'Sign In'
|
|
203
|
+
})
|
|
204
|
+
]
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
```
|
|
165
208
|
|
|
166
|
-
|
|
209
|
+
## Comprehensive Example: Theme-Switching & Conditional Components
|
|
210
|
+
```tsx
|
|
167
211
|
'use client'
|
|
168
212
|
/**
|
|
169
|
-
* This file
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* and
|
|
213
|
+
* This file showcases the integration of React hooks with @meonode/ui components
|
|
214
|
+
* for building declarative user interfaces. It demonstrates different rendering
|
|
215
|
+
* approaches, the use of Higher-Order Components (HOCs), and how theme context
|
|
216
|
+
* is managed and propagated within the @meonode/ui component tree.
|
|
173
217
|
*/
|
|
174
|
-
import { Component, Column, Row,
|
|
175
|
-
import { useState, useEffect } from 'react'
|
|
176
|
-
import { CssBaseline, TextField } from '@meonode/mui'
|
|
218
|
+
import { Component, Column, Row, P, Node, Button, Theme, Center, NodeInstance, Absolute } from '@meonode/ui'
|
|
219
|
+
import { useState, useEffect, ReactElement, ReactNode } from 'react'
|
|
220
|
+
import { CssBaseline, FormControlLabel, TextField } from '@meonode/mui'
|
|
221
|
+
import { Switch as MUISwitch } from '@mui/material'
|
|
222
|
+
import { styled } from '@mui/material'
|
|
223
|
+
|
|
224
|
+
const MaterialUISwitch = styled(MUISwitch)(({ theme }) => ({
|
|
225
|
+
width: 62,
|
|
226
|
+
height: 34,
|
|
227
|
+
padding: 7,
|
|
228
|
+
'& .MuiSwitch-switchBase': {
|
|
229
|
+
margin: 1,
|
|
230
|
+
padding: 0,
|
|
231
|
+
transform: 'translateX(6px)',
|
|
232
|
+
'&.Mui-checked': {
|
|
233
|
+
color: '#fff',
|
|
234
|
+
transform: 'translateX(22px)',
|
|
235
|
+
'& .MuiSwitch-thumb:before': {
|
|
236
|
+
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(
|
|
237
|
+
'#fff',
|
|
238
|
+
)}" 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>')`,
|
|
239
|
+
},
|
|
240
|
+
'& + .MuiSwitch-track': {
|
|
241
|
+
opacity: 1,
|
|
242
|
+
backgroundColor: '#aab4be',
|
|
243
|
+
...theme.applyStyles('dark', {
|
|
244
|
+
backgroundColor: '#8796A5',
|
|
245
|
+
}),
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
'& .MuiSwitch-thumb': {
|
|
250
|
+
backgroundColor: '#001e3c',
|
|
251
|
+
width: 32,
|
|
252
|
+
height: 32,
|
|
253
|
+
'&::before': {
|
|
254
|
+
content: "''",
|
|
255
|
+
position: 'absolute',
|
|
256
|
+
width: '100%',
|
|
257
|
+
height: '100%',
|
|
258
|
+
left: 0,
|
|
259
|
+
top: 0,
|
|
260
|
+
backgroundRepeat: 'no-repeat',
|
|
261
|
+
backgroundPosition: 'center',
|
|
262
|
+
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(
|
|
263
|
+
'#fff',
|
|
264
|
+
)}" 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>')`,
|
|
265
|
+
},
|
|
266
|
+
...theme.applyStyles('dark', {
|
|
267
|
+
backgroundColor: '#003892',
|
|
268
|
+
}),
|
|
269
|
+
},
|
|
270
|
+
'& .MuiSwitch-track': {
|
|
271
|
+
opacity: 1,
|
|
272
|
+
backgroundColor: '#aab4be',
|
|
273
|
+
borderRadius: 20 / 2,
|
|
274
|
+
...theme.applyStyles('dark', {
|
|
275
|
+
backgroundColor: '#8796A5',
|
|
276
|
+
}),
|
|
277
|
+
},
|
|
278
|
+
}))
|
|
177
279
|
|
|
178
280
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
281
|
+
* Defines the color palette for the light theme.
|
|
282
|
+
* These color values are used by @meonode/ui components when they encounter
|
|
283
|
+
* theme string references (e.g., 'theme.primary') and the current theme mode is 'light'.
|
|
284
|
+
* In a larger application, this theme object would typically reside in a dedicated theme file.
|
|
183
285
|
*/
|
|
184
|
-
const
|
|
185
|
-
|
|
286
|
+
const lightTheme: Theme = {
|
|
287
|
+
mode: 'light',
|
|
288
|
+
colors: {
|
|
289
|
+
primary: '#2563eb',
|
|
290
|
+
secondary: '#64748b',
|
|
291
|
+
accent: '#10b981',
|
|
292
|
+
background: '#ffffff',
|
|
293
|
+
foreground: '#0f172a',
|
|
294
|
+
border: '#e2e8f0',
|
|
295
|
+
muted: '#f8fafc',
|
|
296
|
+
success: '#16a34a',
|
|
297
|
+
warning: '#eab308',
|
|
298
|
+
danger: '#dc2626',
|
|
299
|
+
},
|
|
186
300
|
}
|
|
187
301
|
|
|
188
302
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
|
|
303
|
+
* Defines the color palette for the dark theme.
|
|
304
|
+
* Similar to the light theme, these colors are used by @meonode/ui components
|
|
305
|
+
* when resolving theme string references, but specifically when the current theme
|
|
306
|
+
* mode is 'dark'.
|
|
307
|
+
*/
|
|
308
|
+
const darkTheme: Theme = {
|
|
309
|
+
mode: 'dark',
|
|
310
|
+
colors: {
|
|
311
|
+
primary: '#3b82f6',
|
|
312
|
+
secondary: '#94a3b8',
|
|
313
|
+
accent: '#34d399',
|
|
314
|
+
background: '#0f172a',
|
|
315
|
+
foreground: '#f8fafc',
|
|
316
|
+
border: '#334155',
|
|
317
|
+
muted: '#1e293b',
|
|
318
|
+
success: '#22c55e',
|
|
319
|
+
warning: '#facc15',
|
|
320
|
+
danger: '#ef4444',
|
|
321
|
+
},
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The main page component, implemented as a functional component using React hooks.
|
|
326
|
+
* It manages the theme mode state and the visibility of additional content.
|
|
327
|
+
*
|
|
328
|
+
* This function is wrapped by the `Component` HOC from `@meonode/ui`. The `Component`
|
|
329
|
+
* HOC transforms the function into a standard React component that returns a `ReactNode`,
|
|
330
|
+
* making it compatible with React's rendering lifecycle and enabling SSR/CSR.
|
|
194
331
|
*/
|
|
195
332
|
export default Component(() => {
|
|
196
333
|
// State hook to control the visibility of additional content sections.
|
|
197
|
-
const [showMore, setShowDetails] = useState(false)
|
|
334
|
+
const [showMore, setShowDetails] = useState(false)
|
|
335
|
+
const [mode, setMode] = useState<'dark' | 'light'>('light')
|
|
336
|
+
const theme = mode === 'dark' ? darkTheme : lightTheme
|
|
198
337
|
|
|
199
338
|
/**
|
|
200
|
-
* The
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* - A
|
|
339
|
+
* The root of the UI tree is a `Column` component from `@meonode/ui`.
|
|
340
|
+
* This `Column` sets the theme context for its children.
|
|
341
|
+
* Its children include:
|
|
342
|
+
* - A theme toggle switch using MUI components wrapped in `@meonode/ui`'s `Node`.
|
|
343
|
+
* - A button to toggle the visibility of the detail sections.
|
|
344
|
+
* - Various examples demonstrating how to render components that return either
|
|
345
|
+
* `@meonode/ui` `Node` instances or `ReactNode`s, illustrating theme propagation
|
|
204
346
|
* both unconditionally and conditionally, highlighting theme propagation.
|
|
205
347
|
*/
|
|
206
348
|
return Column({
|
|
207
|
-
theme
|
|
349
|
+
theme: theme.colors,
|
|
208
350
|
padding: 20,
|
|
209
351
|
gap: 15,
|
|
352
|
+
minHeight: '100vh',
|
|
353
|
+
backgroundColor: 'theme.background',
|
|
354
|
+
color: 'theme.foreground',
|
|
210
355
|
children: [
|
|
211
|
-
CssBaseline, // Applies baseline
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
356
|
+
CssBaseline, // Applies baseline Material UI styles for consistent rendering.
|
|
357
|
+
// Theme toggle switch using MUI components wrapped with @meonode/ui's Node HOC.
|
|
358
|
+
Center({
|
|
359
|
+
children: FormControlLabel({
|
|
360
|
+
control: Node(MaterialUISwitch).render() as ReactElement,
|
|
361
|
+
alignItems: 'center',
|
|
362
|
+
label: mode === 'dark' ? 'Dark Mode' : 'Light Mode',
|
|
363
|
+
labelPlacement: 'start',
|
|
364
|
+
checked: mode === 'dark',
|
|
365
|
+
onChange: () => setMode(prev => (prev === 'dark' ? 'light' : 'dark')),
|
|
366
|
+
}),
|
|
367
|
+
}),
|
|
368
|
+
// Button to show modal.
|
|
369
|
+
Button('Show Modal', {
|
|
370
|
+
onClick: () => Modal({ theme }), // Click handler to show modal immedietelly.
|
|
371
|
+
cursor: 'pointer', // Visual cue for clickability.
|
|
372
|
+
userSelect: 'none', // Prevents text selection on the button.
|
|
373
|
+
padding: '10px 20px',
|
|
374
|
+
backgroundColor: 'theme.primary', // Background color sourced from the theme context.
|
|
375
|
+
borderRadius: 5,
|
|
376
|
+
fontWeight: 'bold',
|
|
377
|
+
color: 'white',
|
|
378
|
+
}),
|
|
379
|
+
// Button to toggle the visibility of the detail sections.
|
|
380
|
+
Button(showMore ? 'Hide' : 'Show More', {
|
|
381
|
+
onClick: () => setShowDetails(prev => !prev), // Click handler to toggle the 'showMore' state.
|
|
382
|
+
cursor: 'pointer', // Visual cue for clickability.
|
|
383
|
+
userSelect: 'none', // Prevents text selection on the button.
|
|
384
|
+
padding: '10px 20px',
|
|
385
|
+
backgroundColor: 'theme.accent', // Background color sourced from the theme context.
|
|
386
|
+
borderRadius: 5,
|
|
387
|
+
fontWeight: 'bold',
|
|
388
|
+
color: 'white',
|
|
227
389
|
}),
|
|
228
390
|
|
|
229
391
|
/**
|
|
230
|
-
* Unconditional
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
392
|
+
* --- Unconditional Rendering Examples ---
|
|
393
|
+
* These examples demonstrate rendering components that return either a
|
|
394
|
+
* `@meonode/ui` `Node` instance (`DetailComponent`) or a `ReactNode`
|
|
395
|
+
* (`ReturnRenderedDetailComponent`), and how the `Node` HOC affects this.
|
|
396
|
+
* Observe how theme context is propagated (or not) in each case.
|
|
234
397
|
*/
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
398
|
+
// 1. Rendering a component that returns a @meonode/ui Node instance directly.
|
|
399
|
+
// The internal Row component correctly receives theme context from the parent Column.
|
|
400
|
+
DetailComponent({ info: 'Detail 1 (Node instance)' }),
|
|
401
|
+
|
|
402
|
+
// 2. Rendering a component that returns a @meonode/ui Node instance, then calling .render().
|
|
403
|
+
// The internal Row component also correctly receives theme context.
|
|
404
|
+
DetailComponent({ info: 'Detail 2 (Node instance + .render())' }).render(),
|
|
238
405
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
Node(
|
|
242
|
-
|
|
406
|
+
// 3. Attempting to wrap a component returning a Node instance with Node HOC.
|
|
407
|
+
// ❌ Fails: The Node HOC expects the wrapped function to return a ReactNode, not a @meonode/ui Node instance.
|
|
408
|
+
// Node(DetailComponent, { info: 'Detail 3 (Node HOC on Node instance)' }),
|
|
409
|
+
|
|
410
|
+
// 4. Rendering a component that explicitly returns a ReactNode (.render() is called internally).
|
|
411
|
+
// The internal Row component correctly receives theme context from the parent Column.
|
|
412
|
+
ReturnRenderedDetailComponent({ info: 'Detail 4 (ReactNode)' }),
|
|
413
|
+
|
|
414
|
+
// 5. Wrapping a component returning ReactNode with Node HOC.
|
|
415
|
+
// Renders successfully. However, the Node HOC does NOT propagate theme context to the wrapped component's children.
|
|
416
|
+
Node(ReturnRenderedDetailComponent, { info: 'Detail 5 (Node HOC on ReactNode)' }),
|
|
417
|
+
|
|
418
|
+
// 6. Wrapping a component returning ReactNode with Node HOC, then calling .render().
|
|
419
|
+
// Renders successfully. Theme context is NOT propagated by the Node HOC.
|
|
420
|
+
Node(ReturnRenderedDetailComponent, { info: 'Detail 6 (Node HOC on ReactNode + .render())' }).render(),
|
|
243
421
|
|
|
244
422
|
/**
|
|
245
423
|
* Conditional rendering examples (shown when 'showMore' is true):
|
|
246
424
|
* These demonstrate various wrapping techniques (inline functions, Component HOC)
|
|
247
425
|
* and their effect on rendering and theme propagation for both types of detail components.
|
|
248
426
|
*/
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
showMore &&
|
|
427
|
+
// 7. Conditional rendering of a component returning a Node instance using an inline function wrapper.
|
|
428
|
+
// Renders successfully when `showMore` is true. The internal Row receives theme context.
|
|
429
|
+
showMore && (() => DetailComponent({ info: 'Detail 7 (Conditional inline function + Node instance)' })),
|
|
430
|
+
|
|
431
|
+
// 8. Conditional rendering of a component returning a Node instance using an inline function wrapper, then calling .render().
|
|
432
|
+
// Renders successfully when `showMore` is true. The internal Row receives theme context.
|
|
433
|
+
showMore && (() => DetailComponent({ info: 'Detail 8 (Conditional inline function + Node instance + .render())' }).render()),
|
|
434
|
+
|
|
435
|
+
// 9. Conditional rendering of a component returning a Node instance using the Component HOC wrapper.
|
|
436
|
+
// Renders successfully when `showMore` is true. The internal Row receives theme context.
|
|
437
|
+
showMore && Component(() => DetailComponent({ info: 'Detail 9 (Conditional Component HOC + Node instance)' })),
|
|
252
438
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
showMore &&
|
|
439
|
+
// 10. Conditional rendering of a component returning ReactNode using an inline function wrapper.
|
|
440
|
+
// Renders successfully when `showMore` is true. The internal Row receives theme context.
|
|
441
|
+
showMore && (() => ReturnRenderedDetailComponent({ info: 'Detail 10 (Conditional inline function + ReactNode)' })),
|
|
442
|
+
|
|
443
|
+
// 11. Conditional rendering of a component returning ReactNode using the Component HOC wrapper.
|
|
444
|
+
// Renders successfully when `showMore` is true. The internal Row receives theme context.
|
|
445
|
+
showMore && Component(() => ReturnRenderedDetailComponent({ info: 'Detail 11 (Conditional Component HOC + ReactNode)' })),
|
|
256
446
|
// showMore && ReturnRenderedDetailComponent({ info: 'Here are some details 15!' }), // ❌ Fails: Direct call to a component function using hooks (ReturnRenderedDetailComponent) inside render logic without a React-aware wrapper. This can violate Rules of Hooks.
|
|
257
447
|
],
|
|
258
448
|
})
|
|
@@ -261,15 +451,17 @@ export default Component(() => {
|
|
|
261
451
|
/**
|
|
262
452
|
* A component that displays a styled detail section.
|
|
263
453
|
* It uses `useEffect` for lifecycle logging. The internal `Row` component
|
|
264
|
-
* sources its theme from the React context
|
|
265
|
-
*
|
|
266
|
-
* This component returns a @meonode/ui `Row` Node instance.
|
|
454
|
+
* sources its theme from the React context provided by an ancestor `@meonode/ui`
|
|
455
|
+
* component (like the main `Column` in this page).
|
|
267
456
|
*
|
|
457
|
+
* This component returns a @meonode/ui `Row` Node instance.
|
|
458
|
+
* This type of component is suitable for direct inclusion as a child within other
|
|
459
|
+
* `@meonode/ui` components that expect `Node` instances or arrays of `Node` instances.
|
|
268
460
|
* @param {object} props - Component properties.
|
|
269
461
|
* @param {string} props.info - Text content to display in the detail section.
|
|
270
|
-
* @returns {
|
|
462
|
+
* @returns {NodeInstance} A @meonode/ui Row Node instance.
|
|
271
463
|
*/
|
|
272
|
-
const DetailComponent = ({ info }: { info: string }) => {
|
|
464
|
+
const DetailComponent = ({ info }: { info: string }): NodeInstance => {
|
|
273
465
|
// useEffect hook for logging component mount and unmount phases (for debugging).
|
|
274
466
|
useEffect(() => {
|
|
275
467
|
console.log('DetailComponent mounted:', info) // Example mount log
|
|
@@ -278,31 +470,34 @@ const DetailComponent = ({ info }: { info: string }) => {
|
|
|
278
470
|
}
|
|
279
471
|
}, [info]) // Effect depends on 'info' prop.
|
|
280
472
|
|
|
281
|
-
// Returns a @meonode/ui Row
|
|
473
|
+
// Returns a @meonode/ui Row Node instance configured with props and children.
|
|
282
474
|
// Its styling (e.g., backgroundColor) will resolve theme strings from React context.
|
|
283
475
|
return Row({
|
|
476
|
+
alignItems: 'center',
|
|
284
477
|
gap: 10,
|
|
285
478
|
padding: 4,
|
|
286
|
-
border: '
|
|
479
|
+
border: '2px solid theme.accent',
|
|
287
480
|
borderRadius: 6,
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
children: [P(info), TextField({ background: 'theme.
|
|
481
|
+
backgroundColor: 'theme.warning', // Background color sourced from theme in React context.
|
|
482
|
+
color: 'theme.danger',
|
|
483
|
+
children: [P(info, { flex: 1, padding: '0 20px' }), TextField({ flex: 1, sx: { background: 'theme.primary' } })],
|
|
291
484
|
})
|
|
292
485
|
}
|
|
293
486
|
|
|
294
487
|
/**
|
|
295
488
|
* An alternative detail component implementation that explicitly calls `.render()`
|
|
296
489
|
* to return a `ReactNode` (a rendered React element) directly.
|
|
297
|
-
* This makes it compatible with
|
|
490
|
+
* This makes it compatible with standard React rendering patterns and wrappers
|
|
491
|
+
* like the `Node` HOC that specifically expect a function returning `ReactNode`.
|
|
298
492
|
* It uses `useEffect` for lifecycle logging. The internal `Row` sources its
|
|
299
493
|
* theme from React context.
|
|
300
494
|
*
|
|
495
|
+
* This component returns a `ReactNode`.
|
|
301
496
|
* @param {object} props - Component properties.
|
|
302
497
|
* @param {string} props.info - Text content to display.
|
|
303
498
|
* @returns {React.ReactNode} A rendered React element (the result of `Row(...).render()`).
|
|
304
499
|
*/
|
|
305
|
-
const ReturnRenderedDetailComponent = ({ info }: { info: string }) => {
|
|
500
|
+
const ReturnRenderedDetailComponent = ({ info }: { info: string }): ReactNode => {
|
|
306
501
|
// useEffect hook for logging component mount and unmount phases (for debugging).
|
|
307
502
|
useEffect(() => {
|
|
308
503
|
console.log('ReturnRenderedDetailComponent mounted:', info) // Example mount log
|
|
@@ -315,72 +510,102 @@ const ReturnRenderedDetailComponent = ({ info }: { info: string }) => {
|
|
|
315
510
|
// The Row itself will attempt to resolve theme strings (e.g., 'theme.background.secondary')
|
|
316
511
|
// from the React context provided by an ancestor @meonode/ui component (like the main Column).
|
|
317
512
|
return Row({
|
|
513
|
+
alignItems: 'center',
|
|
318
514
|
gap: 10,
|
|
319
515
|
padding: 4,
|
|
320
|
-
border: '
|
|
516
|
+
border: '2px solid theme.accent',
|
|
321
517
|
borderRadius: 6,
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
children: [P(info), TextField({ background: 'theme.
|
|
518
|
+
backgroundColor: 'theme.warning', // Theme-aware background; relies on theme from React context.
|
|
519
|
+
color: 'theme.danger',
|
|
520
|
+
children: [P(info, { flex: 1, padding: '0 20px' }), TextField({ flex: 1, sx: { background: 'theme.primary' } })],
|
|
325
521
|
}).render() // Explicitly renders to ReactNode.
|
|
326
522
|
}
|
|
327
|
-
```
|
|
328
523
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
524
|
+
const Modal = ({ theme }: { theme: Theme }) => {
|
|
525
|
+
const modal = Absolute({
|
|
526
|
+
theme: theme.colors,
|
|
527
|
+
top: 0,
|
|
528
|
+
left: 0,
|
|
529
|
+
right: 0,
|
|
530
|
+
bottom: 0,
|
|
531
|
+
display: 'flex',
|
|
532
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
533
|
+
justifyContent: 'center',
|
|
534
|
+
alignItems: 'center',
|
|
535
|
+
onClick: e => {
|
|
536
|
+
if (e.target === e.currentTarget) {
|
|
537
|
+
modal?.unmount()
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
children: Column({
|
|
541
|
+
width: '50%',
|
|
542
|
+
height: '50%',
|
|
543
|
+
backgroundColor: 'theme.background',
|
|
544
|
+
borderRadius: '8px',
|
|
545
|
+
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
|
|
546
|
+
transition: 'transform 0.2s ease-in-out',
|
|
547
|
+
padding: 10,
|
|
548
|
+
gap: 10,
|
|
549
|
+
color: 'theme.foreground',
|
|
550
|
+
children: [
|
|
551
|
+
Center({ fontWeight: 'bold', children: 'Hello There' }),
|
|
552
|
+
TextField({
|
|
553
|
+
sx: {
|
|
554
|
+
'& .MuiFormLabel-root': {
|
|
555
|
+
color: 'theme.foreground',
|
|
556
|
+
'&.Mui-focused': {
|
|
557
|
+
color: 'theme.foreground',
|
|
558
|
+
},
|
|
559
|
+
},
|
|
560
|
+
'& .MuiOutlinedInput-root': {
|
|
561
|
+
color: 'theme.foreground',
|
|
562
|
+
'& fieldset': {
|
|
563
|
+
borderColor: 'theme.foreground',
|
|
564
|
+
},
|
|
565
|
+
'&:hover fieldset': {
|
|
566
|
+
borderColor: 'theme.foreground',
|
|
567
|
+
},
|
|
568
|
+
'&.Mui-focused fieldset': {
|
|
569
|
+
borderColor: 'theme.foreground',
|
|
570
|
+
},
|
|
571
|
+
borderRadius: 2,
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
label: 'Hello',
|
|
575
|
+
fullWidth: true,
|
|
576
|
+
}),
|
|
577
|
+
],
|
|
578
|
+
}),
|
|
579
|
+
}).toPortal()
|
|
580
|
+
}
|
|
333
581
|
```
|
|
334
582
|
|
|
335
|
-
|
|
336
|
-
import { Button, TextField } from '@meonode/mui';
|
|
337
|
-
|
|
338
|
-
const MuiLoginForm = Component(() =>
|
|
339
|
-
Div({
|
|
340
|
-
maxWidth: '400px',
|
|
341
|
-
margin: '0 auto',
|
|
342
|
-
children: [
|
|
343
|
-
TextField({
|
|
344
|
-
label: 'Email',
|
|
345
|
-
fullWidth: true,
|
|
346
|
-
margin: 'normal'
|
|
347
|
-
}),
|
|
348
|
-
TextField({
|
|
349
|
-
label: 'Password',
|
|
350
|
-
type: 'password',
|
|
351
|
-
fullWidth: true,
|
|
352
|
-
margin: 'normal'
|
|
353
|
-
}),
|
|
354
|
-
Button({
|
|
355
|
-
variant: 'contained',
|
|
356
|
-
color: 'primary',
|
|
357
|
-
children: 'Sign In'
|
|
358
|
-
})
|
|
359
|
-
]
|
|
360
|
-
})
|
|
361
|
-
);
|
|
362
|
-
```
|
|
583
|
+
---
|
|
363
584
|
|
|
364
585
|
## API Reference
|
|
365
586
|
|
|
366
587
|
### Core Functions
|
|
367
588
|
|
|
368
|
-
| Function
|
|
369
|
-
|
|
370
|
-
| `Node`
|
|
371
|
-
| `Component`
|
|
589
|
+
| Function | Parameters | Description |
|
|
590
|
+
|-------------|-----------------------------------------|----------------------------------------------|
|
|
591
|
+
| `Node` | `element: string | React.ComponentType`, `baseProps: object` | Creates a configurable UI node |
|
|
592
|
+
| `Component` | `(props: P) => Node | ReactNode` | Converts node trees to React components |
|
|
372
593
|
|
|
594
|
+
---
|
|
373
595
|
|
|
374
596
|
## Contributing
|
|
375
597
|
|
|
376
598
|
We welcome contributions! Please follow these steps:
|
|
377
|
-
1. Fork the repository
|
|
378
|
-
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
379
|
-
3. Commit your changes
|
|
380
|
-
4. Push to the branch
|
|
381
|
-
5. Open a Pull Request
|
|
382
599
|
|
|
383
|
-
|
|
600
|
+
1. **Fork** the repository
|
|
601
|
+
2. **Clone** your fork: `git clone https://github.com/your-username/meonode-ui.git`
|
|
602
|
+
3. **Install dependencies**: `yarn install` (or npm/pnpm)
|
|
603
|
+
4. **Create a feature branch**: `git checkout -b feature/amazing-feature`
|
|
604
|
+
5. **Commit changes** with descriptive messages
|
|
605
|
+
6. **Push** to your branch: `git push origin feature/amazing-feature`
|
|
606
|
+
7. **Open a Pull Request**
|
|
607
|
+
|
|
608
|
+
For major changes, please open an issue first to discuss your proposal.
|
|
384
609
|
|
|
385
610
|
---
|
|
386
611
|
|