view-contracts 0.3.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) 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 +153 -0
  6. package/dist/preview.mjs.map +7 -0
  7. package/dist/view-contracts.bundle.mjs +399 -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 +23 -0
  13. package/renderers/react/defaults/tokens.yaml +15 -0
  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 +15 -34
  18. package/renderers/react/paths.ts +1 -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/theme.css +9 -17
  25. package/renderers/react/style/foldLiterals.ts +11 -0
  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 +542 -0
  31. package/renderers/swiftui/lowering/modifiers.ts +142 -0
  32. package/renderers/swiftui/renderComponents.ts +79 -0
  33. package/renderers/swiftui/renderRuntime.ts +91 -0
  34. package/renderers/swiftui/renderTheme.ts +208 -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/src/generated/schema/allowed-components.ts +0 -36
  39. package/src/generated/schema/dsl-enums.ts +0 -19
  40. package/src/generated/schema/dsl-properties.ts +0 -64
  41. package/src/generated/schema/index.ts +0 -4
  42. package/src/generated/schema/lowering-style-properties.ts +0 -35
  43. package/src/generated/schema/react-renderer-element-config.ts +0 -27
  44. 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.0",
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,29 @@ 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}"
9
32
  variants:
10
33
  Button:
11
34
  primary:
@@ -61,6 +61,9 @@ ui:
61
61
  font-bold:
62
62
  $type: layout
63
63
  $value: "700"
64
+ border-1:
65
+ $type: layout
66
+ $value: "1px solid"
64
67
  space:
65
68
  radius-md:
66
69
  $type: dimension
@@ -68,3 +71,15 @@ ui:
68
71
  button-padding:
69
72
  $type: dimension
70
73
  $value: "10px 14px"
74
+ input-padding:
75
+ $type: dimension
76
+ $value: "9px 12px"
77
+ input-min-height:
78
+ $type: dimension
79
+ $value: "42px"
80
+ select-padding:
81
+ $type: dimension
82
+ $value: "9px 36px 9px 12px"
83
+ textarea-padding:
84
+ $type: dimension
85
+ $value: "10px 12px"
@@ -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');
@@ -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 @@
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 { reactTemplatesDir, reactRuntimeDir } 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';
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Preview vocabulary stubs — React components for authoring/preview only.
3
+ * Production apps use compile-time HTML lowering (lowerToJsx), not these stubs.
4
+ *
5
+ * All element names and style property lists come from dsl-catalog (SSoT).
6
+ */
7
+ import type { FormEvent, ReactElement, ReactNode } from 'react';
8
+ import { dispatchPreviewAction } from '../runtime/actionRuntime.js';
9
+ import { isHidden, toneClass } from '../runtime/style.js';
10
+ import { foldLiteralsToCss } from '../style/foldLiterals.js';
11
+ import { ELEMENT_CATALOG, LOWERING_STYLE_PROPERTIES } from '../../../src/schema/dsl-catalog.js';
12
+ import type { Action } from '../runtime/types.js';
13
+
14
+ function componentToKebab(name: string): string {
15
+ return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
16
+ }
17
+
18
+ function previewClassName(elementName: string, props: Record<string, unknown>): string {
19
+ const kebab = componentToKebab(elementName);
20
+ const parts = [`vc-${kebab}`];
21
+ if (typeof props.level === 'number') {
22
+ parts.push(`vc-${kebab}-${props.level}`);
23
+ }
24
+ const variant = props.variant ?? props.tone;
25
+ if (typeof variant === 'string') {
26
+ parts.push(`vc-${kebab}--${variant}`);
27
+ if (props.tone) parts.push(toneClass(variant));
28
+ }
29
+ return parts.join(' ');
30
+ }
31
+
32
+ function previewStyleProps(props: Record<string, unknown>): Record<string, unknown> {
33
+ const out: Record<string, unknown> = {};
34
+ for (const [key, value] of Object.entries(props)) {
35
+ if (LOWERING_STYLE_PROPERTIES.has(key)) out[key] = value;
36
+ }
37
+ return out;
38
+ }
39
+
40
+ function vocabularyStub(elementName: string) {
41
+ return function VocabularyElement(props: Record<string, unknown>): ReactElement | null {
42
+ if (isHidden(props)) return null;
43
+ const { children, action, onClick, onSubmit, label } = props;
44
+ const clickAction = (action ?? onClick) as Action | undefined;
45
+ const submitAction = (action ?? onSubmit) as Action | undefined;
46
+ const onClickHandler = clickAction
47
+ ? () => { void dispatchPreviewAction(clickAction); }
48
+ : undefined;
49
+ const onSubmitHandler = submitAction
50
+ ? (event: FormEvent<HTMLDivElement>) => {
51
+ event.preventDefault();
52
+ void dispatchPreviewAction(submitAction);
53
+ }
54
+ : undefined;
55
+ const content = (children as ReactNode) ?? (typeof label === 'string' ? label : null);
56
+ const inlineStyle = foldLiteralsToCss(previewStyleProps(props));
57
+ return (
58
+ <div
59
+ data-vc-element={elementName}
60
+ className={previewClassName(elementName, props)}
61
+ style={Object.keys(inlineStyle).length > 0 ? inlineStyle : undefined}
62
+ onClick={onClickHandler}
63
+ onSubmit={onSubmitHandler}
64
+ >
65
+ {content}
66
+ </div>
67
+ );
68
+ };
69
+ }
70
+
71
+ /** All vocabulary stubs keyed by element name, derived from ELEMENT_CATALOG. */
72
+ export const vocabularyStubs: Record<string, ReturnType<typeof vocabularyStub>> =
73
+ Object.fromEntries(
74
+ Object.keys(ELEMENT_CATALOG).map((name) => [name, vocabularyStub(name)]),
75
+ );
@@ -20,13 +20,6 @@ button, input, textarea, select { font: inherit; }
20
20
  .vc-link { color: var(--ui-primary); text-decoration: none; }
