view-contracts 0.3.0 → 0.3.3

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.
@@ -1,15 +1,16 @@
1
1
  import type { CSSProperties } from 'react';
2
- import type { Align, ContentAlign, FontWeight, Justify } from '../../../src/generated/schema/dsl-enums.js';
3
- import type { CommonProps } from './types.js';
2
+ import type { LoweredStyleInput } from './types.js';
4
3
 
5
- export function mapAlign(value?: Align): string | undefined {
4
+ /** Map flex alignment shorthand (from lowering) to CSS align-items / align-self values. */
5
+ export function mapAlign(value?: string): string | undefined {
6
6
  if (!value) return undefined;
7
7
  if (value === 'start') return 'flex-start';
8
8
  if (value === 'end') return 'flex-end';
9
9
  return value;
10
10
  }
11
11
 
12
- export function mapJustify(value?: Justify | string): string | undefined {
12
+ /** Map flex justification shorthand (from lowering) to CSS justify-content values. */
13
+ export function mapJustify(value?: string): string | undefined {
13
14
  if (!value) return undefined;
14
15
  if (value === 'start') return 'flex-start';
15
16
  if (value === 'end') return 'flex-end';
@@ -23,7 +24,7 @@ function len(value: number): string {
23
24
  return `${value}px`;
24
25
  }
25
26
 
26
- const CONTENT_ALIGN: Record<ContentAlign, string> = {
27
+ const PLACE_ITEMS: Record<string, string> = {
27
28
  topStart: 'start start',
28
29
  top: 'center start',
29
30
  topEnd: 'end start',
@@ -40,7 +41,7 @@ function fontSizeStyle(size: number | string | undefined): string | undefined {
40
41
  return typeof size === 'number' ? len(size) : size;
41
42
  }
42
43
 
43
- export function commonStyle(props: CommonProps): CSSProperties {
44
+ export function commonStyle(props: LoweredStyleInput): CSSProperties {
44
45
  const style: CSSProperties = {};
45
46
  if (typeof props.width === 'number') style.width = len(props.width);
46
47
  if (typeof props.minWidth === 'number') style.minWidth = len(props.minWidth);
@@ -51,24 +52,29 @@ export function commonStyle(props: CommonProps): CSSProperties {
51
52
  if (props.aspectRatio !== undefined) style.aspectRatio = String(props.aspectRatio);
52
53
  if (typeof props.weight === 'number') style.flexGrow = props.weight;
53
54
  if (typeof props.padding === 'number') style.padding = len(props.padding);
54
- if (typeof (props as { margin?: number }).margin === 'number') style.margin = len((props as { margin: number }).margin);
55
+ if (typeof props.margin === 'number') style.margin = len(props.margin);
55
56
  if (typeof props.gap === 'number') style.gap = len(props.gap);
56
- if (props.textAlign) style.textAlign = props.textAlign === 'start' ? 'left' : props.textAlign === 'end' ? 'right' : 'center';
57
- if (props.contentAlign) style.placeItems = CONTENT_ALIGN[props.contentAlign];
58
- if ((props as { overflow?: string }).overflow) style.overflow = (props as { overflow: string }).overflow;
59
- if (props.selfAlign) style.alignSelf = props.selfAlign === 'start' ? 'flex-start' : props.selfAlign === 'end' ? 'flex-end' : props.selfAlign;
57
+ if (props.textAlign) {
58
+ style.textAlign = props.textAlign === 'start' ? 'left' : props.textAlign === 'end' ? 'right' : props.textAlign as CSSProperties['textAlign'];
59
+ }
60
+ if (props.contentAlign) style.placeItems = PLACE_ITEMS[props.contentAlign];
61
+ if (props.overflow) style.overflow = props.overflow as CSSProperties['overflow'];
62
+ if (props.selfAlign) {
63
+ style.alignSelf = props.selfAlign === 'start' ? 'flex-start' : props.selfAlign === 'end' ? 'flex-end' : props.selfAlign;
64
+ }
60
65
  const fontSize = fontSizeStyle(props.size);
61
66
  if (fontSize) style.fontSize = fontSize;
62
67
  const fontWeight = typeof props.weight === 'string' ? props.weight : props.fontWeight;
63
- if (fontWeight) style.fontWeight = fontWeight as FontWeight;
68
+ if (fontWeight) style.fontWeight = fontWeight;
64
69
  return style;
65
70
  }
66
71
 
67
- export function isHidden(props: CommonProps): boolean {
68
- return props.visible === false;
72
+ /** Preview vocabulary helper lowered production JSX does not use visible. */
73
+ export function isHidden(props: { visible?: boolean; hidden?: boolean }): boolean {
74
+ return props.hidden === true || props.visible === false;
69
75
  }
70
76
 
71
- /** Sample/extension helper tone is not in ui-dsl.schema.json */
77
+ /** Sample/extension helper for preview tone class names. */
72
78
  export function toneClass(tone: string | undefined): string {
73
79
  return tone ? `ui-tone-${tone}` : 'ui-tone-default';
74
80
  }
@@ -97,5 +103,3 @@ export function extensionClassName(
97
103
  if (options?.extra) parts.push(options.extra);
98
104
  return parts.filter(Boolean).join(' ');
99
105
  }
100
-
101
- export type { ContentAlign };
@@ -1,22 +1,3 @@
1
- :root {
2
- --ui-bg: #f6f8fb;
3
- --ui-surface: #ffffff;
4
- --ui-surface-muted: #f8fafc;
5
- --ui-text: #111827;
6
- --ui-text-muted: #6b7280;
7
- --ui-primary: #2563eb;
8
- --ui-primary-weak: #dbeafe;
9
- --ui-success: #047857;
10
- --ui-success-weak: #d1fae5;
11
- --ui-warning: #b45309;
12
- --ui-warning-weak: #fef3c7;
13
- --ui-danger: #dc2626;
14
- --ui-danger-weak: #fee2e2;
15
- --ui-border: #e5e7eb;
16
- --ui-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
17
- --ui-font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
18
- }
19
-
20
1
  * { box-sizing: border-box; }
21
2
  body { margin: 0; background: var(--ui-bg); color: var(--ui-text); font-family: var(--ui-font); }
22
3
  button, input, textarea, select { font: inherit; }
@@ -53,31 +34,32 @@ button, input, textarea, select { font: inherit; }
53
34
  .vc-field-label { font-size: 13px; font-weight: 700; color: var(--ui-text); }
54
35
  .vc-field-help { font-size: 12px; color: var(--ui-text-muted); }
55
36
  .vc-field-error { font-size: 12px; color: var(--ui-danger); }
56
- .vc-text-input, .vc-select, .vc-text-area { width: 100%; border: 1px solid var(--ui-border); border-radius: 10px; background: #fff; color: var(--ui-text); outline: none; }
57
- .vc-text-input:focus, .vc-select:focus, .vc-text-area:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
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; }
38
+ .vc-text-input:focus, .vc-text-area:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
58
39
  .vc-field-sm { min-height: 34px; padding: 6px 10px; }
59
- .vc-field-md, .vc-text-input, .vc-select { min-height: 42px; padding: 9px 12px; }
40
+ .vc-field-md, .vc-text-input { min-height: 42px; padding: 9px 12px; }
41
+ .vc-field-lg { min-height: 50px; padding: 12px 14px; }
42
+ .vc-select {
43
+ 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
+ outline: none;
51
+ appearance: none;
52
+ -webkit-appearance: none;
53
+ -moz-appearance: none;
54
+ cursor: pointer;
55
+ 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");
56
+ background-repeat: no-repeat;
57
+ background-position: right 12px center;
58
+ background-size: 16px;
59
+ }
60
+ .vc-select:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
60
61
  .vc-field-lg { min-height: 50px; padding: 12px 14px; }
61
62
  .vc-text-area { padding: 10px 12px; resize: vertical; }
62
63
  .vc-checkbox { display: inline-flex; align-items: center; gap: 8px; }
63
64
  .vc-list { margin: 0; padding-left: 20px; }
64
65
  .vc-list-item { margin: 0; padding: 2px 0; }
65
-
66
- /* Extension components — structural layout; variant colors from theme.yaml via emitVariantCss */
67
- .vc-container { width: 100%; max-width: 1180px; margin: 0 auto; }
68
- .vc-grid { display: grid; }
69
- .vc-card { border: 1px solid var(--ui-border); }
70
- .vc-badge { display: inline-flex; align-items: center; justify-content: center; padding: 4px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; }
71
- .vc-alert { display: flex; gap: 12px; border: 1px solid var(--ui-border); border-radius: 12px; padding: 14px; }
72
- .vc-alert--success { border-color: #a7f3d0; }
73
- .vc-alert--warning { border-color: #fde68a; }
74
- .vc-alert--danger { border-color: #fecaca; }
75
- .vc-table-wrap { overflow-x: auto; border: 1px solid var(--ui-border); border-radius: 14px; background: #fff; }
76
- .vc-table { width: 100%; border-collapse: collapse; }
77
- .vc-table th { background: var(--ui-surface-muted); color: var(--ui-text-muted); font-size: 12px; text-align: left; padding: 12px 14px; }
78
- .vc-table td { border-top: 1px solid var(--ui-border); padding: 14px; font-size: 14px; }
79
- .vc-admin-shell { display: grid; grid-template-columns: 240px 1fr; min-height: 100vh; }
80
- .vc-admin-sidebar { background: #0f172a; color: #cbd5e1; padding: 24px 18px; }
81
- .vc-admin-nav-item { padding: 10px 12px; border-radius: 10px; color: #cbd5e1; }
82
- .vc-admin-nav-item--active { background: rgba(255,255,255,0.1); color: #fff; }
83
- .vc-admin-content { padding: 32px; }
@@ -1,26 +1,4 @@
1
- import type { ReactNode } from 'react';
2
- import type { DslProperties } from '../../../src/generated/schema/dsl-properties.js';
3
- import type { FontWeight, Justify, Shadow, WidthSize } from '../../../src/generated/schema/dsl-enums.js';
4
-
5
- export type {
6
- Align,
7
- Axis,
8
- Border,
9
- Clip,
10
- ContentAlign,
11
- Direction,
12
- FontWeight,
13
- HeightSize,
14
- ImageFit,
15
- Justify,
16
- KeyboardType,
17
- LineLimit,
18
- TextAlign,
19
- TextOverflow,
20
- WidthSize,
21
- } from '../../../src/generated/schema/dsl-enums.js';
22
-
23
- export type { DslProperties };
1
+ /** Runtime types for lowered React output and project extensions — no DSL schema imports. */
24
2
 
25
3
  export interface ViewAction {
26
4
  name: string;
@@ -29,35 +7,34 @@ export interface ViewAction {
29
7
 
30
8
  export type DispatchableAction = ViewAction;
31
9
 
32
- /** view-contracts view TSX action binding (schema property: onClick / onChange). */
10
+ /** Action binding from lowered views or hand-written extensions. */
33
11
  export type Action = ViewAction;
34
12
 
35
13
  /**
36
- * Preview + sample extension props layered on the DSL catalog.
37
- * Widens a few catalog types so sample views can use pragmatic literals (e.g. radius={16}, justify="between").
14
+ * Style props passed to commonStyle() by the React lowering pass.
15
+ * Numeric dimensions are logical units; commonStyle maps them to px.
38
16
  */
39
- export interface CommonProps extends Omit<DslProperties, 'justify' | 'radius' | 'shadow' | 'width' | 'weight' | 'fontWeight'> {
40
- children?: ReactNode;
41
- /** View-layer alias for onClick in interactive components. */
42
- action?: ViewAction;
43
- justify?: Justify | 'between' | 'around';
44
- radius?: string | number;
45
- shadow?: Shadow | boolean;
46
- width?: WidthSize | number;
47
- /** Flex grow (number) or sample font weight alias (string) on Text. */
17
+ export interface LoweredStyleInput {
18
+ width?: number | string;
19
+ minWidth?: number | string;
20
+ maxWidth?: number | string;
21
+ height?: number | string;
22
+ minHeight?: number | string;
23
+ maxHeight?: number | string;
24
+ aspectRatio?: number | string;
48
25
  weight?: number | string;
49
- fontWeight?: FontWeight | string;
50
- /** Sample/preview extension props (not in ui-dsl.schema.json). */
51
- tone?: string;
26
+ padding?: number | string;
27
+ margin?: number;
28
+ gap?: number | string;
29
+ textAlign?: string;
30
+ contentAlign?: string;
31
+ selfAlign?: string;
32
+ overflow?: string;
52
33
  size?: number | string;
53
- ariaLabel?: string;
54
- testId?: string;
34
+ fontWeight?: string;
55
35
  }
56
36
 
57
37
  export interface Option {
58
38
  label: string;
59
39
  value: string;
60
40
  }
61
-
62
- /** Preview-only field density (not in ui-dsl.schema.json). */
63
- export type FieldSize = 'sm' | 'md' | 'lg';
@@ -0,0 +1,77 @@
1
+ import { readFile, writeFile, mkdir, stat, rm } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import type { ViewContractsConfig } from '../../src/config.js';
4
+ import { projectRoot } from '../../src/config.js';
5
+ import { renderTemplate } from '../../src/renderer/template.js';
6
+ import { reactRuntimeDir, reactTemplatesDir } from './paths.js';
7
+
8
+ async function pathExists(target: string): Promise<boolean> {
9
+ try {
10
+ await stat(target);
11
+ return true;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ async function removeStaleSchemaDir(componentsDir: string): Promise<void> {
18
+ const schemaDest = resolve(componentsDir, 'schema');
19
+ if (await pathExists(schemaDest)) {
20
+ await rm(schemaDest, { recursive: true, force: true });
21
+ }
22
+ }
23
+
24
+ async function copyStyleBundle(componentsDir: string): Promise<void> {
25
+ const runtimeSrc = reactRuntimeDir();
26
+ await writeFile(resolve(componentsDir, 'style.ts'), await readFile(resolve(runtimeSrc, 'style.ts'), 'utf-8'), 'utf-8');
27
+ await writeFile(resolve(componentsDir, 'types.ts'), await readFile(resolve(runtimeSrc, 'types.ts'), 'utf-8'), 'utf-8');
28
+ }
29
+
30
+ /** Copy package runtime (style, types, dispatch) into generated/ — standalone output, no renderers/ imports. */
31
+ export async function syncGeneratedRuntime(
32
+ configPath: string,
33
+ config: ViewContractsConfig,
34
+ templatesDir?: string,
35
+ ): Promise<{ componentsDir: string; runtimeDir: string }> {
36
+ const root = projectRoot(configPath);
37
+ const templates = reactTemplatesDir(templatesDir ?? (config.rendererTemplatesDir ? resolve(root, config.rendererTemplatesDir) : undefined));
38
+ const componentsDir = resolve(config.generated.reactDir, '../components');
39
+ const runtimeDir = resolve(config.generated.reactDir, '../runtime');
40
+
41
+ await mkdir(componentsDir, { recursive: true });
42
+ await mkdir(runtimeDir, { recursive: true });
43
+
44
+ await removeStaleSchemaDir(componentsDir);
45
+ await copyStyleBundle(componentsDir);
46
+ await writeFile(
47
+ resolve(componentsDir, 'style-helpers.ts'),
48
+ await renderTemplate(templates, 'style-helpers.ts.hbs', {}),
49
+ 'utf-8',
50
+ );
51
+ await writeFile(
52
+ resolve(componentsDir, 'index.ts'),
53
+ await renderTemplate(templates, 'components-index.ts.hbs', {}),
54
+ 'utf-8',
55
+ );
56
+ await writeFile(
57
+ resolve(runtimeDir, 'types.ts'),
58
+ await renderTemplate(templates, 'runtime-types.ts.hbs', {}),
59
+ 'utf-8',
60
+ );
61
+ await writeFile(
62
+ resolve(runtimeDir, 'dispatch.ts'),
63
+ await renderTemplate(templates, 'dispatch.ts.hbs', {}),
64
+ 'utf-8',
65
+ );
66
+
67
+ return { componentsDir, runtimeDir };
68
+ }
69
+
70
+ /** Rewrite preview virtual imports to generated/components-relative paths. */
71
+ export function rewriteExtensionImportsForGenerated(body: string): string {
72
+ return body
73
+ .replace(/from 'view-contracts:runtime\/dispatch'/g, "from '../runtime/dispatch.js'")
74
+ .replace(/from "view-contracts:runtime\/dispatch"/g, 'from "../runtime/dispatch.js"')
75
+ .replace(/from 'view-contracts:runtime\/types'/g, "from './types.js'")
76
+ .replace(/from "view-contracts:runtime\/types"/g, 'from "./types.js"');
77
+ }
@@ -2,4 +2,4 @@
2
2
  export * from './extensions.js';
3
3
  export type { TableColumn } from './extensions.js';
4
4
  export { commonStyle, mapAlign, mapJustify, isHidden, toneClass } from './style.js';
5
- export type { Action, CommonProps, Option } from './types.js';
5
+ export type { Action, Option, LoweredStyleInput } from './types.js';
@@ -0,0 +1,62 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://view-contracts.dev/spec/ui-dsl-extensions.meta.json",
4
+ "title": "View Contracts UI DSL Extensions Registry",
5
+ "type": "object",
6
+ "required": ["extensions"],
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "extensions": {
10
+ "type": "object",
11
+ "minProperties": 1,
12
+ "additionalProperties": {
13
+ "type": "object",
14
+ "required": ["description", "children", "renderer", "properties"],
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "description": { "type": "string", "minLength": 1 },
18
+ "children": {
19
+ "type": "string",
20
+ "enum": ["none", "any"]
21
+ },
22
+ "renderer": {
23
+ "type": "string",
24
+ "enum": ["react-only", "all"]
25
+ },
26
+ "properties": {
27
+ "type": "object",
28
+ "additionalProperties": {
29
+ "type": "object",
30
+ "required": ["type", "category"],
31
+ "additionalProperties": false,
32
+ "properties": {
33
+ "type": {
34
+ "type": "string",
35
+ "enum": ["string", "number", "boolean", "binding", "array", "object"]
36
+ },
37
+ "category": {
38
+ "type": "string",
39
+ "enum": [
40
+ "identity",
41
+ "state",
42
+ "size",
43
+ "spacing",
44
+ "alignment",
45
+ "direction",
46
+ "overflow",
47
+ "visual",
48
+ "text",
49
+ "image",
50
+ "interaction",
51
+ "data",
52
+ "validation"
53
+ ]
54
+ }
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
@@ -1,4 +1,4 @@
1
- /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.0 — DO NOT EDIT */
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.2 — DO NOT EDIT */
2
2
  export const ALLOWED_COMPONENTS = new Set([
3
3
  'App',
4
4
  'Box',
@@ -1,4 +1,4 @@
1
- /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.0 — DO NOT EDIT */
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.2 — DO NOT EDIT */
2
2
  export type Role = "button" | "link" | "heading" | "list" | "listitem" | "main" | "dialog" | "status";
3
3
  export type WidthSize = "wrap" | "fill";
4
4
  export type HeightSize = "wrap" | "fill";
@@ -1,4 +1,4 @@
1
- /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.0 — DO NOT EDIT */
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.2 — DO NOT EDIT */
2
2
  export interface ViewAction { name: string; params?: Record<string, unknown> }
3
3
  import type { Role, WidthSize, HeightSize, Align, Justify, ContentAlign, Axis, Direction, Scroll, Border, Shadow, TextAlign, LineLimit, TextOverflow, FontWeight, ImageFit, KeyboardType, Clip } from './dsl-enums.js';
4
4
 
@@ -1,4 +1,4 @@
1
- /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.0 — DO NOT EDIT */
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.2 — DO NOT EDIT */
2
2
  export * from './dsl-enums.js';
3
3
  export * from './dsl-properties.js';
4
4
  export * from './schema-document.js';
@@ -1,4 +1,4 @@
1
- /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.0 — DO NOT EDIT */
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.3.2 — DO NOT EDIT */
2
2
  /** Property names passed to commonStyle() during React HTML lowering (x-category from DSL + sample extensions). */
3
3
  export const LOWERING_STYLE_PROPERTIES = new Set<string>([
4
4
  "align",
@@ -1,4 +1,4 @@
1
- /** Auto-generated from renderers/react/element-config.meta.json — view-contracts v0.3.0 — DO NOT EDIT */
1
+ /** Auto-generated from renderers/react/element-config.meta.json — view-contracts v0.3.2 — DO NOT EDIT */
2
2
  /** Shape of entries in renderers/react/elements.yaml (target-specific HTML map, not DSL vocabulary). */
3
3
  export interface ReactRendererElementConfig {
4
4
  tag: string;
@@ -17,6 +17,8 @@ export interface ReactRendererElementConfig {
17
17
  sizeClass?: string;
18
18
  optionsProp?: string;
19
19
  checkbox?: boolean;
20
+ fieldContainer?: boolean;
21
+ scrollContainer?: boolean;
20
22
  defaultRole?: string;
21
23
  }
22
24
 
@@ -1,4 +1,4 @@
1
- /** Auto-generated from spec/ui-dsl.meta.json + ui-dsl.schema.json — view-contracts v0.3.0 — DO NOT EDIT */
1
+ /** Auto-generated from spec/ui-dsl.meta.json + ui-dsl.schema.json — view-contracts v0.3.2 — DO NOT EDIT */
2
2
  import type { Role, WidthSize, HeightSize, Align, Justify, ContentAlign, Axis, Direction, Scroll, Border, Shadow, TextAlign, LineLimit, TextOverflow, FontWeight, ImageFit, KeyboardType, Clip } from './dsl-enums.js';
3
3
 
4
4
  export type PropertyType = 'string' | 'number' | 'boolean' | 'array' | 'action' | 'object' | 'null' | string | string[];