@wire-dsl/engine 0.4.0 → 0.5.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 +163 -54
- package/dist/index.d.cts +52 -7
- package/dist/index.d.ts +52 -7
- package/dist/index.js +163 -54
- package/package.json +2 -2
package/README.md
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -2245,7 +2245,21 @@ var IRComponentNodeSchema = import_zod.z.object({
|
|
|
2245
2245
|
style: IRNodeStyleSchema,
|
|
2246
2246
|
meta: IRMetaSchema
|
|
2247
2247
|
});
|
|
2248
|
-
var
|
|
2248
|
+
var IRInstanceNodeSchema = import_zod.z.object({
|
|
2249
|
+
id: import_zod.z.string(),
|
|
2250
|
+
kind: import_zod.z.literal("instance"),
|
|
2251
|
+
definitionName: import_zod.z.string(),
|
|
2252
|
+
definitionKind: import_zod.z.enum(["component", "layout"]),
|
|
2253
|
+
invocationProps: import_zod.z.record(import_zod.z.string(), import_zod.z.union([import_zod.z.string(), import_zod.z.number()])),
|
|
2254
|
+
expandedRoot: import_zod.z.object({ ref: import_zod.z.string() }),
|
|
2255
|
+
style: IRNodeStyleSchema,
|
|
2256
|
+
meta: IRMetaSchema
|
|
2257
|
+
});
|
|
2258
|
+
var IRNodeSchema = import_zod.z.discriminatedUnion("kind", [
|
|
2259
|
+
IRContainerNodeSchema,
|
|
2260
|
+
IRComponentNodeSchema,
|
|
2261
|
+
IRInstanceNodeSchema
|
|
2262
|
+
]);
|
|
2249
2263
|
var IRScreenSchema = import_zod.z.object({
|
|
2250
2264
|
id: import_zod.z.string(),
|
|
2251
2265
|
name: import_zod.z.string(),
|
|
@@ -2468,7 +2482,7 @@ ${messages}`);
|
|
|
2468
2482
|
const layoutChildren = layout.children;
|
|
2469
2483
|
const layoutDefinition = this.definedLayouts.get(layout.layoutType);
|
|
2470
2484
|
if (layoutDefinition) {
|
|
2471
|
-
return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
|
|
2485
|
+
return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context, layout._meta?.nodeId);
|
|
2472
2486
|
}
|
|
2473
2487
|
const nodeId = this.idGen.generate("node");
|
|
2474
2488
|
const childRefs = [];
|
|
@@ -2507,8 +2521,8 @@ ${messages}`);
|
|
|
2507
2521
|
children: childRefs,
|
|
2508
2522
|
style,
|
|
2509
2523
|
meta: {
|
|
2510
|
-
nodeId
|
|
2511
|
-
|
|
2524
|
+
// Scope nodeId per instance so each expansion gets a unique identifier
|
|
2525
|
+
nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
|
|
2512
2526
|
}
|
|
2513
2527
|
};
|
|
2514
2528
|
this.nodes[nodeId] = containerNode;
|
|
@@ -2537,8 +2551,7 @@ ${messages}`);
|
|
|
2537
2551
|
// Cells have no padding by default - grid gap handles spacing
|
|
2538
2552
|
meta: {
|
|
2539
2553
|
source: "cell",
|
|
2540
|
-
nodeId: cell._meta?.nodeId
|
|
2541
|
-
// Pass SourceMap nodeId from AST
|
|
2554
|
+
nodeId: context?.instanceScope ? `${cell._meta?.nodeId}@${context.instanceScope}` : cell._meta?.nodeId
|
|
2542
2555
|
}
|
|
2543
2556
|
};
|
|
2544
2557
|
this.nodes[nodeId] = containerNode;
|
|
@@ -2565,7 +2578,7 @@ ${messages}`);
|
|
|
2565
2578
|
const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
|
|
2566
2579
|
const definition = this.definedComponents.get(component.componentType);
|
|
2567
2580
|
if (definition) {
|
|
2568
|
-
return this.expandDefinedComponent(definition, resolvedProps, context);
|
|
2581
|
+
return this.expandDefinedComponent(definition, resolvedProps, component._meta?.nodeId, context);
|
|
2569
2582
|
}
|
|
2570
2583
|
const builtInComponents = /* @__PURE__ */ new Set([
|
|
2571
2584
|
"Button",
|
|
@@ -2575,7 +2588,7 @@ ${messages}`);
|
|
|
2575
2588
|
"Label",
|
|
2576
2589
|
"Image",
|
|
2577
2590
|
"Card",
|
|
2578
|
-
"
|
|
2591
|
+
"Stat",
|
|
2579
2592
|
"Topbar",
|
|
2580
2593
|
"Table",
|
|
2581
2594
|
"Chart",
|
|
@@ -2611,35 +2624,49 @@ ${messages}`);
|
|
|
2611
2624
|
props: resolvedProps,
|
|
2612
2625
|
style: {},
|
|
2613
2626
|
meta: {
|
|
2614
|
-
nodeId
|
|
2615
|
-
|
|
2627
|
+
// Scope nodeId per instance so each expansion gets a unique identifier
|
|
2628
|
+
nodeId: context?.instanceScope ? `${component._meta?.nodeId}@${context.instanceScope}` : component._meta?.nodeId
|
|
2616
2629
|
}
|
|
2617
2630
|
};
|
|
2618
2631
|
this.nodes[nodeId] = componentNode;
|
|
2619
2632
|
return nodeId;
|
|
2620
2633
|
}
|
|
2621
|
-
expandDefinedComponent(definition, invocationArgs, parentContext) {
|
|
2634
|
+
expandDefinedComponent(definition, invocationArgs, callSiteNodeId, parentContext) {
|
|
2622
2635
|
const context = {
|
|
2623
2636
|
args: invocationArgs,
|
|
2624
2637
|
providedArgNames: new Set(Object.keys(invocationArgs)),
|
|
2625
2638
|
usedArgNames: /* @__PURE__ */ new Set(),
|
|
2626
2639
|
definitionName: definition.name,
|
|
2627
2640
|
definitionKind: "component",
|
|
2628
|
-
allowChildrenSlot: false
|
|
2641
|
+
allowChildrenSlot: false,
|
|
2642
|
+
// Scope internal nodeIds using the call-site nodeId
|
|
2643
|
+
instanceScope: callSiteNodeId
|
|
2629
2644
|
};
|
|
2645
|
+
let expandedRootId = null;
|
|
2630
2646
|
if (definition.body.type === "layout") {
|
|
2631
|
-
|
|
2632
|
-
this.reportUnusedArguments(context);
|
|
2633
|
-
return result;
|
|
2647
|
+
expandedRootId = this.convertLayout(definition.body, context);
|
|
2634
2648
|
} else if (definition.body.type === "component") {
|
|
2635
|
-
|
|
2636
|
-
this.reportUnusedArguments(context);
|
|
2637
|
-
return result;
|
|
2649
|
+
expandedRootId = this.convertComponent(definition.body, context);
|
|
2638
2650
|
} else {
|
|
2639
2651
|
throw new Error(`Invalid defined component body type for "${definition.name}"`);
|
|
2640
2652
|
}
|
|
2653
|
+
this.reportUnusedArguments(context);
|
|
2654
|
+
if (!expandedRootId) return null;
|
|
2655
|
+
const instanceNodeId = this.idGen.generate("node");
|
|
2656
|
+
const instanceNode = {
|
|
2657
|
+
id: instanceNodeId,
|
|
2658
|
+
kind: "instance",
|
|
2659
|
+
definitionName: definition.name,
|
|
2660
|
+
definitionKind: "component",
|
|
2661
|
+
invocationProps: invocationArgs,
|
|
2662
|
+
expandedRoot: { ref: expandedRootId },
|
|
2663
|
+
style: {},
|
|
2664
|
+
meta: { nodeId: callSiteNodeId }
|
|
2665
|
+
};
|
|
2666
|
+
this.nodes[instanceNodeId] = instanceNode;
|
|
2667
|
+
return instanceNodeId;
|
|
2641
2668
|
}
|
|
2642
|
-
expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
|
|
2669
|
+
expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext, callSiteNodeId) {
|
|
2643
2670
|
if (invocationChildren.length !== 1) {
|
|
2644
2671
|
this.errors.push({
|
|
2645
2672
|
type: "layout-children-arity",
|
|
@@ -2655,11 +2682,26 @@ ${messages}`);
|
|
|
2655
2682
|
definitionName: definition.name,
|
|
2656
2683
|
definitionKind: "layout",
|
|
2657
2684
|
allowChildrenSlot: true,
|
|
2658
|
-
childrenSlot: resolvedSlot
|
|
2685
|
+
childrenSlot: resolvedSlot,
|
|
2686
|
+
// Scope internal nodeIds using the call-site nodeId
|
|
2687
|
+
instanceScope: callSiteNodeId
|
|
2659
2688
|
};
|
|
2660
|
-
const
|
|
2689
|
+
const expandedRootId = this.convertLayout(definition.body, context);
|
|
2661
2690
|
this.reportUnusedArguments(context);
|
|
2662
|
-
return
|
|
2691
|
+
if (!callSiteNodeId) return expandedRootId;
|
|
2692
|
+
const instanceNodeId = this.idGen.generate("node");
|
|
2693
|
+
const instanceNode = {
|
|
2694
|
+
id: instanceNodeId,
|
|
2695
|
+
kind: "instance",
|
|
2696
|
+
definitionName: definition.name,
|
|
2697
|
+
definitionKind: "layout",
|
|
2698
|
+
invocationProps: invocationParams,
|
|
2699
|
+
expandedRoot: { ref: expandedRootId },
|
|
2700
|
+
style: {},
|
|
2701
|
+
meta: { nodeId: callSiteNodeId }
|
|
2702
|
+
};
|
|
2703
|
+
this.nodes[instanceNodeId] = instanceNode;
|
|
2704
|
+
return instanceNodeId;
|
|
2663
2705
|
}
|
|
2664
2706
|
resolveChildrenSlot(slot, parentContext) {
|
|
2665
2707
|
if (slot.type === "component" && slot.componentType === "Children") {
|
|
@@ -3025,6 +3067,8 @@ var LayoutEngine = class {
|
|
|
3025
3067
|
}
|
|
3026
3068
|
if (node.kind === "container") {
|
|
3027
3069
|
this.calculateContainer(node, nodeId, x, y, width, height);
|
|
3070
|
+
} else if (node.kind === "instance") {
|
|
3071
|
+
this.calculateInstance(node, nodeId, x, y, width, height, parentContainerType);
|
|
3028
3072
|
} else {
|
|
3029
3073
|
this.calculateComponent(node, nodeId, x, y, width, height);
|
|
3030
3074
|
}
|
|
@@ -3057,7 +3101,7 @@ var LayoutEngine = class {
|
|
|
3057
3101
|
this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
|
|
3058
3102
|
break;
|
|
3059
3103
|
}
|
|
3060
|
-
if (isVerticalStack || node.containerType === "card") {
|
|
3104
|
+
if ((isVerticalStack || node.containerType === "card") && node.children.length > 0) {
|
|
3061
3105
|
let containerMaxY = y;
|
|
3062
3106
|
node.children.forEach((childRef) => {
|
|
3063
3107
|
const childPos = this.result[childRef.ref];
|
|
@@ -3200,6 +3244,10 @@ var LayoutEngine = class {
|
|
|
3200
3244
|
const gap = this.resolveSpacing(node.style.gap);
|
|
3201
3245
|
const padding = this.resolveSpacing(node.style.padding);
|
|
3202
3246
|
let totalHeight = padding * 2;
|
|
3247
|
+
const EMPTY_CONTAINER_MIN_HEIGHT = 40;
|
|
3248
|
+
if (node.children.length === 0) {
|
|
3249
|
+
return Math.max(totalHeight, EMPTY_CONTAINER_MIN_HEIGHT);
|
|
3250
|
+
}
|
|
3203
3251
|
if (node.containerType === "grid") {
|
|
3204
3252
|
const columns = Number(node.params.columns) || 12;
|
|
3205
3253
|
const colWidth = (availableWidth - gap * (columns - 1)) / columns;
|
|
@@ -3452,6 +3500,20 @@ var LayoutEngine = class {
|
|
|
3452
3500
|
}
|
|
3453
3501
|
});
|
|
3454
3502
|
}
|
|
3503
|
+
/**
|
|
3504
|
+
* Calculate layout for an instance node.
|
|
3505
|
+
* The instance is a transparent wrapper — its bounding box equals the
|
|
3506
|
+
* expanded root's bounding box. We calculate the expanded root first and
|
|
3507
|
+
* then copy its position to the instance nodeId so the renderer can use it.
|
|
3508
|
+
*/
|
|
3509
|
+
calculateInstance(node, nodeId, x, y, width, height, parentContainerType) {
|
|
3510
|
+
const expandedRootId = node.expandedRoot.ref;
|
|
3511
|
+
this.calculateNode(expandedRootId, x, y, width, height, parentContainerType);
|
|
3512
|
+
const expandedPos = this.result[expandedRootId];
|
|
3513
|
+
if (expandedPos) {
|
|
3514
|
+
this.result[nodeId] = { ...expandedPos };
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3455
3517
|
calculateComponent(node, nodeId, x, y, width, height) {
|
|
3456
3518
|
if (node.kind !== "component") return;
|
|
3457
3519
|
const componentWidth = Number(node.props.width) || width;
|
|
@@ -3669,7 +3731,7 @@ var LayoutEngine = class {
|
|
|
3669
3731
|
if (node.componentType === "Textarea") return 100 + controlLabelOffset;
|
|
3670
3732
|
if (node.componentType === "Modal") return 300;
|
|
3671
3733
|
if (node.componentType === "Card") return 120;
|
|
3672
|
-
if (node.componentType === "
|
|
3734
|
+
if (node.componentType === "Stat") return 120;
|
|
3673
3735
|
if (node.componentType === "Chart" || node.componentType === "ChartPlaceholder") return 250;
|
|
3674
3736
|
if (node.componentType === "List") {
|
|
3675
3737
|
const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
@@ -3720,7 +3782,10 @@ var LayoutEngine = class {
|
|
|
3720
3782
|
const density = this.style.density || "normal";
|
|
3721
3783
|
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
3722
3784
|
const textWidth = this.estimateTextWidth(text, fontSize);
|
|
3723
|
-
|
|
3785
|
+
const iconName = String(node.props.icon || "").trim();
|
|
3786
|
+
const iconSize = iconName ? Math.round(fontSize * 1.1) : 0;
|
|
3787
|
+
const iconGap = iconName ? 8 : 0;
|
|
3788
|
+
return Math.max(60, Math.ceil(textWidth + iconSize + iconGap + (paddingX + extraPadding) * 2));
|
|
3724
3789
|
}
|
|
3725
3790
|
if (node.componentType === "Label" || node.componentType === "Text") {
|
|
3726
3791
|
const text = String(node.props.text || "");
|
|
@@ -3751,7 +3816,7 @@ var LayoutEngine = class {
|
|
|
3751
3816
|
if (node.componentType === "Table") {
|
|
3752
3817
|
return 400;
|
|
3753
3818
|
}
|
|
3754
|
-
if (node.componentType === "
|
|
3819
|
+
if (node.componentType === "Stat" || node.componentType === "Card") {
|
|
3755
3820
|
return 280;
|
|
3756
3821
|
}
|
|
3757
3822
|
if (node.componentType === "SidebarMenu") {
|
|
@@ -4617,7 +4682,8 @@ var SVGRenderer = class {
|
|
|
4617
4682
|
height: options?.height || 720,
|
|
4618
4683
|
theme: colorScheme,
|
|
4619
4684
|
includeLabels: options?.includeLabels ?? true,
|
|
4620
|
-
screenName: options?.screenName
|
|
4685
|
+
screenName: options?.screenName,
|
|
4686
|
+
showDiagnostics: options?.showDiagnostics ?? false
|
|
4621
4687
|
};
|
|
4622
4688
|
this.colorResolver = new ColorResolver();
|
|
4623
4689
|
this.buildParentContainerIndex();
|
|
@@ -4717,13 +4783,30 @@ var SVGRenderer = class {
|
|
|
4717
4783
|
if (node.containerType === "split") {
|
|
4718
4784
|
this.renderSplitDecoration(node, pos, containerGroup);
|
|
4719
4785
|
}
|
|
4720
|
-
node.children.
|
|
4721
|
-
this.
|
|
4722
|
-
}
|
|
4786
|
+
if (node.children.length === 0 && this.options.showDiagnostics) {
|
|
4787
|
+
containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
|
|
4788
|
+
} else {
|
|
4789
|
+
node.children.forEach((childRef) => {
|
|
4790
|
+
this.renderNode(childRef.ref, containerGroup);
|
|
4791
|
+
});
|
|
4792
|
+
}
|
|
4723
4793
|
if (hasNodeId) {
|
|
4724
4794
|
containerGroup.push("</g>");
|
|
4725
4795
|
}
|
|
4726
4796
|
output.push(...containerGroup);
|
|
4797
|
+
} else if (node.kind === "instance") {
|
|
4798
|
+
const instanceGroup = [];
|
|
4799
|
+
if (node.meta.nodeId) {
|
|
4800
|
+
instanceGroup.push(`<g data-node-id="${node.meta.nodeId}">`);
|
|
4801
|
+
}
|
|
4802
|
+
instanceGroup.push(
|
|
4803
|
+
`<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
|
|
4804
|
+
);
|
|
4805
|
+
this.renderNode(node.expandedRoot.ref, instanceGroup);
|
|
4806
|
+
if (node.meta.nodeId) {
|
|
4807
|
+
instanceGroup.push("</g>");
|
|
4808
|
+
}
|
|
4809
|
+
output.push(...instanceGroup);
|
|
4727
4810
|
} else if (node.kind === "component") {
|
|
4728
4811
|
const componentSvg = this.renderComponent(node, pos);
|
|
4729
4812
|
if (componentSvg) {
|
|
@@ -4789,8 +4872,8 @@ var SVGRenderer = class {
|
|
|
4789
4872
|
return this.renderModal(node, pos);
|
|
4790
4873
|
case "List":
|
|
4791
4874
|
return this.renderList(node, pos);
|
|
4792
|
-
case "
|
|
4793
|
-
return this.
|
|
4875
|
+
case "Stat":
|
|
4876
|
+
return this.renderStat(node, pos);
|
|
4794
4877
|
case "Image":
|
|
4795
4878
|
return this.renderImage(node, pos);
|
|
4796
4879
|
// Icon components
|
|
@@ -4836,6 +4919,7 @@ var SVGRenderer = class {
|
|
|
4836
4919
|
const text = String(node.props.text || "Button");
|
|
4837
4920
|
const variant = String(node.props.variant || "default");
|
|
4838
4921
|
const size = String(node.props.size || "md");
|
|
4922
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
4839
4923
|
const density = this.ir.project.style.density || "normal";
|
|
4840
4924
|
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
4841
4925
|
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
@@ -4881,7 +4965,7 @@ var SVGRenderer = class {
|
|
|
4881
4965
|
textX = pos.x + buttonWidth / 2;
|
|
4882
4966
|
textAnchor = "middle";
|
|
4883
4967
|
}
|
|
4884
|
-
let svg = `<g${this.getDataNodeId(node)}>
|
|
4968
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
4885
4969
|
<rect x="${pos.x}" y="${buttonY}"
|
|
4886
4970
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
4887
4971
|
rx="${radius}"
|
|
@@ -4947,6 +5031,7 @@ var SVGRenderer = class {
|
|
|
4947
5031
|
const placeholder = String(node.props.placeholder || "");
|
|
4948
5032
|
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
4949
5033
|
const iconRightName = String(node.props.iconRight || "").trim();
|
|
5034
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
4950
5035
|
const radius = this.tokens.input.radius;
|
|
4951
5036
|
const fontSize = this.tokens.input.fontSize;
|
|
4952
5037
|
const paddingX = this.tokens.input.paddingX;
|
|
@@ -4963,7 +5048,7 @@ var SVGRenderer = class {
|
|
|
4963
5048
|
const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
|
|
4964
5049
|
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
4965
5050
|
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
4966
|
-
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5051
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
|
|
4967
5052
|
if (label) {
|
|
4968
5053
|
svg += `
|
|
4969
5054
|
<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
@@ -5014,7 +5099,7 @@ var SVGRenderer = class {
|
|
|
5014
5099
|
const variant = String(node.props.variant || "default");
|
|
5015
5100
|
const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
|
|
5016
5101
|
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
5017
|
-
const showBackground = this.parseBooleanProp(node.props.background
|
|
5102
|
+
const showBackground = this.parseBooleanProp(node.props.background, false);
|
|
5018
5103
|
const radiusMap = {
|
|
5019
5104
|
none: 0,
|
|
5020
5105
|
sm: 4,
|
|
@@ -5153,6 +5238,21 @@ var SVGRenderer = class {
|
|
|
5153
5238
|
</g>`;
|
|
5154
5239
|
output.push(svg);
|
|
5155
5240
|
}
|
|
5241
|
+
/**
|
|
5242
|
+
* Renders a yellow warning placeholder for containers with no children.
|
|
5243
|
+
* Only shown when `showDiagnostics` is enabled (editor/canvas mode).
|
|
5244
|
+
*/
|
|
5245
|
+
renderEmptyContainerDiagnostic(pos, containerType) {
|
|
5246
|
+
const diagColor = "#F59E0B";
|
|
5247
|
+
const diagBg = "#FFFBEB";
|
|
5248
|
+
const diagText = "#92400E";
|
|
5249
|
+
const minHeight = 40;
|
|
5250
|
+
const h = Math.max(pos.height, minHeight);
|
|
5251
|
+
const cx = pos.x + pos.width / 2;
|
|
5252
|
+
const cy = pos.y + h / 2;
|
|
5253
|
+
const label = containerType ? `Empty ${containerType}` : "Empty layout";
|
|
5254
|
+
return `<g><rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${h}" rx="4" fill="${diagBg}" stroke="${diagColor}" stroke-width="1" stroke-dasharray="6 3"/><text x="${cx}" y="${cy}" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="${diagText}" text-anchor="middle" dominant-baseline="middle">${label}</text></g>`;
|
|
5255
|
+
}
|
|
5156
5256
|
renderSplitDecoration(node, pos, output) {
|
|
5157
5257
|
if (node.kind !== "container") return;
|
|
5158
5258
|
const gap = this.resolveSpacing(node.style.gap);
|
|
@@ -5201,7 +5301,7 @@ var SVGRenderer = class {
|
|
|
5201
5301
|
const hasCaption = caption.length > 0;
|
|
5202
5302
|
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
5203
5303
|
const showOuterBackground = this.parseBooleanProp(
|
|
5204
|
-
node.props.background
|
|
5304
|
+
node.props.background,
|
|
5205
5305
|
false
|
|
5206
5306
|
);
|
|
5207
5307
|
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
@@ -5586,6 +5686,7 @@ var SVGRenderer = class {
|
|
|
5586
5686
|
const placeholder = String(node.props.placeholder || "Select...");
|
|
5587
5687
|
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
5588
5688
|
const iconRightName = String(node.props.iconRight || "").trim();
|
|
5689
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5589
5690
|
const labelOffset = this.getControlLabelOffset(label);
|
|
5590
5691
|
const controlY = pos.y + labelOffset;
|
|
5591
5692
|
const controlHeight = Math.max(16, pos.height - labelOffset);
|
|
@@ -5599,7 +5700,7 @@ var SVGRenderer = class {
|
|
|
5599
5700
|
const chevronWidth = 20;
|
|
5600
5701
|
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
5601
5702
|
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
5602
|
-
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5703
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
|
|
5603
5704
|
if (label) {
|
|
5604
5705
|
svg += `
|
|
5605
5706
|
<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
@@ -5648,10 +5749,11 @@ var SVGRenderer = class {
|
|
|
5648
5749
|
renderCheckbox(node, pos) {
|
|
5649
5750
|
const label = String(node.props.label || "Checkbox");
|
|
5650
5751
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
5752
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5651
5753
|
const controlColor = this.resolveControlColor();
|
|
5652
5754
|
const checkboxSize = 18;
|
|
5653
5755
|
const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
|
|
5654
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5756
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5655
5757
|
<rect x="${pos.x}" y="${checkboxY}"
|
|
5656
5758
|
width="${checkboxSize}" height="${checkboxSize}"
|
|
5657
5759
|
rx="4"
|
|
@@ -5672,10 +5774,11 @@ var SVGRenderer = class {
|
|
|
5672
5774
|
renderRadio(node, pos) {
|
|
5673
5775
|
const label = String(node.props.label || "Radio");
|
|
5674
5776
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
5777
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5675
5778
|
const controlColor = this.resolveControlColor();
|
|
5676
5779
|
const radioSize = 16;
|
|
5677
5780
|
const radioY = pos.y + pos.height / 2 - radioSize / 2;
|
|
5678
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5781
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5679
5782
|
<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
|
|
5680
5783
|
r="${radioSize / 2}"
|
|
5681
5784
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -5693,11 +5796,12 @@ var SVGRenderer = class {
|
|
|
5693
5796
|
renderToggle(node, pos) {
|
|
5694
5797
|
const label = String(node.props.label || "Toggle");
|
|
5695
5798
|
const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
|
|
5799
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5696
5800
|
const controlColor = this.resolveControlColor();
|
|
5697
5801
|
const toggleWidth = 40;
|
|
5698
5802
|
const toggleHeight = 20;
|
|
5699
5803
|
const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
|
|
5700
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5804
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5701
5805
|
<rect x="${pos.x}" y="${toggleY}"
|
|
5702
5806
|
width="${toggleWidth}" height="${toggleHeight}"
|
|
5703
5807
|
rx="10"
|
|
@@ -5993,7 +6097,7 @@ var SVGRenderer = class {
|
|
|
5993
6097
|
text-anchor="middle">${node.componentType}</text>
|
|
5994
6098
|
</g>`;
|
|
5995
6099
|
}
|
|
5996
|
-
|
|
6100
|
+
renderStat(node, pos) {
|
|
5997
6101
|
const title = String(node.props.title || "Metric");
|
|
5998
6102
|
const value = String(node.props.value || "0");
|
|
5999
6103
|
const rawCaption = String(node.props.caption || "");
|
|
@@ -6001,7 +6105,9 @@ var SVGRenderer = class {
|
|
|
6001
6105
|
const hasCaption = caption.trim().length > 0;
|
|
6002
6106
|
const iconName = String(node.props.icon || "").trim();
|
|
6003
6107
|
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
6004
|
-
const
|
|
6108
|
+
const variant = String(node.props.variant || "default");
|
|
6109
|
+
const baseAccent = this.resolveAccentColor();
|
|
6110
|
+
const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
|
|
6005
6111
|
const padding = this.resolveSpacing(node.style.padding) || 16;
|
|
6006
6112
|
const innerX = pos.x + padding;
|
|
6007
6113
|
const innerY = pos.y + padding;
|
|
@@ -6025,7 +6131,7 @@ var SVGRenderer = class {
|
|
|
6025
6131
|
const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
|
|
6026
6132
|
const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, innerWidth), captionSize) : "";
|
|
6027
6133
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
6028
|
-
<!--
|
|
6134
|
+
<!-- Stat Background -->
|
|
6029
6135
|
<rect x="${pos.x}" y="${pos.y}"
|
|
6030
6136
|
width="${pos.width}" height="${pos.height}"
|
|
6031
6137
|
rx="8"
|
|
@@ -6668,10 +6774,11 @@ var SVGRenderer = class {
|
|
|
6668
6774
|
buildParentContainerIndex() {
|
|
6669
6775
|
this.parentContainerByChildId.clear();
|
|
6670
6776
|
Object.values(this.ir.project.nodes).forEach((node) => {
|
|
6671
|
-
if (node.kind
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6777
|
+
if (node.kind === "container") {
|
|
6778
|
+
node.children.forEach((childRef) => {
|
|
6779
|
+
this.parentContainerByChildId.set(childRef.ref, node);
|
|
6780
|
+
});
|
|
6781
|
+
}
|
|
6675
6782
|
});
|
|
6676
6783
|
}
|
|
6677
6784
|
escapeXml(text) {
|
|
@@ -7106,7 +7213,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7106
7213
|
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7107
7214
|
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
7108
7215
|
const showOuterBackground = this.parseBooleanProp(
|
|
7109
|
-
node.props.background
|
|
7216
|
+
node.props.background,
|
|
7110
7217
|
false
|
|
7111
7218
|
);
|
|
7112
7219
|
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
@@ -7221,7 +7328,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7221
7328
|
const variant = String(node.props.variant || "default");
|
|
7222
7329
|
const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
|
|
7223
7330
|
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
7224
|
-
const showBackground = this.parseBooleanProp(node.props.background
|
|
7331
|
+
const showBackground = this.parseBooleanProp(node.props.background, false);
|
|
7225
7332
|
const radiusMap = {
|
|
7226
7333
|
none: 0,
|
|
7227
7334
|
sm: 4,
|
|
@@ -7287,9 +7394,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7287
7394
|
return svg;
|
|
7288
7395
|
}
|
|
7289
7396
|
/**
|
|
7290
|
-
* Render
|
|
7397
|
+
* Render Stat with gray blocks instead of values
|
|
7291
7398
|
*/
|
|
7292
|
-
|
|
7399
|
+
renderStat(node, pos) {
|
|
7293
7400
|
const hasIcon = String(node.props.icon || "").trim().length > 0;
|
|
7294
7401
|
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7295
7402
|
const iconSize = 20;
|
|
@@ -8416,7 +8523,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8416
8523
|
/**
|
|
8417
8524
|
* Render stat card with sketch filter and Comic Sans
|
|
8418
8525
|
*/
|
|
8419
|
-
|
|
8526
|
+
renderStat(node, pos) {
|
|
8420
8527
|
const title = String(node.props.title || "Metric");
|
|
8421
8528
|
const value = String(node.props.value || "0");
|
|
8422
8529
|
const rawCaption = String(node.props.caption || "");
|
|
@@ -8424,7 +8531,9 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8424
8531
|
const hasCaption = caption.trim().length > 0;
|
|
8425
8532
|
const iconName = String(node.props.icon || "").trim();
|
|
8426
8533
|
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
8427
|
-
const
|
|
8534
|
+
const variant = String(node.props.variant || "default");
|
|
8535
|
+
const baseAccent = this.resolveAccentColor();
|
|
8536
|
+
const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
|
|
8428
8537
|
const padding = this.resolveSpacing(node.style.padding);
|
|
8429
8538
|
const innerX = pos.x + padding;
|
|
8430
8539
|
const innerY = pos.y + padding;
|
|
@@ -8447,7 +8556,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8447
8556
|
const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
|
|
8448
8557
|
const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, pos.width - padding * 2), captionSize) : "";
|
|
8449
8558
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
8450
|
-
<!--
|
|
8559
|
+
<!-- Stat Background -->
|
|
8451
8560
|
<rect x="${pos.x}" y="${pos.y}"
|
|
8452
8561
|
width="${pos.width}" height="${pos.height}"
|
|
8453
8562
|
rx="8"
|
package/dist/index.d.cts
CHANGED
|
@@ -267,7 +267,7 @@ interface IRScreen {
|
|
|
267
267
|
ref: string;
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
|
-
type IRNode = IRContainerNode | IRComponentNode;
|
|
270
|
+
type IRNode = IRContainerNode | IRComponentNode | IRInstanceNode;
|
|
271
271
|
interface IRContainerNode {
|
|
272
272
|
id: string;
|
|
273
273
|
kind: 'container';
|
|
@@ -298,6 +298,28 @@ interface IRMeta {
|
|
|
298
298
|
source?: string;
|
|
299
299
|
nodeId?: string;
|
|
300
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Wraps an expanded user-defined component or layout instance.
|
|
303
|
+
* Preserves the call-site identity (nodeId) in the SVG so the canvas
|
|
304
|
+
* can select and edit the instance independently from its definition.
|
|
305
|
+
*/
|
|
306
|
+
interface IRInstanceNode {
|
|
307
|
+
id: string;
|
|
308
|
+
kind: 'instance';
|
|
309
|
+
/** Name of the user-defined component or layout (e.g. "MyComp") */
|
|
310
|
+
definitionName: string;
|
|
311
|
+
/** Whether it originated from a `define Component` or `define Layout` */
|
|
312
|
+
definitionKind: 'component' | 'layout';
|
|
313
|
+
/** Props/params passed at the call site (e.g. { text: "Hello" }) */
|
|
314
|
+
invocationProps: Record<string, string | number>;
|
|
315
|
+
/** Reference to the root IR node produced by expanding the definition */
|
|
316
|
+
expandedRoot: {
|
|
317
|
+
ref: string;
|
|
318
|
+
};
|
|
319
|
+
style: IRNodeStyle;
|
|
320
|
+
/** meta.nodeId = SourceMap nodeId of the call-site AST node */
|
|
321
|
+
meta: IRMeta;
|
|
322
|
+
}
|
|
301
323
|
declare class IRGenerator {
|
|
302
324
|
private idGen;
|
|
303
325
|
private nodes;
|
|
@@ -441,6 +463,13 @@ declare class LayoutEngine {
|
|
|
441
463
|
private calculateSplit;
|
|
442
464
|
private calculatePanel;
|
|
443
465
|
private calculateCard;
|
|
466
|
+
/**
|
|
467
|
+
* Calculate layout for an instance node.
|
|
468
|
+
* The instance is a transparent wrapper — its bounding box equals the
|
|
469
|
+
* expanded root's bounding box. We calculate the expanded root first and
|
|
470
|
+
* then copy its position to the instance nodeId so the renderer can use it.
|
|
471
|
+
*/
|
|
472
|
+
private calculateInstance;
|
|
444
473
|
private calculateComponent;
|
|
445
474
|
private resolveSpacing;
|
|
446
475
|
private getSeparateSize;
|
|
@@ -568,6 +597,12 @@ interface SVGRenderOptions {
|
|
|
568
597
|
theme?: 'light' | 'dark';
|
|
569
598
|
includeLabels?: boolean;
|
|
570
599
|
screenName?: string;
|
|
600
|
+
/**
|
|
601
|
+
* When true, renders visual diagnostic overlays for invalid DSL states
|
|
602
|
+
* (e.g. empty containers). Enable in editor/canvas mode; leave false for
|
|
603
|
+
* clean exports (CLI, PDF, PNG).
|
|
604
|
+
*/
|
|
605
|
+
showDiagnostics?: boolean;
|
|
571
606
|
}
|
|
572
607
|
interface SVGComponent {
|
|
573
608
|
tag: string;
|
|
@@ -641,6 +676,16 @@ declare class SVGRenderer {
|
|
|
641
676
|
protected renderTopbar(node: IRComponentNode, pos: any): string;
|
|
642
677
|
protected renderPanelBorder(node: IRNode, pos: any, output: string[]): void;
|
|
643
678
|
protected renderCardBorder(node: IRNode, pos: any, output: string[]): void;
|
|
679
|
+
/**
|
|
680
|
+
* Renders a yellow warning placeholder for containers with no children.
|
|
681
|
+
* Only shown when `showDiagnostics` is enabled (editor/canvas mode).
|
|
682
|
+
*/
|
|
683
|
+
protected renderEmptyContainerDiagnostic(pos: {
|
|
684
|
+
x: number;
|
|
685
|
+
y: number;
|
|
686
|
+
width: number;
|
|
687
|
+
height: number;
|
|
688
|
+
}, containerType?: string): string;
|
|
644
689
|
protected renderSplitDecoration(node: IRNode, pos: any, output: string[]): void;
|
|
645
690
|
protected renderTable(node: IRComponentNode, pos: any): string;
|
|
646
691
|
protected renderChartPlaceholder(node: IRComponentNode, pos: any): string;
|
|
@@ -661,7 +706,7 @@ declare class SVGRenderer {
|
|
|
661
706
|
protected renderModal(node: IRComponentNode, pos: any): string;
|
|
662
707
|
protected renderList(node: IRComponentNode, pos: any): string;
|
|
663
708
|
protected renderGenericComponent(node: IRComponentNode, pos: any): string;
|
|
664
|
-
protected
|
|
709
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
665
710
|
protected renderImage(node: IRComponentNode, pos: any): string;
|
|
666
711
|
protected renderBreadcrumbs(node: IRComponentNode, pos: any): string;
|
|
667
712
|
protected renderSidebarMenu(node: IRComponentNode, pos: any): string;
|
|
@@ -750,7 +795,7 @@ declare class SVGRenderer {
|
|
|
750
795
|
* Get data-node-id attribute string for SVG elements
|
|
751
796
|
* Enables bidirectional selection between code and canvas
|
|
752
797
|
*/
|
|
753
|
-
protected getDataNodeId(node: IRComponentNode | IRContainerNode): string;
|
|
798
|
+
protected getDataNodeId(node: IRComponentNode | IRContainerNode | IRInstanceNode): string;
|
|
754
799
|
}
|
|
755
800
|
declare function renderToSVG(ir: IRContract, layout: LayoutResult, options?: SVGRenderOptions): string;
|
|
756
801
|
declare function createSVGElement(tag: string, attrs: Record<string, string | number>, children?: string[]): string;
|
|
@@ -847,9 +892,9 @@ declare class SkeletonSVGRenderer extends SVGRenderer {
|
|
|
847
892
|
*/
|
|
848
893
|
protected renderTopbar(node: IRComponentNode, pos: any): string;
|
|
849
894
|
/**
|
|
850
|
-
* Render
|
|
895
|
+
* Render Stat with gray blocks instead of values
|
|
851
896
|
*/
|
|
852
|
-
protected
|
|
897
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
853
898
|
/**
|
|
854
899
|
* Render icon as gray square instead of hiding it
|
|
855
900
|
*/
|
|
@@ -992,7 +1037,7 @@ declare class SketchSVGRenderer extends SVGRenderer {
|
|
|
992
1037
|
/**
|
|
993
1038
|
* Render stat card with sketch filter and Comic Sans
|
|
994
1039
|
*/
|
|
995
|
-
protected
|
|
1040
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
996
1041
|
/**
|
|
997
1042
|
* Render image with sketch filter
|
|
998
1043
|
*/
|
|
@@ -1348,4 +1393,4 @@ declare class SourceMapResolver {
|
|
|
1348
1393
|
|
|
1349
1394
|
declare const version = "0.0.1";
|
|
1350
1395
|
|
|
1351
|
-
export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTDefinedLayout, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
|
|
1396
|
+
export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTDefinedLayout, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRInstanceNode, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
|
package/dist/index.d.ts
CHANGED
|
@@ -267,7 +267,7 @@ interface IRScreen {
|
|
|
267
267
|
ref: string;
|
|
268
268
|
};
|
|
269
269
|
}
|
|
270
|
-
type IRNode = IRContainerNode | IRComponentNode;
|
|
270
|
+
type IRNode = IRContainerNode | IRComponentNode | IRInstanceNode;
|
|
271
271
|
interface IRContainerNode {
|
|
272
272
|
id: string;
|
|
273
273
|
kind: 'container';
|
|
@@ -298,6 +298,28 @@ interface IRMeta {
|
|
|
298
298
|
source?: string;
|
|
299
299
|
nodeId?: string;
|
|
300
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Wraps an expanded user-defined component or layout instance.
|
|
303
|
+
* Preserves the call-site identity (nodeId) in the SVG so the canvas
|
|
304
|
+
* can select and edit the instance independently from its definition.
|
|
305
|
+
*/
|
|
306
|
+
interface IRInstanceNode {
|
|
307
|
+
id: string;
|
|
308
|
+
kind: 'instance';
|
|
309
|
+
/** Name of the user-defined component or layout (e.g. "MyComp") */
|
|
310
|
+
definitionName: string;
|
|
311
|
+
/** Whether it originated from a `define Component` or `define Layout` */
|
|
312
|
+
definitionKind: 'component' | 'layout';
|
|
313
|
+
/** Props/params passed at the call site (e.g. { text: "Hello" }) */
|
|
314
|
+
invocationProps: Record<string, string | number>;
|
|
315
|
+
/** Reference to the root IR node produced by expanding the definition */
|
|
316
|
+
expandedRoot: {
|
|
317
|
+
ref: string;
|
|
318
|
+
};
|
|
319
|
+
style: IRNodeStyle;
|
|
320
|
+
/** meta.nodeId = SourceMap nodeId of the call-site AST node */
|
|
321
|
+
meta: IRMeta;
|
|
322
|
+
}
|
|
301
323
|
declare class IRGenerator {
|
|
302
324
|
private idGen;
|
|
303
325
|
private nodes;
|
|
@@ -441,6 +463,13 @@ declare class LayoutEngine {
|
|
|
441
463
|
private calculateSplit;
|
|
442
464
|
private calculatePanel;
|
|
443
465
|
private calculateCard;
|
|
466
|
+
/**
|
|
467
|
+
* Calculate layout for an instance node.
|
|
468
|
+
* The instance is a transparent wrapper — its bounding box equals the
|
|
469
|
+
* expanded root's bounding box. We calculate the expanded root first and
|
|
470
|
+
* then copy its position to the instance nodeId so the renderer can use it.
|
|
471
|
+
*/
|
|
472
|
+
private calculateInstance;
|
|
444
473
|
private calculateComponent;
|
|
445
474
|
private resolveSpacing;
|
|
446
475
|
private getSeparateSize;
|
|
@@ -568,6 +597,12 @@ interface SVGRenderOptions {
|
|
|
568
597
|
theme?: 'light' | 'dark';
|
|
569
598
|
includeLabels?: boolean;
|
|
570
599
|
screenName?: string;
|
|
600
|
+
/**
|
|
601
|
+
* When true, renders visual diagnostic overlays for invalid DSL states
|
|
602
|
+
* (e.g. empty containers). Enable in editor/canvas mode; leave false for
|
|
603
|
+
* clean exports (CLI, PDF, PNG).
|
|
604
|
+
*/
|
|
605
|
+
showDiagnostics?: boolean;
|
|
571
606
|
}
|
|
572
607
|
interface SVGComponent {
|
|
573
608
|
tag: string;
|
|
@@ -641,6 +676,16 @@ declare class SVGRenderer {
|
|
|
641
676
|
protected renderTopbar(node: IRComponentNode, pos: any): string;
|
|
642
677
|
protected renderPanelBorder(node: IRNode, pos: any, output: string[]): void;
|
|
643
678
|
protected renderCardBorder(node: IRNode, pos: any, output: string[]): void;
|
|
679
|
+
/**
|
|
680
|
+
* Renders a yellow warning placeholder for containers with no children.
|
|
681
|
+
* Only shown when `showDiagnostics` is enabled (editor/canvas mode).
|
|
682
|
+
*/
|
|
683
|
+
protected renderEmptyContainerDiagnostic(pos: {
|
|
684
|
+
x: number;
|
|
685
|
+
y: number;
|
|
686
|
+
width: number;
|
|
687
|
+
height: number;
|
|
688
|
+
}, containerType?: string): string;
|
|
644
689
|
protected renderSplitDecoration(node: IRNode, pos: any, output: string[]): void;
|
|
645
690
|
protected renderTable(node: IRComponentNode, pos: any): string;
|
|
646
691
|
protected renderChartPlaceholder(node: IRComponentNode, pos: any): string;
|
|
@@ -661,7 +706,7 @@ declare class SVGRenderer {
|
|
|
661
706
|
protected renderModal(node: IRComponentNode, pos: any): string;
|
|
662
707
|
protected renderList(node: IRComponentNode, pos: any): string;
|
|
663
708
|
protected renderGenericComponent(node: IRComponentNode, pos: any): string;
|
|
664
|
-
protected
|
|
709
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
665
710
|
protected renderImage(node: IRComponentNode, pos: any): string;
|
|
666
711
|
protected renderBreadcrumbs(node: IRComponentNode, pos: any): string;
|
|
667
712
|
protected renderSidebarMenu(node: IRComponentNode, pos: any): string;
|
|
@@ -750,7 +795,7 @@ declare class SVGRenderer {
|
|
|
750
795
|
* Get data-node-id attribute string for SVG elements
|
|
751
796
|
* Enables bidirectional selection between code and canvas
|
|
752
797
|
*/
|
|
753
|
-
protected getDataNodeId(node: IRComponentNode | IRContainerNode): string;
|
|
798
|
+
protected getDataNodeId(node: IRComponentNode | IRContainerNode | IRInstanceNode): string;
|
|
754
799
|
}
|
|
755
800
|
declare function renderToSVG(ir: IRContract, layout: LayoutResult, options?: SVGRenderOptions): string;
|
|
756
801
|
declare function createSVGElement(tag: string, attrs: Record<string, string | number>, children?: string[]): string;
|
|
@@ -847,9 +892,9 @@ declare class SkeletonSVGRenderer extends SVGRenderer {
|
|
|
847
892
|
*/
|
|
848
893
|
protected renderTopbar(node: IRComponentNode, pos: any): string;
|
|
849
894
|
/**
|
|
850
|
-
* Render
|
|
895
|
+
* Render Stat with gray blocks instead of values
|
|
851
896
|
*/
|
|
852
|
-
protected
|
|
897
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
853
898
|
/**
|
|
854
899
|
* Render icon as gray square instead of hiding it
|
|
855
900
|
*/
|
|
@@ -992,7 +1037,7 @@ declare class SketchSVGRenderer extends SVGRenderer {
|
|
|
992
1037
|
/**
|
|
993
1038
|
* Render stat card with sketch filter and Comic Sans
|
|
994
1039
|
*/
|
|
995
|
-
protected
|
|
1040
|
+
protected renderStat(node: IRComponentNode, pos: any): string;
|
|
996
1041
|
/**
|
|
997
1042
|
* Render image with sketch filter
|
|
998
1043
|
*/
|
|
@@ -1348,4 +1393,4 @@ declare class SourceMapResolver {
|
|
|
1348
1393
|
|
|
1349
1394
|
declare const version = "0.0.1";
|
|
1350
1395
|
|
|
1351
|
-
export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTDefinedLayout, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
|
|
1396
|
+
export { type AST, type ASTCell, type ASTComponent, type ASTDefinedComponent, type ASTDefinedLayout, type ASTLayout, type ASTScreen, type CapturedTokens, type CodeRange, DENSITY_TOKENS, DEVICE_PRESETS, type DesignTokens, type DevicePreset, type IRComponent, type IRComponentNode, type IRContainerNode, type IRContract, IRGenerator, type IRInstanceNode, type IRLayout, type IRMeta, type IRMetadata, type IRNode, type IRNodeStyle, type IRProject, type IRScreen, type IRStyle, type IRWireframe, type InsertionPoint, LayoutEngine, type LayoutPosition, type LayoutResult, type ParseDiagnosticsResult, type ParseError, type ParseResult, type ParseWireDSLWithSourceMapOptions, type ParsedComponent, type ParsedWireframe, type Position, type PositionQueryResult, type PropertySourceMap, type SVGComponent, type SVGRenderOptions, SVGRenderer, SkeletonSVGRenderer, SketchSVGRenderer, SourceMapBuilder, type SourceMapEntry, type SourceMapNodeType, SourceMapResolver, buildSVG, calculateLayout, createSVGElement, generateIR, generateStableNodeId, getTypeFromNodeId, isValidDevice, isValidNodeId, parseWireDSL, parseWireDSLWithSourceMap, renderToSVG, resolveDevicePreset, resolveGridPosition, resolveTokens, version };
|
package/dist/index.js
CHANGED
|
@@ -2199,7 +2199,21 @@ var IRComponentNodeSchema = z.object({
|
|
|
2199
2199
|
style: IRNodeStyleSchema,
|
|
2200
2200
|
meta: IRMetaSchema
|
|
2201
2201
|
});
|
|
2202
|
-
var
|
|
2202
|
+
var IRInstanceNodeSchema = z.object({
|
|
2203
|
+
id: z.string(),
|
|
2204
|
+
kind: z.literal("instance"),
|
|
2205
|
+
definitionName: z.string(),
|
|
2206
|
+
definitionKind: z.enum(["component", "layout"]),
|
|
2207
|
+
invocationProps: z.record(z.string(), z.union([z.string(), z.number()])),
|
|
2208
|
+
expandedRoot: z.object({ ref: z.string() }),
|
|
2209
|
+
style: IRNodeStyleSchema,
|
|
2210
|
+
meta: IRMetaSchema
|
|
2211
|
+
});
|
|
2212
|
+
var IRNodeSchema = z.discriminatedUnion("kind", [
|
|
2213
|
+
IRContainerNodeSchema,
|
|
2214
|
+
IRComponentNodeSchema,
|
|
2215
|
+
IRInstanceNodeSchema
|
|
2216
|
+
]);
|
|
2203
2217
|
var IRScreenSchema = z.object({
|
|
2204
2218
|
id: z.string(),
|
|
2205
2219
|
name: z.string(),
|
|
@@ -2422,7 +2436,7 @@ ${messages}`);
|
|
|
2422
2436
|
const layoutChildren = layout.children;
|
|
2423
2437
|
const layoutDefinition = this.definedLayouts.get(layout.layoutType);
|
|
2424
2438
|
if (layoutDefinition) {
|
|
2425
|
-
return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context);
|
|
2439
|
+
return this.expandDefinedLayout(layoutDefinition, layoutParams, layoutChildren, context, layout._meta?.nodeId);
|
|
2426
2440
|
}
|
|
2427
2441
|
const nodeId = this.idGen.generate("node");
|
|
2428
2442
|
const childRefs = [];
|
|
@@ -2461,8 +2475,8 @@ ${messages}`);
|
|
|
2461
2475
|
children: childRefs,
|
|
2462
2476
|
style,
|
|
2463
2477
|
meta: {
|
|
2464
|
-
nodeId
|
|
2465
|
-
|
|
2478
|
+
// Scope nodeId per instance so each expansion gets a unique identifier
|
|
2479
|
+
nodeId: context?.instanceScope ? `${layout._meta?.nodeId}@${context.instanceScope}` : layout._meta?.nodeId
|
|
2466
2480
|
}
|
|
2467
2481
|
};
|
|
2468
2482
|
this.nodes[nodeId] = containerNode;
|
|
@@ -2491,8 +2505,7 @@ ${messages}`);
|
|
|
2491
2505
|
// Cells have no padding by default - grid gap handles spacing
|
|
2492
2506
|
meta: {
|
|
2493
2507
|
source: "cell",
|
|
2494
|
-
nodeId: cell._meta?.nodeId
|
|
2495
|
-
// Pass SourceMap nodeId from AST
|
|
2508
|
+
nodeId: context?.instanceScope ? `${cell._meta?.nodeId}@${context.instanceScope}` : cell._meta?.nodeId
|
|
2496
2509
|
}
|
|
2497
2510
|
};
|
|
2498
2511
|
this.nodes[nodeId] = containerNode;
|
|
@@ -2519,7 +2532,7 @@ ${messages}`);
|
|
|
2519
2532
|
const resolvedProps = this.resolveComponentProps(component.componentType, component.props, context);
|
|
2520
2533
|
const definition = this.definedComponents.get(component.componentType);
|
|
2521
2534
|
if (definition) {
|
|
2522
|
-
return this.expandDefinedComponent(definition, resolvedProps, context);
|
|
2535
|
+
return this.expandDefinedComponent(definition, resolvedProps, component._meta?.nodeId, context);
|
|
2523
2536
|
}
|
|
2524
2537
|
const builtInComponents = /* @__PURE__ */ new Set([
|
|
2525
2538
|
"Button",
|
|
@@ -2529,7 +2542,7 @@ ${messages}`);
|
|
|
2529
2542
|
"Label",
|
|
2530
2543
|
"Image",
|
|
2531
2544
|
"Card",
|
|
2532
|
-
"
|
|
2545
|
+
"Stat",
|
|
2533
2546
|
"Topbar",
|
|
2534
2547
|
"Table",
|
|
2535
2548
|
"Chart",
|
|
@@ -2565,35 +2578,49 @@ ${messages}`);
|
|
|
2565
2578
|
props: resolvedProps,
|
|
2566
2579
|
style: {},
|
|
2567
2580
|
meta: {
|
|
2568
|
-
nodeId
|
|
2569
|
-
|
|
2581
|
+
// Scope nodeId per instance so each expansion gets a unique identifier
|
|
2582
|
+
nodeId: context?.instanceScope ? `${component._meta?.nodeId}@${context.instanceScope}` : component._meta?.nodeId
|
|
2570
2583
|
}
|
|
2571
2584
|
};
|
|
2572
2585
|
this.nodes[nodeId] = componentNode;
|
|
2573
2586
|
return nodeId;
|
|
2574
2587
|
}
|
|
2575
|
-
expandDefinedComponent(definition, invocationArgs, parentContext) {
|
|
2588
|
+
expandDefinedComponent(definition, invocationArgs, callSiteNodeId, parentContext) {
|
|
2576
2589
|
const context = {
|
|
2577
2590
|
args: invocationArgs,
|
|
2578
2591
|
providedArgNames: new Set(Object.keys(invocationArgs)),
|
|
2579
2592
|
usedArgNames: /* @__PURE__ */ new Set(),
|
|
2580
2593
|
definitionName: definition.name,
|
|
2581
2594
|
definitionKind: "component",
|
|
2582
|
-
allowChildrenSlot: false
|
|
2595
|
+
allowChildrenSlot: false,
|
|
2596
|
+
// Scope internal nodeIds using the call-site nodeId
|
|
2597
|
+
instanceScope: callSiteNodeId
|
|
2583
2598
|
};
|
|
2599
|
+
let expandedRootId = null;
|
|
2584
2600
|
if (definition.body.type === "layout") {
|
|
2585
|
-
|
|
2586
|
-
this.reportUnusedArguments(context);
|
|
2587
|
-
return result;
|
|
2601
|
+
expandedRootId = this.convertLayout(definition.body, context);
|
|
2588
2602
|
} else if (definition.body.type === "component") {
|
|
2589
|
-
|
|
2590
|
-
this.reportUnusedArguments(context);
|
|
2591
|
-
return result;
|
|
2603
|
+
expandedRootId = this.convertComponent(definition.body, context);
|
|
2592
2604
|
} else {
|
|
2593
2605
|
throw new Error(`Invalid defined component body type for "${definition.name}"`);
|
|
2594
2606
|
}
|
|
2607
|
+
this.reportUnusedArguments(context);
|
|
2608
|
+
if (!expandedRootId) return null;
|
|
2609
|
+
const instanceNodeId = this.idGen.generate("node");
|
|
2610
|
+
const instanceNode = {
|
|
2611
|
+
id: instanceNodeId,
|
|
2612
|
+
kind: "instance",
|
|
2613
|
+
definitionName: definition.name,
|
|
2614
|
+
definitionKind: "component",
|
|
2615
|
+
invocationProps: invocationArgs,
|
|
2616
|
+
expandedRoot: { ref: expandedRootId },
|
|
2617
|
+
style: {},
|
|
2618
|
+
meta: { nodeId: callSiteNodeId }
|
|
2619
|
+
};
|
|
2620
|
+
this.nodes[instanceNodeId] = instanceNode;
|
|
2621
|
+
return instanceNodeId;
|
|
2595
2622
|
}
|
|
2596
|
-
expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext) {
|
|
2623
|
+
expandDefinedLayout(definition, invocationParams, invocationChildren, parentContext, callSiteNodeId) {
|
|
2597
2624
|
if (invocationChildren.length !== 1) {
|
|
2598
2625
|
this.errors.push({
|
|
2599
2626
|
type: "layout-children-arity",
|
|
@@ -2609,11 +2636,26 @@ ${messages}`);
|
|
|
2609
2636
|
definitionName: definition.name,
|
|
2610
2637
|
definitionKind: "layout",
|
|
2611
2638
|
allowChildrenSlot: true,
|
|
2612
|
-
childrenSlot: resolvedSlot
|
|
2639
|
+
childrenSlot: resolvedSlot,
|
|
2640
|
+
// Scope internal nodeIds using the call-site nodeId
|
|
2641
|
+
instanceScope: callSiteNodeId
|
|
2613
2642
|
};
|
|
2614
|
-
const
|
|
2643
|
+
const expandedRootId = this.convertLayout(definition.body, context);
|
|
2615
2644
|
this.reportUnusedArguments(context);
|
|
2616
|
-
return
|
|
2645
|
+
if (!callSiteNodeId) return expandedRootId;
|
|
2646
|
+
const instanceNodeId = this.idGen.generate("node");
|
|
2647
|
+
const instanceNode = {
|
|
2648
|
+
id: instanceNodeId,
|
|
2649
|
+
kind: "instance",
|
|
2650
|
+
definitionName: definition.name,
|
|
2651
|
+
definitionKind: "layout",
|
|
2652
|
+
invocationProps: invocationParams,
|
|
2653
|
+
expandedRoot: { ref: expandedRootId },
|
|
2654
|
+
style: {},
|
|
2655
|
+
meta: { nodeId: callSiteNodeId }
|
|
2656
|
+
};
|
|
2657
|
+
this.nodes[instanceNodeId] = instanceNode;
|
|
2658
|
+
return instanceNodeId;
|
|
2617
2659
|
}
|
|
2618
2660
|
resolveChildrenSlot(slot, parentContext) {
|
|
2619
2661
|
if (slot.type === "component" && slot.componentType === "Children") {
|
|
@@ -2979,6 +3021,8 @@ var LayoutEngine = class {
|
|
|
2979
3021
|
}
|
|
2980
3022
|
if (node.kind === "container") {
|
|
2981
3023
|
this.calculateContainer(node, nodeId, x, y, width, height);
|
|
3024
|
+
} else if (node.kind === "instance") {
|
|
3025
|
+
this.calculateInstance(node, nodeId, x, y, width, height, parentContainerType);
|
|
2982
3026
|
} else {
|
|
2983
3027
|
this.calculateComponent(node, nodeId, x, y, width, height);
|
|
2984
3028
|
}
|
|
@@ -3011,7 +3055,7 @@ var LayoutEngine = class {
|
|
|
3011
3055
|
this.calculateCard(node, innerX, innerY, innerWidth, innerHeight);
|
|
3012
3056
|
break;
|
|
3013
3057
|
}
|
|
3014
|
-
if (isVerticalStack || node.containerType === "card") {
|
|
3058
|
+
if ((isVerticalStack || node.containerType === "card") && node.children.length > 0) {
|
|
3015
3059
|
let containerMaxY = y;
|
|
3016
3060
|
node.children.forEach((childRef) => {
|
|
3017
3061
|
const childPos = this.result[childRef.ref];
|
|
@@ -3154,6 +3198,10 @@ var LayoutEngine = class {
|
|
|
3154
3198
|
const gap = this.resolveSpacing(node.style.gap);
|
|
3155
3199
|
const padding = this.resolveSpacing(node.style.padding);
|
|
3156
3200
|
let totalHeight = padding * 2;
|
|
3201
|
+
const EMPTY_CONTAINER_MIN_HEIGHT = 40;
|
|
3202
|
+
if (node.children.length === 0) {
|
|
3203
|
+
return Math.max(totalHeight, EMPTY_CONTAINER_MIN_HEIGHT);
|
|
3204
|
+
}
|
|
3157
3205
|
if (node.containerType === "grid") {
|
|
3158
3206
|
const columns = Number(node.params.columns) || 12;
|
|
3159
3207
|
const colWidth = (availableWidth - gap * (columns - 1)) / columns;
|
|
@@ -3406,6 +3454,20 @@ var LayoutEngine = class {
|
|
|
3406
3454
|
}
|
|
3407
3455
|
});
|
|
3408
3456
|
}
|
|
3457
|
+
/**
|
|
3458
|
+
* Calculate layout for an instance node.
|
|
3459
|
+
* The instance is a transparent wrapper — its bounding box equals the
|
|
3460
|
+
* expanded root's bounding box. We calculate the expanded root first and
|
|
3461
|
+
* then copy its position to the instance nodeId so the renderer can use it.
|
|
3462
|
+
*/
|
|
3463
|
+
calculateInstance(node, nodeId, x, y, width, height, parentContainerType) {
|
|
3464
|
+
const expandedRootId = node.expandedRoot.ref;
|
|
3465
|
+
this.calculateNode(expandedRootId, x, y, width, height, parentContainerType);
|
|
3466
|
+
const expandedPos = this.result[expandedRootId];
|
|
3467
|
+
if (expandedPos) {
|
|
3468
|
+
this.result[nodeId] = { ...expandedPos };
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3409
3471
|
calculateComponent(node, nodeId, x, y, width, height) {
|
|
3410
3472
|
if (node.kind !== "component") return;
|
|
3411
3473
|
const componentWidth = Number(node.props.width) || width;
|
|
@@ -3623,7 +3685,7 @@ var LayoutEngine = class {
|
|
|
3623
3685
|
if (node.componentType === "Textarea") return 100 + controlLabelOffset;
|
|
3624
3686
|
if (node.componentType === "Modal") return 300;
|
|
3625
3687
|
if (node.componentType === "Card") return 120;
|
|
3626
|
-
if (node.componentType === "
|
|
3688
|
+
if (node.componentType === "Stat") return 120;
|
|
3627
3689
|
if (node.componentType === "Chart" || node.componentType === "ChartPlaceholder") return 250;
|
|
3628
3690
|
if (node.componentType === "List") {
|
|
3629
3691
|
const itemsFromProps = String(node.props.items || "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
@@ -3674,7 +3736,10 @@ var LayoutEngine = class {
|
|
|
3674
3736
|
const density = this.style.density || "normal";
|
|
3675
3737
|
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
3676
3738
|
const textWidth = this.estimateTextWidth(text, fontSize);
|
|
3677
|
-
|
|
3739
|
+
const iconName = String(node.props.icon || "").trim();
|
|
3740
|
+
const iconSize = iconName ? Math.round(fontSize * 1.1) : 0;
|
|
3741
|
+
const iconGap = iconName ? 8 : 0;
|
|
3742
|
+
return Math.max(60, Math.ceil(textWidth + iconSize + iconGap + (paddingX + extraPadding) * 2));
|
|
3678
3743
|
}
|
|
3679
3744
|
if (node.componentType === "Label" || node.componentType === "Text") {
|
|
3680
3745
|
const text = String(node.props.text || "");
|
|
@@ -3705,7 +3770,7 @@ var LayoutEngine = class {
|
|
|
3705
3770
|
if (node.componentType === "Table") {
|
|
3706
3771
|
return 400;
|
|
3707
3772
|
}
|
|
3708
|
-
if (node.componentType === "
|
|
3773
|
+
if (node.componentType === "Stat" || node.componentType === "Card") {
|
|
3709
3774
|
return 280;
|
|
3710
3775
|
}
|
|
3711
3776
|
if (node.componentType === "SidebarMenu") {
|
|
@@ -4571,7 +4636,8 @@ var SVGRenderer = class {
|
|
|
4571
4636
|
height: options?.height || 720,
|
|
4572
4637
|
theme: colorScheme,
|
|
4573
4638
|
includeLabels: options?.includeLabels ?? true,
|
|
4574
|
-
screenName: options?.screenName
|
|
4639
|
+
screenName: options?.screenName,
|
|
4640
|
+
showDiagnostics: options?.showDiagnostics ?? false
|
|
4575
4641
|
};
|
|
4576
4642
|
this.colorResolver = new ColorResolver();
|
|
4577
4643
|
this.buildParentContainerIndex();
|
|
@@ -4671,13 +4737,30 @@ var SVGRenderer = class {
|
|
|
4671
4737
|
if (node.containerType === "split") {
|
|
4672
4738
|
this.renderSplitDecoration(node, pos, containerGroup);
|
|
4673
4739
|
}
|
|
4674
|
-
node.children.
|
|
4675
|
-
this.
|
|
4676
|
-
}
|
|
4740
|
+
if (node.children.length === 0 && this.options.showDiagnostics) {
|
|
4741
|
+
containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
|
|
4742
|
+
} else {
|
|
4743
|
+
node.children.forEach((childRef) => {
|
|
4744
|
+
this.renderNode(childRef.ref, containerGroup);
|
|
4745
|
+
});
|
|
4746
|
+
}
|
|
4677
4747
|
if (hasNodeId) {
|
|
4678
4748
|
containerGroup.push("</g>");
|
|
4679
4749
|
}
|
|
4680
4750
|
output.push(...containerGroup);
|
|
4751
|
+
} else if (node.kind === "instance") {
|
|
4752
|
+
const instanceGroup = [];
|
|
4753
|
+
if (node.meta.nodeId) {
|
|
4754
|
+
instanceGroup.push(`<g data-node-id="${node.meta.nodeId}">`);
|
|
4755
|
+
}
|
|
4756
|
+
instanceGroup.push(
|
|
4757
|
+
`<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
|
|
4758
|
+
);
|
|
4759
|
+
this.renderNode(node.expandedRoot.ref, instanceGroup);
|
|
4760
|
+
if (node.meta.nodeId) {
|
|
4761
|
+
instanceGroup.push("</g>");
|
|
4762
|
+
}
|
|
4763
|
+
output.push(...instanceGroup);
|
|
4681
4764
|
} else if (node.kind === "component") {
|
|
4682
4765
|
const componentSvg = this.renderComponent(node, pos);
|
|
4683
4766
|
if (componentSvg) {
|
|
@@ -4743,8 +4826,8 @@ var SVGRenderer = class {
|
|
|
4743
4826
|
return this.renderModal(node, pos);
|
|
4744
4827
|
case "List":
|
|
4745
4828
|
return this.renderList(node, pos);
|
|
4746
|
-
case "
|
|
4747
|
-
return this.
|
|
4829
|
+
case "Stat":
|
|
4830
|
+
return this.renderStat(node, pos);
|
|
4748
4831
|
case "Image":
|
|
4749
4832
|
return this.renderImage(node, pos);
|
|
4750
4833
|
// Icon components
|
|
@@ -4790,6 +4873,7 @@ var SVGRenderer = class {
|
|
|
4790
4873
|
const text = String(node.props.text || "Button");
|
|
4791
4874
|
const variant = String(node.props.variant || "default");
|
|
4792
4875
|
const size = String(node.props.size || "md");
|
|
4876
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
4793
4877
|
const density = this.ir.project.style.density || "normal";
|
|
4794
4878
|
const extraPadding = resolveControlHorizontalPadding(String(node.props.padding || "none"), density);
|
|
4795
4879
|
const labelOffset = this.parseBooleanProp(node.props.labelSpace, false) ? 18 : 0;
|
|
@@ -4835,7 +4919,7 @@ var SVGRenderer = class {
|
|
|
4835
4919
|
textX = pos.x + buttonWidth / 2;
|
|
4836
4920
|
textAnchor = "middle";
|
|
4837
4921
|
}
|
|
4838
|
-
let svg = `<g${this.getDataNodeId(node)}>
|
|
4922
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
4839
4923
|
<rect x="${pos.x}" y="${buttonY}"
|
|
4840
4924
|
width="${buttonWidth}" height="${buttonHeight}"
|
|
4841
4925
|
rx="${radius}"
|
|
@@ -4901,6 +4985,7 @@ var SVGRenderer = class {
|
|
|
4901
4985
|
const placeholder = String(node.props.placeholder || "");
|
|
4902
4986
|
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
4903
4987
|
const iconRightName = String(node.props.iconRight || "").trim();
|
|
4988
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
4904
4989
|
const radius = this.tokens.input.radius;
|
|
4905
4990
|
const fontSize = this.tokens.input.fontSize;
|
|
4906
4991
|
const paddingX = this.tokens.input.paddingX;
|
|
@@ -4917,7 +5002,7 @@ var SVGRenderer = class {
|
|
|
4917
5002
|
const textX = pos.x + (iconLeftSvg ? leftOffset : paddingX);
|
|
4918
5003
|
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
4919
5004
|
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
4920
|
-
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5005
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
|
|
4921
5006
|
if (label) {
|
|
4922
5007
|
svg += `
|
|
4923
5008
|
<text x="${pos.x + paddingX}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
@@ -4968,7 +5053,7 @@ var SVGRenderer = class {
|
|
|
4968
5053
|
const variant = String(node.props.variant || "default");
|
|
4969
5054
|
const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
|
|
4970
5055
|
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
4971
|
-
const showBackground = this.parseBooleanProp(node.props.background
|
|
5056
|
+
const showBackground = this.parseBooleanProp(node.props.background, false);
|
|
4972
5057
|
const radiusMap = {
|
|
4973
5058
|
none: 0,
|
|
4974
5059
|
sm: 4,
|
|
@@ -5107,6 +5192,21 @@ var SVGRenderer = class {
|
|
|
5107
5192
|
</g>`;
|
|
5108
5193
|
output.push(svg);
|
|
5109
5194
|
}
|
|
5195
|
+
/**
|
|
5196
|
+
* Renders a yellow warning placeholder for containers with no children.
|
|
5197
|
+
* Only shown when `showDiagnostics` is enabled (editor/canvas mode).
|
|
5198
|
+
*/
|
|
5199
|
+
renderEmptyContainerDiagnostic(pos, containerType) {
|
|
5200
|
+
const diagColor = "#F59E0B";
|
|
5201
|
+
const diagBg = "#FFFBEB";
|
|
5202
|
+
const diagText = "#92400E";
|
|
5203
|
+
const minHeight = 40;
|
|
5204
|
+
const h = Math.max(pos.height, minHeight);
|
|
5205
|
+
const cx = pos.x + pos.width / 2;
|
|
5206
|
+
const cy = pos.y + h / 2;
|
|
5207
|
+
const label = containerType ? `Empty ${containerType}` : "Empty layout";
|
|
5208
|
+
return `<g><rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${h}" rx="4" fill="${diagBg}" stroke="${diagColor}" stroke-width="1" stroke-dasharray="6 3"/><text x="${cx}" y="${cy}" font-family="Arial, Helvetica, sans-serif" font-size="12" fill="${diagText}" text-anchor="middle" dominant-baseline="middle">${label}</text></g>`;
|
|
5209
|
+
}
|
|
5110
5210
|
renderSplitDecoration(node, pos, output) {
|
|
5111
5211
|
if (node.kind !== "container") return;
|
|
5112
5212
|
const gap = this.resolveSpacing(node.style.gap);
|
|
@@ -5155,7 +5255,7 @@ var SVGRenderer = class {
|
|
|
5155
5255
|
const hasCaption = caption.length > 0;
|
|
5156
5256
|
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
5157
5257
|
const showOuterBackground = this.parseBooleanProp(
|
|
5158
|
-
node.props.background
|
|
5258
|
+
node.props.background,
|
|
5159
5259
|
false
|
|
5160
5260
|
);
|
|
5161
5261
|
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
@@ -5540,6 +5640,7 @@ var SVGRenderer = class {
|
|
|
5540
5640
|
const placeholder = String(node.props.placeholder || "Select...");
|
|
5541
5641
|
const iconLeftName = String(node.props.iconLeft || "").trim();
|
|
5542
5642
|
const iconRightName = String(node.props.iconRight || "").trim();
|
|
5643
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5543
5644
|
const labelOffset = this.getControlLabelOffset(label);
|
|
5544
5645
|
const controlY = pos.y + labelOffset;
|
|
5545
5646
|
const controlHeight = Math.max(16, pos.height - labelOffset);
|
|
@@ -5553,7 +5654,7 @@ var SVGRenderer = class {
|
|
|
5553
5654
|
const chevronWidth = 20;
|
|
5554
5655
|
const iconColor = this.hexToRgba(this.resolveMutedColor(), 0.8);
|
|
5555
5656
|
const iconCenterY = controlY + (controlHeight - iconSize) / 2;
|
|
5556
|
-
let svg = `<g${this.getDataNodeId(node)}>`;
|
|
5657
|
+
let svg = `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>`;
|
|
5557
5658
|
if (label) {
|
|
5558
5659
|
svg += `
|
|
5559
5660
|
<text x="${pos.x}" y="${this.getControlLabelBaselineY(pos.y)}"
|
|
@@ -5602,10 +5703,11 @@ var SVGRenderer = class {
|
|
|
5602
5703
|
renderCheckbox(node, pos) {
|
|
5603
5704
|
const label = String(node.props.label || "Checkbox");
|
|
5604
5705
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
5706
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5605
5707
|
const controlColor = this.resolveControlColor();
|
|
5606
5708
|
const checkboxSize = 18;
|
|
5607
5709
|
const checkboxY = pos.y + pos.height / 2 - checkboxSize / 2;
|
|
5608
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5710
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5609
5711
|
<rect x="${pos.x}" y="${checkboxY}"
|
|
5610
5712
|
width="${checkboxSize}" height="${checkboxSize}"
|
|
5611
5713
|
rx="4"
|
|
@@ -5626,10 +5728,11 @@ var SVGRenderer = class {
|
|
|
5626
5728
|
renderRadio(node, pos) {
|
|
5627
5729
|
const label = String(node.props.label || "Radio");
|
|
5628
5730
|
const checked = String(node.props.checked || "false").toLowerCase() === "true";
|
|
5731
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5629
5732
|
const controlColor = this.resolveControlColor();
|
|
5630
5733
|
const radioSize = 16;
|
|
5631
5734
|
const radioY = pos.y + pos.height / 2 - radioSize / 2;
|
|
5632
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5735
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5633
5736
|
<circle cx="${pos.x + radioSize / 2}" cy="${radioY + radioSize / 2}"
|
|
5634
5737
|
r="${radioSize / 2}"
|
|
5635
5738
|
fill="${this.renderTheme.cardBg}"
|
|
@@ -5647,11 +5750,12 @@ var SVGRenderer = class {
|
|
|
5647
5750
|
renderToggle(node, pos) {
|
|
5648
5751
|
const label = String(node.props.label || "Toggle");
|
|
5649
5752
|
const enabled = String(node.props.enabled || "false").toLowerCase() === "true";
|
|
5753
|
+
const disabled = this.parseBooleanProp(node.props.disabled, false);
|
|
5650
5754
|
const controlColor = this.resolveControlColor();
|
|
5651
5755
|
const toggleWidth = 40;
|
|
5652
5756
|
const toggleHeight = 20;
|
|
5653
5757
|
const toggleY = pos.y + pos.height / 2 - toggleHeight / 2;
|
|
5654
|
-
return `<g${this.getDataNodeId(node)}>
|
|
5758
|
+
return `<g${this.getDataNodeId(node)}${disabled ? ' opacity="0.45"' : ""}>
|
|
5655
5759
|
<rect x="${pos.x}" y="${toggleY}"
|
|
5656
5760
|
width="${toggleWidth}" height="${toggleHeight}"
|
|
5657
5761
|
rx="10"
|
|
@@ -5947,7 +6051,7 @@ var SVGRenderer = class {
|
|
|
5947
6051
|
text-anchor="middle">${node.componentType}</text>
|
|
5948
6052
|
</g>`;
|
|
5949
6053
|
}
|
|
5950
|
-
|
|
6054
|
+
renderStat(node, pos) {
|
|
5951
6055
|
const title = String(node.props.title || "Metric");
|
|
5952
6056
|
const value = String(node.props.value || "0");
|
|
5953
6057
|
const rawCaption = String(node.props.caption || "");
|
|
@@ -5955,7 +6059,9 @@ var SVGRenderer = class {
|
|
|
5955
6059
|
const hasCaption = caption.trim().length > 0;
|
|
5956
6060
|
const iconName = String(node.props.icon || "").trim();
|
|
5957
6061
|
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
5958
|
-
const
|
|
6062
|
+
const variant = String(node.props.variant || "default");
|
|
6063
|
+
const baseAccent = this.resolveAccentColor();
|
|
6064
|
+
const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
|
|
5959
6065
|
const padding = this.resolveSpacing(node.style.padding) || 16;
|
|
5960
6066
|
const innerX = pos.x + padding;
|
|
5961
6067
|
const innerY = pos.y + padding;
|
|
@@ -5979,7 +6085,7 @@ var SVGRenderer = class {
|
|
|
5979
6085
|
const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
|
|
5980
6086
|
const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, innerWidth), captionSize) : "";
|
|
5981
6087
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
5982
|
-
<!--
|
|
6088
|
+
<!-- Stat Background -->
|
|
5983
6089
|
<rect x="${pos.x}" y="${pos.y}"
|
|
5984
6090
|
width="${pos.width}" height="${pos.height}"
|
|
5985
6091
|
rx="8"
|
|
@@ -6622,10 +6728,11 @@ var SVGRenderer = class {
|
|
|
6622
6728
|
buildParentContainerIndex() {
|
|
6623
6729
|
this.parentContainerByChildId.clear();
|
|
6624
6730
|
Object.values(this.ir.project.nodes).forEach((node) => {
|
|
6625
|
-
if (node.kind
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6731
|
+
if (node.kind === "container") {
|
|
6732
|
+
node.children.forEach((childRef) => {
|
|
6733
|
+
this.parentContainerByChildId.set(childRef.ref, node);
|
|
6734
|
+
});
|
|
6735
|
+
}
|
|
6629
6736
|
});
|
|
6630
6737
|
}
|
|
6631
6738
|
escapeXml(text) {
|
|
@@ -7060,7 +7167,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7060
7167
|
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7061
7168
|
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
7062
7169
|
const showOuterBackground = this.parseBooleanProp(
|
|
7063
|
-
node.props.background
|
|
7170
|
+
node.props.background,
|
|
7064
7171
|
false
|
|
7065
7172
|
);
|
|
7066
7173
|
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
@@ -7175,7 +7282,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7175
7282
|
const variant = String(node.props.variant || "default");
|
|
7176
7283
|
const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
|
|
7177
7284
|
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
7178
|
-
const showBackground = this.parseBooleanProp(node.props.background
|
|
7285
|
+
const showBackground = this.parseBooleanProp(node.props.background, false);
|
|
7179
7286
|
const radiusMap = {
|
|
7180
7287
|
none: 0,
|
|
7181
7288
|
sm: 4,
|
|
@@ -7241,9 +7348,9 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7241
7348
|
return svg;
|
|
7242
7349
|
}
|
|
7243
7350
|
/**
|
|
7244
|
-
* Render
|
|
7351
|
+
* Render Stat with gray blocks instead of values
|
|
7245
7352
|
*/
|
|
7246
|
-
|
|
7353
|
+
renderStat(node, pos) {
|
|
7247
7354
|
const hasIcon = String(node.props.icon || "").trim().length > 0;
|
|
7248
7355
|
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7249
7356
|
const iconSize = 20;
|
|
@@ -8370,7 +8477,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8370
8477
|
/**
|
|
8371
8478
|
* Render stat card with sketch filter and Comic Sans
|
|
8372
8479
|
*/
|
|
8373
|
-
|
|
8480
|
+
renderStat(node, pos) {
|
|
8374
8481
|
const title = String(node.props.title || "Metric");
|
|
8375
8482
|
const value = String(node.props.value || "0");
|
|
8376
8483
|
const rawCaption = String(node.props.caption || "");
|
|
@@ -8378,7 +8485,9 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8378
8485
|
const hasCaption = caption.trim().length > 0;
|
|
8379
8486
|
const iconName = String(node.props.icon || "").trim();
|
|
8380
8487
|
const iconSvg = iconName ? getIcon(iconName) : null;
|
|
8381
|
-
const
|
|
8488
|
+
const variant = String(node.props.variant || "default");
|
|
8489
|
+
const baseAccent = this.resolveAccentColor();
|
|
8490
|
+
const accentColor = variant !== "default" ? this.resolveVariantColor(variant, baseAccent) : baseAccent;
|
|
8382
8491
|
const padding = this.resolveSpacing(node.style.padding);
|
|
8383
8492
|
const innerX = pos.x + padding;
|
|
8384
8493
|
const innerY = pos.y + padding;
|
|
@@ -8401,7 +8510,7 @@ var SketchSVGRenderer = class extends SVGRenderer {
|
|
|
8401
8510
|
const visibleTitle = this.truncateTextToWidth(title, titleMaxWidth, titleSize);
|
|
8402
8511
|
const visibleCaption = hasCaption ? this.truncateTextToWidth(caption, Math.max(20, pos.width - padding * 2), captionSize) : "";
|
|
8403
8512
|
let svg = `<g${this.getDataNodeId(node)}>
|
|
8404
|
-
<!--
|
|
8513
|
+
<!-- Stat Background -->
|
|
8405
8514
|
<rect x="${pos.x}" y="${pos.y}"
|
|
8406
8515
|
width="${pos.width}" height="${pos.height}"
|
|
8407
8516
|
rx="8"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wire-dsl/engine",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "25.2.0",
|