view-contracts 0.5.1 → 0.5.3
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 +19 -0
- package/cli-contract.yaml +1 -1
- package/dist/preview.mjs +167 -167
- package/dist/preview.mjs.map +3 -3
- package/dist/view-contracts.bundle.mjs +180 -180
- package/dist/view-contracts.bundle.mjs.map +3 -3
- package/docs/cli-reference.md +1 -1
- package/package.json +1 -1
- package/renderers/compose/lowering/lowerToCompose.ts +18 -4
- package/renderers/react/lowering/lowerToJsx.ts +50 -7
- package/renderers/swiftui/lowering/lowerToSwiftUI.ts +30 -2
package/docs/cli-reference.md
CHANGED
package/package.json
CHANGED
|
@@ -343,6 +343,18 @@ function bindingExpr(binding: IrBinding, ctx: ComposeRenderContext, itemScope?:
|
|
|
343
343
|
return binding.path ? `${root}.${binding.path}` : root;
|
|
344
344
|
}
|
|
345
345
|
|
|
346
|
+
function resolveComposeValue(props: Record<string, IrValue>, itemScope?: string): string {
|
|
347
|
+
const valueRaw = props.value;
|
|
348
|
+
if (!valueRaw) return '""';
|
|
349
|
+
if (typeof valueRaw === 'string') return JSON.stringify(valueRaw);
|
|
350
|
+
if (typeof valueRaw === 'object' && valueRaw !== null && 'kind' in valueRaw && valueRaw.kind === 'binding') {
|
|
351
|
+
const binding = valueRaw as { kind: 'binding'; path: string; scope?: string };
|
|
352
|
+
const root = binding.scope ?? (itemScope ?? 'vm');
|
|
353
|
+
return binding.path ? `${root}.${binding.path}` : root;
|
|
354
|
+
}
|
|
355
|
+
return '""';
|
|
356
|
+
}
|
|
357
|
+
|
|
346
358
|
function wrapWithInheritedStyles(childBlock: string, modifiers: string[], level: number): string {
|
|
347
359
|
const contentColor = extractContentColor(modifiers);
|
|
348
360
|
const inheritParts = extractInheritedTextStyleParts(modifiers);
|
|
@@ -674,15 +686,16 @@ function renderLeaf(
|
|
|
674
686
|
const textStyleLine = textStyleParts.length > 0
|
|
675
687
|
? `${pad} textStyle = TextStyle(${textStyleParts.join(', ')}),`
|
|
676
688
|
: `${pad} textStyle = TextStyle(color = ${textColor}, fontSize = 14.sp),`;
|
|
689
|
+
const composeVal = resolveComposeValue(node.props, itemScope);
|
|
677
690
|
return [
|
|
678
691
|
`${pad}BasicTextField(`,
|
|
679
|
-
`${pad} value =
|
|
692
|
+
`${pad} value = ${composeVal},`,
|
|
680
693
|
`${pad} onValueChange = {},`,
|
|
681
694
|
`${pad} modifier = ${modifierExpr},`,
|
|
682
695
|
textStyleLine,
|
|
683
696
|
`${pad} decorationBox = { innerTextField ->`,
|
|
684
697
|
`${pad} Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart) {`,
|
|
685
|
-
`${pad} if (
|
|
698
|
+
`${pad} if (${composeVal}.isEmpty()) {`,
|
|
686
699
|
`${pad} Text(text = ${ph}, style = TextStyle(color = ${mutedColor}, fontSize = 14.sp))`,
|
|
687
700
|
`${pad} }`,
|
|
688
701
|
`${pad} innerTextField()`,
|
|
@@ -704,15 +717,16 @@ function renderLeaf(
|
|
|
704
717
|
const containerExpr = applyModifiers('Modifier', dedupeModifiers(containerMods));
|
|
705
718
|
const textStyleParts = extractTextStyleModifiers(textMods);
|
|
706
719
|
const textStyleSuffix = textStyleParts.length > 0 ? `, textStyle = TextStyle(${textStyleParts.join(', ')})` : '';
|
|
720
|
+
const composeVal = resolveComposeValue(node.props, itemScope);
|
|
707
721
|
return [
|
|
708
722
|
`${pad}Box(modifier = ${containerExpr}.heightIn(min = ${minHeight}.dp)) {`,
|
|
709
723
|
`${pad} BasicTextField(`,
|
|
710
|
-
`${pad} value =
|
|
724
|
+
`${pad} value = ${composeVal},`,
|
|
711
725
|
`${pad} onValueChange = {},`,
|
|
712
726
|
`${pad} modifier = Modifier.fillMaxWidth()${textStyleSuffix},`,
|
|
713
727
|
`${pad} decorationBox = { innerTextField ->`,
|
|
714
728
|
`${pad} Box(modifier = Modifier.fillMaxWidth()) {`,
|
|
715
|
-
`${pad} if (
|
|
729
|
+
`${pad} if (${composeVal}.isEmpty()) {`,
|
|
716
730
|
`${pad} Text(text = ${ph}, modifier = Modifier.align(Alignment.TopStart).padding(top = 8.dp, start = 4.dp), style = TextStyle(color = ${mutedColor}, fontSize = 14.sp))`,
|
|
717
731
|
`${pad} }`,
|
|
718
732
|
`${pad} innerTextField()`,
|
|
@@ -109,6 +109,21 @@ function actionHandler(props: Record<string, IrValue>, event: 'onClick' | 'onSub
|
|
|
109
109
|
return `onClick={() => { void dispatchAction(${expr}); }}`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
function onChangeHandler(props: Record<string, IrValue>, targetProp = 'e.target.value'): string | undefined {
|
|
113
|
+
const onChange = props.onChange;
|
|
114
|
+
if (!onChange || typeof onChange !== 'object' || !('kind' in onChange) || onChange.kind !== 'action') {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const action = onChange as IrAction;
|
|
118
|
+
if (!action.params || Object.keys(action.params).length === 0) {
|
|
119
|
+
return `onChange={(e) => { void dispatchAction({ name: ${JSON.stringify(action.name)}, params: { value: ${targetProp} } } as ActionDescriptor); }}`;
|
|
120
|
+
}
|
|
121
|
+
const existingParams = Object.entries(action.params)
|
|
122
|
+
.map(([key, val]) => `${key}: ${renderValue(val, 0)}`)
|
|
123
|
+
.join(', ');
|
|
124
|
+
return `onChange={(e) => { void dispatchAction({ name: ${JSON.stringify(action.name)}, params: { ${existingParams}, value: ${targetProp} } } as ActionDescriptor); }}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
112
127
|
function componentToKebab(name: string): string {
|
|
113
128
|
return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
114
129
|
}
|
|
@@ -182,10 +197,13 @@ function emitFieldWrap(inner: string, props: Record<string, IrValue>, level: num
|
|
|
182
197
|
function emitCheckbox(props: Record<string, IrValue>, level: number): string {
|
|
183
198
|
const name = propExpr(props, 'name') ?? '""';
|
|
184
199
|
const label = propExpr(props, 'label') ?? '""';
|
|
185
|
-
const checked = props.checked ? `defaultChecked={${propExpr(props, 'checked')}}` : '';
|
|
186
200
|
const disabled = props.enabled ? `disabled={${propExpr(props, 'enabled')} === false}` : '';
|
|
201
|
+
const changeAttr = onChangeHandler(props, 'e.target.checked') ?? '';
|
|
202
|
+
const checkedAttr = props.checked
|
|
203
|
+
? (changeAttr ? `checked={${propExpr(props, 'checked')}}` : `defaultChecked={${propExpr(props, 'checked')}}`)
|
|
204
|
+
: '';
|
|
187
205
|
return `${indent(level)}<label className="vc-checkbox">
|
|
188
|
-
${indent(level + 1)}<input type="checkbox" name={${name}} ${
|
|
206
|
+
${indent(level + 1)}<input type="checkbox" name={${name}} ${checkedAttr} ${disabled} ${changeAttr} />
|
|
189
207
|
${indent(level + 1)}<span>{${label}}</span>
|
|
190
208
|
${indent(level)}</label>`;
|
|
191
209
|
}
|
|
@@ -194,10 +212,13 @@ function emitRadio(props: Record<string, IrValue>, level: number): string {
|
|
|
194
212
|
const name = propExpr(props, 'binding') ?? propExpr(props, 'name') ?? '""';
|
|
195
213
|
const value = propExpr(props, 'value') ?? '""';
|
|
196
214
|
const label = propExpr(props, 'label') ?? '""';
|
|
197
|
-
const checked = props.checked ? `defaultChecked={${propExpr(props, 'checked')}}` : '';
|
|
198
215
|
const disabled = props.enabled ? `disabled={${propExpr(props, 'enabled')} === false}` : '';
|
|
216
|
+
const changeAttr = onChangeHandler(props, 'e.target.value') ?? '';
|
|
217
|
+
const checkedAttr = props.checked
|
|
218
|
+
? (changeAttr ? `checked={${propExpr(props, 'checked')}}` : `defaultChecked={${propExpr(props, 'checked')}}`)
|
|
219
|
+
: '';
|
|
199
220
|
return `${indent(level)}<label className="vc-radio">
|
|
200
|
-
${indent(level + 1)}<input type="radio" name={${name}} value={${value}} ${
|
|
221
|
+
${indent(level + 1)}<input type="radio" name={${name}} value={${value}} ${checkedAttr} ${disabled} ${changeAttr} />
|
|
201
222
|
${indent(level + 1)}<span>{${label}}</span>
|
|
202
223
|
${indent(level)}</label>`;
|
|
203
224
|
}
|
|
@@ -205,10 +226,13 @@ ${indent(level)}</label>`;
|
|
|
205
226
|
function emitSwitch(props: Record<string, IrValue>, level: number): string {
|
|
206
227
|
const name = propExpr(props, 'binding') ?? propExpr(props, 'name') ?? '""';
|
|
207
228
|
const label = propExpr(props, 'label') ?? '""';
|
|
208
|
-
const checked = props.checked ? `defaultChecked={${propExpr(props, 'checked')}}` : '';
|
|
209
229
|
const disabled = props.enabled ? `disabled={${propExpr(props, 'enabled')} === false}` : '';
|
|
230
|
+
const changeAttr = onChangeHandler(props, 'e.target.checked') ?? '';
|
|
231
|
+
const checkedAttr = props.checked
|
|
232
|
+
? (changeAttr ? `checked={${propExpr(props, 'checked')}}` : `defaultChecked={${propExpr(props, 'checked')}}`)
|
|
233
|
+
: '';
|
|
210
234
|
return `${indent(level)}<label className="vc-switch">
|
|
211
|
-
${indent(level + 1)}<input type="checkbox" role="switch" name={${name}} ${
|
|
235
|
+
${indent(level + 1)}<input type="checkbox" role="switch" name={${name}} ${checkedAttr} ${disabled} ${changeAttr} />
|
|
212
236
|
${indent(level + 1)}<span>{${label}}</span>
|
|
213
237
|
${indent(level)}</label>`;
|
|
214
238
|
}
|
|
@@ -267,7 +291,7 @@ function emitVoidInput(
|
|
|
267
291
|
}
|
|
268
292
|
if (props.name) attrs.push(`name={${propExpr(props, 'name')}}`);
|
|
269
293
|
if (props.placeholder) attrs.push(`placeholder={${propExpr(props, 'placeholder')}}`);
|
|
270
|
-
if (entry.inputType) attrs.push(`type={${propExpr(props, '
|
|
294
|
+
if (entry.inputType) attrs.push(`type={${propExpr(props, 'inputType') ?? JSON.stringify(entry.inputType)}}`);
|
|
271
295
|
if (props.required) attrs.push(`required={${propExpr(props, 'required')}}`);
|
|
272
296
|
if (props.readonly) attrs.push(`readOnly={${propExpr(props, 'readonly')}}`);
|
|
273
297
|
if (props.enabled) attrs.push(`disabled={${propExpr(props, 'enabled')} === false}`);
|
|
@@ -275,6 +299,13 @@ function emitVoidInput(
|
|
|
275
299
|
if (entry.tag === 'img' && props.alt) attrs.push(`alt={${propExpr(props, 'alt')}}`);
|
|
276
300
|
if (entry.tag === 'progress' && props.value) attrs.push(`value={${propExpr(props, 'value')}}`);
|
|
277
301
|
if (props.rows) attrs.push(`rows={${propExpr(props, 'rows') ?? '4'}}`);
|
|
302
|
+
const changeHandler = onChangeHandler(props);
|
|
303
|
+
if (changeHandler) {
|
|
304
|
+
if (props.value && entry.tag !== 'progress') attrs.push(`value={${propExpr(props, 'value')}}`);
|
|
305
|
+
attrs.push(changeHandler);
|
|
306
|
+
} else if (props.value && entry.tag !== 'progress') {
|
|
307
|
+
attrs.push(`defaultValue={${propExpr(props, 'value')}}`);
|
|
308
|
+
}
|
|
278
309
|
return `${indent(level)}<${entry.tag} ${attrs.join(' ')} />`;
|
|
279
310
|
}
|
|
280
311
|
|
|
@@ -298,6 +329,18 @@ function emitOpenTag(
|
|
|
298
329
|
const handler = actionHandler(props, entry.actionEvent);
|
|
299
330
|
if (handler) attrs.push(handler);
|
|
300
331
|
}
|
|
332
|
+
if (props.name) attrs.push(`name={${propExpr(props, 'name')}}`);
|
|
333
|
+
if (entry.tag === 'select' || entry.tag === 'textarea' || entry.tag === 'input') {
|
|
334
|
+
if (props.required) attrs.push(`required={${propExpr(props, 'required')}}`);
|
|
335
|
+
if (props.enabled) attrs.push(`disabled={${propExpr(props, 'enabled')} === false}`);
|
|
336
|
+
const changeHandler = onChangeHandler(props);
|
|
337
|
+
if (changeHandler) {
|
|
338
|
+
if (props.value) attrs.push(`value={${propExpr(props, 'value')}}`);
|
|
339
|
+
attrs.push(changeHandler);
|
|
340
|
+
} else if (props.value) {
|
|
341
|
+
attrs.push(`defaultValue={${propExpr(props, 'value')}}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
301
344
|
if (entry.tag === 'button' && props.enabled) attrs.push(`disabled={${propExpr(props, 'enabled')} === false}`);
|
|
302
345
|
if (entry.tag === 'a' && props.href) attrs.push(`href={${propExpr(props, 'href')}}`);
|
|
303
346
|
if (entry.tag === 'button') attrs.push(`type={${propExpr(props, 'type') ?? '"button"'}}`);
|
|
@@ -245,6 +245,25 @@ function bindingExpr(binding: IrBinding, ctx: SwiftUIRenderContext, itemScope?:
|
|
|
245
245
|
return binding.path ? `${root}.${binding.path}` : root;
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
function resolveValueBinding(props: Record<string, IrValue>, ctx: SwiftUIRenderContext, itemScope?: string): { valueExpr: string; hasOnChange: boolean } {
|
|
249
|
+
const vmRef = itemScope ?? ctx.propsRoot ?? 'vm';
|
|
250
|
+
const valueRaw = props.value;
|
|
251
|
+
const onChange = props.onChange;
|
|
252
|
+
|
|
253
|
+
const hasOnChange = onChange !== undefined && typeof onChange === 'object' && onChange !== null && 'kind' in onChange && onChange.kind === 'action';
|
|
254
|
+
|
|
255
|
+
let valueExpr: string;
|
|
256
|
+
if (valueRaw && typeof valueRaw === 'object' && 'kind' in valueRaw && valueRaw.kind === 'binding') {
|
|
257
|
+
valueExpr = bindingExpr(valueRaw as IrBinding, ctx, itemScope);
|
|
258
|
+
} else if (typeof valueRaw === 'string') {
|
|
259
|
+
valueExpr = JSON.stringify(valueRaw);
|
|
260
|
+
} else {
|
|
261
|
+
valueExpr = '""';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return { valueExpr, hasOnChange };
|
|
265
|
+
}
|
|
266
|
+
|
|
248
267
|
function renderTextContent(children: IrNode[], ctx: SwiftUIRenderContext, itemScope?: string): string | null {
|
|
249
268
|
for (const child of children) {
|
|
250
269
|
if (child.kind === 'text') return JSON.stringify((child as IrTextNode).value);
|
|
@@ -309,7 +328,11 @@ function renderLeaf(
|
|
|
309
328
|
const placeholder = propLiteral(node.props, 'placeholder');
|
|
310
329
|
const ph = typeof placeholder === 'string' ? JSON.stringify(placeholder) : '""';
|
|
311
330
|
const mutedColor = tokenHexColorExpr(ctx.tokens, 'ui.text-muted', '#6b7280');
|
|
312
|
-
const
|
|
331
|
+
const { valueExpr, hasOnChange } = resolveValueBinding(node.props, ctx, itemScope);
|
|
332
|
+
const textBinding = hasOnChange
|
|
333
|
+
? `.init(get: { ${valueExpr} }, set: { _ in })`
|
|
334
|
+
: `.constant(${valueExpr})`;
|
|
335
|
+
const line = `${pad}TextField("", text: ${textBinding}, prompt: Text(${ph}).foregroundStyle(${mutedColor}))`;
|
|
313
336
|
const allMods = [...modifiers, ...leafExtraModifiers(node.name)];
|
|
314
337
|
const modLines = allMods.map((mod) => `${pad}${mod}`);
|
|
315
338
|
return [line, ...modLines].join('\n');
|
|
@@ -322,6 +345,11 @@ function renderLeaf(
|
|
|
322
345
|
const minHeightMod = modifiers.find((mod) => mod.startsWith('.frame(minHeight:'));
|
|
323
346
|
const minHeight = minHeightMod?.match(/minHeight: (\d+)/)?.[1] ?? '72';
|
|
324
347
|
|
|
348
|
+
const { valueExpr: teValueExpr, hasOnChange: teHasOnChange } = resolveValueBinding(node.props, ctx, itemScope);
|
|
349
|
+
const teTextBinding = teHasOnChange
|
|
350
|
+
? `.init(get: { ${teValueExpr} }, set: { _ in })`
|
|
351
|
+
: `.constant(${teValueExpr})`;
|
|
352
|
+
|
|
325
353
|
// Split modifiers: text-related go on TextEditor, visual/layout go on ZStack
|
|
326
354
|
const textMods = modifiers.filter((mod) =>
|
|
327
355
|
mod.startsWith('.foregroundStyle(') || mod.startsWith('.font(') || mod.startsWith('.fontWeight(')
|
|
@@ -343,7 +371,7 @@ function renderLeaf(
|
|
|
343
371
|
`${pad} .padding(.horizontal, 4)`,
|
|
344
372
|
`${pad} .allowsHitTesting(false)`,
|
|
345
373
|
`${pad} }`,
|
|
346
|
-
`${pad} TextEditor(text:
|
|
374
|
+
`${pad} TextEditor(text: ${teTextBinding})`,
|
|
347
375
|
`${pad} .scrollContentBackground(.hidden)`,
|
|
348
376
|
...textModLines,
|
|
349
377
|
`${pad}}`,
|