view-contracts 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-contract.yaml +1 -1
- package/dist/view-contracts.bundle.mjs +161 -153
- package/dist/view-contracts.bundle.mjs.map +4 -4
- package/docs/cli-reference.md +1 -1
- package/package.json +14 -4
- package/renderers/compose/templates/Screen.kt.hbs +15 -0
- package/renderers/compose/templates/VcTheme.kt.hbs +19 -0
- package/renderers/react/README.md +19 -3
- package/renderers/react/defaults/theme.yaml +41 -0
- package/renderers/react/defaults/tokens.yaml +59 -0
- package/renderers/react/element-config.meta.json +22 -0
- package/renderers/react/elementMap.ts +55 -0
- package/renderers/react/elements.yaml +86 -0
- package/renderers/react/index.ts +2 -0
- package/renderers/react/lowering/lowerToJsx.ts +433 -0
- package/renderers/react/paths.ts +10 -0
- package/renderers/react/renderComponents.ts +65 -0
- package/renderers/react/renderRuntime.ts +157 -0
- package/renderers/react/routes.yaml +3 -2
- package/{src/renderer → renderers}/react/runtime/actionRuntime.ts +3 -3
- package/renderers/react/runtime/dispatch.ts +21 -0
- package/renderers/react/runtime/index.ts +5 -0
- package/renderers/react/runtime/style.ts +101 -0
- package/renderers/react/runtime/theme.css +83 -0
- package/renderers/react/runtime/types.ts +63 -0
- package/renderers/react/templates/components-index.ts.hbs +5 -0
- package/renderers/react/templates/dispatch.ts.hbs +22 -0
- package/renderers/react/templates/handlers.ts.hbs +4 -0
- package/renderers/react/templates/runtime-types.ts.hbs +10 -0
- package/renderers/react/templates/style-helpers.ts.hbs +2 -0
- package/renderers/react/templates/theme.css.hbs +2 -0
- package/renderers/swiftui/templates/Screen.swift.hbs +21 -0
- package/renderers/swiftui/templates/VcTheme.swift.hbs +21 -0
- package/spec/ui-dsl.schema.json +24 -16
- package/src/generated/schema/allowed-components.ts +1 -1
- package/src/generated/schema/{tokens.ts → dsl-enums.ts} +1 -1
- package/src/generated/schema/dsl-properties.ts +3 -2
- package/src/generated/schema/index.ts +2 -2
- package/src/generated/schema/lowering-style-properties.ts +28 -0
- package/src/generated/schema/react-renderer-element-config.ts +25 -0
- package/src/generated/schema/schema-document.ts +2 -2
- package/src/renderer/react/runtime/components.tsx +0 -216
- package/src/renderer/react/runtime/index.ts +0 -4
- package/src/renderer/react/runtime/shared.tsx +0 -53
- package/src/renderer/react/runtime/style.ts +0 -69
- package/src/renderer/react/runtime/theme.css +0 -68
- package/src/renderer/react/runtime/types.ts +0 -46
package/docs/cli-reference.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "view-contracts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Contract-first UI design toolchain — restricted TSX to View IR to platform renderers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"workspaces": [
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"files": [
|
|
13
13
|
"dist/view-contracts.bundle.mjs",
|
|
14
14
|
"dist/view-contracts.bundle.mjs.map",
|
|
15
|
-
"
|
|
15
|
+
"renderers/react/runtime",
|
|
16
|
+
"renderers/react/defaults",
|
|
16
17
|
"src/generated/schema",
|
|
17
18
|
"spec",
|
|
18
19
|
"renderers",
|
|
@@ -43,7 +44,10 @@
|
|
|
43
44
|
"contracts:check": "tsx src/cli.ts contracts check -c examples/admin-dashboard/view-contracts.config.yaml",
|
|
44
45
|
"schema:generate": "tsx scripts/schema-generate.ts",
|
|
45
46
|
"cli-contracts:validate": "cli-contracts validate",
|
|
46
|
-
"test
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:unit": "vitest run",
|
|
49
|
+
"test:smoke": "node dist/view-contracts.bundle.mjs --version && node dist/view-contracts.bundle.mjs --help",
|
|
50
|
+
"test:ci": "npm run test:unit && npm run build:publish && npm run test:smoke",
|
|
47
51
|
"version": "node -e \"const fs=require('fs');const f='cli-contract.yaml';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace(/^ version: .*/m,' version: '+process.env.npm_package_version))\" && git add cli-contract.yaml",
|
|
48
52
|
"prepublishOnly": "npm run build:publish && npm run test:ci"
|
|
49
53
|
},
|
|
@@ -73,6 +77,7 @@
|
|
|
73
77
|
},
|
|
74
78
|
"dependencies": {
|
|
75
79
|
"commander": "^14.0.3",
|
|
80
|
+
"handlebars": "^4.7.9",
|
|
76
81
|
"js-yaml": "^4.1.1",
|
|
77
82
|
"micro-contracts": "^0.17.10",
|
|
78
83
|
"react": "^19.2.0",
|
|
@@ -80,6 +85,7 @@
|
|
|
80
85
|
"typescript": "^5.9.3"
|
|
81
86
|
},
|
|
82
87
|
"devDependencies": {
|
|
88
|
+
"@types/handlebars": "^4.1.0",
|
|
83
89
|
"@types/js-yaml": "^4.0.9",
|
|
84
90
|
"@types/node": "^22.19.21",
|
|
85
91
|
"@types/react": "^19.2.17",
|
|
@@ -88,7 +94,11 @@
|
|
|
88
94
|
"ajv": "^8.18.0",
|
|
89
95
|
"cli-contracts": "^0.33.13",
|
|
90
96
|
"esbuild": "^0.28.0",
|
|
97
|
+
"pixelmatch": "^7.2.0",
|
|
98
|
+
"playwright": "^1.61.0",
|
|
99
|
+
"pngjs": "^7.0.0",
|
|
91
100
|
"tsx": "^4.21.0",
|
|
92
|
-
"vite": "^6.4.1"
|
|
101
|
+
"vite": "^6.4.1",
|
|
102
|
+
"vitest": "^4.1.9"
|
|
93
103
|
}
|
|
94
104
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// {{screenName}} — Jetpack Compose scaffold (Phase 5)
|
|
2
|
+
// Theme IR: {{themeJsonPath}}
|
|
3
|
+
// Token IR: {{tokensJsonPath}}
|
|
4
|
+
//
|
|
5
|
+
// TODO(Phase C): Load theme.json and emit Modifier.primaryButton(), vcTextMuted(), etc.
|
|
6
|
+
|
|
7
|
+
package {{packageName}}
|
|
8
|
+
|
|
9
|
+
import androidx.compose.material3.Text
|
|
10
|
+
import androidx.compose.runtime.Composable
|
|
11
|
+
|
|
12
|
+
@Composable
|
|
13
|
+
fun {{screenName}}Screen() {
|
|
14
|
+
Text("{{screenName}} — Compose scaffold")
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// VcTheme — Compose theme scaffold (Phase 5)
|
|
2
|
+
// Load Token IR + Theme IR from assets:
|
|
3
|
+
// {{tokensJsonPath}}
|
|
4
|
+
// {{themeJsonPath}}
|
|
5
|
+
//
|
|
6
|
+
// TODO(Phase C): Map Theme IR variants to Modifier extensions and ColorScheme.
|
|
7
|
+
|
|
8
|
+
package {{packageName}}
|
|
9
|
+
|
|
10
|
+
import android.content.Context
|
|
11
|
+
|
|
12
|
+
object VcTheme {
|
|
13
|
+
const val TOKENS_ASSET = "{{tokensJsonPath}}"
|
|
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() }
|
|
19
|
+
}
|
|
@@ -3,8 +3,24 @@
|
|
|
3
3
|
Per-target mapping for view-contracts UI DSL → React.
|
|
4
4
|
|
|
5
5
|
- `routes.yaml` — generated output file routing
|
|
6
|
-
- `elements.yaml` — element →
|
|
7
|
-
- `
|
|
6
|
+
- `elements.yaml` — vocabulary element → HTML tag (SSoT for compile-time lowering)
|
|
7
|
+
- `element-config.meta.json` — shape of `elements.yaml` entries (drives schema codegen)
|
|
8
|
+
- `templates/*.hbs` — Handlebars templates for generated runtime files (dispatch, types, theme, etc.)
|
|
9
|
+
- `renderComponents.ts` / `renderRuntime.ts` — compile-time codegen (IR → JSX, runtime bundle)
|
|
10
|
+
- `lowering/lowerToJsx.ts` — element-map-driven JSX emission
|
|
11
|
+
- `runtime/` — preview/toolchain primitives (style, dispatch) — **not** copied to `generated/`
|
|
8
12
|
|
|
9
13
|
Vocabulary (elements/properties) is defined in `spec/ui-dsl.schema.json` (from `ui-dsl-*.csv`).
|
|
10
|
-
|
|
14
|
+
Preview vocabulary stubs are generated to `src/generated/preview/vocabulary.tsx`.
|
|
15
|
+
Sample-only preview helpers (Sidebar, Card, etc.) are **not** part of the vocabulary — they live in project `src/components/extensions.tsx`.
|
|
16
|
+
|
|
17
|
+
Generic renderer utilities (template engine, YAML element-map validation) live under `src/renderer/`.
|
|
18
|
+
|
|
19
|
+
## Compile-time lowering
|
|
20
|
+
|
|
21
|
+
`renderComponents.ts` reads `elements.yaml` and emits native HTML/JSX into `generated/react/*.generated.tsx`.
|
|
22
|
+
`renderers/react/runtime/` is **not** copied to `generated/`.
|
|
23
|
+
|
|
24
|
+
## Template customization
|
|
25
|
+
|
|
26
|
+
Set `rendererTemplatesDir` in `view-contracts.config.yaml` to override default templates.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Default theme variants — component appearance via .vc-{element}--{variant} rules.
|
|
2
|
+
variants:
|
|
3
|
+
Button:
|
|
4
|
+
primary:
|
|
5
|
+
background: "{ui.primary}"
|
|
6
|
+
foreground: "{ui.on-primary}"
|
|
7
|
+
secondary:
|
|
8
|
+
background: "{ui.button-secondary}"
|
|
9
|
+
foreground: "{ui.text}"
|
|
10
|
+
ghost:
|
|
11
|
+
background: "{ui.transparent}"
|
|
12
|
+
foreground: "{ui.text}"
|
|
13
|
+
danger:
|
|
14
|
+
background: "{ui.danger}"
|
|
15
|
+
foreground: "{ui.on-primary}"
|
|
16
|
+
Text:
|
|
17
|
+
muted:
|
|
18
|
+
foreground: "{ui.text-muted}"
|
|
19
|
+
Card:
|
|
20
|
+
default:
|
|
21
|
+
background: "{ui.surface}"
|
|
22
|
+
Badge:
|
|
23
|
+
default:
|
|
24
|
+
background: "{ui.surface-muted}"
|
|
25
|
+
primary:
|
|
26
|
+
background: "{ui.primary-weak}"
|
|
27
|
+
success:
|
|
28
|
+
background: "{ui.success-weak}"
|
|
29
|
+
warning:
|
|
30
|
+
background: "{ui.warning-weak}"
|
|
31
|
+
danger:
|
|
32
|
+
background: "{ui.danger-weak}"
|
|
33
|
+
Alert:
|
|
34
|
+
default:
|
|
35
|
+
background: "{ui.surface}"
|
|
36
|
+
success:
|
|
37
|
+
background: "{ui.success-weak}"
|
|
38
|
+
warning:
|
|
39
|
+
background: "{ui.warning-weak}"
|
|
40
|
+
danger:
|
|
41
|
+
background: "{ui.danger-weak}"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Default design tokens — mirrors renderers/react/runtime/theme.css :root variables.
|
|
2
|
+
ui:
|
|
3
|
+
bg:
|
|
4
|
+
$type: color
|
|
5
|
+
$value: "#f6f8fb"
|
|
6
|
+
surface:
|
|
7
|
+
$type: color
|
|
8
|
+
$value: "#ffffff"
|
|
9
|
+
surface-muted:
|
|
10
|
+
$type: color
|
|
11
|
+
$value: "#f8fafc"
|
|
12
|
+
text:
|
|
13
|
+
$type: color
|
|
14
|
+
$value: "#111827"
|
|
15
|
+
text-muted:
|
|
16
|
+
$type: color
|
|
17
|
+
$value: "#6b7280"
|
|
18
|
+
primary:
|
|
19
|
+
$type: color
|
|
20
|
+
$value: "#2563eb"
|
|
21
|
+
primary-weak:
|
|
22
|
+
$type: color
|
|
23
|
+
$value: "#dbeafe"
|
|
24
|
+
success:
|
|
25
|
+
$type: color
|
|
26
|
+
$value: "#047857"
|
|
27
|
+
success-weak:
|
|
28
|
+
$type: color
|
|
29
|
+
$value: "#d1fae5"
|
|
30
|
+
warning:
|
|
31
|
+
$type: color
|
|
32
|
+
$value: "#b45309"
|
|
33
|
+
warning-weak:
|
|
34
|
+
$type: color
|
|
35
|
+
$value: "#fef3c7"
|
|
36
|
+
danger:
|
|
37
|
+
$type: color
|
|
38
|
+
$value: "#dc2626"
|
|
39
|
+
danger-weak:
|
|
40
|
+
$type: color
|
|
41
|
+
$value: "#fee2e2"
|
|
42
|
+
on-primary:
|
|
43
|
+
$type: color
|
|
44
|
+
$value: "#ffffff"
|
|
45
|
+
button-secondary:
|
|
46
|
+
$type: color
|
|
47
|
+
$value: "#eef2f7"
|
|
48
|
+
transparent:
|
|
49
|
+
$type: color
|
|
50
|
+
$value: "transparent"
|
|
51
|
+
border:
|
|
52
|
+
$type: color
|
|
53
|
+
$value: "#e5e7eb"
|
|
54
|
+
shadow:
|
|
55
|
+
$type: shadow
|
|
56
|
+
$value: "0 10px 30px rgba(15, 23, 42, 0.08)"
|
|
57
|
+
font:
|
|
58
|
+
$type: typography
|
|
59
|
+
$value: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Per-element fields allowed in renderers/react/elements.yaml (React HTML lowering config — not UI DSL vocabulary).",
|
|
3
|
+
"fields": {
|
|
4
|
+
"tag": { "type": "string", "required": true },
|
|
5
|
+
"className": { "type": "string" },
|
|
6
|
+
"ariaLabel": { "type": "boolean" },
|
|
7
|
+
"ariaHidden": { "type": "boolean" },
|
|
8
|
+
"flex": { "type": "enum", "values": ["row", "column"] },
|
|
9
|
+
"wrapProp": { "type": "string" },
|
|
10
|
+
"flexGrowProp": { "type": "string" },
|
|
11
|
+
"void": { "type": "boolean" },
|
|
12
|
+
"levelProp": { "type": "string" },
|
|
13
|
+
"defaultLevel": { "type": "number" },
|
|
14
|
+
"actionEvent": { "type": "enum", "values": ["onClick", "onSubmit"] },
|
|
15
|
+
"fieldWrap": { "type": "boolean" },
|
|
16
|
+
"inputType": { "type": "string" },
|
|
17
|
+
"sizeClass": { "type": "string" },
|
|
18
|
+
"optionsProp": { "type": "string" },
|
|
19
|
+
"checkbox": { "type": "boolean" },
|
|
20
|
+
"defaultRole": { "type": "string" }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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';
|
|
4
|
+
import { packageRoot } from '../../src/lib/packageRoot.js';
|
|
5
|
+
import {
|
|
6
|
+
loadYamlFile,
|
|
7
|
+
validateMapEntryFields,
|
|
8
|
+
validateVocabularyElementNames,
|
|
9
|
+
} from '../../src/renderer/elementMapValidation.js';
|
|
10
|
+
import { loadUiDslSchema } from '../../src/schema/loadSchema.js';
|
|
11
|
+
|
|
12
|
+
export type { ReactElementMapFile, ReactRendererElementConfig };
|
|
13
|
+
export type ReactElementMap = ReactElementMapFile;
|
|
14
|
+
export type ElementMapEntry = ReactRendererElementConfig;
|
|
15
|
+
|
|
16
|
+
const SOURCE_LABEL = 'renderers/react/elements.yaml';
|
|
17
|
+
|
|
18
|
+
let cachedMap: ReactElementMapFile | null = null;
|
|
19
|
+
|
|
20
|
+
function validateElementMap(map: ReactElementMapFile): void {
|
|
21
|
+
validateVocabularyElementNames(Object.keys(map.elements), ALLOWED_COMPONENTS, SOURCE_LABEL);
|
|
22
|
+
validateMapEntryFields(map.elements, ['tag'], SOURCE_LABEL);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function loadReactElementMap(customPath?: string): Promise<ReactElementMapFile> {
|
|
26
|
+
if (!customPath && cachedMap) return cachedMap;
|
|
27
|
+
const mapPath = customPath ?? resolve(packageRoot(), 'renderers/react/elements.yaml');
|
|
28
|
+
const raw = await loadYamlFile<ReactElementMapFile>(mapPath);
|
|
29
|
+
validateElementMap(raw);
|
|
30
|
+
if (!customPath) cachedMap = raw;
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** DSL vocabulary element with a React HTML lowering entry (excludes project extensions). */
|
|
35
|
+
export function isLowerableVocabularyElement(
|
|
36
|
+
name: string,
|
|
37
|
+
map: ReactElementMapFile,
|
|
38
|
+
allowlistExtra: string[] = [],
|
|
39
|
+
): boolean {
|
|
40
|
+
return ALLOWED_COMPONENTS.has(name) && !allowlistExtra.includes(name) && name in map.elements;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function assertElementMapCoversImplementedVocabulary(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));
|
|
46
|
+
if (missing.length > 0) {
|
|
47
|
+
console.warn(
|
|
48
|
+
`${SOURCE_LABEL}: ${missing.length} DSL element(s) have no React HTML map yet: ${missing.join(', ')}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function isVocabularyElement(name: string, map: ReactElementMapFile, allowlistExtra: string[] = []): boolean {
|
|
54
|
+
return name in map.elements && !allowlistExtra.includes(name);
|
|
55
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# React renderer element → HTML lowering map (SSoT).
|
|
2
|
+
# Vocabulary from spec/ui-dsl.schema.json. Extension components are project-specific
|
|
3
|
+
# (allowlistExtra) and stay as React component imports — not listed here.
|
|
4
|
+
|
|
5
|
+
elements:
|
|
6
|
+
Page:
|
|
7
|
+
tag: main
|
|
8
|
+
className: vc-page
|
|
9
|
+
ariaLabel: true
|
|
10
|
+
Box:
|
|
11
|
+
tag: div
|
|
12
|
+
className: vc-box
|
|
13
|
+
ariaLabel: true
|
|
14
|
+
Column:
|
|
15
|
+
tag: div
|
|
16
|
+
className: vc-column
|
|
17
|
+
flex: column
|
|
18
|
+
ariaLabel: true
|
|
19
|
+
Row:
|
|
20
|
+
tag: div
|
|
21
|
+
className: vc-row
|
|
22
|
+
flex: row
|
|
23
|
+
wrapProp: wrap
|
|
24
|
+
ariaLabel: true
|
|
25
|
+
Stack:
|
|
26
|
+
tag: div
|
|
27
|
+
className: vc-stack
|
|
28
|
+
ariaLabel: true
|
|
29
|
+
Spacer:
|
|
30
|
+
tag: div
|
|
31
|
+
className: vc-spacer
|
|
32
|
+
ariaHidden: true
|
|
33
|
+
flexGrowProp: weight
|
|
34
|
+
Divider:
|
|
35
|
+
tag: hr
|
|
36
|
+
className: vc-divider
|
|
37
|
+
void: true
|
|
38
|
+
Heading:
|
|
39
|
+
tag: dynamic-heading
|
|
40
|
+
className: vc-heading
|
|
41
|
+
levelProp: level
|
|
42
|
+
defaultLevel: 2
|
|
43
|
+
Text:
|
|
44
|
+
tag: p
|
|
45
|
+
className: vc-text
|
|
46
|
+
Link:
|
|
47
|
+
tag: a
|
|
48
|
+
className: vc-link
|
|
49
|
+
actionEvent: onClick
|
|
50
|
+
Button:
|
|
51
|
+
tag: button
|
|
52
|
+
className: vc-button
|
|
53
|
+
actionEvent: onClick
|
|
54
|
+
Form:
|
|
55
|
+
tag: form
|
|
56
|
+
className: vc-form
|
|
57
|
+
actionEvent: onSubmit
|
|
58
|
+
ariaLabel: true
|
|
59
|
+
TextInput:
|
|
60
|
+
tag: input
|
|
61
|
+
className: vc-text-input
|
|
62
|
+
fieldWrap: true
|
|
63
|
+
inputType: text
|
|
64
|
+
sizeClass: vc-field
|
|
65
|
+
TextArea:
|
|
66
|
+
tag: textarea
|
|
67
|
+
className: vc-text-area
|
|
68
|
+
fieldWrap: true
|
|
69
|
+
Select:
|
|
70
|
+
tag: select
|
|
71
|
+
className: vc-select
|
|
72
|
+
fieldWrap: true
|
|
73
|
+
sizeClass: vc-field
|
|
74
|
+
optionsProp: options
|
|
75
|
+
Checkbox:
|
|
76
|
+
tag: label
|
|
77
|
+
className: vc-checkbox
|
|
78
|
+
checkbox: true
|
|
79
|
+
List:
|
|
80
|
+
tag: ul
|
|
81
|
+
className: vc-list
|
|
82
|
+
defaultRole: list
|
|
83
|
+
ListItem:
|
|
84
|
+
tag: li
|
|
85
|
+
className: vc-list-item
|
|
86
|
+
defaultRole: listitem
|