21
21
  .vc-link:hover { text-decoration: underline; }
22
22
 
23
- .ui-tone-default { color: var(--ui-text); }
24
- .ui-tone-muted { color: var(--ui-text-muted); }
25
- .ui-tone-primary { color: var(--ui-primary); }
26
- .ui-tone-success { color: var(--ui-success); }
27
- .ui-tone-warning { color: var(--ui-warning); }
28
- .ui-tone-danger { color: var(--ui-danger); }
29
-
30
23
  .vc-button { border: 0; cursor: pointer; }
31
24
 
32
25
  .vc-form { display: flex; flex-direction: column; }
@@ -34,19 +27,13 @@ button, input, textarea, select { font: inherit; }
34
27
  .vc-field-label { font-size: 13px; font-weight: 700; color: var(--ui-text); }
35
28
  .vc-field-help { font-size: 12px; color: var(--ui-text-muted); }
36
29
  .vc-field-error { font-size: 12px; color: var(--ui-danger); }
37
- .vc-text-input, .vc-text-area { width: 100%; border: 1px solid var(--ui-border); border-radius: 10px; background: #fff; color: var(--ui-text); outline: none; }
30
+ .vc-text-input, .vc-text-area { width: 100%; outline: none; }
38
31
  .vc-text-input:focus, .vc-text-area:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
39
32
  .vc-field-sm { min-height: 34px; padding: 6px 10px; }
40
- .vc-field-md, .vc-text-input { min-height: 42px; padding: 9px 12px; }
33
+ .vc-field-md { min-height: 42px; }
41
34
  .vc-field-lg { min-height: 50px; padding: 12px 14px; }
42
35
  .vc-select {
43
36
  width: 100%;
44
- min-height: 42px;
45
- padding: 9px 36px 9px 12px;
46
- border: 1px solid var(--ui-border);
47
- border-radius: 10px;
48
- background-color: #fff;
49
- color: var(--ui-text);
50
37
  outline: none;
51
38
  appearance: none;
52
39
  -webkit-appearance: none;
@@ -58,8 +45,13 @@ button, input, textarea, select { font: inherit; }
58
45
  background-size: 16px;
59
46
  }
60
47
  .vc-select:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
61
- .vc-field-lg { min-height: 50px; padding: 12px 14px; }
62
- .vc-text-area { padding: 10px 12px; resize: vertical; }
48
+ .vc-text-area { resize: vertical; }
63
49
  .vc-checkbox { display: inline-flex; align-items: center; gap: 8px; }
64
50
  .vc-list { margin: 0; padding-left: 20px; }
65
51
  .vc-list-item { margin: 0; padding: 2px 0; }
52
+
53
+ .vc-table { width: 100%; display: flex; flex-direction: column; }
54
+ .vc-table-header { display: flex; background: var(--ui-surface-muted, #f8f9fa); }
55
+ .vc-table-column { text-align: left; padding: 12px 14px; font-size: 12px; font-weight: 600; color: var(--ui-text-muted, #6b7280); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
56
+ .vc-table-row { display: flex; align-items: center; border-top: 1px solid var(--ui-border, #e5e7eb); }
57
+ .vc-table-cell { padding: 14px; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
@@ -116,6 +116,17 @@ export function foldLiteralsToCss(literals: Record<string, unknown>): FoldedCss
116
116
  if (mapped) style.justifyContent = mapped;
117
117
  }
118
118
 
119
+ if (typeof literals.display === 'string') style.display = literals.display;
120
+ if (typeof literals.gridTemplateColumns === 'string') style.gridTemplateColumns = literals.gridTemplateColumns;
121
+ if (typeof literals.flexDirection === 'string') style.flexDirection = literals.flexDirection;
122
+ if (typeof literals.alignItems === 'string') style.alignItems = literals.alignItems;
123
+
124
+ const borderColor = literals.borderColor;
125
+ if (typeof borderColor === 'string') style.borderColor = tokenRefToCssVar(borderColor) ?? borderColor;
126
+
127
+ const flex = literals.flex;
128
+ if (typeof flex === 'number') style.flex = flex;
129
+
119
130
  return style;
120
131
  }
121
132