@wire-dsl/engine 0.4.1 → 0.6.0
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 +1 -1
- package/dist/index.cjs +123 -46
- package/dist/index.d.cts +19 -6
- package/dist/index.d.ts +19 -6
- package/dist/index.js +123 -46
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -2220,8 +2220,8 @@ var IRStyleSchema = import_zod.z.object({
|
|
|
2220
2220
|
var IRNodeStyleSchema = import_zod.z.object({
|
|
2221
2221
|
padding: import_zod.z.string().optional(),
|
|
2222
2222
|
gap: import_zod.z.string().optional(),
|
|
2223
|
-
|
|
2224
|
-
|
|
2223
|
+
justify: import_zod.z.enum(["start", "center", "end", "stretch", "spaceBetween", "spaceAround"]).optional(),
|
|
2224
|
+
align: import_zod.z.enum(["start", "center", "end"]).optional(),
|
|
2225
2225
|
background: import_zod.z.string().optional()
|
|
2226
2226
|
});
|
|
2227
2227
|
var IRMetaSchema = import_zod.z.object({
|
|
@@ -2507,6 +2507,9 @@ ${messages}`);
|
|
|
2507
2507
|
if (layoutParams.gap !== void 0) {
|
|
2508
2508
|
style.gap = String(layoutParams.gap);
|
|
2509
2509
|
}
|
|
2510
|
+
if (layoutParams.justify !== void 0) {
|
|
2511
|
+
style.justify = layoutParams.justify;
|
|
2512
|
+
}
|
|
2510
2513
|
if (layoutParams.align !== void 0) {
|
|
2511
2514
|
style.align = layoutParams.align;
|
|
2512
2515
|
}
|
|
@@ -2588,7 +2591,7 @@ ${messages}`);
|
|
|
2588
2591
|
"Label",
|
|
2589
2592
|
"Image",
|
|
2590
2593
|
"Card",
|
|
2591
|
-
"
|
|
2594
|
+
"Stat",
|
|
2592
2595
|
"Topbar",
|
|
2593
2596
|
"Table",
|
|
2594
2597
|
"Chart",
|
|
@@ -3153,8 +3156,9 @@ var LayoutEngine = class {
|
|
|
3153
3156
|
}
|
|
3154
3157
|
});
|
|
3155
3158
|
} else {
|
|
3156
|
-
const
|
|
3157
|
-
|
|
3159
|
+
const justify = node.style.justify || "stretch";
|
|
3160
|
+
const crossAlign = node.style.align || "start";
|
|
3161
|
+
if (justify === "stretch") {
|
|
3158
3162
|
let currentX = x;
|
|
3159
3163
|
const childWidth = this.calculateChildWidth(children.length, width, gap);
|
|
3160
3164
|
let stackHeight = 0;
|
|
@@ -3176,43 +3180,44 @@ var LayoutEngine = class {
|
|
|
3176
3180
|
});
|
|
3177
3181
|
} else {
|
|
3178
3182
|
const childWidths = [];
|
|
3183
|
+
const childHeights = [];
|
|
3179
3184
|
const explicitHeightFlags = [];
|
|
3180
|
-
const
|
|
3185
|
+
const flexIndices = /* @__PURE__ */ new Set();
|
|
3181
3186
|
let stackHeight = 0;
|
|
3182
3187
|
children.forEach((childRef, index) => {
|
|
3183
3188
|
const childNode = this.nodes[childRef.ref];
|
|
3184
|
-
let childWidth = this.getIntrinsicComponentWidth(childNode);
|
|
3185
|
-
let childHeight = this.getComponentHeight();
|
|
3186
3189
|
const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
|
|
3187
3190
|
const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
|
|
3188
3191
|
const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
|
|
3189
|
-
|
|
3192
|
+
const isFlexContainer = !hasExplicitWidth && childNode?.kind === "container" && !this.containerHasIntrinsicWidth(childNode);
|
|
3193
|
+
let childWidth;
|
|
3194
|
+
if (isBlockButton || isFlexContainer) {
|
|
3190
3195
|
childWidth = 0;
|
|
3191
|
-
|
|
3196
|
+
flexIndices.add(index);
|
|
3192
3197
|
} else if (hasExplicitWidth) {
|
|
3193
3198
|
childWidth = Number(childNode.props.width);
|
|
3194
|
-
}
|
|
3195
|
-
|
|
3196
|
-
childHeight = Number(childNode.props.height);
|
|
3199
|
+
} else {
|
|
3200
|
+
childWidth = this.getIntrinsicWidth(childNode, width);
|
|
3197
3201
|
}
|
|
3198
3202
|
childWidths.push(childWidth);
|
|
3203
|
+
childHeights.push(this.getComponentHeight());
|
|
3199
3204
|
explicitHeightFlags.push(hasExplicitHeight);
|
|
3200
3205
|
});
|
|
3201
3206
|
const totalGapWidth = gap * Math.max(0, children.length - 1);
|
|
3202
|
-
if (
|
|
3207
|
+
if (flexIndices.size > 0) {
|
|
3203
3208
|
const fixedWidth = childWidths.reduce((sum, w, idx) => {
|
|
3204
|
-
return
|
|
3209
|
+
return flexIndices.has(idx) ? sum : sum + w;
|
|
3205
3210
|
}, 0);
|
|
3206
3211
|
const remainingWidth = width - totalGapWidth - fixedWidth;
|
|
3207
|
-
const
|
|
3208
|
-
|
|
3209
|
-
childWidths[
|
|
3212
|
+
const widthPerFlex = Math.max(1, remainingWidth / flexIndices.size);
|
|
3213
|
+
flexIndices.forEach((idx) => {
|
|
3214
|
+
childWidths[idx] = widthPerFlex;
|
|
3210
3215
|
});
|
|
3211
3216
|
}
|
|
3212
3217
|
children.forEach((childRef, index) => {
|
|
3213
3218
|
const childNode = this.nodes[childRef.ref];
|
|
3214
|
-
let childHeight = this.getComponentHeight();
|
|
3215
3219
|
const childWidth = childWidths[index];
|
|
3220
|
+
let childHeight = this.getComponentHeight();
|
|
3216
3221
|
if (explicitHeightFlags[index] && childNode?.kind === "component") {
|
|
3217
3222
|
childHeight = Number(childNode.props.height);
|
|
3218
3223
|
} else if (childNode?.kind === "container") {
|
|
@@ -3220,21 +3225,37 @@ var LayoutEngine = class {
|
|
|
3220
3225
|
} else if (childNode?.kind === "component") {
|
|
3221
3226
|
childHeight = this.getIntrinsicComponentHeight(childNode, childWidth);
|
|
3222
3227
|
}
|
|
3228
|
+
childHeights[index] = childHeight;
|
|
3223
3229
|
stackHeight = Math.max(stackHeight, childHeight);
|
|
3224
3230
|
});
|
|
3225
3231
|
const totalChildWidth = childWidths.reduce((sum, w) => sum + w, 0);
|
|
3226
3232
|
const totalContentWidth = totalChildWidth + totalGapWidth;
|
|
3227
3233
|
let startX = x;
|
|
3228
|
-
|
|
3234
|
+
let dynamicGap = gap;
|
|
3235
|
+
if (justify === "center") {
|
|
3229
3236
|
startX = x + (width - totalContentWidth) / 2;
|
|
3230
|
-
} else if (
|
|
3237
|
+
} else if (justify === "end") {
|
|
3231
3238
|
startX = x + width - totalContentWidth;
|
|
3239
|
+
} else if (justify === "spaceBetween") {
|
|
3240
|
+
startX = x;
|
|
3241
|
+
dynamicGap = children.length > 1 ? (width - totalChildWidth) / (children.length - 1) : 0;
|
|
3242
|
+
} else if (justify === "spaceAround") {
|
|
3243
|
+
const spacing = children.length > 0 ? (width - totalChildWidth) / children.length : 0;
|
|
3244
|
+
startX = x + spacing / 2;
|
|
3245
|
+
dynamicGap = spacing;
|
|
3232
3246
|
}
|
|
3233
3247
|
let currentX = startX;
|
|
3234
3248
|
children.forEach((childRef, index) => {
|
|
3235
3249
|
const childWidth = childWidths[index];
|
|
3236
|
-
|
|
3237
|
-
|
|
3250
|
+
const childHeight = childHeights[index];
|
|
3251
|
+
let childY = y;
|
|
3252
|
+
if (crossAlign === "center") {
|
|
3253
|
+
childY = y + Math.round((stackHeight - childHeight) / 2);
|
|
3254
|
+
} else if (crossAlign === "end") {
|
|
3255
|
+
childY = y + stackHeight - childHeight;
|
|
3256
|
+
}
|
|
3257
|
+
this.calculateNode(childRef.ref, currentX, childY, childWidth, childHeight, "stack");
|
|
3258
|
+
currentX += childWidth + dynamicGap;
|
|
3238
3259
|
});
|
|
3239
3260
|
}
|
|
3240
3261
|
}
|
|
@@ -3731,7 +3752,7 @@ var LayoutEngine = class {
|
|
|
3731
3752
|
if (node.componentType === "Textarea") return 100 + controlLabelOffset;
|
|
3732
3753
|
if (node.componentType === "Modal") return 300;
|
|
3733
3754
|
if (node.componentType === "Card") return 120;
|
|
3734
|
-
if (node.componentType === "
|
|
3755
|
+
if (node.componentType === "Stat") return 120;
|
|
3735
3756
|
if (node.componentType === "Chart" || node.componentType === "ChartPlaceholder") return 250;
|
|
3736
3757
|
if (node.componentType === "List") {
|
|
3737
3758
|
const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
@@ -3757,6 +3778,47 @@ var LayoutEngine = class {
|
|
|
3757
3778
|
getControlLabelOffset(label) {
|
|
3758
3779
|
return label.trim().length > 0 ? 18 : 0;
|
|
3759
3780
|
}
|
|
3781
|
+
/**
|
|
3782
|
+
* Returns true when a container's width can be calculated from its children
|
|
3783
|
+
* (i.e. it is a horizontal non-stretch stack). False means the container
|
|
3784
|
+
* behaves like `flex-grow:1` and should absorb remaining space.
|
|
3785
|
+
*/
|
|
3786
|
+
containerHasIntrinsicWidth(node) {
|
|
3787
|
+
if (node.kind !== "container") return false;
|
|
3788
|
+
return node.containerType === "stack" && String(node.params.direction || "vertical") === "horizontal" && (node.style.justify || "stretch") !== "stretch";
|
|
3789
|
+
}
|
|
3790
|
+
/**
|
|
3791
|
+
* Returns the natural (intrinsic) width of any node — component or container.
|
|
3792
|
+
* For horizontal non-stretch containers the width is the sum of their children's
|
|
3793
|
+
* intrinsic widths plus gaps, capped at `availableWidth`. All other containers
|
|
3794
|
+
* are assumed to take the full available width (they stretch or grow).
|
|
3795
|
+
*/
|
|
3796
|
+
getIntrinsicWidth(node, availableWidth) {
|
|
3797
|
+
if (!node) return 120;
|
|
3798
|
+
if (node.kind === "component") {
|
|
3799
|
+
return this.getIntrinsicComponentWidth(node);
|
|
3800
|
+
}
|
|
3801
|
+
if (node.kind === "container") {
|
|
3802
|
+
if (this.containerHasIntrinsicWidth(node)) {
|
|
3803
|
+
const gap = this.resolveSpacing(node.style.gap);
|
|
3804
|
+
const padding = this.resolveSpacing(node.style.padding);
|
|
3805
|
+
const innerAvailable = Math.max(0, availableWidth - padding * 2);
|
|
3806
|
+
const children = node.children ?? [];
|
|
3807
|
+
let total = padding * 2;
|
|
3808
|
+
children.forEach((childRef, idx) => {
|
|
3809
|
+
const child = this.nodes[childRef.ref];
|
|
3810
|
+
total += this.getIntrinsicWidth(child, innerAvailable);
|
|
3811
|
+
if (idx < children.length - 1) total += gap;
|
|
3812
|
+
});
|
|
3813
|
+
return Math.min(total, availableWidth);
|
|
3814
|
+
}
|
|
3815
|
+
return availableWidth;
|
|
3816
|
+
}
|
|
3817
|
+
if (node.kind === "instance") {
|
|
3818
|
+
return availableWidth;
|
|
3819
|
+
}
|
|
3820
|
+
return 120;
|
|
3821
|
+
}
|
|
3760
3822
|
getIntrinsicComponentWidth(node) {
|
|
3761
3823
|
if (!node || node.kind !== "component") {
|
|
3762
3824
|
return 120;
|
|
@@ -3782,7 +3844,10 @@ var LayoutEngine = class {
|
|
|
3782
3844
|
const density = this.style.density || "normal";
|
|
3783
3845
|
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
3784
3846
|
const textWidth = this.estimateTextWidth(text, fontSize);
|
|
3785
|
-
|
|
3847
|
+
const iconName = String(node.props.icon || "").trim();
|
|
3848
|
+
const iconSize = iconName ? Math.round(fontSize * 1.1) : 0;
|
|
3849
|
+
const iconGap = iconName ? 8 : 0;
|
|
3850
|
+
return Math.max(60, Math.ceil(textWidth + iconSize + iconGap + (paddingX + extraPadding) * 2));
|
|
3786
3851
|
}
|
|
3787
3852
|
if (node.componentType === "Label" || node.componentType === "Text") {
|
|
3788
3853
|
const text = String(node.props.text || "");
|
|
@@ -3813,7 +3878,7 @@ var LayoutEngine = class {
|
|
|
3813
3878
|
if (node.componentType === "Table") {
|
|
3814
3879
|
return 400;
|
|
3815
3880
|
}
|
|
3816
|
-
if (node.componentType === "
|
|
3881
|
+
if (node.componentType === "Stat" || node.componentType === "Card") {
|
|
3817
3882
|
return 280;
|
|
3818
3883
|
}
|
|
3819
3884
|
if (node.componentType === "SidebarMenu") {
|
|
@@ -4869,8 +4934,8 @@ var SVGRenderer = class {
|
|
|
4869
4934
|
return this.renderModal(node, pos);
|
|
4870
4935
|
case "List":
|
|
4871
4936
|
return this.renderList(node, pos);
|
|
4872
|
-
case "
|
|
4873
|
-
return this.
|
|
4937
|
+
case "Stat":
|
|
4938
|
+
return this.renderStat(node, pos);
|
|
4874
4939
|
case "Image":
|
|
4875
4940
|
return this.renderImage(node, pos);
|
|
4876
4941
|
// Icon components
|
|
@@ -4916,6 +4981,7 @@ var SVGRenderer = class {
|
|
|
4916
4981
|
const text = String(node.props.text || "Button");
|
|
4917
4982
|
const variant = String(node.props.variant || "default");
|
|
4918
4983
|
const size = String(node.props.size || "md");
|
|
4984
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
4919
4985
|
const density = this.ir.project.style.density || "normal";
|
|
4920
4986
|
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
4921
4987
|
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
@@ -4961,7 +5027,7 @@ var SVGRenderer = class {
|
|
|
4961
5027
|
textX = pos.x + buttonWidth / 2;
|
|
4962
5028
|
textAnchor = "middle";
|
|
4963
5029
|
}
|
|
4964
|
-
let svg = `<g${this.getDataNodeId(node)}>
|
|
5030
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
4965
5031
|
<rect x="${pos.x}" y="${buttonY}"
|
|
4966
5032
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
4967
5033
|
rx="${radius}"
|
|
@@ -5027,6 +5093,7 @@ var SVGRenderer = class {
|
|
|
5027
5093
|
const placeholder = String(node.props.placeholder || "");
|
|
5028
5094
|
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
5029
5095
|
const iconRightName = String(node.props.iconRight || "").trim();
|
|
5096
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5030
5097
|
const radius = this.tokens.input.radius;
|
|
5031
5098
|
const fontSize = this.tokens.input.fontSize;
|
|
5032
5099
|
const paddingX = this.tokens.input.paddingX;
|
|
@@ -5043,7 +5110,7 @@ var SVGRenderer = class {
|
|
|
5043
5110
|
const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
|
|
5044
5111
|
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
5045
5112
|
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
5046
|
-
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5113
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
|
|
5047
5114
|
if (label) {
|
|
5048
5115
|
svg += `
|
|
5049
5116
|
<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
@@ -5612,7 +5679,8 @@ var SVGRenderer = class {
|
|
|
5612
5679
|
const fontSize = this.tokens.text.fontSize;
|
|
5613
5680
|
const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
|
|
5614
5681
|
const lines = this.wrapTextToLines(text, pos.width, fontSize);
|
|
5615
|
-
const
|
|
5682
|
+
const totalTextHeight = lines.length * lineHeightPx;
|
|
5683
|
+
const firstLineY = pos.y + Math.round(Math.max(0, (pos.height - totalTextHeight) / 2)) + fontSize;
|
|
5616
5684
|
const tspans = lines.map(
|
|
5617
5685
|
(line, index) => `<tspan x="${pos.x}" dy="${index === 0 ? 0 : lineHeightPx}">${this.escapeXml(line)}</tspan>`
|
|
5618
5686
|
).join("");
|
|
@@ -5625,8 +5693,9 @@ var SVGRenderer = class {
|
|
|
5625
5693
|
}
|
|
5626
5694
|
renderLabel(node, pos) {
|
|
5627
5695
|
const text = String(node.props.text || "Label");
|
|
5696
|
+
const textY = pos.y + Math.round(pos.height / 2) + 4;
|
|
5628
5697
|
return `<g${this.getDataNodeId(node)}>
|
|
5629
|
-
<text x="${pos.x}" y="${
|
|
5698
|
+
<text x="${pos.x}" y="${textY}"
|
|
5630
5699
|
font-family="Arial, Helvetica, sans-serif"
|
|
5631
5700
|
font-size="12"
|
|
5632
5701
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
|
|
@@ -5681,6 +5750,7 @@ var SVGRenderer = class {
|
|
|
5681
5750
|
const placeholder = String(node.props.placeholder || "Select...");
|
|
5682
5751
|
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
5683
5752
|
const iconRightName = String(node.props.iconRight || "").trim();
|
|
5753
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5684
5754
|
const labelOffset = this.getControlLabelOffset(label);
|
|
5685
5755
|
const controlY = pos.y + labelOffset;
|
|
5686
5756
|
const controlHeight = Math.max(16, pos.height - labelOffset);
|
|
@@ -5694,7 +5764,7 @@ var SVGRenderer = class {
|
|
|
5694
5764
|
const chevronWidth = 20;
|
|
5695
5765
|
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
5696
5766
|
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
5697
|
-
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5767
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
|
|
5698
5768
|
if (label) {
|
|
5699
5769
|
svg += `
|
|
5700
5770
|
<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
@@ -5743,10 +5813,11 @@ var SVGRenderer = class {
|
|
|
5743
5813
|
renderCheckbox(node, pos) {
|
|
5744
5814
|
const label = String(node.props.label || "Checkbox");
|
|
5745
5815
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
5816
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5746
5817
|
const controlColor = this.resolveControlColor();
|
|
5747
5818
|
const checkboxSize = 18;
|
|
5748
5819
|
const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
|
|
5749
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5820
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5750
5821
|
<rect x="${pos.x}" y="${checkboxY}"
|
|
5751
5822
|
width="${checkboxSize}" height="${checkboxSize}"
|
|
5752
5823
|
rx="4"
|
|
@@ -5767,10 +5838,11 @@ var SVGRenderer = class {
|
|
|
5767
5838
|
renderRadio(node, pos) {
|
|
5768
5839
|
const label = String(node.props.label || "Radio");
|
|
5769
5840
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
5841
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5770
5842
|
const controlColor = this.resolveControlColor();
|
|
5771
5843
|
const radioSize = 16;
|
|
5772
5844
|
const radioY = pos.y + pos.height / 2 - radioSize / 2;
|
|
5773
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5845
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5774
5846
|
<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
|
|
5775
5847
|
r="${radioSize / 2}"
|
|
5776
5848
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -5788,11 +5860,12 @@ var SVGRenderer = class {
|
|
|
5788
5860
|
renderToggle(node, pos) {
|
|
5789
5861
|
const label = String(node.props.label || "Toggle");
|
|
5790
5862
|
const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
|
|
5863
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5791
5864
|
const controlColor = this.resolveControlColor();
|
|
5792
5865
|
const toggleWidth = 40;
|
|
5793
5866
|
const toggleHeight = 20;
|
|
5794
5867
|
const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
|
|
5795
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5868
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5796
5869
|
<rect x="${pos.x}" y="${toggleY}"
|
|
5797
5870
|
width="${toggleWidth}" height="${toggleHeight}"
|
|
5798
5871
|
rx="10"
|
|
@@ -6088,7 +6161,7 @@ var SVGRenderer = class {
|
|
|
6088
6161
|
text-anchor="middle">${node.componentType}</text>
|
|
6089
6162
|
</g>`;
|
|
6090
6163
|
}
|
|
6091
|
-
|
|
6164
|
+
renderStat(node, pos) {
|
|
6092
6165
|
const title = String(node.props.title || "Metric");
|
|
6093
6166
|
const value = String(node.props.value || "0");
|
|
6094
6167
|
const rawCaption = String(node.props.caption || "");
|
|
@@ -6096,7 +6169,9 @@ var SVGRenderer = class {
|
|
|
6096
6169
|
const hasCaption = caption.trim().length > 0;
|
|
6097
6170
|
const iconName = String(node.props.icon || "").trim();
|
|
6098
6171
|
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
6099
|
-
const
|
|
6172
|
+
const variant = String(node.props.variant || "default");
|
|
6173
|
+
const baseAccent = this.resolveAccentColor();
|
|
6174
|
+
const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
|
|
6100
6175
|
const padding = this.resolveSpacing(node.style.padding) || 16;
|
|
6101
6176
|
const innerX = pos.x + padding;
|
|
6102
6177
|
const innerY = pos.y + padding;
|
|
@@ -6120,7 +6195,7 @@ var SVGRenderer = class {
|
|
|
6120
6195
|
const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
|
|
6121
6196
|
const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, innerWidth), captionSize) : "";
|
|
6122
6197
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
6123
|
-
<!--
|
|
6198
|
+
<!-- Stat Background -->
|
|
6124
6199
|
<rect x="${pos.x}" y="${pos.y}"
|
|
6125
6200
|
width="${pos.width}" height="${pos.height}"
|
|
6126
6201
|
rx="8"
|
|
@@ -6757,8 +6832,8 @@ var SVGRenderer = class {
|
|
|
6757
6832
|
return false;
|
|
6758
6833
|
}
|
|
6759
6834
|
const direction = String(parent.params.direction || "vertical");
|
|
6760
|
-
const
|
|
6761
|
-
return direction === "horizontal" &&
|
|
6835
|
+
const justify = parent.style.justify || "stretch";
|
|
6836
|
+
return direction === "horizontal" && justify === "stretch";
|
|
6762
6837
|
}
|
|
6763
6838
|
buildParentContainerIndex() {
|
|
6764
6839
|
this.parentContainerByChildId.clear();
|
|
@@ -7383,9 +7458,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7383
7458
|
return svg;
|
|
7384
7459
|
}
|
|
7385
7460
|
/**
|
|
7386
|
-
* Render
|
|
7461
|
+
* Render Stat with gray blocks instead of values
|
|
7387
7462
|
*/
|
|
7388
|
-
|
|
7463
|
+
renderStat(node, pos) {
|
|
7389
7464
|
const hasIcon = String(node.props.icon || "").trim().length > 0;
|
|
7390
7465
|
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7391
7466
|
const iconSize = 20;
|
|
@@ -8512,7 +8587,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8512
8587
|
/**
|
|
8513
8588
|
* Render stat card with sketch filter and Comic Sans
|
|
8514
8589
|
*/
|
|
8515
|
-
|
|
8590
|
+
renderStat(node, pos) {
|
|
8516
8591
|
const title = String(node.props.title || "Metric");
|
|
8517
8592
|
const value = String(node.props.value || "0");
|
|
8518
8593
|
const rawCaption = String(node.props.caption || "");
|
|
@@ -8520,7 +8595,9 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8520
8595
|
const hasCaption = caption.trim().length > 0;
|
|
8521
8596
|
const iconName = String(node.props.icon || "").trim();
|
|
8522
8597
|
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
8523
|
-
const
|
|
8598
|
+
const variant = String(node.props.variant || "default");
|
|
8599
|
+
const baseAccent = this.resolveAccentColor();
|
|
8600
|
+
const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
|
|
8524
8601
|
const padding = this.resolveSpacing(node.style.padding);
|
|
8525
8602
|
const innerX = pos.x + padding;
|
|
8526
8603
|
const innerY = pos.y + padding;
|
|
@@ -8543,7 +8620,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8543
8620
|
const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
|
|
8544
8621
|
const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, pos.width - padding * 2), captionSize) : "";
|
|
8545
8622
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
8546
|
-
<!--
|
|
8623
|
+
<!-- Stat Background -->
|
|
8547
8624
|
<rect x="${pos.x}" y="${pos.y}"
|
|
8548
8625
|
width="${pos.width}" height="${pos.height}"
|
|
8549
8626
|
rx="8"
|
package/dist/index.d.cts
CHANGED
|
@@ -290,8 +290,8 @@ interface IRComponentNode {
|
|
|
290
290
|
interface IRNodeStyle {
|
|
291
291
|
padding?: string;
|
|
292
292
|
gap?: string;
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
justify?: 'start' | 'center' | 'end' | 'stretch' | 'spaceBetween' | 'spaceAround';
|
|
294
|
+
align?: 'start' | 'center' | 'end';
|
|
295
295
|
background?: string;
|
|
296
296
|
}
|
|
297
297
|
interface IRMeta {
|
|
@@ -480,6 +480,19 @@ declare class LayoutEngine {
|
|
|
480
480
|
private wrapTextToLines;
|
|
481
481
|
private getIntrinsicComponentHeight;
|
|
482
482
|
private getControlLabelOffset;
|
|
483
|
+
/**
|
|
484
|
+
* Returns true when a container's width can be calculated from its children
|
|
485
|
+
* (i.e. it is a horizontal non-stretch stack). False means the container
|
|
486
|
+
* behaves like `flex-grow:1` and should absorb remaining space.
|
|
487
|
+
*/
|
|
488
|
+
private containerHasIntrinsicWidth;
|
|
489
|
+
/**
|
|
490
|
+
* Returns the natural (intrinsic) width of any node — component or container.
|
|
491
|
+
* For horizontal non-stretch containers the width is the sum of their children's
|
|
492
|
+
* intrinsic widths plus gaps, capped at `availableWidth`. All other containers
|
|
493
|
+
* are assumed to take the full available width (they stretch or grow).
|
|
494
|
+
*/
|
|
495
|
+
private getIntrinsicWidth;
|
|
483
496
|
private getIntrinsicComponentWidth;
|
|
484
497
|
private calculateChildHeight;
|
|
485
498
|
private calculateChildWidth;
|
|
@@ -706,7 +719,7 @@ declare class SVGRenderer {
|
|
|
706
719
|
protected renderModal(node: IRComponentNode, pos: any): string;
|
|
707
720
|
protected renderList(node: IRComponentNode, pos: any): string;
|
|
708
721
|
protected renderGenericComponent(node: IRComponentNode, pos: any): string;
|
|
709
|
-
protected
|
|
722
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
710
723
|
protected renderImage(node: IRComponentNode, pos: any): string;
|
|
711
724
|
protected renderBreadcrumbs(node: IRComponentNode, pos: any): string;
|
|
712
725
|
protected renderSidebarMenu(node: IRComponentNode, pos: any): string;
|
|
@@ -892,9 +905,9 @@ declare class SkeletonSVGRenderer extends SVGRenderer {
|
|
|
892
905
|
*/
|
|
893
906
|
protected renderTopbar(node: IRComponentNode, pos: any): string;
|
|
894
907
|
/**
|
|
895
|
-
* Render
|
|
908
|
+
* Render Stat with gray blocks instead of values
|
|
896
909
|
*/
|
|
897
|
-
protected
|
|
910
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
898
911
|
/**
|
|
899
912
|
* Render icon as gray square instead of hiding it
|
|
900
913
|
*/
|
|
@@ -1037,7 +1050,7 @@ declare class SketchSVGRenderer extends SVGRenderer {
|
|
|
1037
1050
|
/**
|
|
1038
1051
|
* Render stat card with sketch filter and Comic Sans
|
|
1039
1052
|
*/
|
|
1040
|
-
protected
|
|
1053
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
1041
1054
|
/**
|
|
1042
1055
|
* Render image with sketch filter
|
|
1043
1056
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -290,8 +290,8 @@ interface IRComponentNode {
|
|
|
290
290
|
interface IRNodeStyle {
|
|
291
291
|
padding?: string;
|
|
292
292
|
gap?: string;
|
|
293
|
-
|
|
294
|
-
|
|
293
|
+
justify?: 'start' | 'center' | 'end' | 'stretch' | 'spaceBetween' | 'spaceAround';
|
|
294
|
+
align?: 'start' | 'center' | 'end';
|
|
295
295
|
background?: string;
|
|
296
296
|
}
|
|
297
297
|
interface IRMeta {
|
|
@@ -480,6 +480,19 @@ declare class LayoutEngine {
|
|
|
480
480
|
private wrapTextToLines;
|
|
481
481
|
private getIntrinsicComponentHeight;
|
|
482
482
|
private getControlLabelOffset;
|
|
483
|
+
/**
|
|
484
|
+
* Returns true when a container's width can be calculated from its children
|
|
485
|
+
* (i.e. it is a horizontal non-stretch stack). False means the container
|
|
486
|
+
* behaves like `flex-grow:1` and should absorb remaining space.
|
|
487
|
+
*/
|
|
488
|
+
private containerHasIntrinsicWidth;
|
|
489
|
+
/**
|
|
490
|
+
* Returns the natural (intrinsic) width of any node — component or container.
|
|
491
|
+
* For horizontal non-stretch containers the width is the sum of their children's
|
|
492
|
+
* intrinsic widths plus gaps, capped at `availableWidth`. All other containers
|
|
493
|
+
* are assumed to take the full available width (they stretch or grow).
|
|
494
|
+
*/
|
|
495
|
+
private getIntrinsicWidth;
|
|
483
496
|
private getIntrinsicComponentWidth;
|
|
484
497
|
private calculateChildHeight;
|
|
485
498
|
private calculateChildWidth;
|
|
@@ -706,7 +719,7 @@ declare class SVGRenderer {
|
|
|
706
719
|
protected renderModal(node: IRComponentNode, pos: any): string;
|
|
707
720
|
protected renderList(node: IRComponentNode, pos: any): string;
|
|
708
721
|
protected renderGenericComponent(node: IRComponentNode, pos: any): string;
|
|
709
|
-
protected
|
|
722
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
710
723
|
protected renderImage(node: IRComponentNode, pos: any): string;
|
|
711
724
|
protected renderBreadcrumbs(node: IRComponentNode, pos: any): string;
|
|
712
725
|
protected renderSidebarMenu(node: IRComponentNode, pos: any): string;
|
|
@@ -892,9 +905,9 @@ declare class SkeletonSVGRenderer extends SVGRenderer {
|
|
|
892
905
|
*/
|
|
893
906
|
protected renderTopbar(node: IRComponentNode, pos: any): string;
|
|
894
907
|
/**
|
|
895
|
-
* Render
|
|
908
|
+
* Render Stat with gray blocks instead of values
|
|
896
909
|
*/
|
|
897
|
-
protected
|
|
910
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
898
911
|
/**
|
|
899
912
|
* Render icon as gray square instead of hiding it
|
|
900
913
|
*/
|
|
@@ -1037,7 +1050,7 @@ declare class SketchSVGRenderer extends SVGRenderer {
|
|
|
1037
1050
|
/**
|
|
1038
1051
|
* Render stat card with sketch filter and Comic Sans
|
|
1039
1052
|
*/
|
|
1040
|
-
protected
|
|
1053
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
1041
1054
|
/**
|
|
1042
1055
|
* Render image with sketch filter
|
|
1043
1056
|
*/
|
package/dist/index.js
CHANGED
|
@@ -2174,8 +2174,8 @@ var IRStyleSchema = z.object({
|
|
|
2174
2174
|
var IRNodeStyleSchema = z.object({
|
|
2175
2175
|
padding: z.string().optional(),
|
|
2176
2176
|
gap: z.string().optional(),
|
|
2177
|
-
|
|
2178
|
-
|
|
2177
|
+
justify: z.enum(["start", "center", "end", "stretch", "spaceBetween", "spaceAround"]).optional(),
|
|
2178
|
+
align: z.enum(["start", "center", "end"]).optional(),
|
|
2179
2179
|
background: z.string().optional()
|
|
2180
2180
|
});
|
|
2181
2181
|
var IRMetaSchema = z.object({
|
|
@@ -2461,6 +2461,9 @@ ${messages}`);
|
|
|
2461
2461
|
if (layoutParams.gap !== void 0) {
|
|
2462
2462
|
style.gap = String(layoutParams.gap);
|
|
2463
2463
|
}
|
|
2464
|
+
if (layoutParams.justify !== void 0) {
|
|
2465
|
+
style.justify = layoutParams.justify;
|
|
2466
|
+
}
|
|
2464
2467
|
if (layoutParams.align !== void 0) {
|
|
2465
2468
|
style.align = layoutParams.align;
|
|
2466
2469
|
}
|
|
@@ -2542,7 +2545,7 @@ ${messages}`);
|
|
|
2542
2545
|
"Label",
|
|
2543
2546
|
"Image",
|
|
2544
2547
|
"Card",
|
|
2545
|
-
"
|
|
2548
|
+
"Stat",
|
|
2546
2549
|
"Topbar",
|
|
2547
2550
|
"Table",
|
|
2548
2551
|
"Chart",
|
|
@@ -3107,8 +3110,9 @@ var LayoutEngine = class {
|
|
|
3107
3110
|
}
|
|
3108
3111
|
});
|
|
3109
3112
|
} else {
|
|
3110
|
-
const
|
|
3111
|
-
|
|
3113
|
+
const justify = node.style.justify || "stretch";
|
|
3114
|
+
const crossAlign = node.style.align || "start";
|
|
3115
|
+
if (justify === "stretch") {
|
|
3112
3116
|
let currentX = x;
|
|
3113
3117
|
const childWidth = this.calculateChildWidth(children.length, width, gap);
|
|
3114
3118
|
let stackHeight = 0;
|
|
@@ -3130,43 +3134,44 @@ var LayoutEngine = class {
|
|
|
3130
3134
|
});
|
|
3131
3135
|
} else {
|
|
3132
3136
|
const childWidths = [];
|
|
3137
|
+
const childHeights = [];
|
|
3133
3138
|
const explicitHeightFlags = [];
|
|
3134
|
-
const
|
|
3139
|
+
const flexIndices = /* @__PURE__ */ new Set();
|
|
3135
3140
|
let stackHeight = 0;
|
|
3136
3141
|
children.forEach((childRef, index) => {
|
|
3137
3142
|
const childNode = this.nodes[childRef.ref];
|
|
3138
|
-
let childWidth = this.getIntrinsicComponentWidth(childNode);
|
|
3139
|
-
let childHeight = this.getComponentHeight();
|
|
3140
3143
|
const hasExplicitHeight = childNode?.kind === "component" && !!childNode.props.height;
|
|
3141
3144
|
const hasExplicitWidth = childNode?.kind === "component" && !!childNode.props.width;
|
|
3142
3145
|
const isBlockButton = childNode?.kind === "component" && childNode.componentType === "Button" && !hasExplicitWidth && this.parseBooleanProp(childNode.props.block, false);
|
|
3143
|
-
|
|
3146
|
+
const isFlexContainer = !hasExplicitWidth && childNode?.kind === "container" && !this.containerHasIntrinsicWidth(childNode);
|
|
3147
|
+
let childWidth;
|
|
3148
|
+
if (isBlockButton || isFlexContainer) {
|
|
3144
3149
|
childWidth = 0;
|
|
3145
|
-
|
|
3150
|
+
flexIndices.add(index);
|
|
3146
3151
|
} else if (hasExplicitWidth) {
|
|
3147
3152
|
childWidth = Number(childNode.props.width);
|
|
3148
|
-
}
|
|
3149
|
-
|
|
3150
|
-
childHeight = Number(childNode.props.height);
|
|
3153
|
+
} else {
|
|
3154
|
+
childWidth = this.getIntrinsicWidth(childNode, width);
|
|
3151
3155
|
}
|
|
3152
3156
|
childWidths.push(childWidth);
|
|
3157
|
+
childHeights.push(this.getComponentHeight());
|
|
3153
3158
|
explicitHeightFlags.push(hasExplicitHeight);
|
|
3154
3159
|
});
|
|
3155
3160
|
const totalGapWidth = gap * Math.max(0, children.length - 1);
|
|
3156
|
-
if (
|
|
3161
|
+
if (flexIndices.size > 0) {
|
|
3157
3162
|
const fixedWidth = childWidths.reduce((sum, w, idx) => {
|
|
3158
|
-
return
|
|
3163
|
+
return flexIndices.has(idx) ? sum : sum + w;
|
|
3159
3164
|
}, 0);
|
|
3160
3165
|
const remainingWidth = width - totalGapWidth - fixedWidth;
|
|
3161
|
-
const
|
|
3162
|
-
|
|
3163
|
-
childWidths[
|
|
3166
|
+
const widthPerFlex = Math.max(1, remainingWidth / flexIndices.size);
|
|
3167
|
+
flexIndices.forEach((idx) => {
|
|
3168
|
+
childWidths[idx] = widthPerFlex;
|
|
3164
3169
|
});
|
|
3165
3170
|
}
|
|
3166
3171
|
children.forEach((childRef, index) => {
|
|
3167
3172
|
const childNode = this.nodes[childRef.ref];
|
|
3168
|
-
let childHeight = this.getComponentHeight();
|
|
3169
3173
|
const childWidth = childWidths[index];
|
|
3174
|
+
let childHeight = this.getComponentHeight();
|
|
3170
3175
|
if (explicitHeightFlags[index] && childNode?.kind === "component") {
|
|
3171
3176
|
childHeight = Number(childNode.props.height);
|
|
3172
3177
|
} else if (childNode?.kind === "container") {
|
|
@@ -3174,21 +3179,37 @@ var LayoutEngine = class {
|
|
|
3174
3179
|
} else if (childNode?.kind === "component") {
|
|
3175
3180
|
childHeight = this.getIntrinsicComponentHeight(childNode, childWidth);
|
|
3176
3181
|
}
|
|
3182
|
+
childHeights[index] = childHeight;
|
|
3177
3183
|
stackHeight = Math.max(stackHeight, childHeight);
|
|
3178
3184
|
});
|
|
3179
3185
|
const totalChildWidth = childWidths.reduce((sum, w) => sum + w, 0);
|
|
3180
3186
|
const totalContentWidth = totalChildWidth + totalGapWidth;
|
|
3181
3187
|
let startX = x;
|
|
3182
|
-
|
|
3188
|
+
let dynamicGap = gap;
|
|
3189
|
+
if (justify === "center") {
|
|
3183
3190
|
startX = x + (width - totalContentWidth) / 2;
|
|
3184
|
-
} else if (
|
|
3191
|
+
} else if (justify === "end") {
|
|
3185
3192
|
startX = x + width - totalContentWidth;
|
|
3193
|
+
} else if (justify === "spaceBetween") {
|
|
3194
|
+
startX = x;
|
|
3195
|
+
dynamicGap = children.length > 1 ? (width - totalChildWidth) / (children.length - 1) : 0;
|
|
3196
|
+
} else if (justify === "spaceAround") {
|
|
3197
|
+
const spacing = children.length > 0 ? (width - totalChildWidth) / children.length : 0;
|
|
3198
|
+
startX = x + spacing / 2;
|
|
3199
|
+
dynamicGap = spacing;
|
|
3186
3200
|
}
|
|
3187
3201
|
let currentX = startX;
|
|
3188
3202
|
children.forEach((childRef, index) => {
|
|
3189
3203
|
const childWidth = childWidths[index];
|
|
3190
|
-
|
|
3191
|
-
|
|
3204
|
+
const childHeight = childHeights[index];
|
|
3205
|
+
let childY = y;
|
|
3206
|
+
if (crossAlign === "center") {
|
|
3207
|
+
childY = y + Math.round((stackHeight - childHeight) / 2);
|
|
3208
|
+
} else if (crossAlign === "end") {
|
|
3209
|
+
childY = y + stackHeight - childHeight;
|
|
3210
|
+
}
|
|
3211
|
+
this.calculateNode(childRef.ref, currentX, childY, childWidth, childHeight, "stack");
|
|
3212
|
+
currentX += childWidth + dynamicGap;
|
|
3192
3213
|
});
|
|
3193
3214
|
}
|
|
3194
3215
|
}
|
|
@@ -3685,7 +3706,7 @@ var LayoutEngine = class {
|
|
|
3685
3706
|
if (node.componentType === "Textarea") return 100 + controlLabelOffset;
|
|
3686
3707
|
if (node.componentType === "Modal") return 300;
|
|
3687
3708
|
if (node.componentType === "Card") return 120;
|
|
3688
|
-
if (node.componentType === "
|
|
3709
|
+
if (node.componentType === "Stat") return 120;
|
|
3689
3710
|
if (node.componentType === "Chart" || node.componentType === "ChartPlaceholder") return 250;
|
|
3690
3711
|
if (node.componentType === "List") {
|
|
3691
3712
|
const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
@@ -3711,6 +3732,47 @@ var LayoutEngine = class {
|
|
|
3711
3732
|
getControlLabelOffset(label) {
|
|
3712
3733
|
return label.trim().length > 0 ? 18 : 0;
|
|
3713
3734
|
}
|
|
3735
|
+
/**
|
|
3736
|
+
* Returns true when a container's width can be calculated from its children
|
|
3737
|
+
* (i.e. it is a horizontal non-stretch stack). False means the container
|
|
3738
|
+
* behaves like `flex-grow:1` and should absorb remaining space.
|
|
3739
|
+
*/
|
|
3740
|
+
containerHasIntrinsicWidth(node) {
|
|
3741
|
+
if (node.kind !== "container") return false;
|
|
3742
|
+
return node.containerType === "stack" && String(node.params.direction || "vertical") === "horizontal" && (node.style.justify || "stretch") !== "stretch";
|
|
3743
|
+
}
|
|
3744
|
+
/**
|
|
3745
|
+
* Returns the natural (intrinsic) width of any node — component or container.
|
|
3746
|
+
* For horizontal non-stretch containers the width is the sum of their children's
|
|
3747
|
+
* intrinsic widths plus gaps, capped at `availableWidth`. All other containers
|
|
3748
|
+
* are assumed to take the full available width (they stretch or grow).
|
|
3749
|
+
*/
|
|
3750
|
+
getIntrinsicWidth(node, availableWidth) {
|
|
3751
|
+
if (!node) return 120;
|
|
3752
|
+
if (node.kind === "component") {
|
|
3753
|
+
return this.getIntrinsicComponentWidth(node);
|
|
3754
|
+
}
|
|
3755
|
+
if (node.kind === "container") {
|
|
3756
|
+
if (this.containerHasIntrinsicWidth(node)) {
|
|
3757
|
+
const gap = this.resolveSpacing(node.style.gap);
|
|
3758
|
+
const padding = this.resolveSpacing(node.style.padding);
|
|
3759
|
+
const innerAvailable = Math.max(0, availableWidth - padding * 2);
|
|
3760
|
+
const children = node.children ?? [];
|
|
3761
|
+
let total = padding * 2;
|
|
3762
|
+
children.forEach((childRef, idx) => {
|
|
3763
|
+
const child = this.nodes[childRef.ref];
|
|
3764
|
+
total += this.getIntrinsicWidth(child, innerAvailable);
|
|
3765
|
+
if (idx < children.length - 1) total += gap;
|
|
3766
|
+
});
|
|
3767
|
+
return Math.min(total, availableWidth);
|
|
3768
|
+
}
|
|
3769
|
+
return availableWidth;
|
|
3770
|
+
}
|
|
3771
|
+
if (node.kind === "instance") {
|
|
3772
|
+
return availableWidth;
|
|
3773
|
+
}
|
|
3774
|
+
return 120;
|
|
3775
|
+
}
|
|
3714
3776
|
getIntrinsicComponentWidth(node) {
|
|
3715
3777
|
if (!node || node.kind !== "component") {
|
|
3716
3778
|
return 120;
|
|
@@ -3736,7 +3798,10 @@ var LayoutEngine = class {
|
|
|
3736
3798
|
const density = this.style.density || "normal";
|
|
3737
3799
|
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
3738
3800
|
const textWidth = this.estimateTextWidth(text, fontSize);
|
|
3739
|
-
|
|
3801
|
+
const iconName = String(node.props.icon || "").trim();
|
|
3802
|
+
const iconSize = iconName ? Math.round(fontSize * 1.1) : 0;
|
|
3803
|
+
const iconGap = iconName ? 8 : 0;
|
|
3804
|
+
return Math.max(60, Math.ceil(textWidth + iconSize + iconGap + (paddingX + extraPadding) * 2));
|
|
3740
3805
|
}
|
|
3741
3806
|
if (node.componentType === "Label" || node.componentType === "Text") {
|
|
3742
3807
|
const text = String(node.props.text || "");
|
|
@@ -3767,7 +3832,7 @@ var LayoutEngine = class {
|
|
|
3767
3832
|
if (node.componentType === "Table") {
|
|
3768
3833
|
return 400;
|
|
3769
3834
|
}
|
|
3770
|
-
if (node.componentType === "
|
|
3835
|
+
if (node.componentType === "Stat" || node.componentType === "Card") {
|
|
3771
3836
|
return 280;
|
|
3772
3837
|
}
|
|
3773
3838
|
if (node.componentType === "SidebarMenu") {
|
|
@@ -4823,8 +4888,8 @@ var SVGRenderer = class {
|
|
|
4823
4888
|
return this.renderModal(node, pos);
|
|
4824
4889
|
case "List":
|
|
4825
4890
|
return this.renderList(node, pos);
|
|
4826
|
-
case "
|
|
4827
|
-
return this.
|
|
4891
|
+
case "Stat":
|
|
4892
|
+
return this.renderStat(node, pos);
|
|
4828
4893
|
case "Image":
|
|
4829
4894
|
return this.renderImage(node, pos);
|
|
4830
4895
|
// Icon components
|
|
@@ -4870,6 +4935,7 @@ var SVGRenderer = class {
|
|
|
4870
4935
|
const text = String(node.props.text || "Button");
|
|
4871
4936
|
const variant = String(node.props.variant || "default");
|
|
4872
4937
|
const size = String(node.props.size || "md");
|
|
4938
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
4873
4939
|
const density = this.ir.project.style.density || "normal";
|
|
4874
4940
|
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
4875
4941
|
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
@@ -4915,7 +4981,7 @@ var SVGRenderer = class {
|
|
|
4915
4981
|
textX = pos.x + buttonWidth / 2;
|
|
4916
4982
|
textAnchor = "middle";
|
|
4917
4983
|
}
|
|
4918
|
-
let svg = `<g${this.getDataNodeId(node)}>
|
|
4984
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
4919
4985
|
<rect x="${pos.x}" y="${buttonY}"
|
|
4920
4986
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
4921
4987
|
rx="${radius}"
|
|
@@ -4981,6 +5047,7 @@ var SVGRenderer = class {
|
|
|
4981
5047
|
const placeholder = String(node.props.placeholder || "");
|
|
4982
5048
|
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
4983
5049
|
const iconRightName = String(node.props.iconRight || "").trim();
|
|
5050
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
4984
5051
|
const radius = this.tokens.input.radius;
|
|
4985
5052
|
const fontSize = this.tokens.input.fontSize;
|
|
4986
5053
|
const paddingX = this.tokens.input.paddingX;
|
|
@@ -4997,7 +5064,7 @@ var SVGRenderer = class {
|
|
|
4997
5064
|
const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
|
|
4998
5065
|
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
4999
5066
|
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
5000
|
-
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5067
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
|
|
5001
5068
|
if (label) {
|
|
5002
5069
|
svg += `
|
|
5003
5070
|
<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
@@ -5566,7 +5633,8 @@ var SVGRenderer = class {
|
|
|
5566
5633
|
const fontSize = this.tokens.text.fontSize;
|
|
5567
5634
|
const lineHeightPx = Math.ceil(fontSize * this.tokens.text.lineHeight);
|
|
5568
5635
|
const lines = this.wrapTextToLines(text, pos.width, fontSize);
|
|
5569
|
-
const
|
|
5636
|
+
const totalTextHeight = lines.length * lineHeightPx;
|
|
5637
|
+
const firstLineY = pos.y + Math.round(Math.max(0, (pos.height - totalTextHeight) / 2)) + fontSize;
|
|
5570
5638
|
const tspans = lines.map(
|
|
5571
5639
|
(line, index) => `<tspan x="${pos.x}" dy="${index === 0 ? 0 : lineHeightPx}">${this.escapeXml(line)}</tspan>`
|
|
5572
5640
|
).join("");
|
|
@@ -5579,8 +5647,9 @@ var SVGRenderer = class {
|
|
|
5579
5647
|
}
|
|
5580
5648
|
renderLabel(node, pos) {
|
|
5581
5649
|
const text = String(node.props.text || "Label");
|
|
5650
|
+
const textY = pos.y + Math.round(pos.height / 2) + 4;
|
|
5582
5651
|
return `<g${this.getDataNodeId(node)}>
|
|
5583
|
-
<text x="${pos.x}" y="${
|
|
5652
|
+
<text x="${pos.x}" y="${textY}"
|
|
5584
5653
|
font-family="Arial, Helvetica, sans-serif"
|
|
5585
5654
|
font-size="12"
|
|
5586
5655
|
fill="${this.renderTheme.textMuted}">${this.escapeXml(text)}</text>
|
|
@@ -5635,6 +5704,7 @@ var SVGRenderer = class {
|
|
|
5635
5704
|
const placeholder = String(node.props.placeholder || "Select...");
|
|
5636
5705
|
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
5637
5706
|
const iconRightName = String(node.props.iconRight || "").trim();
|
|
5707
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5638
5708
|
const labelOffset = this.getControlLabelOffset(label);
|
|
5639
5709
|
const controlY = pos.y + labelOffset;
|
|
5640
5710
|
const controlHeight = Math.max(16, pos.height - labelOffset);
|
|
@@ -5648,7 +5718,7 @@ var SVGRenderer = class {
|
|
|
5648
5718
|
const chevronWidth = 20;
|
|
5649
5719
|
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
5650
5720
|
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
5651
|
-
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5721
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
|
|
5652
5722
|
if (label) {
|
|
5653
5723
|
svg += `
|
|
5654
5724
|
<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
@@ -5697,10 +5767,11 @@ var SVGRenderer = class {
|
|
|
5697
5767
|
renderCheckbox(node, pos) {
|
|
5698
5768
|
const label = String(node.props.label || "Checkbox");
|
|
5699
5769
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
5770
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5700
5771
|
const controlColor = this.resolveControlColor();
|
|
5701
5772
|
const checkboxSize = 18;
|
|
5702
5773
|
const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
|
|
5703
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5774
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5704
5775
|
<rect x="${pos.x}" y="${checkboxY}"
|
|
5705
5776
|
width="${checkboxSize}" height="${checkboxSize}"
|
|
5706
5777
|
rx="4"
|
|
@@ -5721,10 +5792,11 @@ var SVGRenderer = class {
|
|
|
5721
5792
|
renderRadio(node, pos) {
|
|
5722
5793
|
const label = String(node.props.label || "Radio");
|
|
5723
5794
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
5795
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5724
5796
|
const controlColor = this.resolveControlColor();
|
|
5725
5797
|
const radioSize = 16;
|
|
5726
5798
|
const radioY = pos.y + pos.height / 2 - radioSize / 2;
|
|
5727
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5799
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5728
5800
|
<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
|
|
5729
5801
|
r="${radioSize / 2}"
|
|
5730
5802
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -5742,11 +5814,12 @@ var SVGRenderer = class {
|
|
|
5742
5814
|
renderToggle(node, pos) {
|
|
5743
5815
|
const label = String(node.props.label || "Toggle");
|
|
5744
5816
|
const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
|
|
5817
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5745
5818
|
const controlColor = this.resolveControlColor();
|
|
5746
5819
|
const toggleWidth = 40;
|
|
5747
5820
|
const toggleHeight = 20;
|
|
5748
5821
|
const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
|
|
5749
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5822
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5750
5823
|
<rect x="${pos.x}" y="${toggleY}"
|
|
5751
5824
|
width="${toggleWidth}" height="${toggleHeight}"
|
|
5752
5825
|
rx="10"
|
|
@@ -6042,7 +6115,7 @@ var SVGRenderer = class {
|
|
|
6042
6115
|
text-anchor="middle">${node.componentType}</text>
|
|
6043
6116
|
</g>`;
|
|
6044
6117
|
}
|
|
6045
|
-
|
|
6118
|
+
renderStat(node, pos) {
|
|
6046
6119
|
const title = String(node.props.title || "Metric");
|
|
6047
6120
|
const value = String(node.props.value || "0");
|
|
6048
6121
|
const rawCaption = String(node.props.caption || "");
|
|
@@ -6050,7 +6123,9 @@ var SVGRenderer = class {
|
|
|
6050
6123
|
const hasCaption = caption.trim().length > 0;
|
|
6051
6124
|
const iconName = String(node.props.icon || "").trim();
|
|
6052
6125
|
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
6053
|
-
const
|
|
6126
|
+
const variant = String(node.props.variant || "default");
|
|
6127
|
+
const baseAccent = this.resolveAccentColor();
|
|
6128
|
+
const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
|
|
6054
6129
|
const padding = this.resolveSpacing(node.style.padding) || 16;
|
|
6055
6130
|
const innerX = pos.x + padding;
|
|
6056
6131
|
const innerY = pos.y + padding;
|
|
@@ -6074,7 +6149,7 @@ var SVGRenderer = class {
|
|
|
6074
6149
|
const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
|
|
6075
6150
|
const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, innerWidth), captionSize) : "";
|
|
6076
6151
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
6077
|
-
<!--
|
|
6152
|
+
<!-- Stat Background -->
|
|
6078
6153
|
<rect x="${pos.x}" y="${pos.y}"
|
|
6079
6154
|
width="${pos.width}" height="${pos.height}"
|
|
6080
6155
|
rx="8"
|
|
@@ -6711,8 +6786,8 @@ var SVGRenderer = class {
|
|
|
6711
6786
|
return false;
|
|
6712
6787
|
}
|
|
6713
6788
|
const direction = String(parent.params.direction || "vertical");
|
|
6714
|
-
const
|
|
6715
|
-
return direction === "horizontal" &&
|
|
6789
|
+
const justify = parent.style.justify || "stretch";
|
|
6790
|
+
return direction === "horizontal" && justify === "stretch";
|
|
6716
6791
|
}
|
|
6717
6792
|
buildParentContainerIndex() {
|
|
6718
6793
|
this.parentContainerByChildId.clear();
|
|
@@ -7337,9 +7412,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7337
7412
|
return svg;
|
|
7338
7413
|
}
|
|
7339
7414
|
/**
|
|
7340
|
-
* Render
|
|
7415
|
+
* Render Stat with gray blocks instead of values
|
|
7341
7416
|
*/
|
|
7342
|
-
|
|
7417
|
+
renderStat(node, pos) {
|
|
7343
7418
|
const hasIcon = String(node.props.icon || "").trim().length > 0;
|
|
7344
7419
|
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7345
7420
|
const iconSize = 20;
|
|
@@ -8466,7 +8541,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8466
8541
|
/**
|
|
8467
8542
|
* Render stat card with sketch filter and Comic Sans
|
|
8468
8543
|
*/
|
|
8469
|
-
|
|
8544
|
+
renderStat(node, pos) {
|
|
8470
8545
|
const title = String(node.props.title || "Metric");
|
|
8471
8546
|
const value = String(node.props.value || "0");
|
|
8472
8547
|
const rawCaption = String(node.props.caption || "");
|
|
@@ -8474,7 +8549,9 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8474
8549
|
const hasCaption = caption.trim().length > 0;
|
|
8475
8550
|
const iconName = String(node.props.icon || "").trim();
|
|
8476
8551
|
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
8477
|
-
const
|
|
8552
|
+
const variant = String(node.props.variant || "default");
|
|
8553
|
+
const baseAccent = this.resolveAccentColor();
|
|
8554
|
+
const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
|
|
8478
8555
|
const padding = this.resolveSpacing(node.style.padding);
|
|
8479
8556
|
const innerX = pos.x + padding;
|
|
8480
8557
|
const innerY = pos.y + padding;
|
|
@@ -8497,7 +8574,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8497
8574
|
const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
|
|
8498
8575
|
const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, pos.width - padding * 2), captionSize) : "";
|
|
8499
8576
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
8500
|
-
<!--
|
|
8577
|
+
<!-- Stat Background -->
|
|
8501
8578
|
<rect x="${pos.x}" y="${pos.y}"
|
|
8502
8579
|
width="${pos.width}" height="${pos.height}"
|
|
8503
8580
|
rx="8"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wire-dsl/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "WireDSL engine - Parser, IR generator, layout engine, and SVG renderer (browser-safe, pure JS/TS)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"chevrotain": "11.1.1",
|
|
34
34
|
"zod": "4.3.6",
|
|
35
|
-
"@wire-dsl/language-support": "0.
|
|
35
|
+
"@wire-dsl/language-support": "0.6.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "25.2.0",
|