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
@@ -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
+ );
@@ -0,0 +1,38 @@
1
+ /* DSL structural layer — layout/control mechanics only; no visual token values. */
2
+ * { box-sizing: border-box; }
3
+ body { margin: 0; background: var(--ui-bg); color: var(--ui-text); font-family: var(--ui-font); font-size: var(--ui-space-font-body); }
4
+ button, input, textarea, select { font: inherit; }
5
+
6
+ .vc-page { min-height: 100vh; min-width: 0; }
7
+ .vc-box { display: block; min-width: 0; }
8
+ .vc-column { display: flex; flex-direction: column; min-width: 0; }
9
+ .vc-stack { display: flex; flex-direction: column; min-width: 0; }
10
+ .vc-row { display: flex; flex-direction: row; min-width: 0; }
11
+ .vc-wrap { flex-wrap: wrap; }
12
+ .vc-spacer { min-width: 0; min-height: 0; }
13
+
14
+ .vc-heading { margin: 0; line-height: 1.2; letter-spacing: -0.02em; }
15
+ .vc-text { margin: 0; line-height: 1.6; }
16
+ .vc-link { color: var(--ui-primary); text-decoration: none; }
17
+ .vc-link:hover { text-decoration: underline; }
18
+ .vc-divider { border: 0; border-top: 1px solid var(--ui-border); width: 100%; }
19
+
20
+ .vc-button { border: 0; cursor: pointer; }
21
+
22
+ .vc-form { display: flex; flex-direction: column; min-width: 0; }
23
+ .vc-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
24
+
25
+ .vc-text-input, .vc-text-area, .vc-select { outline: none; }
26
+ .vc-text-input:focus, .vc-text-area:focus, .vc-select:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
27
+ .vc-select { appearance: none; -webkit-appearance: none; -moz-appearance: none; cursor: pointer; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath d='M4 6l4 4 4-4' stroke='%236b7280' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px; }
28
+ .vc-text-area { resize: vertical; }
29
+
30
+ .vc-checkbox { display: inline-flex; align-items: center; gap: 8px; }
31
+ .vc-list { margin: 0; padding-left: 20px; }
32
+ .vc-list-item { margin: 0; padding: 2px 0; }
33
+
34
+ .vc-table { width: 100%; display: flex; flex-direction: column; min-width: 0; }
35
+ .vc-table-header { display: flex; align-items: center; min-width: 0; }
36
+ .vc-table-column { display: flex; align-items: center; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
37
+ .vc-table-row { display: flex; align-items: center; border-top: 1px solid var(--ui-border); min-width: 0; }
38
+ .vc-table-cell { display: flex; align-items: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
@@ -39,13 +39,23 @@ export function foldLiteralsToCss(literals: Record<string, unknown>): FoldedCss
39
39
  const style: FoldedCss = {};
40
40
  const width = literals.width;
41
41
  if (typeof width === 'number') style.width = len(width);
42
- else if (typeof width === 'string' && width !== 'wrap' && width !== 'fill') style.width = width;
42
+ else if (width === 'fill') style.width = '100%';
43
+ else if (typeof width === 'string' && width !== 'wrap') style.width = width;
43
44
 
44
45
  if (typeof literals.minWidth === 'number') style.minWidth = len(literals.minWidth);
45
- if (typeof literals.maxWidth === 'number') style.maxWidth = len(literals.maxWidth);
46
- if (typeof literals.height === 'number') style.height = len(literals.height);
46
+ const maxWidth = literals.maxWidth;
47
+ if (typeof maxWidth === 'number') style.maxWidth = len(maxWidth);
48
+ else if (maxWidth === 'fill') style.maxWidth = '100%';
49
+
50
+ const height = literals.height;
51
+ if (typeof height === 'number') style.height = len(height);
52
+ else if (height === 'fill') style.height = '100%';
53
+ else if (typeof height === 'string' && height !== 'wrap') style.height = height;
54
+
47
55
  if (typeof literals.minHeight === 'number') style.minHeight = len(literals.minHeight);
48
- if (typeof literals.maxHeight === 'number') style.maxHeight = len(literals.maxHeight);
56
+ const maxHeight = literals.maxHeight;
57
+ if (typeof maxHeight === 'number') style.maxHeight = len(maxHeight);
58
+ else if (maxHeight === 'fill') style.maxHeight = '100%';
49
59
  if (literals.aspectRatio !== undefined) style.aspectRatio = String(literals.aspectRatio);
50
60
 
51
61
  const weight = literals.weight;
@@ -116,6 +126,17 @@ export function foldLiteralsToCss(literals: Record<string, unknown>): FoldedCss
116
126
  if (mapped) style.justifyContent = mapped;
117
127
  }
118
128
 
129
+ if (typeof literals.display === 'string') style.display = literals.display;
130
+ if (typeof literals.gridTemplateColumns === 'string') style.gridTemplateColumns = literals.gridTemplateColumns;
131
+ if (typeof literals.flexDirection === 'string') style.flexDirection = literals.flexDirection;
132
+ if (typeof literals.alignItems === 'string') style.alignItems = literals.alignItems;
133
+
134
+ const borderColor = literals.borderColor;
135
+ if (typeof borderColor === 'string') style.borderColor = tokenRefToCssVar(borderColor) ?? borderColor;
136
+
137
+ const flex = literals.flex;
138
+ if (typeof flex === 'number') style.flex = flex;
139
+
119
140
  return style;
120
141
  }
