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.
Files changed (45) hide show
  1. package/README.md +10 -3
  2. package/cli-contract.yaml +40 -0
  3. package/dist/preview-style-helpers.js +2 -0
  4. package/dist/preview-style-helpers.js.map +7 -0
  5. package/dist/preview.mjs +152 -0
  6. package/dist/preview.mjs.map +7 -0
  7. package/dist/view-contracts.bundle.mjs +457 -248
  8. package/dist/view-contracts.bundle.mjs.map +4 -4
  9. package/docs/cli-reference.md +36 -0
  10. package/package.json +28 -1
  11. package/renderers/compose/renderRuntime.ts +40 -0
  12. package/renderers/react/defaults/theme.yaml +52 -0
  13. package/renderers/react/defaults/tokens.yaml +76 -1
  14. package/renderers/react/elementMap.ts +30 -6
  15. package/renderers/react/elements.yaml +17 -5
  16. package/renderers/react/lowering/foldStyle.ts +13 -1
  17. package/renderers/react/lowering/lowerToJsx.ts +21 -40
  18. package/renderers/react/paths.ts +5 -10
  19. package/renderers/react/preview/components.ts +23 -0
  20. package/renderers/react/preview/index.ts +3 -0
  21. package/renderers/react/preview/start.tsx +42 -0
  22. package/renderers/react/preview/style-helpers.ts +5 -0
  23. package/renderers/react/preview/vocabulary.tsx +75 -0
  24. package/renderers/react/runtime/structural.css +38 -0
  25. package/renderers/react/style/foldLiterals.ts +25 -4
  26. package/renderers/swiftui/README.md +48 -10
  27. package/renderers/swiftui/elementMap.ts +45 -0
  28. package/renderers/swiftui/elements.yaml +82 -0
  29. package/renderers/swiftui/index.ts +5 -0
  30. package/renderers/swiftui/lowering/lowerToSwiftUI.ts +761 -0
  31. package/renderers/swiftui/lowering/modifiers.ts +221 -0
  32. package/renderers/swiftui/renderComponents.ts +79 -0
  33. package/renderers/swiftui/renderRuntime.ts +139 -0
  34. package/renderers/swiftui/renderTheme.ts +274 -0
  35. package/spec/ui-dsl.linter-rules.json +0 -7
  36. package/spec/ui-dsl.meta.json +5 -0
  37. package/spec/ui-dsl.schema.json +333 -259
  38. package/renderers/react/runtime/theme.css +0 -65
  39. package/src/generated/schema/allowed-components.ts +0 -36
  40. package/src/generated/schema/dsl-enums.ts +0 -19
  41. package/src/generated/schema/dsl-properties.ts +0 -64
  42. package/src/generated/schema/index.ts +0 -4
  43. package/src/generated/schema/lowering-style-properties.ts +0 -35
  44. package/src/generated/schema/react-renderer-element-config.ts +0 -27
  45. package/src/generated/schema/schema-document.ts +0 -33
@@ -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.5",
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 — mirrors renderers/react/runtime/theme.css :root variables.
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/generated/schema/allowed-components.js';
3
- import type { ReactElementMapFile, ReactRendererElementConfig } from '../../src/generated/schema/react-renderer-element-config.js';
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
- export type { ReactElementMapFile, ReactRendererElementConfig };
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 [schema, map] = await Promise.all([loadUiDslSchema(), loadReactElementMap(customPath)]);
45
- const missing = Object.keys(schema.elements).filter((name) => !(name in map.elements));
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/generated/schema/lowering-style-properties.js';
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, headingLevel));
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);
@@ -1,10 +1,5 @@
1
- import { resolve } from 'node:path';
2
- import { packageRoot } from '../../src/lib/packageRoot.js';
3
-
4
- export function reactTemplatesDir(customDir?: string): string {
5
- return customDir ?? resolve(packageRoot(), 'renderers/react/templates');
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,3 @@
1
+ export { startPreview, PreviewApp } from './start.js';
2
+ export type { PreviewOptions } from './start.js';
3
+ export { PreviewShell } from '../../../src/preview/shell.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
+ }
@@ -0,0 +1,5 @@
1
+ /** Preview style helpers — re-export from renderer runtime. */
2
+ export {
3
+ extensionClassName,
4
+ resolveExtensionVariant,
5
+ } from '../runtime/style.js';