Skip to content

Commit 257bf71

Browse files
Copilothotlong
andcommitted
fix: UI protocol design issues - add widget id, unify action types, conditional validation, easing naming, migrate HTTP schemas, rename theme enums, add responsive to ListView
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 317a19a commit 257bf71

9 files changed

Lines changed: 137 additions & 38 deletions

File tree

packages/spec/src/shared/http.zod.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,30 @@ export const HttpMethod = z.enum([
2828

2929
export type HttpMethod = z.infer<typeof HttpMethod>;
3030

31+
/**
32+
* HTTP Method Schema (subset for UI/View data sources)
33+
* Common HTTP methods used in view data source configurations.
34+
* Migrated from ui/view.zod.ts to shared for reuse across modules.
35+
*/
36+
export const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
37+
38+
export type HttpMethodType = z.infer<typeof HttpMethodSchema>;
39+
40+
/**
41+
* HTTP Request Configuration Schema
42+
* Defines a complete HTTP request configuration used by API data providers.
43+
* Migrated from ui/view.zod.ts to shared for reuse across modules.
44+
*/
45+
export const HttpRequestSchema = z.object({
46+
url: z.string().describe('API endpoint URL'),
47+
method: HttpMethodSchema.optional().default('GET').describe('HTTP method'),
48+
headers: z.record(z.string(), z.string()).optional().describe('Custom HTTP headers'),
49+
params: z.record(z.string(), z.unknown()).optional().describe('Query parameters'),
50+
body: z.unknown().optional().describe('Request body for POST/PUT/PATCH'),
51+
});
52+
53+
export type HttpRequest = z.infer<typeof HttpRequestSchema>;
54+
3155
// ==========================================
3256
// CORS Configuration
3357
// ==========================================

packages/spec/src/stack.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,8 +1013,8 @@ describe('defineStack - Example-Level Strict Validation', () => {
10131013
name: 'task_overview',
10141014
label: 'Task Overview',
10151015
widgets: [
1016-
{ title: 'Total Tasks', type: 'metric', object: 'task', aggregate: 'count', layout: { x: 0, y: 0, w: 3, h: 2 } },
1017-
{ title: 'By Status', type: 'pie', object: 'task', categoryField: 'status', aggregate: 'count', layout: { x: 3, y: 0, w: 6, h: 4 } },
1016+
{ id: 'total_tasks', title: 'Total Tasks', type: 'metric', object: 'task', aggregate: 'count', layout: { x: 0, y: 0, w: 3, h: 2 } },
1017+
{ id: 'by_status', title: 'By Status', type: 'pie', object: 'task', categoryField: 'status', aggregate: 'count', layout: { x: 3, y: 0, w: 6, h: 4 } },
10181018
],
10191019
},
10201020
],
@@ -1094,7 +1094,7 @@ describe('defineStack - Example-Level Strict Validation', () => {
10941094
name: 'sales_overview',
10951095
label: 'Sales Overview',
10961096
widgets: [
1097-
{ title: 'Pipeline Value', type: 'metric', object: 'opportunity', valueField: 'amount', aggregate: 'sum', layout: { x: 0, y: 0, w: 4, h: 2 } },
1097+
{ id: 'pipeline_value', title: 'Pipeline Value', type: 'metric', object: 'opportunity', valueField: 'amount', aggregate: 'sum', layout: { x: 0, y: 0, w: 4, h: 2 } },
10981098
],
10991099
},
11001100
],

packages/spec/src/ui/animation.zod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export const TransitionConfigSchema = z.object({
4646
easing: EasingFunctionSchema.optional().describe('Easing function for the transition'),
4747
delay: z.number().optional().describe('Delay before transition starts in milliseconds'),
4848
customKeyframes: z.string().optional().describe('CSS @keyframes name for custom animations'),
49+
themeToken: z.string().optional().describe('Reference to a theme animation token (e.g. "animation.duration.fast")'),
4950
}).describe('Animation transition configuration');
5051

5152
export type TransitionConfig = z.infer<typeof TransitionConfigSchema>;

packages/spec/src/ui/dashboard.zod.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ export const WidgetColorVariantSchema = z.enum([
2525
* Action type for widget action buttons.
2626
*/
2727
export const WidgetActionTypeSchema = z.enum([
28+
'script',
2829
'url',
2930
'modal',
3031
'flow',
32+
'api',
3133
]).describe('Widget action type');
3234

3335
/**
@@ -86,6 +88,9 @@ export const WidgetMeasureSchema = z.object({
8688
* A single component on the dashboard grid.
8789
*/
8890
export const DashboardWidgetSchema = z.object({
91+
/** Unique widget identifier (snake_case, used for targetWidgets references) */
92+
id: SnakeCaseIdentifierSchema.describe('Unique widget identifier (snake_case)'),
93+
8994
/** Widget Title */
9095
title: I18nLabelSchema.optional().describe('Widget title'),
9196

packages/spec/src/ui/page.test.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ describe('PageSchema', () => {
203203
const types: Array<Page['type']> = [
204204
'record', 'home', 'app', 'utility',
205205
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
206-
'timeline', 'form', 'record_detail', 'record_review', 'overview', 'blank',
206+
'timeline', 'form', 'record_detail', 'overview',
207207
];
208208

209209
types.forEach(type => {
@@ -215,6 +215,29 @@ describe('PageSchema', () => {
215215
});
216216
expect(page.type).toBe(type);
217217
});
218+
219+
// record_review requires recordReview config
220+
const reviewPage = PageSchema.parse({
221+
name: 'test_page',
222+
label: 'Test Page',
223+
type: 'record_review',
224+
regions: [],
225+
recordReview: {
226+
object: 'case',
227+
actions: [{ label: 'Approve', type: 'approve' }],
228+
},
229+
});
230+
expect(reviewPage.type).toBe('record_review');
231+
232+
// blank requires blankLayout config
233+
const blankPage = PageSchema.parse({
234+
name: 'test_page',
235+
label: 'Test Page',
236+
type: 'blank',
237+
regions: [],
238+
blankLayout: { items: [] },
239+
});
240+
expect(blankPage.type).toBe('blank');
218241
});
219242

220243
it('should accept record page', () => {
@@ -564,6 +587,7 @@ describe('PageSchema with page types', () => {
564587
name: 'page_overview',
565588
label: 'Overview',
566589
type: 'blank',
590+
blankLayout: { items: [] },
567591
regions: [],
568592
});
569593

@@ -616,6 +640,7 @@ describe('PageSchema with page types', () => {
616640
name: 'page_filtered',
617641
label: 'Filtered View',
618642
type: 'blank',
643+
blankLayout: { items: [] },
619644
variables: [
620645
{ name: 'selectedId', type: 'string' },
621646
{ name: 'showArchived', type: 'boolean', defaultValue: false },
@@ -627,19 +652,40 @@ describe('PageSchema with page types', () => {
627652
});
628653

629654
it('should accept all interface page types', () => {
630-
const types = [
655+
const basicTypes = [
631656
'dashboard', 'grid', 'list', 'gallery', 'kanban', 'calendar',
632-
'timeline', 'form', 'record_detail', 'record_review', 'overview', 'blank',
657+
'timeline', 'form', 'record_detail', 'overview',
633658
];
634659

635-
types.forEach(type => {
660+
basicTypes.forEach(type => {
636661
expect(() => PageSchema.parse({
637662
name: 'test_page',
638663
label: 'Test',
639664
type,
640665
regions: [],
641666
})).not.toThrow();
642667
});
668+
669+
// record_review requires recordReview config
670+
expect(() => PageSchema.parse({
671+
name: 'test_page',
672+
label: 'Test',
673+
type: 'record_review',
674+
regions: [],
675+
recordReview: {
676+
object: 'case',
677+
actions: [{ label: 'Approve', type: 'approve' }],
678+
},
679+
})).not.toThrow();
680+
681+
// blank requires blankLayout config
682+
expect(() => PageSchema.parse({
683+
name: 'test_page',
684+
label: 'Test',
685+
type: 'blank',
686+
regions: [],
687+
blankLayout: { items: [] },
688+
})).not.toThrow();
643689
});
644690

645691
it('should accept page with icon', () => {
@@ -839,6 +885,7 @@ describe('PageVariableSchema record_id type', () => {
839885
name: 'blank_picker',
840886
label: 'Picker Page',
841887
type: 'blank',
888+
blankLayout: { items: [] },
842889
variables: [
843890
{ name: 'selected_id', type: 'record_id', source: 'account_picker' },
844891
{ name: 'show_details', type: 'boolean', defaultValue: false },

packages/spec/src/ui/page.zod.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,21 @@ export const PageSchema = z.object({
300300

301301
/** ARIA accessibility attributes */
302302
aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes'),
303+
}).superRefine((data, ctx) => {
304+
if (data.type === 'record_review' && !data.recordReview) {
305+
ctx.addIssue({
306+
code: z.ZodIssueCode.custom,
307+
path: ['recordReview'],
308+
message: 'recordReview is required when type is "record_review"',
309+
});
310+
}
311+
if (data.type === 'blank' && !data.blankLayout) {
312+
ctx.addIssue({
313+
code: z.ZodIssueCode.custom,
314+
path: ['blankLayout'],
315+
message: 'blankLayout is required when type is "blank"',
316+
});
317+
}
303318
});
304319

305320
export type Page = z.infer<typeof PageSchema>;

packages/spec/src/ui/theme.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,9 @@ describe('ThemeSchema', () => {
379379
},
380380
timing: {
381381
ease: 'cubic-bezier(0.4, 0, 0.2, 1)',
382-
easeIn: 'cubic-bezier(0.4, 0, 1, 1)',
383-
easeOut: 'cubic-bezier(0, 0, 0.2, 1)',
384-
easeInOut: 'cubic-bezier(0.4, 0, 0.2, 1)',
382+
ease_in: 'cubic-bezier(0.4, 0, 1, 1)',
383+
ease_out: 'cubic-bezier(0, 0, 0.2, 1)',
384+
ease_in_out: 'cubic-bezier(0.4, 0, 0.2, 1)',
385385
},
386386
},
387387
};

packages/spec/src/ui/theme.zod.ts

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ export const AnimationSchema = z.object({
156156
timing: z.object({
157157
linear: z.string().optional().describe('Linear timing function'),
158158
ease: z.string().optional().describe('Ease timing function'),
159-
easeIn: z.string().optional().describe('Ease-in timing function'),
160-
easeOut: z.string().optional().describe('Ease-out timing function'),
161-
easeInOut: z.string().optional().describe('Ease-in-out timing function'),
159+
ease_in: z.string().optional().describe('Ease-in timing function'),
160+
ease_out: z.string().optional().describe('Ease-out timing function'),
161+
ease_in_out: z.string().optional().describe('Ease-in-out timing function'),
162162
}).optional(),
163163
});
164164

@@ -178,21 +178,30 @@ export const ZIndexSchema = z.object({
178178
});
179179

180180
/**
181-
* Theme Mode Enum
181+
* Theme Mode Schema
182182
*/
183-
export const ThemeMode = z.enum(['light', 'dark', 'auto']);
183+
export const ThemeModeSchema = z.enum(['light', 'dark', 'auto']);
184+
185+
/** @deprecated Use ThemeModeSchema instead */
186+
export const ThemeMode = ThemeModeSchema;
184187

185188
/**
186-
* Density Mode Enum
189+
* Density Mode Schema
187190
* Controls spacing and sizing for different use cases.
188191
*/
189-
export const DensityMode = z.enum(['compact', 'regular', 'spacious']);
192+
export const DensityModeSchema = z.enum(['compact', 'regular', 'spacious']);
193+
194+
/** @deprecated Use DensityModeSchema instead */
195+
export const DensityMode = DensityModeSchema;
190196

191197
/**
192-
* WCAG Contrast Level
198+
* WCAG Contrast Level Schema
193199
* Web Content Accessibility Guidelines color contrast requirements.
194200
*/
195-
export const WcagContrastLevel = z.enum(['AA', 'AAA']);
201+
export const WcagContrastLevelSchema = z.enum(['AA', 'AAA']);
202+
203+
/** @deprecated Use WcagContrastLevelSchema instead */
204+
export const WcagContrastLevel = WcagContrastLevelSchema;
196205

197206
/**
198207
* Theme Configuration Schema
@@ -204,7 +213,7 @@ export const ThemeSchema = z.object({
204213
description: z.string().optional().describe('Theme description'),
205214

206215
/** Theme mode */
207-
mode: ThemeMode.default('light').describe('Theme mode (light, dark, or auto)'),
216+
mode: ThemeModeSchema.default('light').describe('Theme mode (light, dark, or auto)'),
208217

209218
/** Color system */
210219
colors: ColorPaletteSchema.describe('Color palette configuration'),
@@ -244,10 +253,10 @@ export const ThemeSchema = z.object({
244253
extends: z.string().optional().describe('Base theme to extend from'),
245254

246255
/** Display density mode */
247-
density: DensityMode.optional().describe('Display density: compact, regular, or spacious'),
256+
density: DensityModeSchema.optional().describe('Display density: compact, regular, or spacious'),
248257

249258
/** WCAG contrast level requirement */
250-
wcagContrast: WcagContrastLevel.optional().describe('WCAG color contrast level (AA or AAA)'),
259+
wcagContrast: WcagContrastLevelSchema.optional().describe('WCAG color contrast level (AA or AAA)'),
251260

252261
/** Right-to-left language support */
253262
rtl: z.boolean().optional().describe('Enable right-to-left layout direction'),
@@ -268,6 +277,6 @@ export type Shadow = z.infer<typeof ShadowSchema>;
268277
export type Breakpoints = z.infer<typeof BreakpointsSchema>;
269278
export type Animation = z.infer<typeof AnimationSchema>;
270279
export type ZIndex = z.infer<typeof ZIndexSchema>;
271-
export type ThemeMode = z.infer<typeof ThemeMode>;
272-
export type DensityMode = z.infer<typeof DensityMode>;
273-
export type WcagContrastLevel = z.infer<typeof WcagContrastLevel>;
280+
export type ThemeMode = z.infer<typeof ThemeModeSchema>;
281+
export type DensityMode = z.infer<typeof DensityModeSchema>;
282+
export type WcagContrastLevel = z.infer<typeof WcagContrastLevelSchema>;

packages/spec/src/ui/view.zod.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,14 @@ import { z } from 'zod';
44
import { SnakeCaseIdentifierSchema } from '../shared/identifiers.zod';
55
import { I18nLabelSchema, AriaPropsSchema } from './i18n.zod';
66
import { SharingConfigSchema } from './sharing.zod';
7+
import { ResponsiveConfigSchema, PerformanceConfigSchema } from './responsive.zod';
78

89
/**
9-
* HTTP Method Enum
10+
* HTTP Method Enum & HTTP Request Schema
11+
* Re-exported from shared/http.zod.ts for backward compatibility.
1012
*/
11-
export const HttpMethodSchema = z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']);
12-
13-
/**
14-
* HTTP Request Configuration for API Provider
15-
*/
16-
export const HttpRequestSchema = z.object({
17-
url: z.string().describe('API endpoint URL'),
18-
method: HttpMethodSchema.optional().default('GET').describe('HTTP method'),
19-
headers: z.record(z.string(), z.string()).optional().describe('Custom HTTP headers'),
20-
params: z.record(z.string(), z.unknown()).optional().describe('Query parameters'),
21-
body: z.unknown().optional().describe('Request body for POST/PUT/PATCH'),
22-
});
13+
export { HttpMethodSchema, HttpRequestSchema } from '../shared/http.zod';
14+
import { HttpMethodSchema, HttpRequestSchema } from '../shared/http.zod';
2315

2416
/**
2517
* View Data Source Configuration
@@ -491,6 +483,12 @@ export const ListViewSchema = z.object({
491483

492484
/** ARIA accessibility attributes */
493485
aria: AriaPropsSchema.optional().describe('ARIA accessibility attributes for the list view'),
486+
487+
/** Responsive layout overrides per breakpoint */
488+
responsive: ResponsiveConfigSchema.optional().describe('Responsive layout configuration'),
489+
490+
/** Performance optimization settings */
491+
performance: PerformanceConfigSchema.optional().describe('Performance optimization settings'),
494492
});
495493

496494
/**

0 commit comments

Comments
 (0)