121
142
 
@@ -1,15 +1,53 @@
1
- # SwiftUI renderer scaffold
1
+ # SwiftUI Renderer
2
2
 
3
- **Not implemented yet** beyond theme / screen stubs. View IR lowering and modifier emission are future work.
3
+ Compiles View IR into native SwiftUI code at build time.
4
4
 
5
- SwiftUI `ViewModifier` chains are **compositional** (not CSS-style override). view-contracts may later introduce a separate **modifier pipeline** concept (wrapper-style transforms, analogous to nested HTML layout shells) — that is **out of scope for the current phase**. When styling is implemented, defaults + variant + instance props will be **folded at compile time** into resolved values, not chained for cascade-like override. See [docs/ARCHITECTURE.md](../../docs/ARCHITECTURE.md) § Native renderers.
5
+ ## Capabilities
6
6
 
7
- - Maps: `elements.yaml`, `properties.yaml` (hand-maintained under this directory)
8
- - Routes: `routes.yaml`
9
- - Templates: `templates/`
10
- - Theme codegen spec: [../NATIVE_THEME_EMIT.md](../NATIVE_THEME_EMIT.md)
7
+ - **Full DSL element coverage** — all core elements (Box, Stack, Row, Table, Form, Button, Text, etc.) map to SwiftUI views via `elements.yaml`
8
+ - **Theme codegen** — `VcTheme.swift` is generated from the design token / theme IR, supporting static and dynamic variant resolution
9
+ - **Modifier cascade** — DSL visual properties are folded at compile time into SwiftUI modifier chains following the correct order: foreground/font → padding → background → clipShape → shadow → overlay → frame → opacity
10
+ - **Padding shorthand** `"24px 18px"` style shorthand is parsed into `.padding(.vertical, N)` + `.padding(.horizontal, N)`
11
+ - **Dynamic variants** — `IrConcat` expressions (e.g. `` `badge-${row.statusTone}` ``) are lowered to runtime `VcTheme.resolve()` calls with a pre-built theme dictionary
12
+ - **Table layout** — `Table` / `TableHeader` / `TableRow` / `TableColumn` / `TableCell` with fixed widths, flex columns, and configurable `separator` / `separatorColor`
13
+ - **justify="between"** — `Row` elements with `justify="between"` emit `Spacer()` between children
14
+ - **Mock data SSoT** — `mock.yaml` is converted to `SampleData.json` and bundled into the Xcode project; Swift ViewModels decode via `JSONDecoder` (no hand-written sample data)
11
15
 
