view-contracts 0.5.1 → 0.5.2
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/cli-contract.yaml +1 -1
- package/dist/preview.mjs +158 -158
- package/dist/preview.mjs.map +3 -3
- package/dist/view-contracts.bundle.mjs +158 -158
- 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 +31 -0
- 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>): 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: e.target.value } } 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: e.target.value } } 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
|
}
|
|
@@ -275,6 +290,13 @@ function emitVoidInput(
|
|
|
275
290
|
if (entry.tag === 'img' && props.alt) attrs.push(`alt={${propExpr(props, 'alt')}}`);
|
|
276
291
|
if (entry.tag === 'progress' && props.value) attrs.push(`value={${propExpr(props, 'value')}}`);
|
|
277
292
|
if (props.rows) attrs.push(`rows={${propExpr(props, 'rows') ?? '4'}}`);
|
|
293
|
+
const changeHandler = onChangeHandler(props);
|
|
294
|
+
if (changeHandler) {
|
|
295
|
+
if (props.value && entry.tag !== 'progress') attrs.push(`value={${propExpr(props, 'value')}}`);
|
|
296
|
+
attrs.push(changeHandler);
|
|
297
|
+
} else if (props.value && entry.tag !== 'progress') {
|
|
298
|
+
attrs.push(`defaultValue={${propExpr(props, 'value')}}`);
|
|
299
|
+
}
|
|
278
300
|
return `${indent(level)}<${entry.tag} ${attrs.join(' ')} />`;
|
|
279
301
|
}
|
|
280
302
|
|
|
@@ -298,6 +320,15 @@ function emitOpenTag(
|
|
|
298
320
|
const handler = actionHandler(props, entry.actionEvent);
|
|
299
321
|
if (handler) attrs.push(handler);
|
|
300
322
|
}
|
|
323
|
+
if (entry.tag === 'select' || entry.tag === 'textarea' || entry.tag === 'input') {
|
|
324
|
+
const changeHandler = onChangeHandler(props);
|
|
325
|
+
if (changeHandler) {
|
|
326
|
+
if (props.value) attrs.push(`value={${propExpr(props, 'value')}}`);
|
|
327
|
+
attrs.push(changeHandler);
|
|
328
|
+
} else if (props.value) {
|
|
329
|
+
attrs.push(`defaultValue={${propExpr(props, 'value')}}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
301
332
|
if (entry.tag === 'button' && props.enabled) attrs.push(`disabled={${propExpr(props, 'enabled')} === false}`);
|
|
302
333
|
if (entry.tag === 'a' && props.href) attrs.push(`href={${propExpr(props, 'href')}}`);
|
|
303
334
|
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}}`,
|