11package cc.unitmesh.devins.ui.nano
22
33import androidx.compose.foundation.background
4+ import androidx.compose.foundation.border
45import androidx.compose.foundation.layout.*
56import androidx.compose.foundation.shape.RoundedCornerShape
67import androidx.compose.material3.*
@@ -20,53 +21,80 @@ import kotlinx.serialization.json.jsonPrimitive
2021 *
2122 * Renders NanoIR components to Compose UI.
2223 * Uses Material 3 components for consistent theming.
24+ *
25+ * This renderer follows the component-specific method pattern from NanoRenderer interface.
26+ * Each component type has its own Render* method, making it easy to identify
27+ * missing implementations when new components are added.
28+ *
29+ * @see cc.unitmesh.xuiper.render.NanoRenderer for the interface pattern
2330 */
2431object ComposeNanoRenderer {
2532
33+ // ============================================================================
34+ // Main Entry Point
35+ // ============================================================================
36+
37+ /* *
38+ * Render a NanoIR node to Compose UI.
39+ * Dispatches to component-specific render methods.
40+ */
2641 @Composable
2742 fun Render (ir : NanoIR , modifier : Modifier = Modifier ) {
43+ RenderNode (ir, modifier)
44+ }
45+
46+ /* *
47+ * Dispatch rendering based on component type.
48+ * Routes to the appropriate component-specific render method.
49+ */
50+ @Composable
51+ fun RenderNode (ir : NanoIR , modifier : Modifier = Modifier ) {
2852 when (ir.type) {
29- " Component " -> RenderComponent (ir, modifier)
53+ // Layout
3054 " VStack" -> RenderVStack (ir, modifier)
3155 " HStack" -> RenderHStack (ir, modifier)
56+ // Container
3257 " Card" -> RenderCard (ir, modifier)
58+ " Form" -> RenderForm (ir, modifier)
59+ // Content
3360 " Text" -> RenderText (ir, modifier)
34- " Button" -> RenderButton (ir, modifier)
3561 " Image" -> RenderImage (ir, modifier)
3662 " Badge" -> RenderBadge (ir, modifier)
63+ " Divider" -> RenderDivider (ir, modifier)
64+ // Input
65+ " Button" -> RenderButton (ir, modifier)
3766 " Input" -> RenderInput (ir, modifier)
3867 " Checkbox" -> RenderCheckbox (ir, modifier)
39- " Divider" -> HorizontalDivider (modifier.padding(vertical = 8 .dp))
68+ " TextArea" -> RenderTextArea (ir, modifier)
69+ " Select" -> RenderSelect (ir, modifier)
70+ // Control Flow
4071 " Conditional" -> RenderConditional (ir, modifier)
4172 " ForLoop" -> RenderForLoop (ir, modifier)
73+ // Meta
74+ " Component" -> RenderComponent (ir, modifier)
4275 else -> RenderUnknown (ir, modifier)
4376 }
4477 }
4578
46- @Composable
47- private fun RenderComponent (ir : NanoIR , modifier : Modifier ) {
48- Column (modifier = modifier) {
49- ir.children?.forEach { child ->
50- Render (child)
51- }
52- }
53- }
79+ // ============================================================================
80+ // Layout Components
81+ // ============================================================================
5482
5583 @Composable
56- private fun RenderVStack (ir : NanoIR , modifier : Modifier ) {
84+ fun RenderVStack (ir : NanoIR , modifier : Modifier = Modifier ) {
5785 val spacing = ir.props[" spacing" ]?.jsonPrimitive?.content?.toSpacing() ? : 8 .dp
5886 Column (
5987 modifier = modifier,
6088 verticalArrangement = Arrangement .spacedBy(spacing)
6189 ) {
6290 ir.children?.forEach { child ->
63- Render (child)
91+ RenderNode (child)
6492 }
6593 }
6694 }
6795
6896 @Composable
69- private fun RenderHStack (ir : NanoIR , modifier : Modifier ) {
97+ fun RenderHStack (ir : NanoIR , modifier : Modifier = Modifier ) {
7098 val spacing = ir.props[" spacing" ]?.jsonPrimitive?.content?.toSpacing() ? : 8 .dp
7199 val align = ir.props[" align" ]?.jsonPrimitive?.content?.toVerticalAlignment() ? : Alignment .CenterVertically
72100 val justify = ir.props[" justify" ]?.jsonPrimitive?.content?.toHorizontalArrangement() ? : Arrangement .Start
@@ -77,13 +105,17 @@ object ComposeNanoRenderer {
77105 verticalAlignment = align
78106 ) {
79107 ir.children?.forEach { child ->
80- Render (child)
108+ RenderNode (child)
81109 }
82110 }
83111 }
84112
113+ // ============================================================================
114+ // Container Components
115+ // ============================================================================
116+
85117 @Composable
86- private fun RenderCard (ir : NanoIR , modifier : Modifier ) {
118+ fun RenderCard (ir : NanoIR , modifier : Modifier = Modifier ) {
87119 val padding = ir.props[" padding" ]?.jsonPrimitive?.content?.toSpacing() ? : 16 .dp
88120 val shadow = ir.props[" shadow" ]?.jsonPrimitive?.content?.toElevation() ? : 2 .dp
89121
@@ -94,14 +126,30 @@ object ComposeNanoRenderer {
94126 ) {
95127 Column (modifier = Modifier .padding(padding)) {
96128 ir.children?.forEach { child ->
97- Render (child)
129+ RenderNode (child)
98130 }
99131 }
100132 }
101133 }
102134
103135 @Composable
104- private fun RenderText (ir : NanoIR , modifier : Modifier ) {
136+ fun RenderForm (ir : NanoIR , modifier : Modifier = Modifier ) {
137+ Column (
138+ modifier = modifier.fillMaxWidth(),
139+ verticalArrangement = Arrangement .spacedBy(16 .dp)
140+ ) {
141+ ir.children?.forEach { child ->
142+ RenderNode (child)
143+ }
144+ }
145+ }
146+
147+ // ============================================================================
148+ // Content Components
149+ // ============================================================================
150+
151+ @Composable
152+ fun RenderText (ir : NanoIR , modifier : Modifier = Modifier ) {
105153 val content = ir.props[" content" ]?.jsonPrimitive?.content ? : " "
106154 val style = ir.props[" style" ]?.jsonPrimitive?.content
107155
@@ -119,24 +167,7 @@ object ComposeNanoRenderer {
119167 }
120168
121169 @Composable
122- private fun RenderButton (ir : NanoIR , modifier : Modifier ) {
123- val label = ir.props[" label" ]?.jsonPrimitive?.content ? : " Button"
124- val intent = ir.props[" intent" ]?.jsonPrimitive?.content
125-
126- val colors = when (intent) {
127- " primary" -> ButtonDefaults .buttonColors()
128- " secondary" -> ButtonDefaults .outlinedButtonColors()
129- " danger" -> ButtonDefaults .buttonColors(containerColor = MaterialTheme .colorScheme.error)
130- else -> ButtonDefaults .buttonColors()
131- }
132-
133- Button (onClick = { }, colors = colors, modifier = modifier) {
134- Text (label)
135- }
136- }
137-
138- @Composable
139- private fun RenderImage (ir : NanoIR , modifier : Modifier ) {
170+ fun RenderImage (ir : NanoIR , modifier : Modifier = Modifier ) {
140171 val src = ir.props[" src" ]?.jsonPrimitive?.content ? : " "
141172 // Placeholder for image - in real app would load from URL
142173 Box (
@@ -152,7 +183,7 @@ object ComposeNanoRenderer {
152183 }
153184
154185 @Composable
155- private fun RenderBadge (ir : NanoIR , modifier : Modifier ) {
186+ fun RenderBadge (ir : NanoIR , modifier : Modifier = Modifier ) {
156187 val text = ir.props[" text" ]?.jsonPrimitive?.content ? : " "
157188 val colorName = ir.props[" color" ]?.jsonPrimitive?.content
158189
@@ -185,19 +216,46 @@ object ComposeNanoRenderer {
185216 }
186217
187218 @Composable
188- private fun RenderInput (ir : NanoIR , modifier : Modifier ) {
219+ fun RenderDivider (ir : NanoIR , modifier : Modifier = Modifier ) {
220+ HorizontalDivider (modifier.padding(vertical = 8 .dp))
221+ }
222+
223+ // ============================================================================
224+ // Input Components
225+ // ============================================================================
226+
227+ @Composable
228+ fun RenderButton (ir : NanoIR , modifier : Modifier = Modifier ) {
229+ val label = ir.props[" label" ]?.jsonPrimitive?.content ? : " Button"
230+ val intent = ir.props[" intent" ]?.jsonPrimitive?.content
231+
232+ val colors = when (intent) {
233+ " primary" -> ButtonDefaults .buttonColors()
234+ " secondary" -> ButtonDefaults .outlinedButtonColors()
235+ " danger" -> ButtonDefaults .buttonColors(containerColor = MaterialTheme .colorScheme.error)
236+ else -> ButtonDefaults .buttonColors()
237+ }
238+
239+ Button (onClick = { }, colors = colors, modifier = modifier) {
240+ Text (label)
241+ }
242+ }
243+
244+ @Composable
245+ fun RenderInput (ir : NanoIR , modifier : Modifier = Modifier ) {
189246 val placeholder = ir.props[" placeholder" ]?.jsonPrimitive?.content ? : " "
190247
191248 OutlinedTextField (
192249 value = " " ,
193250 onValueChange = { },
194251 placeholder = { Text (placeholder) },
195- modifier = modifier.fillMaxWidth()
252+ modifier = modifier.fillMaxWidth(),
253+ singleLine = true
196254 )
197255 }
198256
199257 @Composable
200- private fun RenderCheckbox (ir : NanoIR , modifier : Modifier ) {
258+ fun RenderCheckbox (ir : NanoIR , modifier : Modifier = Modifier ) {
201259 Row (
202260 modifier = modifier,
203261 verticalAlignment = Alignment .CenterVertically
@@ -207,37 +265,92 @@ object ComposeNanoRenderer {
207265 }
208266
209267 @Composable
210- private fun RenderConditional (ir : NanoIR , modifier : Modifier ) {
268+ fun RenderTextArea (ir : NanoIR , modifier : Modifier = Modifier ) {
269+ val placeholder = ir.props[" placeholder" ]?.jsonPrimitive?.content ? : " "
270+ val rows = ir.props[" rows" ]?.jsonPrimitive?.content?.toIntOrNull() ? : 4
271+
272+ OutlinedTextField (
273+ value = " " ,
274+ onValueChange = { },
275+ placeholder = { Text (placeholder) },
276+ modifier = modifier
277+ .fillMaxWidth()
278+ .height((rows * 24 + 32 ).dp),
279+ minLines = rows,
280+ maxLines = rows
281+ )
282+ }
283+
284+ @Composable
285+ fun RenderSelect (ir : NanoIR , modifier : Modifier = Modifier ) {
286+ val placeholder = ir.props[" placeholder" ]?.jsonPrimitive?.content ? : " Select..."
287+
288+ // Simple dropdown placeholder - in real app would use ExposedDropdownMenuBox
289+ Box (
290+ modifier = modifier
291+ .fillMaxWidth()
292+ .border(1 .dp, MaterialTheme .colorScheme.outline, RoundedCornerShape (4 .dp))
293+ .padding(horizontal = 12 .dp, vertical = 16 .dp)
294+ ) {
295+ Text (
296+ text = placeholder,
297+ color = MaterialTheme .colorScheme.onSurfaceVariant
298+ )
299+ }
300+ }
301+
302+ // ============================================================================
303+ // Control Flow Components
304+ // ============================================================================
305+
306+ @Composable
307+ fun RenderConditional (ir : NanoIR , modifier : Modifier = Modifier ) {
211308 // Render the then branch (conditional evaluation happens at runtime)
212309 // In static preview, we just render children directly
213310 Column (modifier = modifier) {
214311 ir.children?.forEach { child ->
215- Render (child)
312+ RenderNode (child)
216313 }
217314 }
218315 }
219316
220317 @Composable
221- private fun RenderForLoop (ir : NanoIR , modifier : Modifier ) {
318+ fun RenderForLoop (ir : NanoIR , modifier : Modifier = Modifier ) {
222319 // Render the loop body once as a preview
223320 // In static preview, show a single iteration of the loop
224321 Column (modifier = modifier) {
225322 ir.children?.forEach { child ->
226- Render (child)
323+ RenderNode (child)
324+ }
325+ }
326+ }
327+
328+ // ============================================================================
329+ // Meta Components
330+ // ============================================================================
331+
332+ @Composable
333+ fun RenderComponent (ir : NanoIR , modifier : Modifier = Modifier ) {
334+ Column (modifier = modifier) {
335+ ir.children?.forEach { child ->
336+ RenderNode (child)
227337 }
228338 }
229339 }
230340
231341 @Composable
232- private fun RenderUnknown (ir : NanoIR , modifier : Modifier ) {
342+ fun RenderUnknown (ir : NanoIR , modifier : Modifier = Modifier ) {
233343 Text (
234344 text = " Unknown: ${ir.type} " ,
235345 modifier = modifier,
236346 color = MaterialTheme .colorScheme.error
237347 )
238348 }
239349
240- // Extension functions for parsing spacing/alignment values
350+ // ============================================================================
351+ // Extension Functions
352+ // ============================================================================
353+
241354 private fun String.toSpacing () = when (this ) {
242355 " xs" -> 4 .dp
243356 " sm" -> 8 .dp
0 commit comments