view-contracts 0.1.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.
@@ -0,0 +1,36 @@
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.1.0 — DO NOT EDIT */
2
+ export const ALLOWED_COMPONENTS = new Set([
3
+ 'App',
4
+ 'Box',
5
+ 'Button',
6
+ 'Checkbox',
7
+ 'Column',
8
+ 'Divider',
9
+ 'Drawer',
10
+ 'Field',
11
+ 'Form',
12
+ 'Heading',
13
+ 'Icon',
14
+ 'IconButton',
15
+ 'Image',
16
+ 'Link',
17
+ 'List',
18
+ 'ListItem',
19
+ 'Modal',
20
+ 'Page',
21
+ 'Popover',
22
+ 'Progress',
23
+ 'Radio',
24
+ 'Row',
25
+ 'SafeArea',
26
+ 'ScrollArea',
27
+ 'Select',
28
+ 'Spacer',
29
+ 'Spinner',
30
+ 'Stack',
31
+ 'Switch',
32
+ 'Text',
33
+ 'TextArea',
34
+ 'TextInput',
35
+ 'Tooltip',
36
+ ]);
@@ -0,0 +1,63 @@
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.1.0 — DO NOT EDIT */
2
+ export interface ViewAction { name: string; params?: Record<string, unknown> }
3
+ import type { Role, WidthSize, HeightSize, Align, Justify, ContentAlign, Axis, Direction, Scroll, Border, Shadow, TextAlign, LineLimit, TextOverflow, FontWeight, ImageFit, KeyboardType, Clip } from './tokens.js';
4
+
5
+ /** Shared DSL attributes from $defs.properties / properties catalog */
6
+ export interface DslProperties {
7
+ id?: string;
8
+ role?: Role;
9
+ label?: string;
10
+ visible?: boolean;
11
+ enabled?: boolean;
12
+ selected?: boolean;
13
+ checked?: boolean;
14
+ readonly?: boolean;
15
+ required?: boolean;
16
+ width?: WidthSize;
17
+ height?: HeightSize;
18
+ minWidth?: number;
19
+ maxWidth?: string | number;
20
+ minHeight?: number;
21
+ maxHeight?: string | number;
22
+ aspectRatio?: number;
23
+ weight?: number;
24
+ padding?: string | number;
25
+ gap?: string | number;
26
+ align?: Align;
27
+ justify?: Justify;
28
+ selfAlign?: Align;
29
+ contentAlign?: ContentAlign;
30
+ axis?: Axis;
31
+ direction?: Direction;
32
+ scroll?: Scroll;
33
+ background?: string;
34
+ foreground?: string;
35
+ border?: Border;
36
+ radius?: string;
37
+ shadow?: Shadow;
38
+ opacity?: number;
39
+ clip?: Clip;
40
+ textStyle?: string;
41
+ textAlign?: TextAlign;
42
+ lineLimit?: LineLimit;
43
+ textOverflow?: TextOverflow;
44
+ fontWeight?: FontWeight;
45
+ headingLevel?: number;
46
+ fit?: ImageFit;
47
+ src?: string;
48
+ alt?: string;
49
+ name?: string;
50
+ onClick?: ViewAction;
51
+ onSubmit?: ViewAction;
52
+ onChange?: ViewAction;
53
+ focusable?: boolean;
54
+ autoFocus?: boolean;
55
+ keyboardType?: KeyboardType;
56
+ value?: string | number | boolean;
57
+ binding?: string;
58
+ placeholder?: string;
59
+ options?: unknown[];
60
+ error?: string | null;
61
+ help?: string | null;
62
+ variant?: string;
63
+ }
@@ -0,0 +1,4 @@
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.1.0 — DO NOT EDIT */
2
+ export * from './tokens.js';
3
+ export * from './dsl-properties.js';
4
+ export * from './schema-document.js';
@@ -0,0 +1,33 @@
1
+ /** Auto-generated from spec/ui-dsl.meta.json + ui-dsl.schema.json — view-contracts v0.1.0 — DO NOT EDIT */
2
+ import type { Role, WidthSize, HeightSize, Align, Justify, ContentAlign, Axis, Direction, Scroll, Border, Shadow, TextAlign, LineLimit, TextOverflow, FontWeight, ImageFit, KeyboardType, Clip } from './tokens.js';
3
+
4
+ export type PropertyType = 'string' | 'number' | 'boolean' | 'array' | 'action' | 'object' | 'null' | string | string[];
5
+
6
+ export interface PropertyDef {
7
+ type: PropertyType;
8
+ enum?: string;
9
+ description?: string;
10
+ 'x-category': string;
11
+ 'x-appliesTo'?: string[];
12
+ 'x-notes'?: string;
13
+ $ref?: string;
14
+ }
15
+
16
+ export interface ChildrenConstraint {
17
+ 'x-kind': 'any' | 'none' | 'single' | 'text' | 'list';
18
+ 'x-items'?: string[];
19
+ }
20
+
21
+ export interface ElementDef {
22
+ description: string;
23
+ 'x-category': string;
24
+ 'x-notes'?: string;
25
+ properties: Record<string, PropertyDef | { $ref: string }>;
26
+ _children: ChildrenConstraint;
27
+ }
28
+
29
+ export interface UiDslSchema {
30
+ tokens: Record<string, string[]>;
31
+ properties: Record<string, PropertyDef>;
32
+ elements: Record<string, ElementDef>;
33
+ }
@@ -0,0 +1,19 @@
1
+ /** Auto-generated from spec/ui-dsl.schema.json — view-contracts v0.1.0 — DO NOT EDIT */
2
+ export type Role = "button" | "link" | "heading" | "list" | "listitem" | "main" | "dialog" | "status";
3
+ export type WidthSize = "wrap" | "fill";
4
+ export type HeightSize = "wrap" | "fill";
5
+ export type Align = "start" | "center" | "end" | "stretch";
6
+ export type Justify = "start" | "center" | "end" | "spaceBetween" | "spaceAround" | "spaceEvenly";
7
+ export type ContentAlign = "topStart" | "top" | "topEnd" | "start" | "center" | "end" | "bottomStart" | "bottom" | "bottomEnd";
8
+ export type Axis = "vertical" | "horizontal";
9
+ export type Direction = "ltr" | "rtl" | "system";
10
+ export type Scroll = "none" | "vertical" | "horizontal" | "both";
11
+ export type Border = "none";
12
+ export type Shadow = "none" | "sm" | "md" | "lg";
13
+ export type TextAlign = "start" | "center" | "end";
14
+ export type LineLimit = "none";
15
+ export type TextOverflow = "clip" | "ellipsis";
16
+ export type FontWeight = "regular" | "medium" | "bold";
17
+ export type ImageFit = "contain" | "cover" | "fill" | "none";
18
+ export type KeyboardType = "text" | "number" | "email" | "url" | "password" | "tel";
19
+ export type Clip = "none" | "bounds" | "shape";
@@ -0,0 +1,26 @@
1
+ import type { ViewAction } from '@view-contracts/core';
2
+ import { dispatchAction } from '@view-contracts/runtime';
3
+
4
+ const globalHandlers: Record<string, (params?: unknown) => void | Promise<void>> = {};
5
+
6
+ export function registerPreviewActions(
7
+ handlers: Record<string, (params?: unknown) => void | Promise<void>>,
8
+ ): void {
9
+ Object.assign(globalHandlers, handlers);
10
+ }
11
+
12
+ export function clearPreviewActions(): void {
13
+ for (const key of Object.keys(globalHandlers)) {
14
+ delete globalHandlers[key];
15
+ }
16
+ }
17
+
18
+ export async function dispatchPreviewAction(action?: ViewAction): Promise<void> {
19
+ if (!action) return;
20
+ const handler = globalHandlers[action.name];
21
+ if (handler) {
22
+ await handler(action.params);
23
+ return;
24
+ }
25
+ await dispatchAction(action);
26
+ }
@@ -0,0 +1,216 @@
1
+ import type { ElementType, FormEvent, ReactElement, ReactNode } from 'react';
2
+ import { dispatchPreviewAction } from './actionRuntime.js';
3
+ import { commonStyle, isHidden, toneClass } from './style.js';
4
+ import type { Action, Align, CommonProps, FieldSize, Justify, Option } from './types.js';
5
+
6
+ function cx(...names: Array<string | false | undefined>): string {
7
+ return names.filter(Boolean).join(' ');
8
+ }
9
+
10
+ function mapAlign(value?: Align): string | undefined {
11
+ if (!value) return undefined;
12
+ if (value === 'start') return 'flex-start';
13
+ if (value === 'end') return 'flex-end';
14
+ return value;
15
+ }
16
+
17
+ function mapJustify(value?: Justify): string | undefined {
18
+ if (!value) return undefined;
19
+ if (value === 'start') return 'flex-start';
20
+ if (value === 'end') return 'flex-end';
21
+ if (value === 'between') return 'space-between';
22
+ if (value === 'around') return 'space-around';
23
+ if (value === 'spaceBetween') return 'space-between';
24
+ if (value === 'spaceAround') return 'space-around';
25
+ if (value === 'spaceEvenly') return 'space-evenly';
26
+ return value;
27
+ }
28
+
29
+ function runAction(action?: Action): void {
30
+ void dispatchPreviewAction(action);
31
+ }
32
+
33
+ export function Page(props: CommonProps & { title?: string }): ReactElement | null {
34
+ if (isHidden(props)) return null;
35
+ return (
36
+ <main id={props.id} role={props.role} aria-label={props.ariaLabel ?? props.label} className="ui-page" style={commonStyle(props)}>
37
+ {props.children}
38
+ </main>
39
+ );
40
+ }
41
+
42
+ export function Box(props: CommonProps): ReactElement | null {
43
+ if (isHidden(props)) return null;
44
+ return (
45
+ <div id={props.id} role={props.role} aria-label={props.ariaLabel ?? props.label} className="ui-box" style={commonStyle(props)}>
46
+ {props.children}
47
+ </div>
48
+ );
49
+ }
50
+
51
+ export function Column(props: CommonProps & { align?: Align; justify?: Justify }): ReactElement | null {
52
+ if (isHidden(props)) return null;
53
+ return (
54
+ <div
55
+ id={props.id}
56
+ role={props.role}
57
+ aria-label={props.ariaLabel ?? props.label}
58
+ className="ui-column"
59
+ style={{ ...commonStyle(props), display: 'flex', flexDirection: 'column', alignItems: mapAlign(props.align), justifyContent: mapJustify(props.justify) }}
60
+ >
61
+ {props.children}
62
+ </div>
63
+ );
64
+ }
65
+
66
+ export function Stack(props: CommonProps & { contentAlign?: CommonProps['contentAlign'] }): ReactElement | null {
67
+ if (isHidden(props)) return null;
68
+ return (
69
+ <div id={props.id} role={props.role} aria-label={props.ariaLabel ?? props.label} className="ui-stack" style={commonStyle(props)}>
70
+ {props.children}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ export function Row(props: CommonProps & { align?: Align; justify?: Justify; wrap?: boolean }): ReactElement | null {
76
+ if (isHidden(props)) return null;
77
+ return (
78
+ <div
79
+ id={props.id}
80
+ role={props.role}
81
+ aria-label={props.ariaLabel ?? props.label}
82
+ className={cx('ui-row', props.wrap && 'ui-wrap')}
83
+ style={{ ...commonStyle(props), display: 'flex', flexDirection: 'row', alignItems: mapAlign(props.align), justifyContent: mapJustify(props.justify) }}
84
+ >
85
+ {props.children}
86
+ </div>
87
+ );
88
+ }
89
+
90
+ export function Spacer(props: CommonProps): ReactElement {
91
+ return <div aria-hidden className="ui-spacer" style={{ ...commonStyle(props), flexGrow: props.weight ?? 1 }} />;
92
+ }
93
+
94
+ export function Divider(props: CommonProps): ReactElement | null {
95
+ if (isHidden(props)) return null;
96
+ return <hr className="ui-divider" style={commonStyle(props)} />;
97
+ }
98
+
99
+ export function Heading(props: CommonProps & { level?: 1 | 2 | 3 | 4 | 5 | 6; children: ReactNode }): ReactElement | null {
100
+ if (isHidden(props)) return null;
101
+ const level = Math.min(6, Math.max(1, props.level ?? 2)) as 1 | 2 | 3 | 4 | 5 | 6;
102
+ const Tag = `h${level}` as ElementType;
103
+ return (
104
+ <Tag className={cx('ui-heading', `ui-heading-${level}`)} style={commonStyle(props)}>
105
+ {props.children}
106
+ </Tag>
107
+ );
108
+ }
109
+
110
+ export function Text(props: CommonProps & { children: ReactNode }): ReactElement | null {
111
+ if (isHidden(props)) return null;
112
+ return (
113
+ <p className="ui-text" style={commonStyle(props)}>
114
+ {props.children}
115
+ </p>
116
+ );
117
+ }
118
+
119
+ export function Link(props: CommonProps & { href?: string; onClick?: Action; children: ReactNode }): ReactElement | null {
120
+ if (isHidden(props)) return null;
121
+ return (
122
+ <a className="ui-link" style={commonStyle(props)} href={props.href} onClick={() => runAction(props.action ?? props.onClick)}>
123
+ {props.children}
124
+ </a>
125
+ );
126
+ }
127
+
128
+ export function Button(props: CommonProps & { type?: 'button' | 'submit'; children: ReactNode }): ReactElement | null {
129
+ if (isHidden(props)) return null;
130
+ return (
131
+ <button
132
+ id={props.id}
133
+ aria-label={props.ariaLabel ?? props.label}
134
+ className={cx('ui-button', `ui-button-${props.variant ?? 'secondary'}`)}
135
+ style={commonStyle(props)}
136
+ type={props.type ?? 'button'}
137
+ disabled={props.enabled === false}
138
+ onClick={() => runAction(props.action)}
139
+ >
140
+ {props.children}
141
+ </button>
142
+ );
143
+ }
144
+
145
+ export function Form(props: CommonProps & { action?: Action; children: ReactNode }): ReactElement | null {
146
+ if (isHidden(props)) return null;
147
+ function onSubmit(event: FormEvent<HTMLFormElement>) {
148
+ event.preventDefault();
149
+ runAction(props.action);
150
+ }
151
+ return (
152
+ <form id={props.id} aria-label={props.ariaLabel ?? props.label} className="ui-form" style={commonStyle(props)} onSubmit={onSubmit}>
153
+ {props.children}
154
+ </form>
155
+ );
156
+ }
157
+
158
+ function FieldFrame(props: { label?: string; help?: string; error?: string; children: ReactNode }): ReactElement {
159
+ return (
160
+ <label className="ui-field">
161
+ {props.label ? <span className="ui-field-label">{props.label}</span> : null}
162
+ {props.children}
163
+ {props.help ? <span className="ui-field-help">{props.help}</span> : null}
164
+ {props.error ? <span className="ui-field-error">{props.error}</span> : null}
165
+ </label>
166
+ );
167
+ }
168
+
169
+ export function TextInput(props: CommonProps & { name: string; label?: string; placeholder?: string; type?: string; required?: boolean; size?: FieldSize }): ReactElement | null {
170
+ if (isHidden(props)) return null;
171
+ return (
172
+ <FieldFrame label={props.label}>
173
+ <input className={cx('ui-input', `ui-field-${props.size ?? 'md'}`)} style={commonStyle(props)} name={props.name} placeholder={props.placeholder} type={props.type ?? 'text'} required={props.required} readOnly={props.readonly} disabled={props.enabled === false} />
174
+ </FieldFrame>
175
+ );
176
+ }
177
+
178
+ export function Select(props: CommonProps & { name: string; label?: string; options: Option[]; size?: FieldSize }): ReactElement | null {
179
+ if (isHidden(props)) return null;
180
+ return (
181
+ <FieldFrame label={props.label} help={props.help} error={props.error}>
182
+ <select className={cx('ui-select', `ui-field-${props.size ?? 'md'}`)} style={commonStyle(props)} name={props.name} disabled={props.enabled === false}>
183
+ {props.options.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
184
+ </select>
185
+ </FieldFrame>
186
+ );
187
+ }
188
+
189
+ export function TextArea(props: CommonProps & { name: string; label?: string; placeholder?: string; rows?: number }): ReactElement | null {
190
+ if (isHidden(props)) return null;
191
+ return (
192
+ <FieldFrame label={props.label} help={props.help} error={props.error}>
193
+ <textarea className="ui-textarea" style={commonStyle(props)} name={props.name} placeholder={props.placeholder} rows={props.rows ?? 4} readOnly={props.readonly} disabled={props.enabled === false} />
194
+ </FieldFrame>
195
+ );
196
+ }
197
+
198
+ export function Checkbox(props: CommonProps & { name: string; label: string }): ReactElement | null {
199
+ if (isHidden(props)) return null;
200
+ return (
201
+ <label className="ui-check">
202
+ <input type="checkbox" name={props.name} defaultChecked={props.checked} disabled={props.enabled === false} />
203
+ <span>{props.label}</span>
204
+ </label>
205
+ );
206
+ }
207
+
208
+ export function List(props: CommonProps & { children: ReactNode }): ReactElement | null {
209
+ if (isHidden(props)) return null;
210
+ return <ul role={props.role ?? 'list'} className="ui-list" style={commonStyle(props)}>{props.children}</ul>;
211
+ }
212
+
213
+ export function ListItem(props: CommonProps & { children: ReactNode }): ReactElement | null {
214
+ if (isHidden(props)) return null;
215
+ return <li role={props.role ?? 'listitem'} className="ui-list-item" style={commonStyle(props)}>{props.children}</li>;
216
+ }
@@ -0,0 +1,4 @@
1
+ export * from './components.js';
2
+ export * from './types.js';
3
+ export { commonStyle, isHidden, toneClass } from './style.js';
4
+ export { registerPreviewActions, clearPreviewActions, dispatchPreviewAction } from './actionRuntime.js';
@@ -0,0 +1,53 @@
1
+ import type { FormEvent, ReactElement, ReactNode } from 'react';
2
+ import { dispatchPreviewAction } from './actionRuntime.js';
3
+ import { commonStyle, isHidden, toneClass } from './style.js';
4
+ import type { Action, Align, CommonProps, Justify } from './types.js';
5
+
6
+ export function cx(...names: Array<string | false | undefined>): string {
7
+ return names.filter(Boolean).join(' ');
8
+ }
9
+
10
+ export function mapAlign(value?: Align): string | undefined {
11
+ if (!value) return undefined;
12
+ if (value === 'start') return 'flex-start';
13
+ if (value === 'end') return 'flex-end';
14
+ return value;
15
+ }
16
+
17
+ export function mapJustify(value?: Justify): string | undefined {
18
+ if (!value) return undefined;
19
+ if (value === 'start') return 'flex-start';
20
+ if (value === 'end') return 'flex-end';
21
+ if (value === 'between' || value === 'spaceBetween') return 'space-between';
22
+ if (value === 'around' || value === 'spaceAround') return 'space-around';
23
+ if (value === 'spaceEvenly') return 'space-evenly';
24
+ return value;
25
+ }
26
+
27
+ export function runAction(action?: Action): void {
28
+ void dispatchPreviewAction(action);
29
+ }
30
+
31
+ export function shellProps(props: CommonProps): Pick<CommonProps, 'id' | 'testId' | 'ariaLabel' | 'role'> {
32
+ return { id: props.id, testId: props.testId, ariaLabel: props.ariaLabel ?? props.label, role: props.role };
33
+ }
34
+
35
+ export function FieldFrame(props: { label?: string; help?: string; error?: string; children: ReactNode }): ReactElement {
36
+ return (
37
+ <div className="ui-field">
38
+ {props.label ? <span className="ui-field-label">{props.label}</span> : null}
39
+ {props.children}
40
+ {props.error ? <span className="ui-field-error">{props.error}</span> : null}
41
+ {!props.error && props.help ? <span className="ui-field-help">{props.help}</span> : null}
42
+ </div>
43
+ );
44
+ }
45
+
46
+ export function onFormSubmit(action?: Action) {
47
+ return (event: FormEvent<HTMLFormElement>) => {
48
+ event.preventDefault();
49
+ runAction(action);
50
+ };
51
+ }
52
+
53
+ export { commonStyle, isHidden, toneClass };
@@ -0,0 +1,69 @@
1
+ import type { CSSProperties } from 'react';
2
+ import type { Clip, ContentAlign, Shadow } from '../../../generated/schema/tokens.js';
3
+ import type { DslProperties } from '../../../generated/schema/dsl-properties.js';
4
+
5
+ function len(value: number): string {
6
+ return `${value}px`;
7
+ }
8
+
9
+ const SHADOW: Record<Exclude<Shadow, 'none'>, string> = {
10
+ sm: '0 1px 3px rgba(15,23,42,0.08)',
11
+ md: '0 10px 30px rgba(15, 23, 42, 0.08)',
12
+ lg: '0 20px 40px rgba(15, 23, 42, 0.12)',
13
+ };
14
+
15
+ const CONTENT_ALIGN: Record<ContentAlign, string> = {
16
+ topStart: 'start start',
17
+ top: 'center start',
18
+ topEnd: 'end start',
19
+ start: 'start center',
20
+ center: 'center center',
21
+ end: 'end center',
22
+ bottomStart: 'start end',
23
+ bottom: 'center end',
24
+ bottomEnd: 'end end',
25
+ };
26
+
27
+ function shadowStyle(shadow: Shadow | undefined): string | undefined {
28
+ if (!shadow || shadow === 'none') return undefined;
29
+ return SHADOW[shadow];
30
+ }
31
+
32
+ export function commonStyle(props: DslProperties): CSSProperties {
33
+ const style: CSSProperties = {};
34
+ if (typeof props.width === 'number') style.width = len(props.width);
35
+ if (typeof props.minWidth === 'number') style.minWidth = len(props.minWidth);
36
+ if (typeof props.maxWidth === 'number') style.maxWidth = len(props.maxWidth);
37
+ if (typeof props.height === 'number') style.height = len(props.height);
38
+ if (typeof props.minHeight === 'number') style.minHeight = len(props.minHeight);
39
+ if (typeof props.maxHeight === 'number') style.maxHeight = len(props.maxHeight);
40
+ if (props.aspectRatio !== undefined) style.aspectRatio = String(props.aspectRatio);
41
+ if (props.weight !== undefined) style.flexGrow = props.weight;
42
+ if (typeof props.padding === 'number') style.padding = len(props.padding);
43
+ if (typeof props.margin === 'number') style.margin = len(props.margin);
44
+ if (typeof props.gap === 'number') style.gap = len(props.gap);
45
+ if (typeof props.radius === 'number') style.borderRadius = len(props.radius);
46
+ if (props.border) style.border = '1px solid var(--ui-border)';
47
+ const shadow = shadowStyle(props.shadow);
48
+ if (shadow) style.boxShadow = shadow;
49
+ if (props.opacity !== undefined) style.opacity = props.opacity;
50
+ if (props.clip && props.clip !== 'none') style.overflow = 'hidden';
51
+ if (props.background) style.background = `var(--ui-${props.background}, ${props.background})`;
52
+ if (props.foreground) style.color = `var(--ui-${props.foreground}, ${props.foreground})`;
53
+ if (props.textAlign) style.textAlign = props.textAlign === 'start' ? 'left' : props.textAlign === 'end' ? 'right' : 'center';
54
+ if (props.contentAlign) style.placeItems = CONTENT_ALIGN[props.contentAlign];
55
+ if (props.overflow) style.overflow = props.overflow;
56
+ if (props.selfAlign) style.alignSelf = props.selfAlign === 'start' ? 'flex-start' : props.selfAlign === 'end' ? 'flex-end' : props.selfAlign;
57
+ return style;
58
+ }
59
+
60
+ export function isHidden(props: DslProperties): boolean {
61
+ return props.visible === false;
62
+ }
63
+
64
+ /** Sample/extension helper — tone is not in ui-dsl.schema.json */
65
+ export function toneClass(tone: string | undefined): string {
66
+ return tone ? `ui-tone-${tone}` : 'ui-tone-default';
67
+ }
68
+
69
+ export type { Clip, ContentAlign, Shadow };
@@ -0,0 +1,68 @@
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
+ * { box-sizing: border-box; }
21
+ body { margin: 0; background: var(--ui-bg); color: var(--ui-text); font-family: var(--ui-font); }
22
+ button, input, textarea, select { font: inherit; }
23
+
24
+ .ui-page { min-height: 100vh; }
25
+ .ui-box { display: block; }
26
+ .ui-column { display: flex; flex-direction: column; }
27
+ .ui-stack { display: flex; flex-direction: column; }
28
+ .ui-row { display: flex; flex-direction: row; }
29
+ .ui-wrap { flex-wrap: wrap; }
30
+ .ui-spacer { min-width: 0; min-height: 0; }
31
+ .ui-divider { border: 0; border-top: 1px solid var(--ui-border); width: 100%; }
32
+
33
+ .ui-heading { margin: 0; line-height: 1.2; letter-spacing: -0.02em; }
34
+ .ui-heading-1 { font-size: 32px; }
35
+ .ui-heading-2 { font-size: 22px; }
36
+ .ui-heading-3 { font-size: 18px; }
37
+ .ui-heading-4 { font-size: 15px; }
38
+ .ui-text { margin: 0; line-height: 1.6; }
39
+ .ui-link { color: var(--ui-primary); text-decoration: none; }
40
+ .ui-link:hover { text-decoration: underline; }
41
+
42
+ .ui-tone-default { color: var(--ui-text); }
43
+ .ui-tone-muted { color: var(--ui-text-muted); }
44
+ .ui-tone-primary { color: var(--ui-primary); }
45
+ .ui-tone-success { color: var(--ui-success); }
46
+ .ui-tone-warning { color: var(--ui-warning); }
47
+ .ui-tone-danger { color: var(--ui-danger); }
48
+
49
+ .ui-button { border: 0; border-radius: 10px; padding: 10px 14px; font-weight: 700; cursor: pointer; }
50
+ .ui-button-primary { background: var(--ui-primary); color: #fff; }
51
+ .ui-button-secondary { background: #eef2f7; color: var(--ui-text); }
52
+ .ui-button-ghost { background: transparent; color: var(--ui-text); }
53
+ .ui-button-danger { background: var(--ui-danger); color: #fff; }
54
+
55
+ .ui-form { display: flex; flex-direction: column; }
56
+ .ui-field { display: flex; flex-direction: column; gap: 6px; }
57
+ .ui-field-label { font-size: 13px; font-weight: 700; color: var(--ui-text); }
58
+ .ui-field-help { font-size: 12px; color: var(--ui-text-muted); }
59
+ .ui-field-error { font-size: 12px; color: var(--ui-danger); }
60
+ .ui-input, .ui-select, .ui-textarea { width: 100%; border: 1px solid var(--ui-border); border-radius: 10px; background: #fff; color: var(--ui-text); outline: none; }
61
+ .ui-input:focus, .ui-select:focus, .ui-textarea:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
62
+ .ui-field-sm { min-height: 34px; padding: 6px 10px; }
63
+ .ui-field-md, .ui-input, .ui-select { min-height: 42px; padding: 9px 12px; }
64
+ .ui-field-lg { min-height: 50px; padding: 12px 14px; }
65
+ .ui-textarea { padding: 10px 12px; resize: vertical; }
66
+ .ui-check { display: inline-flex; align-items: center; gap: 8px; }
67
+ .ui-list { margin: 0; padding-left: 20px; }
68
+ .ui-list-item { margin: 0; padding: 2px 0; }
@@ -0,0 +1,46 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ViewAction } from '@view-contracts/core';
3
+ import type { DslProperties } from '../../../generated/schema/dsl-properties.js';
4
+
5
+ export type {
6
+ Align,
7
+ Axis,
8
+ Border,
9
+ Breakpoint,
10
+ Clip,
11
+ ContentAlign,
12
+ Direction,
13
+ FontWeight,
14
+ HeightSize,
15
+ ImageFit,
16
+ Justify,
17
+ KeyboardType,
18
+ LineLimit,
19
+ Overflow,
20
+ Role,
21
+ Scroll,
22
+ Shadow,
23
+ TextAlign,
24
+ TextOverflow,
25
+ WidthSize,
26
+ } from '../../../generated/schema/tokens.js';
27
+
28
+ export type { DslProperties };
29
+
30
+ /** view-contracts view TSX action binding (schema property: onClick / onChange). */
31
+ export type Action = ViewAction;
32
+
33
+ /** React preview props: schema catalog + JSX children + view action alias. */
34
+ export interface CommonProps extends DslProperties {
35
+ children?: ReactNode;
36
+ /** View-layer alias for onClick in interactive components. */
37
+ action?: ViewAction;
38
+ }
39
+
40
+ export interface Option {
41
+ label: string;
42
+ value: string;
43
+ }
44
+
45
+ /** Preview-only field density (not in ui-dsl.schema.json). */
46
+ export type FieldSize = 'sm' | 'md' | 'lg';