view-contracts 0.3.5 → 0.4.1
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 +152 -0
- package/dist/preview.mjs.map +7 -0
- package/dist/view-contracts.bundle.mjs +457 -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 +52 -0
- package/renderers/react/defaults/tokens.yaml +76 -1
- 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 +21 -40
- package/renderers/react/paths.ts +5 -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/structural.css +38 -0
- package/renderers/react/style/foldLiterals.ts +25 -4
- 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 +761 -0
- package/renderers/swiftui/lowering/modifiers.ts +221 -0
- package/renderers/swiftui/renderComponents.ts +79 -0
- package/renderers/swiftui/renderRuntime.ts +139 -0
- package/renderers/swiftui/renderTheme.ts +274 -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/renderers/react/runtime/theme.css +0 -65
- 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
package/docs/cli-reference.md
CHANGED
|
@@ -13,6 +13,7 @@ Contract-first UI design toolchain. Compiles restricted view-contract TSX to lan
|
|
|
13
13
|
- [build](#view-contracts-build)
|
|
14
14
|
- [contracts.generate](#view-contracts-contracts-generate)
|
|
15
15
|
- [contracts.check](#view-contracts-contracts-check)
|
|
16
|
+
- [preview](#view-contracts-preview)
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -200,3 +201,38 @@ view-contracts contracts check -c view-contracts.config.yaml
|
|
|
200
201
|
**Exit 1:** Lint failed.
|
|
201
202
|
|
|
202
203
|
---
|
|
204
|
+
|
|
205
|
+
### preview
|
|
206
|
+
|
|
207
|
+
Start DSL preview HTTP server.
|
|
208
|
+
|
|
209
|
+
Compiles views/*.view.tsx on demand and serves a React preview over HTTP. No Vite setup required in the consumer project.
|
|
210
|
+
|
|
211
|
+
**Usage:**
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
view-contracts preview
|
|
215
|
+
```
|
|
216
|
+
```
|
|
217
|
+
view-contracts preview -c view-contracts.config.yaml
|
|
218
|
+
```
|
|
219
|
+
```
|
|
220
|
+
view-contracts preview --port 3000 --screen AdminDashboard
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Options
|
|
224
|
+
|
|
225
|
+
| Option | Aliases | Required | Default | Description |
|
|
226
|
+
|---|---|---|---|---|
|
|
227
|
+
| `--config` | -c | No | | Path to view-contracts.config.yaml (discovered upward from cwd when omitted). |
|
|
228
|
+
| `--port` | -p | No | | HTTP listen port. |
|
|
229
|
+
| `--host` | | No | | HTTP listen host. |
|
|
230
|
+
| `--screen` | | No | | Screen name from view-bindings.yaml (default first react target). |
|
|
231
|
+
|
|
232
|
+
#### Exit Codes
|
|
233
|
+
|
|
234
|
+
**Exit 0:** Server stopped normally.
|
|
235
|
+
|
|
236
|
+
**Exit 1:** Startup or compile failed.
|
|
237
|
+
|
|
238
|
+
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "view-contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Contract-first UI design toolchain — restricted TSX to View IR to platform renderers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -9,9 +9,18 @@
|
|
|
9
9
|
"bin": {
|
|
10
10
|
"view-contracts": "dist/view-contracts.bundle.mjs"
|
|
11
11
|
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/view-contracts.bundle.mjs",
|
|
14
|
+
"./preview": "./dist/preview.mjs",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
12
17
|
"files": [
|
|
13
18
|
"dist/view-contracts.bundle.mjs",
|
|
14
19
|
"dist/view-contracts.bundle.mjs.map",
|
|
20
|
+
"dist/preview.mjs",
|
|
21
|
+
"dist/preview.mjs.map",
|
|
22
|
+
"dist/preview-style-helpers.js",
|
|
23
|
+
"dist/preview-style-helpers.js.map",
|
|
15
24
|
"renderers/react/runtime",
|
|
16
25
|
"renderers/react/defaults",
|
|
17
26
|
"src/generated/schema",
|
|
@@ -36,6 +45,7 @@
|
|
|
36
45
|
"bundle": "node esbuild.bundle.mjs",
|
|
37
46
|
"bundle:min": "node esbuild.bundle.mjs --minify",
|
|
38
47
|
"build:views": "tsx src/cli.ts build -c examples/admin-dashboard/view-contracts.config.yaml",
|
|
48
|
+
"render:swiftui-prototype": "tsx scripts/render-swiftui-prototype.ts",
|
|
39
49
|
"build:example": "vite build --config examples/admin-dashboard/.vite/vite.config.ts",
|
|
40
50
|
"compile": "tsx src/cli.ts compile -c examples/admin-dashboard/view-contracts.config.yaml",
|
|
41
51
|
"render:react": "tsx src/cli.ts render react -c examples/admin-dashboard/view-contracts.config.yaml",
|
|
@@ -74,8 +84,25 @@
|
|
|
74
84
|
"engines": {
|
|
75
85
|
"node": ">=20.0.0"
|
|
76
86
|
},
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"vite": "^5.0.0 || ^6.0.0",
|
|
89
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
90
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
91
|
+
},
|
|
92
|
+
"peerDependenciesMeta": {
|
|
93
|
+
"vite": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"react": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
99
|
+
"react-dom": {
|
|
100
|
+
"optional": true
|
|
101
|
+
}
|
|
102
|
+
},
|
|
77
103
|
"dependencies": {
|
|
78
104
|
"commander": "^14.0.3",
|
|
105
|
+
"esbuild": "^0.28.0",
|
|
79
106
|
"handlebars": "^4.7.9",
|
|
80
107
|
"js-yaml": "^4.1.1",
|
|
81
108
|
"micro-contracts": "^0.17.10",
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
|
|
7
|
+
function composeTemplatesDir(): string {
|
|
8
|
+
return resolve(packageRoot(), 'renderers/compose/templates');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function renderComposeRuntime(
|
|
12
|
+
config: ViewContractsConfig,
|
|
13
|
+
bindings: ViewBindingsFile,
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
const composeDir = resolve(config.generated.reactDir, '../compose');
|
|
16
|
+
await mkdir(composeDir, { recursive: true });
|
|
17
|
+
|
|
18
|
+
await writeFile(
|
|
19
|
+
resolve(composeDir, 'VcTheme.kt'),
|
|
20
|
+
await renderTemplate(composeTemplatesDir(), 'VcTheme.kt.hbs', {
|
|
21
|
+
packageName: 'generated.compose',
|
|
22
|
+
}),
|
|
23
|
+
'utf-8',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
for (const view of bindings.views) {
|
|
27
|
+
if (view.targets.includes('compose')) {
|
|
28
|
+
const out = resolve(composeDir, `${view.screenName}Screen.kt`);
|
|
29
|
+
await writeFile(
|
|
30
|
+
out,
|
|
31
|
+
await renderTemplate(composeTemplatesDir(), 'Screen.kt.hbs', {
|
|
32
|
+
screenName: view.screenName,
|
|
33
|
+
packageName: 'generated.compose',
|
|
34
|
+
}),
|
|
35
|
+
'utf-8',
|
|
36
|
+
);
|
|
37
|
+
console.log(` compose → ${out}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -6,6 +6,44 @@ defaults:
|
|
|
6
6
|
radius: "{ui.space.radius-md}"
|
|
7
7
|
padding: "{ui.space.button-padding}"
|
|
8
8
|
fontWeight: "{ui.layout.font-bold}"
|
|
9
|
+
TextInput:
|
|
10
|
+
background: "{ui.surface}"
|
|
11
|
+
foreground: "{ui.text}"
|
|
12
|
+
radius: "{ui.space.radius-md}"
|
|
13
|
+
padding: "{ui.space.input-padding}"
|
|
14
|
+
border: "{ui.layout.border-1}"
|
|
15
|
+
borderColor: "{ui.border}"
|
|
16
|
+
minHeight: "{ui.space.input-min-height}"
|
|
17
|
+
TextArea:
|
|
18
|
+
background: "{ui.surface}"
|
|
19
|
+
foreground: "{ui.text}"
|
|
20
|
+
radius: "{ui.space.radius-md}"
|
|
21
|
+
padding: "{ui.space.textarea-padding}"
|
|
22
|
+
border: "{ui.layout.border-1}"
|
|
23
|
+
borderColor: "{ui.border}"
|
|
24
|
+
Select:
|
|
25
|
+
background: "{ui.surface}"
|
|
26
|
+
foreground: "{ui.text}"
|
|
27
|
+
radius: "{ui.space.radius-md}"
|
|
28
|
+
padding: "{ui.space.select-padding}"
|
|
29
|
+
border: "{ui.layout.border-1}"
|
|
30
|
+
borderColor: "{ui.border}"
|
|
31
|
+
minHeight: "{ui.space.input-min-height}"
|
|
32
|
+
TableHeader:
|
|
33
|
+
background: "{ui.surface-muted}"
|
|
34
|
+
alignItems: "{ui.layout.align-center}"
|
|
35
|
+
TableColumn:
|
|
36
|
+
padding: "{ui.space.table-column-padding}"
|
|
37
|
+
fontSize: "{ui.space.table-column-font-size}"
|
|
38
|
+
fontWeight: "{ui.layout.font-semibold}"
|
|
39
|
+
foreground: "{ui.text-muted}"
|
|
40
|
+
TableCell:
|
|
41
|
+
padding: "{ui.space.table-cell-padding}"
|
|
42
|
+
fontSize: "{ui.space.table-cell-font-size}"
|
|
43
|
+
TableRow:
|
|
44
|
+
alignItems: "{ui.layout.align-center}"
|
|
45
|
+
Box:
|
|
46
|
+
width: "{ui.layout.width-full}"
|
|
9
47
|
variants:
|
|
10
48
|
Button:
|
|
11
49
|
primary:
|
|
@@ -23,6 +61,16 @@ variants:
|
|
|
23
61
|
Text:
|
|
24
62
|
muted:
|
|
25
63
|
foreground: "{ui.text-muted}"
|
|
64
|
+
field-label:
|
|
65
|
+
fontSize: "{ui.space.font-field-label}"
|
|
66
|
+
fontWeight: "{ui.layout.font-bold}"
|
|
67
|
+
foreground: "{ui.text}"
|
|
68
|
+
field-help:
|
|
69
|
+
fontSize: "{ui.space.font-field-help}"
|
|
70
|
+
foreground: "{ui.text-muted}"
|
|
71
|
+
field-error:
|
|
72
|
+
fontSize: "{ui.space.font-field-help}"
|
|
73
|
+
foreground: "{ui.danger}"
|
|
26
74
|
primary:
|
|
27
75
|
foreground: "{ui.primary}"
|
|
28
76
|
success:
|
|
@@ -31,3 +79,7 @@ variants:
|
|
|
31
79
|
foreground: "{ui.warning}"
|
|
32
80
|
danger:
|
|
33
81
|
foreground: "{ui.danger}"
|
|
82
|
+
Box:
|
|
83
|
+
card:
|
|
84
|
+
background: "{ui.surface}"
|
|
85
|
+
width: "{ui.layout.width-full}"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Default design tokens —
|
|
1
|
+
# Default design tokens — merged into generated/react/theme.css via emitTokenCss().
|
|
2
2
|
ui:
|
|
3
3
|
bg:
|
|
4
4
|
$type: color
|
|
@@ -61,6 +61,18 @@ ui:
|
|
|
61
61
|
font-bold:
|
|
62
62
|
$type: layout
|
|
63
63
|
$value: "700"
|
|
64
|
+
font-semibold:
|
|
65
|
+
$type: layout
|
|
66
|
+
$value: "600"
|
|
67
|
+
border-1:
|
|
68
|
+
$type: layout
|
|
69
|
+
$value: "1px solid"
|
|
70
|
+
align-center:
|
|
71
|
+
$type: layout
|
|
72
|
+
$value: "center"
|
|
73
|
+
width-full:
|
|
74
|
+
$type: layout
|
|
75
|
+
$value: "100%"
|
|
64
76
|
space:
|
|
65
77
|
radius-md:
|
|
66
78
|
$type: dimension
|
|
@@ -68,3 +80,66 @@ ui:
|
|
|
68
80
|
button-padding:
|
|
69
81
|
$type: dimension
|
|
70
82
|
$value: "10px 14px"
|
|
83
|
+
input-padding:
|
|
84
|
+
$type: dimension
|
|
85
|
+
$value: "9px 12px"
|
|
86
|
+
input-min-height:
|
|
87
|
+
$type: dimension
|
|
88
|
+
$value: "42px"
|
|
89
|
+
select-padding:
|
|
90
|
+
$type: dimension
|
|
91
|
+
$value: "9px 36px 9px 12px"
|
|
92
|
+
textarea-padding:
|
|
93
|
+
$type: dimension
|
|
94
|
+
$value: "10px 12px"
|
|
95
|
+
field-gap:
|
|
96
|
+
$type: dimension
|
|
97
|
+
$value: "6px"
|
|
98
|
+
field-inner-gap:
|
|
99
|
+
$type: dimension
|
|
100
|
+
$value: "4px"
|
|
101
|
+
checkbox-gap:
|
|
102
|
+
$type: dimension
|
|
103
|
+
$value: "8px"
|
|
104
|
+
list-indent:
|
|
105
|
+
$type: dimension
|
|
106
|
+
$value: "20px"
|
|
107
|
+
list-item-padding:
|
|
108
|
+
$type: dimension
|
|
109
|
+
$value: "2px 0"
|
|
110
|
+
line-height-body:
|
|
111
|
+
$type: dimension
|
|
112
|
+
$value: "1.6"
|
|
113
|
+
line-height-heading:
|
|
114
|
+
$type: dimension
|
|
115
|
+
$value: "1.2"
|
|
116
|
+
font-field-label:
|
|
117
|
+
$type: dimension
|
|
118
|
+
$value: "13px"
|
|
119
|
+
font-field-help:
|
|
120
|
+
$type: dimension
|
|
121
|
+
$value: "12px"
|
|
122
|
+
table-column-padding:
|
|
123
|
+
$type: dimension
|
|
124
|
+
$value: "12px 14px"
|
|
125
|
+
table-column-font-size:
|
|
126
|
+
$type: dimension
|
|
127
|
+
$value: "12px"
|
|
128
|
+
table-cell-padding:
|
|
129
|
+
$type: dimension
|
|
130
|
+
$value: "14px"
|
|
131
|
+
table-cell-font-size:
|
|
132
|
+
$type: dimension
|
|
133
|
+
$value: "14px"
|
|
134
|
+
input-min-height-sm:
|
|
135
|
+
$type: dimension
|
|
136
|
+
$value: "34px"
|
|
137
|
+
input-min-height-lg:
|
|
138
|
+
$type: dimension
|
|
139
|
+
$value: "50px"
|
|
140
|
+
input-padding-sm:
|
|
141
|
+
$type: dimension
|
|
142
|
+
$value: "6px 10px"
|
|
143
|
+
input-padding-lg:
|
|
144
|
+
$type: dimension
|
|
145
|
+
$value: "12px 14px"
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import { ALLOWED_COMPONENTS } from '../../src/
|
|
3
|
-
import
|
|
2
|
+
import { ALLOWED_COMPONENTS } from '../../src/schema/dsl-catalog.js';
|
|
3
|
+
import { ELEMENT_CATALOG } from '../../src/schema/dsl-catalog.js';
|
|
4
4
|
import { packageRoot } from '../../src/lib/packageRoot.js';
|
|
5
5
|
import {
|
|
6
6
|
loadYamlFile,
|
|
7
7
|
validateMapEntryFields,
|
|
8
8
|
validateVocabularyElementNames,
|
|
9
9
|
} from '../../src/renderer/elementMapValidation.js';
|
|
10
|
-
import { loadUiDslSchema } from '../../src/schema/loadSchema.js';
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
/** Shape of entries in renderers/react/elements.yaml (renderer-owned SSoT). */
|
|
12
|
+
export interface ReactRendererElementConfig {
|
|
13
|
+
tag: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
ariaLabel?: boolean;
|
|
16
|
+
ariaHidden?: boolean;
|
|
17
|
+
flex?: 'row' | 'column';
|
|
18
|
+
wrapProp?: string;
|
|
19
|
+
flexGrowProp?: string;
|
|
20
|
+
void?: boolean;
|
|
21
|
+
actionEvent?: 'onClick' | 'onSubmit';
|
|
22
|
+
fieldWrap?: boolean;
|
|
23
|
+
inputType?: string;
|
|
24
|
+
sizeClass?: string;
|
|
25
|
+
optionsProp?: string;
|
|
26
|
+
checkbox?: boolean;
|
|
27
|
+
fieldContainer?: boolean;
|
|
28
|
+
scrollContainer?: boolean;
|
|
29
|
+
defaultRole?: string;
|
|
30
|
+
tableCell?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ReactElementMapFile {
|
|
34
|
+
elements: Record<string, ReactRendererElementConfig>;
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
export type ReactElementMap = ReactElementMapFile;
|
|
14
38
|
export type ElementMapEntry = ReactRendererElementConfig;
|
|
15
39
|
|
|
@@ -41,8 +65,8 @@ export function isLowerableVocabularyElement(
|
|
|
41
65
|
}
|
|
42
66
|
|
|
43
67
|
export async function validateElementMapCoversVocabulary(customPath?: string): Promise<void> {
|
|
44
|
-
const
|
|
45
|
-
const missing = Object.keys(
|
|
68
|
+
const map = await loadReactElementMap(customPath);
|
|
69
|
+
const missing = Object.keys(ELEMENT_CATALOG).filter((name) => !(name in map.elements));
|
|
46
70
|
if (missing.length > 0) {
|
|
47
71
|
throw new Error(
|
|
48
72
|
`${SOURCE_LABEL}: ${missing.length} DSL element(s) have no React HTML map yet: ${missing.join(', ')}`,
|
|
@@ -45,11 +45,6 @@ elements:
|
|
|
45
45
|
tag: hr
|
|
46
46
|
className: vc-divider
|
|
47
47
|
void: true
|
|
48
|
-
Heading:
|
|
49
|
-
tag: dynamic-heading
|
|
50
|
-
className: vc-heading
|
|
51
|
-
levelProp: level
|
|
52
|
-
defaultLevel: 2
|
|
53
48
|
Text:
|
|
54
49
|
tag: p
|
|
55
50
|
className: vc-text
|
|
@@ -140,3 +135,20 @@ elements:
|
|
|
140
135
|
tag: li
|
|
141
136
|
className: vc-list-item
|
|
142
137
|
defaultRole: listitem
|
|
138
|
+
Table:
|
|
139
|
+
tag: table
|
|
140
|
+
className: vc-table
|
|
141
|
+
TableHeader:
|
|
142
|
+
tag: thead
|
|
143
|
+
className: vc-table-header
|
|
144
|
+
TableColumn:
|
|
145
|
+
tag: th
|
|
146
|
+
className: vc-table-column
|
|
147
|
+
tableCell: true
|
|
148
|
+
TableRow:
|
|
149
|
+
tag: tr
|
|
150
|
+
className: vc-table-row
|
|
151
|
+
TableCell:
|
|
152
|
+
tag: td
|
|
153
|
+
className: vc-table-cell
|
|
154
|
+
tableCell: true
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Compile-time style folding for React lowering.
|
|
3
3
|
* Production generated JSX must not call runtime style helpers.
|
|
4
4
|
*/
|
|
5
|
-
import { LOWERING_STYLE_PROPERTIES } from '../../../src/
|
|
5
|
+
import { LOWERING_STYLE_PROPERTIES } from '../../../src/schema/dsl-catalog.js';
|
|
6
6
|
import type { IrLiteral, IrValue } from '../../../src/ir/types.js';
|
|
7
7
|
import type { ElementMapEntry } from '../elementMap.js';
|
|
8
8
|
import { foldLiteralsToCss, foldedStyleToJsx, type FoldedCss } from '../style/foldLiterals.js';
|
|
@@ -57,6 +57,18 @@ export function styleAttrForElement(entry: ElementMapEntry, props: Record<string
|
|
|
57
57
|
let folded = foldLiteralsToCss(literals);
|
|
58
58
|
folded = { ...folded, ...foldFlexContainer(entry) };
|
|
59
59
|
|
|
60
|
+
if (entry.tableCell) {
|
|
61
|
+
if ('alignItems' in folded) {
|
|
62
|
+
const ai = String(folded.alignItems);
|
|
63
|
+
folded.textAlign = ai === 'flex-end' ? 'right' : ai === 'center' ? 'center' : 'left';
|
|
64
|
+
delete folded.alignItems;
|
|
65
|
+
}
|
|
66
|
+
if ('width' in folded && !('flex' in folded)) {
|
|
67
|
+
folded.flexShrink = 0;
|
|
68
|
+
folded.flexGrow = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
60
72
|
if (entry.scrollContainer) {
|
|
61
73
|
const scrollLit = propLiteral(props, 'scroll');
|
|
62
74
|
const axisLit = propLiteral(props, 'axis');
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
IrAction,
|
|
5
5
|
IrBinding,
|
|
6
6
|
IrComponentNode,
|
|
7
|
+
IrConcat,
|
|
7
8
|
IrFragmentNode,
|
|
8
9
|
IrLiteral,
|
|
9
10
|
IrMapNode,
|
|
@@ -22,6 +23,14 @@ function bindingExpr(binding: IrBinding, vmRef = 'vm'): string {
|
|
|
22
23
|
return binding.path ? `${root}.${binding.path}` : root;
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
function concatSegments(concat: IrConcat): string {
|
|
27
|
+
return concat.parts.map(p => typeof p === 'string' ? p : `\${${bindingExpr(p)}}`).join('');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function renderConcat(concat: IrConcat): string {
|
|
31
|
+
return `\`${concatSegments(concat)}\``;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
function renderAction(action: IrAction, cast = false): string {
|
|
26
35
|
const suffix = cast ? ' as ActionDescriptor' : '';
|
|
27
36
|
if (!action.params || Object.keys(action.params).length === 0) {
|
|
@@ -62,6 +71,7 @@ export function renderValue(value: IrValue, level: number, castActions = false):
|
|
|
62
71
|
if (value.kind === 'binding') return bindingExpr(value as IrBinding);
|
|
63
72
|
if (value.kind === 'action') return renderAction(value as IrAction, castActions);
|
|
64
73
|
if (value.kind === 'literal') return renderLiteralValue((value as IrLiteral).value, level, castActions);
|
|
74
|
+
if (value.kind === 'concat') return renderConcat(value as IrConcat);
|
|
65
75
|
}
|
|
66
76
|
return 'undefined';
|
|
67
77
|
}
|
|
@@ -108,6 +118,10 @@ function variantClassSuffix(elementName: string, props: Record<string, IrValue>)
|
|
|
108
118
|
if (typeof variantLit === 'string') {
|
|
109
119
|
return ` vc-${componentToKebab(elementName)}--${variantLit}`;
|
|
110
120
|
}
|
|
121
|
+
const variantVal = props.variant;
|
|
122
|
+
if (variantVal && typeof variantVal === 'object' && 'kind' in variantVal && variantVal.kind === 'concat') {
|
|
123
|
+
return ` vc-${componentToKebab(elementName)}--${concatSegments(variantVal as IrConcat)}`;
|
|
124
|
+
}
|
|
111
125
|
const variant = propExpr(props, 'variant');
|
|
112
126
|
if (variant) return ` vc-${componentToKebab(elementName)}--\${${variant}}`;
|
|
113
127
|
return undefined;
|
|
@@ -117,19 +131,8 @@ function classNameAttr(
|
|
|
117
131
|
elementName: string,
|
|
118
132
|
entry: ElementMapEntry,
|
|
119
133
|
props: Record<string, IrValue>,
|
|
120
|
-
headingLevel?: number,
|
|
121
134
|
): string {
|
|
122
135
|
const base = entry.className ?? '';
|
|
123
|
-
if (entry.tag === 'dynamic-heading' && headingLevel !== undefined) {
|
|
124
|
-
const levelClass = `${base} vc-heading-${headingLevel}`;
|
|
125
|
-
const variantSuffix = variantClassSuffix(elementName, props);
|
|
126
|
-
if (!variantSuffix) return `className="${levelClass}"`;
|
|
127
|
-
const variantLit = propLiteral(props, 'variant');
|
|
128
|
-
if (typeof variantLit === 'string') {
|
|
129
|
-
return `className="${levelClass}${variantSuffix}"`;
|
|
130
|
-
}
|
|
131
|
-
return `className={\`${levelClass}${variantSuffix}\`}`;
|
|
132
|
-
}
|
|
133
136
|
const variantSuffix = variantClassSuffix(elementName, props);
|
|
134
137
|
if (variantSuffix) {
|
|
135
138
|
const variantLit = propLiteral(props, 'variant');
|
|
@@ -157,20 +160,20 @@ function emitFieldWrap(inner: string, props: Record<string, IrValue>, level: num
|
|
|
157
160
|
const error = propExpr(props, 'error');
|
|
158
161
|
const lines = [`${indent(level)}<label className="vc-field">`];
|
|
159
162
|
if (typeof labelLit === 'string') {
|
|
160
|
-
lines.push(`${indent(level + 1)}<span className="vc-field-label">{${JSON.stringify(labelLit)}}</span>`);
|
|
163
|
+
lines.push(`${indent(level + 1)}<span className="vc-text vc-text--field-label">{${JSON.stringify(labelLit)}}</span>`);
|
|
161
164
|
} else if (label) {
|
|
162
|
-
lines.push(`${indent(level + 1)}{${label} ? <span className="vc-field-label">{${label}}</span> : null}`);
|
|
165
|
+
lines.push(`${indent(level + 1)}{${label} ? <span className="vc-text vc-text--field-label">{${label}}</span> : null}`);
|
|
163
166
|
}
|
|
164
167
|
lines.push(`${indent(level + 1)}${inner}`);
|
|
165
168
|
if (typeof helpLit === 'string') {
|
|
166
|
-
lines.push(`${indent(level + 1)}<span className="vc-field-help">{${JSON.stringify(helpLit)}}</span>`);
|
|
169
|
+
lines.push(`${indent(level + 1)}<span className="vc-text vc-text--field-help">{${JSON.stringify(helpLit)}}</span>`);
|
|
167
170
|
} else if (help) {
|
|
168
|
-
lines.push(`${indent(level + 1)}{${help} ? <span className="vc-field-help">{${help}}</span> : null}`);
|
|
171
|
+
lines.push(`${indent(level + 1)}{${help} ? <span className="vc-text vc-text--field-help">{${help}}</span> : null}`);
|
|
169
172
|
}
|
|
170
173
|
if (typeof errorLit === 'string') {
|
|
171
|
-
lines.push(`${indent(level + 1)}<span className="vc-field-error">{${JSON.stringify(errorLit)}}</span>`);
|
|
174
|
+
lines.push(`${indent(level + 1)}<span className="vc-text vc-text--field-error">{${JSON.stringify(errorLit)}}</span>`);
|
|
172
175
|
} else if (error) {
|
|
173
|
-
lines.push(`${indent(level + 1)}{${error} ? <span className="vc-field-error">{${error}}</span> : null}`);
|
|
176
|
+
lines.push(`${indent(level + 1)}{${error} ? <span className="vc-text vc-text--field-error">{${error}}</span> : null}`);
|
|
174
177
|
}
|
|
175
178
|
lines.push(`${indent(level)}</label>`);
|
|
176
179
|
return lines.join('\n');
|
|
@@ -280,7 +283,6 @@ function emitOpenTag(
|
|
|
280
283
|
entry: ElementMapEntry,
|
|
281
284
|
props: Record<string, IrValue>,
|
|
282
285
|
tag: string,
|
|
283
|
-
headingLevel?: number,
|
|
284
286
|
): string {
|
|
285
287
|
const attrs: string[] = [];
|
|
286
288
|
if (entry.ariaHidden) attrs.push('aria-hidden');
|
|
@@ -299,7 +301,7 @@ function emitOpenTag(
|
|
|
299
301
|
if (entry.tag === 'button' && props.enabled) attrs.push(`disabled={${propExpr(props, 'enabled')} === false}`);
|
|
300
302
|
if (entry.tag === 'a' && props.href) attrs.push(`href={${propExpr(props, 'href')}}`);
|
|
301
303
|
if (entry.tag === 'button') attrs.push(`type={${propExpr(props, 'type') ?? '"button"'}}`);
|
|
302
|
-
attrs.push(classNameAttr(elementName, entry, props
|
|
304
|
+
attrs.push(classNameAttr(elementName, entry, props));
|
|
303
305
|
pushStyleAttr(attrs, entry, props);
|
|
304
306
|
return `<${tag} ${attrs.join(' ')}`;
|
|
305
307
|
}
|
|
@@ -402,27 +404,6 @@ function lowerVocabularyElement(
|
|
|
402
404
|
return visibilityGuard(emitFieldWrap(inner, props, level), props, level);
|
|
403
405
|
}
|
|
404
406
|
|
|
405
|
-
if (entry.tag === 'dynamic-heading') {
|
|
406
|
-
const levelLit = propLiteral(props, 'level');
|
|
407
|
-
if (typeof levelLit === 'number') {
|
|
408
|
-
const clamped = Math.min(6, Math.max(1, levelLit));
|
|
409
|
-
const tag = `h${clamped}`;
|
|
410
|
-
const open = emitOpenTag(node.name, entry, props, tag, clamped);
|
|
411
|
-
const body = childContent
|
|
412
|
-
? `${indent(level)}${open}>\n${childContent}\n${indent(level)}</${tag}>`
|
|
413
|
-
: `${indent(level)}${open} />`;
|
|
414
|
-
return visibilityGuard(body, props, level);
|
|
415
|
-
}
|
|
416
|
-
const levelExpr = propExpr(props, 'level') ?? '2';
|
|
417
|
-
const foldedStyle = foldedStyleToJsx(foldLiteralsToCss(collectLiteralStyleProps(props)));
|
|
418
|
-
const styleFragment = foldedStyle ? ` ${foldedStyle}` : '';
|
|
419
|
-
const inner = childContent
|
|
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>; })()}`
|
|
421
|
-
: '';
|
|
422
|
-
const fallbackStyle = styleAttrForElement(entry, props);
|
|
423
|
-
return visibilityGuard(inner || `${indent(level)}<h2 ${classNameAttr(node.name, entry, props, 2)}${fallbackStyle ? ` ${fallbackStyle}` : ''} />`, props, level);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
407
|
const tag = entry.tag;
|
|
427
408
|
if (entry.void) {
|
|
428
409
|
return visibilityGuard(emitVoidInput(node.name, entry, props, level), props, level);
|
package/renderers/react/paths.ts
CHANGED
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function reactRuntimeDir(): string {
|
|
9
|
-
return resolve(packageRoot(), 'renderers/react/runtime');
|
|
10
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
reactTemplatesDir,
|
|
3
|
+
reactRuntimeDir,
|
|
4
|
+
reactStructuralCssPath,
|
|
5
|
+
} from '../../src/lib/rendererPaths.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React preview composition root.
|
|
3
|
+
* Wires vocabulary stubs + renderer runtime + project extensions.
|
|
4
|
+
* This file belongs to the React renderer, NOT to core src/.
|
|
5
|
+
*/
|
|
6
|
+
import { vocabularyStubs } from './vocabulary.js';
|
|
7
|
+
|
|
8
|
+
export const {
|
|
9
|
+
App, Box, Button, Checkbox, Column, Divider, Drawer,
|
|
10
|
+
Field, Form, Heading, Icon, IconButton, Image, Link,
|
|
11
|
+
List, ListItem, Modal, Page, Popover, Progress, Radio,
|
|
12
|
+
Row, SafeArea, ScrollArea, Select, Spacer, Spinner,
|
|
13
|
+
Stack, Switch, Table, TableCell, TableColumn, TableHeader,
|
|
14
|
+
TableRow, Text, TextArea, TextInput, Tooltip,
|
|
15
|
+
} = vocabularyStubs;
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
isHidden,
|
|
19
|
+
toneClass,
|
|
20
|
+
registerPreviewActions,
|
|
21
|
+
clearPreviewActions,
|
|
22
|
+
dispatchPreviewAction,
|
|
23
|
+
} from '../runtime/index.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ComponentType, ReactElement } from 'react';
|
|
2
|
+
import type { ActionHandlers } from '@view-contracts/core';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import ReactDOM from 'react-dom/client';
|
|
5
|
+
import '@view-contracts/theme';
|
|
6
|
+
import { registerPreviewActions } from '../runtime/actionRuntime.js';
|
|
7
|
+
import { PreviewShell } from '../../../src/preview/shell.js';
|
|
8
|
+
|
|
9
|
+
export interface PreviewOptions<TVm> {
|
|
10
|
+
View: ComponentType<{ vm: TVm; actions?: ActionHandlers }>;
|
|
11
|
+
vm: TVm;
|
|
12
|
+
actions?: ActionHandlers;
|
|
13
|
+
onAction?: ActionHandlers;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Mount a view-contract screen for design preview (views/ + view-contracts packages only). */
|
|
17
|
+
export function startPreview<TVm>(options: PreviewOptions<TVm>): void {
|
|
18
|
+
const actions = options.actions ?? {};
|
|
19
|
+
if (options.onAction) {
|
|
20
|
+
registerPreviewActions({ ...actions, ...options.onAction });
|
|
21
|
+
} else {
|
|
22
|
+
registerPreviewActions(actions);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const View = options.View;
|
|
26
|
+
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
|
27
|
+
<React.StrictMode>
|
|
28
|
+
<PreviewShell>
|
|
29
|
+
<View vm={options.vm} actions={actions} />
|
|
30
|
+
</PreviewShell>
|
|
31
|
+
</React.StrictMode>,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function PreviewApp<TVm>(props: PreviewOptions<TVm>): ReactElement {
|
|
36
|
+
const View = props.View;
|
|
37
|
+
return (
|
|
38
|
+
<PreviewShell>
|
|
39
|
+
<View vm={props.vm} actions={props.actions} />
|
|
40
|
+
</PreviewShell>
|
|
41
|
+
);
|
|
42
|
+
}
|