view-contracts 0.4.4 → 0.5.0

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.
@@ -0,0 +1,449 @@
1
+ import type { VisualPropSet } from '../../../src/renderer/style/mergeVisualProps.js';
2
+
3
+ function parsePx(value: string | number | boolean): number | null {
4
+ if (typeof value === 'number') return value;
5
+ if (typeof value === 'boolean') return null;
6
+ const match = value.match(/^([\d.]+)px$/);
7
+ return match ? Number(match[1]) : null;
8
+ }
9
+
10
+ function parseHexColor(value: string): string | null {
11
+ const match = value.match(/^#([0-9a-fA-F]{6})$/);
12
+ if (!match) return null;
13
+ const hex = match[1]!;
14
+ const r = parseInt(hex.slice(0, 2), 16);
15
+ const g = parseInt(hex.slice(2, 4), 16);
16
+ const b = parseInt(hex.slice(4, 6), 16);
17
+ return `Color(${r}, ${g}, ${b})`;
18
+ }
19
+
20
+ function parseRgbaColor(value: string): string | null {
21
+ const match = value.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/);
22
+ if (!match) return null;
23
+ const r = Number(match[1]) / 255;
24
+ const g = Number(match[2]) / 255;
25
+ const b = Number(match[3]) / 255;
26
+ const a = Number(match[4]);
27
+ return `Color(${r}f, ${g}f, ${b}f, ${a}f)`;
28
+ }
29
+
30
+ export function composeColorExpr(value: string): string | null {
31
+ if (value === 'transparent') return 'Color.Transparent';
32
+ return parseHexColor(value) ?? parseRgbaColor(value) ?? null;
33
+ }
34
+
35
+ type PaddingResult =
36
+ | { kind: 'uniform'; value: number }
37
+ | { kind: 'shorthand'; vertical: number; horizontal: number };
38
+
39
+ function parsePaddingShorthand(value: string | number | boolean): PaddingResult | null {
40
+ const single = parsePx(value);
41
+ if (single !== null) return { kind: 'uniform', value: single };
42
+ if (typeof value !== 'string') return null;
43
+ const parts = value.trim().split(/\s+/);
44
+ if (parts.length === 2) {
45
+ const v = parsePx(parts[0]!);
46
+ const h = parsePx(parts[1]!);
47
+ if (v !== null && h !== null) return { kind: 'shorthand', vertical: v, horizontal: h };
48
+ }
49
+ return null;
50
+ }
51
+
52
+ function composeFontWeight(value: string | number | boolean): string | null {
53
+ if (typeof value !== 'string') return null;
54
+ const map: Record<string, string> = {
55
+ bold: 'FontWeight.Bold',
56
+ semibold: 'FontWeight.SemiBold',
57
+ medium: 'FontWeight.Medium',
58
+ regular: 'FontWeight.Normal',
59
+ '700': 'FontWeight.Bold',
60
+ '600': 'FontWeight.SemiBold',
61
+ '500': 'FontWeight.Medium',
62
+ '400': 'FontWeight.Normal',
63
+ };
64
+ return map[value] ?? null;
65
+ }
66
+
67
+ function composeTextAlign(value: string | number | boolean): string | null {
68
+ if (typeof value !== 'string') return null;
69
+ const map: Record<string, string> = {
70
+ start: 'TextAlign.Start',
71
+ center: 'TextAlign.Center',
72
+ end: 'TextAlign.End',
73
+ };
74
+ return map[value] ?? null;
75
+ }
76
+
77
+ function composeSelfAlign(value: string | number | boolean): string | null {
78
+ if (typeof value !== 'string') return null;
79
+ const map: Record<string, string> = {
80
+ start: 'Alignment.Start',
81
+ center: 'Alignment.CenterVertically',
82
+ end: 'Alignment.Bottom',
83
+ stretch: 'Alignment.CenterVertically',
84
+ };
85
+ return map[value] ?? null;
86
+ }
87
+
88
+ export function composeContentAlignment(value: string): string {
89
+ const map: Record<string, string> = {
90
+ topStart: 'Alignment.TopStart',
91
+ top: 'Alignment.TopCenter',
92
+ topEnd: 'Alignment.TopEnd',
93
+ start: 'Alignment.CenterStart',
94
+ center: 'Alignment.Center',
95
+ end: 'Alignment.CenterEnd',
96
+ bottomStart: 'Alignment.BottomStart',
97
+ bottom: 'Alignment.BottomCenter',
98
+ bottomEnd: 'Alignment.BottomEnd',
99
+ };
100
+ return map[value] ?? 'Alignment.TopStart';
101
+ }
102
+
103
+ export interface VisualPropModifierOptions {
104
+ /** When true, text appearance props target the Text composable directly. */
105
+ isLeaf?: boolean;
106
+ }
107
+
108
+ /**
109
+ * Convert merged DSL visual props to Compose modifier / text-behavior tags.
110
+ *
111
+ * Order for Compose (outside-in, opposite of SwiftUI):
112
+ * foreground/font → fillMax frame → shadow → background → clip → border → fixed sizes → padding → margin → opacity → text behavior
113
+ */
114
+ export function visualPropSetToModifiers(
115
+ merged: VisualPropSet,
116
+ options: VisualPropModifierOptions = {},
117
+ ): string[] {
118
+ const mods: string[] = [];
119
+ const isLeaf = options.isLeaf ?? false;
120
+
121
+ // 1. Text appearance (inherited or leaf-local)
122
+ const foreground = merged.foreground;
123
+ if (typeof foreground === 'string') {
124
+ const color = composeColorExpr(foreground);
125
+ if (color) {
126
+ mods.push(isLeaf ? `@textStyle(color = ${color})` : `@contentColor(${color})`);
127
+ }
128
+ }
129
+
130
+ const sizeValue = merged.size ?? merged.fontSize;
131
+ if (sizeValue !== undefined && sizeValue !== '') {
132
+ const px = typeof sizeValue === 'number' ? sizeValue : parsePx(sizeValue);
133
+ if (px !== null) {
134
+ mods.push(isLeaf ? `@textStyle(fontSize = ${px}.sp)` : `@inheritTextStyle(fontSize = ${px}.sp)`);
135
+ }
136
+ }
137
+
138
+ const fontWeight = composeFontWeight(merged.fontWeight ?? merged.weight ?? '');
139
+ if (fontWeight) {
140
+ mods.push(isLeaf ? `@textStyle(fontWeight = ${fontWeight})` : `@inheritTextStyle(fontWeight = ${fontWeight})`);
141
+ }
142
+
143
+ const width = merged.width;
144
+ const height = merged.height;
145
+ const minWidth = merged.minWidth;
146
+ const minHeight = merged.minHeight;
147
+ const maxWidth = merged.maxWidth;
148
+ const maxHeight = merged.maxHeight;
149
+ const margin = merged.margin;
150
+ const hasCenteringMargin = margin === '0 auto';
151
+
152
+ const needsFillWidth = width === '100%' || width === 'fill' || maxWidth === 'fill';
153
+ const needsFillHeight = height === '100%' || height === 'fill' || minHeight === '100vh';
154
+ const maxWidthPx = maxWidth === 'fill' ? null : (maxWidth !== undefined ? parsePx(maxWidth) : null);
155
+
156
+ // 2. Fill frame constraints — BEFORE background so bg fills the expanded area
157
+ let addedFillHeight = false;
158
+ if (needsFillWidth && needsFillHeight) {
159
+ if (maxWidthPx !== null) {
160
+ mods.push(`.widthIn(max = ${maxWidthPx}.dp).fillMaxHeight()`);
161
+ } else {
162
+ mods.push('.fillMaxWidth().fillMaxHeight()');
163
+ }
164
+ addedFillHeight = true;
165
+ } else if (needsFillWidth) {
166
+ if (maxWidthPx !== null) {
167
+ mods.push(`.widthIn(max = ${maxWidthPx}.dp)`);
168
+ } else {
169
+ mods.push('.fillMaxWidth()');
170
+ }
171
+ } else if (needsFillHeight) {
172
+ mods.push('.fillMaxHeight()');
173
+ addedFillHeight = true;
174
+ }
175
+
176
+ if (!addedFillHeight && minHeight === '100vh') {
177
+ mods.push('.fillMaxHeight()');
178
+ }
179
+
180
+ const radiusPx = parsePx(merged.radius ?? '');
181
+
182
+ // 3. Shadow (outermost decoration in Compose)
183
+ if (merged.shadow === true || merged.shadow === 'true') {
184
+ mods.push(`.shadow(elevation = 4.dp, shape = RoundedCornerShape(${radiusPx ?? 0}.dp), clip = false, ambientColor = Color.Black.copy(alpha = 0.08f), spotColor = Color.Black.copy(alpha = 0.08f))`);
185
+ }
186
+
187
+ // 4. Background
188
+ const background = merged.background;
189
+ if (typeof background === 'string' && background !== 'transparent') {
190
+ const color = composeColorExpr(background);
191
+ if (color && color !== 'Color.Transparent') {
192
+ mods.push(`.background(${color}, RoundedCornerShape(${radiusPx ?? 0}.dp))`);
193
+ }
194
+ }
195
+
196
+ // 5. Clip shape
197
+ const clipMode = merged.clip;
198
+ if (clipMode !== 'none' && radiusPx !== null) {
199
+ mods.push(`.clip(RoundedCornerShape(${radiusPx}.dp))`);
200
+ }
201
+
202
+ // 6. Border
203
+ const border = merged.border;
204
+ const borderColor = merged.borderColor;
205
+ if (border || borderColor) {
206
+ const borderWidth = parsePx(border ?? '1px') ?? 1;
207
+ const colorExpr = typeof borderColor === 'string' ? composeColorExpr(borderColor) ?? 'Color.Gray' : 'Color.Gray';
208
+ mods.push(`.border(${borderWidth}.dp, ${colorExpr}, RoundedCornerShape(${radiusPx ?? 0}.dp))`);
209
+ }
210
+
211
+ // 7. Non-fill frame constraints
212
+ if (!needsFillWidth) {
213
+ if (typeof width === 'number') {
214
+ mods.push(`.width(${width}.dp)`);
215
+ } else if (width !== undefined && width !== 'wrap') {
216
+ const px = parsePx(width);
217
+ if (px !== null) mods.push(`.width(${px}.dp)`);
218
+ }
219
+ if (typeof minWidth === 'number') {
220
+ mods.push(`.widthIn(min = ${minWidth}.dp)`);
221
+ } else if (minWidth !== undefined) {
222
+ const px = parsePx(minWidth);
223
+ if (px !== null) mods.push(`.widthIn(min = ${px}.dp)`);
224
+ }
225
+ if (maxWidth !== undefined && maxWidth !== 'fill') {
226
+ const px = parsePx(maxWidth);
227
+ if (px !== null) mods.push(`.widthIn(max = ${px}.dp)`);
228
+ }
229
+ }
230
+
231
+ if (!needsFillHeight) {
232
+ if (typeof height === 'number') {
233
+ mods.push(`.height(${height}.dp)`);
234
+ } else if (height !== undefined && height !== 'wrap') {
235
+ const px = parsePx(height);
236
+ if (px !== null) mods.push(`.height(${px}.dp)`);
237
+ }
238
+ if (minHeight !== undefined && minHeight !== '100vh') {
239
+ if (typeof minHeight === 'number') {
240
+ mods.push(`.heightIn(min = ${minHeight}.dp)`);
241
+ } else {
242
+ const px = parsePx(minHeight);
243
+ if (px !== null) mods.push(`.heightIn(min = ${px}.dp)`);
244
+ }
245
+ }
246
+ if (maxHeight !== undefined && maxHeight !== 'fill') {
247
+ const px = parsePx(maxHeight);
248
+ if (px !== null) mods.push(`.heightIn(max = ${px}.dp)`);
249
+ }
250
+ }
251
+
252
+ if (typeof merged.aspectRatio === 'number') {
253
+ mods.push(`.aspectRatio(${merged.aspectRatio}f)`);
254
+ }
255
+
256
+ if (typeof merged.weight === 'number') {
257
+ mods.push(`.weight(${merged.weight}f)`);
258
+ }
259
+
260
+ // 8. Padding (innermost in Compose)
261
+ const paddingRaw = merged.padding ?? '';
262
+ const paddingParsed = parsePaddingShorthand(paddingRaw);
263
+ if (paddingParsed) {
264
+ if (paddingParsed.kind === 'uniform') {
265
+ mods.push(`.padding(${paddingParsed.value}.dp)`);
266
+ } else {
267
+ mods.push(`.padding(horizontal = ${paddingParsed.horizontal}.dp, vertical = ${paddingParsed.vertical}.dp)`);
268
+ }
269
+ }
270
+
271
+ // 9. margin: 0 auto → center within parent
272
+ if (hasCenteringMargin) {
273
+ mods.push('@wrapContentCenter()');
274
+ } else if (typeof margin === 'number') {
275
+ mods.push(`.padding(${margin}.dp)`);
276
+ } else if (margin !== undefined && typeof margin === 'string') {
277
+ const px = parsePx(margin);
278
+ if (px !== null) mods.push(`.padding(${px}.dp)`);
279
+ }
280
+
281
+ // 8. Opacity
282
+ const opacity = merged.opacity;
283
+ if (typeof opacity === 'number' && opacity < 1) mods.push(`.alpha(${opacity}f)`);
284
+
285
+ // Text truncation / alignment (consumed by formatTextComposable or cellgroup)
286
+ const textAlign = composeTextAlign(merged.textAlign ?? '');
287
+ if (textAlign) mods.push(`@textAlign(${textAlign})`);
288
+
289
+ const lineLimit = merged.lineLimit;
290
+ if (lineLimit !== undefined && lineLimit !== 'none' && lineLimit !== '') {
291
+ const lines = typeof lineLimit === 'number' ? lineLimit : parsePx(lineLimit);
292
+ if (lines !== null) mods.push(`@textMaxLines(${lines})`);
293
+ }
294
+
295
+ const textOverflow = merged.textOverflow;
296
+ if (textOverflow === 'ellipsis') {
297
+ mods.push('@textOverflow(TextOverflow.Ellipsis)');
298
+ } else if (textOverflow === 'clip') {
299
+ mods.push('@textOverflow(TextOverflow.Clip)');
300
+ }
301
+
302
+ const selfAlign = composeSelfAlign(merged.selfAlign ?? '');
303
+ if (selfAlign) mods.push(`.align(${selfAlign})`);
304
+
305
+ return mods;
306
+ }
307
+
308
+ const NON_MODIFIER_TAGS = [
309
+ '@textStyle(',
310
+ '@contentColor(',
311
+ '@inheritTextStyle(',
312
+ '@wrapContentCenter()',
313
+ '@textMaxLines(',
314
+ '@textOverflow(',
315
+ '@textAlign(',
316
+ '@textFillMaxWidth()',
317
+ ];
318
+
319
+ export function stripLayoutFrameModifiers(mods: string[]): string[] {
320
+ return mods.filter((m) =>
321
+ !m.includes('.width(')
322
+ && !m.includes('.widthIn(')
323
+ && !m.includes('.height(')
324
+ && !m.includes('.heightIn(')
325
+ && !m.includes('.weight(')
326
+ && m !== '.fillMaxWidth()'
327
+ && m !== '.fillMaxHeight()'
328
+ && !m.startsWith('.fillMaxWidth().')
329
+ && !m.endsWith('.fillMaxWidth()')
330
+ && !m.includes('.fillMaxHeight()'),
331
+ );
332
+ }
333
+
334
+ export function stripBackgroundModifiers(mods: string[]): string[] {
335
+ return mods.filter((m) => !m.includes('.background('));
336
+ }
337
+
338
+ export function stripPaddingModifiers(mods: string[]): string[] {
339
+ return mods.filter((m) => !m.includes('.padding('));
340
+ }
341
+
342
+ export function stripClipModifiers(mods: string[]): string[] {
343
+ return mods.filter((m) => !m.includes('.clip(RoundedCornerShape('));
344
+ }
345
+
346
+ export function extractPaddingModifiers(mods: string[]): string[] {
347
+ return mods.filter((m) => m.includes('.padding('));
348
+ }
349
+
350
+ export function extractClipRadiusDp(mods: string[]): number | null {
351
+ const clip = mods.find((m) => m.includes('.clip(RoundedCornerShape('));
352
+ const match = clip?.match(/RoundedCornerShape\((\d+)\.dp\)/);
353
+ return match ? Number(match[1]) : null;
354
+ }
355
+
356
+ /** Convert extracted `.padding(...)` modifiers to a Compose PaddingValues expression. */
357
+ export function paddingModifiersToPaddingValues(mods: string[]): string | null {
358
+ if (mods.length === 0) return null;
359
+ const mod = mods[0]!;
360
+ const horizontalVertical = mod.match(/\.padding\(horizontal = (\d+)\.dp, vertical = (\d+)\.dp\)/);
361
+ if (horizontalVertical) {
362
+ return `PaddingValues(horizontal = ${horizontalVertical[1]}.dp, vertical = ${horizontalVertical[2]}.dp)`;
363
+ }
364
+ const uniform = mod.match(/\.padding\((\d+)\.dp\)/);
365
+ if (uniform) {
366
+ return `PaddingValues(${uniform[1]}.dp)`;
367
+ }
368
+ return null;
369
+ }
370
+
371
+ export function hasStyledContainerModifiers(mods: string[]): boolean {
372
+ return mods.some((m) => m.startsWith('@runtimeStyle(') || m.includes('.background('));
373
+ }
374
+
375
+ export function hasFillMaxHeight(modifiers: string[]): boolean {
376
+ return modifiers.some((m) => m.includes('.fillMaxHeight()'));
377
+ }
378
+
379
+ export function dedupeModifiers(mods: string[]): string[] {
380
+ const seen = new Set<string>();
381
+ const out: string[] = [];
382
+ for (const mod of mods) {
383
+ if (seen.has(mod)) continue;
384
+ seen.add(mod);
385
+ out.push(mod);
386
+ }
387
+ return out;
388
+ }
389
+
390
+ export function applyModifiers(base: string, modifiers: string[]): string {
391
+ if (modifiers.length === 0) return base;
392
+ return modifiers
393
+ .filter((m) => !NON_MODIFIER_TAGS.some((tag) => m.startsWith(tag) || m === tag))
394
+ .map((m) => (m.startsWith('@runtimeStyle(')
395
+ ? `.applyVcStyle(${m.replace(/^@runtimeStyle\(/, '').replace(/\)$/, '')})`
396
+ : m))
397
+ .reduce((acc, mod) => `${acc}${mod}`, base);
398
+ }
399
+
400
+ export function extractTextStyleModifiers(modifiers: string[]): string[] {
401
+ const parts = modifiers
402
+ .filter((m) => m.startsWith('@textStyle('))
403
+ .map((m) => m.replace(/^@textStyle\(/, '').replace(/\)$/, ''));
404
+ const textAlign = modifiers.find((m) => m.startsWith('@textAlign('));
405
+ if (textAlign) {
406
+ const value = textAlign.replace(/^@textAlign\(/, '').replace(/\)$/, '');
407
+ parts.push(`textAlign = ${value}`);
408
+ }
409
+ return parts;
410
+ }
411
+
412
+ export function extractTextMaxLines(modifiers: string[]): number | null {
413
+ const match = modifiers.find((m) => m.startsWith('@textMaxLines('));
414
+ if (!match) return null;
415
+ const value = match.replace(/^@textMaxLines\(/, '').replace(/\)$/, '');
416
+ const parsed = Number(value);
417
+ return Number.isFinite(parsed) ? parsed : null;
418
+ }
419
+
420
+ export function extractTextOverflow(modifiers: string[]): string | null {
421
+ const match = modifiers.find((m) => m.startsWith('@textOverflow('));
422
+ if (!match) return null;
423
+ return match.replace(/^@textOverflow\(/, '').replace(/\)$/, '');
424
+ }
425
+
426
+ export function hasTextFillMaxWidth(modifiers: string[]): boolean {
427
+ return modifiers.includes('@textFillMaxWidth()');
428
+ }
429
+
430
+ export function extractContentColor(modifiers: string[]): string | null {
431
+ const match = modifiers.find((m) => m.startsWith('@contentColor('));
432
+ if (!match) return null;
433
+ return match.replace(/^@contentColor\(/, '').replace(/\)$/, '');
434
+ }
435
+
436
+ export function extractInheritedTextStyleParts(modifiers: string[]): string[] {
437
+ return modifiers
438
+ .filter((m) => m.startsWith('@inheritTextStyle('))
439
+ .map((m) => m.replace(/^@inheritTextStyle\(/, '').replace(/\)$/, ''));
440
+ }
441
+
442
+ export function hasWrapContentCenter(modifiers: string[]): boolean {
443
+ return modifiers.includes('@wrapContentCenter()');
444
+ }
445
+
446
+ export function extractApplyVcStyleExpr(modifierExpr: string): string | null {
447
+ const match = modifierExpr.match(/\.applyVcStyle\((VcTheme\.resolve\([^)]+\))\)/);
448
+ return match?.[1] ?? null;
449
+ }
@@ -0,0 +1,34 @@
1
+ # Compose-specific property lowering hints.
2
+ # These rules are descriptive metadata for renderer maintenance.
3
+
4
+ properties:
5
+ width:
6
+ lowering: modifier.fillMaxWidth | modifier.width
7
+ height:
8
+ lowering: modifier.fillMaxHeight | modifier.height
9
+ maxWidth:
10
+ lowering: modifier.widthIn(max)
11
+ minHeight:
12
+ lowering: modifier.heightIn(min)
13
+ padding:
14
+ lowering: modifier.padding
15
+ margin:
16
+ lowering: wrapper Box + contentAlignment
17
+ background:
18
+ lowering: modifier.background
19
+ foreground:
20
+ lowering: text color/style
21
+ radius:
22
+ lowering: rounded corner shape
23
+ border:
24
+ lowering: modifier.border
25
+ borderColor:
26
+ lowering: modifier.border color
27
+ shadow:
28
+ lowering: modifier.shadow
29
+ opacity:
30
+ lowering: modifier.alpha
31
+ fontSize:
32
+ lowering: TextStyle.fontSize
33
+ fontWeight:
34
+ lowering: TextStyle.fontWeight
@@ -0,0 +1,118 @@
1
+ import type { ViewIrDocument } from '../../src/ir/types.js';
2
+ import type { ThemeIR, TokenIR } from '../../src/design/types.js';
3
+ import { loadComposeElementMap } from './elementMap.js';
4
+ import { lowerIrToComposeBody, type ComposeRenderContext } from './lowering/lowerToCompose.js';
5
+
6
+ export interface RenderComposeOptions {
7
+ theme: ThemeIR;
8
+ tokens: TokenIR;
9
+ allowlistExtra?: string[];
10
+ elementMapPath?: string;
11
+ }
12
+
13
+ function tsTypeToKotlin(type: string): string {
14
+ const arrayMatch = type.match(/^(\w+)\[\]$/);
15
+ if (arrayMatch) return `List<${tsTypeToKotlin(arrayMatch[1]!)}>`;
16
+ if (type === 'string') return 'String';
17
+ if (type === 'number') return 'Double';
18
+ if (type === 'boolean') return 'Boolean';
19
+ return type;
20
+ }
21
+
22
+ function partialPropsSignature(doc: ViewIrDocument): string {
23
+ if (!doc.propsType) return '';
24
+ const match = doc.propsType.match(/^\{\s*([^}]+)\s*\}$/);
25
+ if (!match) return `props: ${doc.propsType}`;
26
+ const inner = match[1].trim();
27
+ return inner.split(',').map((part) => {
28
+ const colon = part.indexOf(':');
29
+ const name = colon >= 0 ? part.slice(0, colon).trim() : part.trim();
30
+ const rawType = colon >= 0 ? part.slice(colon + 1).trim() : 'Any';
31
+ const type = tsTypeToKotlin(rawType);
32
+ return `${name}: ${type}`;
33
+ }).join(', ');
34
+ }
35
+
36
+ function propsRootFromDoc(doc: ViewIrDocument): string | undefined {
37
+ if (!doc.propsType) return undefined;
38
+ const match = doc.propsType.match(/^\{\s*([^}]+)\s*\}$/);
39
+ if (!match) return undefined;
40
+ const first = match[1].split(',')[0]!.split(':')[0]!.trim();
41
+ return first;
42
+ }
43
+
44
+ export async function renderComposeComponent(
45
+ doc: ViewIrDocument,
46
+ options: RenderComposeOptions,
47
+ ): Promise<string> {
48
+ const map = await loadComposeElementMap(options.elementMapPath);
49
+ const allowlistExtra = options.allowlistExtra ?? [];
50
+ const functionName = doc.isPartial ? doc.exportName : `${doc.exportName.replace(/View$/, '') || doc.exportName}Screen`;
51
+
52
+ const ctx: ComposeRenderContext = {
53
+ map,
54
+ theme: options.theme,
55
+ tokens: options.tokens,
56
+ allowlistExtra,
57
+ propsRoot: propsRootFromDoc(doc),
58
+ };
59
+
60
+ const body = lowerIrToComposeBody(doc.root, ctx, 2);
61
+ const params = doc.isPartial
62
+ ? partialPropsSignature(doc)
63
+ : (doc.viewModel ? `vm: ${doc.viewModel}` : '');
64
+
65
+ const packageName = 'generated.compose';
66
+
67
+ return `/**
68
+ * Auto-generated Compose view from ${doc.source}
69
+ * DO NOT EDIT — generated by view-contracts.
70
+ */
71
+ @file:OptIn(ExperimentalLayoutApi::class)
72
+
73
+ package ${packageName}
74
+
75
+ import androidx.compose.foundation.background
76
+ import androidx.compose.foundation.border
77
+ import androidx.compose.foundation.layout.*
78
+ import androidx.compose.foundation.layout.ExperimentalLayoutApi
79
+ import androidx.compose.foundation.lazy.grid.GridCells
80
+ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
81
+ import androidx.compose.foundation.lazy.grid.items as gridItems
82
+ import androidx.compose.foundation.lazy.items as lazyItems
83
+ import androidx.compose.foundation.rememberScrollState
84
+ import androidx.compose.foundation.shape.RoundedCornerShape
85
+ import androidx.compose.foundation.text.BasicTextField
86
+ import androidx.compose.foundation.verticalScroll
87
+ import androidx.compose.material.icons.Icons
88
+ import androidx.compose.material.icons.filled.ArrowDropDown
89
+ import androidx.compose.material.icons.filled.Image
90
+ import androidx.compose.material3.*
91
+ import androidx.compose.runtime.*
92
+ import androidx.compose.runtime.CompositionLocalProvider
93
+ import androidx.compose.material3.LocalContentColor
94
+ import androidx.compose.material3.LocalTextStyle
95
+ import androidx.compose.ui.Alignment
96
+ import androidx.compose.ui.Modifier
97
+ import androidx.compose.ui.draw.alpha
98
+ import androidx.compose.ui.draw.clip
99
+ import androidx.compose.ui.draw.shadow
100
+ import androidx.compose.ui.graphics.Color
101
+ import androidx.compose.ui.text.TextStyle
102
+ import androidx.compose.ui.text.font.FontWeight
103
+ import androidx.compose.ui.text.style.TextAlign
104
+ import androidx.compose.ui.text.style.TextOverflow
105
+ import androidx.compose.ui.unit.dp
106
+ import androidx.compose.ui.unit.sp
107
+ import generated.compose.components.*
108
+ import generated.compose.VcButton
109
+ import generated.compose.VcTheme
110
+ import generated.compose.applyVcStyle
111
+ import generated.compose.asTextStyle
112
+
113
+ @Composable
114
+ fun ${functionName}(${params}) {
115
+ ${body}
116
+ }
117
+ `;
118
+ }