@proulr/sdui-contract 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.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # proulr-sdui-contract
2
+
3
+ Contrato SDUI compartilhado (wire snake_case + domínio camelCase).
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ npm install
9
+ npm run typecheck
10
+ ```
11
+
12
+ ## Consumidores
13
+
14
+ - `@proulr/sdui-components` — codec + types
15
+ - BFFs — wire payload
16
+ - `proulr-app` — decode no fetch
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@proulr/sdui-contract",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/saviorbp/proulr-sdui-contract.git"
16
+ },
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit",
19
+ "prepublishOnly": "npm run typecheck"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ }
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './sdui-contract-types';
2
+ export * from './sdui-screen-codec';
@@ -0,0 +1,186 @@
1
+ /** SDUI screen contract: wire (snake_case) and domain (camelCase). */
2
+
3
+ export const SDUI_MIN_SCHEMA_VERSION = 1;
4
+ export const SDUI_MAX_SCHEMA_VERSION = 1;
5
+
6
+ export type SduiOfflinePolicy = 'cache_only' | 'fail' | 'queue';
7
+
8
+ export type SduiActionType =
9
+ | 'navigate'
10
+ | 'open_sheet'
11
+ | 'api_call'
12
+ | 'track'
13
+ | 'toggle_search'
14
+ | 'refresh_screen'
15
+ | 'sign_out'
16
+ | 'noop';
17
+
18
+ export type SduiBlockWire = {
19
+ id: string;
20
+ type: string;
21
+ props?: Record<string, unknown>;
22
+ children?: SduiBlockWire[];
23
+ };
24
+
25
+ export type SduiHeaderWire = {
26
+ title?: string;
27
+ subtitle?: string;
28
+ brand?: boolean;
29
+ show_back?: boolean;
30
+ hidden?: boolean;
31
+ slot_blocks?: SduiBlockWire[];
32
+ header_search?: { kind: 'vibes' | 'messages' };
33
+ };
34
+
35
+ export type SduiActionWire = {
36
+ type: SduiActionType;
37
+ route?: string;
38
+ params?: Record<string, string>;
39
+ sheet_id?: string;
40
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
41
+ path?: string;
42
+ body?: Record<string, unknown>;
43
+ event_name?: string;
44
+ event_props?: Record<string, unknown>;
45
+ };
46
+
47
+ export type SduiScreenMetaWire = {
48
+ cache_ttl_ms?: number;
49
+ offline_policy?: SduiOfflinePolicy;
50
+ bff?: string;
51
+ };
52
+
53
+ export type SduiScreenWire = {
54
+ schema_version: number;
55
+ screen_id: string;
56
+ header?: SduiHeaderWire;
57
+ blocks: SduiBlockWire[];
58
+ data?: Record<string, unknown>;
59
+ actions?: Record<string, SduiActionWire>;
60
+ meta?: SduiScreenMetaWire;
61
+ };
62
+
63
+ export type SduiBlock = {
64
+ id: string;
65
+ type: string;
66
+ props: Record<string, unknown>;
67
+ children: SduiBlock[];
68
+ };
69
+
70
+ export type SduiHeader = {
71
+ title?: string;
72
+ subtitle?: string;
73
+ brand?: boolean;
74
+ showBack?: boolean;
75
+ hidden?: boolean;
76
+ slotBlocks: SduiBlock[];
77
+ headerSearch?: { kind: 'vibes' | 'messages' };
78
+ };
79
+
80
+ export type SduiAction = {
81
+ type: SduiActionType;
82
+ route?: string;
83
+ params?: Record<string, string>;
84
+ sheetId?: string;
85
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
86
+ path?: string;
87
+ body?: Record<string, unknown>;
88
+ eventName?: string;
89
+ eventProps?: Record<string, unknown>;
90
+ };
91
+
92
+ export type SduiScreenMeta = {
93
+ cacheTtlMs: number;
94
+ offlinePolicy: SduiOfflinePolicy;
95
+ bff?: string;
96
+ };
97
+
98
+ export type SduiScreen = {
99
+ schemaVersion: number;
100
+ screenId: string;
101
+ header?: SduiHeader;
102
+ blocks: SduiBlock[];
103
+ data: Record<string, unknown>;
104
+ actions: Record<string, SduiAction>;
105
+ meta: SduiScreenMeta;
106
+ };
107
+
108
+ export type SduiScreenFetchParams = {
109
+ screenId: string;
110
+ routeParams?: Record<string, string>;
111
+ context?: Record<string, unknown>;
112
+ };
113
+
114
+ export type SduiScreenMode = 'legacy' | 'hybrid' | 'sdui';
115
+
116
+ export type SduiScreenBuilderContext = {
117
+ routeParams?: Record<string, string>;
118
+ query?: Record<string, string>;
119
+ authToken?: string;
120
+ };
121
+
122
+ export type SduiScreenBuilder = (
123
+ ctx: SduiScreenBuilderContext,
124
+ ) => SduiScreenWire | Promise<SduiScreenWire>;
125
+
126
+ // ─── Style tokens (BFF customization via wire) ────────────────────────────
127
+
128
+ export type ColorToken =
129
+ | 'primary'
130
+ | 'onPrimary'
131
+ | 'primaryContainer'
132
+ | 'secondary'
133
+ | 'onSecondary'
134
+ | 'secondaryContainer'
135
+ | 'tertiary'
136
+ | 'surface'
137
+ | 'onSurface'
138
+ | 'onSurfaceVariant'
139
+ | 'surfaceContainer'
140
+ | 'surfaceContainerHigh'
141
+ | 'surfaceContainerHighest'
142
+ | 'outline'
143
+ | 'error'
144
+ | 'onError';
145
+
146
+ export type SpacingToken = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
147
+
148
+ export type ShapeToken =
149
+ | 'cornerExtraSmall'
150
+ | 'cornerSmall'
151
+ | 'cornerMedium'
152
+ | 'cornerLarge'
153
+ | 'cornerExtraLarge'
154
+ | 'cornerFull';
155
+
156
+ export type TypeToken =
157
+ | 'display'
158
+ | 'headline'
159
+ | 'title'
160
+ | 'titleSmall'
161
+ | 'body'
162
+ | 'bodySmall'
163
+ | 'label'
164
+ | 'labelSmall';
165
+
166
+ export type StyleWire = {
167
+ background_color?: ColorToken;
168
+ color?: ColorToken;
169
+ padding?: SpacingToken | number;
170
+ padding_horizontal?: SpacingToken | number;
171
+ padding_vertical?: SpacingToken | number;
172
+ margin_top?: SpacingToken | number;
173
+ gap?: SpacingToken | number;
174
+ border_radius?: ShapeToken | number;
175
+ border_width?: number;
176
+ border_color?: ColorToken;
177
+ opacity?: number;
178
+ flex?: number;
179
+ align_items?: 'flex-start' | 'center' | 'flex-end';
180
+ typography?: TypeToken;
181
+ };
182
+
183
+ export type StyleProps = {
184
+ variant?: string;
185
+ style?: StyleWire;
186
+ };
@@ -0,0 +1,132 @@
1
+ import {
2
+ SDUI_MAX_SCHEMA_VERSION,
3
+ SDUI_MIN_SCHEMA_VERSION,
4
+ type SduiAction,
5
+ type SduiActionWire,
6
+ type SduiBlock,
7
+ type SduiBlockWire,
8
+ type SduiHeader,
9
+ type SduiHeaderWire,
10
+ type SduiOfflinePolicy,
11
+ type SduiScreen,
12
+ type SduiScreenMeta,
13
+ type SduiScreenMetaWire,
14
+ type SduiScreenWire,
15
+ } from './sdui-contract-types';
16
+
17
+ const DEFAULT_CACHE_TTL_MS = 30_000;
18
+ const DEFAULT_OFFLINE_POLICY: SduiOfflinePolicy = 'cache_only';
19
+
20
+ export class SduiCodecError extends Error {
21
+ constructor(message: string) {
22
+ super(message);
23
+ this.name = 'SduiCodecError';
24
+ }
25
+ }
26
+
27
+ function mapBlockWire(wire: SduiBlockWire): SduiBlock {
28
+ return {
29
+ id: wire.id,
30
+ type: wire.type,
31
+ props: wire.props ?? {},
32
+ children: (wire.children ?? []).map(mapBlockWire),
33
+ };
34
+ }
35
+
36
+ function mapActionWire(wire: SduiActionWire): SduiAction {
37
+ return {
38
+ type: wire.type,
39
+ route: wire.route,
40
+ params: wire.params,
41
+ sheetId: wire.sheet_id,
42
+ method: wire.method,
43
+ path: wire.path,
44
+ body: wire.body,
45
+ eventName: wire.event_name,
46
+ eventProps: wire.event_props,
47
+ };
48
+ }
49
+
50
+ function mapHeaderWire(wire: SduiHeaderWire): SduiHeader {
51
+ return {
52
+ title: wire.title,
53
+ subtitle: wire.subtitle,
54
+ brand: wire.brand,
55
+ showBack: wire.show_back,
56
+ hidden: wire.hidden,
57
+ slotBlocks: (wire.slot_blocks ?? []).map(mapBlockWire),
58
+ headerSearch: wire.header_search,
59
+ };
60
+ }
61
+
62
+ function mapMetaWire(wire: SduiScreenMetaWire | undefined): SduiScreenMeta {
63
+ return {
64
+ cacheTtlMs: wire?.cache_ttl_ms ?? DEFAULT_CACHE_TTL_MS,
65
+ offlinePolicy: wire?.offline_policy ?? DEFAULT_OFFLINE_POLICY,
66
+ bff: wire?.bff,
67
+ };
68
+ }
69
+
70
+ export function decodeSduiScreenWire(wire: unknown): SduiScreen {
71
+ if (!wire || typeof wire !== 'object') {
72
+ throw new SduiCodecError('screen payload must be an object');
73
+ }
74
+
75
+ const payload = wire as SduiScreenWire;
76
+
77
+ if (typeof payload.schema_version !== 'number') {
78
+ throw new SduiCodecError('schema_version is required');
79
+ }
80
+
81
+ if (
82
+ payload.schema_version < SDUI_MIN_SCHEMA_VERSION ||
83
+ payload.schema_version > SDUI_MAX_SCHEMA_VERSION
84
+ ) {
85
+ throw new SduiCodecError(
86
+ `unsupported schema_version ${payload.schema_version} (supported ${SDUI_MIN_SCHEMA_VERSION}-${SDUI_MAX_SCHEMA_VERSION})`,
87
+ );
88
+ }
89
+
90
+ if (typeof payload.screen_id !== 'string' || !payload.screen_id) {
91
+ throw new SduiCodecError('screen_id is required');
92
+ }
93
+
94
+ if (!Array.isArray(payload.blocks)) {
95
+ throw new SduiCodecError('blocks must be an array');
96
+ }
97
+
98
+ return {
99
+ schemaVersion: payload.schema_version,
100
+ screenId: payload.screen_id,
101
+ header: payload.header ? mapHeaderWire(payload.header) : undefined,
102
+ blocks: payload.blocks.map(mapBlockWire),
103
+ data: payload.data ?? {},
104
+ actions: Object.fromEntries(
105
+ Object.entries(payload.actions ?? {}).map(([key, action]) => [key, mapActionWire(action)]),
106
+ ),
107
+ meta: mapMetaWire(payload.meta),
108
+ };
109
+ }
110
+
111
+ export function createScreenWire(
112
+ screenId: string,
113
+ blocks: SduiBlockWire[],
114
+ options?: {
115
+ header?: SduiHeaderWire;
116
+ data?: Record<string, unknown>;
117
+ actions?: Record<string, SduiActionWire>;
118
+ meta?: SduiScreenMetaWire;
119
+ },
120
+ ): SduiScreenWire {
121
+ return {
122
+ schema_version: 1,
123
+ screen_id: screenId,
124
+ header: options?.header,
125
+ blocks,
126
+ data: options?.data,
127
+ actions: options?.actions,
128
+ meta: options?.meta,
129
+ };
130
+ }
131
+
132
+ export * from './sdui-contract-types';