view-contracts 0.3.5 → 0.4.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/README.md +10 -3
- package/cli-contract.yaml +40 -0
- package/dist/preview-style-helpers.js +2 -0
- package/dist/preview-style-helpers.js.map +7 -0
- package/dist/preview.mjs +153 -0
- package/dist/preview.mjs.map +7 -0
- package/dist/view-contracts.bundle.mjs +399 -248
- package/dist/view-contracts.bundle.mjs.map +4 -4
- package/docs/cli-reference.md +36 -0
- package/package.json +28 -1
- package/renderers/compose/renderRuntime.ts +40 -0
- package/renderers/react/defaults/theme.yaml +23 -0
- package/renderers/react/defaults/tokens.yaml +15 -0
- package/renderers/react/elementMap.ts +30 -6
- package/renderers/react/elements.yaml +17 -5
- package/renderers/react/lowering/foldStyle.ts +13 -1
- package/renderers/react/lowering/lowerToJsx.ts +15 -34
- package/renderers/react/paths.ts +1 -10
- package/renderers/react/preview/components.ts +23 -0
- package/renderers/react/preview/index.ts +3 -0
- package/renderers/react/preview/start.tsx +42 -0
- package/renderers/react/preview/style-helpers.ts +5 -0
- package/renderers/react/preview/vocabulary.tsx +75 -0
- package/renderers/react/runtime/theme.css +9 -17
- package/renderers/react/style/foldLiterals.ts +11 -0
- package/renderers/swiftui/README.md +48 -10
- package/renderers/swiftui/elementMap.ts +45 -0
- package/renderers/swiftui/elements.yaml +82 -0
- package/renderers/swiftui/index.ts +5 -0
- package/renderers/swiftui/lowering/lowerToSwiftUI.ts +542 -0
- package/renderers/swiftui/lowering/modifiers.ts +142 -0
- package/renderers/swiftui/renderComponents.ts +79 -0
- package/renderers/swiftui/renderRuntime.ts +91 -0
- package/renderers/swiftui/renderTheme.ts +208 -0
- package/spec/ui-dsl.linter-rules.json +0 -7
- package/spec/ui-dsl.meta.json +5 -0
- package/spec/ui-dsl.schema.json +333 -259
- package/src/generated/schema/allowed-components.ts +0 -36
- package/src/generated/schema/dsl-enums.ts +0 -19
- package/src/generated/schema/dsl-properties.ts +0 -64
- package/src/generated/schema/index.ts +0 -4
- package/src/generated/schema/lowering-style-properties.ts +0 -35
- package/src/generated/schema/react-renderer-element-config.ts +0 -27
- package/src/generated/schema/schema-document.ts +0 -33
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ViewIrDocument } from '../../src/ir/types.js';
|
|
2
|
+
import type { ThemeIR, TokenIR } from '../../src/design/types.js';
|
|
3
|
+
import { loadSwiftUIElementMap } from './elementMap.js';
|
|
4
|
+
import { lowerIrToSwiftBody, type SwiftUIRenderContext } from './lowering/lowerToSwiftUI.js';
|
|
5
|
+
|
|
6
|
+
export interface RenderSwiftOptions {
|
|
7
|
+
theme: ThemeIR;
|
|
8
|
+
tokens: TokenIR;
|
|
9
|
+
allowlistExtra?: string[];
|
|
10
|
+
elementMapPath?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function tsTypeToSwift(type: string): string {
|
|
14
|
+
const arrayMatch = type.match(/^(\w+)\[\]$/);
|
|
15
|
+
if (arrayMatch) return `[${tsTypeToSwift(arrayMatch[1]!)}]`;
|
|
16
|
+
if (type === 'string') return 'String';
|
|
17
|
+
if (type === 'number') return 'Double';
|
|
18
|
+
if (type === 'boolean') return 'Bool';
|
|
19
|
+
return type;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function partialPropsLines(doc: ViewIrDocument): string {
|
|
23
|
+
if (!doc.propsType) return '';
|
|
24
|
+
const match = doc.propsType.match(/^\{\s*([^}]+)\s*\}$/);
|
|
25
|
+
if (!match) return ` let props: ${doc.propsType}\n`;
|
|
26
|
+
const inner = match[1].trim();
|
|
27
|
+
const lines = 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 = tsTypeToSwift(rawType);
|
|
32
|
+
return ` let ${name}: ${type}`;
|
|
33
|
+
});
|
|
34
|
+
return `${lines.join('\n')}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function propsRootFromDoc(doc: ViewIrDocument): string | undefined {
|
|
38
|
+
if (!doc.propsType) return undefined;
|
|
39
|
+
const match = doc.propsType.match(/^\{\s*([^}]+)\s*\}$/);
|
|
40
|
+
if (!match) return undefined;
|
|
41
|
+
const first = match[1].split(',')[0]!.split(':')[0]!.trim();
|
|
42
|
+
return first;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function renderSwiftComponent(
|
|
46
|
+
doc: ViewIrDocument,
|
|
47
|
+
options: RenderSwiftOptions,
|
|
48
|
+
): Promise<string> {
|
|
49
|
+
const map = await loadSwiftUIElementMap(options.elementMapPath);
|
|
50
|
+
const allowlistExtra = options.allowlistExtra ?? [];
|
|
51
|
+
const structName = doc.isPartial ? doc.exportName : doc.exportName.replace(/View$/, '') || doc.exportName;
|
|
52
|
+
|
|
53
|
+
const ctx: SwiftUIRenderContext = {
|
|
54
|
+
map,
|
|
55
|
+
theme: options.theme,
|
|
56
|
+
tokens: options.tokens,
|
|
57
|
+
allowlistExtra,
|
|
58
|
+
propsRoot: propsRootFromDoc(doc),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const body = lowerIrToSwiftBody(doc.root, ctx, 4);
|
|
62
|
+
const propsLines = partialPropsLines(doc);
|
|
63
|
+
const vmLine = !doc.isPartial && doc.viewModel ? ` let vm: ${doc.viewModel}\n` : '';
|
|
64
|
+
|
|
65
|
+
const header = `/**
|
|
66
|
+
* Auto-generated SwiftUI view from ${doc.source}
|
|
67
|
+
* DO NOT EDIT — prototype SwiftUI lowering (#21)
|
|
68
|
+
*/
|
|
69
|
+
import SwiftUI
|
|
70
|
+
|
|
71
|
+
struct ${structName}: View {
|
|
72
|
+
${vmLine}${propsLines} var body: some View {
|
|
73
|
+
${body}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
return header;
|
|
79
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import type { ViewBindingsFile, ViewContractsConfig } from '../../src/config.js';
|
|
4
|
+
import { packageRoot } from '../../src/lib/packageRoot.js';
|
|
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 { renderSwiftTheme } from './renderTheme.js';
|
|
9
|
+
import { renderSwiftComponent } from './renderComponents.js';
|
|
10
|
+
import { loadPreviewMockYaml, previewMockFilePath, pathExists } from '../../src/preview/previewMock.js';
|
|
11
|
+
|
|
12
|
+
function swiftuiTemplatesDir(): string {
|
|
13
|
+
return resolve(packageRoot(), 'renderers/swiftui/templates');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function renderSwiftUIRuntime(
|
|
17
|
+
configPath: string,
|
|
18
|
+
config: ViewContractsConfig,
|
|
19
|
+
bindings: ViewBindingsFile,
|
|
20
|
+
design?: { theme: ThemeIR; tokens: TokenIR },
|
|
21
|
+
compiledDocs?: { screens: Map<string, ViewIrDocument>; partials: ViewIrDocument[] },
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const swiftuiDir = resolve(config.generated.reactDir, '../swiftui');
|
|
24
|
+
await mkdir(swiftuiDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
if (design) {
|
|
27
|
+
await writeFile(resolve(swiftuiDir, 'VcTheme.swift'), renderSwiftTheme(design.theme, design.tokens), 'utf-8');
|
|
28
|
+
} else {
|
|
29
|
+
await writeFile(
|
|
30
|
+
resolve(swiftuiDir, 'VcTheme.swift'),
|
|
31
|
+
await renderTemplate(swiftuiTemplatesDir(), 'VcTheme.swift.hbs', {}),
|
|
32
|
+
'utf-8',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (compiledDocs && design) {
|
|
37
|
+
for (const partial of compiledDocs.partials) {
|
|
38
|
+
try {
|
|
39
|
+
const swift = await renderSwiftComponent(partial, { theme: design.theme, tokens: design.tokens });
|
|
40
|
+
const out = resolve(swiftuiDir, `${partial.exportName}.generated.swift`);
|
|
41
|
+
await writeFile(out, swift, 'utf-8');
|
|
42
|
+
console.log(` swiftui/${partial.exportName} → ${out}`);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.warn(` swiftui/${partial.exportName}: skipped (${(err as Error).message})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const view of bindings.views) {
|
|
49
|
+
const doc = compiledDocs.screens.get(view.screenName);
|
|
50
|
+
if (doc) {
|
|
51
|
+
try {
|
|
52
|
+
const swift = await renderSwiftComponent(doc, { theme: design.theme, tokens: design.tokens });
|
|
53
|
+
const out = resolve(swiftuiDir, `${view.screenName}.generated.swift`);
|
|
54
|
+
await writeFile(out, swift, 'utf-8');
|
|
55
|
+
console.log(` swiftui/${view.screenName} → ${out}`);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.warn(` swiftui/${view.screenName}: skipped (${(err as Error).message})`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const view of bindings.views) {
|
|
64
|
+
if (view.targets.includes('swiftui')) {
|
|
65
|
+
const out = resolve(swiftuiDir, `${view.screenName}.swift`);
|
|
66
|
+
await writeFile(
|
|
67
|
+
out,
|
|
68
|
+
await renderTemplate(swiftuiTemplatesDir(), 'Screen.swift.hbs', {
|
|
69
|
+
screenName: view.screenName,
|
|
70
|
+
}),
|
|
71
|
+
'utf-8',
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Generate SampleData JSON from mock.yaml (SSoT)
|
|
77
|
+
for (const view of bindings.views) {
|
|
78
|
+
const mockPath = previewMockFilePath(configPath, config, view.screenName);
|
|
79
|
+
if (await pathExists(mockPath)) {
|
|
80
|
+
try {
|
|
81
|
+
const mockData = await loadPreviewMockYaml(mockPath);
|
|
82
|
+
const json = JSON.stringify(mockData, null, 2) + '\n';
|
|
83
|
+
const out = resolve(swiftuiDir, `${view.screenName}.SampleData.json`);
|
|
84
|
+
await writeFile(out, json, 'utf-8');
|
|
85
|
+
console.log(` swiftui/${view.screenName}.SampleData.json → ${out}`);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.warn(` swiftui/${view.screenName}.SampleData.json: skipped (${(err as Error).message})`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate VcTheme.swift with variant style dictionary for runtime lookup.
|
|
3
|
+
* Enables dynamic variant resolution (IrConcat / IrBinding) in SwiftUI lowering.
|
|
4
|
+
*/
|
|
5
|
+
import type { ThemeIR, TokenIR, VariantPropertyRef } from '../../src/design/types.js';
|
|
6
|
+
|
|
7
|
+
function resolveTokenValue(tokens: TokenIR, ref: string): string {
|
|
8
|
+
const entry = tokens.entries.find((item) => item.path === ref);
|
|
9
|
+
return entry?.value ?? '';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseHexColor(value: string): string | null {
|
|
13
|
+
const match = value.match(/^#([0-9a-fA-F]{6})$/);
|
|
14
|
+
if (!match) return null;
|
|
15
|
+
const hex = match[1]!;
|
|
16
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
17
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
18
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
19
|
+
return `Color(red: ${(r / 255).toFixed(3)}, green: ${(g / 255).toFixed(3)}, blue: ${(b / 255).toFixed(3)})`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parsePx(value: string): number | null {
|
|
23
|
+
const match = value.match(/^([\d.]+)px$/);
|
|
24
|
+
return match ? Number(match[1]) : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FONT_WEIGHT_MAP: Record<string, string> = {
|
|
28
|
+
bold: '.bold', semibold: '.semibold', medium: '.medium', regular: '.regular',
|
|
29
|
+
'600': '.semibold', '700': '.bold', '500': '.medium', '400': '.regular',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function swiftColorLiteral(value: string): string | null {
|
|
33
|
+
return parseHexColor(value) ?? (value ? `Color(${JSON.stringify(value)})` : null);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ResolvedEntry {
|
|
37
|
+
property: string;
|
|
38
|
+
swiftValue: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveVariantEntries(
|
|
42
|
+
props: Record<string, VariantPropertyRef>,
|
|
43
|
+
tokens: TokenIR,
|
|
44
|
+
): ResolvedEntry[] {
|
|
45
|
+
const entries: ResolvedEntry[] = [];
|
|
46
|
+
for (const [property, binding] of Object.entries(props)) {
|
|
47
|
+
const value = resolveTokenValue(tokens, binding.ref);
|
|
48
|
+
if (!value) continue;
|
|
49
|
+
|
|
50
|
+
switch (property) {
|
|
51
|
+
case 'background':
|
|
52
|
+
case 'foreground': {
|
|
53
|
+
const color = swiftColorLiteral(value);
|
|
54
|
+
if (color) entries.push({ property, swiftValue: color });
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
case 'padding': {
|
|
58
|
+
const px = parsePx(value);
|
|
59
|
+
if (px !== null) {
|
|
60
|
+
entries.push({ property, swiftValue: String(px) });
|
|
61
|
+
} else {
|
|
62
|
+
const parts = value.trim().split(/\s+/);
|
|
63
|
+
if (parts.length === 2) {
|
|
64
|
+
const v = parsePx(parts[0]!);
|
|
65
|
+
const h = parsePx(parts[1]!);
|
|
66
|
+
if (v !== null) entries.push({ property: 'paddingV', swiftValue: String(v) });
|
|
67
|
+
if (h !== null) entries.push({ property: 'paddingH', swiftValue: String(h) });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case 'radius':
|
|
73
|
+
case 'fontSize':
|
|
74
|
+
case 'minHeight':
|
|
75
|
+
case 'maxWidth':
|
|
76
|
+
case 'width':
|
|
77
|
+
case 'margin': {
|
|
78
|
+
const px = parsePx(value);
|
|
79
|
+
if (px !== null) entries.push({ property, swiftValue: String(px) });
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case 'fontWeight': {
|
|
83
|
+
const mapped = FONT_WEIGHT_MAP[value];
|
|
84
|
+
if (mapped) entries.push({ property, swiftValue: mapped });
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
case 'shadow': {
|
|
88
|
+
entries.push({ property, swiftValue: value === 'true' || value === 'none' ? 'false' : 'true' });
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
default:
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return entries;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const STRUCT_FIELD_ORDER = ['foreground', 'background', 'padding', 'paddingH', 'paddingV', 'radius', 'fontSize', 'fontWeight', 'shadow', 'minHeight', 'maxWidth'];
|
|
99
|
+
|
|
100
|
+
function emitVariantStyleInit(entries: ResolvedEntry[], indent: string): string {
|
|
101
|
+
if (entries.length === 0) return `${indent}VcVariantStyle()`;
|
|
102
|
+
const sorted = [...entries].sort(
|
|
103
|
+
(a, b) => STRUCT_FIELD_ORDER.indexOf(a.property) - STRUCT_FIELD_ORDER.indexOf(b.property),
|
|
104
|
+
);
|
|
105
|
+
const args = sorted.map(e => `${e.property}: ${e.swiftValue}`).join(', ');
|
|
106
|
+
return `${indent}VcVariantStyle(${args})`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Merge base defaults into variant entries so the dictionary contains
|
|
111
|
+
* the full cascade: base < variant. Variant values override base.
|
|
112
|
+
*/
|
|
113
|
+
function mergeBaseIntoVariant(
|
|
114
|
+
base: Record<string, VariantPropertyRef>,
|
|
115
|
+
variant: Record<string, VariantPropertyRef>,
|
|
116
|
+
): Record<string, VariantPropertyRef> {
|
|
117
|
+
return { ...base, ...variant };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function renderSwiftTheme(theme: ThemeIR, tokens: TokenIR): string {
|
|
121
|
+
const lines: string[] = [];
|
|
122
|
+
lines.push(`/**`);
|
|
123
|
+
lines.push(` * Auto-generated SwiftUI theme — variant style dictionary.`);
|
|
124
|
+
lines.push(` * Each entry is pre-merged: base theme defaults < variant overrides.`);
|
|
125
|
+
lines.push(` * DO NOT EDIT — regenerate with: view-contracts build`);
|
|
126
|
+
lines.push(` */`);
|
|
127
|
+
lines.push(`import SwiftUI`);
|
|
128
|
+
lines.push(``);
|
|
129
|
+
|
|
130
|
+
lines.push(`struct VcVariantStyle {`);
|
|
131
|
+
lines.push(` var foreground: Color?`);
|
|
132
|
+
lines.push(` var background: Color?`);
|
|
133
|
+
lines.push(` var padding: CGFloat?`);
|
|
134
|
+
lines.push(` var paddingH: CGFloat?`);
|
|
135
|
+
lines.push(` var paddingV: CGFloat?`);
|
|
136
|
+
lines.push(` var radius: CGFloat?`);
|
|
137
|
+
lines.push(` var fontSize: CGFloat?`);
|
|
138
|
+
lines.push(` var fontWeight: Font.Weight?`);
|
|
139
|
+
lines.push(` var shadow: Bool?`);
|
|
140
|
+
lines.push(` var minHeight: CGFloat?`);
|
|
141
|
+
lines.push(` var maxWidth: CGFloat?`);
|
|
142
|
+
lines.push(`}`);
|
|
143
|
+
lines.push(``);
|
|
144
|
+
|
|
145
|
+
// Nil-transparent: only apply properties the style actually defines.
|
|
146
|
+
lines.push(`extension View {`);
|
|
147
|
+
lines.push(` func applyVcStyle(_ s: VcVariantStyle) -> some View {`);
|
|
148
|
+
lines.push(` self`);
|
|
149
|
+
lines.push(` .ifSet(s.foreground) { $0.foregroundStyle($1) }`);
|
|
150
|
+
lines.push(` .ifSet(s.fontSize) { $0.font(.system(size: $1, weight: s.fontWeight ?? .regular)) }`);
|
|
151
|
+
lines.push(` .ifSet(s.padding) { $0.padding($1) }`);
|
|
152
|
+
lines.push(` .ifSet(s.paddingH) { $0.padding(.horizontal, $1) }`);
|
|
153
|
+
lines.push(` .ifSet(s.paddingV) { $0.padding(.vertical, $1) }`);
|
|
154
|
+
lines.push(` .ifSet(s.background) { $0.background($1) }`);
|
|
155
|
+
lines.push(` .ifSet(s.radius) { $0.clipShape(RoundedRectangle(cornerRadius: $1)) }`);
|
|
156
|
+
lines.push(` .ifSet(s.shadow) { v, on in on ? AnyView(v.shadow(color: Color.black.opacity(0.08), radius: 4, x: 0, y: 2)) : AnyView(v) }`);
|
|
157
|
+
lines.push(` }`);
|
|
158
|
+
lines.push(`}`);
|
|
159
|
+
lines.push(``);
|
|
160
|
+
lines.push(`extension View {`);
|
|
161
|
+
lines.push(` @ViewBuilder`);
|
|
162
|
+
lines.push(` func ifSet<T, V: View>(_ value: T?, _ transform: (Self, T) -> V) -> some View {`);
|
|
163
|
+
lines.push(` if let value { transform(self, value) } else { self }`);
|
|
164
|
+
lines.push(` }`);
|
|
165
|
+
lines.push(`}`);
|
|
166
|
+
lines.push(``);
|
|
167
|
+
|
|
168
|
+
lines.push(`enum VcTheme {`);
|
|
169
|
+
|
|
170
|
+
// Emit base defaults per element
|
|
171
|
+
lines.push(` static let defaults: [String: VcVariantStyle] = [`);
|
|
172
|
+
for (const [elementName, baseProps] of Object.entries(theme.defaults)) {
|
|
173
|
+
if (Object.keys(baseProps).length === 0) continue;
|
|
174
|
+
const resolved = resolveVariantEntries(baseProps, tokens);
|
|
175
|
+
if (resolved.length === 0) continue;
|
|
176
|
+
lines.push(` ${JSON.stringify(elementName)}: ${emitVariantStyleInit(resolved, '').trim()},`);
|
|
177
|
+
}
|
|
178
|
+
lines.push(` ]`);
|
|
179
|
+
lines.push(``);
|
|
180
|
+
|
|
181
|
+
// Emit pre-merged (base + variant) dictionary
|
|
182
|
+
lines.push(` static let variants: [String: [String: VcVariantStyle]] = [`);
|
|
183
|
+
for (const [elementName, elementVariants] of Object.entries(theme.variants)) {
|
|
184
|
+
const variantEntries = Object.entries(elementVariants);
|
|
185
|
+
if (variantEntries.length === 0) continue;
|
|
186
|
+
|
|
187
|
+
const baseProps = theme.defaults[elementName] ?? {};
|
|
188
|
+
lines.push(` ${JSON.stringify(elementName)}: [`);
|
|
189
|
+
for (const [variantName, variantProps] of variantEntries) {
|
|
190
|
+
const merged = mergeBaseIntoVariant(baseProps, variantProps);
|
|
191
|
+
const resolved = resolveVariantEntries(merged, tokens);
|
|
192
|
+
const init = emitVariantStyleInit(resolved, ' ');
|
|
193
|
+
lines.push(` ${JSON.stringify(variantName)}: ${init.trim()},`);
|
|
194
|
+
}
|
|
195
|
+
lines.push(` ],`);
|
|
196
|
+
}
|
|
197
|
+
lines.push(` ]`);
|
|
198
|
+
lines.push(``);
|
|
199
|
+
|
|
200
|
+
// resolve() falls back to base defaults, not empty style
|
|
201
|
+
lines.push(` static func resolve(_ element: String, variant: String) -> VcVariantStyle {`);
|
|
202
|
+
lines.push(` variants[element]?[variant] ?? defaults[element] ?? VcVariantStyle()`);
|
|
203
|
+
lines.push(` }`);
|
|
204
|
+
lines.push(`}`);
|
|
205
|
+
lines.push(``);
|
|
206
|
+
|
|
207
|
+
return lines.join('\n');
|
|
208
|
+
}
|
|
@@ -45,13 +45,6 @@
|
|
|
45
45
|
"element": "Icon",
|
|
46
46
|
"requiredProperties": ["name"]
|
|
47
47
|
},
|
|
48
|
-
{
|
|
49
|
-
"id": "heading-level-required",
|
|
50
|
-
"severity": "error",
|
|
51
|
-
"description": "Heading requires headingLevel for semantic hierarchy",
|
|
52
|
-
"element": "Heading",
|
|
53
|
-
"requiredProperties": ["headingLevel"]
|
|
54
|
-
},
|
|
55
48
|
{
|
|
56
49
|
"id": "form-no-onclick",
|
|
57
50
|
"severity": "error",
|
package/spec/ui-dsl.meta.json
CHANGED
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
"type": "array",
|
|
53
53
|
"items": { "type": "string" }
|
|
54
54
|
},
|
|
55
|
+
"x-lowering": {
|
|
56
|
+
"type": "string",
|
|
57
|
+
"enum": ["inline-css", "class-name", "attribute", "none"],
|
|
58
|
+
"description": "How this property is lowered: inline-css → static style, class-name → CSS class, attribute → HTML attr, none → behavioral/semantic"
|
|
59
|
+
},
|
|
55
60
|
"x-notes": { "type": "string" }
|
|
56
61
|
}
|
|
57
62
|
},
|