view-contracts 0.4.5 → 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.
- package/cli-contract.yaml +1 -1
- package/dist/preview.mjs +266 -153
- package/dist/preview.mjs.map +4 -4
- package/dist/view-contracts.bundle.mjs +308 -195
- package/dist/view-contracts.bundle.mjs.map +4 -4
- package/docs/cli-reference.md +1 -1
- package/package.json +1 -1
- package/renderers/compose/README.md +7 -8
- package/renderers/compose/element-config.meta.json +20 -0
- package/renderers/compose/elementMap.ts +44 -0
- package/renderers/compose/elements.yaml +82 -0
- package/renderers/compose/index.ts +5 -0
- package/renderers/compose/lowering/lowerToCompose.ts +1062 -0
- package/renderers/compose/lowering/modifiers.ts +449 -0
- package/renderers/compose/properties.yaml +34 -0
- package/renderers/compose/renderComponents.ts +118 -0
- package/renderers/compose/renderRuntime.ts +78 -13
- package/renderers/compose/renderTheme.ts +304 -0
- package/renderers/compose/routes.yaml +7 -0
- package/renderers/compose/templates/Screen.kt.hbs +3 -4
- package/renderers/react/routes.yaml +3 -2
- package/renderers/registry.ts +12 -1
|
@@ -3,38 +3,103 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import type { ViewBindingsFile, ViewContractsConfig } from '../../src/config.js';
|
|
4
4
|
import { packageRoot } from '../../src/lib/packageRoot.js';
|
|
5
5
|
import { renderTemplate } from '../../src/renderer/template.js';
|
|
6
|
+
import type { ThemeIR, TokenIR } from '../../src/design/types.js';
|
|
7
|
+
import type { ViewIrDocument } from '../../src/ir/types.js';
|
|
8
|
+
import { renderComposeTheme } from './renderTheme.js';
|
|
9
|
+
import { renderComposeComponent } from './renderComponents.js';
|
|
10
|
+
import { loadPreviewMockYaml, pathExists, previewMockFilePath } from '../../src/preview/previewMock.js';
|
|
6
11
|
|
|
7
12
|
function composeTemplatesDir(): string {
|
|
8
13
|
return resolve(packageRoot(), 'renderers/compose/templates');
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
export async function renderComposeRuntime(
|
|
17
|
+
configPath: string,
|
|
12
18
|
config: ViewContractsConfig,
|
|
13
19
|
bindings: ViewBindingsFile,
|
|
20
|
+
design?: { theme: ThemeIR; tokens: TokenIR },
|
|
21
|
+
compiledDocs?: { screens: Map<string, ViewIrDocument>; partials: ViewIrDocument[] },
|
|
14
22
|
): Promise<void> {
|
|
15
23
|
const composeDir = resolve(config.generated.reactDir, '../compose');
|
|
24
|
+
const componentsDir = resolve(composeDir, 'components');
|
|
16
25
|
await mkdir(composeDir, { recursive: true });
|
|
26
|
+
await mkdir(componentsDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
if (design) {
|
|
29
|
+
await writeFile(resolve(composeDir, 'VcTheme.kt'), renderComposeTheme(design.theme, design.tokens), 'utf-8');
|
|
30
|
+
} else {
|
|
31
|
+
await writeFile(
|
|
32
|
+
resolve(composeDir, 'VcTheme.kt'),
|
|
33
|
+
await renderTemplate(composeTemplatesDir(), 'VcTheme.kt.hbs', {
|
|
34
|
+
packageName: 'generated.compose',
|
|
35
|
+
}),
|
|
36
|
+
'utf-8',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
17
39
|
|
|
18
40
|
await writeFile(
|
|
19
|
-
resolve(composeDir, '
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
41
|
+
resolve(composeDir, 'Dispatch.kt'),
|
|
42
|
+
`package generated.compose
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Dispatch stub for generated Compose screens.
|
|
46
|
+
* Wire this to your actual action runtime.
|
|
47
|
+
*/
|
|
48
|
+
suspend fun dispatch(action: String, payload: Map<String, Any?> = emptyMap()) {
|
|
49
|
+
// TODO: implement app-specific dispatcher bridge.
|
|
50
|
+
}
|
|
51
|
+
`,
|
|
23
52
|
'utf-8',
|
|
24
53
|
);
|
|
25
54
|
|
|
55
|
+
if (compiledDocs && design) {
|
|
56
|
+
for (const partial of compiledDocs.partials) {
|
|
57
|
+
try {
|
|
58
|
+
const kotlin = await renderComposeComponent(partial, { theme: design.theme, tokens: design.tokens });
|
|
59
|
+
const out = resolve(componentsDir, `${partial.exportName}.kt`);
|
|
60
|
+
await writeFile(out, kotlin.replace('package generated.compose', 'package generated.compose.components'), 'utf-8');
|
|
61
|
+
console.log(` compose/${partial.exportName} → ${out}`);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.warn(` compose/${partial.exportName}: skipped (${(err as Error).message})`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
26
68
|
for (const view of bindings.views) {
|
|
27
69
|
if (view.targets.includes('compose')) {
|
|
28
70
|
const out = resolve(composeDir, `${view.screenName}Screen.kt`);
|
|
29
|
-
await
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
71
|
+
let rendered = await renderTemplate(composeTemplatesDir(), 'Screen.kt.hbs', {
|
|
72
|
+
screenName: view.screenName,
|
|
73
|
+
packageName: 'generated.compose',
|
|
74
|
+
componentName: view.exportName,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (compiledDocs && design) {
|
|
78
|
+
const doc = compiledDocs.screens.get(view.screenName);
|
|
79
|
+
if (doc) {
|
|
80
|
+
const kotlin = await renderComposeComponent(doc, { theme: design.theme, tokens: design.tokens });
|
|
81
|
+
rendered = kotlin;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await writeFile(out, rendered, 'utf-8');
|
|
86
|
+
console.log(` compose/${view.screenName} → ${out}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const view of bindings.views) {
|
|
91
|
+
if (!view.targets.includes('compose')) continue;
|
|
92
|
+
const mockPath = previewMockFilePath(configPath, config, view.screenName);
|
|
93
|
+
if (await pathExists(mockPath)) {
|
|
94
|
+
try {
|
|
95
|
+
const mockData = await loadPreviewMockYaml(mockPath);
|
|
96
|
+
const json = JSON.stringify(mockData, null, 2) + '\n';
|
|
97
|
+
const out = resolve(composeDir, `${view.screenName}.SampleData.json`);
|
|
98
|
+
await writeFile(out, json, 'utf-8');
|
|
99
|
+
console.log(` compose/${view.screenName}.SampleData.json → ${out}`);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.warn(` compose/${view.screenName}.SampleData.json: skipped (${(err as Error).message})`);
|
|
102
|
+
}
|
|
38
103
|
}
|
|
39
104
|
}
|
|
40
105
|
}
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate VcTheme.kt with variant style dictionary for runtime lookup.
|
|
3
|
+
*/
|
|
4
|
+
import type { ThemeIR, TokenIR, VariantPropertyRef } from '../../src/design/types.js';
|
|
5
|
+
|
|
6
|
+
function resolveTokenValue(tokens: TokenIR, ref: string): string {
|
|
7
|
+
const entry = tokens.entries.find((item) => item.path === ref);
|
|
8
|
+
return entry?.value ?? '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseHexColor(value: string): string | null {
|
|
12
|
+
const match = value.match(/^#([0-9a-fA-F]{6})$/);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
const hex = match[1]!;
|
|
15
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
16
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
17
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
18
|
+
return `Color(${r}, ${g}, ${b})`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseRgbaColor(value: string): string | null {
|
|
22
|
+
const match = value.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/);
|
|
23
|
+
if (!match) return null;
|
|
24
|
+
const r = Number(match[1]) / 255;
|
|
25
|
+
const g = Number(match[2]) / 255;
|
|
26
|
+
const b = Number(match[3]) / 255;
|
|
27
|
+
const a = Number(match[4]);
|
|
28
|
+
return `Color(${r}f, ${g}f, ${b}f, ${a}f)`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function kotlinColorLiteral(value: string): string | null {
|
|
32
|
+
if (value === 'transparent') return 'Color.Transparent';
|
|
33
|
+
return parseHexColor(value) ?? parseRgbaColor(value) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parsePx(value: string): number | null {
|
|
37
|
+
const match = value.match(/^([\d.]+)px$/);
|
|
38
|
+
return match ? Number(match[1]) : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const FONT_WEIGHT_MAP: Record<string, string> = {
|
|
42
|
+
bold: 'FontWeight.Bold',
|
|
43
|
+
semibold: 'FontWeight.SemiBold',
|
|
44
|
+
medium: 'FontWeight.Medium',
|
|
45
|
+
regular: 'FontWeight.Normal',
|
|
46
|
+
'600': 'FontWeight.SemiBold',
|
|
47
|
+
'700': 'FontWeight.Bold',
|
|
48
|
+
'500': 'FontWeight.Medium',
|
|
49
|
+
'400': 'FontWeight.Normal',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
interface ResolvedEntry {
|
|
53
|
+
property: string;
|
|
54
|
+
kotlinValue: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveVariantEntries(
|
|
58
|
+
props: Record<string, VariantPropertyRef>,
|
|
59
|
+
tokens: TokenIR,
|
|
60
|
+
): ResolvedEntry[] {
|
|
61
|
+
const entries: ResolvedEntry[] = [];
|
|
62
|
+
for (const [property, binding] of Object.entries(props)) {
|
|
63
|
+
const value = resolveTokenValue(tokens, binding.ref);
|
|
64
|
+
if (!value) continue;
|
|
65
|
+
switch (property) {
|
|
66
|
+
case 'background':
|
|
67
|
+
case 'foreground': {
|
|
68
|
+
const color = kotlinColorLiteral(value);
|
|
69
|
+
if (color) entries.push({ property, kotlinValue: color });
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case 'padding': {
|
|
73
|
+
const px = parsePx(value);
|
|
74
|
+
if (px !== null) {
|
|
75
|
+
entries.push({ property, kotlinValue: `${px}.dp` });
|
|
76
|
+
} else {
|
|
77
|
+
const parts = value.trim().split(/\s+/);
|
|
78
|
+
if (parts.length === 2) {
|
|
79
|
+
const v = parsePx(parts[0]!);
|
|
80
|
+
const h = parsePx(parts[1]!);
|
|
81
|
+
if (v !== null) entries.push({ property: 'paddingV', kotlinValue: `${v}.dp` });
|
|
82
|
+
if (h !== null) entries.push({ property: 'paddingH', kotlinValue: `${h}.dp` });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'radius':
|
|
88
|
+
case 'fontSize':
|
|
89
|
+
case 'minHeight':
|
|
90
|
+
case 'maxWidth': {
|
|
91
|
+
const px = parsePx(value);
|
|
92
|
+
if (px !== null) entries.push({ property, kotlinValue: `${px}.dp` });
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'width': {
|
|
96
|
+
if (value === '100%' || value === 'fill') entries.push({ property: 'widthFill', kotlinValue: 'true' });
|
|
97
|
+
else {
|
|
98
|
+
const px = parsePx(value);
|
|
99
|
+
if (px !== null) entries.push({ property, kotlinValue: `${px}.dp` });
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'height': {
|
|
104
|
+
if (value === '100%' || value === 'fill') entries.push({ property: 'heightFill', kotlinValue: 'true' });
|
|
105
|
+
else {
|
|
106
|
+
const px = parsePx(value);
|
|
107
|
+
if (px !== null) entries.push({ property, kotlinValue: `${px}.dp` });
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'border': {
|
|
112
|
+
const match = value.match(/^([\d.]+)px/);
|
|
113
|
+
if (match) entries.push({ property: 'borderWidth', kotlinValue: `${Number(match[1])}.dp` });
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'borderColor': {
|
|
117
|
+
const color = kotlinColorLiteral(value);
|
|
118
|
+
if (color) entries.push({ property: 'borderColor', kotlinValue: color });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'fontWeight': {
|
|
122
|
+
const mapped = FONT_WEIGHT_MAP[value];
|
|
123
|
+
if (mapped) entries.push({ property, kotlinValue: mapped });
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
case 'shadow': {
|
|
127
|
+
entries.push({ property, kotlinValue: value === 'none' ? 'false' : 'true' });
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
default:
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return entries;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const STRUCT_FIELD_ORDER = [
|
|
138
|
+
'foreground', 'background', 'padding', 'paddingH', 'paddingV', 'widthFill', 'heightFill',
|
|
139
|
+
'radius', 'fontSize', 'fontWeight', 'shadow', 'borderWidth', 'borderColor', 'minHeight', 'maxWidth',
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
function emitVariantStyleInit(entries: ResolvedEntry[]): string {
|
|
143
|
+
if (entries.length === 0) return 'VcVariantStyle()';
|
|
144
|
+
const sorted = [...entries].sort(
|
|
145
|
+
(a, b) => STRUCT_FIELD_ORDER.indexOf(a.property) - STRUCT_FIELD_ORDER.indexOf(b.property),
|
|
146
|
+
);
|
|
147
|
+
return `VcVariantStyle(${sorted.map((e) => `${e.property} = ${e.kotlinValue}`).join(', ')})`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function mergeBaseIntoVariant(
|
|
151
|
+
base: Record<string, VariantPropertyRef>,
|
|
152
|
+
variant: Record<string, VariantPropertyRef>,
|
|
153
|
+
): Record<string, VariantPropertyRef> {
|
|
154
|
+
return { ...base, ...variant };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function renderComposeTheme(theme: ThemeIR, tokens: TokenIR): string {
|
|
158
|
+
const lines: string[] = [];
|
|
159
|
+
lines.push('/** Auto-generated Compose theme — variant style dictionary. */');
|
|
160
|
+
lines.push('package generated.compose');
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push('import androidx.compose.foundation.background');
|
|
163
|
+
lines.push('import androidx.compose.foundation.border');
|
|
164
|
+
lines.push('import androidx.compose.foundation.layout.Arrangement');
|
|
165
|
+
lines.push('import androidx.compose.foundation.layout.Row');
|
|
166
|
+
lines.push('import androidx.compose.foundation.layout.RowScope');
|
|
167
|
+
lines.push('import androidx.compose.foundation.layout.fillMaxHeight');
|
|
168
|
+
lines.push('import androidx.compose.foundation.layout.fillMaxWidth');
|
|
169
|
+
lines.push('import androidx.compose.foundation.layout.heightIn');
|
|
170
|
+
lines.push('import androidx.compose.foundation.layout.widthIn');
|
|
171
|
+
lines.push('import androidx.compose.foundation.layout.PaddingValues');
|
|
172
|
+
lines.push('import androidx.compose.foundation.layout.padding');
|
|
173
|
+
lines.push('import androidx.compose.foundation.shape.RoundedCornerShape');
|
|
174
|
+
lines.push('import androidx.compose.ui.graphics.Shape');
|
|
175
|
+
lines.push('import androidx.compose.material3.LocalContentColor');
|
|
176
|
+
lines.push('import androidx.compose.material3.Surface');
|
|
177
|
+
lines.push('import androidx.compose.runtime.Composable');
|
|
178
|
+
lines.push('import androidx.compose.ui.Alignment');
|
|
179
|
+
lines.push('import androidx.compose.ui.Modifier');
|
|
180
|
+
lines.push('import androidx.compose.ui.draw.clip');
|
|
181
|
+
lines.push('import androidx.compose.ui.draw.shadow');
|
|
182
|
+
lines.push('import androidx.compose.ui.graphics.Color');
|
|
183
|
+
lines.push('import androidx.compose.ui.text.TextStyle');
|
|
184
|
+
lines.push('import androidx.compose.ui.text.font.FontWeight');
|
|
185
|
+
lines.push('import androidx.compose.ui.unit.Dp');
|
|
186
|
+
lines.push('import androidx.compose.ui.unit.dp');
|
|
187
|
+
lines.push('import androidx.compose.ui.unit.sp');
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push('data class VcVariantStyle(');
|
|
190
|
+
lines.push(' val foreground: Color? = null,');
|
|
191
|
+
lines.push(' val background: Color? = null,');
|
|
192
|
+
lines.push(' val padding: Dp? = null,');
|
|
193
|
+
lines.push(' val paddingH: Dp? = null,');
|
|
194
|
+
lines.push(' val paddingV: Dp? = null,');
|
|
195
|
+
lines.push(' val widthFill: Boolean? = null,');
|
|
196
|
+
lines.push(' val heightFill: Boolean? = null,');
|
|
197
|
+
lines.push(' val radius: Dp? = null,');
|
|
198
|
+
lines.push(' val fontSize: Dp? = null,');
|
|
199
|
+
lines.push(' val fontWeight: FontWeight? = null,');
|
|
200
|
+
lines.push(' val shadow: Boolean? = null,');
|
|
201
|
+
lines.push(' val borderWidth: Dp? = null,');
|
|
202
|
+
lines.push(' val borderColor: Color? = null,');
|
|
203
|
+
lines.push(' val minHeight: Dp? = null,');
|
|
204
|
+
lines.push(' val maxWidth: Dp? = null,');
|
|
205
|
+
lines.push(')');
|
|
206
|
+
lines.push('');
|
|
207
|
+
lines.push('fun VcVariantStyle.asTextStyle(): TextStyle {');
|
|
208
|
+
lines.push(' var style = TextStyle()');
|
|
209
|
+
lines.push(' foreground?.let { style = style.copy(color = it) }');
|
|
210
|
+
lines.push(' fontSize?.let { style = style.copy(fontSize = it.value.sp) }');
|
|
211
|
+
lines.push(' fontWeight?.let { style = style.copy(fontWeight = it) }');
|
|
212
|
+
lines.push(' return style');
|
|
213
|
+
lines.push('}');
|
|
214
|
+
lines.push('');
|
|
215
|
+
lines.push('private fun vcSoftShadowModifier(radius: Dp): Modifier =');
|
|
216
|
+
lines.push(' Modifier.shadow(');
|
|
217
|
+
lines.push(' elevation = 4.dp,');
|
|
218
|
+
lines.push(' shape = RoundedCornerShape(radius),');
|
|
219
|
+
lines.push(' clip = false,');
|
|
220
|
+
lines.push(' ambientColor = Color.Black.copy(alpha = 0.08f),');
|
|
221
|
+
lines.push(' spotColor = Color.Black.copy(alpha = 0.08f),');
|
|
222
|
+
lines.push(' )');
|
|
223
|
+
lines.push('');
|
|
224
|
+
lines.push('fun Modifier.applyVcStyle(style: VcVariantStyle): Modifier {');
|
|
225
|
+
lines.push(' var out = this');
|
|
226
|
+
lines.push(' if (style.widthFill == true) out = out.fillMaxWidth()');
|
|
227
|
+
lines.push(' if (style.heightFill == true) out = out.fillMaxHeight()');
|
|
228
|
+
lines.push(' if (style.shadow == true) out = out.then(vcSoftShadowModifier(style.radius ?: 0.dp))');
|
|
229
|
+
lines.push(' style.background?.let { out = out.background(it, RoundedCornerShape(style.radius ?: 0.dp)) }');
|
|
230
|
+
lines.push(' style.radius?.let { out = out.clip(RoundedCornerShape(it)) }');
|
|
231
|
+
lines.push(' if (style.borderColor != null) out = out.border(style.borderWidth ?: 1.dp, style.borderColor, RoundedCornerShape(style.radius ?: 0.dp))');
|
|
232
|
+
lines.push(' style.minHeight?.let { out = out.heightIn(min = it) }');
|
|
233
|
+
lines.push(' style.maxWidth?.let { out = out.widthIn(max = it) }');
|
|
234
|
+
lines.push(' style.padding?.let { out = out.padding(it) }');
|
|
235
|
+
lines.push(' style.paddingH?.let { out = out.padding(horizontal = it) }');
|
|
236
|
+
lines.push(' style.paddingV?.let { out = out.padding(vertical = it) }');
|
|
237
|
+
lines.push(' return out');
|
|
238
|
+
lines.push('}');
|
|
239
|
+
lines.push('');
|
|
240
|
+
lines.push('@Composable');
|
|
241
|
+
lines.push('fun VcButton(');
|
|
242
|
+
lines.push(' onClick: () -> Unit,');
|
|
243
|
+
lines.push(' modifier: Modifier = Modifier,');
|
|
244
|
+
lines.push(' containerColor: Color = Color.Transparent,');
|
|
245
|
+
lines.push(' contentColor: Color = Color.Unspecified,');
|
|
246
|
+
lines.push(' shape: Shape = RoundedCornerShape(0.dp),');
|
|
247
|
+
lines.push(' contentPadding: PaddingValues = PaddingValues(0.dp),');
|
|
248
|
+
lines.push(' enabled: Boolean = true,');
|
|
249
|
+
lines.push(' content: @Composable RowScope.() -> Unit,');
|
|
250
|
+
lines.push(') {');
|
|
251
|
+
lines.push(' val resolvedContentColor = if (contentColor == Color.Unspecified) LocalContentColor.current else contentColor');
|
|
252
|
+
lines.push(' Surface(');
|
|
253
|
+
lines.push(' onClick = onClick,');
|
|
254
|
+
lines.push(' modifier = modifier,');
|
|
255
|
+
lines.push(' enabled = enabled,');
|
|
256
|
+
lines.push(' shape = shape,');
|
|
257
|
+
lines.push(' color = containerColor,');
|
|
258
|
+
lines.push(' contentColor = resolvedContentColor,');
|
|
259
|
+
lines.push(' shadowElevation = 0.dp,');
|
|
260
|
+
lines.push(' tonalElevation = 0.dp,');
|
|
261
|
+
lines.push(' ) {');
|
|
262
|
+
lines.push(' Row(');
|
|
263
|
+
lines.push(' modifier = Modifier.padding(contentPadding),');
|
|
264
|
+
lines.push(' horizontalArrangement = Arrangement.Center,');
|
|
265
|
+
lines.push(' verticalAlignment = Alignment.CenterVertically,');
|
|
266
|
+
lines.push(' content = content,');
|
|
267
|
+
lines.push(' )');
|
|
268
|
+
lines.push(' }');
|
|
269
|
+
lines.push('}');
|
|
270
|
+
lines.push('');
|
|
271
|
+
lines.push('object VcTheme {');
|
|
272
|
+
lines.push(' val defaults: Map<String, VcVariantStyle> = mapOf(');
|
|
273
|
+
for (const [elementName, baseProps] of Object.entries(theme.defaults)) {
|
|
274
|
+
if (Object.keys(baseProps).length === 0) continue;
|
|
275
|
+
const resolved = resolveVariantEntries(baseProps, tokens);
|
|
276
|
+
if (resolved.length === 0) continue;
|
|
277
|
+
lines.push(` ${JSON.stringify(elementName)} to ${emitVariantStyleInit(resolved)},`);
|
|
278
|
+
}
|
|
279
|
+
lines.push(' )');
|
|
280
|
+
lines.push('');
|
|
281
|
+
lines.push(' val variants: Map<String, Map<String, VcVariantStyle>> = mapOf(');
|
|
282
|
+
for (const [elementName, elementVariants] of Object.entries(theme.variants)) {
|
|
283
|
+
const variantEntries = Object.entries(elementVariants);
|
|
284
|
+
if (variantEntries.length === 0) continue;
|
|
285
|
+
const baseProps = theme.defaults[elementName] ?? {};
|
|
286
|
+
lines.push(` ${JSON.stringify(elementName)} to mapOf(`);
|
|
287
|
+
for (const [variantName, variantProps] of variantEntries) {
|
|
288
|
+
const merged = mergeBaseIntoVariant(baseProps, variantProps);
|
|
289
|
+
let resolved = resolveVariantEntries(merged, tokens);
|
|
290
|
+
if (elementName === 'Box' && variantName === 'card') {
|
|
291
|
+
resolved = resolved.filter((entry) => entry.property !== 'heightFill');
|
|
292
|
+
}
|
|
293
|
+
lines.push(` ${JSON.stringify(variantName)} to ${emitVariantStyleInit(resolved)},`);
|
|
294
|
+
}
|
|
295
|
+
lines.push(' ),');
|
|
296
|
+
}
|
|
297
|
+
lines.push(' )');
|
|
298
|
+
lines.push('');
|
|
299
|
+
lines.push(' fun resolve(element: String, variant: String): VcVariantStyle =');
|
|
300
|
+
lines.push(' variants[element]?.get(variant) ?: defaults[element] ?: VcVariantStyle()');
|
|
301
|
+
lines.push('}');
|
|
302
|
+
lines.push('');
|
|
303
|
+
return lines.join('\n');
|
|
304
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
compose:
|
|
2
|
+
description: Jetpack Compose renderer (vocabulary lowering via renderers/compose/elements.yaml)
|
|
3
|
+
outputs:
|
|
4
|
+
screen:
|
|
5
|
+
pattern: '{{projectRoot}}/generated/compose/{{screenName}}Screen.kt'
|
|
6
|
+
partial:
|
|
7
|
+
pattern: '{{projectRoot}}/generated/compose/components/{{exportName}}.kt'
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
// {{screenName}} — Jetpack Compose scaffold
|
|
2
|
-
// View IR lowering: renderers/compose/lowerToCompose.ts (planned)
|
|
3
|
-
// Theme: renderers/NATIVE_THEME_EMIT.md
|
|
4
2
|
|
|
5
3
|
package {{packageName}}
|
|
6
4
|
|
|
7
|
-
import androidx.compose.material3.Text
|
|
8
5
|
import androidx.compose.runtime.Composable
|
|
6
|
+
import generated.compose.components.{{componentName}}
|
|
9
7
|
|
|
10
8
|
@Composable
|
|
11
9
|
fun {{screenName}}Screen() {
|
|
12
|
-
|
|
10
|
+
// TODO: Inject real view model and dispatcher bindings.
|
|
11
|
+
{{componentName}}()
|
|
13
12
|
}
|
|
@@ -24,8 +24,9 @@ swiftui:
|
|
|
24
24
|
pattern: '{{projectRoot}}/generated/swiftui/{{exportName}}.generated.swift'
|
|
25
25
|
|
|
26
26
|
compose:
|
|
27
|
-
description: Jetpack Compose renderer (
|
|
27
|
+
description: Jetpack Compose renderer (screen + component partial generation)
|
|
28
28
|
outputs:
|
|
29
29
|
screen:
|
|
30
30
|
pattern: '{{projectRoot}}/generated/compose/{{screenName}}Screen.kt'
|
|
31
|
-
|
|
31
|
+
partial:
|
|
32
|
+
pattern: '{{projectRoot}}/generated/compose/components/{{exportName}}.kt'
|
package/renderers/registry.ts
CHANGED
|
@@ -53,10 +53,21 @@ export async function renderSwiftComponent(
|
|
|
53
53
|
return mod.renderSwiftComponent(doc, options);
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export async function renderComposeComponent(
|
|
57
|
+
doc: ViewIrDocument,
|
|
58
|
+
options: { theme: ThemeIR; tokens: TokenIR; allowlistExtra?: string[] },
|
|
59
|
+
): Promise<string> {
|
|
60
|
+
const mod = await import('./compose/renderComponents.js');
|
|
61
|
+
return mod.renderComposeComponent(doc, options);
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
export async function renderComposeRuntime(
|
|
65
|
+
configPath: string,
|
|
57
66
|
config: ViewContractsConfig,
|
|
58
67
|
bindings: ViewBindingsFile,
|
|
68
|
+
design?: { theme: ThemeIR; tokens: TokenIR },
|
|
69
|
+
compiledDocs?: { screens: Map<string, ViewIrDocument>; partials: ViewIrDocument[] },
|
|
59
70
|
): Promise<void> {
|
|
60
71
|
const mod = await import('./compose/renderRuntime.js');
|
|
61
|
-
return mod.renderComposeRuntime(config, bindings);
|
|
72
|
+
return mod.renderComposeRuntime(configPath, config, bindings, design, compiledDocs);
|
|
62
73
|
}
|