view-contracts 0.3.3 → 0.3.5
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 +6 -6
- package/cli-contract.yaml +1 -1
- package/dist/view-contracts.bundle.mjs +169 -153
- package/dist/view-contracts.bundle.mjs.map +4 -4
- package/docs/cli-reference.md +1 -1
- package/package.json +1 -1
- package/renderers/NATIVE_THEME_EMIT.md +73 -0
- package/renderers/compose/README.md +7 -0
- package/renderers/compose/templates/Screen.kt.hbs +3 -5
- package/renderers/compose/templates/VcTheme.kt.hbs +7 -13
- package/renderers/react/defaults/theme.yaml +7 -0
- package/renderers/react/defaults/tokens.yaml +11 -0
- package/renderers/react/lowering/foldStyle.ts +87 -0
- package/renderers/react/lowering/lowerToJsx.ts +58 -69
- package/renderers/react/renderComponents.ts +180 -24
- package/renderers/react/renderRuntime.ts +36 -43
- package/renderers/react/routes.yaml +2 -2
- package/renderers/react/runtime/index.ts +1 -1
- package/renderers/react/runtime/style.ts +3 -55
- package/renderers/react/runtime/theme.css +1 -1
- package/renderers/react/runtime/types.ts +0 -24
- package/renderers/react/style/foldLiterals.ts +129 -0
- package/renderers/react/syncGeneratedRuntime.ts +35 -47
- package/renderers/react/templates/components-index.ts.hbs +2 -2
- package/renderers/react/templates/runtime.ts.hbs +10 -0
- package/renderers/react/templates/style-helpers.ts.hbs +2 -2
- package/renderers/swiftui/README.md +7 -0
- package/renderers/swiftui/templates/Screen.swift.hbs +3 -5
- package/renderers/swiftui/templates/VcTheme.swift.hbs +8 -16
- package/spec/ui-dsl.schema.json +1331 -345
- package/src/generated/schema/allowed-components.ts +1 -1
- package/src/generated/schema/dsl-enums.ts +1 -1
- package/src/generated/schema/dsl-properties.ts +1 -1
- package/src/generated/schema/index.ts +1 -1
- package/src/generated/schema/lowering-style-properties.ts +9 -2
- package/src/generated/schema/react-renderer-element-config.ts +1 -1
- package/src/generated/schema/schema-document.ts +1 -1
package/docs/cli-reference.md
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Native theme codegen (SwiftUI / Compose)
|
|
2
|
+
|
|
3
|
+
Web と同様、**トークンとテーマはビルド時に解決し、プラットフォーム専用の生成コードとして出力する**。アプリが JSON を読み込む設計は採用しない。
|
|
4
|
+
|
|
5
|
+
## Pipeline(React と同型)
|
|
6
|
+
|
|
7
|
+
```txt
|
|
8
|
+
views/design/tokens.yaml + theme.yaml
|
|
9
|
+
↓ loadValidateDesign() → Token IR + Theme IR(メモリ上のみ)
|
|
10
|
+
↓
|
|
11
|
+
├ react → emitTokenCss + emitDefaultCss + emitVariantCss → generated/react/theme.css
|
|
12
|
+
├ swiftui → emitSwiftUITheme(tokenIR, themeIR) → generated/swiftui/Theme/*.swift
|
|
13
|
+
└ compose → emitComposeTheme(tokenIR, themeIR) → generated/compose/theme/*.kt
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
IR は renderer の **入力**であり、成果物ではない。React が `theme.css` を出すのと同じ関係で、ネイティブは Swift / Kotlin を出す。
|
|
17
|
+
|
|
18
|
+
## 実装方針(変換テンプレートで完結)
|
|
19
|
+
|
|
20
|
+
`src/design/emitDefaultCss.ts` / `emitVariantCss.ts` と同じ Token IR + Theme IR を、Handlebars テンプレートへ渡してコード生成する。
|
|
21
|
+
|
|
22
|
+
| モジュール | テンプレート | 出力例 |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `src/design/emit/swiftui.ts` | `renderers/swiftui/templates/theme/*.hbs` | `VcColors.swift`, `VcButtonStyles.swift` |
|
|
25
|
+
| `src/design/emit/compose.ts` | `renderers/compose/templates/theme/*.hbs` | `VcColors.kt`, `VcButtonModifiers.kt` |
|
|
26
|
+
|
|
27
|
+
### Token IR → 定数
|
|
28
|
+
|
|
29
|
+
`TokenEntry.value` はビルド時に解決済み。テンプレートでは型ごとに写像する。
|
|
30
|
+
|
|
31
|
+
| `$type` | SwiftUI | Compose |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `color` | `Color(red:…)` または `Color("path")` + Asset catalog | `Color(0xFF2563EB)` |
|
|
34
|
+
| `dimension` | `CGFloat` 定数 | `Dp` 定数 |
|
|
35
|
+
| `shadow` | `Shadow` 構造体 | `Shadow` data class |
|
|
36
|
+
| typography | `Font` ヘルパ | `TextStyle` |
|
|
37
|
+
|
|
38
|
+
### Theme IR → variant スタイル
|
|
39
|
+
|
|
40
|
+
`theme.defaults` / `theme.variants` の token ref を、上記定数へ解決してスタイル定義を生成する。
|
|
41
|
+
|
|
42
|
+
| Theme | Web(現行) | SwiftUI | Compose |
|
|
43
|
+
| --- | --- | --- | --- |
|
|
44
|
+
| `Button.primary` | `.vc-button--primary` | `PrimaryButtonStyle` | `Modifier.primaryButton()` |
|
|
45
|
+
| `Text.muted` | `.vc-text--muted` | `VcTextMutedModifier` | `Modifier.vcTextMuted()` |
|
|
46
|
+
|
|
47
|
+
命名規則: `{Component}{VariantPascal}Style` / `Modifier.{variant}{Component}()` — Theme IR から決定論的に導出。
|
|
48
|
+
|
|
49
|
+
### View lowering との接続
|
|
50
|
+
|
|
51
|
+
`lowerToSwiftUI.ts` / `lowerToCompose.ts` は View IR の `variant` prop を **生成済みスタイル名**へ写像するだけ。runtime で theme を解決しない。
|
|
52
|
+
|
|
53
|
+
```swift
|
|
54
|
+
// generated — variant は compile-time class/style 参照
|
|
55
|
+
Button("Save") { }
|
|
56
|
+
.buttonStyle(PrimaryButtonStyle())
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## スキャフォールドとの境界
|
|
60
|
+
|
|
61
|
+
`scaffoldNativeTargets()` が出す `VcTheme.swift` / `VcTheme.kt` は **Screen stub のみ**(native target 未実装時のプレースホルダ)。テーマ本体は `emit/swiftui.ts` / `emit/compose.ts` が `build` 時に上書き生成する。
|
|
62
|
+
|
|
63
|
+
## 参照実装(React)
|
|
64
|
+
|
|
65
|
+
| 関心 | ファイル |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| IR 読み込み | `src/design/index.ts` → `loadValidateDesign()` |
|
|
68
|
+
| Token → CSS 変数 | `src/design/emitTokenCss.ts` |
|
|
69
|
+
| defaults / variants → CSS class | `src/design/emitDefaultCss.ts`, `emitVariantCss.ts` |
|
|
70
|
+
| ビルド統合 | `src/design/buildThemeCss.ts`, `renderers/react/renderRuntime.ts` |
|
|
71
|
+
| variant class(lowering) | `renderers/react/lowering/lowerToJsx.ts` |
|
|
72
|
+
|
|
73
|
+
ネイティブ emit は上記 CSS emit と **同じ IR・同じ解決ロジック**を共有し、出力形式だけテンプレートで差し替える。
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# Compose renderer scaffold
|
|
2
2
|
|
|
3
|
+
**Not implemented yet** beyond theme / screen stubs. View IR lowering and modifier emission are future work.
|
|
4
|
+
|
|
5
|
+
Compose `Modifier` chains are **compositional** (not CSS-style override). view-contracts may later introduce a separate **modifier pipeline** concept (wrapper-style transforms, analogous to nested HTML layout shells) — that is **out of scope for the current phase**. When styling is implemented, defaults + variant + instance props will be **folded at compile time** into resolved values, not chained for cascade-like override. See [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md) § Native renderers.
|
|
6
|
+
|
|
3
7
|
- Maps: `elements.yaml`, `properties.yaml` (hand-maintained under this directory)
|
|
4
8
|
- Routes: `routes.yaml`
|
|
5
9
|
- Templates: `templates/`
|
|
10
|
+
- Theme codegen spec: [../NATIVE_THEME_EMIT.md](../NATIVE_THEME_EMIT.md)
|
|
6
11
|
|
|
7
12
|
Implement Compose code generation by reading View IR + renderer maps.
|
|
13
|
+
Theme is emitted at build time via `src/design/emit/compose.ts` and `templates/theme/*.hbs` — same Token IR + Theme IR as React `theme.css`, not runtime JSON.
|
|
14
|
+
|
|
8
15
|
Vocabulary: `spec/ui-dsl.schema.json` (from `ui-dsl-*.csv`).
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
// {{screenName}} — Jetpack Compose scaffold
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// TODO(Phase C): Load theme.json and emit Modifier.primaryButton(), vcTextMuted(), etc.
|
|
1
|
+
// {{screenName}} — Jetpack Compose scaffold
|
|
2
|
+
// View IR lowering: renderers/compose/lowerToCompose.ts (planned)
|
|
3
|
+
// Theme: renderers/NATIVE_THEME_EMIT.md
|
|
6
4
|
|
|
7
5
|
package {{packageName}}
|
|
8
6
|
|
|
@@ -1,19 +1,13 @@
|
|
|
1
|
-
// VcTheme — Compose theme scaffold
|
|
2
|
-
// Load Token IR + Theme IR from assets:
|
|
3
|
-
// {{tokensJsonPath}}
|
|
4
|
-
// {{themeJsonPath}}
|
|
1
|
+
// VcTheme — Compose theme (scaffold placeholder)
|
|
5
2
|
//
|
|
6
|
-
//
|
|
3
|
+
// Theme is emitted at build time from Token IR + Theme IR via Handlebars templates.
|
|
4
|
+
// See renderers/NATIVE_THEME_EMIT.md — not loaded from JSON at runtime.
|
|
5
|
+
//
|
|
6
|
+
// Implementation: src/design/emit/compose.ts + renderers/compose/templates/theme/*.hbs
|
|
7
|
+
// Output target: generated/compose/theme/
|
|
7
8
|
|
|
8
9
|
package {{packageName}}
|
|
9
10
|
|
|
10
|
-
import android.content.Context
|
|
11
|
-
|
|
12
11
|
object VcTheme {
|
|
13
|
-
|
|
14
|
-
const val THEME_ASSET = "{{themeJsonPath}}"
|
|
15
|
-
|
|
16
|
-
/** Placeholder — read theme.json from assets in Phase C. */
|
|
17
|
-
fun loadThemeJson(context: Context): String? =
|
|
18
|
-
context.assets.open(THEME_ASSET).bufferedReader().use { it.readText() }
|
|
12
|
+
// Populated by emitComposeTheme() when compose target is enabled.
|
|
19
13
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
# Default theme variants — generic vocabulary only (no sample-specific layouts).
|
|
2
|
+
defaults:
|
|
3
|
+
Button:
|
|
4
|
+
background: "{ui.button-secondary}"
|
|
5
|
+
foreground: "{ui.text}"
|
|
6
|
+
radius: "{ui.space.radius-md}"
|
|
7
|
+
padding: "{ui.space.button-padding}"
|
|
8
|
+
fontWeight: "{ui.layout.font-bold}"
|
|
2
9
|
variants:
|
|
3
10
|
Button:
|
|
4
11
|
primary:
|
|
@@ -57,3 +57,14 @@ ui:
|
|
|
57
57
|
font:
|
|
58
58
|
$type: typography
|
|
59
59
|
$value: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
60
|
+
layout:
|
|
61
|
+
font-bold:
|
|
62
|
+
$type: layout
|
|
63
|
+
$value: "700"
|
|
64
|
+
space:
|
|
65
|
+
radius-md:
|
|
66
|
+
$type: dimension
|
|
67
|
+
$value: "10px"
|
|
68
|
+
button-padding:
|
|
69
|
+
$type: dimension
|
|
70
|
+
$value: "10px 14px"
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compile-time style folding for React lowering.
|
|
3
|
+
* Production generated JSX must not call runtime style helpers.
|
|
4
|
+
*/
|
|
5
|
+
import { LOWERING_STYLE_PROPERTIES } from '../../../src/generated/schema/lowering-style-properties.js';
|
|
6
|
+
import type { IrLiteral, IrValue } from '../../../src/ir/types.js';
|
|
7
|
+
import type { ElementMapEntry } from '../elementMap.js';
|
|
8
|
+
import { foldLiteralsToCss, foldedStyleToJsx, type FoldedCss } from '../style/foldLiterals.js';
|
|
9
|
+
|
|
10
|
+
function propLiteral(props: Record<string, IrValue>, key: string): unknown | undefined {
|
|
11
|
+
const value = props[key];
|
|
12
|
+
if (value === null || value === undefined) return undefined;
|
|
13
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
|
|
14
|
+
if (typeof value === 'object' && 'kind' in value && value.kind === 'literal') {
|
|
15
|
+
return (value as IrLiteral).value;
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function assertLiteralStyleProp(key: string, value: IrValue): void {
|
|
21
|
+
if (value === null || value === undefined) return;
|
|
22
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return;
|
|
23
|
+
if (typeof value === 'object' && 'kind' in value && value.kind === 'literal') return;
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Style property "${key}" must be a compile-time literal; ViewModel bindings are not allowed on layout/visual props`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function collectLiteralStyleProps(props: Record<string, IrValue>): Record<string, unknown> {
|
|
30
|
+
const literals: Record<string, unknown> = {};
|
|
31
|
+
for (const [key, value] of Object.entries(props)) {
|
|
32
|
+
if (!LOWERING_STYLE_PROPERTIES.has(key)) continue;
|
|
33
|
+
assertLiteralStyleProp(key, value);
|
|
34
|
+
const lit = propLiteral(props, key);
|
|
35
|
+
if (lit !== undefined) literals[key] = lit;
|
|
36
|
+
}
|
|
37
|
+
return literals;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function foldFlexContainer(entry: ElementMapEntry): FoldedCss {
|
|
41
|
+
if (!entry.flex) return {};
|
|
42
|
+
return {
|
|
43
|
+
display: 'flex',
|
|
44
|
+
flexDirection: entry.flex === 'column' ? 'column' : 'row',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scrollOverflowLiteral(mode: string): FoldedCss {
|
|
49
|
+
if (mode === 'horizontal') return { overflowX: 'auto', overflowY: 'hidden' };
|
|
50
|
+
if (mode === 'both') return { overflowX: 'auto', overflowY: 'auto' };
|
|
51
|
+
if (mode === 'none') return { overflowX: 'hidden', overflowY: 'hidden' };
|
|
52
|
+
return { overflowX: 'hidden', overflowY: 'auto' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function styleAttrForElement(entry: ElementMapEntry, props: Record<string, IrValue>): string {
|
|
56
|
+
const literals = collectLiteralStyleProps(props);
|
|
57
|
+
let folded = foldLiteralsToCss(literals);
|
|
58
|
+
folded = { ...folded, ...foldFlexContainer(entry) };
|
|
59
|
+
|
|
60
|
+
if (entry.scrollContainer) {
|
|
61
|
+
const scrollLit = propLiteral(props, 'scroll');
|
|
62
|
+
const axisLit = propLiteral(props, 'axis');
|
|
63
|
+
const literalMode = typeof scrollLit === 'string'
|
|
64
|
+
? scrollLit
|
|
65
|
+
: typeof axisLit === 'string'
|
|
66
|
+
? (axisLit === 'horizontal' ? 'horizontal' : 'vertical')
|
|
67
|
+
: undefined;
|
|
68
|
+
if (literalMode) {
|
|
69
|
+
folded = { ...folded, ...scrollOverflowLiteral(literalMode) };
|
|
70
|
+
return foldedStyleToJsx(folded);
|
|
71
|
+
}
|
|
72
|
+
throw new Error('ScrollArea scroll/axis must be compile-time literals');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (entry.flexGrowProp && props[entry.flexGrowProp]) {
|
|
76
|
+
const growLit = propLiteral(props, entry.flexGrowProp);
|
|
77
|
+
if (typeof growLit === 'number') {
|
|
78
|
+
folded.flexGrow = growLit;
|
|
79
|
+
return foldedStyleToJsx(folded);
|
|
80
|
+
}
|
|
81
|
+
throw new Error(`"${entry.flexGrowProp}" on <Spacer> must be a compile-time literal`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return foldedStyleToJsx(folded);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { foldLiteralsToCss, foldedStyleToJsx } from '../style/foldLiterals.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ElementMapEntry, ReactElementMap } from '../elementMap.js';
|
|
2
|
-
import {
|
|
2
|
+
import { styleAttrForElement, collectLiteralStyleProps, foldLiteralsToCss, foldedStyleToJsx } from './foldStyle.js';
|
|
3
3
|
import type {
|
|
4
4
|
IrAction,
|
|
5
5
|
IrBinding,
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
IrNode,
|
|
11
11
|
IrTextNode,
|
|
12
12
|
IrValue,
|
|
13
|
+
IrViewRefNode,
|
|
13
14
|
} from '../../../src/ir/types.js';
|
|
14
15
|
|
|
15
16
|
function indent(level: number): string {
|
|
@@ -81,61 +82,9 @@ function propExpr(props: Record<string, IrValue>, key: string): string | undefin
|
|
|
81
82
|
return renderValue(value, 0);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
function
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
const body = entries.map(([key, val]) => `${key}: ${renderValue(val, 0)}`).join(', ');
|
|
88
|
-
return `{ ${body} }`;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function extraFlexStyle(entry: ElementMapEntry, props: Record<string, IrValue>): string {
|
|
92
|
-
if (!entry.flex) return '';
|
|
93
|
-
const parts = [`display: 'flex'`, `flexDirection: '${entry.flex === 'column' ? 'column' : 'row'}'`];
|
|
94
|
-
if (props.align) parts.push(`alignItems: mapAlign(${propExpr(props, 'align')})`);
|
|
95
|
-
if (props.justify) parts.push(`justifyContent: mapJustify(${propExpr(props, 'justify')})`);
|
|
96
|
-
return `, ${parts.join(', ')}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function scrollOverflowLiteral(mode: string): { overflowX: string; overflowY: string } {
|
|
100
|
-
if (mode === 'horizontal') return { overflowX: 'auto', overflowY: 'hidden' };
|
|
101
|
-
if (mode === 'both') return { overflowX: 'auto', overflowY: 'auto' };
|
|
102
|
-
if (mode === 'none') return { overflowX: 'hidden', overflowY: 'hidden' };
|
|
103
|
-
return { overflowX: 'hidden', overflowY: 'auto' };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function styleAttr(entry: ElementMapEntry, props: Record<string, IrValue>): string {
|
|
107
|
-
const base = collectStyleProps(props);
|
|
108
|
-
const flex = extraFlexStyle(entry, props);
|
|
109
|
-
if (entry.scrollContainer) {
|
|
110
|
-
const scrollLit = propLiteral(props, 'scroll');
|
|
111
|
-
const axisLit = propLiteral(props, 'axis');
|
|
112
|
-
const literalMode = typeof scrollLit === 'string'
|
|
113
|
-
? scrollLit
|
|
114
|
-
: typeof axisLit === 'string'
|
|
115
|
-
? (axisLit === 'horizontal' ? 'horizontal' : 'vertical')
|
|
116
|
-
: undefined;
|
|
117
|
-
if (literalMode) {
|
|
118
|
-
const { overflowX, overflowY } = scrollOverflowLiteral(literalMode);
|
|
119
|
-
return `style={{ ...commonStyle(${base})${flex}, overflowX: '${overflowX}', overflowY: '${overflowY}' }}`;
|
|
120
|
-
}
|
|
121
|
-
const scroll = propExpr(props, 'scroll');
|
|
122
|
-
const axis = propExpr(props, 'axis');
|
|
123
|
-
const modeExpr = scroll
|
|
124
|
-
?? (axis ? `(${axis} === 'horizontal' ? 'horizontal' : 'vertical')` : `'vertical'`);
|
|
125
|
-
return `style={{ ...commonStyle(${base})${flex}, ...(() => {
|
|
126
|
-
const __scroll = ${modeExpr};
|
|
127
|
-
if (__scroll === 'horizontal') return { overflowX: 'auto', overflowY: 'hidden' };
|
|
128
|
-
if (__scroll === 'both') return { overflowX: 'auto', overflowY: 'auto' };
|
|
129
|
-
if (__scroll === 'none') return { overflowX: 'hidden', overflowY: 'hidden' };
|
|
130
|
-
return { overflowX: 'hidden', overflowY: 'auto' };
|
|
131
|
-
})() }}`;
|
|
132
|
-
}
|
|
133
|
-
if (entry.flexGrowProp && props[entry.flexGrowProp]) {
|
|
134
|
-
const grow = propExpr(props, entry.flexGrowProp);
|
|
135
|
-
return `style={{ ...commonStyle(${base})${flex}, flexGrow: ${grow} ?? 1 }}`;
|
|
136
|
-
}
|
|
137
|
-
if (flex) return `style={{ ...commonStyle(${base})${flex} }}`;
|
|
138
|
-
return `style={commonStyle(${base})}`;
|
|
85
|
+
function pushStyleAttr(attrs: string[], entry: ElementMapEntry, props: Record<string, IrValue>): void {
|
|
86
|
+
const style = styleAttrForElement(entry, props);
|
|
87
|
+
if (style) attrs.push(style);
|
|
139
88
|
}
|
|
140
89
|
|
|
141
90
|
function actionHandler(props: Record<string, IrValue>, event: 'onClick' | 'onSubmit'): string | undefined {
|
|
@@ -267,19 +216,19 @@ function emitIcon(node: IrComponentNode, entry: ElementMapEntry, level: number):
|
|
|
267
216
|
if (hasAlt) {
|
|
268
217
|
const attrs: string[] = [
|
|
269
218
|
classNameAttr(node.name, entry, props),
|
|
270
|
-
styleAttr(entry, props),
|
|
271
219
|
`src={${propExpr(props, 'name') ?? '""'}}`,
|
|
272
220
|
`alt={${propExpr(props, 'alt') ?? '""'}}`,
|
|
273
221
|
];
|
|
222
|
+
pushStyleAttr(attrs, entry, props);
|
|
274
223
|
if (props.id) attrs.push(`id={${propExpr(props, 'id')}}`);
|
|
275
224
|
return `${indent(level)}<img ${attrs.join(' ')} />`;
|
|
276
225
|
}
|
|
277
226
|
|
|
278
227
|
const attrs: string[] = [
|
|
279
228
|
classNameAttr(node.name, entry, props),
|
|
280
|
-
styleAttr(entry, props),
|
|
281
229
|
`aria-hidden`,
|
|
282
230
|
];
|
|
231
|
+
pushStyleAttr(attrs, entry, props);
|
|
283
232
|
if (props.id) attrs.push(`id={${propExpr(props, 'id')}}`);
|
|
284
233
|
if (props.name) attrs.push(`data-icon={${propExpr(props, 'name')}}`);
|
|
285
234
|
return `${indent(level)}<span ${attrs.join(' ')} />`;
|
|
@@ -302,7 +251,8 @@ function emitVoidInput(
|
|
|
302
251
|
props: Record<string, IrValue>,
|
|
303
252
|
level: number,
|
|
304
253
|
): string {
|
|
305
|
-
const attrs: string[] = [classNameAttr(elementName, entry, props)
|
|
254
|
+
const attrs: string[] = [classNameAttr(elementName, entry, props)];
|
|
255
|
+
pushStyleAttr(attrs, entry, props);
|
|
306
256
|
if (entry.ariaHidden) attrs.push('aria-hidden');
|
|
307
257
|
if (props.id) attrs.push(`id={${propExpr(props, 'id')}}`);
|
|
308
258
|
if (props.role) attrs.push(`role={${propExpr(props, 'role')}}`);
|
|
@@ -350,7 +300,7 @@ function emitOpenTag(
|
|
|
350
300
|
if (entry.tag === 'a' && props.href) attrs.push(`href={${propExpr(props, 'href')}}`);
|
|
351
301
|
if (entry.tag === 'button') attrs.push(`type={${propExpr(props, 'type') ?? '"button"'}}`);
|
|
352
302
|
attrs.push(classNameAttr(elementName, entry, props, headingLevel));
|
|
353
|
-
attrs
|
|
303
|
+
pushStyleAttr(attrs, entry, props);
|
|
354
304
|
return `<${tag} ${attrs.join(' ')}`;
|
|
355
305
|
}
|
|
356
306
|
|
|
@@ -360,7 +310,7 @@ function visibilityGuard(inner: string, props: Record<string, IrValue>, level: n
|
|
|
360
310
|
return `${indent(level)}{${vis} !== false ? (\n${inner}\n${indent(level)}) : null}`;
|
|
361
311
|
}
|
|
362
312
|
|
|
363
|
-
type NodeRenderer = (node:
|
|
313
|
+
type NodeRenderer = (node: IrNode, level: number, itemScope?: string) => string;
|
|
364
314
|
|
|
365
315
|
function renderChildren(
|
|
366
316
|
children: IrNode[],
|
|
@@ -382,15 +332,14 @@ function renderChildren(
|
|
|
382
332
|
if (child.kind === 'fragment') {
|
|
383
333
|
return renderChildren(child.children, level, renderNested, itemScope);
|
|
384
334
|
}
|
|
385
|
-
|
|
386
|
-
return '';
|
|
335
|
+
return renderNested(child, level, itemScope);
|
|
387
336
|
}).filter(Boolean);
|
|
388
337
|
return parts.join('\n');
|
|
389
338
|
}
|
|
390
339
|
|
|
391
340
|
function renderMap(node: IrMapNode, level: number, renderNested: NodeRenderer): string {
|
|
392
341
|
const item = node.item;
|
|
393
|
-
let body = renderNested(node.body
|
|
342
|
+
let body = renderNested(node.body, level + 1, item);
|
|
394
343
|
if (node.key) {
|
|
395
344
|
const keyAttr = ` key={${bindingExpr(node.key, item)}}`;
|
|
396
345
|
body = body.replace(/^( +)<(\w+)/, `$1<$2${keyAttr}`);
|
|
@@ -427,9 +376,9 @@ function lowerVocabularyElement(
|
|
|
427
376
|
if (node.name === 'Spinner') {
|
|
428
377
|
const attrs = [
|
|
429
378
|
classNameAttr(node.name, entry, props),
|
|
430
|
-
styleAttr(entry, props),
|
|
431
379
|
'aria-busy="true"',
|
|
432
380
|
];
|
|
381
|
+
pushStyleAttr(attrs, entry, props);
|
|
433
382
|
if (props.id) attrs.push(`id={${propExpr(props, 'id')}}`);
|
|
434
383
|
if (props.role) attrs.push(`role={${propExpr(props, 'role')}}`);
|
|
435
384
|
else if (entry.defaultRole) attrs.push(`role="${entry.defaultRole}"`);
|
|
@@ -465,10 +414,13 @@ function lowerVocabularyElement(
|
|
|
465
414
|
return visibilityGuard(body, props, level);
|
|
466
415
|
}
|
|
467
416
|
const levelExpr = propExpr(props, 'level') ?? '2';
|
|
417
|
+
const foldedStyle = foldedStyleToJsx(foldLiteralsToCss(collectLiteralStyleProps(props)));
|
|
418
|
+
const styleFragment = foldedStyle ? ` ${foldedStyle}` : '';
|
|
468
419
|
const inner = childContent
|
|
469
|
-
? `${indent(level + 1)}{(() => { const __level = Math.min(6, Math.max(1, ${levelExpr} ?? 2)); const Tag = ('h' + __level) as keyof JSX.IntrinsicElements; return <Tag className={\`vc-heading vc-heading-\${__level}\`}
|
|
420
|
+
? `${indent(level + 1)}{(() => { const __level = Math.min(6, Math.max(1, ${levelExpr} ?? 2)); const Tag = ('h' + __level) as keyof JSX.IntrinsicElements; return <Tag className={\`vc-heading vc-heading-\${__level}\`}${styleFragment}>{/* children below */}</Tag>; })()}`
|
|
470
421
|
: '';
|
|
471
|
-
|
|
422
|
+
const fallbackStyle = styleAttrForElement(entry, props);
|
|
423
|
+
return visibilityGuard(inner || `${indent(level)}<h2 ${classNameAttr(node.name, entry, props, 2)}${fallbackStyle ? ` ${fallbackStyle}` : ''} />`, props, level);
|
|
472
424
|
}
|
|
473
425
|
|
|
474
426
|
const tag = entry.tag;
|
|
@@ -488,6 +440,22 @@ function lowerVocabularyElement(
|
|
|
488
440
|
return visibilityGuard(body, props, level);
|
|
489
441
|
}
|
|
490
442
|
|
|
443
|
+
function renderViewRefProps(props: Record<string, IrValue>, itemScope?: string): string {
|
|
444
|
+
const vmRef = itemScope ?? 'vm';
|
|
445
|
+
return Object.entries(props).map(([key, value]) => {
|
|
446
|
+
if (typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'binding') {
|
|
447
|
+
return `${key}={${bindingExpr(value as IrBinding, vmRef)}}`;
|
|
448
|
+
}
|
|
449
|
+
return `${key}={${renderValue(value, 0)}}`;
|
|
450
|
+
}).join(' ');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function renderViewRefElement(node: IrViewRefNode, level: number, itemScope?: string): string {
|
|
454
|
+
const props = renderViewRefProps(node.props, itemScope);
|
|
455
|
+
const propStr = props ? ` ${props}` : '';
|
|
456
|
+
return `${indent(level)}<${node.name}${propStr} />`;
|
|
457
|
+
}
|
|
458
|
+
|
|
491
459
|
function renderExtensionProps(props: Record<string, IrValue>, itemScope?: string): string {
|
|
492
460
|
const vmRef = itemScope ?? 'vm';
|
|
493
461
|
return Object.entries(props).map(([key, value]) => {
|
|
@@ -521,6 +489,21 @@ function isVocabulary(name: string, map: ReactElementMap, allowlistExtra: string
|
|
|
521
489
|
return name in map.elements && !allowlistExtra.includes(name);
|
|
522
490
|
}
|
|
523
491
|
|
|
492
|
+
export function collectViewRefComponents(node: IrNode, names: Set<string>): void {
|
|
493
|
+
if (node.kind === 'viewRef') {
|
|
494
|
+
names.add(node.name);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (node.kind === 'component') {
|
|
498
|
+
for (const child of node.children) collectViewRefComponents(child, names);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (node.kind === 'map') collectViewRefComponents(node.body, names);
|
|
502
|
+
if (node.kind === 'fragment') {
|
|
503
|
+
for (const child of node.children) collectViewRefComponents(child, names);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
524
507
|
export function collectExtensionComponents(
|
|
525
508
|
node: IrNode,
|
|
526
509
|
map: ReactElementMap,
|
|
@@ -541,12 +524,18 @@ export function collectExtensionComponents(
|
|
|
541
524
|
export function createNodeRenderer(
|
|
542
525
|
map: ReactElementMap,
|
|
543
526
|
allowlistExtra: string[],
|
|
544
|
-
):
|
|
527
|
+
): (node: IrComponentNode, level: number, itemScope?: string) => string {
|
|
545
528
|
const renderNested: NodeRenderer = (node, level, itemScope) => {
|
|
529
|
+
if (node.kind === 'viewRef') {
|
|
530
|
+
return renderViewRefElement(node, level, itemScope);
|
|
531
|
+
}
|
|
532
|
+
if (node.kind !== 'component') {
|
|
533
|
+
return '';
|
|
534
|
+
}
|
|
546
535
|
if (isVocabulary(node.name, map, allowlistExtra)) {
|
|
547
536
|
return lowerVocabularyElement(node, map.elements[node.name]!, level, itemScope, renderNested);
|
|
548
537
|
}
|
|
549
538
|
return renderExtensionElement(node, level, itemScope, renderNested);
|
|
550
539
|
};
|
|
551
|
-
return renderNested;
|
|
540
|
+
return (node, level, itemScope) => renderNested(node, level, itemScope);
|
|
552
541
|
}
|