view-contracts 0.5.3 → 0.5.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "view-contracts",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "Contract-first UI design toolchain — restricted TSX to View IR to platform renderers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -24,6 +24,7 @@ import {
24
24
  extractClipRadiusDp,
25
25
  extractPaddingModifiers,
26
26
  extractTextMaxLines,
27
+ composeRowArrangement,
27
28
  extractTextOverflow,
28
29
  extractTextStyleModifiers,
29
30
  hasFillMaxHeight,
@@ -429,11 +430,51 @@ function renderStyledBox(
429
430
  ].join('\n');
430
431
  }
431
432
 
433
+ function isInlineDisplayVariant(
434
+ elementName: string,
435
+ variantLit: string,
436
+ ctx: ComposeRenderContext,
437
+ ): boolean {
438
+ const merged = mergeVisualProps({
439
+ elementName,
440
+ theme: ctx.theme,
441
+ tokens: ctx.tokens,
442
+ variantLiteral: variantLit,
443
+ instanceLiterals: {},
444
+ });
445
+ const display = merged.display;
446
+ return typeof display === 'string' && display.includes('inline');
447
+ }
448
+
449
+ function isShrinkWrapStyledText(
450
+ props: Record<string, IrValue>,
451
+ ctx: ComposeRenderContext,
452
+ itemScope?: string,
453
+ ): boolean {
454
+ if (ctx.cellTextBehavior) return true;
455
+ const variantLit = propLiteral(props, 'variant');
456
+ if (typeof variantLit === 'string') {
457
+ return isInlineDisplayVariant('Text', variantLit, ctx);
458
+ }
459
+ if (isDynamicVariant(props)) {
460
+ const v = props.variant;
461
+ if (typeof v === 'object' && v !== null && 'kind' in v && v.kind === 'concat') {
462
+ const concat = v as IrConcat;
463
+ const first = concat.parts[0];
464
+ if (first === 'badge-') return true;
465
+ if (typeof first === 'string' && first.startsWith('badge-')) return true;
466
+ }
467
+ }
468
+ return false;
469
+ }
470
+
432
471
  function formatTextComposable(
433
472
  pad: string,
434
473
  content: string,
435
474
  modifiers: string[],
436
475
  ctx: ComposeRenderContext,
476
+ textProps?: Record<string, IrValue>,
477
+ itemScope?: string,
437
478
  ): string {
438
479
  const paddingMods = extractPaddingModifiers(modifiers);
439
480
  const runtimeStyleExpr = extractApplyVcStyleExpr(applyModifiers('Modifier', modifiers));
@@ -464,7 +505,8 @@ function formatTextComposable(
464
505
  const textLine = `${pad} Text(text = ${content}${maxLinesParam}${overflowParam}${styleParam})`;
465
506
 
466
507
  if (runtimeStyleExpr) {
467
- const styleBase = ctx.cellTextBehavior ? 'Modifier' : 'Modifier.fillMaxWidth()';
508
+ const shrinkWrap = textProps ? isShrinkWrapStyledText(textProps, ctx, itemScope) : ctx.cellTextBehavior === true;
509
+ const styleBase = shrinkWrap ? 'Modifier' : 'Modifier.fillMaxWidth()';
468
510
  const styleModifier = `${styleBase}.applyVcStyle(${runtimeStyleExpr})`;
469
511
  if (paddingMods.length > 0) {
470
512
  const outerModifier = applyModifiers(modifierBase, [...layoutMods, ...paddingMods]);
@@ -670,7 +712,7 @@ function renderLeaf(
670
712
 
671
713
  if (entry.leaf === 'text') {
672
714
  const content = renderTextContent(node.children, ctx, itemScope) ?? '""';
673
- return formatTextComposable(pad, content, modifiers, ctx);
715
+ return formatTextComposable(pad, content, modifiers, ctx, node.props, itemScope);
674
716
  }
675
717
  if (entry.leaf === 'button') {
676
718
  return renderVcButton(node, level, itemScope, ctx, modifiers);
@@ -763,7 +805,9 @@ function renderLeaf(
763
805
  return `${pad}Spacer(modifier = ${modifierExpr})`;
764
806
  }
765
807
  if (entry.leaf === 'divider') {
766
- return `${pad}HorizontalDivider(modifier = ${modifierExpr})`;
808
+ const axisLit = propLiteral(node.props, 'axis');
809
+ const divider = axisLit === 'vertical' ? 'VerticalDivider' : 'HorizontalDivider';
810
+ return `${pad}${divider}(modifier = ${modifierExpr})`;
767
811
  }
768
812
  if (entry.leaf === 'image') {
769
813
  const src = propLiteral(node.props, 'src') ?? propLiteral(node.props, 'name');
@@ -814,13 +858,7 @@ function renderContainer(
814
858
  const spacing = typeof spacingLit === 'number' ? `${spacingLit}.dp` : '0.dp';
815
859
  const justifyLit = propLiteral(node.props, 'justify');
816
860
  const wrapLit = propLiteral(node.props, 'wrap');
817
- const arrangement = justifyLit === 'between'
818
- ? 'Arrangement.SpaceBetween'
819
- : justifyLit === 'end'
820
- ? 'Arrangement.End'
821
- : justifyLit === 'center'
822
- ? 'Arrangement.Center'
823
- : `Arrangement.spacedBy(${spacing})`;
861
+ const arrangement = composeRowArrangement(justifyLit, spacing);
824
862
  const alignLit = propLiteral(node.props, 'align');
825
863
  const alignItemsLit = propLiteral(node.props, 'alignItems');
826
864
  const heightLit = propLiteral(node.props, 'height');
@@ -848,7 +886,7 @@ function renderContainer(
848
886
  const childContent = wrapWithInheritedStyles(renderChildren(node.children, level + 1, renderNested, ctx, itemScope), modifiers, level + 1);
849
887
  if (node.name === 'TableHeader') ctx.inTableHeader = prevTableHeader;
850
888
 
851
- if (wrapLit === true && justifyLit !== 'between') {
889
+ if (wrapLit === true && justifyLit !== 'between' && justifyLit !== 'spaceBetween') {
852
890
  return [
853
891
  `${indent(level)}FlowRow(modifier = ${rowModifier}, horizontalArrangement = Arrangement.spacedBy(${spacing}), verticalArrangement = Arrangement.spacedBy(${spacing})) {`,
854
892
  childContent,
@@ -987,6 +1025,8 @@ function renderContainer(
987
1025
  JSON.stringify((child as IrTextNode).value),
988
1026
  dedupeModifiers([...cellTextBehaviorMods(), `@textAlign(${textAlign})`, ...paddingMods, ...inheritedMods.map((m) => m)]),
989
1027
  ctx,
1028
+ undefined,
1029
+ itemScope,
990
1030
  );
991
1031
  }
992
1032
  if (child.kind === 'binding') {
@@ -995,6 +1035,8 @@ function renderContainer(
995
1035
  bindingExpr(child as IrBinding, ctx, itemScope),
996
1036
  dedupeModifiers([...cellTextBehaviorMods(), `@textAlign(${textAlign})`, ...paddingMods, ...inheritedMods.map((m) => m)]),
997
1037
  ctx,
1038
+ undefined,
1039
+ itemScope,
998
1040
  );
999
1041
  }
1000
1042
  return renderNested(child, level + 1, itemScope);
@@ -85,6 +85,16 @@ function composeSelfAlign(value: string | number | boolean): string | null {
85
85
  return map[value] ?? null;
86
86
  }
87
87
 
88
+ /** Map DSL justify enum to Compose Row horizontalArrangement (accepts schema + legacy aliases). */
89
+ export function composeRowArrangement(justifyLit: unknown, spacing: string): string {
90
+ if (justifyLit === 'between' || justifyLit === 'spaceBetween') return 'Arrangement.SpaceBetween';
91
+ if (justifyLit === 'around' || justifyLit === 'spaceAround') return 'Arrangement.SpaceAround';
92
+ if (justifyLit === 'spaceEvenly') return 'Arrangement.SpaceEvenly';
93
+ if (justifyLit === 'end') return 'Arrangement.End';
94
+ if (justifyLit === 'center') return 'Arrangement.Center';
95
+ return `Arrangement.spacedBy(${spacing})`;
96
+ }
97
+
88
98
  export function composeContentAlignment(value: string): string {
89
99
  const map: Record<string, string> = {
90
100
  topStart: 'Alignment.TopStart',
@@ -89,7 +89,10 @@ function resolveVariantEntries(
89
89
  case 'minHeight':
90
90
  case 'maxWidth': {
91
91
  const px = parsePx(value);
92
- if (px !== null) entries.push({ property, kotlinValue: `${px}.dp` });
92
+ if (px !== null) {
93
+ const kotlinValue = property === 'fontSize' ? `${px}.sp` : `${px}.dp`;
94
+ entries.push({ property, kotlinValue });
95
+ }
93
96
  break;
94
97
  }
95
98
  case 'width': {
@@ -127,6 +130,12 @@ function resolveVariantEntries(
127
130
  entries.push({ property, kotlinValue: value === 'none' ? 'false' : 'true' });
128
131
  break;
129
132
  }
133
+ case 'display': {
134
+ if (value.includes('inline')) {
135
+ entries.push({ property: 'shrinkWrap', kotlinValue: 'true' });
136
+ }
137
+ break;
138
+ }
130
139
  default:
131
140
  break;
132
141
  }
@@ -135,13 +144,21 @@ function resolveVariantEntries(
135
144
  }
136
145
 
137
146
  const STRUCT_FIELD_ORDER = [
138
- 'foreground', 'background', 'padding', 'paddingH', 'paddingV', 'widthFill', 'heightFill',
147
+ 'foreground', 'background', 'padding', 'paddingH', 'paddingV', 'widthFill', 'heightFill', 'shrinkWrap',
139
148
  'radius', 'fontSize', 'fontWeight', 'shadow', 'borderWidth', 'borderColor', 'minHeight', 'maxWidth',
140
149
  ];
141
150
 
151
+ function finalizeVariantEntries(entries: ResolvedEntry[]): ResolvedEntry[] {
152
+ if (entries.some((entry) => entry.property === 'shrinkWrap')) {
153
+ return entries.filter((entry) => entry.property !== 'widthFill');
154
+ }
155
+ return entries;
156
+ }
157
+
142
158
  function emitVariantStyleInit(entries: ResolvedEntry[]): string {
143
- if (entries.length === 0) return 'VcVariantStyle()';
144
- const sorted = [...entries].sort(
159
+ const finalized = finalizeVariantEntries(entries);
160
+ if (finalized.length === 0) return 'VcVariantStyle()';
161
+ const sorted = [...finalized].sort(
145
162
  (a, b) => STRUCT_FIELD_ORDER.indexOf(a.property) - STRUCT_FIELD_ORDER.indexOf(b.property),
146
163
  );
147
164
  return `VcVariantStyle(${sorted.map((e) => `${e.property} = ${e.kotlinValue}`).join(', ')})`;
@@ -183,6 +200,7 @@ export function renderComposeTheme(theme: ThemeIR, tokens: TokenIR): string {
183
200
  lines.push('import androidx.compose.ui.text.TextStyle');
184
201
  lines.push('import androidx.compose.ui.text.font.FontWeight');
185
202
  lines.push('import androidx.compose.ui.unit.Dp');
203
+ lines.push('import androidx.compose.ui.unit.TextUnit');
186
204
  lines.push('import androidx.compose.ui.unit.dp');
187
205
  lines.push('import androidx.compose.ui.unit.sp');
188
206
  lines.push('');
@@ -194,8 +212,9 @@ export function renderComposeTheme(theme: ThemeIR, tokens: TokenIR): string {
194
212
  lines.push(' val paddingV: Dp? = null,');
195
213
  lines.push(' val widthFill: Boolean? = null,');
196
214
  lines.push(' val heightFill: Boolean? = null,');
215
+ lines.push(' val shrinkWrap: Boolean? = null,');
197
216
  lines.push(' val radius: Dp? = null,');
198
- lines.push(' val fontSize: Dp? = null,');
217
+ lines.push(' val fontSize: TextUnit? = null,');
199
218
  lines.push(' val fontWeight: FontWeight? = null,');
200
219
  lines.push(' val shadow: Boolean? = null,');
201
220
  lines.push(' val borderWidth: Dp? = null,');
@@ -207,7 +226,7 @@ export function renderComposeTheme(theme: ThemeIR, tokens: TokenIR): string {
207
226
  lines.push('fun VcVariantStyle.asTextStyle(): TextStyle {');
208
227
  lines.push(' var style = TextStyle()');
209
228
  lines.push(' foreground?.let { style = style.copy(color = it) }');
210
- lines.push(' fontSize?.let { style = style.copy(fontSize = it.value.sp) }');
229
+ lines.push(' fontSize?.let { style = style.copy(fontSize = it) }');
211
230
  lines.push(' fontWeight?.let { style = style.copy(fontWeight = it) }');
212
231
  lines.push(' return style');
213
232
  lines.push('}');
@@ -223,7 +242,7 @@ export function renderComposeTheme(theme: ThemeIR, tokens: TokenIR): string {
223
242
  lines.push('');
224
243
  lines.push('fun Modifier.applyVcStyle(style: VcVariantStyle): Modifier {');
225
244
  lines.push(' var out = this');
226
- lines.push(' if (style.widthFill == true) out = out.fillMaxWidth()');
245
+ lines.push(' if (style.widthFill == true && style.shrinkWrap != true) out = out.fillMaxWidth()');
227
246
  lines.push(' if (style.heightFill == true) out = out.fillMaxHeight()');
228
247
  lines.push(' if (style.shadow == true) out = out.then(vcSoftShadowModifier(style.radius ?: 0.dp))');
229
248
  lines.push(' style.background?.let { out = out.background(it, RoundedCornerShape(style.radius ?: 0.dp)) }');