view-contracts 0.4.0 → 0.4.2

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.
@@ -72,8 +72,39 @@ function resolveVariantEntries(
72
72
  case 'radius':
73
73
  case 'fontSize':
74
74
  case 'minHeight':
75
- case 'maxWidth':
76
- case 'width':
75
+ case 'maxWidth': {
76
+ const px = parsePx(value);
77
+ if (px !== null) entries.push({ property, swiftValue: String(px) });
78
+ break;
79
+ }
80
+ case 'width': {
81
+ if (value === '100%' || value === 'fill') {
82
+ entries.push({ property: 'widthFill', swiftValue: 'true' });
83
+ } else {
84
+ const px = parsePx(value);
85
+ if (px !== null) entries.push({ property, swiftValue: String(px) });
86
+ }
87
+ break;
88
+ }
89
+ case 'height': {
90
+ if (value === '100%' || value === 'fill') {
91
+ entries.push({ property: 'heightFill', swiftValue: 'true' });
92
+ } else {
93
+ const px = parsePx(value);
94
+ if (px !== null) entries.push({ property, swiftValue: String(px) });
95
+ }
96
+ break;
97
+ }
98
+ case 'border': {
99
+ const match = value.match(/^([\d.]+)px/);
100
+ if (match) entries.push({ property: 'borderWidth', swiftValue: String(Number(match[1])) });
101
+ break;
102
+ }
103
+ case 'borderColor': {
104
+ const color = swiftColorLiteral(value);
105
+ if (color) entries.push({ property: 'borderColor', swiftValue: color });
106
+ break;
107
+ }
77
108
  case 'margin': {
78
109
  const px = parsePx(value);
79
110
  if (px !== null) entries.push({ property, swiftValue: String(px) });
@@ -88,14 +119,27 @@ function resolveVariantEntries(
88
119
  entries.push({ property, swiftValue: value === 'true' || value === 'none' ? 'false' : 'true' });
89
120
  break;
90
121
  }
122
+ case 'display':
123
+ case 'flexDirection':
124
+ case 'gridTemplateColumns':
125
+ case 'alignItems':
126
+ case 'justifyContent':
127
+ case 'typography':
128
+ // Consumed at compile time by lowerToSwiftUI.ts (container type / alignment selection).
129
+ // Not representable as a runtime VcVariantStyle field.
130
+ break;
91
131
  default:
132
+ console.warn(
133
+ `[view-contracts] SwiftUI theme: variant property "${property}" is not handled ` +
134
+ `(value: "${value}"). Add a case to renderTheme.ts resolveVariantEntries().`,
135
+ );
92
136
  break;
93
137
  }
94
138
  }
95
139
  return entries;
96
140
  }
97
141
 
98
- const STRUCT_FIELD_ORDER = ['foreground', 'background', 'padding', 'paddingH', 'paddingV', 'radius', 'fontSize', 'fontWeight', 'shadow', 'minHeight', 'maxWidth'];
142
+ const STRUCT_FIELD_ORDER = ['foreground', 'background', 'padding', 'paddingH', 'paddingV', 'widthFill', 'heightFill', 'radius', 'fontSize', 'fontWeight', 'shadow', 'borderWidth', 'borderColor', 'minHeight', 'maxWidth'];
99
143
 
