@proulr/sdui-contract 0.1.17 → 0.1.19
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/package.json +1 -1
- package/src/sdui-contract-types.test.ts +257 -0
- package/src/sdui-contract-types.ts +325 -0
package/package.json
CHANGED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
decodeSduiScreenWire,
|
|
4
|
+
isSduiAvatarWireProps,
|
|
5
|
+
isSduiContentImageWireProps,
|
|
6
|
+
isSduiIconButtonWireProps,
|
|
7
|
+
isSduiRowOverflowMenuWireProps,
|
|
8
|
+
isSduiRowTemplateEntryWireProps,
|
|
9
|
+
isSduiRowTemplateSetWireProps,
|
|
10
|
+
isSduiRowTemplateWireProps,
|
|
11
|
+
isSduiSelectedTrue,
|
|
12
|
+
isSduiVideoWireProps,
|
|
13
|
+
} from './index';
|
|
14
|
+
import type { SduiBlockWire } from './sdui-contract-types';
|
|
15
|
+
|
|
16
|
+
const templateBlock: SduiBlockWire = {
|
|
17
|
+
id: '0.row_template.0.0',
|
|
18
|
+
type: 'column',
|
|
19
|
+
props: { action_id: 'open_post_{{item.id}}' },
|
|
20
|
+
children: [
|
|
21
|
+
{ id: '0.row_template.0.0.0', type: 'text', props: { text: '{{item.author.name}}' }, children: [] },
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe('isSduiRowTemplateWireProps', () => {
|
|
26
|
+
it('accepts host props carrying a row template', () => {
|
|
27
|
+
const props: Record<string, unknown> = {
|
|
28
|
+
data_ref: 'social.posts',
|
|
29
|
+
row_template: [{ id: '0.row_template.0.0', type: 'text', props: { text: '{{item.body}}' } }],
|
|
30
|
+
item_key: 'id',
|
|
31
|
+
section: 'posts',
|
|
32
|
+
};
|
|
33
|
+
expect(isSduiRowTemplateWireProps(props)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects props without template or without item_key', () => {
|
|
37
|
+
expect(isSduiRowTemplateWireProps(undefined)).toBe(false);
|
|
38
|
+
expect(isSduiRowTemplateWireProps({})).toBe(false);
|
|
39
|
+
expect(isSduiRowTemplateWireProps({ row_template: [] })).toBe(false);
|
|
40
|
+
expect(isSduiRowTemplateWireProps({ row_template: [], item_key: '' })).toBe(false);
|
|
41
|
+
expect(isSduiRowTemplateWireProps({ row_template: 'nope', item_key: 'id' })).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('multi-template hosts (row_templates)', () => {
|
|
46
|
+
const entry = (itemKey = 'id', section?: string): Record<string, unknown> => ({
|
|
47
|
+
row_template: [{ id: 't', type: 'text', props: { text: 'x' } }],
|
|
48
|
+
item_key: itemKey,
|
|
49
|
+
...(section ? { section } : {}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('isSduiRowTemplateEntryWireProps validates one ordered template', () => {
|
|
53
|
+
expect(isSduiRowTemplateEntryWireProps(entry('id', 'posts'))).toBe(true);
|
|
54
|
+
expect(isSduiRowTemplateEntryWireProps(entry('post.id'))).toBe(true);
|
|
55
|
+
expect(isSduiRowTemplateEntryWireProps(null)).toBe(false);
|
|
56
|
+
expect(isSduiRowTemplateEntryWireProps({ row_template: [] })).toBe(false);
|
|
57
|
+
expect(isSduiRowTemplateEntryWireProps({ row_template: [], item_key: '' })).toBe(false);
|
|
58
|
+
expect(isSduiRowTemplateEntryWireProps({ row_template: [], item_key: 'id', section: 3 })).toBe(
|
|
59
|
+
false,
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('isSduiRowTemplateSetWireProps accepts single hosts (no row_templates) and valid sets', () => {
|
|
64
|
+
expect(
|
|
65
|
+
isSduiRowTemplateSetWireProps({ row_template: [], item_key: 'id' }),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
expect(
|
|
68
|
+
isSduiRowTemplateSetWireProps({
|
|
69
|
+
row_template: [],
|
|
70
|
+
item_key: 'id',
|
|
71
|
+
row_templates: [entry(), entry('comment_id', 'comments')],
|
|
72
|
+
}),
|
|
73
|
+
).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('isSduiRowTemplateSetWireProps rejects malformed sets', () => {
|
|
77
|
+
expect(
|
|
78
|
+
isSduiRowTemplateSetWireProps({ row_template: [], item_key: 'id', row_templates: [] }),
|
|
79
|
+
).toBe(false);
|
|
80
|
+
expect(
|
|
81
|
+
isSduiRowTemplateSetWireProps({
|
|
82
|
+
row_template: [],
|
|
83
|
+
item_key: 'id',
|
|
84
|
+
row_templates: [{ row_template: 'nope', item_key: 'id' }],
|
|
85
|
+
}),
|
|
86
|
+
).toBe(false);
|
|
87
|
+
expect(isSduiRowTemplateSetWireProps({ row_templates: [entry()] })).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('row_templates survives the codec untouched (ordered, snake_case entries)', () => {
|
|
91
|
+
const screen = decodeSduiScreenWire({
|
|
92
|
+
schema_version: 1,
|
|
93
|
+
screen_id: 'social.feed',
|
|
94
|
+
blocks: [
|
|
95
|
+
{
|
|
96
|
+
id: 'feed',
|
|
97
|
+
type: 'social_post_feed',
|
|
98
|
+
props: {
|
|
99
|
+
row_template: [templateBlock],
|
|
100
|
+
item_key: 'id',
|
|
101
|
+
row_templates: [entry('id', 'posts'), entry('comment_id', 'comments')],
|
|
102
|
+
},
|
|
103
|
+
children: [],
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
const props = screen.blocks[0].props;
|
|
108
|
+
expect(props.item_key).toBe('id');
|
|
109
|
+
expect(props.row_template).toEqual([templateBlock]);
|
|
110
|
+
expect(props.row_templates).toEqual([entry('id', 'posts'), entry('comment_id', 'comments')]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('video block wire props', () => {
|
|
115
|
+
it('isSduiVideoWireProps accepts string path (binding allowed), rejects others', () => {
|
|
116
|
+
expect(isSduiVideoWireProps({ path: '{{item.video_path}}', aspect: 'video' })).toBe(true);
|
|
117
|
+
expect(isSduiVideoWireProps({ path: 'clips/2026/a.mp4', poster_path: 'clips/2026/a.jpg' })).toBe(
|
|
118
|
+
true,
|
|
119
|
+
);
|
|
120
|
+
expect(isSduiVideoWireProps({})).toBe(false);
|
|
121
|
+
expect(isSduiVideoWireProps({ poster_path: 'a.jpg' })).toBe(false);
|
|
122
|
+
expect(isSduiVideoWireProps(undefined)).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('row_overflow_menu block wire props', () => {
|
|
127
|
+
it('isSduiRowOverflowMenuWireProps requires the actions array', () => {
|
|
128
|
+
expect(
|
|
129
|
+
isSduiRowOverflowMenuWireProps({
|
|
130
|
+
actions: [{ action_id: 'delete_{{item.id}}', label: 'Excluir', destructive: true }],
|
|
131
|
+
icon: 'overflow',
|
|
132
|
+
}),
|
|
133
|
+
).toBe(true);
|
|
134
|
+
expect(isSduiRowOverflowMenuWireProps({ actions: [] })).toBe(true);
|
|
135
|
+
expect(isSduiRowOverflowMenuWireProps({ icon: 'overflow' })).toBe(false);
|
|
136
|
+
expect(isSduiRowOverflowMenuWireProps(undefined)).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('row_template wire through the codec', () => {
|
|
141
|
+
it('passes row_template props through untouched (blocks without template unchanged)', () => {
|
|
142
|
+
const templateBlock: SduiBlockWire = {
|
|
143
|
+
id: '0.row_template.0.0',
|
|
144
|
+
type: 'column',
|
|
145
|
+
props: { action_id: 'open_post_{{item.id}}' },
|
|
146
|
+
children: [
|
|
147
|
+
{ id: '0.row_template.0.0.0', type: 'text', props: { text: '{{item.author.name}}' }, children: [] },
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
const screen = decodeSduiScreenWire({
|
|
151
|
+
schema_version: 1,
|
|
152
|
+
screen_id: 'social.feed',
|
|
153
|
+
blocks: [
|
|
154
|
+
{
|
|
155
|
+
id: 'feed',
|
|
156
|
+
type: 'social_post_feed',
|
|
157
|
+
props: { data_ref: 'social.posts', row_template: [templateBlock], item_key: 'id' },
|
|
158
|
+
children: [],
|
|
159
|
+
},
|
|
160
|
+
{ id: 'plain', type: 'text', props: { text: 'hello' }, children: [] },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const host = screen.blocks[0];
|
|
165
|
+
expect(host.props.row_template).toEqual([templateBlock]);
|
|
166
|
+
expect(host.props.item_key).toBe('id');
|
|
167
|
+
// Nested wire blocks keep their shape (codec does not recurse into props).
|
|
168
|
+
const nested = (host.props.row_template as SduiBlockWire[])[0];
|
|
169
|
+
expect(nested.children?.[0]?.props?.text).toBe('{{item.author.name}}');
|
|
170
|
+
expect(screen.blocks[1].props).toEqual({ text: 'hello' });
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('media/feed row block wire props', () => {
|
|
175
|
+
it('isSduiContentImageWireProps accepts string path (binding allowed), rejects others', () => {
|
|
176
|
+
expect(isSduiContentImageWireProps({ path: '{{item.media_path}}', aspect: 'square' })).toBe(true);
|
|
177
|
+
expect(isSduiContentImageWireProps({ path: 'feeds/2026/photo.jpg' })).toBe(true);
|
|
178
|
+
expect(isSduiContentImageWireProps({})).toBe(false);
|
|
179
|
+
expect(isSduiContentImageWireProps({ width: 100 })).toBe(false);
|
|
180
|
+
expect(isSduiContentImageWireProps(undefined)).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('isSduiAvatarWireProps accepts path, name or initials', () => {
|
|
184
|
+
expect(isSduiAvatarWireProps({ path: '{{item.avatar_path}}' })).toBe(true);
|
|
185
|
+
expect(isSduiAvatarWireProps({ name: '{{item.author.name}}', size: 'md' })).toBe(true);
|
|
186
|
+
expect(isSduiAvatarWireProps({ initials: 'AB' })).toBe(true);
|
|
187
|
+
expect(isSduiAvatarWireProps({})).toBe(false);
|
|
188
|
+
expect(isSduiAvatarWireProps({ size: 'sm' })).toBe(false);
|
|
189
|
+
expect(isSduiAvatarWireProps(undefined)).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('isSduiIconButtonWireProps requires string icon', () => {
|
|
193
|
+
expect(isSduiIconButtonWireProps({ icon: 'heart', selected: '{{item.liked_by_me}}' })).toBe(true);
|
|
194
|
+
expect(isSduiIconButtonWireProps({ icon: 'heart', selected: false })).toBe(true);
|
|
195
|
+
expect(isSduiIconButtonWireProps({ action_id: 'like' })).toBe(false);
|
|
196
|
+
expect(isSduiIconButtonWireProps(undefined)).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('isSduiSelectedTrue implements the stringly-typed binding rule', () => {
|
|
200
|
+
expect(isSduiSelectedTrue(true)).toBe(true);
|
|
201
|
+
expect(isSduiSelectedTrue('true')).toBe(true);
|
|
202
|
+
expect(isSduiSelectedTrue(' True ')).toBe(true);
|
|
203
|
+
expect(isSduiSelectedTrue('false')).toBe(false);
|
|
204
|
+
expect(isSduiSelectedTrue(false)).toBe(false);
|
|
205
|
+
expect(isSduiSelectedTrue(undefined)).toBe(false);
|
|
206
|
+
expect(isSduiSelectedTrue(1)).toBe(false);
|
|
207
|
+
expect(isSduiSelectedTrue('yes')).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('selected/media props survive the codec untouched (block props never camelCased)', () => {
|
|
211
|
+
const screen = decodeSduiScreenWire({
|
|
212
|
+
schema_version: 1,
|
|
213
|
+
screen_id: 'social.feed',
|
|
214
|
+
blocks: [
|
|
215
|
+
{
|
|
216
|
+
id: 'row',
|
|
217
|
+
type: 'column',
|
|
218
|
+
props: {},
|
|
219
|
+
children: [
|
|
220
|
+
{
|
|
221
|
+
id: 'row.img',
|
|
222
|
+
type: 'content_image',
|
|
223
|
+
props: { path: '{{item.media_path}}', aspect: 'square', corner: 'cornerMedium', blur_fill: false },
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: 'row.avatar',
|
|
227
|
+
type: 'avatar',
|
|
228
|
+
props: { path: '{{item.author.avatar_path}}', name: '{{item.author.name}}', size: 'sm' },
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'row.like',
|
|
232
|
+
type: 'icon_button',
|
|
233
|
+
props: {
|
|
234
|
+
icon: 'heart',
|
|
235
|
+
selected_icon: 'heart-filled',
|
|
236
|
+
action_id: 'like_post_{{item.id}}',
|
|
237
|
+
selected: 'true',
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const [image, avatar, like] = screen.blocks[0].children;
|
|
246
|
+
expect(image.type).toBe('content_image');
|
|
247
|
+
expect(image.props).toEqual({
|
|
248
|
+
path: '{{item.media_path}}',
|
|
249
|
+
aspect: 'square',
|
|
250
|
+
corner: 'cornerMedium',
|
|
251
|
+
blur_fill: false,
|
|
252
|
+
});
|
|
253
|
+
expect(avatar.props.name).toBe('{{item.author.name}}');
|
|
254
|
+
expect(like.props.selected).toBe('true');
|
|
255
|
+
expect(isSduiSelectedTrue(like.props.selected)).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -25,6 +25,331 @@ export type SduiBlockWire = {
|
|
|
25
25
|
children?: SduiBlockWire[];
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Row template hoisted onto a host hybrid block's props by `compileBdcToWire`
|
|
30
|
+
* (BDC `<ListRowTemplate>` — authoring lives in proulr-sdui-components).
|
|
31
|
+
*
|
|
32
|
+
* Wire shape (snake_case, inside the host block's `props`):
|
|
33
|
+
* ```
|
|
34
|
+
* {
|
|
35
|
+
* row_template: SduiBlockWire[], // compiled subtree roots of the template
|
|
36
|
+
* item_key: string, // identifier field of each live item, e.g. 'id' (dot path allowed)
|
|
37
|
+
* section?: string, // optional section marker for multi-section feeds
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* Binding syntax inside template props: `{{item.field}}` / `{{item.a.b}}`
|
|
42
|
+
* (dot path against each live item; `{{index}}` is NOT supported). Any string
|
|
43
|
+
* prop value — including action ids and nested action payloads — may embed
|
|
44
|
+
* placeholders; the app resolves them at materialize time
|
|
45
|
+
* (`materializeRowTemplate` in `@proulr/sdui-components/renderer`).
|
|
46
|
+
* Unresolved paths render as empty string.
|
|
47
|
+
*/
|
|
48
|
+
export type SduiRowTemplateWireProps = {
|
|
49
|
+
row_template: SduiBlockWire[];
|
|
50
|
+
item_key: string;
|
|
51
|
+
section?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Type guard for block props carrying a `row_template` (wire/props stay uncamelCased by the codec). */
|
|
56
|
+
export function isSduiRowTemplateWireProps(
|
|
57
|
+
props: Record<string, unknown> | undefined,
|
|
58
|
+
): props is SduiRowTemplateWireProps {
|
|
59
|
+
return (
|
|
60
|
+
!!props &&
|
|
61
|
+
Array.isArray(props.row_template) &&
|
|
62
|
+
typeof props.item_key === 'string' &&
|
|
63
|
+
props.item_key.length > 0
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One ordered template of a **multi-template host** (BDC: several
|
|
69
|
+
* `<ListRowTemplate>` children, each carrying its own `item_key`/`section`).
|
|
70
|
+
* Shape is identical to `SduiRowTemplateWireProps` — it is one entry of the
|
|
71
|
+
* host's `props.row_templates` array.
|
|
72
|
+
*/
|
|
73
|
+
export type SduiRowTemplateEntryWireProps = {
|
|
74
|
+
row_template: SduiBlockWire[];
|
|
75
|
+
item_key: string;
|
|
76
|
+
section?: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Wire shape for a host block compiled from **multiple** `<ListRowTemplate>`
|
|
81
|
+
* children (see `hoistRowTemplates` in `compileBdcToWire`):
|
|
82
|
+
* ```
|
|
83
|
+
* {
|
|
84
|
+
* row_template: SduiBlockWire[], // ALWAYS the FIRST template (backward compat)
|
|
85
|
+
* item_key: string, // item_key of the first template
|
|
86
|
+
* section?: string, // section of the first template (when set)
|
|
87
|
+
* row_templates: SduiRowTemplateEntryWireProps[], // ordered, one entry per template
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
* Single-template hosts keep the exact `SduiRowTemplateWireProps` shape —
|
|
91
|
+
* `row_templates` only appears when the host has two or more templates, so
|
|
92
|
+
* existing readers (`readSduiRowTemplate`) keep working unchanged. New
|
|
93
|
+
* readers use `readSduiRowTemplates` (`@proulr/sdui-components/renderer`).
|
|
94
|
+
*/
|
|
95
|
+
export type SduiRowTemplateSetWireProps = SduiRowTemplateWireProps & {
|
|
96
|
+
row_templates?: SduiRowTemplateEntryWireProps[];
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Type guard for one entry of `row_templates`. */
|
|
100
|
+
export function isSduiRowTemplateEntryWireProps(
|
|
101
|
+
value: unknown,
|
|
102
|
+
): value is SduiRowTemplateEntryWireProps {
|
|
103
|
+
if (!value || typeof value !== 'object') return false;
|
|
104
|
+
const entry = value as Record<string, unknown>;
|
|
105
|
+
return (
|
|
106
|
+
Array.isArray(entry.row_template) &&
|
|
107
|
+
typeof entry.item_key === 'string' &&
|
|
108
|
+
entry.item_key.length > 0 &&
|
|
109
|
+
(entry.section === undefined || typeof entry.section === 'string')
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Type guard for host props carrying an ordered template set. A plain
|
|
115
|
+
* single-template host also passes (`row_templates` optional); entries, when
|
|
116
|
+
* present, must each be a valid template.
|
|
117
|
+
*/
|
|
118
|
+
export function isSduiRowTemplateSetWireProps(
|
|
119
|
+
props: Record<string, unknown> | undefined,
|
|
120
|
+
): props is SduiRowTemplateSetWireProps {
|
|
121
|
+
if (!isSduiRowTemplateWireProps(props)) return false;
|
|
122
|
+
const set = props as SduiRowTemplateSetWireProps;
|
|
123
|
+
return (
|
|
124
|
+
set.row_templates === undefined ||
|
|
125
|
+
(Array.isArray(set.row_templates) &&
|
|
126
|
+
set.row_templates.length > 0 &&
|
|
127
|
+
set.row_templates.every(isSduiRowTemplateEntryWireProps))
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Media / feed row blocks (generic wire blocks, snake_case) ────────────
|
|
132
|
+
//
|
|
133
|
+
// Block `props` pass through the codec untouched (see `mapBlockWire`), so the
|
|
134
|
+
// wire shape below is also the domain shape for these blocks. Values inside
|
|
135
|
+
// `path` / `selected` may embed row-template bindings (`{{item.field}}`); the
|
|
136
|
+
// app resolves them at materialize time BEFORE the block renders. The media
|
|
137
|
+
// PATH is data-driven; turning it into a fetchable URL is a generic
|
|
138
|
+
// app-provided capability (`SduiMediaResolver` in
|
|
139
|
+
// `@proulr/sdui-components/renderer`) — never product logic in the lib.
|
|
140
|
+
|
|
141
|
+
/** Aspect tokens for content images (ratio values live in the renderer lib). */
|
|
142
|
+
export type SduiMediaAspectToken = 'square' | 'portrait' | 'story' | 'video' | 'wide';
|
|
143
|
+
|
|
144
|
+
/** Resize tokens for content images (maps to RN resizeMode in the renderer). */
|
|
145
|
+
export type SduiMediaFitToken = 'cover' | 'contain';
|
|
146
|
+
|
|
147
|
+
/** Size tokens for avatars (px values live in the renderer lib). */
|
|
148
|
+
export type SduiAvatarSizeToken = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* State-driven icon flag (stringly-typed binding rule): `selected` is either a
|
|
152
|
+
* real boolean OR a string produced by row-template materialization —
|
|
153
|
+
* `{{item.liked_by_me}}` renders booleans as `'true'` / `'false'` strings.
|
|
154
|
+
* `isSduiSelectedTrue` is the single coercion contract: `true` and the string
|
|
155
|
+
* `'true'` (case-insensitive, trimmed) mean selected; everything else
|
|
156
|
+
* (`'false'`, `false`, missing, any other string) means default.
|
|
157
|
+
*/
|
|
158
|
+
export type SduiSelectedFlag = string | boolean;
|
|
159
|
+
|
|
160
|
+
/** Coercion contract for `selected`-style stringly-typed wire flags. */
|
|
161
|
+
export function isSduiSelectedTrue(value: unknown): boolean {
|
|
162
|
+
if (value === true) return true;
|
|
163
|
+
return typeof value === 'string' && value.trim().toLowerCase() === 'true';
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Wire shape for block `content_image` (BDC `<ContentImage>`):
|
|
168
|
+
* ```
|
|
169
|
+
* {
|
|
170
|
+
* path: string, // storage path; may contain {{item.*}} binding (required)
|
|
171
|
+
* width?: number, height?: number,
|
|
172
|
+
* aspect?: 'square'|'portrait'|'story'|'video'|'wide',
|
|
173
|
+
* corner?: ShapeToken, // default cornerMedium
|
|
174
|
+
* fit?: 'cover'|'contain', // default cover
|
|
175
|
+
* blur_fill?: boolean, // fill frame behind `contain` media
|
|
176
|
+
* test_id?: string
|
|
177
|
+
* }
|
|
178
|
+
* ```
|
|
179
|
+
* Renderer contract: empty `path` or no app `SduiMediaResolver` → renders null
|
|
180
|
+
* (never a product fallback URL).
|
|
181
|
+
*/
|
|
182
|
+
export type SduiContentImageWireProps = {
|
|
183
|
+
path: string;
|
|
184
|
+
width?: number;
|
|
185
|
+
height?: number;
|
|
186
|
+
aspect?: SduiMediaAspectToken;
|
|
187
|
+
corner?: ShapeToken;
|
|
188
|
+
fit?: SduiMediaFitToken;
|
|
189
|
+
blur_fill?: boolean;
|
|
190
|
+
test_id?: string;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/** Type guard for `content_image` block props. */
|
|
194
|
+
export function isSduiContentImageWireProps(
|
|
195
|
+
props: Record<string, unknown> | undefined,
|
|
196
|
+
): props is SduiContentImageWireProps {
|
|
197
|
+
return !!props && typeof props.path === 'string';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Wire shape for block `avatar` (BDC `<Avatar>`):
|
|
202
|
+
* ```
|
|
203
|
+
* {
|
|
204
|
+
* path?: string, // storage path; may contain {{item.*}} binding
|
|
205
|
+
* name?: string, // display name — initials derived when no image
|
|
206
|
+
* initials?: string, // explicit initials (override derivation)
|
|
207
|
+
* size?: 'xs'|'sm'|'md'|'lg'|'xl', // default md
|
|
208
|
+
* corner?: ShapeToken, // default cornerFull
|
|
209
|
+
* test_id?: string
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
* No resolvable image (no path, empty path, or no resolver) falls back to
|
|
213
|
+
* initials / generic person glyph placeholder — never to a product URL.
|
|
214
|
+
*/
|
|
215
|
+
export type SduiAvatarWireProps = {
|
|
216
|
+
path?: string;
|
|
217
|
+
name?: string;
|
|
218
|
+
initials?: string;
|
|
219
|
+
size?: SduiAvatarSizeToken;
|
|
220
|
+
corner?: ShapeToken;
|
|
221
|
+
test_id?: string;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/** Type guard for `avatar` block props. */
|
|
225
|
+
export function isSduiAvatarWireProps(
|
|
226
|
+
props: Record<string, unknown> | undefined,
|
|
227
|
+
): props is SduiAvatarWireProps {
|
|
228
|
+
if (!props) return false;
|
|
229
|
+
return (
|
|
230
|
+
typeof props.path === 'string' ||
|
|
231
|
+
typeof props.name === 'string' ||
|
|
232
|
+
typeof props.initials === 'string'
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Wire shape for block `icon_button` (BDC `<IconButton>`):
|
|
238
|
+
* ```
|
|
239
|
+
* {
|
|
240
|
+
* icon: string, // AppIconName kebab-case (required)
|
|
241
|
+
* selected_icon?: string, // AppIconName used when selected (e.g. 'heart-filled')
|
|
242
|
+
* action_id?: string, // may contain {{item.*}} binding in row templates
|
|
243
|
+
* selected?: string | boolean,// SduiSelectedFlag: '{{item.liked_by_me}}' → 'true'/'false'
|
|
244
|
+
* size?: number, // glyph px (default feedAction)
|
|
245
|
+
* color?: ColorToken, // default tint (unselected)
|
|
246
|
+
* selected_color?: ColorToken,// default tint (selected)
|
|
247
|
+
* label?: string, // accessibility label (copy via Rosetta in BFF)
|
|
248
|
+
* disabled?: boolean,
|
|
249
|
+
* test_id?: string
|
|
250
|
+
* }
|
|
251
|
+
* ```
|
|
252
|
+
* `selected` coercion: `isSduiSelectedTrue` — `'true'`/`true` render filled/tinted
|
|
253
|
+
* via theme tokens, `'false'`/`false` render default.
|
|
254
|
+
*/
|
|
255
|
+
export type SduiIconButtonWireProps = {
|
|
256
|
+
icon: string;
|
|
257
|
+
selected_icon?: string;
|
|
258
|
+
action_id?: string;
|
|
259
|
+
selected?: SduiSelectedFlag;
|
|
260
|
+
size?: number;
|
|
261
|
+
color?: ColorToken;
|
|
262
|
+
selected_color?: ColorToken;
|
|
263
|
+
label?: string;
|
|
264
|
+
disabled?: boolean;
|
|
265
|
+
test_id?: string;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
/** Type guard for `icon_button` block props. */
|
|
269
|
+
export function isSduiIconButtonWireProps(
|
|
270
|
+
props: Record<string, unknown> | undefined,
|
|
271
|
+
): props is SduiIconButtonWireProps {
|
|
272
|
+
return !!props && typeof props.icon === 'string';
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Wire shape for block `video` (BDC `<Video>`):
|
|
277
|
+
* ```
|
|
278
|
+
* {
|
|
279
|
+
* path: string, // storage path; may contain {{item.*}} binding (required)
|
|
280
|
+
* width?: number, height?: number,
|
|
281
|
+
* aspect?: 'square'|'portrait'|'story'|'video'|'wide',
|
|
282
|
+
* corner?: ShapeToken, // default cornerMedium
|
|
283
|
+
* fit?: 'cover'|'contain', // default cover
|
|
284
|
+
* poster_path?: string, // still frame (binding ok); shown when no player plugged
|
|
285
|
+
* test_id?: string
|
|
286
|
+
* }
|
|
287
|
+
* ```
|
|
288
|
+
* Renderer contract: `path`/`poster_path` resolve through the SAME app
|
|
289
|
+
* capability as `content_image` (`SduiMediaResolver`); playback is an
|
|
290
|
+
* injectable slot (`SduiVideoPlayerProvider` in
|
|
291
|
+
* `@proulr/sdui-components/renderer`) — the RN player lives in the app.
|
|
292
|
+
* Without a player the block renders a poster/still FRAME via the resolver;
|
|
293
|
+
* empty `path` or no resolver → renders null (never a product fallback URL).
|
|
294
|
+
*/
|
|
295
|
+
export type SduiVideoWireProps = {
|
|
296
|
+
path: string;
|
|
297
|
+
width?: number;
|
|
298
|
+
height?: number;
|
|
299
|
+
aspect?: SduiMediaAspectToken;
|
|
300
|
+
corner?: ShapeToken;
|
|
301
|
+
fit?: SduiMediaFitToken;
|
|
302
|
+
poster_path?: string;
|
|
303
|
+
test_id?: string;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/** Type guard for `video` block props. */
|
|
307
|
+
export function isSduiVideoWireProps(
|
|
308
|
+
props: Record<string, unknown> | undefined,
|
|
309
|
+
): props is SduiVideoWireProps {
|
|
310
|
+
return !!props && typeof props.path === 'string';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* One action entry of block `row_overflow_menu` (nested objects stay
|
|
315
|
+
* snake_case on the wire — normalized by the BDC factory, not the codec).
|
|
316
|
+
* `action_id` and `label` may contain `{{item.*}}` bindings (template-safe).
|
|
317
|
+
*/
|
|
318
|
+
export type SduiRowOverflowMenuActionWire = {
|
|
319
|
+
action_id: string;
|
|
320
|
+
label: string;
|
|
321
|
+
/** AppIconName kebab-case (catalog), e.g. 'trash'. */
|
|
322
|
+
icon?: string;
|
|
323
|
+
/** Tint the entry with the theme's error token. */
|
|
324
|
+
destructive?: boolean;
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Wire shape for block `row_overflow_menu` (BDC `<RowOverflowMenu>`):
|
|
329
|
+
* ```
|
|
330
|
+
* {
|
|
331
|
+
* actions: SduiRowOverflowMenuActionWire[], // ordered menu entries
|
|
332
|
+
* icon?: string, // trigger AppIconName (default 'overflow')
|
|
333
|
+
* test_id?: string
|
|
334
|
+
* }
|
|
335
|
+
* ```
|
|
336
|
+
* All copy is caller-supplied (BFF via Rosetta); the lib never invents
|
|
337
|
+
* product strings. Renderer opens a Modal-based menu (same mechanism as
|
|
338
|
+
* `floating_create_menu`) and dispatches `action_id` through `onAction`.
|
|
339
|
+
*/
|
|
340
|
+
export type SduiRowOverflowMenuWireProps = {
|
|
341
|
+
actions: SduiRowOverflowMenuActionWire[];
|
|
342
|
+
icon?: string;
|
|
343
|
+
test_id?: string;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
/** Type guard for `row_overflow_menu` block props. */
|
|
347
|
+
export function isSduiRowOverflowMenuWireProps(
|
|
348
|
+
props: Record<string, unknown> | undefined,
|
|
349
|
+
): props is SduiRowOverflowMenuWireProps {
|
|
350
|
+
return !!props && Array.isArray(props.actions);
|
|
351
|
+
}
|
|
352
|
+
|
|
28
353
|
export type SduiHeaderWire = {
|
|
29
354
|
title?: string;
|
|
30
355
|
subtitle?: string;
|