view-contracts 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/preview.mjs +61 -62
- package/dist/preview.mjs.map +4 -4
- package/dist/view-contracts.bundle.mjs +232 -174
- package/dist/view-contracts.bundle.mjs.map +4 -4
- package/package.json +1 -1
- package/renderers/react/defaults/theme.yaml +29 -0
- package/renderers/react/defaults/tokens.yaml +61 -1
- package/renderers/react/lowering/lowerToJsx.ts +6 -6
- package/renderers/react/paths.ts +5 -1
- package/renderers/react/runtime/structural.css +38 -0
- package/renderers/react/style/foldLiterals.ts +14 -4
- package/renderers/swiftui/lowering/lowerToSwiftUI.ts +250 -31
- package/renderers/swiftui/lowering/modifiers.ts +90 -11
- package/renderers/swiftui/renderRuntime.ts +48 -0
- package/renderers/swiftui/renderTheme.ts +69 -3
- package/renderers/react/runtime/theme.css +0 -57
package/package.json
CHANGED
|
@@ -29,6 +29,21 @@ defaults:
|
|
|
29
29
|
border: "{ui.layout.border-1}"
|
|
30
30
|
borderColor: "{ui.border}"
|
|
31
31
|
minHeight: "{ui.space.input-min-height}"
|
|
32
|
+
TableHeader:
|
|
33
|
+
background: "{ui.surface-muted}"
|
|
34
|
+
alignItems: "{ui.layout.align-center}"
|
|
35
|
+
TableColumn:
|
|
36
|
+
padding: "{ui.space.table-column-padding}"
|
|
37
|
+
fontSize: "{ui.space.table-column-font-size}"
|
|
38
|
+
fontWeight: "{ui.layout.font-semibold}"
|
|
39
|
+
foreground: "{ui.text-muted}"
|
|
40
|
+
TableCell:
|
|
41
|
+
padding: "{ui.space.table-cell-padding}"
|
|
42
|
+
fontSize: "{ui.space.table-cell-font-size}"
|
|
43
|
+
TableRow:
|
|
44
|
+
alignItems: "{ui.layout.align-center}"
|
|
45
|
+
Box:
|
|
46
|
+
width: "{ui.layout.width-full}"
|
|
32
47
|
variants:
|
|
33
48
|
Button:
|
|
34
49
|
primary:
|
|
@@ -46,6 +61,16 @@ variants:
|
|
|
46
61
|
Text:
|
|
47
62
|
muted:
|
|
48
63
|
foreground: "{ui.text-muted}"
|
|
64
|
+
field-label:
|
|
65
|
+
fontSize: "{ui.space.font-field-label}"
|
|
66
|
+
fontWeight: "{ui.layout.font-bold}"
|
|
67
|
+
foreground: "{ui.text}"
|
|
68
|
+
field-help:
|
|
69
|
+
fontSize: "{ui.space.font-field-help}"
|
|
70
|
+
foreground: "{ui.text-muted}"
|
|
71
|
+
field-error:
|
|
72
|
+
fontSize: "{ui.space.font-field-help}"
|
|
73
|
+
foreground: "{ui.danger}"
|
|
49
74
|
primary:
|
|
50
75
|
foreground: "{ui.primary}"
|
|
51
76
|
success:
|
|
@@ -54,3 +79,7 @@ variants:
|
|
|
54
79
|
foreground: "{ui.warning}"
|
|
55
80
|
danger:
|
|
56
81
|
foreground: "{ui.danger}"
|
|
82
|
+
Box:
|
|
83
|
+
card:
|
|
84
|
+
background: "{ui.surface}"
|
|
85
|
+
width: "{ui.layout.width-full}"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Default design tokens —
|
|
1
|
+
# Default design tokens — merged into generated/react/theme.css via emitTokenCss().
|
|
2
2
|
ui:
|
|
3
3
|
bg:
|
|
4
4
|
$type: color
|
|
@@ -61,9 +61,18 @@ ui:
|
|
|
61
61
|
font-bold:
|
|
62
62
|
$type: layout
|
|
63
63
|
$value: "700"
|
|
64
|
+
font-semibold:
|
|
65
|
+
$type: layout
|
|
66
|
+
$value: "600"
|
|
64
67
|
border-1:
|
|
65
68
|
$type: layout
|
|
66
69
|
$value: "1px solid"
|
|
70
|
+
align-center:
|
|
71
|
+
$type: layout
|
|
72
|
+
$value: "center"
|
|
73
|
+
width-full:
|
|
74
|
+
$type: layout
|
|
75
|
+
$value: "100%"
|
|
67
76
|
space:
|
|
68
77
|
radius-md:
|
|
69
78
|
$type: dimension
|
|
@@ -83,3 +92,54 @@ ui:
|
|
|
83
92
|
textarea-padding:
|
|
84
93
|
$type: dimension
|
|
85
94
|
$value: "10px 12px"
|
|
95
|
+
field-gap:
|
|
96
|
+
$type: dimension
|
|
97
|
+
$value: "6px"
|
|
98
|
+
field-inner-gap:
|
|
99
|
+
$type: dimension
|
|
100
|
+
$value: "4px"
|
|
101
|
+
checkbox-gap:
|
|
102
|
+
$type: dimension
|
|
103
|
+
$value: "8px"
|
|
104
|
+
list-indent:
|
|
105
|
+
$type: dimension
|
|
106
|
+
$value: "20px"
|
|
107
|
+
list-item-padding:
|
|
108
|
+
$type: dimension
|
|
109
|
+
$value: "2px 0"
|
|
110
|
+
line-height-body:
|
|
111
|
+
$type: dimension
|
|
112
|
+
$value: "1.6"
|
|
113
|
+
line-height-heading:
|
|
114
|
+
$type: dimension
|
|
115
|
+
$value: "1.2"
|
|
116
|
+
font-field-label:
|
|
117
|
+
$type: dimension
|
|
118
|
+
$value: "13px"
|
|
119
|
+
font-field-help:
|
|
120
|
+
$type: dimension
|
|
121
|
+
$value: "12px"
|
|
122
|
+
table-column-padding:
|
|
123
|
+
$type: dimension
|
|
124
|
+
$value: "12px 14px"
|
|
125
|
+
table-column-font-size:
|
|
126
|
+
$type: dimension
|
|
127
|
+
$value: "12px"
|
|
128
|
+
table-cell-padding:
|
|
129
|
+
$type: dimension
|
|
130
|
+
$value: "14px"
|
|
131
|
+
table-cell-font-size:
|
|
132
|
+
$type: dimension
|
|
133
|
+
$value: "14px"
|
|
134
|
+
input-min-height-sm:
|
|
135
|
+
$type: dimension
|
|
136
|
+
$value: "34px"
|
|
137
|
+
input-min-height-lg:
|
|
138
|
+
$type: dimension
|
|
139
|
+
$value: "50px"
|
|
140
|
+
input-padding-sm:
|
|
141
|
+
$type: dimension
|
|
142
|
+
$value: "6px 10px"
|
|
143
|
+
input-padding-lg:
|
|
144
|
+
$type: dimension
|
|
145
|
+
$value: "12px 14px"
|
|
@@ -160,20 +160,20 @@ function emitFieldWrap(inner: string, props: Record<string, IrValue>, level: num
|
|
|
160
160
|
const error = propExpr(props, 'error');
|
|
161
161
|
const lines = [`${indent(level)}<label className="vc-field">`];
|
|
162
162
|
if (typeof labelLit === 'string') {
|
|
163
|
-
lines.push(`${indent(level + 1)}<span className="vc-field-label">{${JSON.stringify(labelLit)}}</span>`);
|
|
163
|
+
lines.push(`${indent(level + 1)}<span className="vc-text vc-text--field-label">{${JSON.stringify(labelLit)}}</span>`);
|
|
164
164
|
} else if (label) {
|
|
165
|
-
lines.push(`${indent(level + 1)}{${label} ? <span className="vc-field-label">{${label}}</span> : null}`);
|
|
165
|
+
lines.push(`${indent(level + 1)}{${label} ? <span className="vc-text vc-text--field-label">{${label}}</span> : null}`);
|
|
166
166
|
}
|
|
167
167
|
lines.push(`${indent(level + 1)}${inner}`);
|
|
168
168
|
if (typeof helpLit === 'string') {
|
|
169
|
-
lines.push(`${indent(level + 1)}<span className="vc-field-help">{${JSON.stringify(helpLit)}}</span>`);
|
|
169
|
+
lines.push(`${indent(level + 1)}<span className="vc-text vc-text--field-help">{${JSON.stringify(helpLit)}}</span>`);
|
|
170
170
|
} else if (help) {
|
|
171
|
-
lines.push(`${indent(level + 1)}{${help} ? <span className="vc-field-help">{${help}}</span> : null}`);
|
|
171
|
+
lines.push(`${indent(level + 1)}{${help} ? <span className="vc-text vc-text--field-help">{${help}}</span> : null}`);
|
|
172
172
|
}
|
|
173
173
|
if (typeof errorLit === 'string') {
|
|
174
|
-
lines.push(`${indent(level + 1)}<span className="vc-field-error">{${JSON.stringify(errorLit)}}</span>`);
|
|
174
|
+
lines.push(`${indent(level + 1)}<span className="vc-text vc-text--field-error">{${JSON.stringify(errorLit)}}</span>`);
|
|
175
175
|
} else if (error) {
|
|
176
|
-
lines.push(`${indent(level + 1)}{${error} ? <span className="vc-field-error">{${error}}</span> : null}`);
|
|
176
|
+
lines.push(`${indent(level + 1)}{${error} ? <span className="vc-text vc-text--field-error">{${error}}</span> : null}`);
|
|
177
177
|
}
|
|
178
178
|
lines.push(`${indent(level)}</label>`);
|
|
179
179
|
return lines.join('\n');
|
package/renderers/react/paths.ts
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* DSL structural layer — layout/control mechanics only; no visual token values. */
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin: 0; background: var(--ui-bg); color: var(--ui-text); font-family: var(--ui-font); font-size: var(--ui-space-font-body); }
|
|
4
|
+
button, input, textarea, select { font: inherit; }
|
|
5
|
+
|
|
6
|
+
.vc-page { min-height: 100vh; min-width: 0; }
|
|
7
|
+
.vc-box { display: block; min-width: 0; }
|
|
8
|
+
.vc-column { display: flex; flex-direction: column; min-width: 0; }
|
|
9
|
+
.vc-stack { display: flex; flex-direction: column; min-width: 0; }
|
|
10
|
+
.vc-row { display: flex; flex-direction: row; min-width: 0; }
|
|
11
|
+
.vc-wrap { flex-wrap: wrap; }
|
|
12
|
+
.vc-spacer { min-width: 0; min-height: 0; }
|
|
13
|
+
|
|
14
|
+
.vc-heading { margin: 0; line-height: 1.2; letter-spacing: -0.02em; }
|
|
15
|
+
.vc-text { margin: 0; line-height: 1.6; }
|
|
16
|
+
.vc-link { color: var(--ui-primary); text-decoration: none; }
|
|
17
|
+
.vc-link:hover { text-decoration: underline; }
|
|
18
|
+
.vc-divider { border: 0; border-top: 1px solid var(--ui-border); width: 100%; }
|
|
19
|
+
|
|
20
|
+
.vc-button { border: 0; cursor: pointer; }
|
|
21
|
+
|
|
22
|
+
.vc-form { display: flex; flex-direction: column; min-width: 0; }
|
|
23
|
+
.vc-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
|
|
24
|
+
|
|
25
|
+
.vc-text-input, .vc-text-area, .vc-select { outline: none; }
|
|
26
|
+
.vc-text-input:focus, .vc-text-area:focus, .vc-select:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
|
|
27
|
+
.vc-select { appearance: none; -webkit-appearance: none; -moz-appearance: none; cursor: pointer; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; }
|
|
28
|
+
.vc-text-area { resize: vertical; }
|
|
29
|
+
|
|
30
|
+
.vc-checkbox { display: inline-flex; align-items: center; gap: 8px; }
|
|
31
|
+
.vc-list { margin: 0; padding-left: 20px; }
|
|
32
|
+
.vc-list-item { margin: 0; padding: 2px 0; }
|
|
33
|
+
|
|
34
|
+
.vc-table { width: 100%; display: flex; flex-direction: column; min-width: 0; }
|
|
35
|
+
.vc-table-header { display: flex; align-items: center; min-width: 0; }
|
|
36
|
+
.vc-table-column { display: flex; align-items: center; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
37
|
+
.vc-table-row { display: flex; align-items: center; border-top: 1px solid var(--ui-border); min-width: 0; }
|
|
38
|
+
.vc-table-cell { display: flex; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
@@ -39,13 +39,23 @@ export function foldLiteralsToCss(literals: Record<string, unknown>): FoldedCss
|
|
|
39
39
|
const style: FoldedCss = {};
|
|
40
40
|
const width = literals.width;
|
|
41
41
|
if (typeof width === 'number') style.width = len(width);
|
|
42
|
-
else if (
|
|
42
|
+
else if (width === 'fill') style.width = '100%';
|
|
43
|
+
else if (typeof width === 'string' && width !== 'wrap') style.width = width;
|
|
43
44
|
|
|
44
45
|
if (typeof literals.minWidth === 'number') style.minWidth = len(literals.minWidth);
|
|
45
|
-
|
|
46
|
-
if (typeof
|
|
46
|
+
const maxWidth = literals.maxWidth;
|
|
47
|
+
if (typeof maxWidth === 'number') style.maxWidth = len(maxWidth);
|
|
48
|
+
else if (maxWidth === 'fill') style.maxWidth = '100%';
|
|
49
|
+
|
|
50
|
+
const height = literals.height;
|
|
51
|
+
if (typeof height === 'number') style.height = len(height);
|
|
52
|
+
else if (height === 'fill') style.height = '100%';
|
|
53
|
+
else if (typeof height === 'string' && height !== 'wrap') style.height = height;
|
|
54
|
+
|
|
47
55
|
if (typeof literals.minHeight === 'number') style.minHeight = len(literals.minHeight);
|
|
48
|
-
|
|
56
|
+
const maxHeight = literals.maxHeight;
|
|
57
|
+
if (typeof maxHeight === 'number') style.maxHeight = len(maxHeight);
|
|
58
|
+
else if (maxHeight === 'fill') style.maxHeight = '100%';
|
|
49
59
|
if (literals.aspectRatio !== undefined) style.aspectRatio = String(literals.aspectRatio);
|
|
50
60
|
|
|
51
61
|
const weight = literals.weight;
|
|
@@ -28,6 +28,45 @@ function indent(level: number): string {
|
|
|
28
28
|
return ' '.repeat(level);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function tokenHexColorExpr(tokens: TokenIR, path: string, fallback: string): string {
|
|
32
|
+
const entry = tokens.entries.find((item) => item.path === path);
|
|
33
|
+
const value = entry?.value ?? fallback;
|
|
34
|
+
const match = value.match(/^#([0-9a-fA-F]{6})$/);
|
|
35
|
+
if (!match) return `Color(${JSON.stringify(value)})`;
|
|
36
|
+
const hex = match[1]!;
|
|
37
|
+
const r = parseInt(hex.slice(0, 2), 16) / 255;
|
|
38
|
+
const g = parseInt(hex.slice(2, 4), 16) / 255;
|
|
39
|
+
const b = parseInt(hex.slice(4, 6), 16) / 255;
|
|
40
|
+
return `Color(red: ${r}, green: ${g}, blue: ${b})`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function renderGridChildBlock(
|
|
44
|
+
children: IrNode[],
|
|
45
|
+
level: number,
|
|
46
|
+
renderNested: NodeRenderer,
|
|
47
|
+
ctx: SwiftUIRenderContext,
|
|
48
|
+
itemScope: string | undefined,
|
|
49
|
+
): string {
|
|
50
|
+
const childFrameMod = `${indent(level + 2)}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)`;
|
|
51
|
+
const parts = children.map((child) => {
|
|
52
|
+
if (child.kind === 'map') {
|
|
53
|
+
const mapNode = child as IrMapNode;
|
|
54
|
+
const item = mapNode.item;
|
|
55
|
+
let body = renderNested(mapNode.body, level + 2, item);
|
|
56
|
+
body += `\n${childFrameMod}`;
|
|
57
|
+
const source = bindingExpr(mapNode.source, ctx, itemScope);
|
|
58
|
+
return `${indent(level + 2)}ForEach(${source}, id: \\.id) { ${item} in\n${body}\n${indent(level + 2)}}`;
|
|
59
|
+
}
|
|
60
|
+
let rendered = '';
|
|
61
|
+
if (child.kind === 'text') rendered = `${indent(level + 2)}Text(${JSON.stringify((child as IrTextNode).value)})`;
|
|
62
|
+
else if (child.kind === 'binding') rendered = `${indent(level + 2)}Text(${bindingExpr(child as IrBinding, ctx, itemScope)})`;
|
|
63
|
+
else rendered = renderNested(child, level + 2, itemScope);
|
|
64
|
+
if (!rendered) return '';
|
|
65
|
+
return `${rendered}\n${childFrameMod}`;
|
|
66
|
+
}).filter(Boolean);
|
|
67
|
+
return parts.join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
31
70
|
function propLiteral(props: Record<string, IrValue>, key: string): unknown | undefined {
|
|
32
71
|
const value = props[key];
|
|
33
72
|
if (value === null || value === undefined) return undefined;
|
|
@@ -103,9 +142,11 @@ function mergedModifiersForProps(
|
|
|
103
142
|
instanceLiterals,
|
|
104
143
|
});
|
|
105
144
|
const instanceModifiers = visualPropSetToModifiers(instanceOnly);
|
|
145
|
+
const paddingMods = instanceModifiers.filter((mod) => mod.startsWith('.padding'));
|
|
146
|
+
const otherInstanceMods = instanceModifiers.filter((mod) => !mod.startsWith('.padding'));
|
|
106
147
|
|
|
107
|
-
//
|
|
108
|
-
return [runtimeLookup, ...
|
|
148
|
+
// Padding must sit inside background/border from applyVcStyle.
|
|
149
|
+
return [...paddingMods, runtimeLookup, ...otherInstanceMods];
|
|
109
150
|
}
|
|
110
151
|
|
|
111
152
|
const merged = mergeVisualProps({
|
|
@@ -118,6 +159,87 @@ function mergedModifiersForProps(
|
|
|
118
159
|
return visualPropSetToModifiers(merged);
|
|
119
160
|
}
|
|
120
161
|
|
|
162
|
+
function emitPickerSelection(
|
|
163
|
+
props: Record<string, IrValue>,
|
|
164
|
+
ctx: SwiftUIRenderContext,
|
|
165
|
+
itemScope: string | undefined,
|
|
166
|
+
): string {
|
|
167
|
+
const options = props.options;
|
|
168
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'literal' && Array.isArray((options as IrLiteral).value)) {
|
|
169
|
+
const items = (options as IrLiteral).value as Array<{ value: string }>;
|
|
170
|
+
return `.constant(${JSON.stringify(items[0]?.value ?? '')})`;
|
|
171
|
+
}
|
|
172
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'binding') {
|
|
173
|
+
const expr = bindingExpr(options as IrBinding, ctx, itemScope);
|
|
174
|
+
return `Binding(get: { ${expr}.first?.value ?? "" }, set: { _ in })`;
|
|
175
|
+
}
|
|
176
|
+
return '.constant("")';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function emitPickerOptions(
|
|
180
|
+
props: Record<string, IrValue>,
|
|
181
|
+
ctx: SwiftUIRenderContext,
|
|
182
|
+
itemScope: string | undefined,
|
|
183
|
+
level: number,
|
|
184
|
+
): string {
|
|
185
|
+
const options = props.options;
|
|
186
|
+
if (!options || typeof options !== 'object' || !('kind' in options)) {
|
|
187
|
+
return `${indent(level + 1)}Text("--").tag("")`;
|
|
188
|
+
}
|
|
189
|
+
if (options.kind === 'binding') {
|
|
190
|
+
const expr = bindingExpr(options as IrBinding, ctx, itemScope);
|
|
191
|
+
return `${indent(level + 1)}ForEach(${expr}, id: \\.value) { option in\n${indent(level + 2)}Text(option.label).tag(option.value)\n${indent(level + 1)}}`;
|
|
192
|
+
}
|
|
193
|
+
if (options.kind === 'literal' && Array.isArray((options as IrLiteral).value)) {
|
|
194
|
+
const items = (options as IrLiteral).value as Array<{ label: string; value: string }>;
|
|
195
|
+
return items.map((opt) =>
|
|
196
|
+
`${indent(level + 1)}Text(${JSON.stringify(opt.label)}).tag(${JSON.stringify(opt.value)})`,
|
|
197
|
+
).join('\n');
|
|
198
|
+
}
|
|
199
|
+
return `${indent(level + 1)}Text("--").tag("")`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function leafExtraModifiers(elementName: string): string[] {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function emitMenuOptions(
|
|
207
|
+
props: Record<string, IrValue>,
|
|
208
|
+
ctx: SwiftUIRenderContext,
|
|
209
|
+
itemScope: string | undefined,
|
|
210
|
+
level: number,
|
|
211
|
+
): string {
|
|
212
|
+
const options = props.options;
|
|
213
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'binding') {
|
|
214
|
+
const expr = bindingExpr(options as IrBinding, ctx, itemScope);
|
|
215
|
+
return `${indent(level + 1)}ForEach(${expr}, id: \\.value) { option in\n${indent(level + 2)}Button(option.label) {}\n${indent(level + 1)}}`;
|
|
216
|
+
}
|
|
217
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'literal' && Array.isArray((options as IrLiteral).value)) {
|
|
218
|
+
const items = (options as IrLiteral).value as Array<{ label: string; value: string }>;
|
|
219
|
+
return items.map((opt) =>
|
|
220
|
+
`${indent(level + 1)}Button(${JSON.stringify(opt.label)}) {}`,
|
|
221
|
+
).join('\n');
|
|
222
|
+
}
|
|
223
|
+
return `${indent(level + 1)}Button("--") {}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function emitSelectLabelText(
|
|
227
|
+
props: Record<string, IrValue>,
|
|
228
|
+
ctx: SwiftUIRenderContext,
|
|
229
|
+
itemScope: string | undefined,
|
|
230
|
+
): string {
|
|
231
|
+
const options = props.options;
|
|
232
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'binding') {
|
|
233
|
+
const expr = bindingExpr(options as IrBinding, ctx, itemScope);
|
|
234
|
+
return `${expr}.first?.label ?? ""`;
|
|
235
|
+
}
|
|
236
|
+
if (typeof options === 'object' && options !== null && 'kind' in options && options.kind === 'literal' && Array.isArray((options as IrLiteral).value)) {
|
|
237
|
+
const items = (options as IrLiteral).value as Array<{ label: string }>;
|
|
238
|
+
return JSON.stringify(items[0]?.label ?? '');
|
|
239
|
+
}
|
|
240
|
+
return '""';
|
|
241
|
+
}
|
|
242
|
+
|
|
121
243
|
function bindingExpr(binding: IrBinding, ctx: SwiftUIRenderContext, itemScope?: string): string {
|
|
122
244
|
const root = binding.scope ?? itemScope ?? ctx.propsRoot ?? 'vm';
|
|
123
245
|
return binding.path ? `${root}.${binding.path}` : root;
|
|
@@ -185,25 +307,84 @@ function renderLeaf(
|
|
|
185
307
|
|
|
186
308
|
if (entry.leaf === 'textfield') {
|
|
187
309
|
const placeholder = propLiteral(node.props, 'placeholder');
|
|
188
|
-
const name = propLiteral(node.props, 'name');
|
|
189
310
|
const ph = typeof placeholder === 'string' ? JSON.stringify(placeholder) : '""';
|
|
190
|
-
const
|
|
191
|
-
const line = `${pad}TextField(
|
|
192
|
-
const
|
|
311
|
+
const mutedColor = tokenHexColorExpr(ctx.tokens, 'ui.text-muted', '#6b7280');
|
|
312
|
+
const line = `${pad}TextField("", text: .constant(""), prompt: Text(${ph}).foregroundStyle(${mutedColor}))`;
|
|
313
|
+
const allMods = [...modifiers, ...leafExtraModifiers(node.name)];
|
|
314
|
+
const modLines = allMods.map((mod) => `${pad}${mod}`);
|
|
193
315
|
return [line, ...modLines].join('\n');
|
|
194
316
|
}
|
|
195
317
|
|
|
196
318
|
if (entry.leaf === 'texteditor') {
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
319
|
+
const placeholder = propLiteral(node.props, 'placeholder');
|
|
320
|
+
const ph = typeof placeholder === 'string' ? JSON.stringify(placeholder) : '""';
|
|
321
|
+
const mutedColor = tokenHexColorExpr(ctx.tokens, 'ui.text-muted', '#6b7280');
|
|
322
|
+
const minHeightMod = modifiers.find((mod) => mod.startsWith('.frame(minHeight:'));
|
|
323
|
+
const minHeight = minHeightMod?.match(/minHeight: (\d+)/)?.[1] ?? '72';
|
|
324
|
+
|
|
325
|
+
// Split modifiers: text-related go on TextEditor, visual/layout go on ZStack
|
|
326
|
+
const textMods = modifiers.filter((mod) =>
|
|
327
|
+
mod.startsWith('.foregroundStyle(') || mod.startsWith('.font(') || mod.startsWith('.fontWeight(')
|
|
328
|
+
);
|
|
329
|
+
const containerMods = modifiers.filter((mod) =>
|
|
330
|
+
!mod.startsWith('.foregroundStyle(') && !mod.startsWith('.font(') &&
|
|
331
|
+
!mod.startsWith('.fontWeight(') && !mod.startsWith('.frame(minHeight:')
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const textModLines = textMods.map((mod) => `${pad} ${mod}`);
|
|
335
|
+
const containerModLines = containerMods.map((mod) => `${pad}${mod}`);
|
|
336
|
+
return [
|
|
337
|
+
`${pad}ZStack(alignment: .topLeading) {`,
|
|
338
|
+
`${pad} if true {`,
|
|
339
|
+
`${pad} Text(${ph})`,
|
|
340
|
+
`${pad} .font(.system(size: 14))`,
|
|
341
|
+
`${pad} .foregroundStyle(${mutedColor})`,
|
|
342
|
+
`${pad} .padding(.top, 8)`,
|
|
343
|
+
`${pad} .padding(.horizontal, 4)`,
|
|
344
|
+
`${pad} .allowsHitTesting(false)`,
|
|
345
|
+
`${pad} }`,
|
|
346
|
+
`${pad} TextEditor(text: .constant(""))`,
|
|
347
|
+
`${pad} .scrollContentBackground(.hidden)`,
|
|
348
|
+
...textModLines,
|
|
349
|
+
`${pad}}`,
|
|
350
|
+
`${pad}.frame(minHeight: ${minHeight})`,
|
|
351
|
+
...containerModLines,
|
|
352
|
+
].join('\n');
|
|
200
353
|
}
|
|
201
354
|
|
|
202
355
|
if (entry.leaf === 'picker') {
|
|
203
|
-
const
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
|
|
356
|
+
const textColor = tokenHexColorExpr(ctx.tokens, 'ui.text', '#111827');
|
|
357
|
+
const mutedColor = tokenHexColorExpr(ctx.tokens, 'ui.text-muted', '#6b7280');
|
|
358
|
+
const optionsBlock = emitMenuOptions(node.props, ctx, itemScope, level);
|
|
359
|
+
const labelText = emitSelectLabelText(node.props, ctx, itemScope);
|
|
360
|
+
const labelMods = modifiers.filter((mod) => !mod.startsWith('.frame'));
|
|
361
|
+
const widthLit = propLiteral(node.props, 'width');
|
|
362
|
+
const widthMod = typeof widthLit === 'number'
|
|
363
|
+
? `${pad}.frame(width: ${widthLit})`
|
|
364
|
+
: `${pad}.frame(maxWidth: .infinity)`;
|
|
365
|
+
const lines = [
|
|
366
|
+
`${pad}Menu {`,
|
|
367
|
+
optionsBlock,
|
|
368
|
+
`${pad}} label: {`,
|
|
369
|
+
`${pad} HStack(spacing: 0) {`,
|
|
370
|
+
`${pad} Text(${labelText})`,
|
|
371
|
+
`${pad} .foregroundStyle(${textColor})`,
|
|
372
|
+
`${pad} .lineLimit(1)`,
|
|
373
|
+
`${pad} Spacer(minLength: 0)`,
|
|
374
|
+
`${pad} Image(systemName: "chevron.down")`,
|
|
375
|
+
`${pad} .font(.system(size: 12, weight: .semibold))`,
|
|
376
|
+
`${pad} .foregroundStyle(${mutedColor})`,
|
|
377
|
+
`${pad} }`,
|
|
378
|
+
`${pad} .padding(.vertical, 9)`,
|
|
379
|
+
`${pad} .padding(.leading, 12)`,
|
|
380
|
+
`${pad} .padding(.trailing, 12)`,
|
|
381
|
+
...labelMods.map((mod) => `${pad} ${mod}`),
|
|
382
|
+
`${pad}}`,
|
|
383
|
+
widthMod,
|
|
384
|
+
`${pad}.frame(minHeight: 42)`,
|
|
385
|
+
`${pad}.frame(maxWidth: .infinity)`,
|
|
386
|
+
];
|
|
387
|
+
return lines.join('\n');
|
|
207
388
|
}
|
|
208
389
|
|
|
209
390
|
if (entry.leaf === 'spacer') {
|
|
@@ -253,21 +434,23 @@ function renderContainer(
|
|
|
253
434
|
const spacingLit = entry.spacingProp ? propLiteral(node.props, entry.spacingProp) : undefined;
|
|
254
435
|
const spacing = typeof spacingLit === 'number' ? spacingLit : 0;
|
|
255
436
|
const inner = `${indent(level + 1)}VStack(alignment: .leading, spacing: ${spacing}) {\n${childBlock}\n${indent(level + 1)}}`;
|
|
256
|
-
return applyModifiers(inner,
|
|
437
|
+
return applyModifiers(inner, modifiers, level + 1);
|
|
257
438
|
}
|
|
258
439
|
|
|
259
440
|
if (entry.container === 'hstack') {
|
|
260
441
|
const spacingLit = entry.spacingProp ? propLiteral(node.props, entry.spacingProp) : undefined;
|
|
261
442
|
const spacing = typeof spacingLit === 'number' ? spacingLit : 0;
|
|
262
443
|
const alignLit = propLiteral(node.props, 'align');
|
|
263
|
-
const alignment = alignLit === 'center' ? '.center' : '.top';
|
|
264
444
|
const heightLit = propLiteral(node.props, 'height');
|
|
445
|
+
const alignment = alignLit === 'center' || typeof heightLit === 'number' ? '.center' : '.top';
|
|
265
446
|
const justifyLit = propLiteral(node.props, 'justify');
|
|
266
447
|
const flexLit = propLiteral(node.props, 'flex');
|
|
448
|
+
const wrapLit = propLiteral(node.props, 'wrap');
|
|
267
449
|
const isTableElement = node.name === 'TableHeader' || node.name === 'TableRow';
|
|
268
450
|
const shouldFill = isTableElement || justifyLit !== undefined || typeof flexLit === 'number';
|
|
451
|
+
const hStackFrameAlign = justifyLit === 'end' ? '.trailing' : justifyLit === 'center' ? '.center' : '.leading';
|
|
269
452
|
const frameMods: string[] = [];
|
|
270
|
-
if (shouldFill) frameMods.push(
|
|
453
|
+
if (shouldFill) frameMods.push(`.frame(maxWidth: .infinity, alignment: ${hStackFrameAlign})`);
|
|
271
454
|
if (typeof heightLit === 'number') frameMods.push(`.frame(height: ${heightLit})`);
|
|
272
455
|
|
|
273
456
|
let block = childBlock;
|
|
@@ -283,6 +466,19 @@ function renderContainer(
|
|
|
283
466
|
const rest = childLines.slice(1);
|
|
284
467
|
block = [first, `${indent(level + 2)}Spacer()`, ...rest].join('\n');
|
|
285
468
|
}
|
|
469
|
+
} else if (wrapLit === true) {
|
|
470
|
+
block = node.children.map((child) => {
|
|
471
|
+
if (child.kind === 'map') return renderMap(child, level + 2, renderNested, ctx);
|
|
472
|
+
if (child.kind === 'text') return `${indent(level + 2)}Text(${JSON.stringify((child as IrTextNode).value)})`;
|
|
473
|
+
if (child.kind === 'binding') return `${indent(level + 2)}Text(${bindingExpr(child as IrBinding, ctx, itemScope)})`;
|
|
474
|
+
const rendered = renderNested(child, level + 2, itemScope);
|
|
475
|
+
if (!rendered) return '';
|
|
476
|
+
return rendered;
|
|
477
|
+
}).filter(Boolean).join('\n');
|
|
478
|
+
|
|
479
|
+
// Use FlowLayout (custom Layout protocol) for wrapping
|
|
480
|
+
const inner = `${indent(level + 1)}FlowLayout(spacing: ${spacing}) {\n${block}\n${indent(level + 1)}}`;
|
|
481
|
+
return applyModifiers(inner, [...modifiers, ...frameMods], level + 1);
|
|
286
482
|
}
|
|
287
483
|
|
|
288
484
|
const inner = `${indent(level + 1)}HStack(alignment: ${alignment}, spacing: ${spacing}) {\n${block}\n${indent(level + 1)}}`;
|
|
@@ -293,12 +489,12 @@ function renderContainer(
|
|
|
293
489
|
const inner = childBlock
|
|
294
490
|
? `${indent(level + 1)}VStack(alignment: .leading, spacing: 0) {\n${childBlock}\n${indent(level + 1)}}`
|
|
295
491
|
: `${indent(level + 1)}VStack { EmptyView() }`;
|
|
296
|
-
return applyModifiers(inner,
|
|
492
|
+
return applyModifiers(inner, modifiers, level + 1);
|
|
297
493
|
}
|
|
298
494
|
|
|
299
495
|
if (entry.container === 'navstack') {
|
|
300
496
|
const inner = childBlock;
|
|
301
|
-
return applyModifiers(inner,
|
|
497
|
+
return applyModifiers(inner, modifiers, level + 1);
|
|
302
498
|
}
|
|
303
499
|
|
|
304
500
|
if (entry.container === 'scrollview') {
|
|
@@ -308,8 +504,12 @@ function renderContainer(
|
|
|
308
504
|
|
|
309
505
|
if (entry.container === 'field') {
|
|
310
506
|
const label = propLiteral(node.props, 'label');
|
|
311
|
-
const
|
|
312
|
-
const
|
|
507
|
+
const gapLit = propLiteral(node.props, 'gap');
|
|
508
|
+
const gap = typeof gapLit === 'number' ? gapLit : 4;
|
|
509
|
+
const labelView = typeof label === 'string'
|
|
510
|
+
? `\n${indent(level + 2)}Text(${JSON.stringify(label)})\n${indent(level + 2)} .applyVcStyle(VcTheme.resolve("Text", variant: "field-label"))`
|
|
511
|
+
: '';
|
|
512
|
+
const inner = `${indent(level + 1)}VStack(alignment: .leading, spacing: ${gap}) {${labelView}\n${childBlock}\n${indent(level + 1)}}`;
|
|
313
513
|
return applyModifiers(inner, modifiers, level + 1);
|
|
314
514
|
}
|
|
315
515
|
|
|
@@ -349,7 +549,7 @@ function renderContainer(
|
|
|
349
549
|
}
|
|
350
550
|
|
|
351
551
|
const inner = `${indent(level + 1)}VStack(alignment: .leading, spacing: 0) {\n${body.join('\n')}\n${indent(level + 1)}}`;
|
|
352
|
-
return applyModifiers(inner,
|
|
552
|
+
return applyModifiers(inner, modifiers, level + 1);
|
|
353
553
|
}
|
|
354
554
|
|
|
355
555
|
if (entry.container === 'cellgroup') {
|
|
@@ -358,15 +558,15 @@ function renderContainer(
|
|
|
358
558
|
const alignLit = propLiteral(node.props, 'align');
|
|
359
559
|
const swiftAlign = alignLit === 'end' ? '.trailing' : alignLit === 'center' ? '.center' : '.leading';
|
|
360
560
|
const isFlex = typeof flexLit === 'number';
|
|
361
|
-
const cellMods = [...modifiers, '.
|
|
561
|
+
const cellMods = [...modifiers, '.lineLimit(1)'];
|
|
362
562
|
if (typeof widthLit === 'number') {
|
|
363
563
|
cellMods.push(`.frame(width: ${widthLit}, alignment: ${swiftAlign})`);
|
|
364
564
|
} else if (isFlex) {
|
|
365
565
|
cellMods.push(`.frame(maxWidth: .infinity, alignment: ${swiftAlign})`);
|
|
366
566
|
}
|
|
367
567
|
const inner = childBlock
|
|
368
|
-
? `${indent(level + 1)}
|
|
369
|
-
: `${indent(level + 1)}
|
|
568
|
+
? `${indent(level + 1)}Group {\n${childBlock}\n${indent(level + 2)}.frame(maxWidth: .infinity, alignment: ${swiftAlign})\n${indent(level + 1)}}`
|
|
569
|
+
: `${indent(level + 1)}Group { EmptyView() }`;
|
|
370
570
|
return applyModifiers(inner, cellMods, level + 1);
|
|
371
571
|
}
|
|
372
572
|
|
|
@@ -393,8 +593,10 @@ function resolvedVisualProps(
|
|
|
393
593
|
});
|
|
394
594
|
}
|
|
395
595
|
|
|
396
|
-
function parseGridColumns(gridTemplateColumns: string | number | boolean | undefined): { kind: 'fixed-columns'; widths: string[] } | { kind: 'adaptive'; minWidth: number } | null {
|
|
596
|
+
function parseGridColumns(gridTemplateColumns: string | number | boolean | undefined): { kind: 'fixed-columns'; widths: string[] } | { kind: 'adaptive'; minWidth: number } | { kind: 'equal-columns'; count: number } | null {
|
|
397
597
|
if (!gridTemplateColumns || typeof gridTemplateColumns !== 'string') return null;
|
|
598
|
+
const equalRepeatMatch = gridTemplateColumns.match(/repeat\((\d+),\s*minmax\(0,\s*1fr\)\)/);
|
|
599
|
+
if (equalRepeatMatch) return { kind: 'equal-columns', count: Number(equalRepeatMatch[1]) };
|
|
398
600
|
const adaptiveMatch = gridTemplateColumns.match(/repeat\(auto-fit,\s*minmax\((\d+)px/);
|
|
399
601
|
if (adaptiveMatch) return { kind: 'adaptive', minWidth: Number(adaptiveMatch[1]) };
|
|
400
602
|
const parts = gridTemplateColumns.trim().split(/\s+/);
|
|
@@ -427,6 +629,9 @@ function renderComponent(
|
|
|
427
629
|
if (parsed?.kind === 'adaptive') {
|
|
428
630
|
return renderGridAdaptive(node, level, itemScope, renderNested, ctx, modifiers, parsed.minWidth);
|
|
429
631
|
}
|
|
632
|
+
if (parsed?.kind === 'equal-columns') {
|
|
633
|
+
return renderGridEqualColumns(node, level, itemScope, renderNested, ctx, modifiers, parsed.count);
|
|
634
|
+
}
|
|
430
635
|
if (parsed?.kind === 'fixed-columns') {
|
|
431
636
|
return renderGridFixedColumns(node, level, itemScope, renderNested, ctx, modifiers, parsed.widths);
|
|
432
637
|
}
|
|
@@ -437,7 +642,7 @@ function renderComponent(
|
|
|
437
642
|
const gapLit = propLiteral(node.props, 'gap');
|
|
438
643
|
const gap = typeof gapLit === 'number' ? gapLit : (typeof resolved.gap === 'number' ? resolved.gap : 0);
|
|
439
644
|
const inner = `${indent(level + 1)}HStack(alignment: .top, spacing: ${gap}) {\n${childBlock}\n${indent(level + 1)}}`;
|
|
440
|
-
return applyModifiers(inner,
|
|
645
|
+
return applyModifiers(inner, modifiers, level + 1);
|
|
441
646
|
}
|
|
442
647
|
}
|
|
443
648
|
|
|
@@ -454,9 +659,21 @@ function renderGridAdaptive(
|
|
|
454
659
|
): string {
|
|
455
660
|
const gapLit = propLiteral(node.props, 'gap');
|
|
456
661
|
const gap = typeof gapLit === 'number' ? gapLit : 16;
|
|
457
|
-
const childBlock =
|
|
458
|
-
const inner = `${indent(level + 1)}LazyVGrid(columns: [GridItem(.adaptive(minimum: ${minWidth}))], spacing: ${gap}) {\n${childBlock}\n${indent(level + 1)}}`;
|
|
459
|
-
return applyModifiers(inner,
|
|
662
|
+
const childBlock = renderGridChildBlock(node.children, level, renderNested, ctx, itemScope);
|
|
663
|
+
const inner = `${indent(level + 1)}LazyVGrid(columns: [GridItem(.adaptive(minimum: ${minWidth}))], spacing: ${gap}) {\n${childBlock}\n${indent(level + 1)}}\n${indent(level + 1)}.frame(maxWidth: .infinity, alignment: .topLeading)`;
|
|
664
|
+
return applyModifiers(inner, modifiers, level + 1);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function renderGridEqualColumns(
|
|
668
|
+
node: IrComponentNode, level: number, itemScope: string | undefined,
|
|
669
|
+
renderNested: NodeRenderer, ctx: SwiftUIRenderContext, modifiers: string[], count: number,
|
|
670
|
+
): string {
|
|
671
|
+
const gapLit = propLiteral(node.props, 'gap');
|
|
672
|
+
const gap = typeof gapLit === 'number' ? gapLit : 16;
|
|
673
|
+
const columns = `Array(repeating: GridItem(.flexible(), spacing: ${gap}), count: ${count})`;
|
|
674
|
+
const childBlock = renderGridChildBlock(node.children, level, renderNested, ctx, itemScope);
|
|
675
|
+
const inner = `${indent(level + 1)}LazyVGrid(columns: ${columns}, spacing: ${gap}) {\n${childBlock}\n${indent(level + 1)}}\n${indent(level + 1)}.frame(maxWidth: .infinity, alignment: .topLeading)`;
|
|
676
|
+
return applyModifiers(inner, modifiers, level + 1);
|
|
460
677
|
}
|
|
461
678
|
|
|
462
679
|
function resolveBackgroundColor(child: IrNode, ctx: SwiftUIRenderContext): string | null {
|
|
@@ -492,10 +709,12 @@ function renderGridFixedColumns(
|
|
|
492
709
|
const pxMatch = w?.match(/^(\d+)px$/);
|
|
493
710
|
if (pxMatch) {
|
|
494
711
|
rendered += `\n${indent(level + 2)}.frame(width: ${Number(pxMatch[1])}, alignment: .topLeading)`;
|
|
495
|
-
rendered += `\n${indent(level + 2)}.frame(maxHeight: .infinity)`;
|
|
712
|
+
rendered += `\n${indent(level + 2)}.frame(maxHeight: .infinity, alignment: .topLeading)`;
|
|
496
713
|
const bgColor = resolveBackgroundColor(child, ctx);
|
|
497
714
|
if (bgColor) rendered += `\n${indent(level + 2)}.background(${bgColor})`;
|
|
498
715
|
} else if (w === '1fr') {
|
|
716
|
+
// Content inside ScrollView must explicitly fill width
|
|
717
|
+
rendered += `\n${indent(level + 3)}.frame(maxWidth: .infinity, alignment: .topLeading)`;
|
|
499
718
|
rendered = `${indent(level + 2)}ScrollView {\n${rendered}\n${indent(level + 2)}}`;
|
|
500
719
|
rendered += `\n${indent(level + 2)}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)`;
|
|
501
720
|
}
|
|
@@ -503,7 +722,7 @@ function renderGridFixedColumns(
|
|
|
503
722
|
}
|
|
504
723
|
const childBlock = parts.join('\n');
|
|
505
724
|
const inner = `${indent(level + 1)}HStack(spacing: 0) {\n${childBlock}\n${indent(level + 1)}}`;
|
|
506
|
-
return applyModifiers(inner, [...modifiers, '.frame(maxWidth: .infinity, maxHeight: .infinity)'], level + 1);
|
|
725
|
+
return applyModifiers(inner, [...modifiers, '.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)'], level + 1);
|
|
507
726
|
}
|
|
508
727
|
|
|
509
728
|
function renderViewRef(node: IrViewRefNode, level: number, itemScope: string | undefined, ctx: SwiftUIRenderContext): string {
|