100
144
  function emitVariantStyleInit(entries: ResolvedEntry[], indent: string): string {
101
145
  if (entries.length === 0) return `${indent}VcVariantStyle()`;
@@ -133,16 +177,21 @@ export function renderSwiftTheme(theme: ThemeIR, tokens: TokenIR): string {
133
177
  lines.push(` var padding: CGFloat?`);
134
178
  lines.push(` var paddingH: CGFloat?`);
135
179
  lines.push(` var paddingV: CGFloat?`);
180
+ lines.push(` var widthFill: Bool?`);
181
+ lines.push(` var heightFill: Bool?`);
136
182
  lines.push(` var radius: CGFloat?`);
137
183
  lines.push(` var fontSize: CGFloat?`);
138
184
  lines.push(` var fontWeight: Font.Weight?`);
139
185
  lines.push(` var shadow: Bool?`);
186
+ lines.push(` var borderWidth: CGFloat?`);
187
+ lines.push(` var borderColor: Color?`);
140
188
  lines.push(` var minHeight: CGFloat?`);
141
189
  lines.push(` var maxWidth: CGFloat?`);
142
190
  lines.push(`}`);
143
191
  lines.push(``);
144
192
 
145
193
  // Nil-transparent: only apply properties the style actually defines.
194
+ // frame(maxWidth/maxHeight: .infinity) MUST come BEFORE background so bg fills expanded area.
146
195
  lines.push(`extension View {`);
147
196
  lines.push(` func applyVcStyle(_ s: VcVariantStyle) -> some View {`);
148
197
  lines.push(` self`);
@@ -151,14 +200,31 @@ export function renderSwiftTheme(theme: ThemeIR, tokens: TokenIR): string {
151
200
  lines.push(` .ifSet(s.padding) { $0.padding($1) }`);
152
201
  lines.push(` .ifSet(s.paddingH) { $0.padding(.horizontal, $1) }`);
153
202
  lines.push(` .ifSet(s.paddingV) { $0.padding(.vertical, $1) }`);
203
+ lines.push(` .vcFillFrame(widthFill: s.widthFill ?? false, heightFill: s.heightFill ?? false)`);
154
204
  lines.push(` .ifSet(s.background) { $0.background($1) }`);
155
205
  lines.push(` .ifSet(s.radius) { $0.clipShape(RoundedRectangle(cornerRadius: $1)) }`);
206
+ lines.push(` .ifSet(s.borderColor) { $0.overlay(RoundedRectangle(cornerRadius: s.radius ?? 0).stroke($1, lineWidth: s.borderWidth ?? 1)) }`);
156
207
  lines.push(` .ifSet(s.shadow) { v, on in on ? AnyView(v.shadow(color: Color.black.opacity(0.08), radius: 4, x: 0, y: 2)) : AnyView(v) }`);
157
208
  lines.push(` }`);
158
209
  lines.push(`}`);
159
210
  lines.push(``);
160
211
  lines.push(`extension View {`);
161
212
  lines.push(` @ViewBuilder`);
213
+ lines.push(` func vcFillFrame(widthFill: Bool, heightFill: Bool) -> some View {`);
214
+ lines.push(` if widthFill && heightFill {`);
215
+ lines.push(` self.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)`);
216
+ lines.push(` } else if widthFill {`);
217
+ lines.push(` self.frame(maxWidth: .infinity, alignment: .leading)`);
218
+ lines.push(` } else if heightFill {`);
219
+ lines.push(` self.frame(maxHeight: .infinity, alignment: .top)`);
220
+ lines.push(` } else {`);
221
+ lines.push(` self`);
222
+ lines.push(` }`);
223
+ lines.push(` }`);
224
+ lines.push(`}`);
225
+ lines.push(``);
226
+ lines.push(`extension View {`);
227
+ lines.push(` @ViewBuilder`);
162
228
  lines.push(` func ifSet<T, V: View>(_ value: T?, _ transform: (Self, T) -> V) -> some View {`);
163
229
  lines.push(` if let value { transform(self, value) } else { self }`);
164
230
  lines.push(` }`);
@@ -0,0 +1,16 @@
1
+ export { screen, view } from './screen.js';
2
+ export type {
3
+ ViewAction,
4
+ ActionHandlers,
5
+ ScreenContext,
6
+ ViewContract,
7
+ ViewPartial,
8
+ ViewScreenProps,
9
+ CommonProps,
10
+ Option,
11
+ Tone,
12
+ Align,
13
+ Justify,
14
+ ButtonVariant,
15
+ FieldSize,
16
+ } from './types.js';
@@ -0,0 +1,54 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { FC } from 'react';
3
+ import type { ScreenContext, ViewContract, ViewPartial, ViewScreenProps } from './types.js';
4
+
5
+ export type {
6
+ ViewAction,
7
+ ActionHandlers,
8
+ ScreenContext,
9
+ ViewContract,
10
+ ViewPartial,
11
+ ViewScreenProps,
12
+ CommonProps,
13
+ Option,
14
+ Tone,
15
+ Align,
16
+ Justify,
17
+ ButtonVariant,
18
+ FieldSize,
19
+ } from './types.js';
20
+
21
+ /**
22
+ * Wrap a restricted view-contract TSX definition.
23
+ *
24
+ * - **Preview runtime**: returns a React component (`vm` + `actions` props).
25
+ * - **Compiler**: statically extracts JSX from the render callback → View IR.
26
+ */
27
+ export function screen<TViewModel>(
28
+ render: (ctx: ScreenContext<TViewModel>) => ReactNode,
29
+ ): FC<ViewScreenProps<TViewModel>> {
30
+ function ViewScreen({ vm, actions = {} }: ViewScreenProps<TViewModel>) {
31
+ return <>{render({ vm, actions })}</>;
32
+ }
33
+ return Object.assign(ViewScreen, {
34
+ __viewContract: true as const,
35
+ render,
36
+ }) as FC<ViewScreenProps<TViewModel>>;
37
+ }
38
+
39
+ /**
40
+ * Reusable view-contract sub-component (composable inside screen() or other view()).
41
+ *
42
+ * Compiled by inlining into the parent View IR; preview runtime renders as a normal React FC.
43
+ */
44
+ export function view<TProps>(
45
+ render: (props: TProps) => ReactNode,
46
+ ): FC<TProps> & ViewPartial<TProps> {
47
+ function ViewComponent(props: TProps) {
48
+ return <>{render(props)}</>;
49
+ }
50
+ return Object.assign(ViewComponent, {
51
+ __viewPartial: true as const,
52
+ render,
53
+ }) as FC<TProps> & ViewPartial<TProps>;
54
+ }
@@ -0,0 +1,68 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ /** Action descriptor referenced from view contracts (names must match OpenAPI operationId). */
4
+ export interface ViewAction {
5
+ name: string;
6
+ params?: Record<string, unknown>;
7
+ }
8
+
9
+ export type ActionHandlers = Record<string, (params?: unknown) => void | Promise<void>>;
10
+
11
+ export interface ScreenContext<TViewModel> {
12
+ vm: TViewModel;
13
+ actions: ActionHandlers;
14
+ }
15
+
16
+ export interface ViewContract<TViewModel> {
17
+ readonly __viewContract: true;
18
+ readonly render: (ctx: ScreenContext<TViewModel>) => ReactNode;
19
+ }
20
+
21
+ export type Tone = 'default' | 'muted' | 'primary' | 'success' | 'warning' | 'danger';
22
+ export type Align = 'start' | 'center' | 'end' | 'stretch';
23
+ export type Justify = 'start' | 'center' | 'end' | 'between' | 'around';
24
+ export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
25
+ export type FieldSize = 'sm' | 'md' | 'lg';
26
+
27
+ /**
28
+ * Shared layout/visual props.
29
+ * Dimension props (width, height, gap, padding, …) are numbers in logical units;
30
+ * the web preview maps them to px.
31
+ */
32
+ export interface CommonProps {
33
+ id?: string;
34
+ testId?: string;
35
+ ariaLabel?: string;
36
+ children?: ReactNode;
37
+ hidden?: boolean;
38
+ width?: number;
39
+ minWidth?: number;
40
+ maxWidth?: number;
41
+ height?: number;
42
+ minHeight?: number;
43
+ maxHeight?: number;
44
+ padding?: number;
45
+ margin?: number;
46
+ gap?: number;
47
+ radius?: number;
48
+ border?: boolean;
49
+ shadow?: boolean;
50
+ tone?: Tone;
51
+ }
52
+
53
+ export interface Option {
54
+ label: string;
55
+ value: string;
56
+ }
57
+
58
+ export interface ViewPartial<TProps> {
59
+ readonly __viewPartial: true;
60
+ readonly render: (props: TProps) => ReactNode;
61
+ }
62
+
63
+ export type ViewScreenProps<TViewModel> = {
64
+ vm: TViewModel;
65
+ actions?: ActionHandlers;
66
+ /** Optional theme id — propagated from App element for future multi-theme runtime. */
67
+ theme?: string;
68
+ };
@@ -1,57 +0,0 @@
1
- * { box-sizing: border-box; }
2
- body { margin: 0; background: var(--ui-bg); color: var(--ui-text); font-family: var(--ui-font); }
3
- button, input, textarea, select { font: inherit; }
4
-
5
- .vc-page { min-height: 100vh; }
6
- .vc-box { display: block; }
7
- .vc-column { display: flex; flex-direction: column; }
8
- .vc-stack { display: flex; flex-direction: column; }
9
- .vc-row { display: flex; flex-direction: row; }
10
- .vc-wrap { flex-wrap: wrap; }
11
- .vc-spacer { min-width: 0; min-height: 0; }
12
- .vc-divider { border: 0; border-top: 1px solid var(--ui-border); width: 100%; }
13
-
14
- .vc-heading { margin: 0; line-height: 1.2; letter-spacing: -0.02em; }
15
- .vc-heading-1 { font-size: 32px; }
16
- .vc-heading-2 { font-size: 22px; }
17
- .vc-heading-3 { font-size: 18px; }
18
- .vc-heading-4 { font-size: 15px; }
19
- .vc-text { margin: 0; line-height: 1.6; }
20
- .vc-link { color: var(--ui-primary); text-decoration: none; }
21
- .vc-link:hover { text-decoration: underline; }
22
-
23
- .vc-button { border: 0; cursor: pointer; }
24
-
25
- .vc-form { display: flex; flex-direction: column; }
26
- .vc-field { display: flex; flex-direction: column; gap: 6px; }
27
- .vc-field-label { font-size: 13px; font-weight: 700; color: var(--ui-text); }
28
- .vc-field-help { font-size: 12px; color: var(--ui-text-muted); }
29
- .vc-field-error { font-size: 12px; color: var(--ui-danger); }
30
- .vc-text-input, .vc-text-area { width: 100%; outline: none; }
31
- .vc-text-input:focus, .vc-text-area:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
32
- .vc-field-sm { min-height: 34px; padding: 6px 10px; }
33
- .vc-field-md { min-height: 42px; }
34
- .vc-field-lg { min-height: 50px; padding: 12px 14px; }
35
- .vc-select {
36
- width: 100%;
37
- outline: none;
38
- appearance: none;
39
- -webkit-appearance: none;
40
- -moz-appearance: none;
41
- cursor: pointer;
42
- 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");
43
- background-repeat: no-repeat;
44
- background-position: right 12px center;
45
- background-size: 16px;
46
- }
47
- .vc-select:focus { border-color: var(--ui-primary); box-shadow: 0 0 0 3px var(--ui-primary-weak); }
48
- .vc-text-area { resize: vertical; }
49
- .vc-checkbox { display: inline-flex; align-items: center; gap: 8px; }
50
- .vc-list { margin: 0; padding-left: 20px; }
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; }