@wire-dsl/engine 0.4.0 → 0.4.1
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/dist/index.cjs +130 -34
- package/dist/index.d.cts +48 -3
- package/dist/index.d.ts +48 -3
- package/dist/index.js +130 -34
- package/package.json +1 -1
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",
|
|
@@ -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;
|
|
@@ -4617,7 +4679,8 @@ var SVGRenderer = class {
|
|
|
4617
4679
|
height: options?.height || 720,
|
|
4618
4680
|
theme: colorScheme,
|
|
4619
4681
|
includeLabels: options?.includeLabels ?? true,
|
|
4620
|
-
screenName: options?.screenName
|
|
4682
|
+
screenName: options?.screenName,
|
|
4683
|
+
showDiagnostics: options?.showDiagnostics ?? false
|
|
4621
4684
|
};
|
|
4622
4685
|
this.colorResolver = new ColorResolver();
|
|
4623
4686
|
this.buildParentContainerIndex();
|
|
@@ -4717,13 +4780,30 @@ var SVGRenderer = class {
|
|
|
4717
4780
|
if (node.containerType === "split") {
|
|
4718
4781
|
this.renderSplitDecoration(node, pos, containerGroup);
|
|
4719
4782
|
}
|
|
4720
|
-
node.children.
|
|
4721
|
-
this.
|
|
4722
|
-
}
|
|
4783
|
+
if (node.children.length === 0 && this.options.showDiagnostics) {
|
|
4784
|
+
containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
|
|
4785
|
+
} else {
|
|
4786
|
+
node.children.forEach((childRef) => {
|
|
4787
|
+
this.renderNode(childRef.ref, containerGroup);
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4723
4790
|
if (hasNodeId) {
|
|
4724
4791
|
containerGroup.push("</g>");
|
|
4725
4792
|
}
|
|
4726
4793
|
output.push(...containerGroup);
|
|
4794
|
+
} else if (node.kind === "instance") {
|
|
4795
|
+
const instanceGroup = [];
|
|
4796
|
+
if (node.meta.nodeId) {
|
|
4797
|
+
instanceGroup.push(`<g data-node-id="${node.meta.nodeId}">`);
|
|
4798
|
+
}
|
|
4799
|
+
instanceGroup.push(
|
|
4800
|
+
`<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
|
|
4801
|
+
);
|
|
4802
|
+
this.renderNode(node.expandedRoot.ref, instanceGroup);
|
|
4803
|
+
if (node.meta.nodeId) {
|
|
4804
|
+
instanceGroup.push("</g>");
|
|
4805
|
+
}
|
|
4806
|
+
output.push(...instanceGroup);
|
|
4727
4807
|
} else if (node.kind === "component") {
|
|
4728
4808
|
const componentSvg = this.renderComponent(node, pos);
|
|
4729
4809
|
if (componentSvg) {
|
|
@@ -5014,7 +5094,7 @@ var SVGRenderer = class {
|
|
|
5014
5094
|
const variant = String(node.props.variant || "default");
|
|
5015
5095
|
const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
|
|
5016
5096
|
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
5017
|
-
const showBackground = this.parseBooleanProp(node.props.background
|
|
5097
|
+
const showBackground = this.parseBooleanProp(node.props.background, false);
|
|
5018
5098
|
const radiusMap = {
|
|
5019
5099
|
none: 0,
|
|
5020
5100
|
sm: 4,
|
|
@@ -5153,6 +5233,21 @@ var SVGRenderer = class {
|
|
|
5153
5233
|
</g>`;
|
|
5154
5234
|
output.push(svg);
|
|
5155
5235
|
}
|
|
5236
|
+
/**
|
|
5237
|
+
* Renders a yellow warning placeholder for containers with no children.
|
|
5238
|
+
* Only shown when `showDiagnostics` is enabled (editor/canvas mode).
|
|
5239
|
+
*/
|
|
5240
|
+
renderEmptyContainerDiagnostic(pos, containerType) {
|
|
5241
|
+
const diagColor = "#F59E0B";
|
|
5242
|
+
const diagBg = "#FFFBEB";
|
|
5243
|
+
const diagText = "#92400E";
|
|
5244
|
+
const minHeight = 40;
|
|
5245
|
+
const h = Math.max(pos.height, minHeight);
|
|
5246
|
+
const cx = pos.x + pos.width / 2;
|
|
5247
|
+
const cy = pos.y + h / 2;
|
|
5248
|
+
const label = containerType ? `Empty ${containerType}` : "Empty layout";
|
|
5249
|
+
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>`;
|
|
5250
|
+
}
|
|
5156
5251
|
renderSplitDecoration(node, pos, output) {
|
|
5157
5252
|
if (node.kind !== "container") return;
|
|
5158
5253
|
const gap = this.resolveSpacing(node.style.gap);
|
|
@@ -5201,7 +5296,7 @@ var SVGRenderer = class {
|
|
|
5201
5296
|
const hasCaption = caption.length > 0;
|
|
5202
5297
|
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
5203
5298
|
const showOuterBackground = this.parseBooleanProp(
|
|
5204
|
-
node.props.background
|
|
5299
|
+
node.props.background,
|
|
5205
5300
|
false
|
|
5206
5301
|
);
|
|
5207
5302
|
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
@@ -6668,10 +6763,11 @@ var SVGRenderer = class {
|
|
|
6668
6763
|
buildParentContainerIndex() {
|
|
6669
6764
|
this.parentContainerByChildId.clear();
|
|
6670
6765
|
Object.values(this.ir.project.nodes).forEach((node) => {
|
|
6671
|
-
if (node.kind
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6766
|
+
if (node.kind === "container") {
|
|
6767
|
+
node.children.forEach((childRef) => {
|
|
6768
|
+
this.parentContainerByChildId.set(childRef.ref, node);
|
|
6769
|
+
});
|
|
6770
|
+
}
|
|
6675
6771
|
});
|
|
6676
6772
|
}
|
|
6677
6773
|
escapeXml(text) {
|
|
@@ -7106,7 +7202,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7106
7202
|
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7107
7203
|
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
7108
7204
|
const showOuterBackground = this.parseBooleanProp(
|
|
7109
|
-
node.props.background
|
|
7205
|
+
node.props.background,
|
|
7110
7206
|
false
|
|
7111
7207
|
);
|
|
7112
7208
|
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
@@ -7221,7 +7317,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7221
7317
|
const variant = String(node.props.variant || "default");
|
|
7222
7318
|
const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
|
|
7223
7319
|
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
7224
|
-
const showBackground = this.parseBooleanProp(node.props.background
|
|
7320
|
+
const showBackground = this.parseBooleanProp(node.props.background, false);
|
|
7225
7321
|
const radiusMap = {
|
|
7226
7322
|
none: 0,
|
|
7227
7323
|
sm: 4,
|
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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -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",
|
|
@@ -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;
|
|
@@ -4571,7 +4633,8 @@ var SVGRenderer = class {
|
|
|
4571
4633
|
height: options?.height || 720,
|
|
4572
4634
|
theme: colorScheme,
|
|
4573
4635
|
includeLabels: options?.includeLabels ?? true,
|
|
4574
|
-
screenName: options?.screenName
|
|
4636
|
+
screenName: options?.screenName,
|
|
4637
|
+
showDiagnostics: options?.showDiagnostics ?? false
|
|
4575
4638
|
};
|
|
4576
4639
|
this.colorResolver = new ColorResolver();
|
|
4577
4640
|
this.buildParentContainerIndex();
|
|
@@ -4671,13 +4734,30 @@ var SVGRenderer = class {
|
|
|
4671
4734
|
if (node.containerType === "split") {
|
|
4672
4735
|
this.renderSplitDecoration(node, pos, containerGroup);
|
|
4673
4736
|
}
|
|
4674
|
-
node.children.
|
|
4675
|
-
this.
|
|
4676
|
-
}
|
|
4737
|
+
if (node.children.length === 0 && this.options.showDiagnostics) {
|
|
4738
|
+
containerGroup.push(this.renderEmptyContainerDiagnostic(pos, node.containerType));
|
|
4739
|
+
} else {
|
|
4740
|
+
node.children.forEach((childRef) => {
|
|
4741
|
+
this.renderNode(childRef.ref, containerGroup);
|
|
4742
|
+
});
|
|
4743
|
+
}
|
|
4677
4744
|
if (hasNodeId) {
|
|
4678
4745
|
containerGroup.push("</g>");
|
|
4679
4746
|
}
|
|
4680
4747
|
output.push(...containerGroup);
|
|
4748
|
+
} else if (node.kind === "instance") {
|
|
4749
|
+
const instanceGroup = [];
|
|
4750
|
+
if (node.meta.nodeId) {
|
|
4751
|
+
instanceGroup.push(`<g data-node-id="${node.meta.nodeId}">`);
|
|
4752
|
+
}
|
|
4753
|
+
instanceGroup.push(
|
|
4754
|
+
`<rect x="${pos.x}" y="${pos.y}" width="${pos.width}" height="${pos.height}" fill="transparent" stroke="none" pointer-events="all"/>`
|
|
4755
|
+
);
|
|
4756
|
+
this.renderNode(node.expandedRoot.ref, instanceGroup);
|
|
4757
|
+
if (node.meta.nodeId) {
|
|
4758
|
+
instanceGroup.push("</g>");
|
|
4759
|
+
}
|
|
4760
|
+
output.push(...instanceGroup);
|
|
4681
4761
|
} else if (node.kind === "component") {
|
|
4682
4762
|
const componentSvg = this.renderComponent(node, pos);
|
|
4683
4763
|
if (componentSvg) {
|
|
@@ -4968,7 +5048,7 @@ var SVGRenderer = class {
|
|
|
4968
5048
|
const variant = String(node.props.variant || "default");
|
|
4969
5049
|
const accentColor = variant === "default" ? this.resolveAccentColor() : this.resolveVariantColor(variant, this.resolveAccentColor());
|
|
4970
5050
|
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
4971
|
-
const showBackground = this.parseBooleanProp(node.props.background
|
|
5051
|
+
const showBackground = this.parseBooleanProp(node.props.background, false);
|
|
4972
5052
|
const radiusMap = {
|
|
4973
5053
|
none: 0,
|
|
4974
5054
|
sm: 4,
|
|
@@ -5107,6 +5187,21 @@ var SVGRenderer = class {
|
|
|
5107
5187
|
</g>`;
|
|
5108
5188
|
output.push(svg);
|
|
5109
5189
|
}
|
|
5190
|
+
/**
|
|
5191
|
+
* Renders a yellow warning placeholder for containers with no children.
|
|
5192
|
+
* Only shown when `showDiagnostics` is enabled (editor/canvas mode).
|
|
5193
|
+
*/
|
|
5194
|
+
renderEmptyContainerDiagnostic(pos, containerType) {
|
|
5195
|
+
const diagColor = "#F59E0B";
|
|
5196
|
+
const diagBg = "#FFFBEB";
|
|
5197
|
+
const diagText = "#92400E";
|
|
5198
|
+
const minHeight = 40;
|
|
5199
|
+
const h = Math.max(pos.height, minHeight);
|
|
5200
|
+
const cx = pos.x + pos.width / 2;
|
|
5201
|
+
const cy = pos.y + h / 2;
|
|
5202
|
+
const label = containerType ? `Empty ${containerType}` : "Empty layout";
|
|
5203
|
+
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>`;
|
|
5204
|
+
}
|
|
5110
5205
|
renderSplitDecoration(node, pos, output) {
|
|
5111
5206
|
if (node.kind !== "container") return;
|
|
5112
5207
|
const gap = this.resolveSpacing(node.style.gap);
|
|
@@ -5155,7 +5250,7 @@ var SVGRenderer = class {
|
|
|
5155
5250
|
const hasCaption = caption.length > 0;
|
|
5156
5251
|
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
5157
5252
|
const showOuterBackground = this.parseBooleanProp(
|
|
5158
|
-
node.props.background
|
|
5253
|
+
node.props.background,
|
|
5159
5254
|
false
|
|
5160
5255
|
);
|
|
5161
5256
|
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
@@ -6622,10 +6717,11 @@ var SVGRenderer = class {
|
|
|
6622
6717
|
buildParentContainerIndex() {
|
|
6623
6718
|
this.parentContainerByChildId.clear();
|
|
6624
6719
|
Object.values(this.ir.project.nodes).forEach((node) => {
|
|
6625
|
-
if (node.kind
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6720
|
+
if (node.kind === "container") {
|
|
6721
|
+
node.children.forEach((childRef) => {
|
|
6722
|
+
this.parentContainerByChildId.set(childRef.ref, node);
|
|
6723
|
+
});
|
|
6724
|
+
}
|
|
6629
6725
|
});
|
|
6630
6726
|
}
|
|
6631
6727
|
escapeXml(text) {
|
|
@@ -7060,7 +7156,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7060
7156
|
const hasCaption = String(node.props.caption || "").trim().length > 0;
|
|
7061
7157
|
const showOuterBorder = this.parseBooleanProp(node.props.border, false);
|
|
7062
7158
|
const showOuterBackground = this.parseBooleanProp(
|
|
7063
|
-
node.props.background
|
|
7159
|
+
node.props.background,
|
|
7064
7160
|
false
|
|
7065
7161
|
);
|
|
7066
7162
|
const showInnerBorder = this.parseBooleanProp(node.props.innerBorder, true);
|
|
@@ -7175,7 +7271,7 @@ var SkeletonSVGRenderer = class extends SVGRenderer {
|
|
|
7175
7271
|
const variant = String(node.props.variant || "default");
|
|
7176
7272
|
const accentBlock = variant === "default" ? this.renderTheme.border : this.hexToRgba(this.resolveVariantColor(variant, this.resolveAccentColor()), 0.35);
|
|
7177
7273
|
const showBorder = this.parseBooleanProp(node.props.border, false);
|
|
7178
|
-
const showBackground = this.parseBooleanProp(node.props.background
|
|
7274
|
+
const showBackground = this.parseBooleanProp(node.props.background, false);
|
|
7179
7275
|
const radiusMap = {
|
|
7180
7276
|
none: 0,
|
|
7181
7277
|
sm: 4,
|