12
- Implement SwiftUI code generation by reading View IR + renderer maps.
13
- Theme is emitted at build time via `src/design/emit/swiftui.ts` and `templates/theme/*.hbs` — same Token IR + Theme IR as React `theme.css`, not runtime JSON.
16
+ ## Files
14
17
 
15
- Extend vocabulary via `spec/ui-dsl.schema.json` (from `ui-dsl-*.csv`), then update renderer maps.
18
+ | File | Role |
19
+ |---|---|
20
+ | `elements.yaml` | DSL element → SwiftUI view mapping |
21
+ | `elementMap.ts` | TypeScript loader for elements.yaml |
22
+ | `renderRuntime.ts` | Build orchestrator (theme, components, sample data JSON) |
23
+ | `renderComponents.ts` | Entry point for per-component Swift file generation |
24
+ | `renderTheme.ts` | `VcTheme.swift` generator (variant dictionary + `applyVcStyle`) |
25
+ | `lowering/lowerToSwiftUI.ts` | IR → SwiftUI body code |
26
+ | `lowering/modifiers.ts` | Visual property set → SwiftUI modifier chain |
27
+ | `templates/` | Handlebars templates for boilerplate Swift files |
28
+
29
+ ## Usage
30
+
31
+ Generated files are placed in `<project>/generated/swiftui/` during `view-contracts build`:
32
+
33
+ ```
34
+ generated/swiftui/
35
+ ├── VcTheme.swift # Theme + variant dictionary
36
+ ├── AdminDashboard.generated.swift # Screen view
37
+ ├── KpiCard.generated.swift # Partial views
38
+ ├── ...
39
+ └── AdminDashboard.SampleData.json # Mock data from YAML SSoT
40
+ ```
41
+
42
+ ## iOS Preview App
43
+
44
+ See `examples/admin-dashboard/ios-preview/` for a working SwiftUI preview app:
45
+
46
+ ```bash
47
+ cd examples/admin-dashboard/ios-preview
48
+ xcodegen generate # generates .xcodeproj from project.yml
49
+ # Open VcPreview.xcodeproj in Xcode, or build from CLI:
50
+ xcodebuild -scheme VcPreview -destination 'platform=iOS Simulator,...' build
51
+ ```
52
+
53
+ The preview app loads `SampleData.json` from the bundle and decodes it into typed `Codable` ViewModels.
@@ -0,0 +1,45 @@
1
+ import { resolve } from 'node:path';
2
+ import { packageRoot } from '../../src/lib/packageRoot.js';
3
+ import { loadYamlFile } from '../../src/renderer/elementMapValidation.js';
4
+
5
+ export interface SwiftUIElementConfig {
6
+ container?: 'group' | 'vstack' | 'hstack' | 'navstack' | 'scrollview' | 'field' | 'cellgroup' | 'tablestack';
7
+ leaf?: 'text' | 'button' | 'textfield' | 'texteditor' | 'picker' | 'spacer' | 'divider' | 'image' | 'progressview' | 'toggle';
8
+ spacingProp?: string;
9
+ }
10
+
11
+ export interface SwiftUIElementMap {
12
+ elements: Record<string, SwiftUIElementConfig>;
13
+ }
14
+
15
+ const SOURCE_LABEL = 'renderers/swiftui/elements.yaml';
16
+
17
+ let cachedMap: SwiftUIElementMap | null = null;
18
+
19
+ function validateSwiftUIElementMap(map: SwiftUIElementMap): void {
20
+ for (const [name, entry] of Object.entries(map.elements)) {
21
+ const hasContainer = entry.container !== undefined && entry.container !== null;
22
+ const hasLeaf = entry.leaf !== undefined && entry.leaf !== null;
23
+ if (hasContainer === hasLeaf) {
24
+ throw new Error(
25
+ `${SOURCE_LABEL}: element "${name}" must have exactly one of "container" or "leaf"`,
26
+ );
27
+ }
28
+ if ((entry.container === 'vstack' || entry.container === 'hstack') && entry.spacingProp === undefined) {
29
+ // spacingProp is optional for hstack/vstack containers without a DSL gap prop (e.g. TableHeader)
30
+ }
31
+ }
32
+ }
33
+
34
+ export async function loadSwiftUIElementMap(customPath?: string): Promise<SwiftUIElementMap> {
35
+ if (!customPath && cachedMap) return cachedMap;
36
+ const mapPath = customPath ?? resolve(packageRoot(), 'renderers/swiftui/elements.yaml');
37
+ const raw = await loadYamlFile<SwiftUIElementMap>(mapPath);
38
+ validateSwiftUIElementMap(raw);
39
+ if (!customPath) cachedMap = raw;
40
+ return raw;
41
+ }
42
+
43
+ export function isSwiftUIVocabulary(name: string, map: SwiftUIElementMap, allowlistExtra: string[] = []): boolean {
44
+ return name in map.elements && !allowlistExtra.includes(name);
45
+ }
@@ -0,0 +1,82 @@
1
+ # SwiftUI renderer element map — maps DSL elements to SwiftUI views.
2
+
3
+ elements:
4
+ Page:
5
+ container: navstack
6
+ Box:
7
+ container: group
8
+ Stack:
9
+ container: vstack
10
+ spacingProp: gap
11
+ Row:
12
+ container: hstack
13
+ spacingProp: gap
14
+ Column:
15
+ container: vstack
16
+ spacingProp: gap
17
+ Text:
18
+ leaf: text
19
+ Button:
20
+ leaf: button
21
+ Form:
22
+ container: vstack
23
+ spacingProp: gap
24
+ Field:
25
+ container: field
26
+ TextInput:
27
+ leaf: textfield
28
+ TextArea:
29
+ leaf: texteditor
30
+ Select:
31
+ leaf: picker
32
+ Table:
33
+ container: tablestack
34
+ TableHeader:
35
+ container: hstack
36
+ TableColumn:
37
+ container: cellgroup
38
+ TableRow:
39
+ container: hstack
40
+ TableCell:
41
+ container: cellgroup
42
+ Spacer:
43
+ leaf: spacer
44
+ Divider:
45
+ leaf: divider
46
+ Image:
47
+ leaf: image
48
+ Icon:
49
+ leaf: image
50
+ ScrollArea:
51
+ container: scrollview
52
+ List:
53
+ container: vstack
54
+ spacingProp: gap
55
+ ListItem:
56
+ container: group
57
+ Link:
58
+ leaf: text
59
+ Modal:
60
+ container: group
61
+ Progress:
62
+ leaf: progressview
63
+ Checkbox:
64
+ leaf: toggle
65
+ Switch:
66
+ leaf: toggle
67
+ Radio:
68
+ leaf: picker
69
+ IconButton:
70
+ leaf: button
71
+ Badge:
72
+ leaf: text
73
+ Chip:
74
+ leaf: text
75
+ Avatar:
76
+ leaf: image
77
+ Card:
78
+ container: group
79
+ SafeArea:
80
+ container: group
81
+ App:
82
+ container: group
@@ -0,0 +1,5 @@
1
+ export { loadSwiftUIElementMap, isSwiftUIVocabulary } from './elementMap.js';
2
+ export type { SwiftUIElementConfig, SwiftUIElementMap } from './elementMap.js';
3
+ export { renderSwiftComponent } from './renderComponents.js';
4
+ export { mergeVisualProps } from '../../src/renderer/style/mergeVisualProps.js';
5
+ export { visualPropSetToModifiers } from './lowering/modifiers.js';