Skip to content

Commit 9d29850

Browse files
committed
feat: add Jewel NanoUI components for IntelliJ IDEA renderer
- Add button components with dynamic dialog support - Add content components: text, badge, icon, divider, code, link, blockquote - Add control flow components: conditional, for loop, unknown - Add data components: chart, table - Add date components: picker, range picker - Add feedback components: modal, alert, progress, spinner
1 parent 8e212e4 commit 9d29850

10 files changed

Lines changed: 1406 additions & 336 deletions

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/nano/JewelNanoRegistry.kt

Lines changed: 63 additions & 336 deletions
Large diffs are not rendered by default.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package cc.unitmesh.devins.idea.renderer.nano.components
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.shape.RoundedCornerShape
7+
import androidx.compose.runtime.*
8+
import androidx.compose.ui.Alignment
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.draw.clip
11+
import androidx.compose.ui.unit.dp
12+
import androidx.compose.ui.unit.sp
13+
import androidx.compose.ui.window.Dialog
14+
import cc.unitmesh.xuiper.eval.evaluator.NanoExpressionEvaluator
15+
import cc.unitmesh.xuiper.ir.stringProp
16+
import cc.unitmesh.xuiper.render.stateful.NanoNodeRenderer
17+
import org.jetbrains.jewel.foundation.theme.JewelTheme
18+
import org.jetbrains.jewel.ui.component.DefaultButton
19+
import org.jetbrains.jewel.ui.component.OutlinedButton
20+
import org.jetbrains.jewel.ui.component.Text
21+
22+
/**
23+
* Jewel button components for NanoUI IntelliJ IDEA renderer.
24+
* Includes: Button with dynamic dialog support
25+
*/
26+
object JewelButtonComponents {
27+
28+
private fun resolveText(ctx: JewelContext, propKey: String): String {
29+
val raw = NanoExpressionEvaluator.resolveStringProp(ctx.node, propKey, ctx.state)
30+
return NanoExpressionEvaluator.interpolateText(raw, ctx.state)
31+
}
32+
33+
val buttonRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
34+
{
35+
val ir = ctx.node
36+
val label = resolveText(ctx, "label").ifBlank { "Button" }
37+
val variant = ir.stringProp("variant") ?: "primary"
38+
val hasChildren = !ir.children.isNullOrEmpty()
39+
40+
var showDialog by remember { mutableStateOf(false) }
41+
42+
val onClick: () -> Unit = {
43+
if (hasChildren) {
44+
showDialog = true
45+
} else {
46+
ir.actions?.get("onClick")?.let { ctx.onAction(it) }
47+
}
48+
}
49+
50+
if (variant == "outline" || variant == "secondary") {
51+
OutlinedButton(onClick = onClick, modifier = ctx.payload) {
52+
Text(label)
53+
}
54+
} else {
55+
DefaultButton(onClick = onClick, modifier = ctx.payload) {
56+
Text(label)
57+
}
58+
}
59+
60+
// Dynamic dialog for button with children
61+
if (showDialog && hasChildren) {
62+
Dialog(onDismissRequest = { showDialog = false }) {
63+
Column(
64+
modifier = Modifier
65+
.clip(RoundedCornerShape(8.dp))
66+
.background(JewelTheme.globalColors.panelBackground)
67+
.border(1.dp, JewelTheme.globalColors.borders.normal, RoundedCornerShape(8.dp))
68+
.padding(16.dp)
69+
.widthIn(min = 300.dp, max = 500.dp)
70+
) {
71+
Text(
72+
text = label,
73+
fontSize = 18.sp,
74+
color = JewelTheme.globalColors.text.normal
75+
)
76+
Spacer(Modifier.height(12.dp))
77+
78+
ir.children?.forEach { child ->
79+
ctx.renderChild(child, Modifier.fillMaxWidth()).invoke()
80+
}
81+
82+
Spacer(Modifier.height(16.dp))
83+
Row(
84+
modifier = Modifier.fillMaxWidth(),
85+
horizontalArrangement = Arrangement.End,
86+
verticalAlignment = Alignment.CenterVertically
87+
) {
88+
OutlinedButton(onClick = { showDialog = false }) {
89+
Text("Cancel")
90+
}
91+
Spacer(Modifier.width(8.dp))
92+
DefaultButton(onClick = {
93+
showDialog = false
94+
ir.actions?.get("onClick")?.let { ctx.onAction(it) }
95+
}) {
96+
Text("Confirm")
97+
}
98+
}
99+
}
100+
}
101+
}
102+
}
103+
}
104+
}
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package cc.unitmesh.devins.idea.renderer.nano.components
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
5+
import androidx.compose.foundation.clickable
6+
import androidx.compose.foundation.layout.*
7+
import androidx.compose.foundation.shape.RoundedCornerShape
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Modifier
10+
import androidx.compose.ui.draw.clip
11+
import androidx.compose.ui.graphics.Color
12+
import androidx.compose.ui.text.font.FontFamily
13+
import androidx.compose.ui.text.font.FontWeight
14+
import androidx.compose.ui.unit.dp
15+
import androidx.compose.ui.unit.sp
16+
import cc.unitmesh.xuiper.eval.evaluator.NanoExpressionEvaluator
17+
import cc.unitmesh.xuiper.ir.stringProp
18+
import cc.unitmesh.xuiper.render.stateful.NanoNodeRenderer
19+
import org.jetbrains.jewel.foundation.theme.JewelTheme
20+
import org.jetbrains.jewel.ui.component.Text
21+
import java.awt.Desktop
22+
import java.net.URI
23+
24+
/**
25+
* Jewel content components for NanoUI IntelliJ IDEA renderer.
26+
* Includes: Text, Badge, Icon, Divider, Code, Link, Blockquote
27+
*/
28+
object JewelContentComponents {
29+
30+
private fun resolveText(ctx: JewelContext, propKey: String): String {
31+
val raw = NanoExpressionEvaluator.resolveStringProp(ctx.node, propKey, ctx.state)
32+
return NanoExpressionEvaluator.interpolateText(raw, ctx.state)
33+
}
34+
35+
val textRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
36+
{
37+
val content = resolveText(ctx, "content")
38+
val style = ctx.node.stringProp("style") ?: "body"
39+
Text(
40+
text = content,
41+
modifier = ctx.payload,
42+
color = JewelTheme.globalColors.text.normal,
43+
fontSize = when (style) {
44+
"h1" -> 24.sp
45+
"h2" -> 20.sp
46+
"h3" -> 18.sp
47+
"h4" -> 16.sp
48+
"caption" -> 12.sp
49+
else -> 14.sp
50+
},
51+
fontWeight = when (style) {
52+
"h1", "h2", "h3", "h4" -> FontWeight.Bold
53+
else -> FontWeight.Normal
54+
}
55+
)
56+
}
57+
}
58+
59+
val badgeRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
60+
{
61+
val content = resolveText(ctx, "content")
62+
val variant = ctx.node.stringProp("variant") ?: "default"
63+
val bgColor = when (variant) {
64+
"success" -> Color(0xFF81C784).copy(alpha = 0.2f)
65+
"warning" -> Color(0xFFFFB74D).copy(alpha = 0.2f)
66+
"error" -> Color(0xFFE57373).copy(alpha = 0.2f)
67+
"info" -> Color(0xFF64B5F6).copy(alpha = 0.2f)
68+
else -> JewelTheme.globalColors.panelBackground
69+
}
70+
Box(
71+
modifier = ctx.payload
72+
.clip(RoundedCornerShape(4.dp))
73+
.background(bgColor)
74+
.padding(horizontal = 8.dp, vertical = 4.dp)
75+
) {
76+
Text(
77+
text = content,
78+
color = JewelTheme.globalColors.text.normal,
79+
fontSize = 12.sp
80+
)
81+
}
82+
}
83+
}
84+
85+
val iconRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
86+
{
87+
val name = ctx.node.stringProp("name") ?: "circle"
88+
val iconText = when (name) {
89+
"check" -> "OK"
90+
"close", "x" -> "X"
91+
"warning" -> "!"
92+
"info" -> "i"
93+
"error" -> "X"
94+
"success" -> "OK"
95+
"train" -> "[T]"
96+
"hotel" -> "[H]"
97+
"food" -> "[F]"
98+
"ticket" -> "[K]"
99+
else -> "o"
100+
}
101+
Text(text = iconText, modifier = ctx.payload, color = JewelTheme.globalColors.text.normal)
102+
}
103+
}
104+
105+
val dividerRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
106+
{
107+
Box(
108+
modifier = ctx.payload
109+
.fillMaxWidth()
110+
.height(1.dp)
111+
.background(JewelTheme.globalColors.borders.normal)
112+
)
113+
}
114+
}
115+
116+
val codeRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
117+
{
118+
val content = resolveText(ctx, "content")
119+
val language = ctx.node.stringProp("language") ?: ""
120+
val inline = ctx.node.stringProp("inline")?.toBoolean() ?: false
121+
122+
if (inline) {
123+
Text(
124+
text = content,
125+
modifier = ctx.payload
126+
.clip(RoundedCornerShape(4.dp))
127+
.background(JewelTheme.globalColors.panelBackground)
128+
.padding(horizontal = 4.dp, vertical = 2.dp),
129+
fontFamily = FontFamily.Monospace,
130+
fontSize = 13.sp,
131+
color = JewelTheme.globalColors.text.normal
132+
)
133+
} else {
134+
Column(
135+
modifier = ctx.payload
136+
.fillMaxWidth()
137+
.clip(RoundedCornerShape(4.dp))
138+
.background(JewelTheme.globalColors.panelBackground)
139+
.border(1.dp, JewelTheme.globalColors.borders.normal, RoundedCornerShape(4.dp))
140+
) {
141+
if (language.isNotBlank()) {
142+
Text(
143+
text = language,
144+
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
145+
fontSize = 11.sp,
146+
color = JewelTheme.globalColors.text.normal.copy(alpha = 0.6f)
147+
)
148+
}
149+
Text(
150+
text = content,
151+
modifier = Modifier.padding(12.dp),
152+
fontFamily = FontFamily.Monospace,
153+
fontSize = 13.sp,
154+
color = JewelTheme.globalColors.text.normal
155+
)
156+
}
157+
}
158+
}
159+
}
160+
161+
val linkRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
162+
{
163+
val content = resolveText(ctx, "content")
164+
val href = ctx.node.stringProp("href") ?: ""
165+
val label = content.ifBlank { href }
166+
167+
Text(
168+
text = label,
169+
modifier = ctx.payload.clickable {
170+
if (href.isNotBlank()) {
171+
try {
172+
Desktop.getDesktop().browse(URI(href))
173+
} catch (_: Exception) {
174+
// Ignore errors
175+
}
176+
}
177+
},
178+
color = JewelTheme.globalColors.text.info,
179+
fontSize = 14.sp
180+
)
181+
}
182+
}
183+
184+
val blockquoteRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
185+
{
186+
val content = resolveText(ctx, "content")
187+
Row(
188+
modifier = ctx.payload
189+
.fillMaxWidth()
190+
.padding(vertical = 4.dp)
191+
) {
192+
Box(
193+
modifier = Modifier
194+
.width(4.dp)
195+
.fillMaxHeight()
196+
.background(JewelTheme.globalColors.borders.normal)
197+
)
198+
Spacer(Modifier.width(12.dp))
199+
Text(
200+
text = content,
201+
color = JewelTheme.globalColors.text.normal.copy(alpha = 0.8f),
202+
fontSize = 14.sp
203+
)
204+
}
205+
}
206+
}
207+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package cc.unitmesh.devins.idea.renderer.nano.components
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.border
5+
import androidx.compose.foundation.layout.*
6+
import androidx.compose.foundation.shape.RoundedCornerShape
7+
import androidx.compose.runtime.Composable
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.draw.clip
10+
import androidx.compose.ui.graphics.Color
11+
import androidx.compose.ui.unit.dp
12+
import androidx.compose.ui.unit.sp
13+
import cc.unitmesh.xuiper.eval.evaluator.NanoExpressionEvaluator
14+
import cc.unitmesh.xuiper.render.stateful.NanoNodeRenderer
15+
import org.jetbrains.jewel.foundation.theme.JewelTheme
16+
import org.jetbrains.jewel.ui.component.Text
17+
18+
/**
19+
* Jewel control flow components for NanoUI IntelliJ IDEA renderer.
20+
* Includes: Conditional, ForLoop, Unknown
21+
*/
22+
object JewelControlFlowComponents {
23+
24+
val conditionalRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
25+
{
26+
val ir = ctx.node
27+
val condition = ir.condition
28+
val isVisible = NanoExpressionEvaluator.evaluateCondition(condition, ctx.state)
29+
30+
if (isVisible) {
31+
Column(modifier = ctx.payload) {
32+
ir.children?.forEach { child ->
33+
ctx.renderChild(child, Modifier).invoke()
34+
}
35+
}
36+
}
37+
}
38+
}
39+
40+
val forLoopRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
41+
{
42+
val ir = ctx.node
43+
val loop = ir.loop
44+
val iterableExpr = loop?.iterable?.trim().orEmpty()
45+
val resolved = if (iterableExpr.isNotBlank()) {
46+
NanoExpressionEvaluator.resolveAny(iterableExpr, ctx.state)
47+
} else null
48+
49+
val items: List<Any?> = when (resolved) {
50+
is List<*> -> resolved
51+
else -> emptyList()
52+
}
53+
54+
Column(modifier = ctx.payload, verticalArrangement = Arrangement.spacedBy(8.dp)) {
55+
items.forEachIndexed { _, _ ->
56+
ir.children?.forEach { child ->
57+
ctx.renderChild(child, Modifier).invoke()
58+
}
59+
}
60+
}
61+
}
62+
}
63+
64+
val unknownRenderer: JewelRenderer = NanoNodeRenderer { ctx ->
65+
{
66+
Box(
67+
modifier = ctx.payload
68+
.clip(RoundedCornerShape(4.dp))
69+
.background(Color(0xFFFFB74D).copy(alpha = 0.1f))
70+
.border(1.dp, Color(0xFFFFB74D).copy(alpha = 0.3f), RoundedCornerShape(4.dp))
71+
.padding(8.dp)
72+
) {
73+
Text(
74+
text = "Unknown: ${ctx.node.type}",
75+
color = JewelTheme.globalColors.text.normal,
76+
fontSize = 12.sp
77+
)
78+
}
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)